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: Callil Capuozzo <callil.capuozzo@gmail.com>
This commit is contained in:
parent
f279b2bea2
commit
c1d35cc8b3
@ -7,6 +7,13 @@
|
||||
conditions = {Country=["BY","CU","IR","IQ","CI","LR","KP","SD","SY","ZW"]}
|
||||
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
|
||||
[[redirects]]
|
||||
from = "https://v2.uniswap.exchange/*"
|
||||
|
@ -8,7 +8,7 @@ import Row from '../Row'
|
||||
import Menu from '../Menu'
|
||||
import Web3Status from '../Web3Status'
|
||||
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { ExternalLink, StyledInternalLink } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { WETH, ChainId } from '@uniswap/sdk'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
@ -163,9 +163,9 @@ export default function Header() {
|
||||
<b>blog post ↗</b>
|
||||
</ExternalLink>
|
||||
or
|
||||
<ExternalLink href="https://migrate.uniswap.exchange/">
|
||||
<StyledInternalLink to="/migrate/v1">
|
||||
<b>migrate your liquidity ↗</b>
|
||||
</ExternalLink>
|
||||
</StyledInternalLink>
|
||||
.
|
||||
</MigrateBanner>
|
||||
<RowBetween padding="1rem">
|
||||
|
55
src/constants/abis/migrator.json
Normal file
55
src/constants/abis/migrator.json
Normal file
@ -0,0 +1,55 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
5
src/constants/abis/migrator.ts
Normal file
5
src/constants/abis/migrator.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import MIGRATOR_ABI from './migrator.json'
|
||||
|
||||
const MIGRATOR_ADDRESS = '0x16D4F26C15f3658ec65B1126ff27DD3dF2a2996b'
|
||||
|
||||
export { MIGRATOR_ADDRESS, MIGRATOR_ABI }
|
@ -1,9 +1,17 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import V1_EXCHANGE_ABI from './v1_exchange.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_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 {
|
||||
: 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
|
||||
export function useAllTokenV1ExchangeAddresses(): string[] {
|
||||
export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
|
||||
const allTokens = useAllTokens()
|
||||
const factory = useV1FactoryContract()
|
||||
const args = useMemo(() => Object.keys(allTokens).map(tokenAddress => [tokenAddress]), [allTokens])
|
||||
|
||||
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
|
||||
export function useUserProbablyHasV1Liquidity(): boolean | undefined {
|
||||
const exchangeAddresses = useAllTokenV1ExchangeAddresses()
|
||||
export function useUserHasLiquidityInAllTokens(): boolean | undefined {
|
||||
const exchanges = useAllTokenV1Exchanges()
|
||||
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const fakeTokens = useMemo(
|
||||
() => (chainId ? exchangeAddresses.map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
|
||||
[chainId, exchangeAddresses]
|
||||
const fakeLiquidityTokens = useMemo(
|
||||
() => (chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
|
||||
[chainId, exchanges]
|
||||
)
|
||||
|
||||
const balances = useTokenBalances(account ?? undefined, fakeTokens)
|
||||
const balances = useTokenBalances(account ?? undefined, fakeLiquidityTokens)
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import { useWeb3React as useWeb3ReactCore } from '@web3-react/core'
|
||||
import { Web3ReactContextInterface } from '@web3-react/core/dist/types'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { injected } from '../connectors'
|
||||
import { NetworkContextName } from '../constants'
|
||||
|
||||
export function useActiveWeb3React() {
|
||||
export function useActiveWeb3React(): Web3ReactContextInterface<Web3Provider> & { chainId?: ChainId } {
|
||||
const context = useWeb3ReactCore<Web3Provider>()
|
||||
const contextNetwork = useWeb3ReactCore<Web3Provider>(NetworkContextName)
|
||||
return context.active ? context : contextNetwork
|
||||
|
@ -3,7 +3,8 @@ import { ChainId } from '@uniswap/sdk'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { useMemo } from 'react'
|
||||
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 { getContract } from '../utils'
|
||||
import { useActiveWeb3React } from './index'
|
||||
@ -25,13 +26,17 @@ function useContract(address?: string, ABI?: any, withSignerIfPossible = true):
|
||||
|
||||
export function useV1FactoryContract(): Contract | null {
|
||||
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 {
|
||||
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 {
|
||||
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import Web3ReactManager from '../components/Web3ReactManager'
|
||||
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
|
||||
import AddLiquidity from './AddLiquidity'
|
||||
import CreatePool from './CreatePool'
|
||||
import MigrateV1 from './MigrateV1'
|
||||
import MigrateV1Exchange from './MigrateV1/MigrateV1Exchange'
|
||||
import Pool from './Pool'
|
||||
import PoolFinder from './PoolFinder'
|
||||
import RemoveLiquidity from './RemoveLiquidity'
|
||||
@ -99,6 +101,8 @@ export default function App() {
|
||||
<Route exact strict path="/create" component={CreatePool} />
|
||||
<Route exact strict path="/add/:tokens" component={AddLiquidity} />
|
||||
<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} />
|
||||
</Switch>
|
||||
</Web3ReactManager>
|
||||
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import NavigationTabs from '../components/NavigationTabs'
|
||||
|
||||
const Body = styled.div`
|
||||
export const BodyWrapper = styled.div`
|
||||
position: relative;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
@ -18,9 +18,9 @@ const Body = styled.div`
|
||||
*/
|
||||
export default function AppBody({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Body>
|
||||
<BodyWrapper>
|
||||
<NavigationTabs />
|
||||
<>{children}</>
|
||||
</Body>
|
||||
</BodyWrapper>
|
||||
)
|
||||
}
|
||||
|
11
src/pages/MigrateV1/EmptyState.tsx
Normal file
11
src/pages/MigrateV1/EmptyState.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
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>
|
||||
)
|
||||
}
|
282
src/pages/MigrateV1/MigrateV1Exchange.tsx
Normal file
282
src/pages/MigrateV1/MigrateV1Exchange.tsx
Normal file
@ -0,0 +1,282 @@
|
||||
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>
|
||||
)
|
||||
}
|
@ -1,40 +1,144 @@
|
||||
import { JSBI, Token } from '@uniswap/sdk'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Fraction, JSBI, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
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 { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useWalletModalToggle } from '../../state/application/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 = (
|
||||
<div>
|
||||
<h1>You must connect a wallet to use this tool.</h1>
|
||||
</div>
|
||||
)
|
||||
const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
|
||||
|
||||
/**
|
||||
* Page component for migrating liquidity from V1
|
||||
*/
|
||||
export default function MigrateV1({}: RouteComponentProps) {
|
||||
export function FormattedPoolTokenAmount({ tokenAmount }: { tokenAmount: TokenAmount }) {
|
||||
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) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const v1ExchangeAddresses = useAllV1ExchangeAddresses()
|
||||
const allV1Exchanges = useAllTokenV1Exchanges()
|
||||
|
||||
const v1ExchangeTokens: Token[] = useMemo(() => {
|
||||
return v1ExchangeAddresses.map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
|
||||
}, [chainId, v1ExchangeAddresses])
|
||||
const v1LiquidityTokens: Token[] = useMemo(() => {
|
||||
return Object.keys(allV1Exchanges).map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
|
||||
}, [chainId, allV1Exchanges])
|
||||
|
||||
const tokenBalances = useTokenBalances(account, v1ExchangeTokens)
|
||||
const v1LiquidityBalances = useTokenBalances(account, v1LiquidityTokens)
|
||||
|
||||
const unmigratedExchangeAddresses = useMemo(
|
||||
const [tokenSearch, setTokenSearch] = useState<string>('')
|
||||
const handleTokenSearchChange = useCallback(e => setTokenSearch(e.target.value), [setTokenSearch])
|
||||
|
||||
const searchedToken: Token | undefined = useTokenByAddressAndAutomaticallyAdd(tokenSearch)
|
||||
|
||||
const unmigratedLiquidityExchangeAddresses: TokenAmount[] = useMemo(
|
||||
() =>
|
||||
Object.keys(tokenBalances).filter(tokenAddress =>
|
||||
tokenBalances[tokenAddress] ? JSBI.greaterThan(tokenBalances[tokenAddress]?.raw, JSBI.BigInt(0)) : false
|
||||
),
|
||||
[tokenBalances]
|
||||
Object.keys(v1LiquidityBalances)
|
||||
.filter(tokenAddress =>
|
||||
v1LiquidityBalances[tokenAddress]
|
||||
? JSBI.greaterThan(v1LiquidityBalances[tokenAddress]?.raw, JSBI.BigInt(0))
|
||||
: false
|
||||
)
|
||||
.map(tokenAddress => v1LiquidityBalances[tokenAddress])
|
||||
.sort((a1, a2) => {
|
||||
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]
|
||||
)
|
||||
|
||||
if (!account) {
|
||||
return PLACEHOLDER_ACCOUNT
|
||||
}
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return <div>{unmigratedExchangeAddresses?.join('\n')}</div>
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
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'
|
||||
import Question from '../../components/QuestionHelper'
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
import PositionCard from '../../components/PositionCard'
|
||||
import { useUserProbablyHasV1Liquidity } from '../../data/V1'
|
||||
import { useUserHasLiquidityInAllTokens } from '../../data/V1'
|
||||
import { useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { ExternalLink, StyledInternalLink, TYPE } from '../../theme'
|
||||
import { StyledInternalLink, TYPE } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { RowBetween } from '../../components/Row'
|
||||
@ -59,7 +59,7 @@ export default function Pool({ history }: RouteComponentProps) {
|
||||
return <PositionCardWrapper key={i} dummyPair={pair} />
|
||||
})
|
||||
|
||||
const hasV1Liquidity = useUserProbablyHasV1Liquidity()
|
||||
const hasV1Liquidity = useUserHasLiquidityInAllTokens()
|
||||
|
||||
return (
|
||||
<AppBody>
|
||||
@ -103,9 +103,9 @@ export default function Pool({ history }: RouteComponentProps) {
|
||||
</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.
|
||||
</ExternalLink>
|
||||
</StyledInternalLink>
|
||||
)}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
|
@ -42,6 +42,14 @@ export function useAllTransactions(): { [txHash: string]: TransactionDetails } {
|
||||
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
|
||||
export function useHasPendingApproval(tokenAddress?: string): boolean {
|
||||
const allTransactions = useAllTransactions()
|
||||
|
Loading…
Reference in New Issue
Block a user