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,6 +46,7 @@ describe('Swap', () => {
cy.contains('Search name or paste address').should('not.exist')
}
describe('Swap on main page', () => {
before(() => {
cy.visit('/swap', { ethereum: 'hardhat' })
})
......@@ -74,44 +78,12 @@ describe('Swap', () => {
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)
selectOutput('WETH')
selectToken('WETH', 'output')
cy.get(getTestSelector('swap-currency-button')).first().click()
verifyToken('input', 'WETH')
......@@ -124,7 +96,7 @@ describe('Swap', () => {
verifyToken('input', 'WETH')
verifyToken('output', null)
selectOutput('Ether')
selectToken('Ether', 'output')
cy.get(getTestSelector('swap-currency-button')).first().click()
verifyToken('input', 'ETH')
......@@ -141,7 +113,7 @@ describe('Swap', () => {
verifyToken('input', 'WETH')
verifyToken('output', null)
selectOutput('Ether')
selectToken('Ether', 'output')
cy.get(getTestSelector('swap-currency-button')).first().click()
verifyToken('input', 'ETH')
......@@ -150,30 +122,11 @@ describe('Swap', () => {
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
cy.visit('/swap')
selectOutput('WETH')
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')
})
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')
})
})
it('Opens and closes the settings menu', () => {
cy.visit('/swap')
cy.contains('Settings').should('not.exist')
......@@ -194,4 +147,103 @@ describe('Swap', () => {
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
})
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)
})
})
})
})
})
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('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('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' }}>
{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]: inputToken ?? undefined,
[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),
},
[onCurrencySelection]
[Field.OUTPUT]: state[Field.OUTPUT],
})
},
[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,8 +563,7 @@ export default function Swap({ className }: { className?: string }) {
)
return (
<Trace page={InterfacePageName.SWAP_PAGE} shouldLogImpression>
<>
<SwapWrapper chainId={chainId} className={className} id="swap-page">
<TokenSafetyModal
isOpen={importTokensNotInDefault.length > 0 && !dismissTokenWarning}
tokenAddress={importTokensNotInDefault[0]?.address}
......@@ -487,17 +572,6 @@ export default function Swap({ className }: { className?: string }) {
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">
<SwapHeader allowedSlippage={allowedSlippage} />
<ConfirmSwapModal
isOpen={showConfirm}
......@@ -521,12 +595,9 @@ export default function Swap({ className }: { className?: string }) {
<Trace section={InterfaceSectionName.CURRENCY_INPUT_PANEL}>
<SwapCurrencyInputPanel
label={
independentField === Field.OUTPUT && !showWrap ? (
<Trans>From (at most)</Trans>
) : (
<Trans>From</Trans>
)
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}
......@@ -550,15 +621,13 @@ export default function Swap({ className }: { className?: string }) {
<ArrowContainer
data-testid="swap-currency-button"
onClick={() => {
onSwitchTokens()
!disableTokenInputs && onSwitchTokens()
}}
color={theme.textPrimary}
>
<ArrowDown
size="16"
color={
currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.textPrimary : theme.textTertiary
}
color={currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.textPrimary : theme.textTertiary}
/>
</ArrowContainer>
</TraceEvent>
......@@ -570,14 +639,9 @@ export default function Swap({ className }: { className?: string }) {
<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>
)
}
label={independentField === Field.INPUT && !showWrap ? <Trans>To (at least)</Trans> : <Trans>To</Trans>}
showMaxButton={false}
hideBalance={false}
fiatValue={fiatValueOutput}
......@@ -590,7 +654,6 @@ export default function Swap({ className }: { className?: string }) {
loading={independentField === Field.INPUT && routeIsSyncing}
/>
</Trace>
{recipient !== null && !showWrap ? (
<>
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
......@@ -635,6 +698,14 @@ export default function Swap({ className }: { className?: string }) {
<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 ? (
......@@ -673,8 +744,8 @@ export default function Swap({ className }: { className?: string }) {
<MouseoverTooltip
text={
<Trans>
Permission is required for Uniswap to swap each token. This will expire after one
month for your security.
Permission is required for Uniswap to swap each token. This will expire after one month for
your security.
</Trans>
}
>
......@@ -729,17 +800,5 @@ export default function Swap({ className }: { className?: string }) {
</div>
</AutoColumn>
</SwapWrapper>
)}
<NetworkAlert />
</PageWrapper>
<SwitchLocaleLink />
{!swapIsUnsupported ? null : (
<UnsupportedCurrencyFooter
show={swapIsUnsupported}
currencies={[currencies[Field.INPUT], currencies[Field.OUTPUT]]}
/>
)}
</>
</Trace>
)
}
......@@ -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