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 9d7a24bd96..1c3f270e49 100644 --- a/packages/arb-token-bridge-ui/src/components/App/App.tsx +++ b/packages/arb-token-bridge-ui/src/components/App/App.tsx @@ -9,24 +9,20 @@ import { useConnectModal } from '@rainbow-me/rainbowkit' import merge from 'lodash-es/merge' -import axios from 'axios' import { createOvermind, Overmind } from 'overmind' import { Provider } from 'overmind-react' +import { useInterval } from 'react-use' import { useLocalStorage } from '@uidotdev/usehooks' -import { ConnectionState } from '../../util' -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, useAppState } from '../../state' import { MainContent } from '../MainContent/MainContent' -import { ArbTokenBridgeStoreSync } from '../syncers/ArbTokenBridgeStoreSync' -import { BalanceUpdater } from '../syncers/BalanceUpdater' import { TokenListSyncer } from '../syncers/TokenListSyncer' import { Header } from '../common/Header' import { HeaderAccountPopover } from '../common/HeaderAccountPopover' -import { getNetworkName, isNetwork, rpcURLs } from '../../util/networks' +import { getNetworkName, rpcURLs } from '../../util/networks' import { ArbQueryParamProvider, useArbQueryParams @@ -34,14 +30,15 @@ import { 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' import { ProviderName, trackEvent } from '../../util/AnalyticsUtils' +import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' import { onDisconnectHandler } from '../../util/walletConnectUtils' import { addressIsSmartContract } from '../../util/AddressUtils' +import { useCCTPIsBlocked } from '../../hooks/CCTP/useCCTPIsBlocked' +import { useWarningTokensList } from '../../hooks/useWarningTokensList' declare global { interface Window { @@ -58,113 +55,6 @@ const rainbowkitTheme = merge(darkTheme(), { } } as Theme) -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() - - 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) - - const { - isArbitrum: isConnectedToArbitrum, - isOrbitChain: isConnectedToOrbitChain - } = isNetwork(networks.sourceChain.id) - const isParentChainEthereum = isNetwork( - parentChain.id - ).isEthereumMainnetOrTestnet - - actions.app.reset(networks.sourceChain.id) - actions.app.setChainIds({ - l1NetworkChainId: parentChain.id, - l2NetworkChainId: childChain.id - }) - - if ( - (isParentChainEthereum && isConnectedToArbitrum) || - isConnectedToOrbitChain - ) { - console.info('Withdrawal mode detected:') - actions.app.setConnectionState(ConnectionState.L2_CONNECTED) - } else { - console.info('Deposit mode detected:') - actions.app.setConnectionState(ConnectionState.L1_CONNECTED) - } - - setTokenBridgeParams({ - l1: { - network: parentChain, - provider: parentChainProvider - }, - l2: { - network: childChain, - provider: childChainProvider - } - }) - }, [ - networks.sourceChain.id, - parentChain.id, - childChain.id, - parentChain, - childChain, - parentChainProvider, - childChainProvider - ]) - - useEffect(() => { - axios - .get( - 'https://raw.githubusercontent.com/OffchainLabs/arb-token-lists/aff40a59608678cfd9b034dd198011c90b65b8b6/src/WarningList/warningTokens.json' - ) - .then(res => { - actions.app.setWarningTokens(res.data) - }) - .catch(err => { - console.warn('Failed to fetch warning tokens:', err) - }) - }, []) - - if (!tokenBridgeParams) { - return null - } - - return -} - // connector names: https://github.com/wagmi-dev/wagmi/blob/b17c07443e407a695dfe9beced2148923b159315/docs/pages/core/connectors/_meta.en-US.json#L4 function getWalletName(connectorName: string): ProviderName { switch (connectorName) { @@ -203,6 +93,23 @@ function AppContent() { const { isBlocked } = useAccountIsBlocked() const [tosAccepted] = useLocalStorage(TOS_LOCALSTORAGE_KEY, false) const { openConnectModal } = useConnectModal() + const { + app: { selectedToken } + } = useAppState() + // We want to be sure this fetch is completed by the time we open the USDC modals + useCCTPIsBlocked() + + useWarningTokensList() + + const { + token: { updateTokenData } + } = useArbTokenBridge() + + useInterval(() => { + if (selectedToken) { + updateTokenData(selectedToken.address) + } + }, 10_000) useEffect(() => { if (tosAccepted && !isConnected) { @@ -280,8 +187,6 @@ function AppContent() { - - ) 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 eaa43c8007..2abb0e3d7d 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx @@ -63,18 +63,6 @@ export function TokenButton({ disabled={disabled} >
- {/* Commenting it out until we update the token image source files to be of better quality */} - {/* {tokenLogo && ( - // SafeImage is used for token logo, we don't know at buildtime - where those images will be loaded from // It would throw error - if it's loaded from external domains // eslint-disable-next-line - @next/next/no-img-element - Token logo - )} */} {tokenSymbol} {!disabled && ( { - await token.updateTokenData(_token.address) + await updateTokenData(_token.address) actions.app.setSelectedToken(_token) }, - [token, actions] + [updateTokenData, actions] ) useEffect(() => { @@ -233,39 +235,8 @@ export function TokenImportDialog({ searchForTokenInLists ]) - useEffect(() => { - if (!isOpen) { - return - } - - if (isL1AddressLoading && !l1Address) { - return - } - - const foundToken = tokensFromUser[l1Address || tokenAddress] - - if (typeof foundToken === 'undefined') { - return - } - - // Listen for the token to be added to the bridge so we can automatically select it - if (foundToken.address !== selectedToken?.address) { - onClose(true) - selectToken(foundToken) - } - }, [ - isL1AddressLoading, - tokenAddress, - isOpen, - l1Address, - onClose, - selectToken, - selectedToken, - tokensFromUser - ]) - async function storeNewToken(newToken: string) { - return token.add(newToken).catch((ex: Error) => { + return addToken(newToken).catch((ex: Error) => { setStatus(ImportStatus.ERROR) if (ex.name === 'TokenDisabledError') { @@ -295,9 +266,18 @@ export function TokenImportDialog({ selectToken(tokenToImport!) } else { // Token is not added to the bridge, so we add it - storeNewToken(l1Address).catch(() => { - setStatus(ImportStatus.ERROR) - }) + addToken(l1Address) + .then(() => { + onClose(true) + selectToken(tokenToImport!) + }) + .catch(ex => { + setStatus(ImportStatus.ERROR) + + if (ex.name === 'TokenDisabledError') { + warningToast('This token is currently paused in the bridge') + } + }) } // do not allow import of withdraw-only tokens at deposit mode diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx index 6017331763..aff6fa309a 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx @@ -7,7 +7,6 @@ import { import { Chain } from 'wagmi' import { Loader } from '../common/atoms/Loader' -import { useAppState } from '../../state' import { listIdsToNames, SPECIAL_ARBITRUM_TOKEN_TOKEN_LIST_ID @@ -30,6 +29,7 @@ import { useAccountType } from '../../hooks/useAccountType' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' +import { useBridgeTokensStore } from '../../hooks/useArbTokenBridge' import { TokenLogoFallback } from './TokenInfo' import { useBalanceOnSourceChain } from '../../hooks/useBalanceOnSourceChain' @@ -237,11 +237,7 @@ function ArbitrumTokenBadge() { } function TokenBalance({ token }: { token: ERC20BridgeToken | null }) { - const { - app: { - arbTokenBridge: { bridgeTokens } - } - } = useAppState() + const { bridgeTokens } = useBridgeTokensStore() const { isLoading: isLoadingAccountType } = useAccountType() const { balance, symbol } = useTokenInfo(token) 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 978b99a587..0012342667 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx @@ -5,11 +5,10 @@ import { useAccount } from 'wagmi' import { AutoSizer, List, ListRowProps } from 'react-virtualized' import { twMerge } from 'tailwind-merge' -import { useActions, useAppState } from '../../state' +import { useActions } from '../../state' import { BRIDGE_TOKEN_LISTS, BridgeTokenList, - addBridgeTokenListToBridge, SPECIAL_ARBITRUM_TOKEN_TOKEN_LIST_ID } from '../../util/TokenListUtils' import { @@ -40,6 +39,10 @@ import { useTransferDisabledDialogStore } from './TransferDisabledDialog' import { isWithdrawOnlyToken } from '../../util/WithdrawOnlyUtils' import { isTransferDisabledToken } from '../../util/TokenTransferDisabledUtils' import { useTokenFromSearchParams } from './TransferPanelUtils' +import { + useArbTokenBridge, + useBridgeTokensStore +} from '../../hooks/useArbTokenBridge' import { Switch } from '../common/atoms/Switch' import { isTeleportEnabledToken } from '../../util/TokenTeleportEnabledUtils' import { useBalances } from '../../hooks/useBalances' @@ -64,19 +67,19 @@ export const ARB_SEPOLIA_NATIVE_USDC_TOKEN = { function TokenListRow({ tokenList }: { tokenList: BridgeTokenList }) { const { - app: { arbTokenBridge } - } = useAppState() - const { bridgeTokens, token } = arbTokenBridge + token: { removeTokensFromList, addBridgeTokenListToBridge } + } = useArbTokenBridge() + const { bridgeTokens } = useBridgeTokensStore() const toggleTokenList = useCallback( (bridgeTokenList: BridgeTokenList, isActive: boolean) => { if (isActive) { - token.removeTokensFromList(bridgeTokenList.id) + removeTokensFromList(bridgeTokenList.id) } else { - addBridgeTokenListToBridge(bridgeTokenList, arbTokenBridge) + addBridgeTokenListToBridge(bridgeTokenList) } }, - [arbTokenBridge, token] + [removeTokensFromList, addBridgeTokenListToBridge] ) const isActive = Object.keys(bridgeTokens ?? []).some(address => { @@ -171,11 +174,10 @@ function TokensPanel({ onTokenSelected: (token: ERC20BridgeToken | null) => void }): JSX.Element { const { address: walletAddress } = useAccount() + const { bridgeTokens } = useBridgeTokensStore() const { - app: { - arbTokenBridge: { token, bridgeTokens } - } - } = useAppState() + token: { addL2NativeToken, add: addToken } + } = useArbTokenBridge() const [networks] = useNetworks() const { childChain, childChainProvider, parentChain, isDepositMode } = useNetworksRelationship(networks) @@ -383,7 +385,7 @@ function TokensPanel({ try { // Try to add the token as an L2-native token - token.addL2NativeToken(newToken) + addL2NativeToken(newToken) isSuccessful = true } catch (error) { // @@ -391,7 +393,7 @@ function TokensPanel({ try { // Try to add the token as a regular bridged token - await token.add(newToken) + await addToken(newToken) isSuccessful = true } catch (ex: any) { if (ex.name === 'TokenDisabledError') { @@ -518,11 +520,10 @@ export function TokenSearch({ close: () => void }) { const { address: walletAddress } = useAccount() + const { bridgeTokens } = useBridgeTokensStore() const { - app: { - arbTokenBridge: { token, bridgeTokens } - } - } = useAppState() + token: { updateTokenData } + } = useArbTokenBridge() const { app: { setSelectedToken } } = useActions() @@ -616,7 +617,7 @@ export function TokenSearch({ }) if (data) { - token.updateTokenData(_token.address) + updateTokenData(_token.address) setSelectedToken({ ...erc20DataToErc20BridgeToken(data), l2Address: _token.l2Address diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearchUtils.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearchUtils.ts index 1d2c279256..018ecaafbd 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearchUtils.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearchUtils.ts @@ -1,5 +1,4 @@ import { useMemo } from 'react' -import { useAppState } from '../../state' import { ContractStorage, ERC20BridgeToken, @@ -9,6 +8,7 @@ import { useTokenLists } from '../../hooks/useTokenLists' import { TokenListWithId } from '../../util/TokenListUtils' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { useNetworks } from '../../hooks/useNetworks' +import { useBridgeTokensStore } from '../../hooks/useArbTokenBridge' export function useTokensFromLists(): ContractStorage { const [networks] = useNetworks() @@ -25,11 +25,7 @@ export function useTokensFromLists(): ContractStorage { } export function useTokensFromUser(): ContractStorage { - const { - app: { - arbTokenBridge: { bridgeTokens } - } - } = useAppState() + const { bridgeTokens } = useBridgeTokensStore() return useMemo(() => { const storage: ContractStorage = {} 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 d3f2ac46a6..176ff1a919 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -65,6 +65,7 @@ import { getBridgeUiConfigForChain } from '../../util/bridgeUiConfig' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { CctpTransferStarter } from '@/token-bridge-sdk/CctpTransferStarter' +import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' import { BridgeTransferStarterFactory } from '@/token-bridge-sdk/BridgeTransferStarterFactory' import { BridgeTransfer, @@ -107,14 +108,11 @@ export function TransferPanel() { useState(false) const { - app: { - connectionState, - selectedToken, - arbTokenBridgeLoaded, - arbTokenBridge: { eth, token }, - warningTokens - } + app: { selectedToken, warningTokens } } = useAppState() + const { + token: { updateTokenData } + } = useArbTokenBridge() const { layout } = useAppContextState() const { isTransferring } = layout const { address: walletAddress, isConnected } = useAccount() @@ -151,8 +149,6 @@ export function TransferPanel() { const { isArbitrumOne, isArbitrumSepolia } = isNetwork(childChain.id) - const latestEth = useLatest(eth) - const isConnectedToArbitrum = useLatest(useIsConnectedToArbitrum()) const isConnectedToOrbitChain = useLatest(useIsConnectedToOrbitChain()) @@ -207,8 +203,7 @@ export function TransferPanel() { } useImportTokenModal({ - importTokenModalStatus, - connectionState + importTokenModalStatus }) const isBridgingANewStandardToken = useMemo(() => { @@ -670,9 +665,7 @@ export function TransferPanel() { // keep checking till we know the connected chain-pair are correct for transfer while ( depositRequiresChainSwitch() || - withdrawalRequiresChainSwitch() || - !latestEth.current || - !arbTokenBridgeLoaded + withdrawalRequiresChainSwitch() ) { await new Promise(r => setTimeout(r, 100)) } @@ -979,7 +972,7 @@ export function TransferPanel() { await Promise.all([updateEthParentBalance(), updateEthChildBalance()]) if (selectedToken) { - token.updateTokenData(selectedToken.address) + updateTokenData(selectedToken.address) } if (nativeCurrency.isCustom) { diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/hooks.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/hooks.ts index 4d748b11ca..53d5c1c517 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/hooks.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/hooks.ts @@ -10,6 +10,7 @@ import { useNetworksRelationship } from '../../../hooks/useNetworksRelationship' import { TokenType } from '../../../hooks/arbTokenBridge.types' import { CommonAddress } from '../../../util/CommonAddressUtils' import { isNetwork } from '../../../util/networks' +import { useArbTokenBridge } from '../../../hooks/useArbTokenBridge' const commonUSDC = { name: 'USD Coin', @@ -22,11 +23,11 @@ const commonUSDC = { export function useUpdateUSDCTokenData() { const actions = useActions() const { - app: { - arbTokenBridge: { token }, - selectedToken - } + app: { selectedToken } } = useAppState() + const { + token: { updateTokenData } + } = useArbTokenBridge() const [networks] = useNetworks() const { isDepositMode } = useNetworksRelationship(networks) const { @@ -47,7 +48,7 @@ export function useUpdateUSDCTokenData() { } if (isArbOneUSDC && isDestinationChainArbitrumOne) { - token.updateTokenData(CommonAddress.Ethereum.USDC) + updateTokenData(CommonAddress.Ethereum.USDC) actions.app.setSelectedToken({ ...commonUSDC, address: CommonAddress.Ethereum.USDC, @@ -56,7 +57,7 @@ export function useUpdateUSDCTokenData() { } if (isArbSepoliaUSDC && isDestinationChainArbitrumSepolia) { - token.updateTokenData(CommonAddress.Sepolia.USDC) + updateTokenData(CommonAddress.Sepolia.USDC) actions.app.setSelectedToken({ ...commonUSDC, address: CommonAddress.Sepolia.USDC, @@ -69,6 +70,6 @@ export function useUpdateUSDCTokenData() { isDestinationChainArbitrumOne, isDestinationChainArbitrumSepolia, selectedToken, - token + updateTokenData ]) } diff --git a/packages/arb-token-bridge-ui/src/components/syncers/ArbTokenBridgeStoreSync.tsx b/packages/arb-token-bridge-ui/src/components/syncers/ArbTokenBridgeStoreSync.tsx deleted file mode 100644 index b528a5c39c..0000000000 --- a/packages/arb-token-bridge-ui/src/components/syncers/ArbTokenBridgeStoreSync.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect } from 'react' -import { - useArbTokenBridge, - TokenBridgeParams -} from '../../hooks/useArbTokenBridge' - -import { useActions } from '../../state' - -// Syncs the arbTokenBridge data with the global store, so we dont have to drill with props but use store hooks to get data -export function ArbTokenBridgeStoreSync({ - tokenBridgeParams -}: { - tokenBridgeParams: TokenBridgeParams -}): JSX.Element { - const actions = useActions() - const arbTokenBridge = useArbTokenBridge(tokenBridgeParams) - - useEffect(() => { - actions.app.setArbTokenBridge(arbTokenBridge) - }, [arbTokenBridge]) - - return <> -} diff --git a/packages/arb-token-bridge-ui/src/components/syncers/BalanceUpdater.tsx b/packages/arb-token-bridge-ui/src/components/syncers/BalanceUpdater.tsx deleted file mode 100644 index 6a35e1cea2..0000000000 --- a/packages/arb-token-bridge-ui/src/components/syncers/BalanceUpdater.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect } from 'react' -import { useLatest } from 'react-use' -import { useAccount } from 'wagmi' - -import { useAppState } from '../../state' -import { useUpdateUSDCBalances } from '../../hooks/CCTP/useUpdateUSDCBalances' - -// Updates all balances periodically -const BalanceUpdater = (): JSX.Element => { - const { - app: { arbTokenBridge, selectedToken } - } = useAppState() - const { address: walletAddress } = useAccount() - const latestTokenBridge = useLatest(arbTokenBridge) - - const { updateUSDCBalances } = useUpdateUSDCBalances({ - walletAddress - }) - - useEffect(() => { - const interval = setInterval(() => { - updateUSDCBalances() - - if (selectedToken) { - latestTokenBridge?.current?.token?.updateTokenData( - selectedToken.address - ) - } - }, 10000) - - return () => clearInterval(interval) - }, [selectedToken]) - - return <> -} - -export { BalanceUpdater } diff --git a/packages/arb-token-bridge-ui/src/components/syncers/TokenListSyncer.tsx b/packages/arb-token-bridge-ui/src/components/syncers/TokenListSyncer.tsx index d2bdb11462..525726b514 100644 --- a/packages/arb-token-bridge-ui/src/components/syncers/TokenListSyncer.tsx +++ b/packages/arb-token-bridge-ui/src/components/syncers/TokenListSyncer.tsx @@ -3,27 +3,20 @@ import { useAccount } from 'wagmi' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' -import { useAppState } from '../../state' -import { - addBridgeTokenListToBridge, - BRIDGE_TOKEN_LISTS -} from '../../util/TokenListUtils' +import { BRIDGE_TOKEN_LISTS } from '../../util/TokenListUtils' +import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' // Adds whitelisted tokens to the bridge data on app load // In the token list we should show later only tokens with positive balances const TokenListSyncer = (): JSX.Element => { - const { - app: { arbTokenBridge, arbTokenBridgeLoaded } - } = useAppState() const { address: walletAddress } = useAccount() const [networks] = useNetworks() + const { + token: { addBridgeTokenListToBridge } + } = useArbTokenBridge() const { childChain } = useNetworksRelationship(networks) useEffect(() => { - if (!arbTokenBridgeLoaded) { - return - } - if (!walletAddress) { return } @@ -41,9 +34,9 @@ const TokenListSyncer = (): JSX.Element => { }) tokenListsToSet.forEach(bridgeTokenList => { - addBridgeTokenListToBridge(bridgeTokenList, arbTokenBridge) + addBridgeTokenListToBridge(bridgeTokenList) }) - }, [walletAddress, childChain.id, arbTokenBridgeLoaded]) + }, [walletAddress, childChain.id, addBridgeTokenListToBridge]) return <> } diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useImportTokenModal.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useImportTokenModal.ts index 32f4a04d69..32c3f1e674 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useImportTokenModal.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useImportTokenModal.ts @@ -1,26 +1,19 @@ import { useEffect } from 'react' import { ImportTokenModalStatus } from '../../components/TransferPanel/TransferPanelUtils' -import { ConnectionState } from '../../util' import { useTokenImportDialogStore } from '../../components/TransferPanel/TokenImportDialog' export function useImportTokenModal({ - importTokenModalStatus, - connectionState + importTokenModalStatus }: { importTokenModalStatus: ImportTokenModalStatus - connectionState: number }) { const { openDialog: openTokenImportDialog } = useTokenImportDialogStore() + useEffect(() => { if (importTokenModalStatus !== ImportTokenModalStatus.IDLE) { return } - if ( - connectionState === ConnectionState.L1_CONNECTED || - connectionState === ConnectionState.L2_CONNECTED - ) { - openTokenImportDialog() - } - }, [connectionState, importTokenModalStatus, openTokenImportDialog]) + openTokenImportDialog() + }, [importTokenModalStatus, openTokenImportDialog]) } diff --git a/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts b/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts index c4b4451e12..d419b80b7e 100644 --- a/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts +++ b/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts @@ -21,6 +21,7 @@ import { Transaction, ParentToChildMessageData } from './useTransactions' +import { BridgeTokenList } from '../util/TokenListUtils' export { OutgoingMessageState } @@ -148,6 +149,7 @@ export interface ArbTokenBridgeToken { add: (erc20L1orL2Address: string) => Promise addL2NativeToken: (erc20L2Address: string) => void addTokensFromList: (tokenList: TokenList, listID: number) => void + addBridgeTokenListToBridge: (tokenList: BridgeTokenList) => void removeTokensFromList: (listID: number) => void updateTokenData: (l1Address: string) => Promise triggerOutbox: (params: { diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index 955e15a097..616d81d316 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -1,4 +1,4 @@ -import { useCallback, useState, useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { Chain, useAccount } from 'wagmi' import { BigNumber } from 'ethers' import { Signer } from '@ethersproject/abstract-signer' @@ -11,6 +11,7 @@ import { ChildToParentTransactionEvent } from '@arbitrum/sdk' import { L2ToL1TransactionEvent as ClassicL2ToL1TransactionEvent } from '@arbitrum/sdk/dist/lib/abi/ArbSys' +import { create } from 'zustand' import useTransactions from './useTransactions' import { @@ -33,6 +34,9 @@ import { import { getL2NativeToken } from '../util/L2NativeUtils' import { CommonAddress } from '../util/CommonAddressUtils' import { isNetwork } from '../util/networks' +import { useNetworks } from './useNetworks' +import { useNetworksRelationship } from './useNetworksRelationship' +import { BridgeTokenList, fetchTokenListFromURL } from '../util/TokenListUtils' import { useDestinationAddressStore } from '../components/TransferPanel/AdvancedSettings' import { isValidTeleportChainPair } from '@/token-bridge-sdk/teleport' import { getProviderForChainId } from '@/token-bridge-sdk/utils' @@ -85,39 +89,52 @@ export interface TokenBridgeParams { l2: { provider: JsonRpcProvider; network: Chain } } -export const useArbTokenBridge = ( - params: TokenBridgeParams -): ArbTokenBridge => { - const { l1, l2 } = params +type BridgeTokens = ContractStorage | undefined +type BridgeTokensStore = { + bridgeTokens: BridgeTokens + setBridgeTokens: ( + fn: (prevBridgeTokens: BridgeTokens) => BridgeTokens + ) => void +} +export const useBridgeTokensStore = create(set => ({ + bridgeTokens: undefined, + setBridgeTokens: fn => { + set(state => ({ bridgeTokens: fn(state.bridgeTokens) })) + } +})) + +export const useArbTokenBridge = (): ArbTokenBridge => { + const [networks] = useNetworks() + const { childChain, childChainProvider, parentChain, parentChainProvider } = + useNetworksRelationship(networks) const { address: walletAddress } = useAccount() - const [bridgeTokens, setBridgeTokens] = useState< - ContractStorage | undefined - >(undefined) + const { bridgeTokens, setBridgeTokens } = useBridgeTokensStore() const { destinationAddress } = useDestinationAddressStore() const { erc20: [, updateErc20L1Balance] } = useBalance({ - chainId: l1.network.id, + chainId: parentChain.id, walletAddress }) const { erc20: [, updateErc20L2Balance] } = useBalance({ - chainId: l2.network.id, + chainId: childChain.id, walletAddress }) + const { erc20: [, updateErc20L1CustomDestinationBalance] } = useBalance({ - chainId: l1.network.id, + chainId: parentChain.id, walletAddress: destinationAddress }) const { erc20: [, updateErc20CustomDestinationL2Balance] } = useBalance({ - chainId: l2.network.id, + chainId: childChain.id, walletAddress: destinationAddress }) @@ -135,413 +152,495 @@ export const useArbTokenBridge = ( React.Dispatch ] - const l1NetworkID = useMemo(() => String(l1.network.id), [l1.network.id]) - const [transactions, { addTransaction, updateTransaction }] = useTransactions() - const removeTokensFromList = (listID: number) => { - setBridgeTokens(prevBridgeTokens => { - const newBridgeTokens = { ...prevBridgeTokens } - for (const address in bridgeTokens) { - const token = bridgeTokens[address] - if (!token) continue + const updateTokenData = useCallback( + async (l1Address: string) => { + if (typeof bridgeTokens === 'undefined') { + return + } + const l1AddressLowerCased = l1Address.toLowerCase() + const bridgeToken = bridgeTokens[l1AddressLowerCased] - token.listIds.delete(listID) + if (!bridgeToken) { + return + } - if (token.listIds.size === 0) { - delete newBridgeTokens[address] + const newBridgeTokens = { [l1AddressLowerCased]: bridgeToken } + setBridgeTokens(oldBridgeTokens => { + return { ...oldBridgeTokens, ...newBridgeTokens } + }) + const { l2Address } = bridgeToken + updateErc20L1Balance([l1AddressLowerCased]) + if (destinationAddress) { + updateErc20L1CustomDestinationBalance([l1AddressLowerCased]) + } + if (l2Address) { + updateErc20L2Balance([l2Address]) + if (destinationAddress) { + updateErc20CustomDestinationL2Balance([l2Address]) } } - return newBridgeTokens - }) - } + }, + [ + bridgeTokens, + destinationAddress, + setBridgeTokens, + updateErc20CustomDestinationL2Balance, + updateErc20L1Balance, + updateErc20L1CustomDestinationBalance, + updateErc20L2Balance + ] + ) - const addTokensFromList = async (arbTokenList: TokenList, listId: number) => { - const l1ChainID = l1.network.id - const l2ChainID = l2.network.id + const addToken = useCallback( + async (erc20L1orL2Address: string) => { + let l1Address: string + let l2Address: string | undefined - const bridgeTokensToAdd: ContractStorage = {} + if (!walletAddress) { + return + } - const candidateUnbridgedTokensToAdd: ERC20BridgeToken[] = [] + const lowercasedErc20L1orL2Address = erc20L1orL2Address.toLowerCase() + const maybeL1Address = await getL1ERC20Address({ + erc20L2Address: lowercasedErc20L1orL2Address, + l2Provider: childChainProvider + }) + + if (maybeL1Address) { + // looks like l2 address was provided + l1Address = maybeL1Address + l2Address = lowercasedErc20L1orL2Address + } else { + // looks like l1 address was provided + l1Address = lowercasedErc20L1orL2Address + + // while deriving the child-chain address, it can be a teleport transfer too, in that case derive L3 address from L1 address + // else, derive the L2 address from L1 address OR L3 address from L2 address + if ( + isValidTeleportChainPair({ + sourceChainId: parentChain.id, + destinationChainId: childChain.id + }) + ) { + // this can be a bit hard to follow, but it will resolve when we have code-wide better naming for variables + // here `l2Address` actually means `childChainAddress`, and `l2.provider` is actually being used as a child-chain-provider, which in this case will be L3 + l2Address = await getL3ERC20Address({ + erc20L1Address: l1Address, + l1Provider: parentChainProvider, + l3Provider: childChainProvider // in case of teleport transfer, the l2.provider being used here is actually the l3 provider + }) + } else { + l2Address = await getL2ERC20Address({ + erc20L1Address: l1Address, + l1Provider: parentChainProvider, + l2Provider: childChainProvider + }) + } + } - for (const tokenData of arbTokenList.tokens) { - const { address, name, symbol, extensions, decimals, logoURI, chainId } = - tokenData + const bridgeTokensToAdd: ContractStorage = {} + const erc20Params = { address: l1Address, provider: parentChainProvider } - if (![l1ChainID, l2ChainID].includes(chainId)) { - continue + if (!(await isValidErc20(erc20Params))) { + throw new Error(`${l1Address} is not a valid ERC-20 token`) } - const bridgeInfo = (() => { - // TODO: parsing the token list format could be from arbts or the tokenlist package - interface Extensions { - bridgeInfo: { - [chainId: string]: { - tokenAddress: string - originBridgeAddress: string - destBridgeAddress: string - } + const { name, symbol, decimals } = await fetchErc20Data(erc20Params) + + const isDisabled = await l1TokenIsDisabled({ + erc20L1Address: l1Address, + l1Provider: parentChainProvider, + l2Provider: childChainProvider + }) + + if (isDisabled) { + throw new TokenDisabledError('Token currently disabled') + } + + const l1AddressLowerCased = l1Address.toLowerCase() + bridgeTokensToAdd[l1AddressLowerCased] = { + name, + type: TokenType.ERC20, + symbol, + address: l1AddressLowerCased, + l2Address: l2Address?.toLowerCase(), + decimals, + listIds: new Set() + } + + setBridgeTokens(oldBridgeTokens => { + return { ...oldBridgeTokens, ...bridgeTokensToAdd } + }) + + updateErc20L1Balance([l1AddressLowerCased]) + if (l2Address) { + updateErc20L2Balance([l2Address]) + } + }, + [ + childChain.id, + childChainProvider, + parentChain.id, + parentChainProvider, + setBridgeTokens, + updateErc20L1Balance, + updateErc20L2Balance, + walletAddress + ] + ) + + const removeTokensFromList = useCallback( + (listID: number) => { + setBridgeTokens(prevBridgeTokens => { + const newBridgeTokens = { ...prevBridgeTokens } + for (const address in bridgeTokens) { + const token = bridgeTokens[address] + if (!token) continue + + token.listIds.delete(listID) + + if (token.listIds.size === 0) { + delete newBridgeTokens[address] } } - const isExtensions = (obj: any): obj is Extensions => { - if (!obj) return false - if (!obj['bridgeInfo']) return false - return Object.keys(obj['bridgeInfo']) - .map(key => obj['bridgeInfo'][key]) - .every( - e => - e && - 'tokenAddress' in e && - 'originBridgeAddress' in e && - 'destBridgeAddress' in e - ) - } - if (!isExtensions(extensions)) { - return null - } else { - return extensions.bridgeInfo - } - })() + return newBridgeTokens + }) + }, + [bridgeTokens, setBridgeTokens] + ) - if (bridgeInfo) { - const l1Address = bridgeInfo[l1NetworkID]?.tokenAddress.toLowerCase() + const addTokensFromList = useCallback( + async (arbTokenList: TokenList, listId: number) => { + const bridgeTokensToAdd: ContractStorage = {} - if (!l1Address) { - return - } + const candidateUnbridgedTokensToAdd: ERC20BridgeToken[] = [] - bridgeTokensToAdd[l1Address] = { + for (const tokenData of arbTokenList.tokens) { + const { + address, name, - type: TokenType.ERC20, symbol, - address: l1Address, - l2Address: address.toLowerCase(), + extensions, decimals, logoURI, - listIds: new Set([listId]) + chainId + } = tokenData + + if (![parentChain.id, childChain.id].includes(chainId)) { + continue } - } - // save potentially unbridged L1 tokens: - // stopgap: giant lists (i.e., CMC list) currently severaly hurts page performace, so for now we only add the bridged tokens - else if (arbTokenList.tokens.length < 1000) { - candidateUnbridgedTokensToAdd.push({ - name, - type: TokenType.ERC20, - symbol, - address: address.toLowerCase(), - decimals, - logoURI, - listIds: new Set([listId]) - }) - } - } - // add L1 tokens only if they aren't already bridged (i.e., if they haven't already beed added as L2 arb-tokens to the list) - const l1AddressesOfBridgedTokens = new Set( - Object.keys(bridgeTokensToAdd).map( - l1Address => - l1Address.toLowerCase() /* lists should have the checksummed case anyway, but just in case (pun unintended) */ - ) - ) - for (const l1TokenData of candidateUnbridgedTokensToAdd) { - if (!l1AddressesOfBridgedTokens.has(l1TokenData.address.toLowerCase())) { - bridgeTokensToAdd[l1TokenData.address] = l1TokenData - } - } + const bridgeInfo = (() => { + // TODO: parsing the token list format could be from arbts or the tokenlist package + interface Extensions { + bridgeInfo: { + [chainId: string]: { + tokenAddress: string + originBridgeAddress: string + destBridgeAddress: string + } + } + } + const isExtensions = (obj: any): obj is Extensions => { + if (!obj) return false + if (!obj['bridgeInfo']) return false + return Object.keys(obj['bridgeInfo']) + .map(key => obj['bridgeInfo'][key]) + .every( + e => + e && + 'tokenAddress' in e && + 'originBridgeAddress' in e && + 'destBridgeAddress' in e + ) + } + if (!isExtensions(extensions)) { + return null + } else { + return extensions.bridgeInfo + } + })() - // Callback is used here, so we can add listId to the set of listIds rather than creating a new set everytime - setBridgeTokens(oldBridgeTokens => { - const l1Addresses: string[] = [] - const l2Addresses: string[] = [] + if (bridgeInfo) { + const l1Address = + bridgeInfo[parentChain.id]?.tokenAddress.toLowerCase() - // USDC is not on any token list as it's unbridgeable - // but we still want to detect its balance on user's wallet - if (isNetwork(l2ChainID).isArbitrumOne) { - l2Addresses.push(CommonAddress.ArbitrumOne.USDC) - } - if (isNetwork(l2ChainID).isArbitrumSepolia) { - l2Addresses.push(CommonAddress.ArbitrumSepolia.USDC) - } + if (!l1Address) { + return + } - for (const tokenAddress in bridgeTokensToAdd) { - const tokenToAdd = bridgeTokensToAdd[tokenAddress] - if (!tokenToAdd) { - return - } - const { address, l2Address } = tokenToAdd - if (address) { - l1Addresses.push(address) + bridgeTokensToAdd[l1Address] = { + name, + type: TokenType.ERC20, + symbol, + address: l1Address, + l2Address: address.toLowerCase(), + decimals, + logoURI, + listIds: new Set([listId]) + } } - if (l2Address) { - l2Addresses.push(l2Address) + // save potentially unbridged L1 tokens: + // stopgap: giant lists (i.e., CMC list) currently severaly hurts page performace, so for now we only add the bridged tokens + else if (arbTokenList.tokens.length < 1000) { + candidateUnbridgedTokensToAdd.push({ + name, + type: TokenType.ERC20, + symbol, + address: address.toLowerCase(), + decimals, + logoURI, + listIds: new Set([listId]) + }) } + } - // Add the new list id being imported (`listId`) to the existing list ids (from `oldBridgeTokens[address]`) - // Set the result to token added to `bridgeTokens` : `tokenToAdd.listIds` - const oldListIds = - oldBridgeTokens?.[tokenToAdd.address]?.listIds || new Set() - tokenToAdd.listIds = new Set([...oldListIds, listId]) + // add L1 tokens only if they aren't already bridged (i.e., if they haven't already beed added as L2 arb-tokens to the list) + const l1AddressesOfBridgedTokens = new Set( + Object.keys(bridgeTokensToAdd).map( + l1Address => + l1Address.toLowerCase() /* lists should have the checksummed case anyway, but just in case (pun unintended) */ + ) + ) + for (const l1TokenData of candidateUnbridgedTokensToAdd) { + if ( + !l1AddressesOfBridgedTokens.has(l1TokenData.address.toLowerCase()) + ) { + bridgeTokensToAdd[l1TokenData.address] = l1TokenData + } } - updateErc20L1Balance(l1Addresses) - updateErc20L2Balance(l2Addresses) + // Callback is used here, so we can add listId to the set of listIds rather than creating a new set everytime + setBridgeTokens(oldBridgeTokens => { + const l1Addresses: string[] = [] + const l2Addresses: string[] = [] - return { - ...oldBridgeTokens, - ...bridgeTokensToAdd - } - }) - } + // USDC is not on any token list as it's unbridgeable + // but we still want to detect its balance on user's wallet + if (isNetwork(childChain.id).isArbitrumOne) { + l2Addresses.push(CommonAddress.ArbitrumOne.USDC) + } + if (isNetwork(childChain.id).isArbitrumSepolia) { + l2Addresses.push(CommonAddress.ArbitrumSepolia.USDC) + } - async function addToken(erc20L1orL2Address: string) { - let l1Address: string - let l2Address: string | undefined - - if (!walletAddress) { - return - } - - const lowercasedErc20L1orL2Address = erc20L1orL2Address.toLowerCase() - const maybeL1Address = await getL1ERC20Address({ - erc20L2Address: lowercasedErc20L1orL2Address, - l2Provider: l2.provider - }) - - if (maybeL1Address) { - // looks like l2 address was provided - l1Address = maybeL1Address - l2Address = lowercasedErc20L1orL2Address - } else { - // looks like l1 address was provided - l1Address = lowercasedErc20L1orL2Address - - // while deriving the child-chain address, it can be a teleport transfer too, in that case derive L3 address from L1 address - // else, derive the L2 address from L1 address OR L3 address from L2 address - if ( - isValidTeleportChainPair({ - sourceChainId: l1.network.id, - destinationChainId: l2.network.id - }) - ) { - // this can be a bit hard to follow, but it will resolve when we have code-wide better naming for variables - // here `l2Address` actually means `childChainAddress`, and `l2.provider` is actually being used as a child-chain-provider, which in this case will be L3 - l2Address = await getL3ERC20Address({ - erc20L1Address: l1Address, - l1Provider: l1.provider, - l3Provider: l2.provider // in case of teleport transfer, the l2.provider being used here is actually the l3 provider - }) - } else { - l2Address = await getL2ERC20Address({ - erc20L1Address: l1Address, - l1Provider: l1.provider, - l2Provider: l2.provider - }) - } - } - - const bridgeTokensToAdd: ContractStorage = {} - const erc20Params = { address: l1Address, provider: l1.provider } - - if (!(await isValidErc20(erc20Params))) { - throw new Error(`${l1Address} is not a valid ERC-20 token`) - } - - const { name, symbol, decimals } = await fetchErc20Data(erc20Params) - - const isDisabled = await l1TokenIsDisabled({ - erc20L1Address: l1Address, - l1Provider: l1.provider, - l2Provider: l2.provider - }) - - if (isDisabled) { - throw new TokenDisabledError('Token currently disabled') - } - - const l1AddressLowerCased = l1Address.toLowerCase() - bridgeTokensToAdd[l1AddressLowerCased] = { - name, - type: TokenType.ERC20, - symbol, - address: l1AddressLowerCased, - l2Address: l2Address?.toLowerCase(), - decimals, - listIds: new Set() - } - - setBridgeTokens(oldBridgeTokens => { - return { ...oldBridgeTokens, ...bridgeTokensToAdd } - }) - - updateErc20L1Balance([l1AddressLowerCased]) - if (l2Address) { - updateErc20L2Balance([l2Address]) - } - } + for (const tokenAddress in bridgeTokensToAdd) { + const tokenToAdd = bridgeTokensToAdd[tokenAddress] + if (!tokenToAdd) { + return + } + const { address, l2Address } = tokenToAdd + if (address) { + l1Addresses.push(address) + } + if (l2Address) { + l2Addresses.push(l2Address) + } - const updateTokenData = useCallback( - async (l1Address: string) => { - if (typeof bridgeTokens === 'undefined') { - return - } - const l1AddressLowerCased = l1Address.toLowerCase() - const bridgeToken = bridgeTokens[l1AddressLowerCased] + // Add the new list id being imported (`listId`) to the existing list ids (from `oldBridgeTokens[address]`) + // Set the result to token added to `bridgeTokens` : `tokenToAdd.listIds` + const oldListIds = + oldBridgeTokens?.[tokenToAdd.address]?.listIds || new Set() + tokenToAdd.listIds = new Set([...oldListIds, listId]) + } - if (!bridgeToken) { - return - } + updateErc20L1Balance(l1Addresses) + updateErc20L2Balance(l2Addresses) - const newBridgeTokens = { [l1AddressLowerCased]: bridgeToken } - setBridgeTokens(oldBridgeTokens => { - return { ...oldBridgeTokens, ...newBridgeTokens } - }) - const { l2Address } = bridgeToken - updateErc20L1Balance([l1AddressLowerCased]) - if (destinationAddress) { - updateErc20L1CustomDestinationBalance([l1AddressLowerCased]) - } - if (l2Address) { - updateErc20L2Balance([l2Address]) - if (destinationAddress) { - updateErc20CustomDestinationL2Balance([l2Address]) + return { + ...oldBridgeTokens, + ...bridgeTokensToAdd } - } + }) }, [ - bridgeTokens, + childChain.id, + parentChain.id, setBridgeTokens, updateErc20L1Balance, - updateErc20L2Balance, - updateErc20L1CustomDestinationBalance, - updateErc20CustomDestinationL2Balance + updateErc20L2Balance ] ) - async function triggerOutboxToken({ - event, - l1Signer - }: { - event: L2ToL1EventResultPlus - l1Signer: Signer - }) { - // sanity check - if (!event) { - throw new Error('Outbox message not found') - } - - if (!walletAddress) { - return - } - - const parentChainProvider = getProviderForChainId(event.parentChainId) - const childChainProvider = getProviderForChainId(event.childChainId) - - const messageWriter = ChildToParentMessage.fromEvent( - l1Signer, + const addBridgeTokenListToBridge = useCallback( + (bridgeTokenList: BridgeTokenList) => { + fetchTokenListFromURL(bridgeTokenList.url).then( + ({ isValid, data: tokenList }) => { + if (!isValid) return + + addTokensFromList(tokenList!, bridgeTokenList.id) + } + ) + }, + [addTokensFromList] + ) + + const addToExecutedMessagesCache = useCallback( + (events: L2ToL1EventResult[]) => { + const added: { [cacheKey: string]: boolean } = {} + + events.forEach((event: L2ToL1EventResult) => { + const cacheKey = getExecutedMessagesCacheKey({ + event, + l2ChainId: childChain.id + }) + + added[cacheKey] = true + }) + + setExecutedMessagesCache({ ...executedMessagesCache, ...added }) + }, + [childChain.id, executedMessagesCache, setExecutedMessagesCache] + ) + + const triggerOutboxToken = useCallback( + async ({ event, - parentChainProvider - ) - const res = await messageWriter.execute(childChainProvider) + l1Signer + }: { + event: L2ToL1EventResultPlus + l1Signer: Signer + }) => { + // sanity check + if (!event) { + throw new Error('Outbox message not found') + } - const rec = await res.wait() + if (!walletAddress) { + return + } - if (rec.status === 1) { - addToExecutedMessagesCache([event]) - } + const parentChainProvider = getProviderForChainId(event.parentChainId) + const childChainProvider = getProviderForChainId(event.childChainId) - return rec - } + const messageWriter = ChildToParentMessage.fromEvent( + l1Signer, + event, + parentChainProvider + ) + const res = await messageWriter.execute(childChainProvider) - function addL2NativeToken(erc20L2Address: string) { - const token = getL2NativeToken(erc20L2Address, l2.network.id) - - setBridgeTokens(oldBridgeTokens => { - return { - ...oldBridgeTokens, - [`L2-NATIVE:${token.address}`]: { - name: token.name, - type: TokenType.ERC20, - symbol: token.symbol, - address: token.address, - l2Address: token.address, - decimals: token.decimals, - logoURI: token.logoURI, - listIds: new Set(), - isL2Native: true - } + const rec = await res.wait() + + if (rec.status === 1) { + addToExecutedMessagesCache([event]) } - }) - } - async function triggerOutboxEth({ - event, - l1Signer - }: { - event: L2ToL1EventResultPlus - l1Signer: Signer - }) { - // sanity check - if (!event) { - throw new Error('Outbox message not found') - } - - if (!walletAddress) { - return - } - - const parentChainProvider = getProviderForChainId(event.parentChainId) - const childChainProvider = getProviderForChainId(event.childChainId) - - const messageWriter = ChildToParentMessage.fromEvent( - l1Signer, - event, - parentChainProvider - ) + return rec + }, + [addToExecutedMessagesCache, walletAddress] + ) - const res = await messageWriter.execute(childChainProvider) + const addL2NativeToken = useCallback( + (erc20L2Address: string) => { + const token = getL2NativeToken(erc20L2Address, childChain.id) - const rec = await res.wait() + setBridgeTokens(oldBridgeTokens => { + return { + ...oldBridgeTokens, + [`L2-NATIVE:${token.address}`]: { + name: token.name, + type: TokenType.ERC20, + symbol: token.symbol, + address: token.address, + l2Address: token.address, + decimals: token.decimals, + logoURI: token.logoURI, + listIds: new Set(), + isL2Native: true + } + } + }) + }, + [childChain.id, setBridgeTokens] + ) - if (rec.status === 1) { - addToExecutedMessagesCache([event]) - } + const triggerOutboxEth = useCallback( + async ({ + event, + l1Signer + }: { + event: L2ToL1EventResultPlus + l1Signer: Signer + }) => { + // sanity check + if (!event) { + throw new Error('Outbox message not found') + } - return rec - } + if (!walletAddress) { + return + } - function addToExecutedMessagesCache(events: L2ToL1EventResult[]) { - const added: { [cacheKey: string]: boolean } = {} + const parentChainProvider = getProviderForChainId(event.parentChainId) + const childChainProvider = getProviderForChainId(event.childChainId) - events.forEach((event: L2ToL1EventResult) => { - const cacheKey = getExecutedMessagesCacheKey({ + const messageWriter = ChildToParentMessage.fromEvent( + l1Signer, event, - l2ChainId: l2.network.id - }) + parentChainProvider + ) - added[cacheKey] = true - }) + const res = await messageWriter.execute(childChainProvider) - setExecutedMessagesCache({ ...executedMessagesCache, ...added }) - } + const rec = await res.wait() - return { - bridgeTokens, - eth: { - triggerOutbox: triggerOutboxEth + if (rec.status === 1) { + addToExecutedMessagesCache([event]) + } + + return rec }, - token: { + [addToExecutedMessagesCache, walletAddress] + ) + + const tokenFunctions = useMemo( + () => ({ add: addToken, addL2NativeToken, addTokensFromList, removeTokensFromList, + addBridgeTokenListToBridge, updateTokenData, triggerOutbox: triggerOutboxToken - }, - transactions: { + }), + [ + addBridgeTokenListToBridge, + addL2NativeToken, + addToken, + addTokensFromList, + removeTokensFromList, + triggerOutboxToken, + updateTokenData + ] + ) + + const ethFunctions = useMemo( + () => ({ + triggerOutbox: triggerOutboxEth + }), + [triggerOutboxEth] + ) + + const transactionsFunctions = useMemo( + () => ({ transactions, updateTransaction, addTransaction - } - } + }), + [addTransaction, , transactions, updateTransaction] + ) + + return useMemo( + () => ({ + bridgeTokens, + eth: ethFunctions, + token: tokenFunctions, + transactions: transactionsFunctions + }), + [bridgeTokens, ethFunctions, tokenFunctions, transactionsFunctions] + ) } diff --git a/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts b/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts index f174dc8fb3..6abc94d3e0 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts @@ -1,7 +1,6 @@ import { useCallback, useState } from 'react' import { useAccount, useSigner } from 'wagmi' -import { useAppState } from '../state' import { MergedTransaction, WithdrawalStatus } from '../state/app/state' import { isUserRejectedError } from '../util/isUserRejectedError' import { errorToast } from '../components/common/atoms/Toast' @@ -13,6 +12,7 @@ import { useTransactionHistory } from './useTransactionHistory' import dayjs from 'dayjs' import { fetchErc20Data } from '../util/TokenUtils' import { fetchNativeCurrency } from './useNativeCurrency' +import { useArbTokenBridge } from './useArbTokenBridge' import { getProviderForChainId } from '@/token-bridge-sdk/utils' import { captureSentryErrorWithExtraData } from '../util/SentryUtils' @@ -25,8 +25,9 @@ export function useClaimWithdrawal( tx: MergedTransaction ): UseClaimWithdrawalResult { const { - app: { arbTokenBridge } - } = useAppState() + eth: { triggerOutbox: ethTriggerOutbox }, + token: { triggerOutbox: erc20TriggerOutbox } + } = useArbTokenBridge() const { address } = useAccount() const { data: signer } = useSigner({ chainId: tx.parentChainId }) const { updatePendingTransaction } = useTransactionHistory(address) @@ -85,12 +86,12 @@ export function useClaimWithdrawal( throw 'Signer is undefined' } if (tx.assetType === AssetType.ETH) { - res = await arbTokenBridge.eth.triggerOutbox({ + res = await ethTriggerOutbox({ event: extendedEvent, l1Signer: signer }) } else { - res = await arbTokenBridge.token.triggerOutbox({ + res = await erc20TriggerOutbox({ event: extendedEvent, l1Signer: signer }) @@ -127,8 +128,8 @@ export function useClaimWithdrawal( setParentChainTxDetailsOfWithdrawalClaimTx(tx, txHash) } }, [ - arbTokenBridge.eth, - arbTokenBridge.token, + erc20TriggerOutbox, + ethTriggerOutbox, isClaiming, signer, tx, diff --git a/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts b/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts index 86eb200d34..48b9156827 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts @@ -1,4 +1,4 @@ -import { useAccount, useNetwork } from 'wagmi' +import { ConnectorData, useAccount, useNetwork } from 'wagmi' import useSWRImmutable from 'swr/immutable' import useSWRInfinite from 'swr/infinite' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -747,18 +747,32 @@ export const useTransactionHistory = ( [updateCachedTransaction] ) + const pause = useCallback(() => { + setFetching(false) + }, [setFetching]) + + const resume = useCallback(() => { + setFetching(true) + setPage(prevPage => prevPage + 1) + }, [setFetching, setPage]) + useEffect(() => { if (!runFetcher || !connector) { return } - connector.on('change', e => { + const handler = (e: ConnectorData) => { // reset state on account change if (e.account) { setPage(1) setPauseCount(0) setFetching(true) } - }) + } + connector.on('change', handler) + + return () => { + connector.off('change', handler) + } }, [connector, runFetcher, setPage]) useEffect(() => { @@ -803,40 +817,57 @@ export const useTransactionHistory = ( if (page === txPages.length) { setPage(prevPage => prevPage + 1) } - }, [txPages, setPage, page, pauseCount, fetching, runFetcher, isValidating]) - - function pause() { - setFetching(false) - } + }, [ + txPages, + setPage, + page, + pauseCount, + fetching, + runFetcher, + isValidating, + pause + ]) - function resume() { - setFetching(true) - setPage(prevPage => prevPage + 1) - } + const emptyState = useMemo(() => [], []) + return useMemo(() => { + if (isLoadingTxsWithoutStatus || error) { + return { + transactions: emptyState, + loading: isLoadingTxsWithoutStatus, + error, + failedChainPairs: emptyState, + completed: true, + pause, + resume, + addPendingTransaction, + updatePendingTransaction + } + } - if (isLoadingTxsWithoutStatus || error) { return { - transactions: [], - loading: isLoadingTxsWithoutStatus, - error, - failedChainPairs: [], - completed: true, + transactions, + loading: isLoadingFirstPage || isLoadingMore, + completed, + error: txPagesError ?? error, + failedChainPairs, pause, resume, addPendingTransaction, updatePendingTransaction } - } - - return { - transactions, - loading: isLoadingFirstPage || isLoadingMore, + }, [ + addPendingTransaction, completed, - error: txPagesError ?? error, + emptyState, + error, failedChainPairs, + isLoadingFirstPage, + isLoadingMore, + isLoadingTxsWithoutStatus, pause, resume, - addPendingTransaction, + transactions, + txPagesError, updatePendingTransaction - } + ]) } diff --git a/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts b/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts index 616ebbaa6e..f1cc760df6 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts @@ -1,4 +1,4 @@ -import { useReducer, useEffect, useMemo } from 'react' +import { useReducer, useEffect, useMemo, useCallback } from 'react' import { TransactionReceipt } from '@ethersproject/abstract-provider' import { AssetType, TransactionActions } from './arbTokenBridge.types' import { BigNumber, ethers } from 'ethers' @@ -324,7 +324,7 @@ const useTransactions = (): [Transaction[], TransactionActions] => { }) }, []) - const addTransaction = (transaction: NewTransaction) => { + const addTransaction = useCallback((transaction: NewTransaction) => { if (!transaction.txID) { console.warn(' Cannot add transaction: TxID not included (???)') return @@ -337,40 +337,47 @@ const useTransactions = (): [Transaction[], TransactionActions] => { type: 'ADD_TRANSACTION', transaction: tx }) - } + }, []) - const updateTxnL1ToL2MsgData = async ( - txID: string, - l1ToL2MsgData: ParentToChildMessageData - ) => { - dispatch({ - type: 'UPDATE_L1TOL2MSG_DATA', - txID: txID, - l1ToL2MsgData - }) - } + const updateTxnL1ToL2MsgData = useCallback( + async (txID: string, l1ToL2MsgData: ParentToChildMessageData) => { + dispatch({ + type: 'UPDATE_L1TOL2MSG_DATA', + txID: txID, + l1ToL2MsgData + }) + }, + [] + ) - const setTransactionSuccess = (txID: string) => { + const setTransactionSuccess = useCallback((txID: string) => { return dispatch({ type: 'SET_SUCCESS', txID: txID }) - } - const setTransactionBlock = (txID: string, blockNumber?: number) => { - return dispatch({ - type: 'SET_BLOCK_NUMBER', - txID, - blockNumber - }) - } - const setResolvedTimestamp = (txID: string, timestamp?: string) => { - return dispatch({ - type: 'SET_RESOLVED_TIMESTAMP', - txID, - timestamp - }) - } - const setTransactionFailure = (txID?: string) => { + }, []) + + const setTransactionBlock = useCallback( + (txID: string, blockNumber?: number) => { + return dispatch({ + type: 'SET_BLOCK_NUMBER', + txID, + blockNumber + }) + }, + [] + ) + const setResolvedTimestamp = useCallback( + (txID: string, timestamp?: string) => { + return dispatch({ + type: 'SET_RESOLVED_TIMESTAMP', + txID, + timestamp + }) + }, + [] + ) + const setTransactionFailure = useCallback((txID?: string) => { if (!txID) { console.warn(' Cannot set transaction failure: TxID not included (???)') return @@ -379,41 +386,54 @@ const useTransactions = (): [Transaction[], TransactionActions] => { type: 'SET_FAILURE', txID: txID }) - } + }, []) - const updateTransaction = ( - txReceipt: TransactionReceipt, - tx?: ethers.ContractTransaction, - l1ToL2MsgData?: ParentToChildMessageData - ) => { - if (!txReceipt.transactionHash) { - return console.warn( - '*** TransactionHash not included in transaction receipt (???) *** ' - ) - } - switch (txReceipt.status) { - case 0: { - setTransactionFailure(txReceipt.transactionHash) - break + const updateTransaction = useCallback( + ( + txReceipt: TransactionReceipt, + tx?: ethers.ContractTransaction, + l1ToL2MsgData?: ParentToChildMessageData + ) => { + if (!txReceipt.transactionHash) { + return console.warn( + '*** TransactionHash not included in transaction receipt (???) *** ' + ) } - case 1: { - setTransactionSuccess(txReceipt.transactionHash) - break + switch (txReceipt.status) { + case 0: { + setTransactionFailure(txReceipt.transactionHash) + break + } + + case 1: { + setTransactionSuccess(txReceipt.transactionHash) + break + } + default: + console.warn('*** Status not included in transaction receipt *** ') + break } - default: - console.warn('*** Status not included in transaction receipt *** ') - break - } - if (tx?.blockNumber) { - setTransactionBlock(txReceipt.transactionHash, tx.blockNumber) - } - if (tx) { - setResolvedTimestamp(txReceipt.transactionHash, new Date().toISOString()) - } - if (l1ToL2MsgData) { - updateTxnL1ToL2MsgData(txReceipt.transactionHash, l1ToL2MsgData) - } - } + if (tx?.blockNumber) { + setTransactionBlock(txReceipt.transactionHash, tx.blockNumber) + } + if (tx) { + setResolvedTimestamp( + txReceipt.transactionHash, + new Date().toISOString() + ) + } + if (l1ToL2MsgData) { + updateTxnL1ToL2MsgData(txReceipt.transactionHash, l1ToL2MsgData) + } + }, + [ + setResolvedTimestamp, + setTransactionBlock, + setTransactionFailure, + setTransactionSuccess, + updateTxnL1ToL2MsgData + ] + ) const transactions = useMemo(() => { return state.filter(tx => !deprecatedTxTypes.has(tx.type)) diff --git a/packages/arb-token-bridge-ui/src/hooks/useWarningTokensList.ts b/packages/arb-token-bridge-ui/src/hooks/useWarningTokensList.ts new file mode 100644 index 0000000000..ea7f9777a8 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/hooks/useWarningTokensList.ts @@ -0,0 +1,17 @@ +import useSWRImmutable from 'swr/immutable' +import { useActions } from '../state' + +export function useWarningTokensList() { + const { + app: { setWarningTokens } + } = useActions() + + return useSWRImmutable( + 'https://raw.githubusercontent.com/OffchainLabs/arb-token-lists/aff40a59608678cfd9b034dd198011c90b65b8b6/src/WarningList/warningTokens.json', + async url => { + const tokenList = await fetch(url).then(response => response.json()) + setWarningTokens(tokenList) + return tokenList + } + ) +} 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 d24b954a93..d53cbd6b44 100644 --- a/packages/arb-token-bridge-ui/src/state/app/actions.ts +++ b/packages/arb-token-bridge-ui/src/state/app/actions.ts @@ -1,25 +1,6 @@ -import { - ArbTokenBridge, - ERC20BridgeToken -} from '../../hooks/arbTokenBridge.types' +import { ERC20BridgeToken } from '../../hooks/arbTokenBridge.types' import { Context } from '..' -import { ConnectionState } from '../../util' -import { WhiteListState, WarningTokens } from './state' - -export const setConnectionState = ( - { state }: Context, - connectionState: ConnectionState -) => { - state.app.connectionState = connectionState -} - -export const setChainIds = ( - { state }: Context, - payload: { l1NetworkChainId: number; l2NetworkChainId: number } -) => { - state.app.l1NetworkChainId = payload.l1NetworkChainId - state.app.l2NetworkChainId = payload.l2NetworkChainId -} +import { WarningTokens } from './state' export const setSelectedToken = ( { state }: Context, @@ -28,50 +9,9 @@ export const setSelectedToken = ( 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 - } - - state.app.arbTokenBridge = {} as ArbTokenBridge - state.app.verifying = WhiteListState.ALLOWED - state.app.connectionState = ConnectionState.LOADING - state.app.arbTokenBridgeLoaded = false -} - export const setWarningTokens = ( { state }: Context, warningTokens: WarningTokens ) => { state.app.warningTokens = warningTokens } - -export const setWhitelistState = ( - { state }: Context, - verifying: WhiteListState -) => { - state.app.verifying = verifying -} - -export const setArbTokenBridgeLoaded = ( - { state }: Context, - loaded: boolean -) => { - state.app.arbTokenBridgeLoaded = loaded -} - -export const setArbTokenBridge = ( - { state, actions }: Context, - atb: ArbTokenBridge -) => { - state.app.arbTokenBridge = atb - if (atb && !state.app.arbTokenBridgeLoaded) { - actions.app.setArbTokenBridgeLoaded(true) - } -} 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 3e42176cea..19cdd1c262 100644 --- a/packages/arb-token-bridge-ui/src/state/app/state.ts +++ b/packages/arb-token-bridge-ui/src/state/app/state.ts @@ -1,6 +1,5 @@ import { BigNumber } from 'ethers' import { - ArbTokenBridge, AssetType, ERC20BridgeToken, NodeBlockDeadlineStatus @@ -11,7 +10,6 @@ import { L2ToL3MessageData, TxnType } from '../../hooks/useTransactions' -import { ConnectionState } from '../../util' import { CCTPSupportedChainId } from '../cctpState' import { Address } from '../../util/AddressUtils' @@ -89,25 +87,19 @@ export interface WarningTokens { } export type AppState = { - arbTokenBridge: ArbTokenBridge warningTokens: WarningTokens - connectionState: number selectedToken: ERC20BridgeToken | null verifying: WhiteListState - l1NetworkChainId: number | null - l2NetworkChainId: number | null - arbTokenBridgeLoaded: boolean + sourceChainId: number | null + destinationChainId: number | null } -export const defaultState: AppState = { - arbTokenBridge: {} as ArbTokenBridge, +const defaultState: AppState = { warningTokens: {} as WarningTokens, - connectionState: ConnectionState.LOADING, - l1NetworkChainId: null, - l2NetworkChainId: null, + sourceChainId: null, + destinationChainId: null, verifying: WhiteListState.ALLOWED, - selectedToken: null, - arbTokenBridgeLoaded: false + selectedToken: null } export const state: AppState = { ...defaultState diff --git a/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts b/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts index 3374dd115c..52298b3eb4 100644 --- a/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts @@ -7,7 +7,6 @@ import UniswapLogo from '@/images/lists/uniswap.png' import CMCLogo from '@/images/lists/cmc.png' import CoinGeckoLogo from '@/images/lists/coinGecko.svg' import ArbitrumLogo from '@/images/lists/ArbitrumLogo.png' -import { ArbTokenBridge } from '../hooks/arbTokenBridge.types' import { ChainId } from './networks' export const SPECIAL_ARBITRUM_TOKEN_TOKEN_LIST_ID = 0 @@ -190,19 +189,6 @@ export const validateTokenList = (tokenList: TokenList) => { return validate(tokenList) } -export const addBridgeTokenListToBridge = ( - bridgeTokenList: BridgeTokenList, - arbTokenBridge: ArbTokenBridge -) => { - fetchTokenListFromURL(bridgeTokenList.url).then( - ({ isValid, data: tokenList }) => { - if (!isValid) return - - arbTokenBridge.token.addTokensFromList(tokenList!, bridgeTokenList.id) - } - ) -} - export async function fetchTokenListFromURL(tokenListURL: string): Promise<{ isValid: boolean data: TokenList | undefined diff --git a/packages/arb-token-bridge-ui/src/util/index.ts b/packages/arb-token-bridge-ui/src/util/index.ts index 8392214fa0..136b8fefd6 100644 --- a/packages/arb-token-bridge-ui/src/util/index.ts +++ b/packages/arb-token-bridge-ui/src/util/index.ts @@ -1,10 +1,3 @@ -export enum ConnectionState { - LOADING, - L1_CONNECTED, - L2_CONNECTED, - NETWORK_ERROR -} - export const sanitizeImageSrc = (url: string): string => { if (url.startsWith('ipfs')) { return `https://ipfs.io/ipfs/${url.substring(7)}`