Commit ca60caf6 authored by eddie's avatar eddie Committed by GitHub

feat: use Swap Component on TDP (#6332)

* test: swap flow cypress tests

* fix: use default parameter

* feat: use Swap Component on TDP

* feat: auto nav for TDP tokens

* chore: merge

* chore: merge

* chore: merge

* chore: merge

* fix: remove extra inputCurrency URL parsing logic

* fix: undo last change

* fix: pass expected chain id to swap component

* fix: search for default tokens on unconnected networks if needed

* test: e2e test for l2 token

* fix: delete irrelevant tests

* fix: address comments

* fix: lint error

* test: update TDP e2e tests

* fix: use pageChainId for filter

* fix: rename chainId

* fix: typecheck

* fix: chainId bug

* fix: chainId required fixes

* fix: bad merge in e2e test

* fix: remove unused test util

* fix: remove unnecessary variable

* fix: token defaults

* fix: address comments

* fix: address comments and fix tests

* fix: e2e test formatting, remove Maybe<>

* fix: remove unused variable

* fix: use feature flag for swap component on TDP

* fix: back button
parent 252acef1
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
import { getClassContainsSelector, getTestSelector } from '../utils'
const UNI_GOERLI = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
const WETH_GOERLI = '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6'
describe('swap widget integration tests', () => {
const verifyInputToken = (inputText: string) => {
cy.get(getClassContainsSelector('TokenButtonRow')).first().contains(inputText)
}
const verifyOutputToken = (outputText: string) => {
cy.get(getClassContainsSelector('TokenButtonRow')).last().contains(outputText)
}
const selectOutputAndSwitch = (outputText: string) => {
// open token selector...
cy.contains('Select token').click()
// select token...
cy.contains(outputText).click({ force: true })
cy.get('body')
.then(($body) => {
if ($body.find(getTestSelector('TokenSafetyWrapper')).length) {
return 'I understand'
}
return 'You pay' // Just click on a random element as a no-op
})
.then((selector) => {
cy.contains(selector).click()
})
// token selector should close...
cy.contains('Search name or paste address').should('not.exist')
cy.get(getClassContainsSelector('ReverseButton')).first().click()
}
describe('widget on swap page', () => {
beforeEach(() => {
cy.viewport(1200, 800)
})
it('should have the correct default input/output and token selection should work', () => {
cy.visit('/swap', { featureFlags: [FeatureFlag.swapWidget] }).then(() => {
cy.wait('@eth_blockNumber')
verifyInputToken('ETH')
verifyOutputToken('Select token')
selectOutputAndSwitch('WETH')
verifyInputToken('WETH')
verifyOutputToken('ETH')
})
})
it('should have the correct default input from URL params ', () => {
cy.visit(`/swap?inputCurrency=${WETH_GOERLI}`, {
featureFlags: [FeatureFlag.swapWidget],
}).then(() => {
cy.wait('@eth_blockNumber')
})
verifyInputToken('WETH')
verifyOutputToken('Select token')
selectOutputAndSwitch('Ether')
verifyInputToken('ETH')
verifyOutputToken('WETH')
})
it('should have the correct default output from URL params ', () => {
cy.visit(`/swap?outputCurrency=${WETH_GOERLI}`, {
featureFlags: [FeatureFlag.swapWidget],
}).then(() => {
cy.wait('@eth_blockNumber')
})
verifyInputToken('Select token')
verifyOutputToken('WETH')
cy.get(getClassContainsSelector('ReverseButton')).first().click()
verifyInputToken('WETH')
verifyOutputToken('Select token')
selectOutputAndSwitch('Ether')
verifyInputToken('ETH')
verifyOutputToken('WETH')
})
})
describe('widget on Token Detail Page', () => {
beforeEach(() => {
cy.viewport(1200, 800)
cy.visit(`/tokens/ethereum/${UNI_GOERLI}`, { featureFlags: [FeatureFlag.swapWidget] }).then(() => {
cy.wait('@eth_blockNumber')
})
})
it('should have the expected output for a tokens detail page', () => {
verifyOutputToken('UNI')
cy.contains('Connect to Ethereum').should('exist')
})
})
})
import { USDC_MAINNET } from '../../src/constants/tokens'
import { WETH9 } from '@uniswap/sdk-core'
import { UNI as UNI_MAINNET, USDC_MAINNET } from '../../src/constants/tokens'
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
import { WETH_GOERLI } from '../fixtures/constants'
import { getTestSelector } from '../utils'
......@@ -19,9 +22,9 @@ describe('Swap', () => {
}
}
const selectOutput = (tokenSymbol: string) => {
const selectToken = (tokenSymbol: string, field: 'input' | 'output') => {
// open token selector...
cy.contains('Select token').click()
cy.get(`#swap-currency-${field} .open-currency-select-button`).click()
// select token...
cy.contains(tokenSymbol).click()
......@@ -43,155 +46,204 @@ describe('Swap', () => {
cy.contains('Search name or paste address').should('not.exist')
}
before(() => {
cy.visit('/swap', { ethereum: 'hardhat' })
})
describe('Swap on main page', () => {
before(() => {
cy.visit('/swap', { ethereum: 'hardhat' })
})
it('starts with ETH selected by default', () => {
verifyAmount('input', '')
verifyToken('input', 'ETH')
verifyAmount('output', null)
verifyToken('output', null)
})
it('starts with ETH selected by default', () => {
verifyAmount('input', '')
verifyToken('input', 'ETH')
verifyAmount('output', null)
verifyToken('output', null)
})
it('can enter an amount into input', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.001').should('have.value', '0.001')
})
it('can enter an amount into input', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.001').should('have.value', '0.001')
})
it('zero swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0').should('have.value', '0.0')
})
it('zero swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0').should('have.value', '0.0')
})
it('invalid swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('\\').should('have.value', '')
})
it('invalid swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('\\').should('have.value', '')
})
it('can enter an amount into output', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.001').should('have.value', '0.001')
})
it('can enter an amount into output', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.001').should('have.value', '0.001')
})
it('zero output amount', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.0').should('have.value', '0.0')
})
it('zero output amount', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.0').should('have.value', '0.0')
})
it('can swap ETH for USDC', () => {
const TOKEN_ADDRESS = USDC_MAINNET.address
const BALANCE_INCREMENT = 1
cy.hardhat().then((hardhat) => {
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.then((initialBalance) => {
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get(getTestSelector('token-search-input')).clear().type(TOKEN_ADDRESS)
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type(BALANCE_INCREMENT.toString())
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
cy.then(() => hardhat.provider.send('hardhat_mine', ['0x1', '0xc'])).then(() => {
// ui check
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
'have.text',
`Balance: ${initialBalance + BALANCE_INCREMENT}`
)
// chain state check
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.should('eq', initialBalance + BALANCE_INCREMENT)
})
})
it('should have the correct default input/output and token selection should work', () => {
cy.visit('/swap')
verifyToken('input', 'ETH')
verifyToken('output', null)
selectToken('WETH', 'output')
cy.get(getTestSelector('swap-currency-button')).first().click()
verifyToken('input', 'WETH')
verifyToken('output', 'ETH')
})
})
it('should have the correct default input/output and token selection should work', () => {
cy.visit('/swap')
verifyToken('input', 'ETH')
verifyToken('output', null)
it('should have the correct default input from URL params ', () => {
cy.visit(`/swap?inputCurrency=${WETH_GOERLI}`)
selectOutput('WETH')
cy.get(getTestSelector('swap-currency-button')).first().click()
verifyToken('input', 'WETH')
verifyToken('output', null)
verifyToken('input', 'WETH')
verifyToken('output', 'ETH')
})
selectToken('Ether', 'output')
cy.get(getTestSelector('swap-currency-button')).first().click()
it('should have the correct default input from URL params ', () => {
cy.visit(`/swap?inputCurrency=${WETH_GOERLI}`)
verifyToken('input', 'ETH')
verifyToken('output', 'WETH')
})
verifyToken('input', 'WETH')
verifyToken('output', null)
it('should have the correct default output from URL params ', () => {
cy.visit(`/swap?outputCurrency=${WETH_GOERLI}`)
selectOutput('Ether')
cy.get(getTestSelector('swap-currency-button')).first().click()
verifyToken('input', null)
verifyToken('output', 'WETH')
verifyToken('input', 'ETH')
verifyToken('output', 'WETH')
})
cy.get(getTestSelector('swap-currency-button')).first().click()
verifyToken('input', 'WETH')
verifyToken('output', null)
it('should have the correct default output from URL params ', () => {
cy.visit(`/swap?outputCurrency=${WETH_GOERLI}`)
selectToken('Ether', 'output')
cy.get(getTestSelector('swap-currency-button')).first().click()
verifyToken('input', null)
verifyToken('output', 'WETH')
verifyToken('input', 'ETH')
verifyToken('output', 'WETH')
})
cy.get(getTestSelector('swap-currency-button')).first().click()
verifyToken('input', 'WETH')
verifyToken('output', null)
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
cy.visit('/swap')
selectToken('WETH', 'output')
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01')
})
selectOutput('Ether')
cy.get(getTestSelector('swap-currency-button')).first().click()
it('Opens and closes the settings menu', () => {
cy.visit('/swap')
cy.contains('Settings').should('not.exist')
cy.get(getTestSelector('swap-settings-button')).click()
cy.contains('Slippage tolerance').should('exist')
cy.contains('Transaction deadline').should('exist')
cy.contains('Auto Router API').should('exist')
cy.contains('Expert Mode').should('exist')
cy.get(getTestSelector('swap-settings-button')).click()
cy.contains('Settings').should('not.exist')
})
verifyToken('input', 'ETH')
verifyToken('output', 'WETH')
})
it('inputs reset when navigating between pages', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
cy.visit('/pool')
cy.visit('/swap')
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
})
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
cy.visit('/swap')
selectOutput('WETH')
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01')
it('can swap ETH for USDC', () => {
cy.visit('/swap', { ethereum: 'hardhat' })
const TOKEN_ADDRESS = USDC_MAINNET.address
const BALANCE_INCREMENT = 1
cy.hardhat().then((hardhat) => {
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.then((initialBalance) => {
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get(getTestSelector('token-search-input')).clear().type(TOKEN_ADDRESS)
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type(BALANCE_INCREMENT.toString())
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
cy.then(() => hardhat.provider.send('hardhat_mine', ['0x1', '0xc'])).then(() => {
// ui check
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
'have.text',
`Balance: ${initialBalance + BALANCE_INCREMENT}`
)
// chain state check
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.should('eq', initialBalance + BALANCE_INCREMENT)
})
})
})
})
})
it('should render and dismiss the wallet rejection modal', () => {
cy.visit('/swap', { ethereum: 'hardhat' })
.hardhat()
.then((hardhat) => {
cy.stub(hardhat.wallet, 'sendTransaction').rejects(new Error('user cancelled'))
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get(getTestSelector('token-search-input')).clear().type(USDC_MAINNET.address)
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type('1')
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.contains('Transaction rejected').should('exist')
cy.contains('Dismiss').click()
cy.contains('Transaction rejected').should('not.exist')
describe('Swap on Token Detail Page', () => {
beforeEach(() => {
// On mobile widths, we just link back to /swap instead of rendering the swap component.
cy.viewport(1200, 800)
cy.visit(`/tokens/ethereum/${UNI_MAINNET[1].address}`, {
ethereum: 'hardhat',
featureFlags: [FeatureFlag.removeWidget],
}).then(() => {
cy.wait('@eth_blockNumber')
cy.scrollTo('top')
})
})
})
it('Opens and closes the settings menu', () => {
cy.visit('/swap')
cy.contains('Settings').should('not.exist')
cy.get(getTestSelector('swap-settings-button')).click()
cy.contains('Slippage tolerance').should('exist')
cy.contains('Transaction deadline').should('exist')
cy.contains('Auto Router API').should('exist')
cy.contains('Expert Mode').should('exist')
cy.get(getTestSelector('swap-settings-button')).click()
cy.contains('Settings').should('not.exist')
})
it('should have the expected output for a tokens detail page', () => {
verifyAmount('input', '')
verifyToken('input', null)
verifyAmount('output', null)
verifyToken('output', 'UNI')
})
it('should automatically navigate to the new TDP', () => {
selectToken('WETH', 'output')
cy.url().should('include', `${WETH9[1].address}`)
cy.url().should('not.include', `${UNI_MAINNET[1].address}`)
})
it('should not share swap state with the main swap page', () => {
verifyToken('output', 'UNI')
selectToken('WETH', 'input')
cy.visit('/swap', { featureFlags: [FeatureFlag.removeWidget] })
cy.contains('UNI').should('not.exist')
cy.contains('WETH').should('not.exist')
})
it('can enter an amount into input', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.001').should('have.value', '0.001')
})
it('zero swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0').should('have.value', '0.0')
})
it('invalid swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('\\').should('have.value', '')
})
it('can enter an amount into output', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.001').should('have.value', '0.001')
})
it('inputs reset when navigating between pages', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
cy.visit('/pool')
cy.visit('/swap')
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
it('zero output amount', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.0').should('have.value', '0.0')
})
it('should show a L2 token even if the user is connected to a different network', () => {
cy.visit('/tokens', { ethereum: 'hardhat', featureFlags: [FeatureFlag.removeWidget] })
cy.get(getTestSelector('tokens-network-filter-selected')).click()
cy.get(getTestSelector('tokens-network-filter-option-arbitrum')).click()
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Arbitrum')
cy.get(getTestSelector('token-table-row-ARB')).click()
verifyToken('output', 'ARB')
cy.contains('Connect to Arbitrum').should('exist')
})
})
})
import { getClassContainsSelector, getTestSelector } from '../utils'
import { getTestSelector } from '../utils'
const UNI_ADDRESS = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
describe('Token details', () => {
before(() => {
cy.visit('/')
beforeEach(() => {
cy.viewport(1440, 900)
})
it('Uniswap token should have all information populated', () => {
......@@ -40,9 +40,6 @@ describe('Token details', () => {
// Contract address should be displayed
cy.contains(UNI_ADDRESS).should('exist')
// Swap widget should have this token pre-selected as the “destination” token
cy.get(getTestSelector('token-select')).should('include.text', 'UNI')
})
it('token with warning and low trading volume should have all information populated', () => {
......@@ -81,36 +78,9 @@ describe('Token details', () => {
// Contract address should be displayed
cy.contains('0xa71d0588EAf47f12B13cF8eC750430d21DF04974').should('exist')
// Swap widget should have this token pre-selected as the “destination” token
cy.get(getTestSelector('token-select')).should('include.text', 'QOM')
// Warning label should show if relevant ([spec](https://www.notion.so/3f7fce6f93694be08a94a6984d50298e))
cy.get('[data-cy="token-safety-message"]')
.should('include.text', 'Warning')
.and('include.text', "This token isn't traded on leading U.S. centralized exchanges")
})
describe('Swap on Token Detail Page', () => {
const verifyOutputToken = (outputText: string) => {
cy.get(getClassContainsSelector('TokenButtonRow')).last().contains(outputText)
}
beforeEach(() => {
// On mobile widths, we just link back to /swap instead of rendering the swap component.
cy.viewport(1200, 800)
cy.visit(`/tokens/goerli/${UNI_ADDRESS}`).then(() => {
cy.wait('@eth_blockNumber')
})
})
it('should have the expected output for a tokens detail page', () => {
verifyOutputToken('UNI')
})
it('should not share swap state with the main swap page', () => {
verifyOutputToken('UNI')
cy.visit('/swap')
cy.contains('UNI').should('not.exist')
})
})
})
export const getTestSelector = (selectorId: string) => `[data-testid=${selectorId}]`
export const getTestSelectorStartsWith = (selectorId: string) => `[data-testid^=${selectorId}]`
export const getClassContainsSelector = (selectorId: string) => `[class*=${selectorId}]`
......@@ -204,6 +204,7 @@ interface SwapCurrencyInputPanelProps {
renderBalance?: (amount: CurrencyAmount<Currency>) => ReactNode
locked?: boolean
loading?: boolean
disabled?: boolean
}
export default function SwapCurrencyInputPanel({
......@@ -226,6 +227,7 @@ export default function SwapCurrencyInputPanel({
hideInput = false,
locked = false,
loading = false,
disabled = false,
...rest
}: SwapCurrencyInputPanelProps) {
const [modalOpen, setModalOpen] = useState(false)
......@@ -258,13 +260,13 @@ export default function SwapCurrencyInputPanel({
className="token-amount-input"
value={value}
onUserInput={onUserInput}
disabled={!chainAllowed}
disabled={!chainAllowed || disabled}
$loading={loading}
/>
)}
<CurrencySelect
disabled={!chainAllowed}
disabled={!chainAllowed || disabled}
visible={currency !== undefined}
selected={!!currency}
hideInput={hideInput}
......
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { DetailsV2Variant, useDetailsV2Flag } from 'featureFlags/flags/nftDetails'
import { useWidgetRemovalFlag, WidgetRemovalVariant } from 'featureFlags/flags/removeWidgetTdp'
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useUpdateAtom } from 'jotai/utils'
......@@ -214,6 +215,12 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.detailsV2}
label="Use the new details page for nfts"
/>
<FeatureFlagOption
variant={WidgetRemovalVariant}
value={useWidgetRemovalFlag()}
featureFlag={FeatureFlag.removeWidget}
label="Swap Component on TDP"
/>
<FeatureFlagGroup name="Debug">
<FeatureFlagOption
variant={TraceJsonRpcVariant}
......
......@@ -85,7 +85,7 @@ export function CurrencySearch({
}
}, [isAddressSearch])
const defaultTokens = useDefaultActiveTokens()
const defaultTokens = useDefaultActiveTokens(chainId)
const filteredTokens: Token[] = useMemo(() => {
return Object.values(defaultTokens).filter(getTokenFilter(debouncedQuery))
}, [defaultTokens, debouncedQuery])
......@@ -123,7 +123,7 @@ export function CurrencySearch({
const filteredSortedTokens = useSortTokensByQuery(debouncedQuery, sortedTokens)
const native = useNativeCurrency()
const native = useNativeCurrency(chainId)
const wrapped = native.wrapped
const searchCurrencies: Currency[] = useMemo(() => {
......
import { Trans } from '@lingui/macro'
import { Trace } from '@uniswap/analytics'
import { InterfacePageName } from '@uniswap/analytics-events'
import { Currency, Field } from '@uniswap/widgets'
import { Currency } from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { AboutSection } from 'components/Tokens/TokenDetails/About'
......@@ -26,6 +26,7 @@ import Widget from 'components/Widget'
import { SwapTokens } from 'components/Widget/inputs'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety'
import { useWidgetRemovalEnabled } from 'featureFlags/flags/removeWidgetTdp'
import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token'
import { QueryToken } from 'graphql/data/Token'
......@@ -34,11 +35,15 @@ import { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency'
import { getTokenAddress } from 'lib/utils/analytics'
import { Swap } from 'pages/Swap'
import { useCallback, useMemo, useState, useTransition } from 'react'
import { ArrowLeft } from 'react-feather'
import { useNavigate } from 'react-router-dom'
import { Field } from 'state/swap/actions'
import { SwapState } from 'state/swap/reducer'
import styled from 'styled-components/macro'
import { isAddress } from 'utils'
import { addressesAreEquivalent } from 'utils/addressesAreEquivalent'
import { OnChangeTimePeriod } from './ChartSection'
import InvalidTokenDetails from './InvalidTokenDetails'
......@@ -111,6 +116,7 @@ export default function TokenDetails({
[urlAddress]
)
const { chainId: connectedChainId } = useWeb3React()
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const tokenQueryData = tokenQuery.token
......@@ -124,11 +130,12 @@ export default function TokenDetails({
)
const { token: detailedToken, didFetchFromChain } = useRelevantToken(address, pageChainId, tokenQueryData)
const { token: inputToken } = useRelevantToken(inputTokenAddress, pageChainId, undefined)
const { token: widgetInputToken } = useRelevantToken(inputTokenAddress, pageChainId, undefined)
const tokenWarning = address ? checkWarning(address) : null
const isBlockedToken = tokenWarning?.canProceed === false
const navigate = useNavigate()
const widgetRemovalEnabled = useWidgetRemovalEnabled()
// Wrapping navigate in a transition prevents Suspense from unnecessarily showing fallbacks again.
const [isPending, startTokenTransition] = useTransition()
......@@ -145,7 +152,6 @@ export default function TokenDetails({
[address, crossChainMap, didFetchFromChain, navigate, detailedToken?.isNative]
)
useOnGlobalChainSwitch(navigateToTokenForChain)
const navigateToWidgetSelectedToken = useCallback(
(tokens: SwapTokens) => {
const newDefaultToken = tokens[Field.OUTPUT] ?? tokens.default
......@@ -163,11 +169,39 @@ export default function TokenDetails({
[chain, navigate]
)
const handleCurrencyChange = useCallback(
(tokens: Pick<SwapState, Field.INPUT | Field.OUTPUT>) => {
if (
addressesAreEquivalent(tokens[Field.INPUT]?.currencyId, address) ||
addressesAreEquivalent(tokens[Field.OUTPUT]?.currencyId, address)
) {
return
}
const newDefaultTokenID = tokens[Field.OUTPUT]?.currencyId ?? tokens[Field.INPUT]?.currencyId
startTokenTransition(() =>
navigate(
getTokenDetailsURL({
// The function falls back to "NATIVE" if the address is null
address: newDefaultTokenID === 'ETH' ? null : newDefaultTokenID,
chain,
inputAddress:
// If only one token was selected before we navigate, then it was the default token and it's being replaced.
// On the new page, the *new* default token becomes the output, and we don't have another option to set as the input token.
tokens[Field.INPUT] && tokens[Field.INPUT]?.currencyId !== newDefaultTokenID
? tokens[Field.INPUT]?.currencyId
: null,
})
)
)
},
[address, chain, navigate]
)
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
const [openTokenSafetyModal, setOpenTokenSafetyModal] = useState(false)
// Show token safety modal if Swap-reviewing a warning token, at all times if the current token is blocked
const shouldShowSpeedbump = !useIsUserAddedTokenOnChain(address, pageChainId) && tokenWarning !== null
const onReviewSwapClick = useCallback(
() => new Promise<boolean>((resolve) => (shouldShowSpeedbump ? setContinueSwap({ resolve }) : resolve(true))),
......@@ -234,14 +268,26 @@ export default function TokenDetails({
<RightPanel onClick={() => isBlockedToken && setOpenTokenSafetyModal(true)}>
<div style={{ pointerEvents: isBlockedToken ? 'none' : 'auto' }}>
<Widget
defaultTokens={{
[Field.INPUT]: inputToken ?? undefined,
default: detailedToken ?? undefined,
}}
onDefaultTokenChange={navigateToWidgetSelectedToken}
onReviewSwapClick={onReviewSwapClick}
/>
{widgetRemovalEnabled ? (
<Swap
chainId={pageChainId}
prefilledState={{
[Field.INPUT]: { currencyId: inputTokenAddress },
[Field.OUTPUT]: { currencyId: address === NATIVE_CHAIN_ID ? 'ETH' : address },
}}
onCurrencyChange={handleCurrencyChange}
disableTokenInputs={pageChainId !== connectedChainId}
/>
) : (
<Widget
defaultTokens={{
[Field.INPUT]: widgetInputToken ?? undefined,
default: detailedToken ?? undefined,
}}
onDefaultTokenChange={navigateToWidgetSelectedToken}
onReviewSwapClick={onReviewSwapClick}
/>
)}
</div>
{tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />}
{detailedToken && <BalanceSummary token={detailedToken} />}
......
......@@ -51,6 +51,8 @@ interface WidgetProps {
onReviewSwapClick?: OnReviewSwapClick
}
// TODO: Remove this component once the TDP is fully migrated to the swap component.
// eslint-disable-next-line import/no-unused-modules
export default function Widget({
defaultTokens,
width = DEFAULT_WIDGET_WIDTH,
......
......@@ -53,7 +53,7 @@ export function AdvancedSwapDetails({
}: AdvancedSwapDetailsProps) {
const theme = useTheme()
const { chainId } = useWeb3React()
const nativeCurrency = useNativeCurrency()
const nativeCurrency = useNativeCurrency(chainId)
const { expectedOutputAmount, priceImpact } = useMemo(() => {
return {
......
......@@ -515,6 +515,13 @@ export function nativeOnChain(chainId: number): NativeCurrency | Token {
return (cachedNativeCurrency[chainId] = nativeCurrency)
}
export function getSwapCurrencyId(currency: Currency): string {
if (currency.isToken) {
return currency.address
}
return NATIVE_CHAIN_ID
}
export const TOKEN_SHORTHANDS: { [shorthand: string]: { [chainId in SupportedChainId]?: string } } = {
USDC: {
[SupportedChainId.MAINNET]: USDC_MAINNET.address,
......
......@@ -8,4 +8,5 @@ export enum FeatureFlag {
swapWidget = 'swap_widget_replacement_enabled',
statsigDummy = 'web_dummy_gate_amplitude_id',
detailsV2 = 'details_v2',
removeWidget = 'remove_widget_tdp',
}
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useWidgetRemovalFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.removeWidget, BaseVariant.Control)
}
export function useWidgetRemovalEnabled(): boolean {
return useWidgetRemovalFlag() === BaseVariant.Enabled
}
export { BaseVariant as WidgetRemovalVariant }
......@@ -13,9 +13,10 @@ import { WrappedTokenInfo } from '../state/lists/wrappedTokenInfo'
import { useUserAddedTokens, useUserAddedTokensOnChain } from '../state/user/hooks'
import { TokenAddressMap, useUnsupportedTokenList } from './../state/lists/hooks'
type Maybe<T> = T | null | undefined
// reduce token map into standard address <-> Token mapping, optionally include user added tokens
function useTokensFromMap(tokenMap: TokenAddressMap): { [address: string]: Token } {
const { chainId } = useWeb3React()
function useTokensFromMap(tokenMap: TokenAddressMap, chainId: Maybe<SupportedChainId>): { [address: string]: Token } {
return useMemo(() => {
if (!chainId) return {}
......@@ -32,9 +33,9 @@ export function useAllTokensMultichain(): TokenAddressMap {
}
// Returns all tokens from the default list + user added tokens
export function useDefaultActiveTokens(): { [address: string]: Token } {
export function useDefaultActiveTokens(chainId: Maybe<SupportedChainId>): { [address: string]: Token } {
const defaultListTokens = useCombinedActiveList()
const tokensFromMap = useTokensFromMap(defaultListTokens)
const tokensFromMap = useTokensFromMap(defaultListTokens, chainId)
const userAddedTokens = useUserAddedTokens()
return useMemo(() => {
return (
......@@ -66,7 +67,7 @@ export function useUnsupportedTokens(): { [address: string]: Token } {
const { chainId } = useWeb3React()
const listsByUrl = useAllLists()
const unsupportedTokensMap = useUnsupportedTokenList()
const unsupportedTokens = useTokensFromMap(unsupportedTokensMap)
const unsupportedTokens = useTokensFromMap(unsupportedTokensMap, chainId)
// checks the default L2 lists to see if `bridgeInfo` has an L1 address value that is unsupported
const l2InferredBlockedTokens: typeof unsupportedTokens = useMemo(() => {
......@@ -110,7 +111,7 @@ export function useSearchInactiveTokenLists(search: string | undefined, minResul
const lists = useAllLists()
const inactiveUrls = DEFAULT_INACTIVE_LIST_URLS
const { chainId } = useWeb3React()
const activeTokens = useDefaultActiveTokens()
const activeTokens = useDefaultActiveTokens(chainId)
return useMemo(() => {
if (!search || search.trim().length === 0) return []
const tokenFilter = getTokenFilter(search)
......@@ -167,11 +168,13 @@ export function useIsUserAddedTokenOnChain(
// null if loading or null was passed
// otherwise returns the token
export function useToken(tokenAddress?: string | null): Token | null | undefined {
const tokens = useDefaultActiveTokens()
const { chainId } = useWeb3React()
const tokens = useDefaultActiveTokens(chainId)
return useTokenFromMapOrNetwork(tokens, tokenAddress)
}
export function useCurrency(currencyId?: string | null): Currency | null | undefined {
const tokens = useDefaultActiveTokens()
return useCurrencyFromMap(tokens, currencyId)
export function useCurrency(currencyId: Maybe<string>, chainId?: SupportedChainId): Currency | null | undefined {
const { chainId: connectedChainId } = useWeb3React()
const tokens = useDefaultActiveTokens(chainId ?? connectedChainId)
return useCurrencyFromMap(tokens, chainId ?? connectedChainId, currencyId)
}
......@@ -81,7 +81,7 @@ export default function useAutoSlippageTolerance(
const nativeGasPrice = useGasPrice()
const gasEstimate = guesstimateGas(trade)
const nativeCurrency = useNativeCurrency()
const nativeCurrency = useNativeCurrency(chainId)
const nativeCurrencyPrice = useStablecoinPrice((trade && nativeCurrency) ?? undefined)
return useMemo(() => {
......
import { Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { useMemo } from 'react'
import { PositionDetails } from 'types/position'
import { hasURL } from 'utils/urlChecks'
......@@ -23,7 +24,8 @@ function getUniqueAddressesFromPositions(positions: PositionDetails[]): string[]
* The hope is that this approach removes the cheapest version of the attack without punishing non-malicious url symbols
*/
export function useFilterPossiblyMaliciousPositions(positions: PositionDetails[]): PositionDetails[] {
const activeTokensList = useDefaultActiveTokens()
const { chainId } = useWeb3React()
const activeTokensList = useDefaultActiveTokens(chainId)
const nonListPositionTokenAddresses = useMemo(
() => getUniqueAddressesFromPositions(positions).filter((address) => !activeTokensList[address]),
......
......@@ -31,7 +31,8 @@ enum WrapInputError {
}
export function WrapErrorText({ wrapInputError }: { wrapInputError: WrapInputError }) {
const native = useNativeCurrency()
const { chainId } = useWeb3React()
const native = useNativeCurrency(chainId)
const wrapped = native?.wrapped
switch (wrapInputError) {
......
......@@ -2,7 +2,7 @@ import { arrayify } from '@ethersproject/bytes'
import { parseBytes32String } from '@ethersproject/strings'
import { Currency, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { isSupportedChain } from 'constants/chains'
import { isSupportedChain, SupportedChainId } from 'constants/chains'
import { useBytes32TokenContract, useTokenContract } from 'hooks/useContract'
import { NEVER_RELOAD, useSingleCallResult } from 'lib/hooks/multicall'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
......@@ -92,9 +92,12 @@ export function useTokenFromMapOrNetwork(tokens: TokenMap, tokenAddress?: string
* Returns null if currency is loading or null was passed.
* Returns undefined if currencyId is invalid or token does not exist.
*/
export function useCurrencyFromMap(tokens: TokenMap, currencyId?: string | null): Currency | null | undefined {
const nativeCurrency = useNativeCurrency()
const { chainId } = useWeb3React()
export function useCurrencyFromMap(
tokens: TokenMap,
chainId: SupportedChainId | undefined,
currencyId?: string | null
): Currency | null | undefined {
const nativeCurrency = useNativeCurrency(chainId)
const isNative = Boolean(nativeCurrency && currencyId?.toUpperCase() === 'ETH')
const shorthandMatchAddress = useMemo(() => {
const chain = supportedChainId(chainId)
......@@ -108,6 +111,5 @@ export function useCurrencyFromMap(tokens: TokenMap, currencyId?: string | null)
// this case so we use our builtin wrapped token instead of wrapped tokens on token lists
const wrappedNative = nativeCurrency?.wrapped
if (wrappedNative?.address?.toUpperCase() === currencyId?.toUpperCase()) return wrappedNative
return isNative ? nativeCurrency : token
}
import { NativeCurrency, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens'
import { useMemo } from 'react'
export default function useNativeCurrency(): NativeCurrency | Token {
const { chainId } = useWeb3React()
export default function useNativeCurrency(chainId: SupportedChainId | null | undefined): NativeCurrency | Token {
return useMemo(
() =>
chainId
......
......@@ -183,7 +183,7 @@ const EthValueWrapper = styled.span<{ totalEthListingValue: boolean }>`
export const ListPage = () => {
const { setProfilePageState: setSellPageState } = useProfilePageState()
const { provider } = useWeb3React()
const { provider, chainId } = useWeb3React()
const isMobile = useIsMobile()
const trace = useTrace({ modal: InterfaceModalName.NFT_LISTING })
const { setGlobalMarketplaces, sellAssets, issues } = useSellAsset(
......@@ -205,7 +205,7 @@ export const ListPage = () => {
)
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
const nativeCurrency = useNativeCurrency()
const nativeCurrency = useNativeCurrency(chainId)
const parsedAmount = tryParseCurrencyAmount(totalEthListingValue.toString(), nativeCurrency)
const usdcValue = useStablecoinValue(parsedAmount)
const usdcAmount = formatCurrencyAmount(usdcValue, NumberType.FiatTokenPrice)
......
......@@ -45,7 +45,7 @@ const ListModalWrapper = styled.div`
`
export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
const { provider } = useWeb3React()
const { provider, chainId } = useWeb3React()
const signer = provider?.getSigner()
const trace = useTrace({ modal: InterfaceModalName.NFT_LISTING })
const sellAssets = useSellAsset((state) => state.sellAssets)
......@@ -72,7 +72,7 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
(s) => (s === Section.APPROVE ? Section.SIGN : Section.APPROVE),
Section.APPROVE
)
const nativeCurrency = useNativeCurrency()
const nativeCurrency = useNativeCurrency(chainId)
const parsedAmount = tryParseCurrencyAmount(totalEthListingValue.toString(), nativeCurrency)
const usdcValue = useStablecoinValue(parsedAmount)
const usdcAmount = formatCurrencyAmount(usdcValue, NumberType.FiatTokenPrice)
......
import { Trans } from '@lingui/macro'
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
import { useWeb3React } from '@web3-react/core'
import Column from 'components/Column'
import { ScrollBarStyles } from 'components/Common'
import Row from 'components/Row'
......@@ -76,7 +77,8 @@ const TweetRow = styled(Row)`
export const SuccessScreen = ({ overlayClick }: { overlayClick: () => void }) => {
const theme = useTheme()
const sellAssets = useSellAsset((state) => state.sellAssets)
const nativeCurrency = useNativeCurrency()
const { chainId } = useWeb3React()
const nativeCurrency = useNativeCurrency(chainId)
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
const parsedAmount = tryParseCurrencyAmount(totalEthListingValue.toString(), nativeCurrency)
......
......@@ -403,7 +403,7 @@ function PositionPageContent() {
// flag for receiving WETH
const [receiveWETH, setReceiveWETH] = useState(false)
const nativeCurrency = useNativeCurrency()
const nativeCurrency = useNativeCurrency(chainId)
const nativeWrappedSymbol = nativeCurrency.wrapped.symbol
// construct Position from details returned
......
......@@ -74,7 +74,7 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
// flag for receiving WETH
const [receiveWETH, setReceiveWETH] = useState(false)
const nativeCurrency = useNativeCurrency()
const nativeCurrency = useNativeCurrency(chainId)
const nativeWrappedSymbol = nativeCurrency.wrapped.symbol
// burn state
......
......@@ -18,14 +18,13 @@ import Loader from 'components/Icons/LoadingSpinner'
import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert'
import PriceImpactWarning from 'components/swap/PriceImpactWarning'
import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import { MouseoverTooltip } from 'components/Tooltip'
import Widget from 'components/Widget'
import { isSupportedChain } from 'constants/chains'
import { useSwapWidgetEnabled } from 'featureFlags/flags/swapWidget'
import { getChainInfo } from 'constants/chainInfo'
import { isSupportedChain, SupportedChainId } from 'constants/chains'
import useENSAddress from 'hooks/useENSAddress'
import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
import usePrevious from 'hooks/usePrevious'
import { useSwapCallback } from 'hooks/useSwapCallback'
import { useUSDPrice } from 'hooks/useUSDPrice'
import JSBI from 'jsbi'
......@@ -40,6 +39,7 @@ import { TradeState } from 'state/routing/types'
import styled, { useTheme } from 'styled-components/macro'
import invariant from 'tiny-invariant'
import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers'
import { switchChain } from 'utils/switchChain'
import AddressInputPanel from '../../components/AddressInputPanel'
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
......@@ -52,13 +52,13 @@ import ConfirmSwapModal from '../../components/swap/ConfirmSwapModal'
import { ArrowWrapper, PageWrapper, SwapCallbackError, SwapWrapper } from '../../components/swap/styleds'
import SwapHeader from '../../components/swap/SwapHeader'
import { SwitchLocaleLink } from '../../components/SwitchLocaleLink'
import { TOKEN_SHORTHANDS } from '../../constants/tokens'
import { getSwapCurrencyId, TOKEN_SHORTHANDS } from '../../constants/tokens'
import { useCurrency, useDefaultActiveTokens } from '../../hooks/Tokens'
import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported'
import useWrapCallback, { WrapErrorText, WrapType } from '../../hooks/useWrapCallback'
import { Field } from '../../state/swap/actions'
import { Field, replaceSwapState } from '../../state/swap/actions'
import { useDefaultsFromURLSearch, useDerivedSwapInfo, useSwapActionHandlers } from '../../state/swap/hooks'
import swapReducer, { initialState as initialSwapState } from '../../state/swap/reducer'
import swapReducer, { initialState as initialSwapState, SwapState } from '../../state/swap/reducer'
import { useExpertModeManager } from '../../state/user/hooks'
import { LinkStyledButton, ThemedText } from '../../theme'
import { computeFiatValuePriceImpact } from '../../utils/computeFiatValuePriceImpact'
......@@ -143,19 +143,65 @@ function largerPercentValue(a?: Percent, b?: Percent) {
const TRADE_STRING = 'SwapRouter'
export default function Swap({ className }: { className?: string }) {
const navigate = useNavigate()
const { account, chainId } = useWeb3React()
export default function SwapPage({ className }: { className?: string }) {
const { chainId: connectedChainId } = useWeb3React()
const loadedUrlParams = useDefaultsFromURLSearch()
return (
<Trace page={InterfacePageName.SWAP_PAGE} shouldLogImpression>
<>
<PageWrapper>
<Swap
className={className}
chainId={connectedChainId}
prefilledState={{
[Field.INPUT]: { currencyId: loadedUrlParams?.[Field.INPUT]?.currencyId },
[Field.OUTPUT]: { currencyId: loadedUrlParams?.[Field.OUTPUT]?.currencyId },
}}
/>
<NetworkAlert />
</PageWrapper>
<SwitchLocaleLink />
</>
</Trace>
)
}
/**
* The swap component displays the swap interface, manages state for the swap, and triggers onchain swaps.
*
* In most cases, chainId should refer to the connected chain, i.e. `useWeb3React().chainId`.
* However if this component is being used in a context that displays information from a different, unconnected
* chain (e.g. the TDP), then chainId should refer to the unconnected chain.
*/
export function Swap({
className,
prefilledState = {},
chainId,
onCurrencyChange,
disableTokenInputs = false,
}: {
className?: string
prefilledState?: Partial<SwapState>
chainId: SupportedChainId | undefined
onCurrencyChange?: (selected: Pick<SwapState, Field.INPUT | Field.OUTPUT>) => void
disableTokenInputs?: boolean
}) {
const { account, chainId: connectedChainId, connector } = useWeb3React()
const [newSwapQuoteNeedsLogging, setNewSwapQuoteNeedsLogging] = useState(true)
const [fetchingSwapQuoteStartTime, setFetchingSwapQuoteStartTime] = useState<Date | undefined>()
const swapWidgetEnabled = useSwapWidgetEnabled()
// token warning stuff
const [loadedInputCurrency, loadedOutputCurrency] = [
useCurrency(loadedUrlParams?.[Field.INPUT]?.currencyId),
useCurrency(loadedUrlParams?.[Field.OUTPUT]?.currencyId),
]
const prefilledInputCurrency = useCurrency(prefilledState?.[Field.INPUT]?.currencyId)
const prefilledOutputCurrency = useCurrency(prefilledState?.[Field.OUTPUT]?.currencyId)
const [loadedInputCurrency, setLoadedInputCurrency] = useState(prefilledInputCurrency)
const [loadedOutputCurrency, setLoadedOutputCurrency] = useState(prefilledOutputCurrency)
useEffect(() => {
setLoadedInputCurrency(prefilledInputCurrency)
setLoadedOutputCurrency(prefilledOutputCurrency)
}, [prefilledInputCurrency, prefilledOutputCurrency])
const [dismissTokenWarning, setDismissTokenWarning] = useState<boolean>(false)
const urlLoadedTokens: Token[] = useMemo(
() => [loadedInputCurrency, loadedOutputCurrency]?.filter((c): c is Token => c?.isToken ?? false) ?? [],
......@@ -166,7 +212,7 @@ export default function Swap({ className }: { className?: string }) {
}, [])
// dismiss warning if all imported tokens are in active lists
const defaultTokens = useDefaultActiveTokens()
const defaultTokens = useDefaultActiveTokens(chainId)
const importTokensNotInDefault = useMemo(
() =>
urlLoadedTokens &&
......@@ -194,8 +240,33 @@ export default function Swap({ className }: { className?: string }) {
// for expert mode
const [isExpertMode] = useExpertModeManager()
// swap state
const [state, dispatch] = useReducer(swapReducer, initialSwapState)
const [state, dispatch] = useReducer(swapReducer, { ...initialSwapState, ...prefilledState })
const { typedValue, recipient, independentField } = state
const previousConnectedChainId = usePrevious(connectedChainId)
const previousPrefilledState = usePrevious(prefilledState)
useEffect(() => {
const combinedInitialState = { ...initialSwapState, ...prefilledState }
const chainChanged = previousConnectedChainId && previousConnectedChainId !== connectedChainId
const prefilledInputChanged =
previousPrefilledState &&
previousPrefilledState?.[Field.INPUT]?.currencyId !== prefilledState?.[Field.INPUT]?.currencyId
const prefilledOutputChanged =
previousPrefilledState &&
previousPrefilledState?.[Field.OUTPUT]?.currencyId !== prefilledState?.[Field.OUTPUT]?.currencyId
if (chainChanged || prefilledInputChanged || prefilledOutputChanged) {
dispatch(
replaceSwapState({
...initialSwapState,
...prefilledState,
field: combinedInitialState.independentField ?? Field.INPUT,
inputCurrencyId: combinedInitialState.INPUT.currencyId ?? undefined,
outputCurrencyId: combinedInitialState.OUTPUT.currencyId ?? undefined,
})
)
}
}, [connectedChainId, prefilledState, previousConnectedChainId, previousPrefilledState])
const {
trade: { state: tradeState, trade },
allowedSlippage,
......@@ -203,7 +274,7 @@ export default function Swap({ className }: { className?: string }) {
parsedAmount,
currencies,
inputError: swapInputError,
} = useDerivedSwapInfo(state)
} = useDerivedSwapInfo(state, chainId)
const {
wrapType,
......@@ -261,6 +332,9 @@ export default function Swap({ className }: { className?: string }) {
[onUserInput]
)
const navigate = useNavigate()
const swapIsUnsupported = useIsSwapUnsupported(currencies[Field.INPUT], currencies[Field.OUTPUT])
// reset if they close warning without tokens in params
const handleDismissTokenWarning = useCallback(() => {
setDismissTokenWarning(true)
......@@ -418,8 +492,14 @@ export default function Swap({ className }: { className?: string }) {
const handleInputSelect = useCallback(
(inputCurrency: Currency) => {
onCurrencySelection(Field.INPUT, inputCurrency)
onCurrencyChange?.({
[Field.INPUT]: {
currencyId: getSwapCurrencyId(inputCurrency),
},
[Field.OUTPUT]: state[Field.OUTPUT],
})
},
[onCurrencySelection]
[onCurrencyChange, onCurrencySelection, state]
)
const handleMaxInput = useCallback(() => {
......@@ -431,12 +511,18 @@ export default function Swap({ className }: { className?: string }) {
}, [maxInputAmount, onUserInput])
const handleOutputSelect = useCallback(
(outputCurrency: Currency) => onCurrencySelection(Field.OUTPUT, outputCurrency),
[onCurrencySelection]
(outputCurrency: Currency) => {
onCurrencySelection(Field.OUTPUT, outputCurrency)
onCurrencyChange?.({
[Field.INPUT]: state[Field.INPUT],
[Field.OUTPUT]: {
currencyId: getSwapCurrencyId(outputCurrency),
},
})
},
[onCurrencyChange, onCurrencySelection, state]
)
const swapIsUnsupported = useIsSwapUnsupported(currencies[Field.INPUT], currencies[Field.OUTPUT])
const priceImpactTooHigh = priceImpactSeverity > 3 && !isExpertMode
const showPriceImpactWarning = largerPriceImpact && priceImpactSeverity > 3
......@@ -477,269 +563,242 @@ export default function Swap({ className }: { className?: string }) {
)
return (
<Trace page={InterfacePageName.SWAP_PAGE} shouldLogImpression>
<>
<TokenSafetyModal
isOpen={importTokensNotInDefault.length > 0 && !dismissTokenWarning}
tokenAddress={importTokensNotInDefault[0]?.address}
secondTokenAddress={importTokensNotInDefault[1]?.address}
onContinue={handleConfirmTokenWarning}
onCancel={handleDismissTokenWarning}
showCancel={true}
/>
<PageWrapper>
{swapWidgetEnabled ? (
<Widget
defaultTokens={{
[Field.INPUT]: loadedInputCurrency ?? undefined,
[Field.OUTPUT]: loadedOutputCurrency ?? undefined,
}}
width="100%"
<SwapWrapper chainId={chainId} className={className} id="swap-page">
<TokenSafetyModal
isOpen={importTokensNotInDefault.length > 0 && !dismissTokenWarning}
tokenAddress={importTokensNotInDefault[0]?.address}
secondTokenAddress={importTokensNotInDefault[1]?.address}
onContinue={handleConfirmTokenWarning}
onCancel={handleDismissTokenWarning}
showCancel={true}
/>
<SwapHeader allowedSlippage={allowedSlippage} />
<ConfirmSwapModal
isOpen={showConfirm}
trade={trade}
originalTrade={tradeToConfirm}
onAcceptChanges={handleAcceptChanges}
attemptingTxn={attemptingTxn}
txHash={txHash}
recipient={recipient}
allowedSlippage={allowedSlippage}
onConfirm={handleSwap}
swapErrorMessage={swapErrorMessage}
onDismiss={handleConfirmDismiss}
swapQuoteReceivedDate={swapQuoteReceivedDate}
fiatValueInput={fiatValueTradeInput}
fiatValueOutput={fiatValueTradeOutput}
/>
<div style={{ display: 'relative' }}>
<SwapSection>
<Trace section={InterfaceSectionName.CURRENCY_INPUT_PANEL}>
<SwapCurrencyInputPanel
label={
independentField === Field.OUTPUT && !showWrap ? <Trans>From (at most)</Trans> : <Trans>From</Trans>
}
disabled={disableTokenInputs}
value={formattedAmounts[Field.INPUT]}
showMaxButton={showMaxButton}
currency={currencies[Field.INPUT] ?? null}
onUserInput={handleTypeInput}
onMax={handleMaxInput}
fiatValue={fiatValueInput}
onCurrencySelect={handleInputSelect}
otherCurrency={currencies[Field.OUTPUT]}
showCommonBases={true}
id={InterfaceSectionName.CURRENCY_INPUT_PANEL}
loading={independentField === Field.OUTPUT && routeIsSyncing}
/>
) : (
<SwapWrapper chainId={chainId} className={className} id="swap-page">
<SwapHeader allowedSlippage={allowedSlippage} />
<ConfirmSwapModal
isOpen={showConfirm}
</Trace>
</SwapSection>
<ArrowWrapper clickable={isSupportedChain(chainId)}>
<TraceEvent
events={[BrowserEvent.onClick]}
name={SwapEventName.SWAP_TOKENS_REVERSED}
element={InterfaceElementName.SWAP_TOKENS_REVERSE_ARROW_BUTTON}
>
<ArrowContainer
data-testid="swap-currency-button"
onClick={() => {
!disableTokenInputs && onSwitchTokens()
}}
color={theme.textPrimary}
>
<ArrowDown
size="16"
color={currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.textPrimary : theme.textTertiary}
/>
</ArrowContainer>
</TraceEvent>
</ArrowWrapper>
</div>
<AutoColumn gap="md">
<div>
<OutputSwapSection showDetailsDropdown={showDetailsDropdown}>
<Trace section={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}>
<SwapCurrencyInputPanel
value={formattedAmounts[Field.OUTPUT]}
disabled={disableTokenInputs}
onUserInput={handleTypeOutput}
label={independentField === Field.INPUT && !showWrap ? <Trans>To (at least)</Trans> : <Trans>To</Trans>}
showMaxButton={false}
hideBalance={false}
fiatValue={fiatValueOutput}
priceImpact={stablecoinPriceImpact}
currency={currencies[Field.OUTPUT] ?? null}
onCurrencySelect={handleOutputSelect}
otherCurrency={currencies[Field.INPUT]}
showCommonBases={true}
id={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}
loading={independentField === Field.INPUT && routeIsSyncing}
/>
</Trace>
{recipient !== null && !showWrap ? (
<>
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
<ArrowWrapper clickable={false}>
<ArrowDown size="16" color={theme.textSecondary} />
</ArrowWrapper>
<LinkStyledButton id="remove-recipient-button" onClick={() => onChangeRecipient(null)}>
<Trans>- Remove recipient</Trans>
</LinkStyledButton>
</AutoRow>
<AddressInputPanel id="recipient" value={recipient} onChange={onChangeRecipient} />
</>
) : null}
</OutputSwapSection>
{showDetailsDropdown && (
<DetailsSwapSection>
<SwapDetailsDropdown
trade={trade}
originalTrade={tradeToConfirm}
onAcceptChanges={handleAcceptChanges}
attemptingTxn={attemptingTxn}
txHash={txHash}
recipient={recipient}
syncing={routeIsSyncing}
loading={routeIsLoading}
allowedSlippage={allowedSlippage}
onConfirm={handleSwap}
swapErrorMessage={swapErrorMessage}
onDismiss={handleConfirmDismiss}
swapQuoteReceivedDate={swapQuoteReceivedDate}
fiatValueInput={fiatValueTradeInput}
fiatValueOutput={fiatValueTradeOutput}
/>
<div style={{ display: 'relative' }}>
<SwapSection>
<Trace section={InterfaceSectionName.CURRENCY_INPUT_PANEL}>
<SwapCurrencyInputPanel
label={
independentField === Field.OUTPUT && !showWrap ? (
<Trans>From (at most)</Trans>
) : (
<Trans>From</Trans>
)
}
value={formattedAmounts[Field.INPUT]}
showMaxButton={showMaxButton}
currency={currencies[Field.INPUT] ?? null}
onUserInput={handleTypeInput}
onMax={handleMaxInput}
fiatValue={fiatValueInput}
onCurrencySelect={handleInputSelect}
otherCurrency={currencies[Field.OUTPUT]}
showCommonBases={true}
id={InterfaceSectionName.CURRENCY_INPUT_PANEL}
loading={independentField === Field.OUTPUT && routeIsSyncing}
/>
</Trace>
</SwapSection>
<ArrowWrapper clickable={isSupportedChain(chainId)}>
<TraceEvent
events={[BrowserEvent.onClick]}
name={SwapEventName.SWAP_TOKENS_REVERSED}
element={InterfaceElementName.SWAP_TOKENS_REVERSE_ARROW_BUTTON}
>
<ArrowContainer
data-testid="swap-currency-button"
onClick={() => {
onSwitchTokens()
}}
color={theme.textPrimary}
>
<ArrowDown
size="16"
color={
currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.textPrimary : theme.textTertiary
}
/>
</ArrowContainer>
</TraceEvent>
</ArrowWrapper>
</div>
<AutoColumn gap="md">
<div>
<OutputSwapSection showDetailsDropdown={showDetailsDropdown}>
<Trace section={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}>
<SwapCurrencyInputPanel
value={formattedAmounts[Field.OUTPUT]}
onUserInput={handleTypeOutput}
label={
independentField === Field.INPUT && !showWrap ? (
<Trans>To (at least)</Trans>
) : (
<Trans>To</Trans>
)
}
showMaxButton={false}
hideBalance={false}
fiatValue={fiatValueOutput}
priceImpact={stablecoinPriceImpact}
currency={currencies[Field.OUTPUT] ?? null}
onCurrencySelect={handleOutputSelect}
otherCurrency={currencies[Field.INPUT]}
showCommonBases={true}
id={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}
loading={independentField === Field.INPUT && routeIsSyncing}
/>
</Trace>
{recipient !== null && !showWrap ? (
<>
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
<ArrowWrapper clickable={false}>
<ArrowDown size="16" color={theme.textSecondary} />
</ArrowWrapper>
<LinkStyledButton id="remove-recipient-button" onClick={() => onChangeRecipient(null)}>
<Trans>- Remove recipient</Trans>
</LinkStyledButton>
</AutoRow>
<AddressInputPanel id="recipient" value={recipient} onChange={onChangeRecipient} />
</>
) : null}
</OutputSwapSection>
{showDetailsDropdown && (
<DetailsSwapSection>
<SwapDetailsDropdown
trade={trade}
syncing={routeIsSyncing}
loading={routeIsLoading}
allowedSlippage={allowedSlippage}
/>
</DetailsSwapSection>
)}
</div>
{showPriceImpactWarning && <PriceImpactWarning priceImpact={largerPriceImpact} />}
<div>
{swapIsUnsupported ? (
<ButtonPrimary disabled={true}>
<ThemedText.DeprecatedMain mb="4px">
<Trans>Unsupported Asset</Trans>
</ThemedText.DeprecatedMain>
</ButtonPrimary>
) : !account ? (
<TraceEvent
events={[BrowserEvent.onClick]}
name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED}
properties={{ received_swap_quote: getIsValidSwapQuote(trade, tradeState, swapInputError) }}
element={InterfaceElementName.CONNECT_WALLET_BUTTON}
>
<ButtonLight onClick={toggleWalletDrawer} fontWeight={600}>
<Trans>Connect Wallet</Trans>
</ButtonLight>
</TraceEvent>
) : showWrap ? (
<ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap} fontWeight={600}>
{wrapInputError ? (
<WrapErrorText wrapInputError={wrapInputError} />
) : wrapType === WrapType.WRAP ? (
<Trans>Wrap</Trans>
) : wrapType === WrapType.UNWRAP ? (
<Trans>Unwrap</Trans>
) : null}
</ButtonPrimary>
) : routeNotFound && userHasSpecifiedInputOutput && !routeIsLoading && !routeIsSyncing ? (
<GrayCard style={{ textAlign: 'center' }}>
<ThemedText.DeprecatedMain mb="4px">
<Trans>Insufficient liquidity for this trade.</Trans>
</ThemedText.DeprecatedMain>
</GrayCard>
) : isValid && allowance.state === AllowanceState.REQUIRED ? (
<ButtonPrimary
onClick={updateAllowance}
disabled={isAllowancePending || isApprovalLoading}
style={{ gap: 14 }}
>
{isAllowancePending ? (
<>
<Loader size="20px" />
<Trans>Approve in your wallet</Trans>
</>
) : isApprovalLoading ? (
<>
<Loader size="20px" />
<Trans>Approval pending</Trans>
</>
) : (
<>
<div style={{ height: 20 }}>
<MouseoverTooltip
text={
<Trans>
Permission is required for Uniswap to swap each token. This will expire after one
month for your security.
</Trans>
}
>
<Info size={20} />
</MouseoverTooltip>
</div>
<Trans>Approve use of {currencies[Field.INPUT]?.symbol}</Trans>
</>
)}
</ButtonPrimary>
) : (
<ButtonError
onClick={() => {
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined,
})
}
}}
id="swap-button"
disabled={
!isValid ||
routeIsSyncing ||
routeIsLoading ||
priceImpactTooHigh ||
allowance.state !== AllowanceState.ALLOWED
</DetailsSwapSection>
)}
</div>
{showPriceImpactWarning && <PriceImpactWarning priceImpact={largerPriceImpact} />}
<div>
{swapIsUnsupported ? (
<ButtonPrimary disabled={true}>
<ThemedText.DeprecatedMain mb="4px">
<Trans>Unsupported Asset</Trans>
</ThemedText.DeprecatedMain>
</ButtonPrimary>
) : !account ? (
<TraceEvent
events={[BrowserEvent.onClick]}
name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED}
properties={{ received_swap_quote: getIsValidSwapQuote(trade, tradeState, swapInputError) }}
element={InterfaceElementName.CONNECT_WALLET_BUTTON}
>
<ButtonLight onClick={toggleWalletDrawer} fontWeight={600}>
<Trans>Connect Wallet</Trans>
</ButtonLight>
</TraceEvent>
) : chainId && chainId !== connectedChainId ? (
<ButtonPrimary
onClick={() => {
switchChain(connector, chainId)
}}
>
Connect to {getChainInfo(chainId)?.label}
</ButtonPrimary>
) : showWrap ? (
<ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap} fontWeight={600}>
{wrapInputError ? (
<WrapErrorText wrapInputError={wrapInputError} />
) : wrapType === WrapType.WRAP ? (
<Trans>Wrap</Trans>
) : wrapType === WrapType.UNWRAP ? (
<Trans>Unwrap</Trans>
) : null}
</ButtonPrimary>
) : routeNotFound && userHasSpecifiedInputOutput && !routeIsLoading && !routeIsSyncing ? (
<GrayCard style={{ textAlign: 'center' }}>
<ThemedText.DeprecatedMain mb="4px">
<Trans>Insufficient liquidity for this trade.</Trans>
</ThemedText.DeprecatedMain>
</GrayCard>
) : isValid && allowance.state === AllowanceState.REQUIRED ? (
<ButtonPrimary
onClick={updateAllowance}
disabled={isAllowancePending || isApprovalLoading}
style={{ gap: 14 }}
>
{isAllowancePending ? (
<>
<Loader size="20px" />
<Trans>Approve in your wallet</Trans>
</>
) : isApprovalLoading ? (
<>
<Loader size="20px" />
<Trans>Approval pending</Trans>
</>
) : (
<>
<div style={{ height: 20 }}>
<MouseoverTooltip
text={
<Trans>
Permission is required for Uniswap to swap each token. This will expire after one month for
your security.
</Trans>
}
error={isValid && priceImpactSeverity > 2 && allowance.state === AllowanceState.ALLOWED}
>
<Text fontSize={20} fontWeight={600}>
{swapInputError ? (
swapInputError
) : routeIsSyncing || routeIsLoading ? (
<Trans>Swap</Trans>
) : priceImpactTooHigh ? (
<Trans>Price Impact Too High</Trans>
) : priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : (
<Trans>Swap</Trans>
)}
</Text>
</ButtonError>
)}
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
</div>
</AutoColumn>
</SwapWrapper>
<Info size={20} />
</MouseoverTooltip>
</div>
<Trans>Approve use of {currencies[Field.INPUT]?.symbol}</Trans>
</>
)}
</ButtonPrimary>
) : (
<ButtonError
onClick={() => {
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined,
})
}
}}
id="swap-button"
disabled={
!isValid ||
routeIsSyncing ||
routeIsLoading ||
priceImpactTooHigh ||
allowance.state !== AllowanceState.ALLOWED
}
error={isValid && priceImpactSeverity > 2 && allowance.state === AllowanceState.ALLOWED}
>
<Text fontSize={20} fontWeight={600}>
{swapInputError ? (
swapInputError
) : routeIsSyncing || routeIsLoading ? (
<Trans>Swap</Trans>
) : priceImpactTooHigh ? (
<Trans>Price Impact Too High</Trans>
) : priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : (
<Trans>Swap</Trans>
)}
</Text>
</ButtonError>
)}
<NetworkAlert />
</PageWrapper>
<SwitchLocaleLink />
{!swapIsUnsupported ? null : (
<UnsupportedCurrencyFooter
show={swapIsUnsupported}
currencies={[currencies[Field.INPUT], currencies[Field.OUTPUT]]}
/>
)}
</>
</Trace>
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
</div>
</AutoColumn>
</SwapWrapper>
)
}
......@@ -16,8 +16,8 @@ export {
// mimics useAllBalances
export function useAllTokenBalances(): [{ [tokenAddress: string]: CurrencyAmount<Token> | undefined }, boolean] {
const { account } = useWeb3React()
const allTokens = useDefaultActiveTokens()
const { account, chainId } = useWeb3React()
const allTokens = useDefaultActiveTokens(chainId)
const allTokensArray = useMemo(() => Object.values(allTokens ?? {}), [allTokens])
const [balances, balancesIsLoading] = useTokenBalancesWithLoadingIndicator(account ?? undefined, allTokensArray)
return [balances ?? {}, balancesIsLoading]
......
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { SupportedChainId } from 'constants/chains'
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { useBestTrade } from 'hooks/useBestTrade'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
......@@ -71,7 +72,10 @@ const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = {
}
// from the current swap inputs, compute the best trade and return it.
export function useDerivedSwapInfo(state: SwapState): {
export function useDerivedSwapInfo(
state: SwapState,
chainId: SupportedChainId | undefined
): {
currencies: { [field in Field]?: Currency | null }
currencyBalances: { [field in Field]?: CurrencyAmount<Currency> }
parsedAmount: CurrencyAmount<Currency> | undefined
......@@ -92,8 +96,8 @@ export function useDerivedSwapInfo(state: SwapState): {
recipient,
} = state
const inputCurrency = useCurrency(inputCurrencyId)
const outputCurrency = useCurrency(outputCurrencyId)
const inputCurrency = useCurrency(inputCurrencyId, chainId)
const outputCurrency = useCurrency(outputCurrencyId, chainId)
const recipientLookup = useENS(recipient ?? undefined)
const to: string | null = (recipient === null ? account : recipientLookup.address) ?? null
......
......@@ -271,7 +271,7 @@ export function toV2LiquidityToken([tokenA, tokenB]: [Token, Token]): Token {
*/
export function useTrackedTokenPairs(): [Token, Token][] {
const { chainId } = useWeb3React()
const tokens = useDefaultActiveTokens()
const tokens = useDefaultActiveTokens(chainId)
// pinned pairs
const pinnedPairs = useMemo(() => (chainId ? PINNED_PAIRS[chainId] ?? [] : []), [chainId])
......
export function addressesAreEquivalent(a?: string, b?: string) {
export function addressesAreEquivalent(a: string | null | undefined, b: string | null | undefined) {
if (!a || !b) return false
return a === b || a.toLowerCase() === b.toLowerCase()
}
......@@ -4,7 +4,7 @@ import { SupportedChainId } from 'constants/chains'
* Returns the input chain ID if chain is supported. If not, return undefined
* @param chainId a chain ID, which will be returned if it is a supported chain ID
*/
export function supportedChainId(chainId: number | undefined): SupportedChainId | undefined {
export function supportedChainId(chainId: number | null | undefined): SupportedChainId | undefined {
if (typeof chainId === 'number' && chainId in SupportedChainId) {
return chainId
}
......
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