From 00e2eeac1725a37500c8d75ace9dcd81fb49c634 Mon Sep 17 00:00:00 2001 From: Nguyen Van Viet Date: Thu, 10 Aug 2023 18:13:22 +0700 Subject: [PATCH] LP earning classic page (#2133) * Add classic page (#2053) * wip: add MyEarning to header * wip: implement earning dashboard * wip: remove transition in pie chart * wip: card background and button variant * null check * wip: multicall work * wip: work get tick data from another chainId * wip: add liquidity button * wip: update style for stats values * wip: check with arbitrum & ethereum * fix backdrop of liquidity range chart by using unique ids * view more positions button * minor style * wip: responsive * wip: display token logo with chain logo in breakdown chart * adapt to new UI design * farm available tooltip * show only active positions * adapt to new UI design * today change diff * wip * handle empty state * fill data for missing days * show/hide closed positions * show number of active/inactive/closed positions * implement search function * show in range/out range position status * Responsive UI for extra small screens * implement Expand/Collapse all pools * implement refresh button * wip: implement Share modal * done share ui * update env * use social utils c c c * cache share url * update api call fix update ch * wip: aggregate data in case data from data is missing * refactor: insert legacy elastic pool to tab list * refactor: use pool address to get pool data * wip: turn off aggregating data from frontend * wip: use prod env * refactor: use pool address to get price range chart data * Revert "refactor: use pool address to get pool data" This reverts commit 3e41dba37ad4886fac3a6e7a5bc8149761991dc5. * Revert "refactor: use pool address to get price range chart data" This reverts commit b71b2e6b61dfa36bc5f0d3e27d59b1e8d5477bbf. * fix: pass poolAddress to usePoolv2 This enables expand button to see positions of a pool in LP page * fix: show loading when re-fetching LP data * Revert "wip: turn off aggregating data from frontend" This reverts commit f7614fc3a3afaa21875c6b0ce4740a2a19dd8813. * fix: set default share mode according to the device * refactor: modify ClassicElasticPools tab * fix: fix hanging when switching tabs * env dev oaatuh * feat: aggregate account earnings across multiple chains * refactor: use data of the last day for other empty days * refactor: fill data for unavailable days * fix: calculate earning breakdown from earning tick * fix: crash due to null field * fix: fix build failed * refactor: select all chains by default * style: take more width and adjust width of single position * small refactor * refactor: fill empty days for position earnings * refactor: fill empty days for ticks * fix: calculate wrong fees due to modifying objects instead of deep-cloning it * chore: use dev env * use proxy token logo * feat: prepare queries for elastic earning * feat: make 2 separate requests for elastic and elastic legacy * refactor: turn off Liquidity chart of legacy positions by default * refactor: display at most 5 tokens in the tooltip of earning overtime panel * refactor: use compact notation for shared earning value * refactor: sort pools by TVL * refactor: group elastic and classic pools * fix: refresh button * wip: prepare for classic positions * fix crash * fix: merge earning tick * refactor: aggregate pool data BE now doesn't return `pools` in the response * feat: pending fees (#1988) * feat: pending fees * chore: fix prettier issue --------- Co-authored-by: Doan Sy Hung * try fix token logo when download * rm ununsed * chore: try debug share img * fix: not show logo when download * loading for btn share * test build * feat: calc apr, staked balance in position view (#1989) * refactor: make zoom out modal smaller * add scroll share modal if overflow * fix: rotate layout * feat: add Kyber logo to earning chart and add label * fix: use hard-coded token symbols * feat: view earnings button * fix: https://team-kyber.atlassian.net/browse/KGP-1259 c * refactor: not show labels if chart is in small container * refactor: use pointer for buttons * fix: not show labels if container width is < 400 * refactor: reduce height of zoom out earning modal * fix: fix wrong layout for zoom out modal * fix: remove border radius from my earnings overtime panel * feat: collect fees * feat: save the time period when viewing in modal * feat: add Analytic button for elastic positions * refactor: update style for Sharing modal * refactor: update url to Add Liquidity * refactor: add url to Remove Liquidity and Increase Liquidity * chore: suppress warnings/errors * style: update Expand All button * refactor: update style for View Earning/Position button * feat: implement classic pools * feat: display classic pool earnings * feat: implement position detail for classic pool * feat: click on each pool to expand/collapse * chore: update classic response * show some properties of Classic pools/positions * update urls for Remove/Add liquidity classic pools * feat: add up classic earnings * fix build failed * use prod env * update pool currency logos * make equal piece for pie chart with values of 0 * use env dev * display LP Token balance * display share of pool * style Share image * show copy address button for elastic pools * update view earnings or positions button * use pointer cursor for Share button * add banner * fix build failed * update add/increase liquidity links using native symbol * show native tokens instead of wrapped ones * try add proxy to logos * update currency logos in Share modal * fix build failed * fix NaN in earning pie chart * hide ConnectWalletFooter in MyEarnings page * sort data top to bottom in pie chart * update style for elastic/classic buttons * display Subscribe button in full * show icon only for Subscribe button in mobile * update placeholder for MyEarnings on mobile * update placeholder * disable remove/increase buttons if liquidity = 0 * update layout of positions * temp disable classic pools button * disable increase liquidity button for legacy positions * update layout for legends of LP pie chart * update link for MyEarnings in the banner * update style for elastic/classic tab * update color for Analytics button * exclude classic data * disable Add Liquidity button for Legacy pools * remove console.log * update position layout * adapt to medium-sized screens * shrink Legacy tag * adapt pie chart to medium screens * update format for fee numbers * move ActionButtons to a separate component * remove liquidity from elastic legacy position * show fees for legacy positions * revert api social env stg env prod * update texts * fix build failed * hardcode stg ks-setting c * refactor: update View Earnings/Positions * refactor: use calculated breakpoints * filter out empty token earnings * add tooltip for percent change * add token logo to token in earning area chart * show relative numbers if too small * handle change network glitch while collecting fees * avoid scientific notation * disable hover dropdown if no value * display 0 for $0 earning * auto refetch earning data after 5 minutes * disable hover dropdown if there's no liquidity * add mixpanel events * test * latest sdk * retain state while refetching data * update number formatters * stop propagation for click buttons in pool * try fix size logo when download * try fix size logo when download * update value formatter * refactor common codes of single pool * comment unused codes * usd value format * display my liquidity balance for each pool * display total APR * fix refetching LP with wrong chains * calculate total of token earnings * fix minor UI issues * get user farm data from custom chainId * adjust decimals for balance * sort positions by TVL * fix calling wrong farm contract to collect fees * revert token logo share modal c * no wrap small balances * remove video tutorial * update texts * update pending texts in modal * fix issue network changed before collecting fees * fix comments * sort closed positions by nft id * use proxy only in share earnings modal * refactor today * remove done todo * fix refreshing issue * sdk version * fix minor comments * remove classic page * Add classic page This reverts commit b87457e7d249eab389cdfee13cfce1cc60b8184c. --------- Co-authored-by: Danh Co-authored-by: Nguyen Van Viet * feat: handle classic * add no liquidity found * update missing info * fix: missing token * amp liq tooltip * disable some network * swithc to prod env * tooltip placement top * fix: select chain * revert env --------- Co-authored-by: Hung Doan <19758667+hungdoansy@users.noreply.github.com> Co-authored-by: Danh --- src/components/EarningAreaChart/index.tsx | 17 +- src/components/SubscribeButton/index.tsx | 10 +- src/constants/networks.ts | 14 +- src/hooks/Tokens.ts | 3 + src/pages/MyEarnings/ChainSelect.tsx | 23 +- src/pages/MyEarnings/ClassicElasticTab.tsx | 63 +-- .../ClassicPools/SinglePool/Position.tsx | 212 ++++++++ .../ClassicPools/SinglePool/index.tsx | 503 ++++++++++++++++++ src/pages/MyEarnings/ClassicPools/index.tsx | 139 +++++ .../SinglePool/SharePoolEarningsButton.tsx | 7 +- .../ElasticPools/SinglePool/index.tsx | 77 +-- src/pages/MyEarnings/ElasticPools/index.tsx | 2 +- .../MultipleChainSelect/PopoverBody.tsx | 25 +- src/pages/MyEarnings/MyEarningStats/index.tsx | 25 +- src/pages/MyEarnings/PoolEarningsSection.tsx | 69 ++- .../ClosedPositionsToggle.tsx | 4 +- src/pages/MyEarnings/Pools/index.tsx | 4 +- src/pages/MyEarnings/ShareModal.tsx | 9 +- .../TotalEarningsAndChainSelect.tsx | 38 +- src/pages/MyEarnings/hooks.ts | 12 +- src/pages/MyEarnings/styled.tsx | 80 +++ src/services/earning/types.ts | 8 +- 22 files changed, 1154 insertions(+), 190 deletions(-) create mode 100644 src/pages/MyEarnings/ClassicPools/SinglePool/Position.tsx create mode 100644 src/pages/MyEarnings/ClassicPools/SinglePool/index.tsx create mode 100644 src/pages/MyEarnings/ClassicPools/index.tsx create mode 100644 src/pages/MyEarnings/styled.tsx diff --git a/src/components/EarningAreaChart/index.tsx b/src/components/EarningAreaChart/index.tsx index 20181a8274..abcaee3b7a 100644 --- a/src/components/EarningAreaChart/index.tsx +++ b/src/components/EarningAreaChart/index.tsx @@ -57,16 +57,17 @@ const subscriptMap: { [key: string]: string } = { } const formatter = (value: string) => { - const num = Number(value) + const num = parseFloat(value) const numberOfZero = -Math.floor(Math.log10(num) + 1) if (num > 0 && num < 1 && numberOfZero > 2) { - const temp = Number(toFixed(num).split('.')[1]) + const temp = Number(toFixed(num).split('.')[1]).toString() + return `$0.0${numberOfZero .toString() .split('') .map(item => subscriptMap[item]) - .join('')}${temp > 10 ? (temp / 10).toFixed(0) : temp}` + .join('')}${temp.substring(0, 2)}` } const formatter = Intl.NumberFormat('en-US', { @@ -108,7 +109,15 @@ const EarningAreaChart: React.FC = ({ data, setHoverValue = EMPTY_FUNCTIO - + bgColor}; } ${({ iconOnly, bgColor }) => iconOnly && cssSubscribeBtnSmall(bgColor)}; - ${({ theme, bgColor }) => theme.mediaWidth.upToExtraSmall` + ${({ theme, bgColor, iconOnly }) => + iconOnly !== false && + theme.mediaWidth.upToExtraSmall` ${cssSubscribeBtnSmall(bgColor)} `} ` @@ -48,13 +50,15 @@ const ButtonText = styled(Text)<{ iconOnly?: boolean }>` font-weight: 500; margin-left: 6px !important; ${({ iconOnly }) => iconOnly && `display: none`}; - ${({ theme }) => theme.mediaWidth.upToExtraSmall` + ${({ theme, iconOnly }) => + iconOnly !== false && + theme.mediaWidth.upToExtraSmall` display: none; `} ` export default function SubscribeNotificationButton({ subscribeTooltip, - iconOnly = false, + iconOnly, trackingEvent, onClick, topicId, diff --git a/src/constants/networks.ts b/src/constants/networks.ts index 2cad0713f3..43ef0bd59f 100644 --- a/src/constants/networks.ts +++ b/src/constants/networks.ts @@ -212,7 +212,19 @@ export const SUPPORTED_NETWORKS_FOR_MY_EARNINGS = [ ChainId.OASIS, ] export const COMING_SOON_NETWORKS_FOR_MY_EARNINGS: ChainId[] = [] -export const COMING_SOON_NETWORKS_FOR_MY_EARNINGS_LEGACY: ChainId[] = [ChainId.MATIC] +export const COMING_SOON_NETWORKS_FOR_MY_EARNINGS_LEGACY: ChainId[] = [] +export const COMING_SOON_NETWORKS_FOR_MY_EARNINGS_CLASSIC: ChainId[] = [ + ChainId.CRONOS, + ChainId.OASIS, + ChainId.MATIC, + ChainId.BSCMAINNET, + ChainId.AVAXMAINNET, + ChainId.CRONOS, + ChainId.BTTC, + ChainId.VELAS, + ChainId.AURORA, + ChainId.OASIS, +] // by pass invalid price impact/unable to calculate price impact/price impact too large export const CHAINS_BYPASS_PRICE_IMPACT = [ChainId.LINEA_TESTNET] diff --git a/src/hooks/Tokens.ts b/src/hooks/Tokens.ts index e4b2146c7e..0b3a22b6f4 100644 --- a/src/hooks/Tokens.ts +++ b/src/hooks/Tokens.ts @@ -304,6 +304,9 @@ export const fetchTokenByAddress = async (address: string, chainId: ChainId, sig } export const fetchListTokenByAddresses = async (address: string[], chainId: ChainId) => { + const cached = filterTruthy(address.map(addr => findCacheToken(addr))) + if (cached.length === address.length) return cached + const response = await axios.get(`${KS_SETTING_API}/v1/tokens?addresses=${address}&chainIds=${chainId}`) const tokens = response?.data?.data?.tokens ?? [] return filterTruthy(tokens.map(formatAndCacheToken)) as WrappedTokenInfo[] diff --git a/src/pages/MyEarnings/ChainSelect.tsx b/src/pages/MyEarnings/ChainSelect.tsx index e06845bea9..6d3bf822a3 100644 --- a/src/pages/MyEarnings/ChainSelect.tsx +++ b/src/pages/MyEarnings/ChainSelect.tsx @@ -5,11 +5,18 @@ import { Flex, Text } from 'rebass' import styled from 'styled-components' import { ButtonOutlined } from 'components/Button' -import { SUPPORTED_NETWORKS_FOR_MY_EARNINGS } from 'constants/networks' +import { + COMING_SOON_NETWORKS_FOR_MY_EARNINGS, + COMING_SOON_NETWORKS_FOR_MY_EARNINGS_CLASSIC, + COMING_SOON_NETWORKS_FOR_MY_EARNINGS_LEGACY, + SUPPORTED_NETWORKS_FOR_MY_EARNINGS, +} from 'constants/networks' +import { VERSION } from 'constants/v2' import { useActiveWeb3React } from 'hooks' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import useTheme from 'hooks/useTheme' import MultipleChainSelect from 'pages/MyEarnings/MultipleChainSelect' +import { useAppSelector } from 'state/hooks' import { selectChains } from 'state/myEarnings/actions' import { useShowMyEarningChart } from 'state/user/hooks' import { MEDIA_WIDTHS } from 'theme' @@ -49,7 +56,19 @@ const ChainSelect = () => { const dispatch = useDispatch() const { mixpanelHandler } = useMixpanel() const { chainId } = useActiveWeb3React() - const isValidNetwork = SUPPORTED_NETWORKS_FOR_MY_EARNINGS.includes(chainId) + + const isLegacy = useAppSelector(state => state.myEarnings.activeTab === VERSION.ELASTIC_LEGACY) + const isClassic = useAppSelector(state => state.myEarnings.activeTab === VERSION.CLASSIC) + + const comingSoonList = isLegacy + ? COMING_SOON_NETWORKS_FOR_MY_EARNINGS_LEGACY + : isClassic + ? COMING_SOON_NETWORKS_FOR_MY_EARNINGS_CLASSIC + : COMING_SOON_NETWORKS_FOR_MY_EARNINGS + + const networkList = SUPPORTED_NETWORKS_FOR_MY_EARNINGS.filter(item => !comingSoonList.includes(item)) + + const isValidNetwork = networkList.includes(chainId) const handleClickCurrentChain = () => { if (!isValidNetwork) { diff --git a/src/pages/MyEarnings/ClassicElasticTab.tsx b/src/pages/MyEarnings/ClassicElasticTab.tsx index 21212c526b..d113720a22 100644 --- a/src/pages/MyEarnings/ClassicElasticTab.tsx +++ b/src/pages/MyEarnings/ClassicElasticTab.tsx @@ -3,7 +3,7 @@ import { rgba } from 'polished' import { stringify } from 'querystring' import { useEffect } from 'react' import { useDispatch } from 'react-redux' -import { Link, useNavigate } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useMedia } from 'react-use' import { Flex, Text } from 'rebass' @@ -11,7 +11,7 @@ import { ReactComponent as DropdownSVG } from 'assets/svg/down.svg' import { PoolClassicIcon, PoolElasticIcon } from 'components/Icons' import Wallet from 'components/Icons/Wallet' import { MouseoverTooltip } from 'components/Tooltip' -import { APP_PATHS, PROMM_ANALYTICS_URL } from 'constants/index' +import { PROMM_ANALYTICS_URL } from 'constants/index' import { VERSION } from 'constants/v2' import { useActiveWeb3React } from 'hooks' import useParsedQueryString from 'hooks/useParsedQueryString' @@ -78,12 +78,10 @@ function ClassicElasticTab() { dispatch(setActiveTab(tab)) }, [dispatch, tab]) - useEffect(() => { - if (tab !== VERSION.ELASTIC && tab !== VERSION.ELASTIC_LEGACY) { - const newQs = { ...qs, tab: VERSION.ELASTIC } - navigate({ search: stringify(newQs) }, { replace: true }) - } - }, [navigate, qs, tab]) + const handleClickClassic = () => { + const newQs = { ...qs, tab: VERSION.CLASSIC } + navigate({ search: stringify(newQs) }, { replace: true }) + } const renderComboPoolButtonsForMobile = () => { return ( @@ -124,8 +122,8 @@ function ClassicElasticTab() { - + Classic Pools @@ -210,38 +208,29 @@ function ClassicElasticTab() { } const renderClassicPoolsButton = () => { - const color = tab === VERSION.CLASSIC ? theme.primary : theme.disableText - const here = here + const color = tab === VERSION.CLASSIC ? theme.primary : theme.subText return ( - - Coming soon. In the meantime, you can still manage your Classic liquidity positions {here} - - } - placement="top" + - - - - - Classic Pools - + - + + Classic Pools + + ) } diff --git a/src/pages/MyEarnings/ClassicPools/SinglePool/Position.tsx b/src/pages/MyEarnings/ClassicPools/SinglePool/Position.tsx new file mode 100644 index 0000000000..52e791e88c --- /dev/null +++ b/src/pages/MyEarnings/ClassicPools/SinglePool/Position.tsx @@ -0,0 +1,212 @@ +import { ChainId, Token } from '@kyberswap/ks-sdk-core' +import { Trans, t } from '@lingui/macro' +import { useMemo } from 'react' +import { useMedia } from 'react-use' +import { Box, Flex, Text } from 'rebass' +import { ClassicPositionEarningWithDetails } from 'services/earning/types' + +import CurrencyLogo from 'components/CurrencyLogo' +import useTheme from 'hooks/useTheme' +import HoverDropdown from 'pages/MyEarnings/HoverDropdown' +import PoolEarningsSection from 'pages/MyEarnings/PoolEarningsSection' +import { WIDTHS } from 'pages/MyEarnings/constants' +import { useShowMyEarningChart } from 'state/user/hooks' +import { formattedNum } from 'utils' +import { formatDollarAmount } from 'utils/numbers' +import { unwrappedToken } from 'utils/wrappedCurrency' + +type ColumnProps = { + label: string + value: React.ReactNode +} +const Column: React.FC = ({ label, value }) => { + const theme = useTheme() + return ( + + + {label} + + + {value} + + ) +} + +type Props = { + chainId: ChainId + poolEarning: ClassicPositionEarningWithDetails +} +const Position: React.FC = ({ poolEarning, chainId }) => { + const theme = useTheme() + const [showEarningChart] = useShowMyEarningChart() + const mobileView = useMedia(`(max-width: ${WIDTHS[2]}px)`) + + const myLiquidityBalance = + poolEarning.liquidityTokenBalanceIncludingStake !== '0' && poolEarning.pool.totalSupply !== '0' + ? formatDollarAmount( + (+poolEarning.liquidityTokenBalanceIncludingStake * +poolEarning.pool.reserveUSD) / + +poolEarning.pool.totalSupply, + ) + : '--' + + const myShareOfPool = +poolEarning.liquidityTokenBalanceIncludingStake / +poolEarning.pool.totalSupply + + const pooledToken0 = +poolEarning.pool.reserve0 * myShareOfPool + const pooledToken1 = +poolEarning.pool.reserve1 * myShareOfPool + + const token0 = useMemo( + () => + new Token( + chainId, + poolEarning.pool.token0.id, + +poolEarning.pool.token0.decimals, + poolEarning.pool.token0.symbol, + poolEarning.pool.token0.name, + ), + [chainId, poolEarning], + ) + + const token1 = useMemo( + () => + new Token( + chainId, + poolEarning.pool.token1.id, + +poolEarning.pool.token1.decimals, + poolEarning.pool.token1.symbol, + poolEarning.pool.token1.name, + ), + [chainId, poolEarning], + ) + + const liquidityStaked = +poolEarning.liquidityTokenBalanceIncludingStake - +poolEarning.liquidityTokenBalance + const myStakedBalance = + liquidityStaked !== 0 + ? formatDollarAmount((liquidityStaked * +poolEarning.pool.reserveUSD) / +poolEarning.pool.totalSupply) + : '--' + + const stakedShare = liquidityStaked / +poolEarning.pool.totalSupply + + const stakedToken0 = +poolEarning.pool.reserve0 * stakedShare + const stakedToken1 = +poolEarning.pool.reserve1 * stakedShare + + return ( + + + My Liquidity Positions + + + {showEarningChart && } + + *:nth-child(2n)': { + alignItems: 'flex-end', + }, + }), + }} + > + + {myLiquidityBalance}} + disabled={poolEarning.liquidityTokenBalance === '0'} + text={ +
+ + + + {formattedNum(pooledToken0)} {unwrappedToken(token0).symbol} + + + + + + {formattedNum(pooledToken1)} {unwrappedToken(token1).symbol} + + +
+ } + /> + + } + /> + + + {myStakedBalance}} + disabled={liquidityStaked === 0} + text={ +
+ + + + {formattedNum(stakedToken0)} {unwrappedToken(token0).symbol} + + + + + + {formattedNum(stakedToken1)} {unwrappedToken(token1).symbol} + + +
+ } + /> + + } + /> + + + + + + +
+
+ ) +} + +export default Position diff --git a/src/pages/MyEarnings/ClassicPools/SinglePool/index.tsx b/src/pages/MyEarnings/ClassicPools/SinglePool/index.tsx new file mode 100644 index 0000000000..f6050cec5c --- /dev/null +++ b/src/pages/MyEarnings/ClassicPools/SinglePool/index.tsx @@ -0,0 +1,503 @@ +import { ChainId, Fraction, Token } from '@kyberswap/ks-sdk-core' +import { Trans, t } from '@lingui/macro' +import JSBI from 'jsbi' +import { rgba } from 'polished' +import { useEffect, useMemo, useState } from 'react' +import { BarChart2, Info, Minus, Plus } from 'react-feather' +import { Link } from 'react-router-dom' +import { useMedia } from 'react-use' +import { Box, Flex, Text } from 'rebass' +import { ClassicPositionEarningWithDetails } from 'services/earning/types' +import styled from 'styled-components' + +import { ReactComponent as DropdownSVG } from 'assets/svg/down.svg' +import CopyHelper from 'components/Copy' +import Divider from 'components/Divider' +import DoubleCurrencyLogo from 'components/DoubleLogo' +import { MoneyBag } from 'components/Icons' +import { MouseoverTooltip, TextDashed } from 'components/Tooltip' +import { APRTooltipContent } from 'components/YieldPools/FarmingPoolAPRCell' +import { APP_PATHS, DMM_ANALYTICS_URL, SUBGRAPH_AMP_MULTIPLIER } from 'constants/index' +import { NETWORKS_INFO } from 'constants/networks' +import useTheme from 'hooks/useTheme' +import Position from 'pages/MyEarnings/ClassicPools/SinglePool/Position' +import { StatItem } from 'pages/MyEarnings/ElasticPools/SinglePool' +import SharePoolEarningsButton from 'pages/MyEarnings/ElasticPools/SinglePool/SharePoolEarningsButton' +import { WIDTHS } from 'pages/MyEarnings/constants' +import { ClassicRow, DownIcon, MobileStat, MobileStatWrapper, Wrapper } from 'pages/MyEarnings/styled' +import { ButtonIcon } from 'pages/Pools/styleds' +import { useAppSelector } from 'state/hooks' +import { TokenAddressMap } from 'state/lists/reducer' +import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo' +import { isAddress, shortenAddress } from 'utils' +import { currencyId } from 'utils/currencyId' +import { formatDollarAmount } from 'utils/numbers' +import { getTokenSymbolWithHardcode } from 'utils/tokenInfo' +import { unwrappedToken } from 'utils/wrappedCurrency' + +const calculateAmpLiquidity = (rawAmp: string, reserveUSD: string) => { + const amp = new Fraction(rawAmp).divide(JSBI.BigInt(SUBGRAPH_AMP_MULTIPLIER)) + const ampLiquidity = parseFloat(amp.toSignificant(5)) * parseFloat(reserveUSD) + return ampLiquidity +} + +const Badge = styled.div<{ $color?: string }>` + display: flex; + align-items: center; + gap: 4px; + + padding: 2px 8px; + font-weight: 500; + font-size: 12px; + line-height: 16px; + border-radius: 16px; + + user-select: none; + + color: ${({ $color, theme }) => $color || theme.subText}; + background: ${({ $color, theme }) => rgba($color || theme.subText, 0.3)}; + + ${({ theme }) => theme.mediaWidth.upToExtraSmall` + height: 16px; + padding: 0 4px; + `} +` + +export type Props = { + chainId: ChainId + poolEarning: ClassicPositionEarningWithDetails +} + +const getCurrencyFromTokenAddress = ( + tokensByChainId: TokenAddressMap, + chainId: ChainId, + address: string, +): WrappedTokenInfo | undefined => { + const tokenAddress = isAddress(chainId, address) + if (!tokenAddress) { + return undefined + } + + const currency = tokensByChainId[chainId][tokenAddress] + return currency +} + +const SinglePool: React.FC = ({ poolEarning, chainId }) => { + const theme = useTheme() + const networkInfo = NETWORKS_INFO[chainId] + const [isExpanded, setExpanded] = useState(false) + const tabletView = useMedia(`(max-width: ${WIDTHS[3]}px)`) + const mobileView = useMedia(`(max-width: ${WIDTHS[2]}px)`) + const tokensByChainId = useAppSelector(state => state.lists.mapWhitelistTokens) + const shouldExpandAllPools = useAppSelector(state => state.myEarnings.shouldExpandAllPools) + const currency0 = + getCurrencyFromTokenAddress(tokensByChainId, chainId, poolEarning.pool.token0.id) || + new Token( + chainId, + poolEarning.pool.token0.id, + +poolEarning.pool.token0.decimals, + poolEarning.pool.token0.symbol, + poolEarning.pool.token0.name, + ) + const currency1 = + getCurrencyFromTokenAddress(tokensByChainId, chainId, poolEarning.pool.token1.id) || + new Token( + chainId, + poolEarning.pool.token1.id, + +poolEarning.pool.token1.decimals, + poolEarning.pool.token1.symbol, + poolEarning.pool.token1.name, + ) + + // Need these because we'll display native tokens instead of wrapped tokens + const visibleCurrency0 = currency0 ? unwrappedToken(currency0) : undefined + const visibleCurrency1 = currency1 ? unwrappedToken(currency1) : undefined + + /* Some tokens have different symbols in our system */ + const visibleCurrency0Symbol = getTokenSymbolWithHardcode( + chainId, + poolEarning.pool.token0.id, + visibleCurrency0?.symbol || poolEarning.pool.token0.symbol, + ) + const visibleCurrency1Symbol = getTokenSymbolWithHardcode( + chainId, + poolEarning.pool.token1.id, + visibleCurrency1?.symbol || poolEarning.pool.token1.symbol, + ) + const myLiquidityBalance = + poolEarning.liquidityTokenBalance !== '0' && poolEarning.pool.totalSupply !== '0' + ? formatDollarAmount( + (+poolEarning.liquidityTokenBalance * +poolEarning.pool.reserveUSD) / +poolEarning.pool.totalSupply, + ) + : '--' + + const here = ( + + here + + ) + + const isFarmingPool = poolEarning.pool.farmApr !== '0' + + const poolEarningToday = useMemo(() => { + const earning = poolEarning.historicalEarning[0]?.total?.reduce( + (acc, tokenEarning) => acc + Number(tokenEarning.amountUSD), + 0, + ) + + return earning || 0 + }, [poolEarning.historicalEarning]) + + useEffect(() => { + setExpanded(shouldExpandAllPools) + }, [shouldExpandAllPools]) + + const amp = +poolEarning.pool.amp / 10_000 + + const ampLiquidity = calculateAmpLiquidity(poolEarning.pool.amp, poolEarning.pool.reserveUSD) + + const renderShareButton = () => { + return ( + + ) + } + + const share = ( + { + e.stopPropagation() + }} + > + + {renderShareButton()} + + ) + + if (tabletView) { + return ( + + + + + + + + + {visibleCurrency0Symbol} - {visibleCurrency1Symbol} + + + + AMP {amp} + + {isFarmingPool && ( + + Available for yield farming. Click {here} to go to the farm. + + } + > + + + + + )} + + + {share} + + + + + + + APR + + } + value={ + + + {(+poolEarning.pool.apr).toFixed(2)}% + + + + } + > + + + + } + /> + + + + + + + + { + e.stopPropagation() + }} + > + + + + {poolEarning.liquidityTokenBalance !== '0' && ( + e.stopPropagation()} + > + + + )} + + e.stopPropagation()} + > + + + + setExpanded(e => !e)} + > + + + + + + {isExpanded && mobileView && } + {isExpanded && } + + ) + } + + return ( + <> + + setExpanded(e => !e)}> + + + + + {NETWORKS_INFO[chainId].name} + + + + + {visibleCurrency0Symbol} - {visibleCurrency1Symbol} + + AMP {+poolEarning.pool.amp / 10000} + + {isFarmingPool && ( + + Available for yield farming. Click {here} to go to the farm. + + } + > + + + + + )} + + + {share} + + + +
+ {formatDollarAmount(ampLiquidity)} + + {formatDollarAmount(+poolEarning.pool.reserveUSD)} + +
+ + + + {(+poolEarning.pool.apr + +poolEarning.pool.farmApr).toFixed(2)}% + + + + } + > + + + + + + {formatDollarAmount(Number(poolEarning.pool.volumeUsd) - Number(poolEarning.pool.volumeUsdOneDayAgo))} + + {formatDollarAmount(Number(poolEarning.pool.feeUSD) - Number(poolEarning.pool.feesUsdOneDayAgo))} + {myLiquidityBalance} + {formatDollarAmount(poolEarningToday)} + + + e.stopPropagation()} + > + + + + {poolEarning.liquidityTokenBalance !== '0' && ( + e.stopPropagation()} + > + + + )} + + e.stopPropagation()} + > + + + +
+ {isExpanded && } +
+ + ) +} + +export default SinglePool diff --git a/src/pages/MyEarnings/ClassicPools/index.tsx b/src/pages/MyEarnings/ClassicPools/index.tsx new file mode 100644 index 0000000000..0ba78db0eb --- /dev/null +++ b/src/pages/MyEarnings/ClassicPools/index.tsx @@ -0,0 +1,139 @@ +import { Trans, t } from '@lingui/macro' +import { useMedia } from 'react-use' +import { Flex, Text } from 'rebass' +import { useGetClassicEarningQuery } from 'services/earning' +import { ClassicPositionEarningWithDetails } from 'services/earning/types' +import styled from 'styled-components' + +import InfoHelper from 'components/InfoHelper' +import { AMP_LIQUIDITY_HINT } from 'constants/index' +import { COMING_SOON_NETWORKS_FOR_MY_EARNINGS_CLASSIC } from 'constants/networks' +import { useActiveWeb3React } from 'hooks' +import useDebounce from 'hooks/useDebounce' +import useTheme from 'hooks/useTheme' +import { chainIdByRoute } from 'pages/MyEarnings/utils' +import { useAppSelector } from 'state/hooks' + +import { WIDTHS } from '../constants' +import SinglePool from './SinglePool' + +const Header = styled.div` + background: ${({ theme }) => theme.tableHeader}; + color: ${({ theme }) => theme.subText}; + text-transform: uppercase; + padding: 16px 12px; + font-size: 12px; + font-weight: 500; + border-top-left-radius: 16px; + border-top-right-radius: 16px; + display: grid; + grid-template-columns: 3fr 2fr repeat(6, 1.3fr); +` + +const ClassicPools = () => { + const { account = '' } = useActiveWeb3React() + const selectedChainIds = useAppSelector(state => state.myEarnings.selectedChains) + + const classicEarningQueryResponse = useGetClassicEarningQuery({ + account, + chainIds: selectedChainIds.filter(item => !COMING_SOON_NETWORKS_FOR_MY_EARNINGS_CLASSIC.includes(item)), + }) + + const originalSearchText = useAppSelector(state => state.myEarnings.searchText) + const searchText = useDebounce(originalSearchText, 300).toLowerCase().trim() + const shouldShowClosedPositions = useAppSelector(state => state.myEarnings.shouldShowClosedPositions) + + const data = classicEarningQueryResponse.data + + const tabletView = useMedia(`(max-width: ${WIDTHS[3]}px)`) + const mobileView = useMedia(`(max-width: ${WIDTHS[2]}px)`) + const theme = useTheme() + + const renderPools = () => { + const filterFn = (item: ClassicPositionEarningWithDetails) => { + const removedFilter = shouldShowClosedPositions + ? true + : item.liquidityTokenBalance !== '0' || item.liquidityTokenBalanceIncludingStake !== '0' + const poolId = item.pool.id.toLowerCase() + const searchFilter = + poolId === searchText || + item.pool.token0.id.toLowerCase() === searchText || + item.pool.token0.symbol.toLowerCase().includes(searchText) || + item.pool.token0.name.toLowerCase().includes(searchText) || + item.pool.token1.id.toLowerCase() === searchText || + item.pool.token1.symbol.toLowerCase().includes(searchText) || + item.pool.token1.name.toLowerCase().includes(searchText) + return searchFilter && removedFilter + } + + if (!data || Object.keys(data).every(key => !data[key]?.positions?.filter(filterFn).length)) { + return ( + + No liquidity found + + ) + } + + return Object.keys(data).map(chainRoute => { + const chainId = chainIdByRoute[chainRoute] + const poolEarnings = data[chainRoute].positions.filter(filterFn) + + return ( + <> + {poolEarnings.map(poolEarning => { + return + })} + + ) + }) + } + + return ( + + {!tabletView && ( +
+ + Pool | AMP + + + AMP Liquidity | TVL + + + + APR + + + + Volume (24h) + + + Fees (24h) + + + My Liquidity + + + My Earnings + + + Actions + +
+ )} + + {renderPools()} +
+ ) +} + +export default ClassicPools diff --git a/src/pages/MyEarnings/ElasticPools/SinglePool/SharePoolEarningsButton.tsx b/src/pages/MyEarnings/ElasticPools/SinglePool/SharePoolEarningsButton.tsx index 292e84cd25..43113cbb2e 100644 --- a/src/pages/MyEarnings/ElasticPools/SinglePool/SharePoolEarningsButton.tsx +++ b/src/pages/MyEarnings/ElasticPools/SinglePool/SharePoolEarningsButton.tsx @@ -12,7 +12,8 @@ type Props = { currency1: Currency | undefined currency0Symbol: string currency1Symbol: string - feePercent: string + feePercent?: string + amp?: string } const SharePoolEarningsButton: React.FC = ({ totalValue, @@ -21,6 +22,7 @@ const SharePoolEarningsButton: React.FC = ({ currency0Symbol, currency1Symbol, feePercent, + amp, }) => { const [isOpen, setOpen] = useState(false) @@ -47,13 +49,14 @@ const SharePoolEarningsButton: React.FC = ({ isOpen={isOpen} setIsOpen={setOpen} poolInfo={ - currency0 && currency1 && feePercent + currency0 && currency1 && (feePercent || amp) ? { currency0, currency1, currency0Symbol, currency1Symbol, feePercent, + amp, } : undefined } diff --git a/src/pages/MyEarnings/ElasticPools/SinglePool/index.tsx b/src/pages/MyEarnings/ElasticPools/SinglePool/index.tsx index 09956acb03..4870eea577 100644 --- a/src/pages/MyEarnings/ElasticPools/SinglePool/index.tsx +++ b/src/pages/MyEarnings/ElasticPools/SinglePool/index.tsx @@ -1,14 +1,12 @@ import { ChainId, Token } from '@kyberswap/ks-sdk-core' import { FeeAmount, Pool, Position } from '@kyberswap/ks-sdk-elastic' import { Trans, t } from '@lingui/macro' -import { rgba } from 'polished' import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { BarChart2, Info, Plus } from 'react-feather' import { Link } from 'react-router-dom' import { useMedia } from 'react-use' import { Box, Flex, Text } from 'rebass' import { ElasticPoolEarningWithDetails, ElasticPositionEarningWithDetails } from 'services/earning/types' -import styled, { css } from 'styled-components' import { ReactComponent as DropdownSVG } from 'assets/svg/down.svg' import { ButtonLight } from 'components/Button' @@ -27,6 +25,7 @@ import useTheme from 'hooks/useTheme' import SharePoolEarningsButton from 'pages/MyEarnings/ElasticPools/SinglePool/SharePoolEarningsButton' import Positions from 'pages/MyEarnings/Positions' import { WIDTHS } from 'pages/MyEarnings/constants' +import { Badge, DownIcon, MobileStat, MobileStatWrapper, Row, Wrapper } from 'pages/MyEarnings/styled' import { ButtonIcon } from 'pages/Pools/styleds' import { useAppSelector } from 'state/hooks' import { isAddress, shortenAddress } from 'utils' @@ -34,77 +33,6 @@ import { formatDollarAmount } from 'utils/numbers' import { getTokenSymbolWithHardcode } from 'utils/tokenInfo' import { unwrappedToken } from 'utils/wrappedCurrency' -const DownIcon = styled(DropdownSVG)<{ isOpen: boolean }>` - transform: rotate(${({ isOpen }) => (!isOpen ? '-90deg' : '0')}); - transition: transform 0.3s; -` - -const Wrapper = styled.div` - border-bottom: 1px solid ${({ theme }) => theme.border}; - - :last-child { - border-bottom: none; - } -` - -const MobileStatWrapper = styled(Flex)` - flex-direction: column; - gap: 1rem; -` - -const MobileStat = styled.div<{ mobileView: boolean }>` - display: flex; - justify-content: space-between; - - ${({ mobileView }) => - mobileView && - css` - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0.75rem; - - > *:nth-child(2n) { - align-items: flex-end; - } - `} -` - -const Row = styled.div` - align-items: center; - padding: 12px; - font-size: 14px; - font-weight: 500; - display: grid; - grid-template-columns: 3fr repeat(7, 1fr); - - :hover { - cursor: pointer; - background: ${({ theme }) => rgba(theme.primary, 0.15)}; - } -` - -const Badge = styled.div<{ $color?: string }>` - display: flex; - align-items: center; - gap: 4px; - - padding: 2px 8px; - font-weight: 500; - font-size: 12px; - line-height: 16px; - border-radius: 16px; - - user-select: none; - - color: ${({ $color, theme }) => $color || theme.subText}; - background: ${({ $color, theme }) => rgba($color || theme.subText, 0.3)}; - - ${({ theme }) => theme.mediaWidth.upToExtraSmall` - height: 16px; - padding: 0 4px; - `} -` - export type Props = { chainId: ChainId poolEarning: ElasticPoolEarningWithDetails @@ -113,7 +41,7 @@ export type Props = { tokenPrices: { [id: string]: number } } -const StatItem = ({ label, value }: { label: ReactNode | string; value: ReactNode | string }) => { +export const StatItem = ({ label, value }: { label: ReactNode | string; value: ReactNode | string }) => { const theme = useTheme() return ( @@ -126,6 +54,7 @@ const StatItem = ({ label, value }: { label: ReactNode | string; value: ReactNod ) } + const SinglePool: React.FC = ({ poolEarning, chainId, positionEarnings, pendingFees, tokenPrices }) => { const theme = useTheme() const { mixpanelHandler } = useMixpanel() diff --git a/src/pages/MyEarnings/ElasticPools/index.tsx b/src/pages/MyEarnings/ElasticPools/index.tsx index 5c93ce23ea..98711f2e31 100644 --- a/src/pages/MyEarnings/ElasticPools/index.tsx +++ b/src/pages/MyEarnings/ElasticPools/index.tsx @@ -123,7 +123,7 @@ const ElasticPools = () => { return ( pool.id.toLowerCase() === searchText || - pool.token0.id.toLowerCase().includes(searchText) || + pool.token0.id.toLowerCase() === searchText || pool.token0.symbol.toLowerCase().includes(searchText) || pool.token0.name.toLowerCase().includes(searchText) || pool.token1.id.toLowerCase() === searchText || diff --git a/src/pages/MyEarnings/MultipleChainSelect/PopoverBody.tsx b/src/pages/MyEarnings/MultipleChainSelect/PopoverBody.tsx index 81706f63dc..288ce73c20 100644 --- a/src/pages/MyEarnings/MultipleChainSelect/PopoverBody.tsx +++ b/src/pages/MyEarnings/MultipleChainSelect/PopoverBody.tsx @@ -11,6 +11,7 @@ import Checkbox from 'components/CheckBox' import { MouseoverTooltip } from 'components/Tooltip' import { COMING_SOON_NETWORKS_FOR_MY_EARNINGS, + COMING_SOON_NETWORKS_FOR_MY_EARNINGS_CLASSIC, COMING_SOON_NETWORKS_FOR_MY_EARNINGS_LEGACY, NETWORKS_INFO, SUPPORTED_NETWORKS_FOR_MY_EARNINGS, @@ -103,16 +104,22 @@ const PopoverBody: React.FC = ({ onClose }) => { const theme = useTheme() const { mixpanelHandler } = useMixpanel() const selectAllRef = useRef(null) - const selectedChains = useSelector((state: AppState) => state.myEarnings.selectedChains) - const dispatch = useDispatch() const isLegacy = useAppSelector(state => state.myEarnings.activeTab === VERSION.ELASTIC_LEGACY) + const isClassic = useAppSelector(state => state.myEarnings.activeTab === VERSION.CLASSIC) - const comingSoonList = isLegacy ? COMING_SOON_NETWORKS_FOR_MY_EARNINGS_LEGACY : COMING_SOON_NETWORKS_FOR_MY_EARNINGS + const comingSoonList = isLegacy + ? COMING_SOON_NETWORKS_FOR_MY_EARNINGS_LEGACY + : isClassic + ? COMING_SOON_NETWORKS_FOR_MY_EARNINGS_CLASSIC + : COMING_SOON_NETWORKS_FOR_MY_EARNINGS - const [localSelectedChains, setLocalSelectedChains] = useState(() => - selectedChains.filter(item => !comingSoonList.includes(item)), + const selectedChains = useSelector((state: AppState) => + state.myEarnings.selectedChains.filter(item => !comingSoonList.includes(item)), ) + const dispatch = useDispatch() + + const [localSelectedChains, setLocalSelectedChains] = useState(() => selectedChains) const networkList = SUPPORTED_NETWORKS_FOR_MY_EARNINGS.filter(item => !comingSoonList.includes(item)) @@ -123,17 +130,17 @@ const PopoverBody: React.FC = ({ onClose }) => { useEffect(() => { setLocalSelectedChains(selectedChains) - }, [selectedChains]) + // eslint-disable-next-line + }, [selectedChains.length]) useEffect(() => { if (!selectAllRef.current) { return } - const indeterminate = - 0 < localSelectedChains.length && localSelectedChains.length < SUPPORTED_NETWORKS_FOR_MY_EARNINGS.length + const indeterminate = 0 < localSelectedChains.length && localSelectedChains.length < networkList.length selectAllRef.current.indeterminate = indeterminate - }, [localSelectedChains]) + }, [localSelectedChains, networkList.length]) const allNetworks = [...networkList, ...comingSoonList] diff --git a/src/pages/MyEarnings/MyEarningStats/index.tsx b/src/pages/MyEarnings/MyEarningStats/index.tsx index 6193881964..6b9f37005d 100644 --- a/src/pages/MyEarnings/MyEarningStats/index.tsx +++ b/src/pages/MyEarnings/MyEarningStats/index.tsx @@ -2,10 +2,13 @@ import { Trans } from '@lingui/macro' import { useMemo } from 'react' import { useMedia } from 'react-use' import { Flex, Text } from 'rebass' -import { useGetElasticEarningQuery, useGetElasticLegacyEarningQuery } from 'services/earning' +import { useGetClassicEarningQuery, useGetElasticEarningQuery, useGetElasticLegacyEarningQuery } from 'services/earning' import styled from 'styled-components' -import { COMING_SOON_NETWORKS_FOR_MY_EARNINGS_LEGACY } from 'constants/networks' +import { + COMING_SOON_NETWORKS_FOR_MY_EARNINGS_CLASSIC, + COMING_SOON_NETWORKS_FOR_MY_EARNINGS_LEGACY, +} from 'constants/networks' import { useActiveWeb3React } from 'hooks' import useTheme from 'hooks/useTheme' import ChainSelect from 'pages/MyEarnings/ChainSelect' @@ -84,23 +87,25 @@ const MyEarningStats = () => { account, chainIds: selectedChainIds.filter(item => !COMING_SOON_NETWORKS_FOR_MY_EARNINGS_LEGACY.includes(item)), }) - // const classicEarningQueryResponse = useGetClassicEarningQuery({ account, chainIds: selectedChainIds }) + const classicEarningQueryResponse = useGetClassicEarningQuery({ + account, + chainIds: selectedChainIds.filter(item => !COMING_SOON_NETWORKS_FOR_MY_EARNINGS_CLASSIC.includes(item)), + }) - const isLoading = elasticEarningQueryResponse.isFetching || elasticLegacyEarningQueryResponse.isFetching + const isLoading = + elasticEarningQueryResponse.isLoading || + elasticLegacyEarningQueryResponse.isLoading || + classicEarningQueryResponse.isLoading // chop the data into the right duration // format pool value // multiple chains const ticks: EarningStatsTick[] | undefined = useMemo(() => { return calculateTicksOfAccountEarningsInMultipleChains( - [ - elasticEarningQueryResponse.data, - elasticLegacyEarningQueryResponse.data, - // classicEarningQueryResponse.data - ], + [elasticEarningQueryResponse.data, elasticLegacyEarningQueryResponse.data, classicEarningQueryResponse.data], tokensByChainId, ) - }, [elasticEarningQueryResponse, elasticLegacyEarningQueryResponse, , tokensByChainId]) + }, [elasticEarningQueryResponse, elasticLegacyEarningQueryResponse, classicEarningQueryResponse, tokensByChainId]) const earningBreakdown: EarningsBreakdown | undefined = useMemo(() => { return calculateEarningBreakdowns(ticks?.[0]) diff --git a/src/pages/MyEarnings/PoolEarningsSection.tsx b/src/pages/MyEarnings/PoolEarningsSection.tsx index b1f7f7d68b..7d936728ed 100644 --- a/src/pages/MyEarnings/PoolEarningsSection.tsx +++ b/src/pages/MyEarnings/PoolEarningsSection.tsx @@ -1,6 +1,6 @@ import { ChainId, WETH } from '@kyberswap/ks-sdk-core' import { t } from '@lingui/macro' -import { useMemo } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useMedia } from 'react-use' import { Box, Flex } from 'rebass' import { HistoricalSingleData } from 'services/earning/types' @@ -8,12 +8,14 @@ import styled from 'styled-components' import { useGetNativeTokenLogo } from 'components/CurrencyLogo' import { NativeCurrencies } from 'constants/tokens' +import { fetchListTokenByAddresses } from 'hooks/Tokens' import useTheme from 'hooks/useTheme' import { calculateEarningStatsTick, getToday } from 'pages/MyEarnings/utils' import { useAppSelector } from 'state/hooks' +import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo' import { MEDIA_WIDTHS } from 'theme' import { EarningStatsTick, EarningsBreakdown } from 'types/myEarnings' -import { isAddress } from 'utils' +import { isAddressString } from 'utils' import OriginalEarningsBreakdownPanel from './EarningsBreakdownPanel' import OriginalMyEarningsOverTimePanel from './MyEarningsOverTimePanel' @@ -88,37 +90,48 @@ const PoolEarningsSection: React.FC = ({ historicalEarning, chainId }) => const tokensByChainId = useAppSelector(state => state.lists.mapWhitelistTokens) const nativeLogo = useGetNativeTokenLogo(chainId) + const [tokens, setTokens] = useState<{ [address: string]: WrappedTokenInfo }>({}) + + const missingTokens = useMemo(() => { + const today = getToday() + + return ( + historicalEarning?.[0]?.day === today + ? historicalEarning?.[0].total?.filter(tokenData => { + return !tokensByChainId[chainId][isAddressString(chainId, tokenData.token)] + }) || [] + : [] + ).map(item => item.token) + }, [historicalEarning, tokensByChainId, chainId]) + + useEffect(() => { + fetchListTokenByAddresses(missingTokens, chainId).then(res => + setTokens(res.reduce((acc, cur) => ({ ...acc, [cur.address]: cur }), {})), + ) + }, [missingTokens, chainId]) + const earningBreakdown: EarningsBreakdown | undefined = useMemo(() => { const data = historicalEarning const today = getToday() const latestData = data?.[0]?.day === today - ? data?.[0].total - ?.filter(tokenData => { - const tokenAddress = isAddress(chainId, tokenData.token) - if (!tokenAddress) { - return false - } - - const currency = tokensByChainId[chainId][tokenAddress] - return !!currency - }) - .map(tokenData => { - const tokenAddress = isAddress(chainId, tokenData.token) - const currency = tokensByChainId[chainId][String(tokenAddress)] - const isNative = currency.isNative || tokenAddress === WETH[chainId].address - const symbol = (isNative ? NativeCurrencies[chainId].symbol : currency.symbol) || 'NO SYMBOL' - const logoUrl = (isNative ? nativeLogo : currency.logoURI) || '' - - return { - address: tokenAddress, - logoUrl, - symbol, - amountUSD: Number(tokenData.amountUSD), - chainId, - } - }) || [] + ? data?.[0].total?.map(tokenData => { + const tokenAddress = isAddressString(chainId, tokenData.token) + const currency = tokensByChainId[chainId][String(tokenAddress)] || tokens[tokenAddress] + + const isNative = currency?.isNative || tokenAddress === WETH[chainId].address + const symbol = (isNative ? NativeCurrencies[chainId].symbol : currency?.symbol) || 'NO SYMBOL' + const logoUrl = (isNative ? nativeLogo : currency?.logoURI) || '' + + return { + address: tokenAddress, + logoUrl, + symbol, + amountUSD: Number(tokenData.amountUSD), + chainId, + } + }) || [] : [] latestData.sort((data1, data2) => data2.amountUSD - data1.amountUSD) @@ -158,7 +171,7 @@ const PoolEarningsSection: React.FC = ({ historicalEarning, chainId }) => totalValue, breakdowns, } - }, [chainId, historicalEarning, tokensByChainId, nativeLogo]) + }, [chainId, historicalEarning, tokensByChainId, nativeLogo, tokens]) // format pool value const ticks: EarningStatsTick[] | undefined = useMemo(() => { diff --git a/src/pages/MyEarnings/PoolFilteringBar/ClosedPositionsToggle.tsx b/src/pages/MyEarnings/PoolFilteringBar/ClosedPositionsToggle.tsx index e89158b485..a1ae8a74d5 100644 --- a/src/pages/MyEarnings/PoolFilteringBar/ClosedPositionsToggle.tsx +++ b/src/pages/MyEarnings/PoolFilteringBar/ClosedPositionsToggle.tsx @@ -3,6 +3,7 @@ import { useDispatch } from 'react-redux' import { Flex, Text } from 'rebass' import Toggle from 'components/Toggle' +import { VERSION } from 'constants/v2' import useTheme from 'hooks/useTheme' import { useAppSelector } from 'state/hooks' import { toggleShowClosedPositions } from 'state/myEarnings/actions' @@ -11,6 +12,7 @@ const ClosedPositionsToggle = () => { const dispatch = useDispatch() const theme = useTheme() const isActive = useAppSelector(state => state.myEarnings.shouldShowClosedPositions) + const activeTab = useAppSelector(state => state.myEarnings.activeTab) const toggle = () => { dispatch(toggleShowClosedPositions()) @@ -33,7 +35,7 @@ const ClosedPositionsToggle = () => { whiteSpace: 'nowrap', }} > - Closed Positions + {activeTab === VERSION.CLASSIC ? Removed Liquidity : Closed Positions} diff --git a/src/pages/MyEarnings/Pools/index.tsx b/src/pages/MyEarnings/Pools/index.tsx index dec87d73e2..99cd13d939 100644 --- a/src/pages/MyEarnings/Pools/index.tsx +++ b/src/pages/MyEarnings/Pools/index.tsx @@ -6,6 +6,8 @@ import ElasticPools from 'pages/MyEarnings/ElasticPools' import PoolFilteringBar from 'pages/MyEarnings/PoolFilteringBar' import { useAppSelector } from 'state/hooks' +import ClassicPools from '../ClassicPools' + const Pools = () => { const activeTab = useAppSelector(state => state.myEarnings.activeTab) @@ -14,7 +16,7 @@ const Pools = () => { return } - return null + return } return ( diff --git a/src/pages/MyEarnings/ShareModal.tsx b/src/pages/MyEarnings/ShareModal.tsx index 0960643634..bbd2b02b1c 100644 --- a/src/pages/MyEarnings/ShareModal.tsx +++ b/src/pages/MyEarnings/ShareModal.tsx @@ -138,7 +138,8 @@ type Props = { currency1: Currency currency0Symbol: string currency1Symbol: string - feePercent: string + feePercent?: string + amp?: string } } @@ -248,7 +249,11 @@ export default function ShareModal({ isOpen, setIsOpen, title, value, poolInfo } {poolInfo.currency0Symbol} - {poolInfo.currency1Symbol} - Fee {poolInfo.feePercent} + {poolInfo.feePercent ? ( + Fee {poolInfo.feePercent} + ) : ( + AMP {poolInfo.amp} + )} ) } diff --git a/src/pages/MyEarnings/TotalEarningsAndChainSelect.tsx b/src/pages/MyEarnings/TotalEarningsAndChainSelect.tsx index 7e09b95510..5aeb85ed0b 100644 --- a/src/pages/MyEarnings/TotalEarningsAndChainSelect.tsx +++ b/src/pages/MyEarnings/TotalEarningsAndChainSelect.tsx @@ -3,14 +3,21 @@ import { rgba } from 'polished' import { useEffect, useRef } from 'react' import { useDispatch } from 'react-redux' import { Button, Flex } from 'rebass' -import earningApi, { useLazyGetElasticEarningQuery, useLazyGetElasticLegacyEarningQuery } from 'services/earning' -import styled from 'styled-components' +import earningApi, { + useLazyGetClassicEarningQuery, + useLazyGetElasticEarningQuery, + useLazyGetElasticLegacyEarningQuery, +} from 'services/earning' +import styled, { keyframes } from 'styled-components' import { ReactComponent as RefreshIcon } from 'assets/svg/refresh.svg' import { formatUSDValue } from 'components/EarningAreaChart/utils' import { MouseoverTooltip } from 'components/Tooltip' import { EMPTY_FUNCTION } from 'constants/index' -import { COMING_SOON_NETWORKS_FOR_MY_EARNINGS_LEGACY } from 'constants/networks' +import { + COMING_SOON_NETWORKS_FOR_MY_EARNINGS_CLASSIC, + COMING_SOON_NETWORKS_FOR_MY_EARNINGS_LEGACY, +} from 'constants/networks' import { useActiveWeb3React } from 'hooks' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import useTheme from 'hooks/useTheme' @@ -40,6 +47,20 @@ const Value = styled.span` overflow: hidden; text-overflow: ellipsis; ` +const rotate = keyframes` + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +` +const StyledRefreshIcon = styled(RefreshIcon)` + width: 1rem; + height: 1rem; + animation: ${rotate} 1.5s linear infinite; +` const RefreshButton = () => { const theme = useTheme() @@ -51,9 +72,9 @@ const RefreshButton = () => { const selectedChainIds = useAppSelector(state => state.myEarnings.selectedChains) const [elasticTrigger, elasticData] = useLazyGetElasticEarningQuery() const [elasticLegacyTrigger, elasticLegacyData] = useLazyGetElasticLegacyEarningQuery() - // const [classicTrigger, classicData] = useLazyGetClassicEarningQuery() + const [classicTrigger, classicData] = useLazyGetClassicEarningQuery() - const isFetching = elasticData.isFetching || elasticLegacyData.isFetching + const isFetching = elasticData.isFetching || elasticLegacyData.isFetching || classicData.isFetching refetchRef.current = () => { if (isFetching || !account) { @@ -66,7 +87,10 @@ const RefreshButton = () => { account, chainIds: selectedChainIds.filter(item => !COMING_SOON_NETWORKS_FOR_MY_EARNINGS_LEGACY.includes(item)), }) - // classicTrigger({ account, chainIds: selectedChainIds }) + classicTrigger({ + account, + chainIds: selectedChainIds.filter(item => !COMING_SOON_NETWORKS_FOR_MY_EARNINGS_CLASSIC.includes(item)), + }) } useEffect(() => { @@ -108,7 +132,7 @@ const RefreshButton = () => { refetchRef.current() }} > - + {isFetching ? : } ) } diff --git a/src/pages/MyEarnings/hooks.ts b/src/pages/MyEarnings/hooks.ts index 134d485cdc..88a0e8007a 100644 --- a/src/pages/MyEarnings/hooks.ts +++ b/src/pages/MyEarnings/hooks.ts @@ -34,6 +34,8 @@ import { unwrappedToken } from 'utils/wrappedCurrency' export type ClassicPoolData = { id: string amp: string + apr: string + farmApr: string fee: number reserve0: string reserve1: string @@ -41,12 +43,12 @@ export type ClassicPoolData = { vReserve1: string totalSupply: string reserveUSD: string - volumeUSD: string + volumeUsd: string + volumeUsdOneDayAgo: string + volumeUsdTwoDaysAgo: string feeUSD: string - oneDayVolumeUSD: string - oneDayVolumeUntracked: string - oneDayFeeUSD: string - oneDayFeeUntracked: string + feesUsdOneDayAgo: string + feesUsdTwoDaysAgo: string token0: { id: string symbol: string diff --git a/src/pages/MyEarnings/styled.tsx b/src/pages/MyEarnings/styled.tsx new file mode 100644 index 0000000000..2aba73d535 --- /dev/null +++ b/src/pages/MyEarnings/styled.tsx @@ -0,0 +1,80 @@ +import { rgba } from 'polished' +import { Flex } from 'rebass' +import styled, { css } from 'styled-components' + +import { ReactComponent as DropdownSVG } from 'assets/svg/down.svg' + +export const DownIcon = styled(DropdownSVG)<{ isOpen: boolean }>` + transform: rotate(${({ isOpen }) => (!isOpen ? '-90deg' : '0')}); + transition: transform 0.3s; +` + +export const Wrapper = styled.div` + border-bottom: 1px solid ${({ theme }) => theme.border}; + + :last-child { + border-bottom: none; + } +` + +export const MobileStatWrapper = styled(Flex)` + flex-direction: column; + gap: 1rem; +` + +export const MobileStat = styled.div<{ mobileView: boolean }>` + display: flex; + justify-content: space-between; + + ${({ mobileView }) => + mobileView && + css` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; + + > *:nth-child(2n) { + align-items: flex-end; + } + `} +` + +export const Row = styled.div` + align-items: center; + padding: 12px; + font-size: 14px; + font-weight: 500; + display: grid; + grid-template-columns: 3fr repeat(7, 1fr); + + :hover { + cursor: pointer; + background: ${({ theme }) => rgba(theme.primary, 0.15)}; + } +` + +export const Badge = styled.div<{ $color?: string }>` + display: flex; + align-items: center; + gap: 4px; + + padding: 2px 8px; + font-weight: 500; + font-size: 12px; + line-height: 16px; + border-radius: 16px; + + user-select: none; + + color: ${({ $color, theme }) => $color || theme.subText}; + background: ${({ $color, theme }) => rgba($color || theme.subText, 0.3)}; + + ${({ theme }) => theme.mediaWidth.upToExtraSmall` + height: 16px; + padding: 0 4px; + `} +` + +export const ClassicRow = styled(Row)` + grid-template-columns: 3fr 2fr repeat(6, 1.3fr); +` diff --git a/src/services/earning/types.ts b/src/services/earning/types.ts index 1d3f25eec4..3813e85228 100644 --- a/src/services/earning/types.ts +++ b/src/services/earning/types.ts @@ -1,5 +1,7 @@ import { ChainId } from '@kyberswap/ks-sdk-core' +import { ClassicPoolData } from 'pages/MyEarnings/hooks' + export type TokenEarning = { token: string amount: string @@ -88,9 +90,9 @@ export type ElasticPositionEarningWithDetails = { export type ClassicPositionEarningWithDetails = { id: string ownerOriginal: string - pool: { - id: string - } + liquidityTokenBalance: string + liquidityTokenBalanceIncludingStake: string + pool: ClassicPoolData } & HistoricalEarning export type ClassicPoolEarningWithDetails = ClassicPositionEarningWithDetails