Uniswap API
diff --git a/src/components/swap/__snapshots__/SwapDetailsDropdown.test.tsx.snap b/src/components/swap/__snapshots__/SwapDetailsDropdown.test.tsx.snap
index 0b004ff4e8..6c1ee5f925 100644
--- a/src/components/swap/__snapshots__/SwapDetailsDropdown.test.tsx.snap
+++ b/src/components/swap/__snapshots__/SwapDetailsDropdown.test.tsx.snap
@@ -42,11 +42,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
color: #0D111C;
}
-.c15 {
+.c12 {
color: #7780A0;
}
-.c13 {
+.c16 {
width: 100%;
height: 1px;
background-color: #D2D9EE;
@@ -66,7 +66,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
justify-content: flex-start;
}
-.c12 {
+.c15 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@@ -89,11 +89,20 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
transition: opacity 0.2s ease-in-out;
}
-.c14 {
+.c10 {
display: inline-block;
height: inherit;
}
+.c11 {
+ margin-right: 4px;
+ height: 18px;
+}
+
+.c11 > * {
+ stroke: #98A1C0;
+}
+
.c8 {
background-color: transparent;
border: none;
@@ -135,7 +144,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
cursor: pointer;
}
-.c10 {
+.c13 {
-webkit-transform: none;
-ms-transform: none;
transform: none;
@@ -144,7 +153,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
transition: transform 0.1s linear;
}
-.c11 {
+.c14 {
padding-top: 12px;
}
@@ -184,8 +193,32 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
-
+
+
+
+
+ gas-icon.svg
+
+
+ $1.00
+
+
+
+
+
+
+
@@ -223,11 +276,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
class="c2 c3 c6"
>
Minimum output
@@ -247,11 +300,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
class="c2 c3 c6"
>
Expected output
@@ -265,18 +318,18 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
Order routing
+
+
Minimum output
@@ -454,14 +474,14 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
class="c5 c6 c7"
>
Expected output
@@ -481,12 +501,12 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
class="c5 c6 c7"
>
Order routing
Output is estimated. You will receive at least
@@ -520,7 +540,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
style="padding: 12px 0px 0px 0px;"
>
Output will be sent to
| undefined
-): Percent {
+export default function useAutoSlippageTolerance(trade?: InterfaceTrade): Percent {
const { chainId } = useWeb3React()
const onL2 = chainId && L2_CHAIN_IDS.includes(chainId)
const outputDollarValue = useStablecoinValue(trade?.outputAmount)
const nativeGasPrice = useGasPrice()
const gasEstimate = guesstimateGas(trade)
+ const gasEstimateUSD = useStablecoinAmountFromFiatValue(trade?.gasUseEstimateUSD) ?? null
const nativeCurrency = useNativeCurrency(chainId)
const nativeCurrencyPrice = useStablecoinPrice((trade && nativeCurrency) ?? undefined)
@@ -100,9 +99,7 @@ export default function useAutoSlippageTolerance(
// NOTE - dont use gas estimate for L2s yet - need to verify accuracy
// if not, use local heuristic
const dollarCostToUse =
- chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && trade?.gasUseEstimateUSD
- ? trade.gasUseEstimateUSD
- : dollarGasCost
+ chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && gasEstimateUSD ? gasEstimateUSD : dollarGasCost
if (outputDollarValue && dollarCostToUse) {
// optimize for highest possible slippage without getting MEV'd
@@ -121,5 +118,15 @@ export default function useAutoSlippageTolerance(
}
return DEFAULT_AUTO_SLIPPAGE
- }, [trade, onL2, nativeGasPrice, gasEstimate, nativeCurrency, nativeCurrencyPrice, chainId, outputDollarValue])
+ }, [
+ trade,
+ onL2,
+ nativeGasPrice,
+ gasEstimate,
+ nativeCurrency,
+ nativeCurrencyPrice,
+ chainId,
+ gasEstimateUSD,
+ outputDollarValue,
+ ])
}
diff --git a/src/hooks/useBestTrade.test.ts b/src/hooks/useBestTrade.test.ts
index d642f804db..241daff080 100644
--- a/src/hooks/useBestTrade.test.ts
+++ b/src/hooks/useBestTrade.test.ts
@@ -83,15 +83,6 @@ describe('#useBestV3Trade ExactIn', () => {
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
-
- it('does not compute client side v3 trade if routing api is SYNCING', () => {
- expectRouterMock(TradeState.SYNCING)
-
- const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
-
- expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
- expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
- })
})
describe('when routing api is in error state', () => {
@@ -167,15 +158,6 @@ describe('#useBestV3Trade ExactOut', () => {
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
-
- it('does not compute client side v3 trade if routing api is SYNCING', () => {
- expectRouterMock(TradeState.SYNCING)
-
- const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
-
- expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
- expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
- })
})
describe('when routing api is in error state', () => {
diff --git a/src/hooks/useBestTrade.ts b/src/hooks/useBestTrade.ts
index a1db4074d3..533f11a9ba 100644
--- a/src/hooks/useBestTrade.ts
+++ b/src/hooks/useBestTrade.ts
@@ -23,7 +23,7 @@ export function useBestTrade(
otherCurrency?: Currency
): {
state: TradeState
- trade: InterfaceTrade | undefined
+ trade?: InterfaceTrade
} {
const { chainId } = useWeb3React()
const autoRouterSupported = useAutoRouterSupported()
diff --git a/src/hooks/useClientSideV3Trade.ts b/src/hooks/useClientSideV3Trade.ts
index 02b2cd1928..6283eb3d59 100644
--- a/src/hooks/useClientSideV3Trade.ts
+++ b/src/hooks/useClientSideV3Trade.ts
@@ -5,7 +5,7 @@ import { SupportedChainId } from 'constants/chains'
import JSBI from 'jsbi'
import { useSingleContractWithCallData } from 'lib/hooks/multicall'
import { useMemo } from 'react'
-import { InterfaceTrade, TradeState } from 'state/routing/types'
+import { ClassicTrade, InterfaceTrade, TradeState } from 'state/routing/types'
import { isCelo } from '../constants/tokens'
import { useAllV3Routes } from './useAllV3Routes'
@@ -33,7 +33,7 @@ export function useClientSideV3Trade(
tradeType: TTradeType,
amountSpecified?: CurrencyAmount,
otherCurrency?: Currency
-): { state: TradeState; trade: InterfaceTrade | undefined } {
+): { state: TradeState; trade: InterfaceTrade | undefined } {
const [currencyIn, currencyOut] =
tradeType === TradeType.EXACT_INPUT
? [amountSpecified?.currency, otherCurrency]
@@ -135,7 +135,7 @@ export function useClientSideV3Trade(
return {
state: TradeState.VALID,
- trade: new InterfaceTrade({
+ trade: new ClassicTrade({
v2Routes: [],
v3Routes: [
{
diff --git a/src/hooks/useUSDPrice.ts b/src/hooks/useUSDPrice.ts
index 37db91e5c9..56782ba2f8 100644
--- a/src/hooks/useUSDPrice.ts
+++ b/src/hooks/useUSDPrice.ts
@@ -41,8 +41,8 @@ function useETHValue(currencyAmount?: CurrencyAmount): {
}
}
- if (!trade || !currencyAmount?.currency || !isGqlSupportedChain(chainId)) {
- return { data: undefined, isLoading: state === TradeState.LOADING || state === TradeState.SYNCING }
+ if (!trade || state === TradeState.LOADING || !currencyAmount?.currency || !isGqlSupportedChain(chainId)) {
+ return { data: undefined, isLoading: state === TradeState.LOADING }
}
const { numerator, denominator } = trade.routes[0].midPrice
diff --git a/src/lib/hooks/routing/clientSideSmartOrderRouter.ts b/src/lib/hooks/routing/clientSideSmartOrderRouter.ts
index 44ec7cb305..82fe4022e8 100644
--- a/src/lib/hooks/routing/clientSideSmartOrderRouter.ts
+++ b/src/lib/hooks/routing/clientSideSmartOrderRouter.ts
@@ -3,8 +3,10 @@ import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
// eslint-disable-next-line no-restricted-imports
import { AlphaRouter, AlphaRouterConfig, ChainId } from '@uniswap/smart-order-router'
import { SupportedChainId } from 'constants/chains'
+import { nativeOnChain } from 'constants/tokens'
import JSBI from 'jsbi'
-import { GetQuoteResult } from 'state/routing/types'
+import { GetQuoteArgs } from 'state/routing/slice'
+import { QuoteResult, QuoteState, SwapRouterNativeAssets } from 'state/routing/types'
import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult'
export function toSupportedChainId(chainId: ChainId): SupportedChainId | undefined {
@@ -19,50 +21,41 @@ export function isSupportedChainId(chainId: ChainId | undefined): boolean {
async function getQuote(
{
- type,
+ tradeType,
tokenIn,
tokenOut,
amount: amountRaw,
}: {
- type: 'exactIn' | 'exactOut'
+ tradeType: TradeType
tokenIn: { address: string; chainId: number; decimals: number; symbol?: string }
tokenOut: { address: string; chainId: number; decimals: number; symbol?: string }
amount: BigintIsh
},
router: AlphaRouter,
- config: Partial
-): Promise<{ data: GetQuoteResult; error?: unknown }> {
- const currencyIn = new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals, tokenIn.symbol)
- const currencyOut = new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals, tokenOut.symbol)
+ routerConfig: Partial
+): Promise {
+ const tokenInIsNative = Object.values(SwapRouterNativeAssets).includes(tokenIn.address as SwapRouterNativeAssets)
+ const tokenOutIsNative = Object.values(SwapRouterNativeAssets).includes(tokenOut.address as SwapRouterNativeAssets)
+
+ const currencyIn = tokenInIsNative
+ ? nativeOnChain(tokenIn.chainId)
+ : new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals, tokenIn.symbol)
+ const currencyOut = tokenOutIsNative
+ ? nativeOnChain(tokenOut.chainId)
+ : new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals, tokenOut.symbol)
+
+ const baseCurrency = tradeType === TradeType.EXACT_INPUT ? currencyIn : currencyOut
+ const quoteCurrency = tradeType === TradeType.EXACT_INPUT ? currencyOut : currencyIn
- const baseCurrency = type === 'exactIn' ? currencyIn : currencyOut
- const quoteCurrency = type === 'exactIn' ? currencyOut : currencyIn
const amount = CurrencyAmount.fromRawAmount(baseCurrency, JSBI.BigInt(amountRaw))
+ // TODO (WEB-2055): explore initializing client side routing on first load (when amountRaw is null) if there are enough users using client-side router preference.
+ const swapRoute = await router.route(amount, quoteCurrency, tradeType, /*swapConfig=*/ undefined, routerConfig)
- const swapRoute = await router.route(
- amount,
- quoteCurrency,
- type === 'exactIn' ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
- /*swapConfig=*/ undefined,
- config
- )
+ if (!swapRoute) {
+ return { state: QuoteState.NOT_FOUND }
+ }
- if (!swapRoute) throw new Error('Failed to generate client side quote')
-
- return { data: transformSwapRouteToGetQuoteResult(type, amount, swapRoute) }
-}
-
-interface QuoteArguments {
- tokenInAddress: string
- tokenInChainId: ChainId
- tokenInDecimals: number
- tokenInSymbol?: string
- tokenOutAddress: string
- tokenOutChainId: ChainId
- tokenOutDecimals: number
- tokenOutSymbol?: string
- amount: string
- type: 'exactIn' | 'exactOut'
+ return transformSwapRouteToGetQuoteResult(tradeType, amount, swapRoute)
}
export async function getClientSideQuote(
@@ -76,14 +69,14 @@ export async function getClientSideQuote(
tokenOutDecimals,
tokenOutSymbol,
amount,
- type,
- }: QuoteArguments,
+ tradeType,
+ }: GetQuoteArgs,
router: AlphaRouter,
config: Partial
) {
return getQuote(
{
- type,
+ tradeType,
tokenIn: {
address: tokenInAddress,
chainId: tokenInChainId,
diff --git a/src/lib/hooks/routing/useRoutingAPIArguments.ts b/src/lib/hooks/routing/useRoutingAPIArguments.ts
index 72d455bbd9..e97504a2a8 100644
--- a/src/lib/hooks/routing/useRoutingAPIArguments.ts
+++ b/src/lib/hooks/routing/useRoutingAPIArguments.ts
@@ -1,6 +1,7 @@
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice'
+import { currencyAddressForSwapQuote } from 'state/routing/utils'
/**
* Returns query arguments for the Routing API query or undefined if the
@@ -26,16 +27,16 @@ export function useRoutingAPIArguments({
? undefined
: {
amount: amount.quotient.toString(),
- tokenInAddress: tokenIn.wrapped.address,
+ tokenInAddress: currencyAddressForSwapQuote(tokenIn),
tokenInChainId: tokenIn.wrapped.chainId,
tokenInDecimals: tokenIn.wrapped.decimals,
tokenInSymbol: tokenIn.wrapped.symbol,
- tokenOutAddress: tokenOut.wrapped.address,
+ tokenOutAddress: currencyAddressForSwapQuote(tokenOut),
tokenOutChainId: tokenOut.wrapped.chainId,
tokenOutDecimals: tokenOut.wrapped.decimals,
tokenOutSymbol: tokenOut.wrapped.symbol,
routerPreference,
- type: (tradeType === TradeType.EXACT_INPUT ? 'exactIn' : 'exactOut') as 'exactIn' | 'exactOut',
+ tradeType,
},
[amount, routerPreference, tokenIn, tokenOut, tradeType]
)
diff --git a/src/lib/utils/analytics.ts b/src/lib/utils/analytics.ts
index f27a6cb76c..a835fbfdce 100644
--- a/src/lib/utils/analytics.ts
+++ b/src/lib/utils/analytics.ts
@@ -39,7 +39,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({
fiatValues,
txHash,
}: {
- trade: InterfaceTrade | Trade
+ trade: InterfaceTrade | Trade
fiatValues: { amountIn: number | undefined; amountOut: number | undefined }
txHash: string
}) => ({
@@ -61,7 +61,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({
export const formatSwapQuoteReceivedEventProperties = (
trade: Trade,
- gasUseEstimateUSD?: CurrencyAmount,
+ gasUseEstimateUSD?: string,
fetchingSwapQuoteStartTime?: Date
) => {
return {
@@ -70,7 +70,7 @@ export const formatSwapQuoteReceivedEventProperties = (
token_in_address: getTokenAddress(trade.inputAmount.currency),
token_out_address: getTokenAddress(trade.outputAmount.currency),
price_impact_basis_points: trade ? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)) : undefined,
- estimated_network_fee_usd: gasUseEstimateUSD ? formatToDecimal(gasUseEstimateUSD, 2) : undefined,
+ estimated_network_fee_usd: gasUseEstimateUSD,
chain_id:
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
? trade.inputAmount.currency.chainId
diff --git a/src/nft/components/bag/BagFooter.tsx b/src/nft/components/bag/BagFooter.tsx
index 2b24105251..1a51800afc 100644
--- a/src/nft/components/bag/BagFooter.tsx
+++ b/src/nft/components/bag/BagFooter.tsx
@@ -3,7 +3,7 @@ import { formatEther, parseEther } from '@ethersproject/units'
import { t, Trans } from '@lingui/macro'
import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, NFTEventName } from '@uniswap/analytics-events'
-import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
+import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { useToggleAccountDrawer } from 'components/AccountDrawer'
import Column from 'components/Column'
@@ -208,7 +208,7 @@ const InputCurrencyValue = ({
totalEthPrice: BigNumber
activeCurrency: Currency | undefined | null
tradeState: TradeState
- trade: InterfaceTrade | undefined
+ trade: InterfaceTrade | undefined
}) => {
if (!usingPayWithAnyToken) {
return (
@@ -219,7 +219,7 @@ const InputCurrencyValue = ({
)
}
- if (tradeState === TradeState.LOADING) {
+ if (tradeState === TradeState.LOADING && !trade) {
return (
Fetching price...
@@ -228,7 +228,7 @@ const InputCurrencyValue = ({
}
return (
-
+
{ethNumberStandardFormatter(trade?.inputAmount.toExact())}
)
diff --git a/src/nft/hooks/useDerivedPayWithAnyTokenSwapInfo.ts b/src/nft/hooks/useDerivedPayWithAnyTokenSwapInfo.ts
index 9bf8413bff..09473ecd89 100644
--- a/src/nft/hooks/useDerivedPayWithAnyTokenSwapInfo.ts
+++ b/src/nft/hooks/useDerivedPayWithAnyTokenSwapInfo.ts
@@ -9,7 +9,7 @@ export default function useDerivedPayWithAnyTokenSwapInfo(
parsedOutputAmount?: CurrencyAmount
): {
state: TradeState
- trade: InterfaceTrade | undefined
+ trade: InterfaceTrade | undefined
maximumAmountIn: CurrencyAmount | undefined
allowedSlippage: Percent
} {
diff --git a/src/nft/hooks/usePayWithAnyTokenSwap.ts b/src/nft/hooks/usePayWithAnyTokenSwap.ts
index e70bdeb7f8..7fa46b08ae 100644
--- a/src/nft/hooks/usePayWithAnyTokenSwap.ts
+++ b/src/nft/hooks/usePayWithAnyTokenSwap.ts
@@ -1,4 +1,4 @@
-import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
+import { Percent } from '@uniswap/sdk-core'
import { PermitInput, TokenTradeRoutesInput, TokenTradeType } from 'graphql/data/__generated__/types-and-hooks'
import { Allowance } from 'hooks/usePermit2Allowance'
import { buildAllTradeRouteInputs } from 'nft/utils/tokenRoutes'
@@ -8,7 +8,7 @@ import { InterfaceTrade } from 'state/routing/types'
import { useTokenInput } from './useTokenInput'
export default function usePayWithAnyTokenSwap(
- trade?: InterfaceTrade | undefined,
+ trade?: InterfaceTrade | undefined,
allowance?: Allowance,
allowedSlippage?: Percent
) {
diff --git a/src/nft/hooks/usePriceImpact.ts b/src/nft/hooks/usePriceImpact.ts
index 328756f4d4..9422e1c625 100644
--- a/src/nft/hooks/usePriceImpact.ts
+++ b/src/nft/hooks/usePriceImpact.ts
@@ -1,4 +1,4 @@
-import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
+import { Percent } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { useTheme } from 'styled-components/macro'
@@ -14,7 +14,7 @@ interface PriceImpactSeverity {
color: string
}
-export function usePriceImpact(trade?: InterfaceTrade): PriceImpact | undefined {
+export function usePriceImpact(trade?: InterfaceTrade): PriceImpact | undefined {
const theme = useTheme()
return useMemo(() => {
diff --git a/src/nft/utils/tokenRoutes.ts b/src/nft/utils/tokenRoutes.ts
index 42f1ccbee8..60a0f4da21 100644
--- a/src/nft/utils/tokenRoutes.ts
+++ b/src/nft/utils/tokenRoutes.ts
@@ -1,5 +1,5 @@
import { IRoute, Protocol } from '@uniswap/router-sdk'
-import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
+import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { Pool } from '@uniswap/v3-sdk'
import { TokenAmountInput, TokenTradeRouteInput, TradePoolInput } from 'graphql/data/__generated__/types-and-hooks'
@@ -108,7 +108,7 @@ function buildTradeRouteInput(swap: Swap): TokenTradeRouteInput {
}
}
-export function buildAllTradeRouteInputs(trade: InterfaceTrade): {
+export function buildAllTradeRouteInputs(trade: InterfaceTrade): {
mixedTokenTradeRouteInputs: TokenTradeRouteInput[] | undefined
v2TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined
v3TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined
diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx
index 2064c6f590..b45b794416 100644
--- a/src/pages/Swap/index.tsx
+++ b/src/pages/Swap/index.tsx
@@ -8,8 +8,7 @@ import {
InterfaceSectionName,
SwapEventName,
} from '@uniswap/analytics-events'
-import { Trade } from '@uniswap/router-sdk'
-import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
+import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { useWeb3React } from '@web3-react/core'
import { useToggleAccountDrawer } from 'components/AccountDrawer'
@@ -115,11 +114,11 @@ const OutputSwapSection = styled(SwapSection)`
`
function getIsValidSwapQuote(
- trade: InterfaceTrade | undefined,
+ trade: InterfaceTrade | undefined,
tradeState: TradeState,
swapInputError?: ReactNode
): boolean {
- return !!swapInputError && !!trade && (tradeState === TradeState.VALID || tradeState === TradeState.SYNCING)
+ return Boolean(swapInputError && trade && tradeState === TradeState.VALID)
}
function largerPercentValue(a?: Percent, b?: Percent) {
@@ -293,7 +292,7 @@ export function Swap({
const fiatValueOutput = useUSDPrice(parsedAmounts[Field.OUTPUT])
const [routeNotFound, routeIsLoading, routeIsSyncing] = useMemo(
- () => [!trade?.swaps, TradeState.LOADING === tradeState, TradeState.SYNCING === tradeState],
+ () => [!trade?.swaps, TradeState.LOADING === tradeState, TradeState.LOADING === tradeState && Boolean(trade)],
[trade, tradeState]
)
@@ -336,7 +335,7 @@ export function Swap({
// modal and loading
const [{ showConfirm, tradeToConfirm, swapErrorMessage, attemptingTxn, txHash }, setSwapState] = useState<{
showConfirm: boolean
- tradeToConfirm: Trade | undefined
+ tradeToConfirm: InterfaceTrade | undefined
attemptingTxn: boolean
swapErrorMessage: string | undefined
txHash: string | undefined
diff --git a/src/state/index.ts b/src/state/index.ts
index 7da06c5e47..d36c815f02 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -14,7 +14,15 @@ const store = configureStore({
reducer,
enhancers: (defaultEnhancers) => defaultEnhancers.concat(sentryEnhancer),
middleware: (getDefaultMiddleware) =>
- getDefaultMiddleware({ thunk: true })
+ getDefaultMiddleware({
+ thunk: true,
+ serializableCheck: {
+ // meta.arg and meta.baseQueryMeta are defaults. payload.trade is a nonserializable return value, but that's ok
+ // because we are not adding it into any persisted store that requires serialization (e.g. localStorage)
+ ignoredActionPaths: ['meta.arg', 'meta.baseQueryMeta', 'payload.trade'],
+ ignoredPaths: [routingApi.reducerPath],
+ },
+ })
.concat(routingApi.middleware)
.concat(save({ states: PERSISTED_KEYS, debounce: 1000 })),
preloadedState: load({ states: PERSISTED_KEYS, disableWarnings: isTestEnv() }),
diff --git a/src/state/routing/slice.ts b/src/state/routing/slice.ts
index 0ee3087a08..4a3d4b11e2 100644
--- a/src/state/routing/slice.ts
+++ b/src/state/routing/slice.ts
@@ -1,5 +1,6 @@
import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
import { Protocol } from '@uniswap/router-sdk'
+import { TradeType } from '@uniswap/sdk-core'
import { AlphaRouter, ChainId } from '@uniswap/smart-order-router'
import { RPC_PROVIDERS } from 'constants/providers'
import { getClientSideQuote, toSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter'
@@ -7,7 +8,8 @@ import ms from 'ms.macro'
import qs from 'qs'
import { trace } from 'tracing/trace'
-import { GetQuoteResult } from './types'
+import { QuoteData, TradeResult } from './types'
+import { isExactInput, transformRoutesToTrade } from './utils'
export enum RouterPreference {
AUTO = 'auto',
@@ -69,7 +71,7 @@ const PRICE_PARAMS = {
distributionPercent: 100,
}
-interface GetQuoteArgs {
+export interface GetQuoteArgs {
tokenInAddress: string
tokenInChainId: ChainId
tokenInDecimals: number
@@ -80,7 +82,12 @@ interface GetQuoteArgs {
tokenOutSymbol?: string
amount: string
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
- type: 'exactIn' | 'exactOut'
+ tradeType: TradeType
+}
+
+enum QuoteState {
+ SUCCESS = 'Success',
+ NOT_FOUND = 'Not found',
}
export const routingApi = createApi({
@@ -89,7 +96,7 @@ export const routingApi = createApi({
baseUrl: 'https://api.uniswap.org/v1/',
}),
endpoints: (build) => ({
- getQuote: build.query({
+ getQuote: build.query({
async onQueryStarted(args: GetQuoteArgs, { queryFulfilled }) {
trace(
'quote',
@@ -119,11 +126,14 @@ export const routingApi = createApi({
)
},
async queryFn(args, _api, _extraOptions, fetch) {
- const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, routerPreference, type } =
- args
-
- try {
- if (routerPreference === RouterPreference.API || routerPreference === RouterPreference.AUTO) {
+ if (
+ args.routerPreference === RouterPreference.API ||
+ args.routerPreference === RouterPreference.AUTO ||
+ args.routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE
+ ) {
+ try {
+ const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, tradeType } = args
+ const type = isExactInput(tradeType) ? 'exactIn' : 'exactOut'
const query = qs.stringify({
...API_QUERY_PARAMS,
tokenInAddress,
@@ -133,21 +143,40 @@ export const routingApi = createApi({
amount,
type,
})
- return (await fetch(`quote?${query}`)) as { data: GetQuoteResult } | { error: FetchBaseQueryError }
- } else {
- const router = getRouter(args.tokenInChainId)
- return await getClientSideQuote(
- args,
- router,
- // TODO(zzmp): Use PRICE_PARAMS for RouterPreference.PRICE.
- // This change is intentionally being deferred to first see what effect router caching has.
- CLIENT_PARAMS
+ const response = await fetch(`quote?${query}`)
+ if (response.error) {
+ try {
+ // cast as any here because we do a runtime check on it being an object before indexing into .errorCode
+ const errorData = response.error.data as any
+ // NO_ROUTE should be treated as a valid response to prevent retries.
+ if (typeof errorData === 'object' && errorData?.errorCode === 'NO_ROUTE') {
+ return { data: { state: QuoteState.NOT_FOUND } }
+ }
+ } catch {
+ throw response.error
+ }
+ }
+
+ const quoteData = response.data as QuoteData
+ const tradeResult = transformRoutesToTrade(args, quoteData)
+ return { data: tradeResult }
+ } catch (error: any) {
+ console.warn(
+ `GetQuote failed on routing API, falling back to client: ${error?.message ?? error?.detail ?? error}`
)
}
- } catch (error) {
- // TODO: fall back to client-side quoter when auto router fails.
- // deprecate 'legacy' v2/v3 routers first.
- return { error: { status: 'CUSTOM_ERROR', error: error.toString() } }
+ }
+ try {
+ const router = getRouter(args.tokenInChainId)
+ const quoteResult = await getClientSideQuote(args, router, CLIENT_PARAMS)
+ if (quoteResult.state === QuoteState.SUCCESS) {
+ return { data: transformRoutesToTrade(args, quoteResult.data) }
+ } else {
+ return { data: quoteResult }
+ }
+ } catch (error: any) {
+ console.warn(`GetQuote failed on client: ${error}`)
+ return { error: { status: 'CUSTOM_ERROR', error: error?.detail ?? error?.message ?? error } }
}
},
keepUnusedDataFor: ms`10s`,
diff --git a/src/state/routing/types.ts b/src/state/routing/types.ts
index 35da496089..dd78fb8cad 100644
--- a/src/state/routing/types.ts
+++ b/src/state/routing/types.ts
@@ -8,7 +8,6 @@ export enum TradeState {
INVALID,
NO_ROUTE_FOUND,
VALID,
- SYNCING,
}
// from https://github.com/Uniswap/routing-api/blob/main/lib/handlers/schema.ts
@@ -49,7 +48,7 @@ export type V2PoolInRoute = {
address?: string
}
-export interface GetQuoteResult {
+export interface QuoteData {
quoteId?: string
blockNumber: string
amount: string
@@ -68,12 +67,12 @@ export interface GetQuoteResult {
routeString: string
}
-export class InterfaceTrade<
+export class ClassicTrade<
TInput extends Currency,
TOutput extends Currency,
TTradeType extends TradeType
> extends Trade {
- gasUseEstimateUSD: CurrencyAmount | null | undefined
+ gasUseEstimateUSD: string | null | undefined
blockNumber: string | null | undefined
constructor({
@@ -81,8 +80,8 @@ export class InterfaceTrade<
blockNumber,
...routes
}: {
- gasUseEstimateUSD?: CurrencyAmount | undefined | null
- blockNumber?: string | null | undefined
+ gasUseEstimateUSD?: string | null
+ blockNumber?: string | null
v2Routes: {
routev2: V2Route
inputAmount: CurrencyAmount
@@ -105,3 +104,42 @@ export class InterfaceTrade<
this.gasUseEstimateUSD = gasUseEstimateUSD
}
}
+
+export type InterfaceTrade = ClassicTrade
+
+export enum QuoteState {
+ SUCCESS = 'Success',
+ NOT_FOUND = 'Not found',
+}
+
+export type QuoteResult =
+ | {
+ state: QuoteState.NOT_FOUND
+ data?: undefined
+ }
+ | {
+ state: QuoteState.SUCCESS
+ data: QuoteData
+ }
+
+export type TradeResult =
+ | {
+ state: QuoteState.NOT_FOUND
+ trade?: undefined
+ }
+ | {
+ state: QuoteState.SUCCESS
+ trade: InterfaceTrade
+ }
+
+export enum PoolType {
+ V2Pool = 'v2-pool',
+ V3Pool = 'v3-pool',
+}
+
+// swap router API special cases these strings to represent native currencies
+// all chains have "ETH" as native currency symbol except for polygon
+export enum SwapRouterNativeAssets {
+ MATIC = 'MATIC',
+ ETH = 'ETH',
+}
diff --git a/src/state/routing/useRoutingAPITrade.ts b/src/state/routing/useRoutingAPITrade.ts
index 4a8e1af492..07ce9dad29 100644
--- a/src/state/routing/useRoutingAPITrade.ts
+++ b/src/state/routing/useRoutingAPITrade.ts
@@ -3,14 +3,16 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router'
import { sendTiming } from 'components/analytics'
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
-import { useStablecoinAmountFromFiatValue } from 'hooks/useStablecoinPrice'
import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments'
import ms from 'ms.macro'
import { useMemo } from 'react'
import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference, useGetQuoteQuery } from 'state/routing/slice'
-import { InterfaceTrade, TradeState } from './types'
-import { computeRoutes, transformRoutesToTrade } from './utils'
+import { InterfaceTrade, QuoteState, TradeState } from './types'
+
+const TRADE_INVALID = { state: TradeState.INVALID, trade: undefined } as const
+const TRADE_NOT_FOUND = { state: TradeState.NO_ROUTE_FOUND, trade: undefined } as const
+const TRADE_LOADING = { state: TradeState.LOADING, trade: undefined } as const
/**
* Returns the best trade by invoking the routing api or the smart order router on the client
@@ -25,7 +27,7 @@ export function useRoutingAPITrade(
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
): {
state: TradeState
- trade: InterfaceTrade | undefined
+ trade?: InterfaceTrade
} {
const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo(
() =>
@@ -44,10 +46,9 @@ export function useRoutingAPITrade(
})
const {
- isLoading,
isError,
- data: quoteResult,
- currentData,
+ data: tradeResult,
+ currentData: currentTradeResult,
} = useGetQuoteQuery(queryArgs ?? skipToken, {
// Price-fetching is informational and costly, so it's done less frequently.
pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms`1m` : AVERAGE_L1_BLOCK_TIME,
@@ -55,72 +56,23 @@ export function useRoutingAPITrade(
refetchOnMountOrArgChange: 2 * 60,
})
- const route = useMemo(
- () => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
- [currencyIn, currencyOut, quoteResult, tradeType]
- )
-
- // get USD gas cost of trade in active chains stablecoin amount
- const gasUseEstimateUSD = useStablecoinAmountFromFiatValue(quoteResult?.gasUseEstimateUSD) ?? null
-
- const isSyncing = currentData !== quoteResult
+ const isCurrent = currentTradeResult === tradeResult
return useMemo(() => {
- if (!currencyIn || !currencyOut || currencyIn.equals(currencyOut)) {
+ if (!amountSpecified || isError || !queryArgs) {
+ return TRADE_INVALID
+ } else if (tradeResult?.state === QuoteState.NOT_FOUND && isCurrent) {
+ return TRADE_NOT_FOUND
+ } else if (!tradeResult?.trade) {
+ // TODO(WEB-3307): use `isLoading` returned by rtk-query hook instead of checking for `trade` status
+ return TRADE_LOADING
+ } else {
return {
- state: TradeState.INVALID,
- trade: undefined,
+ state: isCurrent ? TradeState.VALID : TradeState.LOADING,
+ trade: tradeResult.trade,
}
}
-
- if (isLoading && !quoteResult) {
- // only on first hook render
- return {
- state: TradeState.LOADING,
- trade: undefined,
- }
- }
-
- let otherAmount = undefined
- if (quoteResult) {
- if (tradeType === TradeType.EXACT_INPUT && currencyOut) {
- otherAmount = CurrencyAmount.fromRawAmount(currencyOut, quoteResult.quote)
- }
-
- if (tradeType === TradeType.EXACT_OUTPUT && currencyIn) {
- otherAmount = CurrencyAmount.fromRawAmount(currencyIn, quoteResult.quote)
- }
- }
-
- if (isError || !otherAmount || !route || route.length === 0 || !queryArgs) {
- return {
- state: TradeState.NO_ROUTE_FOUND,
- trade: undefined,
- }
- }
-
- try {
- const trade = transformRoutesToTrade(route, tradeType, quoteResult?.blockNumber, gasUseEstimateUSD)
- return {
- // always return VALID regardless of isFetching status
- state: isSyncing ? TradeState.SYNCING : TradeState.VALID,
- trade,
- }
- } catch (e) {
- return { state: TradeState.INVALID, trade: undefined }
- }
- }, [
- currencyIn,
- currencyOut,
- quoteResult,
- isLoading,
- tradeType,
- isError,
- route,
- queryArgs,
- gasUseEstimateUSD,
- isSyncing,
- ])
+ }, [amountSpecified, isCurrent, isError, queryArgs, tradeResult])
}
// only want to enable this when app hook called
diff --git a/src/state/routing/utils.test.ts b/src/state/routing/utils.test.ts
index 2a7d7dd071..c24bf2d542 100644
--- a/src/state/routing/utils.test.ts
+++ b/src/state/routing/utils.test.ts
@@ -1,51 +1,42 @@
-import { Token, TradeType } from '@uniswap/sdk-core'
+import { Token } from '@uniswap/sdk-core'
+import { SupportedChainId } from 'constants/chains'
+import { nativeOnChain } from 'constants/tokens'
-import { nativeOnChain } from '../../constants/tokens'
+import { PoolType } from './types'
import { computeRoutes } from './utils'
const USDC = new Token(1, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC')
const DAI = new Token(1, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 6, 'DAI')
const MKR = new Token(1, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 6, 'MKR')
-const ETH = nativeOnChain(1)
+const ETH = nativeOnChain(SupportedChainId.MAINNET)
// helper function to make amounts more readable
const amount = (raw: TemplateStringsArray) => (parseInt(raw[0]) * 1e6).toString()
describe('#useRoute', () => {
- it('handles an undefined payload', () => {
- const result = computeRoutes(undefined, undefined, TradeType.EXACT_INPUT, undefined)
-
- expect(result).toBeUndefined()
- })
-
it('handles empty edges and nodes', () => {
- const result = computeRoutes(USDC, DAI, TradeType.EXACT_INPUT, {
- route: [],
- })
-
+ const result = computeRoutes(false, false, [])
expect(result).toEqual([])
})
it('handles a single route trade from DAI to USDC from v3', () => {
- const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, {
- route: [
- [
- {
- type: 'v3-pool',
- address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
- amountIn: amount`1`,
- amountOut: amount`5`,
- fee: '500',
- sqrtRatioX96: '2437312313659959819381354528',
- liquidity: '10272714736694327408',
- tickCurrent: '-69633',
- tokenIn: DAI,
- tokenOut: USDC,
- },
- ],
+ const result = computeRoutes(false, false, [
+ [
+ {
+ type: 'v3-pool',
+ address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
+ amountIn: amount`1`,
+ amountOut: amount`5`,
+ fee: '500',
+ sqrtRatioX96: '2437312313659959819381354528',
+ liquidity: '10272714736694327408',
+ tickCurrent: '-69633',
+ tokenIn: DAI,
+ tokenOut: USDC,
+ },
],
- })
+ ])
const r = result?.[0]
@@ -60,28 +51,26 @@ describe('#useRoute', () => {
})
it('handles a single route trade from DAI to USDC from v2', () => {
- const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, {
- route: [
- [
- {
- type: 'v2-pool',
- address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
- amountIn: amount`1`,
- amountOut: amount`5`,
- tokenIn: DAI,
- tokenOut: USDC,
- reserve0: {
- token: DAI,
- quotient: amount`100`,
- },
- reserve1: {
- token: USDC,
- quotient: amount`200`,
- },
+ const result = computeRoutes(false, false, [
+ [
+ {
+ type: 'v2-pool',
+ address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
+ amountIn: amount`1`,
+ amountOut: amount`5`,
+ tokenIn: DAI,
+ tokenOut: USDC,
+ reserve0: {
+ token: DAI,
+ quotient: amount`100`,
},
- ],
+ reserve1: {
+ token: USDC,
+ quotient: amount`200`,
+ },
+ },
],
- })
+ ])
const r = result?.[0]
@@ -96,54 +85,52 @@ describe('#useRoute', () => {
})
it('handles a multi-route trade from DAI to USDC', () => {
- const result = computeRoutes(DAI, USDC, TradeType.EXACT_OUTPUT, {
- route: [
- [
- {
- type: 'v2-pool',
- address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
- amountIn: amount`5`,
- amountOut: amount`6`,
- tokenIn: DAI,
- tokenOut: USDC,
- reserve0: {
- token: DAI,
- quotient: amount`1000`,
- },
- reserve1: {
- token: USDC,
- quotient: amount`500`,
- },
+ const result = computeRoutes(false, false, [
+ [
+ {
+ type: 'v2-pool',
+ address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
+ amountIn: amount`5`,
+ amountOut: amount`6`,
+ tokenIn: DAI,
+ tokenOut: USDC,
+ reserve0: {
+ token: DAI,
+ quotient: amount`1000`,
},
- ],
- [
- {
- type: 'v3-pool',
- address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2',
- amountIn: amount`10`,
- amountOut: amount`1`,
- fee: '3000',
- tokenIn: DAI,
- tokenOut: MKR,
- sqrtRatioX96: '2437312313659959819381354528',
- liquidity: '10272714736694327408',
- tickCurrent: '-69633',
+ reserve1: {
+ token: USDC,
+ quotient: amount`500`,
},
- {
- type: 'v3-pool',
- address: '0x3f8F72aA9304c8B593d555F12eF6589cC3A579A2',
- amountIn: amount`1`,
- amountOut: amount`200`,
- fee: '10000',
- tokenIn: MKR,
- tokenOut: USDC,
- sqrtRatioX96: '2437312313659959819381354528',
- liquidity: '10272714736694327408',
- tickCurrent: '-69633',
- },
- ],
+ },
],
- })
+ [
+ {
+ type: 'v3-pool',
+ address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2',
+ amountIn: amount`10`,
+ amountOut: amount`1`,
+ fee: '3000',
+ tokenIn: DAI,
+ tokenOut: MKR,
+ sqrtRatioX96: '2437312313659959819381354528',
+ liquidity: '10272714736694327408',
+ tickCurrent: '-69633',
+ },
+ {
+ type: 'v3-pool',
+ address: '0x3f8F72aA9304c8B593d555F12eF6589cC3A579A2',
+ amountIn: amount`1`,
+ amountOut: amount`200`,
+ fee: '10000',
+ tokenIn: MKR,
+ tokenOut: USDC,
+ sqrtRatioX96: '2437312313659959819381354528',
+ liquidity: '10272714736694327408',
+ tickCurrent: '-69633',
+ },
+ ],
+ ])
expect(result).toBeDefined()
expect(result?.length).toBe(2)
@@ -165,38 +152,36 @@ describe('#useRoute', () => {
})
it('handles a single route trade with same token pair, different fee tiers', () => {
- const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, {
- route: [
- [
- {
- type: 'v3-pool',
- address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
- amountIn: amount`1`,
- amountOut: amount`5`,
- fee: '500',
- tokenIn: DAI,
- tokenOut: USDC,
- sqrtRatioX96: '2437312313659959819381354528',
- liquidity: '10272714736694327408',
- tickCurrent: '-69633',
- },
- ],
- [
- {
- type: 'v3-pool',
- address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2',
- amountIn: amount`10`,
- amountOut: amount`50`,
- fee: '3000',
- tokenIn: DAI,
- tokenOut: USDC,
- sqrtRatioX96: '2437312313659959819381354528',
- liquidity: '10272714736694327408',
- tickCurrent: '-69633',
- },
- ],
+ const result = computeRoutes(false, false, [
+ [
+ {
+ type: 'v3-pool',
+ address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
+ amountIn: amount`1`,
+ amountOut: amount`5`,
+ fee: '500',
+ tokenIn: DAI,
+ tokenOut: USDC,
+ sqrtRatioX96: '2437312313659959819381354528',
+ liquidity: '10272714736694327408',
+ tickCurrent: '-69633',
+ },
],
- })
+ [
+ {
+ type: 'v3-pool',
+ address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2',
+ amountIn: amount`10`,
+ amountOut: amount`50`,
+ fee: '3000',
+ tokenIn: DAI,
+ tokenOut: USDC,
+ sqrtRatioX96: '2437312313659959819381354528',
+ liquidity: '10272714736694327408',
+ tickCurrent: '-69633',
+ },
+ ],
+ ])
expect(result).toBeDefined()
expect(result?.length).toBe(2)
@@ -206,28 +191,68 @@ describe('#useRoute', () => {
expect(result?.[0].inputAmount.toSignificant()).toBe('1')
})
+ it('computes mixed routes correctly', () => {
+ const result = computeRoutes(false, false, [
+ [
+ {
+ type: PoolType.V3Pool,
+ address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
+ amountIn: amount`1`,
+ amountOut: amount`5`,
+ fee: '500',
+ tokenIn: DAI,
+ tokenOut: USDC,
+ sqrtRatioX96: '2437312313659959819381354528',
+ liquidity: '10272714736694327408',
+ tickCurrent: '-69633',
+ },
+ {
+ type: PoolType.V2Pool,
+ address: 'x2f8F72aA9304c8B593d555F12eF6589cC3A579A2',
+ amountIn: amount`10`,
+ amountOut: amount`50`,
+ tokenIn: USDC,
+ tokenOut: MKR,
+ reserve0: {
+ token: USDC,
+ quotient: amount`100`,
+ },
+ reserve1: {
+ token: MKR,
+ quotient: amount`200`,
+ },
+ },
+ ],
+ ])
+
+ expect(result).toBeDefined()
+ expect(result?.length).toBe(1)
+ expect(result?.[0].routev3).toBeNull()
+ expect(result?.[0].routev2).toBeNull()
+ expect(result?.[0].mixedRoute?.output).toStrictEqual(MKR)
+ expect(result?.[0].inputAmount.toSignificant()).toBe('1')
+ })
+
describe('with ETH', () => {
it('outputs native ETH as input currency', () => {
const WETH = ETH.wrapped
- const result = computeRoutes(ETH, USDC, TradeType.EXACT_OUTPUT, {
- route: [
- [
- {
- type: 'v3-pool',
- address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
- amountIn: (1e18).toString(),
- amountOut: amount`5`,
- fee: '500',
- sqrtRatioX96: '2437312313659959819381354528',
- liquidity: '10272714736694327408',
- tickCurrent: '-69633',
- tokenIn: WETH,
- tokenOut: USDC,
- },
- ],
+ const result = computeRoutes(true, false, [
+ [
+ {
+ type: 'v3-pool',
+ address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
+ amountIn: (1e18).toString(),
+ amountOut: amount`5`,
+ fee: '500',
+ sqrtRatioX96: '2437312313659959819381354528',
+ liquidity: '10272714736694327408',
+ tickCurrent: '-69633',
+ tokenIn: WETH,
+ tokenOut: USDC,
+ },
],
- })
+ ])
expect(result).toBeDefined()
expect(result?.length).toBe(1)
@@ -239,24 +264,22 @@ describe('#useRoute', () => {
it('outputs native ETH as output currency', () => {
const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH')
- const result = computeRoutes(USDC, ETH, TradeType.EXACT_OUTPUT, {
- route: [
- [
- {
- type: 'v3-pool',
- address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
- amountIn: amount`5`,
- amountOut: (1e18).toString(),
- fee: '500',
- sqrtRatioX96: '2437312313659959819381354528',
- liquidity: '10272714736694327408',
- tickCurrent: '-69633',
- tokenIn: USDC,
- tokenOut: WETH,
- },
- ],
+ const result = computeRoutes(false, true, [
+ [
+ {
+ type: 'v3-pool',
+ address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
+ amountIn: amount`5`,
+ amountOut: (1e18).toString(),
+ fee: '500',
+ sqrtRatioX96: '2437312313659959819381354528',
+ liquidity: '10272714736694327408',
+ tickCurrent: '-69633',
+ tokenIn: USDC,
+ tokenOut: WETH,
+ },
],
- })
+ ])
expect(result?.length).toBe(1)
expect(result?.[0].routev3?.input).toStrictEqual(USDC)
@@ -268,28 +291,26 @@ describe('#useRoute', () => {
it('outputs native ETH as input currency for v2 routes', () => {
const WETH = ETH.wrapped
- const result = computeRoutes(ETH, USDC, TradeType.EXACT_OUTPUT, {
- route: [
- [
- {
- type: 'v2-pool',
- address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
- amountIn: (1e18).toString(),
- amountOut: amount`5`,
- tokenIn: WETH,
- tokenOut: USDC,
- reserve0: {
- token: WETH,
- quotient: amount`100`,
- },
- reserve1: {
- token: USDC,
- quotient: amount`200`,
- },
+ const result = computeRoutes(true, false, [
+ [
+ {
+ type: 'v2-pool',
+ address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
+ amountIn: (1e18).toString(),
+ amountOut: amount`5`,
+ tokenIn: WETH,
+ tokenOut: USDC,
+ reserve0: {
+ token: WETH,
+ quotient: amount`100`,
},
- ],
+ reserve1: {
+ token: USDC,
+ quotient: amount`200`,
+ },
+ },
],
- })
+ ])
expect(result).toBeDefined()
expect(result?.length).toBe(1)
@@ -301,28 +322,26 @@ describe('#useRoute', () => {
it('outputs native ETH as output currency for v2 routes', () => {
const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH')
- const result = computeRoutes(USDC, ETH, TradeType.EXACT_OUTPUT, {
- route: [
- [
- {
- type: 'v2-pool',
- address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
- amountIn: amount`5`,
- amountOut: (1e18).toString(),
- tokenIn: USDC,
- tokenOut: WETH,
- reserve0: {
- token: WETH,
- quotient: amount`100`,
- },
- reserve1: {
- token: USDC,
- quotient: amount`200`,
- },
+ const result = computeRoutes(false, true, [
+ [
+ {
+ type: 'v2-pool',
+ address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
+ amountIn: amount`5`,
+ amountOut: (1e18).toString(),
+ tokenIn: USDC,
+ tokenOut: WETH,
+ reserve0: {
+ token: WETH,
+ quotient: amount`100`,
},
- ],
+ reserve1: {
+ token: USDC,
+ quotient: amount`200`,
+ },
+ },
],
- })
+ ])
expect(result?.length).toBe(1)
expect(result?.[0].routev2?.input).toStrictEqual(USDC)
diff --git a/src/state/routing/utils.ts b/src/state/routing/utils.ts
index 57cd3d9c3f..473ee2208d 100644
--- a/src/state/routing/utils.ts
+++ b/src/state/routing/utils.ts
@@ -1,32 +1,50 @@
-import { MixedRouteSDK, Protocol } from '@uniswap/router-sdk'
+import { MixedRouteSDK } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { Pair, Route as V2Route } from '@uniswap/v2-sdk'
import { FeeAmount, Pool, Route as V3Route } from '@uniswap/v3-sdk'
+import { isPolygonChain } from 'constants/chains'
+import { nativeOnChain } from 'constants/tokens'
-import { GetQuoteResult, InterfaceTrade, V2PoolInRoute, V3PoolInRoute } from './types'
+import { GetQuoteArgs } from './slice'
+import {
+ ClassicTrade,
+ PoolType,
+ QuoteData,
+ QuoteState,
+ SwapRouterNativeAssets,
+ TradeResult,
+ V2PoolInRoute,
+ V3PoolInRoute,
+} from './types'
/**
- * Transforms a Routing API quote into an array of routes that can be used to create
- * a `Trade`.
+ * Transforms a Routing API quote into an array of routes that can be used to
+ * create a `Trade`.
*/
export function computeRoutes(
- currencyIn: Currency | undefined,
- currencyOut: Currency | undefined,
- tradeType: TradeType,
- quoteResult: Pick | undefined
-) {
- if (!quoteResult || !quoteResult.route || !currencyIn || !currencyOut) return undefined
+ tokenInIsNative: boolean,
+ tokenOutIsNative: boolean,
+ routes: QuoteData['route']
+):
+ | {
+ routev3: V3Route | null
+ routev2: V2Route | null
+ mixedRoute: MixedRouteSDK | null
+ inputAmount: CurrencyAmount
+ outputAmount: CurrencyAmount
+ }[]
+ | undefined {
+ if (routes.length === 0) return []
- if (quoteResult.route.length === 0) return []
+ const tokenIn = routes[0]?.[0]?.tokenIn
+ const tokenOut = routes[0]?.[routes[0]?.length - 1]?.tokenOut
+ if (!tokenIn || !tokenOut) throw new Error('Expected both tokenIn and tokenOut to be present')
- const parsedTokenIn = parseToken(quoteResult.route[0][0].tokenIn)
- const parsedTokenOut = parseToken(quoteResult.route[0][quoteResult.route[0].length - 1].tokenOut)
- if (parsedTokenIn.address !== currencyIn.wrapped.address) return undefined
- if (parsedTokenOut.address !== currencyOut.wrapped.address) return undefined
- if (parsedTokenIn.wrapped.equals(parsedTokenOut.wrapped)) return undefined
+ const parsedCurrencyIn = tokenInIsNative ? nativeOnChain(tokenIn.chainId) : parseToken(tokenIn)
+ const parsedCurrencyOut = tokenOutIsNative ? nativeOnChain(tokenOut.chainId) : parseToken(tokenOut)
try {
- return quoteResult.route.map((route) => {
+ return routes.map((route) => {
if (route.length === 0) {
throw new Error('Expected route to have at least one pair or pool')
}
@@ -37,68 +55,90 @@ export function computeRoutes(
throw new Error('Expected both amountIn and amountOut to be present')
}
- const routeProtocol = getRouteProtocol(route)
+ const isOnlyV2 = isVersionedRoute(PoolType.V2Pool, route)
+ const isOnlyV3 = isVersionedRoute(PoolType.V3Pool, route)
return {
- routev3:
- routeProtocol === Protocol.V3
- ? new V3Route(route.map(genericPoolPairParser) as Pool[], currencyIn, currencyOut)
- : null,
- routev2:
- routeProtocol === Protocol.V2
- ? new V2Route(route.map(genericPoolPairParser) as Pair[], currencyIn, currencyOut)
- : null,
+ routev3: isOnlyV3 ? new V3Route(route.map(parsePool), parsedCurrencyIn, parsedCurrencyOut) : null,
+ routev2: isOnlyV2 ? new V2Route(route.map(parsePair), parsedCurrencyIn, parsedCurrencyOut) : null,
mixedRoute:
- routeProtocol === Protocol.MIXED
- ? new MixedRouteSDK(route.map(genericPoolPairParser), currencyIn, currencyOut)
+ !isOnlyV3 && !isOnlyV2
+ ? new MixedRouteSDK(route.map(parsePoolOrPair), parsedCurrencyIn, parsedCurrencyOut)
: null,
- inputAmount: CurrencyAmount.fromRawAmount(currencyIn, rawAmountIn),
- outputAmount: CurrencyAmount.fromRawAmount(currencyOut, rawAmountOut),
+ inputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyIn, rawAmountIn),
+ outputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyOut, rawAmountOut),
}
})
} catch (e) {
- // `Route` constructor may throw if inputs/outputs are temporarily out of sync
- // (RTK-Query always returns the latest data which may not be the right inputs/outputs)
- // This is not fatal and will fix itself in future render cycles
- console.error(e)
- return undefined
+ console.error('Error computing routes', e)
+ return
}
}
-export function transformRoutesToTrade(
- route: ReturnType,
- tradeType: TTradeType,
- blockNumber?: string | null,
- gasUseEstimateUSD?: CurrencyAmount | null
-): InterfaceTrade {
- return new InterfaceTrade({
- v2Routes:
- route
- ?.filter((r): r is typeof route[0] & { routev2: NonNullable } => r.routev2 !== null)
- .map(({ routev2, inputAmount, outputAmount }) => ({ routev2, inputAmount, outputAmount })) ?? [],
- v3Routes:
- route
- ?.filter((r): r is typeof route[0] & { routev3: NonNullable } => r.routev3 !== null)
- .map(({ routev3, inputAmount, outputAmount }) => ({ routev3, inputAmount, outputAmount })) ?? [],
- mixedRoutes:
- route
- ?.filter(
- (r): r is typeof route[0] & { mixedRoute: NonNullable } =>
- r.mixedRoute !== null
- )
- .map(({ mixedRoute, inputAmount, outputAmount }) => ({ mixedRoute, inputAmount, outputAmount })) ?? [],
- tradeType,
- gasUseEstimateUSD,
- blockNumber,
- })
+const parsePoolOrPair = (pool: V3PoolInRoute | V2PoolInRoute): Pool | Pair => {
+ return pool.type === PoolType.V3Pool ? parsePool(pool) : parsePair(pool)
}
-const parseToken = ({ address, chainId, decimals, symbol }: GetQuoteResult['route'][0][0]['tokenIn']): Token => {
+function isVersionedRoute(
+ type: T['type'],
+ route: (V3PoolInRoute | V2PoolInRoute)[]
+): route is T[] {
+ return route.every((pool) => pool.type === type)
+}
+
+export function transformRoutesToTrade(args: GetQuoteArgs, data: QuoteData): TradeResult {
+ const { tokenInAddress, tokenOutAddress, tradeType } = args
+ const tokenInIsNative = Object.values(SwapRouterNativeAssets).includes(tokenInAddress as SwapRouterNativeAssets)
+ const tokenOutIsNative = Object.values(SwapRouterNativeAssets).includes(tokenOutAddress as SwapRouterNativeAssets)
+ const { gasUseEstimateUSD, blockNumber } = data
+ const routes = computeRoutes(tokenInIsNative, tokenOutIsNative, data.route)
+
+ const trade = new ClassicTrade({
+ v2Routes:
+ routes
+ ?.filter(
+ (r): r is typeof routes[0] & { routev2: NonNullable } => r.routev2 !== null
+ )
+ .map(({ routev2, inputAmount, outputAmount }) => ({
+ routev2,
+ inputAmount,
+ outputAmount,
+ })) ?? [],
+ v3Routes:
+ routes
+ ?.filter(
+ (r): r is typeof routes[0] & { routev3: NonNullable } => r.routev3 !== null
+ )
+ .map(({ routev3, inputAmount, outputAmount }) => ({
+ routev3,
+ inputAmount,
+ outputAmount,
+ })) ?? [],
+ mixedRoutes:
+ routes
+ ?.filter(
+ (r): r is typeof routes[0] & { mixedRoute: NonNullable } =>
+ r.mixedRoute !== null
+ )
+ .map(({ mixedRoute, inputAmount, outputAmount }) => ({
+ mixedRoute,
+ inputAmount,
+ outputAmount,
+ })) ?? [],
+ tradeType,
+ gasUseEstimateUSD: parseFloat(gasUseEstimateUSD).toFixed(2).toString(),
+ blockNumber,
+ })
+
+ return { state: QuoteState.SUCCESS, trade }
+}
+
+function parseToken({ address, chainId, decimals, symbol }: QuoteData['route'][0][0]['tokenIn']): Token {
return new Token(chainId, address, parseInt(decimals.toString()), symbol)
}
-const parsePool = ({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOut }: V3PoolInRoute): Pool =>
- new Pool(
+function parsePool({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOut }: V3PoolInRoute): Pool {
+ return new Pool(
parseToken(tokenIn),
parseToken(tokenOut),
parseInt(fee) as FeeAmount,
@@ -106,6 +146,7 @@ const parsePool = ({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOu
liquidity,
parseInt(tickCurrent)
)
+}
const parsePair = ({ reserve0, reserve1 }: V2PoolInRoute): Pair =>
new Pair(
@@ -113,12 +154,15 @@ const parsePair = ({ reserve0, reserve1 }: V2PoolInRoute): Pair =>
CurrencyAmount.fromRawAmount(parseToken(reserve1.token), reserve1.quotient)
)
-const genericPoolPairParser = (pool: V3PoolInRoute | V2PoolInRoute): Pool | Pair => {
- return pool.type === 'v3-pool' ? parsePool(pool) : parsePair(pool)
+// TODO(WEB-2050): Convert other instances of tradeType comparison to use this utility function
+export function isExactInput(tradeType: TradeType): boolean {
+ return tradeType === TradeType.EXACT_INPUT
}
-function getRouteProtocol(route: (V3PoolInRoute | V2PoolInRoute)[]): Protocol {
- if (route.every((pool) => pool.type === 'v2-pool')) return Protocol.V2
- if (route.every((pool) => pool.type === 'v3-pool')) return Protocol.V3
- return Protocol.MIXED
+export function currencyAddressForSwapQuote(currency: Currency): string {
+ if (currency.isNative) {
+ return isPolygonChain(currency.chainId) ? SwapRouterNativeAssets.MATIC : SwapRouterNativeAssets.ETH
+ }
+
+ return currency.address
}
diff --git a/src/state/swap/hooks.tsx b/src/state/swap/hooks.tsx
index 751a8457d2..ea172fe76e 100644
--- a/src/state/swap/hooks.tsx
+++ b/src/state/swap/hooks.tsx
@@ -81,7 +81,7 @@ export function useDerivedSwapInfo(
parsedAmount: CurrencyAmount | undefined
inputError?: ReactNode
trade: {
- trade: InterfaceTrade | undefined
+ trade?: InterfaceTrade
state: TradeState
}
allowedSlippage: Percent
diff --git a/src/test-utils/constants.ts b/src/test-utils/constants.ts
index a4ca99a523..6d7e432d5d 100644
--- a/src/test-utils/constants.ts
+++ b/src/test-utils/constants.ts
@@ -2,7 +2,7 @@ import { CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { V3Route } from '@uniswap/smart-order-router'
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
import JSBI from 'jsbi'
-import { InterfaceTrade } from 'state/routing/types'
+import { ClassicTrade } from 'state/routing/types'
export const TEST_TOKEN_1 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 'ABC', 'Abc')
export const TEST_TOKEN_2 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 'DEF', 'Def')
@@ -30,7 +30,7 @@ export const TEST_POOL_13 = new Pool(
export const toCurrencyAmount = (token: Token, amount: number) =>
CurrencyAmount.fromRawAmount(token, JSBI.BigInt(amount))
-export const TEST_TRADE_EXACT_INPUT = new InterfaceTrade({
+export const TEST_TRADE_EXACT_INPUT = new ClassicTrade({
v3Routes: [
{
routev3: new V3Route([TEST_POOL_12], TEST_TOKEN_1, TEST_TOKEN_2),
@@ -40,9 +40,10 @@ export const TEST_TRADE_EXACT_INPUT = new InterfaceTrade({
],
v2Routes: [],
tradeType: TradeType.EXACT_INPUT,
+ gasUseEstimateUSD: '1.00',
})
-export const TEST_TRADE_EXACT_OUTPUT = new InterfaceTrade({
+export const TEST_TRADE_EXACT_OUTPUT = new ClassicTrade({
v3Routes: [
{
routev3: new V3Route([TEST_POOL_13], TEST_TOKEN_1, TEST_TOKEN_3),
diff --git a/src/utils/getRoutingDiagramEntries.ts b/src/utils/getRoutingDiagramEntries.ts
index 7a795bea11..782d2a52e1 100644
--- a/src/utils/getRoutingDiagramEntries.ts
+++ b/src/utils/getRoutingDiagramEntries.ts
@@ -15,9 +15,7 @@ const V2_DEFAULT_FEE_TIER = 3000
/**
* Loops through all routes on a trade and returns an array of diagram entries.
*/
-export default function getRoutingDiagramEntries(
- trade: InterfaceTrade
-): RoutingDiagramEntry[] {
+export default function getRoutingDiagramEntries(trade: InterfaceTrade): RoutingDiagramEntry[] {
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
const portion =
trade.tradeType === TradeType.EXACT_INPUT
diff --git a/src/utils/transformSwapRouteToGetQuoteResult.ts b/src/utils/transformSwapRouteToGetQuoteResult.ts
index 6a1d8502ec..ce92c8c9af 100644
--- a/src/utils/transformSwapRouteToGetQuoteResult.ts
+++ b/src/utils/transformSwapRouteToGetQuoteResult.ts
@@ -1,12 +1,13 @@
import { Protocol } from '@uniswap/router-sdk'
-import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
+import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { routeAmountsToString, SwapRoute } from '@uniswap/smart-order-router'
import { Pool } from '@uniswap/v3-sdk'
-import { GetQuoteResult, V2PoolInRoute, V3PoolInRoute } from 'state/routing/types'
+import { QuoteResult, QuoteState } from 'state/routing/types'
+import { QuoteData, V2PoolInRoute, V3PoolInRoute } from 'state/routing/types'
// from routing-api (https://github.com/Uniswap/routing-api/blob/main/lib/handlers/quote/quote.ts#L243-L311)
export function transformSwapRouteToGetQuoteResult(
- type: 'exactIn' | 'exactOut',
+ tradeType: TradeType,
amount: CurrencyAmount,
{
quote,
@@ -19,7 +20,7 @@ export function transformSwapRouteToGetQuoteResult(
methodParameters,
blockNumber,
}: SwapRoute
-): GetQuoteResult {
+): QuoteResult {
const routeResponse: Array<(V3PoolInRoute | V2PoolInRoute)[]> = []
for (const subRoute of route) {
@@ -34,12 +35,12 @@ export function transformSwapRouteToGetQuoteResult(
let edgeAmountIn = undefined
if (i === 0) {
- edgeAmountIn = type === 'exactIn' ? amount.quotient.toString() : quote.quotient.toString()
+ edgeAmountIn = tradeType === TradeType.EXACT_INPUT ? amount.quotient.toString() : quote.quotient.toString()
}
let edgeAmountOut = undefined
if (i === pools.length - 1) {
- edgeAmountOut = type === 'exactIn' ? quote.quotient.toString() : amount.quotient.toString()
+ edgeAmountOut = tradeType === TradeType.EXACT_INPUT ? quote.quotient.toString() : amount.quotient.toString()
}
if (nextPool instanceof Pool) {
@@ -109,7 +110,7 @@ export function transformSwapRouteToGetQuoteResult(
routeResponse.push(curRoute)
}
- const result: GetQuoteResult = {
+ const result: QuoteData = {
methodParameters,
blockNumber: blockNumber.toString(),
amount: amount.quotient.toString(),
@@ -127,5 +128,5 @@ export function transformSwapRouteToGetQuoteResult(
routeString: routeAmountsToString(route),
}
- return result
+ return { state: QuoteState.SUCCESS, data: result }
}