From 81accd1864729bdcef7e946b6e16434c18cb121b Mon Sep 17 00:00:00 2001 From: Charles Bachmeier Date: Mon, 2 Oct 2023 09:56:17 -0700 Subject: [PATCH] feat: [info] Add Liquidity and Swap buttons on PDP (#7382) * feat: setup initial pool details page and route * add pool data query and call on enw page * make query dynamic to url chainId * Get and display Header info * add token symbols * split header into its own file * add helper function to not default to eth chain * add helper function tests * add header component tests * add mocked test for PDP * use valid values * allow unsupported BE chains supported by thegraph * typecheck * remove useless row * no longer needed child * use first and last child * move mock consts to their own file * skele linear task * return null * descriptiive pool not found bool * modify correct logo container * update snapshots * instantiate all chain apollo clients * added snapshot test * merge main and update snapshots * Update src/pages/PoolDetails/PoolDetailsHeader.tsx Co-authored-by: Nate Wienert * type feeTier * setup init stats component * correctly query pool data for t24, t48, and tWeek timestamps * add comments * sanitize pool data and update tests * correct test data * add todo * lint * show correct data * remove logs * use formatter * showing colored bars * styled graph * get muted color * refactor: move getColor to src * refactor useColor to use getColor function * remove consts * refactor files * 1st class var support courtesy of carter * remove logging and adds comments * mobile styling * move Stats to its own file * add test cases * add test file * update padding * remove old test file * respond to feedback * right column wrapper * add non-functional pdp buttons * update tests * add button functionality * working tokenId for position * split buttons in their own file * add tests * reduce screenshots --------- Co-authored-by: Nate Wienert --- .../MiniPortfolio/Pools/index.test.tsx | 52 +---- .../PoolDetailsStatsButtons.test.tsx | 63 ++++++ .../PoolDetails/PoolDetailsStatsButtons.tsx | 89 ++++++++ .../PoolDetailsStatsButtons.test.tsx.snap | 141 +++++++++++++ .../__snapshots__/index.test.tsx.snap | 192 ++++++++++++++---- src/pages/PoolDetails/index.tsx | 2 + src/test-utils/pools/fixtures.ts | 88 ++++++-- 7 files changed, 520 insertions(+), 107 deletions(-) create mode 100644 src/pages/PoolDetails/PoolDetailsStatsButtons.test.tsx create mode 100644 src/pages/PoolDetails/PoolDetailsStatsButtons.tsx create mode 100644 src/pages/PoolDetails/__snapshots__/PoolDetailsStatsButtons.test.tsx.snap diff --git a/src/components/AccountDrawer/MiniPortfolio/Pools/index.test.tsx b/src/components/AccountDrawer/MiniPortfolio/Pools/index.test.tsx index bb8a288d6b..324295df0d 100644 --- a/src/components/AccountDrawer/MiniPortfolio/Pools/index.test.tsx +++ b/src/components/AccountDrawer/MiniPortfolio/Pools/index.test.tsx @@ -1,8 +1,5 @@ -import { BigNumber } from '@ethersproject/bignumber' -import { ChainId, WETH9 } from '@uniswap/sdk-core' -import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk' -import { USDC_MAINNET } from 'constants/tokens' import { mocked } from 'test-utils/mocked' +import { owner, useMultiChainPositionsReturnValue } from 'test-utils/pools/fixtures' import { render } from 'test-utils/render' import Pools from '.' @@ -12,53 +9,6 @@ jest.mock('./useMultiChainPositions') jest.spyOn(console, 'warn').mockImplementation() -const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c' - -const pool = new Pool( - USDC_MAINNET, - WETH9[ChainId.MAINNET], - FeeAmount.MEDIUM, - '1851127709498178402383049949138810', - '7076437181775065414', - 201189 -) - -const position = new Position({ - pool, - liquidity: 1341008833950736, - tickLower: 200040, - tickUpper: 202560, -}) -const details = { - nonce: BigNumber.from('0'), - tokenId: BigNumber.from('0'), - operator: '0x0', - token0: USDC_MAINNET.address, - token1: WETH9[ChainId.MAINNET].address, - fee: FeeAmount.MEDIUM, - tickLower: -100, - tickUpper: 100, - liquidity: BigNumber.from('9000'), - feeGrowthInside0LastX128: BigNumber.from('0'), - feeGrowthInside1LastX128: BigNumber.from('0'), - tokensOwed0: BigNumber.from('0'), - tokensOwed1: BigNumber.from('0'), -} -const useMultiChainPositionsReturnValue = { - positions: [ - { - owner, - chainId: ChainId.MAINNET, - position, - pool, - details, - inRange: true, - closed: false, - }, - ], - loading: false, -} - beforeEach(() => { mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue) }) diff --git a/src/pages/PoolDetails/PoolDetailsStatsButtons.test.tsx b/src/pages/PoolDetails/PoolDetailsStatsButtons.test.tsx new file mode 100644 index 0000000000..356a7e75d5 --- /dev/null +++ b/src/pages/PoolDetails/PoolDetailsStatsButtons.test.tsx @@ -0,0 +1,63 @@ +import userEvent from '@testing-library/user-event' +import useMultiChainPositions from 'components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions' +import { mocked } from 'test-utils/mocked' +import { useMultiChainPositionsReturnValue, validPoolToken0, validPoolToken1 } from 'test-utils/pools/fixtures' +import { act, render, screen } from 'test-utils/render' + +import { PoolDetailsStatsButtons } from './PoolDetailsStatsButtons' + +jest.mock('components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions') + +describe('PoolDetailsStatsButton', () => { + const mockProps = { + chainId: 1, + token0: validPoolToken0, + token1: validPoolToken1, + feeTier: 500, + } + + const mockPropsTokensReversed = { + ...mockProps, + token0: validPoolToken1, + token1: validPoolToken0, + } + + beforeEach(() => { + mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue) + }) + + it('renders both buttons correctly', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + + expect(screen.getByTestId('pool-details-add-liquidity-button')).toBeVisible() + expect(screen.getByTestId('pool-details-swap-button')).toBeVisible() + }) + + it('clicking swap goes to correct url', async () => { + render() + + await act(() => userEvent.click(screen.getByTestId('pool-details-swap-button'))) + expect(global.window.location.href).toContain( + '/swap?inputCurrency=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&outputCurrency=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' + ) + }) + + it('clicking swap goes to correct url with tokens reversed', async () => { + render() + + await act(() => userEvent.click(screen.getByTestId('pool-details-swap-button'))) + expect(global.window.location.href).toContain( + '/swap?inputCurrency=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2&outputCurrency=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' + ) + }) + + it('clicking add liquidity goes to correct url', async () => { + render() + + await act(() => userEvent.click(screen.getByTestId('pool-details-add-liquidity-button'))) + expect(global.window.location.href).toContain( + '/increase/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/500' + ) + }) +}) diff --git a/src/pages/PoolDetails/PoolDetailsStatsButtons.tsx b/src/pages/PoolDetails/PoolDetailsStatsButtons.tsx new file mode 100644 index 0000000000..497b202725 --- /dev/null +++ b/src/pages/PoolDetails/PoolDetailsStatsButtons.tsx @@ -0,0 +1,89 @@ +import { Trans } from '@lingui/macro' +import { useWeb3React } from '@web3-react/core' +import { PositionInfo } from 'components/AccountDrawer/MiniPortfolio/Pools/cache' +import useMultiChainPositions from 'components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions' +import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button' +import Row from 'components/Row' +import { Token } from 'graphql/thegraph/__generated__/types-and-hooks' +import { useCurrency } from 'hooks/Tokens' +import { useSwitchChain } from 'hooks/useSwitchChain' +import { useNavigate } from 'react-router-dom' +import styled from 'styled-components' +import { BREAKPOINTS } from 'theme' +import { currencyId } from 'utils/currencyId' + +const PoolDetailsStatsButtonsRow = styled(Row)` + gap: 12px; + + @media (max-width: ${BREAKPOINTS.lg - 1}px) { + display: none; + } +` + +const PoolButton = styled(ThemeButton)` + padding: 12px 16px 12px 12px; + border-radius: 900px; + width: 50%; +` + +interface PoolDetailsStatsButtonsProps { + chainId?: number + token0?: Token + token1?: Token + feeTier?: number +} + +function findMatchingPosition(positions: PositionInfo[], token0?: Token, token1?: Token, feeTier?: number) { + return positions?.find( + (position) => + (position?.details.token0.toLowerCase() === token0?.id || + position?.details.token0.toLowerCase() === token1?.id) && + (position?.details.token1.toLowerCase() === token0?.id || + position?.details.token1.toLowerCase() === token1?.id) && + position?.details.fee == feeTier && + !position.closed + ) +} + +export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier }: PoolDetailsStatsButtonsProps) { + const { chainId: walletChainId, connector, account } = useWeb3React() + const { positions: userOwnedPositions } = useMultiChainPositions(account ?? '', chainId ? [chainId] : undefined) + const position = userOwnedPositions && findMatchingPosition(userOwnedPositions, token0, token1, feeTier) + const tokenId = position?.details.tokenId + const switchChain = useSwitchChain() + const navigate = useNavigate() + const currency0 = useCurrency(token0?.id, chainId) + const currency1 = useCurrency(token1?.id, chainId) + const handleOnClick = async (toSwap: boolean) => { + if (currency0 && currency1) { + if (walletChainId !== chainId && chainId) await switchChain(connector, chainId) + navigate( + toSwap + ? `/swap?inputCurrency=${currencyId(currency0)}&outputCurrency=${currencyId(currency1)}` + : `/increase/${currencyId(currency0)}/${currencyId(currency1)}/${feeTier}${tokenId ? `/${tokenId}` : ''}` + ) + } + } + if (!currency0 || !currency1) return null + return ( + + handleOnClick(false)} + data-testid="pool-details-add-liquidity-button" + > + Add liquidity + + + handleOnClick(true)} + data-testid="pool-details-swap-button" + > + Swap + + + ) +} diff --git a/src/pages/PoolDetails/__snapshots__/PoolDetailsStatsButtons.test.tsx.snap b/src/pages/PoolDetails/__snapshots__/PoolDetailsStatsButtons.test.tsx.snap new file mode 100644 index 0000000000..2b8fcc5fb7 --- /dev/null +++ b/src/pages/PoolDetails/__snapshots__/PoolDetailsStatsButtons.test.tsx.snap @@ -0,0 +1,141 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` + + .c0 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c1 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c6 { + background-color: transparent; + bottom: 0; + border-radius: inherit; + height: 100%; + left: 0; + position: absolute; + right: 0; + top: 0; + -webkit-transition: 150ms ease background-color; + transition: 150ms ease background-color; + width: 100%; +} + +.c3 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: #FFEFFF; + border-radius: 16px; + border: 0; + color: #FC72FF; + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + font-size: 16px; + font-weight: 535; + gap: 12px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 20px; + padding: 10px 12px; + position: relative; + -webkit-transition: 150ms ease opacity; + transition: 150ms ease opacity; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c3:active .c5 { + background-color: #B8C0DC3d; +} + +.c3:focus .c5 { + background-color: #B8C0DC3d; +} + +.c3:hover .c5 { + background-color: #98A1C014; +} + +.c3:disabled { + cursor: default; + opacity: 0.6; +} + +.c3:disabled:active .c5, +.c3:disabled:focus .c5, +.c3:disabled:hover .c5 { + background-color: transparent; +} + +.c2 { + gap: 12px; +} + +.c4 { + padding: 12px 16px 12px 12px; + border-radius: 900px; + width: 50%; +} + +@media (max-width:1023px) { + .c2 { + display: none; + } +} + +
+ + +
+
+`; diff --git a/src/pages/PoolDetails/__snapshots__/index.test.tsx.snap b/src/pages/PoolDetails/__snapshots__/index.test.tsx.snap index 84c96e5367..fde90bd8fb 100644 --- a/src/pages/PoolDetails/__snapshots__/index.test.tsx.snap +++ b/src/pages/PoolDetails/__snapshots__/index.test.tsx.snap @@ -17,7 +17,7 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the width: max-content; } -.c26 { +.c31 { box-sizing: border-box; margin: 0; min-width: 0; @@ -82,7 +82,7 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the gap: 8px; } -.c27 { +.c32 { width: -webkit-max-content; width: -moz-max-content; width: max-content; @@ -124,6 +124,78 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the justify-content: flex-start; } +.c21 { + background-color: transparent; + bottom: 0; + border-radius: inherit; + height: 100%; + left: 0; + position: absolute; + right: 0; + top: 0; + -webkit-transition: 150ms ease background-color; + transition: 150ms ease background-color; + width: 100%; +} + +.c18 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: #FFEFFF; + border-radius: 16px; + border: 0; + color: #FC72FF; + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + font-size: 16px; + font-weight: 535; + gap: 12px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 20px; + padding: 10px 12px; + position: relative; + -webkit-transition: 150ms ease opacity; + transition: 150ms ease opacity; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c18:active .c20 { + background-color: #B8C0DC3d; +} + +.c18:focus .c20 { + background-color: #B8C0DC3d; +} + +.c18:hover .c20 { + background-color: #98A1C014; +} + +.c18:disabled { + cursor: default; + opacity: 0.6; +} + +.c18:disabled:active .c20, +.c18:disabled:focus .c20, +.c18:disabled:hover .c20 { + background-color: transparent; +} + .c4 { gap: 36px; } @@ -207,17 +279,17 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the border-radius: 50%; } -.c28 { +.c33 { color: #FF5F52; } -.c18 { +.c23 { font-weight: 485; font-size: 24px; line-height: 36px; } -.c17 { +.c22 { gap: 24px; padding: 20px; border-radius: 20px; @@ -225,7 +297,7 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the width: 100%; } -.c19 { +.c24 { gap: 8px; -webkit-flex: 1; -ms-flex: 1; @@ -233,14 +305,14 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the min-width: 180px; } -.c20 { +.c25 { -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; } -.c21 { +.c26 { font-weight: 485; font-size: 18px; line-height: 24px; @@ -249,7 +321,7 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the width: max-content; } -.c22 { +.c27 { height: 8px; width: 40.698463777008904%; background: #2172E5; @@ -258,7 +330,7 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the border-right: 1px solid #F9F9F9; } -.c23 { +.c28 { height: 8px; width: 59.3015362229911%; background: #2172E5; @@ -267,7 +339,7 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the border-left: 1px solid #F9F9F9; } -.c24 { +.c29 { gap: 4px; width: 100%; -webkit-align-items: flex-end; @@ -276,13 +348,23 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the align-items: flex-end; } -.c25 { +.c30 { color: #222222; font-size: 36px; font-weight: 485; line-height: 44px; } +.c17 { + gap: 12px; +} + +.c19 { + padding: 12px 16px 12px 12px; + border-radius: 900px; + width: 50%; +} + .c2 { padding: 48px; width: 100%; @@ -300,13 +382,13 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the } @media (max-width:1023px) { - .c18 { + .c23 { width: 100%; } } @media (max-width:1023px) { - .c17 { + .c22 { -webkit-flex-direction: row; -ms-flex-direction: row; flex-direction: row; @@ -323,13 +405,13 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the } @media (max-width:640px) { - .c19 { + .c24 { min-width: 150px; } } @media (max-width:1023px) { - .c20 { + .c25 { -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; @@ -337,7 +419,7 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the } @media (max-width:1023px) { - .c21 { + .c26 { font-size: 20px; line-height: 28px; width: 100%; @@ -345,7 +427,7 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the } @media (max-width:1023px) { - .c24 { + .c29 { -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; @@ -358,12 +440,18 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the } @media (max-width:1023px) { - .c25 { + .c30 { font-size: 20px; line-height: 28px; } } +@media (max-width:1023px) { + .c17 { + display: none; + } +} + @media (max-width:1023px) { .c2 { -webkit-flex-direction: column; @@ -485,15 +573,39 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the class="c3 c16" >
+ + +
+
Stats
90.93M USDC
82,526.49 WETH
@@ -519,15 +631,15 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the data-testid="pool-balance-chart" >
$223.2M
$233.4M
$116.7K
diff --git a/src/pages/PoolDetails/index.tsx b/src/pages/PoolDetails/index.tsx index 4c4cca6d02..427a63e1a2 100644 --- a/src/pages/PoolDetails/index.tsx +++ b/src/pages/PoolDetails/index.tsx @@ -11,6 +11,7 @@ import { isAddress } from 'utils' import { PoolDetailsHeader } from './PoolDetailsHeader' import { PoolDetailsStats } from './PoolDetailsStats' +import { PoolDetailsStatsButtons } from './PoolDetailsStatsButtons' const PageWrapper = styled(Row)` padding: 48px; @@ -67,6 +68,7 @@ export default function PoolDetailsPage() { toggleReversed={toggleReversed} /> + {poolData && } diff --git a/src/test-utils/pools/fixtures.ts b/src/test-utils/pools/fixtures.ts index d4815873dc..d070cd15b4 100644 --- a/src/test-utils/pools/fixtures.ts +++ b/src/test-utils/pools/fixtures.ts @@ -1,6 +1,76 @@ +import { BigNumber } from '@ethersproject/bignumber' +import { ChainId, WETH9 } from '@uniswap/sdk-core' +import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk' +import { USDC_MAINNET } from 'constants/tokens' import { Token } from 'graphql/thegraph/__generated__/types-and-hooks' export const validParams = { poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640', chainName: 'ethereum' } + +export const validPoolToken0 = { + id: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + name: 'USD Coin', + decimals: '6', + derivedETH: '0.0006240873011635544626425964678706127', + __typename: 'Token', +} as Token + +export const validPoolToken1 = { + id: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + symbol: 'WETH', + name: 'Wrapped Ether', + decimals: '18', + derivedETH: '1', + __typename: 'Token', +} as Token + +export const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c' + +const pool = new Pool( + USDC_MAINNET, + WETH9[ChainId.MAINNET], + FeeAmount.MEDIUM, + '1851127709498178402383049949138810', + '7076437181775065414', + 201189 +) + +const position = new Position({ + pool, + liquidity: 1341008833950736, + tickLower: 200040, + tickUpper: 202560, +}) +const details = { + nonce: BigNumber.from('0'), + tokenId: BigNumber.from('0'), + operator: '0x0', + token0: USDC_MAINNET.address, + token1: WETH9[ChainId.MAINNET].address, + fee: FeeAmount.MEDIUM, + tickLower: -100, + tickUpper: 100, + liquidity: BigNumber.from('9000'), + feeGrowthInside0LastX128: BigNumber.from('0'), + feeGrowthInside1LastX128: BigNumber.from('0'), + tokensOwed0: BigNumber.from('0'), + tokensOwed1: BigNumber.from('0'), +} +export const useMultiChainPositionsReturnValue = { + positions: [ + { + owner, + chainId: ChainId.MAINNET, + position, + pool, + details, + inRange: true, + closed: false, + }, + ], + loading: false, +} + export const validPoolDataResponse = { data: { id: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640', @@ -8,22 +78,8 @@ export const validPoolDataResponse = { liquidity: parseFloat('26414803986874770777'), sqrtPrice: parseFloat('1977320351696380862605029898750440'), tick: 202508, - token0: { - id: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - symbol: 'USDC', - name: 'USD Coin', - decimals: '6', - derivedETH: '0.0006240873011635544626425964678706127', - __typename: 'Token', - } as Token, - token1: { - id: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', - symbol: 'WETH', - name: 'Wrapped Ether', - decimals: '18', - derivedETH: '1', - __typename: 'Token', - } as Token, + token0: validPoolToken0, + token1: validPoolToken1, token0Price: 1605.481, token1Price: 0.000622, volumeUSD: 233379442.64648438,