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 <lint-action@samuelmeuli.com>
Co-authored-by: Ian Lapham <ian@uniswap.org>
Co-authored-by: Callil Capuozzo <callil.capuozzo@gmail.com>
This commit is contained in:
Justin Domingue 2021-12-16 14:44:03 -05:00 committed by GitHub
parent 642a4177d8
commit 9e1a775c13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 2997 additions and 976 deletions

@ -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",

@ -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);

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0047 9.26921H10.2714C11.0078 9.26921 11.6047 9.86617 11.6047 10.6025V12.1359C11.6047 12.7987 12.142 13.3359 12.8047 13.3359C13.4675 13.3359 14.0047 12.7995 14.0047 12.1367V5.22059C14.0047 4.86697 13.7758 4.56227 13.5258 4.31223L10.6714 1.33594M4.00472 2.00254H8.00472C8.7411 2.00254 9.33805 2.59949 9.33805 3.33587V14.0015H2.67139V3.33587C2.67139 2.59949 3.26834 2.00254 4.00472 2.00254ZM14.0047 5.33587C14.0047 6.07225 13.4078 6.66921 12.6714 6.66921C11.935 6.66921 11.3381 6.07225 11.3381 5.33587C11.3381 4.59949 11.935 4.00254 12.6714 4.00254C13.4078 4.00254 14.0047 4.59949 14.0047 5.33587Z" stroke="white"/>
<line x1="4" y1="9.99414" x2="8" y2="9.99414" stroke="white"/>
<line x1="4" y1="11.9941" x2="8" y2="11.9941" stroke="white"/>
<path d="M4 8.16113H8" stroke="white"/>
</svg>

After

Width:  |  Height:  |  Size: 895 B

@ -0,0 +1,12 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_988_5781)">
<path d="M11.3333 12.5C7.33329 12.5 6.66663 8.5 3.99996 8.5M3.99996 8.5C6.66663 8.5 7.33329 4.5 11.3333 4.5M3.99996 8.5H1.66663" stroke="#888D9B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="13.3334" cy="4.5" r="2" stroke="#888D9B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="13.3334" cy="12.5" r="2" stroke="#888D9B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_988_5781">
<rect width="16" height="16" fill="white" transform="translate(0 0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 733 B

@ -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 (
<animated.div
style={{
...props,
overflow: 'hidden',
width: '100%',
willChange: 'height',
}}
>
<div ref={ref}>{children}</div>
</animated.div>
)
}

@ -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;

