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:
parent
642a4177d8
commit
9e1a775c13
@ -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);
|
||||
|
6
src/assets/images/gas-icon.svg
Normal file
6
src/assets/images/gas-icon.svg
Normal file
@ -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 |
12
src/assets/images/router-icon-grey.svg
Normal file
12
src/assets/images/router-icon-grey.svg
Normal file
@ -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 |
34
src/components/AnimatedDropdown/index.tsx
Normal file
34
src/components/AnimatedDropdown/index.tsx
Normal file
@ -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} 
|
||||
<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} 
|
||||
</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]
|
||||
)
|
||||
|
||||
|
105
src/components/swap/GasEstimateBadge.tsx
Normal file
105
src/components/swap/GasEstimateBadge.tsx
Normal file
@ -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}>
|
||||
|
202
src/components/swap/SwapDetailsDropdown.tsx
Normal file
202
src/components/swap/SwapDetailsDropdown.tsx
Normal file
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
75
src/components/swap/SwapWarningDropdown.tsx
Normal file
75
src/components/swap/SwapWarningDropdown.tsx
Normal file
@ -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])
|
||||
}
|
||||
|
8
src/hooks/useAutoRouterSupported.tsx
Normal file
8
src/hooks/useAutoRouterSupported.tsx
Normal file
@ -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[]
|
43
src/state/routing/clientSideSmartOrderRouter/dependencies.ts
Normal file
43
src/state/routing/clientSideSmartOrderRouter/dependencies.ts
Normal file
@ -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())
|
52
src/state/routing/clientSideSmartOrderRouter/index.ts
Normal file
52
src/state/routing/clientSideSmartOrderRouter/index.ts
Normal file
@ -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
|
||||
}
|
102
src/utils/getTxOptimizedSwapRouter.test.ts
Normal file
102
src/utils/getTxOptimizedSwapRouter.test.ts
Normal file
@ -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)
|
||||
})
|
||||
})
|
32
src/utils/getTxOptimizedSwapRouter.ts
Normal file
32
src/utils/getTxOptimizedSwapRouter.ts
Normal file
@ -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)
|
||||
|
150
src/utils/transformSwapRouteToGetQuoteResult.ts
Normal file
150
src/utils/transformSwapRouteToGetQuoteResult.ts
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user