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
This commit is contained in:
Ian Lapham 2021-01-12 17:36:57 -05:00 committed by GitHub
parent 587b659816
commit a70aa41df2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 150 additions and 46 deletions

@ -9,7 +9,8 @@ import {
useDarkModeManager,
useExpertModeManager,
useUserTransactionTTL,
useUserSlippageTolerance
useUserSlippageTolerance,
useUserSingleHopOnly
} from '../../state/user/hooks'
import { TYPE } from '../../theme'
import { ButtonError } from '../Button'
@ -135,6 +136,8 @@ export default function SettingsTab() {
const [expertMode, toggleExpertMode] = useExpertModeManager()
const [singleHopOnly, setSingleHopOnly] = useUserSingleHopOnly()
const [darkMode, toggleDarkMode] = useDarkModeManager()
// show confirmation view before turning on
@ -230,6 +233,19 @@ export default function SettingsTab() {
}
/>
</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>
<RowFixed>
<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(
// 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 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
export const BLOCKED_ADDRESSES: string[] = [

@ -3,11 +3,9 @@ import {
BigintIsh,
Currency,
CurrencyAmount,
currencyEquals,
ETHER,
JSBI,
Pair,
Percent,
Route,
Token,
TokenAmount,
@ -157,31 +155,3 @@ export function useV1TradeExchangeAddress(trade: Trade | undefined): string | un
}, [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 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)
}
}

@ -1,12 +1,14 @@
import { isTradeBetter } from 'utils/trades'
import { Currency, CurrencyAmount, Pair, Token, Trade } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap'
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 { wrappedCurrency } from '../utils/wrappedCurrency'
import { useActiveWeb3React } from './index'
import { useUserSingleHopOnly } from 'state/user/hooks'
function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
const { chainId } = useActiveWeb3React()
@ -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
*/
export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?: Currency): Trade | null {
const allowedPairs = useAllCommonPairs(currencyAmountIn?.currency, currencyOut)
const [singleHopOnly] = useUserSingleHopOnly()
return useMemo(() => {
if (currencyAmountIn && currencyOut && allowedPairs.length > 0) {
if (singleHopOnly) {
return (
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
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
}, [allowedPairs, currencyAmountIn, currencyOut])
}, [allowedPairs, currencyAmountIn, currencyOut, singleHopOnly])
}
/**
@ -99,13 +122,28 @@ export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?:
export function useTradeExactOut(currencyIn?: Currency, currencyAmountOut?: CurrencyAmount): Trade | null {
const allowedPairs = useAllCommonPairs(currencyIn, currencyAmountOut?.currency)
const [singleHopOnly] = useUserSingleHopOnly()
return useMemo(() => {
if (currencyIn && currencyAmountOut && allowedPairs.length > 0) {
if (singleHopOnly) {
return (
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: 3, maxNumResults: 1 })[0] ??
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: 1, maxNumResults: 1 })[0] ??
null
)
}
return null
}, [allowedPairs, currencyIn, currencyAmountOut])
// 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
}, [currencyIn, currencyAmountOut, allowedPairs, singleHopOnly])
}

@ -21,7 +21,7 @@ import TokenWarningModal from '../../components/TokenWarningModal'
import ProgressSteps from '../../components/ProgressSteps'
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 { useCurrency } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
@ -37,13 +37,14 @@ import {
useSwapActionHandlers,
useSwapState
} 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 { maxAmountSpend } from '../../utils/maxAmountSpend'
import { computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody'
import { ClickableText } from '../Pool/styleds'
import Loader from '../../components/Loader'
import { isTradeBetter } from 'utils/trades'
export default function Swap() {
const loadedUrlParams = useDefaultsFromURLSearch()
@ -183,6 +184,8 @@ export default function Swap() {
const { priceImpactWithoutFee } = computeTradePriceBreakdown(trade)
const [singleHopOnly] = useUserSingleHopOnly()
const handleSwap = useCallback(() => {
if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) {
return
@ -209,6 +212,11 @@ export default function Swap() {
getTradeVersion(trade)
].join('/')
})
ReactGA.event({
category: 'Routing',
action: singleHopOnly ? 'Swap with multihop disabled' : 'Swap with multihop enabled'
})
})
.catch(error => {
setSwapState({
@ -219,7 +227,17 @@ export default function Swap() {
txHash: undefined
})
})
}, [tradeToConfirm, account, priceImpactWithoutFee, recipient, recipientAddress, showConfirm, swapCallback, trade])
}, [
priceImpactWithoutFee,
swapCallback,
tradeToConfirm,
showConfirm,
recipient,
recipientAddress,
account,
trade,
singleHopOnly
])
// errors
const [showInverted, setShowInverted] = useState<boolean>(false)
@ -384,6 +402,7 @@ export default function Swap() {
) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
{singleHopOnly && <TYPE.main mb="4px">Try enabling multi-hop trades.</TYPE.main>}
</GreyCard>
) : showApproveFlow ? (
<RowBetween>

@ -16,6 +16,7 @@ export interface SerializedPair {
export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>('user/updateMatchesDarkMode')
export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode')
export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode')
export const updateUserSingleHopOnly = createAction<{ userSingleHopOnly: boolean }>('user/updateUserSingleHopOnly')
export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number }>(
'user/updateUserSlippageTolerance'
)

@ -1,5 +1,6 @@
import { ChainId, Pair, Token } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap'
import ReactGA from 'react-ga'
import { useCallback, useMemo } from 'react'
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
import { BASES_TO_TRACK_LIQUIDITY_FOR, PINNED_PAIRS } from '../../constants'
@ -17,7 +18,8 @@ import {
updateUserDeadline,
updateUserExpertMode,
updateUserSlippageTolerance,
toggleURLWarning
toggleURLWarning,
updateUserSingleHopOnly
} from './actions'
function serializeToken(token: Token): SerializedToken {
@ -81,6 +83,27 @@ export function useExpertModeManager(): [boolean, () => void] {
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] {
const dispatch = useDispatch<AppDispatch>()
const userSlippageTolerance = useSelector<AppState, AppState['user']['userSlippageTolerance']>(state => {

@ -13,7 +13,8 @@ import {
updateUserExpertMode,
updateUserSlippageTolerance,
updateUserDeadline,
toggleURLWarning
toggleURLWarning,
updateUserSingleHopOnly
} from './actions'
const currentTimestamp = () => new Date().getTime()
@ -27,6 +28,8 @@ export interface UserState {
userExpertMode: boolean
userSingleHopOnly: boolean // only allow swaps on direct pairs
// user defined slippage tolerance in bips, used in all txns
userSlippageTolerance: number
@ -58,6 +61,7 @@ export const initialState: UserState = {
userDarkMode: null,
matchesDarkMode: false,
userExpertMode: false,
userSingleHopOnly: false,
userSlippageTolerance: INITIAL_ALLOWED_SLIPPAGE,
userDeadline: DEFAULT_DEADLINE_FROM_NOW,
tokens: {},
@ -103,6 +107,9 @@ export default createReducer(initialState, builder =>
state.userDeadline = action.payload.userDeadline
state.timestamp = currentTimestamp()
})
.addCase(updateUserSingleHopOnly, (state, action) => {
state.userSingleHopOnly = action.payload.userSingleHopOnly
})
.addCase(addSerializedToken, (state, { payload: { serializedToken } }) => {
state.tokens[serializedToken.chainId] = state.tokens[serializedToken.chainId] || {}
state.tokens[serializedToken.chainId][serializedToken.address] = serializedToken

29
src/utils/trades.ts Normal file

@ -0,0 +1,29 @@
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)
}
}