diff --git a/src/components/Settings/index.tsx b/src/components/Settings/index.tsx
index a58054eae0..75615c9c48 100644
--- a/src/components/Settings/index.tsx
+++ b/src/components/Settings/index.tsx
@@ -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() {
}
/>
+
+
+
+ Disable Multihops
+
+
+
+ (singleHopOnly ? setSingleHopOnly(false) : setSingleHopOnly(true))}
+ />
+
diff --git a/src/constants/index.ts b/src/constants/index.ts
index 70d304cbb7..3496527231 100644
--- a/src/constants/index.ts
+++ b/src/constants/index.ts
@@ -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[] = [
diff --git a/src/data/V1.ts b/src/data/V1.ts
index 25120543ab..7829c7c0fa 100644
--- a/src/data/V1.ts
+++ b/src/data/V1.ts
@@ -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)
- }
-}
diff --git a/src/hooks/Trades.ts b/src/hooks/Trades.ts
index 0b2c55c00a..867ddadcd9 100644
--- a/src/hooks/Trades.ts
+++ b/src/hooks/Trades.ts
@@ -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) {
- return (
- Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
- )
+ if (singleHopOnly) {
+ 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
- }, [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) {
- return (
- Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: 3, maxNumResults: 1 })[0] ??
- null
- )
+ if (singleHopOnly) {
+ return (
+ 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
- }, [allowedPairs, currencyIn, currencyAmountOut])
+ }, [currencyIn, currencyAmountOut, allowedPairs, singleHopOnly])
}
diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx
index 696f1cc4cb..707ddea6e6 100644
--- a/src/pages/Swap/index.tsx
+++ b/src/pages/Swap/index.tsx
@@ -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(false)
@@ -384,6 +402,7 @@ export default function Swap() {
) : noRoute && userHasSpecifiedInputOutput ? (
Insufficient liquidity for this trade.
+ {singleHopOnly && Try enabling multi-hop trades.}
) : showApproveFlow ? (
diff --git a/src/state/user/actions.ts b/src/state/user/actions.ts
index 8433f43b07..f6701c3b94 100644
--- a/src/state/user/actions.ts
+++ b/src/state/user/actions.ts
@@ -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'
)
diff --git a/src/state/user/hooks.tsx b/src/state/user/hooks.tsx
index 5c4bd61878..178cfbd665 100644
--- a/src/state/user/hooks.tsx
+++ b/src/state/user/hooks.tsx
@@ -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()
+
+ const singleHopOnly = useSelector(
+ 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()
const userSlippageTolerance = useSelector(state => {
diff --git a/src/state/user/reducer.ts b/src/state/user/reducer.ts
index 8dd046a6af..e1a8cf11b9 100644
--- a/src/state/user/reducer.ts
+++ b/src/state/user/reducer.ts
@@ -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
diff --git a/src/utils/trades.ts b/src/utils/trades.ts
new file mode 100644
index 0000000000..154d7294ac
--- /dev/null
+++ b/src/utils/trades.ts
@@ -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)
+ }
+}