From 05b2711a8a05aaed65e3af955765d09c05197ac2 Mon Sep 17 00:00:00 2001 From: Ian Lapham Date: Wed, 2 Feb 2022 17:47:49 -0500 Subject: [PATCH] feat: update widget with client side SOR (#3210) * start SOR by creating custom widget hook * update best trade hook to use SOR in widget * update organization for client side SOR logic * fix auto router chain id import * remove dependency on react GA for widget * update dependencies for SOr * remove new useBestTrade.ts * update loading logic for fetching hook * update dependencies with import from ethersproject * update import version * add try catch on SOR usage * code cleanup, nit fixes --- package.json | 4 +- src/components/Settings/index.tsx | 2 +- src/hooks/useAutoRouterSupported.tsx | 2 +- .../clientSideSmartOrderRouter/constants.ts | 0 .../dependencies.ts | 24 +--- .../clientSideSmartOrderRouter/index.ts | 52 +++++++- .../useClientSideSmartOrderRouterTrade.ts | 119 ++++++++++++++++++ .../hooks/routing/useRoutingAPIArguments.ts | 41 ++++++ src/lib/hooks/swap/useSwapInfo.tsx | 8 +- src/state/routing/slice.ts | 46 +------ src/state/routing/useRoutingAPITrade.ts | 62 ++++----- yarn.lock | 25 ++++ 12 files changed, 271 insertions(+), 114 deletions(-) rename src/{state => lib/hooks}/routing/clientSideSmartOrderRouter/constants.ts (100%) rename src/{state => lib/hooks}/routing/clientSideSmartOrderRouter/dependencies.ts (50%) rename src/{state => lib/hooks}/routing/clientSideSmartOrderRouter/index.ts (62%) create mode 100644 src/lib/hooks/routing/useClientSideSmartOrderRouterTrade.ts create mode 100644 src/lib/hooks/routing/useRoutingAPIArguments.ts diff --git a/package.json b/package.json index 264ed619d4..3b384b72e9 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "@uniswap/governance": "^1.0.2", "@uniswap/liquidity-staker": "^1.0.2", "@uniswap/merkle-distributor": "1.0.1", - "@uniswap/smart-order-router": "^2.5.10", "@uniswap/v2-core": "1.0.0", "@uniswap/v2-periphery": "^1.1.0-beta.0", "@uniswap/v3-core": "1.0.0", @@ -162,7 +161,7 @@ "@ethersproject/constants": "^5.4.0", "@ethersproject/contracts": "^5.4.1", "@ethersproject/hash": "^5.4.0", - "@ethersproject/providers": "^5.4.5", + "@ethersproject/providers": "5.4.0", "@ethersproject/solidity": "^5.4.0", "@ethersproject/strings": "^5.4.0", "@ethersproject/units": "^5.4.0", @@ -176,6 +175,7 @@ "@uniswap/redux-multicall": "^1.0.0", "@uniswap/router-sdk": "^1.0.3", "@uniswap/sdk-core": "^3.0.1", + "@uniswap/smart-order-router": "^2.5.10", "@uniswap/token-lists": "^1.0.0-beta.27", "@uniswap/v2-sdk": "^3.0.1", "@uniswap/v3-sdk": "^3.7.1", diff --git a/src/components/Settings/index.tsx b/src/components/Settings/index.tsx index 9374b54c85..d62451f0e4 100644 --- a/src/components/Settings/index.tsx +++ b/src/components/Settings/index.tsx @@ -2,11 +2,11 @@ import { t, Trans } from '@lingui/macro' import { Percent } from '@uniswap/sdk-core' import useActiveWeb3React from 'hooks/useActiveWeb3React' +import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'lib/hooks/routing/clientSideSmartOrderRouter/constants' import { useContext, useRef, useState } from 'react' import { Settings, X } from 'react-feather' import ReactGA from 'react-ga' import { Text } from 'rebass' -import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'state/routing/clientSideSmartOrderRouter/constants' import styled, { ThemeContext } from 'styled-components/macro' import { useOnClickOutside } from '../../hooks/useOnClickOutside' diff --git a/src/hooks/useAutoRouterSupported.tsx b/src/hooks/useAutoRouterSupported.tsx index 7ae0111e3f..e3cc91fda0 100644 --- a/src/hooks/useAutoRouterSupported.tsx +++ b/src/hooks/useAutoRouterSupported.tsx @@ -1,5 +1,5 @@ import useActiveWeb3React from 'hooks/useActiveWeb3React' -import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'state/routing/clientSideSmartOrderRouter/constants' +import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'lib/hooks/routing/clientSideSmartOrderRouter/constants' export default function useAutoRouterSupported(): boolean { const { chainId } = useActiveWeb3React() diff --git a/src/state/routing/clientSideSmartOrderRouter/constants.ts b/src/lib/hooks/routing/clientSideSmartOrderRouter/constants.ts similarity index 100% rename from src/state/routing/clientSideSmartOrderRouter/constants.ts rename to src/lib/hooks/routing/clientSideSmartOrderRouter/constants.ts diff --git a/src/state/routing/clientSideSmartOrderRouter/dependencies.ts b/src/lib/hooks/routing/clientSideSmartOrderRouter/dependencies.ts similarity index 50% rename from src/state/routing/clientSideSmartOrderRouter/dependencies.ts rename to src/lib/hooks/routing/clientSideSmartOrderRouter/dependencies.ts index dd06d2ba4d..2bce20510b 100644 --- a/src/state/routing/clientSideSmartOrderRouter/dependencies.ts +++ b/src/lib/hooks/routing/clientSideSmartOrderRouter/dependencies.ts @@ -1,8 +1,7 @@ -import { AlphaRouterParams, IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router' +import { JsonRpcProvider } from '@ethersproject/providers' +import { AlphaRouterParams } from '@uniswap/smart-order-router' import { INFURA_NETWORK_URLS } from 'constants/chainInfo' import { SupportedChainId } from 'constants/chains' -import { providers } from 'ethers/lib/ethers' -import ReactGA from 'react-ga' import { AUTO_ROUTER_SUPPORTED_CHAINS } from './constants' @@ -14,7 +13,7 @@ export type Dependencies = { export function buildDependencies(): Dependencies { const dependenciesByChain: Dependencies = {} for (const chainId of AUTO_ROUTER_SUPPORTED_CHAINS) { - const provider = new providers.JsonRpcProvider(INFURA_NETWORK_URLS[chainId]) + const provider = new JsonRpcProvider(INFURA_NETWORK_URLS[chainId]) dependenciesByChain[chainId] = { chainId, @@ -24,20 +23,3 @@ export function buildDependencies(): Dependencies { return dependenciesByChain } - -class GAMetric extends IMetric { - putDimensions() { - return - } - - putMetric(key: string, value: number, unit?: MetricLoggerUnit) { - ReactGA.timing({ - category: 'Routing API', - variable: `${key} | ${unit}`, - value, - label: 'client', - }) - } -} - -setGlobalMetric(new GAMetric()) diff --git a/src/state/routing/clientSideSmartOrderRouter/index.ts b/src/lib/hooks/routing/clientSideSmartOrderRouter/index.ts similarity index 62% rename from src/state/routing/clientSideSmartOrderRouter/index.ts rename to src/lib/hooks/routing/clientSideSmartOrderRouter/index.ts index 89c19e724c..75061cfcfb 100644 --- a/src/state/routing/clientSideSmartOrderRouter/index.ts +++ b/src/lib/hooks/routing/clientSideSmartOrderRouter/index.ts @@ -1,3 +1,4 @@ +import { Protocol } from '@uniswap/router-sdk' import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' import { AlphaRouter, AlphaRouterConfig, ChainId } from '@uniswap/smart-order-router' import JSBI from 'jsbi' @@ -8,7 +9,7 @@ import { buildDependencies } from './dependencies' const routerParamsByChain = buildDependencies() -export async function getQuote( +async function getQuote( { type, chainId, @@ -50,3 +51,52 @@ export async function getQuote( return { data: transformSwapRouteToGetQuoteResult(type, amount, swapRoute) } } + +const protocols: Protocol[] = [Protocol.V2, Protocol.V3] + +interface QuoteArguments { + tokenInAddress: string + tokenInChainId: ChainId + tokenInDecimals: number + tokenInSymbol?: string + tokenOutAddress: string + tokenOutChainId: ChainId + tokenOutDecimals: number + tokenOutSymbol?: string + amount: string + type: 'exactIn' | 'exactOut' +} + +export async function getClientSideQuote({ + tokenInAddress, + tokenInChainId, + tokenInDecimals, + tokenInSymbol, + tokenOutAddress, + tokenOutChainId, + tokenOutDecimals, + tokenOutSymbol, + amount, + type, +}: QuoteArguments) { + return getQuote( + { + type, + chainId: tokenInChainId, + tokenIn: { + address: tokenInAddress, + chainId: tokenInChainId, + decimals: tokenInDecimals, + symbol: tokenInSymbol, + }, + tokenOut: { + address: tokenOutAddress, + chainId: tokenOutChainId, + decimals: tokenOutDecimals, + symbol: tokenOutSymbol, + }, + amount, + }, + { protocols } + ) +} diff --git a/src/lib/hooks/routing/useClientSideSmartOrderRouterTrade.ts b/src/lib/hooks/routing/useClientSideSmartOrderRouterTrade.ts new file mode 100644 index 0000000000..ee3ff97548 --- /dev/null +++ b/src/lib/hooks/routing/useClientSideSmartOrderRouterTrade.ts @@ -0,0 +1,119 @@ +import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice' +import { useEffect, useMemo, useState } from 'react' +import { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types' +import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils' + +import { getClientSideQuote } from './clientSideSmartOrderRouter' +import { useRoutingAPIArguments } from './useRoutingAPIArguments' + +export default function useClientSideSmartOrderRouterTrade( + tradeType: TTradeType, + amountSpecified?: CurrencyAmount, + otherCurrency?: Currency +): { + state: TradeState + trade: InterfaceTrade | undefined +} { + const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo( + () => + tradeType === TradeType.EXACT_INPUT + ? [amountSpecified?.currency, otherCurrency] + : [otherCurrency, amountSpecified?.currency], + [amountSpecified, otherCurrency, tradeType] + ) + + const queryArgs = useRoutingAPIArguments({ + tokenIn: currencyIn, + tokenOut: currencyOut, + amount: amountSpecified, + tradeType, + useClientSideRouter: true, + }) + + const [loading, setLoading] = useState(false) + const [{ quoteResult, error }, setFetchedResult] = useState<{ + quoteResult: GetQuoteResult | undefined + error: unknown + }>({ + quoteResult: undefined, + error: undefined, + }) + + // When arguments update, make a new call to SOR for updated quote + useEffect(() => { + setLoading(true) + fetchQuote() + + async function fetchQuote() { + try { + if (queryArgs) { + const result = await getClientSideQuote(queryArgs) + setFetchedResult({ + quoteResult: result.data, + error: result.error, + }) + } + } catch (e) { + setFetchedResult({ + quoteResult: undefined, + error: true, + }) + } finally { + setLoading(false) + } + } + }, [queryArgs]) + + 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 + + return useMemo(() => { + if (!currencyIn || !currencyOut) { + return { + state: TradeState.INVALID, + trade: undefined, + } + } + + if (loading && !quoteResult) { + // only on first hook render + return { + state: TradeState.LOADING, + trade: undefined, + } + } + + let otherAmount = undefined + if (tradeType === TradeType.EXACT_INPUT && currencyOut && quoteResult) { + otherAmount = CurrencyAmount.fromRawAmount(currencyOut, quoteResult.quote) + } + if (tradeType === TradeType.EXACT_OUTPUT && currencyIn && quoteResult) { + otherAmount = CurrencyAmount.fromRawAmount(currencyIn, quoteResult.quote) + } + + if (error || !otherAmount || !route || route.length === 0 || !queryArgs) { + return { + state: TradeState.NO_ROUTE_FOUND, + trade: undefined, + } + } + + try { + const trade = transformRoutesToTrade(route, tradeType, gasUseEstimateUSD) + return { + // always return VALID regardless of isFetching status + state: TradeState.VALID, + trade, + } + } catch (e) { + console.debug('transformRoutesToTrade failed: ', e) + return { state: TradeState.INVALID, trade: undefined } + } + }, [currencyIn, currencyOut, loading, quoteResult, tradeType, error, route, queryArgs, gasUseEstimateUSD]) +} diff --git a/src/lib/hooks/routing/useRoutingAPIArguments.ts b/src/lib/hooks/routing/useRoutingAPIArguments.ts new file mode 100644 index 0000000000..2912327fd9 --- /dev/null +++ b/src/lib/hooks/routing/useRoutingAPIArguments.ts @@ -0,0 +1,41 @@ +import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { useMemo } from 'react' + +/** + * Returns query arguments for the Routing API query or undefined if the + * query should be skipped. Input arguments do not need to be memoized, as they will + * be destructured. + */ +export function useRoutingAPIArguments({ + tokenIn, + tokenOut, + amount, + tradeType, + useClientSideRouter, +}: { + tokenIn: Currency | undefined + tokenOut: Currency | undefined + amount: CurrencyAmount | undefined + tradeType: TradeType + useClientSideRouter: boolean +}) { + return useMemo( + () => + !tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut) + ? undefined + : { + amount: amount.quotient.toString(), + tokenInAddress: tokenIn.wrapped.address, + tokenInChainId: tokenIn.wrapped.chainId, + tokenInDecimals: tokenIn.wrapped.decimals, + tokenInSymbol: tokenIn.wrapped.symbol, + tokenOutAddress: tokenOut.wrapped.address, + tokenOutChainId: tokenOut.wrapped.chainId, + tokenOutDecimals: tokenOut.wrapped.decimals, + tokenOutSymbol: tokenOut.wrapped.symbol, + useClientSideRouter, + type: (tradeType === TradeType.EXACT_INPUT ? 'exactIn' : 'exactOut') as 'exactIn' | 'exactOut', + }, + [amount, tokenIn, tokenOut, tradeType, useClientSideRouter] + ) +} diff --git a/src/lib/hooks/swap/useSwapInfo.tsx b/src/lib/hooks/swap/useSwapInfo.tsx index 90f7515bba..d612d6eb8a 100644 --- a/src/lib/hooks/swap/useSwapInfo.tsx +++ b/src/lib/hooks/swap/useSwapInfo.tsx @@ -1,7 +1,6 @@ import { Trans } from '@lingui/macro' import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance' -import { useClientSideV3Trade } from 'hooks/useClientSideV3Trade' import { atom } from 'jotai' import { useAtomValue, useUpdateAtom } from 'jotai/utils' import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance' @@ -12,6 +11,7 @@ import { ReactNode, useEffect, useMemo } from 'react' import { InterfaceTrade, TradeState } from 'state/routing/types' import { isAddress } from '../../../utils' +import useClientSideSmartOrderRouterTrade from '../routing/useClientSideSmartOrderRouterTrade' import useActiveWeb3React from '../useActiveWeb3React' interface SwapInfo { @@ -55,10 +55,8 @@ function useComputeSwapInfo(): SwapInfo { [inputCurrency, isExactIn, outputCurrency, amount] ) - /** - * @TODO (ianlapham): eventually need a strategy for routing API here - */ - const trade = useClientSideV3Trade( + //@TODO(ianlapham): this would eventually be replaced with routing api logic. + const trade = useClientSideSmartOrderRouterTrade( isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, parsedAmount, (isExactIn ? outputCurrency : inputCurrency) ?? undefined diff --git a/src/state/routing/slice.ts b/src/state/routing/slice.ts index 4a59f7dade..c47e757953 100644 --- a/src/state/routing/slice.ts +++ b/src/state/routing/slice.ts @@ -1,6 +1,7 @@ import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react' import { Protocol } from '@uniswap/router-sdk' import { ChainId } from '@uniswap/smart-order-router' +import { getClientSideQuote } from 'lib/hooks/routing/clientSideSmartOrderRouter' import ms from 'ms.macro' import qs from 'qs' @@ -15,51 +16,6 @@ const DEFAULT_QUERY_PARAMS = { // minSplits: '5', } -async function getClientSideQuote({ - tokenInAddress, - tokenInChainId, - tokenInDecimals, - tokenInSymbol, - tokenOutAddress, - tokenOutChainId, - tokenOutDecimals, - tokenOutSymbol, - amount, - type, -}: { - tokenInAddress: string - tokenInChainId: ChainId - tokenInDecimals: number - tokenInSymbol?: string - tokenOutAddress: string - tokenOutChainId: ChainId - tokenOutDecimals: number - tokenOutSymbol?: string - amount: string - type: 'exactIn' | 'exactOut' -}) { - return (await import('./clientSideSmartOrderRouter')).getQuote( - { - type, - chainId: tokenInChainId, - tokenIn: { - address: tokenInAddress, - chainId: tokenInChainId, - decimals: tokenInDecimals, - symbol: tokenInSymbol, - }, - tokenOut: { - address: tokenOutAddress, - chainId: tokenOutChainId, - decimals: tokenOutDecimals, - symbol: tokenOutSymbol, - }, - amount, - }, - { protocols } - ) -} - export const routingApi = createApi({ reducerPath: 'routingApi', baseQuery: fetchBaseQuery({ diff --git a/src/state/routing/useRoutingAPITrade.ts b/src/state/routing/useRoutingAPITrade.ts index b450891c12..a2fb36cd9e 100644 --- a/src/state/routing/useRoutingAPITrade.ts +++ b/src/state/routing/useRoutingAPITrade.ts @@ -1,9 +1,12 @@ import { skipToken } from '@reduxjs/toolkit/query/react' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router' import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice' +import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments' import useBlockNumber from 'lib/hooks/useBlockNumber' import ms from 'ms.macro' import { useMemo } from 'react' +import ReactGA from 'react-ga' import { useGetQuoteQuery } from 'state/routing/slice' import { useClientSideRouter } from 'state/user/hooks' @@ -21,44 +24,6 @@ function useFreshData(data: T, dataBlockNumber: number, maxBlockAge = 10): T return data } -/** - * Returns query arguments for the Routing API query or undefined if the - * query should be skipped. - */ -function useRoutingAPIArguments({ - tokenIn, - tokenOut, - amount, - tradeType, -}: { - tokenIn: Currency | undefined - tokenOut: Currency | undefined - amount: CurrencyAmount | undefined - tradeType: TradeType -}) { - const [clientSideRouter] = useClientSideRouter() - - return useMemo( - () => - !tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut) - ? undefined - : { - amount: amount.quotient.toString(), - tokenInAddress: tokenIn.wrapped.address, - tokenInChainId: tokenIn.wrapped.chainId, - tokenInDecimals: tokenIn.wrapped.decimals, - tokenInSymbol: tokenIn.wrapped.symbol, - tokenOutAddress: tokenOut.wrapped.address, - tokenOutChainId: tokenOut.wrapped.chainId, - tokenOutDecimals: tokenOut.wrapped.decimals, - tokenOutSymbol: tokenOut.wrapped.symbol, - useClientSideRouter: clientSideRouter, - type: (tradeType === TradeType.EXACT_INPUT ? 'exactIn' : 'exactOut') as 'exactIn' | 'exactOut', - }, - [amount, clientSideRouter, tokenIn, tokenOut, tradeType] - ) -} - /** * Returns the best trade by invoking the routing api or the smart order router on the client * @param tradeType whether the swap is an exact in/out @@ -81,11 +46,14 @@ export function useRoutingAPITrade( [amountSpecified, otherCurrency, tradeType] ) + const [clientSideRouter] = useClientSideRouter() + const queryArgs = useRoutingAPIArguments({ tokenIn: currencyIn, tokenOut: currencyOut, amount: amountSpecified, tradeType, + useClientSideRouter: clientSideRouter, }) const { isLoading, isError, data } = useGetQuoteQuery(queryArgs ?? skipToken, { @@ -148,3 +116,21 @@ export function useRoutingAPITrade( } }, [currencyIn, currencyOut, isLoading, quoteResult, tradeType, isError, route, queryArgs, gasUseEstimateUSD]) } + +// only want to enable this when app hook called +class GAMetric extends IMetric { + putDimensions() { + return + } + + putMetric(key: string, value: number, unit?: MetricLoggerUnit) { + ReactGA.timing({ + category: 'Routing API', + variable: `${key} | ${unit}`, + value, + label: 'client', + }) + } +} + +setGlobalMetric(new GAMetric()) diff --git a/yarn.lock b/yarn.lock index e75876ff55..fac4045cb6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1828,6 +1828,31 @@ dependencies: "@ethersproject/logger" "^5.5.0" +"@ethersproject/providers@5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.4.0.tgz#1b9eeaf394d790a662ec66373d7219c82f4433f2" + integrity sha512-XRmI9syLnkNdLA8ikEeg0duxmwSWTTt9S+xabnTOyI51JPJyhQ0QUNT+wvmod218ebb7rLupHDPQ7UVe2/+Tjg== + dependencies: + "@ethersproject/abstract-provider" "^5.4.0" + "@ethersproject/abstract-signer" "^5.4.0" + "@ethersproject/address" "^5.4.0" + "@ethersproject/basex" "^5.4.0" + "@ethersproject/bignumber" "^5.4.0" + "@ethersproject/bytes" "^5.4.0" + "@ethersproject/constants" "^5.4.0" + "@ethersproject/hash" "^5.4.0" + "@ethersproject/logger" "^5.4.0" + "@ethersproject/networks" "^5.4.0" + "@ethersproject/properties" "^5.4.0" + "@ethersproject/random" "^5.4.0" + "@ethersproject/rlp" "^5.4.0" + "@ethersproject/sha2" "^5.4.0" + "@ethersproject/strings" "^5.4.0" + "@ethersproject/transactions" "^5.4.0" + "@ethersproject/web" "^5.4.0" + bech32 "1.1.4" + ws "7.4.6" + "@ethersproject/providers@5.4.5": version "5.4.5" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.4.5.tgz#eb2ea2a743a8115f79604a8157233a3a2c832928"