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 @@
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>
&nbsp;or&nbsp;
<ExternalLink href="https://migrate.uniswap.exchange/">
<StyledInternalLink to="/migrate/v1">
<b>migrate your liquidity ↗</b>
</ExternalLink>
</StyledInternalLink>
.
</MigrateBanner>
<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 { 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(
() =>
......
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>
)
}
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>
)
}
This diff is collapsed.
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)
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'
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()
......
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