diff --git a/package.json b/package.json index aefbbfa3d7..7ff6a103a5 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,11 @@ "i18n:compile": "lingui compile", "i18n:extract": "lingui extract", "lint": "eslint --ext ts,tsx src", - "start": "vite --mode production", + "start": "vite --mode production --host", "start-adpr": "vite --mode adpr --host", - "start-dev": "vite --mode dev", - "start-stg": "vite --mode stg", - "start-prod": "vite --mode production", + "start-dev": "vite --mode dev --host", + "start-stg": "vite --mode stg --host", + "start-prod": "vite --mode production --host", "test-e2e": "synpress run -cf cypress.config.ts -c baseUrl=http://127.0.0.1:4173/ -e grepTags=smoke,grepFilterSpecs=true,grepOmitFiltered=true", "test-schedule": "synpress run -cf cypress.config.ts -c baseUrl=https://kyberswap.com/ -e grepTags=regression,grepFilterSpecs=true,grepOmitFiltered=true" }, @@ -47,7 +47,7 @@ "@apollo/client": "^3.7.1", "@datadog/browser-rum": "^4.23.3", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", - "@kybernetwork/oauth2": "0.0.9", + "@kybernetwork/oauth2": "1.0.0", "@kyberswap/ks-sdk-classic": "^1.0.3", "@kyberswap/ks-sdk-core": "1.0.7-rc2", "@kyberswap/ks-sdk-elastic": "^1.1.2", @@ -114,6 +114,7 @@ "react-helmet": "^6.1.0", "react-indiana-drag-scroll": "^2.0.1", "react-loading-skeleton": "^3.1.0", + "react-minimal-pie-chart": "8.4.0", "react-popper": "^2.2.3", "react-qrcode-logo": "^2.8.0", "react-redux": "^7.2.9", diff --git a/src/assets/images/bg_share_my_earning.png b/src/assets/images/bg_share_my_earning.png new file mode 100644 index 0000000000..51c4065e7b Binary files /dev/null and b/src/assets/images/bg_share_my_earning.png differ diff --git a/src/assets/images/bg_share_my_earning_mb.png b/src/assets/images/bg_share_my_earning_mb.png new file mode 100644 index 0000000000..41a24000a9 Binary files /dev/null and b/src/assets/images/bg_share_my_earning_mb.png differ diff --git a/src/assets/images/card-background2.png b/src/assets/images/card-background2.png new file mode 100644 index 0000000000..967d608a80 Binary files /dev/null and b/src/assets/images/card-background2.png differ diff --git a/src/assets/images/my-earnings-placeholder.png b/src/assets/images/my-earnings-placeholder.png new file mode 100644 index 0000000000..a63907bf57 Binary files /dev/null and b/src/assets/images/my-earnings-placeholder.png differ diff --git a/src/assets/images/my_earnings_placeholder_desktop_dark.png b/src/assets/images/my_earnings_placeholder_desktop_dark.png new file mode 100644 index 0000000000..3b30e2a9eb Binary files /dev/null and b/src/assets/images/my_earnings_placeholder_desktop_dark.png differ diff --git a/src/assets/images/my_earnings_placeholder_desktop_light.png b/src/assets/images/my_earnings_placeholder_desktop_light.png new file mode 100644 index 0000000000..9317b748c1 Binary files /dev/null and b/src/assets/images/my_earnings_placeholder_desktop_light.png differ diff --git a/src/assets/images/my_earnings_placeholder_mobile_dark.png b/src/assets/images/my_earnings_placeholder_mobile_dark.png new file mode 100644 index 0000000000..d48c528e5c Binary files /dev/null and b/src/assets/images/my_earnings_placeholder_mobile_dark.png differ diff --git a/src/assets/images/my_earnings_placeholder_mobile_light.png b/src/assets/images/my_earnings_placeholder_mobile_light.png new file mode 100644 index 0000000000..3f3eb15ea1 Binary files /dev/null and b/src/assets/images/my_earnings_placeholder_mobile_light.png differ diff --git a/src/assets/svg/barchart.svg b/src/assets/svg/barchart.svg new file mode 100644 index 0000000000..755112a59b --- /dev/null +++ b/src/assets/svg/barchart.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/svg/logo_kyber.svg b/src/assets/svg/logo_kyber.svg new file mode 100644 index 0000000000..9b2ec36771 --- /dev/null +++ b/src/assets/svg/logo_kyber.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/svg/refresh.svg b/src/assets/svg/refresh.svg new file mode 100644 index 0000000000..e25649ae61 --- /dev/null +++ b/src/assets/svg/refresh.svg @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/assets/svg/sprite.svg b/src/assets/svg/sprite.svg index 461aead5be..449d60d81f 100644 --- a/src/assets/svg/sprite.svg +++ b/src/assets/svg/sprite.svg @@ -498,5 +498,22 @@ /> + + + + + + + + + + + diff --git a/src/components/ConfirmAddModalBottom/index.tsx b/src/components/ConfirmAddModalBottom/index.tsx index a6d1edb24b..8207156411 100644 --- a/src/components/ConfirmAddModalBottom/index.tsx +++ b/src/components/ConfirmAddModalBottom/index.tsx @@ -80,7 +80,7 @@ export function ConfirmAddModalBottom({ - {parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} + {parsedAmounts[Field.CURRENCY_A]?.toSignificant(10)} {estimatedUsd && !!estimatedUsd[0] && ( @@ -99,7 +99,7 @@ export function ConfirmAddModalBottom({ - {parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} + {parsedAmounts[Field.CURRENCY_B]?.toSignificant(10)} {estimatedUsd && !!estimatedUsd[1] && ( diff --git a/src/components/CurrencyLogo/index.tsx b/src/components/CurrencyLogo/index.tsx index f353e9f14d..ce05bfcf72 100644 --- a/src/components/CurrencyLogo/index.tsx +++ b/src/components/CurrencyLogo/index.tsx @@ -1,5 +1,5 @@ import { Currency } from '@kyberswap/ks-sdk-core' -import React, { memo, useMemo } from 'react' +import React, { memo, useCallback, useMemo } from 'react' import styled from 'styled-components' import Logo from 'components/Logo' @@ -7,6 +7,7 @@ import { NETWORKS_INFO } from 'constants/networks' import useHttpLocations from 'hooks/useHttpLocations' import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo' import { getTokenLogoURL } from 'utils' +import { getProxyTokenLogo } from 'utils/tokenInfo' const StyledNativeCurrencyLogo = styled.img<{ size: string }>` width: ${({ size }) => size}; @@ -27,26 +28,39 @@ function CurrencyLogo({ currency, size = '24px', style, + useProxy = false, }: { currency?: Currency | WrappedTokenInfo | null size?: string style?: React.CSSProperties + useProxy?: boolean }) { + const wrapWithProxy = useCallback( + (uri: T): T | string => { + if (!useProxy || !uri) { + return uri + } + + return getProxyTokenLogo(uri) + }, + [useProxy], + ) + const logoURI = currency instanceof WrappedTokenInfo ? currency?.logoURI : undefined - const uriLocations = useHttpLocations(logoURI) + const uriLocations = useHttpLocations(wrapWithProxy(logoURI)) const srcs: string[] = useMemo(() => { if (currency?.isNative) return [] if (currency?.isToken) { if (logoURI) { - return [...uriLocations, getTokenLogoURL(currency.address, currency.chainId)] + return [...uriLocations, wrapWithProxy(getTokenLogoURL(currency.address, currency.chainId))] } - return [getTokenLogoURL((currency as any)?.address, currency.chainId)] + return [wrapWithProxy(getTokenLogoURL((currency as any)?.address, currency.chainId))] } return [] - }, [currency, uriLocations, logoURI]) + }, [currency, logoURI, uriLocations, wrapWithProxy]) if (currency?.isNative) { return ( diff --git a/src/components/EarningAreaChart/TooltipContent.tsx b/src/components/EarningAreaChart/TooltipContent.tsx new file mode 100644 index 0000000000..1f49820110 --- /dev/null +++ b/src/components/EarningAreaChart/TooltipContent.tsx @@ -0,0 +1,221 @@ +import { Trans } from '@lingui/macro' +import { darken, rgba } from 'polished' +import { useEffect, useMemo } from 'react' +import { Flex, Text } from 'rebass' +import styled from 'styled-components' + +import Logo, { NetworkLogo } from 'components/Logo' +import useTheme from 'hooks/useTheme' +import { EarningStatsTick } from 'types/myEarnings' +import { formattedNum } from 'utils' + +const formatUSDValue = (v: number) => { + if (v === 0) { + return '$0' + } + + if (v < 0.0001) { + return '< $0.0001' + } + + const formatter = Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + notation: 'compact', + maximumSignificantDigits: 4, + }) + + return formatter.format(v) +} + +const TokensWrapper = styled.div` + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px 16px; + color: ${({ theme }) => theme.subText}; + font-weight: 500; +` + +const formatTokenAmount = (a: number | string) => { + return formattedNum(String(a), false) +} + +type TokensProps = { + tokens: EarningStatsTick['tokens'] +} + +const Tokens: React.FC = ({ tokens }) => { + const theme = useTheme() + const { visibleTokens, hasOthers } = useMemo(() => { + let visibleTokens = [...tokens] + let hasOthers = false + + if (visibleTokens.length > 5) { + visibleTokens = visibleTokens.slice(0, 5) + hasOthers = true + } + + return { + visibleTokens, + hasOthers, + } + }, [tokens]) + + return ( + + {visibleTokens.map((token, i) => { + return ( + + + + + + + {formatTokenAmount(token.amount)} + + + ) + })} + + {hasOthers && ( + + + + Others + + + )} + + ) +} + +type Props = { + setHoverValue: React.Dispatch> + dataEntry: EarningStatsTick +} +const TooltipContent: React.FC = ({ dataEntry, setHoverValue }) => { + const theme = useTheme() + + useEffect(() => { + setHoverValue(dataEntry.totalValue) + }, [dataEntry.totalValue, setHoverValue]) + + return ( + + + {dataEntry.date} + + + + My Total Earnings: {formatUSDValue(dataEntry.totalValue)} + + + + Pool Fees: {formatUSDValue(dataEntry.poolFeesValue)} + + + + Farm Rewards: {formatUSDValue(dataEntry.farmRewardsValue)} + + + + + + + ) +} + +export default TooltipContent diff --git a/src/components/EarningAreaChart/index.tsx b/src/components/EarningAreaChart/index.tsx new file mode 100644 index 0000000000..20181a8274 --- /dev/null +++ b/src/components/EarningAreaChart/index.tsx @@ -0,0 +1,149 @@ +import { useState } from 'react' +import { isMobile } from 'react-device-detect' +import { Area, AreaChart, Customized, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' + +import { EMPTY_FUNCTION } from 'constants/index' +import useTheme from 'hooks/useTheme' +import { TimePeriod } from 'pages/MyEarnings/MyEarningsOverTimePanel/TimePeriodSelect' +import KyberLogo from 'pages/TrueSightV2/components/chart/KyberLogo' +import { EarningStatsTick } from 'types/myEarnings' +import { toFixed } from 'utils/numbers' + +import TooltipContent from './TooltipContent' +import { formatUSDValue } from './utils' + +const labelGapByTimePeriod: Record = { + ['7D']: isMobile ? 2 : 1, + ['1M']: isMobile ? 8 : 3, + ['6M']: isMobile ? 40 : 20, + ['1Y']: isMobile ? 90 : 30, +} + +const CustomizedLabel = (props: any) => { + const theme = useTheme() + const { x, y, value, index, period } = props + const show = (index + 1) % (labelGapByTimePeriod[period as TimePeriod] || 1) === 0 + return ( + <> + {show && ( + + {formatUSDValue(value)} + + )} + + ) +} + +const subscriptMap: { [key: string]: string } = { + '0': '₀', + '1': '₁', + '2': '₂', + '3': '₃', + '4': '₄', + '5': '₅', + '6': '₆', + '7': '₇', + '8': '₈', + '9': '₉', +} + +const formatter = (value: string) => { + const num = Number(value) + const numberOfZero = -Math.floor(Math.log10(num) + 1) + + if (num > 0 && num < 1 && numberOfZero > 2) { + const temp = Number(toFixed(num).split('.')[1]) + return `$0.0${numberOfZero + .toString() + .split('') + .map(item => subscriptMap[item]) + .join('')}${temp > 10 ? (temp / 10).toFixed(0) : temp}` + } + + const formatter = Intl.NumberFormat('en-US', { + notation: num >= 1000 ? 'compact' : 'standard', + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + maximumFractionDigits: 2, + minimumSignificantDigits: 1, + maximumSignificantDigits: 2, + }) + + return formatter.format(num) +} + +type Props = { + period: TimePeriod + setHoverValue?: React.Dispatch> + data: EarningStatsTick[] +} +const EarningAreaChart: React.FC = ({ data, setHoverValue = EMPTY_FUNCTION, period }) => { + const theme = useTheme() + const [containerWidth, setContainerWidth] = useState(0) + const shouldShowLabel = containerWidth > 400 + + return ( + setContainerWidth(width)}> + setHoverValue(null)} + > + + + + + + + + formatter(String(value))} + width={54} + /> + + { + const payload = props.payload as Array<{ + payload: EarningStatsTick + }> + const dataEntry = payload?.[0]?.payload // they are all the same + + if (!dataEntry) { + return null + } + + return + }} + cursor={true} + /> + : undefined} + /> + + + ) +} + +export default EarningAreaChart diff --git a/src/components/EarningAreaChart/utils.ts b/src/components/EarningAreaChart/utils.ts new file mode 100644 index 0000000000..2c7cc4c85d --- /dev/null +++ b/src/components/EarningAreaChart/utils.ts @@ -0,0 +1,19 @@ +export const formatUSDValue = (v: number, compact = true): string => { + if (v === 0) { + return '$0' + } + + if (v < 0.01) { + return '< $0.01' + } + + const formatter = Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + notation: compact ? 'compact' : 'standard', + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }) + + return formatter.format(v) +} diff --git a/src/components/EarningPieChart/index.tsx b/src/components/EarningPieChart/index.tsx new file mode 100644 index 0000000000..82c35d204a --- /dev/null +++ b/src/components/EarningPieChart/index.tsx @@ -0,0 +1,485 @@ +import { ChainId } from '@kyberswap/ks-sdk-core' +import { Trans, t } from '@lingui/macro' +import { darken, rgba } from 'polished' +import { useCallback, useMemo, useState } from 'react' +import { PieChart, pieChartDefaultProps } from 'react-minimal-pie-chart' +import { Flex, Text } from 'rebass' +import styled from 'styled-components' + +import Logo, { NetworkLogo } from 'components/Logo' +import { EMPTY_ARRAY } from 'constants/index' +import useTheme from 'hooks/useTheme' +import { Loading } from 'pages/ProAmmPool/ContentLoader' + +const formatUSDValue = (v: string) => { + const num = Number(v) + + if (num === 0) { + return '$0' + } + + if (num < 0.01) { + return '< $0.01' + } + + const formatter = Intl.NumberFormat('en-US', { + notation: 'compact', + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + maximumFractionDigits: num < 0.1 ? 2 : 1, + }) + + return formatter.format(num) +} + +const formatPercent = (num: number) => { + if (num < 0.01) { + return '< 0.01%' + } + + const formatter = Intl.NumberFormat('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }) + + return formatter.format(num) + '%' +} + +const LegendsWrapper = styled.div` + display: flex; + gap: 4px; +` + +const LegendsColumn = styled.div` + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +` + +const LoadingSkeletonForLegends = () => { + return ( + + {Array(4) + .fill(0) + .map((_, i) => { + return ( + + + + ) + })} + + ) +} + +type LegendProps = { + logoUrl?: string + chainId?: ChainId + label: string + value: string + percent: number + active?: boolean + + onMouseOver: () => void + onMouseOut: () => void +} +const Legend: React.FC = ({ + label, + value, + percent, + logoUrl, + chainId, + active, + onMouseOut, + onMouseOver, +}) => { + const theme = useTheme() + return ( + + + {logoUrl ? ( + + ) : ( + + )} + {chainId && ( + + )} + + + + + {label}: + + + + {formatUSDValue(value)} ({formatPercent(percent)}) + + + + ) +} + +const COLORS = [ + '#2a9d8f', + '#e9c46a', + '#9b59b6', + '#fca311', + '#0086E7', + '#3EC000', + '#e76f51', + '#219ebc', + '#fee440', + '#c0392b', +] + +type DataEntry = { + chainId?: ChainId + logoUrl?: string + symbol: string + value: string + percent: number +} + +type Props = { + className?: string + + isLoading?: boolean + totalValue?: string + data?: DataEntry[] + horizontalLayout?: boolean +} + +const customStyles: React.CSSProperties = { transition: 'all .3s', cursor: 'pointer' } + +const LoadingData = [ + { + title: t`loading`, + value: 100, + color: '#95a5a6', + }, +] + +const EarningPieChart: React.FC = ({ + data, + totalValue = '', + className, + isLoading = false, + horizontalLayout, +}) => { + const [selectedIndex, setSelectedIndex] = useState(-1) + const [isHoveringChart, setHoveringChart] = useState(false) + const theme = useTheme() + + const chartData = useMemo(() => { + if (isLoading || !data) { + return LoadingData + } + + if (data.length === 0) { + return [ + { + title: 'empty', + value: 100, + color: theme.subText, + }, + ] + } + + return data.map((entry, i) => { + const color = selectedIndex === i ? darken(0.15, COLORS[i]) : COLORS[i] + + return { + title: entry.symbol, + value: entry.percent, + color, + } + }) + }, [isLoading, data, theme.subText, selectedIndex]) + + const legendData: Array> = useMemo(() => { + if (isLoading || !data) { + return [EMPTY_ARRAY] + } + + const coloredData = data.map((entry, i) => { + return { + ...entry, + color: COLORS[i], + } + }) + + if (coloredData.length <= 5) { + return [coloredData] + } + + const half = Math.ceil(coloredData.length / 2) + return [coloredData.slice(0, half), coloredData.slice(half)] + }, [data, isLoading]) + + const handleMouseOver = useCallback( + (_: any, index: number) => { + if (isLoading) { + return + } + + if (index >= 0) { + setHoveringChart(true) + setSelectedIndex(index) + } + }, + [isLoading], + ) + + const handleMouseOut = useCallback(() => { + setSelectedIndex(-1) + setHoveringChart(false) + }, []) + + if (horizontalLayout) { + return ( + + + + (!isHoveringChart && index === selectedIndex && chartData.length >= 2 ? 2 : 0)} + onMouseOver={handleMouseOver} + onMouseOut={handleMouseOut} + /> + + + + {isLoading ? loading... : totalValue} + + + + {isLoading ? ( + + ) : ( + + {legendData.map((columnData, columnIndex) => { + if (!columnData.length) { + return null + } + + return ( + + {columnData.map((entry, i) => { + const index = (legendData?.[columnIndex - 1]?.length || 0) + i + return ( + setSelectedIndex(index)} + onMouseOut={() => setSelectedIndex(-1)} + /> + ) + })} + + ) + })} + + )} + + ) + } + + return ( + + + + (!isHoveringChart && index === selectedIndex && chartData.length >= 2 ? 2 : 0)} + onMouseOver={handleMouseOver} + onMouseOut={handleMouseOut} + /> + + + + {isLoading ? loading... : totalValue} + + + + {isLoading ? ( + + ) : ( + + {legendData.map((columnData, columnIndex) => { + if (!columnData.length) { + return null + } + + return ( + + {columnData.map((entry, i) => { + const index = (legendData?.[columnIndex - 1]?.length || 0) + i + return ( + setSelectedIndex(index)} + onMouseOut={() => setSelectedIndex(-1)} + /> + ) + })} + + ) + })} + + )} + + ) +} + +export default EarningPieChart diff --git a/src/components/FarmTag.tsx b/src/components/FarmTag.tsx index 9b3fc34c8f..68c62e97d4 100644 --- a/src/components/FarmTag.tsx +++ b/src/components/FarmTag.tsx @@ -1,3 +1,4 @@ +import { ChainId } from '@kyberswap/ks-sdk-core' import { Trans } from '@lingui/macro' import { transparentize } from 'polished' import { Link } from 'react-router-dom' @@ -5,14 +6,15 @@ import { Text } from 'rebass' import styled from 'styled-components' import { APP_PATHS } from 'constants/index' +import { NETWORKS_INFO } from 'constants/networks' import { useActiveWeb3React } from 'hooks' import { MoneyBag } from './Icons' import { MouseoverTooltip } from './Tooltip' -const FarmAvailableTag = styled.div` +const FarmAvailableTag = styled.div<{ padding: string }>` border-radius: 999px; - padding: 4px 8px; + padding: ${({ padding }) => padding}; height: 20px; background: ${({ theme }) => transparentize(0.7, theme.primary)}; color: ${({ theme }) => theme.primary}; @@ -23,13 +25,23 @@ const FarmAvailableTag = styled.div` gap: 4px; ` -export const FarmTag = ({ address, noTooltip }: { address?: string; noTooltip?: boolean }) => { - const { networkInfo } = useActiveWeb3React() +export const FarmTag = ({ + address, + noTooltip, + noText, + chainId, +}: { + address?: string + noTooltip?: boolean + noText?: boolean + chainId?: ChainId +}) => { + const { chainId: currentChainId } = useActiveWeb3React() const tag = ( - + - Farming + {!noText && Farming} ) @@ -41,7 +53,13 @@ export const FarmTag = ({ address, noTooltip }: { address?: string; noTooltip?: Participate in the Elastic farm to earn more rewards. Click{' '} - here{' '} + + here + {' '} to go to the farm. diff --git a/src/components/Header/groups/EarnNavGroup.tsx b/src/components/Header/groups/EarnNavGroup.tsx index 65c763cf3e..2dfdee206e 100644 --- a/src/components/Header/groups/EarnNavGroup.tsx +++ b/src/components/Header/groups/EarnNavGroup.tsx @@ -3,6 +3,8 @@ import { useLocation } from 'react-router-dom' import { useMedia } from 'react-use' import { Flex } from 'rebass' +import { MoneyBag } from 'components/Icons' +import Icon from 'components/Icons/Icon' import { NewLabel } from 'components/Menu' import { TutorialIds } from 'components/Tutorial/TutorialSwap/constant' import { APP_PATHS } from 'constants/index' @@ -45,15 +47,10 @@ const EarnNavGroup = () => { to={`${APP_PATHS.POOLS}/${networkInfo.route}`} style={{ width: '100%' }} > - Pools - - - - My Pools + + + Pools + { data-testid="farms-nav-link" to={`${APP_PATHS.FARMS}/${networkInfo.route}`} > - Farms - - New - + + + Farms + + + + + + + My Earnings + + New + + + + + + + + My Pools + } diff --git a/src/components/Header/groups/NavGroup.tsx b/src/components/Header/groups/NavGroup.tsx index 013e718b73..65e8227484 100644 --- a/src/components/Header/groups/NavGroup.tsx +++ b/src/components/Header/groups/NavGroup.tsx @@ -1,4 +1,6 @@ import { darken } from 'polished' +import { useState } from 'react' +import { isMobile } from 'react-device-detect' import { Flex } from 'rebass' import styled, { css } from 'styled-components' @@ -51,7 +53,7 @@ const cssDropDown = css` color: ${({ theme }) => darken(0.1, theme.primary)}; } ` -const HoverDropdown = styled.div<{ active: boolean; forceShowDropdown?: boolean }>` +const HoverDropdown = styled.div<{ active: boolean; forceShowDropdown?: boolean; isHovered?: boolean }>` position: relative; display: inline-block; width: fit-content; @@ -64,9 +66,7 @@ const HoverDropdown = styled.div<{ active: boolean; forceShowDropdown?: boolean ${({ forceShowDropdown }) => forceShowDropdown && cssDropDown} - &:hover { - ${cssDropDown} - } + ${({ isHovered }) => isHovered && cssDropDown} ` type Props = { @@ -87,8 +87,20 @@ const NavGroup: React.FC = ({ dropdownAlign = 'left', className, }) => { + const [isHovered, setIsHovered] = useState(false) return ( - + setIsHovered(true)} + onClick={() => { + setIsHovered(true) + }} + onMouseLeave={() => setIsHovered(false)} + > = ({ {anchor} {dropdownContent && } - {dropdownContent && {dropdownContent}} + {dropdownContent && ( + { + e.stopPropagation() + isMobile && setIsHovered(false) + }} + $align={dropdownAlign} + > + {dropdownContent} + + )} ) } diff --git a/src/components/Icons/Search.tsx b/src/components/Icons/Search.tsx index 734f259ea6..ffa283cc52 100644 --- a/src/components/Icons/Search.tsx +++ b/src/components/Icons/Search.tsx @@ -1,6 +1,7 @@ -function Search({ size = 24, color }: { size?: number; color?: string }) { +function Search({ size = 24, color, onClick }: { size?: number; color?: string; onClick?: () => void }) { return ( d.price0 const yAccessor = (d: ChartEntry) => d.activeLiquidity +const Zoom = styled(OriginalZoom)<{ $interactive: boolean }>` + ${({ $interactive }) => (!$interactive ? 'top: -46px;' : '')} +` + export function Chart({ - id = 'liquidityChartRangeInput', data: { series, current }, ticksAtLimit, styles, @@ -26,6 +30,8 @@ export function Chart({ onBrushDomainChange, zoomLevels, }: LiquidityChartRangeInputProps) { + const id = useId() + const viewBoxHeight = 200 const zoomRef = useRef(null) @@ -79,9 +85,9 @@ export function Chart({ return ( <> @@ -60,6 +61,7 @@ export default function Zoom({ showResetButton: boolean zoomLevels: ZoomLevels style?: CSSProperties + className?: string }) { const zoomBehavior = useRef>() @@ -114,7 +116,7 @@ export default function Zoom({ }, [zoomInitial, zoomLevels]) return ( - + {showResetButton && ( ) } + +export default styled(Zoom)`` diff --git a/src/components/LiquidityChartRangeInput/index.tsx b/src/components/LiquidityChartRangeInput/index.tsx index c8d4ef8d35..0517f7f4b8 100644 --- a/src/components/LiquidityChartRangeInput/index.tsx +++ b/src/components/LiquidityChartRangeInput/index.tsx @@ -118,6 +118,7 @@ export default function LiquidityChartRangeInput({ interactive, style = {}, height, + className, }: { currencyA: Currency | undefined currencyB: Currency | undefined @@ -131,6 +132,7 @@ export default function LiquidityChartRangeInput({ interactive: boolean style?: CSSProperties height?: string + className?: string }) { const theme = useTheme() const ref = useRef(null) @@ -220,7 +222,7 @@ export default function LiquidityChartRangeInput({ }, [height, clientWidth]) return ( - + {isUninitialized ? ( Your position will appear here.} diff --git a/src/components/NavigationTabs/index.tsx b/src/components/NavigationTabs/index.tsx index 75fd7c35cc..674a0b2a10 100644 --- a/src/components/NavigationTabs/index.tsx +++ b/src/components/NavigationTabs/index.tsx @@ -1,6 +1,6 @@ import { Trans, t } from '@lingui/macro' import { ArrowLeft, ChevronLeft, Trash } from 'react-feather' -import { useNavigate } from 'react-router-dom' +import { useLocation, useNavigate } from 'react-router-dom' import { useMedia } from 'react-use' import { Flex, Text } from 'rebass' import styled, { css } from 'styled-components' @@ -89,9 +89,11 @@ export const StyledMenuButton = styled.button<{ active?: boolean }>` export function FindPoolTabs() { const navigate = useNavigate() - + const location = useLocation() const goBack = () => { - navigate(-1) + // https://github.com/remix-run/react-router/discussions/9922 + if (location.key === 'default') navigate('/') + else navigate(-1) } return ( @@ -143,9 +145,12 @@ export function AddRemoveTabs({ }) { const { chainId } = useActiveWeb3React() const navigate = useNavigate() + const location = useLocation() const below768 = useMedia('(max-width: 768px)') const goBack = () => { - navigate(-1) + // https://github.com/remix-run/react-router/discussions/9922 + if (location.key === 'default') navigate('/') + else navigate(-1) } const theme = useTheme() diff --git a/src/components/PoolPriceBar/index.tsx b/src/components/PoolPriceBar/index.tsx index 4885564e8a..77ebdb561c 100644 --- a/src/components/PoolPriceBar/index.tsx +++ b/src/components/PoolPriceBar/index.tsx @@ -106,7 +106,7 @@ export function PoolPriceBar({ {nativeB?.symbol} per {nativeA?.symbol} - {price?.toSignificant(6) ?? '-'} + {price?.toSignificant(10) ?? '-'} @@ -115,7 +115,7 @@ export function PoolPriceBar({ {nativeA?.symbol} per {nativeB?.symbol} - {price?.invert()?.toSignificant(6) ?? '-'} + {price?.invert()?.toSignificant(10) ?? '-'} diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 282cfb1b2d..5d99e5900d 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -79,7 +79,7 @@ const Search = ({ searchValue, onSearch, placeholder, minWidth, style }: SearchP )} - + onSearch(searchValue)} /> ) diff --git a/src/components/ShareModal/index.tsx b/src/components/ShareModal/index.tsx index 816aed6368..dfa1d935b1 100644 --- a/src/components/ShareModal/index.tsx +++ b/src/components/ShareModal/index.tsx @@ -115,6 +115,8 @@ export const ShareGroupButtons = ({ renderItem?: (props: PropsItem) => JSX.Element size?: number }) => { + const { telegram, twitter, facebook, discord } = getSocialShareUrls(shareUrl) + const ShareItem = (props: PropsItem) => ( {props.children} @@ -122,14 +124,11 @@ export const ShareGroupButtons = ({ ) return ( - <> + {(color: string) => ( <> - + {showLabel && Telegram} @@ -139,10 +138,7 @@ export const ShareGroupButtons = ({ {(color: string) => ( <> - + {showLabel && Twitter} @@ -152,10 +148,7 @@ export const ShareGroupButtons = ({ {(color: string) => ( <> - + {showLabel && Facebook} @@ -165,17 +158,26 @@ export const ShareGroupButtons = ({ {(color: string) => ( <> - + {showLabel && Discord} )} - + ) } +export const getSocialShareUrls = (shareUrl: string) => { + return { + telegram: 'https://telegram.me/share/url?url=' + encodeURIComponent(shareUrl), + twitter: 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(shareUrl), + facebook: 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(shareUrl), + discord: 'https://discord.com/app/', + } +} + const noop = () => { // empty } @@ -218,9 +220,9 @@ export default function ShareModal({ - - - + + + diff --git a/src/components/SubscribeButton/index.tsx b/src/components/SubscribeButton/index.tsx index 2cb6de326e..6c27ce3ac7 100644 --- a/src/components/SubscribeButton/index.tsx +++ b/src/components/SubscribeButton/index.tsx @@ -1,104 +1,104 @@ -import { Trans } from '@lingui/macro' -import { ReactNode, useCallback, useMemo } from 'react' -import { useNavigate } from 'react-router-dom' -import { Text } from 'rebass' -import styled, { css } from 'styled-components' - -import NotificationIcon from 'components/Icons/NotificationIcon' -import { APP_PATHS } from 'constants/index' -import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' -import useNotification from 'hooks/useNotification' -import useTheme from 'hooks/useTheme' -import { PROFILE_MANAGE_ROUTES } from 'pages/NotificationCenter/const' - -import { ButtonPrimary } from '../Button' -import { MouseoverTooltipDesktopOnly } from '../Tooltip' - -const cssSubscribeBtnSmall = (bgColor: string) => css` - width: 36px; - min-width: 36px; - padding: 6px; - background: ${bgColor}; - &:hover { - background: ${bgColor}; - } -` -const SubscribeBtn = styled(ButtonPrimary)<{ - isDisabled?: boolean - iconOnly?: boolean - bgColor: string -}>` - overflow: hidden; - width: fit-content; - height: 36px; - padding: 8px 12px; - background: ${({ bgColor }) => bgColor}; - color: ${({ theme, isDisabled }) => (isDisabled ? theme.border : theme.textReverse)}; - &:hover { - background: ${({ bgColor }) => bgColor}; - } - ${({ iconOnly, bgColor }) => iconOnly && cssSubscribeBtnSmall(bgColor)}; - ${({ theme, bgColor }) => theme.mediaWidth.upToExtraSmall` - ${cssSubscribeBtnSmall(bgColor)} - `} -` - -const ButtonText = styled(Text)<{ iconOnly?: boolean }>` - font-size: 14px; - font-weight: 500; - margin-left: 6px !important; - ${({ iconOnly }) => iconOnly && `display: none`}; - ${({ theme }) => theme.mediaWidth.upToExtraSmall` - display: none; - `} -` -export default function SubscribeNotificationButton({ - subscribeTooltip, - iconOnly = false, - trackingEvent, - onClick, - topicId, -}: { - subscribeTooltip?: ReactNode - iconOnly?: boolean - trackingEvent?: MIXPANEL_TYPE - onClick?: () => void - topicId?: string -}) { - const theme = useTheme() - - const { mixpanelHandler } = useMixpanel() - const { topicGroups } = useNotification() - const hasSubscribe = useMemo(() => { - return topicId - ? topicGroups.some(group => - group.topics.some(topic => topic.isSubscribed && String(topic.id) === String(topicId)), - ) - : false - }, [topicGroups, topicId]) - - const navigate = useNavigate() - const showNotificationModal = useCallback(() => { - navigate(`${APP_PATHS.PROFILE_MANAGE}${PROFILE_MANAGE_ROUTES.PREFERENCE}`) - }, [navigate]) - - const onClickBtn = () => { - showNotificationModal() - onClick?.() - if (trackingEvent) - setTimeout(() => { - mixpanelHandler(trackingEvent) - }, 100) - } - - return ( - - - - - {hasSubscribe ? Unsubscribe : Subscribe} - - - - ) -} +import { Trans } from '@lingui/macro' +import { ReactNode, useCallback, useMemo } from 'react' +import { useNavigate } from 'react-router-dom' +import { Text } from 'rebass' +import styled, { css } from 'styled-components' + +import NotificationIcon from 'components/Icons/NotificationIcon' +import { APP_PATHS } from 'constants/index' +import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' +import useNotification from 'hooks/useNotification' +import useTheme from 'hooks/useTheme' +import { PROFILE_MANAGE_ROUTES } from 'pages/NotificationCenter/const' + +import { ButtonPrimary } from '../Button' +import { MouseoverTooltipDesktopOnly } from '../Tooltip' + +const cssSubscribeBtnSmall = (bgColor: string) => css` + width: 36px; + min-width: 36px; + padding: 6px; + background: ${bgColor}; + &:hover { + background: ${bgColor}; + } +` +const SubscribeBtn = styled(ButtonPrimary)<{ + isDisabled?: boolean + iconOnly?: boolean + bgColor: string +}>` + overflow: hidden; + width: fit-content; + height: 36px; + padding: 8px 12px; + background: ${({ bgColor }) => bgColor}; + color: ${({ theme, isDisabled }) => (isDisabled ? theme.border : theme.textReverse)}; + &:hover { + background: ${({ bgColor }) => bgColor}; + } + ${({ iconOnly, bgColor }) => iconOnly && cssSubscribeBtnSmall(bgColor)}; + ${({ theme, bgColor }) => theme.mediaWidth.upToExtraSmall` + ${cssSubscribeBtnSmall(bgColor)} + `} +` + +const ButtonText = styled(Text)<{ iconOnly?: boolean }>` + font-size: 14px; + font-weight: 500; + margin-left: 6px !important; + ${({ iconOnly }) => iconOnly && `display: none`}; + ${({ theme }) => theme.mediaWidth.upToExtraSmall` + display: none; + `} +` +export default function SubscribeNotificationButton({ + subscribeTooltip, + iconOnly = false, + trackingEvent, + onClick, + topicId, +}: { + subscribeTooltip?: ReactNode + iconOnly?: boolean + trackingEvent?: MIXPANEL_TYPE + onClick?: () => void + topicId?: string +}) { + const theme = useTheme() + + const { mixpanelHandler } = useMixpanel() + const { topicGroups } = useNotification() + const hasSubscribe = useMemo(() => { + return topicId + ? topicGroups.some(group => + group.topics.some(topic => topic.isSubscribed && String(topic.id) === String(topicId)), + ) + : false + }, [topicGroups, topicId]) + + const navigate = useNavigate() + const showNotificationModal = useCallback(() => { + navigate(`${APP_PATHS.PROFILE_MANAGE}${PROFILE_MANAGE_ROUTES.PREFERENCE}`) + }, [navigate]) + + const onClickBtn = () => { + showNotificationModal() + onClick?.() + if (trackingEvent) + setTimeout(() => { + mixpanelHandler(trackingEvent) + }, 100) + } + + return ( + + + + + {hasSubscribe ? Unsubscribe : Subscribe} + + + + ) +} diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx index 704af63ba2..7015a0deba 100644 --- a/src/components/Tooltip/index.tsx +++ b/src/components/Tooltip/index.tsx @@ -19,6 +19,11 @@ export const TextDashed = styled(Text)<{ color?: string; underlineColor?: string border-bottom: 1px dotted ${({ theme, underlineColor }) => underlineColor || theme.border}; ` +export const TextDotted = styled(Text)<{ $underlineColor?: string }>` + width: fit-content; + border-bottom: 1px dotted ${({ theme, $underlineColor }) => $underlineColor || theme.border}; +` + interface TooltipProps extends Omit { text: string | ReactNode delay?: number diff --git a/src/components/YieldPools/FarmingPoolAPRCell.tsx b/src/components/YieldPools/FarmingPoolAPRCell.tsx index 19304b7f3e..bebfd9565f 100644 --- a/src/components/YieldPools/FarmingPoolAPRCell.tsx +++ b/src/components/YieldPools/FarmingPoolAPRCell.tsx @@ -16,15 +16,6 @@ import { useTokenPrices } from 'state/tokenPrices/hooks' import { MEDIA_WIDTHS } from 'theme' import { useFarmApr } from 'utils/dmm' -type Props = { - poolAPR: number - farmV1APR?: number - farmV2APR?: number - fairlaunchAddress: string - pid: number - tooltipPlacement?: Placement -} - export const APRTooltipContent = ({ poolAPR, farmAPR, @@ -97,7 +88,7 @@ export const APRTooltipContent = ({ }} > - Dynamic Farm APR:{' '} + Farm APR:{' '} {farmAPR.toFixed(2)}% @@ -123,7 +114,7 @@ export const APRTooltipContent = ({ }} > - Static Farm APR:{' '} + Farm APR:{' '} {farmV2APR.toFixed(2)}% @@ -143,6 +134,15 @@ export const APRTooltipContent = ({ ) } +type Props = { + poolAPR: number + farmV1APR?: number + farmV2APR?: number + fairlaunchAddress: string + pid: number + tooltipPlacement?: Placement +} + const FarmingPoolAPRCell: React.FC = ({ poolAPR, farmV1APR, diff --git a/src/constants/index.ts b/src/constants/index.ts index 191046c877..d5b001251f 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -12,6 +12,9 @@ import { ENV_TYPE } from './type' export const EMPTY_OBJECT: any = {} export const EMPTY_ARRAY: any[] = [] +export const EMPTY_FUNCTION = () => { + // empty +} export const BAD_RECIPIENT_ADDRESSES: string[] = [ NETWORKS_INFO[ChainId.MAINNET].classic.static.factory, @@ -210,6 +213,7 @@ export const APP_PATHS = { ELASTIC_REMOVE_POOL: '/elastic/remove', FARMS: '/farms', MY_POOLS: '/myPools', + MY_EARNINGS: '/my-earnings', DISCOVER: '/discover', KYBERAI: '/KyberAI', KYBERAI_ABOUT: '/KyberAI/About', diff --git a/src/constants/networks.ts b/src/constants/networks.ts index af5dd1e457..1a44747bc4 100644 --- a/src/constants/networks.ts +++ b/src/constants/networks.ts @@ -201,5 +201,32 @@ export const DEFAULT_REWARDS: { [key: string]: string[] } = { [ChainId.MAINNET]: ['0x9F52c8ecbEe10e00D9faaAc5Ee9Ba0fF6550F511'], } +export const SUPPORTED_NETWORKS_FOR_MY_EARNINGS = [ + ChainId.MAINNET, + ChainId.ARBITRUM, + ChainId.OPTIMISM, + // ChainId.MATIC, + // ChainId.BSCMAINNET, + // ChainId.AVAXMAINNET, + // ChainId.FANTOM, + // ChainId.CRONOS, + // ChainId.BTTC, + // ChainId.VELAS, + // ChainId.AURORA, + // ChainId.OASIS, +] +export const COMING_SOON_NETWORKS_FOR_MY_EARNINGS = [ + ChainId.MATIC, + ChainId.BSCMAINNET, + ChainId.AVAXMAINNET, + ChainId.FANTOM, + ChainId.CRONOS, + ChainId.BTTC, + ChainId.VELAS, + ChainId.AURORA, + ChainId.OASIS, + ChainId.ZKSYNC, +] + // 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 fec377ee77..00a1a361cb 100644 --- a/src/hooks/Tokens.ts +++ b/src/hooks/Tokens.ts @@ -234,9 +234,10 @@ export function useToken(tokenAddress?: string): Token | NativeCurrency | undefi } // This function is intended to use for EVM chains only -export function useFetchERC20TokenFromRPC() { - const { chainId } = useActiveWeb3React() - const multicallContract = useMulticallContract() +export function useFetchERC20TokenFromRPC(customChainId?: ChainId) { + const { chainId: activeChainId } = useActiveWeb3React() + const chainId = customChainId || activeChainId + const multicallContract = useMulticallContract(chainId) const fetcher = useCallback( async (tokenAddress: string) => { diff --git a/src/hooks/useElasticLegacy.ts b/src/hooks/useElasticLegacy.ts index 128e942c51..465b472cf3 100644 --- a/src/hooks/useElasticLegacy.ts +++ b/src/hooks/useElasticLegacy.ts @@ -27,6 +27,7 @@ export const config: { subgraphUrl: string farmContract?: string positionManagerContract: string + tickReaderContract: string } } = { [ChainId.MAINNET]: { @@ -34,64 +35,75 @@ export const config: { 'https://ethereum-graph.kyberengineering.io/subgraphs/name/kybernetwork/kyberswap-elastic-ethereum-legacy', farmContract: '0xb85ebe2e4ea27526f817ff33fb55fb240057c03f', positionManagerContract: '0x2B1c7b41f6A8F2b2bc45C3233a5d5FB3cD6dC9A8', + tickReaderContract: '0x165c68077ac06c83800d19200e6E2B08D02dE75D', }, [ChainId.BSCMAINNET]: { subgraphUrl: 'https://bsc-graph.kyberengineering.io/subgraphs/name/kybernetwork/kyberswap-elastic-bsc-legacy', farmContract: '', positionManagerContract: '0x2B1c7b41f6A8F2b2bc45C3233a5d5FB3cD6dC9A8', + tickReaderContract: '0x165c68077ac06c83800d19200e6E2B08D02dE75D', }, [ChainId.ARBITRUM]: { subgraphUrl: 'https://arbitrum-graph.kyberengineering.io/subgraphs/name/kybernetwork/kyberswap-elastic-arbitrum-legacy', farmContract: '0xbdec4a045446f583dc564c0a227ffd475b329bf0', positionManagerContract: '0x2B1c7b41f6A8F2b2bc45C3233a5d5FB3cD6dC9A8', + tickReaderContract: '0x165c68077ac06c83800d19200e6E2B08D02dE75D', }, [ChainId.AVAXMAINNET]: { subgraphUrl: 'https://avalanche-graph.kyberengineering.io/subgraphs/name/kybernetwork/kyberswap-elastic-avalanche-legacy', farmContract: '0xbdec4a045446f583dc564c0a227ffd475b329bf0', positionManagerContract: '0x2B1c7b41f6A8F2b2bc45C3233a5d5FB3cD6dC9A8', + tickReaderContract: '0x165c68077ac06c83800d19200e6E2B08D02dE75D', }, [ChainId.OPTIMISM]: { subgraphUrl: 'https://optimism-graph.kyberengineering.io/subgraphs/name/kybernetwork/kyberswap-elastic-optimism-legacy', farmContract: '0xb85ebe2e4ea27526f817ff33fb55fb240057c03f', positionManagerContract: '0x2B1c7b41f6A8F2b2bc45C3233a5d5FB3cD6dC9A8', + tickReaderContract: '0x165c68077ac06c83800d19200e6E2B08D02dE75D', }, [ChainId.MATIC]: { subgraphUrl: 'https://polygon-graph.kyberengineering.io/subgraphs/name/kybernetwork/kyberswap-elastic-polygon-legacy', farmContract: '0xbdec4a045446f583dc564c0a227ffd475b329bf0', positionManagerContract: '0x2B1c7b41f6A8F2b2bc45C3233a5d5FB3cD6dC9A8', + tickReaderContract: '0x165c68077ac06c83800d19200e6E2B08D02dE75D', }, [ChainId.FANTOM]: { subgraphUrl: 'https://fantom-graph.kyberengineering.io/subgraphs/name/kybernetwork/kyberswap-elastic-fantom-legacy', farmContract: '', positionManagerContract: '0x2B1c7b41f6A8F2b2bc45C3233a5d5FB3cD6dC9A8', + tickReaderContract: '0x165c68077ac06c83800d19200e6E2B08D02dE75D', }, [ChainId.BTTC]: { subgraphUrl: 'https://bttc-graph.kyberengineering.io/subgraphs/name/kybernetwork/kyberswap-elastic-bttc-legacy', farmContract: '', positionManagerContract: '0x2B1c7b41f6A8F2b2bc45C3233a5d5FB3cD6dC9A8', + tickReaderContract: '0x165c68077ac06c83800d19200e6E2B08D02dE75D', }, [ChainId.CRONOS]: { subgraphUrl: 'https://cronos-graph.kyberengineering.io/subgraphs/name/kybernetwork/kyberswap-elastic-cronos-legacy', farmContract: '', positionManagerContract: '0x2B1c7b41f6A8F2b2bc45C3233a5d5FB3cD6dC9A8', + tickReaderContract: '0x165c68077ac06c83800d19200e6E2B08D02dE75D', }, [ChainId.VELAS]: { subgraphUrl: 'https://velas-graph.kyberengineering.io/subgraphs/name/kybernetwork/kyberswap-elastic-velas-legacy', farmContract: '', positionManagerContract: '0x2B1c7b41f6A8F2b2bc45C3233a5d5FB3cD6dC9A8', + tickReaderContract: '0x165c68077ac06c83800d19200e6E2B08D02dE75D', }, [ChainId.OASIS]: { subgraphUrl: 'https://oasis-graph.kyberengineering.io/subgraphs/name/kybernetwork/kyberswap-elastic-oasis-legacy', farmContract: '', positionManagerContract: '0x2B1c7b41f6A8F2b2bc45C3233a5d5FB3cD6dC9A8', + tickReaderContract: '0x165c68077ac06c83800d19200e6E2B08D02dE75D', }, } diff --git a/src/hooks/useMixpanel.ts b/src/hooks/useMixpanel.ts index c894197dd1..7e4bca5b85 100644 --- a/src/hooks/useMixpanel.ts +++ b/src/hooks/useMixpanel.ts @@ -261,6 +261,18 @@ export enum MIXPANEL_TYPE { CROSS_CHAIN_CLICK_DISCLAIMER_CHECKBOX, CROSS_CHAIN_TXS_SUBMITTED, CROSS_CHAIN_CLICK_SUBSCRIBE, + + // earning dashboard + EARNING_DASHBOARD_CLICK_TOP_LEVEL_SHARE_BUTTON, + EARNING_DASHBOARD_SHARE_SUCCESSFULLY, + EARNING_DASHBOARD_CLICK_POOL_EXPAND, + EARNING_DASHBOARD_CLICK_ALL_CHAINS_BUTTON, + EARNING_DASHBOARD_CLICK_REFRESH_BUTTON, + EARNING_DASHBOARD_CLICK_CHANGE_TIMEFRAME_EARNING_CHART, + EARNING_DASHBOARD_CLICK_ADD_LIQUIDITY_BUTTON, + EARNING_DASHBOARD_CLICK_CURRENT_CHAIN_BUTTON, + EARNING_DASHBOARD_VIEW_PAGE, + EARNING_DASHBOARD_CLICK_SUBSCRIBE, } export const NEED_CHECK_SUBGRAPH_TRANSACTION_TYPES: readonly TRANSACTION_TYPE[] = [ @@ -1254,6 +1266,63 @@ export default function useMixpanel(currencies?: { [field in Field]?: Currency } mixpanel.track('KyberAI - Click Awesome Button', payload) break } + + case MIXPANEL_TYPE.EARNING_DASHBOARD_CLICK_TOP_LEVEL_SHARE_BUTTON: { + mixpanel.track('Earning Dashboard - Share button click') + break + } + + case MIXPANEL_TYPE.EARNING_DASHBOARD_SHARE_SUCCESSFULLY: { + mixpanel.track('Earning Dashboard - Share success', { + option: payload, + }) + break + } + + case MIXPANEL_TYPE.EARNING_DASHBOARD_CLICK_POOL_EXPAND: { + const { pool_name, pool_address } = payload as { + pool_name: string + pool_address: string + } + + mixpanel.track('Earning Dashboard - Pool expand click', { pool_name, pool_address }) + break + } + + case MIXPANEL_TYPE.EARNING_DASHBOARD_CLICK_ALL_CHAINS_BUTTON: { + mixpanel.track('Earning Dashboard - All Chain button click') + break + } + + case MIXPANEL_TYPE.EARNING_DASHBOARD_CLICK_REFRESH_BUTTON: { + mixpanel.track('Earning Dashboard - Refresh button click') + break + } + + case MIXPANEL_TYPE.EARNING_DASHBOARD_CLICK_CHANGE_TIMEFRAME_EARNING_CHART: { + mixpanel.track('Earning Dashboard - Multi chain earning chart - Change timeframe') + break + } + + case MIXPANEL_TYPE.EARNING_DASHBOARD_CLICK_ADD_LIQUIDITY_BUTTON: { + mixpanel.track('Earning Dashboard - Add liquidity button click') + break + } + + case MIXPANEL_TYPE.EARNING_DASHBOARD_CLICK_CURRENT_CHAIN_BUTTON: { + mixpanel.track('Earning Dashboard - Current chain button click') + break + } + + case MIXPANEL_TYPE.EARNING_DASHBOARD_VIEW_PAGE: { + mixpanel.track('Earning Dashboard - Page View') + break + } + + case MIXPANEL_TYPE.EARNING_DASHBOARD_CLICK_SUBSCRIBE: { + mixpanel.track('Earning Dashboard - Subscribe Click') + break + } } // Whitelist protected events diff --git a/src/hooks/usePoolTickData.ts b/src/hooks/usePoolTickData.ts index f75963918c..75d25b493b 100644 --- a/src/hooks/usePoolTickData.ts +++ b/src/hooks/usePoolTickData.ts @@ -1,16 +1,17 @@ import { useQuery } from '@apollo/client' -import { Currency } from '@kyberswap/ks-sdk-core' +import { ChainId, Currency } from '@kyberswap/ks-sdk-core' import { FeeAmount, TICK_SPACINGS, tickToPrice } from '@kyberswap/ks-sdk-elastic' import JSBI from 'jsbi' import { useMemo } from 'react' import { ALL_TICKS, Tick } from 'apollo/queries/promm' +import { isEVM as isEVMNetwork } from 'constants/networks' import { useActiveWeb3React } from 'hooks' +import { usePoolv2 } from 'hooks/usePoolv2' import { useKyberSwapConfig } from 'state/application/hooks' import computeSurroundingTicks from 'utils/computeSurroundingTicks' -import { PoolState, usePool } from './usePools' -import useProAmmPoolInfo from './useProAmmPoolInfo' +import { PoolState } from './usePools' const PRICE_FIXED_DIGITS = 8 @@ -27,33 +28,20 @@ const getActiveTick = (tickCurrent: number | undefined, feeAmount: FeeAmount | u ? Math.floor(tickCurrent / TICK_SPACINGS[feeAmount]) * TICK_SPACINGS[feeAmount] : undefined -const useAllTicks = (poolAddress: string) => { - const { isEVM } = useActiveWeb3React() - const { elasticClient } = useKyberSwapConfig() +// Fetches all ticks for a given pool +function useAllV3Ticks(chainId: ChainId, poolAddress = '') { + const { elasticClient } = useKyberSwapConfig(chainId) + const isEVM = isEVMNetwork(chainId) - return useQuery(ALL_TICKS(poolAddress?.toLowerCase()), { + const { + loading: isLoading, + data, + error, + } = useQuery(ALL_TICKS(poolAddress.toLowerCase()), { client: elasticClient, pollInterval: 30_000, skip: !isEVM || !poolAddress, }) -} - -// Fetches all ticks for a given pool -function useAllV3Ticks( - currencyA: Currency | undefined, - currencyB: Currency | undefined, - feeAmount: FeeAmount | undefined, -) { - const poolAddress = useProAmmPoolInfo(currencyA?.wrapped, currencyB?.wrapped, feeAmount) - - const { loading: isLoading, data, error } = useAllTicks(poolAddress) - - // const { isLoading, isError, error, isUninitialized, data } = useAllV3TicksQuery( - // poolAddress ? { poolAddress: poolAddress?.toLowerCase(), skip: 0 } : skipToken, - // { - // pollingInterval: 30_000, - // }, - // ) return { isLoading, @@ -76,25 +64,28 @@ export function usePoolActiveLiquidity( activeTick: number | undefined data: TickProcessed[] | undefined } { - const pool = usePool(currencyA, currencyB, feeAmount) + const { chainId: currentChainId } = useActiveWeb3React() + const chainId = currencyA?.chainId || currentChainId + + const { pool, poolState, computedPoolAddress } = usePoolv2(chainId, currencyA, currencyB, feeAmount) // Find nearest valid tick for pool in case tick is not initialized. - const activeTick = useMemo(() => getActiveTick(pool[1]?.tickCurrent, feeAmount), [pool, feeAmount]) - const { isLoading, isUninitialized, isError, error, ticks } = useAllV3Ticks(currencyA, currencyB, feeAmount) + const activeTick = useMemo(() => getActiveTick(pool?.tickCurrent, feeAmount), [pool, feeAmount]) + const { isLoading, isUninitialized, isError, error, ticks } = useAllV3Ticks(chainId, computedPoolAddress) return useMemo(() => { if ( !currencyA || !currencyB || activeTick === undefined || - pool[0] !== PoolState.EXISTS || + poolState !== PoolState.EXISTS || !ticks || ticks.length === 0 || isLoading || isUninitialized ) { return { - isLoading: isLoading || pool[0] === PoolState.LOADING, + isLoading: isLoading || poolState === PoolState.LOADING, isUninitialized, isError, error, @@ -125,7 +116,7 @@ export function usePoolActiveLiquidity( } const activeTickProcessed: TickProcessed = { - liquidityActive: JSBI.BigInt(pool[1]?.liquidity ?? 0), + liquidityActive: JSBI.BigInt(pool?.liquidity ?? 0), tickIdx: activeTick, liquidityNet: Number(ticks[pivot].tickIdx) === activeTick ? JSBI.BigInt(ticks[pivot].liquidityNet) : JSBI.BigInt(0), @@ -146,5 +137,5 @@ export function usePoolActiveLiquidity( activeTick, data: ticksProcessed, } - }, [currencyA, currencyB, activeTick, pool, ticks, isLoading, isUninitialized, isError, error]) + }, [activeTick, currencyA, currencyB, error, isError, isLoading, isUninitialized, pool?.liquidity, poolState, ticks]) } diff --git a/src/hooks/usePools.ts b/src/hooks/usePools.ts index f5d9a029a1..cb42287a8d 100644 --- a/src/hooks/usePools.ts +++ b/src/hooks/usePools.ts @@ -39,13 +39,16 @@ export function usePools( return transformed.map(value => { if (!proAmmCoreFactoryAddress || !value || value[0].equals(value[1])) return undefined - return computePoolAddress({ + + const param = { factoryAddress: proAmmCoreFactoryAddress, tokenA: value[0], tokenB: value[1], fee: value[2], initCodeHashManualOverride: (networkInfo as EVMNetworkInfo).elastic.initCodeHash, - }) + } + + return computePoolAddress(param) }) }, [transformed, isEVM, networkInfo]) diff --git a/src/hooks/usePoolv2.ts b/src/hooks/usePoolv2.ts new file mode 100644 index 0000000000..4ba3949706 --- /dev/null +++ b/src/hooks/usePoolv2.ts @@ -0,0 +1,190 @@ +import { Interface } from '@ethersproject/abi' +import { ChainId, Currency, Token } from '@kyberswap/ks-sdk-core' +import { FeeAmount, Pool, computePoolAddress } from '@kyberswap/ks-sdk-elastic' +import { useCallback, useEffect, useMemo, useState } from 'react' + +import ProAmmPoolStateABI from 'constants/abis/v2/ProAmmPoolState.json' +import { NETWORKS_INFO, isEVM as isEVMNetwork } from 'constants/networks' +import { EVMNetworkInfo } from 'constants/networks/type' +import { useMulticallContract } from 'hooks/useContract' +import { PoolState } from 'hooks/usePools' + +const PoolStateInterface = new Interface(ProAmmPoolStateABI.abi) + +type CallParam = { + callData: string + target: string + fragment: string + key: string +} +const formatResult = (responseData: any, calls: CallParam[]): any => { + const response: any = responseData.returnData + ? responseData.returnData.map((item: [boolean, string]) => item[1]) + : responseData + + const resultList: { [key: string]: any } = {} + if (!response) { + return resultList + } + for (let i = 0, len = calls.length; i < len; i++) { + const item = calls[i] + if (!response[i]) continue + let value: any + try { + value = PoolStateInterface?.decodeFunctionResult(item.fragment, response[i]) + } catch (error) { + continue + } + const output = value || undefined + if (output) resultList[item.key] = output + } + + return resultList +} + +const defaultValue = { slot0State: undefined, liquidityState: undefined } + +const useGetPool = ( + chainId: ChainId, + poolAddress: string | undefined, + token0: Token | undefined, + token1: Token | undefined, + fee: FeeAmount | undefined, +): [PoolState, Pool | undefined] => { + const [isLoading, setLoading] = useState(false) + const [data, setData] = useState<{ slot0State: any; liquidityState: any }>(defaultValue) + const multicallContract = useMulticallContract(chainId) + + const getPool = useCallback(async () => { + if (!multicallContract || !poolAddress || !token0 || !token1 || !fee) { + setData(defaultValue) + setLoading(false) + return + } + + const callParams: CallParam[] = [ + { + callData: PoolStateInterface.encodeFunctionData('getPoolState'), + target: poolAddress, + fragment: 'getPoolState', + key: 'slot0State', + }, + { + callData: PoolStateInterface.encodeFunctionData('getLiquidityState'), + target: poolAddress, + fragment: 'getLiquidityState', + key: 'liquidityState', + }, + ] + + setLoading(true) + const returnData = await multicallContract.callStatic.tryBlockAndAggregate( + false, + callParams.map(({ callData, target }) => ({ callData, target })), + ) + + const { slot0State, liquidityState } = formatResult(returnData, callParams) + + setData({ slot0State, liquidityState }) + setLoading(false) + }, [token0, token1, fee, multicallContract, poolAddress]) + + useEffect(() => { + getPool() + }, [getPool]) + + return useMemo(() => { + if (!token0 || !token1 || !fee) { + return [PoolState.INVALID, undefined] + } + + if (isLoading) { + return [PoolState.LOADING, undefined] + } + + if (!data.slot0State || !data.liquidityState) { + return [PoolState.NOT_EXISTS, undefined] + } + + if (!data.slot0State.sqrtP || data.slot0State.sqrtP.eq(0)) { + return [PoolState.NOT_EXISTS, undefined] + } + + try { + const pool = new Pool( + token0, + token1, + fee, + data.slot0State.sqrtP, + data.liquidityState.baseL, + data.liquidityState.reinvestL, + data.slot0State.currentTick, + ) + return [PoolState.EXISTS, pool] + } catch (error) { + console.error('Error when constructing the pool', error) + return [PoolState.NOT_EXISTS, undefined] + } + }, [data.liquidityState, data.slot0State, fee, isLoading, token0, token1]) +} + +export function usePoolv2( + chainId: ChainId, + currencyA: Currency | undefined, + currencyB: Currency | undefined, + feeAmount: FeeAmount | undefined, + poolAddress?: string, +): { + poolState: PoolState + pool: Pool | undefined + computedPoolAddress: string | undefined +} { + const isEVM = isEVMNetwork(chainId) + const networkInfo = NETWORKS_INFO[chainId] + + const values: [Token, Token, FeeAmount] | undefined = useMemo(() => { + if (!currencyA || !currencyB || !feeAmount) { + return undefined + } + + const tokenA = currencyA?.wrapped + const tokenB = currencyB?.wrapped + if (!tokenA || !tokenB || tokenA.equals(tokenB)) { + return undefined + } + + const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA] + return [token0, token1, feeAmount] + }, [currencyA, currencyB, feeAmount]) + + const computedPoolAddress: string | undefined = useMemo(() => { + if (poolAddress) { + return poolAddress + } + + if (!isEVM || !values) { + return undefined + } + + const proAmmCoreFactoryAddress = (networkInfo as EVMNetworkInfo).elastic.coreFactory + const param = { + factoryAddress: proAmmCoreFactoryAddress, + tokenA: values[0], + tokenB: values[1], + fee: values[2], + initCodeHashManualOverride: (networkInfo as EVMNetworkInfo).elastic.initCodeHash, + } + + return computePoolAddress(param) + }, [isEVM, networkInfo, poolAddress, values]) + + const [poolState, pool] = useGetPool(chainId, computedPoolAddress, values?.[0], values?.[1], values?.[2]) + + return useMemo(() => { + return { + poolState, + pool, + computedPoolAddress, + } + }, [pool, computedPoolAddress, poolState]) +} diff --git a/src/hooks/usePositionsFees.ts b/src/hooks/usePositionsFees.ts new file mode 100644 index 0000000000..97e5b0a9c6 --- /dev/null +++ b/src/hooks/usePositionsFees.ts @@ -0,0 +1,76 @@ +import { ChainId } from '@kyberswap/ks-sdk-core' +import { Interface } from 'ethers/lib/utils' +import { useCallback, useEffect, useState } from 'react' + +import TickReaderABI from 'constants/abis/v2/ProAmmTickReader.json' +import { NETWORKS_INFO } from 'constants/networks' +import { EVMNetworkInfo } from 'constants/networks/type' +import { useActiveWeb3React } from 'hooks' +import { config } from 'hooks/useElasticLegacy' + +import { useMulticallContract } from './useContract' + +const tickReaderInterface = new Interface(TickReaderABI.abi) + +export function usePositionsFees( + positions: { poolAddress: string; id: string | number }[], + isLegacy: boolean, + customChainId?: ChainId, +): { + [tokenId: string]: [string, string] +} { + const { chainId: activeChainId } = useActiveWeb3React() + const multicallContract = useMulticallContract(customChainId) + const chainId = customChainId || activeChainId + + const [feeRewards, setFeeRewards] = useState<{ + [tokenId: string]: [string, string] + }>(() => positions.reduce((acc, item) => ({ ...acc, [item.id]: ['0', '0'] }), {})) + + let tickReaderAddress = (NETWORKS_INFO[chainId] as EVMNetworkInfo)?.elastic.tickReader + let nftManagerContract = (NETWORKS_INFO[chainId] as EVMNetworkInfo)?.elastic.nonfungiblePositionManager + + if (isLegacy) { + tickReaderAddress = config[chainId].tickReaderContract + nftManagerContract = config[chainId].positionManagerContract + } + + const getPositionFee = useCallback(async () => { + if (!multicallContract) return + const fragment = tickReaderInterface.getFunction('getTotalFeesOwedToPosition') + const callParams = positions.map(item => { + return { + target: tickReaderAddress, + callData: tickReaderInterface.encodeFunctionData(fragment, [nftManagerContract, item.poolAddress, item.id]), + } + }) + + const { returnData } = await multicallContract?.callStatic.tryBlockAndAggregate(false, callParams) + setFeeRewards( + returnData.reduce( + ( + acc: { [tokenId: string]: [string, string] }, + item: { success: boolean; returnData: string }, + index: number, + ) => { + if (item.success) { + const tmp = tickReaderInterface.decodeFunctionResult(fragment, item.returnData) + return { + ...acc, + [positions[index].id]: [tmp.token0Owed.toString(), tmp.token1Owed.toString()], + } + } + return { ...acc, [positions[index].id]: ['0', '0'] } + }, + {} as { [tokenId: string]: [string, string] }, + ), + ) + // eslint-disable-next-line + }, [multicallContract, positions.length, nftManagerContract, tickReaderAddress]) + + useEffect(() => { + getPositionFee() + }, [getPositionFee]) + + return feeRewards +} diff --git a/src/hooks/useShareImage.ts b/src/hooks/useShareImage.ts index 580afd6eb0..76ca1a3629 100644 --- a/src/hooks/useShareImage.ts +++ b/src/hooks/useShareImage.ts @@ -31,14 +31,13 @@ const useShareImage = () => { metaImageURL: imageUrl, }).unwrap() return resolve({ imageUrl, blob }) - } else { - const shareUrl = await createShareLink({ - metaImageURL: imageUrl, - redirectURL: window.location.href, - type, - }).unwrap() - return shareUrl ? resolve({ shareUrl, imageUrl, blob }) : reject() } + const shareUrl = await createShareLink({ + metaImageURL: imageUrl, + redirectURL: window.location.href, + type, + }).unwrap() + return shareUrl ? resolve({ shareUrl, imageUrl, blob }) : reject() }, 'image/png') }) }, diff --git a/src/hooks/useTokenBalance.ts b/src/hooks/useTokenBalance.ts index f0992ff3c7..cfc9e537d1 100644 --- a/src/hooks/useTokenBalance.ts +++ b/src/hooks/useTokenBalance.ts @@ -1,10 +1,10 @@ -import { WETH } from '@kyberswap/ks-sdk-core' +import { ChainId, WETH } from '@kyberswap/ks-sdk-core' import { BigNumber, Contract } from 'ethers' import { useCallback, useEffect, useState } from 'react' import ERC20_ABI from 'constants/abis/erc20.json' import { useActiveWeb3React } from 'hooks' -import { useContract } from 'hooks/useContract' +import { useContractForReading } from 'hooks/useContract' import useTransactionStatus from 'hooks/useTransactionStatus' import { useKyberSwapConfig } from 'state/application/hooks' import { isAddress } from 'utils' @@ -14,15 +14,16 @@ interface BalanceProps { decimals: number } -function useTokenBalance(tokenAddress: string) { +function useTokenBalance(tokenAddress: string, customChainId?: ChainId) { const [balance, setBalance] = useState({ value: BigNumber.from(0), decimals: 18 }) - const { account, chainId } = useActiveWeb3React() + const { account, chainId: activeChainId } = useActiveWeb3React() + const chainId = customChainId || activeChainId const { readProvider } = useKyberSwapConfig(chainId) //const currentBlockNumber = useBlockNumber() // allows balance to update given transaction updates const currentTransactionStatus = useTransactionStatus() const addressCheckSum = isAddress(chainId, tokenAddress) - const tokenContract = useContract(addressCheckSum ? addressCheckSum : undefined, ERC20_ABI, false) + const tokenContract = useContractForReading(addressCheckSum ? addressCheckSum : undefined, ERC20_ABI, chainId) const fetchBalance = useCallback(async () => { const getBalance = async (contract: Contract | null, owner: string | null | undefined): Promise => { diff --git a/src/pages/AddLiquidityV2/index.tsx b/src/pages/AddLiquidityV2/index.tsx index 551f224ecc..c477211a40 100644 --- a/src/pages/AddLiquidityV2/index.tsx +++ b/src/pages/AddLiquidityV2/index.tsx @@ -1357,9 +1357,6 @@ export default function AddLiquidity() { onFieldBInput('0') navigate(`/${networkInfo.route}${APP_PATHS.ELASTIC_CREATE_POOL}`) }} - onBack={() => { - navigate(`${APP_PATHS.POOLS}/${networkInfo.route}?tab=elastic`) - }} tutorialType={TutorialType.ELASTIC_ADD_LIQUIDITY} /> diff --git a/src/pages/App.tsx b/src/pages/App.tsx index d306a5e500..2aa6c33bf5 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -52,6 +52,7 @@ const SwapV3 = lazy(() => import('./SwapV3')) // const Bridge = lazy(() => import('./Bridge')) const Pools = lazy(() => import('./Pools')) const MyPools = lazy(() => import('./Pool')) +const MyEarnings = lazy(() => import('./MyEarnings')) const Farm = lazy(() => import('./Farm')) @@ -325,6 +326,8 @@ export default function App() { } /> + } /> + <> {/* Pools Routes */} } /> @@ -428,6 +431,7 @@ export default function App() { {showFooter &&