Commit cd229558 authored by Justin Domingue's avatar Justin Domingue Committed by GitHub

feat: routing api integration (#2116)

* initial routing api integration

* add routing api slice

* display route in dialog

* addressed pr feedback

* improved routing

* switch to `get`

* first pass at integration new MultiRouteTrade

* initial implementation of RoutingDiagram

* add RoutingDiagram tests

* improve tests in RoutingDiagram

* integrate with v3-sdk 3.3.1

* removed references to MultiRouteTrade

* revert swapcallback

* fix abi compilation error

* added useRoute hook to build a Route from edges and nodes

* added react-hooks-testing-library

* integrated latest changes

* renamed router hook to routerTrade

* improve integration

* fixed routing

* usability

* mock RoutingDiagram children to reduce size

* undo mocked children

* adjust ui

* better support long routes

* use routing api logo and adjust ux

* set default percent to 0

* added intermediary hook to combine local and routing api trades

* added intermediary hook to combine local and routing api trades

* make account optional

* improve ux

* improve router

* fixed duplicate pool bug and inputAmount undefined bug

* extract input/outputAmounts from routes

* add todo

* fixed uninitialized issue and added %

* fixed tests

* fix duplicate pool bug

* added routing api setting

* change router label based on router version

* improve useRoutes and fix duplicate pool bug

* debounce routing api/local routing

* removed single hop setting

* fix bug when moving between v2/v3

* consider isUnitialized non loading

* ui fixes

* reverted change to usedebounce

* use new route schema

* visual updates

* log quoteId for polish session

* fix: persist advanced swap details toggle state

* fix no route found

* poll every 10s

* derive currencies from pool rather than input

* polish query status handling in useRouterTrade

* removed RouterVersion

* update ui

* update ui

* update loading state

* animate auto router

* apply loading treatment to out

* disable routing api on l2 and support auto slippage

* use opacity on the whole element

* show loading card when syncing

* updated gradient

* polished ui

* create routerlabel component

* disable router on all bu mainnet

* polish

* feat: [draft] routing api polish (#2224)

* show loading card when syncing

* updated gradient

* polished ui

* create routerlabel component

* disable router on all bu mainnet

* polish

* polished loading state

* add dashes

* fixed tooltip styles

* fixed merge conflict

* few updates

* polish

* updated yarn.lock

* fixed styles

* updated routing diagram

* Fix code style issues with ESLint

* routing api enabled without localstorage upgrade

* fixed lint error

* Fix code style issues with ESLint

* refined mocks in routing diagram tests

* addressed pr feedback

* polish

* revert sending eth

* improved loading animation

* handle stale routing api

* Fix code style issues with ESLint

* updated yarn.lock

* support native eth

* Compute gas adjusted quote for V2 trade and compare to V3 gas adjusted quote

* Incorporate approval gas cost estimate

* feat: simplify routing api ux (#2258)

* support native eth

* simplified ui

* perf optimization

* implement realized lp fee

* improved route realized lp fee

* fix lp realized fee

* fix auto router gradient

* initial route overlay

* add auto router svg

* adjusted ux to mocks

* fix lp fee

* upddated routing diagram

* optimize tradeBetter hook

* adjust type and name

* add useBetterTrade

* useBetterTrade takes gasEstimateWei

* implement gasEstimateForApproval

* import state from react

* use gas estimate

* improve integration with gas estimate comparison

* remove dependency on account

* fix currency switch bug

* improve syncing state

* add loadingbar

* style tooltip container

* updated tooltip styles

* increase opacity range

* always keep dependent currency input interactable

* show placeholders in tooltips

* Revert v2 gas estimates and approval estimates

* Add debug logs

* refactor

* fix bug

* removed comment

* update engish key

* add try-catch

* addressed pr feedback

* remove loading bar for price impact

* addressed pr feedback and bug bash feedback

* fix: use url to force version

* addressed pr feedback and bug bash feedback

* stop fetching when losing focus

* only show auto router label when activated

* avoid showing syncing status

* move V3TradeSTate to own file

* make useRoutes a function rather than hook

* use logo from active list when possible

* renamed and refactored hook

* renamed and refactored hook

* update status

* polish

* remove unused import

* fixed merge error

* updated combined trade tests

* remove priceimpact while loading

* Design tweaks

* polish latest design

* removed some styles

* log gaevent on tooltip open and clean up origin

* Small tweaks

* addressed pr feedback

* wrap route length in a loading container

* renamed local to clientside

* fix percent and token logo

* addressed pr feedback

* avoid comparing trades when v3 not ready

* some refactor
Co-authored-by: default avatarLint Action <lint-action@samuelmeuli.com>
Co-authored-by: default avatarWill Pote <will@uniswap.org>
Co-authored-by: default avatarCallil Capuozzo <callil.capuozzo@gmail.com>
parent fb33a068
<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
\ No newline at end of file
import { Pair } from '@uniswap/v2-sdk' import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import { useState, useCallback, ReactNode } from 'react' import { Pair } from '@uniswap/v2-sdk'
import styled from 'styled-components/macro' import { AutoColumn } from 'components/Column'
import { loadingOpacityMixin, LoadingOpacityContainer } from 'components/Loader/styled'
import { darken } from 'polished' import { darken } from 'polished'
import { ReactNode, useCallback, useState } from 'react'
import { Lock } from 'react-feather'
import styled from 'styled-components/macro'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import useTheme from '../../hooks/useTheme'
import { useActiveWeb3React } from '../../hooks/web3'
import { useCurrencyBalance } from '../../state/wallet/hooks' import { useCurrencyBalance } from '../../state/wallet/hooks'
import CurrencySearchModal from '../SearchModal/CurrencySearchModal' import { TYPE } from '../../theme'
import { ButtonGray } from '../Button'
import CurrencyLogo from '../CurrencyLogo' import CurrencyLogo from '../CurrencyLogo'
import DoubleCurrencyLogo from '../DoubleLogo' import DoubleCurrencyLogo from '../DoubleLogo'
import { ButtonGray } from '../Button'
import { RowBetween, RowFixed } from '../Row'
import { TYPE } from '../../theme'
import { Input as NumericalInput } from '../NumericalInput' import { Input as NumericalInput } from '../NumericalInput'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg' import { RowBetween, RowFixed } from '../Row'
import { useActiveWeb3React } from '../../hooks/web3' import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
import { Trans } from '@lingui/macro'
import useTheme from '../../hooks/useTheme'
import { Lock } from 'react-feather'
import { AutoColumn } from 'components/Column'
import { FiatValue } from './FiatValue' import { FiatValue } from './FiatValue'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
const InputPanel = styled.div<{ hideInput?: boolean }>` const InputPanel = styled.div<{ hideInput?: boolean }>`
${({ theme }) => theme.flexColumnNoWrap} ${({ theme }) => theme.flexColumnNoWrap}
...@@ -81,6 +82,7 @@ const CurrencySelect = styled(ButtonGray)<{ visible: boolean; selected: boolean; ...@@ -81,6 +82,7 @@ const CurrencySelect = styled(ButtonGray)<{ visible: boolean; selected: boolean;
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;
padding: ${({ selected }) => (selected ? ' 1rem 1rem 0.75rem 1rem' : '1rem 1rem 0.75rem 1rem')}; padding: ${({ selected }) => (selected ? ' 1rem 1rem 0.75rem 1rem' : '1rem 1rem 0.75rem 1rem')};
` `
...@@ -145,6 +147,10 @@ const StyledBalanceMax = styled.button<{ disabled?: boolean }>` ...@@ -145,6 +147,10 @@ const StyledBalanceMax = styled.button<{ disabled?: boolean }>`
`}; `};
` `
const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean }>`
${loadingOpacityMixin}
`
interface CurrencyInputPanelProps { interface CurrencyInputPanelProps {
value: string value: string
onUserInput: (value: string) => void onUserInput: (value: string) => void
...@@ -165,6 +171,7 @@ interface CurrencyInputPanelProps { ...@@ -165,6 +171,7 @@ interface CurrencyInputPanelProps {
disableNonToken?: boolean disableNonToken?: boolean
renderBalance?: (amount: CurrencyAmount<Currency>) => ReactNode renderBalance?: (amount: CurrencyAmount<Currency>) => ReactNode
locked?: boolean locked?: boolean
loading?: boolean
} }
export default function CurrencyInputPanel({ export default function CurrencyInputPanel({
...@@ -186,6 +193,7 @@ export default function CurrencyInputPanel({ ...@@ -186,6 +193,7 @@ export default function CurrencyInputPanel({
pair = null, // used for double token logo pair = null, // used for double token logo
hideInput = false, hideInput = false,
locked = false, locked = false,
loading = false,
...rest ...rest
}: CurrencyInputPanelProps) { }: CurrencyInputPanelProps) {
const [modalOpen, setModalOpen] = useState(false) const [modalOpen, setModalOpen] = useState(false)
...@@ -249,15 +257,12 @@ export default function CurrencyInputPanel({ ...@@ -249,15 +257,12 @@ export default function CurrencyInputPanel({
</Aligner> </Aligner>
</CurrencySelect> </CurrencySelect>
{!hideInput && ( {!hideInput && (
<> <StyledNumericalInput
<NumericalInput className="token-amount-input"
className="token-amount-input" value={value}
value={value} onUserInput={onUserInput}
onUserInput={(val) => { $loading={loading}
onUserInput(val) />
}}
/>
</>
)} )}
</InputRow> </InputRow>
{!hideInput && !hideBalance && ( {!hideInput && !hideBalance && (
...@@ -291,7 +296,9 @@ export default function CurrencyInputPanel({ ...@@ -291,7 +296,9 @@ export default function CurrencyInputPanel({
) : ( ) : (
<span /> <span />
)} )}
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} /> <LoadingOpacityContainer $loading={loading}>
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
</LoadingOpacityContainer>
</RowBetween> </RowBetween>
</FiatRow> </FiatRow>
)} )}
......
import styled, { css, keyframes } from 'styled-components/macro'
export const loadingAnimation = keyframes`
0% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
`
export const LoadingRows = styled.div`
display: grid;
& > div {
animation: ${loadingAnimation} 1.5s infinite;
animation-fill-mode: both;
background: linear-gradient(
to left,
${({ theme }) => theme.bg1} 25%,
${({ theme }) => theme.bg2} 50%,
${({ theme }) => theme.bg1} 75%
);
background-size: 400%;
border-radius: 12px;
height: 2.4em;
will-change: background-position;
}
`
export const loadingOpacityMixin = css<{ $loading: boolean }>`
filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')};
opacity: ${({ $loading }) => ($loading ? '0.4' : '1')};
transition: opacity 0.2s ease-in-out;
`
export const LoadingOpacityContainer = styled.div<{ $loading: boolean }>`
${loadingOpacityMixin}
`
import { Placement } from '@popperjs/core' import { Options, Placement } from '@popperjs/core'
import { transparentize } from 'polished' import Portal from '@reach/portal'
import React, { useCallback, useState } from 'react' import React, { useCallback, useMemo, useState } from 'react'
import { usePopper } from 'react-popper' import { usePopper } from 'react-popper'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import useInterval from '../../hooks/useInterval' import useInterval from '../../hooks/useInterval'
import Portal from '@reach/portal'
const PopoverContainer = styled.div<{ show: boolean }>` const PopoverContainer = styled.div<{ show: boolean }>`
z-index: 9999; z-index: 9999;
visibility: ${(props) => (props.show ? 'visible' : 'hidden')}; visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
opacity: ${(props) => (props.show ? 1 : 0)}; opacity: ${(props) => (props.show ? 1 : 0)};
transition: visibility 150ms linear, opacity 150ms linear; transition: visibility 150ms linear, opacity 150ms linear;
background: ${({ theme }) => theme.bg2};
border: 1px solid ${({ theme }) => theme.bg3};
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)};
color: ${({ theme }) => theme.text2}; color: ${({ theme }) => theme.text2};
border-radius: 8px;
` `
const ReferenceElement = styled.div` const ReferenceElement = styled.div`
...@@ -34,9 +29,9 @@ const Arrow = styled.div` ...@@ -34,9 +29,9 @@ const Arrow = styled.div`
z-index: 9998; z-index: 9998;
content: ''; content: '';
border: 1px solid ${({ theme }) => theme.bg3}; border: 1px solid ${({ theme }) => theme.bg2};
transform: rotate(45deg); transform: rotate(45deg);
background: ${({ theme }) => theme.bg2}; background: ${({ theme }) => theme.bg0};
} }
&.arrow-top { &.arrow-top {
...@@ -84,14 +79,22 @@ export default function Popover({ content, show, children, placement = 'auto' }: ...@@ -84,14 +79,22 @@ export default function Popover({ content, show, children, placement = 'auto' }:
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null) const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null)
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null) const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null) const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
const { styles, update, attributes } = usePopper(referenceElement, popperElement, {
placement, const options = useMemo(
strategy: 'fixed', (): Options => ({
modifiers: [ placement,
{ name: 'offset', options: { offset: [8, 8] } }, strategy: 'fixed',
{ name: 'arrow', options: { element: arrowElement } }, modifiers: [
], { name: 'offset', options: { offset: [8, 8] } },
}) { name: 'arrow', options: { element: arrowElement } },
{ name: 'preventOverflow', options: { padding: 8 } },
],
}),
[arrowElement, placement]
)
const { styles, update, attributes } = usePopper(referenceElement, popperElement, options)
const updateCallback = useCallback(() => { const updateCallback = useCallback(() => {
update && update() update && update()
}, [update]) }, [update])
......
import { Currency, Percent } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { DAI, USDC, WBTC } from 'constants/tokens'
import { render } from 'test-utils'
import RoutingDiagram, { RoutingDiagramEntry } from './RoutingDiagram'
const percent = (strings: TemplateStringsArray) => new Percent(parseInt(strings[0]), 100)
const singleRoute: RoutingDiagramEntry = { percent: percent`100`, path: [[USDC, DAI, FeeAmount.LOW]] }
const multiRoute: RoutingDiagramEntry[] = [
{ percent: percent`75`, path: [[USDC, DAI, FeeAmount.LOW]] },
{
percent: percent`25`,
path: [
[USDC, WBTC, FeeAmount.MEDIUM],
[WBTC, DAI, FeeAmount.HIGH],
],
},
]
jest.mock(
'components/CurrencyLogo',
() =>
({ currency }: { currency: Currency }) =>
`CurrencyLogo currency=${currency.symbol}`
)
jest.mock(
'components/DoubleLogo',
() =>
({ currency0, currency1 }: { currency0: Currency; currency1: Currency }) =>
`DoubleCurrencyLogo currency0=${currency0.symbol} currency1=${currency1.symbol}`
)
jest.mock('../Popover', () => () => 'Popover')
jest.mock('hooks/useTokenInfoFromActiveList', () => ({
useTokenInfoFromActiveList: (currency: Currency) => currency,
}))
it('renders when no routes are provided', () => {
const { asFragment } = render(<RoutingDiagram currencyIn={DAI} currencyOut={USDC} routes={[]} />)
expect(asFragment()).toMatchSnapshot()
})
it('renders single route', () => {
const { asFragment } = render(<RoutingDiagram currencyIn={USDC} currencyOut={DAI} routes={[singleRoute]} />)
expect(asFragment()).toMatchSnapshot()
})
it('renders multi route', () => {
const { asFragment } = render(<RoutingDiagram currencyIn={USDC} currencyOut={DAI} routes={multiRoute} />)
expect(asFragment()).toMatchSnapshot()
})
import { Currency, Percent } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import Badge from 'components/Badge'
import CurrencyLogo from 'components/CurrencyLogo'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import Row, { AutoRow } from 'components/Row'
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
import { Box } from 'rebass'
import styled from 'styled-components/macro'
import { TYPE } from 'theme'
export interface RoutingDiagramEntry {
percent: Percent
path: [Currency, Currency, FeeAmount][]
}
const Wrapper = styled(Box)`
align-items: center;
background-color: ${({ theme }) => theme.bg0};
width: 400px;
`
const RouteContainerRow = styled(Row)`
display: grid;
grid-gap: 8px;
grid-template-columns: 24px 1fr 24px;
`
const RouteRow = styled(Row)`
align-items: center;
display: flex;
justify-content: center;
padding: 0.1rem 0.5rem;
position: relative;
`
const PoolBadge = styled(Badge)`
display: flex;
padding: 0.25rem 0.5rem;
`
const DottedLine = styled.div`
border-color: ${({ theme }) => theme.bg4};
border-top-style: dotted;
border-width: 4px;
height: 0px;
position: absolute;
width: calc(100%);
z-index: 1;
`
const OpaqueBadge = styled(Badge)`
background-color: ${({ theme }) => theme.bg2};
z-index: 2;
`
export default function RoutingDiagram({
currencyIn,
currencyOut,
routes,
}: {
currencyIn: Currency
currencyOut: Currency
routes: RoutingDiagramEntry[]
}) {
const tokenIn = useTokenInfoFromActiveList(currencyIn)
const tokenOut = useTokenInfoFromActiveList(currencyOut)
return (
<Wrapper>
{routes.map(({ percent, path }, index) => (
<RouteContainerRow key={index}>
<CurrencyLogo currency={tokenIn} />
<Route percent={percent} path={path} />
<CurrencyLogo currency={tokenOut} />
</RouteContainerRow>
))}
</Wrapper>
)
}
function Route({ percent, path }: { percent: RoutingDiagramEntry['percent']; path: RoutingDiagramEntry['path'] }) {
return (
<RouteRow>
<DottedLine />
<OpaqueBadge>
<TYPE.small fontSize={12} style={{ wordBreak: 'normal' }}>
{percent.toSignificant(2)}%
</TYPE.small>
</OpaqueBadge>
<AutoRow gap="1px" width="100%" style={{ justifyContent: 'space-evenly', zIndex: 2 }}>
{path.map(([currency0, currency1, feeAmount], index) => (
<Pool key={index} currency0={currency0} currency1={currency1} feeAmount={feeAmount} />
))}
</AutoRow>
</RouteRow>
)
}
function Pool({ currency0, currency1, feeAmount }: { currency0: Currency; currency1: Currency; feeAmount: FeeAmount }) {
const tokenInfo0 = useTokenInfoFromActiveList(currency0)
const tokenInfo1 = useTokenInfoFromActiveList(currency1)
return (
<PoolBadge>
<Box margin="0 5px 0 10px">
<DoubleCurrencyLogo currency0={tokenInfo0} currency1={tokenInfo1} size={20} />
</Box>
<TYPE.small fontSize={12}>{feeAmount / 10000}%</TYPE.small>
</PoolBadge>
)
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders multi route 1`] = `
<DocumentFragment>
<div
class="RoutingDiagram__Wrapper-sc-o1ook0-0 fUoVYh css-vurnku"
>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV iiQQUx"
>
CurrencyLogo currency=USDC
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteRow-sc-o1ook0-2 lmTMKd itvFNV fzMiot"
>
<div
class="RoutingDiagram__DottedLine-sc-o1ook0-4 gwivhA"
/>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-5 gayll cvyxdH"
>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9"
style="word-break: normal;"
>
75%
</div>
</div>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 Row__AutoRow-sc-nrd8cx-3 iqvZFe itvFNV kkMfuq"
style="justify-content: space-evenly; z-index: 2;"
width="100%"
>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__PoolBadge-sc-o1ook0-3 gayll bRJvWg"
>
<div
class="css-1t7xebc"
>
DoubleCurrencyLogo currency0=USDC currency1=DAI
</div>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9"
>
0.05%
</div>
</div>
</div>
</div>
CurrencyLogo currency=DAI
</div>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV iiQQUx"
>
CurrencyLogo currency=USDC
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteRow-sc-o1ook0-2 lmTMKd itvFNV fzMiot"
>
<div
class="RoutingDiagram__DottedLine-sc-o1ook0-4 gwivhA"
/>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-5 gayll cvyxdH"
>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9"
style="word-break: normal;"
>
25%
</div>
</div>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 Row__AutoRow-sc-nrd8cx-3 iqvZFe itvFNV kkMfuq"
style="justify-content: space-evenly; z-index: 2;"
width="100%"
>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__PoolBadge-sc-o1ook0-3 gayll bRJvWg"
>
<div
class="css-1t7xebc"
>
DoubleCurrencyLogo currency0=USDC currency1=WBTC
</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=WBTC currency1=DAI
</div>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9"
>
1%
</div>
</div>
</div>
</div>
CurrencyLogo currency=DAI
</div>
</div>
</DocumentFragment>
`;
exports[`renders single route 1`] = `
<DocumentFragment>
<div
class="RoutingDiagram__Wrapper-sc-o1ook0-0 fUoVYh css-vurnku"
>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV iiQQUx"
>
CurrencyLogo currency=USDC
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteRow-sc-o1ook0-2 lmTMKd itvFNV fzMiot"
>
<div
class="RoutingDiagram__DottedLine-sc-o1ook0-4 gwivhA"
/>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-5 gayll cvyxdH"
>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9"
style="word-break: normal;"
>
100%
</div>
</div>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 Row__AutoRow-sc-nrd8cx-3 iqvZFe itvFNV kkMfuq"
style="justify-content: space-evenly; z-index: 2;"
width="100%"
>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__PoolBadge-sc-o1ook0-3 gayll bRJvWg"
>
<div
class="css-1t7xebc"
>
DoubleCurrencyLogo currency0=USDC currency1=DAI
</div>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab css-15li2d9"
>
0.05%
</div>
</div>
</div>
</div>
CurrencyLogo currency=DAI
</div>
</div>
</DocumentFragment>
`;
exports[`renders when no routes are provided 1`] = `
<DocumentFragment>
<div
class="RoutingDiagram__Wrapper-sc-o1ook0-0 fUoVYh css-vurnku"
/>
</DocumentFragment>
`;
...@@ -7,7 +7,7 @@ import styled, { ThemeContext } from 'styled-components/macro' ...@@ -7,7 +7,7 @@ import styled, { ThemeContext } from 'styled-components/macro'
import { useOnClickOutside } from '../../hooks/useOnClickOutside' import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import { ApplicationModal } from '../../state/application/actions' import { ApplicationModal } from '../../state/application/actions'
import { useModalOpen, useToggleSettingsMenu } from '../../state/application/hooks' import { useModalOpen, useToggleSettingsMenu } from '../../state/application/hooks'
import { useExpertModeManager, useUserSingleHopOnly } from '../../state/user/hooks' import { useExpertModeManager, useClientSideRouter } from '../../state/user/hooks'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { ButtonError } from '../Button' import { ButtonError } from '../Button'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
...@@ -17,6 +17,8 @@ import { RowBetween, RowFixed } from '../Row' ...@@ -17,6 +17,8 @@ import { RowBetween, RowFixed } from '../Row'
import Toggle from '../Toggle' import Toggle from '../Toggle'
import TransactionSettings from '../TransactionSettings' import TransactionSettings from '../TransactionSettings'
import { Percent } from '@uniswap/sdk-core' import { Percent } from '@uniswap/sdk-core'
import { useActiveWeb3React } from 'hooks/web3'
import { SupportedChainId } from 'constants/chains'
const StyledMenuIcon = styled(Settings)` const StyledMenuIcon = styled(Settings)`
height: 20px; height: 20px;
...@@ -115,6 +117,8 @@ const ModalContentWrapper = styled.div` ...@@ -115,6 +117,8 @@ const ModalContentWrapper = styled.div`
` `
export default function SettingsTab({ placeholderSlippage }: { placeholderSlippage: Percent }) { export default function SettingsTab({ placeholderSlippage }: { placeholderSlippage: Percent }) {
const { chainId } = useActiveWeb3React()
const node = useRef<HTMLDivElement>() const node = useRef<HTMLDivElement>()
const open = useModalOpen(ApplicationModal.SETTINGS) const open = useModalOpen(ApplicationModal.SETTINGS)
const toggle = useToggleSettingsMenu() const toggle = useToggleSettingsMenu()
...@@ -123,7 +127,7 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa ...@@ -123,7 +127,7 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
const [expertMode, toggleExpertMode] = useExpertModeManager() const [expertMode, toggleExpertMode] = useExpertModeManager()
const [singleHopOnly, setSingleHopOnly] = useUserSingleHopOnly() const [clientSideRouter, setClientSideRouter] = useClientSideRouter()
// show confirmation view before turning on // show confirmation view before turning on
const [showConfirmation, setShowConfirmation] = useState(false) const [showConfirmation, setShowConfirmation] = useState(false)
...@@ -193,10 +197,35 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa ...@@ -193,10 +197,35 @@ 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 === SupportedChainId.MAINNET && (
<RowBetween>
<RowFixed>
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
<Trans>Auto Router</Trans>
</TYPE.black>
<QuestionHelper
text={<Trans>Use the Uniswap Labs API to get better pricing through a more efficient route.</Trans>}
/>
</RowFixed>
<Toggle
id="toggle-optimized-router-button"
isActive={!clientSideRouter}
toggle={() => {
ReactGA.event({
category: 'Routing',
action: clientSideRouter ? 'enable routing API' : 'disable routing API',
})
setClientSideRouter(!clientSideRouter)
}}
/>
</RowBetween>
)}
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}> <TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
<Trans>Toggle Expert Mode</Trans> <Trans>Expert Mode</Trans>
</TYPE.black> </TYPE.black>
<QuestionHelper <QuestionHelper
text={ text={
...@@ -220,25 +249,6 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa ...@@ -220,25 +249,6 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
} }
/> />
</RowBetween> </RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
<Trans>Disable Multihops</Trans>
</TYPE.black>
<QuestionHelper text={<Trans>Restricts swaps to direct pairs only.</Trans>} />
</RowFixed>
<Toggle
id="toggle-disable-multihop-button"
isActive={singleHopOnly}
toggle={() => {
ReactGA.event({
category: 'Routing',
action: singleHopOnly ? 'disable single hop' : 'enable single hop',
})
setSingleHopOnly(!singleHopOnly)
}}
/>
</RowBetween>
</AutoColumn> </AutoColumn>
</MenuFlyout> </MenuFlyout>
)} )}
......
import { TextInput, ResizingTextArea } from './' import { TextInput, ResizingTextArea } from './'
import { render, screen, fireEvent } from 'test-utils' import { render, screen, fireEvent } from 'test-utils'
// include style rules in snapshots
import 'jest-styled-components'
describe('TextInput', () => { describe('TextInput', () => {
it('renders correctly', () => { it('renders correctly', () => {
......
import { transparentize } from 'polished'
import { ReactNode, useCallback, useState } from 'react' import { ReactNode, useCallback, useState } from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import Popover, { PopoverProps } from '../Popover' import Popover, { PopoverProps } from '../Popover'
const TooltipContainer = styled.div` export const TooltipContainer = styled.div`
width: 256px; width: 256px;
padding: 0.6rem 1rem; padding: 0.6rem 1rem;
font-weight: 400; font-weight: 400;
word-break: break-word; word-break: break-word;
background: ${({ theme }) => theme.bg0};
border-radius: 12px;
border: 1px solid ${({ theme }) => theme.bg2};
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)};
` `
interface TooltipProps extends Omit<PopoverProps, 'content'> { interface TooltipProps extends Omit<PopoverProps, 'content'> {
...@@ -15,14 +21,17 @@ interface TooltipProps extends Omit<PopoverProps, 'content'> { ...@@ -15,14 +21,17 @@ interface TooltipProps extends Omit<PopoverProps, 'content'> {
interface TooltipContentProps extends Omit<PopoverProps, 'content'> { interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
content: ReactNode content: ReactNode
onOpen?: () => void
// whether to wrap the content in a `TooltipContainer`
wrap?: boolean
} }
export default function Tooltip({ text, ...rest }: TooltipProps) { export default function Tooltip({ text, ...rest }: TooltipProps) {
return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} /> return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} />
} }
function TooltipContent({ content, ...rest }: TooltipContentProps) { function TooltipContent({ content, wrap = false, ...rest }: TooltipContentProps) {
return <Popover content={<TooltipContainer>{content}</TooltipContainer>} {...rest} /> return <Popover content={wrap ? <TooltipContainer>{content}</TooltipContainer> : content} {...rest} />
} }
export function MouseoverTooltip({ children, ...rest }: Omit<TooltipProps, 'show'>) { export function MouseoverTooltip({ children, ...rest }: Omit<TooltipProps, 'show'>) {
...@@ -38,9 +47,17 @@ export function MouseoverTooltip({ children, ...rest }: Omit<TooltipProps, 'show ...@@ -38,9 +47,17 @@ export function MouseoverTooltip({ children, ...rest }: Omit<TooltipProps, 'show
) )
} }
export function MouseoverTooltipContent({ content, children, ...rest }: Omit<TooltipContentProps, 'show'>) { export function MouseoverTooltipContent({
content,
children,
onOpen: openCallback = undefined,
...rest
}: Omit<TooltipContentProps, 'show'>) {
const [show, setShow] = useState(false) const [show, setShow] = useState(false)
const open = useCallback(() => setShow(true), [setShow]) const open = useCallback(() => {
setShow(true)
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={content}>
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Percent, Currency, 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 { Trade as V3Trade } from '@uniswap/v3-sdk'
import { LoadingRows } from 'components/Loader/styled'
import { useContext, useMemo } from 'react' import { useContext, useMemo } from 'react'
import { ThemeContext } from 'styled-components/macro' import { ThemeContext } from 'styled-components/macro'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
...@@ -9,14 +10,33 @@ import { computeRealizedLPFeePercent } from '../../utils/prices' ...@@ -9,14 +10,33 @@ 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 SwapRoute from './SwapRoute' import { TransactionDetailsLabel } from './styleds'
interface AdvancedSwapDetailsProps { interface AdvancedSwapDetailsProps {
trade?: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> trade?: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
allowedSlippage: Percent allowedSlippage: Percent
syncing?: boolean
} }
export function AdvancedSwapDetails({ trade, allowedSlippage }: AdvancedSwapDetailsProps) { function TextWithLoadingPlaceholder({
syncing,
width,
children,
}: {
syncing: boolean
width: number
children: JSX.Element
}) {
return syncing ? (
<LoadingRows>
<div style={{ height: '15px', width: `${width}px` }} />
</LoadingRows>
) : (
children
)
}
export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const { realizedLPFee, priceImpact } = useMemo(() => { const { realizedLPFee, priceImpact } = useMemo(() => {
...@@ -30,61 +50,61 @@ export function AdvancedSwapDetails({ trade, allowedSlippage }: AdvancedSwapDeta ...@@ -30,61 +50,61 @@ export function AdvancedSwapDetails({ trade, allowedSlippage }: AdvancedSwapDeta
return !trade ? null : ( return !trade ? null : (
<AutoColumn gap="8px"> <AutoColumn gap="8px">
<TransactionDetailsLabel fontWeight={500} fontSize={14}>
<Trans>Transaction Details</Trans>
</TransactionDetailsLabel>
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
<TYPE.black fontSize={12} fontWeight={400} color={theme.text2}> <TYPE.subHeader color={theme.text1}>
<Trans>Liquidity Provider Fee</Trans> <Trans>Liquidity Provider Fee</Trans>
</TYPE.black> </TYPE.subHeader>
</RowFixed> </RowFixed>
<TYPE.black textAlign="right" fontSize={12} color={theme.text1}> <TextWithLoadingPlaceholder syncing={syncing} width={65}>
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${realizedLPFee.currency.symbol}` : '-'} <TYPE.black textAlign="right" fontSize={14}>
</TYPE.black> {realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${realizedLPFee.currency.symbol}` : '-'}
</RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontSize={12} fontWeight={400} color={theme.text2}>
<Trans>Route</Trans>
</TYPE.black> </TYPE.black>
</RowFixed> </TextWithLoadingPlaceholder>
<TYPE.black textAlign="right" fontSize={12} color={theme.text1}>
<SwapRoute trade={trade} />
</TYPE.black>
</RowBetween> </RowBetween>
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
<TYPE.black fontSize={12} fontWeight={400} color={theme.text2}> <TYPE.subHeader color={theme.text1}>
<Trans>Price Impact</Trans> <Trans>Price Impact</Trans>
</TYPE.black> </TYPE.subHeader>
</RowFixed> </RowFixed>
<TYPE.black textAlign="right" fontSize={12} color={theme.text1}> <TextWithLoadingPlaceholder syncing={syncing} width={50}>
<FormattedPriceImpact priceImpact={priceImpact} /> <TYPE.black textAlign="right" fontSize={14}>
</TYPE.black> <FormattedPriceImpact priceImpact={priceImpact} />
</TYPE.black>
</TextWithLoadingPlaceholder>
</RowBetween> </RowBetween>
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
<TYPE.black fontSize={12} fontWeight={400} color={theme.text2}> <TYPE.subHeader color={theme.text1}>
{trade.tradeType === TradeType.EXACT_INPUT ? <Trans>Minimum received</Trans> : <Trans>Maximum sent</Trans>} <Trans>Allowed Slippage</Trans>
</TYPE.black> </TYPE.subHeader>
</RowFixed> </RowFixed>
<TYPE.black textAlign="right" fontSize={12} color={theme.text1}> <TextWithLoadingPlaceholder syncing={syncing} width={45}>
{trade.tradeType === TradeType.EXACT_INPUT <TYPE.black textAlign="right" fontSize={14}>
? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}` {allowedSlippage.toFixed(2)}%
: `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`} </TYPE.black>
</TYPE.black> </TextWithLoadingPlaceholder>
</RowBetween> </RowBetween>
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
<TYPE.black fontSize={12} fontWeight={400} color={theme.text2}> <TYPE.subHeader color={theme.text1}>
<Trans>Slippage tolerance</Trans> {trade.tradeType === TradeType.EXACT_INPUT ? <Trans>Minimum received</Trans> : <Trans>Maximum sent</Trans>}
</TYPE.black> </TYPE.subHeader>
</RowFixed> </RowFixed>
<TYPE.black textAlign="right" fontSize={12} color={theme.text1}> <TextWithLoadingPlaceholder syncing={syncing} width={70}>
{allowedSlippage.toFixed(2)}% <TYPE.black textAlign="right" fontSize={14}>
</TYPE.black> {trade.tradeType === TradeType.EXACT_INPUT
? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}`
: `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`}
</TYPE.black>
</TextWithLoadingPlaceholder>
</RowBetween> </RowBetween>
</AutoColumn> </AutoColumn>
) )
......
import { stringify } from 'qs'
import { useMemo } from 'react'
import { useLocation } from 'react-router'
import { Link } from 'react-router-dom'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import { DEFAULT_VERSION, Version } from '../../hooks/useToggledVersion'
import { HideSmall, TYPE, SmallOnly } from '../../theme'
import { ButtonPrimary } from '../Button'
import styled from 'styled-components/macro'
import { Zap } from 'react-feather'
const ResponsiveButton = styled(ButtonPrimary)`
width: fit-content;
padding: 0.2rem 0.5rem;
word-break: keep-all;
height: 24px;
margin-left: 0.75rem;
${({ theme }) => theme.mediaWidth.upToSmall`
padding: 4px;
border-radius: 8px;
`};
`
export default function BetterTradeLink({
version,
otherTradeNonexistent = false,
}: {
version: Version
otherTradeNonexistent: boolean
}) {
const location = useLocation()
const search = useParsedQueryString()
const linkDestination = useMemo(() => {
return {
...location,
search: `?${stringify({
...search,
use: version !== DEFAULT_VERSION ? version : undefined,
})}`,
}
}, [location, search, version])
return (
<ResponsiveButton as={Link} to={linkDestination}>
<Zap size={12} style={{ marginRight: '0.25rem' }} />
<HideSmall>
<TYPE.small style={{ lineHeight: '120%' }} fontSize={12}>
{otherTradeNonexistent
? `No liquidity! Click to trade with ${version.toUpperCase()}`
: `Get a better price on ${version.toUpperCase()}`}
</TYPE.small>
</HideSmall>
<SmallOnly>
<TYPE.small style={{ lineHeight: '120%' }} fontSize={12}>
{otherTradeNonexistent
? `No liquidity! Click to trade with ${version.toUpperCase()}`
: `Better ${version.toUpperCase()} price`}
</TYPE.small>
</SmallOnly>
</ResponsiveButton>
)
}
...@@ -7,7 +7,7 @@ import { ErrorText } from './styleds' ...@@ -7,7 +7,7 @@ import { ErrorText } from './styleds'
*/ */
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) { export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
return ( return (
<ErrorText fontWeight={500} fontSize={12} severity={warningSeverity(priceImpact)}> <ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}>
{priceImpact ? `${priceImpact.multiply(-1).toFixed(2)}%` : '-'} {priceImpact ? `${priceImpact.multiply(-1).toFixed(2)}%` : '-'}
</ErrorText> </ErrorText>
) )
......
import { Trans } from '@lingui/macro'
import { useRoutingAPIEnabled } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { TYPE } from 'theme'
import { ReactComponent as AutoRouterIcon } from '../../assets/svg/auto_router.svg'
const StyledAutoRouterIcon = styled(AutoRouterIcon)`
height: 16px;
width: 16px;
stroke: ${({ theme }) => theme.blue1};
`
const DisabledAutoRouterIcon = styled(StyledAutoRouterIcon)`
stroke: ${({ theme }) => theme.text3};
:hover {
stroke: ${({ theme }) => theme.text1};
}
`
const StyledAutoRouterLabel = styled(TYPE.black)`
line-height: 1rem;
/* fallback color */
color: ${({ theme }) => theme.green1};
@supports (-webkit-background-clip: text) and (-webkit-text-fill-color: transparent) {
background-image: linear-gradient(90deg, #2172e5 0%, #54e521 163.16%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
`
export function AutoRouterLogo() {
const routingAPIEnabled = useRoutingAPIEnabled()
return routingAPIEnabled ? <StyledAutoRouterIcon /> : <DisabledAutoRouterIcon />
}
export function AutoRouterLabel() {
const routingAPIEnabled = useRoutingAPIEnabled()
return routingAPIEnabled ? (
<StyledAutoRouterLabel fontSize={14}>Auto Router</StyledAutoRouterLabel>
) : (
<TYPE.black fontSize={14}>
<Trans>Trade Route</Trans>
</TYPE.black>
)
}
import { Currency, TradeType } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro'
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, FeeAmount } from '@uniswap/v3-sdk' import { FeeAmount, Trade as V3Trade } from '@uniswap/v3-sdk'
import { Fragment, memo, useContext } from 'react' import Badge from 'components/Badge'
import { ChevronRight } from 'react-feather' import { AutoColumn } from 'components/Column'
import { Flex } from 'rebass' import { LoadingRows } from 'components/Loader/styled'
import { ThemeContext } from 'styled-components/macro' import RoutingDiagram, { RoutingDiagramEntry } from 'components/RoutingDiagram/RoutingDiagram'
import { TYPE } from '../../theme' import { AutoRow, RowBetween } from 'components/Row'
import { unwrappedToken } from 'utils/unwrappedToken' import { Version } from 'hooks/useToggledVersion'
import { memo } from 'react'
function LabeledArrow({}: { fee: FeeAmount }) { import { useRoutingAPIEnabled } from 'state/user/hooks'
const theme = useContext(ThemeContext) import styled from 'styled-components/macro'
import { TYPE } from 'theme'
// todo: render the fee in the label import { getTradeVersion } from 'utils/getTradeVersion'
return <ChevronRight size={14} color={theme.text2} /> import { AutoRouterLabel, AutoRouterLogo } from './RouterLabel'
}
const Separator = styled.div`
border-top: 1px solid ${({ theme }) => theme.bg2};
height: 1px;
width: 100%;
`
const V2_DEFAULT_FEE_TIER = 3000
export default memo(function SwapRoute({ export default memo(function SwapRoute({
trade, trade,
syncing,
}: { }: {
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
syncing: boolean
}) { }) {
const tokenPath = trade instanceof V2Trade ? trade.route.path : trade.route.tokenPath const routingAPIEnabled = useRoutingAPIEnabled()
const theme = useContext(ThemeContext)
return ( return (
<Flex flexWrap="wrap" width="100%" justifyContent="flex-start" alignItems="center"> <AutoColumn gap="12px">
{tokenPath.map((token, i, path) => { <RowBetween>
const isLastItem: boolean = i === path.length - 1 <AutoRow gap="4px" width="auto">
const currency = unwrappedToken(token) <AutoRouterLogo />
return ( <AutoRouterLabel />
<Fragment key={i}> </AutoRow>
<Flex alignItems="end"> {syncing ? (
<TYPE.black color={theme.text1} ml="0.145rem" mr="0.145rem"> <LoadingRows>
{currency.symbol} <div style={{ width: '30px', height: '24px' }} />
</TYPE.black> </LoadingRows>
</Flex> ) : (
{isLastItem ? null : trade instanceof V2Trade ? ( <Badge>
<ChevronRight size={14} color={theme.text2} /> <TYPE.black fontSize={12}>
) : ( {getTradeVersion(trade) === Version.v2 ? <Trans>V2</Trans> : <Trans>V3</Trans>}
<LabeledArrow fee={trade.route.pools[i].fee} /> </TYPE.black>
)} </Badge>
</Fragment> )}
) </RowBetween>
})} <Separator />
</Flex> {syncing ? (
<LoadingRows>
<div style={{ width: '400px', height: '30px' }} />
</LoadingRows>
) : (
<RoutingDiagram
currencyIn={trade.inputAmount.currency}
currencyOut={trade.outputAmount.currency}
routes={getTokenPath(trade)}
/>
)}
{routingAPIEnabled && (
<TYPE.main fontSize={12} width={400}>
<Trans>This route optimizes your price by considering split routes, multiple hops, and gas costs.</Trans>
</TYPE.main>
)}
</AutoColumn>
) )
}) })
function getTokenPath(
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
): RoutingDiagramEntry[] {
// convert V2 path to a list of routes
if (trade instanceof V2Trade) {
const { path: tokenPath } = (trade as V2Trade<Currency, Currency, TradeType>).route
const path = []
for (let i = 1; i < tokenPath.length; i++) {
path.push([tokenPath[i - 1], tokenPath[i], V2_DEFAULT_FEE_TIER] as RoutingDiagramEntry['path'][0])
}
return [{ percent: new Percent(100, 100), path }]
}
return trade.swaps.map(({ route: { tokenPath, pools }, inputAmount, outputAmount }) => {
const portion =
trade.tradeType === TradeType.EXACT_INPUT
? inputAmount.divide(trade.inputAmount)
: outputAmount.divide(trade.outputAmount)
const percent = new Percent(portion.numerator, portion.denominator)
const path: [Currency, Currency, FeeAmount][] = []
for (let i = 0; i < pools.length; i++) {
const nextPool = pools[i]
const tokenIn = tokenPath[i]
const tokenOut = tokenPath[i + 1]
path.push([tokenIn, tokenOut, nextPool.fee])
}
return {
percent,
path,
}
})
}
import { useCallback } from 'react' import { useCallback, useContext } from 'react'
import { Price, Currency } from '@uniswap/sdk-core' import { Currency, Price } from '@uniswap/sdk-core'
import { useContext } from 'react'
import { Text } from 'rebass' import { Text } from 'rebass'
import styled, { ThemeContext } from 'styled-components/macro' import styled, { ThemeContext } from 'styled-components/macro'
......
...@@ -5,6 +5,10 @@ import { AlertTriangle } from 'react-feather' ...@@ -5,6 +5,10 @@ import { AlertTriangle } from 'react-feather'
import styled, { css } from 'styled-components/macro' import styled, { css } from 'styled-components/macro'
import { Text } from 'rebass' import { Text } from 'rebass'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import { TYPE } from 'theme'
import { TooltipContainer } from 'components/Tooltip'
import TradePrice from './TradePrice'
import { loadingOpacityMixin } from 'components/Loader/styled'
export const Wrapper = styled.div` export const Wrapper = styled.div`
position: relative; position: relative;
...@@ -128,3 +132,24 @@ export const SwapShowAcceptChanges = styled(AutoColumn)` ...@@ -128,3 +132,24 @@ export const SwapShowAcceptChanges = styled(AutoColumn)`
border-radius: 12px; border-radius: 12px;
margin-top: 8px; margin-top: 8px;
` `
export const TransactionDetailsLabel = styled(TYPE.black)`
border-bottom: 1px solid ${({ theme }) => theme.bg2};
padding-bottom: 0.5rem;
`
export const ResponsiveTooltipContainer = styled(TooltipContainer)<{ origin?: string; width?: string }>`
background-color: ${({ theme }) => theme.bg0};
border: 1px solid ${({ theme }) => theme.bg2};
padding: 1rem;
width: ${({ width }) => width ?? 'auto'};
${({ theme, origin }) => theme.mediaWidth.upToExtraSmall`
transform: scale(0.8);
transform-origin: ${origin ?? 'top left'};
`}
`
export const StyledTradePrice = styled(TradePrice)<{ $loading: boolean }>`
${loadingOpacityMixin}
`
...@@ -36,4 +36,5 @@ export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt( ...@@ -36,4 +36,5 @@ export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(
export const BETTER_TRADE_LESS_HOPS_THRESHOLD = new Percent(JSBI.BigInt(50), BIPS_BASE) export const BETTER_TRADE_LESS_HOPS_THRESHOLD = new Percent(JSBI.BigInt(50), BIPS_BASE)
export const ZERO_PERCENT = new Percent('0') export const ZERO_PERCENT = new Percent('0')
export const TWO_PERCENT = new Percent(JSBI.BigInt(200), BIPS_BASE)
export const ONE_HUNDRED_PERCENT = new Percent('1') export const ONE_HUNDRED_PERCENT = new Percent('1')
import { Currency } from '@uniswap/sdk-core' import { Currency } from '@uniswap/sdk-core'
import { Pool, Route } from '@uniswap/v3-sdk' import { Pool, Route } from '@uniswap/v3-sdk'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useUserSingleHopOnly } from '../state/user/hooks'
import { useActiveWeb3React } from './web3' import { useActiveWeb3React } from './web3'
import { useV3SwapPools } from './useV3SwapPools' import { useV3SwapPools } from './useV3SwapPools'
...@@ -66,12 +65,10 @@ export function useAllV3Routes( ...@@ -66,12 +65,10 @@ export function useAllV3Routes(
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const { pools, loading: poolsLoading } = useV3SwapPools(currencyIn, currencyOut) const { pools, loading: poolsLoading } = useV3SwapPools(currencyIn, currencyOut)
const [singleHopOnly] = useUserSingleHopOnly()
return useMemo(() => { return useMemo(() => {
if (poolsLoading || !chainId || !pools || !currencyIn || !currencyOut) return { loading: true, routes: [] } if (poolsLoading || !chainId || !pools || !currencyIn || !currencyOut) return { loading: true, routes: [] }
const routes = computeAllRoutes(currencyIn, currencyOut, pools, chainId, [], [], currencyIn, singleHopOnly ? 1 : 2) const routes = computeAllRoutes(currencyIn, currencyOut, pools, chainId, [], [], currencyIn, 2)
return { loading: false, routes } return { loading: false, routes }
}, [chainId, currencyIn, currencyOut, pools, poolsLoading, singleHopOnly]) }, [chainId, currencyIn, currencyOut, pools, poolsLoading])
} }
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { Route, Trade, SwapQuoter } from '@uniswap/v3-sdk' import { Route, SwapQuoter, Trade } from '@uniswap/v3-sdk'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { BigNumber } from 'ethers' import { BigNumber } from 'ethers'
import { useMemo } from 'react' import { useMemo } from 'react'
import { V3TradeState } from 'state/routing/types'
import { useSingleContractWithCallData } from '../state/multicall/hooks' import { useSingleContractWithCallData } from '../state/multicall/hooks'
import { useAllV3Routes } from './useAllV3Routes' import { useAllV3Routes } from './useAllV3Routes'
import { useV3Quoter } from './useContract' import { useV3Quoter } from './useContract'
import { useActiveWeb3React } from './web3' import { useActiveWeb3React } from './web3'
export enum V3TradeState {
LOADING,
INVALID,
NO_ROUTE_FOUND,
VALID,
SYNCING,
}
const QUOTE_GAS_OVERRIDES: { [chainId: number]: number } = { const QUOTE_GAS_OVERRIDES: { [chainId: number]: number } = {
[SupportedChainId.OPTIMISM]: 6_000_000, [SupportedChainId.OPTIMISM]: 6_000_000,
[SupportedChainId.OPTIMISTIC_KOVAN]: 6_000_000, [SupportedChainId.OPTIMISTIC_KOVAN]: 6_000_000,
...@@ -28,7 +21,7 @@ const DEFAULT_GAS_QUOTE = 2_000_000 ...@@ -28,7 +21,7 @@ const DEFAULT_GAS_QUOTE = 2_000_000
* @param amountIn the amount to swap in * @param amountIn the amount to swap in
* @param currencyOut the desired output currency * @param currencyOut the desired output currency
*/ */
export function useBestV3TradeExactIn( export function useClientV3TradeExactIn(
amountIn?: CurrencyAmount<Currency>, amountIn?: CurrencyAmount<Currency>,
currencyOut?: Currency currencyOut?: Currency
): { state: V3TradeState; trade: Trade<Currency, Currency, TradeType.EXACT_INPUT> | null } { ): { state: V3TradeState; trade: Trade<Currency, Currency, TradeType.EXACT_INPUT> | null } {
...@@ -97,10 +90,8 @@ export function useBestV3TradeExactIn( ...@@ -97,10 +90,8 @@ export function useBestV3TradeExactIn(
} }
} }
const isSyncing = quotesResults.some(({ syncing }) => syncing)
return { return {
state: isSyncing ? V3TradeState.SYNCING : V3TradeState.VALID, state: V3TradeState.VALID,
trade: Trade.createUncheckedTrade({ trade: Trade.createUncheckedTrade({
route: bestRoute, route: bestRoute,
tradeType: TradeType.EXACT_INPUT, tradeType: TradeType.EXACT_INPUT,
...@@ -116,7 +107,7 @@ export function useBestV3TradeExactIn( ...@@ -116,7 +107,7 @@ export function useBestV3TradeExactIn(
* @param currencyIn the desired input currency * @param currencyIn the desired input currency
* @param amountOut the amount to swap out * @param amountOut the amount to swap out
*/ */
export function useBestV3TradeExactOut( export function useClientSideV3TradeExactOut(
currencyIn?: Currency, currencyIn?: Currency,
amountOut?: CurrencyAmount<Currency> amountOut?: CurrencyAmount<Currency>
): { state: V3TradeState; trade: Trade<Currency, Currency, TradeType.EXACT_OUTPUT> | null } { ): { state: V3TradeState; trade: Trade<Currency, Currency, TradeType.EXACT_OUTPUT> | null } {
...@@ -186,10 +177,8 @@ export function useBestV3TradeExactOut( ...@@ -186,10 +177,8 @@ export function useBestV3TradeExactOut(
} }
} }
const isSyncing = quotesResults.some(({ syncing }) => syncing)
return { return {
state: isSyncing ? V3TradeState.SYNCING : V3TradeState.VALID, state: V3TradeState.VALID,
trade: Trade.createUncheckedTrade({ trade: Trade.createUncheckedTrade({
route: bestRoute, route: bestRoute,
tradeType: TradeType.EXACT_OUTPUT, tradeType: TradeType.EXACT_OUTPUT,
......
import { renderHook } from '@testing-library/react-hooks'
import { CurrencyAmount } from '@uniswap/sdk-core'
import { DAI, USDC } from 'constants/tokens'
import { V3TradeState } from 'state/routing/types'
import { useRoutingAPIEnabled } from 'state/user/hooks'
import { useRoutingAPITradeExactIn, useRoutingAPITradeExactOut } from '../state/routing/useRoutingAPITrade'
import { useV3TradeExactIn, useV3TradeExactOut } from './useCombinedV3Trade'
import useDebounce from './useDebounce'
import useIsWindowVisible from './useIsWindowVisible'
import { useClientV3TradeExactIn, useClientSideV3TradeExactOut } from './useClientSideV3Trade'
const USDCAmount = CurrencyAmount.fromRawAmount(USDC, '10000')
const DAIAmount = CurrencyAmount.fromRawAmount(DAI, '10000')
jest.mock('./useDebounce')
const mockUseDebounce = useDebounce as jest.MockedFunction<typeof useDebounce>
// mock modules containing hooks
jest.mock('state/routing/useRoutingAPITrade')
jest.mock('./useClientSideV3Trade')
jest.mock('state/user/hooks')
jest.mock('./useIsWindowVisible')
const mockUseRoutingAPIEnabled = useRoutingAPIEnabled as jest.MockedFunction<typeof useRoutingAPIEnabled>
const mockUseIsWindowVisible = useIsWindowVisible as jest.MockedFunction<typeof useIsWindowVisible>
// useRouterTrade mocks
const mockUseRoutingAPITradeExactIn = useRoutingAPITradeExactIn as jest.MockedFunction<typeof useRoutingAPITradeExactIn>
const mockUseRoutingAPITradeExactOut = useRoutingAPITradeExactOut as jest.MockedFunction<
typeof useRoutingAPITradeExactOut
>
// useClientSideV3Trade mocks
const mockUseClientSideV3TradeExactIn = useClientV3TradeExactIn as jest.MockedFunction<typeof useClientV3TradeExactIn>
const mockUseClientSideV3TradeExactOut = useClientSideV3TradeExactOut as jest.MockedFunction<
typeof useClientSideV3TradeExactOut
>
// helpers to set mock expectations
const expectRouterMock = (state: V3TradeState) => {
mockUseRoutingAPITradeExactIn.mockReturnValue({ state, trade: null })
mockUseRoutingAPITradeExactOut.mockReturnValue({ state, trade: null })
}
const expectClientSideMock = (state: V3TradeState) => {
mockUseClientSideV3TradeExactIn.mockReturnValue({ state, trade: null })
mockUseClientSideV3TradeExactOut.mockReturnValue({ state, trade: null })
}
beforeEach(() => {
// ignore debounced value
mockUseDebounce.mockImplementation((value) => value)
mockUseIsWindowVisible.mockReturnValue(true)
mockUseRoutingAPIEnabled.mockReturnValue(true)
})
describe('#useV3TradeExactIn', () => {
it('does not compute routing api trade when routing API is disabled', () => {
mockUseRoutingAPIEnabled.mockReturnValue(false)
expectRouterMock(V3TradeState.INVALID)
expectClientSideMock(V3TradeState.VALID)
const { result } = renderHook(() => useV3TradeExactIn(USDCAmount, DAI))
expect(mockUseRoutingAPITradeExactIn).toHaveBeenCalledWith(undefined, DAI)
expect(mockUseClientSideV3TradeExactIn).toHaveBeenCalledWith(USDCAmount, DAI)
expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null })
})
it('does not compute routing api trade when window is not focused', () => {
mockUseIsWindowVisible.mockReturnValue(false)
expectRouterMock(V3TradeState.NO_ROUTE_FOUND)
expectClientSideMock(V3TradeState.VALID)
const { result } = renderHook(() => useV3TradeExactIn(USDCAmount, DAI))
expect(mockUseRoutingAPITradeExactIn).toHaveBeenCalledWith(undefined, DAI)
expect(mockUseClientSideV3TradeExactIn).toHaveBeenCalledWith(USDCAmount, DAI)
expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null })
})
describe('when routing api is in non-error state', () => {
it('does not compute client side v3 trade if routing api is LOADING', () => {
expectRouterMock(V3TradeState.LOADING)
const { result } = renderHook(() => useV3TradeExactIn(USDCAmount, DAI))
expect(mockUseClientSideV3TradeExactIn).toHaveBeenCalledWith(undefined, undefined)
expect(result.current).toEqual({ state: V3TradeState.LOADING, trade: null })
})
it('does not compute client side v3 trade if routing api is VALID', () => {
expectRouterMock(V3TradeState.VALID)
const { result } = renderHook(() => useV3TradeExactIn(USDCAmount, DAI))
expect(mockUseClientSideV3TradeExactIn).toHaveBeenCalledWith(undefined, undefined)
expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null })
})
it('does not compute client side v3 trade if routing api is SYNCING', () => {
expectRouterMock(V3TradeState.SYNCING)
const { result } = renderHook(() => useV3TradeExactIn(USDCAmount, DAI))
expect(mockUseClientSideV3TradeExactIn).toHaveBeenCalledWith(undefined, undefined)
expect(result.current).toEqual({ state: V3TradeState.SYNCING, trade: null })
})
})
describe('when routing api is in error state', () => {
it('does not compute client side v3 trade if routing api is INVALID', () => {
expectRouterMock(V3TradeState.INVALID)
expectClientSideMock(V3TradeState.VALID)
renderHook(() => useV3TradeExactIn(USDCAmount, DAI))
expect(mockUseClientSideV3TradeExactIn).toHaveBeenCalledWith(undefined, undefined)
})
it('computes client side v3 trade if routing api is NO_ROUTE_FOUND', () => {
expectRouterMock(V3TradeState.NO_ROUTE_FOUND)
expectClientSideMock(V3TradeState.VALID)
const { result } = renderHook(() => useV3TradeExactIn(USDCAmount, DAI))
expect(mockUseClientSideV3TradeExactIn).toHaveBeenCalledWith(USDCAmount, DAI)
expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null })
})
})
})
describe('#useV3TradeExactOut', () => {
it('does not compute routing api trade when routing API is disabled', () => {
mockUseRoutingAPIEnabled.mockReturnValue(false)
expectRouterMock(V3TradeState.INVALID)
expectClientSideMock(V3TradeState.VALID)
const { result } = renderHook(() => useV3TradeExactOut(USDC, DAIAmount))
expect(mockUseRoutingAPITradeExactOut).toHaveBeenCalledWith(undefined, DAIAmount)
expect(mockUseClientSideV3TradeExactOut).toHaveBeenCalledWith(USDC, DAIAmount)
expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null })
})
it('does not compute routing api trade when window is not focused', () => {
mockUseIsWindowVisible.mockReturnValue(false)
expectRouterMock(V3TradeState.NO_ROUTE_FOUND)
expectClientSideMock(V3TradeState.VALID)
const { result } = renderHook(() => useV3TradeExactOut(USDC, DAIAmount))
expect(mockUseRoutingAPITradeExactOut).toHaveBeenCalledWith(undefined, DAIAmount)
expect(mockUseClientSideV3TradeExactOut).toHaveBeenCalledWith(USDC, DAIAmount)
expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null })
})
describe('when routing api is in non-error state', () => {
it('does not compute client side v3 trade if routing api is LOADING', () => {
expectRouterMock(V3TradeState.LOADING)
const { result } = renderHook(() => useV3TradeExactOut(USDC, DAIAmount))
expect(mockUseClientSideV3TradeExactOut).toHaveBeenCalledWith(undefined, undefined)
expect(result.current).toEqual({ state: V3TradeState.LOADING, trade: null })
})
it('does not compute client side v3 trade if routing api is VALID', () => {
expectRouterMock(V3TradeState.VALID)
const { result } = renderHook(() => useV3TradeExactOut(USDC, DAIAmount))
expect(mockUseClientSideV3TradeExactOut).toHaveBeenCalledWith(undefined, undefined)
expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null })
})
it('does not compute client side v3 trade if routing api is SYNCING', () => {
expectRouterMock(V3TradeState.SYNCING)
const { result } = renderHook(() => useV3TradeExactOut(USDC, DAIAmount))
expect(mockUseClientSideV3TradeExactOut).toHaveBeenCalledWith(undefined, undefined)
expect(result.current).toEqual({ state: V3TradeState.SYNCING, trade: null })
})
})
describe('when routing api is in error state', () => {
it('computes client side v3 trade if routing api is INVALID', () => {
expectRouterMock(V3TradeState.INVALID)
expectClientSideMock(V3TradeState.VALID)
renderHook(() => useV3TradeExactOut(USDC, DAIAmount))
expect(mockUseClientSideV3TradeExactOut).toHaveBeenCalledWith(undefined, undefined)
})
it('computes client side v3 trade if routing api is NO_ROUTE_FOUND', () => {
expectRouterMock(V3TradeState.NO_ROUTE_FOUND)
expectClientSideMock(V3TradeState.VALID)
const { result } = renderHook(() => useV3TradeExactOut(USDC, DAIAmount))
expect(mockUseClientSideV3TradeExactOut).toHaveBeenCalledWith(USDC, DAIAmount)
expect(result.current).toEqual({ state: V3TradeState.VALID, trade: null })
})
})
})
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { Trade } from '@uniswap/v3-sdk'
import { V3TradeState } from 'state/routing/types'
import { useRoutingAPITradeExactIn, useRoutingAPITradeExactOut } from 'state/routing/useRoutingAPITrade'
import { useRoutingAPIEnabled } from 'state/user/hooks'
import useDebounce from './useDebounce'
import useIsWindowVisible from './useIsWindowVisible'
import { useClientV3TradeExactIn, useClientSideV3TradeExactOut } from './useClientSideV3Trade'
/**
* Returns the best v3 trade for a desired exact input swap.
* Uses optimized routes from the Routing API and falls back to the v3 router.
* @param amountIn The amount to swap in
* @param currentOut the desired output currency
*/
export function useV3TradeExactIn(
amountIn?: CurrencyAmount<Currency>,
currencyOut?: Currency
): {
state: V3TradeState
trade: Trade<Currency, Currency, TradeType.EXACT_INPUT> | null
} {
const routingAPIEnabled = useRoutingAPIEnabled()
const isWindowVisible = useIsWindowVisible()
const debouncedAmountIn = useDebounce(amountIn, 100)
const routingAPITradeExactIn = useRoutingAPITradeExactIn(
routingAPIEnabled && isWindowVisible ? debouncedAmountIn : undefined,
currencyOut
)
const isLoading = amountIn !== undefined && debouncedAmountIn === undefined
// consider trade debouncing when inputs/outputs do not match
const debouncing =
routingAPITradeExactIn.trade &&
amountIn &&
(!routingAPITradeExactIn.trade.inputAmount.equalTo(amountIn) ||
!amountIn.currency.equals(routingAPITradeExactIn.trade.inputAmount.currency) ||
!currencyOut?.equals(routingAPITradeExactIn.trade.outputAmount.currency))
const useFallback =
!routingAPIEnabled || (!debouncing && routingAPITradeExactIn.state === V3TradeState.NO_ROUTE_FOUND)
// only use client side router if routing api trade failed
const bestV3TradeExactIn = useClientV3TradeExactIn(
useFallback ? debouncedAmountIn : undefined,
useFallback ? currencyOut : undefined
)
return {
...(useFallback ? bestV3TradeExactIn : routingAPITradeExactIn),
...(debouncing ? { state: V3TradeState.SYNCING } : {}),
...(isLoading ? { state: V3TradeState.LOADING } : {}),
}
}
/**
* Returns the best v3 trade for a desired exact output swap.
* Uses optimized routes from the Routing API and falls back to the v3 router.
* @param currentIn the desired input currency
* @param amountOut The amount to swap out
*/
export function useV3TradeExactOut(
currencyIn?: Currency,
amountOut?: CurrencyAmount<Currency>
): {
state: V3TradeState
trade: Trade<Currency, Currency, TradeType.EXACT_OUTPUT> | null
} {
const routingAPIEnabled = useRoutingAPIEnabled()
const isWindowVisible = useIsWindowVisible()
const debouncedAmountOut = useDebounce(amountOut, 100)
const routingAPITradeExactOut = useRoutingAPITradeExactOut(
routingAPIEnabled && isWindowVisible ? currencyIn : undefined,
debouncedAmountOut
)
const isLoading = amountOut !== undefined && debouncedAmountOut === undefined
const debouncing =
routingAPITradeExactOut.trade &&
amountOut &&
(!routingAPITradeExactOut.trade.outputAmount.equalTo(amountOut) ||
!currencyIn?.equals(routingAPITradeExactOut.trade.inputAmount.currency) ||
!amountOut.currency.equals(routingAPITradeExactOut.trade.outputAmount.currency))
const useFallback =
!routingAPIEnabled || (!debouncing && routingAPITradeExactOut.state === V3TradeState.NO_ROUTE_FOUND)
const bestV3TradeExactOut = useClientSideV3TradeExactOut(
useFallback ? currencyIn : undefined,
useFallback ? debouncedAmountOut : undefined
)
return {
...(useFallback ? bestV3TradeExactOut : routingAPITradeExactOut),
...(debouncing ? { state: V3TradeState.SYNCING } : {}),
...(isLoading ? { state: V3TradeState.LOADING } : {}),
}
}
import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import ms from 'ms.macro'
import { useBlockNumber } from 'state/application/hooks'
import { useGetQuoteQuery } from 'state/routing/slice'
import { useActiveWeb3React } from './web3'
export function useRouterTradeExactIn(amountIn?: CurrencyAmount<Currency>, currencyOut?: Currency) {
const { account } = useActiveWeb3React()
const blockNumber = useBlockNumber()
const { isLoading, isError, data } = useGetQuoteQuery(
amountIn && currencyOut && account && blockNumber
? {
tokenInAddress: amountIn.currency.wrapped.address,
tokenInChainId: amountIn.currency.chainId,
tokenOutAddress: currencyOut.wrapped.address,
tokenOutChainId: currencyOut.chainId,
amount: amountIn.quotient.toString(),
type: 'exactIn',
}
: skipToken,
{ pollingInterval: ms`10s` }
)
// todo(judo): validate block number for freshness
return !isLoading && !isError ? data?.routeString : undefined
}
...@@ -5,12 +5,10 @@ export enum Version { ...@@ -5,12 +5,10 @@ export enum Version {
v3 = 'V3', v3 = 'V3',
} }
export const DEFAULT_VERSION: Version = Version.v3 export default function useToggledVersion(): Version | undefined {
export default function useToggledVersion(): Version {
const { use } = useParsedQueryString() const { use } = useParsedQueryString()
if (typeof use !== 'string') { if (typeof use !== 'string') {
return DEFAULT_VERSION return undefined
} }
switch (use.toLowerCase()) { switch (use.toLowerCase()) {
case 'v2': case 'v2':
...@@ -18,6 +16,6 @@ export default function useToggledVersion(): Version { ...@@ -18,6 +16,6 @@ export default function useToggledVersion(): Version {
case 'v3': case 'v3':
return Version.v3 return Version.v3
default: default:
return Version.v3 return undefined
} }
} }
import { Currency } from '@uniswap/sdk-core'
import { useActiveWeb3React } from 'hooks/web3'
import { useMemo } from 'react'
import { useCombinedActiveList } from 'state/lists/hooks'
/**
* Returns a WrappedTokenInfo from the active token lists when possible,
* or the passed token otherwise. */
export function useTokenInfoFromActiveList(currency: Currency) {
const { chainId } = useActiveWeb3React()
const activeList = useCombinedActiveList()
return useMemo(() => {
if (!chainId) return
if (currency.isNative) return currency
try {
return activeList[chainId][currency.wrapped.address].token
} catch (e) {
return currency
}
}, [activeList, chainId, currency])
}
...@@ -3,7 +3,7 @@ import { useMemo } from 'react' ...@@ -3,7 +3,7 @@ import { useMemo } from 'react'
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'
import { useV2TradeExactOut } from './useV2Trade' import { useV2TradeExactOut } from './useV2Trade'
import { useBestV3TradeExactOut } from './useBestV3Trade' import { useClientSideV3TradeExactOut } from './useClientSideV3Trade'
import { useActiveWeb3React } from './web3' 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.
...@@ -27,7 +27,7 @@ export default function useUSDCPrice(currency?: Currency): Price<Currency, Token ...@@ -27,7 +27,7 @@ export default function useUSDCPrice(currency?: Currency): Price<Currency, Token
const v2USDCTrade = useV2TradeExactOut(currency, amountOut, { const v2USDCTrade = useV2TradeExactOut(currency, amountOut, {
maxHops: 2, maxHops: 2,
}) })
const v3USDCTrade = useBestV3TradeExactOut(currency, amountOut) const v3USDCTrade = useClientSideV3TradeExactOut(currency, amountOut)
return useMemo(() => { return useMemo(() => {
if (!currency || !stablecoin) { if (!currency || !stablecoin) {
......
...@@ -9,7 +9,6 @@ import { unwrappedToken } from 'utils/unwrappedToken' ...@@ -9,7 +9,6 @@ import { unwrappedToken } from 'utils/unwrappedToken'
import { usePositionTokenURI } from '../../hooks/usePositionTokenURI' import { usePositionTokenURI } from '../../hooks/usePositionTokenURI'
import { calculateGasMargin } from '../../utils/calculateGasMargin' import { calculateGasMargin } from '../../utils/calculateGasMargin'
import { getExplorerLink, ExplorerDataType } from '../../utils/getExplorerLink' import { getExplorerLink, ExplorerDataType } from '../../utils/getExplorerLink'
import { LoadingRows } from './styleds'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { AutoColumn } from 'components/Column' import { AutoColumn } from 'components/Column'
import { RowBetween, RowFixed } from 'components/Row' import { RowBetween, RowFixed } from 'components/Row'
...@@ -45,6 +44,7 @@ import { Bound } from 'state/mint/v3/actions' ...@@ -45,6 +44,7 @@ import { Bound } from 'state/mint/v3/actions'
import useIsTickAtLimit from 'hooks/useIsTickAtLimit' import useIsTickAtLimit from 'hooks/useIsTickAtLimit'
import { formatTickPrice } from 'utils/formatTickPrice' import { formatTickPrice } from 'utils/formatTickPrice'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { LoadingRows } from './styleds'
const PageWrapper = styled.div` const PageWrapper = styled.div`
min-width: 800px; min-width: 800px;
......
import { LoadingRows as BaseLoadingRows } from 'components/Loader/styled'
import { Text } from 'rebass' import { Text } from 'rebass'
import styled, { keyframes } from 'styled-components/macro' import styled from 'styled-components/macro'
export const Wrapper = styled.div` export const Wrapper = styled.div`
position: relative; position: relative;
...@@ -56,36 +57,13 @@ export const Dots = styled.span` ...@@ -56,36 +57,13 @@ export const Dots = styled.span`
} }
` `
const loadingAnimation = keyframes` export const LoadingRows = styled(BaseLoadingRows)`
0% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
`
export const LoadingRows = styled.div`
display: grid;
min-width: 75%; min-width: 75%;
max-width: 960px; max-width: 960px;
grid-column-gap: 0.5em; grid-column-gap: 0.5em;
grid-row-gap: 0.8em; grid-row-gap: 0.8em;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
& > div {
animation: ${loadingAnimation} 1.5s infinite;
animation-fill-mode: both;
background: linear-gradient(
to left,
${({ theme }) => theme.bg1} 25%,
${({ theme }) => theme.bg2} 50%,
${({ theme }) => theme.bg1} 75%
);
background-size: 400%;
border-radius: 12px;
height: 2.4em;
will-change: background-position;
}
& > div:nth-child(4n + 1) { & > div:nth-child(4n + 1) {
grid-column: 1 / 3; grid-column: 1 / 3;
} }
......
...@@ -2,42 +2,50 @@ import { Trans } from '@lingui/macro' ...@@ -2,42 +2,50 @@ import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, 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 { LoadingOpacityContainer } from 'components/Loader/styled'
import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert' import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert'
import { AdvancedSwapDetails } from 'components/swap/AdvancedSwapDetails' import { AdvancedSwapDetails } from 'components/swap/AdvancedSwapDetails'
import { AutoRouterLogo } from 'components/swap/RouterLabel'
import SwapRoute from 'components/swap/SwapRoute'
import TradePrice from 'components/swap/TradePrice'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter' import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import { MouseoverTooltip, MouseoverTooltipContent } from 'components/Tooltip' import { MouseoverTooltip, MouseoverTooltipContent } from 'components/Tooltip'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { useCallback, useContext, useEffect, useMemo, useState } from 'react' import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { ArrowDown, ArrowLeft, CheckCircle, HelpCircle, Info } from 'react-feather' import { ArrowDown, CheckCircle, HelpCircle, Info } from 'react-feather'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { Link, RouteComponentProps } from 'react-router-dom' import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
import { V3TradeState } from 'state/routing/types'
import styled, { ThemeContext } from 'styled-components/macro' import styled, { ThemeContext } from 'styled-components/macro'
import AddressInputPanel from '../../components/AddressInputPanel' import AddressInputPanel from '../../components/AddressInputPanel'
import { ButtonConfirmed, ButtonError, ButtonGray, ButtonLight, ButtonPrimary } from '../../components/Button' import { ButtonConfirmed, ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
import { GreyCard } from '../../components/Card' import { GreyCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column' import { AutoColumn } from '../../components/Column'
import CurrencyInputPanel from '../../components/CurrencyInputPanel' import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import CurrencyLogo from '../../components/CurrencyLogo' import CurrencyLogo from '../../components/CurrencyLogo'
import Loader from '../../components/Loader' import Loader from '../../components/Loader'
import Row, { AutoRow, RowFixed } from '../../components/Row' import Row, { AutoRow, RowFixed } from '../../components/Row'
import BetterTradeLink from '../../components/swap/BetterTradeLink'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee' import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
import ConfirmSwapModal from '../../components/swap/ConfirmSwapModal' import ConfirmSwapModal from '../../components/swap/ConfirmSwapModal'
import { ArrowWrapper, Dots, SwapCallbackError, Wrapper } from '../../components/swap/styleds' import {
ArrowWrapper,
Dots,
ResponsiveTooltipContainer,
SwapCallbackError,
Wrapper,
} from '../../components/swap/styleds'
import SwapHeader from '../../components/swap/SwapHeader' import SwapHeader from '../../components/swap/SwapHeader'
import TradePrice from '../../components/swap/TradePrice'
import { SwitchLocaleLink } from '../../components/SwitchLocaleLink' import { SwitchLocaleLink } from '../../components/SwitchLocaleLink'
import TokenWarningModal from '../../components/TokenWarningModal' import TokenWarningModal from '../../components/TokenWarningModal'
import { useAllTokens, useCurrency } from '../../hooks/Tokens' import { useAllTokens, useCurrency } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback' import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
import { V3TradeState } from '../../hooks/useBestV3Trade'
import useENSAddress from '../../hooks/useENSAddress' import useENSAddress from '../../hooks/useENSAddress'
import { useERC20PermitFromTrade, UseERC20PermitState } from '../../hooks/useERC20Permit' import { useERC20PermitFromTrade, UseERC20PermitState } from '../../hooks/useERC20Permit'
import useIsArgentWallet from '../../hooks/useIsArgentWallet' import useIsArgentWallet from '../../hooks/useIsArgentWallet'
import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported' import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported'
import { useSwapCallback } from '../../hooks/useSwapCallback' import { useSwapCallback } from '../../hooks/useSwapCallback'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion' import useToggledVersion from '../../hooks/useToggledVersion'
import { useUSDCValue } from '../../hooks/useUSDCPrice' import { useUSDCValue } from '../../hooks/useUSDCPrice'
import useWrapCallback, { WrapType } from '../../hooks/useWrapCallback' import useWrapCallback, { WrapType } from '../../hooks/useWrapCallback'
import { useActiveWeb3React } from '../../hooks/web3' import { useActiveWeb3React } from '../../hooks/web3'
...@@ -49,22 +57,21 @@ import { ...@@ -49,22 +57,21 @@ import {
useSwapActionHandlers, useSwapActionHandlers,
useSwapState, useSwapState,
} from '../../state/swap/hooks' } from '../../state/swap/hooks'
import { useExpertModeManager, useUserSingleHopOnly } from '../../state/user/hooks' import { useExpertModeManager } from '../../state/user/hooks'
import { HideSmall, LinkStyledButton, TYPE } from '../../theme' import { LinkStyledButton, TYPE } from '../../theme'
import { computeFiatValuePriceImpact } from '../../utils/computeFiatValuePriceImpact' import { computeFiatValuePriceImpact } from '../../utils/computeFiatValuePriceImpact'
import { getTradeVersion } from '../../utils/getTradeVersion' import { getTradeVersion } from '../../utils/getTradeVersion'
import { isTradeBetter } from '../../utils/isTradeBetter'
import { maxAmountSpend } from '../../utils/maxAmountSpend' import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { warningSeverity } from '../../utils/prices' import { warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody' import AppBody from '../AppBody'
const StyledInfo = styled(Info)` const StyledInfo = styled(Info)`
opacity: 0.4;
color: ${({ theme }) => theme.text1};
height: 16px; height: 16px;
width: 16px; width: 16px;
margin-left: 4px;
color: ${({ theme }) => theme.text3};
:hover { :hover {
opacity: 0.8; color: ${({ theme }) => theme.text1};
} }
` `
...@@ -108,9 +115,8 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -108,9 +115,8 @@ export default function Swap({ history }: RouteComponentProps) {
// swap state // swap state
const { independentField, typedValue, recipient } = useSwapState() const { independentField, typedValue, recipient } = useSwapState()
const { const {
v2Trade, v3Trade: { state: v3TradeState },
v3TradeState: { trade: v3Trade, state: v3TradeState }, bestTrade: trade,
toggledTrade: trade,
allowedSlippage, allowedSlippage,
currencyBalances, currencyBalances,
parsedAmount, parsedAmount,
...@@ -140,9 +146,18 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -140,9 +146,18 @@ export default function Swap({ history }: RouteComponentProps) {
[independentField, parsedAmount, showWrap, trade] [independentField, parsedAmount, showWrap, trade]
) )
const [routeNotFound, routeIsLoading, routeIsSyncing] = useMemo(
() => [
trade instanceof V3Trade ? !trade?.swaps : !trade?.route,
V3TradeState.LOADING === v3TradeState,
V3TradeState.SYNCING === v3TradeState,
],
[trade, v3TradeState]
)
const fiatValueInput = useUSDCValue(parsedAmounts[Field.INPUT]) const fiatValueInput = useUSDCValue(parsedAmounts[Field.INPUT])
const fiatValueOutput = useUSDCValue(parsedAmounts[Field.OUTPUT]) const fiatValueOutput = useUSDCValue(parsedAmounts[Field.OUTPUT])
const priceImpact = computeFiatValuePriceImpact(fiatValueInput, fiatValueOutput) const priceImpact = routeIsSyncing ? undefined : computeFiatValuePriceImpact(fiatValueInput, fiatValueOutput)
const { onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient } = useSwapActionHandlers() const { onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient } = useSwapActionHandlers()
const isValid = !swapInputError const isValid = !swapInputError
...@@ -192,8 +207,6 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -192,8 +207,6 @@ export default function Swap({ history }: RouteComponentProps) {
const userHasSpecifiedInputOutput = Boolean( const userHasSpecifiedInputOutput = Boolean(
currencies[Field.INPUT] && currencies[Field.OUTPUT] && parsedAmounts[independentField]?.greaterThan(JSBI.BigInt(0)) currencies[Field.INPUT] && currencies[Field.OUTPUT] && parsedAmounts[independentField]?.greaterThan(JSBI.BigInt(0))
) )
const routeNotFound = !trade?.route
const isLoadingRoute = toggledVersion === Version.v3 && V3TradeState.LOADING === v3TradeState
// check whether the user has approved the router on the input token // check whether the user has approved the router on the input token
const [approvalState, approveCallback] = useApproveCallbackFromTrade(trade, allowedSlippage) const [approvalState, approveCallback] = useApproveCallbackFromTrade(trade, allowedSlippage)
...@@ -245,8 +258,6 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -245,8 +258,6 @@ export default function Swap({ history }: RouteComponentProps) {
signatureData signatureData
) )
const [singleHopOnly] = useUserSingleHopOnly()
const handleSwap = useCallback(() => { const handleSwap = useCallback(() => {
if (!swapCallback) { if (!swapCallback) {
return return
...@@ -270,7 +281,7 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -270,7 +281,7 @@ export default function Swap({ history }: RouteComponentProps) {
trade?.inputAmount?.currency?.symbol, trade?.inputAmount?.currency?.symbol,
trade?.outputAmount?.currency?.symbol, trade?.outputAmount?.currency?.symbol,
getTradeVersion(trade), getTradeVersion(trade),
singleHopOnly ? 'SH' : 'MH', 'MH',
].join('/'), ].join('/'),
}) })
}) })
...@@ -283,17 +294,7 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -283,17 +294,7 @@ export default function Swap({ history }: RouteComponentProps) {
txHash: undefined, txHash: undefined,
}) })
}) })
}, [ }, [swapCallback, priceImpact, tradeToConfirm, showConfirm, recipient, recipientAddress, account, trade])
swapCallback,
priceImpact,
tradeToConfirm,
showConfirm,
recipient,
recipientAddress,
account,
trade,
singleHopOnly,
])
// errors // errors
const [showInverted, setShowInverted] = useState<boolean>(false) const [showInverted, setShowInverted] = useState<boolean>(false)
...@@ -381,7 +382,7 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -381,7 +382,7 @@ export default function Swap({ history }: RouteComponentProps) {
onDismiss={handleConfirmDismiss} onDismiss={handleConfirmDismiss}
/> />
<AutoColumn gap={'md'}> <AutoColumn gap={'sm'}>
<div style={{ display: 'relative' }}> <div style={{ display: 'relative' }}>
<CurrencyInputPanel <CurrencyInputPanel
label={ label={
...@@ -397,6 +398,7 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -397,6 +398,7 @@ export default function Swap({ history }: RouteComponentProps) {
otherCurrency={currencies[Field.OUTPUT]} otherCurrency={currencies[Field.OUTPUT]}
showCommonBases={true} showCommonBases={true}
id="swap-currency-input" id="swap-currency-input"
loading={independentField === Field.OUTPUT && routeIsSyncing}
/> />
<ArrowWrapper clickable> <ArrowWrapper clickable>
<ArrowDown <ArrowDown
...@@ -421,6 +423,7 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -421,6 +423,7 @@ export default function Swap({ history }: RouteComponentProps) {
otherCurrency={currencies[Field.INPUT]} otherCurrency={currencies[Field.INPUT]}
showCommonBases={true} showCommonBases={true}
id="swap-currency-output" id="swap-currency-output"
loading={independentField === Field.INPUT && routeIsSyncing}
/> />
</div> </div>
...@@ -438,74 +441,60 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -438,74 +441,60 @@ export default function Swap({ history }: RouteComponentProps) {
</> </>
) : null} ) : null}
{showWrap ? null : ( {!showWrap && trade && (
<Row style={{ justifyContent: !trade ? 'center' : 'space-between' }}> <Row justify={!trade ? 'center' : 'space-between'}>
<RowFixed> <RowFixed style={{ position: 'relative' }}>
{[V3TradeState.VALID, V3TradeState.SYNCING, V3TradeState.NO_ROUTE_FOUND].includes(v3TradeState) && <MouseoverTooltipContent
(toggledVersion === Version.v3 && isTradeBetter(v3Trade, v2Trade) ? ( wrap={false}
<BetterTradeLink version={Version.v2} otherTradeNonexistent={!v3Trade} /> content={
) : toggledVersion === Version.v2 && isTradeBetter(v2Trade, v3Trade) ? ( <ResponsiveTooltipContainer>
<BetterTradeLink version={Version.v3} otherTradeNonexistent={!v2Trade} /> <SwapRoute trade={trade} syncing={routeIsSyncing} />
) : ( </ResponsiveTooltipContainer>
toggledVersion === Version.v2 && ( }
<ButtonGray placement="bottom"
width="fit-content" onOpen={() =>
padding="0.1rem 0.5rem 0.1rem 0.35rem" ReactGA.event({
as={Link} category: 'Swap',
to="/swap" action: 'Router Tooltip Open',
style={{ })
display: 'flex', }
justifyContent: 'space-between', >
alignItems: 'center', <AutoRow gap="4px" width="auto">
height: '24px', <AutoRouterLogo />
lineHeight: '120%', <LoadingOpacityContainer $loading={routeIsSyncing}>
marginLeft: '0.75rem', {trade instanceof V3Trade && trade.swaps.length > 1 && (
}} <TYPE.blue fontSize={14}>{trade.swaps.length} routes</TYPE.blue>
> )}
<ArrowLeft color={theme.text3} size={12} /> &nbsp; </LoadingOpacityContainer>
<TYPE.main style={{ lineHeight: '120%' }} fontSize={12}> </AutoRow>
<Trans> </MouseoverTooltipContent>
<HideSmall>Back to </HideSmall>
V3
</Trans>
</TYPE.main>
</ButtonGray>
)
))}
{toggledVersion === Version.v3 && trade && isTradeBetter(v2Trade, v3Trade) && (
<ButtonGray
width="fit-content"
padding="0.1rem 0.5rem"
disabled
style={{
display: 'flex',
justifyContent: 'space-between',
height: '24px',
opacity: 0.8,
marginLeft: '0.25rem',
}}
>
<TYPE.black fontSize={12}>
<Trans>V3</Trans>
</TYPE.black>
</ButtonGray>
)}
</RowFixed> </RowFixed>
{trade ? ( <RowFixed>
<RowFixed> <LoadingOpacityContainer $loading={routeIsSyncing}>
<TradePrice <TradePrice
price={trade.executionPrice} price={trade.executionPrice}
showInverted={showInverted} showInverted={showInverted}
setShowInverted={setShowInverted} setShowInverted={setShowInverted}
/> />
<MouseoverTooltipContent </LoadingOpacityContainer>
content={<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} />} <MouseoverTooltipContent
> wrap={false}
<StyledInfo /> content={
</MouseoverTooltipContent> <ResponsiveTooltipContainer origin="top right" width={'295px'}>
</RowFixed> <AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={routeIsSyncing} />
) : null} </ResponsiveTooltipContainer>
}
placement="bottom"
onOpen={() =>
ReactGA.event({
category: 'Swap',
action: 'Transaction Details Tooltip Open',
})
}
>
<StyledInfo />
</MouseoverTooltipContent>
</RowFixed>
</Row> </Row>
)} )}
...@@ -529,18 +518,18 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -529,18 +518,18 @@ export default function Swap({ history }: RouteComponentProps) {
<Trans>Unwrap</Trans> <Trans>Unwrap</Trans>
) : null)} ) : null)}
</ButtonPrimary> </ButtonPrimary>
) : routeIsSyncing || routeIsLoading ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">
<Dots>
<Trans>Loading</Trans>
</Dots>
</TYPE.main>
</GreyCard>
) : routeNotFound && userHasSpecifiedInputOutput ? ( ) : routeNotFound && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}> <GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px"> <TYPE.main mb="4px">
{isLoadingRoute ? ( <Trans>Insufficient liquidity for this trade.</Trans>
<Dots>
<Trans>Loading</Trans>
</Dots>
) : singleHopOnly ? (
<Trans>Insufficient liquidity for this trade. Try enabling multi-hop trades.</Trans>
) : (
<Trans>Insufficient liquidity for this trade.</Trans>
)}
</TYPE.main> </TYPE.main>
</GreyCard> </GreyCard>
) : showApproveFlow ? ( ) : showApproveFlow ? (
......
// jest custom assertions // jest custom assertions
import '@testing-library/jest-dom' import '@testing-library/jest-dom'
// include style rules in snapshots
import 'jest-styled-components'
import { configureStore } from '@reduxjs/toolkit' import { configureStore } from '@reduxjs/toolkit'
import { save, load } from 'redux-localstorage-simple' import { save, load } from 'redux-localstorage-simple'
import { setupListeners } from '@reduxjs/toolkit/query/react'
import application from './application/reducer' import application from './application/reducer'
import { updateVersion } from './global/actions' import { updateVersion } from './global/actions'
...@@ -44,6 +45,8 @@ const store = configureStore({ ...@@ -44,6 +45,8 @@ const store = configureStore({
store.dispatch(updateVersion()) store.dispatch(updateVersion())
setupListeners(store.dispatch)
export default store export default store
export type AppState = ReturnType<typeof store.getState> export type AppState = ReturnType<typeof store.getState>
......
import { computeRoutes } from './computeRoutes'
import { Ether, Token } from '@uniswap/sdk-core'
const USDC = new Token(1, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC')
const DAI = new Token(1, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 6, 'DAI')
const MKR = new Token(1, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 6, 'MKR')
const ETH = Ether.onChain(1)
// helper function to make amounts more readable
const amount = (raw: TemplateStringsArray) => (parseInt(raw[0]) * 1e6).toString()
describe('#useRoute', () => {
it('handles an undefined payload', () => {
const result = computeRoutes(undefined, undefined, undefined)
expect(result).toBeUndefined()
})
it('handles empty edges and nodes', () => {
const result = computeRoutes(USDC, DAI, {
route: [],
})
expect(result).toEqual([])
})
it('handles a single route trade from DAI to USDC', () => {
const result = computeRoutes(DAI, USDC, {
route: [
[
{
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
amountIn: amount`1`,
amountOut: amount`5`,
fee: '500',
sqrtRatioX96: '2437312313659959819381354528',
liquidity: '10272714736694327408',
tickCurrent: '-69633',
tokenIn: DAI,
tokenOut: USDC,
},
],
],
})
expect(result).toBeDefined()
expect(result?.length).toBe(1)
expect(result && result[0].route.input).toStrictEqual(DAI)
expect(result && result[0].route.output).toStrictEqual(USDC)
expect(result && result[0].route.tokenPath).toStrictEqual([DAI, USDC])
expect(result && result[0].inputAmount.toSignificant()).toBe('1')
expect(result && result[0].outputAmount.toSignificant()).toBe('5')
})
it('handles a multi-route trade from DAI to USDC', () => {
const result = computeRoutes(DAI, USDC, {
route: [
[
{
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
amountIn: amount`5`,
amountOut: amount`6`,
fee: '500',
tokenIn: DAI,
tokenOut: USDC,
sqrtRatioX96: '2437312313659959819381354528',
liquidity: '10272714736694327408',
tickCurrent: '-69633',
},
],
[
{
address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2',
amountIn: amount`10`,
amountOut: amount`1`,
fee: '3000',
tokenIn: DAI,
tokenOut: MKR,
sqrtRatioX96: '2437312313659959819381354528',
liquidity: '10272714736694327408',
tickCurrent: '-69633',
},
{
address: '0x3f8F72aA9304c8B593d555F12eF6589cC3A579A2',
amountIn: amount`1`,
amountOut: amount`200`,
fee: '10000',
tokenIn: MKR,
tokenOut: USDC,
sqrtRatioX96: '2437312313659959819381354528',
liquidity: '10272714736694327408',
tickCurrent: '-69633',
},
],
],
})
expect(result).toBeDefined()
expect(result?.length).toBe(2)
expect(result && result[0].route.input).toStrictEqual(DAI)
expect(result && result[0].route.output).toStrictEqual(USDC)
expect(result && result[0].route.tokenPath).toEqual([DAI, USDC])
expect(result && result[1].route.input).toStrictEqual(DAI)
expect(result && result[1].route.output).toStrictEqual(USDC)
expect(result && result[1].route.tokenPath).toEqual([DAI, MKR, USDC])
expect(result && result[0].inputAmount.toSignificant()).toBe('5')
expect(result && result[0].outputAmount.toSignificant()).toBe('6')
expect(result && result[1].inputAmount.toSignificant()).toBe('10')
expect(result && result[1].outputAmount.toSignificant()).toBe('200')
})
it('handles a single route trade with same token pair, different fee tiers', () => {
const result = computeRoutes(DAI, USDC, {
route: [
[
{
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
amountIn: amount`1`,
amountOut: amount`5`,
fee: '500',
tokenIn: DAI,
tokenOut: USDC,
sqrtRatioX96: '2437312313659959819381354528',
liquidity: '10272714736694327408',
tickCurrent: '-69633',
},
],
[
{
address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2',
amountIn: amount`10`,
amountOut: amount`50`,
fee: '3000',
tokenIn: DAI,
tokenOut: USDC,
sqrtRatioX96: '2437312313659959819381354528',
liquidity: '10272714736694327408',
tickCurrent: '-69633',
},
],
],
})
expect(result).toBeDefined()
expect(result?.length).toBe(2)
expect(result && result[0].route.input).toStrictEqual(DAI)
expect(result && result[0].route.output).toStrictEqual(USDC)
expect(result && result[0].route.tokenPath).toEqual([DAI, USDC])
expect(result && result[0].inputAmount.toSignificant()).toBe('1')
expect(result && result[0].outputAmount.toSignificant()).toBe('5')
})
describe('with ETH', () => {
it('outputs native ETH as input currency', () => {
const WETH = ETH.wrapped
const result = computeRoutes(ETH, USDC, {
route: [
[
{
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
amountIn: (1e18).toString(),
amountOut: amount`5`,
fee: '500',
sqrtRatioX96: '2437312313659959819381354528',
liquidity: '10272714736694327408',
tickCurrent: '-69633',
tokenIn: WETH,
tokenOut: USDC,
},
],
],
})
expect(result).toBeDefined()
expect(result?.length).toBe(1)
expect(result && result[0].route.input).toStrictEqual(ETH)
expect(result && result[0].route.output).toStrictEqual(USDC)
expect(result && result[0].route.tokenPath).toStrictEqual([WETH, USDC])
expect(result && result[0].inputAmount.toSignificant()).toBe('1')
expect(result && result[0].outputAmount.toSignificant()).toBe('5')
})
it('outputs native ETH as output currency', () => {
const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH')
const result = computeRoutes(USDC, ETH, {
route: [
[
{
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
amountIn: amount`5`,
amountOut: (1e18).toString(),
fee: '500',
sqrtRatioX96: '2437312313659959819381354528',
liquidity: '10272714736694327408',
tickCurrent: '-69633',
tokenIn: USDC,
tokenOut: WETH,
},
],
],
})
expect(result?.length).toBe(1)
expect(result && result[0].route.input).toStrictEqual(USDC)
expect(result && result[0].route.output).toStrictEqual(ETH)
expect(result && result[0].route.tokenPath).toStrictEqual([USDC, WETH])
expect(result && result[0].inputAmount.toSignificant()).toBe('5')
expect(result && result[0].outputAmount.toSignificant()).toBe('1')
})
})
})
import { Currency, CurrencyAmount, Ether, Token } from '@uniswap/sdk-core'
import { FeeAmount, Pool, Route } from '@uniswap/v3-sdk'
import { GetQuoteResult } from './types'
/**
* Transforms a Routing API quote into an array of routes that
* can be used to create a V3 `Trade`.
*/
export function computeRoutes(
currencyIn: Currency | undefined,
currencyOut: Currency | 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.route.length === 0) return []
const parsedCurrencyIn = currencyIn.isNative
? Ether.onChain(currencyIn.chainId)
: parseToken(quoteResult.route[0][0].tokenIn)
const parsedCurrencyOut = currencyOut.isNative
? Ether.onChain(currencyOut.chainId)
: parseToken(quoteResult.route[0][quoteResult.route[0].length - 1].tokenOut)
try {
return quoteResult.route.map((route) => {
const rawAmountIn = route[0].amountIn
const rawAmountOut = route[route.length - 1].amountOut
if (!rawAmountIn || !rawAmountOut) {
throw new Error('Expected both amountIn and amountOut to be present')
}
return {
route: new Route(route.map(parsePool), parsedCurrencyIn, parsedCurrencyOut),
inputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyIn, rawAmountIn),
outputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyOut, rawAmountOut),
}
})
} catch (e) {
// `Route` constructor may throw if inputs/outputs are temporarily out of sync
// (RTK-Query always returns the latest data which may not be the right inputs/outputs)
// This is not fatal and will fix itself in future render cycles
return undefined
}
}
const parseToken = ({ address, chainId, decimals, symbol }: GetQuoteResult['route'][0][0]['tokenIn']): Token => {
return new Token(chainId, address, parseInt(decimals.toString()), symbol)
}
const parsePool = ({
fee,
sqrtRatioX96,
liquidity,
tickCurrent,
tokenIn,
tokenOut,
}: GetQuoteResult['route'][0][0]): Pool =>
new Pool(
parseToken(tokenIn),
parseToken(tokenOut),
parseInt(fee) as FeeAmount,
sqrtRatioX96,
liquidity,
parseInt(tickCurrent)
)
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import qs from 'qs' import qs from 'qs'
import { GetQuoteResult } from './types'
export interface GetQuoteResult {
blockNumber: string
gasPriceWei: string
gasUseEstimate: string
gasUseEstimateQuote: string
gasUseEstimateQuoteDecimals: string
gasUseEstimateUSD: string
methodParameters: { calldata: string; value: string }
quote: string
quoteDecimals: string
quoteGasAdjusted: string
quoteGasAdjustedDecimals: string
quoteId: string
routeEdges: {
fee: string
id: string
inId: string
outId: string
percent: number
type: string
}[]
routeNodes: { chainId: number; id: string; symbol: string; type: string }[]
routeString: string
}
export const routingApi = createApi({ export const routingApi = createApi({
baseQuery: fetchBaseQuery({ baseQuery: fetchBaseQuery({
...@@ -41,9 +17,6 @@ export const routingApi = createApi({ ...@@ -41,9 +17,6 @@ export const routingApi = createApi({
tokenOutChainId: SupportedChainId tokenOutChainId: SupportedChainId
amount: string amount: string
type: 'exactIn' | 'exactOut' type: 'exactIn' | 'exactOut'
recipient?: string
slippageTolerance?: string
deadline?: string
} }
>({ >({
query: (args) => `quote?${qs.stringify(args)}`, query: (args) => `quote?${qs.stringify(args)}`,
......
export enum V3TradeState {
LOADING,
INVALID,
NO_ROUTE_FOUND,
VALID,
SYNCING,
}
export interface GetQuoteResult {
blockNumber: string
gasPriceWei: string
gasUseEstimate: string
gasUseEstimateQuote: string
gasUseEstimateQuoteDecimals: string
gasUseEstimateUSD: string
methodParameters: { calldata: string; value: string }
quote: string
quoteDecimals: string
quoteGasAdjusted: string
quoteGasAdjustedDecimals: string
quoteId: string
route: {
address: string
amountIn?: string
amountOut?: string
fee: string
liquidity: string
sqrtRatioX96: string
tickCurrent: string
tokenIn: {
address: string
chainId: number
decimals: string | number
symbol?: string
}
tokenOut: {
address: string
chainId: number
decimals: string | number
symbol?: string
}
}[][]
routeString: string
}
import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { Trade } from '@uniswap/v3-sdk'
import { BigNumber } from 'ethers'
import ms from 'ms.macro'
import { useMemo } from 'react'
import { useBlockNumber } from 'state/application/hooks'
import { useGetQuoteQuery } from 'state/routing/slice'
import { V3TradeState } from './types'
import { computeRoutes } from './computeRoutes'
function useFreshData<T>(data: T, dataBlockNumber: number, maxBlockAge = 10): T | undefined {
const localBlockNumber = useBlockNumber()
if (!localBlockNumber) return undefined
if (localBlockNumber - dataBlockNumber > maxBlockAge) {
return undefined
}
return data
}
/**
* Returns query arguments for the Routing API query or undefined if the
* query should be skipped.
*/
function useRoutingAPIArguments({
tokenIn,
tokenOut,
amount,
type,
}: {
tokenIn: Currency | undefined
tokenOut: Currency | undefined
amount: CurrencyAmount<Currency> | undefined
type: 'exactIn' | 'exactOut'
}) {
if (!tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut)) {
return undefined
}
return {
tokenInAddress: tokenIn.wrapped.address,
tokenInChainId: tokenIn.chainId,
tokenOutAddress: tokenOut.wrapped.address,
tokenOutChainId: tokenOut.chainId,
amount: amount.quotient.toString(),
type,
}
}
export function useRoutingAPITradeExactIn(amountIn?: CurrencyAmount<Currency>, currencyOut?: Currency) {
const queryArgs = useRoutingAPIArguments({
tokenIn: amountIn?.currency,
tokenOut: currencyOut,
amount: amountIn,
type: 'exactIn',
})
const { isLoading, isError, data } = useGetQuoteQuery(queryArgs ?? skipToken, {
pollingInterval: ms`10s`,
refetchOnFocus: true,
})
const quoteResult = useFreshData(data, Number(data?.blockNumber) || 0)
const routes = useMemo(
() => computeRoutes(amountIn?.currency, currencyOut, quoteResult),
[amountIn, currencyOut, quoteResult]
)
return useMemo(() => {
if (!amountIn || !currencyOut) {
return {
state: V3TradeState.INVALID,
trade: null,
}
}
if (isLoading && !quoteResult) {
// only on first hook render
return {
state: V3TradeState.LOADING,
trade: null,
}
}
const amountOut =
currencyOut && quoteResult ? CurrencyAmount.fromRawAmount(currencyOut, quoteResult.quote) : undefined
if (isError || !amountOut || !routes || routes.length === 0 || !queryArgs) {
return {
state: V3TradeState.NO_ROUTE_FOUND,
trade: null,
}
}
const trade = Trade.createUncheckedTradeWithMultipleRoutes<Currency, Currency, TradeType.EXACT_INPUT>({
routes,
tradeType: TradeType.EXACT_INPUT,
})
const gasPriceWei = BigNumber.from(quoteResult?.gasPriceWei)
const gasUseEstimate = BigNumber.from(quoteResult?.gasUseEstimate)
return {
// always return VALID regardless of isFetching status
state: V3TradeState.VALID,
trade,
gasPriceWei,
gasUseEstimate,
}
}, [amountIn, currencyOut, isLoading, quoteResult, isError, routes, queryArgs])
}
export function useRoutingAPITradeExactOut(currencyIn?: Currency, amountOut?: CurrencyAmount<Currency>) {
const queryArgs = useRoutingAPIArguments({
tokenIn: currencyIn,
tokenOut: amountOut?.currency,
amount: amountOut,
type: 'exactOut',
})
const { isLoading, isError, data } = useGetQuoteQuery(queryArgs ?? skipToken, {
pollingInterval: ms`10s`,
refetchOnFocus: true,
})
const quoteResult = useFreshData(data, Number(data?.blockNumber) || 0)
const routes = useMemo(
() => computeRoutes(currencyIn, amountOut?.currency, quoteResult),
[amountOut, currencyIn, quoteResult]
)
return useMemo(() => {
if (!amountOut || !currencyIn) {
return {
state: V3TradeState.INVALID,
trade: null,
}
}
if (isLoading && !quoteResult) {
return {
state: V3TradeState.LOADING,
trade: null,
}
}
const amountIn = currencyIn && quoteResult ? CurrencyAmount.fromRawAmount(currencyIn, quoteResult.quote) : undefined
if (isError || !amountIn || !routes || routes.length === 0 || !queryArgs) {
return {
state: V3TradeState.NO_ROUTE_FOUND,
trade: null,
}
}
const trade = Trade.createUncheckedTradeWithMultipleRoutes<Currency, Currency, TradeType.EXACT_OUTPUT>({
routes,
tradeType: TradeType.EXACT_OUTPUT,
})
const gasPriceWei = BigNumber.from(quoteResult?.gasPriceWei)
const gasUseEstimate = BigNumber.from(quoteResult?.gasUseEstimate)
return {
state: V3TradeState.VALID,
trade,
gasPriceWei,
gasUseEstimate,
}
}, [amountOut, currencyIn, isLoading, quoteResult, isError, routes, queryArgs])
}
import { t } from '@lingui/macro'
import JSBI from 'jsbi'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { useBestV3TradeExactIn, useBestV3TradeExactOut, V3TradeState } from '../../hooks/useBestV3Trade'
import useENS from '../../hooks/useENS'
import { parseUnits } from '@ethersproject/units' import { parseUnits } from '@ethersproject/units'
import { t } 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 { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { TWO_PERCENT } from 'constants/misc'
import { useV3TradeExactIn, useV3TradeExactOut } from 'hooks/useCombinedV3Trade'
import JSBI from 'jsbi'
import { ParsedQs } from 'qs' import { ParsedQs } from 'qs'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useActiveWeb3React } from '../../hooks/web3' import { useAppDispatch, useAppSelector } from 'state/hooks'
import { V3TradeState } 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 useParsedQueryString from '../../hooks/useParsedQueryString'
import useSwapSlippageTolerance from '../../hooks/useSwapSlippageTolerance' import useSwapSlippageTolerance from '../../hooks/useSwapSlippageTolerance'
import { Version } from '../../hooks/useToggledVersion' import { Version } from '../../hooks/useToggledVersion'
import { useV2TradeExactIn, useV2TradeExactOut } from '../../hooks/useV2Trade' import { useV2TradeExactIn, useV2TradeExactOut } from '../../hooks/useV2Trade'
import useParsedQueryString from '../../hooks/useParsedQueryString' import { useActiveWeb3React } from '../../hooks/web3'
import { isAddress } from '../../utils' import { isAddress } from '../../utils'
import { AppState } from '../index' import { AppState } from '../index'
import { useCurrencyBalances } from '../wallet/hooks' import { useCurrencyBalances } from '../wallet/hooks'
import { Field, replaceSwapState, selectCurrency, setRecipient, switchCurrencies, typeInput } from './actions' import { Field, replaceSwapState, selectCurrency, setRecipient, switchCurrencies, typeInput } from './actions'
import { SwapState } from './reducer' import { SwapState } from './reducer'
import { useUserSingleHopOnly } from 'state/user/hooks'
import { useAppDispatch, useAppSelector } from 'state/hooks'
export function useSwapState(): AppState['swap'] { export function useSwapState(): AppState['swap'] {
return useAppSelector((state) => state.swap) return useAppSelector((state) => state.swap)
...@@ -114,20 +116,21 @@ function involvesAddress( ...@@ -114,20 +116,21 @@ function involvesAddress(
} }
// 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): { export function useDerivedSwapInfo(toggledVersion: Version | undefined): {
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?: string inputError?: string
v2Trade: V2Trade<Currency, Currency, TradeType> | undefined v2Trade: V2Trade<Currency, Currency, TradeType> | undefined
v3TradeState: { trade: V3Trade<Currency, Currency, TradeType> | null; state: V3TradeState } v3Trade: {
toggledTrade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined trade: V3Trade<Currency, Currency, TradeType> | null
state: V3TradeState
}
bestTrade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined
allowedSlippage: Percent allowedSlippage: Percent
} { } {
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
const [singleHopOnly] = useUserSingleHopOnly()
const { const {
independentField, independentField,
typedValue, typedValue,
...@@ -147,20 +150,48 @@ export function useDerivedSwapInfo(toggledVersion: Version): { ...@@ -147,20 +150,48 @@ export function useDerivedSwapInfo(toggledVersion: Version): {
]) ])
const isExactIn: boolean = independentField === Field.INPUT const isExactIn: boolean = independentField === Field.INPUT
const parsedAmount = tryParseAmount(typedValue, (isExactIn ? inputCurrency : outputCurrency) ?? undefined) const parsedAmount = useMemo(
() => tryParseAmount(typedValue, (isExactIn ? inputCurrency : outputCurrency) ?? undefined),
[inputCurrency, isExactIn, outputCurrency, typedValue]
)
const bestV2TradeExactIn = useV2TradeExactIn(isExactIn ? parsedAmount : undefined, outputCurrency ?? undefined, { // get v2 and v3 quotes
maxHops: singleHopOnly ? 1 : undefined, // skip if other version is toggled
}) const bestV2TradeExactIn = useV2TradeExactIn(
const bestV2TradeExactOut = useV2TradeExactOut(inputCurrency ?? undefined, !isExactIn ? parsedAmount : undefined, { toggledVersion !== Version.v3 && isExactIn ? parsedAmount : undefined,
maxHops: singleHopOnly ? 1 : undefined, outputCurrency ?? undefined
}) )
const bestV2TradeExactOut = useV2TradeExactOut(
inputCurrency ?? undefined,
toggledVersion !== Version.v3 && !isExactIn ? parsedAmount : undefined
)
const bestV3TradeExactIn = useBestV3TradeExactIn(isExactIn ? parsedAmount : undefined, outputCurrency ?? undefined) const bestV3TradeExactIn = useV3TradeExactIn(
const bestV3TradeExactOut = useBestV3TradeExactOut(inputCurrency ?? undefined, !isExactIn ? parsedAmount : undefined) toggledVersion !== Version.v2 && isExactIn ? parsedAmount : undefined,
outputCurrency ?? undefined
)
const bestV3TradeExactOut = useV3TradeExactOut(
inputCurrency ?? undefined,
toggledVersion !== Version.v2 && !isExactIn ? parsedAmount : undefined
)
const v2Trade = isExactIn ? bestV2TradeExactIn : bestV2TradeExactOut const v2Trade = isExactIn ? bestV2TradeExactIn : bestV2TradeExactOut
const v3Trade = (isExactIn ? bestV3TradeExactIn : bestV3TradeExactOut) ?? undefined const v3Trade = isExactIn ? bestV3TradeExactIn : bestV3TradeExactOut
const isV2TradeBetter = useMemo(() => {
try {
// avoids comparing trades when V3Trade is not in a ready state.
return [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
}
}, [v2Trade, v3Trade])
const bestTrade = isV2TradeBetter == undefined ? undefined : isV2TradeBetter ? v2Trade : v3Trade.trade
const currencyBalances = { const currencyBalances = {
[Field.INPUT]: relevantTokenBalances[0], [Field.INPUT]: relevantTokenBalances[0],
...@@ -198,11 +229,10 @@ export function useDerivedSwapInfo(toggledVersion: Version): { ...@@ -198,11 +229,10 @@ export function useDerivedSwapInfo(toggledVersion: Version): {
} }
} }
const toggledTrade = (toggledVersion === Version.v2 ? v2Trade : v3Trade.trade) ?? undefined const allowedSlippage = useSwapSlippageTolerance(bestTrade ?? undefined)
const allowedSlippage = useSwapSlippageTolerance(toggledTrade)
// compare input balance to max input based on version // compare input balance to max input based on version
const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], toggledTrade?.maximumAmountIn(allowedSlippage)] const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], bestTrade?.maximumAmountIn(allowedSlippage)]
if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) { if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) {
inputError = t`Insufficient ${amountIn.currency.symbol} balance` inputError = t`Insufficient ${amountIn.currency.symbol} balance`
...@@ -214,8 +244,8 @@ export function useDerivedSwapInfo(toggledVersion: Version): { ...@@ -214,8 +244,8 @@ export function useDerivedSwapInfo(toggledVersion: Version): {
parsedAmount, parsedAmount,
inputError, inputError,
v2Trade: v2Trade ?? undefined, v2Trade: v2Trade ?? undefined,
v3TradeState: v3Trade, v3Trade,
toggledTrade, bestTrade: bestTrade ?? undefined,
allowedSlippage, allowedSlippage,
} }
} }
......
...@@ -21,7 +21,9 @@ export const updateArbitrumAlphaAcknowledged = createAction<{ arbitrumAlphaAckno ...@@ -21,7 +21,9 @@ export const updateArbitrumAlphaAcknowledged = createAction<{ arbitrumAlphaAckno
export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode') export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode')
export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode') export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode')
export const updateUserLocale = createAction<{ userLocale: SupportedLocale }>('user/updateUserLocale') export const updateUserLocale = createAction<{ userLocale: SupportedLocale }>('user/updateUserLocale')
export const updateUserSingleHopOnly = createAction<{ userSingleHopOnly: boolean }>('user/updateUserSingleHopOnly') export const updateUserClientSideRouter = createAction<{ userClientSideRouter: boolean }>(
'user/updateUserClientSideRouter'
)
export const updateHideClosedPositions = createAction<{ userHideClosedPositions: boolean }>('user/hideClosedPositions') export const updateHideClosedPositions = createAction<{ userHideClosedPositions: boolean }>('user/hideClosedPositions')
export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number | 'auto' }>( export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number | 'auto' }>(
'user/updateUserSlippageTolerance' 'user/updateUserSlippageTolerance'
......
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 } from 'constants/chains' import { L2_CHAIN_IDS, SupportedChainId } 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'
...@@ -23,8 +23,8 @@ import { ...@@ -23,8 +23,8 @@ import {
updateUserDarkMode, updateUserDarkMode,
updateUserDeadline, updateUserDeadline,
updateUserExpertMode, updateUserExpertMode,
updateUserClientSideRouter,
updateUserLocale, updateUserLocale,
updateUserSingleHopOnly,
updateUserSlippageTolerance, updateUserSlippageTolerance,
} from './actions' } from './actions'
...@@ -104,19 +104,26 @@ export function useExpertModeManager(): [boolean, () => void] { ...@@ -104,19 +104,26 @@ export function useExpertModeManager(): [boolean, () => void] {
return [expertMode, toggleSetExpertMode] return [expertMode, toggleSetExpertMode]
} }
export function useUserSingleHopOnly(): [boolean, (newSingleHopOnly: boolean) => void] { export function useClientSideRouter(): [boolean, (userClientSideRouter: boolean) => void] {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const singleHopOnly = useAppSelector((state) => state.user.userSingleHopOnly) const clientSideRouter = useAppSelector((state) => Boolean(state.user.userClientSideRouter))
const setSingleHopOnly = useCallback( const setClientSideRouter = useCallback(
(newSingleHopOnly: boolean) => { (newClientSideRouter: boolean) => {
dispatch(updateUserSingleHopOnly({ userSingleHopOnly: newSingleHopOnly })) dispatch(updateUserClientSideRouter({ userClientSideRouter: newClientSideRouter }))
}, },
[dispatch] [dispatch]
) )
return [singleHopOnly, setSingleHopOnly] 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 {
......
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants/misc'
import { createReducer } from '@reduxjs/toolkit' import { createReducer } from '@reduxjs/toolkit'
import { SupportedLocale } from 'constants/locales'
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants/misc'
import { updateVersion } from '../global/actions' import { updateVersion } from '../global/actions'
import { import {
addSerializedPair, addSerializedPair,
...@@ -8,17 +9,16 @@ import { ...@@ -8,17 +9,16 @@ import {
removeSerializedToken, removeSerializedToken,
SerializedPair, SerializedPair,
SerializedToken, SerializedToken,
updateArbitrumAlphaAcknowledged,
updateHideClosedPositions,
updateMatchesDarkMode, updateMatchesDarkMode,
updateUserDarkMode, updateUserDarkMode,
updateUserExpertMode,
updateUserSlippageTolerance,
updateUserDeadline, updateUserDeadline,
updateUserSingleHopOnly, updateUserExpertMode,
updateHideClosedPositions,
updateUserLocale, updateUserLocale,
updateArbitrumAlphaAcknowledged, updateUserClientSideRouter,
updateUserSlippageTolerance,
} from './actions' } from './actions'
import { SupportedLocale } from 'constants/locales'
const currentTimestamp = () => new Date().getTime() const currentTimestamp = () => new Date().getTime()
...@@ -35,7 +35,7 @@ export interface UserState { ...@@ -35,7 +35,7 @@ export interface UserState {
userExpertMode: boolean userExpertMode: boolean
userSingleHopOnly: boolean // only allow swaps on direct pairs userClientSideRouter: boolean // whether routes should be calculated with the client side router only
// hides closed (inactive) positions across the app // hides closed (inactive) positions across the app
userHideClosedPositions: boolean userHideClosedPositions: boolean
...@@ -74,7 +74,7 @@ export const initialState: UserState = { ...@@ -74,7 +74,7 @@ export const initialState: UserState = {
matchesDarkMode: false, matchesDarkMode: false,
userExpertMode: false, userExpertMode: false,
userLocale: null, userLocale: null,
userSingleHopOnly: false, userClientSideRouter: false,
userHideClosedPositions: false, userHideClosedPositions: false,
userSlippageTolerance: 'auto', userSlippageTolerance: 'auto',
userSlippageToleranceHasBeenMigratedToAuto: true, userSlippageToleranceHasBeenMigratedToAuto: true,
...@@ -147,8 +147,8 @@ export default createReducer(initialState, (builder) => ...@@ -147,8 +147,8 @@ export default createReducer(initialState, (builder) =>
state.userDeadline = action.payload.userDeadline state.userDeadline = action.payload.userDeadline
state.timestamp = currentTimestamp() state.timestamp = currentTimestamp()
}) })
.addCase(updateUserSingleHopOnly, (state, action) => { .addCase(updateUserClientSideRouter, (state, action) => {
state.userSingleHopOnly = action.payload.userSingleHopOnly state.userClientSideRouter = action.payload.userClientSideRouter
}) })
.addCase(updateHideClosedPositions, (state, action) => { .addCase(updateHideClosedPositions, (state, action) => {
state.userHideClosedPositions = action.payload.userHideClosedPositions state.userHideClosedPositions = action.payload.userHideClosedPositions
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ 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,
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))
...@@ -28,13 +29,24 @@ export function computeRealizedLPFeePercent( ...@@ -28,13 +29,24 @@ export function computeRealizedLPFeePercent(
) )
) )
} else { } else {
percent = ONE_HUNDRED_PERCENT.subtract( //TODO(judo): validate this
trade.route.pools.reduce<Percent>( percent = ZERO_PERCENT
(currentFee: Percent, pool): Percent => for (const swap of trade.swaps) {
currentFee.multiply(ONE_HUNDRED_PERCENT.subtract(new Fraction(pool.fee, 1_000_000))), const { numerator, denominator } = swap.inputAmount.divide(trade.inputAmount)
ONE_HUNDRED_PERCENT const overallPercent = new Percent(numerator, denominator)
const routeRealizedLPFeePercent = overallPercent.multiply(
ONE_HUNDRED_PERCENT.subtract(
swap.route.pools.reduce<Percent>(
(currentFee: Percent, pool): Percent =>
currentFee.multiply(ONE_HUNDRED_PERCENT.subtract(new Fraction(pool.fee, 1_000_000))),
ONE_HUNDRED_PERCENT
)
)
) )
)
percent = percent.add(routeRealizedLPFeePercent)
}
} }
return new Percent(percent.numerator, percent.denominator) return new Percent(percent.numerator, percent.denominator)
......
...@@ -3422,6 +3422,17 @@ ...@@ -3422,6 +3422,17 @@
lodash "^4.17.15" lodash "^4.17.15"
redent "^3.0.0" redent "^3.0.0"
"@testing-library/react-hooks@^7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz#3388d07f562d91e7f2431a4a21b5186062ecfee0"
integrity sha512-dYxpz8u9m4q1TuzfcUApqi8iFfR6R0FaMbr2hjZJy1uC8z+bO/K4v8Gs9eogGKYQop7QsrBTFkv/BCF7MzD2Cg==
dependencies:
"@babel/runtime" "^7.12.5"
"@types/react" ">=16.9.0"
"@types/react-dom" ">=16.9.0"
"@types/react-test-renderer" ">=16.9.0"
react-error-boundary "^3.1.0"
"@testing-library/react@^12.0.0": "@testing-library/react@^12.0.0":
version "12.0.0" version "12.0.0"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.0.0.tgz#9aeb2264521522ab9b68f519eaf15136148f164a" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.0.0.tgz#9aeb2264521522ab9b68f519eaf15136148f164a"
...@@ -3948,6 +3959,13 @@ ...@@ -3948,6 +3959,13 @@
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
"@types/react-dom@>=16.9.0":
version "17.0.9"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
integrity sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==
dependencies:
"@types/react" "*"
"@types/react-dom@^17.0.1": "@types/react-dom@^17.0.1":
version "17.0.9" version "17.0.9"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
...@@ -3982,6 +4000,13 @@ ...@@ -3982,6 +4000,13 @@
"@types/history" "*" "@types/history" "*"
"@types/react" "*" "@types/react" "*"
"@types/react-test-renderer@>=16.9.0":
version "17.0.1"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b"
integrity sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==
dependencies:
"@types/react" "*"
"@types/react-virtualized-auto-sizer@^1.0.0": "@types/react-virtualized-auto-sizer@^1.0.0":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz#b3187dae1dfc4c15880c9cfc5b45f2719ea6ebd4" resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz#b3187dae1dfc4c15880c9cfc5b45f2719ea6ebd4"
...@@ -4005,6 +4030,15 @@ ...@@ -4005,6 +4030,15 @@
"@types/scheduler" "*" "@types/scheduler" "*"
csstype "^3.0.2" csstype "^3.0.2"
"@types/react@>=16.9.0":
version "17.0.20"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.20.tgz#a4284b184d47975c71658cd69e759b6bd37c3b8c"
integrity sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/rebass@^4.0.7": "@types/rebass@^4.0.7":
version "4.0.9" version "4.0.9"
resolved "https://registry.yarnpkg.com/@types/rebass/-/rebass-4.0.9.tgz#526b6e2ceab2b7e76e45cbeed264e250118b8036" resolved "https://registry.yarnpkg.com/@types/rebass/-/rebass-4.0.9.tgz#526b6e2ceab2b7e76e45cbeed264e250118b8036"
...@@ -15984,6 +16018,13 @@ react-dom@^17.0.1: ...@@ -15984,6 +16018,13 @@ react-dom@^17.0.1:
object-assign "^4.1.1" object-assign "^4.1.1"
scheduler "^0.20.2" scheduler "^0.20.2"
react-error-boundary@^3.1.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.3.tgz#276bfa05de8ac17b863587c9e0647522c25e2a0b"
integrity sha512-A+F9HHy9fvt9t8SNDlonq01prnU8AmkjvGKV4kk8seB9kU3xMEO8J/PQlLVmoOIDODl5U2kufSBs4vrWIqhsAA==
dependencies:
"@babel/runtime" "^7.12.5"
react-error-overlay@^6.0.9: react-error-overlay@^6.0.9:
version "6.0.9" version "6.0.9"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
......
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