Commit 9e1a775c authored by Justin Domingue's avatar Justin Domingue Committed by GitHub

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: default avatarLint Action <lint-action@samuelmeuli.com>
Co-authored-by: default avatarIan Lapham <ian@uniswap.org>
Co-authored-by: default avatarCallil Capuozzo <callil.capuozzo@gmail.com>
parent 642a4177
...@@ -67,6 +67,7 @@ ...@@ -67,6 +67,7 @@
html { html {
font-size: 16px; font-size: 16px;
font-variant: none; font-variant: none;
font-smooth: always;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
......
<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>
<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>
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)< ...@@ -33,6 +33,7 @@ export const BaseButton = styled(RebassButton)<
position: relative; position: relative;
z-index: 1; z-index: 1;
&:disabled { &:disabled {
opacity: 50%;
cursor: auto; cursor: auto;
pointer-events: none; pointer-events: none;
} }
...@@ -236,7 +237,7 @@ const ButtonConfirmedStyle = styled(BaseButton)` ...@@ -236,7 +237,7 @@ const ButtonConfirmedStyle = styled(BaseButton)`
/* border: 1px solid ${({ theme }) => theme.green1}; */ /* border: 1px solid ${({ theme }) => theme.green1}; */
&:disabled { &:disabled {
/* opacity: 50%; */ opacity: 50%;
background-color: ${({ theme }) => theme.bg2}; background-color: ${({ theme }) => theme.bg2};
color: ${({ theme }) => theme.text2}; color: ${({ theme }) => theme.text2};
cursor: auto; cursor: auto;
......
import { Trans } from '@lingui/macro' 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 { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import HoverInlineText from 'components/HoverInlineText' import HoverInlineText from 'components/HoverInlineText'
import { useMemo } from 'react' import { useMemo } from 'react'
...@@ -6,6 +8,7 @@ import { useMemo } from 'react' ...@@ -6,6 +8,7 @@ import { useMemo } from 'react'
import useTheme from '../../hooks/useTheme' import useTheme from '../../hooks/useTheme'
import { ThemedText } from '../../theme' import { ThemedText } from '../../theme'
import { warningSeverity } from '../../utils/prices' import { warningSeverity } from '../../utils/prices'
import { MouseoverTooltip } from '../Tooltip'
export function FiatValue({ export function FiatValue({
fiatValue, fiatValue,
...@@ -25,10 +28,14 @@ export function FiatValue({ ...@@ -25,10 +28,14 @@ export function FiatValue({
}, [priceImpact, theme.green1, theme.red1, theme.text3, theme.yellow1]) }, [priceImpact, theme.green1, theme.red1, theme.text3, theme.yellow1])
return ( return (
<ThemedText.Body fontSize={14} color={fiatValue ? theme.text2 : theme.text4}> <ThemedText.Body fontSize={14} color={fiatValue ? theme.text3 : theme.text4}>
{fiatValue ? ( {fiatValue ? (
<Trans> <Trans>
~$ <HoverInlineText text={fiatValue?.toSignificant(6, { groupSeparator: ',' })} /> $
<HoverInlineText
text={fiatValue?.toSignificant(6, { groupSeparator: ',' })}
textColor={fiatValue ? theme.text3 : theme.text4}
/>
</Trans> </Trans>
) : ( ) : (
'' ''
...@@ -36,7 +43,9 @@ export function FiatValue({ ...@@ -36,7 +43,9 @@ export function FiatValue({
{priceImpact ? ( {priceImpact ? (
<span style={{ color: priceImpactColor }}> <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> </span>
) : null} ) : null}
</ThemedText.Body> </ThemedText.Body>
......
...@@ -29,6 +29,8 @@ const InputPanel = styled.div<{ hideInput?: boolean }>` ...@@ -29,6 +29,8 @@ const InputPanel = styled.div<{ hideInput?: boolean }>`
background-color: ${({ theme, hideInput }) => (hideInput ? 'transparent' : theme.bg2)}; background-color: ${({ theme, hideInput }) => (hideInput ? 'transparent' : theme.bg2)};
z-index: 1; z-index: 1;
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')}; width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
transition: height 1s ease;
will-change: height;
` `
const FixedContainer = styled.div` const FixedContainer = styled.div`
...@@ -36,8 +38,7 @@ const FixedContainer = styled.div` ...@@ -36,8 +38,7 @@ const FixedContainer = styled.div`
height: 100%; height: 100%;
position: absolute; position: absolute;
border-radius: 20px; border-radius: 20px;
background-color: ${({ theme }) => theme.bg1}; background-color: ${({ theme }) => theme.bg2};
opacity: 0.95;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
...@@ -46,7 +47,7 @@ const FixedContainer = styled.div` ...@@ -46,7 +47,7 @@ const FixedContainer = styled.div`
const Container = styled.div<{ hideInput: boolean }>` const Container = styled.div<{ hideInput: boolean }>`
border-radius: ${({ hideInput }) => (hideInput ? '16px' : '20px')}; 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}; background-color: ${({ theme }) => theme.bg1};
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')}; width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
:focus, :focus,
...@@ -56,35 +57,35 @@ const Container = styled.div<{ hideInput: boolean }>` ...@@ -56,35 +57,35 @@ const Container = styled.div<{ hideInput: boolean }>`
` `
const CurrencySelect = styled(ButtonGray)<{ visible: boolean; selected: boolean; hideInput?: boolean }>` const CurrencySelect = styled(ButtonGray)<{ visible: boolean; selected: boolean; hideInput?: boolean }>`
visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')};
align-items: center; align-items: center;
font-size: 24px; background-color: ${({ selected, theme }) => (selected ? theme.bg2 : theme.primary1)};
font-weight: 500;
background-color: ${({ selected, theme }) => (selected ? theme.bg0 : theme.primary1)};
color: ${({ selected, theme }) => (selected ? theme.text1 : theme.white)};
border-radius: 16px;
box-shadow: ${({ selected }) => (selected ? 'none' : '0px 6px 10px rgba(0, 0, 0, 0.075)')}; 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); box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
outline: none; color: ${({ selected, theme }) => (selected ? theme.text1 : theme.white)};
cursor: pointer; cursor: pointer;
border-radius: 16px;
outline: none;
user-select: none; user-select: none;
border: none; border: none;
font-size: 24px;
font-weight: 500;
height: ${({ hideInput }) => (hideInput ? '2.8rem' : '2.4rem')}; height: ${({ hideInput }) => (hideInput ? '2.8rem' : '2.4rem')};
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')}; width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
padding: 0 8px; padding: 0 8px;
justify-content: space-between; justify-content: space-between;
margin-right: ${({ hideInput }) => (hideInput ? '0' : '12px')}; margin-left: ${({ hideInput }) => (hideInput ? '0' : '12px')};
:focus, :focus,
:hover { :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 }>` const InputRow = styled.div<{ selected: boolean }>`
${({ theme }) => theme.flexRowNoWrap} ${({ theme }) => theme.flexRowNoWrap}
align-items: center; align-items: center;
justify-content: space-between; 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` const LabelRow = styled.div`
...@@ -128,28 +129,30 @@ const StyledTokenName = styled.span<{ active?: boolean }>` ...@@ -128,28 +129,30 @@ const StyledTokenName = styled.span<{ active?: boolean }>`
const StyledBalanceMax = styled.button<{ disabled?: boolean }>` const StyledBalanceMax = styled.button<{ disabled?: boolean }>`
background-color: transparent; background-color: transparent;
background-color: ${({ theme }) => theme.primary5};
border: none; border: none;
border-radius: 12px; border-radius: 12px;
font-size: 14px; color: ${({ theme }) => theme.primary1};
font-weight: 500;
cursor: pointer; cursor: pointer;
padding: 0; font-size: 11px;
color: ${({ theme }) => theme.primaryText1}; font-weight: 500;
margin-left: 0.25rem;
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)}; opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
padding: 4px 6px;
pointer-events: ${({ disabled }) => (!disabled ? 'initial' : 'none')}; pointer-events: ${({ disabled }) => (!disabled ? 'initial' : 'none')};
margin-left: 0.25rem;
:hover {
opacity: ${({ disabled }) => (!disabled ? 0.8 : 0.4)};
}
:focus { :focus {
outline: none; outline: none;
} }
${({ theme }) => theme.mediaWidth.upToExtraSmall`
margin-right: 0.5rem;
`};
` `
const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean }>` const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean }>`
${loadingOpacityMixin} ${loadingOpacityMixin};
text-align: left;
` `
interface CurrencyInputPanelProps { interface CurrencyInputPanelProps {
...@@ -220,6 +223,15 @@ export default function CurrencyInputPanel({ ...@@ -220,6 +223,15 @@ export default function CurrencyInputPanel({
)} )}
<Container hideInput={hideInput}> <Container hideInput={hideInput}>
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={!onCurrencySelect}> <InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={!onCurrencySelect}>
{!hideInput && (
<StyledNumericalInput
className="token-amount-input"
value={value}
onUserInput={onUserInput}
$loading={loading}
/>
)}
<CurrencySelect <CurrencySelect
visible={currency !== undefined} visible={currency !== undefined}
selected={!!currency} selected={!!currency}
...@@ -257,24 +269,19 @@ export default function CurrencyInputPanel({ ...@@ -257,24 +269,19 @@ export default function CurrencyInputPanel({
{onCurrencySelect && <StyledDropDown selected={!!currency} />} {onCurrencySelect && <StyledDropDown selected={!!currency} />}
</Aligner> </Aligner>
</CurrencySelect> </CurrencySelect>
{!hideInput && (
<StyledNumericalInput
className="token-amount-input"
value={value}
onUserInput={onUserInput}
$loading={loading}
/>
)}
</InputRow> </InputRow>
{!hideInput && !hideBalance && ( {!hideInput && !hideBalance && currency && (
<FiatRow> <FiatRow>
<RowBetween> <RowBetween>
<LoadingOpacityContainer $loading={loading}>
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
</LoadingOpacityContainer>
{account ? ( {account ? (
<RowFixed style={{ height: '17px' }}> <RowFixed style={{ height: '17px' }}>
<ThemedText.Body <ThemedText.Body
onClick={onMax} onClick={onMax}
color={theme.text2} color={theme.text3}
fontWeight={400} fontWeight={500}
fontSize={14} fontSize={14}
style={{ display: 'inline', cursor: 'pointer' }} style={{ display: 'inline', cursor: 'pointer' }}
> >
...@@ -282,24 +289,19 @@ export default function CurrencyInputPanel({ ...@@ -282,24 +289,19 @@ export default function CurrencyInputPanel({
renderBalance ? ( renderBalance ? (
renderBalance(selectedCurrencyBalance) renderBalance(selectedCurrencyBalance)
) : ( ) : (
<Trans> <Trans>Balance: {formatCurrencyAmount(selectedCurrencyBalance, 4)}</Trans>
Balance: {formatCurrencyAmount(selectedCurrencyBalance, 4)} {currency.symbol}
</Trans>
) )
) : null} ) : null}
</ThemedText.Body> </ThemedText.Body>
{showMaxButton && selectedCurrencyBalance ? ( {showMaxButton && selectedCurrencyBalance ? (
<StyledBalanceMax onClick={onMax}> <StyledBalanceMax onClick={onMax}>
<Trans>(Max)</Trans> <Trans>MAX</Trans>
</StyledBalanceMax> </StyledBalanceMax>
) : null} ) : null}
</RowFixed> </RowFixed>
) : ( ) : (
<span /> <span />
)} )}
<LoadingOpacityContainer $loading={loading}>
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
</LoadingOpacityContainer>
</RowBetween> </RowBetween>
</FiatRow> </FiatRow>
)} )}
......
...@@ -37,16 +37,24 @@ export const getTokenLogoURL = ( ...@@ -37,16 +37,24 @@ export const getTokenLogoURL = (
const StyledEthereumLogo = styled.img<{ size: string }>` const StyledEthereumLogo = styled.img<{ size: string }>`
width: ${({ size }) => size}; width: ${({ size }) => size};
height: ${({ size }) => size}; height: ${({ size }) => size};
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075); background: radial-gradient(white 50%, #ffffff00 calc(75% + 1px), #ffffff00 100%);
border-radius: 24px;
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 }>` const StyledLogo = styled(Logo)<{ size: string }>`
width: ${({ size }) => size}; width: ${({ size }) => size};
height: ${({ size }) => size}; height: ${({ size }) => size};
border-radius: ${({ size }) => size}; background: radial-gradient(white 50%, #ffffff00 calc(75% + 1px), #ffffff00 100%);
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075); border-radius: 50%;
background-color: ${({ theme }) => theme.white}; -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({ export default function CurrencyLogo({
......
...@@ -34,17 +34,16 @@ const ActiveRowLinkList = styled.div` ...@@ -34,17 +34,16 @@ const ActiveRowLinkList = styled.div`
text-decoration: none; text-decoration: none;
} }
& > a:first-child { & > a:first-child {
border-top: 1px solid ${({ theme }) => theme.text2};
margin: 0; margin: 0;
margin-top: 6px; margin-top: 0px;
padding-top: 10px; padding-top: 10px;
} }
` `
const ActiveRowWrapper = styled.div` const ActiveRowWrapper = styled.div`
background-color: ${({ theme }) => theme.bg2}; background-color: ${({ theme }) => theme.bg1};
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
padding: 8px 0 8px 0; padding: 8px;
width: 100%; width: 100%;
` `
const FlyoutHeader = styled.div` const FlyoutHeader = styled.div`
...@@ -53,7 +52,7 @@ const FlyoutHeader = styled.div` ...@@ -53,7 +52,7 @@ const FlyoutHeader = styled.div`
` `
const FlyoutMenu = styled.div` const FlyoutMenu = styled.div`
align-items: flex-start; 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), 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); 0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 20px; border-radius: 20px;
...@@ -75,7 +74,7 @@ const FlyoutMenu = styled.div` ...@@ -75,7 +74,7 @@ const FlyoutMenu = styled.div`
` `
const FlyoutRow = styled.div<{ active: boolean }>` const FlyoutRow = styled.div<{ active: boolean }>`
align-items: center; align-items: center;
background-color: ${({ active, theme }) => (active ? theme.bg2 : 'transparent')}; background-color: ${({ active, theme }) => (active ? theme.bg1 : 'transparent')};
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
...@@ -113,8 +112,8 @@ const SelectorLabel = styled(NetworkLabel)` ...@@ -113,8 +112,8 @@ const SelectorLabel = styled(NetworkLabel)`
` `
const SelectorControls = styled.div<{ interactive: boolean }>` const SelectorControls = styled.div<{ interactive: boolean }>`
align-items: center; align-items: center;
background-color: ${({ theme }) => theme.bg1}; background-color: ${({ theme }) => theme.bg0};
border: 2px solid ${({ theme }) => theme.bg1}; border: 2px solid ${({ theme }) => theme.bg0};
border-radius: 12px; border-radius: 12px;
color: ${({ theme }) => theme.text1}; color: ${({ theme }) => theme.text1};
cursor: ${({ interactive }) => (interactive ? 'pointer' : 'auto')}; cursor: ${({ interactive }) => (interactive ? 'pointer' : 'auto')};
......
import { Trans } from '@lingui/macro'
import { RowFixed } from 'components/Row'
import { CHAIN_INFO } from 'constants/chains' import { CHAIN_INFO } from 'constants/chains'
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp' import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
import useGasPrice from 'hooks/useGasPrice'
import useMachineTimeMs from 'hooks/useMachineTime' import useMachineTimeMs from 'hooks/useMachineTime'
import useTheme from 'hooks/useTheme'
import { useActiveWeb3React } from 'hooks/web3' import { useActiveWeb3React } from 'hooks/web3'
import JSBI from 'jsbi'
import ms from 'ms.macro' import ms from 'ms.macro'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useBlockNumber } from 'state/application/hooks' import { useBlockNumber } from 'state/application/hooks'
...@@ -9,6 +14,7 @@ import styled, { keyframes } from 'styled-components/macro' ...@@ -9,6 +14,7 @@ import styled, { keyframes } from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme' import { ExternalLink, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { MouseoverTooltip } from '../Tooltip'
import { ChainConnectivityWarning } from './ChainConnectivityWarning' import { ChainConnectivityWarning } from './ChainConnectivityWarning'
const StyledPolling = styled.div<{ warning: boolean }>` const StyledPolling = styled.div<{ warning: boolean }>`
...@@ -31,6 +37,14 @@ const StyledPollingNumber = styled(ThemedText.Small)<{ breathe: boolean; hoverin ...@@ -31,6 +37,14 @@ const StyledPollingNumber = styled(ThemedText.Small)<{ breathe: boolean; hoverin
:hover { :hover {
opacity: 1; opacity: 1;
} }
a {
color: unset;
}
a:hover {
text-decoration: none;
color: unset;
}
` `
const StyledPollingDot = styled.div<{ warning: boolean }>` const StyledPollingDot = styled.div<{ warning: boolean }>`
width: 8px; width: 8px;
...@@ -43,6 +57,17 @@ const StyledPollingDot = styled.div<{ warning: boolean }>` ...@@ -43,6 +57,17 @@ const StyledPollingDot = styled.div<{ warning: boolean }>`
transition: 250ms ease background-color; 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` const rotate360 = keyframes`
from { from {
transform: rotate(0deg); transform: rotate(0deg);
...@@ -81,6 +106,10 @@ export default function Polling() { ...@@ -81,6 +106,10 @@ export default function Polling() {
const [isHover, setIsHover] = useState(false) const [isHover, setIsHover] = useState(false)
const machineTime = useMachineTimeMs(NETWORK_HEALTH_CHECK_MS) const machineTime = useMachineTimeMs(NETWORK_HEALTH_CHECK_MS)
const blockTime = useCurrentBlockTimestamp() const blockTime = useCurrentBlockTimestamp()
const theme = useTheme()
const ethGasPrice = useGasPrice()
const priceGwei = ethGasPrice ? JSBI.divide(ethGasPrice, JSBI.BigInt(1000000000)) : undefined
const waitMsBeforeWarning = const waitMsBeforeWarning =
(chainId ? CHAIN_INFO[chainId]?.blockWaitMsBeforeWarning : DEFAULT_MS_BEFORE_WARNING) ?? DEFAULT_MS_BEFORE_WARNING (chainId ? CHAIN_INFO[chainId]?.blockWaitMsBeforeWarning : DEFAULT_MS_BEFORE_WARNING) ?? DEFAULT_MS_BEFORE_WARNING
...@@ -105,19 +134,48 @@ export default function Polling() { ...@@ -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) //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 ( return (
<> <>
<ExternalLink <RowFixed>
href={chainId && blockNumber ? getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK) : ''}
>
<StyledPolling onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)} warning={warning}> <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}> <StyledPollingNumber breathe={isMounting} hovering={isHover}>
{blockNumber}&ensp; <ExternalLink
href={
chainId && blockNumber ? getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK) : ''
}
>
<MouseoverTooltip
text={<Trans>{`The most recent block number on this network. Prices update on every block.`}</Trans>}
>
{blockNumber}&ensp;
</MouseoverTooltip>
</ExternalLink>
</StyledPollingNumber> </StyledPollingNumber>
<StyledPollingDot warning={warning}>{isMounting && <Spinner warning={warning} />}</StyledPollingDot>{' '} <StyledPollingDot warning={warning}>{isMounting && <Spinner warning={warning} />}</StyledPollingDot>{' '}
</StyledPolling> </StyledPolling>
</ExternalLink> {warning && <ChainConnectivityWarning />}
{warning && <ChainConnectivityWarning />} </RowFixed>
</> </>
) )
} }
...@@ -2,9 +2,15 @@ import Tooltip from 'components/Tooltip' ...@@ -2,9 +2,15 @@ import Tooltip from 'components/Tooltip'
import { useState } from 'react' import { useState } from 'react'
import styled from 'styled-components/macro' 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'}; 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'}; font-size: ${({ fontSize }) => fontSize ?? 'inherit'};
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
...@@ -18,6 +24,7 @@ const HoverInlineText = ({ ...@@ -18,6 +24,7 @@ const HoverInlineText = ({
margin = false, margin = false,
adjustSize = false, adjustSize = false,
fontSize, fontSize,
textColor,
link, link,
...rest ...rest
}: { }: {
...@@ -26,6 +33,7 @@ const HoverInlineText = ({ ...@@ -26,6 +33,7 @@ const HoverInlineText = ({
margin?: boolean margin?: boolean
adjustSize?: boolean adjustSize?: boolean
fontSize?: string fontSize?: string
textColor?: string
link?: boolean link?: boolean
}) => { }) => {
const [showHover, setShowHover] = useState(false) const [showHover, setShowHover] = useState(false)
...@@ -42,6 +50,7 @@ const HoverInlineText = ({ ...@@ -42,6 +50,7 @@ const HoverInlineText = ({
onMouseLeave={() => setShowHover(false)} onMouseLeave={() => setShowHover(false)}
margin={margin} margin={margin}
adjustSize={adjustSize} adjustSize={adjustSize}
textColor={textColor}
link={link} link={link}
fontSize={fontSize} fontSize={fontSize}
{...rest} {...rest}
...@@ -53,7 +62,14 @@ const HoverInlineText = ({ ...@@ -53,7 +62,14 @@ const HoverInlineText = ({
} }
return ( return (
<TextWrapper margin={margin} adjustSize={adjustSize} link={link} fontSize={fontSize} {...rest}> <TextWrapper
margin={margin}
adjustSize={adjustSize}
link={link}
fontSize={fontSize}
textColor={textColor}
{...rest}
>
{text} {text}
</TextWrapper> </TextWrapper>
) )
......
...@@ -31,7 +31,11 @@ export default function Identicon() { ...@@ -31,7 +31,11 @@ export default function Identicon() {
if (icon) { if (icon) {
current?.appendChild(icon) current?.appendChild(icon)
return () => { return () => {
current?.removeChild(icon) try {
current?.removeChild(icon)
} catch (e) {
console.error('Avatar icon not found')
}
} }
} }
return return
......
...@@ -12,7 +12,7 @@ const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: s ...@@ -12,7 +12,7 @@ const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: s
border: none; border: none;
flex: 1 1 auto; flex: 1 1 auto;
background-color: ${({ theme }) => theme.bg1}; background-color: ${({ theme }) => theme.bg1};
font-size: ${({ fontSize }) => fontSize ?? '24px'}; font-size: ${({ fontSize }) => fontSize ?? '28px'};
text-align: ${({ align }) => align && align}; text-align: ${({ align }) => align && align};
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
......
import { Protocol } from '@uniswap/router-sdk'
import { Currency, Percent } from '@uniswap/sdk-core' import { Currency, Percent } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk' import { FeeAmount } from '@uniswap/v3-sdk'
import { DAI, USDC, WBTC } from 'constants/tokens' import { DAI, USDC, WBTC } from 'constants/tokens'
...@@ -7,16 +8,21 @@ import RoutingDiagram, { RoutingDiagramEntry } from './RoutingDiagram' ...@@ -7,16 +8,21 @@ import RoutingDiagram, { RoutingDiagramEntry } from './RoutingDiagram'
const percent = (strings: TemplateStringsArray) => new Percent(parseInt(strings[0]), 100) 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[] = [ 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`, percent: percent`25`,
path: [ path: [
[USDC, WBTC, FeeAmount.MEDIUM], [USDC, WBTC, FeeAmount.MEDIUM],
[WBTC, DAI, FeeAmount.HIGH], [WBTC, DAI, FeeAmount.HIGH],
], ],
protocol: Protocol.V3,
}, },
] ]
......
import { Trans } from '@lingui/macro'
import { Protocol } from '@uniswap/router-sdk'
import { Currency, Percent } from '@uniswap/sdk-core' import { Currency, Percent } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk' import { FeeAmount } from '@uniswap/v3-sdk'
import Badge from 'components/Badge' import Badge from 'components/Badge'
...@@ -7,24 +9,24 @@ import Row, { AutoRow } from 'components/Row' ...@@ -7,24 +9,24 @@ import Row, { AutoRow } from 'components/Row'
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList' import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
import { Box } from 'rebass' import { Box } from 'rebass'
import styled from 'styled-components/macro' 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 { ReactComponent as DotLine } from '../../assets/svg/dot_line.svg'
import { MouseoverTooltip } from '../Tooltip'
export interface RoutingDiagramEntry { export interface RoutingDiagramEntry {
percent: Percent percent: Percent
path: [Currency, Currency, FeeAmount][] path: [Currency, Currency, FeeAmount][]
protocol: Protocol
} }
const Wrapper = styled(Box)` const Wrapper = styled(Box)`
align-items: center; align-items: center;
background-color: ${({ theme }) => theme.bg0}; width: 100%;
width: 400px;
` `
const RouteContainerRow = styled(Row)` const RouteContainerRow = styled(Row)`
display: grid; display: grid;
grid-gap: 8px;
grid-template-columns: 24px 1fr 24px; grid-template-columns: 24px 1fr 24px;
` `
...@@ -38,7 +40,7 @@ const RouteRow = styled(Row)` ...@@ -38,7 +40,7 @@ const RouteRow = styled(Row)`
const PoolBadge = styled(Badge)` const PoolBadge = styled(Badge)`
display: flex; display: flex;
padding: 0.25rem 0.5rem; padding: 4px 4px;
` `
const DottedLine = styled.div` const DottedLine = styled.div`
...@@ -58,7 +60,27 @@ const DotColor = styled(DotLine)` ...@@ -58,7 +60,27 @@ const DotColor = styled(DotLine)`
const OpaqueBadge = styled(Badge)` const OpaqueBadge = styled(Badge)`
background-color: ${({ theme }) => theme.bg2}; 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({ export default function RoutingDiagram({
...@@ -75,29 +97,31 @@ export default function RoutingDiagram({ ...@@ -75,29 +97,31 @@ export default function RoutingDiagram({
return ( return (
<Wrapper> <Wrapper>
{routes.map(({ percent, path }, index) => ( {routes.map((entry, index) => (
<RouteContainerRow key={index}> <RouteContainerRow key={index}>
<CurrencyLogo currency={tokenIn} /> <CurrencyLogo currency={tokenIn} size={'20px'} />
<Route percent={percent} path={path} /> <Route entry={entry} />
<CurrencyLogo currency={tokenOut} /> <CurrencyLogo currency={tokenOut} size={'20px'} />
</RouteContainerRow> </RouteContainerRow>
))} ))}
</Wrapper> </Wrapper>
) )
} }
function Route({ percent, path }: { percent: RoutingDiagramEntry['percent']; path: RoutingDiagramEntry['path'] }) { function Route({ entry: { percent, path, protocol } }: { entry: RoutingDiagramEntry }) {
return ( return (
<RouteRow> <RouteRow>
<DottedLine> <DottedLine>
<DotColor /> <DotColor />
</DottedLine> </DottedLine>
<OpaqueBadge> <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)}% {percent.toSignificant(2)}%
</ThemedText.Small> </BadgeText>
</OpaqueBadge> </OpaqueBadge>
<AutoRow gap="1px" width="100%" style={{ justifyContent: 'space-evenly', zIndex: 2 }}> <AutoRow gap="1px" width="100%" style={{ justifyContent: 'space-evenly', zIndex: 2 }}>
{path.map(([currency0, currency1, feeAmount], index) => ( {path.map(([currency0, currency1, feeAmount], index) => (
<Pool key={index} currency0={currency0} currency1={currency1} feeAmount={feeAmount} /> <Pool key={index} currency0={currency0} currency1={currency1} feeAmount={feeAmount} />
...@@ -111,12 +135,17 @@ function Pool({ currency0, currency1, feeAmount }: { currency0: Currency; curren ...@@ -111,12 +135,17 @@ function Pool({ currency0, currency1, feeAmount }: { currency0: Currency; curren
const tokenInfo0 = useTokenInfoFromActiveList(currency0) const tokenInfo0 = useTokenInfoFromActiveList(currency0)
const tokenInfo1 = useTokenInfoFromActiveList(currency1) const tokenInfo1 = useTokenInfoFromActiveList(currency1)
// TODO - link pool icon to info.uniswap.org via query params
return ( return (
<PoolBadge> <MouseoverTooltip
<Box margin="0 5px 0 10px"> text={<Trans>{tokenInfo0?.symbol + '/' + tokenInfo1?.symbol + ' ' + feeAmount / 10000}% pool</Trans>}
<DoubleCurrencyLogo currency0={tokenInfo1} currency1={tokenInfo0} size={20} /> >
</Box> <PoolBadge>
<ThemedText.Small fontSize={12}>{feeAmount / 10000}%</ThemedText.Small> <Box margin="0 4px 0 12px">
</PoolBadge> <DoubleCurrencyLogo currency0={tokenInfo1} currency1={tokenInfo0} size={20} />
</Box>
<ThemedText.Small fontSize={14}>{feeAmount / 10000}%</ThemedText.Small>
</PoolBadge>
</MouseoverTooltip>
) )
} }
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
exports[`renders multi route 1`] = ` exports[`renders multi route 1`] = `
<DocumentFragment> <DocumentFragment>
<div <div
class="RoutingDiagram__Wrapper-sc-o1ook0-0 fUoVYh css-vurnku" class="RoutingDiagram__Wrapper-sc-o1ook0-0 ePDWDk css-vurnku"
> >
<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 CurrencyLogo currency=USDC
<div <div
...@@ -22,11 +22,20 @@ exports[`renders multi route 1`] = ` ...@@ -22,11 +22,20 @@ exports[`renders multi route 1`] = `
</svg> </svg>
</div> </div>
<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 <div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9" class="Badge-sc-1mhw5si-0 RoutingDiagram__ProtocolBadge-sc-o1ook0-7 gayll bNVqMw"
style="word-break: normal;" >
<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% 75%
</div> </div>
...@@ -36,26 +45,13 @@ exports[`renders multi route 1`] = ` ...@@ -36,26 +45,13 @@ exports[`renders multi route 1`] = `
style="justify-content: space-evenly; z-index: 2;" style="justify-content: space-evenly; z-index: 2;"
width="100%" width="100%"
> >
<div Popover
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>
</div> </div>
</div> </div>
CurrencyLogo currency=DAI CurrencyLogo currency=DAI
</div> </div>
<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 CurrencyLogo currency=USDC
<div <div
...@@ -71,11 +67,20 @@ exports[`renders multi route 1`] = ` ...@@ -71,11 +67,20 @@ exports[`renders multi route 1`] = `
</svg> </svg>
</div> </div>
<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 <div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9" class="Badge-sc-1mhw5si-0 RoutingDiagram__ProtocolBadge-sc-o1ook0-7 gayll bNVqMw"
style="word-break: normal;" >
<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% 25%
</div> </div>
...@@ -85,34 +90,7 @@ exports[`renders multi route 1`] = ` ...@@ -85,34 +90,7 @@ exports[`renders multi route 1`] = `
style="justify-content: space-evenly; z-index: 2;" style="justify-content: space-evenly; z-index: 2;"
width="100%" width="100%"
> >
<div PopoverPopover
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>
</div> </div>
</div> </div>
CurrencyLogo currency=DAI CurrencyLogo currency=DAI
...@@ -124,10 +102,10 @@ exports[`renders multi route 1`] = ` ...@@ -124,10 +102,10 @@ exports[`renders multi route 1`] = `
exports[`renders single route 1`] = ` exports[`renders single route 1`] = `
<DocumentFragment> <DocumentFragment>
<div <div
class="RoutingDiagram__Wrapper-sc-o1ook0-0 fUoVYh css-vurnku" class="RoutingDiagram__Wrapper-sc-o1ook0-0 ePDWDk css-vurnku"
> >
<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 CurrencyLogo currency=USDC
<div <div
...@@ -143,11 +121,20 @@ exports[`renders single route 1`] = ` ...@@ -143,11 +121,20 @@ exports[`renders single route 1`] = `
</svg> </svg>
</div> </div>
<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 <div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9" class="Badge-sc-1mhw5si-0 RoutingDiagram__ProtocolBadge-sc-o1ook0-7 gayll bNVqMw"
style="word-break: normal;" >
<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% 100%
</div> </div>
...@@ -157,20 +144,7 @@ exports[`renders single route 1`] = ` ...@@ -157,20 +144,7 @@ exports[`renders single route 1`] = `
style="justify-content: space-evenly; z-index: 2;" style="justify-content: space-evenly; z-index: 2;"
width="100%" width="100%"
> >
<div Popover
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>
</div> </div>
</div> </div>
CurrencyLogo currency=DAI CurrencyLogo currency=DAI
...@@ -182,7 +156,7 @@ exports[`renders single route 1`] = ` ...@@ -182,7 +156,7 @@ exports[`renders single route 1`] = `
exports[`renders when no routes are provided 1`] = ` exports[`renders when no routes are provided 1`] = `
<DocumentFragment> <DocumentFragment>
<div <div
class="RoutingDiagram__Wrapper-sc-o1ook0-0 fUoVYh css-vurnku" class="RoutingDiagram__Wrapper-sc-o1ook0-0 ePDWDk css-vurnku"
/> />
</DocumentFragment> </DocumentFragment>
`; `;
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { t, Trans } from '@lingui/macro' import { t, Trans } from '@lingui/macro'
import { Percent } from '@uniswap/sdk-core' import { Percent } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains'
import { useActiveWeb3React } from 'hooks/web3' import { useActiveWeb3React } from 'hooks/web3'
import { useContext, useRef, useState } from 'react' import { useContext, useRef, useState } from 'react'
import { Settings, X } from 'react-feather' import { Settings, X } from 'react-feather'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { Text } from 'rebass' import { Text } from 'rebass'
import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'state/routing/clientSideSmartOrderRouter/constants'
import styled, { ThemeContext } from 'styled-components/macro' import styled, { ThemeContext } from 'styled-components/macro'
import { useOnClickOutside } from '../../hooks/useOnClickOutside' import { useOnClickOutside } from '../../hooks/useOnClickOutside'
...@@ -27,7 +27,7 @@ const StyledMenuIcon = styled(Settings)` ...@@ -27,7 +27,7 @@ const StyledMenuIcon = styled(Settings)`
width: 20px; width: 20px;
> * { > * {
stroke: ${({ theme }) => theme.text2}; stroke: ${({ theme }) => theme.text1};
} }
:hover { :hover {
...@@ -199,16 +199,13 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa ...@@ -199,16 +199,13 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
<Text fontWeight={600} fontSize={14}> <Text fontWeight={600} fontSize={14}>
<Trans>Interface Settings</Trans> <Trans>Interface Settings</Trans>
</Text> </Text>
{chainId && AUTO_ROUTER_SUPPORTED_CHAINS.includes(chainId) && (
{chainId === SupportedChainId.MAINNET && (
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
<ThemedText.Black fontWeight={400} fontSize={14} color={theme.text2}> <ThemedText.Black fontWeight={400} fontSize={14} color={theme.text2}>
<Trans>Auto Router</Trans> <Trans>Auto Router API</Trans>
</ThemedText.Black> </ThemedText.Black>
<QuestionHelper <QuestionHelper text={<Trans>Use the Uniswap Labs API to get faster quotes.</Trans>} />
text={<Trans>Use the Uniswap Labs API to get better pricing through a more efficient route.</Trans>}
/>
</RowFixed> </RowFixed>
<Toggle <Toggle
id="toggle-optimized-router-button" id="toggle-optimized-router-button"
...@@ -223,7 +220,6 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa ...@@ -223,7 +220,6 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
/> />
</RowBetween> </RowBetween>
)} )}
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
<ThemedText.Black fontWeight={400} fontSize={14} color={theme.text2}> <ThemedText.Black fontWeight={400} fontSize={14} color={theme.text2}>
......
...@@ -5,7 +5,7 @@ import styled from 'styled-components/macro' ...@@ -5,7 +5,7 @@ import styled from 'styled-components/macro'
import Popover, { PopoverProps } from '../Popover' import Popover, { PopoverProps } from '../Popover'
export const TooltipContainer = styled.div` export const TooltipContainer = styled.div`
width: 256px; max-width: 256px;
padding: 0.6rem 1rem; padding: 0.6rem 1rem;
font-weight: 400; font-weight: 400;
word-break: break-word; word-break: break-word;
...@@ -25,6 +25,7 @@ interface TooltipContentProps extends Omit<PopoverProps, 'content'> { ...@@ -25,6 +25,7 @@ interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
onOpen?: () => void onOpen?: () => void
// whether to wrap the content in a `TooltipContainer` // whether to wrap the content in a `TooltipContainer`
wrap?: boolean wrap?: boolean
disableHover?: boolean // disable the hover and content display
} }
export default function Tooltip({ text, ...rest }: TooltipProps) { export default function Tooltip({ text, ...rest }: TooltipProps) {
...@@ -52,6 +53,7 @@ export function MouseoverTooltipContent({ ...@@ -52,6 +53,7 @@ export function MouseoverTooltipContent({
content, content,
children, children,
onOpen: openCallback = undefined, onOpen: openCallback = undefined,
disableHover,
...rest ...rest
}: Omit<TooltipContentProps, 'show'>) { }: Omit<TooltipContentProps, 'show'>) {
const [show, setShow] = useState(false) const [show, setShow] = useState(false)
...@@ -61,7 +63,7 @@ export function MouseoverTooltipContent({ ...@@ -61,7 +63,7 @@ export function MouseoverTooltipContent({
}, [openCallback]) }, [openCallback])
const close = useCallback(() => setShow(false), [setShow]) const close = useCallback(() => setShow(false), [setShow])
return ( return (
<TooltipContent {...rest} show={show} content={content}> <TooltipContent {...rest} show={show} content={disableHover ? null : content}>
<div <div
style={{ display: 'inline-block', lineHeight: 0, padding: '0.25rem' }} style={{ display: 'inline-block', lineHeight: 0, padding: '0.25rem' }}
onMouseEnter={open} onMouseEnter={open}
......
...@@ -110,7 +110,7 @@ const HoverText = styled.div` ...@@ -110,7 +110,7 @@ const HoverText = styled.div`
` `
const LinkCard = styled(Card)` const LinkCard = styled(Card)`
background-color: ${({ theme }) => theme.primary1}; background-color: ${({ theme }) => theme.bg1};
color: ${({ theme }) => theme.white}; color: ${({ theme }) => theme.white};
:hover { :hover {
...@@ -402,6 +402,16 @@ export default function WalletModal({ ...@@ -402,6 +402,16 @@ export default function WalletModal({
</ThemedText.Black> </ThemedText.Black>
</AutoRow> </AutoRow>
</LightCard> </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)}> <LinkCard padding=".5rem" $borderRadius=".75rem" onClick={() => setWalletView(WALLET_VIEWS.LEGAL)}>
<RowBetween> <RowBetween>
<AutoRow gap="4px"> <AutoRow gap="4px">
...@@ -413,16 +423,6 @@ export default function WalletModal({ ...@@ -413,16 +423,6 @@ export default function WalletModal({
<ArrowRight size={16} /> <ArrowRight size={16} />
</RowBetween> </RowBetween>
</LinkCard> </LinkCard>
{walletView === WALLET_VIEWS.PENDING ? (
<PendingView
connector={pendingWallet}
error={pendingError}
setPendingError={setPendingError}
tryActivation={tryActivation}
/>
) : (
<OptionGrid>{getOptions()}</OptionGrid>
)}
</AutoColumn> </AutoColumn>
</ContentWrapper> </ContentWrapper>
</UpperSection> </UpperSection>
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk' import Card from 'components/Card'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { LoadingRows } from 'components/Loader/styled' import { LoadingRows } from 'components/Loader/styled'
import { useActiveWeb3React } from 'hooks/web3'
import { useContext, useMemo } from 'react' 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 { computeRealizedLPFeePercent } from '../../utils/prices'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row' import { RowBetween, RowFixed } from '../Row'
import FormattedPriceImpact from './FormattedPriceImpact' import FormattedPriceImpact from './FormattedPriceImpact'
import { TransactionDetailsLabel } from './styleds' import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from './GasEstimateBadge'
const StyledCard = styled(Card)`
padding: 0;
`
interface AdvancedSwapDetailsProps { interface AdvancedSwapDetailsProps {
trade?: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> trade?: InterfaceTrade<Currency, Currency, TradeType>
allowedSlippage: Percent allowedSlippage: Percent
syncing?: boolean syncing?: boolean
hideRouteDiagram?: boolean
} }
function TextWithLoadingPlaceholder({ function TextWithLoadingPlaceholder({
...@@ -39,74 +45,78 @@ function TextWithLoadingPlaceholder({ ...@@ -39,74 +45,78 @@ function TextWithLoadingPlaceholder({
export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) { export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const { chainId } = useActiveWeb3React()
const { realizedLPFee, priceImpact } = useMemo(() => { const { expectedOutputAmount, priceImpact } = useMemo(() => {
if (!trade) return { realizedLPFee: undefined, priceImpact: undefined } if (!trade) return { expectedOutputAmount: undefined, priceImpact: undefined }
const expectedOutputAmount = trade.outputAmount
const realizedLpFeePercent = computeRealizedLPFeePercent(trade) const realizedLpFeePercent = computeRealizedLPFeePercent(trade)
const realizedLPFee = trade.inputAmount.multiply(realizedLpFeePercent)
const priceImpact = trade.priceImpact.subtract(realizedLpFeePercent) const priceImpact = trade.priceImpact.subtract(realizedLpFeePercent)
return { priceImpact, realizedLPFee } return { expectedOutputAmount, priceImpact }
}, [trade]) }, [trade])
return !trade ? null : ( return !trade ? null : (
<AutoColumn gap="8px"> <StyledCard>
<TransactionDetailsLabel fontWeight={500} fontSize={14}> <AutoColumn gap="8px">
<Trans>Transaction Details</Trans> <RowBetween>
</TransactionDetailsLabel> <RowFixed>
<RowBetween> <ThemedText.SubHeader color={theme.text1}>
<RowFixed> <Trans>Expected Output</Trans>
<ThemedText.SubHeader color={theme.text1}> </ThemedText.SubHeader>
<Trans>Liquidity Provider Fee</Trans> </RowFixed>
</ThemedText.SubHeader> <TextWithLoadingPlaceholder syncing={syncing} width={65}>
</RowFixed> <ThemedText.Black textAlign="right" fontSize={14}>
<TextWithLoadingPlaceholder syncing={syncing} width={65}> {expectedOutputAmount
<ThemedText.Black textAlign="right" fontSize={14}> ? `${expectedOutputAmount.toSignificant(6)} ${expectedOutputAmount.currency.symbol}`
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${realizedLPFee.currency.symbol}` : '-'} : '-'}
</ThemedText.Black> </ThemedText.Black>
</TextWithLoadingPlaceholder> </TextWithLoadingPlaceholder>
</RowBetween> </RowBetween>
<RowBetween>
<RowBetween> <RowFixed>
<RowFixed> <ThemedText.SubHeader color={theme.text1}>
<ThemedText.SubHeader color={theme.text1}> <Trans>Price Impact</Trans>
<Trans>Price Impact</Trans> </ThemedText.SubHeader>
</ThemedText.SubHeader> </RowFixed>
</RowFixed> <TextWithLoadingPlaceholder syncing={syncing} width={50}>
<TextWithLoadingPlaceholder syncing={syncing} width={50}> <ThemedText.Black textAlign="right" fontSize={14}>
<ThemedText.Black textAlign="right" fontSize={14}> <FormattedPriceImpact priceImpact={priceImpact} />
<FormattedPriceImpact priceImpact={priceImpact} /> </ThemedText.Black>
</ThemedText.Black> </TextWithLoadingPlaceholder>
</TextWithLoadingPlaceholder> </RowBetween>
</RowBetween> <Separator />
<RowBetween>
<RowBetween> <RowFixed style={{ marginRight: '20px' }}>
<RowFixed> <ThemedText.SubHeader color={theme.text3}>
<ThemedText.SubHeader color={theme.text1}> {trade.tradeType === TradeType.EXACT_INPUT ? (
<Trans>Allowed Slippage</Trans> <Trans>Minimum received</Trans>
</ThemedText.SubHeader> ) : (
</RowFixed> <Trans>Maximum sent</Trans>
<TextWithLoadingPlaceholder syncing={syncing} width={45}> )}{' '}
<ThemedText.Black textAlign="right" fontSize={14}> <Trans>after slippage</Trans> ({allowedSlippage.toFixed(2)}%)
{allowedSlippage.toFixed(2)}% </ThemedText.SubHeader>
</ThemedText.Black> </RowFixed>
</TextWithLoadingPlaceholder> <TextWithLoadingPlaceholder syncing={syncing} width={70}>
</RowBetween> <ThemedText.Black textAlign="right" fontSize={14} color={theme.text3}>
{trade.tradeType === TradeType.EXACT_INPUT
<RowBetween> ? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}`
<RowFixed> : `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`}
<ThemedText.SubHeader color={theme.text1}> </ThemedText.Black>
{trade.tradeType === TradeType.EXACT_INPUT ? <Trans>Minimum received</Trans> : <Trans>Maximum sent</Trans>} </TextWithLoadingPlaceholder>
</ThemedText.SubHeader> </RowBetween>
</RowFixed> {!trade?.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
<TextWithLoadingPlaceholder syncing={syncing} width={70}> <RowBetween>
<ThemedText.Black textAlign="right" fontSize={14}> <ThemedText.SubHeader color={theme.text3}>
{trade.tradeType === TradeType.EXACT_INPUT <Trans>Network Fee</Trans>
? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}` </ThemedText.SubHeader>
: `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`} <TextWithLoadingPlaceholder syncing={syncing} width={50}>
</ThemedText.Black> <ThemedText.Black textAlign="right" fontSize={14} color={theme.text3}>
</TextWithLoadingPlaceholder> ~${trade.gasUseEstimateUSD.toFixed(2)}
</RowBetween> </ThemedText.Black>
</AutoColumn> </TextWithLoadingPlaceholder>
</RowBetween>
)}
</AutoColumn>
</StyledCard>
) )
} }
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' 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 { ReactNode, useCallback, useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import TransactionConfirmationModal, { import TransactionConfirmationModal, {
ConfirmationModalContent, ConfirmationModalContent,
...@@ -16,9 +16,7 @@ import SwapModalHeader from './SwapModalHeader' ...@@ -16,9 +16,7 @@ import SwapModalHeader from './SwapModalHeader'
* @param args either a pair of V2 trades or a pair of V3 trades * @param args either a pair of V2 trades or a pair of V3 trades
*/ */
function tradeMeaningfullyDiffers( function tradeMeaningfullyDiffers(
...args: ...args: [Trade<Currency, Currency, TradeType>, Trade<Currency, Currency, TradeType>]
| [V2Trade<Currency, Currency, TradeType>, V2Trade<Currency, Currency, TradeType>]
| [V3Trade<Currency, Currency, TradeType>, V3Trade<Currency, Currency, TradeType>]
): boolean { ): boolean {
const [tradeA, tradeB] = args const [tradeA, tradeB] = args
return ( return (
...@@ -44,8 +42,8 @@ export default function ConfirmSwapModal({ ...@@ -44,8 +42,8 @@ export default function ConfirmSwapModal({
txHash, txHash,
}: { }: {
isOpen: boolean isOpen: boolean
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
originalTrade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined originalTrade: Trade<Currency, Currency, TradeType> | undefined
attemptingTxn: boolean attemptingTxn: boolean
txHash: string | undefined txHash: string | undefined
recipient: string | null recipient: string | null
...@@ -56,15 +54,7 @@ export default function ConfirmSwapModal({ ...@@ -56,15 +54,7 @@ export default function ConfirmSwapModal({
onDismiss: () => void onDismiss: () => void
}) { }) {
const showAcceptChanges = useMemo( const showAcceptChanges = useMemo(
() => () => Boolean(trade && originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)),
Boolean(
(trade instanceof V2Trade &&
originalTrade instanceof V2Trade &&
tradeMeaningfullyDiffers(trade, originalTrade)) ||
(trade instanceof V3Trade &&
originalTrade instanceof V3Trade &&
tradeMeaningfullyDiffers(trade, originalTrade))
),
[originalTrade, trade] [originalTrade, trade]
) )
......
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>
)
}
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { useRoutingAPIEnabled } from 'state/user/hooks' import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
...@@ -40,15 +40,15 @@ const StyledAutoRouterLabel = styled(ThemedText.Black)` ...@@ -40,15 +40,15 @@ const StyledAutoRouterLabel = styled(ThemedText.Black)`
` `
export function AutoRouterLogo() { export function AutoRouterLogo() {
const routingAPIEnabled = useRoutingAPIEnabled() const autoRouterSupported = useAutoRouterSupported()
return routingAPIEnabled ? <StyledAutoRouterIcon /> : <StyledStaticRouterIcon /> return autoRouterSupported ? <StyledAutoRouterIcon /> : <StyledStaticRouterIcon />
} }
export function AutoRouterLabel() { export function AutoRouterLabel() {
const routingAPIEnabled = useRoutingAPIEnabled() const autoRouterSupported = useAutoRouterSupported()
return routingAPIEnabled ? ( return autoRouterSupported ? (
<StyledAutoRouterLabel fontSize={14}>Auto Router</StyledAutoRouterLabel> <StyledAutoRouterLabel fontSize={14}>Auto Router</StyledAutoRouterLabel>
) : ( ) : (
<ThemedText.Black fontSize={14}> <ThemedText.Black fontSize={14}>
......
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>
)
}
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Trade } from '@uniswap/router-sdk'
import { Currency, TradeType } from '@uniswap/sdk-core' 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 { ReactNode } from 'react'
import { Text } from 'rebass' import { Text } from 'rebass'
...@@ -14,7 +13,7 @@ export default function SwapModalFooter({ ...@@ -14,7 +13,7 @@ export default function SwapModalFooter({
swapErrorMessage, swapErrorMessage,
disabledConfirm, disabledConfirm,
}: { }: {
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> trade: Trade<Currency, Currency, TradeType>
onConfirm: () => void onConfirm: () => void
swapErrorMessage: ReactNode | undefined swapErrorMessage: ReactNode | undefined
disabledConfirm: boolean disabledConfirm: boolean
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' 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 { useContext, useState } from 'react'
import { AlertTriangle, ArrowDown } from 'react-feather' import { AlertTriangle, ArrowDown } from 'react-feather'
import { Text } from 'rebass' import { Text } from 'rebass'
import { InterfaceTrade } from 'state/routing/types'
import styled, { ThemeContext } from 'styled-components/macro' import styled, { ThemeContext } from 'styled-components/macro'
import { useUSDCValue } from '../../hooks/useUSDCPrice' import { useUSDCValue } from '../../hooks/useUSDCPrice'
...@@ -46,7 +45,7 @@ export default function SwapModalHeader({ ...@@ -46,7 +45,7 @@ export default function SwapModalHeader({
showAcceptChanges, showAcceptChanges,
onAcceptChanges, onAcceptChanges,
}: { }: {
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> trade: InterfaceTrade<Currency, Currency, TradeType>
allowedSlippage: Percent allowedSlippage: Percent
recipient: string | null recipient: string | null
showAcceptChanges: boolean showAcceptChanges: boolean
...@@ -63,19 +62,7 @@ export default function SwapModalHeader({ ...@@ -63,19 +62,7 @@ export default function SwapModalHeader({
<AutoColumn gap={'4px'} style={{ marginTop: '1rem' }}> <AutoColumn gap={'4px'} style={{ marginTop: '1rem' }}>
<LightCard padding="0.75rem 1rem"> <LightCard padding="0.75rem 1rem">
<AutoColumn gap={'8px'}> <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"> <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'}> <RowFixed gap={'0px'}>
<TruncatedText <TruncatedText
fontSize={24} fontSize={24}
...@@ -85,6 +72,15 @@ export default function SwapModalHeader({ ...@@ -85,6 +72,15 @@ export default function SwapModalHeader({
{trade.inputAmount.toSignificant(6)} {trade.inputAmount.toSignificant(6)}
</TruncatedText> </TruncatedText>
</RowFixed> </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> </RowBetween>
</AutoColumn> </AutoColumn>
</LightCard> </LightCard>
...@@ -93,43 +89,35 @@ export default function SwapModalHeader({ ...@@ -93,43 +89,35 @@ export default function SwapModalHeader({
</ArrowWrapper> </ArrowWrapper>
<LightCard padding="0.75rem 1rem" style={{ marginBottom: '0.25rem' }}> <LightCard padding="0.75rem 1rem" style={{ marginBottom: '0.25rem' }}>
<AutoColumn gap={'8px'}> <AutoColumn gap={'8px'}>
<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}
priceImpact={computeFiatValuePriceImpact(fiatValueInput, fiatValueOutput)}
/>
</ThemedText.Body>
</RowBetween>
<RowBetween align="flex-end"> <RowBetween align="flex-end">
<RowFixed gap={'0px'}>
<TruncatedText fontSize={24} fontWeight={500}>
{trade.outputAmount.toSignificant(6)}
</TruncatedText>
</RowFixed>
<RowFixed gap={'0px'}> <RowFixed gap={'0px'}>
<CurrencyLogo currency={trade.outputAmount.currency} size={'20px'} style={{ marginRight: '12px' }} /> <CurrencyLogo currency={trade.outputAmount.currency} size={'20px'} style={{ marginRight: '12px' }} />
<Text fontSize={20} fontWeight={500}> <Text fontSize={20} fontWeight={500}>
{trade.outputAmount.currency.symbol} {trade.outputAmount.currency.symbol}
</Text> </Text>
</RowFixed> </RowFixed>
<RowFixed gap={'0px'}> </RowBetween>
<TruncatedText fontSize={24} fontWeight={500}> <RowBetween>
{trade.outputAmount.toSignificant(6)} <ThemedText.Body fontSize={14} color={theme.text3}>
</TruncatedText> <FiatValue
</RowFixed> fiatValue={fiatValueOutput}
priceImpact={computeFiatValuePriceImpact(fiatValueInput, fiatValueOutput)}
/>
</ThemedText.Body>
</RowBetween> </RowBetween>
</AutoColumn> </AutoColumn>
</LightCard> </LightCard>
<RowBetween style={{ marginTop: '0.25rem', padding: '0 1rem' }}> <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} /> <TradePrice price={trade.executionPrice} showInverted={showInverted} setShowInverted={setShowInverted} />
</RowBetween> </RowBetween>
<LightCard style={{ padding: '.75rem', marginTop: '0.5rem' }}> <LightCard style={{ padding: '.75rem', marginTop: '0.5rem' }}>
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} /> <AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} />
</LightCard> </LightCard>
{showAcceptChanges ? ( {showAcceptChanges ? (
<SwapShowAcceptChanges justify="flex-start" gap={'0px'}> <SwapShowAcceptChanges justify="flex-start" gap={'0px'}>
<RowBetween> <RowBetween>
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk' import { Pair } from '@uniswap/v2-sdk'
import { FeeAmount, Trade as V3Trade } from '@uniswap/v3-sdk' import AnimatedDropdown from 'components/AnimatedDropdown'
import Badge from 'components/Badge'
import { AutoColumn } from 'components/Column' import { AutoColumn } from 'components/Column'
import { LoadingRows } from 'components/Loader/styled' import { LoadingRows } from 'components/Loader/styled'
import RoutingDiagram, { RoutingDiagramEntry } from 'components/RoutingDiagram/RoutingDiagram' import RoutingDiagram, { RoutingDiagramEntry } from 'components/RoutingDiagram/RoutingDiagram'
import { AutoRow, RowBetween } from 'components/Row' import { AutoRow, RowBetween } from 'components/Row'
import { Version } from 'hooks/useToggledVersion' import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
import { memo } from 'react' import { useActiveWeb3React } from 'hooks/web3'
import { useRoutingAPIEnabled } from 'state/user/hooks' 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 styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { Separator, ThemedText } from 'theme'
import { getTradeVersion } from 'utils/getTradeVersion'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from './GasEstimateBadge'
import { AutoRouterLabel, AutoRouterLogo } from './RouterLabel' import { AutoRouterLabel, AutoRouterLogo } from './RouterLabel'
const Separator = styled.div` const Wrapper = styled(AutoColumn)<{ darkMode?: boolean; fixedOpen?: boolean }>`
border-top: 1px solid ${({ theme }) => theme.bg2}; padding: ${({ fixedOpen }) => (fixedOpen ? '12px' : '12px 8px 12px 12px')};
height: 1px; border-radius: 16px;
width: 100%; 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 const V2_DEFAULT_FEE_TIER = 3000
export default memo(function SwapRoute({ interface SwapRouteProps extends React.HTMLAttributes<HTMLDivElement> {
trade, trade: InterfaceTrade<Currency, Currency, TradeType>
syncing,
}: {
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
syncing: boolean syncing: boolean
}) { fixedOpen?: boolean // fixed in open state, hide open/close icon
const routingAPIEnabled = useRoutingAPIEnabled() }
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 ( return (
<AutoColumn gap="12px"> <Wrapper {...rest} darkMode={darkMode} fixedOpen={fixedOpen}>
<RowBetween> <RowBetween onClick={() => setOpen(!open)}>
<AutoRow gap="4px" width="auto"> <AutoRow gap="4px" width="auto">
<AutoRouterLogo /> <AutoRouterLogo />
<AutoRouterLabel /> <AutoRouterLabel />
</AutoRow> </AutoRow>
{syncing ? ( {fixedOpen ? null : <OpenCloseIcon open={open} />}
<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>
)}
</RowBetween> </RowBetween>
<Separator /> <AnimatedDropdown open={open || fixedOpen}>
{syncing ? ( <AutoRow gap="4px" width="auto" style={{ paddingTop: '12px', margin: 0 }}>
<LoadingRows> {syncing ? (
<div style={{ width: '400px', height: '30px' }} /> <LoadingRows>
</LoadingRows> <div style={{ width: '400px', height: '30px' }} />
) : ( </LoadingRows>
<RoutingDiagram ) : (
currencyIn={trade.inputAmount.currency} <RoutingDiagram
currencyOut={trade.outputAmount.currency} currencyIn={trade.inputAmount.currency}
routes={getTokenPath(trade)} currencyOut={trade.outputAmount.currency}
/> routes={routes}
)} />
{routingAPIEnabled && ( )}
<ThemedText.Main fontSize={12} width={400}> <Separator />
<Trans>This route optimizes your price by considering split routes, multiple hops, and gas costs.</Trans> {autoRouterSupported &&
</ThemedText.Main> (syncing ? (
)} <LoadingRows>
</AutoColumn> <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( function getTokenPath(trade: Trade<Currency, Currency, TradeType>): RoutingDiagramEntry[] {
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
): 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 }) => {
const portion = const portion =
trade.tradeType === TradeType.EXACT_INPUT trade.tradeType === TradeType.EXACT_INPUT
? inputAmount.divide(trade.inputAmount) ? inputAmount.divide(trade.inputAmount)
...@@ -94,18 +115,25 @@ function getTokenPath( ...@@ -94,18 +115,25 @@ function getTokenPath(
const percent = new Percent(portion.numerator, portion.denominator) 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++) { for (let i = 0; i < pools.length; i++) {
const nextPool = pools[i] const nextPool = pools[i]
const tokenIn = tokenPath[i] const tokenIn = tokenPath[i]
const tokenOut = tokenPath[i + 1] 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 { return {
percent, percent,
path, path,
protocol,
} }
}) })
} }
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 { ...@@ -13,16 +13,20 @@ interface TradePriceProps {
} }
const StyledPriceContainer = styled.button` const StyledPriceContainer = styled.button`
align-items: center;
background-color: transparent; background-color: transparent;
border: none; border: none;
cursor: pointer; cursor: pointer;
display: grid; align-items: center
height: 24px; justify-content: flex-start;
justify-content: center;
padding: 0; padding: 0;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
grid-gap: 0.25rem; 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) { export default function TradePrice({ price, showInverted, setShowInverted }: TradePriceProps) {
...@@ -44,8 +48,14 @@ export default function TradePrice({ price, showInverted, setShowInverted }: Tra ...@@ -44,8 +48,14 @@ export default function TradePrice({ price, showInverted, setShowInverted }: Tra
const text = `${'1 ' + labelInverted + ' = ' + formattedPrice ?? '-'} ${label}` const text = `${'1 ' + labelInverted + ' = ' + formattedPrice ?? '-'} ${label}`
return ( return (
<StyledPriceContainer onClick={flipPrice} title={text}> <StyledPriceContainer
<Text fontWeight={500} fontSize={14} color={theme.text1}> onClick={(e) => {
e.stopPropagation() // dont want this click to affect dropdowns / hovers
flipPrice()
}}
title={text}
>
<Text fontWeight={500} color={theme.text1}>
{text} {text}
</Text>{' '} </Text>{' '}
{usdcPrice && ( {usdcPrice && (
......
...@@ -19,7 +19,7 @@ if (typeof INFURA_KEY === 'undefined') { ...@@ -19,7 +19,7 @@ if (typeof INFURA_KEY === 'undefined') {
throw new Error(`REACT_APP_INFURA_KEY must be a defined environment variable`) 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.MAINNET]: `https://mainnet.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.RINKEBY]: `https://rinkeby.infura.io/v3/${INFURA_KEY}`, [SupportedChainId.RINKEBY]: `https://rinkeby.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.ROPSTEN]: `https://ropsten.infura.io/v3/${INFURA_KEY}`, [SupportedChainId.ROPSTEN]: `https://ropsten.infura.io/v3/${INFURA_KEY}`,
......
...@@ -16,7 +16,20 @@ export const MULTICALL_ADDRESS: AddressMap = { ...@@ -16,7 +16,20 @@ export const MULTICALL_ADDRESS: AddressMap = {
[SupportedChainId.ARBITRUM_RINKEBY]: '0xa501c031958F579dB7676fF1CE78AD305794d579', [SupportedChainId.ARBITRUM_RINKEBY]: '0xa501c031958F579dB7676fF1CE78AD305794d579',
} }
export const V2_FACTORY_ADDRESSES: AddressMap = constructSameAddressMap(V2_FACTORY_ADDRESS) export const V2_FACTORY_ADDRESSES: AddressMap = constructSameAddressMap(V2_FACTORY_ADDRESS)
export const V2_ROUTER_ADDRESS: AddressMap = constructSameAddressMap('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D') 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 * The oldest V0 governance address
...@@ -75,12 +88,7 @@ export const ENS_REGISTRAR_ADDRESSES: AddressMap = { ...@@ -75,12 +88,7 @@ export const ENS_REGISTRAR_ADDRESSES: AddressMap = {
export const SOCKS_CONTROLLER_ADDRESSES: AddressMap = { export const SOCKS_CONTROLLER_ADDRESSES: AddressMap = {
[SupportedChainId.MAINNET]: '0x65770b5283117639760beA3F867b69b3697a91dd', [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', [ export const V3_MIGRATOR_ADDRESSES: AddressMap = constructSameAddressMap('0xA5644E29708357803b5A882D272c41cC0dF92B34', [
SupportedChainId.ARBITRUM_ONE, SupportedChainId.ARBITRUM_ONE,
SupportedChainId.ARBITRUM_RINKEBY, SupportedChainId.ARBITRUM_RINKEBY,
......
import { MaxUint256 } from '@ethersproject/constants' import { MaxUint256 } from '@ethersproject/constants'
import { TransactionResponse } from '@ethersproject/providers' import { TransactionResponse } from '@ethersproject/providers'
import { Protocol, Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk' import { Pair, Route as V2Route, Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk' import { Pool, Route as V3Route, Trade as V3Trade } from '@uniswap/v3-sdk'
import { useCallback, useMemo } from 'react' 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 { TransactionType } from '../state/transactions/actions'
import { useHasPendingApproval, useTransactionAdder } from '../state/transactions/hooks' import { useHasPendingApproval, useTransactionAdder } from '../state/transactions/hooks'
import { calculateGasMargin } from '../utils/calculateGasMargin' import { calculateGasMargin } from '../utils/calculateGasMargin'
...@@ -20,18 +22,14 @@ export enum ApprovalState { ...@@ -20,18 +22,14 @@ export enum ApprovalState {
APPROVED = 'APPROVED', APPROVED = 'APPROVED',
} }
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns export function useApprovalState(amountToApprove?: CurrencyAmount<Currency>, spender?: string) {
export function useApproveCallback( const { account } = useActiveWeb3React()
amountToApprove?: CurrencyAmount<Currency>,
spender?: string
): [ApprovalState, () => Promise<void>] {
const { account, chainId } = useActiveWeb3React()
const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined
const currentAllowance = useTokenAllowance(token, account ?? undefined, spender) const currentAllowance = useTokenAllowance(token, account ?? undefined, spender)
const pendingApproval = useHasPendingApproval(token?.address, spender) const pendingApproval = useHasPendingApproval(token?.address, spender)
// check the current approval status return useMemo(() => {
const approvalState: ApprovalState = useMemo(() => {
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
if (amountToApprove.currency.isNative) return ApprovalState.APPROVED if (amountToApprove.currency.isNative) return ApprovalState.APPROVED
// we might not have enough data to know whether or not we need to approve // we might not have enough data to know whether or not we need to approve
...@@ -44,6 +42,40 @@ export function useApproveCallback( ...@@ -44,6 +42,40 @@ export function useApproveCallback(
: ApprovalState.NOT_APPROVED : ApprovalState.NOT_APPROVED
: ApprovalState.APPROVED : ApprovalState.APPROVED
}, [amountToApprove, currentAllowance, pendingApproval, spender]) }, [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 tokenContract = useTokenContract(token?.address)
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
...@@ -103,23 +135,91 @@ export function useApproveCallback( ...@@ -103,23 +135,91 @@ export function useApproveCallback(
// wraps useApproveCallback in the context of a swap // wraps useApproveCallback in the context of a swap
export function useApproveCallbackFromTrade( 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 allowedSlippage: Percent
) { ) {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const v3SwapRouterAddress = chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined
const amountToApprove = useMemo( const amountToApprove = useMemo(
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined), () => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
[trade, allowedSlippage] [trade, allowedSlippage]
) )
return useApproveCallback(
const approveCallback = useApproveCallback(
amountToApprove, amountToApprove,
chainId chainId
? trade instanceof V2Trade ? trade instanceof V2Trade
? V2_ROUTER_ADDRESS[chainId] ? V2_ROUTER_ADDRESS[chainId]
: trade instanceof V3Trade : trade instanceof V3Trade
? v3SwapRouterAddress ? V3_ROUTER_ADDRESS[chainId]
: undefined : SWAP_ROUTER_ADDRESSES[chainId]
: undefined : 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])
} }
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))
}
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { Trade } from '@uniswap/v3-sdk' import { InterfaceTrade, TradeState } from 'state/routing/types'
import { V3TradeState } from 'state/routing/types'
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade' import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { useRoutingAPIEnabled } from 'state/user/hooks'
import useAutoRouterSupported from './useAutoRouterSupported'
import { useClientSideV3Trade } from './useClientSideV3Trade' import { useClientSideV3Trade } from './useClientSideV3Trade'
import useDebounce from './useDebounce' import useDebounce from './useDebounce'
import useIsWindowVisible from './useIsWindowVisible' import useIsWindowVisible from './useIsWindowVisible'
/** /**
* Returns the best v3 trade for a desired swap. * Returns the best v2+v3 trade for a desired swap.
* Uses optimized routes from the Routing API and falls back to the v3 router.
* @param tradeType whether the swap is an exact in/out * @param tradeType whether the swap is an exact in/out
* @param amountSpecified the exact amount to swap in/out * @param amountSpecified the exact amount to swap in/out
* @param otherCurrency the desired output/payment currency * @param otherCurrency the desired output/payment currency
*/ */
export function useBestV3Trade( export function useBestTrade(
tradeType: TradeType, tradeType: TradeType,
amountSpecified?: CurrencyAmount<Currency>, amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency otherCurrency?: Currency
): { ): {
state: V3TradeState state: TradeState
trade: Trade<Currency, Currency, typeof tradeType> | null trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
} { } {
const routingAPIEnabled = useRoutingAPIEnabled() const autoRouterSupported = useAutoRouterSupported()
const isWindowVisible = useIsWindowVisible() const isWindowVisible = useIsWindowVisible()
const [debouncedAmount, debouncedOtherCurrency] = useDebounce([amountSpecified, otherCurrency], 200) const [debouncedAmount, debouncedOtherCurrency] = useDebounce([amountSpecified, otherCurrency], 200)
const routingAPITrade = useRoutingAPITrade( const routingAPITrade = useRoutingAPITrade(
tradeType, tradeType,
routingAPIEnabled && isWindowVisible ? debouncedAmount : undefined, autoRouterSupported && isWindowVisible ? debouncedAmount : undefined,
debouncedOtherCurrency debouncedOtherCurrency
) )
...@@ -48,18 +46,19 @@ export function useBestV3Trade( ...@@ -48,18 +46,19 @@ export function useBestV3Trade(
!amountSpecified.currency.equals(routingAPITrade.trade.outputAmount.currency) || !amountSpecified.currency.equals(routingAPITrade.trade.outputAmount.currency) ||
!debouncedOtherCurrency?.equals(routingAPITrade.trade.inputAmount.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( const bestV3Trade = useClientSideV3Trade(
tradeType, tradeType,
useFallback ? debouncedAmount : undefined, useFallback ? debouncedAmount : undefined,
useFallback ? debouncedOtherCurrency : undefined useFallback ? debouncedOtherCurrency : undefined
) )
// only return gas estimate from api if routing api trade is used
return { return {
...(useFallback ? bestV3Trade : routingAPITrade), ...(useFallback ? bestV3Trade : routingAPITrade),
...(debouncing ? { state: V3TradeState.SYNCING } : {}), ...(debouncing ? { state: TradeState.SYNCING } : {}),
...(isLoading ? { state: V3TradeState.LOADING } : {}), ...(isLoading ? { state: TradeState.LOADING } : {}),
} }
} }
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' 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 { SupportedChainId } from 'constants/chains'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { useMemo } from 'react' import { useMemo } from 'react'
import { V3TradeState } from 'state/routing/types' import { InterfaceTrade, TradeState } from 'state/routing/types'
import { useSingleContractWithCallData } from '../state/multicall/hooks' import { useSingleContractWithCallData } from '../state/multicall/hooks'
import { useAllV3Routes } from './useAllV3Routes' import { useAllV3Routes } from './useAllV3Routes'
...@@ -27,7 +27,7 @@ export function useClientSideV3Trade<TTradeType extends TradeType>( ...@@ -27,7 +27,7 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
tradeType: TTradeType, tradeType: TTradeType,
amountSpecified?: CurrencyAmount<Currency>, amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency otherCurrency?: Currency
): { state: V3TradeState; trade: Trade<Currency, Currency, TTradeType> | null } { ): { state: TradeState; trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined } {
const [currencyIn, currencyOut] = useMemo( const [currencyIn, currencyOut] = useMemo(
() => () =>
tradeType === TradeType.EXACT_INPUT tradeType === TradeType.EXACT_INPUT
...@@ -61,15 +61,15 @@ export function useClientSideV3Trade<TTradeType extends TradeType>( ...@@ -61,15 +61,15 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
: amountSpecified.currency.equals(currencyIn)) : amountSpecified.currency.equals(currencyIn))
) { ) {
return { return {
state: V3TradeState.INVALID, state: TradeState.INVALID,
trade: null, trade: undefined,
} }
} }
if (routesLoading || quotesResults.some(({ loading }) => loading)) { if (routesLoading || quotesResults.some(({ loading }) => loading)) {
return { return {
state: V3TradeState.LOADING, state: TradeState.LOADING,
trade: null, trade: undefined,
} }
} }
...@@ -117,18 +117,23 @@ export function useClientSideV3Trade<TTradeType extends TradeType>( ...@@ -117,18 +117,23 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
if (!bestRoute || !amountIn || !amountOut) { if (!bestRoute || !amountIn || !amountOut) {
return { return {
state: V3TradeState.NO_ROUTE_FOUND, state: TradeState.NO_ROUTE_FOUND,
trade: null, trade: undefined,
} }
} }
return { return {
state: V3TradeState.VALID, state: TradeState.VALID,
trade: Trade.createUncheckedTrade({ trade: new InterfaceTrade({
route: bestRoute, v2Routes: [],
v3Routes: [
{
routev3: bestRoute,
inputAmount: amountIn,
outputAmount: amountOut,
},
],
tradeType, tradeType,
inputAmount: amountIn,
outputAmount: amountOut,
}), }),
} }
}, [amountSpecified, currencyIn, currencyOut, quotesResults, routes, routesLoading, tradeType]) }, [amountSpecified, currencyIn, currencyOut, quotesResults, routes, routesLoading, tradeType])
......
import { splitSignature } from '@ethersproject/bytes' import { splitSignature } from '@ethersproject/bytes'
import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk' import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk' import { Trade as V3Trade } from '@uniswap/v3-sdk'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { useMemo, useState } from 'react' 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 { DAI, UNI, USDC } from '../constants/tokens'
import { useSingleCallResult } from '../state/multicall/hooks' import { useSingleCallResult } from '../state/multicall/hooks'
import { useEIP2612Contract } from './useContract' import { useEIP2612Contract } from './useContract'
...@@ -272,20 +273,26 @@ export function useV2LiquidityTokenPermit( ...@@ -272,20 +273,26 @@ export function useV2LiquidityTokenPermit(
} }
export function useERC20PermitFromTrade( 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 allowedSlippage: Percent
) { ) {
const { chainId } = useActiveWeb3React() 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( const amountToApprove = useMemo(
() => (trade ? trade.maximumAmountIn(allowedSlippage) : undefined), () => (trade ? trade.maximumAmountIn(allowedSlippage) : undefined),
[trade, allowedSlippage] [trade, allowedSlippage]
) )
return useERC20Permit( return useERC20Permit(amountToApprove, swapRouterAddress, null)
amountToApprove,
// v2 router does not support
trade instanceof V2Trade ? undefined : trade instanceof V3Trade ? swapRouterAddress : undefined,
null
)
} }
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { t, Trans } from '@lingui/macro' import { t, Trans } from '@lingui/macro'
import { SwapRouter, Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Router, Trade as V2Trade } from '@uniswap/v2-sdk' import { Router as V2SwapRouter, Trade as V2Trade } from '@uniswap/v2-sdk'
import { SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk' import { SwapRouter as V3SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk'
import { ReactNode, useMemo } from 'react' 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 { TransactionType } from '../state/transactions/actions'
import { useTransactionAdder } from '../state/transactions/hooks' import { useTransactionAdder } from '../state/transactions/hooks'
import approveAmountCalldata from '../utils/approveAmountCalldata' import approveAmountCalldata from '../utils/approveAmountCalldata'
...@@ -20,6 +21,11 @@ import { SignatureData } from './useERC20Permit' ...@@ -20,6 +21,11 @@ import { SignatureData } from './useERC20Permit'
import useTransactionDeadline from './useTransactionDeadline' import useTransactionDeadline from './useTransactionDeadline'
import { useActiveWeb3React } from './web3' import { useActiveWeb3React } from './web3'
type AnyTrade =
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
enum SwapCallbackState { enum SwapCallbackState {
INVALID, INVALID,
LOADING, LOADING,
...@@ -45,7 +51,6 @@ interface FailedCall extends SwapCallEstimate { ...@@ -45,7 +51,6 @@ interface FailedCall extends SwapCallEstimate {
call: SwapCall call: SwapCall
error: Error error: Error
} }
/** /**
* Returns the swap calls that can be used to make the trade * Returns the swap calls that can be used to make the trade
* @param trade trade to execute * @param trade trade to execute
...@@ -54,7 +59,7 @@ interface FailedCall extends SwapCallEstimate { ...@@ -54,7 +59,7 @@ interface FailedCall extends SwapCallEstimate {
* @param signatureData the signature data of the permit of the input token amount, if available * @param signatureData the signature data of the permit of the input token amount, if available
*/ */
function useSwapCallArguments( 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 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 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 signatureData: SignatureData | null | undefined
...@@ -75,7 +80,7 @@ function useSwapCallArguments( ...@@ -75,7 +80,7 @@ function useSwapCallArguments(
const swapMethods = [] const swapMethods = []
swapMethods.push( swapMethods.push(
Router.swapCallParameters(trade, { V2SwapRouter.swapCallParameters(trade, {
feeOnTransfer: false, feeOnTransfer: false,
allowedSlippage, allowedSlippage,
recipient, recipient,
...@@ -85,7 +90,7 @@ function useSwapCallArguments( ...@@ -85,7 +90,7 @@ function useSwapCallArguments(
if (trade.tradeType === TradeType.EXACT_INPUT) { if (trade.tradeType === TradeType.EXACT_INPUT) {
swapMethods.push( swapMethods.push(
Router.swapCallParameters(trade, { V2SwapRouter.swapCallParameters(trade, {
feeOnTransfer: true, feeOnTransfer: true,
allowedSlippage, allowedSlippage,
recipient, recipient,
...@@ -118,14 +123,10 @@ function useSwapCallArguments( ...@@ -118,14 +123,10 @@ function useSwapCallArguments(
} }
}) })
} else { } else {
// trade is V3Trade // swap options shared by v3 and v2+v3 swap routers
const swapRouterAddress = chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined const sharedSwapOptions = {
if (!swapRouterAddress) return []
const { value, calldata } = SwapRouter.swapCallParameters(trade, {
recipient, recipient,
slippageTolerance: allowedSlippage, slippageTolerance: allowedSlippage,
deadline: deadline.toString(),
...(signatureData ...(signatureData
? { ? {
inputTokenPermit: inputTokenPermit:
...@@ -146,7 +147,26 @@ function useSwapCallArguments( ...@@ -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) { if (argentWalletContract && trade.inputAmount.currency.isToken) {
return [ return [
{ {
...@@ -174,16 +194,16 @@ function useSwapCallArguments( ...@@ -174,16 +194,16 @@ function useSwapCallArguments(
] ]
} }
}, [ }, [
trade,
recipient,
library,
account, account,
allowedSlippage,
argentWalletContract,
chainId, chainId,
deadline, deadline,
library,
recipient,
routerContract, routerContract,
allowedSlippage,
argentWalletContract,
signatureData, signatureData,
trade,
]) ])
} }
...@@ -267,7 +287,7 @@ function swapErrorToUserReadableMessage(error: any): ReactNode { ...@@ -267,7 +287,7 @@ function swapErrorToUserReadableMessage(error: any): ReactNode {
// returns a function that will execute a swap, if the parameters are all valid // 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 // and the user has approved the slippage adjusted input amount for the trade
export function useSwapCallback( 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 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 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 signatureData: SignatureData | undefined | null
......
import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk' import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'components/swap/GasEstimateBadge'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { L2_CHAIN_IDS } from 'constants/chains' import { L2_CHAIN_IDS } from 'constants/chains'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { useMemo } from 'react' import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { useUserSlippageToleranceWithDefault } from '../state/user/hooks' import { useUserSlippageToleranceWithDefault } from '../state/user/hooks'
import { useCurrency } from './Tokens' import { useCurrency } from './Tokens'
...@@ -11,7 +12,6 @@ import useGasPrice from './useGasPrice' ...@@ -11,7 +12,6 @@ import useGasPrice from './useGasPrice'
import useUSDCPrice, { useUSDCValue } from './useUSDCPrice' import useUSDCPrice, { useUSDCValue } from './useUSDCPrice'
import { useActiveWeb3React } from './web3' 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 V3_SWAP_DEFAULT_SLIPPAGE = new Percent(50, 10_000) // .50%
const ONE_TENTHS_PERCENT = new Percent(10, 10_000) // .10% const ONE_TENTHS_PERCENT = new Percent(10, 10_000) // .10%
...@@ -19,12 +19,8 @@ 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 * 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 * @param trade the trade for which to _guess_ the amount of gas it would cost to execute
*/ */
function guesstimateGas( function guesstimateGas(trade: Trade<Currency, Currency, TradeType> | undefined): number | undefined {
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined if (!!trade) {
): number | undefined {
if (trade instanceof V2Trade) {
return 90_000 + trade.route.pairs.length * 30_000
} else if (trade instanceof V3Trade) {
return 100_000 + trade.swaps.reduce((memo, swap) => swap.route.pools.length + memo, 0) * 30_000 return 100_000 + trade.swaps.reduce((memo, swap) => swap.route.pools.length + memo, 0) * 30_000
} }
return undefined return undefined
...@@ -34,7 +30,7 @@ const MIN_AUTO_SLIPPAGE_TOLERANCE = new Percent(5, 1000) // 0.5% ...@@ -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% const MAX_AUTO_SLIPPAGE_TOLERANCE = new Percent(25, 100) // 25%
export default function useSwapSlippageTolerance( export default function useSwapSlippageTolerance(
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
): Percent { ): Percent {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const onL2 = chainId && L2_CHAIN_IDS.includes(chainId) const onL2 = chainId && L2_CHAIN_IDS.includes(chainId)
...@@ -53,19 +49,26 @@ export default function useSwapSlippageTolerance( ...@@ -53,19 +49,26 @@ export default function useSwapSlippageTolerance(
const dollarGasCost = const dollarGasCost =
ether && ethGasCost && etherPrice ? etherPrice.quote(CurrencyAmount.fromRawAmount(ether, ethGasCost)) : undefined 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 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 // 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) const result = new Percent(fraction.numerator, fraction.denominator)
if (result.greaterThan(MAX_AUTO_SLIPPAGE_TOLERANCE)) return MAX_AUTO_SLIPPAGE_TOLERANCE if (result.greaterThan(MAX_AUTO_SLIPPAGE_TOLERANCE)) return MAX_AUTO_SLIPPAGE_TOLERANCE
if (result.lessThan(MIN_AUTO_SLIPPAGE_TOLERANCE)) return MIN_AUTO_SLIPPAGE_TOLERANCE if (result.lessThan(MIN_AUTO_SLIPPAGE_TOLERANCE)) return MIN_AUTO_SLIPPAGE_TOLERANCE
return result return result
} }
if (trade instanceof V2Trade) return V2_SWAP_DEFAULT_SLIPPAGE
return V3_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) return useUserSlippageToleranceWithDefault(defaultSlippageTolerance)
} }
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
}
}
import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react' import { useMemo } from 'react'
import { tryParseAmount } from 'state/swap/hooks'
import { SupportedChainId } from '../constants/chains' import { SupportedChainId } from '../constants/chains'
import { DAI_OPTIMISM, USDC, USDC_ARBITRUM } from '../constants/tokens' import { DAI_OPTIMISM, USDC, USDC_ARBITRUM } from '../constants/tokens'
...@@ -9,7 +10,7 @@ import { useActiveWeb3React } from './web3' ...@@ -9,7 +10,7 @@ import { useActiveWeb3React } from './web3'
// Stablecoin amounts used when calculating spot price for a given currency. // Stablecoin amounts used when calculating spot price for a given currency.
// The amount is large enough to filter low liquidity pairs. // 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.MAINNET]: CurrencyAmount.fromRawAmount(USDC, 100_000e6),
[SupportedChainId.ARBITRUM_ONE]: CurrencyAmount.fromRawAmount(USDC_ARBITRUM, 10_000e6), [SupportedChainId.ARBITRUM_ONE]: CurrencyAmount.fromRawAmount(USDC_ARBITRUM, 10_000e6),
[SupportedChainId.OPTIMISM]: CurrencyAmount.fromRawAmount(DAI_OPTIMISM, 10_000e18), [SupportedChainId.OPTIMISM]: CurrencyAmount.fromRawAmount(DAI_OPTIMISM, 10_000e18),
...@@ -20,11 +21,12 @@ const STABLECOIN_AMOUNT_OUT: { [chainId: number]: CurrencyAmount<Token> } = { ...@@ -20,11 +21,12 @@ const STABLECOIN_AMOUNT_OUT: { [chainId: number]: CurrencyAmount<Token> } = {
* @param currency currency to compute the USDC price of * @param currency currency to compute the USDC price of
*/ */
export default function useUSDCPrice(currency?: Currency): Price<Currency, Token> | undefined { 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 amountOut = chainId ? STABLECOIN_AMOUNT_OUT[chainId] : undefined
const stablecoin = amountOut?.currency const stablecoin = amountOut?.currency
// TODO(#2808): remove dependency on useBestV2Trade
const v2USDCTrade = useBestV2Trade(TradeType.EXACT_OUTPUT, amountOut, currency, { const v2USDCTrade = useBestV2Trade(TradeType.EXACT_OUTPUT, amountOut, currency, {
maxHops: 2, maxHops: 2,
}) })
...@@ -45,7 +47,7 @@ export default function useUSDCPrice(currency?: Currency): Price<Currency, Token ...@@ -45,7 +47,7 @@ export default function useUSDCPrice(currency?: Currency): Price<Currency, Token
const { numerator, denominator } = v2USDCTrade.route.midPrice const { numerator, denominator } = v2USDCTrade.route.midPrice
return new Price(currency, stablecoin, denominator, numerator) return new Price(currency, stablecoin, denominator, numerator)
} else if (v3USDCTrade.trade) { } 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) return new Price(currency, stablecoin, denominator, numerator)
} }
...@@ -65,3 +67,27 @@ export function useUSDCValue(currencyAmount: CurrencyAmount<Currency> | undefine ...@@ -65,3 +67,27 @@ export function useUSDCValue(currencyAmount: CurrencyAmount<Currency> | undefine
} }
}, [currencyAmount, price]) }, [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` ...@@ -48,7 +48,7 @@ const BodyWrapper = styled.div`
z-index: 1; z-index: 1;
${({ theme }) => theme.mediaWidth.upToSmall` ${({ theme }) => theme.mediaWidth.upToSmall`
padding: 6rem 16px 16px 16px; padding: 4rem 8px 16px 8px;
`}; `};
` `
......
This diff is collapsed.
import { ChainId } from '@uniswap/smart-order-router'
export const AUTO_ROUTER_SUPPORTED_CHAINS: ChainId[] = Object.values(ChainId) as number[]
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())
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) }
}
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
import { SupportedChainId } from 'constants/chains' import { Protocol } from '@uniswap/router-sdk'
import { ChainId } from '@uniswap/smart-order-router'
import ms from 'ms.macro'
import qs from 'qs' import qs from 'qs'
import { GetQuoteResult } from './types' 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({ export const routingApi = createApi({
reducerPath: 'routingApi', reducerPath: 'routingApi',
baseQuery: fetchBaseQuery({ baseQuery: fetchBaseQuery({
...@@ -14,14 +70,51 @@ export const routingApi = createApi({ ...@@ -14,14 +70,51 @@ export const routingApi = createApi({
GetQuoteResult, GetQuoteResult,
{ {
tokenInAddress: string tokenInAddress: string
tokenInChainId: SupportedChainId tokenInChainId: ChainId
tokenInDecimals: number
tokenInSymbol?: string
tokenOutAddress: string tokenOutAddress: string
tokenOutChainId: SupportedChainId tokenOutChainId: ChainId
tokenOutDecimals: number
tokenOutSymbol?: string
amount: string amount: string
useClientSideRouter: boolean // included in key to invalidate on change
type: 'exactIn' | 'exactOut' 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,
},
}), }),
}), }),
}) })
......
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, LOADING,
INVALID, INVALID,
NO_ROUTE_FOUND, NO_ROUTE_FOUND,
...@@ -8,11 +11,12 @@ export enum V3TradeState { ...@@ -8,11 +11,12 @@ export enum V3TradeState {
SYNCING, 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 TokenInRoute = Pick<Token, 'address' | 'chainId' | 'symbol' | 'decimals'>
export type PoolInRoute = { export type V3PoolInRoute = {
type: 'v3-pool' type: 'v3-pool'
address: string
tokenIn: TokenInRoute tokenIn: TokenInRoute
tokenOut: TokenInRoute tokenOut: TokenInRoute
sqrtRatioX96: string sqrtRatioX96: string
...@@ -21,6 +25,28 @@ export type PoolInRoute = { ...@@ -21,6 +25,28 @@ export type PoolInRoute = {
fee: string fee: string
amountIn?: string amountIn?: string
amountOut?: 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 { export interface GetQuoteResult {
...@@ -38,6 +64,35 @@ export interface GetQuoteResult { ...@@ -38,6 +64,35 @@ export interface GetQuoteResult {
quoteDecimals: string quoteDecimals: string
quoteGasAdjusted: string quoteGasAdjusted: string
quoteGasAdjustedDecimals: string quoteGasAdjustedDecimals: string
route: PoolInRoute[][] route: Array<V3PoolInRoute[] | V2PoolInRoute[]>
routeString: string 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
}
}
import { skipToken } from '@reduxjs/toolkit/query/react' import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' 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 ms from 'ms.macro'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useBlockNumber } from 'state/application/hooks' import { useBlockNumber } from 'state/application/hooks'
import { useGetQuoteQuery } from 'state/routing/slice' import { useGetQuoteQuery } from 'state/routing/slice'
import { useClientSideRouter } from 'state/user/hooks'
import { V3TradeState } from './types' import { GetQuoteResult, InterfaceTrade, TradeState } from './types'
import { computeRoutes } from './utils' import { computeRoutes, transformRoutesToTrade } from './utils'
function useFreshData<T>(data: T, dataBlockNumber: number, maxBlockAge = 10): T | undefined { function useFreshData<T>(data: T, dataBlockNumber: number, maxBlockAge = 10): T | undefined {
const localBlockNumber = useBlockNumber() const localBlockNumber = useBlockNumber()
...@@ -35,22 +36,31 @@ function useRoutingAPIArguments({ ...@@ -35,22 +36,31 @@ function useRoutingAPIArguments({
amount: CurrencyAmount<Currency> | undefined amount: CurrencyAmount<Currency> | undefined
tradeType: TradeType tradeType: TradeType
}) { }) {
if (!tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut)) { const [clientSideRouter] = useClientSideRouter()
return undefined
}
return { return useMemo(
tokenInAddress: tokenIn.wrapped.address, () =>
tokenInChainId: tokenIn.chainId, !tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut)
tokenOutAddress: tokenOut.wrapped.address, ? undefined
tokenOutChainId: tokenOut.chainId, : {
amount: amount.quotient.toString(), amount: amount.quotient.toString(),
type: (tradeType === TradeType.EXACT_INPUT ? 'exactIn' : 'exactOut') as 'exactIn' | 'exactOut', 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 tradeType whether the swap is an exact in/out
* @param amountSpecified the exact amount to swap in/out * @param amountSpecified the exact amount to swap in/out
* @param otherCurrency the desired output/payment currency * @param otherCurrency the desired output/payment currency
...@@ -59,7 +69,10 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -59,7 +69,10 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
tradeType: TTradeType, tradeType: TTradeType,
amountSpecified?: CurrencyAmount<Currency>, amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: 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( const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo(
() => () =>
tradeType === TradeType.EXACT_INPUT tradeType === TradeType.EXACT_INPUT
...@@ -76,30 +89,33 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -76,30 +89,33 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
}) })
const { isLoading, isError, data } = useGetQuoteQuery(queryArgs ?? skipToken, { const { isLoading, isError, data } = useGetQuoteQuery(queryArgs ?? skipToken, {
pollingInterval: ms`10s`, pollingInterval: ms`15s`,
refetchOnFocus: true, refetchOnFocus: true,
}) })
const quoteResult = useFreshData(data, Number(data?.blockNumber) || 0) const quoteResult: GetQuoteResult | undefined = useFreshData(data, Number(data?.blockNumber) || 0)
const routes = useMemo( const route = useMemo(
() => computeRoutes(currencyIn, currencyOut, quoteResult), () => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
[currencyIn, currencyOut, quoteResult] [currencyIn, currencyOut, quoteResult, tradeType]
) )
// get USD gas cost of trade in active chains stablecoin amount
const gasUseEstimateUSD = useStablecoinAmountFromFiatValue(quoteResult?.gasUseEstimateUSD) ?? null
return useMemo(() => { return useMemo(() => {
if (!currencyIn || !currencyOut) { if (!currencyIn || !currencyOut) {
return { return {
state: V3TradeState.INVALID, state: TradeState.INVALID,
trade: null, trade: undefined,
} }
} }
if (isLoading && !quoteResult) { if (isLoading && !quoteResult) {
// only on first hook render // only on first hook render
return { return {
state: V3TradeState.LOADING, state: TradeState.LOADING,
trade: null, trade: undefined,
} }
} }
...@@ -112,22 +128,23 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -112,22 +128,23 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
? CurrencyAmount.fromRawAmount(currencyIn, quoteResult.quote) ? CurrencyAmount.fromRawAmount(currencyIn, quoteResult.quote)
: undefined : undefined
if (isError || !otherAmount || !routes || routes.length === 0 || !queryArgs) { if (isError || !otherAmount || !route || route.length === 0 || !queryArgs) {
return { return {
state: V3TradeState.NO_ROUTE_FOUND, state: TradeState.NO_ROUTE_FOUND,
trade: null, trade: undefined,
} }
} }
const trade = Trade.createUncheckedTradeWithMultipleRoutes<Currency, Currency, TTradeType>({ try {
routes, const trade = transformRoutesToTrade(route, tradeType, gasUseEstimateUSD)
tradeType, return {
}) // always return VALID regardless of isFetching status
state: TradeState.VALID,
return { trade,
// always return VALID regardless of isFetching status }
state: V3TradeState.VALID, } catch (e) {
trade, 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])
} }
This diff is collapsed.
import { Currency, CurrencyAmount, Ether, Token } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Ether, Token, TradeType } from '@uniswap/sdk-core'
import { FeeAmount, Pool, Route } from '@uniswap/v3-sdk' 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 * Transforms a Routing API quote into an array of routes that can be used to create
* can be used to create a V3 `Trade`. * a `Trade`.
*/ */
export function computeRoutes( export function computeRoutes(
currencyIn: Currency | undefined, currencyIn: Currency | undefined,
currencyOut: Currency | undefined, currencyOut: Currency | undefined,
tradeType: TradeType,
quoteResult: Pick<GetQuoteResult, 'route'> | undefined 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 || !quoteResult.route || !currencyIn || !currencyOut) return undefined
if (quoteResult.route.length === 0) return [] if (quoteResult.route.length === 0) return []
const parsedCurrencyIn = currencyIn.isNative const parsedTokenIn = parseToken(quoteResult.route[0][0].tokenIn)
? Ether.onChain(currencyIn.chainId) const parsedTokenOut = parseToken(quoteResult.route[0][quoteResult.route[0].length - 1].tokenOut)
: parseToken(quoteResult.route[0][0].tokenIn)
const parsedCurrencyOut = currencyOut.isNative if (parsedTokenIn.address !== currencyIn.wrapped.address) return undefined
? Ether.onChain(currencyOut.chainId) if (parsedTokenOut.address !== currencyOut.wrapped.address) return undefined
: parseToken(quoteResult.route[0][quoteResult.route[0].length - 1].tokenOut)
const parsedCurrencyIn = currencyIn.isNative ? Ether.onChain(currencyIn.chainId) : parsedTokenIn
const parsedCurrencyOut = currencyOut.isNative ? Ether.onChain(currencyOut.chainId) : parsedTokenOut
try { try {
return quoteResult.route.map((route) => { 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 rawAmountIn = route[0].amountIn
const rawAmountOut = route[route.length - 1].amountOut const rawAmountOut = route[route.length - 1].amountOut
...@@ -40,7 +41,8 @@ export function computeRoutes( ...@@ -40,7 +41,8 @@ export function computeRoutes(
} }
return { 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), inputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyIn, rawAmountIn),
outputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyOut, rawAmountOut), outputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyOut, rawAmountOut),
} }
...@@ -49,22 +51,35 @@ export function computeRoutes( ...@@ -49,22 +51,35 @@ export function computeRoutes(
// `Route` constructor may throw if inputs/outputs are temporarily out of sync // `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) // (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 // This is not fatal and will fix itself in future render cycles
console.error(e)
return undefined 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 => { const parseToken = ({ address, chainId, decimals, symbol }: GetQuoteResult['route'][0][0]['tokenIn']): Token => {
return new Token(chainId, address, parseInt(decimals.toString()), symbol) return new Token(chainId, address, parseInt(decimals.toString()), symbol)
} }
const parsePool = ({ const parsePool = ({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOut }: V3PoolInRoute): Pool =>
fee,
sqrtRatioX96,
liquidity,
tickCurrent,
tokenIn,
tokenOut,
}: GetQuoteResult['route'][0][0]): Pool =>
new Pool( new Pool(
parseToken(tokenIn), parseToken(tokenIn),
parseToken(tokenOut), parseToken(tokenOut),
...@@ -73,3 +88,13 @@ const parsePool = ({ ...@@ -73,3 +88,13 @@ const parsePool = ({
liquidity, liquidity,
parseInt(tickCurrent) 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'
}
import { parseUnits } from '@ethersproject/units' import { parseUnits } from '@ethersproject/units'
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk' import { useBestTrade } from 'hooks/useBestTrade'
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 JSBI from 'jsbi' import JSBI from 'jsbi'
import { ParsedQs } from 'qs' import { ParsedQs } from 'qs'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { useAppDispatch, useAppSelector } from 'state/hooks' import { useAppDispatch, useAppSelector } from 'state/hooks'
import { V3TradeState } from 'state/routing/types' import { InterfaceTrade, TradeState } from 'state/routing/types'
import { isTradeBetter } from 'utils/isTradeBetter'
import { useCurrency } from '../../hooks/Tokens' import { useCurrency } from '../../hooks/Tokens'
import useENS from '../../hooks/useENS' import useENS from '../../hooks/useENS'
import useParsedQueryString from '../../hooks/useParsedQueryString' import useParsedQueryString from '../../hooks/useParsedQueryString'
import useSwapSlippageTolerance from '../../hooks/useSwapSlippageTolerance' import useSwapSlippageTolerance from '../../hooks/useSwapSlippageTolerance'
import { Version } from '../../hooks/useToggledVersion'
import { useActiveWeb3React } from '../../hooks/web3' import { useActiveWeb3React } from '../../hooks/web3'
import { isAddress } from '../../utils' import { isAddress } from '../../utils'
import { AppState } from '../index' import { AppState } from '../index'
...@@ -98,36 +92,16 @@ const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = { ...@@ -98,36 +92,16 @@ const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = {
'0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D': true, // v2 router 02 '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. // 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 } currencies: { [field in Field]?: Currency | null }
currencyBalances: { [field in Field]?: CurrencyAmount<Currency> } currencyBalances: { [field in Field]?: CurrencyAmount<Currency> }
parsedAmount: CurrencyAmount<Currency> | undefined parsedAmount: CurrencyAmount<Currency> | undefined
inputError?: ReactNode inputError?: ReactNode
v2Trade: V2Trade<Currency, Currency, TradeType> | undefined trade: {
v3Trade: { trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
trade: V3Trade<Currency, Currency, TradeType> | null state: TradeState
state: V3TradeState
} }
bestTrade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined
allowedSlippage: Percent allowedSlippage: Percent
} { } {
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
...@@ -156,35 +130,12 @@ export function useDerivedSwapInfo(toggledVersion: Version | undefined): { ...@@ -156,35 +130,12 @@ export function useDerivedSwapInfo(toggledVersion: Version | undefined): {
[inputCurrency, isExactIn, outputCurrency, typedValue] [inputCurrency, isExactIn, outputCurrency, typedValue]
) )
// get v2 and v3 quotes const trade = useBestTrade(
// skip if other version is toggled
const v2Trade = useBestV2Trade(
isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, 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 (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 = { const currencyBalances = {
[Field.INPUT]: relevantTokenBalances[0], [Field.INPUT]: relevantTokenBalances[0],
[Field.OUTPUT]: relevantTokenBalances[1], [Field.OUTPUT]: relevantTokenBalances[1],
...@@ -200,27 +151,27 @@ export function useDerivedSwapInfo(toggledVersion: Version | undefined): { ...@@ -200,27 +151,27 @@ export function useDerivedSwapInfo(toggledVersion: Version | undefined): {
inputError = <Trans>Connect Wallet</Trans> inputError = <Trans>Connect Wallet</Trans>
} }
if (!parsedAmount) {
inputError = inputError ?? <Trans>Enter an amount</Trans>
}
if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) { if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) {
inputError = inputError ?? <Trans>Select a token</Trans> inputError = inputError ?? <Trans>Select a token</Trans>
} }
if (!parsedAmount) {
inputError = inputError ?? <Trans>Enter an amount</Trans>
}
const formattedTo = isAddress(to) const formattedTo = isAddress(to)
if (!to || !formattedTo) { if (!to || !formattedTo) {
inputError = inputError ?? <Trans>Enter a recipient</Trans> inputError = inputError ?? <Trans>Enter a recipient</Trans>
} else { } else {
if (BAD_RECIPIENT_ADDRESSES[formattedTo] || (v2Trade && involvesAddress(v2Trade, formattedTo))) { if (BAD_RECIPIENT_ADDRESSES[formattedTo]) {
inputError = inputError ?? <Trans>Invalid recipient</Trans> 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 // 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)) { if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) {
inputError = <Trans>Insufficient {amountIn.currency.symbol} balance</Trans> inputError = <Trans>Insufficient {amountIn.currency.symbol} balance</Trans>
...@@ -231,9 +182,7 @@ export function useDerivedSwapInfo(toggledVersion: Version | undefined): { ...@@ -231,9 +182,7 @@ export function useDerivedSwapInfo(toggledVersion: Version | undefined): {
currencyBalances, currencyBalances,
parsedAmount, parsedAmount,
inputError, inputError,
v2Trade: v2Trade ?? undefined, trade,
v3Trade,
bestTrade: bestTrade ?? undefined,
allowedSlippage, allowedSlippage,
} }
} }
......
import { Percent, Token } from '@uniswap/sdk-core' import { Percent, Token } from '@uniswap/sdk-core'
import { computePairAddress, Pair } from '@uniswap/v2-sdk' 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 { SupportedLocale } from 'constants/locales'
import { L2_DEADLINE_FROM_NOW } from 'constants/misc' import { L2_DEADLINE_FROM_NOW } from 'constants/misc'
import JSBI from 'jsbi' import JSBI from 'jsbi'
...@@ -121,13 +121,6 @@ export function useClientSideRouter(): [boolean, (userClientSideRouter: boolean) ...@@ -121,13 +121,6 @@ export function useClientSideRouter(): [boolean, (userClientSideRouter: boolean)
return [clientSideRouter, setClientSideRouter] 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 { export function useSetUserSlippageTolerance(): (slippageTolerance: Percent | 'auto') => void {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
......
...@@ -261,3 +261,9 @@ export const SmallOnly = styled.span` ...@@ -261,3 +261,9 @@ export const SmallOnly = styled.span`
display: block; 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 ...@@ -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 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 SkipFirst<T extends readonly any[], N extends number> = TupleSplit<T, N>[1]
export type NonNullable<T> = T extends null | undefined ? never : T
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
}
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)
})
})
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
}
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk' 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' import { ONE_HUNDRED_PERCENT, ZERO_PERCENT } from '../constants/misc'
// returns whether tradeB is better than tradeA by at least a threshold percentage amount // returns whether tradeB is better than tradeA by at least a threshold percentage amount
// only used by v2 hooks
export function isTradeBetter( export function isTradeBetter(
tradeA: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined | null, tradeA: V2Trade<Currency, Currency, TradeType> | undefined | null,
tradeB: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined | null, tradeB: V2Trade<Currency, Currency, TradeType> | undefined | null,
minimumDelta: Percent = ZERO_PERCENT minimumDelta: Percent = ZERO_PERCENT
): boolean | undefined { ): boolean | undefined {
if (tradeA && !tradeB) return false if (tradeA && !tradeB) return false
......
import { Trade } from '@uniswap/router-sdk'
import { CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' 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 JSBI from 'jsbi'
import { computeRealizedLPFeeAmount, warningSeverity } from './prices' import { computeRealizedLPFeeAmount, warningSeverity } from './prices'
describe('prices', () => { const token1 = new Token(1, '0x0000000000000000000000000000000000000001', 18)
const token1 = new Token(1, '0x0000000000000000000000000000000000000001', 18) const token2 = new Token(1, '0x0000000000000000000000000000000000000002', 18)
const token2 = new Token(1, '0x0000000000000000000000000000000000000002', 18) const token3 = new Token(1, '0x0000000000000000000000000000000000000003', 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 pair12 = new Pair( const pool12 = new Pool(token1, token2, FeeAmount.HIGH, '2437312313659959819381354528', '10272714736694327408', -69633)
CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(10000)), const pool13 = new Pool(
CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(20000)) token1,
) token3,
const pair23 = new Pair( FeeAmount.MEDIUM,
CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(20000)), '2437312313659959819381354528',
CurrencyAmount.fromRawAmount(token3, JSBI.BigInt(30000)) '10272714736694327408',
) -69633
)
const currencyAmount = (token: Token, amount: number) => CurrencyAmount.fromRawAmount(token, JSBI.BigInt(amount))
describe('prices', () => {
describe('#computeRealizedLPFeeAmount', () => { describe('#computeRealizedLPFeeAmount', () => {
it('returns undefined for undefined', () => { it('returns undefined for undefined', () => {
expect(computeRealizedLPFeeAmount(undefined)).toEqual(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({
v2Routes: [
{
routev2: new V2Route([pair12], token1, token2),
inputAmount: currencyAmount(token1, 1000),
outputAmount: currencyAmount(token2, 1000),
},
],
v3Routes: [],
tradeType: TradeType.EXACT_INPUT,
})
)
).toEqual(currencyAmount(token1, 3)) // 3% realized fee
})
it('correct realized lp fee for single hop on v3', () => {
// v3
expect( expect(
computeRealizedLPFeeAmount( computeRealizedLPFeeAmount(
new Trade( new Trade({
new Route([pair12], token1, token2), v3Routes: [
CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(1000)), {
TradeType.EXACT_INPUT routev3: new V3Route([pool12], token1, token2),
) inputAmount: currencyAmount(token1, 1000),
outputAmount: currencyAmount(token2, 1000),
},
],
v2Routes: [],
tradeType: TradeType.EXACT_INPUT,
})
) )
).toEqual(CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(3))) ).toEqual(currencyAmount(token1, 10)) // 3% realized fee
}) })
it('correct realized lp fee for double hop', () => { it('correct realized lp fee for double hop', () => {
expect( expect(
computeRealizedLPFeeAmount( computeRealizedLPFeeAmount(
new Trade( new Trade({
new Route([pair12, pair23], token1, token3), v2Routes: [
CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(1000)), {
TradeType.EXACT_INPUT routev2: new V2Route([pair12, pair23], token1, token3),
) inputAmount: currencyAmount(token1, 1000),
outputAmount: currencyAmount(token3, 1000),
},
],
v3Routes: [],
tradeType: TradeType.EXACT_INPUT,
})
)
).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.fromRawAmount(token1, JSBI.BigInt(5))) ).toEqual(currencyAmount(token1, 8))
}) })
}) })
......
import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Fraction, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Fraction, Percent, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk' import { Pair } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk' import { FeeAmount } from '@uniswap/v3-sdk'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { import {
...@@ -8,29 +9,28 @@ import { ...@@ -8,29 +9,28 @@ import {
ALLOWED_PRICE_IMPACT_LOW, ALLOWED_PRICE_IMPACT_LOW,
ALLOWED_PRICE_IMPACT_MEDIUM, ALLOWED_PRICE_IMPACT_MEDIUM,
BLOCKED_PRICE_IMPACT_NON_EXPERT, BLOCKED_PRICE_IMPACT_NON_EXPERT,
ONE_HUNDRED_PERCENT,
ZERO_PERCENT, ZERO_PERCENT,
} from '../constants/misc' } from '../constants/misc'
const THIRTY_BIPS_FEE = new Percent(JSBI.BigInt(30), JSBI.BigInt(10000)) 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) const INPUT_FRACTION_AFTER_FEE = ONE_HUNDRED_PERCENT.subtract(THIRTY_BIPS_FEE)
// computes realized lp fee as a percent // computes realized lp fee as a percent
export function computeRealizedLPFeePercent( export function computeRealizedLPFeePercent(trade: Trade<Currency, Currency, TradeType>): Percent {
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
): Percent {
let percent: 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 // 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)) // e.g. for 3 tokens/2 hops: 1 - ((1 - .03) * (1-.03))
percent = ONE_HUNDRED_PERCENT.subtract( percent = ONE_HUNDRED_PERCENT.subtract(
trade.route.pairs.reduce<Percent>( trade.swaps.reduce<Percent>(
(currentFee: Percent): Percent => currentFee.multiply(INPUT_FRACTION_AFTER_FEE), (currentFee: Percent): Percent => currentFee.multiply(INPUT_FRACTION_AFTER_FEE),
ONE_HUNDRED_PERCENT ONE_HUNDRED_PERCENT
) )
) )
} else { } else {
//TODO(judo): validate this
percent = ZERO_PERCENT percent = ZERO_PERCENT
for (const swap of trade.swaps) { for (const swap of trade.swaps) {
const { numerator, denominator } = swap.inputAmount.divide(trade.inputAmount) const { numerator, denominator } = swap.inputAmount.divide(trade.inputAmount)
...@@ -38,11 +38,14 @@ export function computeRealizedLPFeePercent( ...@@ -38,11 +38,14 @@ export function computeRealizedLPFeePercent(
const routeRealizedLPFeePercent = overallPercent.multiply( const routeRealizedLPFeePercent = overallPercent.multiply(
ONE_HUNDRED_PERCENT.subtract( ONE_HUNDRED_PERCENT.subtract(
swap.route.pools.reduce<Percent>( swap.route.pools.reduce<Percent>((currentFee: Percent, pool): Percent => {
(currentFee: Percent, pool): Percent => const fee =
currentFee.multiply(ONE_HUNDRED_PERCENT.subtract(new Fraction(pool.fee, 1_000_000))), pool instanceof Pair
ONE_HUNDRED_PERCENT ? // 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( ...@@ -55,7 +58,7 @@ export function computeRealizedLPFeePercent(
// computes price breakdown for the trade // computes price breakdown for the trade
export function computeRealizedLPFeeAmount( export function computeRealizedLPFeeAmount(
trade?: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | null trade?: Trade<Currency, Currency, TradeType> | null
): CurrencyAmount<Currency> | undefined { ): CurrencyAmount<Currency> | undefined {
if (trade) { if (trade) {
const realizedLPFee = computeRealizedLPFeePercent(trade) const realizedLPFee = computeRealizedLPFeePercent(trade)
......
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
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment