Commit e2093670 authored by Noah Zinsmeister's avatar Noah Zinsmeister Committed by GitHub

improvement(migration): improve v1 migration flow (#885)

* first stab at improving v1 migration flow

* lint errors

* improve UI

* fix loading indicator

* switch back to dedicated migration UI

* address comments

* make migrate consistent with new token behavior

* hooks -> utils
parent 2fda2c8c
...@@ -51,10 +51,10 @@ export const ButtonPrimary = styled(Base)` ...@@ -51,10 +51,10 @@ export const ButtonPrimary = styled(Base)`
} }
&:disabled { &:disabled {
background-color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? theme.primary1 : theme.bg3)}; background-color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? theme.primary1 : theme.bg3)};
color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)} color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)};
cursor: auto; cursor: auto;
box-shadow: none; box-shadow: none;
border: 1px solid transparent;; border: 1px solid transparent;
outline: none; outline: none;
} }
` `
...@@ -197,7 +197,6 @@ export const ButtonEmpty = styled(Base)` ...@@ -197,7 +197,6 @@ export const ButtonEmpty = styled(Base)`
export const ButtonWhite = styled(Base)` export const ButtonWhite = styled(Base)`
border: 1px solid #edeef2; border: 1px solid #edeef2;
background-color: ${({ theme }) => theme.bg1}; background-color: ${({ theme }) => theme.bg1};
};
color: black; color: black;
&:focus { &:focus {
...@@ -264,7 +263,7 @@ export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProp ...@@ -264,7 +263,7 @@ export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProp
} }
} }
export function ButtonDropwdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) { export function ButtonDropdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
return ( return (
<ButtonPrimary {...rest} disabled={disabled}> <ButtonPrimary {...rest} disabled={disabled}>
<RowBetween> <RowBetween>
...@@ -275,7 +274,7 @@ export function ButtonDropwdown({ disabled = false, children, ...rest }: { disab ...@@ -275,7 +274,7 @@ export function ButtonDropwdown({ disabled = false, children, ...rest }: { disab
) )
} }
export function ButtonDropwdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) { export function ButtonDropdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
return ( return (
<ButtonOutlined {...rest} disabled={disabled}> <ButtonOutlined {...rest} disabled={disabled}>
<RowBetween> <RowBetween>
......
...@@ -13,7 +13,7 @@ interface DoubleTokenLogoProps { ...@@ -13,7 +13,7 @@ interface DoubleTokenLogoProps {
margin?: boolean margin?: boolean
size?: number size?: number
a0: string a0: string
a1: string a1?: string
} }
const HigherLogo = styled(TokenLogo)` const HigherLogo = styled(TokenLogo)`
...@@ -28,7 +28,7 @@ export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: D ...@@ -28,7 +28,7 @@ export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: D
return ( return (
<TokenWrapper sizeraw={size} margin={margin}> <TokenWrapper sizeraw={size} margin={margin}>
<HigherLogo address={a0} size={size.toString() + 'px'} /> <HigherLogo address={a0} size={size.toString() + 'px'} />
<CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} /> {a1 && <CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />}
</TokenWrapper> </TokenWrapper>
) )
} }
...@@ -18,7 +18,13 @@ const VersionLabel = styled.span<{ enabled: boolean }>` ...@@ -18,7 +18,13 @@ const VersionLabel = styled.span<{ enabled: boolean }>`
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.primary3)}; color: ${({ theme, enabled }) => (enabled ? theme.white : theme.primary3)};
} }
` `
const VersionToggle = styled(Link)<{ enabled: boolean }>`
interface VersionToggleProps extends React.ComponentProps<typeof Link> {
enabled: boolean
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const VersionToggle = styled(({ enabled, ...rest }: VersionToggleProps) => <Link {...rest} />)<VersionToggleProps>`
border-radius: 16px; border-radius: 16px;
opacity: ${({ enabled }) => (enabled ? 1 : 0.5)}; opacity: ${({ enabled }) => (enabled ? 1 : 0.5)};
cursor: ${({ enabled }) => (enabled ? 'pointer' : 'default')}; cursor: ${({ enabled }) => (enabled ? 'pointer' : 'default')};
......
import React, { useContext } from 'react'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
import { Text } from 'rebass'
import { AutoColumn } from '../Column'
import { ButtonSecondary } from '../Button'
import { RowBetween, RowFixed } from '../Row'
import { FixedHeightRow, HoverCard } from './index'
import DoubleTokenLogo from '../DoubleLogo'
import { useActiveWeb3React } from '../../hooks'
import { ThemeContext } from 'styled-components'
interface PositionCardProps extends RouteComponentProps<{}> {
token: Token
V1LiquidityBalance: TokenAmount
}
function V1PositionCard({ token, V1LiquidityBalance, history }: PositionCardProps) {
const theme = useContext(ThemeContext)
const { chainId } = useActiveWeb3React()
return (
<HoverCard>
<AutoColumn gap="12px">
<FixedHeightRow>
<RowFixed>
<DoubleTokenLogo a0={token.address} margin={true} size={20} />
<Text fontWeight={500} fontSize={20} style={{ marginLeft: '' }}>
{`${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
</Text>
<Text
fontSize={12}
fontWeight={500}
ml="0.5rem"
px="0.75rem"
py="0.25rem"
style={{ borderRadius: '1rem' }}
backgroundColor={theme.yellow1}
color={'black'}
>
V1
</Text>
</RowFixed>
</FixedHeightRow>
<AutoColumn gap="8px">
<RowBetween marginTop="10px">
<ButtonSecondary
width="68%"
onClick={() => {
history.push(`/migrate/v1/${V1LiquidityBalance.token.address}`)
}}
>
Migrate
</ButtonSecondary>
<ButtonSecondary
style={{ backgroundColor: 'transparent' }}
width="28%"
onClick={() => {
history.push(`/remove/v1/${V1LiquidityBalance.token.address}`)
}}
>
Remove
</ButtonSecondary>
</RowBetween>
</AutoColumn>
</AutoColumn>
</HoverCard>
)
}
export default withRouter(V1PositionCard)
...@@ -17,12 +17,13 @@ import { AutoColumn } from '../Column' ...@@ -17,12 +17,13 @@ import { AutoColumn } from '../Column'
import { ChevronDown, ChevronUp } from 'react-feather' import { ChevronDown, ChevronUp } from 'react-feather'
import { ButtonSecondary } from '../Button' import { ButtonSecondary } from '../Button'
import { RowBetween, RowFixed, AutoRow } from '../Row' import { RowBetween, RowFixed, AutoRow } from '../Row'
import { Dots } from '../swap/styleds'
const FixedHeightRow = styled(RowBetween)` export const FixedHeightRow = styled(RowBetween)`
height: 24px; height: 24px;
` `
const HoverCard = styled(Card)` export const HoverCard = styled(Card)`
border: 1px solid ${({ theme }) => theme.bg2}; border: 1px solid ${({ theme }) => theme.bg2};
:hover { :hover {
border: 1px solid ${({ theme }) => darken(0.06, theme.bg2)}; border: 1px solid ${({ theme }) => darken(0.06, theme.bg2)};
...@@ -72,7 +73,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr ...@@ -72,7 +73,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
<FixedHeightRow> <FixedHeightRow>
<RowFixed> <RowFixed>
<Text fontWeight={500} fontSize={16}> <Text fontWeight={500} fontSize={16}>
Your current position Your position
</Text> </Text>
</RowFixed> </RowFixed>
</FixedHeightRow> </FixedHeightRow>
...@@ -96,7 +97,6 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr ...@@ -96,7 +97,6 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
</Text> </Text>
{token0Deposited ? ( {token0Deposited ? (
<RowFixed> <RowFixed>
{!minimal && <TokenLogo address={token0?.address} />}
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}> <Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token0Deposited?.toSignificant(6)} {token0Deposited?.toSignificant(6)}
</Text> </Text>
...@@ -111,7 +111,6 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr ...@@ -111,7 +111,6 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
</Text> </Text>
{token1Deposited ? ( {token1Deposited ? (
<RowFixed> <RowFixed>
{!minimal && <TokenLogo address={token1?.address} />}
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}> <Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token1Deposited?.toSignificant(6)} {token1Deposited?.toSignificant(6)}
</Text> </Text>
...@@ -134,7 +133,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr ...@@ -134,7 +133,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
<RowFixed> <RowFixed>
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} /> <DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
<Text fontWeight={500} fontSize={20}> <Text fontWeight={500} fontSize={20}>
{token0?.symbol}/{token1?.symbol} {!token0 || !token1 ? <Dots>Loading</Dots> : `${token0.symbol}/${token1.symbol}`}
</Text> </Text>
</RowFixed> </RowFixed>
<RowFixed> <RowFixed>
...@@ -158,7 +157,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr ...@@ -158,7 +157,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}> <Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token0Deposited?.toSignificant(6)} {token0Deposited?.toSignificant(6)}
</Text> </Text>
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />} <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />
</RowFixed> </RowFixed>
) : ( ) : (
'-' '-'
...@@ -176,32 +175,28 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr ...@@ -176,32 +175,28 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}> <Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token1Deposited?.toSignificant(6)} {token1Deposited?.toSignificant(6)}
</Text> </Text>
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />} <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />
</RowFixed> </RowFixed>
) : ( ) : (
'-' '-'
)} )}
</FixedHeightRow> </FixedHeightRow>
{!minimal && ( <FixedHeightRow>
<FixedHeightRow> <Text fontSize={16} fontWeight={500}>
<Text fontSize={16} fontWeight={500}> Your pool tokens:
Your pool tokens: </Text>
</Text> <Text fontSize={16} fontWeight={500}>
<Text fontSize={16} fontWeight={500}> {userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'} </Text>
</Text> </FixedHeightRow>
</FixedHeightRow> <FixedHeightRow>
)} <Text fontSize={16} fontWeight={500}>
{!minimal && ( Your pool share:
<FixedHeightRow> </Text>
<Text fontSize={16} fontWeight={500}> <Text fontSize={16} fontWeight={500}>
Your pool share {poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
</Text> </Text>
<Text fontSize={16} fontWeight={500}> </FixedHeightRow>
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
</Text>
</FixedHeightRow>
)}
<AutoRow justify="center" marginTop={'10px'}> <AutoRow justify="center" marginTop={'10px'}>
<ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}> <ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>
......
import { ChainId, JSBI, Token, TokenAmount } from '@uniswap/sdk' import { JSBI, Token, TokenAmount } from '@uniswap/sdk'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FixedSizeList } from 'react-window' import { FixedSizeList } from 'react-window'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { ALL_TOKENS } from '../../constants/tokens'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens' import { useAllTokens } from '../../hooks/Tokens'
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks' import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
...@@ -15,10 +14,7 @@ import { RowFixed } from '../Row' ...@@ -15,10 +14,7 @@ import { RowFixed } from '../Row'
import TokenLogo from '../TokenLogo' import TokenLogo from '../TokenLogo'
import { FadedSpan, GreySpan, MenuItem, ModalInfo } from './styleds' import { FadedSpan, GreySpan, MenuItem, ModalInfo } from './styleds'
import Loader from '../Loader' import Loader from '../Loader'
import { isDefaultToken, isCustomAddedToken } from '../../utils'
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
return Boolean(chainId && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
}
export default function TokenList({ export default function TokenList({
tokens, tokens,
...@@ -61,8 +57,8 @@ export default function TokenList({ ...@@ -61,8 +57,8 @@ export default function TokenList({
const token = tokens[index] const token = tokens[index]
const { address, symbol } = token const { address, symbol } = token
const isDefault = isDefaultToken(address, chainId) const isDefault = isDefaultToken(token)
const customAdded = Boolean(!isDefault && allTokens[address]) const customAdded = isCustomAddedToken(allTokens, token)
const balance = allTokenBalances[address] const balance = allTokenBalances[address]
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw) const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
......
...@@ -3,13 +3,12 @@ import { transparentize } from 'polished' ...@@ -3,13 +3,12 @@ import { transparentize } from 'polished'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { ReactComponent as Close } from '../../assets/images/x.svg' import { ReactComponent as Close } from '../../assets/images/x.svg'
import { ALL_TOKENS } from '../../constants/tokens'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens' import { useAllTokens } from '../../hooks/Tokens'
import { Field } from '../../state/swap/actions' import { Field } from '../../state/swap/actions'
import { useTokenWarningDismissal } from '../../state/user/hooks' import { useTokenWarningDismissal } from '../../state/user/hooks'
import { ExternalLink, TYPE } from '../../theme' import { ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink } from '../../utils' import { getEtherscanLink, isDefaultToken } from '../../utils'
import PropsOfExcluding from '../../utils/props-of-excluding' import PropsOfExcluding from '../../utils/props-of-excluding'
import QuestionHelper from '../QuestionHelper' import QuestionHelper from '../QuestionHelper'
import TokenLogo from '../TokenLogo' import TokenLogo from '../TokenLogo'
...@@ -68,9 +67,8 @@ interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error' ...@@ -68,9 +67,8 @@ interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) { export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const isDefaultToken = Boolean(
token && token.address && chainId && ALL_TOKENS[chainId] && ALL_TOKENS[chainId][token.address] const isDefault = isDefaultToken(token)
)
const tokenSymbol = token?.symbol?.toLowerCase() ?? '' const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
const tokenName = token?.name?.toLowerCase() ?? '' const tokenName = token?.name?.toLowerCase() ?? ''
...@@ -80,7 +78,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro ...@@ -80,7 +78,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
const allTokens = useAllTokens() const allTokens = useAllTokens()
const duplicateNameOrSymbol = useMemo(() => { const duplicateNameOrSymbol = useMemo(() => {
if (isDefaultToken || !token || !chainId) return false if (isDefault || !token || !chainId) return false
return Object.keys(allTokens).some(tokenAddress => { return Object.keys(allTokens).some(tokenAddress => {
const userToken = allTokens[tokenAddress] const userToken = allTokens[tokenAddress]
...@@ -89,9 +87,9 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro ...@@ -89,9 +87,9 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
} }
return userToken.symbol.toLowerCase() === tokenSymbol || userToken.name.toLowerCase() === tokenName return userToken.symbol.toLowerCase() === tokenSymbol || userToken.name.toLowerCase() === tokenName
}) })
}, [isDefaultToken, token, chainId, allTokens, tokenSymbol, tokenName]) }, [isDefault, token, chainId, allTokens, tokenSymbol, tokenName])
if (isDefaultToken || !token || dismissed) return null if (isDefault || !token || dismissed) return null
return ( return (
<Wrapper error={duplicateNameOrSymbol} {...rest}> <Wrapper error={duplicateNameOrSymbol} {...rest}>
......
...@@ -3,29 +3,15 @@ import { useMemo } from 'react' ...@@ -3,29 +3,15 @@ import { useMemo } from 'react'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json' import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { Interface } from '@ethersproject/abi' import { Interface } from '@ethersproject/abi'
import { usePairContract } from '../hooks/useContract' import { useMultipleContractSingleData } from '../state/multicall/hooks'
import { useSingleCallResult, useMultipleContractSingleData } from '../state/multicall/hooks'
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
/* /*
* if loading, return undefined * if loading, return undefined
* if no pair created yet, return null * if no pair created yet, return null
* if pair already created (even if 0 reserves), return pair * if pair already created (even if 0 reserves), return pair
*/ */
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
const pairAddress = tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
const contract = usePairContract(pairAddress, false)
const { result: reserves, loading } = useSingleCallResult(contract, 'getReserves')
return useMemo(() => {
if (loading || !tokenA || !tokenB) return undefined
if (!reserves) return null
const { reserve0, reserve1 } = reserves
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
}, [loading, reserves, tokenA, tokenB])
}
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
export function usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] { export function usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] {
const pairAddresses = useMemo( const pairAddresses = useMemo(
() => () =>
...@@ -51,3 +37,7 @@ export function usePairs(tokens: [Token | undefined, Token | undefined][]): (und ...@@ -51,3 +37,7 @@ export function usePairs(tokens: [Token | undefined, Token | undefined][]): (und
}) })
}, [results, tokens]) }, [results, tokens])
} }
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
return usePairs([[tokenA, tokenB]])[0]
}
...@@ -6,6 +6,7 @@ import { useV1FactoryContract } from '../hooks/useContract' ...@@ -6,6 +6,7 @@ import { useV1FactoryContract } from '../hooks/useContract'
import { Version } from '../hooks/useToggledVersion' import { Version } from '../hooks/useToggledVersion'
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks' import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks' import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
import { AddressZero } from '@ethersproject/constants'
export function useV1ExchangeAddress(tokenAddress?: string): string | undefined { export function useV1ExchangeAddress(tokenAddress?: string): string | undefined {
const contract = useV1FactoryContract() const contract = useV1FactoryContract()
...@@ -39,8 +40,8 @@ export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } { ...@@ -39,8 +40,8 @@ export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
return useMemo( return useMemo(
() => () =>
data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => { data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => {
const token = allTokens[args[ix][0]] if (result?.[0] && result[0] !== AddressZero) {
if (result?.[0]) { const token = allTokens[args[ix][0]]
memo[result[0]] = token memo[result[0]] = token
} }
return memo return memo
...@@ -51,12 +52,13 @@ export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } { ...@@ -51,12 +52,13 @@ export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
// returns whether any of the tokens in the user's token list have liquidity on v1 // returns whether any of the tokens in the user's token list have liquidity on v1
export function useUserHasLiquidityInAllTokens(): boolean | undefined { export function useUserHasLiquidityInAllTokens(): boolean | undefined {
const exchanges = useAllTokenV1Exchanges()
const { account, chainId } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
const exchanges = useAllTokenV1Exchanges()
const fakeLiquidityTokens = useMemo( const fakeLiquidityTokens = useMemo(
() => (chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1')) : []), () =>
chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1', 'Uniswap V1')) : [],
[chainId, exchanges] [chainId, exchanges]
) )
......
...@@ -11,6 +11,7 @@ import AddLiquidity from './AddLiquidity' ...@@ -11,6 +11,7 @@ import AddLiquidity from './AddLiquidity'
import CreatePool from './CreatePool' import CreatePool from './CreatePool'
import MigrateV1 from './MigrateV1' import MigrateV1 from './MigrateV1'
import MigrateV1Exchange from './MigrateV1/MigrateV1Exchange' import MigrateV1Exchange from './MigrateV1/MigrateV1Exchange'
import RemoveV1Exchange from './MigrateV1/RemoveV1Exchange'
import Pool from './Pool' import Pool from './Pool'
import PoolFinder from './PoolFinder' import PoolFinder from './PoolFinder'
import RemoveLiquidity from './RemoveLiquidity' import RemoveLiquidity from './RemoveLiquidity'
...@@ -84,6 +85,7 @@ export default function App() { ...@@ -84,6 +85,7 @@ export default function App() {
<Route exact strict path="/remove/:tokens" component={RemoveLiquidity} /> <Route exact strict path="/remove/:tokens" component={RemoveLiquidity} />
<Route exact strict path="/migrate/v1" component={MigrateV1} /> <Route exact strict path="/migrate/v1" component={MigrateV1} />
<Route exact strict path="/migrate/v1/:address" component={MigrateV1Exchange} /> <Route exact strict path="/migrate/v1/:address" component={MigrateV1Exchange} />
<Route exact strict path="/remove/v1/:address" component={RemoveV1Exchange} />
<Route component={RedirectPathToSwapOnly} /> <Route component={RedirectPathToSwapOnly} />
</Switch> </Switch>
</Web3ReactManager> </Web3ReactManager>
......
...@@ -10,7 +10,7 @@ import { Text } from 'rebass' ...@@ -10,7 +10,7 @@ import { Text } from 'rebass'
import { Plus } from 'react-feather' import { Plus } from 'react-feather'
import { TYPE, StyledInternalLink } from '../../theme' import { TYPE, StyledInternalLink } from '../../theme'
import { AutoColumn, ColumnCenter } from '../../components/Column' import { AutoColumn, ColumnCenter } from '../../components/Column'
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../../components/Button' import { ButtonPrimary, ButtonDropdown, ButtonDropdownLight } from '../../components/Button'
import { useToken } from '../../hooks/Tokens' import { useToken } from '../../hooks/Tokens'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
...@@ -59,16 +59,16 @@ export default function CreatePool({ location }: RouteComponentProps) { ...@@ -59,16 +59,16 @@ export default function CreatePool({ location }: RouteComponentProps) {
<AutoColumn gap="20px"> <AutoColumn gap="20px">
<AutoColumn gap="24px"> <AutoColumn gap="24px">
{!token0Address ? ( {!token0Address ? (
<ButtonDropwdown <ButtonDropdown
onClick={() => { onClick={() => {
setShowSearch(true) setShowSearch(true)
setActiveField(Fields.TOKEN0) setActiveField(Fields.TOKEN0)
}} }}
> >
<Text fontSize={20}>Select first token</Text> <Text fontSize={20}>Select first token</Text>
</ButtonDropwdown> </ButtonDropdown>
) : ( ) : (
<ButtonDropwdownLight <ButtonDropdownLight
onClick={() => { onClick={() => {
setShowSearch(true) setShowSearch(true)
setActiveField(Fields.TOKEN0) setActiveField(Fields.TOKEN0)
...@@ -83,13 +83,13 @@ export default function CreatePool({ location }: RouteComponentProps) { ...@@ -83,13 +83,13 @@ export default function CreatePool({ location }: RouteComponentProps) {
{token0?.address === WETH[chainId]?.address && '(default)'} {token0?.address === WETH[chainId]?.address && '(default)'}
</TYPE.darkGray> </TYPE.darkGray>
</Row> </Row>
</ButtonDropwdownLight> </ButtonDropdownLight>
)} )}
<ColumnCenter> <ColumnCenter>
<Plus size="16" color="#888D9B" /> <Plus size="16" color="#888D9B" />
</ColumnCenter> </ColumnCenter>
{!token1Address ? ( {!token1Address ? (
<ButtonDropwdown <ButtonDropdown
onClick={() => { onClick={() => {
setShowSearch(true) setShowSearch(true)
setActiveField(Fields.TOKEN1) setActiveField(Fields.TOKEN1)
...@@ -97,9 +97,9 @@ export default function CreatePool({ location }: RouteComponentProps) { ...@@ -97,9 +97,9 @@ export default function CreatePool({ location }: RouteComponentProps) {
disabled={step !== STEP.SELECT_TOKENS} disabled={step !== STEP.SELECT_TOKENS}
> >
<Text fontSize={20}>Select second token</Text> <Text fontSize={20}>Select second token</Text>
</ButtonDropwdown> </ButtonDropdown>
) : ( ) : (
<ButtonDropwdownLight <ButtonDropdownLight
onClick={() => { onClick={() => {
setShowSearch(true) setShowSearch(true)
setActiveField(Fields.TOKEN1) setActiveField(Fields.TOKEN1)
...@@ -111,7 +111,7 @@ export default function CreatePool({ location }: RouteComponentProps) { ...@@ -111,7 +111,7 @@ export default function CreatePool({ location }: RouteComponentProps) {
{token1?.symbol} {token1?.symbol}
</Text> </Text>
</Row> </Row>
</ButtonDropwdownLight> </ButtonDropdownLight>
)} )}
{pair ? ( // pair already exists - prompt to add liquidity to existing pool {pair ? ( // pair already exists - prompt to add liquidity to existing pool
<AutoRow padding="10px" justify="center"> <AutoRow padding="10px" justify="center">
......
...@@ -8,7 +8,7 @@ import { ButtonConfirmed } from '../../components/Button' ...@@ -8,7 +8,7 @@ import { ButtonConfirmed } from '../../components/Button'
import { PinkCard, YellowCard, LightCard } from '../../components/Card' import { PinkCard, YellowCard, LightCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column' import { AutoColumn } from '../../components/Column'
import QuestionHelper from '../../components/QuestionHelper' import QuestionHelper from '../../components/QuestionHelper'
import { AutoRow, RowBetween } from '../../components/Row' import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
import { Dots } from '../../components/swap/styleds' import { Dots } from '../../components/swap/styleds'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants' import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { MIGRATOR_ADDRESS } from '../../constants/abis/migrator' import { MIGRATOR_ADDRESS } from '../../constants/abis/migrator'
...@@ -21,19 +21,84 @@ import { useV1ExchangeContract, useV2MigratorContract } from '../../hooks/useCon ...@@ -21,19 +21,84 @@ import { useV1ExchangeContract, useV2MigratorContract } from '../../hooks/useCon
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks' import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks' import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks'
import { useETHBalances, useTokenBalance } from '../../state/wallet/hooks' import { useETHBalances, useTokenBalance } from '../../state/wallet/hooks'
import { TYPE } from '../../theme' import { TYPE, ExternalLink } from '../../theme'
import { isAddress } from '../../utils' import { isAddress, getEtherscanLink } from '../../utils'
import { BodyWrapper } from '../AppBody' import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState' import { EmptyState } from './EmptyState'
import TokenLogo from '../../components/TokenLogo' import TokenLogo from '../../components/TokenLogo'
import { FormattedPoolTokenAmount } from './index' import { AddressZero } from '@ethersproject/constants'
import { Text } from 'rebass'
const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18)) const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
const ZERO = JSBI.BigInt(0) const ZERO = JSBI.BigInt(0)
const ONE = JSBI.BigInt(1) const ONE = JSBI.BigInt(1)
const ZERO_FRACTION = new Fraction(ZERO, ONE) const ZERO_FRACTION = new Fraction(ZERO, ONE)
const ALLOWED_OUTPUT_MIN_PERCENT = new Percent(JSBI.BigInt(10000 - INITIAL_ALLOWED_SLIPPAGE), JSBI.BigInt(10000)) const ALLOWED_OUTPUT_MIN_PERCENT = new Percent(JSBI.BigInt(10000 - INITIAL_ALLOWED_SLIPPAGE), JSBI.BigInt(10000))
function FormattedPoolTokenAmount({ tokenAmount }: { tokenAmount: TokenAmount }) {
return (
<>
{tokenAmount.equalTo(JSBI.BigInt(0))
? '0'
: tokenAmount.greaterThan(POOL_TOKEN_AMOUNT_MIN)
? tokenAmount.toSignificant(4)
: `<${POOL_TOKEN_AMOUNT_MIN.toSignificant(1)}`}
</>
)
}
export function V1LiquidityInfo({
token,
liquidityTokenAmount,
tokenWorth,
ethWorth
}: {
token: Token
liquidityTokenAmount: TokenAmount
tokenWorth: TokenAmount
ethWorth: Fraction
}) {
const { chainId } = useActiveWeb3React()
return (
<>
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
<TokenLogo size="24px" address={token.address} />
<div style={{ marginLeft: '.75rem' }}>
<TYPE.mediumHeader>
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />}{' '}
{token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH
</TYPE.mediumHeader>
</div>
</AutoRow>
<RowBetween my="1rem">
<Text fontSize={16} fontWeight={500}>
Pooled {token.equals(WETH[chainId]) ? 'WETH' : token.symbol}:
</Text>
<RowFixed>
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{tokenWorth.toSignificant(4)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token.address} />
</RowFixed>
</RowBetween>
<RowBetween mb="1rem">
<Text fontSize={16} fontWeight={500}>
Pooled ETH:
</Text>
<RowFixed>
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{ethWorth.toSignificant(4)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={WETH[chainId].address} />
</RowFixed>
</RowBetween>
</>
)
}
function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount: TokenAmount; token: Token }) { function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount: TokenAmount; token: Token }) {
const { account, chainId } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
const totalSupply = useTotalSupply(liquidityTokenAmount.token) const totalSupply = useTotalSupply(liquidityTokenAmount.token)
...@@ -126,69 +191,96 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount ...@@ -126,69 +191,96 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
}) })
}, [minAmountToken, minAmountETH, migrator, token, account, addTransaction]) }, [minAmountToken, minAmountETH, migrator, token, account, addTransaction])
const noLiquidityTokens = liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO) const noLiquidityTokens = !!liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
const largePriceDifference = Boolean(priceDifferenceAbs && !priceDifferenceAbs.lessThan(JSBI.BigInt(5))) const largePriceDifference = !!priceDifferenceAbs && !priceDifferenceAbs.lessThan(JSBI.BigInt(5))
const isSuccessfullyMigrated = Boolean(noLiquidityTokens && pendingMigrationHash) const isSuccessfullyMigrated = !!pendingMigrationHash && !!noLiquidityTokens
return ( return (
<AutoColumn gap="20px"> <AutoColumn gap="20px">
{!isFirstLiquidityProvider ? ( <TYPE.body my={9} style={{ fontWeight: 400 }}>
largePriceDifference ? ( This tool will safely migrate your V1 liquidity to V2 with minimal price risk. The process is completely
<YellowCard> trustless thanks to the{' '}
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}> <ExternalLink href={getEtherscanLink(chainId, MIGRATOR_ADDRESS, 'address')}>
It is best to deposit liquidity into Uniswap V2 at a price you believe is correct. If you believe the <TYPE.blue display="inline">Uniswap migration contract↗</TYPE.blue>
price is incorrect, you can either make a swap to move the price or wait for someone else to do so. </ExternalLink>
</TYPE.body> .
<AutoColumn gap="8px"> </TYPE.body>
<RowBetween>
<TYPE.body>V1 Price:</TYPE.body> {!isFirstLiquidityProvider && largePriceDifference ? (
<TYPE.black> <YellowCard>
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH <TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
</TYPE.black> It{"'"}s best to deposit liquidity into Uniswap V2 at a price you believe is correct. If the V2 price seems
</RowBetween> incorrect, you can either make a swap to move the price or wait for someone else to do so.
<RowBetween> </TYPE.body>
<TYPE.body>V2 Price:</TYPE.body> <AutoColumn gap="8px">
<TYPE.black> <RowBetween>
{v2SpotPrice?.toSignificant(6)} {token.symbol}/ETH <TYPE.body>V1 Price:</TYPE.body>
</TYPE.black> <TYPE.black>
</RowBetween> {v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
<RowBetween> </TYPE.black>
<div>Price Difference:</div> </RowBetween>
<div>{priceDifferenceAbs.toSignificant(4)}%</div> <RowBetween>
</RowBetween> <div />
</AutoColumn> <TYPE.black>
</YellowCard>
) : null
) : (
<PinkCard>
<AutoColumn gap="10px">
<div>
You are the first liquidity provider for this pair on Uniswap V2. Your liquidity will be migrated at the
current V1 price. Your transaction cost also includes the gas to create the pool.
</div>
<div>V1 Price</div>
<AutoColumn>
<div>
{v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol} {v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
</div> </TYPE.black>
<div> </RowBetween>
<RowBetween>
<TYPE.body>V2 Price:</TYPE.body>
<TYPE.black>
{v2SpotPrice?.toSignificant(6)} {token.symbol}/ETH
</TYPE.black>
</RowBetween>
<RowBetween>
<div />
<TYPE.black>
{v2SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
</TYPE.black>
</RowBetween>
<RowBetween>
<TYPE.body color="inherit">Price Difference:</TYPE.body>
<TYPE.black color="inherit">{priceDifferenceAbs.toSignificant(4)}%</TYPE.black>
</RowBetween>
</AutoColumn>
</YellowCard>
) : null}
{isFirstLiquidityProvider && (
<PinkCard>
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
You are the first liquidity provider for this pair on Uniswap V2. Your liquidity will be migrated at the
current V1 price. Your transaction cost also includes the gas to create the pool.
</TYPE.body>
<AutoColumn gap="8px">
<RowBetween>
<TYPE.body>V1 Price:</TYPE.body>
<TYPE.black>
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH {v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
</div> </TYPE.black>
</AutoColumn> </RowBetween>
<RowBetween>
<div />
<TYPE.black>
{v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
</TYPE.black>
</RowBetween>
</AutoColumn> </AutoColumn>
</PinkCard> </PinkCard>
)} )}
<LightCard> <LightCard>
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}> <V1LiquidityInfo
<TokenLogo size="24px" address={token.address} />{' '} token={token}
<div style={{ marginLeft: '.75rem' }}> liquidityTokenAmount={liquidityTokenAmount}
<TYPE.mediumHeader> tokenWorth={tokenWorth}
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />} {token.symbol} Pool Tokens ethWorth={ethWorth}
</TYPE.mediumHeader> />
</div>
</AutoRow>
<div style={{ display: 'flex', marginTop: '1rem' }}> <div style={{ display: 'flex', marginTop: '1rem' }}>
<AutoColumn gap="12px" style={{ flex: '1', marginRight: 12 }}> <AutoColumn gap="12px" style={{ flex: '1', marginRight: 12 }}>
<ButtonConfirmed <ButtonConfirmed
...@@ -217,13 +309,13 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount ...@@ -217,13 +309,13 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
} }
onClick={migrate} onClick={migrate}
> >
{isSuccessfullyMigrated ? 'Success' : isMigrationPending ? 'Migrating...' : 'Migrate'} {isSuccessfullyMigrated ? 'Success' : isMigrationPending ? <Dots>Migrating</Dots> : 'Migrate'}
</ButtonConfirmed> </ButtonConfirmed>
</AutoColumn> </AutoColumn>
</div> </div>
</LightCard> </LightCard>
<TYPE.darkGray style={{ textAlign: 'center' }}> <TYPE.darkGray style={{ textAlign: 'center' }}>
{'Your ' + token.symbol + ' liquidity will become Uniswap V2 ' + token.symbol + '/ETH liquidity.'} {`Your Uniswap V1 ${token.symbol}/ETH liquidity will become Uniswap V2 ${token.symbol}/ETH liquidity.`}
</TYPE.darkGray> </TYPE.darkGray>
</AutoColumn> </AutoColumn>
) )
...@@ -235,39 +327,33 @@ export default function MigrateV1Exchange({ ...@@ -235,39 +327,33 @@ export default function MigrateV1Exchange({
params: { address } params: { address }
} }
}: RouteComponentProps<{ address: string }>) { }: RouteComponentProps<{ address: string }>) {
const validated = isAddress(address) const validatedAddress = isAddress(address)
const { chainId, account } = useActiveWeb3React() const { chainId, account } = useActiveWeb3React()
const exchangeContract = useV1ExchangeContract(validated ? validated : undefined) const exchangeContract = useV1ExchangeContract(validatedAddress ? validatedAddress : undefined)
const tokenAddress = useSingleCallResult(exchangeContract, 'tokenAddress', undefined, NEVER_RELOAD)?.result?.[0] const tokenAddress = useSingleCallResult(exchangeContract, 'tokenAddress', undefined, NEVER_RELOAD)?.result?.[0]
const token = useToken(tokenAddress) const token = useToken(tokenAddress)
const liquidityToken: Token | undefined = useMemo( const liquidityToken: Token | undefined = useMemo(
() => (validated && token ? new Token(chainId, validated, 18, `UNI-V1-${token.symbol}`) : undefined), () =>
[chainId, token, validated] validatedAddress && token
? new Token(chainId, validatedAddress, 18, `UNI-V1-${token.symbol}`, 'Uniswap V1')
: undefined,
[chainId, validatedAddress, token]
) )
const userLiquidityBalance = useTokenBalance(account, liquidityToken) const userLiquidityBalance = useTokenBalance(account, liquidityToken)
const handleBack = useCallback(() => { const handleBack = useCallback(() => {
history.push('/migrate/v1') history.push('/migrate/v1')
}, [history]) }, [history])
if (!validated) { // redirect for invalid url params
if (!validatedAddress || tokenAddress === AddressZero) {
console.error('Invalid address in path', address) console.error('Invalid address in path', address)
return <Redirect to="/migrate/v1" /> return <Redirect to="/migrate/v1" />
} }
if (!account) {
return (
<BodyWrapper>
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
</BodyWrapper>
)
}
return ( return (
<BodyWrapper style={{ padding: 24 }}> <BodyWrapper style={{ padding: 24 }}>
<AutoColumn gap="16px"> <AutoColumn gap="16px">
...@@ -275,13 +361,30 @@ export default function MigrateV1Exchange({ ...@@ -275,13 +361,30 @@ export default function MigrateV1Exchange({
<div style={{ cursor: 'pointer' }}> <div style={{ cursor: 'pointer' }}>
<ArrowLeft onClick={handleBack} /> <ArrowLeft onClick={handleBack} />
</div> </div>
<TYPE.mediumHeader>Migrate {token?.symbol} Pool Tokens</TYPE.mediumHeader> <TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader>
<div> <div>
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." /> <QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />
</div> </div>
</AutoRow> </AutoRow>
{userLiquidityBalance && token ? ( {!account ? (
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
) : validatedAddress && token?.equals(WETH[chainId]) ? (
<>
<TYPE.body my={9} style={{ fontWeight: 400 }}>
Because Uniswap V2 uses WETH under the hood, your Uniswap V1 WETH/ETH liquidity cannot be migrated. You
may want to remove your liquidity instead.
</TYPE.body>
<ButtonConfirmed
onClick={() => {
history.push(`/remove/v1/${validatedAddress}`)
}}
>
Remove
</ButtonConfirmed>
</>
) : userLiquidityBalance && token ? (
<V1PairMigration liquidityTokenAmount={userLiquidityBalance} token={token} /> <V1PairMigration liquidityTokenAmount={userLiquidityBalance} token={token} />
) : ( ) : (
<EmptyState message="Loading..." /> <EmptyState message="Loading..." />
......
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'
import { LightCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import QuestionHelper from '../../components/QuestionHelper'
import { AutoRow } from '../../components/Row'
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants'
import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
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 { isAddress } from '../../utils'
import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState'
import { V1LiquidityInfo } from './MigrateV1Exchange'
import { AddressZero } from '@ethersproject/constants'
import { Dots } from '../../components/swap/styleds'
import { Contract } from '@ethersproject/contracts'
import { useTotalSupply } from '../../data/TotalSupply'
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
const ZERO = JSBI.BigInt(0)
const ONE = JSBI.BigInt(1)
const ZERO_FRACTION = new Fraction(ZERO, ONE)
function V1PairRemoval({
exchangeContract,
liquidityTokenAmount,
token
}: {
exchangeContract: Contract
liquidityTokenAmount: TokenAmount
token: Token
}) {
const { chainId } = useActiveWeb3React()
const totalSupply = useTotalSupply(liquidityTokenAmount.token)
const exchangeETHBalance = useETHBalances([liquidityTokenAmount.token.address])?.[liquidityTokenAmount.token.address]
const exchangeTokenBalance = useTokenBalance(liquidityTokenAmount.token.address, token)
const [confirmingRemoval, setConfirmingRemoval] = useState<boolean>(false)
const [pendingRemovalHash, setPendingRemovalHash] = useState<string | null>(null)
const shareFraction: Fraction = totalSupply ? new Percent(liquidityTokenAmount.raw, totalSupply.raw) : ZERO_FRACTION
const ethWorth: Fraction = exchangeETHBalance
? new Fraction(shareFraction.multiply(exchangeETHBalance).quotient, WEI_DENOM)
: ZERO_FRACTION
const tokenWorth: TokenAmount = exchangeTokenBalance
? new TokenAmount(token, shareFraction.multiply(exchangeTokenBalance.raw).quotient)
: new TokenAmount(token, ZERO)
const addTransaction = useTransactionAdder()
const isRemovalPending = useIsTransactionPending(pendingRemovalHash)
const remove = useCallback(() => {
if (!liquidityTokenAmount) return
setConfirmingRemoval(true)
exchangeContract
.removeLiquidity(
liquidityTokenAmount.raw.toString(),
1, // min_eth, this is safe because we're removing liquidity
1, // min_tokens, this is safe because we're removing liquidity
Math.floor(new Date().getTime() / 1000) + DEFAULT_DEADLINE_FROM_NOW
)
.then((response: TransactionResponse) => {
ReactGA.event({
category: 'Remove',
action: 'V1',
label: token?.symbol
})
addTransaction(response, {
summary: `Remove ${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH V1 liquidity`
})
setPendingRemovalHash(response.hash)
})
.catch(error => {
console.error(error)
setConfirmingRemoval(false)
})
}, [exchangeContract, liquidityTokenAmount, token, chainId, addTransaction])
const noLiquidityTokens = !!liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
const isSuccessfullyRemoved = !!pendingRemovalHash && !!noLiquidityTokens
return (
<AutoColumn gap="20px">
<TYPE.body my={9} style={{ fontWeight: 400 }}>
This tool will remove your V1 liquidity and send the underlying assets to your wallet.
</TYPE.body>
<LightCard>
<V1LiquidityInfo
token={token}
liquidityTokenAmount={liquidityTokenAmount}
tokenWorth={tokenWorth}
ethWorth={ethWorth}
/>
<div style={{ display: 'flex', marginTop: '1rem' }}>
<ButtonConfirmed
confirmed={isSuccessfullyRemoved}
disabled={isSuccessfullyRemoved || noLiquidityTokens || isRemovalPending || confirmingRemoval}
onClick={remove}
>
{isSuccessfullyRemoved ? 'Success' : isRemovalPending ? <Dots>Removing</Dots> : 'Remove'}
</ButtonConfirmed>
</div>
</LightCard>
<TYPE.darkGray style={{ textAlign: 'center' }}>
{`Your Uniswap V1 ${
token.equals(WETH[chainId]) ? 'WETH' : token.symbol
}/ETH liquidity will be redeemed for underlying assets.`}
</TYPE.darkGray>
</AutoColumn>
)
}
export default function RemoveV1Exchange({
history,
match: {
params: { address }
}
}: RouteComponentProps<{ address: string }>) {
const validatedAddress = isAddress(address)
const { chainId, account } = useActiveWeb3React()
const exchangeContract = useV1ExchangeContract(validatedAddress ? validatedAddress : undefined, true)
const tokenAddress = useSingleCallResult(exchangeContract, 'tokenAddress', undefined, NEVER_RELOAD)?.result?.[0]
const token = useToken(tokenAddress)
const liquidityToken: Token | undefined = useMemo(
() =>
validatedAddress && token
? new Token(chainId, validatedAddress, 18, `UNI-V1-${token.symbol}`, 'Uniswap V1')
: undefined,
[chainId, validatedAddress, token]
)
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)
return <Redirect to="/migrate/v1" />
}
return (
<BodyWrapper style={{ padding: 24 }}>
<AutoColumn gap="16px">
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
<div style={{ cursor: 'pointer' }}>
<ArrowLeft onClick={handleBack} />
</div>
<TYPE.mediumHeader>Remove V1 Liquidity</TYPE.mediumHeader>
<div>
<QuestionHelper text="Remove your Uniswap V1 liquidity tokens." />
</div>
</AutoRow>
{!account ? (
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
) : userLiquidityBalance && token ? (
<V1PairRemoval
exchangeContract={exchangeContract}
liquidityTokenAmount={userLiquidityBalance}
token={token}
/>
) : (
<EmptyState message="Loading..." />
)}
</AutoColumn>
</BodyWrapper>
)
}
import { Fraction, JSBI, Token, TokenAmount } from '@uniswap/sdk' import { JSBI, Token } from '@uniswap/sdk'
import React, { useCallback, useContext, useMemo, useState } from 'react' import React, { useCallback, useContext, useMemo, useState, useEffect } from 'react'
import { ArrowLeft } from 'react-feather' import { ArrowLeft } from 'react-feather'
import { RouteComponentProps } from 'react-router' import { RouteComponentProps } from 'react-router'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { ButtonPrimary } from '../../components/Button'
import { AutoColumn } from '../../components/Column' import { AutoColumn } from '../../components/Column'
import { AutoRow } from '../../components/Row' import { AutoRow } from '../../components/Row'
import { SearchInput } from '../../components/SearchModal/styleds' import { SearchInput } from '../../components/SearchModal/styleds'
import TokenLogo from '../../components/TokenLogo'
import { useAllTokenV1Exchanges } from '../../data/V1' import { useAllTokenV1Exchanges } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens' import { useToken, useAllTokens } from '../../hooks/Tokens'
import { useWalletModalToggle } from '../../state/application/hooks' import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
import { useTokenBalances } from '../../state/wallet/hooks'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { GreyCard } from '../../components/Card' import { LightCard } from '../../components/Card'
import { BodyWrapper } from '../AppBody' import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState' import { EmptyState } from './EmptyState'
import V1PositionCard from '../../components/PositionCard/V1'
const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000)) import QuestionHelper from '../../components/QuestionHelper'
import { Dots } from '../../components/swap/styleds'
export function FormattedPoolTokenAmount({ tokenAmount }: { tokenAmount: TokenAmount }) { import { useAddUserToken } from '../../state/user/hooks'
return ( import { isDefaultToken, isCustomAddedToken } from '../../utils'
<>
{tokenAmount.equalTo(JSBI.BigInt(0))
? '0'
: tokenAmount.greaterThan(POOL_TOKEN_AMOUNT_MIN)
? tokenAmount.toSignificant(6)
: `<${POOL_TOKEN_AMOUNT_MIN.toSignificant(1)}`}
</>
)
}
export default function MigrateV1({ history }: RouteComponentProps) { export default function MigrateV1({ history }: RouteComponentProps) {
const theme = useContext(ThemeContext)
const { account, chainId } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
const allV1Exchanges = useAllTokenV1Exchanges()
const v1LiquidityTokens: Token[] = useMemo(() => {
return Object.keys(allV1Exchanges).map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
}, [chainId, allV1Exchanges])
const v1LiquidityBalances = useTokenBalances(account, v1LiquidityTokens)
const [tokenSearch, setTokenSearch] = useState<string>('') const [tokenSearch, setTokenSearch] = useState<string>('')
const handleTokenSearchChange = useCallback(e => setTokenSearch(e.target.value), [setTokenSearch]) const handleTokenSearchChange = useCallback(e => setTokenSearch(e.target.value), [setTokenSearch])
const searchedToken: Token | undefined = useToken(tokenSearch) // automatically add the search token
const token = useToken(tokenSearch)
const isDefault = isDefaultToken(token)
const allTokens = useAllTokens()
const isCustomAdded = isCustomAddedToken(allTokens, token)
const addToken = useAddUserToken()
useEffect(() => {
if (token && !isDefault && !isCustomAdded) {
addToken(token)
}
}, [token, isDefault, isCustomAdded, addToken])
const unmigratedLiquidityExchangeAddresses: TokenAmount[] = useMemo( // get V1 LP balances
() => const V1Exchanges = useAllTokenV1Exchanges()
Object.keys(v1LiquidityBalances) const V1LiquidityTokens: Token[] = useMemo(() => {
.filter(tokenAddress => return Object.keys(V1Exchanges).map(
v1LiquidityBalances[tokenAddress] exchangeAddress => new Token(chainId, exchangeAddress, 18, 'UNI-V1', 'Uniswap V1')
? JSBI.greaterThan(v1LiquidityBalances[tokenAddress]?.raw, JSBI.BigInt(0)) )
: false }, [chainId, V1Exchanges])
) const [V1LiquidityBalances, V1LiquidityBalancesLoading] = useTokenBalancesWithLoadingIndicator(
.map(tokenAddress => v1LiquidityBalances[tokenAddress]) account,
.sort((a1, a2) => { V1LiquidityTokens
if (searchedToken) {
if (allV1Exchanges[a1.token.address].address === searchedToken.address) return -1
if (allV1Exchanges[a2.token.address].address === searchedToken.address) return 1
}
return a1.token.address < a2.token.address ? -1 : 1
}),
[allV1Exchanges, searchedToken, v1LiquidityBalances]
) )
const allV1PairsWithLiquidity = V1LiquidityTokens.filter(V1LiquidityToken => {
return (
V1LiquidityBalances?.[V1LiquidityToken.address] &&
JSBI.greaterThan(V1LiquidityBalances[V1LiquidityToken.address].raw, JSBI.BigInt(0))
)
}).map(V1LiquidityToken => {
return (
<V1PositionCard
key={V1LiquidityToken.address}
token={V1Exchanges[V1LiquidityToken.address]}
V1LiquidityBalance={V1LiquidityBalances[V1LiquidityToken.address]}
/>
)
})
const theme = useContext(ThemeContext) // should never always be false, because a V1 exhchange exists for WETH on all testnets
const isLoading = Object.keys(V1Exchanges)?.length === 0 || V1LiquidityBalancesLoading
const toggleWalletModal = useWalletModalToggle()
const handleBackClick = useCallback(() => { const handleBackClick = useCallback(() => {
history.push('/pool') history.push('/pool')
}, [history]) }, [history])
return ( return (
<BodyWrapper style={{ maxWidth: 450, padding: 24 }}> <BodyWrapper style={{ padding: 24 }}>
<AutoColumn gap="24px"> <AutoColumn gap="16px">
<AutoRow style={{ justifyContent: 'space-between' }}> <AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
<div style={{ cursor: 'pointer' }}>
<ArrowLeft onClick={handleBackClick} />
</div>
<TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader>
<div> <div>
<ArrowLeft style={{ cursor: 'pointer' }} onClick={handleBackClick} /> <QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />
</div> </div>
<TYPE.largeHeader>Migrate Liquidity</TYPE.largeHeader>
<div></div>
</AutoRow>
<GreyCard>
<TYPE.main style={{ lineHeight: '140%' }}>
For each pool, approve the migration helper and click migrate liquidity. Your liquidity will be withdrawn
from Uniswap V1 and deposited into Uniswap V2.
</TYPE.main>
<TYPE.black padding={'1rem 0 0 0'} style={{ lineHeight: '140%' }}>
If your liquidity does not appear below automatically, you may need to find it by pasting the token address
into the search box below.
</TYPE.black>
</GreyCard>
<AutoRow>
<SearchInput
value={tokenSearch}
onChange={handleTokenSearchChange}
placeholder="Find liquidity by pasting a token address."
/>
</AutoRow> </AutoRow>
{unmigratedLiquidityExchangeAddresses.map(poolTokenAmount => ( <TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
<div For each pool shown below, click migrate to remove your liquidity from Uniswap V1 and deposit it into Uniswap
key={poolTokenAmount.token.address} V2.
style={{ borderRadius: '20px', padding: 16, backgroundColor: theme.bg2 }} </TYPE.body>
>
<AutoRow style={{ justifyContent: 'space-between' }}>
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
<TokenLogo size="32px" address={allV1Exchanges[poolTokenAmount.token.address].address} />{' '}
<div style={{ marginLeft: '.75rem' }}>
<TYPE.main fontWeight={600}>
<FormattedPoolTokenAmount tokenAmount={poolTokenAmount} />
</TYPE.main>
<TYPE.main fontWeight={500}>
{allV1Exchanges[poolTokenAmount.token.address].symbol} Pool Tokens
</TYPE.main>
</div>
</AutoRow>
<div>
<ButtonPrimary
onClick={() => {
history.push(`/migrate/v1/${poolTokenAmount.token.address}`)
}}
style={{ padding: '8px 12px', borderRadius: '12px' }}
>
Migrate
</ButtonPrimary>
</div>
</AutoRow>
</div>
))}
{account && unmigratedLiquidityExchangeAddresses.length === 0 ? ( {!account ? (
<EmptyState message="No V1 Liquidity found." /> <LightCard padding="40px">
) : null} <TYPE.body color={theme.text3} textAlign="center">
Connect to a wallet to view your V1 liquidity.
{!account ? <ButtonPrimary onClick={toggleWalletModal}>Connect to a wallet</ButtonPrimary> : null} </TYPE.body>
</LightCard>
) : isLoading ? (
<LightCard padding="40px">
<TYPE.body color={theme.text3} textAlign="center">
<Dots>Loading</Dots>
</TYPE.body>
</LightCard>
) : (
<>
<AutoRow>
<SearchInput
value={tokenSearch}
onChange={handleTokenSearchChange}
placeholder="Enter a token address to find liquidity"
/>
</AutoRow>
{allV1PairsWithLiquidity?.length > 0 ? (
<>{allV1PairsWithLiquidity}</>
) : (
<EmptyState message="No V1 Liquidity found." />
)}
</>
)}
</AutoColumn> </AutoColumn>
</BodyWrapper> </BodyWrapper>
) )
......
import React, { useState, useContext, useCallback } from 'react' import React, { useState, useContext, useCallback } from 'react'
import styled, { ThemeContext } from 'styled-components' import styled, { ThemeContext } from 'styled-components'
import { JSBI, Pair } from '@uniswap/sdk' import { JSBI } from '@uniswap/sdk'
import { RouteComponentProps } from 'react-router-dom' import { RouteComponentProps } from 'react-router-dom'
import Question from '../../components/QuestionHelper' import Question from '../../components/QuestionHelper'
import PairSearchModal from '../../components/SearchModal/PairSearchModal' import PairSearchModal from '../../components/SearchModal/PairSearchModal'
import PositionCard from '../../components/PositionCard' import PositionCard from '../../components/PositionCard'
import { useUserHasLiquidityInAllTokens } from '../../data/V1' import { useUserHasLiquidityInAllTokens } from '../../data/V1'
import { useTokenBalances } from '../../state/wallet/hooks' import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
import { StyledInternalLink, TYPE } from '../../theme' import { StyledInternalLink, TYPE } from '../../theme'
import { Text } from 'rebass' import { Text } from 'rebass'
import { LightCard } from '../../components/Card' import { LightCard } from '../../components/Card'
...@@ -16,9 +16,10 @@ import { ButtonPrimary, ButtonSecondary } from '../../components/Button' ...@@ -16,9 +16,10 @@ import { ButtonPrimary, ButtonSecondary } from '../../components/Button'
import { AutoColumn, ColumnCenter } from '../../components/Column' import { AutoColumn, ColumnCenter } from '../../components/Column'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { usePair } from '../../data/Reserves' import { usePairs } from '../../data/Reserves'
import { useAllDummyPairs } from '../../state/user/hooks' import { useAllDummyPairs } from '../../state/user/hooks'
import AppBody from '../AppBody' import AppBody from '../AppBody'
import { Dots } from '../../components/swap/styleds'
const Positions = styled.div` const Positions = styled.div`
position: relative; position: relative;
...@@ -31,33 +32,35 @@ const FixedBottom = styled.div` ...@@ -31,33 +32,35 @@ const FixedBottom = styled.div`
width: 100%; width: 100%;
` `
function PositionCardWrapper({ dummyPair }: { dummyPair: Pair }) {
const pair = usePair(dummyPair.token0, dummyPair.token1)
return <PositionCard pair={pair} />
}
export default function Pool({ history }: RouteComponentProps) { export default function Pool({ history }: RouteComponentProps) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
const [showPoolSearch, setShowPoolSearch] = useState(false) const [showPoolSearch, setShowPoolSearch] = useState(false)
// initiate listener for LP balances // fetch the user's balances of all tracked V2 LP tokens
const pairs = useAllDummyPairs() const V2DummyPairs = useAllDummyPairs()
const pairBalances = useTokenBalances( const [V2PairsBalances, fetchingV2PairBalances] = useTokenBalancesWithLoadingIndicator(
account, account,
pairs?.map(p => p.liquidityToken) 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 V2Pairs = usePairs(
V2DummyPairsWithABalance.map(V2DummyPairWithABalance => [
V2DummyPairWithABalance.token0,
V2DummyPairWithABalance.token1
])
) )
const V2IsLoading =
fetchingV2PairBalances || V2Pairs?.length < V2DummyPairsWithABalance.length || V2Pairs?.some(V2Pair => !!!V2Pair)
const filteredExchangeList = pairs const allV2PairsWithLiquidity = V2Pairs.filter(V2Pair => !!V2Pair).map(V2Pair => (
.filter(pair => { <PositionCard key={V2Pair.liquidityToken.address} pair={V2Pair} />
return ( ))
pairBalances?.[pair.liquidityToken.address] &&
JSBI.greaterThan(pairBalances[pair.liquidityToken.address].raw, JSBI.BigInt(0))
)
})
.map((pair, i) => {
return <PositionCardWrapper key={i} dummyPair={pair} />
})
const hasV1Liquidity = useUserHasLiquidityInAllTokens() const hasV1Liquidity = useUserHasLiquidityInAllTokens()
...@@ -76,42 +79,49 @@ export default function Pool({ history }: RouteComponentProps) { ...@@ -76,42 +79,49 @@ export default function Pool({ history }: RouteComponentProps) {
}} }}
> >
<Text fontWeight={500} fontSize={20}> <Text fontWeight={500} fontSize={20}>
Join {filteredExchangeList?.length > 0 ? 'another' : 'a'} pool Join {allV2PairsWithLiquidity?.length > 0 ? 'another' : 'a'} pool
</Text> </Text>
</ButtonPrimary> </ButtonPrimary>
<Positions> <Positions>
<AutoColumn gap="12px"> <AutoColumn gap="12px">
<RowBetween padding={'0 8px'}> <RowBetween padding={'0 8px'}>
<Text color={theme.text1} fontWeight={500}> <Text color={theme.text1} fontWeight={500}>
Your Pooled Liquidity Your Liquidity
</Text> </Text>
<Question text="When you add liquidity, you are given pool tokens that represent your share. If you don’t see a pool you joined in this list, try importing a pool below." /> <Question text="When you add liquidity, you are given pool tokens that represent your share. If you don’t see a pool you joined in this list, try importing a pool below." />
</RowBetween> </RowBetween>
{filteredExchangeList?.length === 0 && (
<LightCard {!account ? (
padding="40px <LightCard padding="40px">
" <TYPE.body color={theme.text3} textAlign="center">
> Connect to a wallet to view your liquidity.
</TYPE.body>
</LightCard>
) : V2IsLoading ? (
<LightCard padding="40px">
<TYPE.body color={theme.text3} textAlign="center">
<Dots>Loading</Dots>
</TYPE.body>
</LightCard>
) : allV2PairsWithLiquidity?.length > 0 ? (
<>{allV2PairsWithLiquidity}</>
) : (
<LightCard padding="40px">
<TYPE.body color={theme.text3} textAlign="center"> <TYPE.body color={theme.text3} textAlign="center">
No liquidity found. No liquidity found.
</TYPE.body> </TYPE.body>
</LightCard> </LightCard>
)} )}
{filteredExchangeList}
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}> <div>
{!hasV1Liquidity ? ( <Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
<> {hasV1Liquidity ? 'Uniswap V1 liquidity found!' : "Don't see a pool you joined?"}{' '}
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '} <StyledInternalLink id="import-pool-link" to={hasV1Liquidity ? '/migrate/v1' : '/find'}>
<StyledInternalLink id="import-pool-link" to="/find"> {hasV1Liquidity ? 'Migrate now.' : 'Import it.'}
Import it.
</StyledInternalLink>
</>
) : (
<StyledInternalLink id="migrate-v1-liquidity-link" to="/migrate/v1">
Migrate your V1 liquidity.
</StyledInternalLink> </StyledInternalLink>
)} </Text>
</Text> </div>
</AutoColumn> </AutoColumn>
<FixedBottom> <FixedBottom>
<ColumnCenter> <ColumnCenter>
......
import { JSBI, Pair, Token, TokenAmount } from '@uniswap/sdk' import { JSBI, Pair, Token, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { Plus } from 'react-feather' import { Plus } from 'react-feather'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ButtonDropwdown, ButtonDropwdownLight, ButtonPrimary } from '../../components/Button' import { ButtonDropdownLight } from '../../components/Button'
import { LightCard } from '../../components/Card' import { LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column' import { AutoColumn, ColumnCenter } from '../../components/Column'
import PositionCard from '../../components/PositionCard' import PositionCard from '../../components/PositionCard'
...@@ -23,31 +22,31 @@ enum Fields { ...@@ -23,31 +22,31 @@ enum Fields {
TOKEN1 = 1 TOKEN1 = 1
} }
export default function PoolFinder({ history }: RouteComponentProps) { export default function PoolFinder() {
const { account } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
const [showSearch, setShowSearch] = useState<boolean>(false) const [showSearch, setShowSearch] = useState<boolean>(false)
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0) const [activeField, setActiveField] = useState<number>(Fields.TOKEN1)
const [token0Address, setToken0Address] = useState<string>() const [token0Address, setToken0Address] = useState<string>(WETH[chainId].address)
const [token1Address, setToken1Address] = useState<string>() const [token1Address, setToken1Address] = useState<string>()
const token0: Token = useToken(token0Address) const token0: Token = useToken(token0Address)
const token1: Token = useToken(token1Address) const token1: Token = useToken(token1Address)
const pair: Pair = usePair(token0, token1) const pair: Pair = usePair(token0, token1)
const addPair = usePairAdder() const addPair = usePairAdder()
useEffect(() => { useEffect(() => {
if (pair) { if (pair) {
addPair(pair) addPair(pair)
} }
}, [pair, addPair]) }, [pair, addPair])
const position: TokenAmount = useTokenBalanceTreatingWETHasETH(account, pair?.liquidityToken)
const newPair: boolean = const newPair: boolean =
pair === null || pair === null ||
(!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0))) (!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0)))
const allowImport: boolean = position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
const position: TokenAmount = useTokenBalanceTreatingWETHasETH(account, pair?.liquidityToken)
const poolImported: boolean = !!position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
const handleTokenSelect = useCallback( const handleTokenSelect = useCallback(
(address: string) => { (address: string) => {
...@@ -63,100 +62,89 @@ export default function PoolFinder({ history }: RouteComponentProps) { ...@@ -63,100 +62,89 @@ export default function PoolFinder({ history }: RouteComponentProps) {
return ( return (
<AppBody> <AppBody>
<AutoColumn gap="md"> <AutoColumn gap="md">
{!token0Address ? ( <ButtonDropdownLight
<ButtonDropwdown onClick={() => {
onClick={() => { setShowSearch(true)
setShowSearch(true) setActiveField(Fields.TOKEN0)
setActiveField(Fields.TOKEN0) }}
}} >
> {token0 ? (
<Text fontSize={20}>Select first token</Text>
</ButtonDropwdown>
) : (
<ButtonDropwdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN0)
}}
>
<Row> <Row>
<TokenLogo address={token0Address} /> <TokenLogo address={token0Address} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}> <Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token0?.symbol} {token0.symbol}
</Text> </Text>
</Row> </Row>
</ButtonDropwdownLight> ) : (
)} <Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
Select a Token
</Text>
)}
</ButtonDropdownLight>
<ColumnCenter> <ColumnCenter>
<Plus size="16" color="#888D9B" /> <Plus size="16" color="#888D9B" />
</ColumnCenter> </ColumnCenter>
{!token1Address ? (
<ButtonDropwdown <ButtonDropdownLight
onClick={() => { onClick={() => {
setShowSearch(true) setShowSearch(true)
setActiveField(Fields.TOKEN1) setActiveField(Fields.TOKEN1)
}} }}
> >
<Text fontSize={20}>Select second token</Text> {token1 ? (
</ButtonDropwdown>
) : (
<ButtonDropwdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN1)
}}
>
<Row> <Row>
<TokenLogo address={token1Address} /> <TokenLogo address={token1Address} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}> <Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token1?.symbol} {token1.symbol}
</Text> </Text>
</Row> </Row>
</ButtonDropwdownLight> ) : (
)} <Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{allowImport && ( Select a Token
</Text>
)}
</ButtonDropdownLight>
{poolImported && (
<ColumnCenter <ColumnCenter
style={{ justifyItems: 'center', backgroundColor: '', padding: '12px 0px', borderRadius: '12px' }} style={{ justifyItems: 'center', backgroundColor: '', padding: '12px 0px', borderRadius: '12px' }}
> >
<Text textAlign="center" fontWeight={500} color=""> <Text textAlign="center" fontWeight={500} color="">
Pool Imported! Pool Found!
</Text> </Text>
</ColumnCenter> </ColumnCenter>
)} )}
{position ? ( {position ? (
!JSBI.equal(position.raw, JSBI.BigInt(0)) ? ( poolImported ? (
<PositionCard pair={pair} minimal={true} border="1px solid #CED0D9" /> <PositionCard pair={pair} minimal={true} border="1px solid #CED0D9" />
) : ( ) : (
<LightCard padding="45px 10px"> <LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center"> <AutoColumn gap="sm" justify="center">
<Text textAlign="center">Pool found, you don’t have liquidity on this pair yet.</Text> <Text textAlign="center">You don’t have liquidity in this pool yet.</Text>
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}> <StyledInternalLink to={`/add/${token0.address}-${token1.address}`}>
<Text textAlign="center">Add liquidity to this pair instead.</Text> <Text textAlign="center">Add liquidity?</Text>
</StyledInternalLink> </StyledInternalLink>
</AutoColumn> </AutoColumn>
</LightCard> </LightCard>
) )
) : newPair ? ( ) : newPair ? (
<LightCard padding="45px"> <LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center"> <AutoColumn gap="sm" justify="center">
<Text color="">No pool found.</Text> <Text textAlign="center">No pool found.</Text>
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Create pool?</StyledInternalLink> <StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Create pool?</StyledInternalLink>
</AutoColumn> </AutoColumn>
</LightCard> </LightCard>
) : ( ) : (
<LightCard padding={'45px'}> <LightCard padding="45px 10px">
<Text color="#C3C5CB" textAlign="center"> <Text textAlign="center">
Select a token pair to find your liquidity. {!account ? 'Connect to a wallet to find pools' : 'Select a token to find your liquidity.'}
</Text> </Text>
</LightCard> </LightCard>
)} )}
<ButtonPrimary disabled={!allowImport} onClick={() => history.goBack()}>
<Text fontWeight={500} fontSize={20}>
Close
</Text>
</ButtonPrimary>
</AutoColumn> </AutoColumn>
<TokenSearchModal <TokenSearchModal
isOpen={showSearch} isOpen={showSearch}
onTokenSelect={handleTokenSelect} onTokenSelect={handleTokenSelect}
......
...@@ -44,10 +44,10 @@ export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [ ...@@ -44,10 +44,10 @@ export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [
/** /**
* Returns a map of token addresses to their eventually consistent token balances for a single account. * Returns a map of token addresses to their eventually consistent token balances for a single account.
*/ */
export function useTokenBalances( export function useTokenBalancesWithLoadingIndicator(
address?: string, address?: string,
tokens?: (Token | undefined)[] tokens?: (Token | undefined)[]
): { [tokenAddress: string]: TokenAmount | undefined } { ): [{ [tokenAddress: string]: TokenAmount | undefined }, boolean] {
const validatedTokens: Token[] = useMemo( const validatedTokens: Token[] = useMemo(
() => tokens?.filter((t?: Token): t is Token => isAddress(t?.address) !== false) ?? [], () => tokens?.filter((t?: Token): t is Token => isAddress(t?.address) !== false) ?? [],
[tokens] [tokens]
...@@ -57,20 +57,32 @@ export function useTokenBalances( ...@@ -57,20 +57,32 @@ export function useTokenBalances(
const balances = useMultipleContractSingleData(validatedTokenAddresses, ERC20_INTERFACE, 'balanceOf', [address]) const balances = useMultipleContractSingleData(validatedTokenAddresses, ERC20_INTERFACE, 'balanceOf', [address])
return useMemo( const anyLoading = balances.some(callState => callState.loading)
() =>
address && validatedTokens.length > 0 return [
? validatedTokens.reduce<{ [tokenAddress: string]: TokenAmount | undefined }>((memo, token, i) => { useMemo(
const value = balances?.[i]?.result?.[0] () =>
const amount = value ? JSBI.BigInt(value.toString()) : undefined address && validatedTokens.length > 0
if (amount) { ? validatedTokens.reduce<{ [tokenAddress: string]: TokenAmount | undefined }>((memo, token, i) => {
memo[token.address] = new TokenAmount(token, amount) const value = balances?.[i]?.result?.[0]
} const amount = value ? JSBI.BigInt(value.toString()) : undefined
return memo if (amount) {
}, {}) memo[token.address] = new TokenAmount(token, amount)
: {}, }
[address, validatedTokens, balances] return memo
) }, {})
: {},
[address, validatedTokens, balances]
),
anyLoading
]
}
export function useTokenBalances(
address?: string,
tokens?: (Token | undefined)[]
): { [tokenAddress: string]: TokenAmount | undefined } {
return useTokenBalancesWithLoadingIndicator(address, tokens)[0]
} }
// contains the hacky logic to treat the WETH token input as if it's ETH to // contains the hacky logic to treat the WETH token input as if it's ETH to
......
...@@ -169,9 +169,15 @@ export const TYPE = { ...@@ -169,9 +169,15 @@ export const TYPE = {
export const FixedGlobalStyle = createGlobalStyle` export const FixedGlobalStyle = createGlobalStyle`
@import url('https://rsms.me/inter/inter.css'); @import url('https://rsms.me/inter/inter.css');
html, body, input, textarea, button { font-family: 'Inter', sans-serif; letter-spacing: -0.018em;}
html, input, textarea, button {
font-family: 'Inter', sans-serif;
letter-spacing: -0.018em;
}
@supports (font-variation-settings: normal) { @supports (font-variation-settings: normal) {
html, body, input, textarea, button { font-family: 'Inter var', sans-serif; } html, input, textarea, button {
font-family: 'Inter var', sans-serif;
}
} }
html, html,
......
...@@ -5,7 +5,8 @@ import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers' ...@@ -5,7 +5,8 @@ import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { abi as IUniswapV2Router02ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router02.json' import { abi as IUniswapV2Router02ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router02.json'
import { ROUTER_ADDRESS } from '../constants' import { ROUTER_ADDRESS } from '../constants'
import { ChainId, JSBI, Percent, TokenAmount } from '@uniswap/sdk' import { ALL_TOKENS } from '../constants/tokens'
import { ChainId, JSBI, Percent, TokenAmount, Token } from '@uniswap/sdk'
// returns the checksummed address if the address is valid, otherwise returns false // returns the checksummed address if the address is valid, otherwise returns false
export function isAddress(value: any): string | false { export function isAddress(value: any): string | false {
...@@ -94,3 +95,12 @@ export function getRouterContract(_: number, library: Web3Provider, account?: st ...@@ -94,3 +95,12 @@ export function getRouterContract(_: number, library: Web3Provider, account?: st
export function escapeRegExp(string: string): string { export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
} }
export function isDefaultToken(token?: Token): boolean {
return Boolean(token && ALL_TOKENS[token.chainId]?.[token.address])
}
export function isCustomAddedToken(allTokens: { [address: string]: Token }, token?: Token): boolean {
const isDefault = isDefaultToken(token)
return Boolean(token && allTokens[token.address] && !isDefault)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment