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 { useState, useCallback, ReactNode } from 'react'
import styled from 'styled-components/macro'
import { Pair } from '@uniswap/v2-sdk'
import { AutoColumn } from 'components/Column'
import { loadingOpacityMixin, LoadingOpacityContainer } from 'components/Loader/styled'
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 CurrencySearchModal from '../SearchModal/CurrencySearchModal'
import { TYPE } from '../../theme'
import { ButtonGray } from '../Button'
import CurrencyLogo from '../CurrencyLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
import { ButtonGray } from '../Button'
import { RowBetween, RowFixed } from '../Row'
import { TYPE } from '../../theme'
import { Input as NumericalInput } from '../NumericalInput'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import { useActiveWeb3React } from '../../hooks/web3'
import { Trans } from '@lingui/macro'
import useTheme from '../../hooks/useTheme'
import { Lock } from 'react-feather'
import { AutoColumn } from 'components/Column'
import { RowBetween, RowFixed } from '../Row'
import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
import { FiatValue } from './FiatValue'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
const InputPanel = styled.div<{ hideInput?: boolean }>`
${({ theme }) => theme.flexColumnNoWrap}
......@@ -81,6 +82,7 @@ const CurrencySelect = styled(ButtonGray)<{ visible: boolean; selected: boolean;
const InputRow = styled.div<{ selected: boolean }>`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
justify-content: space-between;
padding: ${({ selected }) => (selected ? ' 1rem 1rem 0.75rem 1rem' : '1rem 1rem 0.75rem 1rem')};
`
......@@ -145,6 +147,10 @@ const StyledBalanceMax = styled.button<{ disabled?: boolean }>`
`};
`
const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean }>`
${loadingOpacityMixin}
`
interface CurrencyInputPanelProps {
value: string
onUserInput: (value: string) => void
......@@ -165,6 +171,7 @@ interface CurrencyInputPanelProps {
disableNonToken?: boolean
renderBalance?: (amount: CurrencyAmount<Currency>) => ReactNode
locked?: boolean
loading?: boolean
}
export default function CurrencyInputPanel({
......@@ -186,6 +193,7 @@ export default function CurrencyInputPanel({
pair = null, // used for double token logo
hideInput = false,
locked = false,
loading = false,
...rest
}: CurrencyInputPanelProps) {
const [modalOpen, setModalOpen] = useState(false)
......@@ -249,15 +257,12 @@ export default function CurrencyInputPanel({
</Aligner>
</CurrencySelect>
{!hideInput && (
<>
<NumericalInput
className="token-amount-input"
value={value}
onUserInput={(val) => {
onUserInput(val)
}}
/>
</>
<StyledNumericalInput
className="token-amount-input"
value={value}
onUserInput={onUserInput}
$loading={loading}
/>
)}
</InputRow>
{!hideInput && !hideBalance && (
......@@ -291,7 +296,9 @@ export default function CurrencyInputPanel({
) : (
<span />
)}
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
<LoadingOpacityContainer $loading={loading}>
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
</LoadingOpacityContainer>
</RowBetween>
</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 { transparentize } from 'polished'
import React, { useCallback, useState } from 'react'
import { Options, Placement } from '@popperjs/core'
import Portal from '@reach/portal'
import React, { useCallback, useMemo, useState } from 'react'
import { usePopper } from 'react-popper'
import styled from 'styled-components/macro'
import useInterval from '../../hooks/useInterval'
import Portal from '@reach/portal'
const PopoverContainer = styled.div<{ show: boolean }>`
z-index: 9999;
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
opacity: ${(props) => (props.show ? 1 : 0)};
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};
border-radius: 8px;
`
const ReferenceElement = styled.div`
......@@ -34,9 +29,9 @@ const Arrow = styled.div`
z-index: 9998;
content: '';
border: 1px solid ${({ theme }) => theme.bg3};
border: 1px solid ${({ theme }) => theme.bg2};
transform: rotate(45deg);
background: ${({ theme }) => theme.bg2};
background: ${({ theme }) => theme.bg0};
}
&.arrow-top {
......@@ -84,14 +79,22 @@ export default function Popover({ content, show, children, placement = 'auto' }:
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null)
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
const { styles, update, attributes } = usePopper(referenceElement, popperElement, {
placement,
strategy: 'fixed',
modifiers: [
{ name: 'offset', options: { offset: [8, 8] } },
{ name: 'arrow', options: { element: arrowElement } },
],
})
const options = useMemo(
(): Options => ({
placement,
strategy: 'fixed',
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(() => {
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'
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import { ApplicationModal } from '../../state/application/actions'
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 { ButtonError } from '../Button'
import { AutoColumn } from '../Column'
......@@ -17,6 +17,8 @@ import { RowBetween, RowFixed } from '../Row'
import Toggle from '../Toggle'
import TransactionSettings from '../TransactionSettings'
import { Percent } from '@uniswap/sdk-core'
import { useActiveWeb3React } from 'hooks/web3'
import { SupportedChainId } from 'constants/chains'
const StyledMenuIcon = styled(Settings)`
height: 20px;
......@@ -115,6 +117,8 @@ const ModalContentWrapper = styled.div`
`
export default function SettingsTab({ placeholderSlippage }: { placeholderSlippage: Percent }) {
const { chainId } = useActiveWeb3React()
const node = useRef<HTMLDivElement>()
const open = useModalOpen(ApplicationModal.SETTINGS)
const toggle = useToggleSettingsMenu()
......@@ -123,7 +127,7 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
const [expertMode, toggleExpertMode] = useExpertModeManager()
const [singleHopOnly, setSingleHopOnly] = useUserSingleHopOnly()
const [clientSideRouter, setClientSideRouter] = useClientSideRouter()
// show confirmation view before turning on
const [showConfirmation, setShowConfirmation] = useState(false)
......@@ -193,10 +197,35 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
<Text fontWeight={600} fontSize={14}>
<Trans>Interface Settings</Trans>
</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>
<RowFixed>
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
<Trans>Toggle Expert Mode</Trans>
<Trans>Expert Mode</Trans>
</TYPE.black>
<QuestionHelper
text={
......@@ -220,25 +249,6 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
}
/>
</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>
</MenuFlyout>
)}
......
import { TextInput, ResizingTextArea } from './'
import { render, screen, fireEvent } from 'test-utils'
// include style rules in snapshots
import 'jest-styled-components'
describe('TextInput', () => {
it('renders correctly', () => {
......
import { transparentize } from 'polished'
import { ReactNode, useCallback, useState } from 'react'
import styled from 'styled-components/macro'
import Popover, { PopoverProps } from '../Popover'
const TooltipContainer = styled.div`
export const TooltipContainer = styled.div`
width: 256px;
padding: 0.6rem 1rem;
font-weight: 400;
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'> {
......@@ -15,14 +21,17 @@ interface TooltipProps extends Omit<PopoverProps, 'content'> {
interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
content: ReactNode
onOpen?: () => void
// whether to wrap the content in a `TooltipContainer`
wrap?: boolean
}
export default function Tooltip({ text, ...rest }: TooltipProps) {
return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} />
}
function TooltipContent({ content, ...rest }: TooltipContentProps) {
return <Popover content={<TooltipContainer>{content}</TooltipContainer>} {...rest} />
function TooltipContent({ content, wrap = false, ...rest }: TooltipContentProps) {
return <Popover content={wrap ? <TooltipContainer>{content}</TooltipContainer> : content} {...rest} />
}
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 open = useCallback(() => setShow(true), [setShow])
const open = useCallback(() => {
setShow(true)
openCallback?.()
}, [openCallback])
const close = useCallback(() => setShow(false), [setShow])
return (
<TooltipContent {...rest} show={show} content={content}>
......
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 V3Trade } from '@uniswap/v3-sdk'
import { LoadingRows } from 'components/Loader/styled'
import { useContext, useMemo } from 'react'
import { ThemeContext } from 'styled-components/macro'
import { TYPE } from '../../theme'
......@@ -9,14 +10,33 @@ import { computeRealizedLPFeePercent } from '../../utils/prices'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
import FormattedPriceImpact from './FormattedPriceImpact'
import SwapRoute from './SwapRoute'
import { TransactionDetailsLabel } from './styleds'
interface AdvancedSwapDetailsProps {
trade?: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
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 { realizedLPFee, priceImpact } = useMemo(() => {
......@@ -30,61 +50,61 @@ export function AdvancedSwapDetails({ trade, allowedSlippage }: AdvancedSwapDeta
return !trade ? null : (
<AutoColumn gap="8px">
<TransactionDetailsLabel fontWeight={500} fontSize={14}>
<Trans>Transaction Details</Trans>
</TransactionDetailsLabel>
<RowBetween>
<RowFixed>
<TYPE.black fontSize={12} fontWeight={400} color={theme.text2}>
<TYPE.subHeader color={theme.text1}>
<Trans>Liquidity Provider Fee</Trans>
</TYPE.black>
</TYPE.subHeader>
</RowFixed>
<TYPE.black textAlign="right" fontSize={12} color={theme.text1}>
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${realizedLPFee.currency.symbol}` : '-'}
</TYPE.black>
</RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontSize={12} fontWeight={400} color={theme.text2}>
<Trans>Route</Trans>
<TextWithLoadingPlaceholder syncing={syncing} width={65}>
<TYPE.black textAlign="right" fontSize={14}>
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${realizedLPFee.currency.symbol}` : '-'}
</TYPE.black>
</RowFixed>
<TYPE.black textAlign="right" fontSize={12} color={theme.text1}>
<SwapRoute trade={trade} />
</TYPE.black>
</TextWithLoadingPlaceholder>
</RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontSize={12} fontWeight={400} color={theme.text2}>
<TYPE.subHeader color={theme.text1}>
<Trans>Price Impact</Trans>
</TYPE.black>
</TYPE.subHeader>
</RowFixed>
<TYPE.black textAlign="right" fontSize={12} color={theme.text1}>
<FormattedPriceImpact priceImpact={priceImpact} />
</TYPE.black>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<TYPE.black textAlign="right" fontSize={14}>
<FormattedPriceImpact priceImpact={priceImpact} />
</TYPE.black>
</TextWithLoadingPlaceholder>
</RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontSize={12} fontWeight={400} color={theme.text2}>
{trade.tradeType === TradeType.EXACT_INPUT ? <Trans>Minimum received</Trans> : <Trans>Maximum sent</Trans>}
</TYPE.black>
<TYPE.subHeader color={theme.text1}>
<Trans>Allowed Slippage</Trans>
</TYPE.subHeader>
</RowFixed>
<TYPE.black textAlign="right" fontSize={12} color={theme.text1}>
{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 syncing={syncing} width={45}>
<TYPE.black textAlign="right" fontSize={14}>
{allowedSlippage.toFixed(2)}%
</TYPE.black>
</TextWithLoadingPlaceholder>
</RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontSize={12} fontWeight={400} color={theme.text2}>
<Trans>Slippage tolerance</Trans>
</TYPE.black>
<TYPE.subHeader color={theme.text1}>
{trade.tradeType === TradeType.EXACT_INPUT ? <Trans>Minimum received</Trans> : <Trans>Maximum sent</Trans>}
</TYPE.subHeader>
</RowFixed>
<TYPE.black textAlign="right" fontSize={12} color={theme.text1}>
{allowedSlippage.toFixed(2)}%
</TYPE.black>
<TextWithLoadingPlaceholder syncing={syncing} width={70}>
<TYPE.black textAlign="right" fontSize={14}>
{trade.tradeType === TradeType.EXACT_INPUT
? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}`
: `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`}
</TYPE.black>
</TextWithLoadingPlaceholder>
</RowBetween>
</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'
*/
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
return (
<ErrorText fontWeight={500} fontSize={12} severity={warningSeverity(priceImpact)}>
<ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}>
{priceImpact ? `${priceImpact.multiply(-1).toFixed(2)}%` : '-'}
</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 V3Trade, FeeAmount } from '@uniswap/v3-sdk'
import { Fragment, memo, useContext } from 'react'
import { ChevronRight } from 'react-feather'
import { Flex } from 'rebass'
import { ThemeContext } from 'styled-components/macro'
import { TYPE } from '../../theme'
import { unwrappedToken } from 'utils/unwrappedToken'
function LabeledArrow({}: { fee: FeeAmount }) {
const theme = useContext(ThemeContext)
// todo: render the fee in the label
return <ChevronRight size={14} color={theme.text2} />
}
import { FeeAmount, Trade as V3Trade } from '@uniswap/v3-sdk'
import Badge from 'components/Badge'
import { AutoColumn } from 'components/Column'
import { LoadingRows } from 'components/Loader/styled'
import RoutingDiagram, { RoutingDiagramEntry } from 'components/RoutingDiagram/RoutingDiagram'
import { AutoRow, RowBetween } from 'components/Row'
import { Version } from 'hooks/useToggledVersion'
import { memo } from 'react'
import { useRoutingAPIEnabled } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { TYPE } from 'theme'
import { getTradeVersion } from 'utils/getTradeVersion'
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({
trade,
syncing,
}: {
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType>
syncing: boolean
}) {
const tokenPath = trade instanceof V2Trade ? trade.route.path : trade.route.tokenPath
const theme = useContext(ThemeContext)
const routingAPIEnabled = useRoutingAPIEnabled()
return (
<Flex flexWrap="wrap" width="100%" justifyContent="flex-start" alignItems="center">
{tokenPath.map((token, i, path) => {
const isLastItem: boolean = i === path.length - 1
const currency = unwrappedToken(token)
return (
<Fragment key={i}>
<Flex alignItems="end">
<TYPE.black color={theme.text1} ml="0.145rem" mr="0.145rem">
{currency.symbol}
</TYPE.black>
</Flex>
{isLastItem ? null : trade instanceof V2Trade ? (
<ChevronRight size={14} color={theme.text2} />
) : (
<LabeledArrow fee={trade.route.pools[i].fee} />
)}
</Fragment>
)
})}
</Flex>
<AutoColumn gap="12px">
<RowBetween>
<AutoRow gap="4px" width="auto">
<AutoRouterLogo />
<AutoRouterLabel />
</AutoRow>
{syncing ? (
<LoadingRows>
<div style={{ width: '30px', height: '24px' }} />
</LoadingRows>
) : (
<Badge>
<TYPE.black fontSize={12}>
{getTradeVersion(trade) === Version.v2 ? <Trans>V2</Trans> : <Trans>V3</Trans>}
</TYPE.black>
</Badge>
)}
</RowBetween>
<Separator />
{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 { Price, Currency } from '@uniswap/sdk-core'
import { useContext } from 'react'
import { useCallback, useContext } from 'react'
import { Currency, Price } from '@uniswap/sdk-core'
import { Text } from 'rebass'
import styled, { ThemeContext } from 'styled-components/macro'
......
......@@ -5,6 +5,10 @@ import { AlertTriangle } from 'react-feather'
import styled, { css } from 'styled-components/macro'
import { Text } from 'rebass'
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`
position: relative;
......@@ -128,3 +132,24 @@ export const SwapShowAcceptChanges = styled(AutoColumn)`
border-radius: 12px;
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(
export const BETTER_TRADE_LESS_HOPS_THRESHOLD = new Percent(JSBI.BigInt(50), BIPS_BASE)
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')
import { Currency } from '@uniswap/sdk-core'
import { Pool, Route } from '@uniswap/v3-sdk'
import { useMemo } from 'react'
import { useUserSingleHopOnly } from '../state/user/hooks'
import { useActiveWeb3React } from './web3'
import { useV3SwapPools } from './useV3SwapPools'
......@@ -66,12 +65,10 @@ export function useAllV3Routes(
const { chainId } = useActiveWeb3React()
const { pools, loading: poolsLoading } = useV3SwapPools(currencyIn, currencyOut)
const [singleHopOnly] = useUserSingleHopOnly()
return useMemo(() => {
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 }
}, [chainId, currencyIn, currencyOut, pools, poolsLoading, singleHopOnly])
}, [chainId, currencyIn, currencyOut, pools, poolsLoading])
}
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 { BigNumber } from 'ethers'
import { useMemo } from 'react'
import { V3TradeState } from 'state/routing/types'
import { useSingleContractWithCallData } from '../state/multicall/hooks'
import { useAllV3Routes } from './useAllV3Routes'
import { useV3Quoter } from './useContract'
import { useActiveWeb3React } from './web3'
export enum V3TradeState {
LOADING,
INVALID,
NO_ROUTE_FOUND,
VALID,
SYNCING,
}
const QUOTE_GAS_OVERRIDES: { [chainId: number]: number } = {
[SupportedChainId.OPTIMISM]: 6_000_000,
[SupportedChainId.OPTIMISTIC_KOVAN]: 6_000_000,
......@@ -28,7 +21,7 @@ const DEFAULT_GAS_QUOTE = 2_000_000
* @param amountIn the amount to swap in
* @param currencyOut the desired output currency
*/
export function useBestV3TradeExactIn(
export function useClientV3TradeExactIn(
amountIn?: CurrencyAmount<Currency>,
currencyOut?: Currency
): { state: V3TradeState; trade: Trade<Currency, Currency, TradeType.EXACT_INPUT> | null } {
......@@ -97,10 +90,8 @@ export function useBestV3TradeExactIn(
}
}
const isSyncing = quotesResults.some(({ syncing }) => syncing)
return {
state: isSyncing ? V3TradeState.SYNCING : V3TradeState.VALID,
state: V3TradeState.VALID,
trade: Trade.createUncheckedTrade({
route: bestRoute,
tradeType: TradeType.EXACT_INPUT,
......@@ -116,7 +107,7 @@ export function useBestV3TradeExactIn(
* @param currencyIn the desired input currency
* @param amountOut the amount to swap out
*/
export function useBestV3TradeExactOut(
export function useClientSideV3TradeExactOut(
currencyIn?: Currency,
amountOut?: CurrencyAmount<Currency>
): { state: V3TradeState; trade: Trade<Currency, Currency, TradeType.EXACT_OUTPUT> | null } {
......@@ -186,10 +177,8 @@ export function useBestV3TradeExactOut(
}
}
const isSyncing = quotesResults.some(({ syncing }) => syncing)
return {
state: isSyncing ? V3TradeState.SYNCING : V3TradeState.VALID,
state: V3TradeState.VALID,
trade: Trade.createUncheckedTrade({
route: bestRoute,
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 {
v3 = 'V3',
}
export const DEFAULT_VERSION: Version = Version.v3
export default function useToggledVersion(): Version {
export default function useToggledVersion(): Version | undefined {
const { use } = useParsedQueryString()
if (typeof use !== 'string') {
return DEFAULT_VERSION
return undefined
}
switch (use.toLowerCase()) {
case 'v2':
......@@ -18,6 +16,6 @@ export default function useToggledVersion(): Version {
case 'v3':
return Version.v3
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'
import { SupportedChainId } from '../constants/chains'
import { DAI_OPTIMISM, USDC, USDC_ARBITRUM } from '../constants/tokens'
import { useV2TradeExactOut } from './useV2Trade'
import { useBestV3TradeExactOut } from './useBestV3Trade'
import { useClientSideV3TradeExactOut } from './useClientSideV3Trade'
import { useActiveWeb3React } from './web3'
// Stablecoin amounts used when calculating spot price for a given currency.
......@@ -27,7 +27,7 @@ export default function useUSDCPrice(currency?: Currency): Price<Currency, Token
const v2USDCTrade = useV2TradeExactOut(currency, amountOut, {
maxHops: 2,
})
const v3USDCTrade = useBestV3TradeExactOut(currency, amountOut)
const v3USDCTrade = useClientSideV3TradeExactOut(currency, amountOut)
return useMemo(() => {
if (!currency || !stablecoin) {
......
......@@ -9,7 +9,6 @@ import { unwrappedToken } from 'utils/unwrappedToken'
import { usePositionTokenURI } from '../../hooks/usePositionTokenURI'
import { calculateGasMargin } from '../../utils/calculateGasMargin'
import { getExplorerLink, ExplorerDataType } from '../../utils/getExplorerLink'
import { LoadingRows } from './styleds'
import styled from 'styled-components/macro'
import { AutoColumn } from 'components/Column'
import { RowBetween, RowFixed } from 'components/Row'
......@@ -45,6 +44,7 @@ import { Bound } from 'state/mint/v3/actions'
import useIsTickAtLimit from 'hooks/useIsTickAtLimit'
import { formatTickPrice } from 'utils/formatTickPrice'
import { SupportedChainId } from 'constants/chains'
import { LoadingRows } from './styleds'
const PageWrapper = styled.div`
min-width: 800px;
......
import { LoadingRows as BaseLoadingRows } from 'components/Loader/styled'
import { Text } from 'rebass'
import styled, { keyframes } from 'styled-components/macro'
import styled from 'styled-components/macro'
export const Wrapper = styled.div`
position: relative;
......@@ -56,36 +57,13 @@ export const Dots = styled.span`
}
`
const loadingAnimation = keyframes`
0% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
`
export const LoadingRows = styled.div`
display: grid;
export const LoadingRows = styled(BaseLoadingRows)`
min-width: 75%;
max-width: 960px;
grid-column-gap: 0.5em;
grid-row-gap: 0.8em;
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) {
grid-column: 1 / 3;
}
......
......@@ -2,42 +2,50 @@ import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { LoadingOpacityContainer } from 'components/Loader/styled'
import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert'
import { AdvancedSwapDetails } from 'components/swap/AdvancedSwapDetails'
import { AutoRouterLogo } from 'components/swap/RouterLabel'
import SwapRoute from 'components/swap/SwapRoute'
import TradePrice from 'components/swap/TradePrice'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import { MouseoverTooltip, MouseoverTooltipContent } from 'components/Tooltip'
import JSBI from 'jsbi'
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 { Link, RouteComponentProps } from 'react-router-dom'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import { V3TradeState } from 'state/routing/types'
import styled, { ThemeContext } from 'styled-components/macro'
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 { AutoColumn } from '../../components/Column'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import CurrencyLogo from '../../components/CurrencyLogo'
import Loader from '../../components/Loader'
import Row, { AutoRow, RowFixed } from '../../components/Row'
import BetterTradeLink from '../../components/swap/BetterTradeLink'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
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 TradePrice from '../../components/swap/TradePrice'
import { SwitchLocaleLink } from '../../components/SwitchLocaleLink'
import TokenWarningModal from '../../components/TokenWarningModal'
import { useAllTokens, useCurrency } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
import { V3TradeState } from '../../hooks/useBestV3Trade'
import useENSAddress from '../../hooks/useENSAddress'
import { useERC20PermitFromTrade, UseERC20PermitState } from '../../hooks/useERC20Permit'
import useIsArgentWallet from '../../hooks/useIsArgentWallet'
import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported'
import { useSwapCallback } from '../../hooks/useSwapCallback'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
import useToggledVersion from '../../hooks/useToggledVersion'
import { useUSDCValue } from '../../hooks/useUSDCPrice'
import useWrapCallback, { WrapType } from '../../hooks/useWrapCallback'
import { useActiveWeb3React } from '../../hooks/web3'
......@@ -49,22 +57,21 @@ import {
useSwapActionHandlers,
useSwapState,
} from '../../state/swap/hooks'
import { useExpertModeManager, useUserSingleHopOnly } from '../../state/user/hooks'
import { HideSmall, LinkStyledButton, TYPE } from '../../theme'
import { useExpertModeManager } from '../../state/user/hooks'
import { LinkStyledButton, TYPE } from '../../theme'
import { computeFiatValuePriceImpact } from '../../utils/computeFiatValuePriceImpact'
import { getTradeVersion } from '../../utils/getTradeVersion'
import { isTradeBetter } from '../../utils/isTradeBetter'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody'
const StyledInfo = styled(Info)`
opacity: 0.4;
color: ${({ theme }) => theme.text1};
height: 16px;
width: 16px;
margin-left: 4px;
color: ${({ theme }) => theme.text3};
:hover {
opacity: 0.8;
color: ${({ theme }) => theme.text1};
}
`
......@@ -108,9 +115,8 @@ export default function Swap({ history }: RouteComponentProps) {
// swap state
const { independentField, typedValue, recipient } = useSwapState()
const {
v2Trade,
v3TradeState: { trade: v3Trade, state: v3TradeState },
toggledTrade: trade,
v3Trade: { state: v3TradeState },
bestTrade: trade,
allowedSlippage,
currencyBalances,
parsedAmount,
......@@ -140,9 +146,18 @@ export default function Swap({ history }: RouteComponentProps) {
[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 fiatValueOutput = useUSDCValue(parsedAmounts[Field.OUTPUT])
const priceImpact = computeFiatValuePriceImpact(fiatValueInput, fiatValueOutput)
const priceImpact = routeIsSyncing ? undefined : computeFiatValuePriceImpact(fiatValueInput, fiatValueOutput)
const { onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient } = useSwapActionHandlers()
const isValid = !swapInputError
......@@ -192,8 +207,6 @@ export default function Swap({ history }: RouteComponentProps) {
const userHasSpecifiedInputOutput = Boolean(
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
const [approvalState, approveCallback] = useApproveCallbackFromTrade(trade, allowedSlippage)
......@@ -245,8 +258,6 @@ export default function Swap({ history }: RouteComponentProps) {
signatureData
)
const [singleHopOnly] = useUserSingleHopOnly()
const handleSwap = useCallback(() => {
if (!swapCallback) {
return
......@@ -270,7 +281,7 @@ export default function Swap({ history }: RouteComponentProps) {
trade?.inputAmount?.currency?.symbol,
trade?.outputAmount?.currency?.symbol,
getTradeVersion(trade),
singleHopOnly ? 'SH' : 'MH',
'MH',
].join('/'),
})
})
......@@ -283,17 +294,7 @@ export default function Swap({ history }: RouteComponentProps) {
txHash: undefined,
})
})
}, [
swapCallback,
priceImpact,
tradeToConfirm,
showConfirm,
recipient,
recipientAddress,
account,
trade,
singleHopOnly,
])
}, [swapCallback, priceImpact, tradeToConfirm, showConfirm, recipient, recipientAddress, account, trade])
// errors
const [showInverted, setShowInverted] = useState<boolean>(false)
......@@ -381,7 +382,7 @@ export default function Swap({ history }: RouteComponentProps) {
onDismiss={handleConfirmDismiss}
/>
<AutoColumn gap={'md'}>
<AutoColumn gap={'sm'}>
<div style={{ display: 'relative' }}>
<CurrencyInputPanel
label={
......@@ -397,6 +398,7 @@ export default function Swap({ history }: RouteComponentProps) {
otherCurrency={currencies[Field.OUTPUT]}
showCommonBases={true}
id="swap-currency-input"
loading={independentField === Field.OUTPUT && routeIsSyncing}
/>
<ArrowWrapper clickable>
<ArrowDown
......@@ -421,6 +423,7 @@ export default function Swap({ history }: RouteComponentProps) {
otherCurrency={currencies[Field.INPUT]}
showCommonBases={true}
id="swap-currency-output"
loading={independentField === Field.INPUT && routeIsSyncing}
/>
</div>
......@@ -438,74 +441,60 @@ export default function Swap({ history }: RouteComponentProps) {
</>
) : null}
{showWrap ? null : (
<Row style={{ justifyContent: !trade ? 'center' : 'space-between' }}>
<RowFixed>
{[V3TradeState.VALID, V3TradeState.SYNCING, V3TradeState.NO_ROUTE_FOUND].includes(v3TradeState) &&
(toggledVersion === Version.v3 && isTradeBetter(v3Trade, v2Trade) ? (
<BetterTradeLink version={Version.v2} otherTradeNonexistent={!v3Trade} />
) : toggledVersion === Version.v2 && isTradeBetter(v2Trade, v3Trade) ? (
<BetterTradeLink version={Version.v3} otherTradeNonexistent={!v2Trade} />
) : (
toggledVersion === Version.v2 && (
<ButtonGray
width="fit-content"
padding="0.1rem 0.5rem 0.1rem 0.35rem"
as={Link}
to="/swap"
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
height: '24px',
lineHeight: '120%',
marginLeft: '0.75rem',
}}
>
<ArrowLeft color={theme.text3} size={12} /> &nbsp;
<TYPE.main style={{ lineHeight: '120%' }} fontSize={12}>
<Trans>
<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>
)}
{!showWrap && trade && (
<Row justify={!trade ? 'center' : 'space-between'}>
<RowFixed style={{ position: 'relative' }}>
<MouseoverTooltipContent
wrap={false}
content={
<ResponsiveTooltipContainer>
<SwapRoute trade={trade} syncing={routeIsSyncing} />
</ResponsiveTooltipContainer>
}
placement="bottom"
onOpen={() =>
ReactGA.event({
category: 'Swap',
action: 'Router Tooltip Open',
})
}
>
<AutoRow gap="4px" width="auto">
<AutoRouterLogo />
<LoadingOpacityContainer $loading={routeIsSyncing}>
{trade instanceof V3Trade && trade.swaps.length > 1 && (
<TYPE.blue fontSize={14}>{trade.swaps.length} routes</TYPE.blue>
)}
</LoadingOpacityContainer>
</AutoRow>
</MouseoverTooltipContent>
</RowFixed>
{trade ? (
<RowFixed>
<RowFixed>
<LoadingOpacityContainer $loading={routeIsSyncing}>
<TradePrice
price={trade.executionPrice}
showInverted={showInverted}
setShowInverted={setShowInverted}
/>
<MouseoverTooltipContent
content={<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} />}
>
<StyledInfo />
</MouseoverTooltipContent>
</RowFixed>
) : null}
</LoadingOpacityContainer>
<MouseoverTooltipContent
wrap={false}
content={
<ResponsiveTooltipContainer origin="top right" width={'295px'}>
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={routeIsSyncing} />
</ResponsiveTooltipContainer>
}
placement="bottom"
onOpen={() =>
ReactGA.event({
category: 'Swap',
action: 'Transaction Details Tooltip Open',
})
}
>
<StyledInfo />
</MouseoverTooltipContent>
</RowFixed>
</Row>
)}
......@@ -529,18 +518,18 @@ export default function Swap({ history }: RouteComponentProps) {
<Trans>Unwrap</Trans>
) : null)}
</ButtonPrimary>
) : routeIsSyncing || routeIsLoading ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">
<Dots>
<Trans>Loading</Trans>
</Dots>
</TYPE.main>
</GreyCard>
) : routeNotFound && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">
{isLoadingRoute ? (
<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>
)}
<Trans>Insufficient liquidity for this trade.</Trans>
</TYPE.main>
</GreyCard>
) : showApproveFlow ? (
......
// jest custom assertions
import '@testing-library/jest-dom'
// include style rules in snapshots
import 'jest-styled-components'
import { configureStore } from '@reduxjs/toolkit'
import { save, load } from 'redux-localstorage-simple'
import { setupListeners } from '@reduxjs/toolkit/query/react'
import application from './application/reducer'
import { updateVersion } from './global/actions'
......@@ -44,6 +45,8 @@ const store = configureStore({
store.dispatch(updateVersion())
setupListeners(store.dispatch)
export default store
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 { SupportedChainId } from 'constants/chains'
import qs from 'qs'
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
}
import { GetQuoteResult } from './types'
export const routingApi = createApi({
baseQuery: fetchBaseQuery({
......@@ -41,9 +17,6 @@ export const routingApi = createApi({
tokenOutChainId: SupportedChainId
amount: string
type: 'exactIn' | 'exactOut'
recipient?: string
slippageTolerance?: string
deadline?: string
}
>({
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 { t } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { TWO_PERCENT } from 'constants/misc'
import { useV3TradeExactIn, useV3TradeExactOut } from 'hooks/useCombinedV3Trade'
import JSBI from 'jsbi'
import { ParsedQs } from 'qs'
import { useCallback, useEffect, useState } from 'react'
import { useActiveWeb3React } from '../../hooks/web3'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { V3TradeState } from 'state/routing/types'
import { isTradeBetter } from 'utils/isTradeBetter'
import { useCurrency } from '../../hooks/Tokens'
import useENS from '../../hooks/useENS'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import useSwapSlippageTolerance from '../../hooks/useSwapSlippageTolerance'
import { Version } from '../../hooks/useToggledVersion'
import { useV2TradeExactIn, useV2TradeExactOut } from '../../hooks/useV2Trade'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import { useActiveWeb3React } from '../../hooks/web3'
import { isAddress } from '../../utils'
import { AppState } from '../index'
import { useCurrencyBalances } from '../wallet/hooks'
import { Field, replaceSwapState, selectCurrency, setRecipient, switchCurrencies, typeInput } from './actions'
import { SwapState } from './reducer'
import { useUserSingleHopOnly } from 'state/user/hooks'
import { useAppDispatch, useAppSelector } from 'state/hooks'
export function useSwapState(): AppState['swap'] {
return useAppSelector((state) => state.swap)
......@@ -114,20 +116,21 @@ function involvesAddress(
}
// 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 }
currencyBalances: { [field in Field]?: CurrencyAmount<Currency> }
parsedAmount: CurrencyAmount<Currency> | undefined
inputError?: string
v2Trade: V2Trade<Currency, Currency, TradeType> | undefined
v3TradeState: { trade: V3Trade<Currency, Currency, TradeType> | null; state: V3TradeState }
toggledTrade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined
v3Trade: {
trade: V3Trade<Currency, Currency, TradeType> | null
state: V3TradeState
}
bestTrade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined
allowedSlippage: Percent
} {
const { account } = useActiveWeb3React()
const [singleHopOnly] = useUserSingleHopOnly()
const {
independentField,
typedValue,
......@@ -147,20 +150,48 @@ export function useDerivedSwapInfo(toggledVersion: Version): {
])
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, {
maxHops: singleHopOnly ? 1 : undefined,
})
const bestV2TradeExactOut = useV2TradeExactOut(inputCurrency ?? undefined, !isExactIn ? parsedAmount : undefined, {
maxHops: singleHopOnly ? 1 : undefined,
})
// get v2 and v3 quotes
// skip if other version is toggled
const bestV2TradeExactIn = useV2TradeExactIn(
toggledVersion !== Version.v3 && isExactIn ? parsedAmount : undefined,
outputCurrency ?? undefined
)
const bestV2TradeExactOut = useV2TradeExactOut(
inputCurrency ?? undefined,
toggledVersion !== Version.v3 && !isExactIn ? parsedAmount : undefined
)
const bestV3TradeExactIn = useBestV3TradeExactIn(isExactIn ? parsedAmount : undefined, outputCurrency ?? undefined)
const bestV3TradeExactOut = useBestV3TradeExactOut(inputCurrency ?? undefined, !isExactIn ? parsedAmount : undefined)
const bestV3TradeExactIn = useV3TradeExactIn(
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 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 = {
[Field.INPUT]: relevantTokenBalances[0],
......@@ -198,11 +229,10 @@ export function useDerivedSwapInfo(toggledVersion: Version): {
}
}
const toggledTrade = (toggledVersion === Version.v2 ? v2Trade : v3Trade.trade) ?? undefined
const allowedSlippage = useSwapSlippageTolerance(toggledTrade)
const allowedSlippage = useSwapSlippageTolerance(bestTrade ?? undefined)
// 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)) {
inputError = t`Insufficient ${amountIn.currency.symbol} balance`
......@@ -214,8 +244,8 @@ export function useDerivedSwapInfo(toggledVersion: Version): {
parsedAmount,
inputError,
v2Trade: v2Trade ?? undefined,
v3TradeState: v3Trade,
toggledTrade,
v3Trade,
bestTrade: bestTrade ?? undefined,
allowedSlippage,
}
}
......
......@@ -21,7 +21,9 @@ export const updateArbitrumAlphaAcknowledged = createAction<{ arbitrumAlphaAckno
export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode')
export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode')
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 updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number | 'auto' }>(
'user/updateUserSlippageTolerance'
......
import { Percent, Token } from '@uniswap/sdk-core'
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 { L2_DEADLINE_FROM_NOW } from 'constants/misc'
import JSBI from 'jsbi'
......@@ -23,8 +23,8 @@ import {
updateUserDarkMode,
updateUserDeadline,
updateUserExpertMode,
updateUserClientSideRouter,
updateUserLocale,
updateUserSingleHopOnly,
updateUserSlippageTolerance,
} from './actions'
......@@ -104,19 +104,26 @@ export function useExpertModeManager(): [boolean, () => void] {
return [expertMode, toggleSetExpertMode]
}
export function useUserSingleHopOnly(): [boolean, (newSingleHopOnly: boolean) => void] {
export function useClientSideRouter(): [boolean, (userClientSideRouter: boolean) => void] {
const dispatch = useAppDispatch()
const singleHopOnly = useAppSelector((state) => state.user.userSingleHopOnly)
const clientSideRouter = useAppSelector((state) => Boolean(state.user.userClientSideRouter))
const setSingleHopOnly = useCallback(
(newSingleHopOnly: boolean) => {
dispatch(updateUserSingleHopOnly({ userSingleHopOnly: newSingleHopOnly }))
const setClientSideRouter = useCallback(
(newClientSideRouter: boolean) => {
dispatch(updateUserClientSideRouter({ userClientSideRouter: newClientSideRouter }))
},
[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 {
......
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants/misc'
import { createReducer } from '@reduxjs/toolkit'
import { SupportedLocale } from 'constants/locales'
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants/misc'
import { updateVersion } from '../global/actions'
import {
addSerializedPair,
......@@ -8,17 +9,16 @@ import {
removeSerializedToken,
SerializedPair,
SerializedToken,
updateArbitrumAlphaAcknowledged,
updateHideClosedPositions,
updateMatchesDarkMode,
updateUserDarkMode,
updateUserExpertMode,
updateUserSlippageTolerance,
updateUserDeadline,
updateUserSingleHopOnly,
updateHideClosedPositions,
updateUserExpertMode,
updateUserLocale,
updateArbitrumAlphaAcknowledged,
updateUserClientSideRouter,
updateUserSlippageTolerance,
} from './actions'
import { SupportedLocale } from 'constants/locales'
const currentTimestamp = () => new Date().getTime()
......@@ -35,7 +35,7 @@ export interface UserState {
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
userHideClosedPositions: boolean
......@@ -74,7 +74,7 @@ export const initialState: UserState = {
matchesDarkMode: false,
userExpertMode: false,
userLocale: null,
userSingleHopOnly: false,
userClientSideRouter: false,
userHideClosedPositions: false,
userSlippageTolerance: 'auto',
userSlippageToleranceHasBeenMigratedToAuto: true,
......@@ -147,8 +147,8 @@ export default createReducer(initialState, (builder) =>
state.userDeadline = action.payload.userDeadline
state.timestamp = currentTimestamp()
})
.addCase(updateUserSingleHopOnly, (state, action) => {
state.userSingleHopOnly = action.payload.userSingleHopOnly
.addCase(updateUserClientSideRouter, (state, action) => {
state.userClientSideRouter = action.payload.userClientSideRouter
})
.addCase(updateHideClosedPositions, (state, action) => {
state.userHideClosedPositions = action.payload.userHideClosedPositions
......
......@@ -7,6 +7,7 @@ import {
ALLOWED_PRICE_IMPACT_LOW,
ALLOWED_PRICE_IMPACT_MEDIUM,
BLOCKED_PRICE_IMPACT_NON_EXPERT,
ZERO_PERCENT,
} from '../constants/misc'
const THIRTY_BIPS_FEE = new Percent(JSBI.BigInt(30), JSBI.BigInt(10000))
......@@ -28,13 +29,24 @@ export function computeRealizedLPFeePercent(
)
)
} else {
percent = ONE_HUNDRED_PERCENT.subtract(
trade.route.pools.reduce<Percent>(
(currentFee: Percent, pool): Percent =>
currentFee.multiply(ONE_HUNDRED_PERCENT.subtract(new Fraction(pool.fee, 1_000_000))),
ONE_HUNDRED_PERCENT
//TODO(judo): validate this
percent = ZERO_PERCENT
for (const swap of trade.swaps) {
const { numerator, denominator } = swap.inputAmount.divide(trade.inputAmount)
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)
......
......@@ -3422,6 +3422,17 @@
lodash "^4.17.15"
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":
version "12.0.0"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.0.0.tgz#9aeb2264521522ab9b68f519eaf15136148f164a"
......@@ -3948,6 +3959,13 @@
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
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":
version "17.0.9"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
......@@ -3982,6 +4000,13 @@
"@types/history" "*"
"@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":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz#b3187dae1dfc4c15880c9cfc5b45f2719ea6ebd4"
......@@ -4005,6 +4030,15 @@
"@types/scheduler" "*"
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":
version "4.0.9"
resolved "https://registry.yarnpkg.com/@types/rebass/-/rebass-4.0.9.tgz#526b6e2ceab2b7e76e45cbeed264e250118b8036"
......@@ -15984,6 +16018,13 @@ react-dom@^17.0.1:
object-assign "^4.1.1"
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:
version "6.0.9"
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