@ -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 (
<ThemedText.Body fontSize={14} color={fiatValue ? theme.text2 : theme.text4}>
<ThemedText.Body fontSize={14} color={fiatValue ? theme.text3 : theme.text4}>
{fiatValue ? (
<Trans>
~$ <HoverInlineText text={fiatValue?.toSignificant(6, { groupSeparator: ',' })} />
$
<HoverInlineText
text={fiatValue?.toSignificant(6, { groupSeparator: ',' })}
textColor={fiatValue ? theme.text3 : theme.text4}
/>
</Trans>
) : (
''
@ -36,7 +43,9 @@ export function FiatValue({
{priceImpact ? (
<span style={{ color: priceImpactColor }}>
{' '}
(<Trans>{priceImpact.multiply(-1).toSignificant(3)}%</Trans>)
<MouseoverTooltip text={t`The estimated difference between the USD values of input and output amounts.`}>
(<Trans>{priceImpact.multiply(-1).toSignificant(3)}%</Trans>)
</MouseoverTooltip>
</span>
) : null}
</ThemedText.Body>

@ -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({
)}
<Container hideInput={hideInput}>
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={!onCurrencySelect}>
{!hideInput && (
<StyledNumericalInput
className="token-amount-input"
value={value}
onUserInput={onUserInput}
$loading={loading}
/>
)}
<CurrencySelect
visible={currency !== undefined}
selected={!!currency}
@ -257,24 +269,19 @@ export default function CurrencyInputPanel({
{onCurrencySelect && <StyledDropDown selected={!!currency} />}
</Aligner>
</CurrencySelect>
{!hideInput && (
<StyledNumericalInput
className="token-amount-input"
value={value}
onUserInput={onUserInput}
$loading={loading}
/>
)}
</InputRow>
{!hideInput && !hideBalance && (
{!hideInput && !hideBalance && currency && (
<FiatRow>
<RowBetween>
<LoadingOpacityContainer $loading={loading}>
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
</LoadingOpacityContainer>
{account ? (
<RowFixed style={{ height: '17px' }}>
<ThemedText.Body
onClick={onMax}
color={theme.text2}
fontWeight={400}
color={theme.text3}
fontWeight={500}
fontSize={14}
style={{ display: 'inline', cursor: 'pointer' }}
>
@ -282,24 +289,19 @@ export default function CurrencyInputPanel({
renderBalance ? (
renderBalance(selectedCurrencyBalance)
) : (
<Trans>
Balance: {formatCurrencyAmount(selectedCurrencyBalance, 4)} {currency.symbol}
</Trans>
<Trans>Balance: {formatCurrencyAmount(selectedCurrencyBalance, 4)}</Trans>
)
) : null}
</ThemedText.Body>
{showMaxButton && selectedCurrencyBalance ? (
<StyledBalanceMax onClick={onMax}>
<Trans>(Max)</Trans>
<Trans>MAX</Trans>
</StyledBalanceMax>
) : null}
</RowFixed>
) : (
<span />
)}
<LoadingOpacityContainer $loading={loading}>
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
</LoadingOpacityContainer>
</RowBetween>
</FiatRow>
)}

@ -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({

@ -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')};

@ -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 (
<>
<ExternalLink
href={chainId && blockNumber ? getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK) : ''}
>
<RowFixed>
<StyledPolling onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)} warning={warning}>
<ExternalLink href={'https://etherscan.io/gastracker'}>
{priceGwei ? (
<RowFixed style={{ marginRight: '8px' }}>
<ThemedText.Main fontSize="11px" mr="8px" color={theme.text3}>
<MouseoverTooltip
text={
<Trans>
{`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. `}
</Trans>
}
>
{priceGwei.toString()} <Trans>gwei</Trans>
</MouseoverTooltip>
</ThemedText.Main>
<StyledGasDot />
</RowFixed>
) : null}
</ExternalLink>
<StyledPollingNumber breathe={isMounting} hovering={isHover}>
{blockNumber}&ensp;
<ExternalLink
href={
chainId && blockNumber ? getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK) : ''
}
>
<MouseoverTooltip
text={<Trans>{`The most recent block number on this network. Prices update on every block.`}</Trans>}
>
{blockNumber}&ensp;
</MouseoverTooltip>
</ExternalLink>
</StyledPollingNumber>
<StyledPollingDot warning={warning}>{isMounting && <Spinner warning={warning} />}</StyledPollingDot>{' '}
</StyledPolling>
</ExternalLink>
{warning && <ChainConnectivityWarning />}
{warning && <ChainConnectivityWarning />}
</RowFixed>
</>
)
}

@ -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 (
<TextWrapper margin={margin} adjustSize={adjustSize} link={link} fontSize={fontSize} {...rest}>
<TextWrapper
margin={margin}
adjustSize={adjustSize}
link={link}
fontSize={fontSize}
textColor={textColor}
{...rest}
>
{text}
</TextWrapper>
)

@ -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

@ -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;

@ -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,
},
]

@ -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 (
<Wrapper>
{routes.map(({ percent, path }, index) => (
{routes.map((entry, index) => (
<RouteContainerRow key={index}>
<CurrencyLogo currency={tokenIn} />
<Route percent={percent} path={path} />
<CurrencyLogo currency={tokenOut} />
<CurrencyLogo currency={tokenIn} size={'20px'} />
<Route entry={entry} />
<CurrencyLogo currency={tokenOut} size={'20px'} />
</RouteContainerRow>
))}
</Wrapper>
)
}
function Route({ percent, path }: { percent: RoutingDiagramEntry['percent']; path: RoutingDiagramEntry['path'] }) {
function Route({ entry: { percent, path, protocol } }: { entry: RoutingDiagramEntry }) {
return (
<RouteRow>
<DottedLine>
<DotColor />
</DottedLine>
<OpaqueBadge>
<ThemedText.Small fontSize={12} style={{ wordBreak: 'normal' }}>
<ProtocolBadge>
<BadgeText fontSize={12}>{protocol.toUpperCase()}</BadgeText>
</ProtocolBadge>
<BadgeText fontSize={14} style={{ minWidth: 'auto' }}>
{percent.toSignificant(2)}%
</ThemedText.Small>
</BadgeText>
</OpaqueBadge>
<AutoRow gap="1px" width="100%" style={{ justifyContent: 'space-evenly', zIndex: 2 }}>
{path.map(([currency0, currency1, feeAmount], index) => (
<Pool key={index} currency0={currency0} currency1={currency1} feeAmount={feeAmount} />
@ -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 (
<PoolBadge>
<Box margin="0 5px 0 10px">
<DoubleCurrencyLogo currency0={tokenInfo1} currency1={tokenInfo0} size={20} />
</Box>
<ThemedText.Small fontSize={12}>{feeAmount / 10000}%</ThemedText.Small>
</PoolBadge>
<MouseoverTooltip
text={<Trans>{tokenInfo0?.symbol + '/' + tokenInfo1?.symbol + ' ' + feeAmount / 10000}% pool</Trans>}
>
<PoolBadge>
<Box margin="0 4px 0 12px">
<DoubleCurrencyLogo currency0={tokenInfo1} currency1={tokenInfo0} size={20} />
</Box>
<ThemedText.Small fontSize={14}>{feeAmount / 10000}%</ThemedText.Small>
</PoolBadge>
</MouseoverTooltip>
)
}

@ -3,10 +3,10 @@
exports[`renders multi route 1`] = `
<DocumentFragment>
<div
class="RoutingDiagram__Wrapper-sc-o1ook0-0 fUoVYh css-vurnku"
class="RoutingDiagram__Wrapper-sc-o1ook0-0 ePDWDk css-vurnku"
>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV iiQQUx"
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV ibRCpr"
>
CurrencyLogo currency=USDC
<div
@ -22,11 +22,20 @@ exports[`renders multi route 1`] = `
</svg>
</div>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-6 gayll khxosM"
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-6 gayll OurGh"
>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9"
style="word-break: normal;"
class="Badge-sc-1mhw5si-0 RoutingDiagram__ProtocolBadge-sc-o1ook0-7 gayll bNVqMw"
>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-15li2d9"
>
V2
</div>
</div>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-1aekuku"
style="min-width: auto;"
>
75%
</div>
@ -36,26 +45,13 @@ exports[`renders multi route 1`] = `
style="justify-content: space-evenly; z-index: 2;"
width="100%"
>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__PoolBadge-sc-o1ook0-3 gayll bRJvWg"
>
<div
class="css-1t7xebc"
>
DoubleCurrencyLogo currency0=DAI currency1=USDC
</div>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9"
>
0.01%
</div>
</div>
Popover
</div>
</div>
CurrencyLogo currency=DAI
</div>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV iiQQUx"
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV ibRCpr"
>
CurrencyLogo currency=USDC
<div
@ -71,11 +67,20 @@ exports[`renders multi route 1`] = `
</svg>
</div>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-6 gayll khxosM"
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-6 gayll OurGh"
>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9"
style="word-break: normal;"
class="Badge-sc-1mhw5si-0 RoutingDiagram__ProtocolBadge-sc-o1ook0-7 gayll bNVqMw"
>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-15li2d9"
>
V3
</div>
</div>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-1aekuku"
style="min-width: auto;"
>
25%
</div>
@ -85,34 +90,7 @@ exports[`renders multi route 1`] = `
style="justify-content: space-evenly; z-index: 2;"
width="100%"
>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__PoolBadge-sc-o1ook0-3 gayll bRJvWg"
>
<div
class="css-1t7xebc"
>
DoubleCurrencyLogo currency0=WBTC currency1=USDC
</div>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9"
>
0.3%
</div>
</div>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__PoolBadge-sc-o1ook0-3 gayll bRJvWg"
>
<div
class="css-1t7xebc"
>
DoubleCurrencyLogo currency0=DAI currency1=WBTC
</div>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9"
>
1%
</div>
</div>
PopoverPopover
</div>
</div>
CurrencyLogo currency=DAI
@ -124,10 +102,10 @@ exports[`renders multi route 1`] = `
exports[`renders single route 1`] = `
<DocumentFragment>
<div
class="RoutingDiagram__Wrapper-sc-o1ook0-0 fUoVYh css-vurnku"
class="RoutingDiagram__Wrapper-sc-o1ook0-0 ePDWDk css-vurnku"
>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV iiQQUx"
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV ibRCpr"
>
CurrencyLogo currency=USDC
<div
@ -143,11 +121,20 @@ exports[`renders single route 1`] = `
</svg>
</div>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-6 gayll khxosM"
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-6 gayll OurGh"
>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9"
style="word-break: normal;"
class="Badge-sc-1mhw5si-0 RoutingDiagram__ProtocolBadge-sc-o1ook0-7 gayll bNVqMw"
>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-15li2d9"
>
V3
</div>
</div>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-1aekuku"
style="min-width: auto;"
>
100%
</div>
@ -157,20 +144,7 @@ exports[`renders single route 1`] = `
style="justify-content: space-evenly; z-index: 2;"
width="100%"
>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__PoolBadge-sc-o1ook0-3 gayll bRJvWg"
>
<div
class="css-1t7xebc"
>
DoubleCurrencyLogo currency0=DAI currency1=USDC
</div>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9"
>
0.05%
</div>
</div>
Popover
</div>
</div>
CurrencyLogo currency=DAI
@ -182,7 +156,7 @@ exports[`renders single route 1`] = `
exports[`renders when no routes are provided 1`] = `
<DocumentFragment>
<div
class="RoutingDiagram__Wrapper-sc-o1ook0-0 fUoVYh css-vurnku"
class="RoutingDiagram__Wrapper-sc-o1ook0-0 ePDWDk css-vurnku"
/>
</DocumentFragment>
`;

@ -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
<Text fontWeight={600} fontSize={14}>
<Trans>Interface Settings</Trans>
</Text>
{chainId === SupportedChainId.MAINNET && (
{chainId && AUTO_ROUTER_SUPPORTED_CHAINS.includes(chainId) && (
<RowBetween>
<RowFixed>
<ThemedText.Black fontWeight={400} fontSize={14} color={theme.text2}>
<Trans>Auto Router</Trans>
<Trans>Auto Router API</Trans>
</ThemedText.Black>
<QuestionHelper
text={<Trans>Use the Uniswap Labs API to get better pricing through a more efficient route.</Trans>}
/>
<QuestionHelper text={<Trans>Use the Uniswap Labs API to get faster quotes.</Trans>} />
</RowFixed>
<Toggle
id="toggle-optimized-router-button"
@ -223,7 +220,6 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
/>
</RowBetween>
)}
<RowBetween>
<RowFixed>
<ThemedText.Black fontWeight={400} fontSize={14} color={theme.text2}>

@ -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<PopoverProps, 'content'> {
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<TooltipContentProps, 'show'>) {
const [show, setShow] = useState(false)
@ -61,7 +63,7 @@ export function MouseoverTooltipContent({
}, [openCallback])
const close = useCallback(() => setShow(false), [setShow])
return (
<TooltipContent {...rest} show={show} content={content}>
<TooltipContent {...rest} show={show} content={disableHover ? null : content}>
<div
style={{ display: 'inline-block', lineHeight: 0, padding: '0.25rem' }}
onMouseEnter={open}

@ -110,7 +110,7 @@ const HoverText = styled.div`
`
const LinkCard = styled(Card)`
background-color: ${({ theme }) => theme.primary1};
background-color: ${({ theme }) => theme.bg1};
color: ${({ theme }) => theme.white};
:hover {
@ -402,6 +402,16 @@ export default function WalletModal({
</ThemedText.Black>
</AutoRow>
</LightCard>
{walletView === WALLET_VIEWS.PENDING ? (
<PendingView
connector={pendingWallet}
error={pendingError}
setPendingError={setPendingError}
tryActivation={tryActivation}
/>
) : (
<OptionGrid>{getOptions()}</OptionGrid>
)}
<LinkCard padding=".5rem" $borderRadius=".75rem" onClick={() => setWalletView(WALLET_VIEWS.LEGAL)}>
<RowBetween>
<AutoRow gap="4px">
@ -413,16 +423,6 @@ export default function WalletModal({
<ArrowRight size={16} />
</RowBetween>
</LinkCard>
{walletView === WALLET_VIEWS.PENDING ? (
<PendingView
connector={pendingWallet}
error={pendingError}
setPendingError={setPendingError}
tryActivation={tryActivation}
/>
) : (
<OptionGrid>{getOptions()}</OptionGrid>
)}
</AutoColumn>
</ContentWrapper>
</UpperSection>

@ -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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
trade?: InterfaceTrade<Currency, Currency, TradeType>
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 : (
<AutoColumn gap="8px">
<TransactionDetailsLabel fontWeight={500} fontSize={14}>
<Trans>Transaction Details</Trans>
</TransactionDetailsLabel>
<RowBetween>
<RowFixed>
<ThemedText.SubHeader color={theme.text1}>
<Trans>Liquidity Provider Fee</Trans>
</ThemedText.SubHeader>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={65}>
<ThemedText.Black textAlign="right" fontSize={14}>
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${realizedLPFee.currency.symbol}` : '-'}
</ThemedText.Black>
</TextWithLoadingPlaceholder>
</RowBetween>
<RowBetween>
<RowFixed>
<ThemedText.SubHeader color={theme.text1}>
<Trans>Price Impact</Trans>
</ThemedText.SubHeader>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.Black textAlign="right" fontSize={14}>
<FormattedPriceImpact priceImpact={priceImpact} />
</ThemedText.Black>
</TextWithLoadingPlaceholder>
</RowBetween>
<RowBetween>
<RowFixed>
<ThemedText.SubHeader color={theme.text1}>
<Trans>Allowed Slippage</Trans>
</ThemedText.SubHeader>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={45}>
<ThemedText.Black textAlign="right" fontSize={14}>
{allowedSlippage.toFixed(2)}%
</ThemedText.Black>
</TextWithLoadingPlaceholder>
</RowBetween>
<RowBetween>
<RowFixed>
<ThemedText.SubHeader color={theme.text1}>
{trade.tradeType === TradeType.EXACT_INPUT ? <Trans>Minimum received</Trans> : <Trans>Maximum sent</Trans>}
</ThemedText.SubHeader>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={70}>
<ThemedText.Black textAlign="right" fontSize={14}>
{trade.tradeType === TradeType.EXACT_INPUT
? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}`
: `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`}
</ThemedText.Black>
</TextWithLoadingPlaceholder>
</RowBetween>
</AutoColumn>
<StyledCard>
<AutoColumn gap="8px">
<RowBetween>
<RowFixed>
<ThemedText.SubHeader color={theme.text1}>
<Trans>Expected Output</Trans>
</ThemedText.SubHeader>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={65}>
<ThemedText.Black textAlign="right" fontSize={14}>
{expectedOutputAmount
? `${expectedOutputAmount.toSignificant(6)} ${expectedOutputAmount.currency.symbol}`
: '-'}
</ThemedText.Black>
</TextWithLoadingPlaceholder>
</RowBetween>
<RowBetween>
<RowFixed>
<ThemedText.SubHeader color={theme.text1}>
<Trans>Price Impact</Trans>
</ThemedText.SubHeader>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.Black textAlign="right" fontSize={14}>
<FormattedPriceImpact priceImpact={priceImpact} />
</ThemedText.Black>
</TextWithLoadingPlaceholder>
</RowBetween>
<Separator />
<RowBetween>
<RowFixed style={{ marginRight: '20px' }}>
<ThemedText.SubHeader color={theme.text3}>
{trade.tradeType === TradeType.EXACT_INPUT ? (
<Trans>Minimum received</Trans>
) : (
<Trans>Maximum sent</Trans>
)}{' '}
<Trans>after slippage</Trans> ({allowedSlippage.toFixed(2)}%)
</ThemedText.SubHeader>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={70}>
<ThemedText.Black textAlign="right" fontSize={14} color={theme.text3}>
{trade.tradeType === TradeType.EXACT_INPUT
? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}`
: `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`}
</ThemedText.Black>
</TextWithLoadingPlaceholder>
</RowBetween>
{!trade?.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
<RowBetween>
<ThemedText.SubHeader color={theme.text3}>
<Trans>Network Fee</Trans>
</ThemedText.SubHeader>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.Black textAlign="right" fontSize={14} color={theme.text3}>
~${trade.gasUseEstimateUSD.toFixed(2)}
</ThemedText.Black>
</TextWithLoadingPlaceholder>
</RowBetween>
)}
</AutoColumn>
</StyledCard>
)
}

@ -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<Currency, Currency, TradeType>, V2Trade<Currency, Currency, TradeType>]
| [V3Trade<Currency, Currency, TradeType>, V3Trade<Currency, Currency, TradeType>]
...args: [Trade<Currency, Currency, TradeType>, Trade<Currency, Currency, TradeType>]
): boolean {
const [tradeA, tradeB] = args
return (
@ -44,8 +42,8 @@ export default function ConfirmSwapModal({
txHash,
}: {
isOpen: boolean
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined
originalTrade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
originalTrade: Trade<Currency, Currency, TradeType> | 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]
)

@ -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<Currency, Currency, TradeType> | 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 (
<MouseoverTooltipContent
wrap={false}
disableHover={disableHover}
content={
loading ? null : (
<ResponsiveTooltipContainer
origin="top right"
style={{
padding: showRoute ? '0' : '12px',
border: 'none',
borderRadius: showRoute ? '16px' : '12px',
maxWidth: '400px',
}}
>
{showRoute ? (
trade ? (
<SwapRoute trade={trade} syncing={loading} fixedOpen={showRoute} />
) : null
) : (
<AutoColumn gap="4px" justify="center">
<ThemedText.Main fontSize="12px" textAlign="center">
<Trans>Estimated network fee</Trans>
</ThemedText.Main>
<ThemedText.Body textAlign="center" fontWeight={500} style={{ userSelect: 'none' }}>
<Trans>${trade?.gasUseEstimateUSD?.toFixed(2)}</Trans>
</ThemedText.Body>
<ThemedText.Main fontSize="10px" textAlign="center" maxWidth="140px" color="text3">
<Trans>Estimate may differ due to your wallet gas settings</Trans>
</ThemedText.Main>
</AutoColumn>
)}
</ResponsiveTooltipContainer>
)
}
placement="bottom"
onOpen={() =>
ReactGA.event({
category: 'Gas',
action: 'Gas Details Tooltip Open',
})
}
>
<LoadingOpacityContainer $loading={loading}>
<GasWrapper>
<StyledGasIcon />
{formattedGasPriceString ?? null}
</GasWrapper>
</LoadingOpacityContainer>
</MouseoverTooltipContent>
)
}

@ -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 ? <StyledAutoRouterIcon /> : <StyledStaticRouterIcon />
return autoRouterSupported ? <StyledAutoRouterIcon /> : <StyledStaticRouterIcon />
}
export function AutoRouterLabel() {
const routingAPIEnabled = useRoutingAPIEnabled()
const autoRouterSupported = useAutoRouterSupported()
return routingAPIEnabled ? (
return autoRouterSupported ? (
<StyledAutoRouterLabel fontSize={14}>Auto Router</StyledAutoRouterLabel>
) : (
<ThemedText.Black fontSize={14}>

@ -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<Currency, Currency, TradeType> | undefined
syncing: boolean
loading: boolean
showInverted: boolean
setShowInverted: React.Dispatch<React.SetStateAction<boolean>>
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 (
<Wrapper>
<AutoColumn gap={'8px'} style={{ width: '100%', marginBottom: '-8px' }}>
<StyledHeaderRow onClick={() => setShowDetails(!showDetails)} disabled={!trade} open={showDetails}>
<RowFixed style={{ position: 'relative' }}>
{loading || syncing ? (
<StyledPolling>
<StyledPollingDot>
<Spinner />
</StyledPollingDot>
</StyledPolling>
) : (
<HideSmall>
<MouseoverTooltipContent
wrap={false}
content={
<ResponsiveTooltipContainer origin="top right" style={{ padding: '0' }}>
<Card padding="12px">
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} />
</Card>
</ResponsiveTooltipContainer>
}
placement="bottom"
disableHover={showDetails}
>
<StyledInfoIcon color={trade ? theme.text3 : theme.bg3} />
</MouseoverTooltipContent>
</HideSmall>
)}
{trade ? (
<LoadingOpacityContainer $loading={syncing}>
<TradePrice
price={trade.executionPrice}
showInverted={showInverted}
setShowInverted={setShowInverted}
/>
</LoadingOpacityContainer>
) : loading || syncing ? (
<ThemedText.Main fontSize={14}>
<Trans>Fetching best price...</Trans>
</ThemedText.Main>
) : null}
</RowFixed>
<RowFixed>
{!trade?.gasUseEstimateUSD ||
showDetails ||
!chainId ||
!SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
<GasEstimateBadge
trade={trade}
loading={syncing || loading}
showRoute={!showDetails}
disableHover={showDetails}
/>
)}
<RotatingArrow stroke={trade ? theme.text3 : theme.bg3} open={Boolean(trade && showDetails)} />
</RowFixed>
</StyledHeaderRow>
<AnimatedDropdown open={showDetails}>
<AutoColumn gap={'8px'} style={{ padding: '0', paddingBottom: '8px' }}>
{trade ? (
<StyledCard>
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} />
</StyledCard>
) : null}
{trade ? <SwapRoute trade={trade} syncing={syncing} /> : null}
</AutoColumn>
</AnimatedDropdown>
</AutoColumn>
</Wrapper>
)
}

@ -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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
trade: Trade<Currency, Currency, TradeType>
onConfirm: () => void
swapErrorMessage: ReactNode | undefined
disabledConfirm: boolean

@ -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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
trade: InterfaceTrade<Currency, Currency, TradeType>
allowedSlippage: Percent
recipient: string | null
showAcceptChanges: boolean
@ -63,19 +62,7 @@ export default function SwapModalHeader({
<AutoColumn gap={'4px'} style={{ marginTop: '1rem' }}>
<LightCard padding="0.75rem 1rem">
<AutoColumn gap={'8px'}>
<RowBetween>
<ThemedText.Body color={theme.text3} fontWeight={500} fontSize={14}>
<Trans>From</Trans>
</ThemedText.Body>
<FiatValue fiatValue={fiatValueInput} />
</RowBetween>
<RowBetween align="center">
<RowFixed gap={'0px'}>
<CurrencyLogo currency={trade.inputAmount.currency} size={'20px'} style={{ marginRight: '12px' }} />
<Text fontSize={20} fontWeight={500}>
{trade.inputAmount.currency.symbol}
</Text>
</RowFixed>
<RowFixed gap={'0px'}>
<TruncatedText
fontSize={24}
@ -85,6 +72,15 @@ export default function SwapModalHeader({
{trade.inputAmount.toSignificant(6)}
</TruncatedText>
</RowFixed>
<RowFixed gap={'0px'}>
<CurrencyLogo currency={trade.inputAmount.currency} size={'20px'} style={{ marginRight: '12px' }} />
<Text fontSize={20} fontWeight={500}>
{trade.inputAmount.currency.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<FiatValue fiatValue={fiatValueInput} />
</RowBetween>
</AutoColumn>
</LightCard>
@ -93,10 +89,20 @@ export default function SwapModalHeader({
</ArrowWrapper>
<LightCard padding="0.75rem 1rem" style={{ marginBottom: '0.25rem' }}>
<AutoColumn gap={'8px'}>
<RowBetween align="flex-end">
<RowFixed gap={'0px'}>
<TruncatedText fontSize={24} fontWeight={500}>
{trade.outputAmount.toSignificant(6)}
</TruncatedText>
</RowFixed>
<RowFixed gap={'0px'}>
<CurrencyLogo currency={trade.outputAmount.currency} size={'20px'} style={{ marginRight: '12px' }} />
<Text fontSize={20} fontWeight={500}>
{trade.outputAmount.currency.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<ThemedText.Body color={theme.text3} fontWeight={500} fontSize={14}>
<Trans>To</Trans>
</ThemedText.Body>
<ThemedText.Body fontSize={14} color={theme.text3}>
<FiatValue
fiatValue={fiatValueOutput}
@ -104,32 +110,14 @@ export default function SwapModalHeader({
/>
</ThemedText.Body>
</RowBetween>
<RowBetween align="flex-end">
<RowFixed gap={'0px'}>
<CurrencyLogo currency={trade.outputAmount.currency} size={'20px'} style={{ marginRight: '12px' }} />
<Text fontSize={20} fontWeight={500}>
{trade.outputAmount.currency.symbol}
</Text>
</RowFixed>
<RowFixed gap={'0px'}>
<TruncatedText fontSize={24} fontWeight={500}>
{trade.outputAmount.toSignificant(6)}
</TruncatedText>
</RowFixed>
</RowBetween>
</AutoColumn>
</LightCard>
<RowBetween style={{ marginTop: '0.25rem', padding: '0 1rem' }}>
<ThemedText.Body color={theme.text2} fontWeight={500} fontSize={14}>
<Trans>Price</Trans>
</ThemedText.Body>
<TradePrice price={trade.executionPrice} showInverted={showInverted} setShowInverted={setShowInverted} />
</RowBetween>
<LightCard style={{ padding: '.75rem', marginTop: '0.5rem' }}>
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} />
</LightCard>
{showAcceptChanges ? (
<SwapShowAcceptChanges justify="flex-start" gap={'0px'}>
<RowBetween>

@ -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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
interface SwapRouteProps extends React.HTMLAttributes<HTMLDivElement> {
trade: InterfaceTrade<Currency, Currency, TradeType>
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 (
<AutoColumn gap="12px">
<RowBetween>
<Wrapper {...rest} darkMode={darkMode} fixedOpen={fixedOpen}>
<RowBetween onClick={() => setOpen(!open)}>
<AutoRow gap="4px" width="auto">
<AutoRouterLogo />
<AutoRouterLabel />
</AutoRow>
{syncing ? (
<LoadingRows>
<div style={{ width: '30px', height: '24px' }} />
</LoadingRows>
) : (
<Badge>
<ThemedText.Black fontSize={12}>
{getTradeVersion(trade) === Version.v2 ? <Trans>V2</Trans> : <Trans>V3</Trans>}
</ThemedText.Black>
</Badge>
)}
{fixedOpen ? null : <OpenCloseIcon open={open} />}
</RowBetween>
<Separator />
{syncing ? (
<LoadingRows>
<div style={{ width: '400px', height: '30px' }} />
</LoadingRows>
) : (
<RoutingDiagram
currencyIn={trade.inputAmount.currency}
currencyOut={trade.outputAmount.currency}
routes={getTokenPath(trade)}
/>
)}
{routingAPIEnabled && (
<ThemedText.Main fontSize={12} width={400}>
<Trans>This route optimizes your price by considering split routes, multiple hops, and gas costs.</Trans>
</ThemedText.Main>
)}
</AutoColumn>
<AnimatedDropdown open={open || fixedOpen}>
<AutoRow gap="4px" width="auto" style={{ paddingTop: '12px', margin: 0 }}>
{syncing ? (
<LoadingRows>
<div style={{ width: '400px', height: '30px' }} />
</LoadingRows>
) : (
<RoutingDiagram
currencyIn={trade.inputAmount.currency}
currencyOut={trade.outputAmount.currency}
routes={routes}
/>
)}
<Separator />
{autoRouterSupported &&
(syncing ? (
<LoadingRows>
<div style={{ width: '250px', height: '15px' }} />
</LoadingRows>
) : (
<ThemedText.Main fontSize={12} width={400} margin={0}>
{trade?.gasUseEstimateUSD && chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? (
<Trans>Best price route costs ~{formattedGasPriceString} in gas. </Trans>
) : null}{' '}
<Trans>
This route optimizes your total output by considering split routes, multiple hops, and the gas cost of
each step.
</Trans>
</ThemedText.Main>
))}
</AutoRow>
</AnimatedDropdown>
</Wrapper>
)
})
function getTokenPath(
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
): RoutingDiagramEntry[] {
// convert V2 path to a list of routes
if (trade instanceof V2Trade) {
const { path: tokenPath } = (trade as V2Trade<Currency, Currency, TradeType>).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<Currency, Currency, TradeType>): 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,
}
})
}

@ -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<Token> | null
trade: InterfaceTrade<Currency, Currency, TradeType> | 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 (
<Wrapper>
{showNetworkFeeWarning ? (
<RowBetween>
<ThemedText.Main fontSize="14px" color="text3">
<Trans>Network fees exceed 50% of the swap amount!</Trans>
</ThemedText.Main>
<MouseoverTooltipContent
wrap={false}
content={
<ResponsiveTooltipContainer origin="top right" style={{ padding: '12px' }}>
<ThemedText.Main fontSize="12px" color="text3" maxWidth="200px">
<Trans>
The cost of sending this transaction is more than half of the value of the input amount.
</Trans>
</ThemedText.Main>
<ThemedText.Main fontSize="12px" color="text3" maxWidth="200px" mt="8px">
<Trans>You might consider waiting until the network fees go down to complete this transaction.</Trans>
</ThemedText.Main>
</ResponsiveTooltipContainer>
}
placement="bottom"
>
<StyledInfoIcon size={16} />
</MouseoverTooltipContent>
</RowBetween>
) : null}
</Wrapper>
)
}

@ -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 (
<StyledPriceContainer onClick={flipPrice} title={text}>
<Text fontWeight={500} fontSize={14} color={theme.text1}>
<StyledPriceContainer
onClick={(e) => {
e.stopPropagation() // dont want this click to affect dropdowns / hovers
flipPrice()
}}
title={text}
>
<Text fontWeight={500} color={theme.text1}>
{text}
</Text>{' '}
{usdcPrice && (

@ -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}`,

@ -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,

@ -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<Currency>,
spender?: string
): [ApprovalState, () => Promise<void>] {
const { account, chainId } = useActiveWeb3React()
export function useApprovalState(amountToApprove?: CurrencyAmount<Currency>, 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<Currency, Currency, TradeType> | 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<Currency>,
spender?: string
): [ApprovalState, () => Promise<void>] {
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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined,
trade:
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
| 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<Currency, Currency, TradeType> | undefined,
allowedSlippage: Percent
):
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
| 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])
}

@ -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))
}

@ -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<typeof useDebounce>
// 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<typeof useRoutingAPIEnabled>
const mockUseDebounce = useDebounce as jest.MockedFunction<typeof useDebounce>
const mockUseAutoRouterSupported = useAutoRouterSupported as jest.MockedFunction<typeof useAutoRouterSupported>
const mockUseIsWindowVisible = useIsWindowVisible as jest.MockedFunction<typeof useIsWindowVisible>
// useRouterTrade mocks
const mockUseRoutingAPITrade = useRoutingAPITrade as jest.MockedFunction<typeof useRoutingAPITrade>
// useClientSideV3Trade mocks
const mockUseClientSideV3Trade = useClientSideV3Trade as jest.MockedFunction<typeof useClientSideV3Trade>
// 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 })
})
})
})

@ -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<Currency>,
otherCurrency?: Currency
): {
state: V3TradeState
trade: Trade<Currency, Currency, typeof tradeType> | null
state: TradeState
trade: InterfaceTrade<Currency, Currency, TradeType> | 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 } : {}),
}
}

@ -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<TTradeType extends TradeType>(
tradeType: TTradeType,
amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency
): { state: V3TradeState; trade: Trade<Currency, Currency, TTradeType> | null } {
): { state: TradeState; trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined } {
const [currencyIn, currencyOut] = useMemo(
() =>
tradeType === TradeType.EXACT_INPUT
@ -61,15 +61,15 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
: 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<TTradeType extends TradeType>(
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])

@ -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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined,
trade:
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
| 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)
}

@ -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<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | 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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | 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

@ -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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | 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<Currency, Currency, TradeType> | 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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined
trade: InterfaceTrade<Currency, Currency, TradeType> | 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)
}

@ -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
}
}

@ -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<Token> } = {
export const STABLECOIN_AMOUNT_OUT: { [chainId: number]: CurrencyAmount<Token> } = {
[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<Token> } = {
* @param currency currency to compute the USDC price of
*/
export default function useUSDCPrice(currency?: Currency): Price<Currency, Token> | 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<Currency, Token
const { numerator, denominator } = v2USDCTrade.route.midPrice
return new Price(currency, stablecoin, denominator, numerator)
} else if (v3USDCTrade.trade) {
const { numerator, denominator } = v3USDCTrade.trade.route.midPrice
const { numerator, denominator } = v3USDCTrade.trade.routes[0].midPrice
return new Price(currency, stablecoin, denominator, numerator)
}
@ -65,3 +67,27 @@ export function useUSDCValue(currencyAmount: CurrencyAmount<Currency> | 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
}
}

@ -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;
`};
`

@ -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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined
tradeToConfirm: Trade<Currency, Currency, TradeType> | 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<boolean>(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<boolean>(false)
@ -454,64 +450,16 @@ export default function Swap({ history }: RouteComponentProps) {
<AddressInputPanel id="recipient" value={recipient} onChange={onChangeRecipient} />
</>
) : null}
{!showWrap && trade && (
<Row justify={!trade ? 'center' : 'space-between'}>
<RowFixed style={{ position: 'relative' }}>
<MouseoverTooltipContent
wrap={false}
content={
<ResponsiveTooltipContainer>
<SwapRoute trade={trade} syncing={routeIsSyncing} />
</ResponsiveTooltipContainer>
}
placement="bottom"
onOpen={() =>
ReactGA.event({
category: 'Swap',
action: 'Router Tooltip Open',
})
}
>
<AutoRow gap="4px" width="auto">
<AutoRouterLogo />
<LoadingOpacityContainer $loading={routeIsSyncing}>
{trade instanceof V3Trade && trade.swaps.length > 1 && (
<ThemedText.Blue fontSize={14}>{trade.swaps.length} routes</ThemedText.Blue>
)}
</LoadingOpacityContainer>
</AutoRow>
</MouseoverTooltipContent>
</RowFixed>
<RowFixed>
<LoadingOpacityContainer $loading={routeIsSyncing}>
<TradePrice
price={trade.executionPrice}
showInverted={showInverted}
setShowInverted={setShowInverted}
/>
</LoadingOpacityContainer>
<MouseoverTooltipContent
wrap={false}
content={
<ResponsiveTooltipContainer origin="top right" width={'295px'}>
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={routeIsSyncing} />
</ResponsiveTooltipContainer>
}
placement="bottom"
onOpen={() =>
ReactGA.event({
category: 'Swap',
action: 'Transaction Details Tooltip Open',
})
}
>
<StyledInfo />
</MouseoverTooltipContent>
</RowFixed>
</Row>
{!showWrap && userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing) && (
<SwapDetailsDropdown
trade={trade}
syncing={routeIsSyncing}
loading={routeIsLoading}
showInverted={showInverted}
setShowInverted={setShowInverted}
allowedSlippage={allowedSlippage}
/>
)}
<div>
{swapIsUnsupported ? (
<ButtonPrimary disabled={true}>
@ -532,15 +480,7 @@ export default function Swap({ history }: RouteComponentProps) {
<Trans>Unwrap</Trans>
) : null)}
</ButtonPrimary>
) : routeIsSyncing || routeIsLoading ? (
<GreyCard style={{ textAlign: 'center' }}>
<ThemedText.Main mb="4px">
<Dots>
<Trans>Loading</Trans>
</Dots>
</ThemedText.Main>
</GreyCard>
) : routeNotFound && userHasSpecifiedInputOutput ? (
) : routeNotFound && userHasSpecifiedInputOutput && !routeIsLoading && !routeIsSyncing ? (
<GreyCard style={{ textAlign: 'center' }}>
<ThemedText.Main mb="4px">
<Trans>Insufficient liquidity for this trade.</Trans>
@ -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) {
<Text fontSize={16} fontWeight={500}>
{priceImpactTooHigh ? (
<Trans>High Price Impact</Trans>
) : priceImpactSeverity > 2 ? (
) : trade && priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : (
<Trans>Swap</Trans>
@ -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}
>
<Text fontSize={20} fontWeight={500}>
{swapInputError ? (
swapInputError
) : priceImpactTooHigh ? (
<Trans>Price Impact Too High</Trans>
) : routeIsSyncing || routeIsLoading ? (
<Trans>Swap</Trans>
) : priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : priceImpactTooHigh ? (
<Trans>Price Impact Too High</Trans>
) : (
<Trans>Swap</Trans>
)}

@ -0,0 +1,3 @@
import { ChainId } from '@uniswap/smart-order-router'
export const AUTO_ROUTER_SUPPORTED_CHAINS: ChainId[] = Object.values(ChainId) as number[]

@ -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())

@ -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<AlphaRouterConfig>
): 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) }
}

@ -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,
},
}),
}),
})

@ -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<Token, 'address' | 'chainId' | 'symbol' | 'decimals'>
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<V3PoolInRoute[] | V2PoolInRoute[]>
routeString: string
}
export class InterfaceTrade<
TInput extends Currency,
TOutput extends Currency,
TTradeType extends TradeType
> extends Trade<TInput, TOutput, TTradeType> {
gasUseEstimateUSD: CurrencyAmount<Token> | null | undefined
constructor({
gasUseEstimateUSD,
...routes
}: {
gasUseEstimateUSD?: CurrencyAmount<Token> | undefined | null
v2Routes: {
routev2: V2Route<TInput, TOutput>
inputAmount: CurrencyAmount<TInput>
outputAmount: CurrencyAmount<TOutput>
}[]
v3Routes: {
routev3: V3Route<TInput, TOutput>
inputAmount: CurrencyAmount<TInput>
outputAmount: CurrencyAmount<TOutput>
}[]
tradeType: TTradeType
}) {
super(routes)
this.gasUseEstimateUSD = gasUseEstimateUSD
}
}

@ -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<T>(data: T, dataBlockNumber: number, maxBlockAge = 10): T | undefined {
const localBlockNumber = useBlockNumber()
@ -35,22 +36,31 @@ function useRoutingAPIArguments({
amount: CurrencyAmount<Currency> | 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<TTradeType extends TradeType>(
tradeType: TTradeType,
amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency
): { state: V3TradeState; trade: Trade<Currency, Currency, TTradeType> | null } {
): {
state: TradeState
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined
} {
const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo(
() =>
tradeType === TradeType.EXACT_INPUT
@ -76,30 +89,33 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
})
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<TTradeType extends TradeType>(
? 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<Currency, Currency, TTradeType>({
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])
}

@ -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')
})
})
})

@ -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<GetQuoteResult, 'route'> | undefined
):
| {
route: Route<Currency, Currency>
inputAmount: CurrencyAmount<Currency>
outputAmount: CurrencyAmount<Currency>
}[]
| 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<TTradeType extends TradeType>(
route: ReturnType<typeof computeRoutes>,
tradeType: TTradeType,
gasUseEstimateUSD?: CurrencyAmount<Token> | null
): InterfaceTrade<Currency, Currency, TTradeType> {
return new InterfaceTrade({
v2Routes:
route
?.filter((r): r is typeof route[0] & { routev2: NonNullable<typeof route[0]['routev2']> } => r.routev2 !== null)
.map(({ routev2, inputAmount, outputAmount }) => ({ routev2, inputAmount, outputAmount })) ?? [],
v3Routes:
route
?.filter((r): r is typeof route[0] & { routev3: NonNullable<typeof route[0]['routev3']> } => 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'
}

@ -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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>,
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<Currency> }
parsedAmount: CurrencyAmount<Currency> | undefined
inputError?: ReactNode
v2Trade: V2Trade<Currency, Currency, TradeType> | undefined
v3Trade: {
trade: V3Trade<Currency, Currency, TradeType> | null
state: V3TradeState
trade: {
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
state: TradeState
}
bestTrade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | 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 = <Trans>Connect Wallet</Trans>
}
if (!parsedAmount) {
inputError = inputError ?? <Trans>Enter an amount</Trans>
}
if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) {
inputError = inputError ?? <Trans>Select a token</Trans>
}
if (!parsedAmount) {
inputError = inputError ?? <Trans>Enter an amount</Trans>
}
const formattedTo = isAddress(to)
if (!to || !formattedTo) {
inputError = inputError ?? <Trans>Enter a recipient</Trans>
} else {
if (BAD_RECIPIENT_ADDRESSES[formattedTo] || (v2Trade && involvesAddress(v2Trade, formattedTo))) {
if (BAD_RECIPIENT_ADDRESSES[formattedTo]) {
inputError = inputError ?? <Trans>Invalid recipient</Trans>
}
}
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 = <Trans>Insufficient {amountIn.currency.symbol} balance</Trans>
@ -231,9 +182,7 @@ export function useDerivedSwapInfo(toggledVersion: Version | undefined): {
currencyBalances,
parsedAmount,
inputError,
v2Trade: v2Trade ?? undefined,
v3Trade,
bestTrade: bestTrade ?? undefined,
trade,
allowedSlippage,
}
}

@ -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()

@ -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};
`

@ -10,3 +10,5 @@ export type TupleSplit<T, N extends number, O extends readonly any[] = readonly
export type TakeFirst<T extends readonly any[], N extends number> = TupleSplit<T, N>[0]
export type SkipFirst<T extends readonly any[], N extends number> = TupleSplit<T, N>[1]
export type NonNullable<T> = T extends null | undefined ? never : T

@ -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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
): Version | undefined {
if (!trade) return undefined
if (trade instanceof V2Trade) return Version.v2
return Version.v3
}

@ -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)
})
})

@ -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
}

@ -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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined | null,
tradeB: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined | null,
tradeA: V2Trade<Currency, Currency, TradeType> | undefined | null,
tradeB: V2Trade<Currency, Currency, TradeType> | undefined | null,
minimumDelta: Percent = ZERO_PERCENT
): boolean | undefined {
if (tradeA && !tradeB) return false

@ -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))
})
})

@ -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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
): Percent {
export function computeRealizedLPFeePercent(trade: Trade<Currency, Currency, TradeType>): 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<Percent>(
trade.swaps.reduce<Percent>(
(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<Percent>(
(currentFee: Percent, pool): Percent =>
currentFee.multiply(ONE_HUNDRED_PERCENT.subtract(new Fraction(pool.fee, 1_000_000))),
ONE_HUNDRED_PERCENT
)
swap.route.pools.reduce<Percent>((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<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | null
trade?: Trade<Currency, Currency, TradeType> | null
): CurrencyAmount<Currency> | undefined {
if (trade) {
const realizedLPFee = computeRealizedLPFeePercent(trade)

@ -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<Currency>,
{
quote,
quoteGasAdjusted,
route,
estimatedGasUsed,
estimatedGasUsedQuoteToken,
estimatedGasUsedUSD,
gasPriceWei,
methodParameters,
blockNumber,
}: SwapRoute
): GetQuoteResult {
const routeResponse: Array<V3PoolInRoute[] | V2PoolInRoute[]> = []
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
}

716
yarn.lock

File diff suppressed because it is too large Load Diff