From 09b4d05176c00426be488f57aafcc745a14d8ced Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Tue, 9 Jun 2020 10:42:04 -0400 Subject: [PATCH 01/17] swap-v1 --- src/data/V1.ts | 65 +++++++++++++++++++++++++++++------------ src/state/swap/hooks.ts | 19 +++++++----- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/data/V1.ts b/src/data/V1.ts index 7f83fc2374d..ed2f6d68209 100644 --- a/src/data/V1.ts +++ b/src/data/V1.ts @@ -1,4 +1,4 @@ -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' @@ -18,9 +18,8 @@ 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 tokenBalance = useTokenBalance(v1PairAddress, token) const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? ''] @@ -43,7 +42,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 +73,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 +): { v1Trade: Trade | undefined; inputIsWETH: boolean; outputIsWETH: boolean } { 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 +103,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,6 +111,37 @@ export function useV1TradeLinkIfBetter( ? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT) : undefined } catch {} + return { v1Trade, inputIsWETH, outputIsWETH } +} + +export function useSwapCallbackV1( + isExactIn?: boolean, + inputToken?: Token, + outputToken?: Token, + exactAmount?: TokenAmount +): null | (() => Promise) { + const { outputIsWETH, inputIsWETH, v1Trade } = useV1Trade(isExactIn, inputToken, outputToken, exactAmount) + return useMemo(() => { + if (!v1Trade) return null + + if (inputIsWETH) { + } else if (outputIsWETH) { + } else { + } + + return null + }, [inputIsWETH, outputIsWETH, v1Trade]) +} + +export function useV1TradeLinkIfBetter( + isExactIn?: boolean, + input?: Token, + output?: Token, + exactAmount?: TokenAmount, + v2Trade?: Trade, + minimumDelta: Percent = new Percent('0') +): string | undefined { + const { v1Trade, inputIsWETH, outputIsWETH } = useV1Trade(isExactIn, input, output, exactAmount) let v1HasBetterTrade = false if (v1Trade) { diff --git a/src/state/swap/hooks.ts b/src/state/swap/hooks.ts index 3d23425eacb..a01453040ae 100644 --- a/src/state/swap/hooks.ts +++ b/src/state/swap/hooks.ts @@ -33,7 +33,7 @@ export function useSwapActionHandlers(): { [dispatch] ) - const onSwapTokens = useCallback(() => { + const onSwitchTokens = useCallback(() => { dispatch(switchTokens()) }, [dispatch]) @@ -45,7 +45,7 @@ export function useSwapActionHandlers(): { ) return { - onSwitchTokens: onSwapTokens, + onSwitchTokens, onTokenSelection, onUserInput } @@ -90,13 +90,16 @@ 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 amount = tryParseAmount(typedValue, (isExactIn ? tokenIn : tokenOut) ?? undefined) - const bestTradeExactIn = useTradeExactIn(isExactIn ? amount : undefined, tokenOut) - const bestTradeExactOut = useTradeExactOut(tokenIn, !isExactIn ? amount : undefined) + const bestTradeExactIn = useTradeExactIn(isExactIn ? amount : undefined, tokenOut ?? undefined) + const bestTradeExactOut = useTradeExactOut(tokenIn ?? undefined, !isExactIn ? amount : undefined) const bestTrade = isExactIn ? bestTradeExactIn : bestTradeExactOut @@ -111,8 +114,8 @@ export function useDerivedSwapInfo(): { } 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 From 78e6943d8a6bf460cbe2f25799a2becb74aa7180 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Wed, 10 Jun 2020 16:28:45 -0400 Subject: [PATCH 02/17] toggle the version switch based on the search query parameter --- src/components/Header/VersionSwitch.tsx | 50 ++++++++++++++++ src/components/Header/index.tsx | 78 ++++++++----------------- 2 files changed, 74 insertions(+), 54 deletions(-) create mode 100644 src/components/Header/VersionSwitch.tsx diff --git a/src/components/Header/VersionSwitch.tsx b/src/components/Header/VersionSwitch.tsx new file mode 100644 index 00000000000..8996c277a63 --- /dev/null +++ b/src/components/Header/VersionSwitch.tsx @@ -0,0 +1,50 @@ +import { parse } from 'qs' +import React, { useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import styled from 'styled-components' + +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.div` + 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; + } +` + +function useParsedQueryString() { + const { search } = useLocation() + return useMemo( + () => (search && search.length > 1 ? parse(search, { parseArrays: false, ignoreQueryPrefix: true }) : {}), + [search] + ) +} + +export function VersionSwitch() { + const parsed = useParsedQueryString() + const isV1 = parsed['use'] === 'v1' + 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 ? ( From 9f5d525b9854f63438e0ddd22eb7de6d8904071d Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Wed, 10 Jun 2020 17:39:48 -0400 Subject: [PATCH 03/17] rework some of the query parameter stuff in send/swap --- src/components/Header/VersionSwitch.tsx | 21 ++----- src/components/swap/V1TradeLink.tsx | 25 +++++++-- src/hooks/useParsedQueryString.ts | 11 ++++ src/hooks/useToggledVersion.ts | 13 +++++ src/pages/Send/index.tsx | 5 +- src/pages/Swap/index.tsx | 5 +- src/state/swap/actions.ts | 7 ++- src/state/swap/hooks.test.ts | 53 ++++++++++++++++++ src/state/swap/hooks.ts | 74 ++++++++++++++++++++++--- src/state/swap/reducer.test.ts | 48 +++------------- src/state/swap/reducer.ts | 61 +++----------------- 11 files changed, 195 insertions(+), 128 deletions(-) create mode 100644 src/hooks/useParsedQueryString.ts create mode 100644 src/hooks/useToggledVersion.ts create mode 100644 src/state/swap/hooks.test.ts diff --git a/src/components/Header/VersionSwitch.tsx b/src/components/Header/VersionSwitch.tsx index 8996c277a63..c8c9be0813f 100644 --- a/src/components/Header/VersionSwitch.tsx +++ b/src/components/Header/VersionSwitch.tsx @@ -1,7 +1,6 @@ -import { parse } from 'qs' -import React, { useMemo } from 'react' -import { useLocation } from 'react-router-dom' +import React from 'react' import styled from 'styled-components' +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')}; @@ -30,21 +29,13 @@ const VersionToggle = styled.div` } ` -function useParsedQueryString() { - const { search } = useLocation() - return useMemo( - () => (search && search.length > 1 ? parse(search, { parseArrays: false, ignoreQueryPrefix: true }) : {}), - [search] - ) -} - export function VersionSwitch() { - const parsed = useParsedQueryString() - const isV1 = parsed['use'] === 'v1' + const version = useToggledVersion() + return ( - V2 - V1 + V2 + V1 ) } diff --git a/src/components/swap/V1TradeLink.tsx b/src/components/swap/V1TradeLink.tsx index 4a65d584e41..0e027d85b08 100644 --- a/src/components/swap/V1TradeLink.tsx +++ b/src/components/swap/V1TradeLink.tsx @@ -1,21 +1,38 @@ -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 }) { const theme = useContext(ThemeContext) + const location = useLocation() + const search = useParsedQueryString() + + const v1Location = useMemo(() => { + return { + ...location, + search: `?${stringify({ + ...search, + use: Version.v1 + })}` + } + }, [location, search]) + return v1TradeLinkIfBetter ? ( There is a better price for this trade on{' '} - + Uniswap V1 ↗ - + 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/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/Send/index.tsx b/src/pages/Send/index.tsx index 484d1514645..65d967ebeda 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' @@ -42,8 +41,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() diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 5406eb2688e..7115592add6 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' @@ -38,8 +37,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) 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 a01453040ae..f7c5e4224fa 100644 --- a/src/state/swap/hooks.ts +++ b/src/state/swap/hooks.ts @@ -1,15 +1,19 @@ 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 { V1_TRADE_LINK_THRESHOLD } from '../../constants' +import { useV1TradeLinkIfBetter } 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) @@ -156,13 +160,65 @@ export function useDerivedSwapInfo(): { } } -// updates the swap state to use the defaults for a given network whenever the query -// string updates -export function useDefaultsFromURLSearch(search?: string) { +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 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 } }) => { From 76871655f90172e8340e6ffa6b66dcc408fcd082 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Wed, 10 Jun 2020 17:44:33 -0400 Subject: [PATCH 04/17] hide the url when they click it --- src/components/swap/V1TradeLink.tsx | 7 ++++--- src/data/V1.ts | 14 ++++---------- src/pages/Send/index.tsx | 11 ++--------- src/pages/Swap/index.tsx | 4 ++-- src/state/swap/hooks.ts | 8 ++++---- 5 files changed, 16 insertions(+), 28 deletions(-) diff --git a/src/components/swap/V1TradeLink.tsx b/src/components/swap/V1TradeLink.tsx index 0e027d85b08..17016078aea 100644 --- a/src/components/swap/V1TradeLink.tsx +++ b/src/components/swap/V1TradeLink.tsx @@ -4,16 +4,17 @@ 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 useToggledVersion, { Version } from '../../hooks/useToggledVersion' import { StyledInternalLink } from '../../theme' import { YellowCard } from '../Card' import { AutoColumn } from '../Column' -export default function V1TradeLink({ v1TradeLinkIfBetter }: { v1TradeLinkIfBetter: string }) { +export default function V1TradeLink({ isV1TradeBetter }: { isV1TradeBetter: boolean }) { const theme = useContext(ThemeContext) const location = useLocation() const search = useParsedQueryString() + const toggled = useToggledVersion() === Version.v1 const v1Location = useMemo(() => { return { @@ -25,7 +26,7 @@ export default function V1TradeLink({ v1TradeLinkIfBetter }: { v1TradeLinkIfBett } }, [location, search]) - return v1TradeLinkIfBetter ? ( + return isV1TradeBetter && !toggled ? ( diff --git a/src/data/V1.ts b/src/data/V1.ts index ed2f6d68209..d5e47cb4c99 100644 --- a/src/data/V1.ts +++ b/src/data/V1.ts @@ -133,16 +133,15 @@ export function useSwapCallbackV1( }, [inputIsWETH, outputIsWETH, v1Trade]) } -export function useV1TradeLinkIfBetter( +export function useIsV1TradeBetter( isExactIn?: boolean, input?: Token, output?: Token, exactAmount?: TokenAmount, v2Trade?: Trade, minimumDelta: Percent = new Percent('0') -): string | undefined { - const { v1Trade, inputIsWETH, outputIsWETH } = useV1Trade(isExactIn, input, output, exactAmount) - +): boolean { + const { v1Trade } = useV1Trade(isExactIn, input, output, exactAmount) let v1HasBetterTrade = false if (v1Trade) { if (isExactIn) { @@ -157,10 +156,5 @@ export function useV1TradeLinkIfBetter( v1HasBetterTrade = !v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount) } } - - return v1HasBetterTrade && input && output - ? `https://v1.uniswap.exchange/swap?inputCurrency=${inputIsWETH ? 'ETH' : input.address}&outputCurrency=${ - outputIsWETH ? 'ETH' : output.address - }` - : undefined + return v1HasBetterTrade } diff --git a/src/pages/Send/index.tsx b/src/pages/Send/index.tsx index 65d967ebeda..82160628672 100644 --- a/src/pages/Send/index.tsx +++ b/src/pages/Send/index.tsx @@ -60,14 +60,7 @@ export default function Send() { // trade details, check query params for initial state const { independentField, typedValue } = useSwapState() - const { - parsedAmounts, - bestTrade, - tokenBalances, - tokens, - error: swapError, - v1TradeLinkIfBetter - } = useDerivedSwapInfo() + const { parsedAmounts, bestTrade, tokenBalances, tokens, error: swapError, isV1TradeBetter } = useDerivedSwapInfo() const isSwapValid = !swapError && !recipientError && bestTrade const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT @@ -537,7 +530,7 @@ export default function Send() { )} - + diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 7115592add6..27a64f6a67b 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -48,7 +48,7 @@ export default function Swap() { // swap state const { independentField, typedValue } = useSwapState() - const { bestTrade, tokenBalances, parsedAmounts, tokens, error, v1TradeLinkIfBetter } = useDerivedSwapInfo() + const { bestTrade, tokenBalances, parsedAmounts, tokens, error, isV1TradeBetter } = useDerivedSwapInfo() const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers() const isValid = !error const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT @@ -333,7 +333,7 @@ export default function Swap() { )} - + diff --git a/src/state/swap/hooks.ts b/src/state/swap/hooks.ts index f7c5e4224fa..5590e5d938b 100644 --- a/src/state/swap/hooks.ts +++ b/src/state/swap/hooks.ts @@ -4,7 +4,7 @@ import { ParsedQs } from 'qs' import { useCallback, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { V1_TRADE_LINK_THRESHOLD } from '../../constants' -import { useV1TradeLinkIfBetter } from '../../data/V1' +import { useIsV1TradeBetter } from '../../data/V1' import { useActiveWeb3React } from '../../hooks' import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens' import { useTradeExactIn, useTradeExactOut } from '../../hooks/Trades' @@ -80,7 +80,7 @@ export function useDerivedSwapInfo(): { parsedAmounts: { [field in Field]?: TokenAmount } bestTrade: Trade | null error?: string - v1TradeLinkIfBetter?: string + isV1TradeBetter: boolean } { const { account } = useActiveWeb3React() @@ -123,7 +123,7 @@ export function useDerivedSwapInfo(): { } // get link to trade on v1, if a better rate exists - const v1TradeLinkIfBetter = useV1TradeLinkIfBetter( + const isV1TradeBetter: boolean = useIsV1TradeBetter( isExactIn, tokens[Field.INPUT], tokens[Field.OUTPUT], @@ -156,7 +156,7 @@ export function useDerivedSwapInfo(): { parsedAmounts, bestTrade, error, - v1TradeLinkIfBetter + isV1TradeBetter } } From a654398db82a96462d6cf67f709452ffdcd3d5ff Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Wed, 10 Jun 2020 17:51:52 -0400 Subject: [PATCH 05/17] allow switching back to v2 via the toggle --- src/components/Header/VersionSwitch.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/Header/VersionSwitch.tsx b/src/components/Header/VersionSwitch.tsx index c8c9be0813f..aca32ddcb1d 100644 --- a/src/components/Header/VersionSwitch.tsx +++ b/src/components/Header/VersionSwitch.tsx @@ -1,5 +1,8 @@ -import React from 'react' +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 }>` @@ -15,7 +18,7 @@ const VersionLabel = styled.span<{ enabled: boolean }>` color: ${({ theme, enabled }) => (enabled ? theme.white : theme.primary3)}; } ` -const VersionToggle = styled.div` +const VersionToggle = styled(Link)` border-radius: 16px; background: ${({ theme }) => theme.primary5}; border: 1px solid ${({ theme }) => theme.primary4}; @@ -31,9 +34,17 @@ const VersionToggle = styled.div` export function VersionSwitch() { const version = useToggledVersion() + const location = useLocation() + const query = useParsedQueryString() + const toggleDest = useMemo(() => { + return { + ...location, + search: `?${stringify({ ...query, use: version === Version.v1 ? undefined : Version.v1 })}` + } + }, [location, query, version]) return ( - + V2 V1 From 6ce8db96366e545b08ec72295d8e4b13a4770cba Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Wed, 10 Jun 2020 18:00:00 -0400 Subject: [PATCH 06/17] represent the v1 trade in the UI if they toggle it on --- src/components/Header/VersionSwitch.tsx | 2 +- src/data/V1.ts | 4 ++-- src/pages/Swap/index.tsx | 15 ++++++++++++++- src/state/swap/hooks.ts | 6 ++++-- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/components/Header/VersionSwitch.tsx b/src/components/Header/VersionSwitch.tsx index aca32ddcb1d..de4f12c5efb 100644 --- a/src/components/Header/VersionSwitch.tsx +++ b/src/components/Header/VersionSwitch.tsx @@ -1,5 +1,5 @@ import { stringify } from 'qs' -import React, { useCallback, useMemo } from 'react' +import React, { useMemo } from 'react' import { Link, useLocation } from 'react-router-dom' import styled from 'styled-components' import useParsedQueryString from '../../hooks/useParsedQueryString' diff --git a/src/data/V1.ts b/src/data/V1.ts index d5e47cb4c99..58230e8f82d 100644 --- a/src/data/V1.ts +++ b/src/data/V1.ts @@ -140,7 +140,7 @@ export function useIsV1TradeBetter( exactAmount?: TokenAmount, v2Trade?: Trade, minimumDelta: Percent = new Percent('0') -): boolean { +): [boolean, Trade | undefined] { const { v1Trade } = useV1Trade(isExactIn, input, output, exactAmount) let v1HasBetterTrade = false if (v1Trade) { @@ -156,5 +156,5 @@ export function useIsV1TradeBetter( v1HasBetterTrade = !v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount) } } - return v1HasBetterTrade + return [v1HasBetterTrade, v1Trade] } diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 27a64f6a67b..cbb051d931b 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -24,6 +24,7 @@ import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, MIN_ETH } from '.. 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 { @@ -48,7 +49,19 @@ export default function Swap() { // swap state const { independentField, typedValue } = useSwapState() - const { bestTrade, tokenBalances, parsedAmounts, tokens, error, isV1TradeBetter } = useDerivedSwapInfo() + const { + bestTrade: bestTradeV2, + tokenBalances, + parsedAmounts, + tokens, + error, + isV1TradeBetter, + v1Trade + } = useDerivedSwapInfo() + const bestTrade = { + [Version.v1]: v1Trade, + [Version.v2]: bestTradeV2 + }[useToggledVersion()] const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers() const isValid = !error const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT diff --git a/src/state/swap/hooks.ts b/src/state/swap/hooks.ts index 5590e5d938b..7501557e862 100644 --- a/src/state/swap/hooks.ts +++ b/src/state/swap/hooks.ts @@ -80,6 +80,7 @@ export function useDerivedSwapInfo(): { parsedAmounts: { [field in Field]?: TokenAmount } bestTrade: Trade | null error?: string + v1Trade: Trade | undefined isV1TradeBetter: boolean } { const { account } = useActiveWeb3React() @@ -123,7 +124,7 @@ export function useDerivedSwapInfo(): { } // get link to trade on v1, if a better rate exists - const isV1TradeBetter: boolean = useIsV1TradeBetter( + const [isV1TradeBetter, v1Trade] = useIsV1TradeBetter( isExactIn, tokens[Field.INPUT], tokens[Field.OUTPUT], @@ -156,7 +157,8 @@ export function useDerivedSwapInfo(): { parsedAmounts, bestTrade, error, - isV1TradeBetter + isV1TradeBetter, + v1Trade } } From a4270988ecdc005c5d68ad05668e6ed8d9169e8d Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Wed, 10 Jun 2020 21:46:41 -0400 Subject: [PATCH 07/17] show trade link in both directions (5% threshold for v1 link) --- .../{V1TradeLink.tsx => BetterTradeLink.tsx} | 19 +++--- src/constants/index.ts | 2 +- src/data/V1.ts | 62 +++++++------------ src/pages/Send/index.tsx | 37 +++++++++-- src/pages/Swap/index.tsx | 33 ++++++---- src/state/swap/hooks.ts | 11 +--- 6 files changed, 86 insertions(+), 78 deletions(-) rename src/components/swap/{V1TradeLink.tsx => BetterTradeLink.tsx} (69%) diff --git a/src/components/swap/V1TradeLink.tsx b/src/components/swap/BetterTradeLink.tsx similarity index 69% rename from src/components/swap/V1TradeLink.tsx rename to src/components/swap/BetterTradeLink.tsx index 17016078aea..dcd3c07ad39 100644 --- a/src/components/swap/V1TradeLink.tsx +++ b/src/components/swap/BetterTradeLink.tsx @@ -4,38 +4,37 @@ import { useLocation } from 'react-router' import { Text } from 'rebass' import { ThemeContext } from 'styled-components' import useParsedQueryString from '../../hooks/useParsedQueryString' -import useToggledVersion, { Version } from '../../hooks/useToggledVersion' +import { Version } from '../../hooks/useToggledVersion' import { StyledInternalLink } from '../../theme' import { YellowCard } from '../Card' import { AutoColumn } from '../Column' -export default function V1TradeLink({ isV1TradeBetter }: { isV1TradeBetter: boolean }) { +export default function BetterTradeLink({ version }: { version: Version }) { const theme = useContext(ThemeContext) const location = useLocation() const search = useParsedQueryString() - const toggled = useToggledVersion() === Version.v1 - const v1Location = useMemo(() => { + const linkDestination = useMemo(() => { return { ...location, search: `?${stringify({ ...search, - use: Version.v1 + use: version })}` } - }, [location, search]) + }, [location, search, version]) - return isV1TradeBetter && !toggled ? ( + return ( There is a better price for this trade on{' '} - - Uniswap V1 ↗ + + Uniswap {version.toUpperCase()} ↗ - ) : 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 58230e8f82d..d519aaf0b71 100644 --- a/src/data/V1.ts +++ b/src/data/V1.ts @@ -114,47 +114,27 @@ export function useV1Trade( return { v1Trade, inputIsWETH, outputIsWETH } } -export function useSwapCallbackV1( - isExactIn?: boolean, - inputToken?: Token, - outputToken?: Token, - exactAmount?: TokenAmount -): null | (() => Promise) { - const { outputIsWETH, inputIsWETH, v1Trade } = useV1Trade(isExactIn, inputToken, outputToken, exactAmount) - return useMemo(() => { - if (!v1Trade) return null - - if (inputIsWETH) { - } else if (outputIsWETH) { - } else { - } - - return null - }, [inputIsWETH, outputIsWETH, v1Trade]) -} +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') + } -export function useIsV1TradeBetter( - isExactIn?: boolean, - input?: Token, - output?: Token, - exactAmount?: TokenAmount, - v2Trade?: Trade, - minimumDelta: Percent = new Percent('0') -): [boolean, Trade | undefined] { - const { v1Trade } = useV1Trade(isExactIn, input, output, exactAmount) - 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) - } + 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) } - return [v1HasBetterTrade, v1Trade] } diff --git a/src/pages/Send/index.tsx b/src/pages/Send/index.tsx index 82160628672..e0f71dead94 100644 --- a/src/pages/Send/index.tsx +++ b/src/pages/Send/index.tsx @@ -19,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 { 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 { @@ -60,9 +67,29 @@ export default function Send() { // trade details, check query params for initial state const { independentField, typedValue } = useSwapState() - const { parsedAmounts, bestTrade, tokenBalances, tokens, error: swapError, isV1TradeBetter } = useDerivedSwapInfo() - const isSwapValid = !swapError && !recipientError && bestTrade + const { + parsedAmounts, + bestTrade: bestTradeV2, + tokenBalances, + tokens, + error: swapError, + 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 isSwapValid = !swapError && !recipientError && bestTrade const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT // modal and loading @@ -530,7 +557,7 @@ export default function Send() { )} - + {betterTradeLinkVersion && } diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index cbb051d931b..1d7d14c6e50 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -18,9 +18,15 @@ 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 { isTradeBetter } from '../../data/V1' import { useActiveWeb3React } from '../../hooks' import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback' import { useSwapCallback } from '../../hooks/useSwapCallback' @@ -49,19 +55,20 @@ export default function Swap() { // swap state const { independentField, typedValue } = useSwapState() - const { - bestTrade: bestTradeV2, - tokenBalances, - parsedAmounts, - tokens, - error, - isV1TradeBetter, - v1Trade - } = useDerivedSwapInfo() + const { bestTrade: bestTradeV2, tokenBalances, parsedAmounts, tokens, error, v1Trade } = useDerivedSwapInfo() + const toggledVersion = useToggledVersion() const bestTrade = { [Version.v1]: v1Trade, [Version.v2]: bestTradeV2 - }[useToggledVersion()] + }[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 { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers() const isValid = !error const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT @@ -346,7 +353,7 @@ export default function Swap() { )} - + {betterTradeLinkVersion && } diff --git a/src/state/swap/hooks.ts b/src/state/swap/hooks.ts index 7501557e862..51d42b67372 100644 --- a/src/state/swap/hooks.ts +++ b/src/state/swap/hooks.ts @@ -3,8 +3,7 @@ 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 { V1_TRADE_LINK_THRESHOLD } from '../../constants' -import { useIsV1TradeBetter } from '../../data/V1' +import { useV1Trade } from '../../data/V1' import { useActiveWeb3React } from '../../hooks' import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens' import { useTradeExactIn, useTradeExactOut } from '../../hooks/Trades' @@ -81,7 +80,6 @@ export function useDerivedSwapInfo(): { bestTrade: Trade | null error?: string v1Trade: Trade | undefined - isV1TradeBetter: boolean } { const { account } = useActiveWeb3React() @@ -124,13 +122,11 @@ export function useDerivedSwapInfo(): { } // get link to trade on v1, if a better rate exists - const [isV1TradeBetter, v1Trade] = useIsV1TradeBetter( + const { v1Trade } = useV1Trade( isExactIn, tokens[Field.INPUT], tokens[Field.OUTPUT], - isExactIn ? parsedAmounts[Field.INPUT] : parsedAmounts[Field.OUTPUT], - bestTrade ?? undefined, - V1_TRADE_LINK_THRESHOLD + isExactIn ? parsedAmounts[Field.INPUT] : parsedAmounts[Field.OUTPUT] ) let error: string | undefined @@ -157,7 +153,6 @@ export function useDerivedSwapInfo(): { parsedAmounts, bestTrade, error, - isV1TradeBetter, v1Trade } } From 4fa8e473b7a694644b29aa5c50fda4668e03a67f Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Wed, 10 Jun 2020 23:57:04 -0400 Subject: [PATCH 08/17] input amounts should reflect v1/v2 --- src/data/V1.ts | 12 +++++++----- src/pages/Send/index.tsx | 8 ++++++-- src/pages/Swap/index.tsx | 8 ++++++-- src/state/swap/hooks.ts | 38 +++++++++++++++----------------------- 4 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/data/V1.ts b/src/data/V1.ts index d519aaf0b71..2d33034c4d2 100644 --- a/src/data/V1.ts +++ b/src/data/V1.ts @@ -13,9 +13,7 @@ function useV1PairAddress(tokenAddress?: string): string | undefined { 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: boolean = token && WETH[token.chainId] ? token.equals(WETH[token.chainId]) : false @@ -81,7 +79,7 @@ export function useV1Trade( inputToken?: Token, outputToken?: Token, exactAmount?: TokenAmount -): { v1Trade: Trade | undefined; inputIsWETH: boolean; outputIsWETH: boolean } { +): Trade | undefined { const { chainId } = useActiveWeb3React() // get the mock v1 pairs @@ -111,7 +109,11 @@ export function useV1Trade( ? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT) : undefined } catch {} - return { v1Trade, inputIsWETH, outputIsWETH } + return v1Trade +} + +export function isTradeV1(trade?: Trade): boolean | undefined { + return trade?.route?.pairs?.some(pair => pair instanceof MockV1Pair) } const ZERO_PERCENT = new Percent('0') diff --git a/src/pages/Send/index.tsx b/src/pages/Send/index.tsx index e0f71dead94..a01bda7f2b1 100644 --- a/src/pages/Send/index.tsx +++ b/src/pages/Send/index.tsx @@ -68,7 +68,7 @@ export default function Send() { // trade details, check query params for initial state const { independentField, typedValue } = useSwapState() const { - parsedAmounts, + parsedAmount, bestTrade: bestTradeV2, tokenBalances, tokens, @@ -89,6 +89,11 @@ export default function Send() { ? 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 @@ -439,7 +444,6 @@ export default function Send() { 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]} diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 1d7d14c6e50..77064d173f1 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -55,7 +55,7 @@ export default function Swap() { // swap state const { independentField, typedValue } = useSwapState() - const { bestTrade: bestTradeV2, tokenBalances, parsedAmounts, tokens, error, v1Trade } = useDerivedSwapInfo() + const { bestTrade: bestTradeV2, tokenBalances, parsedAmount, tokens, error, v1Trade } = useDerivedSwapInfo() const toggledVersion = useToggledVersion() const bestTrade = { [Version.v1]: v1Trade, @@ -69,6 +69,11 @@ export default function Swap() { ? 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 @@ -264,7 +269,6 @@ export default function Swap() { 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]} diff --git a/src/state/swap/hooks.ts b/src/state/swap/hooks.ts index 51d42b67372..c1f3b92affc 100644 --- a/src/state/swap/hooks.ts +++ b/src/state/swap/hooks.ts @@ -76,7 +76,7 @@ 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 v1Trade: Trade | undefined @@ -99,18 +99,13 @@ export function useDerivedSwapInfo(): { ]) const isExactIn: boolean = independentField === Field.INPUT - const amount = tryParseAmount(typedValue, (isExactIn ? tokenIn : tokenOut) ?? undefined) + const parsedAmount = tryParseAmount(typedValue, (isExactIn ? tokenIn : tokenOut) ?? undefined) - const bestTradeExactIn = useTradeExactIn(isExactIn ? amount : undefined, tokenOut ?? undefined) - const bestTradeExactOut = useTradeExactOut(tokenIn ?? undefined, !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 ?? ''] @@ -122,35 +117,32 @@ export function useDerivedSwapInfo(): { } // get link to trade on v1, if a better rate exists - const { v1Trade } = useV1Trade( - isExactIn, - tokens[Field.INPUT], - tokens[Field.OUTPUT], - isExactIn ? parsedAmounts[Field.INPUT] : parsedAmounts[Field.OUTPUT] - ) + 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, v1Trade From 2f84343a3d4bdf96f7fc98665a4e40fdb474fb55 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Thu, 11 Jun 2020 00:03:57 -0400 Subject: [PATCH 09/17] perform the approve on v1 exchange for v1 trades --- src/data/V1.ts | 2 +- src/hooks/useApproveCallback.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/data/V1.ts b/src/data/V1.ts index 2d33034c4d2..9394cddae10 100644 --- a/src/data/V1.ts +++ b/src/data/V1.ts @@ -6,7 +6,7 @@ import { useV1FactoryContract } from '../hooks/useContract' 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 useV1PairAddress(tokenAddress?: string): string | undefined { const contract = useV1FactoryContract() const inputs = useMemo(() => [tokenAddress], [tokenAddress]) diff --git a/src/hooks/useApproveCallback.ts b/src/hooks/useApproveCallback.ts index bdc4816581a..429e39ef8f9 100644 --- a/src/hooks/useApproveCallback.ts +++ b/src/hooks/useApproveCallback.ts @@ -4,6 +4,7 @@ import { Trade, WETH, TokenAmount } from '@uniswap/sdk' import { useCallback, useMemo } from 'react' import { ROUTER_ADDRESS } from '../constants' import { useTokenAllowance } from '../data/Allowances' +import { isTradeV1, useV1PairAddress } from '../data/V1' import { Field } from '../state/swap/actions' import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks' import { computeSlippageAdjustedAmounts } from '../utils/prices' @@ -91,5 +92,7 @@ export function useApproveCallbackFromTrade(trade?: Trade, allowedSlippage = 0) () => (trade ? computeSlippageAdjustedAmounts(trade, allowedSlippage)[Field.INPUT] : undefined), [trade, allowedSlippage] ) - return useApproveCallback(amountToApprove, ROUTER_ADDRESS) + const tradeIsV1 = isTradeV1(trade) + const v1Exchange = useV1PairAddress(trade && tradeIsV1 ? trade.inputAmount.token.address : undefined) + return useApproveCallback(amountToApprove, v1Exchange ?? ROUTER_ADDRESS) } From baf67c8126e07869927a38c0afae7386a25b22ad Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Thu, 11 Jun 2020 10:42:56 -0400 Subject: [PATCH 10/17] get swap on v1 working --- src/data/V1.ts | 12 ++- src/hooks/useApproveCallback.ts | 7 +- src/hooks/useContract.ts | 14 +-- src/hooks/useSwapCallback.ts | 179 ++++++++++++++++++++++---------- src/utils/index.ts | 5 - 5 files changed, 146 insertions(+), 71 deletions(-) diff --git a/src/data/V1.ts b/src/data/V1.ts index 9394cddae10..50ebef5b997 100644 --- a/src/data/V1.ts +++ b/src/data/V1.ts @@ -3,10 +3,11 @@ 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' -export function useV1PairAddress(tokenAddress?: string): string | undefined { +export function useV1ExchangeAddress(tokenAddress?: string): string | undefined { const contract = useV1FactoryContract() const inputs = useMemo(() => [tokenAddress], [tokenAddress]) @@ -18,7 +19,7 @@ class MockV1Pair extends Pair {} function useMockV1Pair(token?: Token): MockV1Pair | undefined { const isWETH: boolean = token && WETH[token.chainId] ? token.equals(WETH[token.chainId]) : false - const v1PairAddress = useV1PairAddress(isWETH ? undefined : token?.address) + const v1PairAddress = useV1ExchangeAddress(isWETH ? undefined : token?.address) const tokenBalance = useTokenBalance(v1PairAddress, token) const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? ''] @@ -112,8 +113,11 @@ export function useV1Trade( return v1Trade } -export function isTradeV1(trade?: Trade): boolean | undefined { - return trade?.route?.pairs?.some(pair => pair instanceof MockV1Pair) +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 } const ZERO_PERCENT = new Percent('0') diff --git a/src/hooks/useApproveCallback.ts b/src/hooks/useApproveCallback.ts index 429e39ef8f9..aee24ec8f6b 100644 --- a/src/hooks/useApproveCallback.ts +++ b/src/hooks/useApproveCallback.ts @@ -4,13 +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 { isTradeV1, useV1PairAddress } from '../data/V1' +import { getTradeVersion, useV1ExchangeAddress } 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, @@ -92,7 +93,7 @@ export function useApproveCallbackFromTrade(trade?: Trade, allowedSlippage = 0) () => (trade ? computeSlippageAdjustedAmounts(trade, allowedSlippage)[Field.INPUT] : undefined), [trade, allowedSlippage] ) - const tradeIsV1 = isTradeV1(trade) - const v1Exchange = useV1PairAddress(trade && tradeIsV1 ? trade.inputAmount.token.address : undefined) + const tradeIsV1 = getTradeVersion(trade) === Version.v1 + const v1Exchange = useV1ExchangeAddress(trade && tradeIsV1 ? trade.inputAmount.token.address : undefined) return useApproveCallback(amountToApprove, v1Exchange ?? 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/useSwapCallback.ts b/src/hooks/useSwapCallback.ts index 130f843381a..720c9217a83 100644 --- a/src/hooks/useSwapCallback.ts +++ b/src/hooks/useSwapCallback.ts @@ -1,15 +1,18 @@ import { BigNumber } from '@ethersproject/bignumber' 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, useV1ExchangeAddress } 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 +20,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 } } } @@ -53,9 +68,19 @@ export function useSwapCallback( const addTransaction = useTransactionAdder() const recipient = to ? isAddress(to) : account const ensName = useENSName(to) + const tradeVersion = getTradeVersion(trade) + const isV1 = tradeVersion === Version.v1 + const v1ExchangeAddress = useV1ExchangeAddress( + isV1 + ? trade && chainId && WETH[chainId] && trade.inputAmount.token.equals(WETH[chainId]) + ? trade.outputAmount.token.address + : trade?.inputAmount?.token?.address + : undefined + ) + const v1Exchange = useV1ExchangeContract(v1ExchangeAddress, true) return useMemo(() => { - if (!trade || !recipient) return null + if (!trade || !recipient || !tradeVersion) return null // will always be defined const { @@ -78,17 +103,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 +170,61 @@ 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(), + 0, + 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(), + 0, + deadlineFromNow, + recipient, + trade.outputAmount.token.address + ] + break } 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 +273,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 +302,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/utils/index.ts b/src/utils/index.ts index 7fbe0496758..3ff9e996fed 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -92,11 +92,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 } From 454cac03213433d9b2a97311bb2bac83c87dcda1 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Thu, 11 Jun 2020 10:53:22 -0400 Subject: [PATCH 11/17] move some code around to reduce duplication --- src/data/V1.ts | 16 ++++++++++++++++ src/hooks/useApproveCallback.ts | 6 +++--- src/hooks/useSwapCallback.ts | 15 ++++----------- src/utils/index.ts | 1 - 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/data/V1.ts b/src/data/V1.ts index 50ebef5b997..e86603db43d 100644 --- a/src/data/V1.ts +++ b/src/data/V1.ts @@ -120,6 +120,22 @@ export function getTradeVersion(trade?: Trade): Version | undefined { return undefined } +// 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 diff --git a/src/hooks/useApproveCallback.ts b/src/hooks/useApproveCallback.ts index aee24ec8f6b..37150b05deb 100644 --- a/src/hooks/useApproveCallback.ts +++ b/src/hooks/useApproveCallback.ts @@ -4,7 +4,7 @@ import { Trade, WETH, TokenAmount } from '@uniswap/sdk' import { useCallback, useMemo } from 'react' import { ROUTER_ADDRESS } from '../constants' import { useTokenAllowance } from '../data/Allowances' -import { getTradeVersion, useV1ExchangeAddress } from '../data/V1' +import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1' import { Field } from '../state/swap/actions' import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks' import { computeSlippageAdjustedAmounts } from '../utils/prices' @@ -94,6 +94,6 @@ export function useApproveCallbackFromTrade(trade?: Trade, allowedSlippage = 0) [trade, allowedSlippage] ) const tradeIsV1 = getTradeVersion(trade) === Version.v1 - const v1Exchange = useV1ExchangeAddress(trade && tradeIsV1 ? trade.inputAmount.token.address : undefined) - return useApproveCallback(amountToApprove, v1Exchange ?? ROUTER_ADDRESS) + const v1ExchangeAddress = useV1TradeExchangeAddress(trade) + return useApproveCallback(amountToApprove, tradeIsV1 ? v1ExchangeAddress : ROUTER_ADDRESS) } diff --git a/src/hooks/useSwapCallback.ts b/src/hooks/useSwapCallback.ts index 720c9217a83..14c53887f62 100644 --- a/src/hooks/useSwapCallback.ts +++ b/src/hooks/useSwapCallback.ts @@ -1,10 +1,11 @@ import { BigNumber } from '@ethersproject/bignumber' +import { MaxUint256 } from '@ethersproject/constants' import { Contract } from '@ethersproject/contracts' 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, useV1ExchangeAddress } from '../data/V1' +import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1' import { Field } from '../state/swap/actions' import { useTransactionAdder } from '../state/transactions/hooks' import { calculateGasMargin, getRouterContract, isAddress } from '../utils' @@ -69,15 +70,7 @@ export function useSwapCallback( const recipient = to ? isAddress(to) : account const ensName = useENSName(to) const tradeVersion = getTradeVersion(trade) - const isV1 = tradeVersion === Version.v1 - const v1ExchangeAddress = useV1ExchangeAddress( - isV1 - ? trade && chainId && WETH[chainId] && trade.inputAmount.token.equals(WETH[chainId]) - ? trade.outputAmount.token.address - : trade?.inputAmount?.token?.address - : undefined - ) - const v1Exchange = useV1ExchangeContract(v1ExchangeAddress, true) + const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true) return useMemo(() => { if (!trade || !recipient || !tradeVersion) return null @@ -214,7 +207,7 @@ export function useSwapCallback( args = [ slippageAdjustedOutput.raw.toString(), slippageAdjustedInput.raw.toString(), - 0, + MaxUint256.toString(), deadlineFromNow, recipient, trade.outputAmount.token.address diff --git a/src/utils/index.ts b/src/utils/index.ts index 3ff9e996fed..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' From b7d7fec6d196b49613013fb312638102c4d2a6fd Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Thu, 11 Jun 2020 10:55:44 -0400 Subject: [PATCH 12/17] fix ts error --- src/hooks/useSwapCallback.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hooks/useSwapCallback.ts b/src/hooks/useSwapCallback.ts index 14c53887f62..89e64b84ee1 100644 --- a/src/hooks/useSwapCallback.ts +++ b/src/hooks/useSwapCallback.ts @@ -213,6 +213,8 @@ export function useSwapCallback( trade.outputAmount.token.address ] break + default: + throw new Error(`Unhandled swap type: ${swapType}`) } const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all( From 7815e8395c04efc242747df67dc9ce24a76e492e Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Thu, 11 Jun 2020 11:21:54 -0400 Subject: [PATCH 13/17] correct input allowance --- src/hooks/useSwapCallback.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/hooks/useSwapCallback.ts b/src/hooks/useSwapCallback.ts index 89e64b84ee1..f4db11c2845 100644 --- a/src/hooks/useSwapCallback.ts +++ b/src/hooks/useSwapCallback.ts @@ -65,12 +65,16 @@ 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 || !tradeVersion) return null From 05767fea3b7e1248c79d844108395b5379156782 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Thu, 11 Jun 2020 11:31:24 -0400 Subject: [PATCH 14/17] fix exact token to token on v1 --- src/components/TxnPopup/index.tsx | 19 +++++++++++-------- src/hooks/useSwapCallback.ts | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) 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 ? ( - - ) : ( - - )} + +
+ {success ? : } +
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)} diff --git a/src/hooks/useSwapCallback.ts b/src/hooks/useSwapCallback.ts index f4db11c2845..b68efccf04d 100644 --- a/src/hooks/useSwapCallback.ts +++ b/src/hooks/useSwapCallback.ts @@ -177,7 +177,7 @@ export function useSwapCallback( args = [ slippageAdjustedInput.raw.toString(), slippageAdjustedOutput.raw.toString(), - 0, + 1, deadlineFromNow, recipient, trade.outputAmount.token.address From a8581e9c033e9cdaaeccd13b1c8d211897739ffc Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Thu, 11 Jun 2020 11:44:14 -0400 Subject: [PATCH 15/17] fix pending approvals to be specific to the spender --- src/hooks/useApproveCallback.ts | 33 +++++++++++++---------- src/pages/MigrateV1/MigrateV1Exchange.tsx | 13 +++++---- src/state/transactions/actions.ts | 2 +- src/state/transactions/hooks.tsx | 27 ++++++++++++------- src/state/transactions/reducer.ts | 6 ++--- 5 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/hooks/useApproveCallback.ts b/src/hooks/useApproveCallback.ts index 37150b05deb..5ab1b141489 100644 --- a/src/hooks/useApproveCallback.ts +++ b/src/hooks/useApproveCallback.ts @@ -23,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 @@ -40,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 } @@ -61,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 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/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 From 64d8839a43c4ddbd9e7f391d60ae2c46295c315d Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Thu, 11 Jun 2020 11:54:48 -0400 Subject: [PATCH 16/17] google analytics for swap version --- src/pages/Send/index.tsx | 8 ++++++-- src/pages/Swap/index.tsx | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/pages/Send/index.tsx b/src/pages/Send/index.tsx index a01bda7f2b1..e73352e449e 100644 --- a/src/pages/Send/index.tsx +++ b/src/pages/Send/index.tsx @@ -28,7 +28,7 @@ import { MIN_ETH, BETTER_TRADE_LINK_THRESHOLD } from '../../constants' -import { isTradeBetter } from '../../data/V1' +import { getTradeVersion, isTradeBetter } from '../../data/V1' import { useActiveWeb3React } from '../../hooks' import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback' import { useSendCallback } from '../../hooks/useSendCallback' @@ -176,7 +176,11 @@ export default function Send() { 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 => { diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 77064d173f1..9f329f67b8b 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -26,7 +26,7 @@ import { MIN_ETH, BETTER_TRADE_LINK_THRESHOLD } from '../../constants' -import { isTradeBetter } from '../../data/V1' +import { getTradeVersion, isTradeBetter } from '../../data/V1' import { useActiveWeb3React } from '../../hooks' import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback' import { useSwapCallback } from '../../hooks/useSwapCallback' @@ -156,7 +156,11 @@ export default function Swap() { 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 => { From 53f5ea9ef7f5f9360fc22a967728572151edf170 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Thu, 11 Jun 2020 12:05:07 -0400 Subject: [PATCH 17/17] disable the version switch on pages other than swap and send --- src/components/Header/VersionSwitch.tsx | 34 +++++++++++++++++-------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/components/Header/VersionSwitch.tsx b/src/components/Header/VersionSwitch.tsx index de4f12c5efb..3a23449a8ac 100644 --- a/src/components/Header/VersionSwitch.tsx +++ b/src/components/Header/VersionSwitch.tsx @@ -1,5 +1,5 @@ import { stringify } from 'qs' -import React, { useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { Link, useLocation } from 'react-router-dom' import styled from 'styled-components' import useParsedQueryString from '../../hooks/useParsedQueryString' @@ -18,14 +18,15 @@ const VersionLabel = styled.span<{ enabled: boolean }>` color: ${({ theme, enabled }) => (enabled ? theme.white : theme.primary3)}; } ` -const VersionToggle = styled(Link)` +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; - cursor: pointer; text-decoration: none; :hover { text-decoration: none; @@ -36,17 +37,28 @@ export function VersionSwitch() { const version = useToggledVersion() const location = useLocation() const query = useParsedQueryString() + const versionSwitchAvailable = location.pathname === '/swap' || location.pathname === '/send' + const toggleDest = useMemo(() => { - return { - ...location, - search: `?${stringify({ ...query, use: version === Version.v1 ? undefined : Version.v1 })}` - } - }, [location, query, version]) + 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 + + V2 + V1 ) }