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

feat(v1-support): Enable executing swaps on v1 or v2 (#883)

* swap-v1

* toggle the version switch based on the search query parameter

* rework some of the query parameter stuff in send/swap

* hide the url when they click it

* allow switching back to v2 via the toggle

* represent the v1 trade in the UI if they toggle it on

* show trade link in both directions (5% threshold for v1 link)

* input amounts should reflect v1/v2

* perform the approve on v1 exchange for v1 trades

* get swap on v1 working

* move some code around to reduce duplication

* fix ts error

* correct input allowance

* fix exact token to token on v1

* fix pending approvals to be specific to the spender

* google analytics for swap version

* disable the version switch on pages other than swap and send
parent 29db0a50
import { stringify } from 'qs'
import React, { useCallback, useMemo } from 'react'
import { Link, useLocation } from 'react-router-dom'
import styled from 'styled-components'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
const VersionLabel = styled.span<{ enabled: boolean }>`
padding: ${({ enabled }) => (enabled ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')};
border-radius: 14px;
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.primary1)};
font-size: 0.825rem;
font-weight: 400;
:hover {
user-select: ${({ enabled }) => (enabled ? 'none' : 'initial')};
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.primary3)};
}
`
const VersionToggle = styled(Link)<{ enabled: boolean }>`
border-radius: 16px;
opacity: ${({ enabled }) => (enabled ? 1 : 0.5)};
cursor: ${({ enabled }) => (enabled ? 'pointer' : 'default')};
background: ${({ theme }) => theme.primary5};
border: 1px solid ${({ theme }) => theme.primary4};
color: ${({ theme }) => theme.primary1};
display: flex;
width: fit-content;
text-decoration: none;
:hover {
text-decoration: none;
}
`
export function VersionSwitch() {
const version = useToggledVersion()
const location = useLocation()
const query = useParsedQueryString()
const versionSwitchAvailable = location.pathname === '/swap' || location.pathname === '/send'
const toggleDest = useMemo(() => {
return versionSwitchAvailable
? {
...location,
search: `?${stringify({ ...query, use: version === Version.v1 ? undefined : Version.v1 })}`
}
: location
}, [location, query, version, versionSwitchAvailable])
const handleClick = useCallback(
e => {
if (!versionSwitchAvailable) e.preventDefault()
},
[versionSwitchAvailable]
)
return (
<VersionToggle enabled={versionSwitchAvailable} to={toggleDest} onClick={handleClick}>
<VersionLabel enabled={version === Version.v2 || !versionSwitchAvailable}>V2</VersionLabel>
<VersionLabel enabled={version === Version.v1 && versionSwitchAvailable}>V1</VersionLabel>
</VersionToggle>
)
}
import { ChainId, WETH } from '@uniswap/sdk'
import React from 'react'
import { isMobile } from 'react-device-detect'
import { Link as HistoryLink } from 'react-router-dom'
import { Text } from 'rebass'
import styled from 'styled-components'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import Row from '../Row'
import Menu from '../Menu'
import Web3Status from '../Web3Status'
import { ExternalLink, StyledInternalLink } from '../../theme'
import { Text } from 'rebass'
import { WETH, ChainId } from '@uniswap/sdk'
import { isMobile } from 'react-device-detect'
import { YellowCard } from '../Card'
import { useActiveWeb3React } from '../../hooks'
import { useDarkModeManager } from '../../state/user/hooks'
import Logo from '../../assets/svg/logo.svg'
import Wordmark from '../../assets/svg/wordmark.svg'
import LogoDark from '../../assets/svg/logo_white.svg'
import Wordmark from '../../assets/svg/wordmark.svg'
import WordmarkDark from '../../assets/svg/wordmark_white.svg'
import { useActiveWeb3React } from '../../hooks'
import { useDarkModeManager } from '../../state/user/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { ExternalLink, StyledInternalLink } from '../../theme'
import { YellowCard } from '../Card'
import { AutoColumn } from '../Column'
import { RowBetween } from '../Row'
import Menu from '../Menu'
import Row, { RowBetween } from '../Row'
import Web3Status from '../Web3Status'
import { VersionSwitch } from './VersionSwitch'
const HeaderFrame = styled.div`
display: flex;
......@@ -122,33 +122,13 @@ const MigrateBanner = styled(AutoColumn)`
`};
`
const VersionLabel = styled.span<{ isV2?: boolean }>`
padding: ${({ isV2 }) => (isV2 ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')};
border-radius: 14px;
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary1)};
font-size: 0.825rem;
font-weight: 400;
:hover {
user-select: ${({ isV2 }) => (isV2 ? 'none' : 'initial')};
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary3)};
}
`
const VersionToggle = styled.a`
border-radius: 16px;
background: ${({ theme }) => theme.primary5};
border: 1px solid ${({ theme }) => theme.primary4};
color: ${({ theme }) => theme.primary1};
display: flex;
width: fit-content;
cursor: pointer;
text-decoration: none;
:hover {
text-decoration: none;
}
`
const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
[ChainId.MAINNET]: null,
[ChainId.RINKEBY]: 'Rinkeby',
[ChainId.ROPSTEN]: 'Ropsten',
[ChainId.GÖRLI]: 'Görli',
[ChainId.KOVAN]: 'Kovan'
}
export default function Header() {
const { account, chainId } = useActiveWeb3React()
......@@ -187,21 +167,11 @@ export default function Header() {
</TitleText>
)}
</Title>
<TestnetWrapper style={{ pointerEvents: 'auto' }}>
{!isMobile && (
<VersionToggle target="_self" href="https://v1.uniswap.exchange">
<VersionLabel isV2={true}>V2</VersionLabel>
<VersionLabel isV2={false}>V1</VersionLabel>
</VersionToggle>
)}
</TestnetWrapper>
<TestnetWrapper style={{ pointerEvents: 'auto' }}>{!isMobile && <VersionSwitch />}</TestnetWrapper>
</HeaderElement>
<HeaderElement>
<TestnetWrapper>
{!isMobile && chainId === ChainId.ROPSTEN && <NetworkCard>Ropsten</NetworkCard>}
{!isMobile && chainId === ChainId.RINKEBY && <NetworkCard>Rinkeby</NetworkCard>}
{!isMobile && chainId === ChainId.GÖRLI && <NetworkCard>Görli</NetworkCard>}
{!isMobile && chainId === ChainId.KOVAN && <NetworkCard>Kovan</NetworkCard>}
{!isMobile && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
</TestnetWrapper>
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
{account && userEthBalance ? (
......
import React, { useCallback, useState } from 'react'
import React, { useCallback, useContext, useState } from 'react'
import { AlertCircle, CheckCircle } from 'react-feather'
import styled from 'styled-components'
import styled, { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import useInterval from '../../hooks/useInterval'
......@@ -51,13 +51,16 @@ export default function TxnPopup({
isRunning ? delay : null
)
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
const theme = useContext(ThemeContext)
return (
<AutoRow onMouseEnter={() => setIsRunning(false)} onMouseLeave={() => setIsRunning(true)}>
{success ? (
<CheckCircle color={'#27AE60'} size={24} style={{ paddingRight: '24px' }} />
) : (
<AlertCircle color={'#FF6871'} size={24} style={{ paddingRight: '24px' }} />
)}
<AutoRow onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<div style={{ paddingRight: 16 }}>
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
</div>
<AutoColumn gap="8px">
<TYPE.body fontWeight={500}>
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
......
import React, { useContext } from 'react'
import { stringify } from 'qs'
import React, { useContext, useMemo } from 'react'
import { useLocation } from 'react-router'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import { Version } from '../../hooks/useToggledVersion'
import { ExternalLink } from '../../theme'
import { StyledInternalLink } from '../../theme'
import { YellowCard } from '../Card'
import { AutoColumn } from '../Column'
export default function V1TradeLink({ v1TradeLinkIfBetter }: { v1TradeLinkIfBetter: string }) {
export default function BetterTradeLink({ version }: { version: Version }) {
const theme = useContext(ThemeContext)
return v1TradeLinkIfBetter ? (
const location = useLocation()
const search = useParsedQueryString()
const linkDestination = useMemo(() => {
return {
...location,
search: `?${stringify({
...search,
use: version
})}`
}
}, [location, search, version])
return (
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
There is a better price for this trade on{' '}
<ExternalLink href={v1TradeLinkIfBetter}>
<b>Uniswap V1</b>
</ExternalLink>
<StyledInternalLink to={linkDestination}>
<b>Uniswap {version.toUpperCase()}</b>
</StyledInternalLink>
</Text>
</AutoColumn>
</YellowCard>
) : null
)
}
......@@ -159,4 +159,4 @@ export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.Bi
// used to ensure the user doesn't send so much ETH so they end up with <.01
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
export const V1_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
import { ChainId, JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
import { JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
import { useMemo } from 'react'
import { useActiveWeb3React } from '../hooks'
import { useAllTokens } from '../hooks/Tokens'
import { useV1FactoryContract } from '../hooks/useContract'
import { Version } from '../hooks/useToggledVersion'
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
function useV1PairAddress(tokenAddress?: string): string | undefined {
export function useV1ExchangeAddress(tokenAddress?: string): string | undefined {
const contract = useV1FactoryContract()
const inputs = useMemo(() => [tokenAddress], [tokenAddress])
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
}
class MockV1Pair extends Pair {
readonly isV1: true = true
}
class MockV1Pair extends Pair {}
function useMockV1Pair(token?: Token): MockV1Pair | undefined {
const isWETH = token?.equals(WETH[token?.chainId])
const isWETH: boolean = token && WETH[token.chainId] ? token.equals(WETH[token.chainId]) : false
// will only return an address on mainnet, and not for WETH
const v1PairAddress = useV1PairAddress(isWETH ? undefined : token?.address)
const v1PairAddress = useV1ExchangeAddress(isWETH ? undefined : token?.address)
const tokenBalance = useTokenBalance(v1PairAddress, token)
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
......@@ -43,7 +41,7 @@ export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => {
const token = allTokens[args[ix][0]]
if (result?.[0]) {
memo[result?.[0]] = token
memo[result[0]] = token
}
return memo
}, {}) ?? {},
......@@ -74,24 +72,23 @@ export function useUserHasLiquidityInAllTokens(): boolean | undefined {
)
}
export function useV1TradeLinkIfBetter(
/**
* Returns the trade to execute on V1 to go between input and output token
*/
export function useV1Trade(
isExactIn?: boolean,
input?: Token,
output?: Token,
exactAmount?: TokenAmount,
v2Trade?: Trade,
minimumDelta: Percent = new Percent('0')
): string | undefined {
inputToken?: Token,
outputToken?: Token,
exactAmount?: TokenAmount
): Trade | undefined {
const { chainId } = useActiveWeb3React()
const isMainnet: boolean = chainId === ChainId.MAINNET
// get the mock v1 pairs
const inputPair = useMockV1Pair(input)
const outputPair = useMockV1Pair(output)
const inputPair = useMockV1Pair(inputToken)
const outputPair = useMockV1Pair(outputToken)
const inputIsWETH = isMainnet && input?.equals(WETH[ChainId.MAINNET])
const outputIsWETH = isMainnet && output?.equals(WETH[ChainId.MAINNET])
const inputIsWETH = (inputToken && chainId && WETH[chainId] && inputToken.equals(WETH[chainId])) ?? false
const outputIsWETH = (outputToken && chainId && WETH[chainId] && outputToken.equals(WETH[chainId])) ?? false
// construct a direct or through ETH v1 route
let pairs: Pair[] = []
......@@ -105,7 +102,7 @@ export function useV1TradeLinkIfBetter(
pairs = [inputPair, outputPair]
}
const route = input && pairs && pairs.length > 0 && new Route(pairs, input)
const route = inputToken && pairs && pairs.length > 0 && new Route(pairs, inputToken)
let v1Trade: Trade | undefined
try {
v1Trade =
......@@ -113,25 +110,53 @@ export function useV1TradeLinkIfBetter(
? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
: undefined
} catch {}
return v1Trade
}
export function getTradeVersion(trade?: Trade): Version | undefined {
const isV1 = trade?.route?.pairs?.some(pair => pair instanceof MockV1Pair)
if (isV1) return Version.v1
if (isV1 === false) return Version.v2
return undefined
}
let v1HasBetterTrade = false
if (v1Trade) {
if (isExactIn) {
// discount the v1 output amount by minimumDelta
const discountedV1Output = v1Trade?.outputAmount.multiply(new Percent('1').subtract(minimumDelta))
// check if the discounted v1 amount is still greater than v2, short-circuiting if no v2 trade exists
v1HasBetterTrade = !v2Trade || discountedV1Output.greaterThan(v2Trade.outputAmount)
} else {
// inflate the v1 amount by minimumDelta
const inflatedV1Input = v1Trade?.inputAmount.multiply(new Percent('1').add(minimumDelta))
// check if the inflated v1 amount is still less than v2, short-circuiting if no v2 trade exists
v1HasBetterTrade = !v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount)
}
// returns the v1 exchange against which a trade should be executed
export function useV1TradeExchangeAddress(trade: Trade | undefined): string | undefined {
const tokenAddress: string | undefined = useMemo(() => {
const tradeVersion = getTradeVersion(trade)
const isV1 = tradeVersion === Version.v1
return isV1
? trade &&
WETH[trade.inputAmount.token.chainId] &&
trade.inputAmount.token.equals(WETH[trade.inputAmount.token.chainId])
? trade.outputAmount.token.address
: trade?.inputAmount?.token?.address
: undefined
}, [trade])
return useV1ExchangeAddress(tokenAddress)
}
const ZERO_PERCENT = new Percent('0')
const ONE_HUNDRED_PERCENT = new Percent('1')
// returns whether tradeB is better than tradeA by at least a threshold
export function isTradeBetter(
tradeA: Trade | undefined,
tradeB: Trade | undefined,
minimumDelta: Percent = ZERO_PERCENT
): boolean | undefined {
if (!tradeA || !tradeB) return undefined
if (
tradeA.tradeType !== tradeB.tradeType ||
!tradeA.inputAmount.token.equals(tradeB.inputAmount.token) ||
!tradeB.outputAmount.token.equals(tradeB.outputAmount.token)
) {
throw new Error('Trades are not comparable')
}
return v1HasBetterTrade && input && output
? `https://v1.uniswap.exchange/swap?inputCurrency=${inputIsWETH ? 'ETH' : input.address}&outputCurrency=${
outputIsWETH ? 'ETH' : output.address
}`
: undefined
if (minimumDelta.equalTo(ZERO_PERCENT)) {
return tradeA.executionPrice.lessThan(tradeB.executionPrice)
} else {
return tradeA.executionPrice.raw.multiply(minimumDelta.add(ONE_HUNDRED_PERCENT)).lessThan(tradeB.executionPrice)
}
}
......@@ -4,12 +4,14 @@ import { Trade, WETH, TokenAmount } from '@uniswap/sdk'
import { useCallback, useMemo } from 'react'
import { ROUTER_ADDRESS } from '../constants'
import { useTokenAllowance } from '../data/Allowances'
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
import { Field } from '../state/swap/actions'
import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks'
import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { calculateGasMargin } from '../utils'
import { useTokenContract } from './useContract'
import { useActiveWeb3React } from './index'
import { Version } from './useToggledVersion'
export enum ApprovalState {
UNKNOWN,
......@@ -21,16 +23,16 @@ export enum ApprovalState {
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns
export function useApproveCallback(
amountToApprove?: TokenAmount,
addressToApprove?: string
spender?: string
): [ApprovalState, () => Promise<void>] {
const { account } = useActiveWeb3React()
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, addressToApprove)
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address)
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, spender)
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address, spender)
// check the current approval status
const approval = useMemo(() => {
if (!amountToApprove) return ApprovalState.UNKNOWN
const approvalState: ApprovalState = useMemo(() => {
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
// we treat WETH as ETH which requires no approvals
if (amountToApprove.token.equals(WETH[amountToApprove.token.chainId])) return ApprovalState.APPROVED
// we might not have enough data to know whether or not we need to approve
......@@ -38,13 +40,13 @@ export function useApproveCallback(
if (pendingApproval) return ApprovalState.PENDING
// amountToApprove will be defined if currentAllowance is
return currentAllowance.lessThan(amountToApprove) ? ApprovalState.NOT_APPROVED : ApprovalState.APPROVED
}, [amountToApprove, currentAllowance, pendingApproval])
}, [amountToApprove, currentAllowance, pendingApproval, spender])
const tokenContract = useTokenContract(amountToApprove?.token?.address)
const addTransaction = useTransactionAdder()
const approve = useCallback(async (): Promise<void> => {
if (approval !== ApprovalState.NOT_APPROVED) {
if (approvalState !== ApprovalState.NOT_APPROVED) {
console.error('approve was called unnecessarily')
return
}
......@@ -59,30 +61,35 @@ export function useApproveCallback(
return
}
if (!spender) {
console.error('no spender')
return
}
let useExact = false
const estimatedGas = await tokenContract.estimateGas.approve(addressToApprove, MaxUint256).catch(() => {
const estimatedGas = await tokenContract.estimateGas.approve(spender, MaxUint256).catch(() => {
// general fallback for tokens who restrict approval amounts
useExact = true
return tokenContract.estimateGas.approve(addressToApprove, amountToApprove.raw.toString())
return tokenContract.estimateGas.approve(spender, amountToApprove.raw.toString())
})
return tokenContract
.approve(addressToApprove, useExact ? amountToApprove.raw.toString() : MaxUint256, {
.approve(spender, useExact ? amountToApprove.raw.toString() : MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas)
})
.then((response: TransactionResponse) => {
addTransaction(response, {
summary: 'Approve ' + amountToApprove?.token?.symbol,
approvalOfToken: amountToApprove?.token?.address
summary: 'Approve ' + amountToApprove.token.symbol,
approval: { tokenAddress: amountToApprove.token.address, spender: spender }
})
})
.catch((error: Error) => {
console.debug('Failed to approve token', error)
throw error
})
}, [approval, tokenContract, addressToApprove, amountToApprove, addTransaction])
}, [approvalState, tokenContract, spender, amountToApprove, addTransaction])
return [approval, approve]
return [approvalState, approve]
}
// wraps useApproveCallback in the context of a swap
......@@ -91,5 +98,7 @@ export function useApproveCallbackFromTrade(trade?: Trade, allowedSlippage = 0)
() => (trade ? computeSlippageAdjustedAmounts(trade, allowedSlippage)[Field.INPUT] : undefined),
[trade, allowedSlippage]
)
return useApproveCallback(amountToApprove, ROUTER_ADDRESS)
const tradeIsV1 = getTradeVersion(trade) === Version.v1
const v1ExchangeAddress = useV1TradeExchangeAddress(trade)
return useApproveCallback(amountToApprove, tradeIsV1 ? v1ExchangeAddress : ROUTER_ADDRESS)
}
......@@ -30,23 +30,23 @@ export function useV1FactoryContract(): Contract | null {
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 {
export function useV1ExchangeContract(address?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(address, V1_EXCHANGE_ABI, withSignerIfPossible)
}
export function useTokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
}
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
}
export function usePairContract(pairAddress?: string, withSignerIfPossible = true): Contract | null {
export function usePairContract(pairAddress?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
}
......
import { parse, ParsedQs } from 'qs'
import { useMemo } from 'react'
import { useLocation } from 'react-router-dom'
export default function useParsedQueryString(): ParsedQs {
const { search } = useLocation()
return useMemo(
() => (search && search.length > 1 ? parse(search, { parseArrays: false, ignoreQueryPrefix: true }) : {}),
[search]
)
}
This diff is collapsed.
import useParsedQueryString from './useParsedQueryString'
export enum Version {
v1 = 'v1',
v2 = 'v2'
}
export default function useToggledVersion(): Version {
const { use } = useParsedQueryString()
if (!use || typeof use !== 'string') return Version.v2
if (use.toLowerCase() === 'v1') return Version.v1
return Version.v2
}
......@@ -9,6 +9,7 @@ import { PinkCard, YellowCard, LightCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import QuestionHelper from '../../components/QuestionHelper'
import { AutoRow, RowBetween } from '../../components/Row'
import { Dots } from '../../components/swap/styleds'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { MIGRATOR_ADDRESS } from '../../constants/abis/migrator'
import { usePair } from '../../data/Reserves'
......@@ -195,11 +196,13 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
disabled={approval !== ApprovalState.NOT_APPROVED}
onClick={approve}
>
{approval === ApprovalState.PENDING
? 'Approving...'
: approval === ApprovalState.APPROVED
? 'Approved'
: 'Approve'}
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED ? (
'Approved'
) : (
'Approve'
)}
</ButtonConfirmed>
</AutoColumn>
<AutoColumn gap="12px" style={{ flex: '1' }}>
......
......@@ -2,7 +2,6 @@ import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useContext, useEffect, useState } from 'react'
import { ArrowDown } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import AddressInputPanel from '../../components/AddressInputPanel'
......@@ -20,14 +19,21 @@ import SwapModalFooter from '../../components/swap/SwapModalFooter'
import { ArrowWrapper, BottomGrouping, Dots, InputGroup, StyledNumerical, Wrapper } from '../../components/swap/styleds'
import TradePrice from '../../components/swap/TradePrice'
import { TransferModalHeader } from '../../components/swap/TransferModalHeader'
import V1TradeLink from '../../components/swap/V1TradeLink'
import BetterTradeLink from '../../components/swap/BetterTradeLink'
import TokenLogo from '../../components/TokenLogo'
import { TokenWarningCards } from '../../components/TokenWarningCard'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, MIN_ETH } from '../../constants'
import {
DEFAULT_DEADLINE_FROM_NOW,
INITIAL_ALLOWED_SLIPPAGE,
MIN_ETH,
BETTER_TRADE_LINK_THRESHOLD
} from '../../constants'
import { getTradeVersion, isTradeBetter } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
import { useSendCallback } from '../../hooks/useSendCallback'
import { useSwapCallback } from '../../hooks/useSwapCallback'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/swap/actions'
import {
......@@ -42,8 +48,8 @@ import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeve
import AppBody from '../AppBody'
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
export default function Send({ location: { search } }: RouteComponentProps) {
useDefaultsFromURLSearch(search)
export default function Send() {
useDefaultsFromURLSearch()
// text translation
// const { t } = useTranslation()
......@@ -62,15 +68,33 @@ export default function Send({ location: { search } }: RouteComponentProps) {
// trade details, check query params for initial state
const { independentField, typedValue } = useSwapState()
const {
parsedAmounts,
bestTrade,
parsedAmount,
bestTrade: bestTradeV2,
tokenBalances,
tokens,
error: swapError,
v1TradeLinkIfBetter
v1Trade
} = useDerivedSwapInfo()
const isSwapValid = !swapError && !recipientError && bestTrade
const toggledVersion = useToggledVersion()
const bestTrade = {
[Version.v1]: v1Trade,
[Version.v2]: bestTradeV2
}[toggledVersion]
const betterTradeLinkVersion: Version | undefined =
toggledVersion === Version.v2 && isTradeBetter(bestTradeV2, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
? Version.v1
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, bestTradeV2)
? Version.v2
: undefined
const parsedAmounts = {
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : bestTrade?.inputAmount,
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : bestTrade?.outputAmount
}
const isSwapValid = !swapError && !recipientError && bestTrade
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
// modal and loading
......@@ -152,7 +176,11 @@ export default function Send({ location: { search } }: RouteComponentProps) {
ReactGA.event({
category: 'Send',
action: recipient === account ? 'Swap w/o Send' : 'Swap w/ Send',
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join(';')
label: [
bestTrade.inputAmount.token.symbol,
bestTrade.outputAmount.token.symbol,
getTradeVersion(bestTrade)
].join('/')
})
})
.catch(error => {
......@@ -420,7 +448,6 @@ export default function Send({ location: { search } }: RouteComponentProps) {
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
// eslint-disable-next-line @typescript-eslint/no-empty-function
label={independentField === Field.INPUT && parsedAmounts[Field.OUTPUT] ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
......@@ -538,7 +565,7 @@ export default function Send({ location: { search } }: RouteComponentProps) {
</Text>
</ButtonError>
)}
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
</BottomGrouping>
</Wrapper>
</AppBody>
......
......@@ -2,7 +2,6 @@ import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useContext, useState, useEffect } from 'react'
import { ArrowDown } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
......@@ -19,12 +18,19 @@ import { ArrowWrapper, BottomGrouping, Dots, Wrapper } from '../../components/sw
import SwapModalFooter from '../../components/swap/SwapModalFooter'
import SwapModalHeader from '../../components/swap/SwapModalHeader'
import TradePrice from '../../components/swap/TradePrice'
import V1TradeLink from '../../components/swap/V1TradeLink'
import BetterTradeLink from '../../components/swap/BetterTradeLink'
import { TokenWarningCards } from '../../components/TokenWarningCard'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, MIN_ETH } from '../../constants'
import {
DEFAULT_DEADLINE_FROM_NOW,
INITIAL_ALLOWED_SLIPPAGE,
MIN_ETH,
BETTER_TRADE_LINK_THRESHOLD
} from '../../constants'
import { getTradeVersion, isTradeBetter } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
import { useSwapCallback } from '../../hooks/useSwapCallback'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/swap/actions'
import {
......@@ -38,8 +44,8 @@ import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeve
import AppBody from '../AppBody'
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
export default function Swap({ location: { search } }: RouteComponentProps) {
useDefaultsFromURLSearch(search)
export default function Swap() {
useDefaultsFromURLSearch()
const { chainId, account } = useActiveWeb3React()
const theme = useContext(ThemeContext)
......@@ -49,7 +55,25 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
// swap state
const { independentField, typedValue } = useSwapState()
const { bestTrade, tokenBalances, parsedAmounts, tokens, error, v1TradeLinkIfBetter } = useDerivedSwapInfo()
const { bestTrade: bestTradeV2, tokenBalances, parsedAmount, tokens, error, v1Trade } = useDerivedSwapInfo()
const toggledVersion = useToggledVersion()
const bestTrade = {
[Version.v1]: v1Trade,
[Version.v2]: bestTradeV2
}[toggledVersion]
const betterTradeLinkVersion: Version | undefined =
toggledVersion === Version.v2 && isTradeBetter(bestTradeV2, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
? Version.v1
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, bestTradeV2)
? Version.v2
: undefined
const parsedAmounts = {
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : bestTrade?.inputAmount,
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : bestTrade?.outputAmount
}
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
const isValid = !error
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
......@@ -132,7 +156,11 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
ReactGA.event({
category: 'Swap',
action: 'Swap w/o Send',
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join('/')
label: [
bestTrade.inputAmount.token.symbol,
bestTrade.outputAmount.token.symbol,
getTradeVersion(bestTrade)
].join('/')
})
})
.catch(error => {
......@@ -245,7 +273,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
// eslint-disable-next-line @typescript-eslint/no-empty-function
label={independentField === Field.INPUT ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
......@@ -334,7 +361,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
</Text>
</ButtonError>
)}
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
</BottomGrouping>
</Wrapper>
</AppBody>
......
......@@ -5,7 +5,12 @@ export enum Field {
OUTPUT = 'OUTPUT'
}
export const setDefaultsFromURLSearch = createAction<{ chainId: number; queryString?: string }>('setDefaultsFromURL')
export const selectToken = createAction<{ field: Field; address: string }>('selectToken')
export const switchTokens = createAction<void>('switchTokens')
export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInput')
export const replaceSwapState = createAction<{
field: Field
typedValue: string
inputTokenAddress?: string
outputTokenAddress?: string
}>('replaceSwapState')
import { ChainId, WETH } from '@uniswap/sdk'
import { parse } from 'qs'
import { Field } from './actions'
import { queryParametersToSwapState } from './hooks'
describe('hooks', () => {
describe('#queryParametersToSwapState', () => {
test('ETH to DAI', () => {
expect(
queryParametersToSwapState(
parse(
'?inputCurrency=ETH&outputCurrency=0x6b175474e89094c44da98b954eedeac495271d0f&exactAmount=20.5&exactField=outPUT',
{ parseArrays: false, ignoreQueryPrefix: true }
),
ChainId.MAINNET
)
).toEqual({
[Field.OUTPUT]: { address: '0x6B175474E89094C44Da98b954EedeAC495271d0F' },
[Field.INPUT]: { address: WETH[ChainId.MAINNET].address },
typedValue: '20.5',
independentField: Field.OUTPUT
})
})
test('does not duplicate eth for invalid output token', () => {
expect(
queryParametersToSwapState(
parse('?outputCurrency=invalid', { parseArrays: false, ignoreQueryPrefix: true }),
ChainId.MAINNET
)
).toEqual({
[Field.INPUT]: { address: '' },
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
typedValue: '',
independentField: Field.INPUT
})
})
test('output ETH only', () => {
expect(
queryParametersToSwapState(
parse('?outputCurrency=eth&exactAmount=20.5', { parseArrays: false, ignoreQueryPrefix: true }),
ChainId.MAINNET
)
).toEqual({
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
[Field.INPUT]: { address: '' },
typedValue: '20.5',
independentField: Field.INPUT
})
})
})
})
import { parseUnits } from '@ethersproject/units'
import { JSBI, Token, TokenAmount, Trade } from '@uniswap/sdk'
import { ChainId, JSBI, Token, TokenAmount, Trade, WETH } from '@uniswap/sdk'
import { ParsedQs } from 'qs'
import { useCallback, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useV1Trade } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useTradeExactIn, useTradeExactOut } from '../../hooks/Trades'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import { isAddress } from '../../utils'
import { AppDispatch, AppState } from '../index'
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
import { Field, selectToken, setDefaultsFromURLSearch, switchTokens, typeInput } from './actions'
import { useV1TradeLinkIfBetter } from '../../data/V1'
import { V1_TRADE_LINK_THRESHOLD } from '../../constants'
import { Field, replaceSwapState, selectToken, switchTokens, typeInput } from './actions'
import { SwapState } from './reducer'
export function useSwapState(): AppState['swap'] {
return useSelector<AppState, AppState['swap']>(state => state.swap)
......@@ -33,7 +36,7 @@ export function useSwapActionHandlers(): {
[dispatch]
)
const onSwapTokens = useCallback(() => {
const onSwitchTokens = useCallback(() => {
dispatch(switchTokens())
}, [dispatch])
......@@ -45,7 +48,7 @@ export function useSwapActionHandlers(): {
)
return {
onSwitchTokens: onSwapTokens,
onSwitchTokens,
onTokenSelection,
onUserInput
}
......@@ -73,10 +76,10 @@ export function tryParseAmount(value?: string, token?: Token): TokenAmount | und
export function useDerivedSwapInfo(): {
tokens: { [field in Field]?: Token }
tokenBalances: { [field in Field]?: TokenAmount }
parsedAmounts: { [field in Field]?: TokenAmount }
parsedAmount: TokenAmount | undefined
bestTrade: Trade | null
error?: string
v1TradeLinkIfBetter?: string
v1Trade: Trade | undefined
} {
const { account } = useActiveWeb3React()
......@@ -90,76 +93,121 @@ export function useDerivedSwapInfo(): {
const tokenIn = useTokenByAddressAndAutomaticallyAdd(tokenInAddress)
const tokenOut = useTokenByAddressAndAutomaticallyAdd(tokenOutAddress)
const relevantTokenBalances = useTokenBalancesTreatWETHAsETH(account ?? undefined, [tokenIn, tokenOut])
const relevantTokenBalances = useTokenBalancesTreatWETHAsETH(account ?? undefined, [
tokenIn ?? undefined,
tokenOut ?? undefined
])
const isExactIn: boolean = independentField === Field.INPUT
const amount = tryParseAmount(typedValue, isExactIn ? tokenIn : tokenOut)
const parsedAmount = tryParseAmount(typedValue, (isExactIn ? tokenIn : tokenOut) ?? undefined)
const bestTradeExactIn = useTradeExactIn(isExactIn ? amount : undefined, tokenOut)
const bestTradeExactOut = useTradeExactOut(tokenIn, !isExactIn ? amount : undefined)
const bestTradeExactIn = useTradeExactIn(isExactIn ? parsedAmount : undefined, tokenOut ?? undefined)
const bestTradeExactOut = useTradeExactOut(tokenIn ?? undefined, !isExactIn ? parsedAmount : undefined)
const bestTrade = isExactIn ? bestTradeExactIn : bestTradeExactOut
const parsedAmounts = {
[Field.INPUT]: isExactIn ? amount : bestTrade?.inputAmount,
[Field.OUTPUT]: isExactIn ? bestTrade?.outputAmount : amount
}
const tokenBalances = {
[Field.INPUT]: relevantTokenBalances?.[tokenIn?.address ?? ''],
[Field.OUTPUT]: relevantTokenBalances?.[tokenOut?.address ?? '']
}
const tokens: { [field in Field]?: Token } = {
[Field.INPUT]: tokenIn,
[Field.OUTPUT]: tokenOut
[Field.INPUT]: tokenIn ?? undefined,
[Field.OUTPUT]: tokenOut ?? undefined
}
// get link to trade on v1, if a better rate exists
const v1TradeLinkIfBetter = useV1TradeLinkIfBetter(
isExactIn,
tokens[Field.INPUT],
tokens[Field.OUTPUT],
isExactIn ? parsedAmounts[Field.INPUT] : parsedAmounts[Field.OUTPUT],
bestTrade ?? undefined,
V1_TRADE_LINK_THRESHOLD
)
const v1Trade = useV1Trade(isExactIn, tokens[Field.INPUT], tokens[Field.OUTPUT], parsedAmount)
let error: string | undefined
if (!account) {
error = 'Connect Wallet'
}
if (!parsedAmounts[Field.INPUT]) {
if (!parsedAmount) {
error = error ?? 'Enter an amount'
}
if (!parsedAmounts[Field.OUTPUT]) {
error = error ?? 'Enter an amount'
if (!tokens[Field.INPUT] || !tokens[Field.OUTPUT]) {
error = error ?? 'Select a token'
}
const [balanceIn, amountIn] = [tokenBalances[Field.INPUT], parsedAmounts[Field.INPUT]]
if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) {
error = 'Insufficient ' + amountIn.token.symbol + ' balance'
}
// this check is incorrect, it should check against the maximum amount in
// rather than the estimated amount in
// const [balanceIn, amountIn] = [tokenBalances[Field.INPUT], parsedAmounts[Field.INPUT]]
// if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) {
// error = 'Insufficient ' + amountIn.token.symbol + ' balance'
// }
return {
tokens,
tokenBalances,
parsedAmounts,
parsedAmount,
bestTrade,
error,
v1TradeLinkIfBetter
v1Trade
}
}
function parseCurrencyFromURLParameter(urlParam: any, chainId: number): string {
if (typeof urlParam === 'string') {
const valid = isAddress(urlParam)
if (valid) return valid
if (urlParam.toLowerCase() === 'eth') return WETH[chainId as ChainId]?.address ?? ''
if (valid === false) return WETH[chainId as ChainId]?.address ?? ''
}
return WETH[chainId as ChainId]?.address
}
// updates the swap state to use the defaults for a given network whenever the query
// string updates
export function useDefaultsFromURLSearch(search?: string) {
function parseTokenAmountURLParameter(urlParam: any): string {
return typeof urlParam === 'string' && !isNaN(parseFloat(urlParam)) ? urlParam : ''
}
function parseIndependentFieldURLParameter(urlParam: any): Field {
return typeof urlParam === 'string' && urlParam.toLowerCase() === 'output' ? Field.OUTPUT : Field.INPUT
}
export function queryParametersToSwapState(parsedQs: ParsedQs, chainId: ChainId): SwapState {
let inputCurrency = parseCurrencyFromURLParameter(parsedQs.inputCurrency, chainId)
let outputCurrency = parseCurrencyFromURLParameter(parsedQs.outputCurrency, chainId)
if (inputCurrency === outputCurrency) {
if (typeof parsedQs.outputCurrency === 'string') {
inputCurrency = ''
} else {
outputCurrency = ''
}
}
return {
[Field.INPUT]: {
address: inputCurrency
},
[Field.OUTPUT]: {
address: outputCurrency
},
typedValue: parseTokenAmountURLParameter(parsedQs.exactAmount),
independentField: parseIndependentFieldURLParameter(parsedQs.exactField)
}
}
// updates the swap state to use the defaults for a given network
export function useDefaultsFromURLSearch() {
const { chainId } = useActiveWeb3React()
const dispatch = useDispatch<AppDispatch>()
const parsedQs = useParsedQueryString()
useEffect(() => {
if (!chainId) return
dispatch(setDefaultsFromURLSearch({ chainId, queryString: search }))
}, [dispatch, search, chainId])
const parsed = queryParametersToSwapState(parsedQs, chainId)
dispatch(
replaceSwapState({
typedValue: parsed.typedValue,
field: parsed.independentField,
inputTokenAddress: parsed[Field.INPUT].address,
outputTokenAddress: parsed[Field.OUTPUT].address
})
)
// eslint-disable-next-line
}, [dispatch, chainId])
}
import { ChainId, WETH } from '@uniswap/sdk'
import { createStore, Store } from 'redux'
import { Field, setDefaultsFromURLSearch } from './actions'
import { Field, selectToken } from './actions'
import reducer, { SwapState } from './reducer'
describe('swap reducer', () => {
......@@ -15,54 +14,21 @@ describe('swap reducer', () => {
})
})
describe('setDefaultsFromURL', () => {
test('ETH to DAI', () => {
describe('selectToken', () => {
it('changes token', () => {
store.dispatch(
setDefaultsFromURLSearch({
chainId: ChainId.MAINNET,
queryString:
'?inputCurrency=ETH&outputCurrency=0x6b175474e89094c44da98b954eedeac495271d0f&exactAmount=20.5&exactField=outPUT'
})
)
expect(store.getState()).toEqual({
[Field.OUTPUT]: { address: '0x6B175474E89094C44Da98b954EedeAC495271d0F' },
[Field.INPUT]: { address: WETH[ChainId.MAINNET].address },
typedValue: '20.5',
independentField: Field.OUTPUT
})
})
test('does not duplicate eth for invalid output token', () => {
store.dispatch(
setDefaultsFromURLSearch({
chainId: ChainId.MAINNET,
queryString: '?outputCurrency=invalid'
selectToken({
field: Field.OUTPUT,
address: '0x0000'
})
)
expect(store.getState()).toEqual({
[Field.OUTPUT]: { address: '0x0000' },
[Field.INPUT]: { address: '' },
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
typedValue: '',
independentField: Field.INPUT
})
})
test('output ETH only', () => {
store.dispatch(
setDefaultsFromURLSearch({
chainId: ChainId.MAINNET,
queryString: '?outputCurrency=eth&exactAmount=20.5'
})
)
expect(store.getState()).toEqual({
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
[Field.INPUT]: { address: '' },
typedValue: '20.5',
independentField: Field.INPUT
})
})
})
})
import { parse } from 'qs'
import { createReducer } from '@reduxjs/toolkit'
import { ChainId, WETH } from '@uniswap/sdk'
import { isAddress } from '../../utils'
import { Field, selectToken, setDefaultsFromURLSearch, switchTokens, typeInput } from './actions'
import { Field, replaceSwapState, selectToken, switchTokens, typeInput } from './actions'
export interface SwapState {
readonly independentField: Field
......@@ -26,58 +23,18 @@ const initialState: SwapState = {
}
}
function parseCurrencyFromURLParameter(urlParam: any, chainId: number): string {
if (typeof urlParam === 'string') {
const valid = isAddress(urlParam)
if (valid) return valid
if (urlParam.toLowerCase() === 'eth') return WETH[chainId as ChainId]?.address ?? ''
if (valid === false) return WETH[chainId as ChainId]?.address ?? ''
}
return WETH[chainId as ChainId]?.address
}
function parseTokenAmountURLParameter(urlParam: any): string {
return typeof urlParam === 'string' && !isNaN(parseFloat(urlParam)) ? urlParam : ''
}
function parseIndependentFieldURLParameter(urlParam: any): Field {
return typeof urlParam === 'string' && urlParam.toLowerCase() === 'output' ? Field.OUTPUT : Field.INPUT
}
export default createReducer<SwapState>(initialState, builder =>
builder
.addCase(setDefaultsFromURLSearch, (_, { payload: { queryString, chainId } }) => {
if (queryString && queryString.length > 1) {
const parsedQs = parse(queryString, { parseArrays: false, ignoreQueryPrefix: true })
let inputCurrency = parseCurrencyFromURLParameter(parsedQs.inputCurrency, chainId)
let outputCurrency = parseCurrencyFromURLParameter(parsedQs.outputCurrency, chainId)
if (inputCurrency === outputCurrency) {
if (typeof parsedQs.outputCurrency === 'string') {
inputCurrency = ''
} else {
outputCurrency = ''
}
}
return {
[Field.INPUT]: {
address: inputCurrency
},
[Field.OUTPUT]: {
address: outputCurrency
},
typedValue: parseTokenAmountURLParameter(parsedQs.exactAmount),
independentField: parseIndependentFieldURLParameter(parsedQs.exactField)
}
}
.addCase(replaceSwapState, (state, { payload: { typedValue, field, inputTokenAddress, outputTokenAddress } }) => {
return {
...initialState,
[Field.INPUT]: {
address: WETH[chainId as ChainId]?.address ?? ''
}
address: inputTokenAddress
},
[Field.OUTPUT]: {
address: outputTokenAddress
},
independentField: field,
typedValue: typedValue
}
})
.addCase(selectToken, (state, { payload: { address, field } }) => {
......
......@@ -15,7 +15,7 @@ export const addTransaction = createAction<{
chainId: number
hash: string
from: string
approvalOfToken?: string
approval?: { tokenAddress: string; spender: string }
summary?: string
}>('addTransaction')
export const clearAllTransactions = createAction<{ chainId: number }>('clearAllTransactions')
......
import { TransactionResponse } from '@ethersproject/providers'
import { useCallback } from 'react'
import { useCallback, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useActiveWeb3React } from '../../hooks'
......@@ -10,7 +10,7 @@ import { TransactionDetails, TransactionState } from './reducer'
// helper that can take a ethers library transaction response and add it to the list of transactions
export function useTransactionAdder(): (
response: TransactionResponse,
customData?: { summary?: string; approvalOfToken?: string }
customData?: { summary?: string; approval?: { tokenAddress: string; spender: string } }
) => void {
const { chainId, account } = useActiveWeb3React()
const dispatch = useDispatch<AppDispatch>()
......@@ -18,7 +18,7 @@ export function useTransactionAdder(): (
return useCallback(
(
response: TransactionResponse,
{ summary, approvalOfToken }: { summary?: string; approvalOfToken?: string } = {}
{ summary, approval }: { summary?: string; approval?: { tokenAddress: string; spender: string } } = {}
) => {
if (!account) return
if (!chainId) return
......@@ -27,7 +27,7 @@ export function useTransactionAdder(): (
if (!hash) {
throw Error('No transaction hash found.')
}
dispatch(addTransaction({ hash, from: account, chainId, approvalOfToken, summary }))
dispatch(addTransaction({ hash, from: account, chainId, approval, summary }))
},
[dispatch, chainId, account]
)
......@@ -51,15 +51,22 @@ export function useIsTransactionPending(transactionHash?: string): boolean {
}
// returns whether a token has a pending approval transaction
export function useHasPendingApproval(tokenAddress?: string): boolean {
export function useHasPendingApproval(tokenAddress: string | undefined, spender: string | undefined): boolean {
const allTransactions = useAllTransactions()
return typeof tokenAddress !== 'string'
? false
: Object.keys(allTransactions).some(hash => {
return useMemo(
() =>
typeof tokenAddress === 'string' &&
typeof spender === 'string' &&
Object.keys(allTransactions).some(hash => {
if (allTransactions[hash]?.receipt) {
return false
} else {
return allTransactions[hash]?.approvalOfToken === tokenAddress
return (
allTransactions[hash]?.approval?.tokenAddress === tokenAddress &&
allTransactions[hash]?.approval?.spender === spender
)
}
})
}),
[allTransactions, spender, tokenAddress]
)
}
......@@ -5,7 +5,7 @@ const now = () => new Date().getTime()
export interface TransactionDetails {
hash: string
approvalOfToken?: string
approval?: { tokenAddress: string; spender: string }
summary?: string
receipt?: SerializableTransactionReceipt
addedTime: number
......@@ -26,12 +26,12 @@ const initialState: TransactionState = {}
export default createReducer(initialState, builder =>
builder
.addCase(addTransaction, (state, { payload: { chainId, from, hash, approvalOfToken, summary } }) => {
.addCase(addTransaction, (state, { payload: { chainId, from, hash, approval, summary } }) => {
if (state[chainId]?.[hash]) {
throw Error('Attempted to add existing transaction.')
}
state[chainId] = state[chainId] ?? {}
state[chainId][hash] = { hash, approvalOfToken, summary, from, addedTime: now() }
state[chainId][hash] = { hash, approval, summary, from, addedTime: now() }
})
.addCase(clearAllTransactions, (state, { payload: { chainId } }) => {
if (!state[chainId]) return
......
......@@ -3,7 +3,6 @@ import { getAddress } from '@ethersproject/address'
import { AddressZero } from '@ethersproject/constants'
import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
import { BigNumber } from '@ethersproject/bignumber'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { abi as IUniswapV2Router02ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router02.json'
import { ROUTER_ADDRESS } from '../constants'
import { ChainId, JSBI, Percent, TokenAmount } from '@uniswap/sdk'
......@@ -92,11 +91,6 @@ export function getRouterContract(_: number, library: Web3Provider, account?: st
return getContract(ROUTER_ADDRESS, IUniswapV2Router02ABI, library, account)
}
// account is optional
export function getExchangeContract(pairAddress: string, library: Web3Provider, account?: string) {
return getContract(pairAddress, IUniswapV2PairABI, library, account)
}
export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
}
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