diff --git a/packages/arb-token-bridge-ui/src/components/App/App.tsx b/packages/arb-token-bridge-ui/src/components/App/App.tsx index 51b398d039..a5382cbf24 100644 --- a/packages/arb-token-bridge-ui/src/components/App/App.tsx +++ b/packages/arb-token-bridge-ui/src/components/App/App.tsx @@ -18,7 +18,7 @@ import { TokenBridgeParams } from '../../hooks/useArbTokenBridge' import { WelcomeDialog } from './WelcomeDialog' import { BlockedDialog } from './BlockedDialog' import { AppContextProvider } from './AppContext' -import { config, useActions, useAppState } from '../../state' +import { config, useActions } from '../../state' import { MainContent } from '../MainContent/MainContent' import { ArbTokenBridgeStoreSync } from '../syncers/ArbTokenBridgeStoreSync' import { TokenListSyncer } from '../syncers/TokenListSyncer' @@ -33,7 +33,6 @@ import { TOS_LOCALSTORAGE_KEY } from '../../constants' import { getProps } from '../../util/wagmi/setup' import { useAccountIsBlocked } from '../../hooks/useAccountIsBlocked' import { useCCTPIsBlocked } from '../../hooks/CCTP/useCCTPIsBlocked' -import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { sanitizeQueryParams, useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { HeaderConnectWalletButton } from '../common/HeaderConnectWalletButton' @@ -59,13 +58,9 @@ const rainbowkitTheme = merge(darkTheme(), { const ArbTokenBridgeStoreSyncWrapper = (): JSX.Element | null => { const actions = useActions() - const { - app: { selectedToken } - } = useAppState() const [networks] = useNetworks() const { childChain, childChainProvider, parentChain, parentChainProvider } = useNetworksRelationship(networks) - const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) // We want to be sure this fetch is completed by the time we open the USDC modals useCCTPIsBlocked() @@ -73,32 +68,12 @@ const ArbTokenBridgeStoreSyncWrapper = (): JSX.Element | null => { const [tokenBridgeParams, setTokenBridgeParams] = useState(null) - useEffect(() => { - if (!nativeCurrency.isCustom) { - return - } - - const selectedTokenAddress = selectedToken?.address.toLowerCase() - const selectedTokenL2Address = selectedToken?.l2Address?.toLowerCase() - // This handles a super weird edge case where, for example: - // - // Your setup is: from Arbitrum One to Mainnet, and you have $ARB selected as the token you want to bridge over. - // You then switch your destination network to a network that has $ARB as its native currency. - // For this network, $ARB can only be bridged as the native currency, and not as a standard ERC-20, which is why we have to reset the selected token. - if ( - selectedTokenAddress === nativeCurrency.address || - selectedTokenL2Address === nativeCurrency.address - ) { - actions.app.setSelectedToken(null) - } - }, [selectedToken, nativeCurrency]) - // Listen for account and network changes useEffect(() => { // Any time one of those changes setTokenBridgeParams(null) actions.app.setConnectionState(ConnectionState.LOADING) - actions.app.reset(networks.sourceChain.id) + actions.app.reset() actions.app.setChainIds({ l1NetworkChainId: parentChain.id, l2NetworkChainId: childChain.id diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/CustomFeeTokenApprovalDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/CustomFeeTokenApprovalDialog.tsx index ab1b9cfa8f..d6b9468c67 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/CustomFeeTokenApprovalDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/CustomFeeTokenApprovalDialog.tsx @@ -10,12 +10,12 @@ import { formatAmount, formatUSD } from '../../util/NumberUtils' import { getExplorerUrl, isNetwork } from '../../util/networks' import { useGasPrice } from '../../hooks/useGasPrice' import { NativeCurrencyErc20 } from '../../hooks/useNativeCurrency' -import { useAppState } from '../../state' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { shortenAddress } from '../../util/CommonUtils' import { NoteBox } from '../common/NoteBox' import { BridgeTransferStarterFactory } from '@/token-bridge-sdk/BridgeTransferStarterFactory' +import { useSelectedToken } from '../../hooks/useSelectedToken' import { useIsBatchTransferSupported } from '../../hooks/TransferPanel/useIsBatchTransferSupported' import { useArbQueryParams } from '../../hooks/useArbQueryParams' @@ -29,8 +29,7 @@ export function CustomFeeTokenApprovalDialog( const { customFeeToken, isOpen } = props const { ethToUSD } = useETHPrice() - const { app } = useAppState() - const { selectedToken } = app + const [selectedToken] = useSelectedToken() const [networks] = useNetworks() const { sourceChain, destinationChain } = networks diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/EstimatedGas.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/EstimatedGas.tsx index 0b1de4f338..f4a5daeba0 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/EstimatedGas.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/EstimatedGas.tsx @@ -2,7 +2,6 @@ import { InformationCircleIcon } from '@heroicons/react/24/outline' import { useMemo } from 'react' import { twMerge } from 'tailwind-merge' -import { useAppState } from '../../state' import { getNetworkName, isNetwork } from '../../util/networks' import { ChainId } from '../../types/ChainId' import { Tooltip } from '../common/Tooltip' @@ -14,6 +13,7 @@ import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { NativeCurrencyPrice, useIsBridgingEth } from './NativeCurrencyPrice' import { isTokenNativeUSDC } from '../../util/TokenUtils' +import { useSelectedToken } from '../../hooks/useSelectedToken' function getGasFeeTooltip(chainId: ChainId) { const { isEthereumMainnetOrTestnet } = isNetwork(chainId) @@ -54,9 +54,7 @@ export function EstimatedGas({ }: { chainType: 'source' | 'destination' }) { - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const [networks] = useNetworks() const { childChain, diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/NativeCurrencyPrice.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/NativeCurrencyPrice.tsx index c1460b5948..be724a608a 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/NativeCurrencyPrice.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/NativeCurrencyPrice.tsx @@ -1,15 +1,13 @@ import { NativeCurrency } from '../../hooks/useNativeCurrency' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' -import { useAppState } from '../../state' import { isNetwork } from '../../util/networks' import { useETHPrice } from '../../hooks/useETHPrice' import { formatUSD } from '../../util/NumberUtils' +import { useSelectedToken } from '../../hooks/useSelectedToken' export function useIsBridgingEth(childChainNativeCurrency: NativeCurrency) { - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const isBridgingEth = selectedToken === null && !childChainNativeCurrency.isCustom return isBridgingEth diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/OneNovaTransferDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/OneNovaTransferDialog.tsx index c48987fa35..e1d8017e50 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/OneNovaTransferDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/OneNovaTransferDialog.tsx @@ -1,7 +1,6 @@ import { Tab } from '@headlessui/react' import Hop from '@/images/bridge/hop.png' -import { useAppState } from '../../state' import { TabButton } from '../common/Tab' import { BridgesTable } from '../common/BridgesTable' import { SecurityNotGuaranteed } from './SecurityLabels' @@ -10,6 +9,7 @@ import { FastBridgeInfo, FastBridgeNames } from '../../util/fastBridges' import { getNetworkName, isNetwork } from '../../util/networks' import { ChainId } from '../../types/ChainId' import { ether } from '../../constants' +import { useSelectedToken } from '../../hooks/useSelectedToken' import { useArbQueryParams } from '../../hooks/useArbQueryParams' import { useNetworks } from '../../hooks/useNetworks' @@ -47,9 +47,7 @@ function getDialogSourceAndDestinationChains({ } export function OneNovaTransferDialog(props: UseDialogProps) { - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const [{ amount }] = useArbQueryParams() const [{ sourceChain, destinationChain }] = useNetworks() diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx index 196a01c634..e7b254fdfa 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx @@ -1,9 +1,9 @@ import { useMemo } from 'react' +import { utils } from 'ethers' import { Popover } from '@headlessui/react' import { ChevronDownIcon } from '@heroicons/react/24/outline' import { twMerge } from 'tailwind-merge' -import { useAppState } from '../../state' import { TokenSearch } from '../TransferPanel/TokenSearch' import { sanitizeTokenSymbol } from '../../util/TokenUtils' import { useNativeCurrency } from '../../hooks/useNativeCurrency' @@ -17,6 +17,10 @@ import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { Transition } from '../common/Transition' import { SafeImage } from '../common/SafeImage' import { useTokensFromLists, useTokensFromUser } from './TokenSearchUtils' +import { Loader } from '../common/atoms/Loader' +import { useSelectedToken } from '../../hooks/useSelectedToken' +import { useTokenLists } from '../../hooks/useTokenLists' +import { useArbQueryParams } from '../../hooks/useArbQueryParams' export type TokenButtonOptions = { symbol?: string @@ -29,13 +33,13 @@ export function TokenButton({ }: { options?: TokenButtonOptions }): JSX.Element { - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const disabled = options?.disabled ?? false const [networks] = useNetworks() - const { childChainProvider } = useNetworksRelationship(networks) + const { childChain, childChainProvider } = useNetworksRelationship(networks) + const { isLoading: isLoadingTokenLists } = useTokenLists(childChain.id) + const [{ token: tokenFromSearchParams }] = useArbQueryParams() const tokensFromLists = useTokensFromLists() const tokensFromUser = useTokensFromUser() @@ -57,6 +61,17 @@ export function TokenButton({ }) }, [selectedToken, networks.sourceChain.id, nativeCurrency.symbol, options]) + const isLoadingToken = useMemo(() => { + // don't show loader if native currency is selected + if (!tokenFromSearchParams) { + return false + } + if (!utils.isAddress(tokenFromSearchParams)) { + return false + } + return isLoadingTokenLists + }, [tokenFromSearchParams, isLoadingTokenLists]) + const tokenLogoSrc = useMemo(() => { if (typeof options?.logoSrc !== 'undefined') { return options.logoSrc || nativeCurrency.logoUrl @@ -90,19 +105,27 @@ export function TokenButton({ disabled={disabled} >
- - {tokenSymbol} - {!disabled && ( - + ) : ( + <> + + {tokenSymbol} + {!disabled && ( + )} - /> + )}
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx index 6709ca10d1..43f73e90a3 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx @@ -4,7 +4,7 @@ import { useLatest } from 'react-use' import { create } from 'zustand' import { useERC20L1Address } from '../../hooks/useERC20L1Address' -import { useActions, useAppState } from '../../state' +import { useAppState } from '../../state' import { erc20DataToErc20BridgeToken, fetchErc20Data, @@ -17,11 +17,9 @@ import { ERC20BridgeToken } from '../../hooks/arbTokenBridge.types' import { warningToast } from '../common/atoms/Toast' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' -import { isTransferDisabledToken } from '../../util/TokenTransferDisabledUtils' -import { useTransferDisabledDialogStore } from './TransferDisabledDialog' import { TokenInfo } from './TokenInfo' import { NoteBox } from '../common/NoteBox' -import { isTeleportEnabledToken } from '../../util/TokenTeleportEnabledUtils' +import { useSelectedToken } from '../../hooks/useSelectedToken' enum ImportStatus { LOADING, @@ -66,19 +64,13 @@ export function TokenImportDialog({ const { address: walletAddress } = useAccount() const { app: { - arbTokenBridge: { bridgeTokens, token }, - selectedToken + arbTokenBridge: { bridgeTokens, token } } } = useAppState() + const [selectedToken, setSelectedToken] = useSelectedToken() const [networks] = useNetworks() - const { - childChain, - childChainProvider, - parentChain, - parentChainProvider, - isTeleportMode - } = useNetworksRelationship(networks) - const actions = useActions() + const { childChainProvider, parentChainProvider } = + useNetworksRelationship(networks) const tokensFromUser = useTokensFromUser() const latestTokensFromUser = useLatest(tokensFromUser) @@ -91,8 +83,6 @@ export function TokenImportDialog({ const [status, setStatus] = useState(ImportStatus.LOADING) const [isImportingToken, setIsImportingToken] = useState(false) const [tokenToImport, setTokenToImport] = useState() - const { openDialog: openTransferDisabledDialog } = - useTransferDisabledDialogStore() const { isOpen } = useTokenImportDialogStore() const [isDialogVisible, setIsDialogVisible] = useState(false) const { data: l1Address, isLoading: isL1AddressLoading } = useERC20L1Address({ @@ -177,9 +167,9 @@ export function TokenImportDialog({ const selectToken = useCallback( async (_token: ERC20BridgeToken) => { await token.updateTokenData(_token.address) - actions.app.setSelectedToken(_token) + setSelectedToken(_token.address) }, - [token, actions] + [token, setSelectedToken] ) useEffect(() => { @@ -258,7 +248,6 @@ export function TokenImportDialog({ l1Address, onClose, selectToken, - selectedToken, tokensFromUser ]) @@ -297,19 +286,6 @@ export function TokenImportDialog({ setStatus(ImportStatus.ERROR) }) } - - if (isTransferDisabledToken(l1Address, childChain.id)) { - openTransferDisabledDialog() - return - } - - if ( - isTeleportMode && - !isTeleportEnabledToken(l1Address, parentChain.id, childChain.id) - ) { - openTransferDisabledDialog() - return - } } if (status === ImportStatus.LOADING) { diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx index bae5436094..5425e36828 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx @@ -5,7 +5,7 @@ import { useAccount } from 'wagmi' import { AutoSizer, List, ListRowProps } from 'react-virtualized' import { twMerge } from 'tailwind-merge' -import { useActions, useAppState } from '../../state' +import { useAppState } from '../../state' import { BRIDGE_TOKEN_LISTS, BridgeTokenList, @@ -14,11 +14,9 @@ import { } from '../../util/TokenListUtils' import { fetchErc20Data, - erc20DataToErc20BridgeToken, isTokenArbitrumOneNativeUSDC, isTokenArbitrumSepoliaNativeUSDC, isTokenArbitrumOneUSDCe, - getL2ERC20Address, isTokenNativeUSDC } from '../../util/TokenUtils' import { Button } from '../common/Button' @@ -29,19 +27,14 @@ import { warningToast } from '../common/atoms/Toast' import { CommonAddress } from '../../util/CommonAddressUtils' import { ArbOneNativeUSDC } from '../../util/L2NativeUtils' import { getNetworkName, isNetwork } from '../../util/networks' -import { useUpdateUsdcBalances } from '../../hooks/CCTP/useUpdateUsdcBalances' -import { useAccountType } from '../../hooks/useAccountType' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { SearchPanelTable } from '../common/SearchPanel/SearchPanelTable' import { SearchPanel } from '../common/SearchPanel/SearchPanel' import { TokenRow } from './TokenRow' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' -import { useTransferDisabledDialogStore } from './TransferDisabledDialog' -import { isTransferDisabledToken } from '../../util/TokenTransferDisabledUtils' -import { useTokenFromSearchParams } from './TransferPanelUtils' import { Switch } from '../common/atoms/Switch' -import { isTeleportEnabledToken } from '../../util/TokenTeleportEnabledUtils' +import { useSelectedToken } from '../../hooks/useSelectedToken' import { useBalances } from '../../hooks/useBalances' import { useSetInputAmount } from '../../hooks/TransferPanel/useSetInputAmount' @@ -369,11 +362,11 @@ function TokensPanel({ isDepositMode, isArbitrumOne, isArbitrumSepolia, - isOrbitChain, isParentChainArbitrumOne, isParentChainArbitrumSepolia, - getBalance, - nativeCurrency + isOrbitChain, + nativeCurrency, + getBalance ]) const storeNewToken = async () => { @@ -527,22 +520,9 @@ export function TokenSearch({ arbTokenBridge: { token, bridgeTokens } } } = useAppState() - const { - app: { setSelectedToken } - } = useActions() + const [, setSelectedToken] = useSelectedToken() const [networks] = useNetworks() - const { - childChain, - childChainProvider, - parentChain, - parentChainProvider, - isTeleportMode - } = useNetworksRelationship(networks) - const { updateUsdcBalances } = useUpdateUsdcBalances({ walletAddress }) - const { isLoading: isLoadingAccountType } = useAccountType() - const { openDialog: openTransferDisabledDialog } = - useTransferDisabledDialogStore() - const { setTokenQueryParam } = useTokenFromSearchParams() + const { childChain, parentChainProvider } = useNetworksRelationship(networks) const { isValidating: isFetchingTokenLists } = useTokenLists(childChain.id) // to show a small loader while token-lists are loading when search panel opens @@ -564,53 +544,13 @@ export function TokenSearch({ } try { - // Native USDC on L2 won't have a corresponding L1 address - const isL2NativeUSDC = - isTokenArbitrumOneNativeUSDC(_token.address) || - isTokenArbitrumSepoliaNativeUSDC(_token.address) - - if (isL2NativeUSDC) { - if (isLoadingAccountType) { - return - } - - updateUsdcBalances() - - // if an Orbit chain is selected we need to fetch its USDC address - let childChainUsdcAddress - try { - childChainUsdcAddress = isNetwork(childChain.id).isOrbitChain - ? ( - await getL2ERC20Address({ - erc20L1Address: _token.address, - l1Provider: parentChainProvider, - l2Provider: childChainProvider - }) - ).toLowerCase() - : undefined - } catch { - // could be never bridged before - } - - setSelectedToken({ - name: 'USD Coin', - type: TokenType.ERC20, - symbol: 'USDC', - address: _token.address, - l2Address: childChainUsdcAddress, - decimals: 6, - listIds: new Set() - }) - return - } - if (typeof bridgeTokens === 'undefined') { return } // Token not added to the bridge, so we'll handle importing it if (typeof bridgeTokens[_token.address] === 'undefined') { - setTokenQueryParam(_token.address) + setSelectedToken(_token.address) return } @@ -625,23 +565,7 @@ export function TokenSearch({ if (data) { token.updateTokenData(_token.address) - setSelectedToken({ - ...erc20DataToErc20BridgeToken(data), - l2Address: _token.l2Address - }) - } - - if (isTransferDisabledToken(_token.address, childChain.id)) { - openTransferDisabledDialog() - return - } - - if ( - isTeleportMode && - !isTeleportEnabledToken(_token.address, parentChain.id, childChain.id) - ) { - openTransferDisabledDialog() - return + setSelectedToken(_token.address) } } catch (error: any) { console.warn(error) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx index 81fb6aef34..ca84c683e6 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx @@ -1,7 +1,5 @@ -import { create } from 'zustand' import { useEffect, useMemo, useState } from 'react' -import { useActions, useAppState } from '../../state' import { Dialog } from '../common/Dialog' import { isTokenEthereumUSDT, sanitizeTokenSymbol } from '../../util/TokenUtils' import { useNetworks } from '../../hooks/useNetworks' @@ -11,36 +9,21 @@ import { ChainId } from '../../types/ChainId' import { getL2ConfigForTeleport } from '../../token-bridge-sdk/teleport' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { withdrawOnlyTokens } from '../../util/WithdrawOnlyUtils' +import { useSelectedToken } from '../../hooks/useSelectedToken' import { useSelectedTokenIsWithdrawOnly } from './hooks/useSelectedTokenIsWithdrawOnly' - -type TransferDisabledDialogStore = { - isOpen: boolean - openDialog: () => void - closeDialog: () => void -} - -export const useTransferDisabledDialogStore = - create(set => ({ - isOpen: false, - openDialog: () => set({ isOpen: true }), - closeDialog: () => set({ isOpen: false }) - })) +import { isTransferDisabledToken } from '../../util/TokenTransferDisabledUtils' +import { isTeleportEnabledToken } from '../../util/TokenTeleportEnabledUtils' export function TransferDisabledDialog() { const [networks] = useNetworks() - const { isDepositMode, isTeleportMode } = useNetworksRelationship(networks) - const { app } = useAppState() - const { selectedToken } = app - const { - app: { setSelectedToken } - } = useActions() + const { isDepositMode, isTeleportMode, parentChain, childChain } = + useNetworksRelationship(networks) + const [selectedToken, setSelectedToken] = useSelectedToken() + // for tracking local state and prevent flickering with async URL params updating + const [selectedTokenAddressLocalValue, setSelectedTokenAddressLocalValue] = + useState(null) const { isSelectedTokenWithdrawOnly, isSelectedTokenWithdrawOnlyLoading } = useSelectedTokenIsWithdrawOnly() - const { - isOpen: isOpenTransferDisabledDialog, - openDialog: openTransferDisabledDialog, - closeDialog: closeTransferDisabledDialog - } = useTransferDisabledDialogStore() const unsupportedToken = sanitizeTokenSymbol(selectedToken?.symbol ?? '', { erc20L1Address: selectedToken?.address, chainId: networks.sourceChain.id @@ -70,27 +53,49 @@ export function TransferDisabledDialog() { updateL2ChainIdForTeleport() }, [isTeleportMode, networks.destinationChainProvider]) - useEffect(() => { - // do not allow import of withdraw-only tokens at deposit mode + const shouldShowDialog = useMemo(() => { + if ( + !selectedToken || + selectedToken.address === selectedTokenAddressLocalValue + ) { + return false + } + + if (isTransferDisabledToken(selectedToken.address, childChain.id)) { + return true + } + + if ( + isTeleportMode && + !isTeleportEnabledToken( + selectedToken.address, + parentChain.id, + childChain.id + ) + ) { + return true + } + if ( isDepositMode && isSelectedTokenWithdrawOnly && !isSelectedTokenWithdrawOnlyLoading ) { - openTransferDisabledDialog() + return true } + + return false }, [ - isSelectedTokenWithdrawOnly, + childChain.id, isDepositMode, - openTransferDisabledDialog, - isSelectedTokenWithdrawOnlyLoading + isSelectedTokenWithdrawOnly, + isSelectedTokenWithdrawOnlyLoading, + isTeleportMode, + parentChain.id, + selectedToken, + selectedTokenAddressLocalValue ]) - const onClose = () => { - setSelectedToken(null) - closeTransferDisabledDialog() - } - const sourceChainName = getNetworkName(networks.sourceChain.id) const destinationChainName = getNetworkName(networks.destinationChain.id) const l2ChainIdForTeleportName = l2ChainIdForTeleport @@ -104,13 +109,30 @@ export function TransferDisabledDialog() { ?.find(_token => _token.symbol === 'GHO') ?.l1Address.toLowerCase() + useEffect(() => { + if ( + selectedTokenAddressLocalValue && + (!selectedToken || + selectedToken.address !== selectedTokenAddressLocalValue) + ) { + setSelectedTokenAddressLocalValue(null) + } + }, [selectedToken, selectedTokenAddressLocalValue]) + + const onClose = () => { + if (selectedToken) { + setSelectedTokenAddressLocalValue(selectedToken.address) + setSelectedToken(null) + } + } + return (
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx index 0b55b4cd81..5d009310f4 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -24,7 +24,7 @@ import { TransferPanelSummary } from './TransferPanelSummary' import { useAppContextActions } from '../App/AppContext' import { trackEvent } from '../../util/AnalyticsUtils' import { TransferPanelMain } from './TransferPanelMain' -import { isGatewayRegistered } from '../../util/TokenUtils' +import { isGatewayRegistered, isTokenNativeUSDC } from '../../util/TokenUtils' import { useSwitchNetworkWithConfig } from '../../hooks/useSwitchNetworkWithConfig' import { errorToast, warningToast } from '../common/atoms/Toast' import { useAccountType } from '../../hooks/useAccountType' @@ -43,8 +43,7 @@ import { } from '../../hooks/arbTokenBridge.types' import { ImportTokenModalStatus, - getWarningTokenDescription, - useTokenFromSearchParams + getWarningTokenDescription } from './TransferPanelUtils' import { useImportTokenModal } from '../../hooks/TransferPanel/useImportTokenModal' import { useTransactionHistory } from '../../hooks/useTransactionHistory' @@ -64,9 +63,12 @@ import { import { getBridgeTransferProperties } from '../../token-bridge-sdk/utils' import { useSetInputAmount } from '../../hooks/TransferPanel/useSetInputAmount' import { getSmartContractWalletTeleportTransfersNotSupportedErrorMessage } from './useTransferReadinessUtils' +import { useTokensFromLists, useTokensFromUser } from './TokenSearchUtils' +import { useSelectedToken } from '../../hooks/useSelectedToken' import { useBalances } from '../../hooks/useBalances' import { captureSentryErrorWithExtraData } from '../../util/SentryUtils' import { useIsBatchTransferSupported } from '../../hooks/TransferPanel/useIsBatchTransferSupported' +import { useTokenLists } from '../../hooks/useTokenLists' import { normalizeTimestamp } from '../../state/app/utils' import { useDestinationAddressError } from './hooks/useDestinationAddressError' import { useIsCctpTransfer } from './hooks/useIsCctpTransfer' @@ -94,9 +96,7 @@ const networkConnectionWarningToast = () => ) export function TransferPanel() { - const { tokenFromSearchParams, setTokenQueryParam } = - useTokenFromSearchParams() - + const [{ token: tokenFromSearchParams }] = useArbQueryParams() const [tokenDepositCheckDialogType, setTokenDepositCheckDialogType] = useState('new-token') const [importTokenModalStatus, setImportTokenModalStatus] = @@ -107,11 +107,11 @@ export function TransferPanel() { const { app: { connectionState, - selectedToken, arbTokenBridge: { token }, warningTokens } } = useAppState() + const [selectedToken, setSelectedToken] = useSelectedToken() const { address: walletAddress } = useAccount() const { switchNetworkAsync } = useSwitchNetworkWithConfig({ isSwitchingNetworkBeforeTx: true @@ -120,6 +120,8 @@ export function TransferPanel() { const latestChain = useLatest(useNetwork()) const [networks] = useNetworks() const latestNetworks = useLatest(networks) + const tokensFromLists = useTokensFromLists() + const tokensFromUser = useTokensFromUser() const { current: { childChain, @@ -130,6 +132,7 @@ export function TransferPanel() { isTeleportMode } } = useLatest(useNetworksRelationship(latestNetworks.current)) + const { isLoading: isLoadingTokenLists } = useTokenLists(childChain.id) const isBatchTransferSupported = useIsBatchTransferSupported() const nativeCurrencyDecimalsOnSourceChain = useSourceChainNativeCurrencyDecimals() @@ -201,7 +204,7 @@ export function TransferPanel() { }, [childChain.id, parentChain.id]) function closeWithResetTokenImportDialog() { - setTokenQueryParam(undefined) + setSelectedToken(null) setImportTokenModalStatus(ImportTokenModalStatus.CLOSED) tokenImportDialogProps.onClose(false) } @@ -217,6 +220,42 @@ export function TransferPanel() { connectionState }) + const isTokenAlreadyImported = useMemo(() => { + const tokenLowercased = tokenFromSearchParams?.toLowerCase() + + if (typeof tokenLowercased === 'undefined') { + return true + } + + if (isTokenNativeUSDC(tokenLowercased)) { + return true + } + + if (isLoadingTokenLists) { + return undefined + } + + // only show import token dialog if the token is not part of the list + // otherwise we show a loader in the TokenButton + if (!tokensFromLists) { + return undefined + } + + if (!tokensFromUser) { + return undefined + } + + return ( + typeof tokensFromLists[tokenLowercased] !== 'undefined' || + typeof tokensFromUser[tokenLowercased] !== 'undefined' + ) + }, [ + isLoadingTokenLists, + tokenFromSearchParams, + tokensFromLists, + tokensFromUser + ]) + const isBridgingANewStandardToken = useMemo(() => { const isUnbridgedToken = selectedToken !== null && typeof selectedToken.l2Address === 'undefined' @@ -1027,7 +1066,7 @@ export function TransferPanel() { /> - {typeof tokenFromSearchParams !== 'undefined' && ( + {isTokenAlreadyImported === false && tokenFromSearchParams && ( () -} +import { useSelectedToken } from '../../../hooks/useSelectedToken' export function useUpdateUSDCTokenData() { const actions = useActions() const { app: { - arbTokenBridge: { token }, - selectedToken + arbTokenBridge: { token } } } = useAppState() + const [selectedToken, setSelectedToken] = useSelectedToken() const [networks] = useNetworks() const { isDepositMode } = useNetworksRelationship(networks) const { @@ -46,22 +38,18 @@ export function useUpdateUSDCTokenData() { return } + if (typeof token === 'undefined') { + return + } + if (isArbOneUSDC && isDestinationChainArbitrumOne) { token.updateTokenData(CommonAddress.Ethereum.USDC) - actions.app.setSelectedToken({ - ...commonUSDC, - address: CommonAddress.Ethereum.USDC, - l2Address: CommonAddress.ArbitrumOne['USDC.e'] - }) + setSelectedToken(CommonAddress.Ethereum.USDC) } if (isArbSepoliaUSDC && isDestinationChainArbitrumSepolia) { token.updateTokenData(CommonAddress.Sepolia.USDC) - actions.app.setSelectedToken({ - ...commonUSDC, - address: CommonAddress.Sepolia.USDC, - l2Address: CommonAddress.ArbitrumSepolia['USDC.e'] - }) + setSelectedToken(CommonAddress.Sepolia.USDC) } }, [ actions.app, @@ -69,6 +57,7 @@ export function useUpdateUSDCTokenData() { isDestinationChainArbitrumOne, isDestinationChainArbitrumSepolia, selectedToken, + setSelectedToken, token ]) } diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useMaxAmount.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useMaxAmount.ts index 330bd0de64..e5cb66634f 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useMaxAmount.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useMaxAmount.ts @@ -3,18 +3,16 @@ import { utils } from 'ethers' import { useNetworksRelationship } from '../../../hooks/useNetworksRelationship' import { useNetworks } from '../../../hooks/useNetworks' -import { useAppState } from '../../../state' import { useSelectedTokenBalances } from '../../../hooks/TransferPanel/useSelectedTokenBalances' import { defaultErc20Decimals } from '../../../defaults' import { useGasSummary } from '../../../hooks/TransferPanel/useGasSummary' import { useNativeCurrency } from '../../../hooks/useNativeCurrency' import { useNativeCurrencyBalances } from './useNativeCurrencyBalances' +import { useSelectedToken } from '../../../hooks/useSelectedToken' import { useSourceChainNativeCurrencyDecimals } from '../../../hooks/useSourceChainNativeCurrencyDecimals' export function useMaxAmount() { - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const selectedTokenBalances = useSelectedTokenBalances() const [networks] = useNetworks() const { childChainProvider, isDepositMode } = diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx index 3757f1ffed..eb2bd4da33 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx @@ -11,10 +11,9 @@ import { TokenButton, TokenButtonOptions } from './TokenButton' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { useSelectedTokenBalances } from '../../hooks/TransferPanel/useSelectedTokenBalances' -import { useAppState } from '../../state' +import { useSelectedToken } from '../../hooks/useSelectedToken' import { TransferReadinessRichErrorMessage } from './useTransferReadinessUtils' import { ExternalLink } from '../common/ExternalLink' -import { useTransferDisabledDialogStore } from './TransferDisabledDialog' import { formatAmount } from '../../util/NumberUtils' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { Loader } from '../common/atoms/Loader' @@ -27,9 +26,7 @@ function MaxButton({ className = '', ...rest }: React.ButtonHTMLAttributes) { - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const selectedTokenBalances = useSelectedTokenBalances() const nativeCurrencyBalances = useNativeCurrencyBalances() @@ -75,9 +72,7 @@ function SourceChainTokenBalance({ balanceOverride?: AmountInputOptions['balance'] symbolOverride?: AmountInputOptions['symbol'] }) { - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const [networks] = useNetworks() const { isDepositMode, childChainProvider } = useNetworksRelationship(networks) @@ -150,9 +145,6 @@ function ErrorMessage({ }: { errorMessage: string | TransferReadinessRichErrorMessage | undefined }) { - const { openDialog: openTransferDisabledDialog } = - useTransferDisabledDialogStore() - if (typeof errorMessage === 'undefined') { return null } @@ -180,14 +172,7 @@ function ErrorMessage({ case TransferReadinessRichErrorMessage.TOKEN_TRANSFER_DISABLED: return (
- This token can't be bridged over.{' '} - - . + This token can't be bridged over.
) } diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelSummary.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelSummary.tsx index 3761043fd8..105615b671 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelSummary.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelSummary.tsx @@ -13,12 +13,12 @@ import { ERC20BridgeToken } from '../../hooks/arbTokenBridge.types' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { NativeCurrencyPrice, useIsBridgingEth } from './NativeCurrencyPrice' -import { useAppState } from '../../state' import { Loader } from '../common/atoms/Loader' import { Tooltip } from '../common/Tooltip' import { isTokenNativeUSDC } from '../../util/TokenUtils' import { NoteBox } from '../common/NoteBox' import { DISABLED_CHAIN_IDS } from './useTransferReadiness' +import { useSelectedToken } from '../../hooks/useSelectedToken' import { useIsBatchTransferSupported } from '../../hooks/TransferPanel/useIsBatchTransferSupported' import { getConfirmationTime } from '../../util/WithdrawalUtils' import LightningIcon from '@/images/LightningIcon.svg' @@ -43,9 +43,7 @@ function StyledLoader() { } function TotalGasFees() { - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const { status: gasSummaryStatus, diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelUtils.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelUtils.ts index 066a7641b8..ea7514e1a8 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelUtils.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelUtils.ts @@ -1,5 +1,3 @@ -import { useArbQueryParams } from '../../hooks/useArbQueryParams' - export enum ImportTokenModalStatus { // "IDLE" is here to distinguish between the modal never being opened, and being closed after a user interaction IDLE, @@ -17,25 +15,3 @@ export function getWarningTokenDescription(warningTokenType: number) { return 'a non-standard ERC20 token' } } - -export function useTokenFromSearchParams(): { - tokenFromSearchParams: string | undefined - setTokenQueryParam: (token: string | undefined) => void -} { - const [{ token: tokenFromSearchParams }, setQueryParams] = useArbQueryParams() - - const setTokenQueryParam = (token: string | undefined) => - setQueryParams({ token }) - - if (!tokenFromSearchParams) { - return { - tokenFromSearchParams: undefined, - setTokenQueryParam - } - } - - return { - tokenFromSearchParams, - setTokenQueryParam - } -} diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/USDCDeposit/USDCDepositConfirmationDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/USDCDeposit/USDCDepositConfirmationDialog.tsx index a49a89ad0c..f64c994248 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/USDCDeposit/USDCDepositConfirmationDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/USDCDeposit/USDCDepositConfirmationDialog.tsx @@ -10,7 +10,6 @@ import { } from '../../../util/fastBridges' import { TabButton } from '../../common/Tab' import { BridgesTable } from '../../common/BridgesTable' -import { useAppState } from '../../../state' import { getExplorerUrl, getNetworkName, @@ -29,6 +28,7 @@ import { useNetworks } from '../../../hooks/useNetworks' import { useNetworksRelationship } from '../../../hooks/useNetworksRelationship' import { SecurityGuaranteed, SecurityNotGuaranteed } from '../SecurityLabels' import { getUSDCAddresses } from '../../../state/cctpState' +import { useSelectedToken } from '../../../hooks/useSelectedToken' type Props = UseDialogProps & { amount: string @@ -43,9 +43,7 @@ enum SelectedTabName { const defaultSelectedTabName: SelectedTabName = SelectedTabName.Cctp export function USDCDepositConfirmationDialog(props: Props) { - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const [networks] = useNetworks() const { childChain, parentChain } = useNetworksRelationship(networks) const { isArbitrumSepolia } = isNetwork(childChain.id) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/WithdrawalConfirmationDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/WithdrawalConfirmationDialog.tsx index 0acfeac024..1d378fab2d 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/WithdrawalConfirmationDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/WithdrawalConfirmationDialog.tsx @@ -8,7 +8,6 @@ import { Checkbox } from '../common/Checkbox' import { ExternalLink } from '../common/ExternalLink' import { TabButton } from '../common/Tab' import { BridgesTable } from '../common/BridgesTable' -import { useAppState } from '../../state' import { trackEvent } from '../../util/AnalyticsUtils' import { getNetworkName, isNetwork } from '../../util/networks' import { getFastBridges } from '../../util/fastBridges' @@ -20,6 +19,7 @@ import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { SecurityGuaranteed, SecurityNotGuaranteed } from './SecurityLabels' +import { useSelectedToken } from '../../hooks/useSelectedToken' import { getWithdrawalConfirmationDate } from '../../hooks/useTransferDuration' import { getConfirmationTime } from '../../util/WithdrawalUtils' @@ -52,9 +52,7 @@ export function WithdrawalConfirmationDialog( const destinationNetworkName = getNetworkName(parentChain.id) - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const nativeCurrency = useNativeCurrency({ provider: childChainProvider diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.ts index df90187285..1ee522410f 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.ts @@ -1,13 +1,11 @@ import { useMemo } from 'react' import { useArbQueryParams } from '../../../hooks/useArbQueryParams' -import { useAppState } from '../../../state' import { constants, utils } from 'ethers' import { useSourceChainNativeCurrencyDecimals } from '../../../hooks/useSourceChainNativeCurrencyDecimals' +import { useSelectedToken } from '../../../hooks/useSelectedToken' export function useAmountBigNumber() { - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const [{ amount }] = useArbQueryParams() const nativeCurrencyDecimalsOnSourceChain = useSourceChainNativeCurrencyDecimals() diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useIsCctpTransfer.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useIsCctpTransfer.ts index 57bc41258c..ebac5e5e22 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useIsCctpTransfer.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useIsCctpTransfer.ts @@ -1,6 +1,6 @@ import { useNetworks } from '../../../hooks/useNetworks' import { useNetworksRelationship } from '../../../hooks/useNetworksRelationship' -import { useAppState } from '../../../state' +import { useSelectedToken } from '../../../hooks/useSelectedToken' import { isNetwork } from '../../../util/networks' import { isTokenArbitrumOneNativeUSDC, @@ -10,9 +10,7 @@ import { } from '../../../util/TokenUtils' export const useIsCctpTransfer = function () { - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const [networks] = useNetworks() const { childChain, isDepositMode, isTeleportMode } = useNetworksRelationship(networks) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useSelectedTokenIsWithdrawOnly.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useSelectedTokenIsWithdrawOnly.ts index 55af4dae7c..0f2c74429d 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useSelectedTokenIsWithdrawOnly.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useSelectedTokenIsWithdrawOnly.ts @@ -1,15 +1,13 @@ import useSWRImmutable from 'swr/immutable' import { useMemo } from 'react' -import { useAppState } from '../../../state' import { useNetworks } from '../../../hooks/useNetworks' import { useNetworksRelationship } from '../../../hooks/useNetworksRelationship' import { isWithdrawOnlyToken } from '../../../util/WithdrawOnlyUtils' +import { useSelectedToken } from '../../../hooks/useSelectedToken' export function useSelectedTokenIsWithdrawOnly() { - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const [networks] = useNetworks() const { isDepositMode, parentChain, childChain } = useNetworksRelationship(networks) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts index df4f371fc1..897b028474 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts @@ -3,7 +3,6 @@ import { useAccount } from 'wagmi' import { utils } from 'ethers' import { useAccountType } from '../../hooks/useAccountType' -import { useAppState } from '../../state' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { isTokenArbitrumSepoliaNativeUSDC, @@ -26,6 +25,7 @@ import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { isTeleportEnabledToken } from '../../util/TokenTeleportEnabledUtils' import { isNetwork } from '../../util/networks' +import { useSelectedToken } from '../../hooks/useSelectedToken' import { useBalances } from '../../hooks/useBalances' import { useArbQueryParams } from '../../hooks/useArbQueryParams' import { formatAmount } from '../../util/NumberUtils' @@ -120,9 +120,7 @@ export type UseTransferReadinessResult = { export function useTransferReadiness(): UseTransferReadinessResult { const [{ amount, amount2 }] = useArbQueryParams() - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const { layout: { isTransferring } } = useAppContextState() diff --git a/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx b/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx index ab0e0fc9b1..a23cd1ade5 100644 --- a/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx @@ -32,6 +32,7 @@ import { shouldOpenOneNovaDialog } from '../TransferPanel/TransferPanelMain/util import { useActions } from '../../state' import { useChainIdsForNetworkSelection } from '../../hooks/TransferPanel/useChainIdsForNetworkSelection' import { useAccountType } from '../../hooks/useAccountType' +import { useSelectedToken } from '../../hooks/useSelectedToken' import { useAdvancedSettingsStore } from '../TransferPanel/AdvancedSettings' type NetworkType = 'core' | 'more' | 'orbit' @@ -404,6 +405,7 @@ export const NetworkSelectionContainer = ( } ) => { const actions = useActions() + const [, setSelectedToken] = useSelectedToken() const [networks, setNetworks] = useNetworks() const [oneNovaTransferDialogProps, openOneNovaTransferDialog] = useDialog() const [, setQueryParams] = useArbQueryParams() @@ -444,7 +446,7 @@ export const NetworkSelectionContainer = ( destinationChainId: isSource ? networks.destinationChain.id : value.id }) - actions.app.setSelectedToken(null) + setSelectedToken(null) setQueryParams({ destinationAddress: undefined }) if (!isSmartContractWallet) { @@ -455,7 +457,7 @@ export const NetworkSelectionContainer = ( isSource, networks, setNetworks, - actions.app, + setSelectedToken, setQueryParams, setAdvancedSettingsCollapsed, openOneNovaTransferDialog, diff --git a/packages/arb-token-bridge-ui/src/components/common/TestnetToggle.tsx b/packages/arb-token-bridge-ui/src/components/common/TestnetToggle.tsx index 2f27694e71..f97eac8d94 100644 --- a/packages/arb-token-bridge-ui/src/components/common/TestnetToggle.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/TestnetToggle.tsx @@ -3,6 +3,8 @@ import { twMerge } from 'tailwind-merge' import { useIsTestnetMode } from '../../hooks/useIsTestnetMode' import { Switch } from './atoms/Switch' +import { useCallback } from 'react' +import { useSelectedToken } from '../../hooks/useSelectedToken' export const TestnetToggle = ({ className, @@ -19,6 +21,12 @@ export const TestnetToggle = ({ includeToggleStateOnLabel?: boolean }) => { const [isTestnetMode, toggleTestnetMode] = useIsTestnetMode() + const [, setSelectedToken] = useSelectedToken() + + const handleTestnetToggle = useCallback(() => { + toggleTestnetMode() + setSelectedToken(null) + }, [setSelectedToken, toggleTestnetMode]) const labelText = includeToggleStateOnLabel ? `${label} ${isTestnetMode ? 'ON' : 'OFF'}` @@ -31,7 +39,7 @@ export const TestnetToggle = ({ label={labelText} description={description} checked={isTestnetMode} - onChange={toggleTestnetMode} + onChange={handleTestnetToggle} /> ) diff --git a/packages/arb-token-bridge-ui/src/components/syncers/useBalanceUpdater.tsx b/packages/arb-token-bridge-ui/src/components/syncers/useBalanceUpdater.tsx index 7b20f2cfa7..4d35c3f149 100644 --- a/packages/arb-token-bridge-ui/src/components/syncers/useBalanceUpdater.tsx +++ b/packages/arb-token-bridge-ui/src/components/syncers/useBalanceUpdater.tsx @@ -2,14 +2,16 @@ import { useInterval, useLatest } from 'react-use' import { useAccount } from 'wagmi' import { useAppState } from '../../state' -import { useUpdateUsdcBalances } from '../../hooks/CCTP/useUpdateUsdcBalances' import { isTokenNativeUSDC } from '../../util/TokenUtils' +import { useSelectedToken } from '../../hooks/useSelectedToken' +import { useUpdateUsdcBalances } from '../../hooks/CCTP/useUpdateUsdcBalances' // Updates all balances periodically export function useBalanceUpdater() { const { - app: { arbTokenBridge, selectedToken } + app: { arbTokenBridge } } = useAppState() + const [selectedToken] = useSelectedToken() const { address: walletAddress } = useAccount() const latestTokenBridge = useLatest(arbTokenBridge) diff --git a/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUsdcBalances.ts b/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUsdcBalances.ts index dde96b1a69..a1109eaf0b 100644 --- a/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUsdcBalances.ts +++ b/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUsdcBalances.ts @@ -99,6 +99,11 @@ export function useUpdateUsdcBalances({ const updateUsdcBalances = useCallback(() => { const parentUsdcAddress = getParentUsdcAddress(parentChain.id) + const { + isEthereumMainnet: isParentEthereumMainnet, + isSepolia: isParentSepolia + } = isNetwork(parentChain.id) + // USDC is not native for the selected networks, do nothing if (!parentUsdcAddress) { return @@ -113,6 +118,14 @@ export function useUpdateUsdcBalances({ if (childUsdcAddress) { updateErc20ChildBalance([childUsdcAddress.toLowerCase()]) } + + if (isParentEthereumMainnet) { + updateErc20ChildBalance([CommonAddress.ArbitrumOne['USDC.e']]) + } + + if (isParentSepolia) { + updateErc20ChildBalance([CommonAddress.ArbitrumSepolia['USDC.e']]) + } }, [ isLoading, childUsdcAddress, diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts index bb14accc4c..10d45e16a3 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts @@ -5,9 +5,9 @@ import { useAccount, useSigner } from 'wagmi' import { DepositGasEstimates, GasEstimates } from '../arbTokenBridge.types' import { BridgeTransferStarterFactory } from '@/token-bridge-sdk/BridgeTransferStarterFactory' import { getProviderForChainId } from '@/token-bridge-sdk/utils' -import { useAppState } from '../../state' import { useBalanceOnSourceChain } from '../useBalanceOnSourceChain' import { useNetworks } from '../useNetworks' +import { useSelectedToken } from '../useSelectedToken' import { useArbQueryParams } from '../useArbQueryParams' async function fetcher([ @@ -55,12 +55,10 @@ export function useGasEstimates({ error: any } { const [{ sourceChain, destinationChain }] = useNetworks() + const [selectedToken] = useSelectedToken() const [{ destinationAddress }] = useArbQueryParams() - const { - app: { selectedToken: token } - } = useAppState() const { address: walletAddress } = useAccount() - const balance = useBalanceOnSourceChain(token) + const balance = useBalanceOnSourceChain(selectedToken) const { data: signer } = useSigner() const amountToTransfer = diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasSummary.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasSummary.ts index 7dfdb5fa9c..669b41ecac 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasSummary.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasSummary.ts @@ -2,7 +2,6 @@ import { constants, utils } from 'ethers' import { useMemo } from 'react' import { useDebounce } from '@uidotdev/usehooks' -import { useAppState } from '../../state' import { useGasPrice } from '../useGasPrice' import { isTokenArbitrumSepoliaNativeUSDC, @@ -18,6 +17,7 @@ import { truncateExtraDecimals } from '../../util/NumberUtils' import { useSelectedTokenDecimals } from './useSelectedTokenDecimals' import { percentIncrease } from '@/token-bridge-sdk/utils' import { DEFAULT_GAS_PRICE_PERCENT_INCREASE } from '@/token-bridge-sdk/Erc20DepositStarter' +import { useSelectedToken } from '../useSelectedToken' export type GasEstimationStatus = | 'loading' @@ -33,9 +33,7 @@ export type UseGasSummaryResult = { } export function useGasSummary(): UseGasSummaryResult { - const { - app: { selectedToken: token } - } = useAppState() + const [selectedToken] = useSelectedToken() const [networks] = useNetworks() const { childChainProvider, parentChainProvider, isDepositMode } = useNetworksRelationship(networks) @@ -58,20 +56,20 @@ export function useGasSummary(): UseGasSummaryResult { const parentChainGasPrice = useGasPrice({ provider: parentChainProvider }) const childChainGasPrice = useGasPrice({ provider: childChainProvider }) - const balance = useBalanceOnSourceChain(token) + const balance = useBalanceOnSourceChain(selectedToken) const { gasEstimates: estimateGasResult, error: gasEstimatesError } = useGasEstimates({ amount: amountBigNumber, sourceChainErc20Address: isDepositMode - ? token?.address - : isTokenArbitrumOneNativeUSDC(token?.address) || - isTokenArbitrumSepoliaNativeUSDC(token?.address) - ? token?.address - : token?.l2Address, + ? selectedToken?.address + : isTokenArbitrumOneNativeUSDC(selectedToken?.address) || + isTokenArbitrumSepoliaNativeUSDC(selectedToken?.address) + ? selectedToken?.address + : selectedToken?.l2Address, destinationChainErc20Address: isDepositMode - ? token?.l2Address - : token?.address + ? selectedToken?.l2Address + : selectedToken?.address }) const estimatedParentChainGasFees = useMemo(() => { @@ -119,8 +117,8 @@ export function useGasSummary(): UseGasSummaryResult { const gasSummary: UseGasSummaryResult = useMemo(() => { if ( !isDepositMode && - (isTokenArbitrumOneNativeUSDC(token?.address) || - isTokenArbitrumSepoliaNativeUSDC(token?.address)) + (isTokenArbitrumOneNativeUSDC(selectedToken?.address) || + isTokenArbitrumSepoliaNativeUSDC(selectedToken?.address)) ) { return { status: 'unavailable', @@ -160,7 +158,7 @@ export function useGasSummary(): UseGasSummaryResult { } }, [ isDepositMode, - token?.address, + selectedToken?.address, balance, amountBigNumber, estimatedParentChainGasFees, diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts index 9ef565c058..a58766417e 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts @@ -1,14 +1,12 @@ -import { useAppState } from '../../state' import { isTokenNativeUSDC } from '../../util/TokenUtils' import { useNetworks } from '../useNetworks' import { useNetworksRelationship } from '../useNetworksRelationship' +import { useSelectedToken } from '../useSelectedToken' export const useIsBatchTransferSupported = () => { const [networks] = useNetworks() const { isDepositMode, isTeleportMode } = useNetworksRelationship(networks) - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() if (!selectedToken) { return false diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenBalances.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenBalances.ts index 5d17483f7a..eb1683c18f 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenBalances.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenBalances.ts @@ -1,6 +1,5 @@ import { BigNumber, constants } from 'ethers' import { useMemo } from 'react' -import { useAppState } from '../../state' import { useNetworks } from '../useNetworks' import { isTokenArbitrumOneNativeUSDC, @@ -8,6 +7,7 @@ import { } from '../../util/TokenUtils' import { CommonAddress } from '../../util/CommonAddressUtils' import { isNetwork } from '../../util/networks' +import { useSelectedToken } from '../useSelectedToken' import { useBalances } from '../useBalances' import { useNetworksRelationship } from '../useNetworksRelationship' @@ -17,8 +17,7 @@ export type Balances = { } export function useSelectedTokenBalances(): Balances { - const { app } = useAppState() - const { selectedToken } = app + const [selectedToken] = useSelectedToken() const [networks] = useNetworks() const { isDepositMode } = useNetworksRelationship(networks) diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenDecimals.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenDecimals.ts index 16a0043a58..83d4a6ef25 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenDecimals.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenDecimals.ts @@ -1,10 +1,8 @@ -import { useAppState } from '../../state' +import { useSelectedToken } from '../useSelectedToken' import { useSourceChainNativeCurrencyDecimals } from '../useSourceChainNativeCurrencyDecimals' export function useSelectedTokenDecimals() { - const { - app: { selectedToken } - } = useAppState() + const [selectedToken] = useSelectedToken() const nativeCurrencyDecimalsOnSourceChain = useSourceChainNativeCurrencyDecimals() diff --git a/packages/arb-token-bridge-ui/src/hooks/__tests__/useSelectedTokenBalances.test.ts b/packages/arb-token-bridge-ui/src/hooks/__tests__/useSelectedTokenBalances.test.ts index 9e93745464..564e5fae11 100644 --- a/packages/arb-token-bridge-ui/src/hooks/__tests__/useSelectedTokenBalances.test.ts +++ b/packages/arb-token-bridge-ui/src/hooks/__tests__/useSelectedTokenBalances.test.ts @@ -6,9 +6,12 @@ import { getWagmiChain } from '../../util/wagmi/getWagmiChain' import { useNetworks } from '../useNetworks' import { useBalances } from '../useBalances' import { useSelectedTokenBalances } from '../TransferPanel/useSelectedTokenBalances' -import { useAppState } from '../../state' +import { useSelectedToken } from '../useSelectedToken' import { ChainId } from '../../types/ChainId' +type BridgeToken = NonNullable[0]> +const Erc20Type = 'ERC20' as BridgeToken['type'] + jest.mock('../useNetworks', () => ({ useNetworks: jest.fn() })) @@ -17,26 +20,25 @@ jest.mock('../useBalances', () => ({ useBalances: jest.fn() })) -jest.mock('../../state', () => ({ - useAppState: jest.fn().mockReturnValue({ - app: { - selectedToken: { - type: 'ERC20', - decimals: 18, - name: 'random', - symbol: 'RAND', - address: '0x123', - l2Address: '0x234', - listIds: new Set('1') - } - } - }) +jest.mock('../useSelectedToken', () => ({ + useSelectedToken: jest.fn().mockReturnValue([ + { + type: 'ERC20', + decimals: 18, + name: 'random', + symbol: 'RAND', + address: '0x123', + l2Address: '0x234', + listIds: new Set('1') + }, + jest.fn() + ]) })) describe('useSelectedTokenBalances', () => { const mockedUseNetworks = jest.mocked(useNetworks) const mockedUseBalances = jest.mocked(useBalances) - const mockedUseAppState = jest.mocked(useAppState) + const mockedUseSelectedToken = jest.mocked(useSelectedToken) beforeAll(() => { mockedUseBalances.mockReturnValue({ @@ -91,18 +93,17 @@ describe('useSelectedTokenBalances', () => { }) it('should return ERC20 parent balance as source balance and zero as destination balance when source chain is Sepolia and destination chain is Arbitrum Sepolia, and selected token address on Sepolia is 0x222 but without child chain address (unbridged token)', () => { - mockedUseAppState.mockReturnValueOnce({ - app: { - selectedToken: { - type: 'ERC20', - decimals: 18, - name: 'random', - symbol: 'RAND', - address: '0x222', - listIds: new Set('2') - } - } - }) + mockedUseSelectedToken.mockReturnValueOnce([ + { + type: Erc20Type, + decimals: 18, + name: 'random', + symbol: 'RAND', + address: '0x222', + listIds: new Set('2') + }, + jest.fn() + ]) mockedUseNetworks.mockReturnValue([ { @@ -122,18 +123,17 @@ describe('useSelectedTokenBalances', () => { }) it('should return zero as source balance and ERC20 parent balance as destination balance when source chain is Arbitrum Sepolia and destination chain is Sepolia, and selected token address on Sepolia is 0x222 but without child chain address (unbridged token)', () => { - mockedUseAppState.mockReturnValueOnce({ - app: { - selectedToken: { - type: 'ERC20', - decimals: 18, - name: 'random', - symbol: 'RAND', - address: '0x222', - listIds: new Set('2') - } - } - }) + mockedUseSelectedToken.mockReturnValueOnce([ + { + type: Erc20Type, + decimals: 18, + name: 'random', + symbol: 'RAND', + address: '0x222', + listIds: new Set('2') + }, + jest.fn() + ]) mockedUseNetworks.mockReturnValue([ { @@ -153,11 +153,7 @@ describe('useSelectedTokenBalances', () => { }) it('should return null as source balance and null as destination balance when source chain is Sepolia and destination chain is Arbitrum Sepolia, and selected token is null', () => { - mockedUseAppState.mockReturnValueOnce({ - app: { - selectedToken: null - } - }) + mockedUseSelectedToken.mockReturnValueOnce([null, jest.fn()]) mockedUseNetworks.mockReturnValue([ { @@ -177,11 +173,7 @@ describe('useSelectedTokenBalances', () => { }) it('should return null as source balance and null as destination balance when source chain is Arbitrum Sepolia and destination chain is Sepolia, and selected token is null', () => { - mockedUseAppState.mockReturnValueOnce({ - app: { - selectedToken: null - } - }) + mockedUseSelectedToken.mockReturnValueOnce([null, jest.fn()]) mockedUseNetworks.mockReturnValue([ { diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx b/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx index 0d61bd7394..dbabe4e211 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx +++ b/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx @@ -51,7 +51,7 @@ export const useArbQueryParams = () => { amount: withDefault(AmountQueryParam, ''), // amount which is filled in Transfer panel amount2: withDefault(AmountQueryParam, ''), // extra eth to send together with erc20 destinationAddress: withDefault(StringParam, undefined), - token: StringParam, // import a new token using a Dialog Box + token: TokenQueryParam, // import a new token using a Dialog Box settingsOpen: withDefault(BooleanParam, false) }) } @@ -109,6 +109,18 @@ export const AmountQueryParam = { } } +const TokenQueryParam = { + encode: (token: string | undefined) => { + return token?.toLowerCase() + }, + decode: (token: string | (string | null)[] | null | undefined) => { + const tokenStr = token?.toString() + // We are not checking for a valid address because we handle it in the UI + // by showing an invalid token dialog + return tokenStr?.toLowerCase() + } +} + // Parse chainId to ChainQueryParam or ChainId for orbit chain export function encodeChainQueryParam( chainId: number | null | undefined diff --git a/packages/arb-token-bridge-ui/src/hooks/useSelectedToken.ts b/packages/arb-token-bridge-ui/src/hooks/useSelectedToken.ts new file mode 100644 index 0000000000..503855e1d4 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/hooks/useSelectedToken.ts @@ -0,0 +1,187 @@ +import { useCallback } from 'react' +import { utils } from 'ethers' +import useSWRImmutable from 'swr/immutable' +import { Provider } from '@ethersproject/providers' +import { + getChainIdFromProvider, + getProviderForChainId +} from '@/token-bridge-sdk/utils' + +import { ERC20BridgeToken, TokenType } from './arbTokenBridge.types' +import { + getL2ERC20Address, + isTokenArbitrumOneNativeUSDC, + isTokenArbitrumSepoliaNativeUSDC, + isTokenMainnetUSDC, + isTokenNativeUSDC, + isTokenSepoliaUSDC +} from '../util/TokenUtils' +import { useNetworks } from './useNetworks' +import { isNetwork } from '../util/networks' +import { CommonAddress } from '../util/CommonAddressUtils' +import { useNetworksRelationship } from './useNetworksRelationship' +import { + useTokensFromLists, + useTokensFromUser +} from '../components/TransferPanel/TokenSearchUtils' +import { useArbQueryParams } from './useArbQueryParams' + +const commonUSDC = { + name: 'USD Coin', + type: TokenType.ERC20, + symbol: 'USDC', + decimals: 6, + listIds: new Set() +} + +export const useSelectedToken = () => { + const [{ token: tokenFromSearchParams }, setQueryParams] = useArbQueryParams() + const [networks] = useNetworks() + const { childChain, parentChain } = useNetworksRelationship(networks) + const tokensFromLists = useTokensFromLists() + const tokensFromUser = useTokensFromUser() + + const { data: usdcToken } = useSWRImmutable( + [ + tokenFromSearchParams, + parentChain.id, + childChain.id, + 'useSelectedToken_usdc' + ], + async ([_tokenAddress, _parentChainId, _childChainId]) => { + if (!_tokenAddress) { + return null + } + + if (!isTokenNativeUSDC(_tokenAddress)) { + return null + } + + const parentProvider = getProviderForChainId(_parentChainId) + const childProvider = getProviderForChainId(_childChainId) + + return getUsdcToken({ + tokenAddress: _tokenAddress, + parentProvider, + childProvider + }) + } + ) + + const setSelectedToken = useCallback( + (erc20ParentAddress: string | null) => + setQueryParams({ token: sanitizeTokenAddress(erc20ParentAddress) }), + [setQueryParams] + ) + + if (!tokenFromSearchParams) { + return [null, setSelectedToken] as const + } + + return [ + tokensFromUser[tokenFromSearchParams] || + tokensFromLists[tokenFromSearchParams] || + usdcToken || + null, + setSelectedToken + ] as const +} + +function sanitizeTokenAddress(tokenAddress: string | null): string | undefined { + if (!tokenAddress) { + return undefined + } + if (utils.isAddress(tokenAddress)) { + return tokenAddress + } + return undefined +} + +async function getUsdcToken({ + tokenAddress, + parentProvider, + childProvider +}: { + tokenAddress: string + parentProvider: Provider + childProvider: Provider +}): Promise { + const parentChainId = await getChainIdFromProvider(parentProvider) + const childChainId = await getChainIdFromProvider(childProvider) + + const { + isEthereumMainnet: isParentChainEthereumMainnet, + isSepolia: isParentChainSepolia, + isArbitrumOne: isParentChainArbitrumOne, + isArbitrumSepolia: isParentChainArbitrumSepolia + } = isNetwork(parentChainId) + + // Ethereum Mainnet USDC + if (isTokenMainnetUSDC(tokenAddress) && isParentChainEthereumMainnet) { + return { + ...commonUSDC, + address: CommonAddress.Ethereum.USDC, + l2Address: CommonAddress.ArbitrumOne['USDC.e'] + } + } + + // Ethereum Sepolia USDC + if (isTokenSepoliaUSDC(tokenAddress) && isParentChainSepolia) { + return { + ...commonUSDC, + address: CommonAddress.Sepolia.USDC, + l2Address: CommonAddress.ArbitrumSepolia['USDC.e'] + } + } + + // Arbitrum One USDC when Ethereum is the parent chain + if ( + isTokenArbitrumOneNativeUSDC(tokenAddress) && + isParentChainEthereumMainnet + ) { + return { + ...commonUSDC, + address: CommonAddress.ArbitrumOne.USDC, + l2Address: CommonAddress.ArbitrumOne.USDC + } + } + + // Arbitrum Sepolia USDC when Ethereum is the parent chain + if (isTokenArbitrumSepoliaNativeUSDC(tokenAddress) && isParentChainSepolia) { + return { + ...commonUSDC, + address: CommonAddress.ArbitrumSepolia.USDC, + l2Address: CommonAddress.ArbitrumSepolia.USDC + } + } + + // Arbitrum USDC with Orbit chains + if ( + (isTokenArbitrumOneNativeUSDC(tokenAddress) && isParentChainArbitrumOne) || + (isTokenArbitrumSepoliaNativeUSDC(tokenAddress) && + isParentChainArbitrumSepolia) + ) { + let childChainUsdcAddress + try { + childChainUsdcAddress = isNetwork(childChainId).isOrbitChain + ? ( + await getL2ERC20Address({ + erc20L1Address: tokenAddress, + l1Provider: parentProvider, + l2Provider: childProvider + }) + ).toLowerCase() + : undefined + } catch { + // could be never bridged before + } + + return { + ...commonUSDC, + address: tokenAddress, + l2Address: childChainUsdcAddress + } + } + + return null +} diff --git a/packages/arb-token-bridge-ui/src/state/app/actions.ts b/packages/arb-token-bridge-ui/src/state/app/actions.ts index ee95ce057d..8b68fe1ae1 100644 --- a/packages/arb-token-bridge-ui/src/state/app/actions.ts +++ b/packages/arb-token-bridge-ui/src/state/app/actions.ts @@ -1,7 +1,4 @@ -import { - ArbTokenBridge, - ERC20BridgeToken -} from '../../hooks/arbTokenBridge.types' +import { ArbTokenBridge } from '../../hooks/arbTokenBridge.types' import { Context } from '..' import { ConnectionState } from '../../util' import { WarningTokens } from './state' @@ -21,24 +18,7 @@ export const setChainIds = ( state.app.l2NetworkChainId = payload.l2NetworkChainId } -export const setSelectedToken = ( - { state }: Context, - token: ERC20BridgeToken | null -) => { - state.app.selectedToken = token ? { ...token } : null -} - -export const reset = ({ state }: Context, newChainId: number) => { - if ( - state.app.l1NetworkChainId !== newChainId && - state.app.l2NetworkChainId !== newChainId - ) { - // only reset the selected token if we are not switching between the pair of l1-l2 networks. - // we dont want to reset the token if we are switching from Mainnet to Arbitrum One for example - // because we are maybe in the process of auto switching the network and triggering deposit or withdraw - state.app.selectedToken = null - } - +export const reset = ({ state }: Context) => { state.app.arbTokenBridge = {} as ArbTokenBridge state.app.connectionState = ConnectionState.LOADING state.app.arbTokenBridgeLoaded = false diff --git a/packages/arb-token-bridge-ui/src/state/app/state.ts b/packages/arb-token-bridge-ui/src/state/app/state.ts index d962b01372..5c799cd7c3 100644 --- a/packages/arb-token-bridge-ui/src/state/app/state.ts +++ b/packages/arb-token-bridge-ui/src/state/app/state.ts @@ -2,7 +2,6 @@ import { BigNumber } from 'ethers' import { ArbTokenBridge, AssetType, - ERC20BridgeToken, NodeBlockDeadlineStatus } from '../../hooks/arbTokenBridge.types' import { @@ -86,7 +85,6 @@ export type AppState = { arbTokenBridge: ArbTokenBridge warningTokens: WarningTokens connectionState: number - selectedToken: ERC20BridgeToken | null l1NetworkChainId: number | null l2NetworkChainId: number | null arbTokenBridgeLoaded: boolean @@ -98,7 +96,6 @@ export const defaultState: AppState = { connectionState: ConnectionState.LOADING, l1NetworkChainId: null, l2NetworkChainId: null, - selectedToken: null, arbTokenBridgeLoaded: false } export const state: AppState = { diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/depositCctp.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/depositCctp.cy.ts index ff663af7a0..713c577e48 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/depositCctp.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/depositCctp.cy.ts @@ -86,7 +86,7 @@ describe('Deposit USDC through CCTP', () => { }) it('should initiate depositing USDC to the same address through CCTP successfully', () => { - cy.clickMoveFundsButton().click() + cy.clickMoveFundsButton({ shouldConfirmInMetamask: false }) confirmAndApproveCctpDeposit() cy.confirmSpending(USDCAmountToSend.toString()) @@ -122,7 +122,7 @@ describe('Deposit USDC through CCTP', () => { */ it.skip('should initiate depositing USDC to custom destination address through CCTP successfully', () => { cy.fillCustomDestinationAddress() - cy.clickMoveFundsButton().click() + cy.clickMoveFundsButton({ shouldConfirmInMetamask: false }) confirmAndApproveCctpDeposit() cy.confirmSpending(USDCAmountToSend.toString()) diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts index b4804bbea2..0037d69fdb 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts @@ -2,24 +2,49 @@ * When user enters the page with query params on URL */ +import { utils } from 'ethers' +import { scaleFrom18DecimalsToNativeTokenDecimals } from '@arbitrum/sdk' import { formatAmount } from '../../../src/util/NumberUtils' import { + getInitialERC20Balance, getInitialETHBalance, - getL1NetworkName, - getL2NetworkName, + getL1NetworkConfig, + getL2NetworkConfig, visitAfterSomeDelay } from '../../support/common' describe('User enters site with query params on URL', () => { let l1ETHbal: number + const nativeTokenSymbol = Cypress.env('NATIVE_TOKEN_SYMBOL') + const nativeTokenDecimals = Cypress.env('NATIVE_TOKEN_DECIMALS') + const isCustomFeeToken = nativeTokenSymbol !== 'ETH' + + const balanceBuffer = scaleFrom18DecimalsToNativeTokenDecimals({ + amount: utils.parseEther('0.001'), + decimals: nativeTokenDecimals + }) + // when all of our tests need to run in a logged-in state // we have to make sure we preserve a healthy LocalStorage state // because it is cleared between each `it` cypress test before(() => { - getInitialETHBalance( - Cypress.env('ETH_RPC_URL'), - Cypress.env('ADDRESS') - ).then(val => (l1ETHbal = parseFloat(formatAmount(val, { decimals: 18 })))) + if (isCustomFeeToken) { + getInitialERC20Balance({ + tokenAddress: Cypress.env('NATIVE_TOKEN_ADDRESS'), + multiCallerAddress: getL1NetworkConfig().multiCall, + address: Cypress.env('ADDRESS'), + rpcURL: Cypress.env('ETH_RPC_URL') + }).then( + val => + (l1ETHbal = Number( + formatAmount(val, { decimals: nativeTokenDecimals }) + )) + ) + } else { + getInitialETHBalance(Cypress.env('ETH_RPC_URL')).then( + val => (l1ETHbal = Number(formatAmount(val))) + ) + } cy.login({ networkType: 'parentChain' }) }) @@ -31,8 +56,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: 'max', - sourceChain: getL1NetworkName(), - destinationChain: getL2NetworkName() + sourceChain: getL1NetworkConfig().networkName, + destinationChain: getL2NetworkConfig().networkName } }) @@ -50,7 +75,8 @@ describe('User enters site with query params on URL', () => { }) cy.findAmountInput().should($el => { const amount = parseFloat(String($el.val())) - expect(amount).to.be.lt(Number(l1ETHbal)) + // Add a little buffer since we round down in the UI + expect(amount).to.be.lt(Number(l1ETHbal) + Number(balanceBuffer)) }) } ) @@ -60,8 +86,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: 'MAX', - sourceChain: getL1NetworkName(), - destinationChain: getL2NetworkName() + sourceChain: getL1NetworkConfig().networkName, + destinationChain: getL2NetworkConfig().networkName } }) @@ -83,7 +109,7 @@ describe('User enters site with query params on URL', () => { ) cy.findAmountInput().should($el => { const amount = parseFloat(String($el.val())) - expect(amount).to.be.lt(Number(l1ETHbal)) + expect(amount).to.be.lt(Number(l1ETHbal) + Number(balanceBuffer)) }) } ) @@ -93,8 +119,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: 'MaX', - sourceChain: getL1NetworkName(), - destinationChain: getL2NetworkName() + sourceChain: getL1NetworkConfig().networkName, + destinationChain: getL2NetworkConfig().networkName } }) @@ -121,7 +147,7 @@ describe('User enters site with query params on URL', () => { ) cy.findAmountInput().should($el => { const amount = parseFloat(String($el.val())) - expect(amount).to.be.lt(Number(l1ETHbal)) + expect(amount).to.be.lt(Number(l1ETHbal) + Number(balanceBuffer)) }) } ) @@ -129,8 +155,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '56', - sourceChain: getL1NetworkName(), - destinationChain: getL2NetworkName() + sourceChain: getL1NetworkConfig().networkName, + destinationChain: getL2NetworkConfig().networkName } }) @@ -140,8 +166,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '1.6678', - sourceChain: getL1NetworkName(), - destinationChain: getL2NetworkName() + sourceChain: getL1NetworkConfig().networkName, + destinationChain: getL2NetworkConfig().networkName } }) @@ -151,8 +177,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '6', - sourceChain: getL1NetworkName(), - destinationChain: getL2NetworkName() + sourceChain: getL1NetworkConfig().networkName, + destinationChain: getL2NetworkConfig().networkName } }) @@ -162,8 +188,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '0.123', - sourceChain: getL1NetworkName(), - destinationChain: getL2NetworkName() + sourceChain: getL1NetworkConfig().networkName, + destinationChain: getL2NetworkConfig().networkName } }) @@ -174,8 +200,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '-0.123', - sourceChain: getL1NetworkName(), - destinationChain: getL2NetworkName() + sourceChain: getL1NetworkConfig().networkName, + destinationChain: getL2NetworkConfig().networkName } }) @@ -185,8 +211,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: 'asdfs', - sourceChain: getL1NetworkName(), - destinationChain: getL2NetworkName() + sourceChain: getL1NetworkConfig().networkName, + destinationChain: getL2NetworkConfig().networkName } }) @@ -196,8 +222,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '0', - sourceChain: getL1NetworkName(), - destinationChain: getL2NetworkName() + sourceChain: getL1NetworkConfig().networkName, + destinationChain: getL2NetworkConfig().networkName } }) @@ -207,8 +233,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '0.0001', - sourceChain: getL1NetworkName(), - destinationChain: getL2NetworkName() + sourceChain: getL1NetworkConfig().networkName, + destinationChain: getL2NetworkConfig().networkName } }) @@ -218,8 +244,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '123,3,43', - sourceChain: getL1NetworkName(), - destinationChain: getL2NetworkName() + sourceChain: getL1NetworkConfig().networkName, + destinationChain: getL2NetworkConfig().networkName } }) @@ -231,14 +257,26 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '0, 123.222, 0.3', - sourceChain: getL1NetworkName(), - destinationChain: getL2NetworkName() + sourceChain: getL1NetworkConfig().networkName, + destinationChain: getL2NetworkConfig().networkName } }) cy.findAmountInput().should('be.empty') } ) + context('should select token using query params', () => { + visitAfterSomeDelay('/', { + qs: { + sourceChain: 'sepolia', + destinationChain: 'arbitrum-sepolia', + // Arbitrum token on Sepolia + token: '0xfa898e8d38b008f3bac64dce019a9480d4f06863' + } + }) + + cy.findSelectTokenButton('ARB') + }) }) }) diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawCctp.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawCctp.cy.ts index 15591ee460..82fc516808 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawCctp.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawCctp.cy.ts @@ -72,7 +72,7 @@ describe('Withdraw USDC through CCTP', () => { 'be.visible' ) cy.findGasFeeForChain(/You'll have to pay Sepolia gas fee upon claiming./i) - cy.clickMoveFundsButton().click() + cy.clickMoveFundsButton({ shouldConfirmInMetamask: false }) confirmAndApproveCctpWithdrawal() cy.confirmSpending(USDCAmountToSend.toString()) @@ -109,7 +109,7 @@ describe('Withdraw USDC through CCTP', () => { ) cy.findGasFeeForChain(/You'll have to pay Sepolia gas fee upon claiming./i) cy.fillCustomDestinationAddress() - cy.clickMoveFundsButton().click() + cy.clickMoveFundsButton({ shouldConfirmInMetamask: false }) confirmAndApproveCctpWithdrawal() cy.confirmSpending(USDCAmountToSend.toString())