Commit c1d35cc8 authored by Moody Salem's avatar Moody Salem Committed by GitHub

feat(migrate): adds the migrate flow to the uniswap exchange site

* links and page

* print all the details of the liquidity

* show working approve/migrate buttons

* testnet v1 factory addresses

* split code up into two pages

* getting closer to styled

* compute min amount out of eth and token

* compute min amount out of eth and token

* add a back button to the list page

* Improve empty states

* Improve the state management

* change the display of the migrate page to be more similar to original

* style fix, pending transaction hook fix

* add forwarding to netlify.toml

* handle case where v2 spot price does not exist because pair does not exist

* make ternaries more accurate

* handle first liquidity provider situation

* Style tweaks for migrate

* merge

* Address feedback
- show pool token amount
- show success when migration complete
- show price warning only if price difference is large
Co-authored-by: default avatarCallil Capuozzo <callil.capuozzo@gmail.com>
parent f279b2be
...@@ -7,6 +7,13 @@ ...@@ -7,6 +7,13 @@
conditions = {Country=["BY","CU","IR","IQ","CI","LR","KP","SD","SY","ZW"]} conditions = {Country=["BY","CU","IR","IQ","CI","LR","KP","SD","SY","ZW"]}
headers = {Link="<https://uniswap.exchange>"} headers = {Link="<https://uniswap.exchange>"}
# forward migrate
[[redirects]]
from = "https://migrate.uniswap.exchange/*"
to = "https://uniswap.exchange/migrate/v1"
status = 301
force = true
# forward v2 subdomain to apex # forward v2 subdomain to apex
[[redirects]] [[redirects]]
from = "https://v2.uniswap.exchange/*" from = "https://v2.uniswap.exchange/*"
......
...@@ -8,7 +8,7 @@ import Row from '../Row' ...@@ -8,7 +8,7 @@ import Row from '../Row'
import Menu from '../Menu' import Menu from '../Menu'
import Web3Status from '../Web3Status' import Web3Status from '../Web3Status'
import { ExternalLink } from '../../theme' import { ExternalLink, StyledInternalLink } from '../../theme'
import { Text } from 'rebass' import { Text } from 'rebass'
import { WETH, ChainId } from '@uniswap/sdk' import { WETH, ChainId } from '@uniswap/sdk'
import { isMobile } from 'react-device-detect' import { isMobile } from 'react-device-detect'
...@@ -163,9 +163,9 @@ export default function Header() { ...@@ -163,9 +163,9 @@ export default function Header() {
<b>blog post ↗</b> <b>blog post ↗</b>
</ExternalLink> </ExternalLink>
&nbsp;or&nbsp; &nbsp;or&nbsp;
<ExternalLink href="https://migrate.uniswap.exchange/"> <StyledInternalLink to="/migrate/v1">
<b>migrate your liquidity ↗</b> <b>migrate your liquidity ↗</b>
</ExternalLink> </StyledInternalLink>
. .
</MigrateBanner> </MigrateBanner>
<RowBetween padding="1rem"> <RowBetween padding="1rem">
......
[
{
"inputs": [
{
"internalType": "address",
"name": "_factoryV1",
"type": "address"
},
{
"internalType": "address",
"name": "_router",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "migrate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]
\ No newline at end of file
import MIGRATOR_ABI from './migrator.json'
const MIGRATOR_ADDRESS = '0x16D4F26C15f3658ec65B1126ff27DD3dF2a2996b'
export { MIGRATOR_ADDRESS, MIGRATOR_ABI }
import { Interface } from '@ethersproject/abi' import { Interface } from '@ethersproject/abi'
import { ChainId } from '@uniswap/sdk'
import V1_EXCHANGE_ABI from './v1_exchange.json' import V1_EXCHANGE_ABI from './v1_exchange.json'
import V1_FACTORY_ABI from './v1_factory.json' import V1_FACTORY_ABI from './v1_factory.json'
const V1_FACTORY_ADDRESS = '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95' const V1_FACTORY_ADDRESSES: { [chainId in ChainId]: string } = {
[ChainId.MAINNET]: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
[ChainId.ROPSTEN]: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351',
[ChainId.RINKEBY]: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36',
[ChainId.GÖRLI]: '0x6Ce570d02D73d4c384b46135E87f8C592A8c86dA',
[ChainId.KOVAN]: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30'
}
const V1_FACTORY_INTERFACE = new Interface(V1_FACTORY_ABI) const V1_FACTORY_INTERFACE = new Interface(V1_FACTORY_ABI)
const V1_EXCHANGE_INTERFACE = new Interface(V1_EXCHANGE_ABI) const V1_EXCHANGE_INTERFACE = new Interface(V1_EXCHANGE_ABI)
export { V1_FACTORY_ADDRESS, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI } export { V1_FACTORY_ADDRESSES, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI }
...@@ -30,42 +30,39 @@ function useMockV1Pair(token?: Token): MockV1Pair | undefined { ...@@ -30,42 +30,39 @@ function useMockV1Pair(token?: Token): MockV1Pair | undefined {
: undefined : undefined
} }
// returns ALL v1 exchange addresses
export function useAllV1ExchangeAddresses(): string[] {
const factory = useV1FactoryContract()
const exchangeCount = useSingleCallResult(factory, 'tokenCount')?.result
const parsedCount = parseInt(exchangeCount?.toString() ?? '0')
const indices = useMemo(() => [...Array(parsedCount).keys()].map(ix => [ix]), [parsedCount])
const data = useSingleContractMultipleData(factory, 'getTokenWithId', indices, NEVER_RELOAD)
return useMemo(() => data?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data])
}
// returns all v1 exchange addresses in the user's token list // returns all v1 exchange addresses in the user's token list
export function useAllTokenV1ExchangeAddresses(): string[] { export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
const allTokens = useAllTokens() const allTokens = useAllTokens()
const factory = useV1FactoryContract() const factory = useV1FactoryContract()
const args = useMemo(() => Object.keys(allTokens).map(tokenAddress => [tokenAddress]), [allTokens]) const args = useMemo(() => Object.keys(allTokens).map(tokenAddress => [tokenAddress]), [allTokens])
const data = useSingleContractMultipleData(factory, 'getExchange', args, NEVER_RELOAD) const data = useSingleContractMultipleData(factory, 'getExchange', args, NEVER_RELOAD)
return useMemo(() => data?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data]) return useMemo(
() =>
data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => {
const token = allTokens[args[ix][0]]
if (result?.[0]) {
memo[result?.[0]] = token
}
return memo
}, {}) ?? {},
[allTokens, args, data]
)
} }
// 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 useUserProbablyHasV1Liquidity(): boolean | undefined { export function useUserHasLiquidityInAllTokens(): boolean | undefined {
const exchangeAddresses = useAllTokenV1ExchangeAddresses() const exchanges = useAllTokenV1Exchanges()
const { account, chainId } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
const fakeTokens = useMemo( const fakeLiquidityTokens = useMemo(
() => (chainId ? exchangeAddresses.map(address => new Token(chainId, address, 18, 'UNI-V1')) : []), () => (chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
[chainId, exchangeAddresses] [chainId, exchanges]
) )
const balances = useTokenBalances(account ?? undefined, fakeTokens) const balances = useTokenBalances(account ?? undefined, fakeLiquidityTokens)
return useMemo( return useMemo(
() => () =>
......
import { Web3Provider } from '@ethersproject/providers' import { Web3Provider } from '@ethersproject/providers'
import { ChainId } from '@uniswap/sdk'
import { useWeb3React as useWeb3ReactCore } from '@web3-react/core' import { useWeb3React as useWeb3ReactCore } from '@web3-react/core'
import { Web3ReactContextInterface } from '@web3-react/core/dist/types'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { isMobile } from 'react-device-detect' import { isMobile } from 'react-device-detect'
import { injected } from '../connectors' import { injected } from '../connectors'
import { NetworkContextName } from '../constants' import { NetworkContextName } from '../constants'
export function useActiveWeb3React() { export function useActiveWeb3React(): Web3ReactContextInterface<Web3Provider> & { chainId?: ChainId } {
const context = useWeb3ReactCore<Web3Provider>() const context = useWeb3ReactCore<Web3Provider>()
const contextNetwork = useWeb3ReactCore<Web3Provider>(NetworkContextName) const contextNetwork = useWeb3ReactCore<Web3Provider>(NetworkContextName)
return context.active ? context : contextNetwork return context.active ? context : contextNetwork
......
...@@ -3,7 +3,8 @@ import { ChainId } from '@uniswap/sdk' ...@@ -3,7 +3,8 @@ import { ChainId } from '@uniswap/sdk'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json' import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { useMemo } from 'react' import { useMemo } from 'react'
import ERC20_ABI from '../constants/abis/erc20.json' import ERC20_ABI from '../constants/abis/erc20.json'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESS } from '../constants/v1' import { MIGRATOR_ABI, MIGRATOR_ADDRESS } from '../constants/abis/migrator'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall' import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
import { getContract } from '../utils' import { getContract } from '../utils'
import { useActiveWeb3React } from './index' import { useActiveWeb3React } from './index'
...@@ -25,13 +26,17 @@ function useContract(address?: string, ABI?: any, withSignerIfPossible = true): ...@@ -25,13 +26,17 @@ function useContract(address?: string, ABI?: any, withSignerIfPossible = true):
export function useV1FactoryContract(): Contract | null { export function useV1FactoryContract(): Contract | null {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
return useContract(chainId === 1 ? V1_FACTORY_ADDRESS : undefined, V1_FACTORY_ABI, false) return useContract(V1_FACTORY_ADDRESSES[chainId as ChainId], V1_FACTORY_ABI, false)
} }
export function useV1ExchangeContract(address: string): Contract | null { export function useV1ExchangeContract(address: string): Contract | null {
return useContract(address, V1_EXCHANGE_ABI, false) return useContract(address, V1_EXCHANGE_ABI, false)
} }
export function useV2MigratorContract(): Contract | null {
return useContract(MIGRATOR_ADDRESS, MIGRATOR_ABI, true)
}
export function useTokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null { export function useTokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible) return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
} }
......
...@@ -9,6 +9,8 @@ import Web3ReactManager from '../components/Web3ReactManager' ...@@ -9,6 +9,8 @@ import Web3ReactManager from '../components/Web3ReactManager'
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader' import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
import AddLiquidity from './AddLiquidity' import AddLiquidity from './AddLiquidity'
import CreatePool from './CreatePool' import CreatePool from './CreatePool'
import MigrateV1 from './MigrateV1'
import MigrateV1Exchange from './MigrateV1/MigrateV1Exchange'
import Pool from './Pool' import Pool from './Pool'
import PoolFinder from './PoolFinder' import PoolFinder from './PoolFinder'
import RemoveLiquidity from './RemoveLiquidity' import RemoveLiquidity from './RemoveLiquidity'
...@@ -99,6 +101,8 @@ export default function App() { ...@@ -99,6 +101,8 @@ export default function App() {
<Route exact strict path="/create" component={CreatePool} /> <Route exact strict path="/create" component={CreatePool} />
<Route exact strict path="/add/:tokens" component={AddLiquidity} /> <Route exact strict path="/add/:tokens" component={AddLiquidity} />
<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/:address" component={MigrateV1Exchange} />
<Route component={RedirectPathToSwapOnly} /> <Route component={RedirectPathToSwapOnly} />
</Switch> </Switch>
</Web3ReactManager> </Web3ReactManager>
......
...@@ -2,7 +2,7 @@ import React from 'react' ...@@ -2,7 +2,7 @@ import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import NavigationTabs from '../components/NavigationTabs' import NavigationTabs from '../components/NavigationTabs'
const Body = styled.div` export const BodyWrapper = styled.div`
position: relative; position: relative;
max-width: 420px; max-width: 420px;
width: 100%; width: 100%;
...@@ -18,9 +18,9 @@ const Body = styled.div` ...@@ -18,9 +18,9 @@ const Body = styled.div`
*/ */
export default function AppBody({ children }: { children: React.ReactNode }) { export default function AppBody({ children }: { children: React.ReactNode }) {
return ( return (
<Body> <BodyWrapper>
<NavigationTabs /> <NavigationTabs />
<>{children}</> <>{children}</>
</Body> </BodyWrapper>
) )
} }
import React from 'react'
import { AutoColumn } from '../../components/Column'
import { TYPE } from '../../theme'
export function EmptyState({ message }: { message: string }) {
return (
<AutoColumn style={{ minHeight: 200, justifyContent: 'center', alignItems: 'center' }}>
<TYPE.body>{message}</TYPE.body>
</AutoColumn>
)
}
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { ChainId, Fraction, JSBI, Percent, Token, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useMemo, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import { Redirect, RouteComponentProps } from 'react-router'
import { ButtonConfirmed } from '../../components/Button'
import { PinkCard, YellowCard, LightCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import QuestionHelper from '../../components/QuestionHelper'
import { AutoRow, RowBetween } from '../../components/Row'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { MIGRATOR_ADDRESS } from '../../constants/abis/migrator'
import { usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { useActiveWeb3React } from '../../hooks'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
import { useV1ExchangeContract, useV2MigratorContract } from '../../hooks/useContract'
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks'
import { useETHBalances, useTokenBalance } from '../../state/wallet/hooks'
import { TYPE } from '../../theme'
import { isAddress } from '../../utils'
import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState'
import TokenLogo from '../../components/TokenLogo'
import { FormattedPoolTokenAmount } from './index'
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)
const ALLOWED_OUTPUT_MIN_PERCENT = new Percent(JSBI.BigInt(10000 - INITIAL_ALLOWED_SLIPPAGE), JSBI.BigInt(10000))
function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount: TokenAmount; token: Token }) {
const { account, chainId } = useActiveWeb3React()
const totalSupply = useTotalSupply(liquidityTokenAmount.token)
const exchangeETHBalance = useETHBalances([liquidityTokenAmount.token.address])?.[liquidityTokenAmount.token.address]
const exchangeTokenBalance = useTokenBalance(liquidityTokenAmount.token.address, token)
const v2Pair = usePair(WETH[chainId as ChainId], token)
const isFirstLiquidityProvider: boolean = v2Pair === null
const v2SpotPrice = v2Pair?.reserveOf(token)?.divide(v2Pair?.reserveOf(WETH[chainId as ChainId]))
const [confirmingMigration, setConfirmingMigration] = useState<boolean>(false)
const [pendingMigrationHash, setPendingMigrationHash] = 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 [approval, approve] = useApproveCallback(liquidityTokenAmount, MIGRATOR_ADDRESS)
const v1SpotPrice =
exchangeTokenBalance && exchangeETHBalance
? exchangeTokenBalance.divide(new Fraction(exchangeETHBalance, WEI_DENOM))
: null
const priceDifferenceFraction: Fraction | undefined =
v1SpotPrice && v2SpotPrice
? v1SpotPrice
.divide(v2SpotPrice)
.multiply('100')
.subtract('100')
: undefined
const priceDifferenceAbs: Fraction | undefined = priceDifferenceFraction?.lessThan(ZERO)
? priceDifferenceFraction?.multiply('-1')
: priceDifferenceFraction
const minAmountETH: JSBI | undefined =
v2SpotPrice && tokenWorth
? tokenWorth
.divide(v2SpotPrice)
.multiply(WEI_DENOM)
.multiply(ALLOWED_OUTPUT_MIN_PERCENT).quotient
: ethWorth?.numerator
const minAmountToken: JSBI | undefined =
v2SpotPrice && ethWorth
? ethWorth
.multiply(v2SpotPrice)
.multiply(JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(token.decimals)))
.multiply(ALLOWED_OUTPUT_MIN_PERCENT).quotient
: tokenWorth?.numerator
const addTransaction = useTransactionAdder()
const isMigrationPending = useIsTransactionPending(pendingMigrationHash)
const migrator = useV2MigratorContract()
const migrate = useCallback(() => {
if (!minAmountToken || !minAmountETH) return
setConfirmingMigration(true)
migrator
.migrate(
token.address,
minAmountToken.toString(),
minAmountETH.toString(),
account,
Math.floor(new Date().getTime() / 1000) + DEFAULT_DEADLINE_FROM_NOW
)
.then((response: TransactionResponse) => {
addTransaction(response, {
summary: `Migrate ${token.symbol} liquidity to V2`
})
setPendingMigrationHash(response.hash)
})
.catch(() => {
setConfirmingMigration(false)
})
}, [minAmountToken, minAmountETH, migrator, token.address, token.symbol, account, addTransaction])
const noLiquidityTokens = liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
const largePriceDifference = Boolean(priceDifferenceAbs && !priceDifferenceAbs.lessThan(JSBI.BigInt(5)))
const isSuccessfullyMigrated = Boolean(noLiquidityTokens && pendingMigrationHash)
return (
<AutoColumn gap="20px">
{!isFirstLiquidityProvider ? (
largePriceDifference ? (
<YellowCard>
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
It is best to deposit liquidity into Uniswap V2 at a price you believe is correct. If you believe the
price is incorrect, you can either make a swap to move the price or wait for someone else to do so.
</TYPE.body>
<AutoColumn gap="8px">
<RowBetween>
<TYPE.body>V1 Price:</TYPE.body>
<TYPE.black>
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
</TYPE.black>
</RowBetween>
<RowBetween>
<TYPE.body>V2 Price:</TYPE.body>
<TYPE.black>
{v2SpotPrice?.toSignificant(6)} {token.symbol}/ETH
</TYPE.black>
</RowBetween>
<RowBetween>
<div>Price Difference:</div>
<div>{priceDifferenceAbs.toSignificant(4)}%</div>
</RowBetween>
</AutoColumn>
</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}
</div>
<div>
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
</div>
</AutoColumn>
</AutoColumn>
</PinkCard>
)}
<LightCard>
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
<TokenLogo size="24px" address={token.address} />{' '}
<div style={{ marginLeft: '.75rem' }}>
<TYPE.mediumHeader>
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />} {token.symbol} Pool Tokens
</TYPE.mediumHeader>
</div>
</AutoRow>
<div style={{ display: 'flex', marginTop: '1rem' }}>
<AutoColumn gap="12px" style={{ flex: '1', marginRight: 12 }}>
<ButtonConfirmed
confirmed={approval === ApprovalState.APPROVED}
disabled={approval !== ApprovalState.NOT_APPROVED}
onClick={approve}
>
{approval === ApprovalState.PENDING
? 'Approving...'
: approval === ApprovalState.APPROVED
? 'Approved'
: 'Approve'}
</ButtonConfirmed>
</AutoColumn>
<AutoColumn gap="12px" style={{ flex: '1' }}>
<ButtonConfirmed
confirmed={isSuccessfullyMigrated}
disabled={
isSuccessfullyMigrated ||
noLiquidityTokens ||
isMigrationPending ||
approval !== ApprovalState.APPROVED ||
confirmingMigration
}
onClick={migrate}
>
{isSuccessfullyMigrated ? 'Success' : isMigrationPending ? 'Migrating...' : 'Migrate'}
</ButtonConfirmed>
</AutoColumn>
</div>
</LightCard>
<TYPE.darkGray style={{ textAlign: 'center' }}>
{'Your ' + token.symbol + ' liquidity will become Uniswap V2 ' + token.symbol + '/ETH liquidity.'}
</TYPE.darkGray>
</AutoColumn>
)
}
export default function MigrateV1Exchange({
history,
match: {
params: { address }
}
}: RouteComponentProps<{ address: string }>) {
const validated = isAddress(address)
const { chainId, account } = useActiveWeb3React()
const exchangeContract = useV1ExchangeContract(validated ? validated : undefined)
const tokenAddress = useSingleCallResult(exchangeContract, 'tokenAddress', undefined, NEVER_RELOAD)?.result?.[0]
const token = useTokenByAddressAndAutomaticallyAdd(tokenAddress)
const liquidityToken: Token | undefined = useMemo(
() => (validated && token ? new Token(chainId, validated, 18, `UNI-V1-${token.symbol}`) : undefined),
[chainId, token, validated]
)
const userLiquidityBalance = useTokenBalance(account, liquidityToken)
const handleBack = useCallback(() => {
history.push('/migrate/v1')
}, [history])
if (!validated) {
console.error('Invalid address in path', address)
return <Redirect to="/migrate/v1" />
}
if (!account) {
return (
<BodyWrapper>
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
</BodyWrapper>
)
}
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>Migrate {token?.symbol} Pool Tokens</TYPE.mediumHeader>
<div>
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />
</div>
</AutoRow>
{userLiquidityBalance && token ? (
<V1PairMigration liquidityTokenAmount={userLiquidityBalance} token={token} />
) : (
<EmptyState message="Loading..." />
)}
</AutoColumn>
</BodyWrapper>
)
}
import { JSBI, Token } from '@uniswap/sdk' import { Fraction, JSBI, Token, TokenAmount } from '@uniswap/sdk'
import React, { useMemo } from 'react' import React, { useCallback, useContext, useMemo, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import { RouteComponentProps } from 'react-router' import { RouteComponentProps } from 'react-router'
import { useAllV1ExchangeAddresses } from '../../data/V1' import { ThemeContext } from 'styled-components'
import { ButtonPrimary } from '../../components/Button'
import { AutoColumn } from '../../components/Column'
import { AutoRow } from '../../components/Row'
import { SearchInput } from '../../components/SearchModal/styleds'
import TokenLogo from '../../components/TokenLogo'
import { useAllTokenV1Exchanges } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useWalletModalToggle } from '../../state/application/hooks'
import { useTokenBalances } from '../../state/wallet/hooks' import { useTokenBalances } from '../../state/wallet/hooks'
import { TYPE } from '../../theme'
import { GreyCard } from '../../components/Card'
import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState'
const PLACEHOLDER_ACCOUNT = ( const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
<div>
<h1>You must connect a wallet to use this tool.</h1> export function FormattedPoolTokenAmount({ tokenAmount }: { tokenAmount: TokenAmount }) {
</div> return (
) <>
{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) {
* Page component for migrating liquidity from V1
*/
export default function MigrateV1({}: RouteComponentProps) {
const { account, chainId } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
const v1ExchangeAddresses = useAllV1ExchangeAddresses() const allV1Exchanges = useAllTokenV1Exchanges()
const v1LiquidityTokens: Token[] = useMemo(() => {
return Object.keys(allV1Exchanges).map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
}, [chainId, allV1Exchanges])
const v1ExchangeTokens: Token[] = useMemo(() => { const v1LiquidityBalances = useTokenBalances(account, v1LiquidityTokens)
return v1ExchangeAddresses.map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
}, [chainId, v1ExchangeAddresses])
const tokenBalances = useTokenBalances(account, v1ExchangeTokens) const [tokenSearch, setTokenSearch] = useState<string>('')
const handleTokenSearchChange = useCallback(e => setTokenSearch(e.target.value), [setTokenSearch])
const unmigratedExchangeAddresses = useMemo( const searchedToken: Token | undefined = useTokenByAddressAndAutomaticallyAdd(tokenSearch)
const unmigratedLiquidityExchangeAddresses: TokenAmount[] = useMemo(
() => () =>
Object.keys(tokenBalances).filter(tokenAddress => Object.keys(v1LiquidityBalances)
tokenBalances[tokenAddress] ? JSBI.greaterThan(tokenBalances[tokenAddress]?.raw, JSBI.BigInt(0)) : false .filter(tokenAddress =>
), v1LiquidityBalances[tokenAddress]
[tokenBalances] ? JSBI.greaterThan(v1LiquidityBalances[tokenAddress]?.raw, JSBI.BigInt(0))
: false
) )
.map(tokenAddress => v1LiquidityBalances[tokenAddress])
if (!account) { .sort((a1, a2) => {
return PLACEHOLDER_ACCOUNT 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 theme = useContext(ThemeContext)
const toggleWalletModal = useWalletModalToggle()
return <div>{unmigratedExchangeAddresses?.join('\n')}</div> const handleBackClick = useCallback(() => {
history.push('/pool')
}, [history])
return (
<BodyWrapper style={{ maxWidth: 450, padding: 24 }}>
<AutoColumn gap="24px">
<AutoRow style={{ justifyContent: 'space-between' }}>
<div>
<ArrowLeft style={{ cursor: 'pointer' }} onClick={handleBackClick} />
</div>
<TYPE.largeHeader>Migrate Liquidity</TYPE.largeHeader>
<div></div>
</AutoRow>
<GreyCard style={{ marginTop: '0', padding: 0, display: 'inline-block' }}>
<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>
{unmigratedLiquidityExchangeAddresses.map(poolTokenAmount => (
<div
key={poolTokenAmount.token.address}
style={{ borderRadius: '20px', padding: 16, backgroundColor: theme.bg2 }}
>
<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 ? (
<EmptyState message="No V1 Liquidity found." />
) : null}
{!account ? <ButtonPrimary onClick={toggleWalletModal}>Connect to a wallet</ButtonPrimary> : null}
</AutoColumn>
</BodyWrapper>
)
} }
...@@ -6,9 +6,9 @@ import { RouteComponentProps } from 'react-router-dom' ...@@ -6,9 +6,9 @@ import { RouteComponentProps } from 'react-router-dom'
import Question from '../../components/QuestionHelper' import Question from '../../components/QuestionHelper'
import SearchModal from '../../components/SearchModal' import SearchModal from '../../components/SearchModal'
import PositionCard from '../../components/PositionCard' import PositionCard from '../../components/PositionCard'
import { useUserProbablyHasV1Liquidity } from '../../data/V1' import { useUserHasLiquidityInAllTokens } from '../../data/V1'
import { useTokenBalances } from '../../state/wallet/hooks' import { useTokenBalances } from '../../state/wallet/hooks'
import { ExternalLink, 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'
import { RowBetween } from '../../components/Row' import { RowBetween } from '../../components/Row'
...@@ -59,7 +59,7 @@ export default function Pool({ history }: RouteComponentProps) { ...@@ -59,7 +59,7 @@ export default function Pool({ history }: RouteComponentProps) {
return <PositionCardWrapper key={i} dummyPair={pair} /> return <PositionCardWrapper key={i} dummyPair={pair} />
}) })
const hasV1Liquidity = useUserProbablyHasV1Liquidity() const hasV1Liquidity = useUserHasLiquidityInAllTokens()
return ( return (
<AppBody> <AppBody>
...@@ -103,9 +103,9 @@ export default function Pool({ history }: RouteComponentProps) { ...@@ -103,9 +103,9 @@ export default function Pool({ history }: RouteComponentProps) {
</StyledInternalLink> </StyledInternalLink>
</> </>
) : ( ) : (
<ExternalLink href="https://migrate.uniswap.exchange" id="migrate-v1-liquidity-link"> <StyledInternalLink id="migrate-v1-liquidity-link" to="/migrate/v1">
Migrate your V1 liquidity. Migrate your V1 liquidity.
</ExternalLink> </StyledInternalLink>
)} )}
</Text> </Text>
</AutoColumn> </AutoColumn>
......
...@@ -42,6 +42,14 @@ export function useAllTransactions(): { [txHash: string]: TransactionDetails } { ...@@ -42,6 +42,14 @@ export function useAllTransactions(): { [txHash: string]: TransactionDetails } {
return state[chainId ?? -1] ?? {} return state[chainId ?? -1] ?? {}
} }
export function useIsTransactionPending(transactionHash?: string): boolean {
const transactions = useAllTransactions()
if (!transactionHash || !transactions[transactionHash]) return false
return !transactions[transactionHash].receipt
}
// returns whether a token has a pending approval transaction // returns whether a token has a pending approval transaction
export function useHasPendingApproval(tokenAddress?: string): boolean { export function useHasPendingApproval(tokenAddress?: string): boolean {
const allTransactions = useAllTransactions() const allTransactions = useAllTransactions()
......
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