diff --git a/src/components/Header/VersionSwitch.tsx b/src/components/Header/VersionSwitch.tsx
new file mode 100644
index 00000000000..3a23449a8ac
--- /dev/null
+++ b/src/components/Header/VersionSwitch.tsx
@@ -0,0 +1,64 @@
+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 (
+
+ V2
+ V1
+
+ )
+}
diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx
index 11314957a87..3298d2866ac 100644
--- a/src/components/Header/index.tsx
+++ b/src/components/Header/index.tsx
@@ -1,27 +1,27 @@
+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() {
)}
-
- {!isMobile && (
-
- V2
- V1
-
- )}
-
+ {!isMobile && }
- {!isMobile && chainId === ChainId.ROPSTEN && Ropsten}
- {!isMobile && chainId === ChainId.RINKEBY && Rinkeby}
- {!isMobile && chainId === ChainId.GÖRLI && Görli}
- {!isMobile && chainId === ChainId.KOVAN && Kovan}
+ {!isMobile && NETWORK_LABELS[chainId] && {NETWORK_LABELS[chainId]}}
{account && userEthBalance ? (
diff --git a/src/components/TxnPopup/index.tsx b/src/components/TxnPopup/index.tsx
index 0656ee33fd8..4c67e76ec49 100644
--- a/src/components/TxnPopup/index.tsx
+++ b/src/components/TxnPopup/index.tsx
@@ -1,7 +1,7 @@
-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 (
- setIsRunning(false)} onMouseLeave={() => setIsRunning(true)}>
- {success ? (
-
- ) : (
-
- )}
+
+
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
diff --git a/src/components/swap/BetterTradeLink.tsx b/src/components/swap/BetterTradeLink.tsx
new file mode 100644
index 00000000000..dcd3c07ad39
--- /dev/null
+++ b/src/components/swap/BetterTradeLink.tsx
@@ -0,0 +1,40 @@
+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 { StyledInternalLink } from '../../theme'
+import { YellowCard } from '../Card'
+import { AutoColumn } from '../Column'
+
+export default function BetterTradeLink({ version }: { version: Version }) {
+ const theme = useContext(ThemeContext)
+ const location = useLocation()
+ const search = useParsedQueryString()
+
+ const linkDestination = useMemo(() => {
+ return {
+ ...location,
+ search: `?${stringify({
+ ...search,
+ use: version
+ })}`
+ }
+ }, [location, search, version])
+
+ return (
+
+
+
+ There is a better price for this trade on{' '}
+
+ Uniswap {version.toUpperCase()} ↗
+
+
+
+
+ )
+}
diff --git a/src/components/swap/V1TradeLink.tsx b/src/components/swap/V1TradeLink.tsx
deleted file mode 100644
index 4a65d584e41..00000000000
--- a/src/components/swap/V1TradeLink.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React, { useContext } from 'react'
-import { Text } from 'rebass'
-import { ThemeContext } from 'styled-components'
-
-import { ExternalLink } from '../../theme'
-import { YellowCard } from '../Card'
-import { AutoColumn } from '../Column'
-
-export default function V1TradeLink({ v1TradeLinkIfBetter }: { v1TradeLinkIfBetter: string }) {
- const theme = useContext(ThemeContext)
- return v1TradeLinkIfBetter ? (
-
-
-
- There is a better price for this trade on{' '}
-
- Uniswap V1 ↗
-
-
-
-
- ) : null
-}
diff --git a/src/constants/index.ts b/src/constants/index.ts
index 21dd2d952fd..48b548b9232 100644
--- a/src/constants/index.ts
+++ b/src/constants/index.ts
@@ -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))
diff --git a/src/data/V1.ts b/src/data/V1.ts
index 7f83fc2374d..e86603db43d 100644
--- a/src/data/V1.ts
+++ b/src/data/V1.ts
@@ -1,27 +1,25 @@
-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)
+ }
}
diff --git a/src/hooks/useApproveCallback.ts b/src/hooks/useApproveCallback.ts
index bdc4816581a..5ab1b141489 100644
--- a/src/hooks/useApproveCallback.ts
+++ b/src/hooks/useApproveCallback.ts
@@ -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] {
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 => {
- 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)
}
diff --git a/src/hooks/useContract.ts b/src/hooks/useContract.ts
index 6069874ed05..654c261e70f 100644
--- a/src/hooks/useContract.ts
+++ b/src/hooks/useContract.ts
@@ -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)
}
diff --git a/src/hooks/useParsedQueryString.ts b/src/hooks/useParsedQueryString.ts
new file mode 100644
index 00000000000..f0a3002f6d6
--- /dev/null
+++ b/src/hooks/useParsedQueryString.ts
@@ -0,0 +1,11 @@
+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]
+ )
+}
diff --git a/src/hooks/useSwapCallback.ts b/src/hooks/useSwapCallback.ts
index 130f843381a..b68efccf04d 100644
--- a/src/hooks/useSwapCallback.ts
+++ b/src/hooks/useSwapCallback.ts
@@ -1,15 +1,19 @@
import { BigNumber } from '@ethersproject/bignumber'
+import { MaxUint256 } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts'
-import { ChainId, Token, Trade, TradeType, WETH } from '@uniswap/sdk'
+import { ChainId, Trade, TradeType, WETH } from '@uniswap/sdk'
import { useMemo } from 'react'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
import { useTokenAllowance } from '../data/Allowances'
+import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
import { Field } from '../state/swap/actions'
import { useTransactionAdder } from '../state/transactions/hooks'
-import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { calculateGasMargin, getRouterContract, isAddress } from '../utils'
+import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { useActiveWeb3React } from './index'
+import { useV1ExchangeContract } from './useContract'
import useENSName from './useENSName'
+import { Version } from './useToggledVersion'
enum SwapType {
EXACT_TOKENS_FOR_TOKENS,
@@ -17,25 +21,37 @@ enum SwapType {
EXACT_ETH_FOR_TOKENS,
TOKENS_FOR_EXACT_TOKENS,
TOKENS_FOR_EXACT_ETH,
- ETH_FOR_EXACT_TOKENS
+ ETH_FOR_EXACT_TOKENS,
+ V1_EXACT_ETH_FOR_TOKENS,
+ V1_EXACT_TOKENS_FOR_ETH,
+ V1_EXACT_TOKENS_FOR_TOKENS,
+ V1_ETH_FOR_EXACT_TOKENS,
+ V1_TOKENS_FOR_EXACT_ETH,
+ V1_TOKENS_FOR_EXACT_TOKENS
}
-function getSwapType(tokens: { [field in Field]?: Token }, isExactIn: boolean, chainId: number): SwapType {
+function getSwapType(trade: Trade | undefined): SwapType | undefined {
+ if (!trade) return undefined
+ const chainId = trade.inputAmount.token.chainId
+ const inputWETH = trade.inputAmount.token.equals(WETH[chainId])
+ const outputWETH = trade.outputAmount.token.equals(WETH[chainId])
+ const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
+ const isV1 = getTradeVersion(trade) === Version.v1
if (isExactIn) {
- if (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) {
- return SwapType.EXACT_ETH_FOR_TOKENS
- } else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) {
- return SwapType.EXACT_TOKENS_FOR_ETH
+ if (inputWETH) {
+ return isV1 ? SwapType.V1_EXACT_ETH_FOR_TOKENS : SwapType.EXACT_ETH_FOR_TOKENS
+ } else if (outputWETH) {
+ return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_ETH : SwapType.EXACT_TOKENS_FOR_ETH
} else {
- return SwapType.EXACT_TOKENS_FOR_TOKENS
+ return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_TOKENS : SwapType.EXACT_TOKENS_FOR_TOKENS
}
} else {
- if (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) {
- return SwapType.ETH_FOR_EXACT_TOKENS
- } else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) {
- return SwapType.TOKENS_FOR_EXACT_ETH
+ if (inputWETH) {
+ return isV1 ? SwapType.V1_ETH_FOR_EXACT_TOKENS : SwapType.ETH_FOR_EXACT_TOKENS
+ } else if (outputWETH) {
+ return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_ETH : SwapType.TOKENS_FOR_EXACT_ETH
} else {
- return SwapType.TOKENS_FOR_EXACT_TOKENS
+ return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_TOKENS : SwapType.TOKENS_FOR_EXACT_TOKENS
}
}
}
@@ -49,13 +65,19 @@ export function useSwapCallback(
to?: string // recipient of output, optional
): null | (() => Promise) {
const { account, chainId, library } = useActiveWeb3React()
- const inputAllowance = useTokenAllowance(trade?.inputAmount?.token, account ?? undefined, ROUTER_ADDRESS)
const addTransaction = useTransactionAdder()
const recipient = to ? isAddress(to) : account
const ensName = useENSName(to)
+ const tradeVersion = getTradeVersion(trade)
+ const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
+ const inputAllowance = useTokenAllowance(
+ trade?.inputAmount?.token,
+ account ?? undefined,
+ tradeVersion === Version.v1 ? v1Exchange?.address : ROUTER_ADDRESS
+ )
return useMemo(() => {
- if (!trade || !recipient) return null
+ if (!trade || !recipient || !tradeVersion) return null
// will always be defined
const {
@@ -78,17 +100,17 @@ export function useSwapCallback(
throw new Error('missing dependencies in onSwap callback')
}
- const routerContract: Contract = getRouterContract(chainId, library, account)
+ const contract: Contract | null =
+ tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
+ if (!contract) {
+ throw new Error('Failed to get a swap contract')
+ }
const path = trade.route.path.map(t => t.address)
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline
- const swapType = getSwapType(
- { [Field.INPUT]: trade.inputAmount.token, [Field.OUTPUT]: trade.outputAmount.token },
- trade.tradeType === TradeType.EXACT_INPUT,
- chainId as ChainId
- )
+ const swapType = getSwapType(trade)
// let estimate: Function, method: Function,
let methodNames: string[],
@@ -145,11 +167,63 @@ export function useSwapCallback(
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
+ case SwapType.V1_EXACT_ETH_FOR_TOKENS:
+ methodNames = ['ethToTokenTransferInput']
+ args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
+ value = BigNumber.from(slippageAdjustedInput.raw.toString())
+ break
+ case SwapType.V1_EXACT_TOKENS_FOR_TOKENS:
+ methodNames = ['tokenToTokenTransferInput']
+ args = [
+ slippageAdjustedInput.raw.toString(),
+ slippageAdjustedOutput.raw.toString(),
+ 1,
+ deadlineFromNow,
+ recipient,
+ trade.outputAmount.token.address
+ ]
+ break
+ case SwapType.V1_EXACT_TOKENS_FOR_ETH:
+ methodNames = ['tokenToEthTransferOutput']
+ args = [
+ slippageAdjustedOutput.raw.toString(),
+ slippageAdjustedInput.raw.toString(),
+ deadlineFromNow,
+ recipient
+ ]
+ break
+ case SwapType.V1_ETH_FOR_EXACT_TOKENS:
+ methodNames = ['ethToTokenTransferOutput']
+ args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
+ value = BigNumber.from(slippageAdjustedInput.raw.toString())
+ break
+ case SwapType.V1_TOKENS_FOR_EXACT_ETH:
+ methodNames = ['tokenToEthTransferOutput']
+ args = [
+ slippageAdjustedOutput.raw.toString(),
+ slippageAdjustedInput.raw.toString(),
+ deadlineFromNow,
+ recipient
+ ]
+ break
+ case SwapType.V1_TOKENS_FOR_EXACT_TOKENS:
+ methodNames = ['tokenToTokenTransferOutput']
+ args = [
+ slippageAdjustedOutput.raw.toString(),
+ slippageAdjustedInput.raw.toString(),
+ MaxUint256.toString(),
+ deadlineFromNow,
+ recipient,
+ trade.outputAmount.token.address
+ ]
+ break
+ default:
+ throw new Error(`Unhandled swap type: ${swapType}`)
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map(methodName =>
- routerContract.estimateGas[methodName](...args, value ? { value } : {})
+ contract.estimateGas[methodName](...args, value ? { value } : {})
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed for ${methodName}`, error)
@@ -198,38 +272,25 @@ export function useSwapCallback(
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
- return routerContract[methodName](...args, {
+ return contract[methodName](...args, {
gasLimit: safeGasEstimate,
...(value ? { value } : {})
})
.then((response: any) => {
- if (recipient === account) {
- addTransaction(response, {
- summary:
- 'Swap ' +
- slippageAdjustedInput.toSignificant(3) +
- ' ' +
- trade.inputAmount.token.symbol +
- ' for ' +
- slippageAdjustedOutput.toSignificant(3) +
- ' ' +
- trade.outputAmount.token.symbol
- })
- } else {
- addTransaction(response, {
- summary:
- 'Swap ' +
- slippageAdjustedInput.toSignificant(3) +
- ' ' +
- trade.inputAmount.token.symbol +
- ' for ' +
- slippageAdjustedOutput.toSignificant(3) +
- ' ' +
- trade.outputAmount.token.symbol +
- ' to ' +
- (ensName ?? recipient)
- })
- }
+ const inputSymbol = trade.inputAmount.token.symbol
+ const outputSymbol = trade.outputAmount.token.symbol
+ const inputAmount = slippageAdjustedInput.toSignificant(3)
+ const outputAmount = slippageAdjustedOutput.toSignificant(3)
+
+ const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}`
+ const withRecipient = recipient === account ? base : `${base} to ${ensName ?? recipient}`
+
+ const withVersion =
+ tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${tradeVersion.toUpperCase()}`
+
+ addTransaction(response, {
+ summary: withVersion
+ })
return response.hash
})
@@ -240,11 +301,24 @@ export function useSwapCallback(
}
// otherwise, the error was unexpected and we need to convey that
else {
- console.error(`swap failed for ${methodName}`, error)
+ console.error(`Swap failed`, error, methodName, args, value)
throw Error('An error occurred while swapping. Please contact support.')
}
})
}
}
- }, [account, allowedSlippage, addTransaction, chainId, deadline, inputAllowance, library, trade, ensName, recipient])
+ }, [
+ trade,
+ recipient,
+ tradeVersion,
+ allowedSlippage,
+ chainId,
+ inputAllowance,
+ library,
+ account,
+ v1Exchange,
+ deadline,
+ addTransaction,
+ ensName
+ ])
}
diff --git a/src/hooks/useToggledVersion.ts b/src/hooks/useToggledVersion.ts
new file mode 100644
index 00000000000..4876918d590
--- /dev/null
+++ b/src/hooks/useToggledVersion.ts
@@ -0,0 +1,13 @@
+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
+}
diff --git a/src/pages/MigrateV1/MigrateV1Exchange.tsx b/src/pages/MigrateV1/MigrateV1Exchange.tsx
index f85f19a63dd..ffc0fc79921 100644
--- a/src/pages/MigrateV1/MigrateV1Exchange.tsx
+++ b/src/pages/MigrateV1/MigrateV1Exchange.tsx
@@ -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 ? (
+ Approving
+ ) : approval === ApprovalState.APPROVED ? (
+ 'Approved'
+ ) : (
+ 'Approve'
+ )}
diff --git a/src/pages/Send/index.tsx b/src/pages/Send/index.tsx
index 484d1514645..e73352e449e 100644
--- a/src/pages/Send/index.tsx
+++ b/src/pages/Send/index.tsx
@@ -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) {
)}
-
+ {betterTradeLinkVersion && }
diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx
index 5406eb2688e..9f329f67b8b 100644
--- a/src/pages/Swap/index.tsx
+++ b/src/pages/Swap/index.tsx
@@ -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) {
)}
-
+ {betterTradeLinkVersion && }
diff --git a/src/state/swap/actions.ts b/src/state/swap/actions.ts
index f618612c606..91cb3ebddc0 100644
--- a/src/state/swap/actions.ts
+++ b/src/state/swap/actions.ts
@@ -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('switchTokens')
export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInput')
+export const replaceSwapState = createAction<{
+ field: Field
+ typedValue: string
+ inputTokenAddress?: string
+ outputTokenAddress?: string
+}>('replaceSwapState')
diff --git a/src/state/swap/hooks.test.ts b/src/state/swap/hooks.test.ts
new file mode 100644
index 00000000000..f62e97581fd
--- /dev/null
+++ b/src/state/swap/hooks.test.ts
@@ -0,0 +1,53 @@
+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
+ })
+ })
+ })
+})
diff --git a/src/state/swap/hooks.ts b/src/state/swap/hooks.ts
index 3d23425eacb..c1f3b92affc 100644
--- a/src/state/swap/hooks.ts
+++ b/src/state/swap/hooks.ts
@@ -1,15 +1,18 @@
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(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()
+ 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])
}
diff --git a/src/state/swap/reducer.test.ts b/src/state/swap/reducer.test.ts
index 0308caab0eb..d1f555dd430 100644
--- a/src/state/swap/reducer.test.ts
+++ b/src/state/swap/reducer.test.ts
@@ -1,6 +1,5 @@
-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
- })
- })
})
})
diff --git a/src/state/swap/reducer.ts b/src/state/swap/reducer.ts
index 90dab767b5b..cc48efbfa32 100644
--- a/src/state/swap/reducer.ts
+++ b/src/state/swap/reducer.ts
@@ -1,8 +1,5 @@
-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(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 } }) => {
diff --git a/src/state/transactions/actions.ts b/src/state/transactions/actions.ts
index 7549064f372..af5792a405e 100644
--- a/src/state/transactions/actions.ts
+++ b/src/state/transactions/actions.ts
@@ -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')
diff --git a/src/state/transactions/hooks.tsx b/src/state/transactions/hooks.tsx
index 0559111c986..a2180487cd8 100644
--- a/src/state/transactions/hooks.tsx
+++ b/src/state/transactions/hooks.tsx
@@ -1,5 +1,5 @@
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()
@@ -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]
+ )
}
diff --git a/src/state/transactions/reducer.ts b/src/state/transactions/reducer.ts
index bada0346bd9..5315b11529f 100644
--- a/src/state/transactions/reducer.ts
+++ b/src/state/transactions/reducer.ts
@@ -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
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 7fbe0496758..c8efae8c9b8 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -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
}