diff --git a/cypress/fixtures/feeTierDistribution.json b/cypress/fixtures/feeTierDistribution.json index 20d8f28a770..4a01d75b84c 100644 --- a/cypress/fixtures/feeTierDistribution.json +++ b/cypress/fixtures/feeTierDistribution.json @@ -5,6 +5,11 @@ } }, "asToken0": [ + { + "feeTier": "100", + "totalValueLockedToken0": "0", + "totalValueLockedToken1": "3" + }, { "feeTier": "500", "totalValueLockedToken0": "0", @@ -13,7 +18,7 @@ { "feeTier": "3000", "totalValueLockedToken0": "0", - "totalValueLockedToken1": "7" + "totalValueLockedToken1": "4" }, { "feeTier": "10000", diff --git a/cypress/integration/add-liquidity.test.ts b/cypress/integration/add-liquidity.test.ts index b4c18d8485c..ab2eb4b3ecd 100644 --- a/cypress/integration/add-liquidity.test.ts +++ b/cypress/integration/add-liquidity.test.ts @@ -58,7 +58,7 @@ describe('Add Liquidity', () => { cy.wait('@feeTierDistributionQuery') cy.get('#add-liquidity-selected-fee .selected-fee-label').should('contain.text', '0.3% fee tier') - cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '70%') + cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '40%') }) }) }) diff --git a/package.json b/package.json index ff039409137..13c00b1358b 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@uniswap/v2-sdk": "^3.0.0-alpha.2", "@uniswap/v3-core": "1.0.0", "@uniswap/v3-periphery": "^1.1.1", - "@uniswap/v3-sdk": "3.6.3", + "@uniswap/v3-sdk": "^3.7.1", "@web3-react/core": "^6.0.9", "@web3-react/fortmatic-connector": "^6.0.9", "@web3-react/injected-connector": "^6.0.7", diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 9edfd32180a..c35e5161e0f 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -315,8 +315,8 @@ const ActiveOutlined = styled(ButtonOutlined)` ` const Circle = styled.div` - height: 20px; - width: 20px; + height: 17px; + width: 17px; border-radius: 50%; background-color: ${({ theme }) => theme.primary1}; display: flex; @@ -325,11 +325,11 @@ const Circle = styled.div` ` const CheckboxWrapper = styled.div` - width: 30px; + width: 20px; padding: 0 10px; position: absolute; - top: 10px; - right: 10px; + top: 11px; + right: 15px; ` const ResponsiveCheck = styled(Check)` diff --git a/src/components/FeeSelector/FeeOption.tsx b/src/components/FeeSelector/FeeOption.tsx new file mode 100644 index 00000000000..83f504844b1 --- /dev/null +++ b/src/components/FeeSelector/FeeOption.tsx @@ -0,0 +1,51 @@ +import { Trans } from '@lingui/macro' +import { FeeAmount } from '@uniswap/v3-sdk' +import { ButtonRadioChecked } from 'components/Button' +import { AutoColumn } from 'components/Column' +import { useFeeTierDistribution } from 'hooks/useFeeTierDistribution' +import { PoolState } from 'hooks/usePools' +import React from 'react' +import styled from 'styled-components/macro' +import { TYPE } from 'theme' + +import { FeeTierPercentageBadge } from './FeeTierPercentageBadge' +import { FEE_AMOUNT_DETAIL } from './shared' + +const ResponsiveText = styled(TYPE.label)` + line-height: 16px; + font-size: 14px; + + ${({ theme }) => theme.mediaWidth.upToSmall` + font-size: 12px; + line-height: 12px; + `}; +` + +interface FeeOptionProps { + feeAmount: FeeAmount + active: boolean + distributions: ReturnType['distributions'] + poolState: PoolState + onClick: () => void +} + +export function FeeOption({ feeAmount, active, poolState, distributions, onClick }: FeeOptionProps) { + return ( + + + + + {FEE_AMOUNT_DETAIL[feeAmount].label}% + + + {FEE_AMOUNT_DETAIL[feeAmount].description} + + + + {distributions && ( + + )} + + + ) +} diff --git a/src/components/FeeSelector/FeeTierPercentageBadge.tsx b/src/components/FeeSelector/FeeTierPercentageBadge.tsx new file mode 100644 index 00000000000..cc87279bc00 --- /dev/null +++ b/src/components/FeeSelector/FeeTierPercentageBadge.tsx @@ -0,0 +1,31 @@ +import { Trans } from '@lingui/macro' +import { FeeAmount } from '@uniswap/v3-sdk' +import Badge from 'components/Badge' +import { useFeeTierDistribution } from 'hooks/useFeeTierDistribution' +import { PoolState } from 'hooks/usePools' +import React from 'react' +import { TYPE } from 'theme' + +export function FeeTierPercentageBadge({ + feeAmount, + distributions, + poolState, +}: { + feeAmount: FeeAmount + distributions: ReturnType['distributions'] + poolState: PoolState +}) { + return ( + + + {!distributions || poolState === PoolState.NOT_EXISTS || poolState === PoolState.INVALID ? ( + Not created + ) : distributions[feeAmount] !== undefined ? ( + {distributions[feeAmount]?.toFixed(0)}% select + ) : ( + No data + )} + + + ) +} diff --git a/src/components/FeeSelector/index.tsx b/src/components/FeeSelector/index.tsx index 545a9ceba79..0cfa5cba288 100644 --- a/src/components/FeeSelector/index.tsx +++ b/src/components/FeeSelector/index.tsx @@ -1,14 +1,14 @@ import { Trans } from '@lingui/macro' import { Currency } from '@uniswap/sdk-core' import { FeeAmount } from '@uniswap/v3-sdk' -import Badge from 'components/Badge' -import { ButtonGray, ButtonRadioChecked } from 'components/Button' +import { ButtonGray } from 'components/Button' import Card from 'components/Card' import { AutoColumn } from 'components/Column' import { RowBetween } from 'components/Row' import { useFeeTierDistribution } from 'hooks/useFeeTierDistribution' import { PoolState, usePools } from 'hooks/usePools' import usePrevious from 'hooks/usePrevious' +import { useActiveWeb3React } from 'hooks/web3' import { DynamicSection } from 'pages/AddLiquidity/styled' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import ReactGA from 'react-ga' @@ -16,6 +16,10 @@ import { Box } from 'rebass' import styled, { keyframes } from 'styled-components/macro' import { TYPE } from 'theme' +import { FeeOption } from './FeeOption' +import { FeeTierPercentageBadge } from './FeeTierPercentageBadge' +import { FEE_AMOUNT_DETAIL } from './shared' + const pulse = (color: string) => keyframes` 0% { box-shadow: 0 0 0 0 ${color}; @@ -29,60 +33,18 @@ const pulse = (color: string) => keyframes` box-shadow: 0 0 0 0 ${color}; } ` - -const ResponsiveText = styled(TYPE.label)` - line-height: 16px; - - ${({ theme }) => theme.mediaWidth.upToSmall` - font-size: 12px; - line-height: 12px; - `}; -` - const FocusedOutlineCard = styled(Card)<{ pulsing: boolean }>` border: 1px solid ${({ theme }) => theme.bg2}; animation: ${({ pulsing, theme }) => pulsing && pulse(theme.primary1)} 0.6s linear; align-self: center; ` -const FeeAmountLabel = { - [FeeAmount.LOW]: { - label: '0.05', - description: Best for stable pairs., - }, - [FeeAmount.MEDIUM]: { - label: '0.3', - description: Best for most pairs., - }, - [FeeAmount.HIGH]: { - label: '1', - description: Best for exotic pairs., - }, -} - -function FeeTierPercentageBadge({ - feeAmount, - distributions, - poolState, -}: { - feeAmount: FeeAmount - distributions: ReturnType['distributions'] - poolState: PoolState -}) { - return ( - - - {!distributions || poolState === PoolState.NOT_EXISTS || poolState === PoolState.INVALID ? ( - Not created - ) : distributions[feeAmount] !== undefined ? ( - {distributions[feeAmount]?.toFixed(0)}% select - ) : ( - No data - )} - - - ) -} +const Select = styled.div` + align-items: flex-start; + display: grid; + grid-auto-flow: column; + grid-gap: 8px; +` export default function FeeSelector({ disabled = false, @@ -97,16 +59,19 @@ export default function FeeSelector({ currencyA?: Currency | undefined currencyB?: Currency | undefined }) { + const { chainId } = useActiveWeb3React() + const { isLoading, isError, largestUsageFeeTier, distributions } = useFeeTierDistribution(currencyA, currencyB) // get pool data on-chain for latest states const pools = usePools([ + [currencyA, currencyB, FeeAmount.LOWEST], [currencyA, currencyB, FeeAmount.LOW], [currencyA, currencyB, FeeAmount.MEDIUM], [currencyA, currencyB, FeeAmount.HIGH], ]) - const poolsByFeeTier = useMemo( + const poolsByFeeTier: Record = useMemo( () => pools.reduce( (acc, [curPoolState, curPool]) => { @@ -118,6 +83,7 @@ export default function FeeSelector({ }, { // default all states to NOT_EXISTS + [FeeAmount.LOWEST]: PoolState.NOT_EXISTS, [FeeAmount.LOW]: PoolState.NOT_EXISTS, [FeeAmount.MEDIUM]: PoolState.NOT_EXISTS, [FeeAmount.HIGH]: PoolState.NOT_EXISTS, @@ -134,7 +100,7 @@ export default function FeeSelector({ const recommended = useRef(false) const handleFeePoolSelectWithEvent = useCallback( - (fee) => { + (fee: FeeAmount) => { ReactGA.event({ category: 'FeePoolSelect', action: 'Manual', @@ -193,7 +159,7 @@ export default function FeeSelector({ ) : ( <> - {FeeAmountLabel[feeAmount].label}% fee tier + {FEE_AMOUNT_DETAIL[feeAmount].label}% fee tier {distributions && ( @@ -214,81 +180,25 @@ export default function FeeSelector({ - {showOptions && ( - - handleFeePoolSelectWithEvent(FeeAmount.LOW)} - > - - - - 0.05% fee - - - Best for stable pairs. - - - - {distributions && ( - + {[FeeAmount.LOWEST, FeeAmount.LOW, FeeAmount.MEDIUM, FeeAmount.HIGH].map((_feeAmount, i) => { + const { supportedChains } = FEE_AMOUNT_DETAIL[_feeAmount] + if (supportedChains.includes(chainId)) { + return ( + handleFeePoolSelectWithEvent(_feeAmount)} distributions={distributions} - feeAmount={FeeAmount.LOW} - poolState={poolsByFeeTier[FeeAmount.LOW]} + poolState={poolsByFeeTier[_feeAmount]} + key={i} /> - )} - - - handleFeePoolSelectWithEvent(FeeAmount.MEDIUM)} - > - - - - 0.3% fee - - - Best for most pairs. - - - - {distributions && ( - - )} - - - handleFeePoolSelectWithEvent(FeeAmount.HIGH)} - > - - - - 1% fee - - - Best for exotic pairs. - - - - {distributions && ( - - )} - - - + ) + } + return null + })} + )} diff --git a/src/components/FeeSelector/shared.tsx b/src/components/FeeSelector/shared.tsx new file mode 100644 index 00000000000..4b93e68d9f4 --- /dev/null +++ b/src/components/FeeSelector/shared.tsx @@ -0,0 +1,30 @@ +import { Trans } from '@lingui/macro' +import { FeeAmount } from '@uniswap/v3-sdk' +import { ALL_SUPPORTED_CHAIN_IDS, SupportedChainId } from 'constants/chains' +import { ReactNode } from 'react' + +export const FEE_AMOUNT_DETAIL: Record< + FeeAmount, + { label: string; description: ReactNode; supportedChains: SupportedChainId[] } +> = { + [FeeAmount.LOWEST]: { + label: '0.01', + description: Best for very stable pairs., + supportedChains: [SupportedChainId.MAINNET], + }, + [FeeAmount.LOW]: { + label: '0.05', + description: Best for stable pairs., + supportedChains: ALL_SUPPORTED_CHAIN_IDS, + }, + [FeeAmount.MEDIUM]: { + label: '0.3', + description: Best for most pairs., + supportedChains: ALL_SUPPORTED_CHAIN_IDS, + }, + [FeeAmount.HIGH]: { + label: '1', + description: Best for exotic pairs., + supportedChains: ALL_SUPPORTED_CHAIN_IDS, + }, +} diff --git a/src/components/LiquidityChartRangeInput/index.tsx b/src/components/LiquidityChartRangeInput/index.tsx index f9133d4aa57..b85fb19585d 100644 --- a/src/components/LiquidityChartRangeInput/index.tsx +++ b/src/components/LiquidityChartRangeInput/index.tsx @@ -20,6 +20,12 @@ import { useDensityChartData } from './hooks' import { ZoomLevels } from './types' const ZOOM_LEVELS: Record = { + [FeeAmount.LOWEST]: { + initialMin: 0.999, + initialMax: 1.001, + min: 0.00001, + max: 1.5, + }, [FeeAmount.LOW]: { initialMin: 0.999, initialMax: 1.001, diff --git a/src/components/RoutingDiagram/RoutingDiagram.test.tsx b/src/components/RoutingDiagram/RoutingDiagram.test.tsx index 178def7e383..ba1a6b43896 100644 --- a/src/components/RoutingDiagram/RoutingDiagram.test.tsx +++ b/src/components/RoutingDiagram/RoutingDiagram.test.tsx @@ -10,7 +10,7 @@ const percent = (strings: TemplateStringsArray) => new Percent(parseInt(strings[ const singleRoute: RoutingDiagramEntry = { percent: percent`100`, path: [[USDC, DAI, FeeAmount.LOW]] } const multiRoute: RoutingDiagramEntry[] = [ - { percent: percent`75`, path: [[USDC, DAI, FeeAmount.LOW]] }, + { percent: percent`75`, path: [[USDC, DAI, FeeAmount.LOWEST]] }, { percent: percent`25`, path: [ diff --git a/src/components/RoutingDiagram/__snapshots__/RoutingDiagram.test.tsx.snap b/src/components/RoutingDiagram/__snapshots__/RoutingDiagram.test.tsx.snap index 5b5ac9b6f12..618620d93fd 100644 --- a/src/components/RoutingDiagram/__snapshots__/RoutingDiagram.test.tsx.snap +++ b/src/components/RoutingDiagram/__snapshots__/RoutingDiagram.test.tsx.snap @@ -47,7 +47,7 @@ exports[`renders multi route 1`] = `
- 0.05% + 0.01%
diff --git a/src/hooks/useFeeTierDistribution.ts b/src/hooks/useFeeTierDistribution.ts index 14209a282a7..be7f4677cf6 100644 --- a/src/hooks/useFeeTierDistribution.ts +++ b/src/hooks/useFeeTierDistribution.ts @@ -19,11 +19,7 @@ interface FeeTierDistribution { largestUsageFeeTier?: FeeAmount | undefined // distributions as percentages of overall liquidity - distributions?: { - [FeeAmount.LOW]: number | undefined - [FeeAmount.MEDIUM]: number | undefined - [FeeAmount.HIGH]: number | undefined - } + distributions?: Record } export function useFeeTierDistribution( @@ -36,6 +32,7 @@ export function useFeeTierDistribution( ) // fetch all pool states to determine pool state + const [poolStateVeryLow] = usePool(currencyA, currencyB, FeeAmount.LOWEST) const [poolStateLow] = usePool(currencyA, currencyB, FeeAmount.LOW) const [poolStateMedium] = usePool(currencyA, currencyB, FeeAmount.MEDIUM) const [poolStateHigh] = usePool(currencyA, currencyB, FeeAmount.HIGH) @@ -58,10 +55,13 @@ export function useFeeTierDistribution( !isLoading && !isError && distributions && + poolStateVeryLow !== PoolState.LOADING && poolStateLow !== PoolState.LOADING && poolStateMedium !== PoolState.LOADING && poolStateHigh !== PoolState.LOADING ? { + [FeeAmount.LOWEST]: + poolStateVeryLow === PoolState.EXISTS ? (distributions[FeeAmount.LOWEST] ?? 0) * 100 : undefined, [FeeAmount.LOW]: poolStateLow === PoolState.EXISTS ? (distributions[FeeAmount.LOW] ?? 0) * 100 : undefined, [FeeAmount.MEDIUM]: poolStateMedium === PoolState.EXISTS ? (distributions[FeeAmount.MEDIUM] ?? 0) * 100 : undefined, @@ -76,7 +76,17 @@ export function useFeeTierDistribution( distributions: percentages, largestUsageFeeTier: largestUsageFeeTier === -1 ? undefined : largestUsageFeeTier, } - }, [isLoading, isFetching, isUninitialized, isError, distributions, poolStateLow, poolStateMedium, poolStateHigh]) + }, [ + isLoading, + isFetching, + isUninitialized, + isError, + distributions, + poolStateVeryLow, + poolStateLow, + poolStateMedium, + poolStateHigh, + ]) } function usePoolTVL(token0: Token | undefined, token1: Token | undefined) { @@ -124,10 +134,11 @@ function usePoolTVL(token0: Token | undefined, token1: Token | undefined) { return acc }, { + [FeeAmount.LOWEST]: [undefined, undefined], [FeeAmount.LOW]: [undefined, undefined], [FeeAmount.MEDIUM]: [undefined, undefined], [FeeAmount.HIGH]: [undefined, undefined], - } + } as Record ) // sum total tvl for token0 and token1 @@ -144,7 +155,13 @@ function usePoolTVL(token0: Token | undefined, token1: Token | undefined) { const mean = (tvl0: number | undefined, sumTvl0: number, tvl1: number | undefined, sumTvl1: number) => tvl0 === undefined && tvl1 === undefined ? undefined : ((tvl0 ?? 0) + (tvl1 ?? 0)) / (sumTvl0 + sumTvl1) || 0 - const distributions = { + const distributions: Record = { + [FeeAmount.LOWEST]: mean( + tvlByFeeTier[FeeAmount.LOWEST][0], + sumToken0Tvl, + tvlByFeeTier[FeeAmount.LOWEST][1], + sumToken1Tvl + ), [FeeAmount.LOW]: mean(tvlByFeeTier[FeeAmount.LOW][0], sumToken0Tvl, tvlByFeeTier[FeeAmount.LOW][1], sumToken1Tvl), [FeeAmount.MEDIUM]: mean( tvlByFeeTier[FeeAmount.MEDIUM][0], diff --git a/src/hooks/useV3SwapPools.ts b/src/hooks/useV3SwapPools.ts index ac901edd66e..05ebea4f43e 100644 --- a/src/hooks/useV3SwapPools.ts +++ b/src/hooks/useV3SwapPools.ts @@ -1,9 +1,11 @@ import { Currency, Token } from '@uniswap/sdk-core' import { FeeAmount, Pool } from '@uniswap/v3-sdk' +import { SupportedChainId } from 'constants/chains' import { useMemo } from 'react' import { useAllCurrencyCombinations } from './useAllCurrencyCombinations' import { PoolState, usePools } from './usePools' +import { useActiveWeb3React } from './web3' /** * Returns all the existing pools that should be considered for swapping between an input currency and an output currency @@ -17,18 +19,27 @@ export function useV3SwapPools( pools: Pool[] loading: boolean } { + const { chainId } = useActiveWeb3React() + const allCurrencyCombinations = useAllCurrencyCombinations(currencyIn, currencyOut) const allCurrencyCombinationsWithAllFees: [Token, Token, FeeAmount][] = useMemo( () => allCurrencyCombinations.reduce<[Token, Token, FeeAmount][]>((list, [tokenA, tokenB]) => { - return list.concat([ - [tokenA, tokenB, FeeAmount.LOW], - [tokenA, tokenB, FeeAmount.MEDIUM], - [tokenA, tokenB, FeeAmount.HIGH], - ]) + return chainId === SupportedChainId.MAINNET + ? list.concat([ + [tokenA, tokenB, FeeAmount.LOW], + [tokenA, tokenB, FeeAmount.MEDIUM], + [tokenA, tokenB, FeeAmount.HIGH], + ]) + : list.concat([ + [tokenA, tokenB, FeeAmount.LOWEST], + [tokenA, tokenB, FeeAmount.LOW], + [tokenA, tokenB, FeeAmount.MEDIUM], + [tokenA, tokenB, FeeAmount.HIGH], + ]) }, []), - [allCurrencyCombinations] + [allCurrencyCombinations, chainId] ) const pools = usePools(allCurrencyCombinationsWithAllFees) diff --git a/yarn.lock b/yarn.lock index 2d7c5b3670d..c604b0a0448 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4666,10 +4666,10 @@ base64-sol "1.0.1" hardhat-watcher "^2.1.1" -"@uniswap/v3-sdk@3.6.3": - version "3.6.3" - resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-3.6.3.tgz#f2fca86cfde1450976581028195fc0ee4b11036d" - integrity sha512-nepNTZMpM1uwLJAlQaUUEDMFrSujS1sOqyVD739zoPWsVHAUPvqVAKQN0GlgZcLXOqeCMKjO3lteaNHEXef1XA== +"@uniswap/v3-sdk@^3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-3.7.1.tgz#8a3740ff6302d8069e7ce4a38b7588721398048b" + integrity sha512-/0FBsrRijfAEOVO0ejCQX36MwaKzjKCaInUA1dNqFyDNZ5dthvv6jUhMADYuNXZnhN6NcSdIj6xhlc/cpgPm9Q== dependencies: "@ethersproject/abi" "^5.0.12" "@ethersproject/solidity" "^5.0.9"