diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index af2dc25336..1521b5bd5e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -39,14 +39,14 @@ jobs: node-version: '12' - name: Install dependencies - run: yarn install --ignore-scripts --frozen-lockfile + run: yarn install --frozen-lockfile - name: Build the IPFS bundle run: yarn build - name: Pin to IPFS id: upload - uses: anantaramdas/ipfs-pinata-deploy-action@v1.5.2 + uses: anantaramdas/ipfs-pinata-deploy-action@39bbda1ce1fe24c69c6f57861b8038278d53688d with: pin-name: Uniswap ${{ needs.bump_version.outputs.new_tag }} path: './build' diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index f800140f72..8a06701905 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -26,7 +26,9 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - - run: yarn install + - run: yarn install --frozen-lockfile + - run: yarn cypress install + - run: yarn build - run: yarn integration-test unit-tests: @@ -48,7 +50,7 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - - run: yarn install --ignore-scripts --frozen-lockfile + - run: yarn install --frozen-lockfile - run: yarn test lint: @@ -70,6 +72,6 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - - run: yarn install --ignore-scripts --frozen-lockfile + - run: yarn install --frozen-lockfile - run: yarn lint diff --git a/cypress/integration/add-liquidity.test.ts b/cypress/integration/add-liquidity.test.ts index 0773be0e8f..f08f02f553 100644 --- a/cypress/integration/add-liquidity.test.ts +++ b/cypress/integration/add-liquidity.test.ts @@ -16,4 +16,29 @@ describe('Add Liquidity', () => { cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL') cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR') }) + + it('single token can be selected', () => { + cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d') + cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL') + cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') + cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR') + }) + + it('redirects /add/token-token to add/token/token', () => { + cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') + cy.url().should( + 'contain', + '/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85' + ) + }) + + it('redirects /add/WETH-token to /add/ETH/token', () => { + cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') + cy.url().should('contain', '/add/ETH/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') + }) + + it('redirects /add/token-WETH to /add/token/ETH', () => { + cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab') + cy.url().should('contain', '/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/ETH') + }) }) diff --git a/cypress/integration/landing.test.ts b/cypress/integration/landing.test.ts index f729536777..a8e5db2260 100644 --- a/cypress/integration/landing.test.ts +++ b/cypress/integration/landing.test.ts @@ -10,11 +10,6 @@ describe('Landing Page', () => { cy.url().should('include', '/swap') }) - it('allows navigation to send', () => { - cy.get('#send-nav-link').click() - cy.url().should('include', '/send') - }) - it('allows navigation to pool', () => { cy.get('#pool-nav-link').click() cy.url().should('include', '/pool') diff --git a/cypress/integration/pool.test.ts b/cypress/integration/pool.test.ts index bf7006c3fb..5969abce71 100644 --- a/cypress/integration/pool.test.ts +++ b/cypress/integration/pool.test.ts @@ -1,13 +1,12 @@ describe('Pool', () => { beforeEach(() => cy.visit('/pool')) - it('can search for a pool', () => { + it('add liquidity links to /add/ETH', () => { cy.get('#join-pool-button').click() - cy.get('#token-search-input').type('DAI', { delay: 200 }) + cy.url().should('contain', '/add/ETH') }) - it('can import a pool', () => { - cy.get('#join-pool-button').click() - cy.get('#import-pool-link').click({ force: true }) // blocked by the grid element in the search box - cy.url().should('include', '/find') + it('import pool links to /import', () => { + cy.get('#import-pool-link').click() + cy.url().should('contain', '/find') }) }) diff --git a/cypress/integration/send.test.ts b/cypress/integration/send.test.ts index 5eb639a76b..35b8dee108 100644 --- a/cypress/integration/send.test.ts +++ b/cypress/integration/send.test.ts @@ -1,7 +1,11 @@ describe('Send', () => { - beforeEach(() => cy.visit('/send')) - it('should redirect', () => { + cy.visit('/send') cy.url().should('include', '/swap') }) + + it('should redirect with url params', () => { + cy.visit('/send?outputCurrency=ETH&recipient=bob.argent.xyz') + cy.url().should('contain', '/swap?outputCurrency=ETH&recipient=bob.argent.xyz') + }) }) diff --git a/package.json b/package.json index 6dce12c104..210815a011 100644 --- a/package.json +++ b/package.json @@ -85,10 +85,7 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", - "lint:fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix", - "cy:run": "cypress run", - "serve:build": "serve -s build -l 3000", - "integration-test": "yarn build && start-server-and-test 'yarn run serve:build' http://localhost:3000 cy:run" + "integration-test": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run'" }, "eslintConfig": { "extends": "react-app" diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index ec2f55b1da..2a14966df1 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -21,6 +21,7 @@ const Base = styled(RebassButton)<{ outline: none; border: 1px solid transparent; color: white; + text-decoration: none; display: flex; justify-content: center; flex-wrap: nowrap; diff --git a/src/components/CurrencyInputPanel/index.tsx b/src/components/CurrencyInputPanel/index.tsx index 794965aa6e..b5e4d78f4b 100644 --- a/src/components/CurrencyInputPanel/index.tsx +++ b/src/components/CurrencyInputPanel/index.tsx @@ -132,6 +132,7 @@ interface CurrencyInputPanelProps { showSendWithSwap?: boolean otherSelectedTokenAddress?: string | null id: string + showCommonBases?: boolean } export default function CurrencyInputPanel({ @@ -150,7 +151,8 @@ export default function CurrencyInputPanel({ hideInput = false, showSendWithSwap = false, otherSelectedTokenAddress = null, - id + id, + showCommonBases }: CurrencyInputPanelProps) { const { t } = useTranslation() @@ -247,6 +249,7 @@ export default function CurrencyInputPanel({ hiddenToken={token?.address} otherSelectedTokenAddress={otherSelectedTokenAddress} otherSelectedText={field === Field.INPUT ? 'Selected as output' : 'Selected as input'} + showCommonBases={showCommonBases} /> )} diff --git a/src/components/DoubleLogo/index.tsx b/src/components/DoubleLogo/index.tsx index 8330d3de9b..7ad0b76998 100644 --- a/src/components/DoubleLogo/index.tsx +++ b/src/components/DoubleLogo/index.tsx @@ -12,7 +12,7 @@ const TokenWrapper = styled.div<{ margin: boolean; sizeraw: number }>` interface DoubleTokenLogoProps { margin?: boolean size?: number - a0: string + a0?: string a1?: string } @@ -27,7 +27,7 @@ const CoveredLogo = styled(TokenLogo)<{ sizeraw: number }>` export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: DoubleTokenLogoProps) { return ( - + {a0 && } {a1 && } ) diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index 0739b0c9ea..cbd1869702 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -14,9 +14,7 @@ import { useActiveWeb3React } from '../../hooks' import { useDarkModeManager } from '../../state/user/hooks' import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks' -import { ExternalLink, StyledInternalLink } from '../../theme' import { YellowCard } from '../Card' -import { AutoColumn } from '../Column' import Settings from '../Settings' import Menu from '../Menu' @@ -107,26 +105,6 @@ const UniIcon = styled(HistoryLink)<{ to: string }>` } ` -const MigrateBanner = styled(AutoColumn)` - width: 100%; - padding: 12px 0; - display: flex; - justify-content: center; - background-color: ${({ theme }) => theme.primary5}; - color: ${({ theme }) => theme.primaryText1}; - font-weight: 400; - text-align: center; - pointer-events: auto; - a { - color: ${({ theme }) => theme.primaryText1}; - } - - ${({ theme }) => theme.mediaWidth.upToSmall` - padding: 0; - display: none; - `}; -` - const HeaderControls = styled.div` display: flex; flex-direction: row; @@ -153,17 +131,6 @@ export default function Header() { return ( - - Uniswap V2 is live! Read the  - - blog post ↗ - -  or  - - migrate your liquidity ↗ - - . - diff --git a/src/components/PositionCard/V1.tsx b/src/components/PositionCard/V1.tsx index 4148532014..ecbf825977 100644 --- a/src/components/PositionCard/V1.tsx +++ b/src/components/PositionCard/V1.tsx @@ -1,5 +1,5 @@ import React, { useContext } from 'react' -import { RouteComponentProps, withRouter } from 'react-router-dom' +import { Link, RouteComponentProps, withRouter } from 'react-router-dom' import { Token, TokenAmount, WETH } from '@uniswap/sdk' import { Text } from 'rebass' @@ -16,7 +16,7 @@ interface PositionCardProps extends RouteComponentProps<{}> { V1LiquidityBalance: TokenAmount } -function V1PositionCard({ token, V1LiquidityBalance, history }: PositionCardProps) { +function V1PositionCard({ token, V1LiquidityBalance }: PositionCardProps) { const theme = useContext(ThemeContext) const { chainId } = useActiveWeb3React() @@ -47,21 +47,15 @@ function V1PositionCard({ token, V1LiquidityBalance, history }: PositionCardProp <AutoColumn gap="8px"> <RowBetween marginTop="10px"> - <ButtonSecondary - width="68%" - onClick={() => { - history.push(`/migrate/v1/${V1LiquidityBalance.token.address}`) - }} - > + <ButtonSecondary width="68%" as={Link} to={`/migrate/v1/${V1LiquidityBalance.token.address}`}> Migrate </ButtonSecondary> <ButtonSecondary style={{ backgroundColor: 'transparent' }} width="28%" - onClick={() => { - history.push(`/remove/v1/${V1LiquidityBalance.token.address}`) - }} + as={Link} + to={`/remove/v1/${V1LiquidityBalance.token.address}`} > Remove </ButtonSecondary> diff --git a/src/components/PositionCard/index.tsx b/src/components/PositionCard/index.tsx index 628d3cc524..8be29620d8 100644 --- a/src/components/PositionCard/index.tsx +++ b/src/components/PositionCard/index.tsx @@ -1,18 +1,19 @@ import React, { useState } from 'react' import styled from 'styled-components' import { darken } from 'polished' -import { RouteComponentProps, withRouter } from 'react-router-dom' +import { Link } from 'react-router-dom' import { Percent, Pair, JSBI } from '@uniswap/sdk' import { useActiveWeb3React } from '../../hooks' import { useTotalSupply } from '../../data/TotalSupply' +import { currencyId } from '../../pages/AddLiquidity/currencyId' import { useTokenBalance } from '../../state/wallet/hooks' import Card, { GreyCard } from '../Card' import TokenLogo from '../TokenLogo' import DoubleLogo from '../DoubleLogo' import { Text } from 'rebass' -import { ExternalLink } from '../../theme/components' +import { ExternalLink } from '../../theme' import { AutoColumn } from '../Column' import { ChevronDown, ChevronUp } from 'react-feather' import { ButtonSecondary } from '../Button' @@ -30,13 +31,97 @@ export const HoverCard = styled(Card)` } ` -interface PositionCardProps extends RouteComponentProps<{}> { - pair: Pair - minimal?: boolean +interface PositionCardProps { + pair: Pair | undefined | null border?: string } -function PositionCard({ pair, history, border, minimal = false }: PositionCardProps) { +export function MinimalPositionCard({ pair, border }: PositionCardProps) { + const { account } = useActiveWeb3React() + + const token0 = pair?.token0 + const token1 = pair?.token1 + + const [showMore, setShowMore] = useState(false) + + const userPoolBalance = useTokenBalance(account, pair?.liquidityToken) + const totalPoolTokens = useTotalSupply(pair?.liquidityToken) + + const [token0Deposited, token1Deposited] = + !!pair && + !!totalPoolTokens && + !!userPoolBalance && + // this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply + JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw) + ? [ + pair.getLiquidityValue(token0, totalPoolTokens, userPoolBalance, false), + pair.getLiquidityValue(token1, totalPoolTokens, userPoolBalance, false) + ] + : [undefined, undefined] + + return ( + <> + {userPoolBalance && ( + <GreyCard border={border}> + <AutoColumn gap="12px"> + <FixedHeightRow> + <RowFixed> + <Text fontWeight={500} fontSize={16}> + Your position + </Text> + </RowFixed> + </FixedHeightRow> + <FixedHeightRow onClick={() => setShowMore(!showMore)}> + <RowFixed> + <DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} /> + <Text fontWeight={500} fontSize={20}> + {token0?.symbol}/{token1?.symbol} + </Text> + </RowFixed> + <RowFixed> + <Text fontWeight={500} fontSize={20}> + {userPoolBalance ? userPoolBalance.toSignificant(4) : '-'} + </Text> + </RowFixed> + </FixedHeightRow> + <AutoColumn gap="4px"> + <FixedHeightRow> + <Text color="#888D9B" fontSize={16} fontWeight={500}> + {token0?.symbol}: + </Text> + {token0Deposited ? ( + <RowFixed> + <Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}> + {token0Deposited?.toSignificant(6)} + </Text> + </RowFixed> + ) : ( + '-' + )} + </FixedHeightRow> + <FixedHeightRow> + <Text color="#888D9B" fontSize={16} fontWeight={500}> + {token1?.symbol}: + </Text> + {token1Deposited ? ( + <RowFixed> + <Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}> + {token1Deposited?.toSignificant(6)} + </Text> + </RowFixed> + ) : ( + '-' + )} + </FixedHeightRow> + </AutoColumn> + </AutoColumn> + </GreyCard> + )} + </> + ) +} + +export default function FullPositionCard({ pair, border }: PositionCardProps) { const { account } = useActiveWeb3React() const token0 = pair?.token0 @@ -64,168 +149,94 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr ] : [undefined, undefined] - if (minimal) { - return ( - <> - {userPoolBalance && ( - <GreyCard border={border}> - <AutoColumn gap="12px"> - <FixedHeightRow> + return ( + <HoverCard border={border}> + <AutoColumn gap="12px"> + <FixedHeightRow onClick={() => setShowMore(!showMore)} style={{ cursor: 'pointer' }}> + <RowFixed> + <DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} /> + <Text fontWeight={500} fontSize={20}> + {!token0 || !token1 ? <Dots>Loading</Dots> : `${token0.symbol}/${token1.symbol}`} + </Text> + </RowFixed> + <RowFixed> + {showMore ? ( + <ChevronUp size="20" style={{ marginLeft: '10px' }} /> + ) : ( + <ChevronDown size="20" style={{ marginLeft: '10px' }} /> + )} + </RowFixed> + </FixedHeightRow> + {showMore && ( + <AutoColumn gap="8px"> + <FixedHeightRow> + <RowFixed> + <Text fontSize={16} fontWeight={500}> + Pooled {token0?.symbol}: + </Text> + </RowFixed> + {token0Deposited ? ( <RowFixed> - <Text fontWeight={500} fontSize={16}> - Your position + <Text fontSize={16} fontWeight={500} marginLeft={'6px'}> + {token0Deposited?.toSignificant(6)} </Text> + <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} /> </RowFixed> - </FixedHeightRow> - <FixedHeightRow onClick={() => setShowMore(!showMore)}> - <RowFixed> - <DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} /> - <Text fontWeight={500} fontSize={20}> - {token0?.symbol}/{token1?.symbol} - </Text> - </RowFixed> - <RowFixed> - <Text fontWeight={500} fontSize={20}> - {userPoolBalance ? userPoolBalance.toSignificant(4) : '-'} - </Text> - </RowFixed> - </FixedHeightRow> - <AutoColumn gap="4px"> - <FixedHeightRow> - <Text color="#888D9B" fontSize={16} fontWeight={500}> - {token0?.symbol}: - </Text> - {token0Deposited ? ( - <RowFixed> - <Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}> - {token0Deposited?.toSignificant(6)} - </Text> - </RowFixed> - ) : ( - '-' - )} - </FixedHeightRow> - <FixedHeightRow> - <Text color="#888D9B" fontSize={16} fontWeight={500}> - {token1?.symbol}: - </Text> - {token1Deposited ? ( - <RowFixed> - <Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}> - {token1Deposited?.toSignificant(6)} - </Text> - </RowFixed> - ) : ( - '-' - )} - </FixedHeightRow> - </AutoColumn> - </AutoColumn> - </GreyCard> - )} - </> - ) - } else - return ( - <HoverCard border={border}> - <AutoColumn gap="12px"> - <FixedHeightRow onClick={() => setShowMore(!showMore)} style={{ cursor: 'pointer' }}> - <RowFixed> - <DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} /> - <Text fontWeight={500} fontSize={20}> - {!token0 || !token1 ? <Dots>Loading</Dots> : `${token0.symbol}/${token1.symbol}`} - </Text> - </RowFixed> - <RowFixed> - {showMore ? ( - <ChevronUp size="20" style={{ marginLeft: '10px' }} /> ) : ( - <ChevronDown size="20" style={{ marginLeft: '10px' }} /> + '-' )} - </RowFixed> - </FixedHeightRow> - {showMore && ( - <AutoColumn gap="8px"> - <FixedHeightRow> - <RowFixed> - <Text fontSize={16} fontWeight={500}> - Pooled {token0?.symbol}: - </Text> - </RowFixed> - {token0Deposited ? ( - <RowFixed> - <Text fontSize={16} fontWeight={500} marginLeft={'6px'}> - {token0Deposited?.toSignificant(6)} - </Text> - <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} /> - </RowFixed> - ) : ( - '-' - )} - </FixedHeightRow> + </FixedHeightRow> - <FixedHeightRow> + <FixedHeightRow> + <RowFixed> + <Text fontSize={16} fontWeight={500}> + Pooled {token1?.symbol}: + </Text> + </RowFixed> + {token1Deposited ? ( <RowFixed> - <Text fontSize={16} fontWeight={500}> - Pooled {token1?.symbol}: + <Text fontSize={16} fontWeight={500} marginLeft={'6px'}> + {token1Deposited?.toSignificant(6)} </Text> + <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} /> </RowFixed> - {token1Deposited ? ( - <RowFixed> - <Text fontSize={16} fontWeight={500} marginLeft={'6px'}> - {token1Deposited?.toSignificant(6)} - </Text> - <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} /> - </RowFixed> - ) : ( - '-' - )} - </FixedHeightRow> - <FixedHeightRow> - <Text fontSize={16} fontWeight={500}> - Your pool tokens: - </Text> - <Text fontSize={16} fontWeight={500}> - {userPoolBalance ? userPoolBalance.toSignificant(4) : '-'} - </Text> - </FixedHeightRow> - <FixedHeightRow> - <Text fontSize={16} fontWeight={500}> - Your pool share: - </Text> - <Text fontSize={16} fontWeight={500}> - {poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'} - </Text> - </FixedHeightRow> + ) : ( + '-' + )} + </FixedHeightRow> + <FixedHeightRow> + <Text fontSize={16} fontWeight={500}> + Your pool tokens: + </Text> + <Text fontSize={16} fontWeight={500}> + {userPoolBalance ? userPoolBalance.toSignificant(4) : '-'} + </Text> + </FixedHeightRow> + <FixedHeightRow> + <Text fontSize={16} fontWeight={500}> + Your pool share: + </Text> + <Text fontSize={16} fontWeight={500}> + {poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'} + </Text> + </FixedHeightRow> - <AutoRow justify="center" marginTop={'10px'}> - <ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}> - View pool information ↗ - </ExternalLink> - </AutoRow> - <RowBetween marginTop="10px"> - <ButtonSecondary - width="48%" - onClick={() => { - history.push('/add/' + token0?.address + '-' + token1?.address) - }} - > - Add - </ButtonSecondary> - <ButtonSecondary - width="48%" - onClick={() => { - history.push('/remove/' + token0?.address + '-' + token1?.address) - }} - > - Remove - </ButtonSecondary> - </RowBetween> - </AutoColumn> - )} - </AutoColumn> - </HoverCard> - ) + <AutoRow justify="center" marginTop={'10px'}> + <ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}> + View pool information ↗ + </ExternalLink> + </AutoRow> + <RowBetween marginTop="10px"> + <ButtonSecondary as={Link} to={`/add/${currencyId(token0)}/${currencyId(token1)}`} width="48%"> + Add + </ButtonSecondary> + <ButtonSecondary as={Link} width="48%" to={`/remove/${token0?.address}-${token1?.address}`}> + Remove + </ButtonSecondary> + </RowBetween> + </AutoColumn> + )} + </AutoColumn> + </HoverCard> + ) } - -export default withRouter(PositionCard) diff --git a/src/components/SearchModal/CommonBases.tsx b/src/components/SearchModal/CommonBases.tsx index 344588b1a6..3ce4c525ab 100644 --- a/src/components/SearchModal/CommonBases.tsx +++ b/src/components/SearchModal/CommonBases.tsx @@ -1,41 +1,56 @@ import React from 'react' import { Text } from 'rebass' -import { Token } from '@uniswap/sdk' +import { ChainId, Token } from '@uniswap/sdk' +import styled from 'styled-components' import { SUGGESTED_BASES } from '../../constants' import { AutoColumn } from '../Column' import QuestionHelper from '../QuestionHelper' import { AutoRow } from '../Row' import TokenLogo from '../TokenLogo' -import { BaseWrapper } from './styleds' + +const BaseWrapper = styled.div<{ disable?: boolean }>` + border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)}; + border-radius: 10px; + display: flex; + padding: 6px; + + align-items: center; + :hover { + cursor: ${({ disable }) => !disable && 'pointer'}; + background-color: ${({ theme, disable }) => !disable && theme.bg2}; + } + + background-color: ${({ theme, disable }) => disable && theme.bg3}; + opacity: ${({ disable }) => disable && '0.4'}; +` export default function CommonBases({ chainId, onSelect, selectedTokenAddress }: { - chainId: number + chainId: ChainId selectedTokenAddress: string onSelect: (tokenAddress: string) => void }) { return ( <AutoColumn gap="md"> <AutoRow> - <Text fontWeight={500} fontSize={16}> - Common Bases + <Text fontWeight={500} fontSize={14}> + Common bases </Text> - <QuestionHelper text="These tokens are commonly used in pairs." /> + <QuestionHelper text="These tokens are commonly paired with other tokens." /> </AutoRow> - <AutoRow gap="10px"> - {(SUGGESTED_BASES[chainId] ?? []).map((token: Token) => { + <AutoRow gap="4px"> + {(SUGGESTED_BASES[chainId as ChainId] ?? []).map((token: Token) => { return ( <BaseWrapper - gap="6px" onClick={() => selectedTokenAddress !== token.address && onSelect(token.address)} disable={selectedTokenAddress === token.address} key={token.address} > - <TokenLogo address={token.address} /> + <TokenLogo address={token.address} style={{ marginRight: 8 }} /> <Text fontWeight={500} fontSize={16}> {token.symbol} </Text> diff --git a/src/components/SearchModal/PairList.tsx b/src/components/SearchModal/PairList.tsx deleted file mode 100644 index 83418fbb45..0000000000 --- a/src/components/SearchModal/PairList.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { JSBI, Pair, TokenAmount } from '@uniswap/sdk' -import React from 'react' -import { FixedSizeList } from 'react-window' -import { Text } from 'rebass' -import { ButtonPrimary } from '../Button' -import DoubleTokenLogo from '../DoubleLogo' -import { RowFixed } from '../Row' -import { MenuItem, ModalInfo } from './styleds' - -export default function PairList({ - pairs, - focusTokenAddress, - pairBalances, - onSelectPair, - onAddLiquidity = onSelectPair -}: { - pairs: Pair[] - focusTokenAddress?: string - pairBalances: { [pairAddress: string]: TokenAmount } - onSelectPair: (pair: Pair) => void - onAddLiquidity: (pair: Pair) => void -}) { - if (pairs.length === 0) { - return <ModalInfo>No Pools Found</ModalInfo> - } - - return ( - <FixedSizeList itemSize={56} height={500} itemCount={pairs.length} width="100%" style={{ flex: '1' }}> - {({ index, style }) => { - const pair = pairs[index] - - // the focused token is shown first - const tokenA = focusTokenAddress === pair.token1.address ? pair.token1 : pair.token0 - const tokenB = tokenA === pair.token0 ? pair.token1 : pair.token0 - - const pairAddress = pair.liquidityToken.address - const balance = pairBalances[pairAddress]?.toSignificant(6) - const zeroBalance = pairBalances[pairAddress]?.raw && JSBI.equal(pairBalances[pairAddress].raw, JSBI.BigInt(0)) - - const selectPair = () => onSelectPair(pair) - const addLiquidity = () => onAddLiquidity(pair) - - return ( - <MenuItem style={style} onClick={selectPair}> - <RowFixed> - <DoubleTokenLogo a0={tokenA.address} a1={tokenB.address} size={24} margin={true} /> - <Text fontWeight={500} fontSize={16}>{`${tokenA.symbol}/${tokenB.symbol}`}</Text> - </RowFixed> - - <ButtonPrimary padding={'6px 8px'} width={'fit-content'} borderRadius={'12px'} onClick={addLiquidity}> - {balance ? (zeroBalance ? 'Join' : 'Add Liquidity') : 'Join'} - </ButtonPrimary> - </MenuItem> - ) - }} - </FixedSizeList> - ) -} diff --git a/src/components/SearchModal/PairSearchModal.tsx b/src/components/SearchModal/PairSearchModal.tsx deleted file mode 100644 index eaae6afbbb..0000000000 --- a/src/components/SearchModal/PairSearchModal.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { Pair } from '@uniswap/sdk' -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' -import { isMobile } from 'react-device-detect' -import { useTranslation } from 'react-i18next' -import { RouteComponentProps, withRouter } from 'react-router-dom' -import { Text } from 'rebass' -import { ThemeContext } from 'styled-components' -import Card from '../../components/Card' -import { useActiveWeb3React } from '../../hooks' -import { useAllTokens } from '../../hooks/Tokens' -import { useAllDummyPairs } from '../../state/user/hooks' -import { useTokenBalances } from '../../state/wallet/hooks' -import { CloseIcon, StyledInternalLink } from '../../theme/components' -import { isAddress } from '../../utils' -import Column from '../Column' -import Modal from '../Modal' -import QuestionHelper from '../QuestionHelper' -import { AutoRow, RowBetween } from '../Row' -import { filterPairs } from './filtering' -import PairList from './PairList' -import { pairComparator } from './sorting' -import { PaddedColumn, SearchInput } from './styleds' - -interface PairSearchModalProps extends RouteComponentProps { - isOpen?: boolean - onDismiss?: () => void -} - -function PairSearchModal({ history, isOpen, onDismiss }: PairSearchModalProps) { - const { t } = useTranslation() - const { account } = useActiveWeb3React() - const theme = useContext(ThemeContext) - - const [searchQuery, setSearchQuery] = useState<string>('') - - const allTokens = useAllTokens() - const allPairs = useAllDummyPairs() - - const allPairBalances = useTokenBalances( - account, - allPairs.map(p => p.liquidityToken) - ) - - // clear the input on open - useEffect(() => { - if (isOpen) setSearchQuery('') - }, [isOpen, setSearchQuery]) - - // manage focus on modal show - const inputRef = useRef<HTMLInputElement>() - function onInput(event) { - const input = event.target.value - const checksummedInput = isAddress(input) - setSearchQuery(checksummedInput || input) - } - - const filteredPairs = useMemo(() => { - return filterPairs(allPairs, searchQuery) - }, [allPairs, searchQuery]) - - const sortedPairList = useMemo(() => { - const query = searchQuery.toLowerCase() - const queryMatches = (pair: Pair): boolean => - pair.token0.symbol.toLowerCase() === query || pair.token1.symbol.toLowerCase() === query - return filteredPairs.sort((a, b): number => { - const [aMatches, bMatches] = [queryMatches(a), queryMatches(b)] - if (aMatches && !bMatches) return -1 - if (bMatches && !aMatches) return 1 - const balanceA = allPairBalances[a.liquidityToken.address] - const balanceB = allPairBalances[b.liquidityToken.address] - return pairComparator(a, b, balanceA, balanceB) - }) - }, [searchQuery, filteredPairs, allPairBalances]) - - const selectPair = useCallback( - (pair: Pair) => { - history.push(`/add/${pair.token0.address}-${pair.token1.address}`) - }, - [history] - ) - - const focusedToken = Object.values(allTokens ?? {}).filter(token => { - return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address - })[0] - - return ( - <Modal - isOpen={isOpen} - onDismiss={onDismiss} - maxHeight={70} - initialFocusRef={isMobile ? undefined : inputRef} - minHeight={70} - > - <Column style={{ width: '100%' }}> - <PaddedColumn gap="20px"> - <RowBetween> - <Text fontWeight={500} fontSize={16}> - Select a pool - <QuestionHelper text="Find a pair by searching for its name below." /> - </Text> - <CloseIcon onClick={onDismiss} /> - </RowBetween> - <SearchInput - type="text" - id="token-search-input" - placeholder={t('tokenSearchPlaceholder')} - value={searchQuery} - ref={inputRef} - onChange={onInput} - /> - <RowBetween> - <Text fontSize={14} fontWeight={500}> - Pool Name - </Text> - </RowBetween> - </PaddedColumn> - <div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} /> - <PairList - pairs={sortedPairList} - focusTokenAddress={focusedToken?.address} - onAddLiquidity={selectPair} - onSelectPair={selectPair} - pairBalances={allPairBalances} - /> - <div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} /> - <Card> - <AutoRow justify={'center'}> - <div> - <Text fontWeight={500}> - {!isMobile && "Don't see a pool? "} - <StyledInternalLink to="/find">{!isMobile ? 'Import it.' : 'Import pool.'}</StyledInternalLink> - </Text> - </div> - </AutoRow> - </Card> - </Column> - </Modal> - ) -} - -export default withRouter(PairSearchModal) diff --git a/src/components/SearchModal/TokenList.tsx b/src/components/SearchModal/TokenList.tsx index 9451e8bed3..3cab742955 100644 --- a/src/components/SearchModal/TokenList.tsx +++ b/src/components/SearchModal/TokenList.tsx @@ -1,5 +1,5 @@ import { JSBI, Token, TokenAmount } from '@uniswap/sdk' -import React, { useContext } from 'react' +import React, { CSSProperties, memo, useContext, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { FixedSizeList } from 'react-window' import { Text } from 'rebass' @@ -40,6 +40,103 @@ export default function TokenList({ const addToken = useAddUserToken() const removeToken = useRemoveUserAddedToken() + const TokenRow = useMemo(() => { + return memo(function TokenRow({ index, style }: { index: number; style: CSSProperties }) { + const token = tokens[index] + const { address, symbol } = token + + const isDefault = isDefaultToken(token) + const customAdded = isCustomAddedToken(allTokens, token) + const balance = allTokenBalances[address] + + const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw) + + return ( + <MenuItem + style={style} + key={address} + className={`token-item-${address}`} + onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))} + disabled={selectedToken && selectedToken === address} + selected={otherToken === address} + > + <RowFixed> + <TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} /> + <Column> + <Text fontWeight={500}> + {symbol} + {otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>} + </Text> + <FadedSpan> + {customAdded ? ( + <TYPE.main fontWeight={500}> + Added by user + <LinkStyledButton + onClick={event => { + event.stopPropagation() + removeToken(chainId, address) + }} + > + (Remove) + </LinkStyledButton> + </TYPE.main> + ) : null} + {!isDefault && !customAdded ? ( + <TYPE.main fontWeight={500}> + Found by address + <LinkStyledButton + onClick={event => { + event.stopPropagation() + addToken(token) + }} + > + (Add) + </LinkStyledButton> + </TYPE.main> + ) : null} + </FadedSpan> + </Column> + </RowFixed> + <AutoColumn> + {balance ? ( + <Text> + {zeroBalance && showSendWithSwap ? ( + <ButtonSecondary padding={'4px 8px'}> + <Text textAlign="center" fontWeight={500} fontSize={14} color={theme.primary1}> + Send With Swap + </Text> + </ButtonSecondary> + ) : balance ? ( + balance.toSignificant(6) + ) : ( + '-' + )} + </Text> + ) : account ? ( + <Loader /> + ) : ( + '-' + )} + </AutoColumn> + </MenuItem> + ) + }) + }, [ + account, + addToken, + allTokenBalances, + allTokens, + chainId, + onTokenSelect, + otherSelectedText, + otherToken, + removeToken, + selectedToken, + showSendWithSwap, + theme.primary1, + tokens + ]) + if (tokens.length === 0) { return <ModalInfo>{t('noToken')}</ModalInfo> } @@ -53,86 +150,7 @@ export default function TokenList({ style={{ flex: '1' }} itemKey={index => tokens[index].address} > - {({ index, style }) => { - const token = tokens[index] - const { address, symbol } = token - - const isDefault = isDefaultToken(token) - const customAdded = isCustomAddedToken(allTokens, token) - const balance = allTokenBalances[address] - - const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw) - - return ( - <MenuItem - style={style} - key={address} - className={`token-item-${address}`} - onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))} - disabled={selectedToken && selectedToken === address} - selected={otherToken === address} - > - <RowFixed> - <TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} /> - <Column> - <Text fontWeight={500}> - {symbol} - {otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>} - </Text> - <FadedSpan> - {customAdded ? ( - <TYPE.main fontWeight={500}> - Added by user - <LinkStyledButton - onClick={event => { - event.stopPropagation() - removeToken(chainId, address) - }} - > - (Remove) - </LinkStyledButton> - </TYPE.main> - ) : null} - {!isDefault && !customAdded ? ( - <TYPE.main fontWeight={500}> - Found by address - <LinkStyledButton - onClick={event => { - event.stopPropagation() - addToken(token) - }} - > - (Add) - </LinkStyledButton> - </TYPE.main> - ) : null} - </FadedSpan> - </Column> - </RowFixed> - <AutoColumn> - {balance ? ( - <Text> - {zeroBalance && showSendWithSwap ? ( - <ButtonSecondary padding={'4px 8px'}> - <Text textAlign="center" fontWeight={500} fontSize={14} color={theme.primary1}> - Send With Swap - </Text> - </ButtonSecondary> - ) : balance ? ( - balance.toSignificant(6) - ) : ( - '-' - )} - </Text> - ) : account ? ( - <Loader /> - ) : ( - '-' - )} - </AutoColumn> - </MenuItem> - ) - }} + {TokenRow} </FixedSizeList> ) } diff --git a/src/components/SearchModal/TokenSearchModal.tsx b/src/components/SearchModal/TokenSearchModal.tsx index ffaf88b95d..6dbdd61f08 100644 --- a/src/components/SearchModal/TokenSearchModal.tsx +++ b/src/components/SearchModal/TokenSearchModal.tsx @@ -1,5 +1,5 @@ import { Token } from '@uniswap/sdk' -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import React, { KeyboardEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { isMobile } from 'react-device-detect' import { useTranslation } from 'react-i18next' import { Text } from 'rebass' @@ -9,7 +9,7 @@ import { useActiveWeb3React } from '../../hooks' import { useAllTokens, useToken } from '../../hooks/Tokens' import useInterval from '../../hooks/useInterval' import { useAllTokenBalancesTreatingWETHasETH, useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks' -import { CloseIcon, LinkStyledButton } from '../../theme/components' +import { CloseIcon, LinkStyledButton } from '../../theme' import { isAddress } from '../../utils' import Column from '../Column' import Modal from '../Modal' @@ -122,6 +122,20 @@ export default function TokenSearchModal({ false ) + const handleEnter = useCallback( + (e: KeyboardEvent<HTMLInputElement>) => { + if (e.key === 'Enter' && filteredSortedTokens.length > 0) { + if ( + filteredSortedTokens[0].symbol.toLowerCase() === searchQuery.trim().toLowerCase() || + filteredSortedTokens.length === 1 + ) { + handleTokenSelect(filteredSortedTokens[0].address) + } + } + }, + [filteredSortedTokens, handleTokenSelect, searchQuery] + ) + return ( <Modal isOpen={isOpen} @@ -131,7 +145,7 @@ export default function TokenSearchModal({ minHeight={70} > <Column style={{ width: '100%' }}> - <PaddedColumn gap="20px"> + <PaddedColumn gap="14px"> <RowBetween> <Text fontWeight={500} fontSize={16}> Select a token @@ -156,6 +170,7 @@ export default function TokenSearchModal({ onChange={handleInput} onFocus={closeTooltip} onBlur={closeTooltip} + onKeyDown={handleEnter} /> </Tooltip> {showCommonBases && ( diff --git a/src/components/SearchModal/styleds.tsx b/src/components/SearchModal/styleds.tsx index 5d57c287de..bed8ef6549 100644 --- a/src/components/SearchModal/styleds.tsx +++ b/src/components/SearchModal/styleds.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components' import { AutoColumn } from '../Column' -import { AutoRow, RowBetween, RowFixed } from '../Row' +import { RowBetween, RowFixed } from '../Row' export const ModalInfo = styled.div` ${({ theme }) => theme.flexRowNoWrap} @@ -61,21 +61,6 @@ export const MenuItem = styled(RowBetween)` opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)}; ` -export const BaseWrapper = styled(AutoRow)<{ disable?: boolean }>` - border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)}; - padding: 0 6px; - border-radius: 10px; - width: 120px; - - :hover { - cursor: ${({ disable }) => !disable && 'pointer'}; - background-color: ${({ theme, disable }) => !disable && theme.bg2}; - } - - background-color: ${({ theme, disable }) => disable && theme.bg3}; - opacity: ${({ disable }) => disable && '0.4'}; -` - export const SearchInput = styled(Input)` transition: border 100ms; :focus { diff --git a/src/pages/AddLiquidity/ConfirmAddModalBottom.tsx b/src/pages/AddLiquidity/ConfirmAddModalBottom.tsx new file mode 100644 index 0000000000..e656dbec9b --- /dev/null +++ b/src/pages/AddLiquidity/ConfirmAddModalBottom.tsx @@ -0,0 +1,63 @@ +import { Fraction, Percent, Token, TokenAmount } from '@uniswap/sdk' +import React from 'react' +import { Text } from 'rebass' +import { ButtonPrimary } from '../../components/Button' +import { RowBetween, RowFixed } from '../../components/Row' +import TokenLogo from '../../components/TokenLogo' +import { Field } from '../../state/mint/actions' +import { TYPE } from '../../theme' + +export function ConfirmAddModalBottom({ + noLiquidity, + price, + tokens, + parsedAmounts, + poolTokenPercentage, + onAdd +}: { + noLiquidity?: boolean + price?: Fraction + tokens: { [field in Field]?: Token } + parsedAmounts: { [field in Field]?: TokenAmount } + poolTokenPercentage?: Percent + onAdd: () => void +}) { + return ( + <> + <RowBetween> + <TYPE.body>{tokens[Field.TOKEN_A]?.symbol} Deposited</TYPE.body> + <RowFixed> + <TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '8px' }} /> + <TYPE.body>{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}</TYPE.body> + </RowFixed> + </RowBetween> + <RowBetween> + <TYPE.body>{tokens[Field.TOKEN_B]?.symbol} Deposited</TYPE.body> + <RowFixed> + <TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '8px' }} /> + <TYPE.body>{parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}</TYPE.body> + </RowFixed> + </RowBetween> + <RowBetween> + <TYPE.body>Rates</TYPE.body> + <TYPE.body> + {`1 ${tokens[Field.TOKEN_A]?.symbol} = ${price?.toSignificant(4)} ${tokens[Field.TOKEN_B]?.symbol}`} + </TYPE.body> + </RowBetween> + <RowBetween style={{ justifyContent: 'flex-end' }}> + <TYPE.body> + {`1 ${tokens[Field.TOKEN_B]?.symbol} = ${price?.invert().toSignificant(4)} ${tokens[Field.TOKEN_A]?.symbol}`} + </TYPE.body> + </RowBetween> + <RowBetween> + <TYPE.body>Share of Pool:</TYPE.body> + <TYPE.body>{noLiquidity ? '100' : poolTokenPercentage?.toSignificant(4)}%</TYPE.body> + </RowBetween> + <ButtonPrimary style={{ margin: '20px 0 0 0' }} onClick={onAdd}> + <Text fontWeight={500} fontSize={20}> + {noLiquidity ? 'Create Pool & Supply' : 'Confirm Supply'} + </Text> + </ButtonPrimary> + </> + ) +} diff --git a/src/pages/AddLiquidity/PoolPriceBar.tsx b/src/pages/AddLiquidity/PoolPriceBar.tsx new file mode 100644 index 0000000000..d2807d4c5f --- /dev/null +++ b/src/pages/AddLiquidity/PoolPriceBar.tsx @@ -0,0 +1,52 @@ +import { Fraction, Percent, Token } from '@uniswap/sdk' +import React, { useContext } from 'react' +import { Text } from 'rebass' +import { ThemeContext } from 'styled-components' +import { AutoColumn } from '../../components/Column' +import { AutoRow } from '../../components/Row' +import { ONE_BIPS } from '../../constants' +import { Field } from '../../state/mint/actions' +import { TYPE } from '../../theme' + +export const PoolPriceBar = ({ + tokens, + noLiquidity, + poolTokenPercentage, + price +}: { + tokens: { [field in Field]?: Token } + noLiquidity?: boolean + poolTokenPercentage?: Percent + price?: Fraction +}) => { + const theme = useContext(ThemeContext) + return ( + <AutoColumn gap="md"> + <AutoRow justify="space-around" gap="4px"> + <AutoColumn justify="center"> + <TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black> + <Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}> + {tokens[Field.TOKEN_B]?.symbol} per {tokens[Field.TOKEN_A]?.symbol} + </Text> + </AutoColumn> + <AutoColumn justify="center"> + <TYPE.black>{price?.invert().toSignificant(6) ?? '0'}</TYPE.black> + <Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}> + {tokens[Field.TOKEN_A]?.symbol} per {tokens[Field.TOKEN_B]?.symbol} + </Text> + </AutoColumn> + <AutoColumn justify="center"> + <TYPE.black> + {noLiquidity && price + ? '100' + : (poolTokenPercentage?.lessThan(ONE_BIPS) ? '<0.01' : poolTokenPercentage?.toFixed(2)) ?? '0'} + % + </TYPE.black> + <Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}> + Share of Pool + </Text> + </AutoColumn> + </AutoRow> + </AutoColumn> + ) +} diff --git a/src/pages/AddLiquidity/currencyId.ts b/src/pages/AddLiquidity/currencyId.ts new file mode 100644 index 0000000000..664f7825a4 --- /dev/null +++ b/src/pages/AddLiquidity/currencyId.ts @@ -0,0 +1,13 @@ +import { Token, ChainId, WETH } from '@uniswap/sdk' + +export function currencyId(...args: [ChainId | undefined, string] | [Token]): string { + if (args.length === 2) { + const [chainId, tokenAddress] = args + return chainId && tokenAddress === WETH[chainId].address ? 'ETH' : tokenAddress + } else if (args.length === 1) { + const [token] = args + return currencyId(token.chainId, token.address) + } else { + throw new Error('unexpected call signature') + } +} diff --git a/src/pages/AddLiquidity/index.tsx b/src/pages/AddLiquidity/index.tsx index 1a6fe851f2..4df57134a7 100644 --- a/src/pages/AddLiquidity/index.tsx +++ b/src/pages/AddLiquidity/index.tsx @@ -1,48 +1,59 @@ import { BigNumber } from '@ethersproject/bignumber' -import { TokenAmount, WETH } from '@uniswap/sdk' -import React, { useContext, useState } from 'react' +import { TransactionResponse } from '@ethersproject/providers' +import { ChainId, Token, TokenAmount, WETH } from '@uniswap/sdk' +import React, { useCallback, useContext, useState } from 'react' import { Plus } from 'react-feather' import ReactGA from 'react-ga' import { RouteComponentProps } from 'react-router-dom' import { Text } from 'rebass' import { ThemeContext } from 'styled-components' -import { ButtonLight, ButtonPrimary, ButtonError } from '../../components/Button' +import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button' import { BlueCard, GreyCard, LightCard } from '../../components/Card' import { AutoColumn, ColumnCenter } from '../../components/Column' import ConfirmationModal from '../../components/ConfirmationModal' import CurrencyInputPanel from '../../components/CurrencyInputPanel' import DoubleLogo from '../../components/DoubleLogo' -import PositionCard from '../../components/PositionCard' -import Row, { AutoRow, RowBetween, RowFixed, RowFlat } from '../../components/Row' +import { AddRemoveTabs } from '../../components/NavigationTabs' +import { MinimalPositionCard } from '../../components/PositionCard' +import Row, { RowBetween, RowFlat } from '../../components/Row' -import TokenLogo from '../../components/TokenLogo' - -import { ROUTER_ADDRESS, MIN_ETH, ONE_BIPS } from '../../constants' +import { ROUTER_ADDRESS } from '../../constants' import { useActiveWeb3React } from '../../hooks' +import { useToken } from '../../hooks/Tokens' +import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback' +import { useWalletModalToggle } from '../../state/application/hooks' +import { Field } from '../../state/mint/actions' +import { useDerivedMintInfo, useMintActionHandlers, useMintState } from '../../state/mint/hooks' import { useTransactionAdder } from '../../state/transactions/hooks' +import { useIsExpertMode, useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks' import { TYPE } from '../../theme' import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils' +import { maxAmountSpend } from '../../utils/maxAmountSpend' import AppBody from '../AppBody' import { Dots, Wrapper } from '../Pool/styleds' -import { - useDefaultsFromURLMatchParams, - useMintState, - useDerivedMintInfo, - useMintActionHandlers -} from '../../state/mint/hooks' -import { Field } from '../../state/mint/actions' -import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback' -import { useWalletModalToggle } from '../../state/application/hooks' -import { useUserSlippageTolerance, useUserDeadline, useIsExpertMode } from '../../state/user/hooks' -import { AddRemoveTabs } from '../../components/NavigationTabs' +import { ConfirmAddModalBottom } from './ConfirmAddModalBottom' +import { currencyId } from './currencyId' +import { PoolPriceBar } from './PoolPriceBar' -export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) { - useDefaultsFromURLMatchParams(params) +function useTokenByCurrencyId(chainId: ChainId | undefined, currencyId: string | undefined): Token | undefined { + const isETH = currencyId?.toUpperCase() === 'ETH' + const token = useToken(isETH ? undefined : currencyId) + return isETH && chainId ? WETH[chainId] : token ?? undefined +} +export default function AddLiquidity({ + match: { + params: { currencyIdA, currencyIdB } + }, + history +}: RouteComponentProps<{ currencyIdA?: string; currencyIdB?: string }>) { const { account, chainId, library } = useActiveWeb3React() const theme = useContext(ThemeContext) + const tokenA = useTokenByCurrencyId(chainId, currencyIdA) + const tokenB = useTokenByCurrencyId(chainId, currencyIdB) + // toggle wallet when disconnected const toggleWalletModal = useWalletModalToggle() @@ -61,8 +72,21 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< liquidityMinted, poolTokenPercentage, error - } = useDerivedMintInfo() - const { onUserInput } = useMintActionHandlers() + } = useDerivedMintInfo(tokenA ?? undefined, tokenB ?? undefined) + const { onUserInput } = useMintActionHandlers(noLiquidity) + + const handleTokenAInput = useCallback( + (field: string, value: string) => { + return onUserInput(Field.TOKEN_A, value) + }, + [onUserInput] + ) + const handleTokenBInput = useCallback( + (field: string, value: string) => { + return onUserInput(Field.TOKEN_B, value) + }, + [onUserInput] + ) const isValid = !error @@ -85,17 +109,7 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< const maxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce((accumulator, field) => { return { ...accumulator, - [field]: - !!tokenBalances[field] && - !!tokens[field] && - !!WETH[chainId] && - tokenBalances[field].greaterThan( - new TokenAmount(tokens[field], tokens[field].equals(WETH[chainId]) ? MIN_ETH : '0') - ) - ? tokens[field].equals(WETH[chainId]) - ? tokenBalances[field].subtract(new TokenAmount(WETH[chainId], MIN_ETH)) - : tokenBalances[field] - : undefined + [field]: maxAmountSpend(tokenBalances[field]) } }, {}) @@ -103,7 +117,7 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< (accumulator, field) => { return { ...accumulator, - [field]: maxAmounts[field] && parsedAmounts[field] ? maxAmounts[field].equalTo(parsedAmounts[field]) : undefined + [field]: maxAmounts[field]?.equalTo(parsedAmounts[field] ?? '0') } }, {} @@ -114,38 +128,48 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.TOKEN_B], ROUTER_ADDRESS) const addTransaction = useTransactionAdder() + async function onAdd() { + if (!chainId || !library || !account) return const router = getRouterContract(chainId, library, account) + const { [Field.TOKEN_A]: parsedAmountA, [Field.TOKEN_B]: parsedAmountB } = parsedAmounts + if (!parsedAmountA || !parsedAmountB || !tokenA || !tokenB) { + return + } + const amountsMin = { - [Field.TOKEN_A]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_A], noLiquidity ? 0 : allowedSlippage)[0], - [Field.TOKEN_B]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_B], noLiquidity ? 0 : allowedSlippage)[0] + [Field.TOKEN_A]: calculateSlippageAmount(parsedAmountA, noLiquidity ? 0 : allowedSlippage)[0], + [Field.TOKEN_B]: calculateSlippageAmount(parsedAmountB, noLiquidity ? 0 : allowedSlippage)[0] } const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline - let estimate, method: Function, args: Array<string | string[] | number>, value: BigNumber | null - if (tokens[Field.TOKEN_A].equals(WETH[chainId]) || tokens[Field.TOKEN_B].equals(WETH[chainId])) { - const tokenBIsETH = tokens[Field.TOKEN_B].equals(WETH[chainId]) + let estimate, + method: (...args: any) => Promise<TransactionResponse>, + args: Array<string | string[] | number>, + value: BigNumber | null + if (tokenA.equals(WETH[chainId]) || tokenB.equals(WETH[chainId])) { + const tokenBIsETH = tokenB.equals(WETH[chainId]) estimate = router.estimateGas.addLiquidityETH method = router.addLiquidityETH args = [ - tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address, // token - parsedAmounts[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].raw.toString(), // token desired + (tokenBIsETH ? tokenA : tokenB).address, // token + (tokenBIsETH ? parsedAmountA : parsedAmountB).raw.toString(), // token desired amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(), // token min amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(), // eth min account, deadlineFromNow ] - value = BigNumber.from(parsedAmounts[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].raw.toString()) + value = BigNumber.from((tokenBIsETH ? parsedAmountB : parsedAmountA).raw.toString()) } else { estimate = router.estimateGas.addLiquidity method = router.addLiquidity args = [ - tokens[Field.TOKEN_A].address, - tokens[Field.TOKEN_B].address, - parsedAmounts[Field.TOKEN_A].raw.toString(), - parsedAmounts[Field.TOKEN_B].raw.toString(), + tokenA.address, + tokenB.address, + parsedAmountA.raw.toString(), + parsedAmountB.raw.toString(), amountsMin[Field.TOKEN_A].toString(), amountsMin[Field.TOKEN_B].toString(), account, @@ -228,76 +252,14 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< const modalBottom = () => { return ( - <> - <RowBetween> - <TYPE.body>{tokens[Field.TOKEN_A]?.symbol} Deposited</TYPE.body> - <RowFixed> - <TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '8px' }} /> - <TYPE.body>{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}</TYPE.body> - </RowFixed> - </RowBetween> - <RowBetween> - <TYPE.body>{tokens[Field.TOKEN_B]?.symbol} Deposited</TYPE.body> - <RowFixed> - <TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '8px' }} /> - <TYPE.body>{parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}</TYPE.body> - </RowFixed> - </RowBetween> - <RowBetween> - <TYPE.body>Rates</TYPE.body> - <TYPE.body> - {`1 ${tokens[Field.TOKEN_A]?.symbol} = ${price?.toSignificant(4)} ${tokens[Field.TOKEN_B]?.symbol}`} - </TYPE.body> - </RowBetween> - <RowBetween style={{ justifyContent: 'flex-end' }}> - <TYPE.body> - {`1 ${tokens[Field.TOKEN_B]?.symbol} = ${price?.invert().toSignificant(4)} ${ - tokens[Field.TOKEN_A]?.symbol - }`} - </TYPE.body> - </RowBetween> - <RowBetween> - <TYPE.body>Share of Pool:</TYPE.body> - <TYPE.body>{noLiquidity ? '100' : poolTokenPercentage?.toSignificant(4)}%</TYPE.body> - </RowBetween> - <ButtonPrimary style={{ margin: '20px 0 0 0' }} onClick={onAdd}> - <Text fontWeight={500} fontSize={20}> - {noLiquidity ? 'Create Pool & Supply' : 'Confirm Supply'} - </Text> - </ButtonPrimary> - </> - ) - } - - const PriceBar = () => { - return ( - <AutoColumn gap="md"> - <AutoRow justify="space-around" gap="4px"> - <AutoColumn justify="center"> - <TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black> - <Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}> - {tokens[Field.TOKEN_B]?.symbol} per {tokens[Field.TOKEN_A]?.symbol} - </Text> - </AutoColumn> - <AutoColumn justify="center"> - <TYPE.black>{price?.invert().toSignificant(6) ?? '0'}</TYPE.black> - <Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}> - {tokens[Field.TOKEN_A]?.symbol} per {tokens[Field.TOKEN_B]?.symbol} - </Text> - </AutoColumn> - <AutoColumn justify="center"> - <TYPE.black> - {noLiquidity && price - ? '100' - : (poolTokenPercentage?.lessThan(ONE_BIPS) ? '<0.01' : poolTokenPercentage?.toFixed(2)) ?? '0'} - % - </TYPE.black> - <Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}> - Share of Pool - </Text> - </AutoColumn> - </AutoRow> - </AutoColumn> + <ConfirmAddModalBottom + price={price} + tokens={tokens} + parsedAmounts={parsedAmounts} + noLiquidity={noLiquidity} + onAdd={onAdd} + poolTokenPercentage={poolTokenPercentage} + /> ) } @@ -305,6 +267,35 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< tokens[Field.TOKEN_A]?.symbol } and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}` + const handleTokenASelect = useCallback( + (tokenAddress: string) => { + const [tokenAId, tokenBId] = [ + currencyId(chainId, tokenAddress), + tokenB ? currencyId(chainId, tokenB.address) : undefined + ] + if (tokenAId === tokenBId) { + history.push(`/add/${tokenAId}/${tokenA ? currencyId(chainId, tokenA.address) : ''}`) + } else { + history.push(`/add/${tokenAId}/${tokenBId}`) + } + }, + [chainId, tokenB, history, tokenA] + ) + const handleTokenBSelect = useCallback( + (tokenAddress: string) => { + const [tokenAId, tokenBId] = [ + tokenA ? currencyId(chainId, tokenA.address) : undefined, + currencyId(chainId, tokenAddress) + ] + if (tokenAId === tokenBId) { + history.push(`/add/${tokenB ? currencyId(chainId, tokenB.address) : ''}/${tokenAId}`) + } else { + history.push(`/add/${currencyIdA ? currencyIdA : 'ETH'}/${currencyId(chainId, tokenAddress)}`) + } + }, + [tokenA, chainId, history, tokenB, currencyIdA] + ) + return ( <> <AppBody> @@ -346,34 +337,35 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< </ColumnCenter> )} <CurrencyInputPanel - disableTokenSelect={true} field={Field.TOKEN_A} value={formattedAmounts[Field.TOKEN_A]} - onUserInput={onUserInput} + onUserInput={handleTokenAInput} onMax={() => { - maxAmounts[Field.TOKEN_A] && onUserInput(Field.TOKEN_A, maxAmounts[Field.TOKEN_A].toExact()) + onUserInput(Field.TOKEN_A, maxAmounts[Field.TOKEN_A]?.toExact() ?? '') }} + onTokenSelection={handleTokenASelect} showMaxButton={!atMaxAmounts[Field.TOKEN_A]} token={tokens[Field.TOKEN_A]} pair={pair} - label="Input" id="add-liquidity-input-tokena" + showCommonBases /> <ColumnCenter> <Plus size="16" color={theme.text2} /> </ColumnCenter> <CurrencyInputPanel - disableTokenSelect={true} field={Field.TOKEN_B} value={formattedAmounts[Field.TOKEN_B]} - onUserInput={onUserInput} + onUserInput={handleTokenBInput} + onTokenSelection={handleTokenBSelect} onMax={() => { - maxAmounts[Field.TOKEN_B] && onUserInput(Field.TOKEN_B, maxAmounts[Field.TOKEN_B].toExact()) + onUserInput(Field.TOKEN_B, maxAmounts[Field.TOKEN_B]?.toExact() ?? '') }} showMaxButton={!atMaxAmounts[Field.TOKEN_B]} token={tokens[Field.TOKEN_B]} pair={pair} id="add-liquidity-input-tokenb" + showCommonBases /> {tokens[Field.TOKEN_A] && tokens[Field.TOKEN_B] && ( <> @@ -384,7 +376,12 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< </TYPE.subHeader> </RowBetween>{' '} <LightCard padding="1rem" borderRadius={'20px'}> - <PriceBar /> + <PoolPriceBar + tokens={tokens} + poolTokenPercentage={poolTokenPercentage} + noLiquidity={noLiquidity} + price={price} + /> </LightCard> </GreyCard> </> @@ -447,7 +444,7 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps< {pair && !noLiquidity ? ( <AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}> - <PositionCard pair={pair} minimal={true} /> + <MinimalPositionCard pair={pair} /> </AutoColumn> ) : null} </> diff --git a/src/pages/AddLiquidity/redirects.tsx b/src/pages/AddLiquidity/redirects.tsx new file mode 100644 index 0000000000..07db9b5fda --- /dev/null +++ b/src/pages/AddLiquidity/redirects.tsx @@ -0,0 +1,42 @@ +import { WETH } from '@uniswap/sdk' +import React from 'react' +import { Redirect, RouteComponentProps } from 'react-router-dom' +import AddLiquidity from './index' + +export function RedirectToAddLiquidity() { + return <Redirect to="/add/" /> +} + +function convertToCurrencyIds(address: string): string { + if (Object.values(WETH).some(weth => weth.address === address)) { + return 'ETH' + } + return address +} + +const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/ +export function RedirectOldAddLiquidityPathStructure(props: RouteComponentProps<{ currencyIdA: string }>) { + const { + match: { + params: { currencyIdA } + } + } = props + const match = currencyIdA.match(OLD_PATH_STRUCTURE) + if (match?.length) { + return <Redirect to={`/add/${convertToCurrencyIds(match[1])}/${convertToCurrencyIds(match[2])}`} /> + } + + return <AddLiquidity {...props} /> +} + +export function RedirectDuplicateTokenIds(props: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) { + const { + match: { + params: { currencyIdA, currencyIdB } + } + } = props + if (currencyIdA.toLowerCase() === currencyIdB.toLowerCase()) { + return <Redirect to={`/add/${currencyIdA}`} /> + } + return <AddLiquidity {...props} /> +} diff --git a/src/pages/AddLiquidity/tsconfig.json b/src/pages/AddLiquidity/tsconfig.json new file mode 100644 index 0000000000..331f44e8ab --- /dev/null +++ b/src/pages/AddLiquidity/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.strict.json", + "include": [ + "**/*", + "../../../node_modules/eslint-plugin-react/lib/types.d.ts" + ] +} \ No newline at end of file diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 38191fd689..37d05206c5 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -7,7 +7,11 @@ import Popups from '../components/Popups' import Web3ReactManager from '../components/Web3ReactManager' import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader' import AddLiquidity from './AddLiquidity' -import CreatePool from './CreatePool' +import { + RedirectDuplicateTokenIds, + RedirectOldAddLiquidityPathStructure, + RedirectToAddLiquidity +} from './AddLiquidity/redirects' import MigrateV1 from './MigrateV1' import MigrateV1Exchange from './MigrateV1/MigrateV1Exchange' import RemoveV1Exchange from './MigrateV1/RemoveV1Exchange' @@ -71,8 +75,10 @@ export default function App() { <Route exact strict path="/send" component={RedirectPathToSwapOnly} /> <Route exact strict path="/find" component={PoolFinder} /> <Route exact strict path="/pool" component={Pool} /> - <Route exact strict path="/create" component={CreatePool} /> - <Route exact strict path="/add/:tokens" component={AddLiquidity} /> + <Route exact strict path="/create" component={RedirectToAddLiquidity} /> + <Route exact path="/add" component={AddLiquidity} /> + <Route exact path="/add/:currencyIdA" component={RedirectOldAddLiquidityPathStructure} /> + <Route exact path="/add/:currencyIdA/:currencyIdB" component={RedirectDuplicateTokenIds} /> <Route exact strict path="/remove/:tokens" component={RemoveLiquidity} /> <Route exact strict path="/migrate/v1" component={MigrateV1} /> <Route exact strict path="/migrate/v1/:address" component={MigrateV1Exchange} /> diff --git a/src/pages/CreatePool/index.tsx b/src/pages/CreatePool/index.tsx deleted file mode 100644 index d4a0745550..0000000000 --- a/src/pages/CreatePool/index.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React, { useState, useEffect } from 'react' -import { RouteComponentProps, Redirect } from 'react-router-dom' -import { Token, WETH } from '@uniswap/sdk' -import { CreatePoolTabs } from '../../components/NavigationTabs' -import AppBody from '../AppBody' - -import Row, { AutoRow } from '../../components/Row' -import TokenLogo from '../../components/TokenLogo' -import TokenSearchModal from '../../components/SearchModal/TokenSearchModal' -import { Text } from 'rebass' -import { Plus } from 'react-feather' -import { TYPE, StyledInternalLink } from '../../theme' -import { AutoColumn, ColumnCenter } from '../../components/Column' -import { ButtonPrimary, ButtonDropdown, ButtonDropdownLight } from '../../components/Button' - -import { useToken } from '../../hooks/Tokens' -import { useActiveWeb3React } from '../../hooks' -import { usePair } from '../../data/Reserves' - -enum Fields { - TOKEN0 = 0, - TOKEN1 = 1 -} - -enum STEP { - SELECT_TOKENS = 'SELECT_TOKENS', // choose input and output tokens - READY_TO_CREATE = 'READY_TO_CREATE', // enable 'create' button - SHOW_CREATE_PAGE = 'SHOW_CREATE_PAGE' // show create page -} - -export default function CreatePool({ location }: RouteComponentProps) { - const { chainId } = useActiveWeb3React() - const [showSearch, setShowSearch] = useState<boolean>(false) - const [activeField, setActiveField] = useState<number>(Fields.TOKEN0) - - const [token0Address, setToken0Address] = useState<string>(WETH[chainId].address) - const [token1Address, setToken1Address] = useState<string>() - - const token0: Token = useToken(token0Address) - const token1: Token = useToken(token1Address) - - const [step, setStep] = useState<string>(STEP.SELECT_TOKENS) - - const pair = usePair(token0, token1) - - // if both tokens selected but pair doesnt exist, enable button to create pair - useEffect(() => { - if (token0Address && token1Address && pair === null) { - setStep(STEP.READY_TO_CREATE) - } - }, [pair, token0Address, token1Address]) - - // if theyve clicked create, show add liquidity page - if (step === STEP.SHOW_CREATE_PAGE) { - return <Redirect to={{ ...location, pathname: `/add/${token0Address}-${token1Address}` }} push={true} /> - } - - return ( - <AppBody> - <CreatePoolTabs /> - <AutoColumn gap="20px"> - <AutoColumn gap="24px"> - {!token0Address ? ( - <ButtonDropdown - onClick={() => { - setShowSearch(true) - setActiveField(Fields.TOKEN0) - }} - > - <Text fontSize={20}>Select first token</Text> - </ButtonDropdown> - ) : ( - <ButtonDropdownLight - onClick={() => { - setShowSearch(true) - setActiveField(Fields.TOKEN0) - }} - > - <Row align="flex-end"> - <TokenLogo address={token0Address} /> - <Text fontWeight={500} fontSize={20} marginLeft={'12px'}> - {token0?.symbol}{' '} - </Text> - <TYPE.darkGray fontWeight={500} fontSize={16} marginLeft={'8px'}> - {token0?.address === WETH[chainId]?.address && '(default)'} - </TYPE.darkGray> - </Row> - </ButtonDropdownLight> - )} - <ColumnCenter> - <Plus size="16" color="#888D9B" /> - </ColumnCenter> - {!token1Address ? ( - <ButtonDropdown - onClick={() => { - setShowSearch(true) - setActiveField(Fields.TOKEN1) - }} - disabled={step !== STEP.SELECT_TOKENS} - > - <Text fontSize={20}>Select second token</Text> - </ButtonDropdown> - ) : ( - <ButtonDropdownLight - onClick={() => { - setShowSearch(true) - setActiveField(Fields.TOKEN1) - }} - > - <Row> - <TokenLogo address={token1Address} /> - <Text fontWeight={500} fontSize={20} marginLeft={'12px'}> - {token1?.symbol} - </Text> - </Row> - </ButtonDropdownLight> - )} - {pair ? ( // pair already exists - prompt to add liquidity to existing pool - <AutoRow padding="10px" justify="center"> - <TYPE.body textAlign="center"> - Pool already exists!{' '} - <StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Join the pool.</StyledInternalLink> - </TYPE.body> - </AutoRow> - ) : ( - <ButtonPrimary disabled={step !== STEP.READY_TO_CREATE} onClick={() => setStep(STEP.SHOW_CREATE_PAGE)}> - <Text fontWeight={500} fontSize={20}> - Create Pool - </Text> - </ButtonPrimary> - )} - </AutoColumn> - <TokenSearchModal - isOpen={showSearch} - onTokenSelect={address => { - activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address) - }} - onDismiss={() => { - setShowSearch(false) - }} - hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address} - showCommonBases={activeField === Fields.TOKEN0} - /> - </AutoColumn> - </AppBody> - ) -} diff --git a/src/pages/MigrateV1/MigrateV1Exchange.tsx b/src/pages/MigrateV1/MigrateV1Exchange.tsx index 2f5a6f386b..cf8aaddcd8 100644 --- a/src/pages/MigrateV1/MigrateV1Exchange.tsx +++ b/src/pages/MigrateV1/MigrateV1Exchange.tsx @@ -1,7 +1,6 @@ import { TransactionResponse } from '@ethersproject/abstract-provider' import { ChainId, Fraction, JSBI, Percent, Token, TokenAmount, WETH } from '@uniswap/sdk' import React, { useCallback, useMemo, useState } from 'react' -import { ArrowLeft } from 'react-feather' import ReactGA from 'react-ga' import { Redirect, RouteComponentProps } from 'react-router' import { ButtonConfirmed } from '../../components/Button' @@ -21,7 +20,7 @@ import { useV1ExchangeContract, useV2MigratorContract } from '../../hooks/useCon import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks' import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks' import { useETHBalances, useTokenBalance } from '../../state/wallet/hooks' -import { TYPE, ExternalLink } from '../../theme' +import { TYPE, ExternalLink, BackArrow } from '../../theme' import { isAddress, getEtherscanLink } from '../../utils' import { BodyWrapper } from '../AppBody' import { EmptyState } from './EmptyState' @@ -344,10 +343,6 @@ export default function MigrateV1Exchange({ ) const userLiquidityBalance = useTokenBalance(account, liquidityToken) - const handleBack = useCallback(() => { - history.push('/migrate/v1') - }, [history]) - // redirect for invalid url params if (!validatedAddress || tokenAddress === AddressZero) { console.error('Invalid address in path', address) @@ -358,9 +353,7 @@ export default function MigrateV1Exchange({ <BodyWrapper style={{ padding: 24 }}> <AutoColumn gap="16px"> <AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px"> - <div style={{ cursor: 'pointer' }}> - <ArrowLeft onClick={handleBack} /> - </div> + <BackArrow to="/migrate/v1" /> <TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader> <div> <QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." /> diff --git a/src/pages/MigrateV1/RemoveV1Exchange.tsx b/src/pages/MigrateV1/RemoveV1Exchange.tsx index c798143060..5c84bfee7b 100644 --- a/src/pages/MigrateV1/RemoveV1Exchange.tsx +++ b/src/pages/MigrateV1/RemoveV1Exchange.tsx @@ -1,7 +1,6 @@ import { TransactionResponse } from '@ethersproject/abstract-provider' import { JSBI, Token, TokenAmount, WETH, Fraction, Percent } from '@uniswap/sdk' import React, { useCallback, useMemo, useState } from 'react' -import { ArrowLeft } from 'react-feather' import ReactGA from 'react-ga' import { Redirect, RouteComponentProps } from 'react-router' import { ButtonConfirmed } from '../../components/Button' @@ -16,7 +15,7 @@ import { useV1ExchangeContract } from '../../hooks/useContract' import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks' import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks' import { useTokenBalance, useETHBalances } from '../../state/wallet/hooks' -import { TYPE } from '../../theme' +import { BackArrow, TYPE } from '../../theme' import { isAddress } from '../../utils' import { BodyWrapper } from '../AppBody' import { EmptyState } from './EmptyState' @@ -128,7 +127,6 @@ function V1PairRemoval({ } export default function RemoveV1Exchange({ - history, match: { params: { address } } @@ -149,10 +147,6 @@ export default function RemoveV1Exchange({ ) const userLiquidityBalance = useTokenBalance(account, liquidityToken) - const handleBack = useCallback(() => { - history.push('/migrate/v1') - }, [history]) - // redirect for invalid url params if (!validatedAddress || tokenAddress === AddressZero) { console.error('Invalid address in path', address) @@ -163,9 +157,7 @@ export default function RemoveV1Exchange({ <BodyWrapper style={{ padding: 24 }}> <AutoColumn gap="16px"> <AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px"> - <div style={{ cursor: 'pointer' }}> - <ArrowLeft onClick={handleBack} /> - </div> + <BackArrow to="/migrate/v1" /> <TYPE.mediumHeader>Remove V1 Liquidity</TYPE.mediumHeader> <div> <QuestionHelper text="Remove your Uniswap V1 liquidity tokens." /> diff --git a/src/pages/MigrateV1/index.tsx b/src/pages/MigrateV1/index.tsx index cf90c1cc90..5654fb8cf5 100644 --- a/src/pages/MigrateV1/index.tsx +++ b/src/pages/MigrateV1/index.tsx @@ -1,7 +1,5 @@ import { JSBI, Token } from '@uniswap/sdk' import React, { useCallback, useContext, useMemo, useState, useEffect } from 'react' -import { ArrowLeft } from 'react-feather' -import { RouteComponentProps } from 'react-router' import { ThemeContext } from 'styled-components' import { AutoColumn } from '../../components/Column' import { AutoRow } from '../../components/Row' @@ -10,7 +8,7 @@ import { useAllTokenV1Exchanges } from '../../data/V1' import { useActiveWeb3React } from '../../hooks' import { useToken, useAllTokens } from '../../hooks/Tokens' import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks' -import { TYPE } from '../../theme' +import { BackArrow, TYPE } from '../../theme' import { LightCard } from '../../components/Card' import { BodyWrapper } from '../AppBody' import { EmptyState } from './EmptyState' @@ -20,7 +18,7 @@ import { Dots } from '../../components/swap/styleds' import { useAddUserToken } from '../../state/user/hooks' import { isDefaultToken, isCustomAddedToken } from '../../utils' -export default function MigrateV1({ history }: RouteComponentProps) { +export default function MigrateV1() { const theme = useContext(ThemeContext) const { account, chainId } = useActiveWeb3React() @@ -68,17 +66,11 @@ export default function MigrateV1({ history }: RouteComponentProps) { // should never always be false, because a V1 exhchange exists for WETH on all testnets const isLoading = Object.keys(V1Exchanges)?.length === 0 || V1LiquidityBalancesLoading - const handleBackClick = useCallback(() => { - history.push('/pool') - }, [history]) - return ( <BodyWrapper style={{ padding: 24 }}> <AutoColumn gap="16px"> <AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px"> - <div style={{ cursor: 'pointer' }}> - <ArrowLeft onClick={handleBackClick} /> - </div> + <BackArrow to="/pool" /> <TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader> <div> <QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." /> diff --git a/src/pages/Pool/index.tsx b/src/pages/Pool/index.tsx index ef64744d04..b5ec558c31 100644 --- a/src/pages/Pool/index.tsx +++ b/src/pages/Pool/index.tsx @@ -1,12 +1,11 @@ -import React, { useState, useContext, useCallback } from 'react' -import styled, { ThemeContext } from 'styled-components' -import { JSBI } from '@uniswap/sdk' -import { RouteComponentProps } from 'react-router-dom' +import React, { useContext } from 'react' +import { ThemeContext } from 'styled-components' +import { Pair } from '@uniswap/sdk' +import { Link } from 'react-router-dom' import { SwapPoolTabs } from '../../components/NavigationTabs' import Question from '../../components/QuestionHelper' -import PairSearchModal from '../../components/SearchModal/PairSearchModal' -import PositionCard from '../../components/PositionCard' +import FullPositionCard from '../../components/PositionCard' import { useUserHasLiquidityInAllTokens } from '../../data/V1' import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks' import { StyledInternalLink, TYPE } from '../../theme' @@ -14,7 +13,7 @@ import { Text } from 'rebass' import { LightCard } from '../../components/Card' import { RowBetween } from '../../components/Row' import { ButtonPrimary, ButtonSecondary } from '../../components/Button' -import { AutoColumn, ColumnCenter } from '../../components/Column' +import { AutoColumn } from '../../components/Column' import { useActiveWeb3React } from '../../hooks' import { usePairs } from '../../data/Reserves' @@ -22,71 +21,47 @@ import { useAllDummyPairs } from '../../state/user/hooks' import AppBody from '../AppBody' import { Dots } from '../../components/swap/styleds' -const Positions = styled.div` - position: relative; - width: 100%; -` - -const FixedBottom = styled.div` - position: absolute; - bottom: -80px; - width: 100%; -` - -export default function Pool({ history }: RouteComponentProps) { +export default function Pool() { const theme = useContext(ThemeContext) const { account } = useActiveWeb3React() - const [showPoolSearch, setShowPoolSearch] = useState(false) // fetch the user's balances of all tracked V2 LP tokens - const V2DummyPairs = useAllDummyPairs() - const [V2PairsBalances, fetchingV2PairBalances] = useTokenBalancesWithLoadingIndicator( - account, - V2DummyPairs?.map(p => p.liquidityToken) + const v2DummyPairs = useAllDummyPairs() + const [v2PairsBalances, fetchingV2PairBalances] = useTokenBalancesWithLoadingIndicator( + account ?? undefined, + v2DummyPairs?.map(p => p.liquidityToken) ) // fetch the reserves for all V2 pools in which the user has a balance - const V2DummyPairsWithABalance = V2DummyPairs.filter( - V2DummyPair => - V2PairsBalances[V2DummyPair.liquidityToken.address] && - JSBI.greaterThan(V2PairsBalances[V2DummyPair.liquidityToken.address].raw, JSBI.BigInt(0)) + const v2DummyPairsWithABalance = v2DummyPairs.filter(dummyPair => + v2PairsBalances[dummyPair.liquidityToken.address]?.greaterThan('0') ) - const V2Pairs = usePairs( - V2DummyPairsWithABalance.map(V2DummyPairWithABalance => [ + const v2Pairs = usePairs( + v2DummyPairsWithABalance.map(V2DummyPairWithABalance => [ V2DummyPairWithABalance.token0, V2DummyPairWithABalance.token1 ]) ) - const V2IsLoading = - fetchingV2PairBalances || V2Pairs?.length < V2DummyPairsWithABalance.length || V2Pairs?.some(V2Pair => !!!V2Pair) + const v2IsLoading = + fetchingV2PairBalances || v2Pairs?.length < v2DummyPairsWithABalance.length || v2Pairs?.some(V2Pair => !V2Pair) - const allV2PairsWithLiquidity = V2Pairs.filter(V2Pair => !!V2Pair).map(V2Pair => ( - <PositionCard key={V2Pair.liquidityToken.address} pair={V2Pair} /> - )) + const allV2PairsWithLiquidity = v2Pairs + .filter((v2Pair): v2Pair is Pair => Boolean(v2Pair)) + .map(V2Pair => <FullPositionCard key={V2Pair.liquidityToken.address} pair={V2Pair} />) const hasV1Liquidity = useUserHasLiquidityInAllTokens() - const handleSearchDismiss = useCallback(() => { - setShowPoolSearch(false) - }, [setShowPoolSearch]) - return ( - <AppBody> - <SwapPoolTabs active={'pool'} /> - <AutoColumn gap="lg" justify="center"> - <ButtonPrimary - id="join-pool-button" - padding="16px" - onClick={() => { - setShowPoolSearch(true) - }} - > - <Text fontWeight={500} fontSize={20}> - Join {allV2PairsWithLiquidity?.length > 0 ? 'another' : 'a'} pool - </Text> - </ButtonPrimary> + <> + <AppBody> + <SwapPoolTabs active={'pool'} /> + <AutoColumn gap="lg" justify="center"> + <ButtonPrimary id="join-pool-button" as={Link} style={{ padding: 16 }} to="/add/ETH"> + <Text fontWeight={500} fontSize={20}> + Add Liquidity + </Text> + </ButtonPrimary> - <Positions> - <AutoColumn gap="12px"> + <AutoColumn gap="12px" style={{ width: '100%' }}> <RowBetween padding={'0 8px'}> <Text color={theme.text1} fontWeight={500}> Your Liquidity @@ -100,7 +75,7 @@ export default function Pool({ history }: RouteComponentProps) { Connect to a wallet to view your liquidity. </TYPE.body> </LightCard> - ) : V2IsLoading ? ( + ) : v2IsLoading ? ( <LightCard padding="40px"> <TYPE.body color={theme.text3} textAlign="center"> <Dots>Loading</Dots> @@ -125,16 +100,14 @@ export default function Pool({ history }: RouteComponentProps) { </Text> </div> </AutoColumn> - <FixedBottom> - <ColumnCenter> - <ButtonSecondary width="136px" padding="8px" borderRadius="10px" onClick={() => history.push('/create')}> - + Create Pool - </ButtonSecondary> - </ColumnCenter> - </FixedBottom> - </Positions> - <PairSearchModal isOpen={showPoolSearch} onDismiss={handleSearchDismiss} /> - </AutoColumn> - </AppBody> + </AutoColumn> + </AppBody> + + <div style={{ display: 'flex', alignItems: 'center', marginTop: '1.5rem' }}> + <ButtonSecondary as={Link} style={{ width: 'initial' }} padding="8px" borderRadius="10px" to="/migrate/v1"> + Migrate V1 Liquidity + </ButtonSecondary> + </div> + </> ) } diff --git a/src/pages/Pool/tsconfig.json b/src/pages/Pool/tsconfig.json new file mode 100644 index 0000000000..638227fff6 --- /dev/null +++ b/src/pages/Pool/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.strict.json", + "include": ["**/*"] +} \ No newline at end of file diff --git a/src/pages/PoolFinder/index.tsx b/src/pages/PoolFinder/index.tsx index 79093fe599..270c03d9bf 100644 --- a/src/pages/PoolFinder/index.tsx +++ b/src/pages/PoolFinder/index.tsx @@ -6,7 +6,7 @@ import { ButtonDropdownLight } from '../../components/Button' import { LightCard } from '../../components/Card' import { AutoColumn, ColumnCenter } from '../../components/Column' import { FindPoolTabs } from '../../components/NavigationTabs' -import PositionCard from '../../components/PositionCard' +import { MinimalPositionCard } from '../../components/PositionCard' import Row from '../../components/Row' import TokenSearchModal from '../../components/SearchModal/TokenSearchModal' import TokenLogo from '../../components/TokenLogo' @@ -29,12 +29,12 @@ export default function PoolFinder() { const [showSearch, setShowSearch] = useState<boolean>(false) const [activeField, setActiveField] = useState<number>(Fields.TOKEN1) - const [token0Address, setToken0Address] = useState<string>(WETH[chainId].address) + const [token0Address, setToken0Address] = useState<string>(chainId ? WETH[chainId].address : '') const [token1Address, setToken1Address] = useState<string>() - const token0: Token = useToken(token0Address) - const token1: Token = useToken(token1Address) + const token0: Token | null | undefined = useToken(token0Address) + const token1: Token | null | undefined = useToken(token1Address) - const pair: Pair = usePair(token0, token1) + const pair: Pair | null | undefined = usePair(token0 ?? undefined, token1 ?? undefined) const addPair = usePairAdder() useEffect(() => { if (pair) { @@ -46,7 +46,7 @@ export default function PoolFinder() { pair === null || (!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0))) - const position: TokenAmount = useTokenBalanceTreatingWETHasETH(account, pair?.liquidityToken) + const position: TokenAmount | undefined = useTokenBalanceTreatingWETHasETH(account ?? undefined, pair?.liquidityToken) const poolImported: boolean = !!position && JSBI.greaterThan(position.raw, JSBI.BigInt(0)) const handleTokenSelect = useCallback( @@ -120,13 +120,13 @@ export default function PoolFinder() { {position ? ( poolImported ? ( - <PositionCard pair={pair} minimal={true} border="1px solid #CED0D9" /> + <MinimalPositionCard pair={pair} border="1px solid #CED0D9" /> ) : ( <LightCard padding="45px 10px"> <AutoColumn gap="sm" justify="center"> <Text textAlign="center">You don’t have liquidity in this pool yet.</Text> - <StyledInternalLink to={`/add/${token0.address}-${token1.address}`}> - <Text textAlign="center">Add liquidity?</Text> + <StyledInternalLink to={`/add/${token0?.address}/${token1?.address}`}> + <Text textAlign="center">Add liquidity.</Text> </StyledInternalLink> </AutoColumn> </LightCard> @@ -135,7 +135,7 @@ export default function PoolFinder() { <LightCard padding="45px 10px"> <AutoColumn gap="sm" justify="center"> <Text textAlign="center">No pool found.</Text> - <StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Create pool?</StyledInternalLink> + <StyledInternalLink to={`/add/${token0Address}/${token1Address}`}>Create pool?</StyledInternalLink> </AutoColumn> </LightCard> ) : ( @@ -151,6 +151,7 @@ export default function PoolFinder() { isOpen={showSearch} onTokenSelect={handleTokenSelect} onDismiss={handleSearchDismiss} + showCommonBases hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address} /> </AppBody> diff --git a/src/pages/PoolFinder/tsconfig.json b/src/pages/PoolFinder/tsconfig.json new file mode 100644 index 0000000000..638227fff6 --- /dev/null +++ b/src/pages/PoolFinder/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.strict.json", + "include": ["**/*"] +} \ No newline at end of file diff --git a/src/pages/RemoveLiquidity/index.tsx b/src/pages/RemoveLiquidity/index.tsx index 59467aa914..c007246f6f 100644 --- a/src/pages/RemoveLiquidity/index.tsx +++ b/src/pages/RemoveLiquidity/index.tsx @@ -14,7 +14,7 @@ import ConfirmationModal from '../../components/ConfirmationModal' import CurrencyInputPanel from '../../components/CurrencyInputPanel' import DoubleLogo from '../../components/DoubleLogo' import { AddRemoveTabs } from '../../components/NavigationTabs' -import PositionCard from '../../components/PositionCard' +import { MinimalPositionCard } from '../../components/PositionCard' import Row, { RowBetween, RowFixed } from '../../components/Row' import Slider from '../../components/Slider' @@ -595,7 +595,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro {pair ? ( <AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}> - <PositionCard pair={pair} minimal={true} /> + <MinimalPositionCard pair={pair} /> </AutoColumn> ) : null} </> diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index f83d88054f..2b6c53eab4 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -1,4 +1,4 @@ -import { JSBI, TokenAmount, WETH } from '@uniswap/sdk' +import { JSBI, TokenAmount } from '@uniswap/sdk' import React, { useContext, useState, useEffect, useCallback } from 'react' import { ArrowDown } from 'react-feather' import ReactGA from 'react-ga' @@ -27,7 +27,7 @@ import { useSwapCallback } from '../../hooks/useSwapCallback' import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks' import { useExpertModeManager, useUserSlippageTolerance, useUserDeadline } from '../../state/user/hooks' -import { INITIAL_ALLOWED_SLIPPAGE, MIN_ETH, BETTER_TRADE_LINK_THRESHOLD } from '../../constants' +import { INITIAL_ALLOWED_SLIPPAGE, BETTER_TRADE_LINK_THRESHOLD } from '../../constants' import { getTradeVersion, isTradeBetter } from '../../data/V1' import useToggledVersion, { Version } from '../../hooks/useToggledVersion' import { Field } from '../../state/swap/actions' @@ -38,6 +38,7 @@ import { useSwapState } from '../../state/swap/hooks' import { CursorPointer, LinkStyledButton, TYPE } from '../../theme' +import { maxAmountSpend } from '../../utils/maxAmountSpend' import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices' import AppBody from '../AppBody' import { ClickableText } from '../Pool/styleds' @@ -45,7 +46,7 @@ import { ClickableText } from '../Pool/styleds' export default function Swap() { useDefaultsFromURLSearch() - const { chainId, account } = useActiveWeb3React() + const { account } = useActiveWeb3React() const theme = useContext(ThemeContext) // toggle wallet when disconnected @@ -128,21 +129,7 @@ export default function Swap() { } }, [approval, approvalSubmitted]) - let maxAmountInput: TokenAmount | undefined - { - const inputToken = tokens[Field.INPUT] - maxAmountInput = - inputToken && - chainId && - WETH[chainId] && - tokenBalances[Field.INPUT]?.greaterThan( - new TokenAmount(inputToken, inputToken.equals(WETH[chainId]) ? MIN_ETH : '0') - ) - ? inputToken.equals(WETH[chainId]) - ? tokenBalances[Field.INPUT]?.subtract(new TokenAmount(WETH[chainId], MIN_ETH)) - : tokenBalances[Field.INPUT] - : undefined - } + const maxAmountInput: TokenAmount | undefined = maxAmountSpend(tokenBalances[Field.INPUT]) const atMaxAmountInput = Boolean(maxAmountInput && parsedAmounts[Field.INPUT]?.equalTo(maxAmountInput)) const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage) diff --git a/src/state/burn/actions.ts b/src/state/burn/actions.ts index 158973e3a3..b0a98f0737 100644 --- a/src/state/burn/actions.ts +++ b/src/state/burn/actions.ts @@ -1,4 +1,5 @@ import { createAction } from '@reduxjs/toolkit' +import { ChainId } from '@uniswap/sdk' export enum Field { LIQUIDITY_PERCENT = 'LIQUIDITY_PERCENT', @@ -8,3 +9,7 @@ export enum Field { } export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInputBurn') +export const setBurnDefaultsFromURLMatchParams = createAction<{ + chainId: ChainId + params: { tokens: string } +}>('setBurnDefaultsFromURLMatchParams') diff --git a/src/state/burn/hooks.ts b/src/state/burn/hooks.ts index 388b0d61aa..b7849e6ffa 100644 --- a/src/state/burn/hooks.ts +++ b/src/state/burn/hooks.ts @@ -3,8 +3,7 @@ import { useDispatch, useSelector } from 'react-redux' import { useActiveWeb3React } from '../../hooks' import { AppDispatch, AppState } from '../index' -import { Field, typeInput } from './actions' -import { setDefaultsFromURLMatchParams } from '../mint/actions' +import { Field, setBurnDefaultsFromURLMatchParams, typeInput } from './actions' import { useToken } from '../../hooks/Tokens' import { Token, Pair, TokenAmount, Percent, JSBI, Route } from '@uniswap/sdk' import { usePair } from '../../data/Reserves' @@ -177,11 +176,11 @@ export function useBurnActionHandlers(): { } // updates the burn state to use the appropriate tokens, given the route -export function useDefaultsFromURLMatchParams(params: { [k: string]: string }) { +export function useDefaultsFromURLMatchParams(params: { tokens: string }) { const { chainId } = useActiveWeb3React() const dispatch = useDispatch<AppDispatch>() useEffect(() => { if (!chainId) return - dispatch(setDefaultsFromURLMatchParams({ chainId, params })) + dispatch(setBurnDefaultsFromURLMatchParams({ chainId, params })) }, [dispatch, chainId, params]) } diff --git a/src/state/burn/reducer.ts b/src/state/burn/reducer.ts index dbdb59ffa7..50bece04f0 100644 --- a/src/state/burn/reducer.ts +++ b/src/state/burn/reducer.ts @@ -1,10 +1,10 @@ import { createReducer } from '@reduxjs/toolkit' +import { ChainId, WETH } from '@uniswap/sdk' +import { isAddress } from '../../utils' -import { Field, typeInput } from './actions' -import { setDefaultsFromURLMatchParams } from '../mint/actions' -import { parseTokens } from '../mint/reducer' +import { Field, setBurnDefaultsFromURLMatchParams, typeInput } from './actions' -export interface MintState { +export interface BurnState { readonly independentField: Field readonly typedValue: string readonly [Field.TOKEN_A]: { @@ -15,7 +15,7 @@ export interface MintState { } } -const initialState: MintState = { +const initialState: BurnState = { independentField: Field.LIQUIDITY_PERCENT, typedValue: '0', [Field.TOKEN_A]: { @@ -26,9 +26,27 @@ const initialState: MintState = { } } -export default createReducer<MintState>(initialState, builder => +export function parseTokens(chainId: ChainId, tokens: string): string[] { + return ( + tokens + // split by '-' + .split('-') + // map to addresses + .map((token): string => + isAddress(token) ? token : token.toLowerCase() === 'ETH'.toLowerCase() ? WETH[chainId]?.address ?? '' : '' + ) + //remove duplicates + .filter((token, i, array) => array.indexOf(token) === i) + // add two empty elements for cases where the array is length 0 + .concat(['', '']) + // only consider the first 2 elements + .slice(0, 2) + ) +} + +export default createReducer<BurnState>(initialState, builder => builder - .addCase(setDefaultsFromURLMatchParams, (state, { payload: { chainId, params } }) => { + .addCase(setBurnDefaultsFromURLMatchParams, (state, { payload: { chainId, params } }) => { const tokens = parseTokens(chainId, params?.tokens ?? '') return { independentField: Field.LIQUIDITY_PERCENT, diff --git a/src/state/mint/actions.ts b/src/state/mint/actions.ts index 4800b6e857..867e2aef45 100644 --- a/src/state/mint/actions.ts +++ b/src/state/mint/actions.ts @@ -1,13 +1,9 @@ import { createAction } from '@reduxjs/toolkit' -import { RouteComponentProps } from 'react-router-dom' export enum Field { TOKEN_A = 'TOKEN_A', TOKEN_B = 'TOKEN_B' } -export const setDefaultsFromURLMatchParams = createAction<{ - chainId: number - params: RouteComponentProps<{ [k: string]: string }>['match']['params'] -}>('setDefaultsFromMatch') export const typeInput = createAction<{ field: Field; typedValue: string; noLiquidity: boolean }>('typeInputMint') +export const resetMintState = createAction<void>('resetMintState') diff --git a/src/state/mint/hooks.ts b/src/state/mint/hooks.ts index 3989fb26cd..6a57b67100 100644 --- a/src/state/mint/hooks.ts +++ b/src/state/mint/hooks.ts @@ -1,11 +1,10 @@ -import { useEffect, useCallback, useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { useDispatch, useSelector } from 'react-redux' import { Token, TokenAmount, Route, JSBI, Price, Percent, Pair } from '@uniswap/sdk' import { useActiveWeb3React } from '../../hooks' import { AppDispatch, AppState } from '../index' -import { setDefaultsFromURLMatchParams, Field, typeInput } from './actions' -import { useToken } from '../../hooks/Tokens' +import { Field, typeInput } from './actions' import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks' import { usePair } from '../../data/Reserves' import { useTotalSupply } from '../../data/TotalSupply' @@ -17,7 +16,10 @@ export function useMintState(): AppState['mint'] { return useSelector<AppState, AppState['mint']>(state => state.mint) } -export function useDerivedMintInfo(): { +export function useDerivedMintInfo( + tokenA: Token | undefined, + tokenB: Token | undefined +): { dependentField: Field tokens: { [field in Field]?: Token } pair?: Pair | null @@ -31,19 +33,11 @@ export function useDerivedMintInfo(): { } { const { account } = useActiveWeb3React() - const { - independentField, - typedValue, - otherTypedValue, - [Field.TOKEN_A]: { address: tokenAAddress }, - [Field.TOKEN_B]: { address: tokenBAddress } - } = useMintState() + const { independentField, typedValue, otherTypedValue } = useMintState() const dependentField = independentField === Field.TOKEN_A ? Field.TOKEN_B : Field.TOKEN_A // tokens - const tokenA = useToken(tokenAAddress) - const tokenB = useToken(tokenBAddress) const tokens: { [field in Field]?: Token } = useMemo( () => ({ [Field.TOKEN_A]: tokenA, @@ -172,16 +166,16 @@ export function useDerivedMintInfo(): { } } -export function useMintActionHandlers(): { +export function useMintActionHandlers( + noLiquidity: boolean | undefined +): { onUserInput: (field: Field, typedValue: string) => void } { const dispatch = useDispatch<AppDispatch>() - const { noLiquidity } = useDerivedMintInfo() - const onUserInput = useCallback( (field: Field, typedValue: string) => { - dispatch(typeInput({ field, typedValue, noLiquidity: noLiquidity === true ? true : false })) + dispatch(typeInput({ field, typedValue, noLiquidity: noLiquidity === true })) }, [dispatch, noLiquidity] ) @@ -190,13 +184,3 @@ export function useMintActionHandlers(): { onUserInput } } - -// updates the mint state to use the appropriate tokens, given the route -export function useDefaultsFromURLMatchParams(params: { [k: string]: string }) { - const { chainId } = useActiveWeb3React() - const dispatch = useDispatch<AppDispatch>() - useEffect(() => { - if (!chainId) return - dispatch(setDefaultsFromURLMatchParams({ chainId, params })) - }, [dispatch, chainId, params]) -} diff --git a/src/state/mint/reducer.test.ts b/src/state/mint/reducer.test.ts index e1018caace..5fdbfebb72 100644 --- a/src/state/mint/reducer.test.ts +++ b/src/state/mint/reducer.test.ts @@ -1,7 +1,6 @@ -import { ChainId, WETH } from '@uniswap/sdk' import { createStore, Store } from 'redux' -import { Field, setDefaultsFromURLMatchParams } from './actions' +import { Field, typeInput } from './actions' import reducer, { MintState } from './reducer' describe('mint reducer', () => { @@ -11,30 +10,19 @@ describe('mint reducer', () => { store = createStore(reducer, { independentField: Field.TOKEN_A, typedValue: '', - otherTypedValue: '', - [Field.TOKEN_A]: { address: '' }, - [Field.TOKEN_B]: { address: '' } + otherTypedValue: '' }) }) - describe('setDefaultsFromURLMatchParams', () => { - test('ETH to DAI', () => { - store.dispatch( - setDefaultsFromURLMatchParams({ - chainId: ChainId.MAINNET, - params: { - tokens: 'ETH-0x6b175474e89094c44da98b954eedeac495271d0f' - } - }) - ) - - expect(store.getState()).toEqual({ - independentField: Field.TOKEN_A, - typedValue: '', - otherTypedValue: '', - [Field.TOKEN_A]: { address: WETH[ChainId.MAINNET].address }, - [Field.TOKEN_B]: { address: '0x6b175474e89094c44da98b954eedeac495271d0f' } - }) + describe('typeInput', () => { + it('sets typed value', () => { + store.dispatch(typeInput({ field: Field.TOKEN_A, typedValue: '1.0', noLiquidity: false })) + expect(store.getState()).toEqual({ independentField: Field.TOKEN_A, typedValue: '1.0', otherTypedValue: '' }) + }) + it('clears other value', () => { + store.dispatch(typeInput({ field: Field.TOKEN_A, typedValue: '1.0', noLiquidity: false })) + store.dispatch(typeInput({ field: Field.TOKEN_B, typedValue: '1.0', noLiquidity: false })) + expect(store.getState()).toEqual({ independentField: Field.TOKEN_B, typedValue: '1.0', otherTypedValue: '' }) }) }) }) diff --git a/src/state/mint/reducer.ts b/src/state/mint/reducer.ts index 425f230a34..ae6174e308 100644 --- a/src/state/mint/reducer.ts +++ b/src/state/mint/reducer.ts @@ -1,71 +1,21 @@ import { createReducer } from '@reduxjs/toolkit' -import { ChainId, WETH } from '@uniswap/sdk' - -import { isAddress } from '../../utils' -import { Field, setDefaultsFromURLMatchParams, typeInput } from './actions' +import { Field, resetMintState, typeInput } from './actions' export interface MintState { readonly independentField: Field readonly typedValue: string readonly otherTypedValue: string // for the case when there's no liquidity - readonly [Field.TOKEN_A]: { - readonly address: string - } - readonly [Field.TOKEN_B]: { - readonly address: string - } } const initialState: MintState = { independentField: Field.TOKEN_A, typedValue: '', - otherTypedValue: '', - [Field.TOKEN_A]: { - address: '' - }, - [Field.TOKEN_B]: { - address: '' - } -} - -export function parseTokens(chainId: number, tokens: string): string[] { - return ( - tokens - // split by '-' - .split('-') - // map to addresses - .map((token): string => - isAddress(token) - ? token - : token.toLowerCase() === 'ETH'.toLowerCase() - ? WETH[chainId as ChainId]?.address ?? '' - : '' - ) - //remove duplicates - .filter((token, i, array) => array.indexOf(token) === i) - // add two empty elements for cases where the array is length 0 - .concat(['', '']) - // only consider the first 2 elements - .slice(0, 2) - ) + otherTypedValue: '' } export default createReducer<MintState>(initialState, builder => builder - .addCase(setDefaultsFromURLMatchParams, (state, { payload: { chainId, params } }) => { - const tokens = parseTokens(chainId, params?.tokens ?? '') - return { - independentField: Field.TOKEN_A, - typedValue: '', - otherTypedValue: '', - [Field.TOKEN_A]: { - address: tokens[0] - }, - [Field.TOKEN_B]: { - address: tokens[1] - } - } - }) + .addCase(resetMintState, () => initialState) .addCase(typeInput, (state, { payload: { field, typedValue, noLiquidity } }) => { if (noLiquidity) { // they're typing into the field they've last typed in diff --git a/src/state/multicall/reducer.test.ts b/src/state/multicall/reducer.test.ts index cfd2c9f76d..88a6616b7d 100644 --- a/src/state/multicall/reducer.test.ts +++ b/src/state/multicall/reducer.test.ts @@ -192,6 +192,54 @@ describe('multicall reducer', () => { } }) }) + + it('updates state to fetching even if already fetching older block', () => { + store.dispatch( + fetchingMulticallResults({ + chainId: 1, + fetchingBlockNumber: 2, + calls: [{ address: DAI_ADDRESS, callData: '0x0' }] + }) + ) + store.dispatch( + fetchingMulticallResults({ + chainId: 1, + fetchingBlockNumber: 3, + calls: [{ address: DAI_ADDRESS, callData: '0x0' }] + }) + ) + expect(store.getState()).toEqual({ + callResults: { + [1]: { + [`${DAI_ADDRESS}-0x0`]: { fetchingBlockNumber: 3 } + } + } + }) + }) + + it('does not do update if fetching newer block', () => { + store.dispatch( + fetchingMulticallResults({ + chainId: 1, + fetchingBlockNumber: 2, + calls: [{ address: DAI_ADDRESS, callData: '0x0' }] + }) + ) + store.dispatch( + fetchingMulticallResults({ + chainId: 1, + fetchingBlockNumber: 1, + calls: [{ address: DAI_ADDRESS, callData: '0x0' }] + }) + ) + expect(store.getState()).toEqual({ + callResults: { + [1]: { + [`${DAI_ADDRESS}-0x0`]: { fetchingBlockNumber: 2 } + } + } + }) + }) }) describe('errorFetchingMulticallResults', () => { diff --git a/src/state/multicall/reducer.ts b/src/state/multicall/reducer.ts index 5efb426fd6..09d4639204 100644 --- a/src/state/multicall/reducer.ts +++ b/src/state/multicall/reducer.ts @@ -79,7 +79,7 @@ export default createReducer(initialState, builder => fetchingBlockNumber } } else { - if (current.fetchingBlockNumber ?? 0 >= fetchingBlockNumber) return + if ((current.fetchingBlockNumber ?? 0) >= fetchingBlockNumber) return state.callResults[chainId][callKey].fetchingBlockNumber = fetchingBlockNumber } }) diff --git a/src/state/multicall/updater.tsx b/src/state/multicall/updater.tsx index 2977d3474c..f24d38c04a 100644 --- a/src/state/multicall/updater.tsx +++ b/src/state/multicall/updater.tsx @@ -77,8 +77,8 @@ export function outdatedListeningKeys( // already fetching it for a recent enough block, don't refetch it if (data.fetchingBlockNumber && data.fetchingBlockNumber >= minDataBlockNumber) return false - // if data is newer than minDataBlockNumber, don't fetch it - return !(data.blockNumber && data.blockNumber >= minDataBlockNumber) + // if data is older than minDataBlockNumber, fetch it + return !data.blockNumber || data.blockNumber < minDataBlockNumber }) } diff --git a/src/theme/components.tsx b/src/theme/components.tsx index bc34bcffab..7f64096095 100644 --- a/src/theme/components.tsx +++ b/src/theme/components.tsx @@ -3,7 +3,7 @@ import ReactGA from 'react-ga' import { Link } from 'react-router-dom' import styled, { keyframes } from 'styled-components' import { darken } from 'polished' -import { X } from 'react-feather' +import { ArrowLeft, X } from 'react-feather' export const Button = styled.button.attrs<{ warning: boolean }, { backgroundColor: string }>(({ warning, theme }) => ({ backgroundColor: warning ? theme.red1 : theme.primary1 @@ -153,3 +153,14 @@ export const CursorPointer = styled.div` cursor: pointer; } ` + +const BackArrowLink = styled(StyledInternalLink)` + color: ${({ theme }) => theme.text1}; +` +export function BackArrow({ to }: { to: string }) { + return ( + <BackArrowLink to={to}> + <ArrowLeft /> + </BackArrowLink> + ) +} diff --git a/src/utils/maxAmountSpend.ts b/src/utils/maxAmountSpend.ts new file mode 100644 index 0000000000..83e64bcca5 --- /dev/null +++ b/src/utils/maxAmountSpend.ts @@ -0,0 +1,18 @@ +import { JSBI, TokenAmount, WETH } from '@uniswap/sdk' +import { MIN_ETH } from '../constants' + +/** + * Given some token amount, return the max that can be spent of it + * @param tokenAmount to return max of + */ +export function maxAmountSpend(tokenAmount?: TokenAmount): TokenAmount | undefined { + if (!tokenAmount) return + if (tokenAmount.token.equals(WETH[tokenAmount.token.chainId])) { + if (JSBI.greaterThan(tokenAmount.raw, MIN_ETH)) { + return new TokenAmount(tokenAmount.token, JSBI.subtract(tokenAmount.raw, MIN_ETH)) + } else { + return new TokenAmount(tokenAmount.token, JSBI.BigInt(0)) + } + } + return tokenAmount +} diff --git a/yarn.lock b/yarn.lock index b4fad3885e..4e4bf3db0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3358,11 +3358,16 @@ aproba@^1.1.1: resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -arch@2.1.1, arch@^2.1.0: +arch@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e" integrity sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg== +arch@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf" + integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ== + arg@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/arg/-/arg-2.0.0.tgz#c06e7ff69ab05b3a4a03ebe0407fac4cba657545" @@ -13158,11 +13163,16 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== +safe-buffer@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-event-emitter@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/safe-event-emitter/-/safe-event-emitter-1.0.1.tgz#5b692ef22329ed8f69fdce607e50ca734f6f20af" @@ -13330,10 +13340,10 @@ serialize-javascript@^2.1.2: resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== -serve-handler@6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.2.tgz#f05b0421a313fff2d257838cba00cbcc512cd2b6" - integrity sha512-RFh49wX7zJmmOVDcIjiDSJnMH+ItQEvyuYLYuDBVoA/xmQSCuj+uRmk1cmBB5QQlI3qOiWKp6p4DUGY+Z5AB2A== +serve-handler@6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.3.tgz#1bf8c5ae138712af55c758477533b9117f6435e8" + integrity sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w== dependencies: bytes "3.0.0" content-disposition "0.5.2" @@ -13368,9 +13378,9 @@ serve-static@1.14.1: send "0.17.1" serve@^11.3.0: - version "11.3.0" - resolved "https://registry.yarnpkg.com/serve/-/serve-11.3.0.tgz#1d342e13e310501ecf17b6602f1f35da640d6448" - integrity sha512-AU0g50Q1y5EVFX56bl0YX5OtVjUX1N737/Htj93dQGKuHiuLvVB45PD8Muar70W6Kpdlz8aNJfoUqTyAq9EE/A== + version "11.3.2" + resolved "https://registry.yarnpkg.com/serve/-/serve-11.3.2.tgz#b905e980616feecd170e51c8f979a7b2374098f5" + integrity sha512-yKWQfI3xbj/f7X1lTBg91fXBP0FqjJ4TEi+ilES5yzH0iKJpN5LjNb1YzIfQg9Rqn4ECUS2SOf2+Kmepogoa5w== dependencies: "@zeit/schemas" "2.6.0" ajv "6.5.3" @@ -13379,7 +13389,7 @@ serve@^11.3.0: chalk "2.4.1" clipboardy "1.2.3" compression "1.7.3" - serve-handler "6.1.2" + serve-handler "6.1.3" update-check "1.5.2" set-blocking@^2.0.0: