From 9e1a775c137ac826ba9affe0f2d702ed61f386c9 Mon Sep 17 00:00:00 2001 From: Justin Domingue Date: Thu, 16 Dec 2021 14:44:03 -0500 Subject: [PATCH] feat: integrate SwapRouter02 on L1/L2 + gas ui * client-side smart order router support * support auto router on L2s * add swap router version in approval/swap callback GA events to save $ on approval txs * add persistent UI view of gas estimate on L1s Co-authored-by: Lint Action Co-authored-by: Ian Lapham Co-authored-by: Callil Capuozzo --- package.json | 5 +- public/index.html | 1 + src/assets/images/gas-icon.svg | 6 + src/assets/images/router-icon-grey.svg | 12 + src/components/AnimatedDropdown/index.tsx | 34 + src/components/Button/index.tsx | 3 +- .../CurrencyInputPanel/FiatValue.tsx | 15 +- src/components/CurrencyInputPanel/index.tsx | 86 ++- src/components/CurrencyLogo/index.tsx | 18 +- src/components/Header/NetworkSelector.tsx | 15 +- src/components/Header/Polling.tsx | 70 +- src/components/HoverInlineText/index.tsx | 22 +- src/components/Identicon/index.tsx | 6 +- src/components/NumericalInput/index.tsx | 2 +- .../RoutingDiagram/RoutingDiagram.test.tsx | 10 +- .../RoutingDiagram/RoutingDiagram.tsx | 69 +- .../RoutingDiagram.test.tsx.snap | 116 ++- src/components/Settings/index.tsx | 14 +- src/components/Tooltip/index.tsx | 6 +- src/components/WalletModal/index.tsx | 22 +- src/components/swap/AdvancedSwapDetails.tsx | 148 ++-- src/components/swap/ConfirmSwapModal.tsx | 22 +- src/components/swap/GasEstimateBadge.tsx | 105 +++ src/components/swap/RouterLabel.tsx | 10 +- src/components/swap/SwapDetailsDropdown.tsx | 202 +++++ src/components/swap/SwapModalFooter.tsx | 5 +- src/components/swap/SwapModalHeader.tsx | 60 +- src/components/swap/SwapRoute.tsx | 160 ++-- src/components/swap/SwapWarningDropdown.tsx | 75 ++ src/components/swap/TradePrice.tsx | 22 +- src/connectors/index.ts | 2 +- src/constants/addresses.ts | 20 +- src/hooks/useApproveCallback.ts | 132 +++- src/hooks/useAutoRouterSupported.tsx | 8 + ...stV3Trade.test.ts => useBestTrade.test.ts} | 140 ++-- .../{useBestV3Trade.ts => useBestTrade.ts} | 27 +- src/hooks/useClientSideV3Trade.ts | 33 +- src/hooks/useERC20Permit.ts | 25 +- src/hooks/useSwapCallback.tsx | 60 +- src/hooks/useSwapSlippageTolerance.ts | 31 +- src/hooks/useToggledVersion.ts | 21 - src/hooks/useUSDCPrice.ts | 32 +- src/pages/App.tsx | 2 +- src/pages/Swap/index.tsx | 188 ++--- .../clientSideSmartOrderRouter/constants.ts | 3 + .../dependencies.ts | 43 ++ .../clientSideSmartOrderRouter/index.ts | 52 ++ src/state/routing/slice.ts | 103 ++- src/state/routing/types.ts | 65 +- src/state/routing/useRoutingAPITrade.ts | 93 ++- src/state/routing/utils.test.ts | 197 +++-- src/state/routing/utils.ts | 79 +- src/state/swap/hooks.tsx | 83 +- src/state/user/hooks.tsx | 9 +- src/theme/components.tsx | 6 + src/types/tuple.ts | 2 + src/utils/getTradeVersion.ts | 13 - src/utils/getTxOptimizedSwapRouter.test.ts | 102 +++ src/utils/getTxOptimizedSwapRouter.ts | 32 + src/utils/isTradeBetter.ts | 6 +- src/utils/prices.test.ts | 124 ++- src/utils/prices.ts | 33 +- .../transformSwapRouteToGetQuoteResult.ts | 150 ++++ yarn.lock | 716 +++++++++++++++++- 64 files changed, 2997 insertions(+), 976 deletions(-) create mode 100644 src/assets/images/gas-icon.svg create mode 100644 src/assets/images/router-icon-grey.svg create mode 100644 src/components/AnimatedDropdown/index.tsx create mode 100644 src/components/swap/GasEstimateBadge.tsx create mode 100644 src/components/swap/SwapDetailsDropdown.tsx create mode 100644 src/components/swap/SwapWarningDropdown.tsx create mode 100644 src/hooks/useAutoRouterSupported.tsx rename src/hooks/{useBestV3Trade.test.ts => useBestTrade.test.ts} (52%) rename src/hooks/{useBestV3Trade.ts => useBestTrade.ts} (69%) delete mode 100644 src/hooks/useToggledVersion.ts create mode 100644 src/state/routing/clientSideSmartOrderRouter/constants.ts create mode 100644 src/state/routing/clientSideSmartOrderRouter/dependencies.ts create mode 100644 src/state/routing/clientSideSmartOrderRouter/index.ts delete mode 100644 src/utils/getTradeVersion.ts create mode 100644 src/utils/getTxOptimizedSwapRouter.test.ts create mode 100644 src/utils/getTxOptimizedSwapRouter.ts create mode 100644 src/utils/transformSwapRouteToGetQuoteResult.ts diff --git a/package.json b/package.json index 19b5368e87..a61c091b34 100644 --- a/package.json +++ b/package.json @@ -58,11 +58,13 @@ "@uniswap/liquidity-staker": "^1.0.2", "@uniswap/merkle-distributor": "1.0.1", "@uniswap/redux-multicall": "^1.0.0", + "@uniswap/router-sdk": "^1.0.1", "@uniswap/sdk-core": "^3.0.1", + "@uniswap/smart-order-router": "^2.5.4", "@uniswap/token-lists": "^1.0.0-beta.27", "@uniswap/v2-core": "1.0.0", "@uniswap/v2-periphery": "^1.1.0-beta.0", - "@uniswap/v2-sdk": "^3.0.0-alpha.2", + "@uniswap/v2-sdk": "^3.0.1", "@uniswap/v3-core": "1.0.0", "@uniswap/v3-periphery": "^1.1.1", "@uniswap/v3-sdk": "^3.7.1", @@ -127,6 +129,7 @@ "typescript": "^4.2.3", "ua-parser-js": "^0.7.28", "use-count-up": "^2.2.5", + "use-resize-observer": "^8.0.0", "wcag-contrast": "^3.0.0", "web-vitals": "^2.1.0", "workbox-core": "^6.1.0", diff --git a/public/index.html b/public/index.html index 06946b28ac..830b4f234e 100644 --- a/public/index.html +++ b/public/index.html @@ -67,6 +67,7 @@ html { font-size: 16px; font-variant: none; + font-smooth: always; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); diff --git a/src/assets/images/gas-icon.svg b/src/assets/images/gas-icon.svg new file mode 100644 index 0000000000..38d202909f --- /dev/null +++ b/src/assets/images/gas-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/router-icon-grey.svg b/src/assets/images/router-icon-grey.svg new file mode 100644 index 0000000000..46a752b701 --- /dev/null +++ b/src/assets/images/router-icon-grey.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/components/AnimatedDropdown/index.tsx b/src/components/AnimatedDropdown/index.tsx new file mode 100644 index 0000000000..9e1f2a0f55 --- /dev/null +++ b/src/components/AnimatedDropdown/index.tsx @@ -0,0 +1,34 @@ +import { animated, useSpring } from 'react-spring' +import useResizeObserver from 'use-resize-observer' + +/** + * @param open conditional to show content or hide + * @returns Wrapper to smoothly hide and expand content + */ +export default function AnimatedDropdown({ open, children }: React.PropsWithChildren<{ open: boolean }>) { + const { ref, height } = useResizeObserver() + + const props = useSpring({ + height: open ? height ?? 0 : 0, + config: { + mass: 1.2, + tension: 300, + friction: 20, + clamp: true, + velocity: 0.01, + }, + }) + + return ( + +
{children}
+
+ ) +} diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index c35e5161e0..2cc1259dcd 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -33,6 +33,7 @@ export const BaseButton = styled(RebassButton)< position: relative; z-index: 1; &:disabled { + opacity: 50%; cursor: auto; pointer-events: none; } @@ -236,7 +237,7 @@ const ButtonConfirmedStyle = styled(BaseButton)` /* border: 1px solid ${({ theme }) => theme.green1}; */ &:disabled { - /* opacity: 50%; */ + opacity: 50%; background-color: ${({ theme }) => theme.bg2}; color: ${({ theme }) => theme.text2}; cursor: auto; diff --git a/src/components/CurrencyInputPanel/FiatValue.tsx b/src/components/CurrencyInputPanel/FiatValue.tsx index 44da9f8602..c09fa9c399 100644 --- a/src/components/CurrencyInputPanel/FiatValue.tsx +++ b/src/components/CurrencyInputPanel/FiatValue.tsx @@ -1,4 +1,6 @@ import { Trans } from '@lingui/macro' +// eslint-disable-next-line no-restricted-imports +import { t } from '@lingui/macro' import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import HoverInlineText from 'components/HoverInlineText' import { useMemo } from 'react' @@ -6,6 +8,7 @@ import { useMemo } from 'react' import useTheme from '../../hooks/useTheme' import { ThemedText } from '../../theme' import { warningSeverity } from '../../utils/prices' +import { MouseoverTooltip } from '../Tooltip' export function FiatValue({ fiatValue, @@ -25,10 +28,14 @@ export function FiatValue({ }, [priceImpact, theme.green1, theme.red1, theme.text3, theme.yellow1]) return ( - + {fiatValue ? ( - ~$ + $ + ) : ( '' @@ -36,7 +43,9 @@ export function FiatValue({ {priceImpact ? ( {' '} - ({priceImpact.multiply(-1).toSignificant(3)}%) + + ({priceImpact.multiply(-1).toSignificant(3)}%) + ) : null} diff --git a/src/components/CurrencyInputPanel/index.tsx b/src/components/CurrencyInputPanel/index.tsx index d07a531ebb..c661322fc5 100644 --- a/src/components/CurrencyInputPanel/index.tsx +++ b/src/components/CurrencyInputPanel/index.tsx @@ -29,6 +29,8 @@ const InputPanel = styled.div<{ hideInput?: boolean }>` background-color: ${({ theme, hideInput }) => (hideInput ? 'transparent' : theme.bg2)}; z-index: 1; width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')}; + transition: height 1s ease; + will-change: height; ` const FixedContainer = styled.div` @@ -36,8 +38,7 @@ const FixedContainer = styled.div` height: 100%; position: absolute; border-radius: 20px; - background-color: ${({ theme }) => theme.bg1}; - opacity: 0.95; + background-color: ${({ theme }) => theme.bg2}; display: flex; align-items: center; justify-content: center; @@ -46,7 +47,7 @@ const FixedContainer = styled.div` const Container = styled.div<{ hideInput: boolean }>` border-radius: ${({ hideInput }) => (hideInput ? '16px' : '20px')}; - border: 1px solid ${({ theme, hideInput }) => (hideInput ? ' transparent' : theme.bg2)}; + border: 1px solid ${({ theme }) => theme.bg0}; background-color: ${({ theme }) => theme.bg1}; width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')}; :focus, @@ -56,35 +57,35 @@ const Container = styled.div<{ hideInput: boolean }>` ` const CurrencySelect = styled(ButtonGray)<{ visible: boolean; selected: boolean; hideInput?: boolean }>` - visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')}; align-items: center; - font-size: 24px; - font-weight: 500; - background-color: ${({ selected, theme }) => (selected ? theme.bg0 : theme.primary1)}; - color: ${({ selected, theme }) => (selected ? theme.text1 : theme.white)}; - border-radius: 16px; + background-color: ${({ selected, theme }) => (selected ? theme.bg2 : theme.primary1)}; box-shadow: ${({ selected }) => (selected ? 'none' : '0px 6px 10px rgba(0, 0, 0, 0.075)')}; box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075); - outline: none; + color: ${({ selected, theme }) => (selected ? theme.text1 : theme.white)}; cursor: pointer; + border-radius: 16px; + outline: none; user-select: none; border: none; + font-size: 24px; + font-weight: 500; height: ${({ hideInput }) => (hideInput ? '2.8rem' : '2.4rem')}; width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')}; padding: 0 8px; justify-content: space-between; - margin-right: ${({ hideInput }) => (hideInput ? '0' : '12px')}; + margin-left: ${({ hideInput }) => (hideInput ? '0' : '12px')}; :focus, :hover { - background-color: ${({ selected, theme }) => (selected ? theme.bg2 : darken(0.05, theme.primary1))}; + background-color: ${({ selected, theme }) => (selected ? theme.bg3 : darken(0.05, theme.primary1))}; } + visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')}; ` const InputRow = styled.div<{ selected: boolean }>` ${({ theme }) => theme.flexRowNoWrap} align-items: center; justify-content: space-between; - padding: ${({ selected }) => (selected ? ' 1rem 1rem 0.75rem 1rem' : '1rem 1rem 0.75rem 1rem')}; + padding: ${({ selected }) => (selected ? ' 1rem 1rem 0.75rem 1rem' : '1rem 1rem 1rem 1rem')}; ` const LabelRow = styled.div` @@ -128,28 +129,30 @@ const StyledTokenName = styled.span<{ active?: boolean }>` const StyledBalanceMax = styled.button<{ disabled?: boolean }>` background-color: transparent; + background-color: ${({ theme }) => theme.primary5}; border: none; border-radius: 12px; - font-size: 14px; - font-weight: 500; + color: ${({ theme }) => theme.primary1}; cursor: pointer; - padding: 0; - color: ${({ theme }) => theme.primaryText1}; - opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)}; - pointer-events: ${({ disabled }) => (!disabled ? 'initial' : 'none')}; + font-size: 11px; + font-weight: 500; margin-left: 0.25rem; + opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)}; + padding: 4px 6px; + pointer-events: ${({ disabled }) => (!disabled ? 'initial' : 'none')}; + + :hover { + opacity: ${({ disabled }) => (!disabled ? 0.8 : 0.4)}; + } :focus { outline: none; } - - ${({ theme }) => theme.mediaWidth.upToExtraSmall` - margin-right: 0.5rem; - `}; ` const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean }>` - ${loadingOpacityMixin} + ${loadingOpacityMixin}; + text-align: left; ` interface CurrencyInputPanelProps { @@ -220,6 +223,15 @@ export default function CurrencyInputPanel({ )} + {!hideInput && ( + + )} + } - {!hideInput && ( - - )} - {!hideInput && !hideBalance && ( + {!hideInput && !hideBalance && currency && ( + + + {account ? ( @@ -282,24 +289,19 @@ export default function CurrencyInputPanel({ renderBalance ? ( renderBalance(selectedCurrencyBalance) ) : ( - - Balance: {formatCurrencyAmount(selectedCurrencyBalance, 4)} {currency.symbol} - + Balance: {formatCurrencyAmount(selectedCurrencyBalance, 4)} ) ) : null} {showMaxButton && selectedCurrencyBalance ? ( - (Max) + MAX ) : null} ) : ( )} - - - )} diff --git a/src/components/CurrencyLogo/index.tsx b/src/components/CurrencyLogo/index.tsx index 79ea9c87eb..e226e8e6ef 100644 --- a/src/components/CurrencyLogo/index.tsx +++ b/src/components/CurrencyLogo/index.tsx @@ -37,16 +37,24 @@ export const getTokenLogoURL = ( const StyledEthereumLogo = styled.img<{ size: string }>` width: ${({ size }) => size}; height: ${({ size }) => size}; - box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075); - border-radius: 24px; + background: radial-gradient(white 50%, #ffffff00 calc(75% + 1px), #ffffff00 100%); + + border-radius: 50%; + -mox-box-shadow: 0 0 1px white; + -webkit-box-shadow: 0 0 1px white; + box-shadow: 0 0 1px white; + border: 0px solid rgba(255, 255, 255, 0); ` const StyledLogo = styled(Logo)<{ size: string }>` width: ${({ size }) => size}; height: ${({ size }) => size}; - border-radius: ${({ size }) => size}; - box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075); - background-color: ${({ theme }) => theme.white}; + background: radial-gradient(white 50%, #ffffff00 calc(75% + 1px), #ffffff00 100%); + border-radius: 50%; + -mox-box-shadow: 0 0 1px black; + -webkit-box-shadow: 0 0 1px black; + box-shadow: 0 0 1px black; + border: 0px solid rgba(255, 255, 255, 0); ` export default function CurrencyLogo({ diff --git a/src/components/Header/NetworkSelector.tsx b/src/components/Header/NetworkSelector.tsx index ff85c8a622..02a5f3e706 100644 --- a/src/components/Header/NetworkSelector.tsx +++ b/src/components/Header/NetworkSelector.tsx @@ -34,17 +34,16 @@ const ActiveRowLinkList = styled.div` text-decoration: none; } & > a:first-child { - border-top: 1px solid ${({ theme }) => theme.text2}; margin: 0; - margin-top: 6px; + margin-top: 0px; padding-top: 10px; } ` const ActiveRowWrapper = styled.div` - background-color: ${({ theme }) => theme.bg2}; + background-color: ${({ theme }) => theme.bg1}; border-radius: 8px; cursor: pointer; - padding: 8px 0 8px 0; + padding: 8px; width: 100%; ` const FlyoutHeader = styled.div` @@ -53,7 +52,7 @@ const FlyoutHeader = styled.div` ` const FlyoutMenu = styled.div` align-items: flex-start; - background-color: ${({ theme }) => theme.bg1}; + background-color: ${({ theme }) => theme.bg0}; box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), 0px 24px 32px rgba(0, 0, 0, 0.01); border-radius: 20px; @@ -75,7 +74,7 @@ const FlyoutMenu = styled.div` ` const FlyoutRow = styled.div<{ active: boolean }>` align-items: center; - background-color: ${({ active, theme }) => (active ? theme.bg2 : 'transparent')}; + background-color: ${({ active, theme }) => (active ? theme.bg1 : 'transparent')}; border-radius: 8px; cursor: pointer; display: flex; @@ -113,8 +112,8 @@ const SelectorLabel = styled(NetworkLabel)` ` const SelectorControls = styled.div<{ interactive: boolean }>` align-items: center; - background-color: ${({ theme }) => theme.bg1}; - border: 2px solid ${({ theme }) => theme.bg1}; + background-color: ${({ theme }) => theme.bg0}; + border: 2px solid ${({ theme }) => theme.bg0}; border-radius: 12px; color: ${({ theme }) => theme.text1}; cursor: ${({ interactive }) => (interactive ? 'pointer' : 'auto')}; diff --git a/src/components/Header/Polling.tsx b/src/components/Header/Polling.tsx index e65e9cb7d4..8daf600c89 100644 --- a/src/components/Header/Polling.tsx +++ b/src/components/Header/Polling.tsx @@ -1,7 +1,12 @@ +import { Trans } from '@lingui/macro' +import { RowFixed } from 'components/Row' import { CHAIN_INFO } from 'constants/chains' import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp' +import useGasPrice from 'hooks/useGasPrice' import useMachineTimeMs from 'hooks/useMachineTime' +import useTheme from 'hooks/useTheme' import { useActiveWeb3React } from 'hooks/web3' +import JSBI from 'jsbi' import ms from 'ms.macro' import { useEffect, useState } from 'react' import { useBlockNumber } from 'state/application/hooks' @@ -9,6 +14,7 @@ import styled, { keyframes } from 'styled-components/macro' import { ExternalLink, ThemedText } from 'theme' import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' +import { MouseoverTooltip } from '../Tooltip' import { ChainConnectivityWarning } from './ChainConnectivityWarning' const StyledPolling = styled.div<{ warning: boolean }>` @@ -31,6 +37,14 @@ const StyledPollingNumber = styled(ThemedText.Small)<{ breathe: boolean; hoverin :hover { opacity: 1; } + + a { + color: unset; + } + a:hover { + text-decoration: none; + color: unset; + } ` const StyledPollingDot = styled.div<{ warning: boolean }>` width: 8px; @@ -43,6 +57,17 @@ const StyledPollingDot = styled.div<{ warning: boolean }>` transition: 250ms ease background-color; ` +const StyledGasDot = styled.div` + background-color: ${({ theme }) => theme.text3}; + border-radius: 50%; + height: 4px; + min-height: 4px; + min-width: 4px; + position: relative; + transition: 250ms ease background-color; + width: 4px; +` + const rotate360 = keyframes` from { transform: rotate(0deg); @@ -81,6 +106,10 @@ export default function Polling() { const [isHover, setIsHover] = useState(false) const machineTime = useMachineTimeMs(NETWORK_HEALTH_CHECK_MS) const blockTime = useCurrentBlockTimestamp() + const theme = useTheme() + + const ethGasPrice = useGasPrice() + const priceGwei = ethGasPrice ? JSBI.divide(ethGasPrice, JSBI.BigInt(1000000000)) : undefined const waitMsBeforeWarning = (chainId ? CHAIN_INFO[chainId]?.blockWaitMsBeforeWarning : DEFAULT_MS_BEFORE_WARNING) ?? DEFAULT_MS_BEFORE_WARNING @@ -105,19 +134,48 @@ export default function Polling() { //if you pass a value to array, like this [data] than clearTimeout will run every time this value changes (useEffect re-run) ) + //TODO - chainlink gas oracle is really slow. Can we get a better data source? + return ( <> - + setIsHover(true)} onMouseLeave={() => setIsHover(false)} warning={warning}> + + {priceGwei ? ( + + + + {`The current fast gas amount for sending a transaction on L1. + Gas fees are paid in Ethereum's native currency Ether (ETH) and denominated in gwei. `} + + } + > + {priceGwei.toString()} gwei + + + + + ) : null} + - {blockNumber}  + + {`The most recent block number on this network. Prices update on every block.`}} + > + {blockNumber}  + + {isMounting && }{' '} - - {warning && } + {warning && } + ) } diff --git a/src/components/HoverInlineText/index.tsx b/src/components/HoverInlineText/index.tsx index 5c866219de..b1c789ab32 100644 --- a/src/components/HoverInlineText/index.tsx +++ b/src/components/HoverInlineText/index.tsx @@ -2,9 +2,15 @@ import Tooltip from 'components/Tooltip' import { useState } from 'react' import styled from 'styled-components/macro' -const TextWrapper = styled.span<{ margin: boolean; link?: boolean; fontSize?: string; adjustSize?: boolean }>` +const TextWrapper = styled.span<{ + margin: boolean + link?: boolean + fontSize?: string + adjustSize?: boolean + textColor?: string +}>` margin-left: ${({ margin }) => margin && '4px'}; - color: ${({ theme, link }) => (link ? theme.blue1 : theme.text1)}; + color: ${({ theme, link, textColor }) => (link ? theme.blue1 : textColor ?? theme.text1)}; font-size: ${({ fontSize }) => fontSize ?? 'inherit'}; @media screen and (max-width: 600px) { @@ -18,6 +24,7 @@ const HoverInlineText = ({ margin = false, adjustSize = false, fontSize, + textColor, link, ...rest }: { @@ -26,6 +33,7 @@ const HoverInlineText = ({ margin?: boolean adjustSize?: boolean fontSize?: string + textColor?: string link?: boolean }) => { const [showHover, setShowHover] = useState(false) @@ -42,6 +50,7 @@ const HoverInlineText = ({ onMouseLeave={() => setShowHover(false)} margin={margin} adjustSize={adjustSize} + textColor={textColor} link={link} fontSize={fontSize} {...rest} @@ -53,7 +62,14 @@ const HoverInlineText = ({ } return ( - + {text} ) diff --git a/src/components/Identicon/index.tsx b/src/components/Identicon/index.tsx index 01043ebbd4..915f6685a4 100644 --- a/src/components/Identicon/index.tsx +++ b/src/components/Identicon/index.tsx @@ -31,7 +31,11 @@ export default function Identicon() { if (icon) { current?.appendChild(icon) return () => { - current?.removeChild(icon) + try { + current?.removeChild(icon) + } catch (e) { + console.error('Avatar icon not found') + } } } return diff --git a/src/components/NumericalInput/index.tsx b/src/components/NumericalInput/index.tsx index e51d154945..ae2613824b 100644 --- a/src/components/NumericalInput/index.tsx +++ b/src/components/NumericalInput/index.tsx @@ -12,7 +12,7 @@ const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: s border: none; flex: 1 1 auto; background-color: ${({ theme }) => theme.bg1}; - font-size: ${({ fontSize }) => fontSize ?? '24px'}; + font-size: ${({ fontSize }) => fontSize ?? '28px'}; text-align: ${({ align }) => align && align}; white-space: nowrap; overflow: hidden; diff --git a/src/components/RoutingDiagram/RoutingDiagram.test.tsx b/src/components/RoutingDiagram/RoutingDiagram.test.tsx index ba1a6b4389..7b80468ab1 100644 --- a/src/components/RoutingDiagram/RoutingDiagram.test.tsx +++ b/src/components/RoutingDiagram/RoutingDiagram.test.tsx @@ -1,3 +1,4 @@ +import { Protocol } from '@uniswap/router-sdk' import { Currency, Percent } from '@uniswap/sdk-core' import { FeeAmount } from '@uniswap/v3-sdk' import { DAI, USDC, WBTC } from 'constants/tokens' @@ -7,16 +8,21 @@ import RoutingDiagram, { RoutingDiagramEntry } from './RoutingDiagram' const percent = (strings: TemplateStringsArray) => new Percent(parseInt(strings[0]), 100) -const singleRoute: RoutingDiagramEntry = { percent: percent`100`, path: [[USDC, DAI, FeeAmount.LOW]] } +const singleRoute: RoutingDiagramEntry = { + percent: percent`100`, + path: [[USDC, DAI, FeeAmount.LOW]], + protocol: Protocol.V3, +} const multiRoute: RoutingDiagramEntry[] = [ - { percent: percent`75`, path: [[USDC, DAI, FeeAmount.LOWEST]] }, + { percent: percent`75`, path: [[USDC, DAI, FeeAmount.LOWEST]], protocol: Protocol.V2 }, { percent: percent`25`, path: [ [USDC, WBTC, FeeAmount.MEDIUM], [WBTC, DAI, FeeAmount.HIGH], ], + protocol: Protocol.V3, }, ] diff --git a/src/components/RoutingDiagram/RoutingDiagram.tsx b/src/components/RoutingDiagram/RoutingDiagram.tsx index c369dafa52..89be51226a 100644 --- a/src/components/RoutingDiagram/RoutingDiagram.tsx +++ b/src/components/RoutingDiagram/RoutingDiagram.tsx @@ -1,3 +1,5 @@ +import { Trans } from '@lingui/macro' +import { Protocol } from '@uniswap/router-sdk' import { Currency, Percent } from '@uniswap/sdk-core' import { FeeAmount } from '@uniswap/v3-sdk' import Badge from 'components/Badge' @@ -7,24 +9,24 @@ import Row, { AutoRow } from 'components/Row' import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList' import { Box } from 'rebass' import styled from 'styled-components/macro' -import { ThemedText } from 'theme' +import { ThemedText, Z_INDEX } from 'theme' import { ReactComponent as DotLine } from '../../assets/svg/dot_line.svg' +import { MouseoverTooltip } from '../Tooltip' export interface RoutingDiagramEntry { percent: Percent path: [Currency, Currency, FeeAmount][] + protocol: Protocol } const Wrapper = styled(Box)` align-items: center; - background-color: ${({ theme }) => theme.bg0}; - width: 400px; + width: 100%; ` const RouteContainerRow = styled(Row)` display: grid; - grid-gap: 8px; grid-template-columns: 24px 1fr 24px; ` @@ -38,7 +40,7 @@ const RouteRow = styled(Row)` const PoolBadge = styled(Badge)` display: flex; - padding: 0.25rem 0.5rem; + padding: 4px 4px; ` const DottedLine = styled.div` @@ -58,7 +60,27 @@ const DotColor = styled(DotLine)` const OpaqueBadge = styled(Badge)` background-color: ${({ theme }) => theme.bg2}; - z-index: 2; + border-radius: 8px; + display: grid; + font-size: 12px; + grid-gap: 4px; + grid-auto-flow: column; + justify-content: start; + padding: 4px 6px 4px 4px; + z-index: ${Z_INDEX.sticky}; +` + +const ProtocolBadge = styled(Badge)` + background-color: ${({ theme }) => theme.bg3}; + border-radius: 4px; + color: ${({ theme }) => theme.text2}; + font-size: 10px; + padding: 2px 4px; + z-index: ${Z_INDEX.sticky + 1}; +` + +const BadgeText = styled(ThemedText.Small)` + word-break: normal; ` export default function RoutingDiagram({ @@ -75,29 +97,31 @@ export default function RoutingDiagram({ return ( - {routes.map(({ percent, path }, index) => ( + {routes.map((entry, index) => ( - - - + + + ))} ) } -function Route({ percent, path }: { percent: RoutingDiagramEntry['percent']; path: RoutingDiagramEntry['path'] }) { +function Route({ entry: { percent, path, protocol } }: { entry: RoutingDiagramEntry }) { return ( - + + {protocol.toUpperCase()} + + {percent.toSignificant(2)}% - + - {path.map(([currency0, currency1, feeAmount], index) => ( @@ -111,12 +135,17 @@ function Pool({ currency0, currency1, feeAmount }: { currency0: Currency; curren const tokenInfo0 = useTokenInfoFromActiveList(currency0) const tokenInfo1 = useTokenInfoFromActiveList(currency1) + // TODO - link pool icon to info.uniswap.org via query params return ( - - - - - {feeAmount / 10000}% - + {tokenInfo0?.symbol + '/' + tokenInfo1?.symbol + ' ' + feeAmount / 10000}% pool} + > + + + + + {feeAmount / 10000}% + + ) } diff --git a/src/components/RoutingDiagram/__snapshots__/RoutingDiagram.test.tsx.snap b/src/components/RoutingDiagram/__snapshots__/RoutingDiagram.test.tsx.snap index 618620d93f..43f1006a48 100644 --- a/src/components/RoutingDiagram/__snapshots__/RoutingDiagram.test.tsx.snap +++ b/src/components/RoutingDiagram/__snapshots__/RoutingDiagram.test.tsx.snap @@ -3,10 +3,10 @@ exports[`renders multi route 1`] = `
CurrencyLogo currency=USDC
+
+ V2 +
+
+
75%
@@ -36,26 +45,13 @@ exports[`renders multi route 1`] = ` style="justify-content: space-evenly; z-index: 2;" width="100%" > -
-
- DoubleCurrencyLogo currency0=DAI currency1=USDC -
-
- 0.01% -
-
+ Popover
CurrencyLogo currency=DAI
CurrencyLogo currency=USDC
+
+ V3 +
+
+
25%
@@ -85,34 +90,7 @@ exports[`renders multi route 1`] = ` style="justify-content: space-evenly; z-index: 2;" width="100%" > -
-
- DoubleCurrencyLogo currency0=WBTC currency1=USDC -
-
- 0.3% -
-
-
-
- DoubleCurrencyLogo currency0=DAI currency1=WBTC -
-
- 1% -
-
+ PopoverPopover
CurrencyLogo currency=DAI @@ -124,10 +102,10 @@ exports[`renders multi route 1`] = ` exports[`renders single route 1`] = `
CurrencyLogo currency=USDC
+
+ V3 +
+
+
100%
@@ -157,20 +144,7 @@ exports[`renders single route 1`] = ` style="justify-content: space-evenly; z-index: 2;" width="100%" > -
-
- DoubleCurrencyLogo currency0=DAI currency1=USDC -
-
- 0.05% -
-
+ Popover
CurrencyLogo currency=DAI @@ -182,7 +156,7 @@ exports[`renders single route 1`] = ` exports[`renders when no routes are provided 1`] = `
`; diff --git a/src/components/Settings/index.tsx b/src/components/Settings/index.tsx index 36a01b4947..36f9aebbf1 100644 --- a/src/components/Settings/index.tsx +++ b/src/components/Settings/index.tsx @@ -1,12 +1,12 @@ // eslint-disable-next-line no-restricted-imports import { t, Trans } from '@lingui/macro' import { Percent } from '@uniswap/sdk-core' -import { SupportedChainId } from 'constants/chains' import { useActiveWeb3React } from 'hooks/web3' 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' @@ -27,7 +27,7 @@ const StyledMenuIcon = styled(Settings)` width: 20px; > * { - stroke: ${({ theme }) => theme.text2}; + stroke: ${({ theme }) => theme.text1}; } :hover { @@ -199,16 +199,13 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa Interface Settings - - {chainId === SupportedChainId.MAINNET && ( + {chainId && AUTO_ROUTER_SUPPORTED_CHAINS.includes(chainId) && ( - Auto Router + Auto Router API - Use the Uniswap Labs API to get better pricing through a more efficient route.} - /> + Use the Uniswap Labs API to get faster quotes.} /> )} - diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx index 143eff36c4..ec8606c1cd 100644 --- a/src/components/Tooltip/index.tsx +++ b/src/components/Tooltip/index.tsx @@ -5,7 +5,7 @@ import styled from 'styled-components/macro' import Popover, { PopoverProps } from '../Popover' export const TooltipContainer = styled.div` - width: 256px; + max-width: 256px; padding: 0.6rem 1rem; font-weight: 400; word-break: break-word; @@ -25,6 +25,7 @@ interface TooltipContentProps extends Omit { onOpen?: () => void // whether to wrap the content in a `TooltipContainer` wrap?: boolean + disableHover?: boolean // disable the hover and content display } export default function Tooltip({ text, ...rest }: TooltipProps) { @@ -52,6 +53,7 @@ export function MouseoverTooltipContent({ content, children, onOpen: openCallback = undefined, + disableHover, ...rest }: Omit) { const [show, setShow] = useState(false) @@ -61,7 +63,7 @@ export function MouseoverTooltipContent({ }, [openCallback]) const close = useCallback(() => setShow(false), [setShow]) return ( - +
theme.primary1}; + background-color: ${({ theme }) => theme.bg1}; color: ${({ theme }) => theme.white}; :hover { @@ -402,6 +402,16 @@ export default function WalletModal({ + {walletView === WALLET_VIEWS.PENDING ? ( + + ) : ( + {getOptions()} + )} setWalletView(WALLET_VIEWS.LEGAL)}> @@ -413,16 +423,6 @@ export default function WalletModal({ - {walletView === WALLET_VIEWS.PENDING ? ( - - ) : ( - {getOptions()} - )} diff --git a/src/components/swap/AdvancedSwapDetails.tsx b/src/components/swap/AdvancedSwapDetails.tsx index c7f7eb82f5..6b22820308 100644 --- a/src/components/swap/AdvancedSwapDetails.tsx +++ b/src/components/swap/AdvancedSwapDetails.tsx @@ -1,22 +1,28 @@ import { Trans } from '@lingui/macro' import { Currency, Percent, TradeType } from '@uniswap/sdk-core' -import { Trade as V2Trade } from '@uniswap/v2-sdk' -import { Trade as V3Trade } from '@uniswap/v3-sdk' +import Card from 'components/Card' import { LoadingRows } from 'components/Loader/styled' +import { useActiveWeb3React } from 'hooks/web3' import { useContext, useMemo } from 'react' -import { ThemeContext } from 'styled-components/macro' +import { InterfaceTrade } from 'state/routing/types' +import styled, { ThemeContext } from 'styled-components/macro' -import { ThemedText } from '../../theme' +import { Separator, ThemedText } from '../../theme' import { computeRealizedLPFeePercent } from '../../utils/prices' import { AutoColumn } from '../Column' import { RowBetween, RowFixed } from '../Row' import FormattedPriceImpact from './FormattedPriceImpact' -import { TransactionDetailsLabel } from './styleds' +import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from './GasEstimateBadge' + +const StyledCard = styled(Card)` + padding: 0; +` interface AdvancedSwapDetailsProps { - trade?: V2Trade | V3Trade + trade?: InterfaceTrade allowedSlippage: Percent syncing?: boolean + hideRouteDiagram?: boolean } function TextWithLoadingPlaceholder({ @@ -39,74 +45,78 @@ function TextWithLoadingPlaceholder({ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) { const theme = useContext(ThemeContext) + const { chainId } = useActiveWeb3React() - const { realizedLPFee, priceImpact } = useMemo(() => { - if (!trade) return { realizedLPFee: undefined, priceImpact: undefined } - + const { expectedOutputAmount, priceImpact } = useMemo(() => { + if (!trade) return { expectedOutputAmount: undefined, priceImpact: undefined } + const expectedOutputAmount = trade.outputAmount const realizedLpFeePercent = computeRealizedLPFeePercent(trade) - const realizedLPFee = trade.inputAmount.multiply(realizedLpFeePercent) const priceImpact = trade.priceImpact.subtract(realizedLpFeePercent) - return { priceImpact, realizedLPFee } + return { expectedOutputAmount, priceImpact } }, [trade]) return !trade ? null : ( - - - Transaction Details - - - - - Liquidity Provider Fee - - - - - {realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${realizedLPFee.currency.symbol}` : '-'} - - - - - - - - Price Impact - - - - - - - - - - - - - Allowed Slippage - - - - - {allowedSlippage.toFixed(2)}% - - - - - - - - {trade.tradeType === TradeType.EXACT_INPUT ? Minimum received : Maximum sent} - - - - - {trade.tradeType === TradeType.EXACT_INPUT - ? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}` - : `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`} - - - - + + + + + + Expected Output + + + + + {expectedOutputAmount + ? `${expectedOutputAmount.toSignificant(6)} ${expectedOutputAmount.currency.symbol}` + : '-'} + + + + + + + Price Impact + + + + + + + + + + + + + {trade.tradeType === TradeType.EXACT_INPUT ? ( + Minimum received + ) : ( + Maximum sent + )}{' '} + after slippage ({allowedSlippage.toFixed(2)}%) + + + + + {trade.tradeType === TradeType.EXACT_INPUT + ? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}` + : `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`} + + + + {!trade?.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : ( + + + Network Fee + + + + ~${trade.gasUseEstimateUSD.toFixed(2)} + + + + )} + + ) } diff --git a/src/components/swap/ConfirmSwapModal.tsx b/src/components/swap/ConfirmSwapModal.tsx index 4324607236..f95f2f2ba3 100644 --- a/src/components/swap/ConfirmSwapModal.tsx +++ b/src/components/swap/ConfirmSwapModal.tsx @@ -1,8 +1,8 @@ import { Trans } from '@lingui/macro' +import { Trade } from '@uniswap/router-sdk' import { Currency, Percent, TradeType } from '@uniswap/sdk-core' -import { Trade as V2Trade } from '@uniswap/v2-sdk' -import { Trade as V3Trade } from '@uniswap/v3-sdk' import { ReactNode, useCallback, useMemo } from 'react' +import { InterfaceTrade } from 'state/routing/types' import TransactionConfirmationModal, { ConfirmationModalContent, @@ -16,9 +16,7 @@ import SwapModalHeader from './SwapModalHeader' * @param args either a pair of V2 trades or a pair of V3 trades */ function tradeMeaningfullyDiffers( - ...args: - | [V2Trade, V2Trade] - | [V3Trade, V3Trade] + ...args: [Trade, Trade] ): boolean { const [tradeA, tradeB] = args return ( @@ -44,8 +42,8 @@ export default function ConfirmSwapModal({ txHash, }: { isOpen: boolean - trade: V2Trade | V3Trade | undefined - originalTrade: V2Trade | V3Trade | undefined + trade: InterfaceTrade | undefined + originalTrade: Trade | undefined attemptingTxn: boolean txHash: string | undefined recipient: string | null @@ -56,15 +54,7 @@ export default function ConfirmSwapModal({ onDismiss: () => void }) { const showAcceptChanges = useMemo( - () => - Boolean( - (trade instanceof V2Trade && - originalTrade instanceof V2Trade && - tradeMeaningfullyDiffers(trade, originalTrade)) || - (trade instanceof V3Trade && - originalTrade instanceof V3Trade && - tradeMeaningfullyDiffers(trade, originalTrade)) - ), + () => Boolean(trade && originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)), [originalTrade, trade] ) diff --git a/src/components/swap/GasEstimateBadge.tsx b/src/components/swap/GasEstimateBadge.tsx new file mode 100644 index 0000000000..341c4990f3 --- /dev/null +++ b/src/components/swap/GasEstimateBadge.tsx @@ -0,0 +1,105 @@ +import { Trans } from '@lingui/macro' +import { Currency, TradeType } from '@uniswap/sdk-core' +import { ChainId } from '@uniswap/smart-order-router' +import { AutoColumn } from 'components/Column' +import { LoadingOpacityContainer } from 'components/Loader/styled' +import { RowFixed } from 'components/Row' +import { MouseoverTooltipContent } from 'components/Tooltip' +import ReactGA from 'react-ga' +import { InterfaceTrade } from 'state/routing/types' +import styled from 'styled-components/macro' +import { ThemedText } from 'theme' + +import { ReactComponent as GasIcon } from '../../assets/images/gas-icon.svg' +import { ResponsiveTooltipContainer } from './styleds' +import SwapRoute from './SwapRoute' + +const GasWrapper = styled(RowFixed)` + border-radius: 8px; + padding: 4px 6px; + height: 24px; + color: ${({ theme }) => theme.text3}; + background-color: ${({ theme }) => theme.bg1}; + font-size: 14px; + font-weight: 500; + user-select: none; +` +const StyledGasIcon = styled(GasIcon)` + margin-right: 4px; + height: 14px; + & > * { + stroke: ${({ theme }) => theme.text3}; + } +` + +export const SUPPORTED_GAS_ESTIMATE_CHAIN_IDS = [ChainId.MAINNET] + +export default function GasEstimateBadge({ + trade, + loading, + showRoute, + disableHover, +}: { + trade: InterfaceTrade | undefined | null // dollar amount in active chain's stablecoin + loading: boolean + showRoute?: boolean // show route instead of gas estimation summary + disableHover?: boolean +}) { + const formattedGasPriceString = trade?.gasUseEstimateUSD + ? trade.gasUseEstimateUSD.toFixed(2) === '0.00' + ? '<$0.01' + : '$' + trade.gasUseEstimateUSD.toFixed(2) + : undefined + + return ( + + {showRoute ? ( + trade ? ( + + ) : null + ) : ( + + + Estimated network fee + + + ${trade?.gasUseEstimateUSD?.toFixed(2)} + + + Estimate may differ due to your wallet gas settings + + + )} + + ) + } + placement="bottom" + onOpen={() => + ReactGA.event({ + category: 'Gas', + action: 'Gas Details Tooltip Open', + }) + } + > + + + + {formattedGasPriceString ?? null} + + + + ) +} diff --git a/src/components/swap/RouterLabel.tsx b/src/components/swap/RouterLabel.tsx index 6879820467..48b75aec40 100644 --- a/src/components/swap/RouterLabel.tsx +++ b/src/components/swap/RouterLabel.tsx @@ -1,5 +1,5 @@ import { Trans } from '@lingui/macro' -import { useRoutingAPIEnabled } from 'state/user/hooks' +import useAutoRouterSupported from 'hooks/useAutoRouterSupported' import styled from 'styled-components/macro' import { ThemedText } from 'theme' @@ -40,15 +40,15 @@ const StyledAutoRouterLabel = styled(ThemedText.Black)` ` export function AutoRouterLogo() { - const routingAPIEnabled = useRoutingAPIEnabled() + const autoRouterSupported = useAutoRouterSupported() - return routingAPIEnabled ? : + return autoRouterSupported ? : } export function AutoRouterLabel() { - const routingAPIEnabled = useRoutingAPIEnabled() + const autoRouterSupported = useAutoRouterSupported() - return routingAPIEnabled ? ( + return autoRouterSupported ? ( Auto Router ) : ( diff --git a/src/components/swap/SwapDetailsDropdown.tsx b/src/components/swap/SwapDetailsDropdown.tsx new file mode 100644 index 0000000000..bcf633a88a --- /dev/null +++ b/src/components/swap/SwapDetailsDropdown.tsx @@ -0,0 +1,202 @@ +import { Trans } from '@lingui/macro' +import { Currency, Percent, TradeType } from '@uniswap/sdk-core' +import AnimatedDropdown from 'components/AnimatedDropdown' +import Card, { OutlineCard } from 'components/Card' +import { AutoColumn } from 'components/Column' +import { LoadingOpacityContainer } from 'components/Loader/styled' +import Row, { RowBetween, RowFixed } from 'components/Row' +import { MouseoverTooltipContent } from 'components/Tooltip' +import { useActiveWeb3React } from 'hooks/web3' +import { darken } from 'polished' +import { useState } from 'react' +import { ChevronDown, Info } from 'react-feather' +import { InterfaceTrade } from 'state/routing/types' +import styled, { keyframes, useTheme } from 'styled-components/macro' +import { HideSmall, ThemedText } from 'theme' + +import { AdvancedSwapDetails } from './AdvancedSwapDetails' +import GasEstimateBadge, { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from './GasEstimateBadge' +import { ResponsiveTooltipContainer } from './styleds' +import SwapRoute from './SwapRoute' +import TradePrice from './TradePrice' + +const Wrapper = styled(Row)` + width: 100%; + justify-content: center; +` + +const StyledInfoIcon = styled(Info)` + height: 16px; + width: 16px; + margin-right: 4px; + color: ${({ theme }) => theme.text3}; +` + +const StyledCard = styled(OutlineCard)` + padding: 12px; + border: 1px solid ${({ theme }) => theme.bg2}; +` + +const StyledHeaderRow = styled(RowBetween)<{ disabled: boolean; open: boolean }>` + padding: 4px 8px; + border-radius: 12px; + background-color: ${({ open, theme }) => (open ? theme.bg1 : 'transparent')}; + align-items: center; + cursor: ${({ disabled }) => (disabled ? 'initial' : 'pointer')}; + min-height: 40px; + + :hover { + background-color: ${({ theme, disabled }) => (disabled ? theme.bg1 : darken(0.015, theme.bg1))}; + } +` + +const RotatingArrow = styled(ChevronDown)<{ open?: boolean }>` + transform: ${({ open }) => (open ? 'rotate(180deg)' : 'none')}; + transition: transform 0.1s linear; +` + +const StyledPolling = styled.div` + display: flex; + height: 16px; + width: 16px; + margin-right: 2px; + margin-left: 10px; + align-items: center; + color: ${({ theme }) => theme.text1}; + transition: 250ms ease color; + + ${({ theme }) => theme.mediaWidth.upToMedium` + display: none; + `} +` + +const StyledPollingDot = styled.div` + width: 8px; + height: 8px; + min-height: 8px; + min-width: 8px; + border-radius: 50%; + position: relative; + background-color: ${({ theme }) => theme.bg2}; + transition: 250ms ease background-color; +` + +const rotate360 = keyframes` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +` + +const Spinner = styled.div` + animation: ${rotate360} 1s cubic-bezier(0.83, 0, 0.17, 1) infinite; + transform: translateZ(0); + border-top: 1px solid transparent; + border-right: 1px solid transparent; + border-bottom: 1px solid transparent; + border-left: 2px solid ${({ theme }) => theme.text1}; + background: transparent; + width: 14px; + height: 14px; + border-radius: 50%; + position: relative; + transition: 250ms ease border-color; + left: -3px; + top: -3px; +` + +interface SwapDetailsInlineProps { + trade: InterfaceTrade | undefined + syncing: boolean + loading: boolean + showInverted: boolean + setShowInverted: React.Dispatch> + allowedSlippage: Percent +} + +export default function SwapDetailsDropdown({ + trade, + syncing, + loading, + showInverted, + setShowInverted, + allowedSlippage, +}: SwapDetailsInlineProps) { + const theme = useTheme() + const { chainId } = useActiveWeb3React() + const [showDetails, setShowDetails] = useState(false) + + return ( + + + setShowDetails(!showDetails)} disabled={!trade} open={showDetails}> + + {loading || syncing ? ( + + + + + + ) : ( + + + + + + + } + placement="bottom" + disableHover={showDetails} + > + + + + )} + {trade ? ( + + + + ) : loading || syncing ? ( + + Fetching best price... + + ) : null} + + + {!trade?.gasUseEstimateUSD || + showDetails || + !chainId || + !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : ( + + )} + + + + + + {trade ? ( + + + + ) : null} + {trade ? : null} + + + + + ) +} diff --git a/src/components/swap/SwapModalFooter.tsx b/src/components/swap/SwapModalFooter.tsx index a63dddde87..97dd995022 100644 --- a/src/components/swap/SwapModalFooter.tsx +++ b/src/components/swap/SwapModalFooter.tsx @@ -1,7 +1,6 @@ import { Trans } from '@lingui/macro' +import { Trade } from '@uniswap/router-sdk' import { Currency, TradeType } from '@uniswap/sdk-core' -import { Trade as V2Trade } from '@uniswap/v2-sdk' -import { Trade as V3Trade } from '@uniswap/v3-sdk' import { ReactNode } from 'react' import { Text } from 'rebass' @@ -14,7 +13,7 @@ export default function SwapModalFooter({ swapErrorMessage, disabledConfirm, }: { - trade: V2Trade | V3Trade + trade: Trade onConfirm: () => void swapErrorMessage: ReactNode | undefined disabledConfirm: boolean diff --git a/src/components/swap/SwapModalHeader.tsx b/src/components/swap/SwapModalHeader.tsx index 1affbed34f..acc218b524 100644 --- a/src/components/swap/SwapModalHeader.tsx +++ b/src/components/swap/SwapModalHeader.tsx @@ -1,10 +1,9 @@ import { Trans } from '@lingui/macro' import { Currency, Percent, TradeType } from '@uniswap/sdk-core' -import { Trade as V2Trade } from '@uniswap/v2-sdk' -import { Trade as V3Trade } from '@uniswap/v3-sdk' import { useContext, useState } from 'react' import { AlertTriangle, ArrowDown } from 'react-feather' import { Text } from 'rebass' +import { InterfaceTrade } from 'state/routing/types' import styled, { ThemeContext } from 'styled-components/macro' import { useUSDCValue } from '../../hooks/useUSDCPrice' @@ -46,7 +45,7 @@ export default function SwapModalHeader({ showAcceptChanges, onAcceptChanges, }: { - trade: V2Trade | V3Trade + trade: InterfaceTrade allowedSlippage: Percent recipient: string | null showAcceptChanges: boolean @@ -63,19 +62,7 @@ export default function SwapModalHeader({ - - - From - - - - - - - {trade.inputAmount.currency.symbol} - - + + + + {trade.inputAmount.currency.symbol} + + + + + @@ -93,10 +89,20 @@ export default function SwapModalHeader({ + + + + {trade.outputAmount.toSignificant(6)} + + + + + + {trade.outputAmount.currency.symbol} + + + - - To - - - - - - {trade.outputAmount.currency.symbol} - - - - - {trade.outputAmount.toSignificant(6)} - - - - - Price - - - {showAcceptChanges ? ( diff --git a/src/components/swap/SwapRoute.tsx b/src/components/swap/SwapRoute.tsx index 1a1fdc80d9..eb61c80979 100644 --- a/src/components/swap/SwapRoute.tsx +++ b/src/components/swap/SwapRoute.tsx @@ -1,92 +1,113 @@ import { Trans } from '@lingui/macro' +import { Trade } from '@uniswap/router-sdk' import { Currency, Percent, TradeType } from '@uniswap/sdk-core' -import { Trade as V2Trade } from '@uniswap/v2-sdk' -import { FeeAmount, Trade as V3Trade } from '@uniswap/v3-sdk' -import Badge from 'components/Badge' +import { Pair } from '@uniswap/v2-sdk' +import AnimatedDropdown from 'components/AnimatedDropdown' import { AutoColumn } from 'components/Column' import { LoadingRows } from 'components/Loader/styled' import RoutingDiagram, { RoutingDiagramEntry } from 'components/RoutingDiagram/RoutingDiagram' import { AutoRow, RowBetween } from 'components/Row' -import { Version } from 'hooks/useToggledVersion' -import { memo } from 'react' -import { useRoutingAPIEnabled } from 'state/user/hooks' +import useAutoRouterSupported from 'hooks/useAutoRouterSupported' +import { useActiveWeb3React } from 'hooks/web3' +import { memo, useState } from 'react' +import { Plus } from 'react-feather' +import { InterfaceTrade } from 'state/routing/types' +import { useDarkModeManager } from 'state/user/hooks' import styled from 'styled-components/macro' -import { ThemedText } from 'theme' -import { getTradeVersion } from 'utils/getTradeVersion' +import { Separator, ThemedText } from 'theme' +import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from './GasEstimateBadge' import { AutoRouterLabel, AutoRouterLogo } from './RouterLabel' -const Separator = styled.div` - border-top: 1px solid ${({ theme }) => theme.bg2}; - height: 1px; - width: 100%; +const Wrapper = styled(AutoColumn)<{ darkMode?: boolean; fixedOpen?: boolean }>` + padding: ${({ fixedOpen }) => (fixedOpen ? '12px' : '12px 8px 12px 12px')}; + border-radius: 16px; + border: 1px solid ${({ theme, fixedOpen }) => (fixedOpen ? 'transparent' : theme.bg2)}; + cursor: pointer; +` + +const OpenCloseIcon = styled(Plus)<{ open?: boolean }>` + margin-left: 8px; + height: 20px; + stroke-width: 2px; + transition: transform 0.1s; + transform: ${({ open }) => (open ? 'rotate(45deg)' : 'none')}; + stroke: ${({ theme }) => theme.text3}; + cursor: pointer; + :hover { + opacity: 0.8; + } ` const V2_DEFAULT_FEE_TIER = 3000 -export default memo(function SwapRoute({ - trade, - syncing, -}: { - trade: V2Trade | V3Trade +interface SwapRouteProps extends React.HTMLAttributes { + trade: InterfaceTrade syncing: boolean -}) { - const routingAPIEnabled = useRoutingAPIEnabled() + fixedOpen?: boolean // fixed in open state, hide open/close icon +} + +export default memo(function SwapRoute({ trade, syncing, fixedOpen = false, ...rest }: SwapRouteProps) { + const autoRouterSupported = useAutoRouterSupported() + const routes = getTokenPath(trade) + const [open, setOpen] = useState(false) + const { chainId } = useActiveWeb3React() + + const [darkMode] = useDarkModeManager() + + const formattedGasPriceString = trade?.gasUseEstimateUSD + ? trade.gasUseEstimateUSD.toFixed(2) === '0.00' + ? '<$0.01' + : '$' + trade.gasUseEstimateUSD.toFixed(2) + : undefined return ( - - + + setOpen(!open)}> - {syncing ? ( - -
- - ) : ( - - - {getTradeVersion(trade) === Version.v2 ? V2 : V3} - - - )} + {fixedOpen ? null : } - - {syncing ? ( - -
- - ) : ( - - )} - {routingAPIEnabled && ( - - This route optimizes your price by considering split routes, multiple hops, and gas costs. - - )} - + + + {syncing ? ( + +
+ + ) : ( + + )} + + {autoRouterSupported && + (syncing ? ( + +
+ + ) : ( + + {trade?.gasUseEstimateUSD && chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? ( + Best price route costs ~{formattedGasPriceString} in gas. + ) : null}{' '} + + This route optimizes your total output by considering split routes, multiple hops, and the gas cost of + each step. + + + ))} + + + ) }) -function getTokenPath( - trade: V2Trade | V3Trade -): RoutingDiagramEntry[] { - // convert V2 path to a list of routes - if (trade instanceof V2Trade) { - const { path: tokenPath } = (trade as V2Trade).route - const path = [] - for (let i = 1; i < tokenPath.length; i++) { - path.push([tokenPath[i - 1], tokenPath[i], V2_DEFAULT_FEE_TIER] as RoutingDiagramEntry['path'][0]) - } - return [{ percent: new Percent(100, 100), path }] - } - - return trade.swaps.map(({ route: { tokenPath, pools }, inputAmount, outputAmount }) => { +function getTokenPath(trade: Trade): RoutingDiagramEntry[] { + return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => { const portion = trade.tradeType === TradeType.EXACT_INPUT ? inputAmount.divide(trade.inputAmount) @@ -94,18 +115,25 @@ function getTokenPath( const percent = new Percent(portion.numerator, portion.denominator) - const path: [Currency, Currency, FeeAmount][] = [] + const path: RoutingDiagramEntry['path'] = [] for (let i = 0; i < pools.length; i++) { const nextPool = pools[i] const tokenIn = tokenPath[i] const tokenOut = tokenPath[i + 1] - path.push([tokenIn, tokenOut, nextPool.fee]) + const entry: RoutingDiagramEntry['path'][0] = [ + tokenIn, + tokenOut, + nextPool instanceof Pair ? V2_DEFAULT_FEE_TIER : nextPool.fee, + ] + + path.push(entry) } return { percent, path, + protocol, } }) } diff --git a/src/components/swap/SwapWarningDropdown.tsx b/src/components/swap/SwapWarningDropdown.tsx new file mode 100644 index 0000000000..56ac4c0f55 --- /dev/null +++ b/src/components/swap/SwapWarningDropdown.tsx @@ -0,0 +1,75 @@ +import { Trans } from '@lingui/macro' +import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' +import { RowBetween } from 'components/Row' +import { MouseoverTooltipContent } from 'components/Tooltip' +import { Info } from 'react-feather' +import { InterfaceTrade } from 'state/routing/types' +import styled from 'styled-components/macro' +import { ThemedText } from 'theme' + +import { ResponsiveTooltipContainer } from './styleds' + +const Wrapper = styled.div` + background-color: ${({ theme }) => theme.bg1}; + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; + padding: 14px; + margin-top: -20px; + padding-top: 32px; +` + +const StyledInfoIcon = styled(Info)` + stroke: ${({ theme }) => theme.text3}; +` + +/** + * @returns Dropdown card for showing edge case warnings outside of button + */ +export default function SwapWarningDropdown({ + fiatValueInput, + trade, +}: { + fiatValueInput: CurrencyAmount | null + trade: InterfaceTrade | undefined +}) { + // gas cost estimate is more than half of input value + const showNetworkFeeWarning = Boolean( + fiatValueInput && + trade?.gasUseEstimateUSD && + parseFloat(trade.gasUseEstimateUSD.toSignificant(6)) > parseFloat(fiatValueInput.toFixed(6)) / 2 + ) + + if (!showNetworkFeeWarning) { + return null + } + + return ( + + {showNetworkFeeWarning ? ( + + + Network fees exceed 50% of the swap amount! + + + + + The cost of sending this transaction is more than half of the value of the input amount. + + + + You might consider waiting until the network fees go down to complete this transaction. + + + } + placement="bottom" + > + + + + ) : null} + + ) +} diff --git a/src/components/swap/TradePrice.tsx b/src/components/swap/TradePrice.tsx index 7aaa39f15d..15f3382436 100644 --- a/src/components/swap/TradePrice.tsx +++ b/src/components/swap/TradePrice.tsx @@ -13,16 +13,20 @@ interface TradePriceProps { } const StyledPriceContainer = styled.button` - align-items: center; background-color: transparent; border: none; cursor: pointer; - display: grid; - height: 24px; - justify-content: center; + align-items: center + justify-content: flex-start; padding: 0; grid-template-columns: 1fr auto; grid-gap: 0.25rem; + display: flex; + flex-direction: row; + text-align: left; + flex-wrap: wrap; + padding: 8px 0; + user-select: text; ` export default function TradePrice({ price, showInverted, setShowInverted }: TradePriceProps) { @@ -44,8 +48,14 @@ export default function TradePrice({ price, showInverted, setShowInverted }: Tra const text = `${'1 ' + labelInverted + ' = ' + formattedPrice ?? '-'} ${label}` return ( - - + { + e.stopPropagation() // dont want this click to affect dropdowns / hovers + flipPrice() + }} + title={text} + > + {text} {' '} {usdcPrice && ( diff --git a/src/connectors/index.ts b/src/connectors/index.ts index 54eaf84ff8..5a242642ba 100644 --- a/src/connectors/index.ts +++ b/src/connectors/index.ts @@ -19,7 +19,7 @@ if (typeof INFURA_KEY === 'undefined') { throw new Error(`REACT_APP_INFURA_KEY must be a defined environment variable`) } -const NETWORK_URLS: { [key in SupportedChainId]: string } = { +export const NETWORK_URLS: { [key in SupportedChainId]: string } = { [SupportedChainId.MAINNET]: `https://mainnet.infura.io/v3/${INFURA_KEY}`, [SupportedChainId.RINKEBY]: `https://rinkeby.infura.io/v3/${INFURA_KEY}`, [SupportedChainId.ROPSTEN]: `https://ropsten.infura.io/v3/${INFURA_KEY}`, diff --git a/src/constants/addresses.ts b/src/constants/addresses.ts index a2a84c01ef..513aa77eea 100644 --- a/src/constants/addresses.ts +++ b/src/constants/addresses.ts @@ -16,7 +16,20 @@ export const MULTICALL_ADDRESS: AddressMap = { [SupportedChainId.ARBITRUM_RINKEBY]: '0xa501c031958F579dB7676fF1CE78AD305794d579', } export const V2_FACTORY_ADDRESSES: AddressMap = constructSameAddressMap(V2_FACTORY_ADDRESS) + export const V2_ROUTER_ADDRESS: AddressMap = constructSameAddressMap('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D') +export const V3_ROUTER_ADDRESS: AddressMap = constructSameAddressMap('0xE592427A0AEce92De3Edee1F18E0157C05861564', [ + SupportedChainId.OPTIMISM, + SupportedChainId.OPTIMISTIC_KOVAN, + SupportedChainId.ARBITRUM_ONE, + SupportedChainId.ARBITRUM_RINKEBY, +]) +export const SWAP_ROUTER_ADDRESSES: AddressMap = constructSameAddressMap('0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', [ + SupportedChainId.OPTIMISM, + SupportedChainId.OPTIMISTIC_KOVAN, + SupportedChainId.ARBITRUM_ONE, + SupportedChainId.ARBITRUM_RINKEBY, +]) /** * The oldest V0 governance address @@ -75,12 +88,7 @@ export const ENS_REGISTRAR_ADDRESSES: AddressMap = { export const SOCKS_CONTROLLER_ADDRESSES: AddressMap = { [SupportedChainId.MAINNET]: '0x65770b5283117639760beA3F867b69b3697a91dd', } -export const SWAP_ROUTER_ADDRESSES: AddressMap = constructSameAddressMap('0xE592427A0AEce92De3Edee1F18E0157C05861564', [ - SupportedChainId.OPTIMISM, - SupportedChainId.OPTIMISTIC_KOVAN, - SupportedChainId.ARBITRUM_ONE, - SupportedChainId.ARBITRUM_RINKEBY, -]) + export const V3_MIGRATOR_ADDRESSES: AddressMap = constructSameAddressMap('0xA5644E29708357803b5A882D272c41cC0dF92B34', [ SupportedChainId.ARBITRUM_ONE, SupportedChainId.ARBITRUM_RINKEBY, diff --git a/src/hooks/useApproveCallback.ts b/src/hooks/useApproveCallback.ts index 5ec7f5c079..4e9ccfadd9 100644 --- a/src/hooks/useApproveCallback.ts +++ b/src/hooks/useApproveCallback.ts @@ -1,11 +1,13 @@ import { MaxUint256 } from '@ethersproject/constants' import { TransactionResponse } from '@ethersproject/providers' +import { Protocol, Trade } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' -import { Trade as V2Trade } from '@uniswap/v2-sdk' -import { Trade as V3Trade } from '@uniswap/v3-sdk' +import { Pair, Route as V2Route, Trade as V2Trade } from '@uniswap/v2-sdk' +import { Pool, Route as V3Route, Trade as V3Trade } from '@uniswap/v3-sdk' import { useCallback, useMemo } from 'react' +import { getTxOptimizedSwapRouter, SwapRouterVersion } from 'utils/getTxOptimizedSwapRouter' -import { SWAP_ROUTER_ADDRESSES, V2_ROUTER_ADDRESS } from '../constants/addresses' +import { SWAP_ROUTER_ADDRESSES, V2_ROUTER_ADDRESS, V3_ROUTER_ADDRESS } from '../constants/addresses' import { TransactionType } from '../state/transactions/actions' import { useHasPendingApproval, useTransactionAdder } from '../state/transactions/hooks' import { calculateGasMargin } from '../utils/calculateGasMargin' @@ -20,18 +22,14 @@ export enum ApprovalState { APPROVED = 'APPROVED', } -// returns a variable indicating the state of the approval and a function which approves if necessary or early returns -export function useApproveCallback( - amountToApprove?: CurrencyAmount, - spender?: string -): [ApprovalState, () => Promise] { - const { account, chainId } = useActiveWeb3React() +export function useApprovalState(amountToApprove?: CurrencyAmount, spender?: string) { + const { account } = useActiveWeb3React() const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined + const currentAllowance = useTokenAllowance(token, account ?? undefined, spender) const pendingApproval = useHasPendingApproval(token?.address, spender) - // check the current approval status - const approvalState: ApprovalState = useMemo(() => { + return useMemo(() => { if (!amountToApprove || !spender) return ApprovalState.UNKNOWN if (amountToApprove.currency.isNative) return ApprovalState.APPROVED // we might not have enough data to know whether or not we need to approve @@ -44,6 +42,40 @@ export function useApproveCallback( : ApprovalState.NOT_APPROVED : ApprovalState.APPROVED }, [amountToApprove, currentAllowance, pendingApproval, spender]) +} + +/** Returns approval state for all known swap routers */ +export function useAllApprovalStates( + trade: Trade | undefined, + allowedSlippage: Percent +) { + const { chainId } = useActiveWeb3React() + + const amountToApprove = useMemo( + () => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined), + [trade, allowedSlippage] + ) + + const v2ApprovalState = useApprovalState(amountToApprove, chainId ? V2_ROUTER_ADDRESS[chainId] : undefined) + const v3ApprovalState = useApprovalState(amountToApprove, chainId ? V3_ROUTER_ADDRESS[chainId] : undefined) + const v2V3ApprovalState = useApprovalState(amountToApprove, chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined) + + return useMemo( + () => ({ v2: v2ApprovalState, v3: v3ApprovalState, v2V3: v2V3ApprovalState }), + [v2ApprovalState, v2V3ApprovalState, v3ApprovalState] + ) +} + +// returns a variable indicating the state of the approval and a function which approves if necessary or early returns +export function useApproveCallback( + amountToApprove?: CurrencyAmount, + spender?: string +): [ApprovalState, () => Promise] { + const { chainId } = useActiveWeb3React() + const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined + + // check the current approval status + const approvalState = useApprovalState(amountToApprove, spender) const tokenContract = useTokenContract(token?.address) const addTransaction = useTransactionAdder() @@ -103,23 +135,91 @@ export function useApproveCallback( // wraps useApproveCallback in the context of a swap export function useApproveCallbackFromTrade( - trade: V2Trade | V3Trade | undefined, + trade: + | V2Trade + | V3Trade + | Trade + | undefined, allowedSlippage: Percent ) { const { chainId } = useActiveWeb3React() - const v3SwapRouterAddress = chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined const amountToApprove = useMemo( () => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined), [trade, allowedSlippage] ) - return useApproveCallback( + + const approveCallback = useApproveCallback( amountToApprove, chainId ? trade instanceof V2Trade ? V2_ROUTER_ADDRESS[chainId] : trade instanceof V3Trade - ? v3SwapRouterAddress - : undefined + ? V3_ROUTER_ADDRESS[chainId] + : SWAP_ROUTER_ADDRESSES[chainId] : undefined ) + + // TODO: remove L162-168 after testing is done. This error will help detect mistakes in the logic. + if ( + (Trade instanceof V2Trade && approveCallback[0] !== ApprovalState.APPROVED) || + (trade instanceof V3Trade && approveCallback[0] !== ApprovalState.APPROVED) + ) { + throw new Error('Trying to approve legacy router') + } + + return approveCallback +} + +export function useApprovalOptimizedTrade( + trade: Trade | undefined, + allowedSlippage: Percent +): + | V2Trade + | V3Trade + | Trade + | undefined { + const onlyV2Routes = trade?.routes.every((route) => route.protocol === Protocol.V2) + const onlyV3Routes = trade?.routes.every((route) => route.protocol === Protocol.V3) + const tradeHasSplits = (trade?.routes.length ?? 0) > 1 + + const approvalStates = useAllApprovalStates(trade, allowedSlippage) + + const optimizedSwapRouter = useMemo( + () => getTxOptimizedSwapRouter({ onlyV2Routes, onlyV3Routes, tradeHasSplits, approvalStates }), + [approvalStates, tradeHasSplits, onlyV2Routes, onlyV3Routes] + ) + + return useMemo(() => { + if (!trade) return undefined + + try { + switch (optimizedSwapRouter) { + case SwapRouterVersion.V2V3: + return trade + case SwapRouterVersion.V2: + const pairs = trade.swaps[0].route.pools.filter((pool) => pool instanceof Pair) as Pair[] + const v2Route = new V2Route(pairs, trade.inputAmount.currency, trade.outputAmount.currency) + return new V2Trade(v2Route, trade.inputAmount, trade.tradeType) + case SwapRouterVersion.V3: + return V3Trade.createUncheckedTradeWithMultipleRoutes({ + routes: trade.swaps.map(({ route, inputAmount, outputAmount }) => ({ + route: new V3Route( + route.pools.filter((p) => p instanceof Pool) as Pool[], + inputAmount.currency, + outputAmount.currency + ), + inputAmount, + outputAmount, + })), + tradeType: trade.tradeType, + }) + default: + return undefined + } + } catch (e) { + // TODO(#2989): remove try-catch + console.debug(e) + return undefined + } + }, [trade, optimizedSwapRouter]) } diff --git a/src/hooks/useAutoRouterSupported.tsx b/src/hooks/useAutoRouterSupported.tsx new file mode 100644 index 0000000000..64c616d1c6 --- /dev/null +++ b/src/hooks/useAutoRouterSupported.tsx @@ -0,0 +1,8 @@ +import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'state/routing/clientSideSmartOrderRouter/constants' + +import { useActiveWeb3React } from './web3' + +export default function useAutoRouterSupported(): boolean { + const { chainId } = useActiveWeb3React() + return Boolean(chainId && AUTO_ROUTER_SUPPORTED_CHAINS.includes(chainId)) +} diff --git a/src/hooks/useBestV3Trade.test.ts b/src/hooks/useBestTrade.test.ts similarity index 52% rename from src/hooks/useBestV3Trade.test.ts rename to src/hooks/useBestTrade.test.ts index c09322daed..b15c423d57 100644 --- a/src/hooks/useBestV3Trade.test.ts +++ b/src/hooks/useBestTrade.test.ts @@ -1,11 +1,11 @@ import { renderHook } from '@testing-library/react-hooks' import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { DAI, USDC } from 'constants/tokens' -import { V3TradeState } from 'state/routing/types' -import { useRoutingAPIEnabled } from 'state/user/hooks' +import { TradeState } from 'state/routing/types' import { useRoutingAPITrade } from '../state/routing/useRoutingAPITrade' -import { useBestV3Trade } from './useBestV3Trade' +import useAutoRouterSupported from './useAutoRouterSupported' +import { useBestTrade } from './useBestTrade' import { useClientSideV3Trade } from './useClientSideV3Trade' import useDebounce from './useDebounce' import useIsWindowVisible from './useIsWindowVisible' @@ -13,31 +13,27 @@ import useIsWindowVisible from './useIsWindowVisible' const USDCAmount = CurrencyAmount.fromRawAmount(USDC, '10000') const DAIAmount = CurrencyAmount.fromRawAmount(DAI, '10000') -jest.mock('./useDebounce') -const mockUseDebounce = useDebounce as jest.MockedFunction - -// mock modules containing hooks -jest.mock('state/routing/useRoutingAPITrade') +jest.mock('./useAutoRouterSupported') jest.mock('./useClientSideV3Trade') -jest.mock('state/user/hooks') +jest.mock('./useDebounce') jest.mock('./useIsWindowVisible') +jest.mock('state/routing/useRoutingAPITrade') +jest.mock('state/user/hooks') -const mockUseRoutingAPIEnabled = useRoutingAPIEnabled as jest.MockedFunction +const mockUseDebounce = useDebounce as jest.MockedFunction +const mockUseAutoRouterSupported = useAutoRouterSupported as jest.MockedFunction const mockUseIsWindowVisible = useIsWindowVisible as jest.MockedFunction -// useRouterTrade mocks const mockUseRoutingAPITrade = useRoutingAPITrade as jest.MockedFunction - -// useClientSideV3Trade mocks const mockUseClientSideV3Trade = useClientSideV3Trade as jest.MockedFunction // helpers to set mock expectations -const expectRouterMock = (state: V3TradeState) => { - mockUseRoutingAPITrade.mockReturnValue({ state, trade: null }) +const expectRouterMock = (state: TradeState) => { + mockUseRoutingAPITrade.mockReturnValue({ state, trade: undefined }) } -const expectClientSideMock = (state: V3TradeState) => { - mockUseClientSideV3Trade.mockReturnValue({ state, trade: null }) +const expectClientSideMock = (state: TradeState) => { + mockUseClientSideV3Trade.mockReturnValue({ state, trade: undefined }) } beforeEach(() => { @@ -45,156 +41,156 @@ beforeEach(() => { mockUseDebounce.mockImplementation((value) => value) mockUseIsWindowVisible.mockReturnValue(true) - mockUseRoutingAPIEnabled.mockReturnValue(true) + mockUseAutoRouterSupported.mockReturnValue(true) }) -describe('#useBestV3TradeExactIn', () => { - it('does not compute routing api trade when routing API is disabled', () => { - mockUseRoutingAPIEnabled.mockReturnValue(false) - expectRouterMock(V3TradeState.INVALID) - expectClientSideMock(V3TradeState.VALID) +describe('#useBestV3Trade ExactIn', () => { + it('does not compute routing api trade when routing API is not supported', () => { + mockUseAutoRouterSupported.mockReturnValue(false) + expectRouterMock(TradeState.INVALID) + expectClientSideMock(TradeState.VALID) - const { result } = renderHook(() => useBestV3Trade(TradeType.EXACT_INPUT, USDCAmount, DAI)) + const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI)) expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI) - expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null }) + expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined }) }) it('does not compute routing api trade when window is not focused', () => { mockUseIsWindowVisible.mockReturnValue(false) - expectRouterMock(V3TradeState.NO_ROUTE_FOUND) - expectClientSideMock(V3TradeState.VALID) + expectRouterMock(TradeState.NO_ROUTE_FOUND) + expectClientSideMock(TradeState.VALID) - const { result } = renderHook(() => useBestV3Trade(TradeType.EXACT_INPUT, USDCAmount, DAI)) + const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI)) expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI) - expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null }) + expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined }) }) describe('when routing api is in non-error state', () => { it('does not compute client side v3 trade if routing api is LOADING', () => { - expectRouterMock(V3TradeState.LOADING) + expectRouterMock(TradeState.LOADING) - const { result } = renderHook(() => useBestV3Trade(TradeType.EXACT_INPUT, USDCAmount, DAI)) + const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI)) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined) - expect(result.current).toEqual({ state: V3TradeState.LOADING, trade: null }) + expect(result.current).toEqual({ state: TradeState.LOADING, trade: undefined }) }) it('does not compute client side v3 trade if routing api is VALID', () => { - expectRouterMock(V3TradeState.VALID) + expectRouterMock(TradeState.VALID) - const { result } = renderHook(() => useBestV3Trade(TradeType.EXACT_INPUT, USDCAmount, DAI)) + const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI)) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined) - expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null }) + expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined }) }) it('does not compute client side v3 trade if routing api is SYNCING', () => { - expectRouterMock(V3TradeState.SYNCING) + expectRouterMock(TradeState.SYNCING) - const { result } = renderHook(() => useBestV3Trade(TradeType.EXACT_INPUT, USDCAmount, DAI)) + const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI)) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined) - expect(result.current).toEqual({ state: V3TradeState.SYNCING, trade: null }) + expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined }) }) }) describe('when routing api is in error state', () => { it('does not compute client side v3 trade if routing api is INVALID', () => { - expectRouterMock(V3TradeState.INVALID) - expectClientSideMock(V3TradeState.VALID) + expectRouterMock(TradeState.INVALID) + expectClientSideMock(TradeState.VALID) - renderHook(() => useBestV3Trade(TradeType.EXACT_INPUT, USDCAmount, DAI)) + renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI)) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined) }) it('computes client side v3 trade if routing api is NO_ROUTE_FOUND', () => { - expectRouterMock(V3TradeState.NO_ROUTE_FOUND) - expectClientSideMock(V3TradeState.VALID) + expectRouterMock(TradeState.NO_ROUTE_FOUND) + expectClientSideMock(TradeState.VALID) - const { result } = renderHook(() => useBestV3Trade(TradeType.EXACT_INPUT, USDCAmount, DAI)) + const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI)) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI) - expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null }) + expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined }) }) }) }) -describe('#useBestV3TradeExactOut', () => { - it('does not compute routing api trade when routing API is disabled', () => { - mockUseRoutingAPIEnabled.mockReturnValue(false) - expectRouterMock(V3TradeState.INVALID) - expectClientSideMock(V3TradeState.VALID) +describe('#useBestV3Trade ExactOut', () => { + it('does not compute routing api trade when routing API is not supported', () => { + mockUseAutoRouterSupported.mockReturnValue(false) + expectRouterMock(TradeState.INVALID) + expectClientSideMock(TradeState.VALID) - const { result } = renderHook(() => useBestV3Trade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) + const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC) - expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null }) + expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined }) }) it('does not compute routing api trade when window is not focused', () => { mockUseIsWindowVisible.mockReturnValue(false) - expectRouterMock(V3TradeState.NO_ROUTE_FOUND) - expectClientSideMock(V3TradeState.VALID) + expectRouterMock(TradeState.NO_ROUTE_FOUND) + expectClientSideMock(TradeState.VALID) - const { result } = renderHook(() => useBestV3Trade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) + const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC) - expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null }) + expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined }) }) describe('when routing api is in non-error state', () => { it('does not compute client side v3 trade if routing api is LOADING', () => { - expectRouterMock(V3TradeState.LOADING) + expectRouterMock(TradeState.LOADING) - const { result } = renderHook(() => useBestV3Trade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) + const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined) - expect(result.current).toEqual({ state: V3TradeState.LOADING, trade: null }) + expect(result.current).toEqual({ state: TradeState.LOADING, trade: undefined }) }) it('does not compute client side v3 trade if routing api is VALID', () => { - expectRouterMock(V3TradeState.VALID) + expectRouterMock(TradeState.VALID) - const { result } = renderHook(() => useBestV3Trade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) + const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined) - expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null }) + expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined }) }) it('does not compute client side v3 trade if routing api is SYNCING', () => { - expectRouterMock(V3TradeState.SYNCING) + expectRouterMock(TradeState.SYNCING) - const { result } = renderHook(() => useBestV3Trade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) + const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined) - expect(result.current).toEqual({ state: V3TradeState.SYNCING, trade: null }) + expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined }) }) }) describe('when routing api is in error state', () => { it('computes client side v3 trade if routing api is INVALID', () => { - expectRouterMock(V3TradeState.INVALID) - expectClientSideMock(V3TradeState.VALID) + expectRouterMock(TradeState.INVALID) + expectClientSideMock(TradeState.VALID) - renderHook(() => useBestV3Trade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) + renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined) }) it('computes client side v3 trade if routing api is NO_ROUTE_FOUND', () => { - expectRouterMock(V3TradeState.NO_ROUTE_FOUND) - expectClientSideMock(V3TradeState.VALID) + expectRouterMock(TradeState.NO_ROUTE_FOUND) + expectClientSideMock(TradeState.VALID) - const { result } = renderHook(() => useBestV3Trade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) + const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC)) expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC) - expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null }) + expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined }) }) }) }) diff --git a/src/hooks/useBestV3Trade.ts b/src/hooks/useBestTrade.ts similarity index 69% rename from src/hooks/useBestV3Trade.ts rename to src/hooks/useBestTrade.ts index bf650163a6..4d8183e5d4 100644 --- a/src/hooks/useBestV3Trade.ts +++ b/src/hooks/useBestTrade.ts @@ -1,36 +1,34 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' -import { Trade } from '@uniswap/v3-sdk' -import { V3TradeState } from 'state/routing/types' +import { InterfaceTrade, TradeState } from 'state/routing/types' import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade' -import { useRoutingAPIEnabled } from 'state/user/hooks' +import useAutoRouterSupported from './useAutoRouterSupported' import { useClientSideV3Trade } from './useClientSideV3Trade' import useDebounce from './useDebounce' import useIsWindowVisible from './useIsWindowVisible' /** - * Returns the best v3 trade for a desired swap. - * Uses optimized routes from the Routing API and falls back to the v3 router. + * Returns the best v2+v3 trade for a desired swap. * @param tradeType whether the swap is an exact in/out * @param amountSpecified the exact amount to swap in/out * @param otherCurrency the desired output/payment currency */ -export function useBestV3Trade( +export function useBestTrade( tradeType: TradeType, amountSpecified?: CurrencyAmount, otherCurrency?: Currency ): { - state: V3TradeState - trade: Trade | null + state: TradeState + trade: InterfaceTrade | undefined } { - const routingAPIEnabled = useRoutingAPIEnabled() + const autoRouterSupported = useAutoRouterSupported() const isWindowVisible = useIsWindowVisible() const [debouncedAmount, debouncedOtherCurrency] = useDebounce([amountSpecified, otherCurrency], 200) const routingAPITrade = useRoutingAPITrade( tradeType, - routingAPIEnabled && isWindowVisible ? debouncedAmount : undefined, + autoRouterSupported && isWindowVisible ? debouncedAmount : undefined, debouncedOtherCurrency ) @@ -48,18 +46,19 @@ export function useBestV3Trade( !amountSpecified.currency.equals(routingAPITrade.trade.outputAmount.currency) || !debouncedOtherCurrency?.equals(routingAPITrade.trade.inputAmount.currency)) - const useFallback = !routingAPIEnabled || (!debouncing && routingAPITrade.state === V3TradeState.NO_ROUTE_FOUND) + const useFallback = !autoRouterSupported || (!debouncing && routingAPITrade.state === TradeState.NO_ROUTE_FOUND) - // only use client side router if routing api trade failed + // only use client side router if routing api trade failed or is not supported const bestV3Trade = useClientSideV3Trade( tradeType, useFallback ? debouncedAmount : undefined, useFallback ? debouncedOtherCurrency : undefined ) + // only return gas estimate from api if routing api trade is used return { ...(useFallback ? bestV3Trade : routingAPITrade), - ...(debouncing ? { state: V3TradeState.SYNCING } : {}), - ...(isLoading ? { state: V3TradeState.LOADING } : {}), + ...(debouncing ? { state: TradeState.SYNCING } : {}), + ...(isLoading ? { state: TradeState.LOADING } : {}), } } diff --git a/src/hooks/useClientSideV3Trade.ts b/src/hooks/useClientSideV3Trade.ts index d2e498ff5a..0a3d890a16 100644 --- a/src/hooks/useClientSideV3Trade.ts +++ b/src/hooks/useClientSideV3Trade.ts @@ -1,9 +1,9 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' -import { Route, SwapQuoter, Trade } from '@uniswap/v3-sdk' +import { Route, SwapQuoter } from '@uniswap/v3-sdk' import { SupportedChainId } from 'constants/chains' import JSBI from 'jsbi' import { useMemo } from 'react' -import { V3TradeState } from 'state/routing/types' +import { InterfaceTrade, TradeState } from 'state/routing/types' import { useSingleContractWithCallData } from '../state/multicall/hooks' import { useAllV3Routes } from './useAllV3Routes' @@ -27,7 +27,7 @@ export function useClientSideV3Trade( tradeType: TTradeType, amountSpecified?: CurrencyAmount, otherCurrency?: Currency -): { state: V3TradeState; trade: Trade | null } { +): { state: TradeState; trade: InterfaceTrade | undefined } { const [currencyIn, currencyOut] = useMemo( () => tradeType === TradeType.EXACT_INPUT @@ -61,15 +61,15 @@ export function useClientSideV3Trade( : amountSpecified.currency.equals(currencyIn)) ) { return { - state: V3TradeState.INVALID, - trade: null, + state: TradeState.INVALID, + trade: undefined, } } if (routesLoading || quotesResults.some(({ loading }) => loading)) { return { - state: V3TradeState.LOADING, - trade: null, + state: TradeState.LOADING, + trade: undefined, } } @@ -117,18 +117,23 @@ export function useClientSideV3Trade( if (!bestRoute || !amountIn || !amountOut) { return { - state: V3TradeState.NO_ROUTE_FOUND, - trade: null, + state: TradeState.NO_ROUTE_FOUND, + trade: undefined, } } return { - state: V3TradeState.VALID, - trade: Trade.createUncheckedTrade({ - route: bestRoute, + state: TradeState.VALID, + trade: new InterfaceTrade({ + v2Routes: [], + v3Routes: [ + { + routev3: bestRoute, + inputAmount: amountIn, + outputAmount: amountOut, + }, + ], tradeType, - inputAmount: amountIn, - outputAmount: amountOut, }), } }, [amountSpecified, currencyIn, currencyOut, quotesResults, routes, routesLoading, tradeType]) diff --git a/src/hooks/useERC20Permit.ts b/src/hooks/useERC20Permit.ts index 3af4a338d0..f5ca9b484d 100644 --- a/src/hooks/useERC20Permit.ts +++ b/src/hooks/useERC20Permit.ts @@ -1,11 +1,12 @@ import { splitSignature } from '@ethersproject/bytes' +import { Trade } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' import { Trade as V2Trade } from '@uniswap/v2-sdk' import { Trade as V3Trade } from '@uniswap/v3-sdk' import JSBI from 'jsbi' import { useMemo, useState } from 'react' -import { SWAP_ROUTER_ADDRESSES } from '../constants/addresses' +import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from '../constants/addresses' import { DAI, UNI, USDC } from '../constants/tokens' import { useSingleCallResult } from '../state/multicall/hooks' import { useEIP2612Contract } from './useContract' @@ -272,20 +273,26 @@ export function useV2LiquidityTokenPermit( } export function useERC20PermitFromTrade( - trade: V2Trade | V3Trade | undefined, + trade: + | V2Trade + | V3Trade + | Trade + | undefined, allowedSlippage: Percent ) { const { chainId } = useActiveWeb3React() - const swapRouterAddress = chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined + const swapRouterAddress = chainId + ? // v2 router does not support + trade instanceof V2Trade + ? undefined + : trade instanceof V3Trade + ? V3_ROUTER_ADDRESS[chainId] + : SWAP_ROUTER_ADDRESSES[chainId] + : undefined const amountToApprove = useMemo( () => (trade ? trade.maximumAmountIn(allowedSlippage) : undefined), [trade, allowedSlippage] ) - return useERC20Permit( - amountToApprove, - // v2 router does not support - trade instanceof V2Trade ? undefined : trade instanceof V3Trade ? swapRouterAddress : undefined, - null - ) + return useERC20Permit(amountToApprove, swapRouterAddress, null) } diff --git a/src/hooks/useSwapCallback.tsx b/src/hooks/useSwapCallback.tsx index 41f1f2fec1..8c56838a34 100644 --- a/src/hooks/useSwapCallback.tsx +++ b/src/hooks/useSwapCallback.tsx @@ -1,12 +1,13 @@ import { BigNumber } from '@ethersproject/bignumber' // eslint-disable-next-line no-restricted-imports import { t, Trans } from '@lingui/macro' +import { SwapRouter, Trade } from '@uniswap/router-sdk' import { Currency, Percent, TradeType } from '@uniswap/sdk-core' -import { Router, Trade as V2Trade } from '@uniswap/v2-sdk' -import { SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk' +import { Router as V2SwapRouter, Trade as V2Trade } from '@uniswap/v2-sdk' +import { SwapRouter as V3SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk' import { ReactNode, useMemo } from 'react' -import { SWAP_ROUTER_ADDRESSES } from '../constants/addresses' +import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from '../constants/addresses' import { TransactionType } from '../state/transactions/actions' import { useTransactionAdder } from '../state/transactions/hooks' import approveAmountCalldata from '../utils/approveAmountCalldata' @@ -20,6 +21,11 @@ import { SignatureData } from './useERC20Permit' import useTransactionDeadline from './useTransactionDeadline' import { useActiveWeb3React } from './web3' +type AnyTrade = + | V2Trade + | V3Trade + | Trade + enum SwapCallbackState { INVALID, LOADING, @@ -45,7 +51,6 @@ interface FailedCall extends SwapCallEstimate { call: SwapCall error: Error } - /** * Returns the swap calls that can be used to make the trade * @param trade trade to execute @@ -54,7 +59,7 @@ interface FailedCall extends SwapCallEstimate { * @param signatureData the signature data of the permit of the input token amount, if available */ function useSwapCallArguments( - trade: V2Trade | V3Trade | undefined, // trade to execute, required + trade: AnyTrade | undefined, // trade to execute, required allowedSlippage: Percent, // in bips recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender signatureData: SignatureData | null | undefined @@ -75,7 +80,7 @@ function useSwapCallArguments( const swapMethods = [] swapMethods.push( - Router.swapCallParameters(trade, { + V2SwapRouter.swapCallParameters(trade, { feeOnTransfer: false, allowedSlippage, recipient, @@ -85,7 +90,7 @@ function useSwapCallArguments( if (trade.tradeType === TradeType.EXACT_INPUT) { swapMethods.push( - Router.swapCallParameters(trade, { + V2SwapRouter.swapCallParameters(trade, { feeOnTransfer: true, allowedSlippage, recipient, @@ -118,14 +123,10 @@ function useSwapCallArguments( } }) } else { - // trade is V3Trade - const swapRouterAddress = chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined - if (!swapRouterAddress) return [] - - const { value, calldata } = SwapRouter.swapCallParameters(trade, { + // swap options shared by v3 and v2+v3 swap routers + const sharedSwapOptions = { recipient, slippageTolerance: allowedSlippage, - deadline: deadline.toString(), ...(signatureData ? { inputTokenPermit: @@ -146,7 +147,26 @@ function useSwapCallArguments( }, } : {}), - }) + } + + const swapRouterAddress = chainId + ? trade instanceof V3Trade + ? V3_ROUTER_ADDRESS[chainId] + : SWAP_ROUTER_ADDRESSES[chainId] + : undefined + if (!swapRouterAddress) return [] + + const { value, calldata } = + trade instanceof V3Trade + ? V3SwapRouter.swapCallParameters(trade, { + ...sharedSwapOptions, + deadline: deadline.toString(), + }) + : SwapRouter.swapCallParameters(trade, { + ...sharedSwapOptions, + deadlineOrPreviousBlockhash: deadline.toString(), + }) + if (argentWalletContract && trade.inputAmount.currency.isToken) { return [ { @@ -174,16 +194,16 @@ function useSwapCallArguments( ] } }, [ + trade, + recipient, + library, account, - allowedSlippage, - argentWalletContract, chainId, deadline, - library, - recipient, routerContract, + allowedSlippage, + argentWalletContract, signatureData, - trade, ]) } @@ -267,7 +287,7 @@ function swapErrorToUserReadableMessage(error: any): ReactNode { // returns a function that will execute a swap, if the parameters are all valid // and the user has approved the slippage adjusted input amount for the trade export function useSwapCallback( - trade: V2Trade | V3Trade | undefined, // trade to execute, required + trade: AnyTrade | undefined, // trade to execute, required allowedSlippage: Percent, // in bips recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender signatureData: SignatureData | undefined | null diff --git a/src/hooks/useSwapSlippageTolerance.ts b/src/hooks/useSwapSlippageTolerance.ts index 4434d6f2e4..b678c7f75d 100644 --- a/src/hooks/useSwapSlippageTolerance.ts +++ b/src/hooks/useSwapSlippageTolerance.ts @@ -1,9 +1,10 @@ +import { Trade } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' -import { Trade as V2Trade } from '@uniswap/v2-sdk' -import { Trade as V3Trade } from '@uniswap/v3-sdk' +import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'components/swap/GasEstimateBadge' import { L2_CHAIN_IDS } from 'constants/chains' import JSBI from 'jsbi' import { useMemo } from 'react' +import { InterfaceTrade } from 'state/routing/types' import { useUserSlippageToleranceWithDefault } from '../state/user/hooks' import { useCurrency } from './Tokens' @@ -11,7 +12,6 @@ import useGasPrice from './useGasPrice' import useUSDCPrice, { useUSDCValue } from './useUSDCPrice' import { useActiveWeb3React } from './web3' -const V2_SWAP_DEFAULT_SLIPPAGE = new Percent(50, 10_000) // .50% const V3_SWAP_DEFAULT_SLIPPAGE = new Percent(50, 10_000) // .50% const ONE_TENTHS_PERCENT = new Percent(10, 10_000) // .10% @@ -19,12 +19,8 @@ const ONE_TENTHS_PERCENT = new Percent(10, 10_000) // .10% * Return a guess of the gas cost used in computing slippage tolerance for a given trade * @param trade the trade for which to _guess_ the amount of gas it would cost to execute */ -function guesstimateGas( - trade: V2Trade | V3Trade | undefined -): number | undefined { - if (trade instanceof V2Trade) { - return 90_000 + trade.route.pairs.length * 30_000 - } else if (trade instanceof V3Trade) { +function guesstimateGas(trade: Trade | undefined): number | undefined { + if (!!trade) { return 100_000 + trade.swaps.reduce((memo, swap) => swap.route.pools.length + memo, 0) * 30_000 } return undefined @@ -34,7 +30,7 @@ const MIN_AUTO_SLIPPAGE_TOLERANCE = new Percent(5, 1000) // 0.5% const MAX_AUTO_SLIPPAGE_TOLERANCE = new Percent(25, 100) // 25% export default function useSwapSlippageTolerance( - trade: V2Trade | V3Trade | undefined + trade: InterfaceTrade | undefined ): Percent { const { chainId } = useActiveWeb3React() const onL2 = chainId && L2_CHAIN_IDS.includes(chainId) @@ -53,19 +49,26 @@ export default function useSwapSlippageTolerance( const dollarGasCost = ether && ethGasCost && etherPrice ? etherPrice.quote(CurrencyAmount.fromRawAmount(ether, ethGasCost)) : undefined - if (outputDollarValue && dollarGasCost) { + // if valid estimate from api and using api trade, use gas estimate from api + // 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 + + if (outputDollarValue && dollarCostToUse) { // the rationale is that a user will not want their trade to fail for a loss due to slippage that is less than // the cost of the gas of the failed transaction - const fraction = dollarGasCost.asFraction.divide(outputDollarValue.asFraction) + const fraction = dollarCostToUse.asFraction.divide(outputDollarValue.asFraction) const result = new Percent(fraction.numerator, fraction.denominator) if (result.greaterThan(MAX_AUTO_SLIPPAGE_TOLERANCE)) return MAX_AUTO_SLIPPAGE_TOLERANCE if (result.lessThan(MIN_AUTO_SLIPPAGE_TOLERANCE)) return MIN_AUTO_SLIPPAGE_TOLERANCE return result } - if (trade instanceof V2Trade) return V2_SWAP_DEFAULT_SLIPPAGE return V3_SWAP_DEFAULT_SLIPPAGE - }, [ethGasPrice, ether, etherPrice, gasEstimate, onL2, outputDollarValue, trade]) + }, [trade, onL2, ethGasPrice, gasEstimate, ether, etherPrice, chainId, outputDollarValue]) return useUserSlippageToleranceWithDefault(defaultSlippageTolerance) } diff --git a/src/hooks/useToggledVersion.ts b/src/hooks/useToggledVersion.ts deleted file mode 100644 index 71f3d5359e..0000000000 --- a/src/hooks/useToggledVersion.ts +++ /dev/null @@ -1,21 +0,0 @@ -import useParsedQueryString from './useParsedQueryString' - -export enum Version { - v2 = 'V2', - v3 = 'V3', -} - -export default function useToggledVersion(): Version | undefined { - const { use } = useParsedQueryString() - if (typeof use !== 'string') { - return undefined - } - switch (use.toLowerCase()) { - case 'v2': - return Version.v2 - case 'v3': - return Version.v3 - default: - return undefined - } -} diff --git a/src/hooks/useUSDCPrice.ts b/src/hooks/useUSDCPrice.ts index 5ca74ebabe..4d1a941359 100644 --- a/src/hooks/useUSDCPrice.ts +++ b/src/hooks/useUSDCPrice.ts @@ -1,5 +1,6 @@ import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-core' import { useMemo } from 'react' +import { tryParseAmount } from 'state/swap/hooks' import { SupportedChainId } from '../constants/chains' import { DAI_OPTIMISM, USDC, USDC_ARBITRUM } from '../constants/tokens' @@ -9,7 +10,7 @@ import { useActiveWeb3React } from './web3' // Stablecoin amounts used when calculating spot price for a given currency. // The amount is large enough to filter low liquidity pairs. -const STABLECOIN_AMOUNT_OUT: { [chainId: number]: CurrencyAmount } = { +export const STABLECOIN_AMOUNT_OUT: { [chainId: number]: CurrencyAmount } = { [SupportedChainId.MAINNET]: CurrencyAmount.fromRawAmount(USDC, 100_000e6), [SupportedChainId.ARBITRUM_ONE]: CurrencyAmount.fromRawAmount(USDC_ARBITRUM, 10_000e6), [SupportedChainId.OPTIMISM]: CurrencyAmount.fromRawAmount(DAI_OPTIMISM, 10_000e18), @@ -20,11 +21,12 @@ const STABLECOIN_AMOUNT_OUT: { [chainId: number]: CurrencyAmount } = { * @param currency currency to compute the USDC price of */ export default function useUSDCPrice(currency?: Currency): Price | undefined { - const { chainId } = useActiveWeb3React() + const chainId = currency?.chainId const amountOut = chainId ? STABLECOIN_AMOUNT_OUT[chainId] : undefined const stablecoin = amountOut?.currency + // TODO(#2808): remove dependency on useBestV2Trade const v2USDCTrade = useBestV2Trade(TradeType.EXACT_OUTPUT, amountOut, currency, { maxHops: 2, }) @@ -45,7 +47,7 @@ export default function useUSDCPrice(currency?: Currency): Price | undefine } }, [currencyAmount, price]) } + +/** + * + * @param fiatValue string representation of a USD amount + * @returns CurrencyAmount where currency is stablecoin on active chain + */ +export function useStablecoinAmountFromFiatValue(fiatValue: string | null | undefined) { + const { chainId } = useActiveWeb3React() + const stablecoin = chainId ? STABLECOIN_AMOUNT_OUT[chainId]?.currency : undefined + + if (fiatValue === null || fiatValue === undefined || !chainId || !stablecoin) { + return undefined + } + + // trim for decimal precision when parsing + const parsedForDecimals = parseFloat(fiatValue).toFixed(stablecoin.decimals).toString() + + try { + // parse USD string into CurrencyAmount based on stablecoin decimals + return tryParseAmount(parsedForDecimals, stablecoin) + } catch (error) { + return undefined + } +} diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 865eb13b80..4fc9a37374 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -48,7 +48,7 @@ const BodyWrapper = styled.div` z-index: 1; ${({ theme }) => theme.mediaWidth.upToSmall` - padding: 6rem 16px 16px 16px; + padding: 4rem 8px 16px 8px; `}; ` diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 414af255bd..dee46c866d 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -1,23 +1,20 @@ import { Trans } from '@lingui/macro' +import { Trade } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' import { Trade as V2Trade } from '@uniswap/v2-sdk' import { Trade as V3Trade } from '@uniswap/v3-sdk' -import { LoadingOpacityContainer } from 'components/Loader/styled' import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert' -import { AdvancedSwapDetails } from 'components/swap/AdvancedSwapDetails' -import { AutoRouterLogo } from 'components/swap/RouterLabel' -import SwapRoute from 'components/swap/SwapRoute' -import TradePrice from 'components/swap/TradePrice' +import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown' import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter' -import { MouseoverTooltip, MouseoverTooltipContent } from 'components/Tooltip' +import { MouseoverTooltip } from 'components/Tooltip' import JSBI from 'jsbi' import { useCallback, useContext, useEffect, useMemo, useState } from 'react' -import { ArrowDown, CheckCircle, HelpCircle, Info } from 'react-feather' +import { ArrowDown, CheckCircle, HelpCircle } from 'react-feather' import ReactGA from 'react-ga' import { RouteComponentProps } from 'react-router-dom' import { Text } from 'rebass' -import { V3TradeState } from 'state/routing/types' -import styled, { ThemeContext } from 'styled-components/macro' +import { TradeState } from 'state/routing/types' +import { ThemeContext } from 'styled-components/macro' import AddressInputPanel from '../../components/AddressInputPanel' import { ButtonConfirmed, ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button' @@ -26,27 +23,20 @@ import { AutoColumn } from '../../components/Column' import CurrencyInputPanel from '../../components/CurrencyInputPanel' import CurrencyLogo from '../../components/CurrencyLogo' import Loader from '../../components/Loader' -import Row, { AutoRow, RowFixed } from '../../components/Row' +import { AutoRow } from '../../components/Row' import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee' import ConfirmSwapModal from '../../components/swap/ConfirmSwapModal' -import { - ArrowWrapper, - Dots, - ResponsiveTooltipContainer, - SwapCallbackError, - Wrapper, -} from '../../components/swap/styleds' +import { ArrowWrapper, SwapCallbackError, Wrapper } from '../../components/swap/styleds' import SwapHeader from '../../components/swap/SwapHeader' import { SwitchLocaleLink } from '../../components/SwitchLocaleLink' import TokenWarningModal from '../../components/TokenWarningModal' import { useAllTokens, useCurrency } from '../../hooks/Tokens' -import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback' +import { ApprovalState, useApprovalOptimizedTrade, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback' import useENSAddress from '../../hooks/useENSAddress' import { useERC20PermitFromTrade, UseERC20PermitState } from '../../hooks/useERC20Permit' import useIsArgentWallet from '../../hooks/useIsArgentWallet' import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported' import { useSwapCallback } from '../../hooks/useSwapCallback' -import useToggledVersion from '../../hooks/useToggledVersion' import { useUSDCValue } from '../../hooks/useUSDCPrice' import useWrapCallback, { WrapType } from '../../hooks/useWrapCallback' import { useActiveWeb3React } from '../../hooks/web3' @@ -61,21 +51,10 @@ import { import { useExpertModeManager } from '../../state/user/hooks' import { LinkStyledButton, ThemedText } from '../../theme' import { computeFiatValuePriceImpact } from '../../utils/computeFiatValuePriceImpact' -import { getTradeVersion } from '../../utils/getTradeVersion' import { maxAmountSpend } from '../../utils/maxAmountSpend' import { warningSeverity } from '../../utils/prices' import AppBody from '../AppBody' -const StyledInfo = styled(Info)` - height: 16px; - width: 16px; - margin-left: 4px; - color: ${({ theme }) => theme.text3}; - :hover { - color: ${({ theme }) => theme.text1}; - } -` - export default function Swap({ history }: RouteComponentProps) { const { account } = useActiveWeb3React() const loadedUrlParams = useDefaultsFromURLSearch() @@ -113,20 +92,16 @@ export default function Swap({ history }: RouteComponentProps) { // for expert mode const [isExpertMode] = useExpertModeManager() - // get version from the url - const toggledVersion = useToggledVersion() - // swap state const { independentField, typedValue, recipient } = useSwapState() const { - v3Trade: { state: v3TradeState }, - bestTrade: trade, + trade: { state: tradeState, trade }, allowedSlippage, currencyBalances, parsedAmount, currencies, inputError: swapInputError, - } = useDerivedSwapInfo(toggledVersion) + } = useDerivedSwapInfo() const { wrapType, @@ -151,12 +126,8 @@ export default function Swap({ history }: RouteComponentProps) { ) const [routeNotFound, routeIsLoading, routeIsSyncing] = useMemo( - () => [ - trade instanceof V3Trade ? !trade?.swaps : !trade?.route, - V3TradeState.LOADING === v3TradeState, - V3TradeState.SYNCING === v3TradeState, - ], - [trade, v3TradeState] + () => [!trade?.swaps, TradeState.LOADING === tradeState, TradeState.SYNCING === tradeState], + [trade, tradeState] ) const fiatValueInput = useUSDCValue(parsedAmounts[Field.INPUT]) @@ -189,7 +160,7 @@ export default function Swap({ history }: RouteComponentProps) { // modal and loading const [{ showConfirm, tradeToConfirm, swapErrorMessage, attemptingTxn, txHash }, setSwapState] = useState<{ showConfirm: boolean - tradeToConfirm: V2Trade | V3Trade | undefined + tradeToConfirm: Trade | undefined attemptingTxn: boolean swapErrorMessage: string | undefined txHash: string | undefined @@ -215,13 +186,21 @@ export default function Swap({ history }: RouteComponentProps) { currencies[Field.INPUT] && currencies[Field.OUTPUT] && parsedAmounts[independentField]?.greaterThan(JSBI.BigInt(0)) ) + const approvalOptimizedTrade = useApprovalOptimizedTrade(trade, allowedSlippage) + const approvalOptimizedTradeString = + approvalOptimizedTrade instanceof V2Trade + ? 'V2SwapRouter' + : approvalOptimizedTrade instanceof V3Trade + ? 'V3SwapRouter' + : 'SwapRouter' + // check whether the user has approved the router on the input token - const [approvalState, approveCallback] = useApproveCallbackFromTrade(trade, allowedSlippage) + const [approvalState, approveCallback] = useApproveCallbackFromTrade(approvalOptimizedTrade, allowedSlippage) const { state: signatureState, signatureData, gatherPermitSignature, - } = useERC20PermitFromTrade(trade, allowedSlippage) + } = useERC20PermitFromTrade(approvalOptimizedTrade, allowedSlippage) const handleApprove = useCallback(async () => { if (signatureState === UseERC20PermitState.NOT_SIGNED && gatherPermitSignature) { @@ -239,10 +218,16 @@ export default function Swap({ history }: RouteComponentProps) { ReactGA.event({ category: 'Swap', action: 'Approve', - label: [trade?.inputAmount.currency.symbol, toggledVersion].join('/'), + label: [approvalOptimizedTradeString, approvalOptimizedTrade?.inputAmount?.currency.symbol].join('/'), }) } - }, [approveCallback, gatherPermitSignature, signatureState, toggledVersion, trade?.inputAmount.currency.symbol]) + }, [ + signatureState, + gatherPermitSignature, + approveCallback, + approvalOptimizedTradeString, + approvalOptimizedTrade?.inputAmount?.currency.symbol, + ]) // check if user has gone through approval process, used to show two step buttons, reset on token change const [approvalSubmitted, setApprovalSubmitted] = useState(false) @@ -262,7 +247,7 @@ export default function Swap({ history }: RouteComponentProps) { // the callback to execute the swap const { callback: swapCallback, error: swapCallbackError } = useSwapCallback( - trade, + approvalOptimizedTrade, allowedSlippage, recipient, signatureData @@ -288,9 +273,9 @@ export default function Swap({ history }: RouteComponentProps) { ? 'Swap w/o Send + recipient' : 'Swap w/ Send', label: [ - trade?.inputAmount?.currency?.symbol, - trade?.outputAmount?.currency?.symbol, - getTradeVersion(trade), + approvalOptimizedTradeString, + approvalOptimizedTrade?.inputAmount?.currency?.symbol, + approvalOptimizedTrade?.outputAmount?.currency?.symbol, 'MH', ].join('/'), }) @@ -304,7 +289,18 @@ export default function Swap({ history }: RouteComponentProps) { txHash: undefined, }) }) - }, [swapCallback, priceImpact, tradeToConfirm, showConfirm, recipient, recipientAddress, account, trade]) + }, [ + swapCallback, + priceImpact, + tradeToConfirm, + showConfirm, + recipient, + recipientAddress, + account, + approvalOptimizedTradeString, + approvalOptimizedTrade?.inputAmount?.currency?.symbol, + approvalOptimizedTrade?.outputAmount?.currency?.symbol, + ]) // errors const [showInverted, setShowInverted] = useState(false) @@ -454,64 +450,16 @@ export default function Swap({ history }: RouteComponentProps) { ) : null} - - {!showWrap && trade && ( - - - - - - } - placement="bottom" - onOpen={() => - ReactGA.event({ - category: 'Swap', - action: 'Router Tooltip Open', - }) - } - > - - - - {trade instanceof V3Trade && trade.swaps.length > 1 && ( - {trade.swaps.length} routes - )} - - - - - - - - - - - - } - placement="bottom" - onOpen={() => - ReactGA.event({ - category: 'Swap', - action: 'Transaction Details Tooltip Open', - }) - } - > - - - - + {!showWrap && userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing) && ( + )} -
{swapIsUnsupported ? ( @@ -532,15 +480,7 @@ export default function Swap({ history }: RouteComponentProps) { Unwrap ) : null)} - ) : routeIsSyncing || routeIsLoading ? ( - - - - Loading - - - - ) : routeNotFound && userHasSpecifiedInputOutput ? ( + ) : routeNotFound && userHasSpecifiedInputOutput && !routeIsLoading && !routeIsSyncing ? ( Insufficient liquidity for this trade. @@ -613,6 +553,8 @@ export default function Swap({ history }: RouteComponentProps) { id="swap-button" disabled={ !isValid || + routeIsSyncing || + routeIsLoading || (approvalState !== ApprovalState.APPROVED && signatureState !== UseERC20PermitState.SIGNED) || priceImpactTooHigh } @@ -621,7 +563,7 @@ export default function Swap({ history }: RouteComponentProps) { {priceImpactTooHigh ? ( High Price Impact - ) : priceImpactSeverity > 2 ? ( + ) : trade && priceImpactSeverity > 2 ? ( Swap Anyway ) : ( Swap @@ -646,16 +588,18 @@ export default function Swap({ history }: RouteComponentProps) { } }} id="swap-button" - disabled={!isValid || priceImpactTooHigh || !!swapCallbackError} + disabled={!isValid || routeIsSyncing || routeIsLoading || priceImpactTooHigh || !!swapCallbackError} error={isValid && priceImpactSeverity > 2 && !swapCallbackError} > {swapInputError ? ( swapInputError - ) : priceImpactTooHigh ? ( - Price Impact Too High + ) : routeIsSyncing || routeIsLoading ? ( + Swap ) : priceImpactSeverity > 2 ? ( Swap Anyway + ) : priceImpactTooHigh ? ( + Price Impact Too High ) : ( Swap )} diff --git a/src/state/routing/clientSideSmartOrderRouter/constants.ts b/src/state/routing/clientSideSmartOrderRouter/constants.ts new file mode 100644 index 0000000000..444980beba --- /dev/null +++ b/src/state/routing/clientSideSmartOrderRouter/constants.ts @@ -0,0 +1,3 @@ +import { ChainId } from '@uniswap/smart-order-router' + +export const AUTO_ROUTER_SUPPORTED_CHAINS: ChainId[] = Object.values(ChainId) as number[] diff --git a/src/state/routing/clientSideSmartOrderRouter/dependencies.ts b/src/state/routing/clientSideSmartOrderRouter/dependencies.ts new file mode 100644 index 0000000000..839d9b5587 --- /dev/null +++ b/src/state/routing/clientSideSmartOrderRouter/dependencies.ts @@ -0,0 +1,43 @@ +import { AlphaRouterParams, IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router' +import { NETWORK_URLS } from 'connectors' +import { SupportedChainId } from 'constants/chains' +import { providers } from 'ethers/lib/ethers' +import ReactGA from 'react-ga' + +import { AUTO_ROUTER_SUPPORTED_CHAINS } from './constants' + +export type Dependencies = { + [chainId in SupportedChainId]?: AlphaRouterParams +} + +/** Minimal set of dependencies for the router to work locally. */ +export function buildDependencies(): Dependencies { + const dependenciesByChain: Dependencies = {} + for (const chainId of AUTO_ROUTER_SUPPORTED_CHAINS) { + const provider = new providers.JsonRpcProvider(NETWORK_URLS[chainId]) + + dependenciesByChain[chainId] = { + chainId, + provider, + } + } + + 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/state/routing/clientSideSmartOrderRouter/index.ts new file mode 100644 index 0000000000..89c19e724c --- /dev/null +++ b/src/state/routing/clientSideSmartOrderRouter/index.ts @@ -0,0 +1,52 @@ +import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' +import { AlphaRouter, AlphaRouterConfig, ChainId } from '@uniswap/smart-order-router' +import JSBI from 'jsbi' +import { GetQuoteResult } from 'state/routing/types' +import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult' + +import { buildDependencies } from './dependencies' + +const routerParamsByChain = buildDependencies() + +export async function getQuote( + { + type, + chainId, + tokenIn, + tokenOut, + amount: amountRaw, + }: { + type: 'exactIn' | 'exactOut' + chainId: ChainId + tokenIn: { address: string; chainId: number; decimals: number; symbol?: string } + tokenOut: { address: string; chainId: number; decimals: number; symbol?: string } + amount: BigintIsh + }, + alphaRouterConfig: Partial +): Promise<{ data: GetQuoteResult; error?: unknown }> { + const params = routerParamsByChain[chainId] + if (!params) { + throw new Error('Router dependencies not initialized.') + } + + const router = new AlphaRouter(params) + + const currencyIn = new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals, tokenIn.symbol) + const currencyOut = new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals, tokenOut.symbol) + + const baseCurrency = type === 'exactIn' ? currencyIn : currencyOut + const quoteCurrency = type === 'exactIn' ? currencyOut : currencyIn + const amount = CurrencyAmount.fromRawAmount(baseCurrency, JSBI.BigInt(amountRaw)) + + const swapRoute = await router.route( + amount, + quoteCurrency, + type === 'exactIn' ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, + /*swapConfig=*/ undefined, + alphaRouterConfig + ) + + if (!swapRoute) throw new Error('Failed to generate client side quote') + + return { data: transformSwapRouteToGetQuoteResult(type, amount, swapRoute) } +} diff --git a/src/state/routing/slice.ts b/src/state/routing/slice.ts index 73ec57bba8..4a59f7dade 100644 --- a/src/state/routing/slice.ts +++ b/src/state/routing/slice.ts @@ -1,9 +1,65 @@ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' -import { SupportedChainId } from 'constants/chains' +import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react' +import { Protocol } from '@uniswap/router-sdk' +import { ChainId } from '@uniswap/smart-order-router' +import ms from 'ms.macro' import qs from 'qs' import { GetQuoteResult } from './types' +const protocols: Protocol[] = [Protocol.V2, Protocol.V3] + +const DEFAULT_QUERY_PARAMS = { + protocols: protocols.map((p) => p.toLowerCase()).join(','), + // example other params + // forceCrossProtocol: 'true', + // 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({ @@ -14,14 +70,51 @@ export const routingApi = createApi({ GetQuoteResult, { tokenInAddress: string - tokenInChainId: SupportedChainId + tokenInChainId: ChainId + tokenInDecimals: number + tokenInSymbol?: string tokenOutAddress: string - tokenOutChainId: SupportedChainId + tokenOutChainId: ChainId + tokenOutDecimals: number + tokenOutSymbol?: string amount: string + useClientSideRouter: boolean // included in key to invalidate on change type: 'exactIn' | 'exactOut' } >({ - query: (args) => `quote?${qs.stringify({ ...args, protocols: 'v3' })}`, + async queryFn(args, _api, _extraOptions, fetch) { + const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, useClientSideRouter, type } = + args + + let result + + try { + if (useClientSideRouter) { + result = await getClientSideQuote(args) + } else { + const query = qs.stringify({ + ...DEFAULT_QUERY_PARAMS, + tokenInAddress, + tokenInChainId, + tokenOutAddress, + tokenOutChainId, + amount, + type, + }) + result = await fetch(`quote?${query}`) + } + + return { data: result.data as GetQuoteResult } + } catch (e) { + // TODO: fall back to client-side quoter when auto router fails. + // deprecate 'legacy' v2/v3 routers first. + return { error: e as FetchBaseQueryError } + } + }, + keepUnusedDataFor: ms`10s`, + extraOptions: { + maxRetries: 0, + }, }), }), }) diff --git a/src/state/routing/types.ts b/src/state/routing/types.ts index d1f214a4ba..a32f301055 100644 --- a/src/state/routing/types.ts +++ b/src/state/routing/types.ts @@ -1,6 +1,9 @@ -import { Token } from '@uniswap/sdk-core' +import { Trade } from '@uniswap/router-sdk' +import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' +import { Route as V2Route } from '@uniswap/v2-sdk' +import { Route as V3Route } from '@uniswap/v3-sdk' -export enum V3TradeState { +export enum TradeState { LOADING, INVALID, NO_ROUTE_FOUND, @@ -8,11 +11,12 @@ export enum V3TradeState { SYNCING, } +// from https://github.com/Uniswap/routing-api/blob/main/lib/handlers/schema.ts + export type TokenInRoute = Pick -export type PoolInRoute = { +export type V3PoolInRoute = { type: 'v3-pool' - address: string tokenIn: TokenInRoute tokenOut: TokenInRoute sqrtRatioX96: string @@ -21,6 +25,28 @@ export type PoolInRoute = { fee: string amountIn?: string amountOut?: string + + // not used in the interface + address?: string +} + +export type V2Reserve = { + token: TokenInRoute + quotient: string +} + +export type V2PoolInRoute = { + type: 'v2-pool' + tokenIn: TokenInRoute + tokenOut: TokenInRoute + reserve0: V2Reserve + reserve1: V2Reserve + amountIn?: string + amountOut?: string + + // not used in the interface + // avoid returning it from the client-side smart-order-router + address?: string } export interface GetQuoteResult { @@ -38,6 +64,35 @@ export interface GetQuoteResult { quoteDecimals: string quoteGasAdjusted: string quoteGasAdjustedDecimals: string - route: PoolInRoute[][] + route: Array routeString: string } + +export class InterfaceTrade< + TInput extends Currency, + TOutput extends Currency, + TTradeType extends TradeType +> extends Trade { + gasUseEstimateUSD: CurrencyAmount | null | undefined + + constructor({ + gasUseEstimateUSD, + ...routes + }: { + gasUseEstimateUSD?: CurrencyAmount | undefined | null + v2Routes: { + routev2: V2Route + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + }[] + v3Routes: { + routev3: V3Route + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + }[] + tradeType: TTradeType + }) { + super(routes) + this.gasUseEstimateUSD = gasUseEstimateUSD + } +} diff --git a/src/state/routing/useRoutingAPITrade.ts b/src/state/routing/useRoutingAPITrade.ts index 7ed0c2db71..1490c5f5ed 100644 --- a/src/state/routing/useRoutingAPITrade.ts +++ b/src/state/routing/useRoutingAPITrade.ts @@ -1,13 +1,14 @@ import { skipToken } from '@reduxjs/toolkit/query/react' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' -import { Trade } from '@uniswap/v3-sdk' +import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice' import ms from 'ms.macro' import { useMemo } from 'react' import { useBlockNumber } from 'state/application/hooks' import { useGetQuoteQuery } from 'state/routing/slice' +import { useClientSideRouter } from 'state/user/hooks' -import { V3TradeState } from './types' -import { computeRoutes } from './utils' +import { GetQuoteResult, InterfaceTrade, TradeState } from './types' +import { computeRoutes, transformRoutesToTrade } from './utils' function useFreshData(data: T, dataBlockNumber: number, maxBlockAge = 10): T | undefined { const localBlockNumber = useBlockNumber() @@ -35,22 +36,31 @@ function useRoutingAPIArguments({ amount: CurrencyAmount | undefined tradeType: TradeType }) { - if (!tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut)) { - return undefined - } + const [clientSideRouter] = useClientSideRouter() - return { - tokenInAddress: tokenIn.wrapped.address, - tokenInChainId: tokenIn.chainId, - tokenOutAddress: tokenOut.wrapped.address, - tokenOutChainId: tokenOut.chainId, - amount: amount.quotient.toString(), - type: (tradeType === TradeType.EXACT_INPUT ? 'exactIn' : 'exactOut') as 'exactIn' | 'exactOut', - } + 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 v3 trade by invoking the routing api + * 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 * @param amountSpecified the exact amount to swap in/out * @param otherCurrency the desired output/payment currency @@ -59,7 +69,10 @@ export function useRoutingAPITrade( tradeType: TTradeType, amountSpecified?: CurrencyAmount, otherCurrency?: Currency -): { state: V3TradeState; trade: Trade | null } { +): { + state: TradeState + trade: InterfaceTrade | undefined +} { const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo( () => tradeType === TradeType.EXACT_INPUT @@ -76,30 +89,33 @@ export function useRoutingAPITrade( }) const { isLoading, isError, data } = useGetQuoteQuery(queryArgs ?? skipToken, { - pollingInterval: ms`10s`, + pollingInterval: ms`15s`, refetchOnFocus: true, }) - const quoteResult = useFreshData(data, Number(data?.blockNumber) || 0) + const quoteResult: GetQuoteResult | undefined = useFreshData(data, Number(data?.blockNumber) || 0) - const routes = useMemo( - () => computeRoutes(currencyIn, currencyOut, quoteResult), - [currencyIn, currencyOut, quoteResult] + 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: V3TradeState.INVALID, - trade: null, + state: TradeState.INVALID, + trade: undefined, } } if (isLoading && !quoteResult) { // only on first hook render return { - state: V3TradeState.LOADING, - trade: null, + state: TradeState.LOADING, + trade: undefined, } } @@ -112,22 +128,23 @@ export function useRoutingAPITrade( ? CurrencyAmount.fromRawAmount(currencyIn, quoteResult.quote) : undefined - if (isError || !otherAmount || !routes || routes.length === 0 || !queryArgs) { + if (isError || !otherAmount || !route || route.length === 0 || !queryArgs) { return { - state: V3TradeState.NO_ROUTE_FOUND, - trade: null, + state: TradeState.NO_ROUTE_FOUND, + trade: undefined, } } - const trade = Trade.createUncheckedTradeWithMultipleRoutes({ - routes, - tradeType, - }) - - return { - // always return VALID regardless of isFetching status - state: V3TradeState.VALID, - trade, + 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, isLoading, quoteResult, isError, routes, queryArgs, tradeType]) + }, [currencyIn, currencyOut, isLoading, quoteResult, tradeType, isError, route, queryArgs, gasUseEstimateUSD]) } diff --git a/src/state/routing/utils.test.ts b/src/state/routing/utils.test.ts index 6e33a05598..bec5c0c079 100644 --- a/src/state/routing/utils.test.ts +++ b/src/state/routing/utils.test.ts @@ -1,4 +1,4 @@ -import { Ether, Token } from '@uniswap/sdk-core' +import { Ether, Token, TradeType } from '@uniswap/sdk-core' import { computeRoutes } from './utils' @@ -13,21 +13,21 @@ const amount = (raw: TemplateStringsArray) => (parseInt(raw[0]) * 1e6).toString( describe('#useRoute', () => { it('handles an undefined payload', () => { - const result = computeRoutes(undefined, undefined, undefined) + const result = computeRoutes(undefined, undefined, TradeType.EXACT_INPUT, undefined) expect(result).toBeUndefined() }) it('handles empty edges and nodes', () => { - const result = computeRoutes(USDC, DAI, { + const result = computeRoutes(USDC, DAI, TradeType.EXACT_INPUT, { route: [], }) expect(result).toEqual([]) }) - it('handles a single route trade from DAI to USDC', () => { - const result = computeRoutes(DAI, USDC, { + it('handles a single route trade from DAI to USDC from v3', () => { + const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, { route: [ [ { @@ -46,30 +46,73 @@ describe('#useRoute', () => { ], }) + const r = result?.[0] + expect(result).toBeDefined() expect(result?.length).toBe(1) - expect(result && result[0].route.input).toStrictEqual(DAI) - expect(result && result[0].route.output).toStrictEqual(USDC) - expect(result && result[0].route.tokenPath).toStrictEqual([DAI, USDC]) - expect(result && result[0].inputAmount.toSignificant()).toBe('1') - expect(result && result[0].outputAmount.toSignificant()).toBe('5') + expect(r?.routev3?.input).toStrictEqual(DAI) + expect(r?.routev3?.output).toStrictEqual(USDC) + expect(r?.routev3?.tokenPath).toStrictEqual([DAI, USDC]) + expect(r?.routev2).toBeNull() + expect(r?.inputAmount.toSignificant()).toBe('1') + expect(r?.outputAmount.toSignificant()).toBe('5') }) - it('handles a multi-route trade from DAI to USDC', () => { - const result = computeRoutes(DAI, USDC, { + it('handles a single route trade from DAI to USDC from v2', () => { + const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, { route: [ [ { - type: 'v3-pool', + 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] + + expect(result).toBeDefined() + expect(result?.length).toBe(1) + expect(r?.routev2?.input).toStrictEqual(DAI) + expect(r?.routev2?.output).toStrictEqual(USDC) + expect(r?.routev2?.path).toStrictEqual([DAI, USDC]) + expect(r?.routev3).toBeNull() + expect(r?.inputAmount.toSignificant()).toBe('1') + expect(r?.outputAmount.toSignificant()).toBe('5') + }) + + 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`, - fee: '500', tokenIn: DAI, tokenOut: USDC, - sqrtRatioX96: '2437312313659959819381354528', - liquidity: '10272714736694327408', - tickCurrent: '-69633', + reserve0: { + token: DAI, + quotient: amount`1000`, + }, + reserve1: { + token: USDC, + quotient: amount`500`, + }, }, ], [ @@ -104,21 +147,24 @@ describe('#useRoute', () => { expect(result).toBeDefined() expect(result?.length).toBe(2) - expect(result && result[0].route.input).toStrictEqual(DAI) - expect(result && result[0].route.output).toStrictEqual(USDC) - expect(result && result[0].route.tokenPath).toEqual([DAI, USDC]) - expect(result && result[1].route.input).toStrictEqual(DAI) - expect(result && result[1].route.output).toStrictEqual(USDC) - expect(result && result[1].route.tokenPath).toEqual([DAI, MKR, USDC]) + // first route is v2 + expect(result?.[0].routev2?.input).toStrictEqual(DAI) + expect(result?.[0].routev2?.output).toStrictEqual(USDC) + expect(result?.[0].routev2?.path).toEqual([DAI, USDC]) + expect(result?.[0].routev3).toBeNull() - expect(result && result[0].inputAmount.toSignificant()).toBe('5') - expect(result && result[0].outputAmount.toSignificant()).toBe('6') - expect(result && result[1].inputAmount.toSignificant()).toBe('10') - expect(result && result[1].outputAmount.toSignificant()).toBe('200') + // second route is v3 + expect(result?.[1].routev3?.input).toStrictEqual(DAI) + expect(result?.[1].routev3?.output).toStrictEqual(USDC) + expect(result?.[1].routev3?.tokenPath).toEqual([DAI, MKR, USDC]) + expect(result?.[1].routev2).toBeNull() + + expect(result?.[0].outputAmount.toSignificant()).toBe('6') + expect(result?.[1].outputAmount.toSignificant()).toBe('200') }) it('handles a single route trade with same token pair, different fee tiers', () => { - const result = computeRoutes(DAI, USDC, { + const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, { route: [ [ { @@ -153,18 +199,17 @@ describe('#useRoute', () => { expect(result).toBeDefined() expect(result?.length).toBe(2) - expect(result && result[0].route.input).toStrictEqual(DAI) - expect(result && result[0].route.output).toStrictEqual(USDC) - expect(result && result[0].route.tokenPath).toEqual([DAI, USDC]) - expect(result && result[0].inputAmount.toSignificant()).toBe('1') - expect(result && result[0].outputAmount.toSignificant()).toBe('5') + expect(result?.[0].routev3?.input).toStrictEqual(DAI) + expect(result?.[0].routev3?.output).toStrictEqual(USDC) + expect(result?.[0].routev3?.tokenPath).toEqual([DAI, USDC]) + 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, { + const result = computeRoutes(ETH, USDC, TradeType.EXACT_OUTPUT, { route: [ [ { @@ -185,16 +230,15 @@ describe('#useRoute', () => { expect(result).toBeDefined() expect(result?.length).toBe(1) - expect(result && result[0].route.input).toStrictEqual(ETH) - expect(result && result[0].route.output).toStrictEqual(USDC) - expect(result && result[0].route.tokenPath).toStrictEqual([WETH, USDC]) - expect(result && result[0].inputAmount.toSignificant()).toBe('1') + expect(result?.[0].routev3?.input).toStrictEqual(ETH) + expect(result?.[0].routev3?.output).toStrictEqual(USDC) + expect(result?.[0].routev3?.tokenPath).toStrictEqual([WETH, USDC]) expect(result && result[0].outputAmount.toSignificant()).toBe('5') }) it('outputs native ETH as output currency', () => { const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH') - const result = computeRoutes(USDC, ETH, { + const result = computeRoutes(USDC, ETH, TradeType.EXACT_OUTPUT, { route: [ [ { @@ -214,11 +258,76 @@ describe('#useRoute', () => { }) expect(result?.length).toBe(1) - expect(result && result[0].route.input).toStrictEqual(USDC) - expect(result && result[0].route.output).toStrictEqual(ETH) - expect(result && result[0].route.tokenPath).toStrictEqual([USDC, WETH]) - expect(result && result[0].inputAmount.toSignificant()).toBe('5') - expect(result && result[0].outputAmount.toSignificant()).toBe('1') + expect(result?.[0].routev3?.input).toStrictEqual(USDC) + expect(result?.[0].routev3?.output).toStrictEqual(ETH) + expect(result?.[0].routev3?.tokenPath).toStrictEqual([USDC, WETH]) + expect(result?.[0].outputAmount.toSignificant()).toBe('1') + }) + + 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`, + }, + }, + ], + ], + }) + + expect(result).toBeDefined() + expect(result?.length).toBe(1) + expect(result?.[0].routev2?.input).toStrictEqual(ETH) + expect(result?.[0].routev2?.output).toStrictEqual(USDC) + expect(result?.[0].routev2?.path).toStrictEqual([WETH, USDC]) + expect(result && result[0].outputAmount.toSignificant()).toBe('5') + }) + + 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`, + }, + }, + ], + ], + }) + + expect(result?.length).toBe(1) + expect(result?.[0].routev2?.input).toStrictEqual(USDC) + expect(result?.[0].routev2?.output).toStrictEqual(ETH) + expect(result?.[0].routev2?.path).toStrictEqual([USDC, WETH]) + expect(result?.[0].outputAmount.toSignificant()).toBe('1') }) }) }) diff --git a/src/state/routing/utils.ts b/src/state/routing/utils.ts index a43ce06553..df8ced9541 100644 --- a/src/state/routing/utils.ts +++ b/src/state/routing/utils.ts @@ -1,37 +1,38 @@ -import { Currency, CurrencyAmount, Ether, Token } from '@uniswap/sdk-core' -import { FeeAmount, Pool, Route } from '@uniswap/v3-sdk' +import { Currency, CurrencyAmount, Ether, 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 { GetQuoteResult } from './types' +import { GetQuoteResult, InterfaceTrade, V2PoolInRoute, V3PoolInRoute } from './types' /** - * Transforms a Routing API quote into an array of routes that - * can be used to create a V3 `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 -): - | { - route: Route - inputAmount: CurrencyAmount - outputAmount: CurrencyAmount - }[] - | undefined { +) { if (!quoteResult || !quoteResult.route || !currencyIn || !currencyOut) return undefined if (quoteResult.route.length === 0) return [] - const parsedCurrencyIn = currencyIn.isNative - ? Ether.onChain(currencyIn.chainId) - : parseToken(quoteResult.route[0][0].tokenIn) + const parsedTokenIn = parseToken(quoteResult.route[0][0].tokenIn) + const parsedTokenOut = parseToken(quoteResult.route[0][quoteResult.route[0].length - 1].tokenOut) - const parsedCurrencyOut = currencyOut.isNative - ? Ether.onChain(currencyOut.chainId) - : 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 + + const parsedCurrencyIn = currencyIn.isNative ? Ether.onChain(currencyIn.chainId) : parsedTokenIn + + const parsedCurrencyOut = currencyOut.isNative ? Ether.onChain(currencyOut.chainId) : parsedTokenOut try { return quoteResult.route.map((route) => { + if (route.length === 0) { + throw new Error('Expected route to have at least one pair or pool') + } const rawAmountIn = route[0].amountIn const rawAmountOut = route[route.length - 1].amountOut @@ -40,7 +41,8 @@ export function computeRoutes( } return { - route: new Route(route.map(parsePool), parsedCurrencyIn, parsedCurrencyOut), + routev3: isV3Route(route) ? new V3Route(route.map(parsePool), parsedCurrencyIn, parsedCurrencyOut) : null, + routev2: !isV3Route(route) ? new V2Route(route.map(parsePair), parsedCurrencyIn, parsedCurrencyOut) : null, inputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyIn, rawAmountIn), outputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyOut, rawAmountOut), } @@ -49,22 +51,35 @@ export function computeRoutes( // `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 } } +export function transformRoutesToTrade( + route: ReturnType, + tradeType: TTradeType, + 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 })) ?? [], + tradeType, + gasUseEstimateUSD, + }) +} + const parseToken = ({ address, chainId, decimals, symbol }: GetQuoteResult['route'][0][0]['tokenIn']): Token => { return new Token(chainId, address, parseInt(decimals.toString()), symbol) } -const parsePool = ({ - fee, - sqrtRatioX96, - liquidity, - tickCurrent, - tokenIn, - tokenOut, -}: GetQuoteResult['route'][0][0]): Pool => +const parsePool = ({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOut }: V3PoolInRoute): Pool => new Pool( parseToken(tokenIn), parseToken(tokenOut), @@ -73,3 +88,13 @@ const parsePool = ({ liquidity, parseInt(tickCurrent) ) + +const parsePair = ({ reserve0, reserve1 }: V2PoolInRoute): Pair => + new Pair( + CurrencyAmount.fromRawAmount(parseToken(reserve0.token), reserve0.quotient), + CurrencyAmount.fromRawAmount(parseToken(reserve1.token), reserve1.quotient) + ) + +function isV3Route(route: V3PoolInRoute[] | V2PoolInRoute[]): route is V3PoolInRoute[] { + return route[0].type === 'v3-pool' +} diff --git a/src/state/swap/hooks.tsx b/src/state/swap/hooks.tsx index 5465034444..c1961371d1 100644 --- a/src/state/swap/hooks.tsx +++ b/src/state/swap/hooks.tsx @@ -1,23 +1,17 @@ import { parseUnits } from '@ethersproject/units' import { Trans } from '@lingui/macro' import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' -import { Trade as V2Trade } from '@uniswap/v2-sdk' -import { Trade as V3Trade } from '@uniswap/v3-sdk' -import { TWO_PERCENT } from 'constants/misc' -import { useBestV2Trade } from 'hooks/useBestV2Trade' -import { useBestV3Trade } from 'hooks/useBestV3Trade' +import { useBestTrade } from 'hooks/useBestTrade' import JSBI from 'jsbi' import { ParsedQs } from 'qs' import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { useAppDispatch, useAppSelector } from 'state/hooks' -import { V3TradeState } from 'state/routing/types' -import { isTradeBetter } from 'utils/isTradeBetter' +import { InterfaceTrade, TradeState } from 'state/routing/types' import { useCurrency } from '../../hooks/Tokens' import useENS from '../../hooks/useENS' import useParsedQueryString from '../../hooks/useParsedQueryString' import useSwapSlippageTolerance from '../../hooks/useSwapSlippageTolerance' -import { Version } from '../../hooks/useToggledVersion' import { useActiveWeb3React } from '../../hooks/web3' import { isAddress } from '../../utils' import { AppState } from '../index' @@ -98,36 +92,16 @@ const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = { '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D': true, // v2 router 02 } -/** - * Returns true if any of the pairs or tokens in a trade have the given checksummed address - * @param trade to check for the given address - * @param checksummedAddress address to check in the pairs and tokens - */ -function involvesAddress( - trade: V2Trade | V3Trade, - checksummedAddress: string -): boolean { - const path = trade instanceof V2Trade ? trade.route.path : trade.route.tokenPath - return ( - path.some((token) => token.address === checksummedAddress) || - (trade instanceof V2Trade - ? trade.route.pairs.some((pair) => pair.liquidityToken.address === checksummedAddress) - : false) - ) -} - // from the current swap inputs, compute the best trade and return it. -export function useDerivedSwapInfo(toggledVersion: Version | undefined): { +export function useDerivedSwapInfo(): { currencies: { [field in Field]?: Currency | null } currencyBalances: { [field in Field]?: CurrencyAmount } parsedAmount: CurrencyAmount | undefined inputError?: ReactNode - v2Trade: V2Trade | undefined - v3Trade: { - trade: V3Trade | null - state: V3TradeState + trade: { + trade: InterfaceTrade | undefined + state: TradeState } - bestTrade: V2Trade | V3Trade | undefined allowedSlippage: Percent } { const { account } = useActiveWeb3React() @@ -156,34 +130,11 @@ export function useDerivedSwapInfo(toggledVersion: Version | undefined): { [inputCurrency, isExactIn, outputCurrency, typedValue] ) - // get v2 and v3 quotes - // skip if other version is toggled - const v2Trade = useBestV2Trade( + const trade = useBestTrade( isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, - toggledVersion !== Version.v3 ? parsedAmount : undefined, + parsedAmount, (isExactIn ? outputCurrency : inputCurrency) ?? undefined ) - const v3Trade = useBestV3Trade( - isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, - toggledVersion !== Version.v2 ? parsedAmount : undefined, - (isExactIn ? outputCurrency : inputCurrency) ?? undefined - ) - - const isV2TradeBetter = useMemo(() => { - try { - // avoids comparing trades when V3Trade is not in a ready state. - return toggledVersion === Version.v2 || - [V3TradeState.VALID, V3TradeState.SYNCING, V3TradeState.NO_ROUTE_FOUND].includes(v3Trade.state) - ? isTradeBetter(v3Trade.trade, v2Trade, TWO_PERCENT) - : undefined - } catch (e) { - // v3 trade may be debouncing or fetching and have different - // inputs/ouputs than v2 - return undefined - } - }, [toggledVersion, v2Trade, v3Trade.state, v3Trade.trade]) - - const bestTrade = isV2TradeBetter === undefined ? undefined : isV2TradeBetter ? v2Trade : v3Trade.trade const currencyBalances = { [Field.INPUT]: relevantTokenBalances[0], @@ -200,27 +151,27 @@ export function useDerivedSwapInfo(toggledVersion: Version | undefined): { inputError = Connect Wallet } - if (!parsedAmount) { - inputError = inputError ?? Enter an amount - } - if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) { inputError = inputError ?? Select a token } + if (!parsedAmount) { + inputError = inputError ?? Enter an amount + } + const formattedTo = isAddress(to) if (!to || !formattedTo) { inputError = inputError ?? Enter a recipient } else { - if (BAD_RECIPIENT_ADDRESSES[formattedTo] || (v2Trade && involvesAddress(v2Trade, formattedTo))) { + if (BAD_RECIPIENT_ADDRESSES[formattedTo]) { inputError = inputError ?? Invalid recipient } } - const allowedSlippage = useSwapSlippageTolerance(bestTrade ?? undefined) + const allowedSlippage = useSwapSlippageTolerance(trade.trade ?? undefined) // compare input balance to max input based on version - const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], bestTrade?.maximumAmountIn(allowedSlippage)] + const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], trade.trade?.maximumAmountIn(allowedSlippage)] if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) { inputError = Insufficient {amountIn.currency.symbol} balance @@ -231,9 +182,7 @@ export function useDerivedSwapInfo(toggledVersion: Version | undefined): { currencyBalances, parsedAmount, inputError, - v2Trade: v2Trade ?? undefined, - v3Trade, - bestTrade: bestTrade ?? undefined, + trade, allowedSlippage, } } diff --git a/src/state/user/hooks.tsx b/src/state/user/hooks.tsx index 3548e24382..6d14d906a1 100644 --- a/src/state/user/hooks.tsx +++ b/src/state/user/hooks.tsx @@ -1,6 +1,6 @@ import { Percent, Token } from '@uniswap/sdk-core' import { computePairAddress, Pair } from '@uniswap/v2-sdk' -import { L2_CHAIN_IDS, SupportedChainId } from 'constants/chains' +import { L2_CHAIN_IDS } from 'constants/chains' import { SupportedLocale } from 'constants/locales' import { L2_DEADLINE_FROM_NOW } from 'constants/misc' import JSBI from 'jsbi' @@ -121,13 +121,6 @@ export function useClientSideRouter(): [boolean, (userClientSideRouter: boolean) return [clientSideRouter, setClientSideRouter] } -export function useRoutingAPIEnabled(): boolean { - const { chainId } = useActiveWeb3React() - const [clientSideRouter] = useClientSideRouter() - - return chainId === SupportedChainId.MAINNET && !clientSideRouter -} - export function useSetUserSlippageTolerance(): (slippageTolerance: Percent | 'auto') => void { const dispatch = useAppDispatch() diff --git a/src/theme/components.tsx b/src/theme/components.tsx index 6b752ffe62..95ed3fe8c0 100644 --- a/src/theme/components.tsx +++ b/src/theme/components.tsx @@ -261,3 +261,9 @@ export const SmallOnly = styled.span` display: block; `}; ` + +export const Separator = styled.div` + width: 100%; + height: 1px; + background-color: ${({ theme }) => theme.bg2}; +` diff --git a/src/types/tuple.ts b/src/types/tuple.ts index ecf2686f64..6091a770c5 100644 --- a/src/types/tuple.ts +++ b/src/types/tuple.ts @@ -10,3 +10,5 @@ export type TupleSplit = TupleSplit[0] export type SkipFirst = TupleSplit[1] + +export type NonNullable = T extends null | undefined ? never : T diff --git a/src/utils/getTradeVersion.ts b/src/utils/getTradeVersion.ts deleted file mode 100644 index 00e7ac2b7f..0000000000 --- a/src/utils/getTradeVersion.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Currency, TradeType } from '@uniswap/sdk-core' -import { Trade as V2Trade } from '@uniswap/v2-sdk' -import { Trade as V3Trade } from '@uniswap/v3-sdk' - -import { Version } from '../hooks/useToggledVersion' - -export function getTradeVersion( - trade?: V2Trade | V3Trade -): Version | undefined { - if (!trade) return undefined - if (trade instanceof V2Trade) return Version.v2 - return Version.v3 -} diff --git a/src/utils/getTxOptimizedSwapRouter.test.ts b/src/utils/getTxOptimizedSwapRouter.test.ts new file mode 100644 index 0000000000..3491a27eac --- /dev/null +++ b/src/utils/getTxOptimizedSwapRouter.test.ts @@ -0,0 +1,102 @@ +import { ApprovalState } from 'hooks/useApproveCallback' + +import { getTxOptimizedSwapRouter, SwapRouterVersion } from './getTxOptimizedSwapRouter' + +const getApprovalState = (approved: SwapRouterVersion[]) => ({ + v2: approved.includes(SwapRouterVersion.V2) ? ApprovalState.APPROVED : ApprovalState.NOT_APPROVED, + v3: approved.includes(SwapRouterVersion.V3) ? ApprovalState.APPROVED : ApprovalState.NOT_APPROVED, + v2V3: approved.includes(SwapRouterVersion.V2V3) ? ApprovalState.APPROVED : ApprovalState.NOT_APPROVED, +}) + +describe(getTxOptimizedSwapRouter, () => { + it('always selects v2v3 when approved', () => { + expect( + getTxOptimizedSwapRouter({ + onlyV2Routes: true, + onlyV3Routes: false, + tradeHasSplits: false, + approvalStates: getApprovalState([SwapRouterVersion.V2V3]), + }) + ).toEqual(SwapRouterVersion.V2V3) + expect( + getTxOptimizedSwapRouter({ + onlyV2Routes: false, + onlyV3Routes: true, + tradeHasSplits: false, + approvalStates: getApprovalState([SwapRouterVersion.V2V3]), + }) + ).toEqual(SwapRouterVersion.V2V3) + expect( + getTxOptimizedSwapRouter({ + onlyV2Routes: false, + onlyV3Routes: true, + tradeHasSplits: false, + approvalStates: getApprovalState([SwapRouterVersion.V2, SwapRouterVersion.V3, SwapRouterVersion.V2V3]), + }) + ).toEqual(SwapRouterVersion.V2V3) + }) + + it('selects the right router when only v2 routes', () => { + const base = { onlyV3Routes: false } + + // selects v2 + expect( + getTxOptimizedSwapRouter({ + ...base, + onlyV2Routes: true, + tradeHasSplits: false, + approvalStates: getApprovalState([SwapRouterVersion.V2, SwapRouterVersion.V3]), + }) + ).toEqual(SwapRouterVersion.V2) + + // selects v2V3 + expect( + getTxOptimizedSwapRouter({ + ...base, + onlyV2Routes: true, + tradeHasSplits: true, + approvalStates: getApprovalState([SwapRouterVersion.V2]), + }) + ).toEqual(SwapRouterVersion.V2V3) + expect( + getTxOptimizedSwapRouter({ + ...base, + onlyV2Routes: true, + tradeHasSplits: true, + approvalStates: getApprovalState([SwapRouterVersion.V2, SwapRouterVersion.V2V3]), + }) + ).toEqual(SwapRouterVersion.V2V3) + }) + + it('selects the right router when only v3 routes', () => { + const base = { onlyV2Routes: false } + + // select v3 + expect( + getTxOptimizedSwapRouter({ + ...base, + onlyV3Routes: true, + tradeHasSplits: false, + approvalStates: getApprovalState([SwapRouterVersion.V2, SwapRouterVersion.V3]), + }) + ).toEqual(SwapRouterVersion.V3) + expect( + getTxOptimizedSwapRouter({ + ...base, + onlyV3Routes: true, + tradeHasSplits: true, + approvalStates: getApprovalState([SwapRouterVersion.V3]), + }) + ).toEqual(SwapRouterVersion.V3) + + // selects v2V3 + expect( + getTxOptimizedSwapRouter({ + ...base, + onlyV3Routes: true, + tradeHasSplits: true, + approvalStates: getApprovalState([SwapRouterVersion.V2, SwapRouterVersion.V2V3]), + }) + ).toEqual(SwapRouterVersion.V2V3) + }) +}) diff --git a/src/utils/getTxOptimizedSwapRouter.ts b/src/utils/getTxOptimizedSwapRouter.ts new file mode 100644 index 0000000000..211c4494cb --- /dev/null +++ b/src/utils/getTxOptimizedSwapRouter.ts @@ -0,0 +1,32 @@ +import { ApprovalState } from 'hooks/useApproveCallback' + +export enum SwapRouterVersion { + V2, + V3, + V2V3, +} + +/** + * Returns the swap router that will result in the least amount of txs (less gas) for a given swap. + * Heuristic: + * - if trade contains a single v2-only trade & V2 SwapRouter is approved: use V2 SwapRouter + * - if trade contains only v3 & V3 SwapRouter is approved: use V3 SwapRouter + * - else: approve and use V2+V3 SwapRouter + */ +export function getTxOptimizedSwapRouter({ + onlyV2Routes, + onlyV3Routes, + tradeHasSplits, + approvalStates, +}: { + onlyV2Routes: boolean | undefined + onlyV3Routes: boolean | undefined + tradeHasSplits: boolean | undefined + approvalStates: { v2: ApprovalState; v3: ApprovalState; v2V3: ApprovalState } +}): SwapRouterVersion | undefined { + if ([approvalStates.v2, approvalStates.v3, approvalStates.v2V3].includes(ApprovalState.PENDING)) return undefined + if (approvalStates.v2V3 === ApprovalState.APPROVED) return SwapRouterVersion.V2V3 + if (approvalStates.v2 === ApprovalState.APPROVED && onlyV2Routes && !tradeHasSplits) return SwapRouterVersion.V2 + if (approvalStates.v3 === ApprovalState.APPROVED && onlyV3Routes) return SwapRouterVersion.V3 + return SwapRouterVersion.V2V3 +} diff --git a/src/utils/isTradeBetter.ts b/src/utils/isTradeBetter.ts index b2137abde6..f4be6e9e52 100644 --- a/src/utils/isTradeBetter.ts +++ b/src/utils/isTradeBetter.ts @@ -1,13 +1,13 @@ import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Trade as V2Trade } from '@uniswap/v2-sdk' -import { Trade as V3Trade } from '@uniswap/v3-sdk' import { ONE_HUNDRED_PERCENT, ZERO_PERCENT } from '../constants/misc' // returns whether tradeB is better than tradeA by at least a threshold percentage amount +// only used by v2 hooks export function isTradeBetter( - tradeA: V2Trade | V3Trade | undefined | null, - tradeB: V2Trade | V3Trade | undefined | null, + tradeA: V2Trade | undefined | null, + tradeB: V2Trade | undefined | null, minimumDelta: Percent = ZERO_PERCENT ): boolean | undefined { if (tradeA && !tradeB) return false diff --git a/src/utils/prices.test.ts b/src/utils/prices.test.ts index 7f721f75ee..538f041260 100644 --- a/src/utils/prices.test.ts +++ b/src/utils/prices.test.ts @@ -1,50 +1,120 @@ +import { Trade } from '@uniswap/router-sdk' import { CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' -import { Pair, Route, Trade } from '@uniswap/v2-sdk' +import { Pair, Route as V2Route } from '@uniswap/v2-sdk' +import { FeeAmount, Pool, Route as V3Route } from '@uniswap/v3-sdk' import JSBI from 'jsbi' import { computeRealizedLPFeeAmount, warningSeverity } from './prices' +const token1 = new Token(1, '0x0000000000000000000000000000000000000001', 18) +const token2 = new Token(1, '0x0000000000000000000000000000000000000002', 18) +const token3 = new Token(1, '0x0000000000000000000000000000000000000003', 18) + +const pair12 = new Pair( + CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(10000)), + CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(20000)) +) +const pair23 = new Pair( + CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(20000)), + CurrencyAmount.fromRawAmount(token3, JSBI.BigInt(30000)) +) + +const pool12 = new Pool(token1, token2, FeeAmount.HIGH, '2437312313659959819381354528', '10272714736694327408', -69633) +const pool13 = new Pool( + token1, + token3, + FeeAmount.MEDIUM, + '2437312313659959819381354528', + '10272714736694327408', + -69633 +) + +const currencyAmount = (token: Token, amount: number) => CurrencyAmount.fromRawAmount(token, JSBI.BigInt(amount)) + describe('prices', () => { - const token1 = new Token(1, '0x0000000000000000000000000000000000000001', 18) - const token2 = new Token(1, '0x0000000000000000000000000000000000000002', 18) - const token3 = new Token(1, '0x0000000000000000000000000000000000000003', 18) - - const pair12 = new Pair( - CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(10000)), - CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(20000)) - ) - const pair23 = new Pair( - CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(20000)), - CurrencyAmount.fromRawAmount(token3, JSBI.BigInt(30000)) - ) - describe('#computeRealizedLPFeeAmount', () => { it('returns undefined for undefined', () => { expect(computeRealizedLPFeeAmount(undefined)).toEqual(undefined) }) - it('correct realized lp fee for single hop', () => { + it('correct realized lp fee for single hop on v2', () => { + // v2 expect( computeRealizedLPFeeAmount( - new Trade( - new Route([pair12], token1, token2), - CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(1000)), - TradeType.EXACT_INPUT - ) + new Trade({ + v2Routes: [ + { + routev2: new V2Route([pair12], token1, token2), + inputAmount: currencyAmount(token1, 1000), + outputAmount: currencyAmount(token2, 1000), + }, + ], + v3Routes: [], + tradeType: TradeType.EXACT_INPUT, + }) ) - ).toEqual(CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(3))) + ).toEqual(currencyAmount(token1, 3)) // 3% realized fee + }) + + it('correct realized lp fee for single hop on v3', () => { + // v3 + expect( + computeRealizedLPFeeAmount( + new Trade({ + v3Routes: [ + { + routev3: new V3Route([pool12], token1, token2), + inputAmount: currencyAmount(token1, 1000), + outputAmount: currencyAmount(token2, 1000), + }, + ], + v2Routes: [], + tradeType: TradeType.EXACT_INPUT, + }) + ) + ).toEqual(currencyAmount(token1, 10)) // 3% realized fee }) it('correct realized lp fee for double hop', () => { expect( computeRealizedLPFeeAmount( - new Trade( - new Route([pair12, pair23], token1, token3), - CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(1000)), - TradeType.EXACT_INPUT - ) + new Trade({ + v2Routes: [ + { + routev2: new V2Route([pair12, pair23], token1, token3), + inputAmount: currencyAmount(token1, 1000), + outputAmount: currencyAmount(token3, 1000), + }, + ], + v3Routes: [], + tradeType: TradeType.EXACT_INPUT, + }) ) - ).toEqual(CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(5))) + ).toEqual(currencyAmount(token1, 5)) + }) + + it('correct realized lp fee for multi route v2+v3', () => { + expect( + computeRealizedLPFeeAmount( + new Trade({ + v2Routes: [ + { + routev2: new V2Route([pair12, pair23], token1, token3), + inputAmount: currencyAmount(token1, 1000), + outputAmount: currencyAmount(token3, 1000), + }, + ], + v3Routes: [ + { + routev3: new V3Route([pool13], token1, token3), + inputAmount: currencyAmount(token1, 1000), + outputAmount: currencyAmount(token3, 1000), + }, + ], + tradeType: TradeType.EXACT_INPUT, + }) + ) + ).toEqual(currencyAmount(token1, 8)) }) }) diff --git a/src/utils/prices.ts b/src/utils/prices.ts index b16f54e9f9..3d68171443 100644 --- a/src/utils/prices.ts +++ b/src/utils/prices.ts @@ -1,6 +1,7 @@ +import { Trade } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, Fraction, Percent, TradeType } from '@uniswap/sdk-core' -import { Trade as V2Trade } from '@uniswap/v2-sdk' -import { Trade as V3Trade } from '@uniswap/v3-sdk' +import { Pair } from '@uniswap/v2-sdk' +import { FeeAmount } from '@uniswap/v3-sdk' import JSBI from 'jsbi' import { @@ -8,29 +9,28 @@ import { ALLOWED_PRICE_IMPACT_LOW, ALLOWED_PRICE_IMPACT_MEDIUM, BLOCKED_PRICE_IMPACT_NON_EXPERT, + ONE_HUNDRED_PERCENT, ZERO_PERCENT, } from '../constants/misc' const THIRTY_BIPS_FEE = new Percent(JSBI.BigInt(30), JSBI.BigInt(10000)) -const ONE_HUNDRED_PERCENT = new Percent(JSBI.BigInt(10000), JSBI.BigInt(10000)) const INPUT_FRACTION_AFTER_FEE = ONE_HUNDRED_PERCENT.subtract(THIRTY_BIPS_FEE) // computes realized lp fee as a percent -export function computeRealizedLPFeePercent( - trade: V2Trade | V3Trade -): Percent { +export function computeRealizedLPFeePercent(trade: Trade): Percent { let percent: Percent - if (trade instanceof V2Trade) { + + // Since routes are either all v2 or all v3 right now, calculate separately + if (trade.swaps[0].route.pools instanceof Pair) { // for each hop in our trade, take away the x*y=k price impact from 0.3% fees // e.g. for 3 tokens/2 hops: 1 - ((1 - .03) * (1-.03)) percent = ONE_HUNDRED_PERCENT.subtract( - trade.route.pairs.reduce( + trade.swaps.reduce( (currentFee: Percent): Percent => currentFee.multiply(INPUT_FRACTION_AFTER_FEE), ONE_HUNDRED_PERCENT ) ) } else { - //TODO(judo): validate this percent = ZERO_PERCENT for (const swap of trade.swaps) { const { numerator, denominator } = swap.inputAmount.divide(trade.inputAmount) @@ -38,11 +38,14 @@ export function computeRealizedLPFeePercent( const routeRealizedLPFeePercent = overallPercent.multiply( ONE_HUNDRED_PERCENT.subtract( - swap.route.pools.reduce( - (currentFee: Percent, pool): Percent => - currentFee.multiply(ONE_HUNDRED_PERCENT.subtract(new Fraction(pool.fee, 1_000_000))), - ONE_HUNDRED_PERCENT - ) + swap.route.pools.reduce((currentFee: Percent, pool): Percent => { + const fee = + pool instanceof Pair + ? // not currently possible given protocol check above, but not fatal + FeeAmount.MEDIUM + : pool.fee + return currentFee.multiply(ONE_HUNDRED_PERCENT.subtract(new Fraction(fee, 1_000_000))) + }, ONE_HUNDRED_PERCENT) ) ) @@ -55,7 +58,7 @@ export function computeRealizedLPFeePercent( // computes price breakdown for the trade export function computeRealizedLPFeeAmount( - trade?: V2Trade | V3Trade | null + trade?: Trade | null ): CurrencyAmount | undefined { if (trade) { const realizedLPFee = computeRealizedLPFeePercent(trade) diff --git a/src/utils/transformSwapRouteToGetQuoteResult.ts b/src/utils/transformSwapRouteToGetQuoteResult.ts new file mode 100644 index 0000000000..e1a7fe64bb --- /dev/null +++ b/src/utils/transformSwapRouteToGetQuoteResult.ts @@ -0,0 +1,150 @@ +import { Protocol } from '@uniswap/router-sdk' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { routeAmountsToString, SwapRoute } from '@uniswap/smart-order-router' +import { GetQuoteResult, 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', + amount: CurrencyAmount, + { + quote, + quoteGasAdjusted, + route, + estimatedGasUsed, + estimatedGasUsedQuoteToken, + estimatedGasUsedUSD, + gasPriceWei, + methodParameters, + blockNumber, + }: SwapRoute +): GetQuoteResult { + const routeResponse: Array = [] + + for (const subRoute of route) { + const { amount, quote, tokenPath } = subRoute + + if (subRoute.protocol === Protocol.V3) { + const pools = subRoute.route.pools + const curRoute: V3PoolInRoute[] = [] + for (let i = 0; i < pools.length; i++) { + const nextPool = pools[i] + const tokenIn = tokenPath[i] + const tokenOut = tokenPath[i + 1] + + let edgeAmountIn = undefined + if (i === 0) { + edgeAmountIn = type === 'exactIn' ? amount.quotient.toString() : quote.quotient.toString() + } + + let edgeAmountOut = undefined + if (i === pools.length - 1) { + edgeAmountOut = type === 'exactIn' ? quote.quotient.toString() : amount.quotient.toString() + } + + curRoute.push({ + type: 'v3-pool', + tokenIn: { + chainId: tokenIn.chainId, + decimals: tokenIn.decimals, + address: tokenIn.address, + symbol: tokenIn.symbol, + }, + tokenOut: { + chainId: tokenOut.chainId, + decimals: tokenOut.decimals, + address: tokenOut.address, + symbol: tokenOut.symbol, + }, + fee: nextPool.fee.toString(), + liquidity: nextPool.liquidity.toString(), + sqrtRatioX96: nextPool.sqrtRatioX96.toString(), + tickCurrent: nextPool.tickCurrent.toString(), + amountIn: edgeAmountIn, + amountOut: edgeAmountOut, + }) + } + + routeResponse.push(curRoute) + } else if (subRoute.protocol === Protocol.V2) { + const pools = subRoute.route.pairs + const curRoute: V2PoolInRoute[] = [] + for (let i = 0; i < pools.length; i++) { + const nextPool = pools[i] + const tokenIn = tokenPath[i] + const tokenOut = tokenPath[i + 1] + + let edgeAmountIn = undefined + if (i === 0) { + edgeAmountIn = type === 'exactIn' ? amount.quotient.toString() : quote.quotient.toString() + } + + let edgeAmountOut = undefined + if (i === pools.length - 1) { + edgeAmountOut = type === 'exactIn' ? quote.quotient.toString() : amount.quotient.toString() + } + + const reserve0 = nextPool.reserve0 + const reserve1 = nextPool.reserve1 + + curRoute.push({ + type: 'v2-pool', + tokenIn: { + chainId: tokenIn.chainId, + decimals: tokenIn.decimals, + address: tokenIn.address, + symbol: tokenIn.symbol, + }, + tokenOut: { + chainId: tokenOut.chainId, + decimals: tokenOut.decimals, + address: tokenOut.address, + symbol: tokenOut.symbol, + }, + reserve0: { + token: { + chainId: reserve0.currency.wrapped.chainId, + decimals: reserve0.currency.wrapped.decimals, + address: reserve0.currency.wrapped.address, + symbol: reserve0.currency.wrapped.symbol, + }, + quotient: reserve0.quotient.toString(), + }, + reserve1: { + token: { + chainId: reserve1.currency.wrapped.chainId, + decimals: reserve1.currency.wrapped.decimals, + address: reserve1.currency.wrapped.address, + symbol: reserve1.currency.wrapped.symbol, + }, + quotient: reserve1.quotient.toString(), + }, + amountIn: edgeAmountIn, + amountOut: edgeAmountOut, + }) + } + + routeResponse.push(curRoute) + } + } + + const result: GetQuoteResult = { + methodParameters, + blockNumber: blockNumber.toString(), + amount: amount.quotient.toString(), + amountDecimals: amount.toExact(), + quote: quote.quotient.toString(), + quoteDecimals: quote.toExact(), + quoteGasAdjusted: quoteGasAdjusted.quotient.toString(), + quoteGasAdjustedDecimals: quoteGasAdjusted.toExact(), + gasUseEstimateQuote: estimatedGasUsedQuoteToken.quotient.toString(), + gasUseEstimateQuoteDecimals: estimatedGasUsedQuoteToken.toExact(), + gasUseEstimate: estimatedGasUsed.toString(), + gasUseEstimateUSD: estimatedGasUsedUSD.toExact(), + gasPriceWei: gasPriceWei.toString(), + route: routeResponse, + routeString: routeAmountsToString(route), + } + + return result +} diff --git a/yarn.lock b/yarn.lock index 8a81dc40d6..c738441149 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1278,6 +1278,11 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@bitauth/libauth@^1.17.1": + version "1.18.1" + resolved "https://registry.yarnpkg.com/@bitauth/libauth/-/libauth-1.18.1.tgz#b1c632ed85f73c16a0ff89d81e8a92809fa15108" + integrity sha512-s7evdGbdGAnGkv7xt6mCbcWTTNvburc1Z9EX/8JKwcRLqofjDs7VAEz+RP3a8OGEo4MWFV6Ydqu/BeJjIA7Kdg== + "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" @@ -1476,6 +1481,21 @@ "@ethersproject/properties" "^5.4.0" "@ethersproject/strings" "^5.4.0" +"@ethersproject/abi@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.5.0.tgz#fb52820e22e50b854ff15ce1647cc508d6660613" + integrity sha512-loW7I4AohP5KycATvc0MgujU6JyCHPqHdeoo9z3Nr9xEiNioxa65ccdm1+fsoJhkuhdRtfcL8cfyGamz2AxZ5w== + dependencies: + "@ethersproject/address" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/constants" "^5.5.0" + "@ethersproject/hash" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + "@ethersproject/abstract-provider@5.4.1", "@ethersproject/abstract-provider@^5.4.0": version "5.4.1" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.4.1.tgz#e404309a29f771bd4d28dbafadcaa184668c2a6e" @@ -1489,6 +1509,19 @@ "@ethersproject/transactions" "^5.4.0" "@ethersproject/web" "^5.4.0" +"@ethersproject/abstract-provider@^5.5.0": + version "5.5.1" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.5.1.tgz#2f1f6e8a3ab7d378d8ad0b5718460f85649710c5" + integrity sha512-m+MA/ful6eKbxpr99xUYeRvLkfnlqzrF8SZ46d/xFB1A7ZVknYc/sXJG0RcufF52Qn2jeFj1hhcoQ7IXjNKUqg== + dependencies: + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/networks" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/transactions" "^5.5.0" + "@ethersproject/web" "^5.5.0" + "@ethersproject/abstract-signer@5.4.1", "@ethersproject/abstract-signer@^5.4.0": version "5.4.1" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.4.1.tgz#e4e9abcf4dd4f1ba0db7dff9746a5f78f355ea81" @@ -1500,6 +1533,17 @@ "@ethersproject/logger" "^5.4.0" "@ethersproject/properties" "^5.4.0" +"@ethersproject/abstract-signer@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.5.0.tgz#590ff6693370c60ae376bf1c7ada59eb2a8dd08d" + integrity sha512-lj//7r250MXVLKI7sVarXAbZXbv9P50lgmJQGr2/is82EwEb8r7HrxsmMqAjTsztMYy7ohrIhGMIml+Gx4D3mA== + dependencies: + "@ethersproject/abstract-provider" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/address@5.4.0", "@ethersproject/address@^5.0.0", "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.4.0.tgz#ba2d00a0f8c4c0854933b963b9a3a9f6eb4a37a3" @@ -1511,6 +1555,17 @@ "@ethersproject/logger" "^5.4.0" "@ethersproject/rlp" "^5.4.0" +"@ethersproject/address@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.5.0.tgz#bcc6f576a553f21f3dd7ba17248f81b473c9c78f" + integrity sha512-l4Nj0eWlTUh6ro5IbPTgbpT4wRbdH5l8CQf7icF7sb/SI3Nhd9Y9HzhonTSTi6CefI0necIw7LJqQPopPLZyWw== + dependencies: + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/rlp" "^5.5.0" + "@ethersproject/base64@5.4.0", "@ethersproject/base64@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.4.0.tgz#7252bf65295954c9048c7ca5f43e5c86441b2a9a" @@ -1518,6 +1573,13 @@ dependencies: "@ethersproject/bytes" "^5.4.0" +"@ethersproject/base64@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.5.0.tgz#881e8544e47ed976930836986e5eb8fab259c090" + integrity sha512-tdayUKhU1ljrlHzEWbStXazDpsx4eg1dBXUSI6+mHlYklOXoXF6lZvw8tnD6oVaWfnMxAgRSKROg3cVKtCcppA== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/basex@5.4.0", "@ethersproject/basex@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.4.0.tgz#0a2da0f4e76c504a94f2b21d3161ed9438c7f8a6" @@ -1535,6 +1597,24 @@ "@ethersproject/logger" "^5.4.0" bn.js "^4.11.9" +"@ethersproject/bignumber@5.4.2", "@ethersproject/bignumber@^5.1.1": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.4.2.tgz#44232e015ae4ce82ac034de549eb3583c71283d8" + integrity sha512-oIBDhsKy5bs7j36JlaTzFgNPaZjiNDOXsdSgSpXRucUl+UA6L/1YLlFeI3cPAoodcenzF4nxNPV13pcy7XbWjA== + dependencies: + "@ethersproject/bytes" "^5.4.0" + "@ethersproject/logger" "^5.4.0" + bn.js "^4.11.9" + +"@ethersproject/bignumber@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.5.0.tgz#875b143f04a216f4f8b96245bde942d42d279527" + integrity sha512-6Xytlwvy6Rn3U3gKEc1vP7nR92frHkv6wtVr95LFR3jREXiCPzdWxKQ1cx4JGQBXxcguAwjA8murlYN2TSiEbg== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + bn.js "^4.11.9" + "@ethersproject/bytes@5.4.0", "@ethersproject/bytes@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.4.0.tgz#56fa32ce3bf67153756dbaefda921d1d4774404e" @@ -1542,6 +1622,13 @@ dependencies: "@ethersproject/logger" "^5.4.0" +"@ethersproject/bytes@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.5.0.tgz#cb11c526de657e7b45d2e0f0246fb3b9d29a601c" + integrity sha512-ABvc7BHWhZU9PNM/tANm/Qx4ostPGadAuQzWTr3doklZOhDlmcBqclrQe/ZXUIj3K8wC28oYeuRa+A37tX9kog== + dependencies: + "@ethersproject/logger" "^5.5.0" + "@ethersproject/constants@5.4.0", "@ethersproject/constants@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.4.0.tgz#ee0bdcb30bf1b532d2353c977bf2ef1ee117958a" @@ -1549,6 +1636,13 @@ dependencies: "@ethersproject/bignumber" "^5.4.0" +"@ethersproject/constants@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.5.0.tgz#d2a2cd7d94bd1d58377d1d66c4f53c9be4d0a45e" + integrity sha512-2MsRRVChkvMWR+GyMGY4N1sAX9Mt3J9KykCsgUFd/1mwS0UH1qw+Bv9k1UJb3X3YJYFco9H20pjSlOIfCG5HYQ== + dependencies: + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/contracts@5.4.1": version "5.4.1" resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.4.1.tgz#3eb4f35b7fe60a962a75804ada2746494df3e470" @@ -1588,6 +1682,20 @@ "@ethersproject/properties" "^5.4.0" "@ethersproject/strings" "^5.4.0" +"@ethersproject/hash@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.5.0.tgz#7cee76d08f88d1873574c849e0207dcb32380cc9" + integrity sha512-dnGVpK1WtBjmnp3mUT0PlU2MpapnwWI0PibldQEq1408tQBAbZpPidkWoVVuNMOl/lISO3+4hXZWCL3YV7qzfg== + dependencies: + "@ethersproject/abstract-signer" "^5.5.0" + "@ethersproject/address" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + "@ethersproject/hdnode@5.4.0", "@ethersproject/hdnode@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.4.0.tgz#4bc9999b9a12eb5ce80c5faa83114a57e4107cac" @@ -1633,11 +1741,24 @@ "@ethersproject/bytes" "^5.4.0" js-sha3 "0.5.7" +"@ethersproject/keccak256@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.5.0.tgz#e4b1f9d7701da87c564ffe336f86dcee82983492" + integrity sha512-5VoFCTjo2rYbBe1l2f4mccaRFN/4VQEYFwwn04aJV2h7qf4ZvI2wFxUE1XOX+snbwCLRzIeikOqtAoPwMza9kg== + dependencies: + "@ethersproject/bytes" "^5.5.0" + js-sha3 "0.8.0" + "@ethersproject/logger@5.4.1", "@ethersproject/logger@^5.4.0": version "5.4.1" resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.4.1.tgz#503bd33683538b923c578c07d1c2c0dd18672054" integrity sha512-DZ+bRinnYLPw1yAC64oRl0QyVZj43QeHIhVKfD/+YwSz4wsv1pfwb5SOFjz+r710YEWzU6LrhuSjpSO+6PeE4A== +"@ethersproject/logger@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.5.0.tgz#0c2caebeff98e10aefa5aef27d7441c7fd18cf5d" + integrity sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg== + "@ethersproject/networks@5.4.2", "@ethersproject/networks@^5.4.0": version "5.4.2" resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.4.2.tgz#2247d977626e97e2c3b8ee73cd2457babde0ce35" @@ -1645,6 +1766,13 @@ dependencies: "@ethersproject/logger" "^5.4.0" +"@ethersproject/networks@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.5.0.tgz#babec47cab892c51f8dd652ce7f2e3e14283981a" + integrity sha512-KWfP3xOnJeF89Uf/FCJdV1a2aDJe5XTN2N52p4fcQ34QhDqQFkgQKZ39VGtiqUgHcLI8DfT0l9azC3KFTunqtA== + dependencies: + "@ethersproject/logger" "^5.5.0" + "@ethersproject/pbkdf2@5.4.0", "@ethersproject/pbkdf2@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.4.0.tgz#ed88782a67fda1594c22d60d0ca911a9d669641c" @@ -1660,6 +1788,13 @@ dependencies: "@ethersproject/logger" "^5.4.0" +"@ethersproject/properties@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.5.0.tgz#61f00f2bb83376d2071baab02245f92070c59995" + integrity sha512-l3zRQg3JkD8EL3CPjNK5g7kMx4qSwiR60/uk5IVjd3oq1MZR5qUg40CNOoEJoX5wc3DyY5bt9EbMk86C7x0DNA== + dependencies: + "@ethersproject/logger" "^5.5.0" + "@ethersproject/providers@5.4.5": version "5.4.5" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.4.5.tgz#eb2ea2a743a8115f79604a8157233a3a2c832928" @@ -1701,6 +1836,14 @@ "@ethersproject/bytes" "^5.4.0" "@ethersproject/logger" "^5.4.0" +"@ethersproject/rlp@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.5.0.tgz#530f4f608f9ca9d4f89c24ab95db58ab56ab99a0" + integrity sha512-hLv8XaQ8PTI9g2RHoQGf/WSxBfTB/NudRacbzdxmst5VHAqd1sMibWG7SENzT5Dj3yZ3kJYx+WiRYEcQTAkcYA== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/sha2@5.4.0", "@ethersproject/sha2@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.4.0.tgz#c9a8db1037014cbc4e9482bd662f86c090440371" @@ -1722,6 +1865,18 @@ elliptic "6.5.4" hash.js "1.1.7" +"@ethersproject/signing-key@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.5.0.tgz#2aa37169ce7e01e3e80f2c14325f624c29cedbe0" + integrity sha512-5VmseH7qjtNmDdZBswavhotYbWB0bOwKIlOTSlX14rKn5c11QmJwGt4GHeo7NrL/Ycl7uo9AHvEqs5xZgFBTng== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + bn.js "^4.11.9" + elliptic "6.5.4" + hash.js "1.1.7" + "@ethersproject/solidity@5.4.0", "@ethersproject/solidity@^5.0.0", "@ethersproject/solidity@^5.0.9": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.4.0.tgz#1305e058ea02dc4891df18b33232b11a14ece9ec" @@ -1742,6 +1897,15 @@ "@ethersproject/constants" "^5.4.0" "@ethersproject/logger" "^5.4.0" +"@ethersproject/strings@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.5.0.tgz#e6784d00ec6c57710755699003bc747e98c5d549" + integrity sha512-9fy3TtF5LrX/wTrBaT8FGE6TDJyVjOvXynXJz5MT5azq+E6D92zuKNx7i29sWW2FjVOaWjAsiZ1ZWznuduTIIQ== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/constants" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/transactions@5.4.0", "@ethersproject/transactions@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.4.0.tgz#a159d035179334bd92f340ce0f77e83e9e1522e0" @@ -1757,6 +1921,21 @@ "@ethersproject/rlp" "^5.4.0" "@ethersproject/signing-key" "^5.4.0" +"@ethersproject/transactions@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.5.0.tgz#7e9bf72e97bcdf69db34fe0d59e2f4203c7a2908" + integrity sha512-9RZYSKX26KfzEd/1eqvv8pLauCKzDTub0Ko4LfIgaERvRuwyaNV78mJs7cpIgZaDl6RJui4o49lHwwCM0526zA== + dependencies: + "@ethersproject/address" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/constants" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/rlp" "^5.5.0" + "@ethersproject/signing-key" "^5.5.0" + "@ethersproject/units@5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.4.0.tgz#d57477a4498b14b88b10396062c8cbbaf20c79fe" @@ -1798,6 +1977,17 @@ "@ethersproject/properties" "^5.4.0" "@ethersproject/strings" "^5.4.0" +"@ethersproject/web@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.5.0.tgz#0e5bb21a2b58fb4960a705bfc6522a6acf461e28" + integrity sha512-BEgY0eL5oH4mAo37TNYVrFeHsIXLRxggCRG/ksRIxI2X5uj5IsjGmcNiRN/VirQOlBxcUhCgHhaDLG4m6XAVoA== + dependencies: + "@ethersproject/base64" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + "@ethersproject/wordlists@5.4.0", "@ethersproject/wordlists@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.4.0.tgz#f34205ec3bbc9e2c49cadaee774cf0b07e7573d7" @@ -2936,6 +3126,11 @@ "@json-rpc-tools/types" "^1.7.6" "@pedrouid/environment" "^1.0.1" +"@juggle/resize-observer@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" + integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== + "@lingui/babel-plugin-extract-messages@^3.9.0": version "3.9.0" resolved "https://registry.npmjs.org/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-3.9.0.tgz" @@ -3073,6 +3268,72 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@oclif/command@^1.5.20", "@oclif/command@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@oclif/command/-/command-1.8.0.tgz#c1a499b10d26e9d1a611190a81005589accbb339" + integrity sha512-5vwpq6kbvwkQwKqAoOU3L72GZ3Ta8RRrewKj9OJRolx28KLJJ8Dg9Rf7obRwt5jQA9bkYd8gqzMTrI7H3xLfaw== + dependencies: + "@oclif/config" "^1.15.1" + "@oclif/errors" "^1.3.3" + "@oclif/parser" "^3.8.3" + "@oclif/plugin-help" "^3" + debug "^4.1.1" + semver "^7.3.2" + +"@oclif/config@^1.15.1": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.17.0.tgz#ba8639118633102a7e481760c50054623d09fcab" + integrity sha512-Lmfuf6ubjQ4ifC/9bz1fSCHc6F6E653oyaRXxg+lgT4+bYf9bk+nqrUpAbrXyABkCqgIBiFr3J4zR/kiFdE1PA== + dependencies: + "@oclif/errors" "^1.3.3" + "@oclif/parser" "^3.8.0" + debug "^4.1.1" + globby "^11.0.1" + is-wsl "^2.1.1" + tslib "^2.0.0" + +"@oclif/errors@^1.2.2", "@oclif/errors@^1.3.3", "@oclif/errors@^1.3.5": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@oclif/errors/-/errors-1.3.5.tgz#a1e9694dbeccab10fe2fe15acb7113991bed636c" + integrity sha512-OivucXPH/eLLlOT7FkCMoZXiaVYf8I/w1eTAM1+gKzfhALwWTusxEx7wBmW0uzvkSg/9ovWLycPaBgJbM3LOCQ== + dependencies: + clean-stack "^3.0.0" + fs-extra "^8.1" + indent-string "^4.0.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +"@oclif/linewrap@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@oclif/linewrap/-/linewrap-1.0.0.tgz#aedcb64b479d4db7be24196384897b5000901d91" + integrity sha512-Ups2dShK52xXa8w6iBWLgcjPJWjais6KPJQq3gQ/88AY6BXoTX+MIGFPrWQO1KLMiQfoTpcLnUwloN4brrVUHw== + +"@oclif/parser@^3.8.0", "@oclif/parser@^3.8.3": + version "3.8.5" + resolved "https://registry.yarnpkg.com/@oclif/parser/-/parser-3.8.5.tgz#c5161766a1efca7343e1f25d769efbefe09f639b" + integrity sha512-yojzeEfmSxjjkAvMRj0KzspXlMjCfBzNRPkWw8ZwOSoNWoJn+OCS/m/S+yfV6BvAM4u2lTzX9Y5rCbrFIgkJLg== + dependencies: + "@oclif/errors" "^1.2.2" + "@oclif/linewrap" "^1.0.0" + chalk "^2.4.2" + tslib "^1.9.3" + +"@oclif/plugin-help@^3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-3.2.3.tgz#cd24010e7eb326782843d3aa6d6b5a4affebb2c3" + integrity sha512-l2Pd0lbOMq4u/7xsl9hqISFqyR9gWEz/8+05xmrXFr67jXyS6EUCQB+mFBa0wepltrmJu0sAFg9AvA2mLaMMqQ== + dependencies: + "@oclif/command" "^1.5.20" + "@oclif/config" "^1.15.1" + "@oclif/errors" "^1.2.2" + chalk "^4.1.0" + indent-string "^4.0.0" + lodash.template "^4.4.0" + string-width "^4.2.0" + strip-ansi "^6.0.0" + widest-line "^3.1.0" + wrap-ansi "^4.0.0" + "@openzeppelin/contracts@3.4.1-solc-0.7-2": version "3.4.1-solc-0.7-2" resolved "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-3.4.1-solc-0.7-2.tgz" @@ -3380,6 +3641,13 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@sinonjs/fake-timers@^7.1.0": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5" + integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@skidding/launch-editor@^2.2.3": version "2.2.3" resolved "https://registry.yarnpkg.com/@skidding/launch-editor/-/launch-editor-2.2.3.tgz#553d3bcf3ce468bd0ee46cb081276342e120d2b3" @@ -3707,6 +3975,18 @@ resolved "https://registry.yarnpkg.com/@types/array.prototype.flatmap/-/array.prototype.flatmap-1.2.2.tgz#9041c2dc907d583ffb80b8882a782b42436d57c1" integrity sha512-dto5M/8GxPzjaScvQeft2IG0EkoZZfPg2+1noM2BWiU1VR2zsGHf76LonTOnLQKDuJlKDLzKaru4b+5Sci0Yhg== +"@types/async-retry@^1.4.2": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@types/async-retry/-/async-retry-1.4.3.tgz#8b78f6ce88d97e568961732cdd9e5325cdc8c246" + integrity sha512-B3C9QmmNULVPL2uSJQ088eGWTNPIeUk35hca6CV8rRDJ8GXuQJP5CCVWA1ZUCrb9xYP7Js/RkLqnNNwKhe+Zsw== + dependencies: + "@types/retry" "*" + +"@types/await-timeout@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@types/await-timeout/-/await-timeout-0.3.1.tgz#3a0baafc3a96c7a14447a4dcfdcc76b21ce97c3b" + integrity sha512-H5PzROT4KuP7XQDua13Iw8did//OCKAZ/3TL15DjvMzDonrk4HvhH1+tLko96f2guU6XaD3AoqRa49ZOwbwNig== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.15" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.15.tgz#2ccfb1ad55a02c83f8e0ad327cbc332f55eb1024" @@ -3747,6 +4027,20 @@ dependencies: "@types/node" "*" +"@types/bunyan-blackhole@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@types/bunyan-blackhole/-/bunyan-blackhole-0.2.2.tgz#469e58c5d129027a9e08bcf9a36232a69b6ad011" + integrity sha512-nbuxFn2FVw1AAT1h6shgluwz1cgpLKaMBYbEZcMU69Jb1UvSsXcwRiIg+FP4+/JjEUp/uPYLC+twWpfCAaVN1g== + dependencies: + "@types/bunyan" "*" + +"@types/bunyan@*", "@types/bunyan@^1.8.6": + version "1.8.7" + resolved "https://registry.yarnpkg.com/@types/bunyan/-/bunyan-1.8.7.tgz#63cc65b5ecff6217d1509409a575e7b991f80831" + integrity sha512-jaNt6xX5poSmXuDAkQrSqx2zkR66OrdRDuVnU8ldvn3k/Ci/7Sf5nooKspQWimDnw337Bzt/yirqSThTjvrHkg== + dependencies: + "@types/node" "*" + "@types/d3-array@^2": version "2.12.3" resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.12.3.tgz#8d16d51fb04ad5a5a8ebe14eb8263a579f1efdd1" @@ -4113,6 +4407,11 @@ "@types/lingui__core" "*" "@types/react" "*" +"@types/lodash@^4.14.168": + version "4.14.177" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.177.tgz#f70c0d19c30fab101cad46b52be60363c43c4578" + integrity sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw== + "@types/long@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" @@ -4140,6 +4439,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.9.tgz#3bf27710839e62a470ddf6bd8dd321f1737ce5b4" integrity sha512-KktxVzS4FPDFVHUUOWyZMvRo//8vqOLITtLMhFSW9IdLsYT/sPyXj3wXtaTcR7A7olCe7R2Xy7R+q5pg2bU46g== +"@types/node@12.12.54": + version "12.12.54" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.54.tgz#a4b58d8df3a4677b6c08bfbc94b7ad7a7a5f82d1" + integrity sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w== + "@types/node@>=12.12.47", "@types/node@>=13.7.0": version "16.11.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.1.tgz#2e50a649a50fc403433a14f829eface1a3443e97" @@ -4285,6 +4589,11 @@ dependencies: "@types/node" "*" +"@types/retry@*": + version "0.12.1" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" + integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g== + "@types/scheduler@*": version "0.16.2" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" @@ -4297,6 +4606,13 @@ dependencies: "@types/node" "*" +"@types/sinon@^10.0.2": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.3.tgz#2d17cf53f42981e8ebd3e2339dade748b0da742a" + integrity sha512-XUaFuUOQ3A/r6gS1qCU/USMleascaqGeQpGR1AZ5JdRtBPlzijRzKsik1TuGzvdtPA0mdq42JqaJmJ+Afg1LJg== + dependencies: + "@sinonjs/fake-timers" "^7.1.0" + "@types/sinonjs__fake-timers@^6.0.2": version "6.0.3" resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.3.tgz#79df6f358ae8f79e628fe35a63608a0ea8e7cf08" @@ -4317,6 +4633,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/stats-lite@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/stats-lite/-/stats-lite-2.2.0.tgz#bc8190bf9dfa1e16b89eaa2b433c99dff0804de9" + integrity sha512-YV6SS4QC+pbzqjMIV8qVSTDOOazgKBLTVaN+7PfuxELjz/eyzc20KwDVGPrbHt2OcYMA7K2ezLB45Cp6DpNOSQ== + "@types/styled-components@*", "@types/styled-components@^5.1.0": version "5.1.13" resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.13.tgz#a2906b68c2c6c811996216983b74ca02e22c6c34" @@ -4542,6 +4863,11 @@ "@typescript-eslint/types" "4.30.0" eslint-visitor-keys "^2.0.0" +"@uniswap/default-token-list@^2.0.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@uniswap/default-token-list/-/default-token-list-2.2.0.tgz#d85a5c2520f57f4920bd989dfc9f01e1b701a567" + integrity sha512-vFPWoGzDjHP4i2l7yLaober/lZMmzOZXXirVF8XNyfNzRxgmYCWKO6SzKtfEUwxpd3/KUebgdK55II4Mnak62A== + "@uniswap/governance@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@uniswap/governance/-/governance-1.0.2.tgz" @@ -4574,6 +4900,28 @@ resolved "https://registry.yarnpkg.com/@uniswap/redux-multicall/-/redux-multicall-1.0.0.tgz#0cee4448909a788ea4700e5ede75ffeba05b5d75" integrity sha512-zR6tNC3XF6JuI6PjGlZW2Hz7tTzRzzVaPJfZ01BBWBJVt/2ixJY0SH514uffD03NHYiXZA//hlPQLfw3TkIxQg== +"@uniswap/router-sdk@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@uniswap/router-sdk/-/router-sdk-1.0.2.tgz#6f104a9e507a14182b90fb93a69aceb59d6a9a62" + integrity sha512-NYsNsSMY+B0F9TuARDPjVsrfS61X87qsLIfHOcza4Tbp8/bLLnXPA67Jwc6iN4u7xUEdDR049mn0ALPlGeTkDw== + dependencies: + "@ethersproject/abi" "^5.5.0" + "@uniswap/sdk-core" "^3.0.1" + "@uniswap/swap-router-contracts" "1.0.0" + "@uniswap/v2-sdk" "^3.0.1" + "@uniswap/v3-sdk" "^3.7.1" + +"@uniswap/router-sdk@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@uniswap/router-sdk/-/router-sdk-1.0.3.tgz#378a8cc96a3f17b5627b811e64bfcf1e0e50a551" + integrity sha512-9Nq0+J4+u5cPkZlLjCnV1IbbmPVVDxlwrIuYh/fPTVMH4DnPVVEcP68BArtkSUG0OSC87rWrkEHIZd+L8CO6sQ== + dependencies: + "@ethersproject/abi" "^5.5.0" + "@uniswap/sdk-core" "^3.0.1" + "@uniswap/swap-router-contracts" "1.1.0" + "@uniswap/v2-sdk" "^3.0.1" + "@uniswap/v3-sdk" "^3.7.1" + "@uniswap/sdk-core@^3.0.0-alpha.3", "@uniswap/sdk-core@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-3.0.1.tgz#d08dd68257983af64b9a5f4d6b9cf26124b4138f" @@ -4586,7 +4934,72 @@ tiny-invariant "^1.1.0" toformat "^2.0.0" -"@uniswap/token-lists@^1.0.0-beta.27": +"@uniswap/smart-order-router@^2.5.4": + version "2.5.4" + resolved "https://registry.yarnpkg.com/@uniswap/smart-order-router/-/smart-order-router-2.5.4.tgz#dee2580d5fd836aecd46d935ab6ca7f7963c0501" + integrity sha512-uInC4+gL/n28XtEVOY/jrV7uT8FFBtgoW7x0Ks6SGoe/Gx/dYRmzhdeIR1rb3Tg1WP10OwnnC0AW73BJghdGxA== + dependencies: + "@bitauth/libauth" "^1.17.1" + "@ethersproject/bignumber" "^5.1.1" + "@oclif/command" "^1.8.0" + "@oclif/errors" "^1.3.5" + "@types/async-retry" "^1.4.2" + "@types/await-timeout" "^0.3.1" + "@types/bunyan" "^1.8.6" + "@types/bunyan-blackhole" "^0.2.2" + "@types/lodash" "^4.14.168" + "@types/sinon" "^10.0.2" + "@types/stats-lite" "^2.2.0" + "@uniswap/default-token-list" "^2.0.0" + "@uniswap/router-sdk" "^1.0.3" + "@uniswap/swap-router-contracts" "1.1.0" + "@uniswap/token-lists" "^1.0.0-beta.25" + "@uniswap/v2-core" "^1.0.1" + "@uniswap/v2-periphery" "^1.1.0-beta.0" + "@uniswap/v2-sdk" "^3.0.1" + "@uniswap/v3-periphery" "^1.1.1" + "@uniswap/v3-sdk" "^3.7.0" + async-retry "^1.3.1" + await-timeout "^1.1.1" + axios "^0.21.1" + bunyan "^1.8.15" + bunyan-blackhole "^1.1.1" + bunyan-debug-stream "^2.0.0" + cli-logger "^0.5.40" + dotenv "^10.0.0" + ethereum-types "^3.5.0" + ethers "^5.1.4" + graphql "^15.5.0" + graphql-request "^3.4.0" + lodash "^4.17.21" + mnemonist "^0.38.3" + node-cache "^5.1.2" + stats-lite "^2.2.0" + tslib "^1.14.1" + +"@uniswap/swap-router-contracts@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@uniswap/swap-router-contracts/-/swap-router-contracts-1.0.0.tgz#357a5b681fa5d5a5b1629271b99845c96932d4fa" + integrity sha512-EOO94glkJ4KI38IVklmMJkZzQLSY2MCNndYBPWlu64g3DkYJ/d04rbiHTEh7zYwT4gvGW8InQpmQA7mzmGIANw== + dependencies: + "@openzeppelin/contracts" "3.4.1-solc-0.7-2" + "@uniswap/v2-core" "1.0.1" + "@uniswap/v3-core" "1.0.0" + "@uniswap/v3-periphery" "1.3.0" + hardhat-watcher "^2.1.1" + +"@uniswap/swap-router-contracts@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@uniswap/swap-router-contracts/-/swap-router-contracts-1.1.0.tgz#e027b14d4c172f231c53c48e1fd708a78d7d94d8" + integrity sha512-GPmpx1lvjXWloB95+YUabr3UHJYr3scnSS8EzaNXnNrIz9nYZ+XQcMaJxOKe85Yi7IfcUQpj0HzD2TW99dtolA== + dependencies: + "@openzeppelin/contracts" "3.4.1-solc-0.7-2" + "@uniswap/v2-core" "1.0.1" + "@uniswap/v3-core" "1.0.0" + "@uniswap/v3-periphery" "1.3.0" + hardhat-watcher "^2.1.1" + +"@uniswap/token-lists@^1.0.0-beta.25", "@uniswap/token-lists@^1.0.0-beta.27": version "1.0.0-beta.27" resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.27.tgz#8b02a979b0b0024cc96f60e694a3f6db6b2ccab2" integrity sha512-x5hmIniQ9TGqOBCRqfWcmZi/U5kB0qrHMDQ9igs3nMbK0wwmYLraL4owbIwXFGR/co6/lJYJC4K/Gjn4wZY5mQ== @@ -4596,7 +5009,7 @@ resolved "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.0.tgz" integrity sha512-BJiXrBGnN8mti7saW49MXwxDBRFiWemGetE58q8zgfnPPzQKq55ADltEILqOt6VFZ22kVeVKbF8gVd8aY3l7pA== -"@uniswap/v2-core@1.0.1": +"@uniswap/v2-core@1.0.1", "@uniswap/v2-core@^1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.1.tgz" integrity sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q== @@ -4609,10 +5022,10 @@ "@uniswap/lib" "1.1.1" "@uniswap/v2-core" "1.0.0" -"@uniswap/v2-sdk@^3.0.0-alpha.2": - version "3.0.0-alpha.2" - resolved "https://registry.npmjs.org/@uniswap/v2-sdk/-/v2-sdk-3.0.0-alpha.2.tgz" - integrity sha512-LkGGZMdJueIIC3OBzgiBMV/1GdZgzTTf2qGEgBAxJUuCSFf5wZQgWNU4KOkoaBrl/FlEctAGSgGh3j3BhWKL5Q== +"@uniswap/v2-sdk@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@uniswap/v2-sdk/-/v2-sdk-3.0.1.tgz#690c484104c1debd1db56a236e5497def53d698b" + integrity sha512-eSpm2gjo2CZh9FACH5fq42str/oSNyWcDxB27o5k44bEew4sxb+pld4gGIf/byJndLBvArR9PtH8c0n/goNOTw== dependencies: "@ethersproject/address" "^5.0.0" "@ethersproject/solidity" "^5.0.0" @@ -4625,6 +5038,18 @@ resolved "https://registry.npmjs.org/@uniswap/v3-core/-/v3-core-1.0.0.tgz" integrity sha512-kSC4djMGKMHj7sLMYVnn61k9nu+lHjMIxgg9CDQT+s2QYLoA56GbSK9Oxr+qJXzzygbkrmuY6cwgP6cW2JXPFA== +"@uniswap/v3-periphery@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.3.0.tgz#37f0a1ef6025221722e50e9f3f2009c2d5d6e4ec" + integrity sha512-HjHdI5RkjBl8zz3bqHShrbULFoZSrjbbrRHoO2vbzn+WRzTa6xY4PWphZv2Tlcb38YEKfKHp6NPl5hVedac8uw== + dependencies: + "@openzeppelin/contracts" "3.4.1-solc-0.7-2" + "@uniswap/lib" "^4.0.1-alpha" + "@uniswap/v2-core" "1.0.1" + "@uniswap/v3-core" "1.0.0" + base64-sol "1.0.1" + hardhat-watcher "^2.1.1" + "@uniswap/v3-periphery@^1.0.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.2.1.tgz#7775630bea774a2cf989ab87ce3c328ac52e0d50" @@ -4649,7 +5074,7 @@ base64-sol "1.0.1" hardhat-watcher "^2.1.1" -"@uniswap/v3-sdk@^3.7.1": +"@uniswap/v3-sdk@^3.7.0", "@uniswap/v3-sdk@^3.7.1": version "3.7.1" resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-3.7.1.tgz#8a3740ff6302d8069e7ce4a38b7588721398048b" integrity sha512-/0FBsrRijfAEOVO0ejCQX36MwaKzjKCaInUA1dNqFyDNZ5dthvv6jUhMADYuNXZnhN6NcSdIj6xhlc/cpgPm9Q== @@ -5660,6 +6085,13 @@ async-mutex@^0.2.6: dependencies: tslib "^2.0.0" +async-retry@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + async@^1.4.2: version "1.5.2" resolved "https://registry.npmjs.org/async/-/async-1.5.2.tgz" @@ -5732,6 +6164,11 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +await-timeout@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/await-timeout/-/await-timeout-1.1.1.tgz#d42062ee6bc4eb271fe4d4f851eb658dae7e3906" + integrity sha512-gsDXAS6XVc4Jt+7S92MPX6Noq69bdeXUPEaXd8dk3+yVr629LTDLxNt4j1ycBbrU+AStK2PhKIyNIM+xzWMVOQ== + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -6661,6 +7098,11 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +bignumber.js@~9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" + integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== + binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -7034,6 +7476,31 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +bunyan-blackhole@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/bunyan-blackhole/-/bunyan-blackhole-1.1.1.tgz#b9208586dc0b4e47f4f713215b1bddd65e4f6257" + integrity sha1-uSCFhtwLTkf09xMhWxvd1l5PYlc= + dependencies: + stream-blackhole "^1.0.3" + +bunyan-debug-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bunyan-debug-stream/-/bunyan-debug-stream-2.0.0.tgz#b9593e38753f594e3f9db3eb2fdebdc2af147a9f" + integrity sha512-Ovl43CJ7nUwalLzdXc6E1nGIy6ift9Z/QpYXUtsjpDAg35ZFKXifKNZyfpMGuN3N7ijLLqbnxPsMMHsXDdXa9A== + dependencies: + colors "^1.0.3" + exception-formatter "^1.0.4" + +bunyan@^1.8.15: + version "1.8.15" + resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.15.tgz#8ce34ca908a17d0776576ca1b2f6cbd916e93b46" + integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig== + optionalDependencies: + dtrace-provider "~0.8" + moment "^2.19.3" + mv "~2" + safe-json-stringify "~1" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -7445,6 +7912,11 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" +circular@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/circular/-/circular-1.0.5.tgz#7da77af98bbde9ce4b5b358cd556b5dded2d3149" + integrity sha1-fad6+Yu96c5LWzWM1Va13e0tMUk= + cjs-module-lexer@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" @@ -7472,6 +7944,13 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +clean-stack@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-3.0.1.tgz#155bf0b2221bf5f4fba89528d24c5953f17fe3a8" + integrity sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg== + dependencies: + escape-string-regexp "4.0.0" + cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz" @@ -7491,6 +7970,19 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-logger@^0.5.40: + version "0.5.40" + resolved "https://registry.yarnpkg.com/cli-logger/-/cli-logger-0.5.40.tgz#097f0e11b072c7c698a26c47f588a29c20b48b0b" + integrity sha1-CX8OEbByx8aYomxH9YiinCC0iws= + dependencies: + circular "^1.0.5" + cli-util "~1.1.27" + +cli-regexp@~0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/cli-regexp/-/cli-regexp-0.1.2.tgz#6bcd93b09fb2ed1025d30a1155d5997954a53512" + integrity sha1-a82TsJ+y7RAl0woRVdWZeVSlNRI= + cli-spinners@^2.5.0: version "2.6.0" resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz" @@ -7529,6 +8021,13 @@ cli-truncate@^2.1.0: slice-ansi "^3.0.0" string-width "^4.2.0" +cli-util@~1.1.27: + version "1.1.27" + resolved "https://registry.yarnpkg.com/cli-util/-/cli-util-1.1.27.tgz#42d69e36a040a321fc9cf851c1513cadc5093054" + integrity sha1-QtaeNqBAoyH8nPhRwVE8rcUJMFQ= + dependencies: + cli-regexp "~0.1.0" + cli-width@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" @@ -7576,16 +8075,16 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +clone@2.x, clone@^2.0.0, clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + clone@^1.0.2: version "1.0.4" resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= -clone@^2.0.0, clone@^2.1.1: - version "2.1.2" - resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz" - integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= - clsx@^1.1.0: version "1.1.1" resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz" @@ -7699,7 +8198,7 @@ colors@1.0.3: resolved "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz" integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= -colors@^1.1.2: +colors@^1.0.3, colors@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== @@ -9295,6 +9794,11 @@ dotenv@8.2.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== +dotenv@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" + integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== + dotenv@^8.2.0: version "8.6.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" @@ -9316,6 +9820,13 @@ drbg.js@^1.0.1: create-hash "^1.1.2" create-hmac "^1.1.4" +dtrace-provider@~0.8: + version "0.8.8" + resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" + integrity sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg== + dependencies: + nan "^2.14.0" + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -9632,16 +10143,16 @@ escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - escodegen@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" @@ -10143,6 +10654,14 @@ ethereum-cryptography@^0.1.3: secp256k1 "^4.0.1" setimmediate "^1.0.5" +ethereum-types@^3.5.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/ethereum-types/-/ethereum-types-3.6.0.tgz#7cf0a7258537b1f8d113dd51d050189a742a9a6e" + integrity sha512-iJX96C9W1elWhCZKUiSQfWn9fC+EO+xU2TvAE/p7QhMwcGibihKsxcG27B/4WZAudd8jNoeIhY4PH2qQPLuUfw== + dependencies: + "@types/node" "12.12.54" + bignumber.js "~9.0.0" + ethereumjs-abi@0.6.5: version "0.6.5" resolved "https://registry.npmjs.org/ethereumjs-abi/-/ethereumjs-abi-0.6.5.tgz" @@ -10277,6 +10796,42 @@ ethereumjs-vm@^2.1.0, ethereumjs-vm@^2.3.4, ethereumjs-vm@^2.6.0: rustbn.js "~0.2.0" safe-buffer "^5.1.1" +ethers@^5.1.4: + version "5.4.7" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.4.7.tgz#0fd491a5da7c9793de2d6058d76b41b1e7efba8f" + integrity sha512-iZc5p2nqfWK1sj8RabwsPM28cr37Bpq7ehTQ5rWExBr2Y09Sn1lDKZOED26n+TsZMye7Y6mIgQ/1cwpSD8XZew== + dependencies: + "@ethersproject/abi" "5.4.1" + "@ethersproject/abstract-provider" "5.4.1" + "@ethersproject/abstract-signer" "5.4.1" + "@ethersproject/address" "5.4.0" + "@ethersproject/base64" "5.4.0" + "@ethersproject/basex" "5.4.0" + "@ethersproject/bignumber" "5.4.2" + "@ethersproject/bytes" "5.4.0" + "@ethersproject/constants" "5.4.0" + "@ethersproject/contracts" "5.4.1" + "@ethersproject/hash" "5.4.0" + "@ethersproject/hdnode" "5.4.0" + "@ethersproject/json-wallets" "5.4.0" + "@ethersproject/keccak256" "5.4.0" + "@ethersproject/logger" "5.4.1" + "@ethersproject/networks" "5.4.2" + "@ethersproject/pbkdf2" "5.4.0" + "@ethersproject/properties" "5.4.1" + "@ethersproject/providers" "5.4.5" + "@ethersproject/random" "5.4.0" + "@ethersproject/rlp" "5.4.0" + "@ethersproject/sha2" "5.4.0" + "@ethersproject/signing-key" "5.4.0" + "@ethersproject/solidity" "5.4.0" + "@ethersproject/strings" "5.4.0" + "@ethersproject/transactions" "5.4.0" + "@ethersproject/units" "5.4.0" + "@ethersproject/wallet" "5.4.0" + "@ethersproject/web" "5.4.0" + "@ethersproject/wordlists" "5.4.0" + ethers@^5.4.0, ethers@^5.4.6: version "5.4.6" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.4.6.tgz#fe0a023956b5502c947f58e82fbcf9a73e5e75b6" @@ -10374,6 +10929,13 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" +exception-formatter@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/exception-formatter/-/exception-formatter-1.0.7.tgz#3291616b86fceabefa97aee6a4708032c6e3b96d" + integrity sha512-zV45vEsjytJrwfGq6X9qd1Ll56cW4NC2mhCO6lqwMk4ZpA1fZ6C3UiaQM/X7if+7wZFmCgss3ahp9B/uVFuLRw== + dependencies: + colors "^1.0.3" + exec-sh@^0.3.2: version "0.3.6" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc" @@ -11045,7 +11607,7 @@ fs-extra@10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@8.1.0, fs-extra@^8.1.0: +fs-extra@8.1.0, fs-extra@^8.1, fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== @@ -11248,6 +11810,17 @@ glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" @@ -11349,7 +11922,7 @@ globby@11.0.3: merge2 "^1.3.0" slash "^3.0.0" -globby@^11.0.3: +globby@^11.0.1, globby@^11.0.3: version "11.0.4" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== @@ -12733,6 +13306,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +isnumber@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isnumber/-/isnumber-1.0.0.tgz#0e3f9759b581d99dd85086f0ec2a74909cfadd01" + integrity sha1-Dj+XWbWB2Z3YUIbw7Cp0kJz63QE= + isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" @@ -13990,7 +14568,7 @@ lodash.once@^4.0.0, lodash.once@^4.1.1: resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= -lodash.template@^4.5.0: +lodash.template@^4.4.0, lodash.template@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== @@ -14535,7 +15113,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@3.0.4, minimatch@^3.0.4: +"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -14619,6 +15197,18 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mnemonist@^0.38.3: + version "0.38.3" + resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.3.tgz#35ec79c1c1f4357cfda2fe264659c2775ccd7d9d" + integrity sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw== + dependencies: + obliterator "^1.6.1" + +moment@^2.19.3: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -14706,6 +15296,15 @@ mute-stream@0.0.8: resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mv@~2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" + integrity sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI= + dependencies: + mkdirp "~0.5.1" + ncp "~2.0.0" + rimraf "~2.4.0" + nan@^2.12.1, nan@^2.14.0, nan@^2.2.1: version "2.14.2" resolved "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz" @@ -14755,6 +15354,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +ncp@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= + negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -14788,6 +15392,13 @@ node-addon-api@^2.0.0: resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== +node-cache@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" + integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg== + dependencies: + clone "2.x" + node-fetch@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz" @@ -15112,6 +15723,11 @@ object.values@^1.1.0, object.values@^1.1.3, object.values@^1.1.4: define-properties "^1.1.3" es-abstract "^1.18.2" +obliterator@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-1.6.1.tgz#dea03e8ab821f6c4d96a299e17aef6a3af994ef3" + integrity sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig== + obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" @@ -18112,6 +18728,11 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -18159,6 +18780,13 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" + integrity sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto= + dependencies: + glob "^6.0.1" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -18339,6 +18967,11 @@ safe-identifier@^0.4.2: resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.2.tgz#cf6bfca31c2897c588092d1750d30ef501d59fcb" integrity sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w== +safe-json-stringify@~1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + safe-json-utils@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/safe-json-utils/-/safe-json-utils-1.1.1.tgz#0e883874467d95ab914c3f511096b89bfb3e63b1" @@ -19109,11 +19742,23 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +stats-lite@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/stats-lite/-/stats-lite-2.2.0.tgz#278a5571fa1d2e8b1691295dccc0235282393bbf" + integrity sha512-/Kz55rgUIv2KP2MKphwYT/NCuSfAlbbMRv2ZWw7wyXayu230zdtzhxxuXXcvsc6EmmhS8bSJl3uS1wmMHFumbA== + dependencies: + isnumber "~1.0.0" + "statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +stream-blackhole@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-blackhole/-/stream-blackhole-1.0.3.tgz#6fc2e2c2e9d9fde6be8c68d3db88de09802e4d63" + integrity sha1-b8LiwunZ/ea+jGjT24jeCYAuTWM= + stream-browserify@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" @@ -19220,7 +19865,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0: +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== @@ -19965,7 +20610,7 @@ tslib@2.0.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== -tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.0.0, tslib@^1.14.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -20404,6 +21049,13 @@ use-elapsed-time@^2.1.6: resolved "https://registry.npmjs.org/use-elapsed-time/-/use-elapsed-time-2.1.8.tgz" integrity sha512-lNLTDffKHdHWweQNvnch9tFI2eRP3tXccSLrwE7U6xrfyWFNEgNQZWWsGhQvtwKa0kJ6L+7E5wKbi3jg86opjg== +use-resize-observer@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-8.0.0.tgz#69bd80c1ddd94f3758563fe107efb25fed85067a" + integrity sha512-n0iKSeiQpJCyaFh5JA0qsVLBIovsF4EIIR1G6XiBwKJN66ZrD4Oj62bjcuTAATPKiSp6an/2UZZxCf/67fk3sQ== + dependencies: + "@juggle/resize-observer" "^3.3.1" + use-sidecar@^1.0.1: version "1.0.5" resolved "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.0.5.tgz" @@ -20930,6 +21582,13 @@ widest-line@^2.0.0: dependencies: string-width "^2.1.1" +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" @@ -21138,6 +21797,15 @@ wrap-ansi@^3.0.1: string-width "^2.1.1" strip-ansi "^4.0.0" +wrap-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-4.0.0.tgz#b3570d7c70156159a2d42be5cc942e957f7b1131" + integrity sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg== + dependencies: + ansi-styles "^3.2.0" + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"