Commit a70aa41d authored by Ian Lapham's avatar Ian Lapham Committed by GitHub

Routing updates only (#1265)

* start routing fix for multi hops

* switch to input amount comparison on exactOut

* make percent logic more clear

* remove uneeded comaprisons

* move logic to functions for testing

* add multi hop disable switch

* add GA

* fix bug to return multihop no single

* update swap details

* code clean

* routing only
parent 587b6598
...@@ -9,7 +9,8 @@ import { ...@@ -9,7 +9,8 @@ import {
useDarkModeManager, useDarkModeManager,
useExpertModeManager, useExpertModeManager,
useUserTransactionTTL, useUserTransactionTTL,
useUserSlippageTolerance useUserSlippageTolerance,
useUserSingleHopOnly
} from '../../state/user/hooks' } from '../../state/user/hooks'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { ButtonError } from '../Button' import { ButtonError } from '../Button'
...@@ -135,6 +136,8 @@ export default function SettingsTab() { ...@@ -135,6 +136,8 @@ export default function SettingsTab() {
const [expertMode, toggleExpertMode] = useExpertModeManager() const [expertMode, toggleExpertMode] = useExpertModeManager()
const [singleHopOnly, setSingleHopOnly] = useUserSingleHopOnly()
const [darkMode, toggleDarkMode] = useDarkModeManager() const [darkMode, toggleDarkMode] = useDarkModeManager()
// show confirmation view before turning on // show confirmation view before turning on
...@@ -230,6 +233,19 @@ export default function SettingsTab() { ...@@ -230,6 +233,19 @@ export default function SettingsTab() {
} }
/> />
</RowBetween> </RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
Disable Multihops
</TYPE.black>
<QuestionHelper text="Restricts swaps to direct pairs only." />
</RowFixed>
<Toggle
id="toggle-disable-multihop-button"
isActive={singleHopOnly}
toggle={() => (singleHopOnly ? setSingleHopOnly(false) : setSingleHopOnly(true))}
/>
</RowBetween>
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}> <TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
......
...@@ -201,6 +201,7 @@ export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt( ...@@ -201,6 +201,7 @@ export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(
// used to ensure the user doesn't send so much ETH so they end up with <.01 // 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 MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
export const BETTER_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))
export const BETTER_TRADE_LESS_HOPS_THRESHOLD = new Percent(JSBI.BigInt(50), JSBI.BigInt(10000))
// SDN OFAC addresses // SDN OFAC addresses
export const BLOCKED_ADDRESSES: string[] = [ export const BLOCKED_ADDRESSES: string[] = [
......
...@@ -3,11 +3,9 @@ import { ...@@ -3,11 +3,9 @@ import {
BigintIsh, BigintIsh,
Currency, Currency,
CurrencyAmount, CurrencyAmount,
currencyEquals,
ETHER, ETHER,
JSBI, JSBI,
Pair, Pair,
Percent,
Route, Route,
Token, Token,
TokenAmount, TokenAmount,
...@@ -157,31 +155,3 @@ export function useV1TradeExchangeAddress(trade: Trade | undefined): string | un ...@@ -157,31 +155,3 @@ export function useV1TradeExchangeAddress(trade: Trade | undefined): string | un
}, [trade]) }, [trade])
return useV1ExchangeAddress(tokenAddress) 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 percentage amount
export function isTradeBetter(
tradeA: Trade | undefined,
tradeB: Trade | undefined,
minimumDelta: Percent = ZERO_PERCENT
): boolean | undefined {
if (tradeA && !tradeB) return false
if (tradeB && !tradeA) return true
if (!tradeA || !tradeB) return undefined
if (
tradeA.tradeType !== tradeB.tradeType ||
!currencyEquals(tradeA.inputAmount.currency, tradeB.inputAmount.currency) ||
!currencyEquals(tradeB.outputAmount.currency, tradeB.outputAmount.currency)
) {
throw new Error('Trades are not comparable')
}
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)
}
}
import { isTradeBetter } from 'utils/trades'
import { Currency, CurrencyAmount, Pair, Token, Trade } from '@uniswap/sdk' import { Currency, CurrencyAmount, Pair, Token, Trade } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap' import flatMap from 'lodash.flatmap'
import { useMemo } from 'react' import { useMemo } from 'react'
import { BASES_TO_CHECK_TRADES_AGAINST, CUSTOM_BASES } from '../constants' import { BASES_TO_CHECK_TRADES_AGAINST, CUSTOM_BASES, BETTER_TRADE_LESS_HOPS_THRESHOLD } from '../constants'
import { PairState, usePairs } from '../data/Reserves' import { PairState, usePairs } from '../data/Reserves'
import { wrappedCurrency } from '../utils/wrappedCurrency' import { wrappedCurrency } from '../utils/wrappedCurrency'
import { useActiveWeb3React } from './index' import { useActiveWeb3React } from './index'
import { useUserSingleHopOnly } from 'state/user/hooks'
function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] { function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
...@@ -78,19 +80,40 @@ function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] { ...@@ -78,19 +80,40 @@ function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
) )
} }
const MAX_HOPS = 3
/** /**
* Returns the best trade for the exact amount of tokens in to the given token out * Returns the best trade for the exact amount of tokens in to the given token out
*/ */
export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?: Currency): Trade | null { export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?: Currency): Trade | null {
const allowedPairs = useAllCommonPairs(currencyAmountIn?.currency, currencyOut) const allowedPairs = useAllCommonPairs(currencyAmountIn?.currency, currencyOut)
const [singleHopOnly] = useUserSingleHopOnly()
return useMemo(() => { return useMemo(() => {
if (currencyAmountIn && currencyOut && allowedPairs.length > 0) { if (currencyAmountIn && currencyOut && allowedPairs.length > 0) {
return ( if (singleHopOnly) {
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null return (
) Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: 1, maxNumResults: 1 })[0] ??
null
)
}
// search through trades with varying hops, find best trade out of them
let bestTradeSoFar: Trade | null = null
for (let i = 1; i <= MAX_HOPS; i++) {
const currentTrade: Trade | null =
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: i, maxNumResults: 1 })[0] ??
null
// if current trade is best yet, save it
if (isTradeBetter(bestTradeSoFar, currentTrade, BETTER_TRADE_LESS_HOPS_THRESHOLD)) {
bestTradeSoFar = currentTrade
}
}
return bestTradeSoFar
} }
return null return null
}, [allowedPairs, currencyAmountIn, currencyOut]) }, [allowedPairs, currencyAmountIn, currencyOut, singleHopOnly])
} }
/** /**
...@@ -99,13 +122,28 @@ export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?: ...@@ -99,13 +122,28 @@ export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?:
export function useTradeExactOut(currencyIn?: Currency, currencyAmountOut?: CurrencyAmount): Trade | null { export function useTradeExactOut(currencyIn?: Currency, currencyAmountOut?: CurrencyAmount): Trade | null {
const allowedPairs = useAllCommonPairs(currencyIn, currencyAmountOut?.currency) const allowedPairs = useAllCommonPairs(currencyIn, currencyAmountOut?.currency)
const [singleHopOnly] = useUserSingleHopOnly()
return useMemo(() => { return useMemo(() => {
if (currencyIn && currencyAmountOut && allowedPairs.length > 0) { if (currencyIn && currencyAmountOut && allowedPairs.length > 0) {
return ( if (singleHopOnly) {
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: 3, maxNumResults: 1 })[0] ?? return (
null Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: 1, maxNumResults: 1 })[0] ??
) null
)
}
// search through trades with varying hops, find best trade out of them
let bestTradeSoFar: Trade | null = null
for (let i = 1; i <= MAX_HOPS; i++) {
const currentTrade =
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: i, maxNumResults: 1 })[0] ??
null
if (isTradeBetter(bestTradeSoFar, currentTrade, BETTER_TRADE_LESS_HOPS_THRESHOLD)) {
bestTradeSoFar = currentTrade
}
}
return bestTradeSoFar
} }
return null return null
}, [allowedPairs, currencyIn, currencyAmountOut]) }, [currencyIn, currencyAmountOut, allowedPairs, singleHopOnly])
} }
...@@ -21,7 +21,7 @@ import TokenWarningModal from '../../components/TokenWarningModal' ...@@ -21,7 +21,7 @@ import TokenWarningModal from '../../components/TokenWarningModal'
import ProgressSteps from '../../components/ProgressSteps' import ProgressSteps from '../../components/ProgressSteps'
import { BETTER_TRADE_LINK_THRESHOLD, INITIAL_ALLOWED_SLIPPAGE } from '../../constants' import { BETTER_TRADE_LINK_THRESHOLD, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { getTradeVersion, isTradeBetter } from '../../data/V1' import { getTradeVersion } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useCurrency } from '../../hooks/Tokens' import { useCurrency } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback' import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
...@@ -37,13 +37,14 @@ import { ...@@ -37,13 +37,14 @@ import {
useSwapActionHandlers, useSwapActionHandlers,
useSwapState useSwapState
} from '../../state/swap/hooks' } from '../../state/swap/hooks'
import { useExpertModeManager, useUserSlippageTolerance } from '../../state/user/hooks' import { useExpertModeManager, useUserSlippageTolerance, useUserSingleHopOnly } from '../../state/user/hooks'
import { LinkStyledButton, TYPE } from '../../theme' import { LinkStyledButton, TYPE } from '../../theme'
import { maxAmountSpend } from '../../utils/maxAmountSpend' import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { computeTradePriceBreakdown, warningSeverity } from '../../utils/prices' import { computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody' import AppBody from '../AppBody'
import { ClickableText } from '../Pool/styleds' import { ClickableText } from '../Pool/styleds'
import Loader from '../../components/Loader' import Loader from '../../components/Loader'
import { isTradeBetter } from 'utils/trades'
export default function Swap() { export default function Swap() {
const loadedUrlParams = useDefaultsFromURLSearch() const loadedUrlParams = useDefaultsFromURLSearch()
...@@ -183,6 +184,8 @@ export default function Swap() { ...@@ -183,6 +184,8 @@ export default function Swap() {
const { priceImpactWithoutFee } = computeTradePriceBreakdown(trade) const { priceImpactWithoutFee } = computeTradePriceBreakdown(trade)
const [singleHopOnly] = useUserSingleHopOnly()
const handleSwap = useCallback(() => { const handleSwap = useCallback(() => {
if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) { if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) {
return return
...@@ -209,6 +212,11 @@ export default function Swap() { ...@@ -209,6 +212,11 @@ export default function Swap() {
getTradeVersion(trade) getTradeVersion(trade)
].join('/') ].join('/')
}) })
ReactGA.event({
category: 'Routing',
action: singleHopOnly ? 'Swap with multihop disabled' : 'Swap with multihop enabled'
})
}) })
.catch(error => { .catch(error => {
setSwapState({ setSwapState({
...@@ -219,7 +227,17 @@ export default function Swap() { ...@@ -219,7 +227,17 @@ export default function Swap() {
txHash: undefined txHash: undefined
}) })
}) })
}, [tradeToConfirm, account, priceImpactWithoutFee, recipient, recipientAddress, showConfirm, swapCallback, trade]) }, [
priceImpactWithoutFee,
swapCallback,
tradeToConfirm,
showConfirm,
recipient,
recipientAddress,
account,
trade,
singleHopOnly
])
// errors // errors
const [showInverted, setShowInverted] = useState<boolean>(false) const [showInverted, setShowInverted] = useState<boolean>(false)
...@@ -384,6 +402,7 @@ export default function Swap() { ...@@ -384,6 +402,7 @@ export default function Swap() {
) : noRoute && userHasSpecifiedInputOutput ? ( ) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}> <GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main> <TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
{singleHopOnly && <TYPE.main mb="4px">Try enabling multi-hop trades.</TYPE.main>}
</GreyCard> </GreyCard>
) : showApproveFlow ? ( ) : showApproveFlow ? (
<RowBetween> <RowBetween>
......
...@@ -16,6 +16,7 @@ export interface SerializedPair { ...@@ -16,6 +16,7 @@ export interface SerializedPair {
export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>('user/updateMatchesDarkMode') export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>('user/updateMatchesDarkMode')
export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode') export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode')
export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode') export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode')
export const updateUserSingleHopOnly = createAction<{ userSingleHopOnly: boolean }>('user/updateUserSingleHopOnly')
export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number }>( export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number }>(
'user/updateUserSlippageTolerance' 'user/updateUserSlippageTolerance'
) )
......
import { ChainId, Pair, Token } from '@uniswap/sdk' import { ChainId, Pair, Token } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap' import flatMap from 'lodash.flatmap'
import ReactGA from 'react-ga'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { shallowEqual, useDispatch, useSelector } from 'react-redux' import { shallowEqual, useDispatch, useSelector } from 'react-redux'
import { BASES_TO_TRACK_LIQUIDITY_FOR, PINNED_PAIRS } from '../../constants' import { BASES_TO_TRACK_LIQUIDITY_FOR, PINNED_PAIRS } from '../../constants'
...@@ -17,7 +18,8 @@ import { ...@@ -17,7 +18,8 @@ import {
updateUserDeadline, updateUserDeadline,
updateUserExpertMode, updateUserExpertMode,
updateUserSlippageTolerance, updateUserSlippageTolerance,
toggleURLWarning toggleURLWarning,
updateUserSingleHopOnly
} from './actions' } from './actions'
function serializeToken(token: Token): SerializedToken { function serializeToken(token: Token): SerializedToken {
...@@ -81,6 +83,27 @@ export function useExpertModeManager(): [boolean, () => void] { ...@@ -81,6 +83,27 @@ export function useExpertModeManager(): [boolean, () => void] {
return [expertMode, toggleSetExpertMode] return [expertMode, toggleSetExpertMode]
} }
export function useUserSingleHopOnly(): [boolean, (newSingleHopOnly: boolean) => void] {
const dispatch = useDispatch<AppDispatch>()
const singleHopOnly = useSelector<AppState, AppState['user']['userSingleHopOnly']>(
state => state.user.userSingleHopOnly
)
const setSingleHopOnly = useCallback(
(newSingleHopOnly: boolean) => {
ReactGA.event({
category: 'Routing',
action: newSingleHopOnly ? 'enable single hop' : 'disable single hop'
})
dispatch(updateUserSingleHopOnly({ userSingleHopOnly: newSingleHopOnly }))
},
[dispatch]
)
return [singleHopOnly, setSingleHopOnly]
}
export function useUserSlippageTolerance(): [number, (slippage: number) => void] { export function useUserSlippageTolerance(): [number, (slippage: number) => void] {
const dispatch = useDispatch<AppDispatch>() const dispatch = useDispatch<AppDispatch>()
const userSlippageTolerance = useSelector<AppState, AppState['user']['userSlippageTolerance']>(state => { const userSlippageTolerance = useSelector<AppState, AppState['user']['userSlippageTolerance']>(state => {
......
...@@ -13,7 +13,8 @@ import { ...@@ -13,7 +13,8 @@ import {
updateUserExpertMode, updateUserExpertMode,
updateUserSlippageTolerance, updateUserSlippageTolerance,
updateUserDeadline, updateUserDeadline,
toggleURLWarning toggleURLWarning,
updateUserSingleHopOnly
} from './actions' } from './actions'
const currentTimestamp = () => new Date().getTime() const currentTimestamp = () => new Date().getTime()
...@@ -27,6 +28,8 @@ export interface UserState { ...@@ -27,6 +28,8 @@ export interface UserState {
userExpertMode: boolean userExpertMode: boolean
userSingleHopOnly: boolean // only allow swaps on direct pairs
// user defined slippage tolerance in bips, used in all txns // user defined slippage tolerance in bips, used in all txns
userSlippageTolerance: number userSlippageTolerance: number
...@@ -58,6 +61,7 @@ export const initialState: UserState = { ...@@ -58,6 +61,7 @@ export const initialState: UserState = {
userDarkMode: null, userDarkMode: null,
matchesDarkMode: false, matchesDarkMode: false,
userExpertMode: false, userExpertMode: false,
userSingleHopOnly: false,
userSlippageTolerance: INITIAL_ALLOWED_SLIPPAGE, userSlippageTolerance: INITIAL_ALLOWED_SLIPPAGE,
userDeadline: DEFAULT_DEADLINE_FROM_NOW, userDeadline: DEFAULT_DEADLINE_FROM_NOW,
tokens: {}, tokens: {},
...@@ -103,6 +107,9 @@ export default createReducer(initialState, builder => ...@@ -103,6 +107,9 @@ export default createReducer(initialState, builder =>
state.userDeadline = action.payload.userDeadline state.userDeadline = action.payload.userDeadline
state.timestamp = currentTimestamp() state.timestamp = currentTimestamp()
}) })
.addCase(updateUserSingleHopOnly, (state, action) => {
state.userSingleHopOnly = action.payload.userSingleHopOnly
})
.addCase(addSerializedToken, (state, { payload: { serializedToken } }) => { .addCase(addSerializedToken, (state, { payload: { serializedToken } }) => {
state.tokens[serializedToken.chainId] = state.tokens[serializedToken.chainId] || {} state.tokens[serializedToken.chainId] = state.tokens[serializedToken.chainId] || {}
state.tokens[serializedToken.chainId][serializedToken.address] = serializedToken state.tokens[serializedToken.chainId][serializedToken.address] = serializedToken
......
import { Trade, currencyEquals, Percent } from '@uniswap/sdk'
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 percentage amount
export function isTradeBetter(
tradeA: Trade | undefined | null,
tradeB: Trade | undefined | null,
minimumDelta: Percent = ZERO_PERCENT
): boolean | undefined {
if (tradeA && !tradeB) return false
if (tradeB && !tradeA) return true
if (!tradeA || !tradeB) return undefined
if (
tradeA.tradeType !== tradeB.tradeType ||
!currencyEquals(tradeA.inputAmount.currency, tradeB.inputAmount.currency) ||
!currencyEquals(tradeB.outputAmount.currency, tradeB.outputAmount.currency)
) {
throw new Error('Trades are not comparable')
}
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)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment