diff --git a/src/pages/OmniFarms/components/FarmList.tsx b/src/pages/OmniFarms/components/FarmList.tsx index fc9c4846a4..163e2832dc 100644 --- a/src/pages/OmniFarms/components/FarmList.tsx +++ b/src/pages/OmniFarms/components/FarmList.tsx @@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro' import { useMemo } from 'react' import Skeleton from 'react-loading-skeleton' import { Text } from 'rebass' -import { useGetFarmsQuery } from 'services/knprotocol' +import { NormalizedFarm, useGetFarmsQuery } from 'services/knprotocol' import styled from 'styled-components' import Pagination from 'components/Pagination' @@ -29,14 +29,14 @@ const Row = styled(HeaderWrapper)(({ theme }) => ({ height: '70px', })) -export default function FarmList() { +export default function FarmList({ onHarvest }: { onHarvest: (farm: NormalizedFarm) => void }) { const theme = useTheme() const { account } = useActiveWeb3React() const [{ chainIds, ...filters }, setFarmFilters] = useFarmFilters() const params = useMemo(() => ({ ...filters, account }), [filters, account]) - const { data, isLoading, isFetching } = useGetFarmsQuery(params, { + const { data, isLoading } = useGetFarmsQuery(params, { pollingInterval: 10_000, }) @@ -58,7 +58,7 @@ export default function FarmList() { - {isLoading || isFetching ? ( + {isLoading ? ( [...Array(10).keys()].map(i => ( {skeleton('80%', 'left')} @@ -72,7 +72,13 @@ export default function FarmList() { )) ) : data?.farmPools.length ? ( data?.farmPools.map(farm => { - return + return ( + onHarvest(farm)} + /> + ) }) ) : ( diff --git a/src/pages/OmniFarms/components/FarmTableRow.tsx b/src/pages/OmniFarms/components/FarmTableRow.tsx index 0c313f829f..dbcced25fd 100644 --- a/src/pages/OmniFarms/components/FarmTableRow.tsx +++ b/src/pages/OmniFarms/components/FarmTableRow.tsx @@ -36,7 +36,7 @@ const Row = styled(HeaderWrapper)(({ theme }) => ({ borderBottom: `1px solid ${theme.border}`, })) -const getFeeOrAMPTag = (pool: NormalizedFarm) => { +export const getFeeOrAMPTag = (pool: NormalizedFarm) => { if (pool.protocol === ProtocolType.Classic && 'amp' in pool.pool) { return `AMP ${pool.pool.amp}` } @@ -45,7 +45,7 @@ const getFeeOrAMPTag = (pool: NormalizedFarm) => { return '' } -export default function FarmTableRow({ farm }: { farm: NormalizedFarm }) { +export default function FarmTableRow({ farm, onHarvest }: { farm: NormalizedFarm; onHarvest: () => void }) { const { account } = useActiveWeb3React() const [showStake, setShowStake] = useState(false) const [showUnStake, setShowUnStake] = useState(false) @@ -80,7 +80,7 @@ export default function FarmTableRow({ farm }: { farm: NormalizedFarm }) { item => item.joinedPositions?.length || !!item.depositedPosition || - !!item.farmV2DepositedPositions?.some(dp => dp.pendingRewards.some(rw => rw !== '0')), + !!item.farmV2DepositedPositions?.some(dp => dp.pendingRewards?.some(rw => rw !== '0')), ) const hasRewards = farm.rewardAmounts.some(rw => rw.toExact() !== '0') @@ -212,7 +212,7 @@ export default function FarmTableRow({ farm }: { farm: NormalizedFarm }) { colorScheme={ButtonColorScheme.APR} disabled={!hasRewards} onClick={() => { - // handleHarvest() + onHarvest() mixpanel.track('ElasticFarmV2 - Harvest Clicked', mixpanelPayload) }} > diff --git a/src/pages/OmniFarms/components/HarvestModal.tsx b/src/pages/OmniFarms/components/HarvestModal.tsx new file mode 100644 index 0000000000..87e849fa63 --- /dev/null +++ b/src/pages/OmniFarms/components/HarvestModal.tsx @@ -0,0 +1,324 @@ +import { defaultAbiCoder } from '@ethersproject/abi' +import { ChainId } from '@kyberswap/ks-sdk-core' +import { Trans, t } from '@lingui/macro' +import { Interface } from 'ethers/lib/utils' +import { useMemo, useState } from 'react' +import { X } from 'react-feather' +import { Box, Flex, Text } from 'rebass' +import { NormalizedFarm } from 'services/knprotocol' + +import { NotificationType } from 'components/Announcement/type' +import { ButtonLight, ButtonPrimary } from 'components/Button' +import CurrencyLogo from 'components/CurrencyLogo' +import Dots from 'components/Dots' +import DoubleCurrencyLogo from 'components/DoubleLogo' +import Harvest from 'components/Icons/Harvest' +import Modal from 'components/Modal' +import Tabs from 'components/Tabs' +import PROMM_FARM_ABI from 'constants/abis/v2/farm.json' +import FarmV2ABI from 'constants/abis/v2/farmv2.json' +import { useActiveWeb3React, useWeb3React } from 'hooks' +import { ProtocolType } from 'hooks/farms/useFarmFilters' +import { NETWORKS_INFO } from 'hooks/useChainsConfig' +import useTheme from 'hooks/useTheme' +import { useChangeNetwork } from 'hooks/web3/useChangeNetwork' +import { useNotify } from 'state/application/hooks' +import { useTransactionAdder } from 'state/transactions/hooks' +import { TRANSACTION_TYPE } from 'state/transactions/type' +import { ButtonText, ExternalLink } from 'theme' +import { calculateGasMargin } from 'utils' +import { friendlyError } from 'utils/errorMessage' +import { formatDisplayNumber } from 'utils/numbers' + +import { ChainLogo, FilterGroup, FilterItem, PositionTable, Tag } from '../styled' +import { getFeeOrAMPTag } from './FarmTableRow' + +const DynamicFarmInterface = new Interface(PROMM_FARM_ABI) +const FarmV2Interface = new Interface(FarmV2ABI) + +export default function HarvestModal({ + farms, + onDismiss, + farmToHarvest, +}: { + farms: NormalizedFarm[] + onDismiss: () => void + farmToHarvest?: NormalizedFarm +}) { + const theme = useTheme() + const addTransactionWithType = useTransactionAdder() + const notify = useNotify() + const { chainId } = useActiveWeb3React() + const { changeNetwork } = useChangeNetwork() + + const protocols = [...new Set(farms.map(item => item.protocol))].sort() + const [selectedProtocol, setSelectedProtocol] = useState(farmToHarvest?.protocol || protocols[0]) + + const chainIdsByProtocol = useMemo(() => { + const res = {} as { [key: string]: ChainId[] } + farms.forEach(farm => { + if (res[farm.protocol]) { + if (!res[farm.protocol].includes(farm.chain.chainId)) + res[farm.protocol] = [...res[farm.protocol], farm.chain.chainId].sort() + } else res[farm.protocol] = [farm.chain.chainId] + }) + return res + }, [farms]) + + const [selectedChainByProtocol, setSelectedChainByProtocol] = useState<{ [key: string]: ChainId }>(() => { + return protocols.reduce( + (acc, cur) => ({ + ...acc, + [cur]: + farmToHarvest && cur === farmToHarvest.protocol ? farmToHarvest.chain.chainId : chainIdsByProtocol[cur][0], + }), + {}, + ) + }) + + const farmsToShow = farms.filter( + item => item.protocol === selectedProtocol && item.chain.chainId === selectedChainByProtocol[selectedProtocol], + ) + + const { library } = useWeb3React() + const [loading, setLoading] = useState(false) + const harvestAll = async () => { + if (!library) return + const nftIds: string[] = [] + const poolIds: string[] = [] + farmsToShow.forEach(farm => { + farm.positions.forEach(pos => { + pos.joinedPositions?.forEach(jp => { + nftIds.push(pos.id) + poolIds.push(jp.pid) + }) + }) + }) + + const encodePIds = poolIds.map(id => defaultAbiCoder.encode(['tupple(uint256[] pIds)'], [{ pIds: [id] }])) + const encodeData = DynamicFarmInterface.encodeFunctionData('harvestMultiplePools', [nftIds, encodePIds]) + const tx = { + to: farmsToShow[0].farmAddress, + data: encodeData, + } + + try { + setLoading(true) + const gas = await library.getSigner().estimateGas(tx) + const res = await library.getSigner().sendTransaction({ + ...tx, + gasLimit: calculateGasMargin(gas), + }) + addTransactionWithType({ + hash: res.hash, + type: TRANSACTION_TYPE.HARVEST, + }) + setLoading(false) + onDismiss() + } catch (error) { + console.error(error) + setLoading(false) + const message = friendlyError(error) + notify( + { + title: t`Harvest Error`, + summary: message, + type: NotificationType.ERROR, + }, + 8000, + ) + } + } + + const [harvestingId, setHarvestingId] = useState('') + const harvestStaticReward = async (farm: NormalizedFarm) => { + if (!library) return + const encodedData = FarmV2Interface.encodeFunctionData('claimReward', [ + farm.fid, + farm.positions.map(item => item.id), + ]) + + const tx = { + to: farm.farmAddress, + data: encodedData, + } + + try { + setHarvestingId(farm.id) + const gas = await library.getSigner().estimateGas(tx) + const res = await library.getSigner().sendTransaction({ + ...tx, + gasLimit: calculateGasMargin(gas), + }) + addTransactionWithType({ + hash: res.hash, + type: TRANSACTION_TYPE.HARVEST, + }) + setHarvestingId('') + onDismiss() + } catch (error) { + console.error(error) + setHarvestingId('') + const message = friendlyError(error) + notify( + { + title: t`Harvest Error`, + summary: message, + type: NotificationType.ERROR, + }, + 8000, + ) + } + } + + return ( + + + + + Harvest your rewards + + + + + + + + + You can claim all your farming rewards here! Read more here ↗ + + + + + {protocols.map(item => ( + setSelectedProtocol(item)} key={item}> + {item[0].toUpperCase() + item.slice(1)} Farms + + ))} + + + + + setSelectedChainByProtocol({ ...selectedChainByProtocol, [selectedProtocol]: +key as ChainId }) + } + items={chainIdsByProtocol[selectedProtocol].map(id => ({ + key: id, + label: ( + + + + {NETWORKS_INFO[id].name} + + + ), + children: ( + <> + + + FARMS + + + REWARDS + + + + {farmsToShow.map(farm => { + return ( + + + + + + + + {farm.token0.symbol} - {farm.token1.symbol} + + {getFeeOrAMPTag(farm)} + + + + {farm.rewardAmounts.map(item => ( + + + {formatDisplayNumber(item.toExact(), { style: 'decimal', significantDigits: 6 })}{' '} + {item.currency.symbol} + + ))} + + {selectedProtocol === ProtocolType.Static && ( + harvestStaticReward(farm)} + > + + + {harvestingId === farm.id ? ( + + Harvesting + + ) : ( + Harvest + )} + + + )} + + + ) + })} + + ), + }))} + /> + + + + {selectedChainByProtocol[selectedProtocol] !== chainId ? ( + changeNetwork(selectedChainByProtocol[selectedProtocol])} + padding="8px 16px" + width="max-content" + > + Switch to {NETWORKS_INFO[selectedChainByProtocol[selectedProtocol]].name} + + ) : ( + selectedProtocol === ProtocolType.Dynamic && ( + + {loading ? ( + + Harvesting + + ) : ( + Harvest + )} + + ) + )} + + + + ) +} diff --git a/src/pages/OmniFarms/components/SummaryUserFarm.tsx b/src/pages/OmniFarms/components/SummaryUserFarm.tsx index f1ace283e5..9cf80a836c 100644 --- a/src/pages/OmniFarms/components/SummaryUserFarm.tsx +++ b/src/pages/OmniFarms/components/SummaryUserFarm.tsx @@ -1,7 +1,9 @@ import { Trans } from '@lingui/macro' import { rgba } from 'polished' import { Fragment } from 'react' +import Skeleton from 'react-loading-skeleton' import { Flex, Text } from 'rebass' +import { PositionTotal } from 'services/knprotocol' import styled from 'styled-components' import { ButtonLight, ButtonOutlined } from 'components/Button' @@ -67,8 +69,29 @@ const rewards = [ }, ] -export default function SummaryUserFarm() { +export default function SummaryUserFarm({ + isLoading, + positionTotal, + onClickHarvest, + disabled, +}: { + isLoading: boolean + positionTotal: PositionTotal | undefined + onClickHarvest: () => void + disabled: boolean +}) { const theme = useTheme() + + const skeleton = ( + + ) + return ( @@ -86,7 +109,15 @@ export default function SummaryUserFarm() { My Staked Liquidity - ~${formatDisplayNumber(13041994, { style: 'decimal', significantDigits: 10 })} + {isLoading ? ( + skeleton + ) : !positionTotal?.depositedUSD ? ( + '--' + ) : ( + + ~${formatDisplayNumber(positionTotal.depositedUSD, { style: 'decimal', significantDigits: 7 })} + + )} @@ -94,10 +125,19 @@ export default function SummaryUserFarm() { My Rewards - ~${formatDisplayNumber(13041994, { style: 'decimal', significantDigits: 10 })} + {isLoading ? ( + skeleton + ) : !positionTotal?.pendingRewardUSD ? ( + '--' + ) : ( + + ~$ + {formatDisplayNumber(positionTotal.pendingRewardUSD, { style: 'decimal', significantDigits: 6 })} + + )} - + Harvest All diff --git a/src/pages/OmniFarms/components/index.ts b/src/pages/OmniFarms/components/index.ts index cdac7da79d..9ed8111eb7 100644 --- a/src/pages/OmniFarms/components/index.ts +++ b/src/pages/OmniFarms/components/index.ts @@ -1,3 +1,4 @@ export { default as FarmList } from './FarmList' export { default as FarmFilter } from './FarmFilter' export { default as SummaryUserFarm } from './SummaryUserFarm' +export { default as HarvestModal } from './HarvestModal' diff --git a/src/pages/OmniFarms/index.tsx b/src/pages/OmniFarms/index.tsx index 9d0e5121a0..c9df9ebe7e 100644 --- a/src/pages/OmniFarms/index.tsx +++ b/src/pages/OmniFarms/index.tsx @@ -1,15 +1,69 @@ +import { useMemo, useState } from 'react' +import { NormalizedFarm, useGetFarmsQuery } from 'services/knprotocol' + import { PageWrapper } from 'components/YieldPools/styleds' +import { MAINNET_NETWORKS } from 'constants/networks' +import { useActiveWeb3React } from 'hooks' +import { FarmType } from 'hooks/farms/useFarmFilters' +import { NETWORKS_INFO } from 'hooks/useChainsConfig' -import { FarmFilter, FarmList, SummaryUserFarm } from './components' +import { FarmFilter, FarmList, HarvestModal, SummaryUserFarm } from './components' import { PageTitle } from './styled' export default function OmniFarms() { + const { account } = useActiveWeb3React() + const { data, isLoading } = useGetFarmsQuery( + { + type: FarmType.MyFarm, + page: 1, + perPage: 1000, + account, + chainNames: MAINNET_NETWORKS.map(item => NETWORKS_INFO[item].aggregatorRoute).join(','), + }, + { + skip: !account, + pollingInterval: 10_000, + }, + ) + + const [showHarvest, setShowHarvest] = useState(false) + const [farmToHarvest, setDefaultFarmToHarvest] = useState(undefined) + + const farmsHasReward = useMemo( + () => + data?.farmPools.filter(farm => { + return farm.rewardAmounts.some(rw => rw.toExact() !== '0') + }) || [], + [data], + ) + return ( Farms - + setShowHarvest(true)} + disabled={!farmsHasReward.length} + /> - + { + setShowHarvest(true) + setDefaultFarmToHarvest(farm) + }} + /> + + {showHarvest && !!farmsHasReward.length && ( + { + setDefaultFarmToHarvest(undefined) + setShowHarvest(false) + }} + farms={farmsHasReward} + farmToHarvest={farmToHarvest} + /> + )} ) } diff --git a/src/pages/OmniFarms/styled.tsx b/src/pages/OmniFarms/styled.tsx index 5874ca37d9..710d01934b 100644 --- a/src/pages/OmniFarms/styled.tsx +++ b/src/pages/OmniFarms/styled.tsx @@ -5,6 +5,7 @@ export const PageTitle = styled.h1(({ theme }) => ({ fontSize: 24, fontWeight: '500', color: theme.text, + marginTop: 0, })) export const ChainLogo = styled.img<{ size?: number }>(({ size }) => ({ diff --git a/src/services/knprotocol.ts b/src/services/knprotocol.ts index 1c3a0881c6..4ed6e3bb74 100644 --- a/src/services/knprotocol.ts +++ b/src/services/knprotocol.ts @@ -6,7 +6,7 @@ import { POOL_FARM_BASE_URL } from 'constants/env' import { RTK_QUERY_TAGS } from 'constants/index' import { EVM_NETWORK, NETWORKS_INFO } from 'constants/networks' import { EVMNetworkInfo } from 'constants/networks/type' -import { ProtocolType } from 'hooks/farms/useFarmFilters' +import { FarmType, ProtocolType } from 'hooks/farms/useFarmFilters' import { chainIdByRoute } from 'pages/MyEarnings/utils' import { SubgraphFarmV2 } from 'state/farms/elasticv2/types' @@ -87,6 +87,8 @@ export type ClassicFarmKN = { interface GetFarmParams { account?: string + type: FarmType + protocol?: ProtocolType perPage: number page: number chainNames: string @@ -245,6 +247,14 @@ export interface NormalizedFarm { }> } +export interface PositionTotal { + amountUSD: string + depositedUSD: string + pendingRewardUSD: string + stakedUSD: string + pendingFeeUSD: string +} + const knProtocolApi = createApi({ reducerPath: 'knProtocol', baseQuery: fetchBaseQuery({ baseUrl: POOL_FARM_BASE_URL }), @@ -269,12 +279,25 @@ const knProtocolApi = createApi({ }), }), - getFarms: builder.query<{ farmPools: NormalizedFarm[]; pagination: { totalRecords: number } }, GetFarmParams>({ + getFarms: builder.query< + { + farmPools: NormalizedFarm[] + pagination: { totalRecords: number } + positionTotal: PositionTotal + }, + GetFarmParams + >({ query: params => ({ url: `/all-chain/api/v1/farm-pools`, params, }), - transformResponse: (response: { data: { farmPools: FarmKn[]; pagination: { totalRecords: number } } }) => { + transformResponse: (response: { + data: { + farmPools: FarmKn[] + pagination: { totalRecords: number } + positionTotal: PositionTotal + } + }) => { const raw = response.data const convertTokenBEToTokenSDK = (chainId: ChainId, token: TokenKn) => { @@ -341,7 +364,6 @@ const knProtocolApi = createApi({ if (farm.protocol === ProtocolType.Static) { const stakedRange = farm.ranges.find(r => item.farmV2DepositedPositions?.[0].range.id === r.id)?.weight || '1' - console.log(stakedRange) stakedLiq = BigNumber.from(item.farmV2DepositedPositions?.[0].liquidity || '0').div( BigNumber.from(stakedRange), )