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