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">
......
This diff is collapsed.
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