improvement(pool): simplify pool flow, remove pool search modal (#941)
* deleting some code first * strict, some refactoring * denser common bases * more add liquidity refactoring * add liquidity paths working * show common bases in the token selects * fix the ability to select duplicate tokens * useless rename * try to handle alllll the duplicate token edge cases * think i got them all lol * remove common bases header * Revert "remove common bases header" This reverts commit 6ac4565d * fix and add integration tests * make gap between rows smaller * get integration tests actually running again * try another format of the command, upgrade serve * frozen lockfile on install * try the cypress github action * install cypress in ci * remove redundant ignore-scripts command * use a specific github commit for the pinata action * fix a bug in the multicall reducer, improve token list rendering performance * improve the enter key on the token search modal * stop using history.push * fix linting errors * position card cleanup before updating to match mock
This commit is contained in:
parent
eeef306bdd
commit
344b4340ae
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@ -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'
|
||||
|
8
.github/workflows/tests.yaml
vendored
8
.github/workflows/tests.yaml
vendored
@ -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
|
||||
|
||||
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</InputPanel>
|
||||
|
@ -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 (
|
||||
<TokenWrapper sizeraw={size} margin={margin}>
|
||||
<HigherLogo address={a0} size={size.toString() + 'px'} />
|
||||
{a0 && <HigherLogo address={a0} size={size.toString() + 'px'} />}
|
||||
{a1 && <CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />}
|
||||
</TokenWrapper>
|
||||
)
|
||||
|
@ -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 (
|
||||
<HeaderFrame>
|
||||
<MigrateBanner>
|
||||
Uniswap V2 is live! Read the
|
||||
<ExternalLink href="https://uniswap.org/blog/launch-uniswap-v2/">
|
||||
<b>blog post ↗</b>
|
||||
</ExternalLink>
|
||||
or
|
||||
<StyledInternalLink to="/migrate/v1">
|
||||
<b>migrate your liquidity ↗</b>
|
||||
</StyledInternalLink>
|
||||
.
|
||||
</MigrateBanner>
|
||||
<RowBetween style={{ alignItems: 'flex-start' }} padding="1rem 1rem 0 1rem">
|
||||
<HeaderElement>
|
||||
<Title>
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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)
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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 && (
|
||||
|
@ -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 {
|
||||
|
63
src/pages/AddLiquidity/ConfirmAddModalBottom.tsx
Normal file
63
src/pages/AddLiquidity/ConfirmAddModalBottom.tsx
Normal file
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
52
src/pages/AddLiquidity/PoolPriceBar.tsx
Normal file
52
src/pages/AddLiquidity/PoolPriceBar.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
13
src/pages/AddLiquidity/currencyId.ts
Normal file
13
src/pages/AddLiquidity/currencyId.ts
Normal file
@ -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')
|
||||
}
|
||||
}
|
@ -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}
|
||||
</>
|
||||
|
42
src/pages/AddLiquidity/redirects.tsx
Normal file
42
src/pages/AddLiquidity/redirects.tsx
Normal file
@ -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} />
|
||||
}
|
7
src/pages/AddLiquidity/tsconfig.json
Normal file
7
src/pages/AddLiquidity/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.strict.json",
|
||||
"include": [
|
||||
"**/*",
|
||||
"../../../node_modules/eslint-plugin-react/lib/types.d.ts"
|
||||
]
|
||||
}
|
@ -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} />
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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." />
|
||||
|
@ -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." />
|
||||
|
@ -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." />
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
4
src/pages/Pool/tsconfig.json
Normal file
4
src/pages/Pool/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
@ -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>
|
||||
|
4
src/pages/PoolFinder/tsconfig.json
Normal file
4
src/pages/PoolFinder/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
@ -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}
|
||||
</>
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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])
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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')
|
||||
|
@ -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])
|
||||
}
|
||||
|
@ -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: '' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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', () => {
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
18
src/utils/maxAmountSpend.ts
Normal file
18
src/utils/maxAmountSpend.ts
Normal file
@ -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
|
||||
}
|
30
yarn.lock
30
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:
|
||||
|
Loading…
Reference in New Issue
Block a user