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:
parent
587b659816
commit
a70aa41df2
@ -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
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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user