From 1a053aa3d2d77e08a6e8202073b015d8fbc89c96 Mon Sep 17 00:00:00 2001 From: Christophe Date: Fri, 26 Jan 2024 16:13:41 +0100 Subject: [PATCH 01/28] Remove useArbTokenBridge from store --- .../src/components/App/App.tsx | 39 +---- .../components/TransferPanel/TokenButton.tsx | 19 +-- .../TransferPanel/TokenImportDialog.tsx | 7 +- .../src/components/TransferPanel/TokenRow.tsx | 8 +- .../components/TransferPanel/TokenSearch.tsx | 20 +-- .../TransferPanel/TokenSearchUtils.ts | 8 +- .../TransferPanel/TransferPanel.tsx | 10 +- .../TransferPanel/TransferPanelMain.tsx | 5 +- .../TransferPanel/TransferPanelSummary.tsx | 5 +- .../syncers/ArbTokenBridgeStoreSync.tsx | 23 --- .../src/components/syncers/BalanceUpdater.tsx | 16 +- .../components/syncers/TokenListSyncer.tsx | 14 +- .../src/hooks/useArbTokenBridge.ts | 139 +++++++++--------- .../src/hooks/useClaimWithdrawal.ts | 5 +- .../src/hooks/useRedeemRetryable.ts | 5 +- .../src/state/app/actions.ts | 24 +-- .../src/state/app/state.ts | 7 +- .../src/util/TokenListUtils.ts | 4 +- 18 files changed, 125 insertions(+), 233 deletions(-) delete mode 100644 packages/arb-token-bridge-ui/src/components/syncers/ArbTokenBridgeStoreSync.tsx 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 20707f7698..942ef5e404 100644 --- a/packages/arb-token-bridge-ui/src/components/App/App.tsx +++ b/packages/arb-token-bridge-ui/src/components/App/App.tsx @@ -14,15 +14,12 @@ import { createOvermind, Overmind } from 'overmind' import { Provider } from 'overmind-react' import { useLocalStorage } from 'react-use' 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 { Alert } from '../common/Alert' import { MainContent } from '../MainContent/MainContent' -import { ArbTokenBridgeStoreSync } from '../syncers/ArbTokenBridgeStoreSync' -import { BalanceUpdater } from '../syncers/BalanceUpdater' import { TokenListSyncer } from '../syncers/TokenListSyncer' import { useDialog } from '../common/Dialog' import { @@ -110,7 +107,6 @@ const AppContent = (): JSX.Element => { - @@ -131,9 +127,6 @@ const Injector = ({ children }: { children: React.ReactNode }): JSX.Element => { // 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 @@ -152,12 +145,11 @@ const Injector = ({ children }: { children: React.ReactNode }): JSX.Element => { ) { actions.app.setSelectedToken(null) } - }, [selectedToken, nativeCurrency]) + }, [selectedToken, nativeCurrency, actions.app]) // Listen for account and network changes useEffect(() => { // Any time one of those changes - setTokenBridgeParams(null) actions.app.setConnectionState(ConnectionState.LOADING) if (!isConnected) { @@ -173,10 +165,6 @@ const Injector = ({ children }: { children: React.ReactNode }): JSX.Element => { ).isEthereumMainnetOrTestnet actions.app.reset(networks.sourceChain.id) - actions.app.setChainIds({ - l1NetworkChainId: parentChain.id, - l2NetworkChainId: childChain.id - }) if ( (isParentChainEthereum && isConnectedToArbitrum) || @@ -188,17 +176,6 @@ const Injector = ({ children }: { children: React.ReactNode }): JSX.Element => { console.info('Deposit mode detected:') actions.app.setConnectionState(ConnectionState.L1_CONNECTED) } - - setTokenBridgeParams({ - l1: { - network: parentChain, - provider: parentChainProvider - }, - l2: { - network: childChain, - provider: childChainProvider - } - }) }, [ isConnected, address, @@ -208,7 +185,8 @@ const Injector = ({ children }: { children: React.ReactNode }): JSX.Element => { parentChain, childChain, parentChainProvider, - childChainProvider + childChainProvider, + actions.app ]) useEffect(() => { @@ -222,7 +200,7 @@ const Injector = ({ children }: { children: React.ReactNode }): JSX.Element => { .catch(err => { console.warn('Failed to fetch warning tokens:', err) }) - }, []) + }, [actions.app]) if (address && isBlocked) { return ( @@ -238,14 +216,7 @@ const Injector = ({ children }: { children: React.ReactNode }): JSX.Element => { ) } - return ( - <> - {tokenBridgeParams && ( - - )} - {children} - - ) + return <>{children} } // connector names: https://github.com/wagmi-dev/wagmi/blob/b17c07443e407a695dfe9beced2148923b159315/docs/pages/core/connectors/_meta.en-US.json#L4 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 f255e95db0..9cba93daf6 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx @@ -9,16 +9,14 @@ import { sanitizeTokenSymbol } from '../../util/TokenUtils' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' +import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' export function TokenButton(): JSX.Element { const { - app: { - selectedToken, - arbTokenBridge: { bridgeTokens }, - arbTokenBridgeLoaded - } + app: { selectedToken } } = useAppState() const [networks] = useNetworks() + const { bridgeTokens } = useArbTokenBridge() const { childChain, childChainProvider, parentChain, isDepositMode } = useNetworksRelationship(networks) @@ -29,9 +27,7 @@ export function TokenButton(): JSX.Element { if (!selectedAddress) { return nativeCurrency.logoUrl } - if (!arbTokenBridgeLoaded) { - return undefined - } + if (typeof bridgeTokens === 'undefined') { return undefined } @@ -40,12 +36,7 @@ export function TokenButton(): JSX.Element { return sanitizeImageSrc(logo) } return undefined - }, [ - nativeCurrency, - bridgeTokens, - selectedToken?.address, - arbTokenBridgeLoaded - ]) + }, [nativeCurrency, bridgeTokens, selectedToken?.address]) const chainId = isDepositMode ? parentChain.id : childChain.id const tokenSymbol = useMemo(() => { 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 87a7a7c18e..94710fbfc5 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx @@ -29,6 +29,7 @@ import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { isWithdrawOnlyToken } from '../../util/WithdrawOnlyUtils' import { isTransferDisabledToken } from '../../util/TokenTransferDisabledUtils' import { useTransferDisabledDialogStore } from './TransferDisabledDialog' +import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' enum ImportStatus { LOADING, @@ -72,11 +73,9 @@ export function TokenImportDialog({ }: TokenImportDialogProps): JSX.Element { const { address: walletAddress } = useAccount() const { - app: { - arbTokenBridge: { bridgeTokens, token }, - selectedToken - } + app: { selectedToken } } = useAppState() + const { bridgeTokens, token } = useArbTokenBridge() const [networks] = useNetworks() const { childChain, 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 fbe5ebec7b..12fa5fb6de 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx @@ -9,7 +9,6 @@ import { constants } from 'ethers' import { Chain, useAccount } from 'wagmi' import { Loader } from '../common/atoms/Loader' -import { useAppState } from '../../state' import { listIdsToNames, SPECIAL_ARBITRUM_TOKEN_TOKEN_LIST_ID @@ -33,6 +32,7 @@ import { useAccountType } from '../../hooks/useAccountType' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' +import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' function tokenListIdsToNames(ids: number[]): string { return ids @@ -82,11 +82,7 @@ export function TokenRow({ token }: TokenRowProps): JSX.Element { const { address: walletAddress } = useAccount() - const { - app: { - arbTokenBridge: { bridgeTokens } - } - } = useAppState() + const { bridgeTokens } = useArbTokenBridge() const { isLoading: isLoadingAccountType } = useAccountType() const [networks] = useNetworks() const { 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 bc5b0d5431..876c84fffd 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx @@ -44,6 +44,7 @@ import { useTransferDisabledDialogStore } from './TransferDisabledDialog' import { isWithdrawOnlyToken } from '../../util/WithdrawOnlyUtils' import { isTransferDisabledToken } from '../../util/TokenTransferDisabledUtils' import { useTokenFromSearchParams } from './TransferPanelUtils' +import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' enum Panel { TOKENS, @@ -69,12 +70,9 @@ const ARB_SEPOLIA_NATIVE_USDC_TOKEN = { } function TokenListsPanel() { - const { - app: { arbTokenBridge } - } = useAppState() + const { bridgeTokens, token } = useArbTokenBridge() const [networks] = useNetworks() const { childChain } = useNetworksRelationship(networks) - const { bridgeTokens, token } = arbTokenBridge const listsToShow: BridgeTokenList[] = useMemo(() => { return BRIDGE_TOKEN_LISTS.filter(tokenList => { @@ -98,7 +96,7 @@ function TokenListsPanel() { if (isActive) { token.removeTokensFromList(bridgeTokenList.id) } else { - addBridgeTokenListToBridge(bridgeTokenList, arbTokenBridge) + addBridgeTokenListToBridge(bridgeTokenList, token) } } @@ -153,11 +151,7 @@ function TokensPanel({ onTokenSelected: (token: ERC20BridgeToken | null) => void }): JSX.Element { const { address: walletAddress } = useAccount() - const { - app: { - arbTokenBridge: { token, bridgeTokens } - } - } = useAppState() + const { token, bridgeTokens } = useArbTokenBridge() const [networks] = useNetworks() const { childChain, childChainProvider, parentChainProvider, isDepositMode } = useNetworksRelationship(networks) @@ -472,11 +466,7 @@ function TokensPanel({ export function TokenSearch({ close }: { close: () => void }) { const { address: walletAddress } = useAccount() - const { - app: { - arbTokenBridge: { token, bridgeTokens } - } - } = useAppState() + const { token, bridgeTokens } = useArbTokenBridge() const { app: { setSelectedToken } } = useActions() 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..5fddd44f08 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 { useArbTokenBridge } 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 } = useArbTokenBridge() 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 408eb28043..fd4618629d 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -70,6 +70,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' const isAllowedL2 = async ({ l1TokenAddress, @@ -118,14 +119,9 @@ export function TransferPanel() { const [showSCWalletTooltip, setShowSCWalletTooltip] = useState(false) const { - app: { - connectionState, - selectedToken, - arbTokenBridgeLoaded, - arbTokenBridge: { eth, token }, - warningTokens - } + app: { connectionState, selectedToken, warningTokens } } = useAppState() + const { eth, token } = useArbTokenBridge() const { layout } = useAppContextState() const { isTransferring } = layout const { address: walletAddress, isConnected } = useAccount() diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx index 2f7c44ae09..02f0e8b17c 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx @@ -74,6 +74,7 @@ import { } from './TransferDisabledDialog' import { getBridgeUiConfigForChain } from '../../util/bridgeUiConfig' import { useIsTestnetMode } from '../../hooks/useIsTestnetMode' +import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' enum NetworkType { l1 = 'l1', @@ -361,8 +362,8 @@ export function TransferPanelMain({ const { app } = useAppState() const { address: walletAddress } = useAccount() - const { arbTokenBridge, selectedToken } = app - const { token } = arbTokenBridge + const { selectedToken } = app + const { token } = useArbTokenBridge() const { destinationAddress, setDestinationAddress } = useDestinationAddressStore() 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 4e2fec1425..a84262a947 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelSummary.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelSummary.tsx @@ -22,6 +22,7 @@ import { ChainLayer, useChainLayers } from '../../hooks/useChainLayers' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' +import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' export type GasEstimationStatus = | 'idle' @@ -63,9 +64,7 @@ export function useGasSummary( token: TransferPanelSummaryToken | null, shouldRunGasEstimation: boolean ): UseGasSummaryResult { - const { - app: { arbTokenBridge } - } = useAppState() + const arbTokenBridge = useArbTokenBridge() const [networks] = useNetworks() const { childChainProvider, parentChainProvider, isDepositMode } = useNetworksRelationship(networks) 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 index cb46de693a..b862e753db 100644 --- a/packages/arb-token-bridge-ui/src/components/syncers/BalanceUpdater.tsx +++ b/packages/arb-token-bridge-ui/src/components/syncers/BalanceUpdater.tsx @@ -2,25 +2,29 @@ import { useEffect } from 'react' import { useLatest } from 'react-use' import { useAppState } from '../../state' +import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' // Updates all balances periodically const BalanceUpdater = (): JSX.Element => { const { - app: { arbTokenBridge, selectedToken } + app: { selectedToken } } = useAppState() - const latestTokenBridge = useLatest(arbTokenBridge) + const { token } = useArbTokenBridge() + + useEffect(() => { + console.log('TOKEN', token) + }, [token]) + const latestToken = useLatest(token) useEffect(() => { const interval = setInterval(() => { if (selectedToken) { - latestTokenBridge?.current?.token?.updateTokenData( - selectedToken.address - ) + latestToken?.current?.updateTokenData(selectedToken.address) } }, 10000) return () => clearInterval(interval) - }, [selectedToken]) + }, [latestToken, selectedToken]) return <> } 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..3b658dcc7f 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,21 @@ 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 { 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 } = useArbTokenBridge() const { childChain } = useNetworksRelationship(networks) useEffect(() => { - if (!arbTokenBridgeLoaded) { - return - } - if (!walletAddress) { return } @@ -41,9 +35,9 @@ const TokenListSyncer = (): JSX.Element => { }) tokenListsToSet.forEach(bridgeTokenList => { - addBridgeTokenListToBridge(bridgeTokenList, arbTokenBridge) + addBridgeTokenListToBridge(bridgeTokenList, token) }) - }, [walletAddress, childChain.id, arbTokenBridgeLoaded]) + }, [walletAddress, childChain.id, token]) return <> } diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index 33ffd3fa3e..b32dd04ba0 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, useState } from 'react' import { Chain, useAccount } from 'wagmi' import { BigNumber, utils } from 'ethers' import { Signer } from '@ethersproject/abstract-signer' @@ -48,6 +48,8 @@ import { addDepositToCache, getProvider } from '../components/TransactionHistory/helpers' +import { useNetworksRelationship } from './useNetworksRelationship' +import { useNetworks } from './useNetworks' export const wait = (ms = 0) => { return new Promise(res => setTimeout(res, ms)) @@ -102,10 +104,10 @@ export interface TokenBridgeParams { l2: { provider: JsonRpcProvider; network: Chain } } -export const useArbTokenBridge = ( - params: TokenBridgeParams -): ArbTokenBridge => { - const { l1, l2 } = params +export const useArbTokenBridge = (): ArbTokenBridge => { + const [networks] = useNetworks() + const { childChain, childChainProvider, parentChain, parentChainProvider } = + useNetworksRelationship(networks) const { address: walletAddress } = useAccount() const [bridgeTokens, setBridgeTokens] = useState< ContractStorage | undefined @@ -117,14 +119,14 @@ export const useArbTokenBridge = ( eth: [, updateEthL1Balance], erc20: [, updateErc20L1Balance] } = useBalance({ - provider: l1.provider, + provider: parentChainProvider, walletAddress }) const { eth: [, updateEthL2Balance], erc20: [, updateErc20L2Balance] } = useBalance({ - provider: l2.provider, + provider: childChainProvider, walletAddress }) @@ -132,7 +134,7 @@ export const useArbTokenBridge = ( [id: string]: boolean } - const nativeCurrency = useNativeCurrency({ provider: l2.provider }) + const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) const { updateUSDCBalances } = useUpdateUSDCBalances({ walletAddress @@ -148,9 +150,6 @@ export const useArbTokenBridge = ( React.Dispatch ] - const l1NetworkID = useMemo(() => String(l1.network.id), [l1.network.id]) - const l2NetworkID = useMemo(() => String(l2.network.id), [l2.network.id]) - const [ transactions, { addTransaction, updateTransaction, fetchAndUpdateL1ToL2MsgStatus } @@ -169,9 +168,10 @@ export const useArbTokenBridge = ( return } - const ethBridger = await EthBridger.fromProvider(l2.provider) - const parentChainBlockTimestamp = (await l1.provider.getBlock('latest')) - .timestamp + const ethBridger = await EthBridger.fromProvider(childChainProvider) + const parentChainBlockTimestamp = ( + await parentChainProvider.getBlock('latest') + ).timestamp const depositRequest = await ethBridger.getDepositRequest({ amount, @@ -181,7 +181,9 @@ export const useArbTokenBridge = ( let tx: L1EthDepositTransaction try { - const gasLimit = await l1.provider.estimateGas(depositRequest.txRequest) + const gasLimit = await parentChainProvider.estimateGas( + depositRequest.txRequest + ) tx = await ethBridger.deposit({ amount, @@ -215,8 +217,8 @@ export const useArbTokenBridge = ( blockNum: null, tokenAddress: null, depositStatus: DepositStatus.L1_PENDING, - parentChainId: Number(l1NetworkID), - childChainId: Number(l2NetworkID) + parentChainId: parentChain.id, + childChainId: childChain.id }) addDepositToCache({ @@ -226,11 +228,11 @@ export const useArbTokenBridge = ( txID: tx.hash, assetName: nativeCurrency.symbol, assetType: AssetType.ETH, - l1NetworkID, - l2NetworkID, + l1NetworkID: parentChain.id.toString(), + l2NetworkID: childChain.id.toString(), value: utils.formatUnits(amount, nativeCurrency.decimals), - parentChainId: Number(l1NetworkID), - childChainId: Number(l2NetworkID), + parentChainId: parentChain.id, + childChainId: childChain.id, direction: 'deposit', type: 'deposit-l1', source: 'local_storage_cache', @@ -261,7 +263,7 @@ export const useArbTokenBridge = ( } try { - const ethBridger = await EthBridger.fromProvider(l2.provider) + const ethBridger = await EthBridger.fromProvider(childChainProvider) const withdrawalRequest = await ethBridger.getWithdrawalRequest({ from: walletAddress, @@ -269,7 +271,7 @@ export const useArbTokenBridge = ( amount }) - const gasLimit = await l2.provider.estimateGas( + const gasLimit = await childChainProvider.estimateGas( withdrawalRequest.txRequest ) @@ -298,8 +300,8 @@ export const useArbTokenBridge = ( isWithdrawal: true, blockNum: null, tokenAddress: null, - parentChainId: Number(l1NetworkID), - childChainId: Number(l2NetworkID) + parentChainId: parentChain.id, + childChainId: childChain.id }) const receipt = await tx.wait() @@ -330,7 +332,7 @@ export const useArbTokenBridge = ( return } - const erc20Bridger = await Erc20Bridger.fromProvider(l2.provider) + const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) const tx = await erc20Bridger.approveToken({ erc20L1Address, @@ -339,7 +341,7 @@ export const useArbTokenBridge = ( const { symbol } = await fetchErc20Data({ address: erc20L1Address, - provider: l1.provider + provider: parentChainProvider }) addTransaction({ @@ -350,7 +352,7 @@ export const useArbTokenBridge = ( assetName: symbol, assetType: AssetType.ERC20, sender: walletAddress, - l1NetworkID + l1NetworkID: parentChain.id.toString() }) const receipt = await tx.wait() @@ -375,13 +377,13 @@ export const useArbTokenBridge = ( if (!l2Address) throw new Error('L2 address not found') const gatewayAddress = await fetchErc20L2GatewayAddress({ erc20L1Address, - l2Provider: l2.provider + l2Provider: childChainProvider }) const contract = await ERC20__factory.connect(l2Address, l2Signer) const tx = await contract.functions.approve(gatewayAddress, MaxUint256) const { symbol } = await fetchErc20Data({ address: erc20L1Address, - provider: l1.provider + provider: parentChainProvider }) addTransaction({ @@ -393,8 +395,8 @@ export const useArbTokenBridge = ( assetType: AssetType.ERC20, sender: walletAddress, blockNumber: tx.blockNumber, - l1NetworkID, - l2NetworkID + l1NetworkID: parentChain.id.toString(), + l2NetworkID: childChain.id.toString() }) const receipt = await tx.wait() @@ -418,19 +420,20 @@ export const useArbTokenBridge = ( if (!walletAddress) { return } - const erc20Bridger = await Erc20Bridger.fromProvider(l2.provider) - const parentChainBlockTimestamp = (await l1.provider.getBlock('latest')) - .timestamp + const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) + const parentChainBlockTimestamp = ( + await parentChainProvider.getBlock('latest') + ).timestamp try { const { symbol, decimals } = await fetchErc20Data({ address: erc20L1Address, - provider: l1.provider + provider: parentChainProvider }) const depositRequest = await erc20Bridger.getDepositRequest({ - l1Provider: l1.provider, - l2Provider: l2.provider, + l1Provider: parentChainProvider, + l2Provider: childChainProvider, from: walletAddress, erc20L1Address, destinationAddress, @@ -442,7 +445,9 @@ export const useArbTokenBridge = ( } }) - const gasLimit = await l1.provider.estimateGas(depositRequest.txRequest) + const gasLimit = await parentChainProvider.estimateGas( + depositRequest.txRequest + ) const tx = await erc20Bridger.deposit({ ...depositRequest, @@ -470,8 +475,8 @@ export const useArbTokenBridge = ( isWithdrawal: false, blockNum: null, tokenAddress: erc20L1Address, - parentChainId: Number(l1NetworkID), - childChainId: Number(l2NetworkID) + parentChainId: parentChain.id, + childChainId: childChain.id }) addDepositToCache({ @@ -481,11 +486,11 @@ export const useArbTokenBridge = ( txID: tx.hash, assetName: symbol, assetType: AssetType.ERC20, - l1NetworkID, - l2NetworkID, + l1NetworkID: parentChain.id.toString(), + l2NetworkID: childChain.id.toString(), value: utils.formatUnits(amount, decimals), - parentChainId: Number(l1NetworkID), - childChainId: Number(l2NetworkID), + parentChainId: parentChain.id, + childChainId: childChain.id, direction: 'deposit', type: 'deposit-l1', source: 'local_storage_cache', @@ -526,7 +531,7 @@ export const useArbTokenBridge = ( } try { - const erc20Bridger = await Erc20Bridger.fromProvider(l2.provider) + const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) const provider = l2Signer.provider const isSmartContractAddress = provider && (await provider.getCode(String(erc20L1Address))).length < 2 @@ -546,7 +551,7 @@ export const useArbTokenBridge = ( } const { symbol, decimals } = await fetchErc20Data({ address: erc20L1Address, - provider: l1.provider + provider: parentChainProvider }) addToken(erc20L1Address) @@ -560,7 +565,7 @@ export const useArbTokenBridge = ( amount }) - const gasLimit = await l2.provider.estimateGas( + const gasLimit = await childChainProvider.estimateGas( withdrawalRequest.txRequest ) @@ -589,8 +594,8 @@ export const useArbTokenBridge = ( isWithdrawal: true, blockNum: null, tokenAddress: erc20L1Address, - parentChainId: Number(l1NetworkID), - childChainId: Number(l2NetworkID) + parentChainId: parentChain.id, + childChainId: childChain.id }) const receipt = await tx.wait() @@ -627,9 +632,6 @@ export const useArbTokenBridge = ( } const addTokensFromList = async (arbTokenList: TokenList, listId: number) => { - const l1ChainID = l1.network.id - const l2ChainID = l2.network.id - const bridgeTokensToAdd: ContractStorage = {} const candidateUnbridgedTokensToAdd: ERC20BridgeToken[] = [] @@ -638,7 +640,7 @@ export const useArbTokenBridge = ( const { address, name, symbol, extensions, decimals, logoURI, chainId } = tokenData - if (![l1ChainID, l2ChainID].includes(chainId)) { + if (![parentChain.id, childChain.id].includes(chainId)) { continue } @@ -674,7 +676,7 @@ export const useArbTokenBridge = ( })() if (bridgeInfo) { - const l1Address = bridgeInfo[l1NetworkID]?.tokenAddress.toLowerCase() + const l1Address = bridgeInfo[parentChain.id]?.tokenAddress.toLowerCase() if (!l1Address) { return @@ -726,10 +728,10 @@ export const useArbTokenBridge = ( // 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) { + if (isNetwork(childChain.id).isArbitrumOne) { l2Addresses.push(CommonAddress.ArbitrumOne.USDC) } - if (isNetwork(l2ChainID).isArbitrumSepolia) { + if (isNetwork(childChain.id).isArbitrumSepolia) { l2Addresses.push(CommonAddress.ArbitrumSepolia.USDC) } @@ -774,7 +776,7 @@ export const useArbTokenBridge = ( const lowercasedErc20L1orL2Address = erc20L1orL2Address.toLowerCase() const maybeL1Address = await getL1ERC20Address({ erc20L2Address: lowercasedErc20L1orL2Address, - l2Provider: l2.provider + l2Provider: childChainProvider }) if (maybeL1Address) { @@ -786,13 +788,13 @@ export const useArbTokenBridge = ( l1Address = lowercasedErc20L1orL2Address l2Address = await getL2ERC20Address({ erc20L1Address: l1Address, - l1Provider: l1.provider, - l2Provider: l2.provider + l1Provider: parentChainProvider, + l2Provider: childChainProvider }) } const bridgeTokensToAdd: ContractStorage = {} - const erc20Params = { address: l1Address, provider: l1.provider } + const erc20Params = { address: l1Address, provider: parentChainProvider } if (!(await isValidErc20(erc20Params))) { throw new Error(`${l1Address} is not a valid ERC-20 token`) @@ -802,8 +804,8 @@ export const useArbTokenBridge = ( const isDisabled = await l1TokenIsDisabled({ erc20L1Address: l1Address, - l1Provider: l1.provider, - l2Provider: l2.provider + l1Provider: parentChainProvider, + l2Provider: childChainProvider }) if (isDisabled) { @@ -855,7 +857,12 @@ export const useArbTokenBridge = ( updateErc20L2Balance([l2Address]) } }, - [bridgeTokens, setBridgeTokens, updateErc20L1Balance, updateErc20L2Balance] + [ + bridgeTokens, + updateErc20L1Balance, + updateErc20L2Balance, + updateUSDCBalances + ] ) const updateEthBalances = async () => { @@ -898,7 +905,7 @@ export const useArbTokenBridge = ( } function addL2NativeToken(erc20L2Address: string) { - const token = getL2NativeToken(erc20L2Address, l2.network.id) + const token = getL2NativeToken(erc20L2Address, childChain.id) setBridgeTokens(oldBridgeTokens => { return { @@ -960,7 +967,7 @@ export const useArbTokenBridge = ( events.forEach((event: L2ToL1EventResult) => { const cacheKey = getExecutedMessagesCacheKey({ event, - l2ChainId: l2.network.id + l2ChainId: childChain.id }) added[cacheKey] = true diff --git a/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts b/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts index c4ae3a54f1..d29780f737 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts @@ -17,6 +17,7 @@ import { useTransactionHistory } from './useTransactionHistory' import dayjs from 'dayjs' import { fetchErc20Data } from '../util/TokenUtils' import { fetchNativeCurrency } from './useNativeCurrency' +import { useArbTokenBridge } from './useArbTokenBridge' export type UseClaimWithdrawalResult = { claim: (tx: MergedTransaction) => Promise @@ -24,9 +25,7 @@ export type UseClaimWithdrawalResult = { } export function useClaimWithdrawal(): UseClaimWithdrawalResult { - const { - app: { arbTokenBridge } - } = useAppState() + const arbTokenBridge = useArbTokenBridge() const { address } = useAccount() const { data: signer } = useSigner() const { updatePendingTransaction } = useTransactionHistory(address) diff --git a/packages/arb-token-bridge-ui/src/hooks/useRedeemRetryable.ts b/packages/arb-token-bridge-ui/src/hooks/useRedeemRetryable.ts index 745c2f0c1d..e624802835 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useRedeemRetryable.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useRedeemRetryable.ts @@ -15,6 +15,7 @@ import { errorToast } from '../components/common/atoms/Toast' import { AssetType } from './arbTokenBridge.types' import { useNetworks } from './useNetworks' import { useNetworksRelationship } from './useNetworksRelationship' +import { useArbTokenBridge } from './useArbTokenBridge' export type UseRedeemRetryableResult = { redeem: (tx: MergedTransaction) => void @@ -22,9 +23,7 @@ export type UseRedeemRetryableResult = { } export function useRedeemRetryable(): UseRedeemRetryableResult { - const { - app: { arbTokenBridge } - } = useAppState() + const arbTokenBridge = useArbTokenBridge() const [networks] = useNetworks() const { childChain, parentChainProvider } = useNetworksRelationship(networks) const { data: signer } = useSigner() 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 c879941d92..38504b7976 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 { ERC20BridgeToken } from '../../hooks/arbTokenBridge.types' import { Context } from '..' import { ConnectionState } from '../../util' import { WhiteListState, WarningTokens } from './state' @@ -39,10 +36,8 @@ export const reset = ({ state }: Context, newChainId: number) => { 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 = ( @@ -58,20 +53,3 @@ export const setWhitelistState = ( ) => { 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 6af36d5a99..5d916a1b2e 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 @@ -79,25 +78,21 @@ 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 } export const defaultState: AppState = { - arbTokenBridge: {} as ArbTokenBridge, warningTokens: {} as WarningTokens, connectionState: ConnectionState.LOADING, l1NetworkChainId: null, l2NetworkChainId: 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 78f8bd61d5..f141811882 100644 --- a/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts @@ -181,13 +181,13 @@ export const validateTokenList = (tokenList: TokenList) => { export const addBridgeTokenListToBridge = ( bridgeTokenList: BridgeTokenList, - arbTokenBridge: ArbTokenBridge + token: ArbTokenBridge['token'] ) => { fetchTokenListFromURL(bridgeTokenList.url).then( ({ isValid, data: tokenList }) => { if (!isValid) return - arbTokenBridge.token.addTokensFromList(tokenList!, bridgeTokenList.id) + token.addTokensFromList(tokenList!, bridgeTokenList.id) } ) } From 0e08dfd92f44322ca1fb15327adb90cdc861cced Mon Sep 17 00:00:00 2001 From: Christophe Date: Fri, 26 Jan 2024 20:52:22 +0100 Subject: [PATCH 02/28] Update syncers --- .../src/components/syncers/BalanceUpdater.tsx | 32 ------- .../components/syncers/TokenListSyncer.tsx | 13 ++- .../src/hooks/useTransactionHistory.ts | 83 +++++++++++++------ 3 files changed, 63 insertions(+), 65 deletions(-) delete mode 100644 packages/arb-token-bridge-ui/src/components/syncers/BalanceUpdater.tsx 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 b862e753db..0000000000 --- a/packages/arb-token-bridge-ui/src/components/syncers/BalanceUpdater.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect } from 'react' -import { useLatest } from 'react-use' - -import { useAppState } from '../../state' -import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' - -// Updates all balances periodically -const BalanceUpdater = (): JSX.Element => { - const { - app: { selectedToken } - } = useAppState() - const { token } = useArbTokenBridge() - - useEffect(() => { - console.log('TOKEN', token) - }, [token]) - const latestToken = useLatest(token) - - useEffect(() => { - const interval = setInterval(() => { - if (selectedToken) { - latestToken?.current?.updateTokenData(selectedToken.address) - } - }, 10000) - - return () => clearInterval(interval) - }, [latestToken, 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 3b658dcc7f..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,10 +3,7 @@ import { useAccount } from 'wagmi' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' -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 @@ -14,7 +11,9 @@ import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' const TokenListSyncer = (): JSX.Element => { const { address: walletAddress } = useAccount() const [networks] = useNetworks() - const { token } = useArbTokenBridge() + const { + token: { addBridgeTokenListToBridge } + } = useArbTokenBridge() const { childChain } = useNetworksRelationship(networks) useEffect(() => { @@ -35,9 +34,9 @@ const TokenListSyncer = (): JSX.Element => { }) tokenListsToSet.forEach(bridgeTokenList => { - addBridgeTokenListToBridge(bridgeTokenList, token) + addBridgeTokenListToBridge(bridgeTokenList) }) - }, [walletAddress, childChain.id, token]) + }, [walletAddress, childChain.id, addBridgeTokenListToBridge]) return <> } diff --git a/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts b/packages/arb-token-bridge-ui/src/hooks/useTransactionHistory.ts index 56d3f3fabb..3bcb1db10f 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' @@ -702,18 +702,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(() => { @@ -758,40 +772,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 - } + ]) } From 14f7d20b2b117f8f90578ac8fde22ccf60bd3024 Mon Sep 17 00:00:00 2001 From: Christophe Date: Fri, 26 Jan 2024 20:54:20 +0100 Subject: [PATCH 03/28] Add useCallback to useArbTokenBridge, move addBridgeTokenListToBridge --- .../src/components/App/App.tsx | 14 +- .../components/TransferPanel/TokenSearch.tsx | 17 +- .../src/hooks/useArbTokenBridge.ts | 1601 ++++++++++------- .../src/util/TokenListUtils.ts | 14 - 4 files changed, 931 insertions(+), 715 deletions(-) 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 942ef5e404..44a24745f8 100644 --- a/packages/arb-token-bridge-ui/src/components/App/App.tsx +++ b/packages/arb-token-bridge-ui/src/components/App/App.tsx @@ -12,7 +12,7 @@ import merge from 'lodash-es/merge' import axios from 'axios' import { createOvermind, Overmind } from 'overmind' import { Provider } from 'overmind-react' -import { useLocalStorage } from 'react-use' +import { useInterval, useLocalStorage } from 'react-use' import { ConnectionState } from '../../util' import { WelcomeDialog } from './WelcomeDialog' import { BlockedDialog } from './BlockedDialog' @@ -44,6 +44,7 @@ import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { HeaderConnectWalletButton } from '../common/HeaderConnectWalletButton' import { AppConnectionFallbackContainer } from './AppConnectionFallbackContainer' import { ProviderName, trackEvent } from '../../util/AnalyticsUtils' +import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' declare global { interface Window { @@ -63,8 +64,17 @@ const rainbowkitTheme = merge(darkTheme(), { const AppContent = (): JSX.Element => { const [{ sourceChain }] = useNetworks() const { - app: { connectionState } + app: { connectionState, selectedToken } } = useAppState() + const { + token: { updateTokenData } + } = useArbTokenBridge() + + useInterval(() => { + if (selectedToken) { + updateTokenData(selectedToken.address) + } + }, 10_000) const headerOverridesProps: HeaderOverridesProps = useMemo(() => { const { isTestnet, isGoerli } = isNetwork(sourceChain.id ?? 0) 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 876c84fffd..0eb54f5f8f 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx @@ -11,11 +11,10 @@ import Image from 'next/image' import { useAccount } from 'wagmi' import { Loader } from '../common/atoms/Loader' -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 { @@ -44,7 +43,10 @@ import { useTransferDisabledDialogStore } from './TransferDisabledDialog' import { isWithdrawOnlyToken } from '../../util/WithdrawOnlyUtils' import { isTransferDisabledToken } from '../../util/TokenTransferDisabledUtils' import { useTokenFromSearchParams } from './TransferPanelUtils' -import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' +import { + useArbTokenBridge, + useBridgeTokensStore +} from '../../hooks/useArbTokenBridge' enum Panel { TOKENS, @@ -70,7 +72,10 @@ const ARB_SEPOLIA_NATIVE_USDC_TOKEN = { } function TokenListsPanel() { - const { bridgeTokens, token } = useArbTokenBridge() + const { + token: { removeTokensFromList, addBridgeTokenListToBridge } + } = useArbTokenBridge() + const { bridgeTokens } = useBridgeTokensStore() const [networks] = useNetworks() const { childChain } = useNetworksRelationship(networks) @@ -94,9 +99,9 @@ function TokenListsPanel() { isActive: boolean ) => { if (isActive) { - token.removeTokensFromList(bridgeTokenList.id) + removeTokensFromList(bridgeTokenList.id) } else { - addBridgeTokenListToBridge(bridgeTokenList, token) + addBridgeTokenListToBridge(bridgeTokenList) } } diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index b32dd04ba0..e76edc45dd 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -1,8 +1,7 @@ -import { useCallback, useState } from 'react' -import { Chain, useAccount } from 'wagmi' +import { useCallback, useMemo } from 'react' +import { useAccount } from 'wagmi' import { BigNumber, utils } from 'ethers' import { Signer } from '@ethersproject/abstract-signer' -import { JsonRpcProvider } from '@ethersproject/providers' import { useLocalStorage } from '@rehooks/local-storage' import { TokenList } from '@uniswap/token-lists' import { MaxUint256 } from '@ethersproject/constants' @@ -50,6 +49,8 @@ import { } from '../components/TransactionHistory/helpers' import { useNetworksRelationship } from './useNetworksRelationship' import { useNetworks } from './useNetworks' +import { BridgeTokenList, fetchTokenListFromURL } from '../util/TokenListUtils' +import { create } from 'zustand' export const wait = (ms = 0) => { return new Promise(res => setTimeout(res, ms)) @@ -99,19 +100,26 @@ function percentIncrease(num: BigNumber, increase: BigNumber): BigNumber { return num.add(num.mul(increase).div(100)) } -export interface TokenBridgeParams { - l1: { provider: JsonRpcProvider; network: Chain } - l2: { provider: JsonRpcProvider; network: Chain } +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 { addPendingTransaction } = useTransactionHistory(walletAddress) @@ -129,6 +137,9 @@ export const useArbTokenBridge = (): ArbTokenBridge => { provider: childChainProvider, walletAddress }) + const updateEthBalances = useCallback(async () => { + Promise.all([updateEthL1Balance(), updateEthL2Balance()]) + }, [updateEthL1Balance, updateEthL2Balance]) interface ExecutedMessagesCache { [id: string]: boolean @@ -155,155 +166,92 @@ export const useArbTokenBridge = (): ArbTokenBridge => { { addTransaction, updateTransaction, fetchAndUpdateL1ToL2MsgStatus } ] = useTransactions() - const depositEth = async ({ - amount, - l1Signer, - txLifecycle - }: { - amount: BigNumber - l1Signer: Signer - txLifecycle?: L1EthDepositTransactionLifecycle - }) => { - if (!walletAddress) { - return - } - - const ethBridger = await EthBridger.fromProvider(childChainProvider) - const parentChainBlockTimestamp = ( - await parentChainProvider.getBlock('latest') - ).timestamp - - const depositRequest = await ethBridger.getDepositRequest({ + const depositEth = useCallback( + async ({ amount, - from: walletAddress - }) - - let tx: L1EthDepositTransaction - - try { - const gasLimit = await parentChainProvider.estimateGas( - depositRequest.txRequest - ) - - tx = await ethBridger.deposit({ - amount, - l1Signer, - overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(5)) } - }) - - if (txLifecycle?.onTxSubmit) { - txLifecycle.onTxSubmit(tx) - } - } catch (error: any) { - if (txLifecycle?.onTxError) { - txLifecycle.onTxError(error) + l1Signer, + txLifecycle + }: { + amount: BigNumber + l1Signer: Signer + txLifecycle?: L1EthDepositTransactionLifecycle + }) => { + if (!walletAddress) { + return } - return error.message - } - - addPendingTransaction({ - sender: walletAddress, - destination: walletAddress, - direction: 'deposit-l1', - status: 'pending', - createdAt: parentChainBlockTimestamp * 1_000, - resolvedAt: null, - txId: tx.hash, - asset: nativeCurrency.symbol, - assetType: AssetType.ETH, - value: utils.formatUnits(amount, nativeCurrency.decimals), - uniqueId: null, - isWithdrawal: false, - blockNum: null, - tokenAddress: null, - depositStatus: DepositStatus.L1_PENDING, - parentChainId: parentChain.id, - childChainId: childChain.id - }) - - addDepositToCache({ - sender: walletAddress, - destination: walletAddress, - status: 'pending', - txID: tx.hash, - assetName: nativeCurrency.symbol, - assetType: AssetType.ETH, - l1NetworkID: parentChain.id.toString(), - l2NetworkID: childChain.id.toString(), - value: utils.formatUnits(amount, nativeCurrency.decimals), - parentChainId: parentChain.id, - childChainId: childChain.id, - direction: 'deposit', - type: 'deposit-l1', - source: 'local_storage_cache', - timestampCreated: String(parentChainBlockTimestamp), - nonce: tx.nonce - }) - - const receipt = await tx.wait() - - if (txLifecycle?.onTxConfirm) { - txLifecycle.onTxConfirm(receipt) - } - - updateEthBalances() - - if (nativeCurrency.isCustom) { - updateErc20L1Balance([nativeCurrency.address]) - } - } - - const withdrawEth: ArbTokenBridgeEth['withdraw'] = async ({ - amount, - l2Signer, - txLifecycle - }) => { - if (!walletAddress) { - return - } - try { const ethBridger = await EthBridger.fromProvider(childChainProvider) + const parentChainBlockTimestamp = ( + await parentChainProvider.getBlock('latest') + ).timestamp - const withdrawalRequest = await ethBridger.getWithdrawalRequest({ - from: walletAddress, - destinationAddress: walletAddress, - amount + const depositRequest = await ethBridger.getDepositRequest({ + amount, + from: walletAddress }) - const gasLimit = await childChainProvider.estimateGas( - withdrawalRequest.txRequest - ) + let tx: L1EthDepositTransaction - const tx = await ethBridger.withdraw({ - ...withdrawalRequest, - l2Signer, - overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(30)) } - }) + try { + const gasLimit = await parentChainProvider.estimateGas( + depositRequest.txRequest + ) - if (txLifecycle?.onTxSubmit) { - txLifecycle.onTxSubmit(tx) + tx = await ethBridger.deposit({ + amount, + l1Signer, + overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(5)) } + }) + + if (txLifecycle?.onTxSubmit) { + txLifecycle.onTxSubmit(tx) + } + } catch (error: any) { + if (txLifecycle?.onTxError) { + txLifecycle.onTxError(error) + } + return error.message } addPendingTransaction({ sender: walletAddress, destination: walletAddress, - direction: 'withdraw', - status: WithdrawalStatus.UNCONFIRMED, - createdAt: dayjs().valueOf(), + direction: 'deposit-l1', + status: 'pending', + createdAt: parentChainBlockTimestamp * 1_000, resolvedAt: null, txId: tx.hash, asset: nativeCurrency.symbol, assetType: AssetType.ETH, value: utils.formatUnits(amount, nativeCurrency.decimals), uniqueId: null, - isWithdrawal: true, + isWithdrawal: false, blockNum: null, tokenAddress: null, + depositStatus: DepositStatus.L1_PENDING, parentChainId: parentChain.id, childChainId: childChain.id }) + addDepositToCache({ + sender: walletAddress, + destination: walletAddress, + status: 'pending', + txID: tx.hash, + assetName: nativeCurrency.symbol, + assetType: AssetType.ETH, + l1NetworkID: parentChain.id.toString(), + l2NetworkID: childChain.id.toString(), + value: utils.formatUnits(amount, nativeCurrency.decimals), + parentChainId: parentChain.id, + childChainId: childChain.id, + direction: 'deposit', + type: 'deposit-l1', + source: 'local_storage_cache', + timestampCreated: String(parentChainBlockTimestamp), + nonce: tx.nonce + }) + const receipt = await tx.wait() if (txLifecycle?.onTxConfirm) { @@ -312,694 +260,961 @@ export const useArbTokenBridge = (): ArbTokenBridge => { updateEthBalances() - return receipt - } catch (error) { - if (txLifecycle?.onTxError) { - txLifecycle.onTxError(error) + if (nativeCurrency.isCustom) { + updateErc20L1Balance([nativeCurrency.address]) } - console.error('withdrawEth err', error) - } - } + }, + [ + addPendingTransaction, + childChain.id, + childChainProvider, + nativeCurrency, + parentChain.id, + parentChainProvider, + updateErc20L1Balance, + updateEthBalances, + walletAddress + ] + ) - const approveToken = async ({ - erc20L1Address, - l1Signer - }: { - erc20L1Address: string - l1Signer: Signer - }) => { - if (!walletAddress) { - return - } + const withdrawEth: ArbTokenBridgeEth['withdraw'] = useCallback( + async ({ amount, l2Signer, txLifecycle }) => { + if (!walletAddress) { + return + } - const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) + try { + const ethBridger = await EthBridger.fromProvider(childChainProvider) - const tx = await erc20Bridger.approveToken({ - erc20L1Address, - l1Signer - }) - - const { symbol } = await fetchErc20Data({ - address: erc20L1Address, - provider: parentChainProvider - }) - - addTransaction({ - type: 'approve', - status: 'pending', - value: null, - txID: tx.hash, - assetName: symbol, - assetType: AssetType.ERC20, - sender: walletAddress, - l1NetworkID: parentChain.id.toString() - }) - - const receipt = await tx.wait() - - updateTransaction(receipt, tx) - updateTokenData(erc20L1Address) - } + const withdrawalRequest = await ethBridger.getWithdrawalRequest({ + from: walletAddress, + destinationAddress: walletAddress, + amount + }) - const approveTokenL2 = async ({ - erc20L1Address, - l2Signer - }: { - erc20L1Address: string - l2Signer: Signer - }) => { - if (typeof bridgeTokens === 'undefined' || !walletAddress) { - return - } - const bridgeToken = bridgeTokens[erc20L1Address] - if (!bridgeToken) throw new Error('Bridge token not found') - const { l2Address } = bridgeToken - if (!l2Address) throw new Error('L2 address not found') - const gatewayAddress = await fetchErc20L2GatewayAddress({ - erc20L1Address, - l2Provider: childChainProvider - }) - const contract = await ERC20__factory.connect(l2Address, l2Signer) - const tx = await contract.functions.approve(gatewayAddress, MaxUint256) - const { symbol } = await fetchErc20Data({ - address: erc20L1Address, - provider: parentChainProvider - }) - - addTransaction({ - type: 'approve-l2', - status: 'pending', - value: null, - txID: tx.hash, - assetName: symbol, - assetType: AssetType.ERC20, - sender: walletAddress, - blockNumber: tx.blockNumber, - l1NetworkID: parentChain.id.toString(), - l2NetworkID: childChain.id.toString() - }) - - const receipt = await tx.wait() - updateTransaction(receipt, tx) - updateTokenData(erc20L1Address) - } + const gasLimit = await childChainProvider.estimateGas( + withdrawalRequest.txRequest + ) - async function depositToken({ - erc20L1Address, - amount, - l1Signer, - txLifecycle, - destinationAddress - }: { - erc20L1Address: string - amount: BigNumber - l1Signer: Signer - txLifecycle?: L1ContractCallTransactionLifecycle - destinationAddress?: string - }) { - if (!walletAddress) { - return - } - const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) - const parentChainBlockTimestamp = ( - await parentChainProvider.getBlock('latest') - ).timestamp - - try { - const { symbol, decimals } = await fetchErc20Data({ - address: erc20L1Address, - provider: parentChainProvider - }) + const tx = await ethBridger.withdraw({ + ...withdrawalRequest, + l2Signer, + overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(30)) } + }) - const depositRequest = await erc20Bridger.getDepositRequest({ - l1Provider: parentChainProvider, - l2Provider: childChainProvider, - from: walletAddress, - erc20L1Address, - destinationAddress, - amount, - retryableGasOverrides: { - // the gas limit may vary by about 20k due to SSTORE (zero vs nonzero) - // the 30% gas limit increase should cover the difference - gasLimit: { percentIncrease: BigNumber.from(30) } + if (txLifecycle?.onTxSubmit) { + txLifecycle.onTxSubmit(tx) } - }) - const gasLimit = await parentChainProvider.estimateGas( - depositRequest.txRequest - ) + addPendingTransaction({ + sender: walletAddress, + destination: walletAddress, + direction: 'withdraw', + status: WithdrawalStatus.UNCONFIRMED, + createdAt: dayjs().valueOf(), + resolvedAt: null, + txId: tx.hash, + asset: nativeCurrency.symbol, + assetType: AssetType.ETH, + value: utils.formatUnits(amount, nativeCurrency.decimals), + uniqueId: null, + isWithdrawal: true, + blockNum: null, + tokenAddress: null, + parentChainId: parentChain.id, + childChainId: childChain.id + }) - const tx = await erc20Bridger.deposit({ - ...depositRequest, - l1Signer, - overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(5)) } + const receipt = await tx.wait() + + if (txLifecycle?.onTxConfirm) { + txLifecycle.onTxConfirm(receipt) + } + + updateEthBalances() + + return receipt + } catch (error) { + if (txLifecycle?.onTxError) { + txLifecycle.onTxError(error) + } + console.error('withdrawEth err', error) + } + }, + [ + addPendingTransaction, + childChain.id, + childChainProvider, + nativeCurrency.decimals, + nativeCurrency.symbol, + parentChain.id, + updateEthBalances, + walletAddress + ] + ) + + const updateTokenData = useCallback( + async (l1Address: string) => { + updateUSDCBalances(l1Address) + + if (typeof bridgeTokens === 'undefined') { + return + } + const l1AddressLowerCased = l1Address.toLowerCase() + const bridgeToken = bridgeTokens[l1AddressLowerCased] + + if (!bridgeToken) { + return + } + + const newBridgeTokens = { [l1AddressLowerCased]: bridgeToken } + setBridgeTokens(oldBridgeTokens => { + return { ...oldBridgeTokens, ...newBridgeTokens } }) + const { l2Address } = bridgeToken + updateErc20L1Balance([l1AddressLowerCased]) + if (l2Address) { + updateErc20L2Balance([l2Address]) + } + }, + [ + bridgeTokens, + setBridgeTokens, + updateErc20L1Balance, + updateErc20L2Balance, + updateUSDCBalances + ] + ) - if (txLifecycle?.onTxSubmit) { - txLifecycle.onTxSubmit(tx) + const approveToken = useCallback( + async ({ + erc20L1Address, + l1Signer + }: { + erc20L1Address: string + l1Signer: Signer + }) => { + if (!walletAddress) { + return } - addPendingTransaction({ - sender: walletAddress, - destination: destinationAddress ?? walletAddress, - direction: 'deposit-l1', - status: 'pending', - createdAt: parentChainBlockTimestamp * 1_000, - resolvedAt: null, - txId: tx.hash, - asset: symbol, - assetType: AssetType.ERC20, - value: utils.formatUnits(amount, decimals), - depositStatus: DepositStatus.L1_PENDING, - uniqueId: null, - isWithdrawal: false, - blockNum: null, - tokenAddress: erc20L1Address, - parentChainId: parentChain.id, - childChainId: childChain.id + const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) + + const tx = await erc20Bridger.approveToken({ + erc20L1Address, + l1Signer }) - addDepositToCache({ - sender: walletAddress, - destination: destinationAddress ?? walletAddress, + const { symbol } = await fetchErc20Data({ + address: erc20L1Address, + provider: parentChainProvider + }) + + addTransaction({ + type: 'approve', status: 'pending', + value: null, txID: tx.hash, assetName: symbol, assetType: AssetType.ERC20, - l1NetworkID: parentChain.id.toString(), - l2NetworkID: childChain.id.toString(), - value: utils.formatUnits(amount, decimals), - parentChainId: parentChain.id, - childChainId: childChain.id, - direction: 'deposit', - type: 'deposit-l1', - source: 'local_storage_cache', - timestampCreated: String(parentChainBlockTimestamp), - nonce: tx.nonce + sender: walletAddress, + l1NetworkID: parentChain.id.toString() }) const receipt = await tx.wait() - if (txLifecycle?.onTxConfirm) { - txLifecycle.onTxConfirm(receipt) - } - + updateTransaction(receipt, tx) updateTokenData(erc20L1Address) - updateEthBalances() + }, + [ + addTransaction, + childChainProvider, + parentChain.id, + parentChainProvider, + updateTokenData, + updateTransaction, + walletAddress + ] + ) - if (nativeCurrency.isCustom) { - updateErc20L1Balance([nativeCurrency.address]) + const approveTokenL2 = useCallback( + async ({ + erc20L1Address, + l2Signer + }: { + erc20L1Address: string + l2Signer: Signer + }) => { + if (typeof bridgeTokens === 'undefined' || !walletAddress) { + return } + const bridgeToken = bridgeTokens[erc20L1Address] + if (!bridgeToken) throw new Error('Bridge token not found') + const { l2Address } = bridgeToken + if (!l2Address) throw new Error('L2 address not found') + const gatewayAddress = await fetchErc20L2GatewayAddress({ + erc20L1Address, + l2Provider: childChainProvider + }) + const contract = await ERC20__factory.connect(l2Address, l2Signer) + const tx = await contract.functions.approve(gatewayAddress, MaxUint256) + const { symbol } = await fetchErc20Data({ + address: erc20L1Address, + provider: parentChainProvider + }) - return receipt - } catch (error) { - if (txLifecycle?.onTxError) { - txLifecycle.onTxError(error) - } - } - } + addTransaction({ + type: 'approve-l2', + status: 'pending', + value: null, + txID: tx.hash, + assetName: symbol, + assetType: AssetType.ERC20, + sender: walletAddress, + blockNumber: tx.blockNumber, + l1NetworkID: parentChain.id.toString(), + l2NetworkID: childChain.id.toString() + }) - const withdrawToken: ArbTokenBridgeToken['withdraw'] = async ({ - erc20L1Address, - amount, - l2Signer, - txLifecycle, - destinationAddress - }) => { - if (!walletAddress) { - return - } - - try { - const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) - const provider = l2Signer.provider - const isSmartContractAddress = - provider && (await provider.getCode(String(erc20L1Address))).length < 2 - if (isSmartContractAddress && !destinationAddress) { - throw new Error(`Missing destination address`) - } + const receipt = await tx.wait() + updateTransaction(receipt, tx) + updateTokenData(erc20L1Address) + }, + [ + addTransaction, + bridgeTokens, + childChain.id, + childChainProvider, + parentChain.id, + parentChainProvider, + updateTokenData, + updateTransaction, + walletAddress + ] + ) - if (typeof bridgeTokens === 'undefined') { + const depositToken = useCallback( + async ({ + erc20L1Address, + amount, + l1Signer, + txLifecycle, + destinationAddress + }: { + erc20L1Address: string + amount: BigNumber + l1Signer: Signer + txLifecycle?: L1ContractCallTransactionLifecycle + destinationAddress?: string + }) => { + if (!walletAddress) { return } - const bridgeToken = bridgeTokens[erc20L1Address] + const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) + const parentChainBlockTimestamp = ( + await parentChainProvider.getBlock('latest') + ).timestamp - const { symbol, decimals } = await (async () => { - if (bridgeToken) { - const { symbol, decimals } = bridgeToken - return { symbol, decimals } - } + try { const { symbol, decimals } = await fetchErc20Data({ address: erc20L1Address, provider: parentChainProvider }) - addToken(erc20L1Address) - return { symbol, decimals } - })() + const depositRequest = await erc20Bridger.getDepositRequest({ + l1Provider: parentChainProvider, + l2Provider: childChainProvider, + from: walletAddress, + erc20L1Address, + destinationAddress, + amount, + retryableGasOverrides: { + // the gas limit may vary by about 20k due to SSTORE (zero vs nonzero) + // the 30% gas limit increase should cover the difference + gasLimit: { percentIncrease: BigNumber.from(30) } + } + }) + + const gasLimit = await parentChainProvider.estimateGas( + depositRequest.txRequest + ) - const withdrawalRequest = await erc20Bridger.getWithdrawalRequest({ - from: walletAddress, - erc20l1Address: erc20L1Address, - destinationAddress: destinationAddress ?? walletAddress, - amount - }) + const tx = await erc20Bridger.deposit({ + ...depositRequest, + l1Signer, + overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(5)) } + }) - const gasLimit = await childChainProvider.estimateGas( - withdrawalRequest.txRequest - ) + if (txLifecycle?.onTxSubmit) { + txLifecycle.onTxSubmit(tx) + } - const tx = await erc20Bridger.withdraw({ - ...withdrawalRequest, - l2Signer, - overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(30)) } - }) + addPendingTransaction({ + sender: walletAddress, + destination: destinationAddress ?? walletAddress, + direction: 'deposit-l1', + status: 'pending', + createdAt: parentChainBlockTimestamp * 1_000, + resolvedAt: null, + txId: tx.hash, + asset: symbol, + assetType: AssetType.ERC20, + value: utils.formatUnits(amount, decimals), + depositStatus: DepositStatus.L1_PENDING, + uniqueId: null, + isWithdrawal: false, + blockNum: null, + tokenAddress: erc20L1Address, + parentChainId: parentChain.id, + childChainId: childChain.id + }) + + addDepositToCache({ + sender: walletAddress, + destination: destinationAddress ?? walletAddress, + status: 'pending', + txID: tx.hash, + assetName: symbol, + assetType: AssetType.ERC20, + l1NetworkID: parentChain.id.toString(), + l2NetworkID: childChain.id.toString(), + value: utils.formatUnits(amount, decimals), + parentChainId: parentChain.id, + childChainId: childChain.id, + direction: 'deposit', + type: 'deposit-l1', + source: 'local_storage_cache', + timestampCreated: String(parentChainBlockTimestamp), + nonce: tx.nonce + }) + + const receipt = await tx.wait() + + if (txLifecycle?.onTxConfirm) { + txLifecycle.onTxConfirm(receipt) + } + + updateTokenData(erc20L1Address) + updateEthBalances() + + if (nativeCurrency.isCustom) { + updateErc20L1Balance([nativeCurrency.address]) + } + + return receipt + } catch (error) { + if (txLifecycle?.onTxError) { + txLifecycle.onTxError(error) + } + } + }, + [ + addPendingTransaction, + childChain.id, + childChainProvider, + nativeCurrency, + parentChain.id, + parentChainProvider, + updateErc20L1Balance, + updateEthBalances, + updateTokenData, + walletAddress + ] + ) + + const addToken = useCallback( + async (erc20L1orL2Address: string) => { + let l1Address: string + let l2Address: string | undefined - if (txLifecycle?.onTxSubmit) { - txLifecycle.onTxSubmit(tx) + if (!walletAddress) { + return } - addPendingTransaction({ - sender: walletAddress, - destination: destinationAddress ?? walletAddress, - direction: 'withdraw', - status: WithdrawalStatus.UNCONFIRMED, - createdAt: dayjs().valueOf(), - resolvedAt: null, - txId: tx.hash, - asset: symbol, - assetType: AssetType.ERC20, - value: utils.formatUnits(amount, decimals), - uniqueId: null, - isWithdrawal: true, - blockNum: null, - tokenAddress: erc20L1Address, - parentChainId: parentChain.id, - childChainId: childChain.id + const lowercasedErc20L1orL2Address = erc20L1orL2Address.toLowerCase() + const maybeL1Address = await getL1ERC20Address({ + erc20L2Address: lowercasedErc20L1orL2Address, + l2Provider: childChainProvider }) - const receipt = await tx.wait() - - if (txLifecycle?.onTxConfirm) { - txLifecycle.onTxConfirm(receipt) + if (maybeL1Address) { + // looks like l2 address was provided + l1Address = maybeL1Address + l2Address = lowercasedErc20L1orL2Address + } else { + // looks like l1 address was provided + l1Address = lowercasedErc20L1orL2Address + l2Address = await getL2ERC20Address({ + erc20L1Address: l1Address, + l1Provider: parentChainProvider, + l2Provider: childChainProvider + }) } - updateTokenData(erc20L1Address) - return receipt - } catch (error) { - if (txLifecycle?.onTxError) { - txLifecycle.onTxError(error) + const bridgeTokensToAdd: ContractStorage = {} + const erc20Params = { address: l1Address, provider: parentChainProvider } + + if (!(await isValidErc20(erc20Params))) { + throw new Error(`${l1Address} is not a valid ERC-20 token`) } - console.warn('withdraw token err', error) - } - } - const removeTokensFromList = (listID: number) => { - setBridgeTokens(prevBridgeTokens => { - const newBridgeTokens = { ...prevBridgeTokens } - for (const address in bridgeTokens) { - const token = bridgeTokens[address] - if (!token) continue + const { name, symbol, decimals } = await fetchErc20Data(erc20Params) - token.listIds.delete(listID) + const isDisabled = await l1TokenIsDisabled({ + erc20L1Address: l1Address, + l1Provider: parentChainProvider, + l2Provider: childChainProvider + }) - if (token.listIds.size === 0) { - delete newBridgeTokens[address] - } + if (isDisabled) { + throw new TokenDisabledError('Token currently disabled') } - return newBridgeTokens - }) - } - const addTokensFromList = async (arbTokenList: TokenList, listId: number) => { - const bridgeTokensToAdd: ContractStorage = {} + const l1AddressLowerCased = l1Address.toLowerCase() + bridgeTokensToAdd[l1AddressLowerCased] = { + name, + type: TokenType.ERC20, + symbol, + address: l1AddressLowerCased, + l2Address: l2Address?.toLowerCase(), + decimals, + listIds: new Set() + } - const candidateUnbridgedTokensToAdd: ERC20BridgeToken[] = [] + setBridgeTokens(oldBridgeTokens => { + return { ...oldBridgeTokens, ...bridgeTokensToAdd } + }) - for (const tokenData of arbTokenList.tokens) { - const { address, name, symbol, extensions, decimals, logoURI, chainId } = - tokenData + updateErc20L1Balance([l1AddressLowerCased]) + if (l2Address) { + updateErc20L2Balance([l2Address]) + } + }, + [ + childChainProvider, + parentChainProvider, + setBridgeTokens, + updateErc20L1Balance, + updateErc20L2Balance, + walletAddress + ] + ) - if (![parentChain.id, childChain.id].includes(chainId)) { - continue + const withdrawToken: ArbTokenBridgeToken['withdraw'] = useCallback( + async ({ + erc20L1Address, + amount, + l2Signer, + txLifecycle, + destinationAddress + }) => { + if (!walletAddress) { + return } - 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 - } - } + try { + const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) + const provider = l2Signer.provider + const isSmartContractAddress = + provider && + (await provider.getCode(String(erc20L1Address))).length < 2 + if (isSmartContractAddress && !destinationAddress) { + throw new Error(`Missing destination 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 (typeof bridgeTokens === 'undefined') { + return } - if (!isExtensions(extensions)) { - return null - } else { - return extensions.bridgeInfo + const bridgeToken = bridgeTokens[erc20L1Address] + + const { symbol, decimals } = await (async () => { + if (bridgeToken) { + const { symbol, decimals } = bridgeToken + return { symbol, decimals } + } + const { symbol, decimals } = await fetchErc20Data({ + address: erc20L1Address, + provider: parentChainProvider + }) + + addToken(erc20L1Address) + return { symbol, decimals } + })() + + const withdrawalRequest = await erc20Bridger.getWithdrawalRequest({ + from: walletAddress, + erc20l1Address: erc20L1Address, + destinationAddress: destinationAddress ?? walletAddress, + amount + }) + + const gasLimit = await childChainProvider.estimateGas( + withdrawalRequest.txRequest + ) + + const tx = await erc20Bridger.withdraw({ + ...withdrawalRequest, + l2Signer, + overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(30)) } + }) + + if (txLifecycle?.onTxSubmit) { + txLifecycle.onTxSubmit(tx) } - })() - if (bridgeInfo) { - const l1Address = bridgeInfo[parentChain.id]?.tokenAddress.toLowerCase() + addPendingTransaction({ + sender: walletAddress, + destination: destinationAddress ?? walletAddress, + direction: 'withdraw', + status: WithdrawalStatus.UNCONFIRMED, + createdAt: dayjs().valueOf(), + resolvedAt: null, + txId: tx.hash, + asset: symbol, + assetType: AssetType.ERC20, + value: utils.formatUnits(amount, decimals), + uniqueId: null, + isWithdrawal: true, + blockNum: null, + tokenAddress: erc20L1Address, + parentChainId: parentChain.id, + childChainId: childChain.id + }) - if (!l1Address) { - return + const receipt = await tx.wait() + + if (txLifecycle?.onTxConfirm) { + txLifecycle.onTxConfirm(receipt) } - bridgeTokensToAdd[l1Address] = { - name, - type: TokenType.ERC20, - symbol, - address: l1Address, - l2Address: address.toLowerCase(), - decimals, - logoURI, - listIds: new Set([listId]) + updateTokenData(erc20L1Address) + return receipt + } catch (error) { + if (txLifecycle?.onTxError) { + txLifecycle.onTxError(error) } + console.warn('withdraw token err', error) } - // 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({ + }, + [ + addPendingTransaction, + addToken, + bridgeTokens, + childChain.id, + childChainProvider, + parentChain.id, + parentChainProvider, + updateTokenData, + 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] + } + } + return newBridgeTokens + }) + }, + [bridgeTokens, setBridgeTokens] + ) + + const addTokensFromList = useCallback( + async (arbTokenList: TokenList, listId: number) => { + const bridgeTokensToAdd: ContractStorage = {} + + const candidateUnbridgedTokensToAdd: ERC20BridgeToken[] = [] + + for (const tokenData of arbTokenList.tokens) { + const { + address, name, - type: TokenType.ERC20, symbol, - address: address.toLowerCase(), + extensions, decimals, logoURI, - listIds: new Set([listId]) - }) - } - } + chainId + } = tokenData - // 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 - } - } + if (![parentChain.id, childChain.id].includes(chainId)) { + continue + } + + 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(childChain.id).isArbitrumOne) { - l2Addresses.push(CommonAddress.ArbitrumOne.USDC) - } - if (isNetwork(childChain.id).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]) } - updateErc20L1Balance(l1Addresses) - updateErc20L2Balance(l2Addresses) - - return { - ...oldBridgeTokens, - ...bridgeTokensToAdd + // 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 + } } - }) - } - 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: childChainProvider - }) - - if (maybeL1Address) { - // looks like l2 address was provided - l1Address = maybeL1Address - l2Address = lowercasedErc20L1orL2Address - } else { - // looks like l1 address was provided - l1Address = lowercasedErc20L1orL2Address - l2Address = await getL2ERC20Address({ - erc20L1Address: l1Address, - l1Provider: parentChainProvider, - l2Provider: childChainProvider - }) - } - - const bridgeTokensToAdd: ContractStorage = {} - const erc20Params = { address: l1Address, provider: parentChainProvider } - - 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: 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]) - } - } + // 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[] = [] - const updateTokenData = useCallback( - async (l1Address: string) => { - updateUSDCBalances(l1Address) + // 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) + } - if (typeof bridgeTokens === 'undefined') { - return - } - const l1AddressLowerCased = l1Address.toLowerCase() - const bridgeToken = bridgeTokens[l1AddressLowerCased] + 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) + } - if (!bridgeToken) { - return - } + // 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]) + } - const newBridgeTokens = { [l1AddressLowerCased]: bridgeToken } - setBridgeTokens(oldBridgeTokens => { - return { ...oldBridgeTokens, ...newBridgeTokens } + updateErc20L1Balance(l1Addresses) + updateErc20L2Balance(l2Addresses) + + return { + ...oldBridgeTokens, + ...bridgeTokensToAdd + } }) - const { l2Address } = bridgeToken - updateErc20L1Balance([l1AddressLowerCased]) - if (l2Address) { - updateErc20L2Balance([l2Address]) - } }, [ - bridgeTokens, + childChain.id, + parentChain.id, + setBridgeTokens, updateErc20L1Balance, - updateErc20L2Balance, - updateUSDCBalances + updateErc20L2Balance ] ) - const updateEthBalances = async () => { - Promise.all([updateEthL1Balance(), updateEthL2Balance()]) - } + const addBridgeTokenListToBridge = useCallback( + (bridgeTokenList: BridgeTokenList) => { + fetchTokenListFromURL(bridgeTokenList.url).then( + ({ isValid, data: tokenList }) => { + if (!isValid) return - 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 = getProvider(event.parentChainId) - const childChainProvider = getProvider(event.childChainId) - - const messageWriter = L2ToL1Message.fromEvent( - l1Signer, + 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 = getProvider(event.parentChainId) + const childChainProvider = getProvider(event.childChainId) - return rec - } + const messageWriter = L2ToL1Message.fromEvent( + l1Signer, + event, + parentChainProvider + ) + const res = await messageWriter.execute(childChainProvider) - function addL2NativeToken(erc20L2Address: string) { - const token = getL2NativeToken(erc20L2Address, childChain.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 = getProvider(event.parentChainId) - const childChainProvider = getProvider(event.childChainId) - - const messageWriter = L2ToL1Message.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 = getProvider(event.parentChainId) + const childChainProvider = getProvider(event.childChainId) - events.forEach((event: L2ToL1EventResult) => { - const cacheKey = getExecutedMessagesCacheKey({ + const messageWriter = L2ToL1Message.fromEvent( + l1Signer, event, - l2ChainId: childChain.id - }) + parentChainProvider + ) - added[cacheKey] = true - }) + const res = await messageWriter.execute(childChainProvider) - setExecutedMessagesCache({ ...executedMessagesCache, ...added }) - } + const rec = await res.wait() - return { - bridgeTokens, - eth: { + if (rec.status === 1) { + addToExecutedMessagesCache([event]) + } + + return rec + }, + [addToExecutedMessagesCache, walletAddress] + ) + + const eth = useMemo( + () => ({ deposit: depositEth, withdraw: withdrawEth, triggerOutbox: triggerOutboxEth - }, - token: { + }), + [depositEth, triggerOutboxEth, withdrawEth] + ) + const token = useMemo( + () => ({ add: addToken, addL2NativeToken, addTokensFromList, removeTokensFromList, + addBridgeTokenListToBridge, updateTokenData, approve: approveToken, approveL2: approveTokenL2, deposit: depositToken, withdraw: withdrawToken, triggerOutbox: triggerOutboxToken - }, - transactions: { + }), + [ + addBridgeTokenListToBridge, + addL2NativeToken, + addToken, + addTokensFromList, + approveToken, + approveTokenL2, + depositToken, + removeTokensFromList, + triggerOutboxToken, + updateTokenData, + withdrawToken + ] + ) + + // TODO: rename + const transactions2 = useMemo( + () => ({ transactions, updateTransaction, addTransaction, fetchAndUpdateL1ToL2MsgStatus - } + }), + [ + addTransaction, + fetchAndUpdateL1ToL2MsgStatus, + transactions, + updateTransaction + ] + ) + + // useEffect(() => { + // console.log('addToken?') + // }, [addToken]) + + // useEffect(() => { + // console.log('addL2NativeToken?') + // }, [addL2NativeToken]) + + // useEffect(() => { + // console.log('addTokensFromList?') + // }, [addTokensFromList]) + + // useEffect(() => { + // console.log('removeTokensFromList?') + // }, [removeTokensFromList]) + + // useEffect(() => { + // console.log('addBridgeTokenListToBridge?') + // }, [addBridgeTokenListToBridge]) + + // useEffect(() => { + // console.log('updateTokenData?') + // }, [updateTokenData]) + + // useEffect(() => { + // console.log('approveToken?') + // }, [approveToken]) + + // useEffect(() => { + // console.log('approveTokenL2?') + // }, [approveTokenL2]) + + // useEffect(() => { + // console.log('depositToken?') + // }, [depositToken]) + + // useEffect(() => { + // console.log('withdrawToken?') + // }, [withdrawToken]) + + // useEffect(() => { + // console.log('triggerOutboxToken?') + // }, [triggerOutboxToken]) + + return { + bridgeTokens, + eth, + token, + transactions: transactions2 } } diff --git a/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts b/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts index f141811882..7cd4b1d556 100644 --- a/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/TokenListUtils.ts @@ -9,7 +9,6 @@ import GeminiLogo from '@/images/lists/gemini.png' import CMCLogo from '@/images/lists/cmc.png' import CoinGeckoLogo from '@/images/lists/coinGecko.svg' import ArbitrumFoundation from '@/images/lists/ArbitrumFoundation.png' -import { ArbTokenBridge } from '../hooks/arbTokenBridge.types' import { ChainId } from './networks' export const SPECIAL_ARBITRUM_TOKEN_TOKEN_LIST_ID = 0 @@ -179,19 +178,6 @@ export const validateTokenList = (tokenList: TokenList) => { return validate(tokenList) } -export const addBridgeTokenListToBridge = ( - bridgeTokenList: BridgeTokenList, - token: ArbTokenBridge['token'] -) => { - fetchTokenListFromURL(bridgeTokenList.url).then( - ({ isValid, data: tokenList }) => { - if (!isValid) return - - token.addTokensFromList(tokenList!, bridgeTokenList.id) - } - ) -} - export async function fetchTokenListFromURL(tokenListURL: string): Promise<{ isValid: boolean data: TokenList | undefined From 955547e9e4bc45b20a577b96cddb31b73a95f94b Mon Sep 17 00:00:00 2001 From: Christophe Date: Fri, 26 Jan 2024 20:54:28 +0100 Subject: [PATCH 04/28] Memoize useTransactions --- .../src/hooks/useTransactions.ts | 211 ++++++++++-------- 1 file changed, 116 insertions(+), 95 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts b/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts index 9f5dd776c9..844028e366 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' @@ -302,7 +302,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 @@ -315,76 +315,85 @@ const useTransactions = (): [Transaction[], TransactionActions] => { type: 'ADD_TRANSACTION', transaction: tx }) - } + }, []) - const updateTxnL1ToL2MsgData = async ( - txID: string, - l1ToL2MsgData: L1ToL2MessageData - ) => { - dispatch({ - type: 'UPDATE_L1TOL2MSG_DATA', - txID: txID, - l1ToL2MsgData - }) - } + const updateTxnL1ToL2MsgData = useCallback( + async (txID: string, l1ToL2MsgData: L1ToL2MessageData) => { + dispatch({ + type: 'UPDATE_L1TOL2MSG_DATA', + txID: txID, + l1ToL2MsgData + }) + }, + [] + ) + + const fetchAndUpdateL1ToL2MsgStatus = useCallback( + async ( + txID: string, + l1ToL2Msg: L1ToL2MessageReader, + isEthDeposit: boolean, + currentStatus: L1ToL2MessageStatus + ) => { + // set fetching: + updateTxnL1ToL2MsgData(txID, { + fetchingUpdate: true, + status: currentStatus, + retryableCreationTxID: l1ToL2Msg.retryableCreationId + }) - const fetchAndUpdateL1ToL2MsgStatus = async ( - txID: string, - l1ToL2Msg: L1ToL2MessageReader, - isEthDeposit: boolean, - currentStatus: L1ToL2MessageStatus - ) => { - // set fetching: - updateTxnL1ToL2MsgData(txID, { - fetchingUpdate: true, - status: currentStatus, - retryableCreationTxID: l1ToL2Msg.retryableCreationId - }) + const res = await l1ToL2Msg.getSuccessfulRedeem() - const res = await l1ToL2Msg.getSuccessfulRedeem() - - const l2TxID = (() => { - if (res.status === L1ToL2MessageStatus.REDEEMED) { - return res.l2TxReceipt.transactionHash - } else if ( - res.status === L1ToL2MessageStatus.FUNDS_DEPOSITED_ON_L2 && - isEthDeposit - ) { - return l1ToL2Msg.retryableCreationId /** for completed eth deposits, retryableCreationId is the l2txid */ - } else { - return undefined - } - })() + const l2TxID = (() => { + if (res.status === L1ToL2MessageStatus.REDEEMED) { + return res.l2TxReceipt.transactionHash + } else if ( + res.status === L1ToL2MessageStatus.FUNDS_DEPOSITED_ON_L2 && + isEthDeposit + ) { + return l1ToL2Msg.retryableCreationId /** for completed eth deposits, retryableCreationId is the l2txid */ + } else { + return undefined + } + })() - updateTxnL1ToL2MsgData(txID, { - status: res.status, - l2TxID, - fetchingUpdate: false, - retryableCreationTxID: l1ToL2Msg.retryableCreationId - }) - } + updateTxnL1ToL2MsgData(txID, { + status: res.status, + l2TxID, + fetchingUpdate: false, + retryableCreationTxID: l1ToL2Msg.retryableCreationId + }) + }, + [updateTxnL1ToL2MsgData] + ) - 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 @@ -393,41 +402,53 @@ const useTransactions = (): [Transaction[], TransactionActions] => { type: 'SET_FAILURE', txID: txID }) - } + }, []) - const updateTransaction = ( - txReceipt: TransactionReceipt, - tx?: ethers.ContractTransaction, - l1ToL2MsgData?: L1ToL2MessageData - ) => { - 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?: L1ToL2MessageData + ) => { + if (!txReceipt.transactionHash) { + return console.warn( + '*** TransactionHash not included in transaction receipt (???) *** ' + ) + } + 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 } - case 1: { - setTransactionSuccess(txReceipt.transactionHash) - break + if (tx?.blockNumber) { + setTransactionBlock(txReceipt.transactionHash, tx.blockNumber) } - 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) { + 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)) From 2d39062e6167f1eea213e005c4a9448212a3e9b5 Mon Sep 17 00:00:00 2001 From: Christophe Date: Fri, 26 Jan 2024 20:54:38 +0100 Subject: [PATCH 05/28] Fix typing --- .../src/components/TransferPanel/TransferPanelMain.tsx | 1 + packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx index 02f0e8b17c..a7f5758015 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx @@ -854,6 +854,7 @@ export function TransferPanelMain({ networks.destinationChain, isTestnetMode, setNetworks, + actions.app, switchNetworksOnTransferPanel, openOneNovaTransferDialog ]) 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 0b8c3c4fb7..55fc6cafd5 100644 --- a/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts +++ b/packages/arb-token-bridge-ui/src/hooks/arbTokenBridge.types.ts @@ -28,6 +28,7 @@ import { Transaction, L1ToL2MessageData } from './useTransactions' +import { BridgeTokenList } from '../util/TokenListUtils' export { OutgoingMessageState } @@ -165,6 +166,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 approve: (params: { From 7dc7d8c3352a1f5825215035b74a1a7b6fdcd280 Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 29 Jan 2024 15:28:52 +0100 Subject: [PATCH 06/28] Reset useArbTokenBridge --- .../src/hooks/useArbTokenBridge.ts | 1618 +++++++---------- 1 file changed, 698 insertions(+), 920 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index e76edc45dd..33ffd3fa3e 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -1,7 +1,8 @@ -import { useCallback, useMemo } from 'react' -import { useAccount } from 'wagmi' +import { useCallback, useState, useMemo } from 'react' +import { Chain, useAccount } from 'wagmi' import { BigNumber, utils } from 'ethers' import { Signer } from '@ethersproject/abstract-signer' +import { JsonRpcProvider } from '@ethersproject/providers' import { useLocalStorage } from '@rehooks/local-storage' import { TokenList } from '@uniswap/token-lists' import { MaxUint256 } from '@ethersproject/constants' @@ -47,10 +48,6 @@ import { addDepositToCache, getProvider } from '../components/TransactionHistory/helpers' -import { useNetworksRelationship } from './useNetworksRelationship' -import { useNetworks } from './useNetworks' -import { BridgeTokenList, fetchTokenListFromURL } from '../util/TokenListUtils' -import { create } from 'zustand' export const wait = (ms = 0) => { return new Promise(res => setTimeout(res, ms)) @@ -100,26 +97,19 @@ function percentIncrease(num: BigNumber, increase: BigNumber): BigNumber { return num.add(num.mul(increase).div(100)) } -type BridgeTokens = ContractStorage | undefined -type BridgeTokensStore = { - bridgeTokens: BridgeTokens - setBridgeTokens: ( - fn: (prevBridgeTokens: BridgeTokens) => BridgeTokens - ) => void +export interface TokenBridgeParams { + l1: { provider: JsonRpcProvider; network: Chain } + l2: { provider: JsonRpcProvider; network: Chain } } -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) +export const useArbTokenBridge = ( + params: TokenBridgeParams +): ArbTokenBridge => { + const { l1, l2 } = params const { address: walletAddress } = useAccount() - const { bridgeTokens, setBridgeTokens } = useBridgeTokensStore() + const [bridgeTokens, setBridgeTokens] = useState< + ContractStorage | undefined + >(undefined) const { addPendingTransaction } = useTransactionHistory(walletAddress) @@ -127,25 +117,22 @@ export const useArbTokenBridge = (): ArbTokenBridge => { eth: [, updateEthL1Balance], erc20: [, updateErc20L1Balance] } = useBalance({ - provider: parentChainProvider, + provider: l1.provider, walletAddress }) const { eth: [, updateEthL2Balance], erc20: [, updateErc20L2Balance] } = useBalance({ - provider: childChainProvider, + provider: l2.provider, walletAddress }) - const updateEthBalances = useCallback(async () => { - Promise.all([updateEthL1Balance(), updateEthL2Balance()]) - }, [updateEthL1Balance, updateEthL2Balance]) interface ExecutedMessagesCache { [id: string]: boolean } - const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) + const nativeCurrency = useNativeCurrency({ provider: l2.provider }) const { updateUSDCBalances } = useUpdateUSDCBalances({ walletAddress @@ -161,95 +148,158 @@ export const useArbTokenBridge = (): ArbTokenBridge => { React.Dispatch ] + const l1NetworkID = useMemo(() => String(l1.network.id), [l1.network.id]) + const l2NetworkID = useMemo(() => String(l2.network.id), [l2.network.id]) + const [ transactions, { addTransaction, updateTransaction, fetchAndUpdateL1ToL2MsgStatus } ] = useTransactions() - const depositEth = useCallback( - async ({ + const depositEth = async ({ + amount, + l1Signer, + txLifecycle + }: { + amount: BigNumber + l1Signer: Signer + txLifecycle?: L1EthDepositTransactionLifecycle + }) => { + if (!walletAddress) { + return + } + + const ethBridger = await EthBridger.fromProvider(l2.provider) + const parentChainBlockTimestamp = (await l1.provider.getBlock('latest')) + .timestamp + + const depositRequest = await ethBridger.getDepositRequest({ amount, - l1Signer, - txLifecycle - }: { - amount: BigNumber - l1Signer: Signer - txLifecycle?: L1EthDepositTransactionLifecycle - }) => { - if (!walletAddress) { - return - } + from: walletAddress + }) + + let tx: L1EthDepositTransaction - const ethBridger = await EthBridger.fromProvider(childChainProvider) - const parentChainBlockTimestamp = ( - await parentChainProvider.getBlock('latest') - ).timestamp + try { + const gasLimit = await l1.provider.estimateGas(depositRequest.txRequest) - const depositRequest = await ethBridger.getDepositRequest({ + tx = await ethBridger.deposit({ amount, - from: walletAddress + l1Signer, + overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(5)) } }) - let tx: L1EthDepositTransaction + if (txLifecycle?.onTxSubmit) { + txLifecycle.onTxSubmit(tx) + } + } catch (error: any) { + if (txLifecycle?.onTxError) { + txLifecycle.onTxError(error) + } + return error.message + } + + addPendingTransaction({ + sender: walletAddress, + destination: walletAddress, + direction: 'deposit-l1', + status: 'pending', + createdAt: parentChainBlockTimestamp * 1_000, + resolvedAt: null, + txId: tx.hash, + asset: nativeCurrency.symbol, + assetType: AssetType.ETH, + value: utils.formatUnits(amount, nativeCurrency.decimals), + uniqueId: null, + isWithdrawal: false, + blockNum: null, + tokenAddress: null, + depositStatus: DepositStatus.L1_PENDING, + parentChainId: Number(l1NetworkID), + childChainId: Number(l2NetworkID) + }) + + addDepositToCache({ + sender: walletAddress, + destination: walletAddress, + status: 'pending', + txID: tx.hash, + assetName: nativeCurrency.symbol, + assetType: AssetType.ETH, + l1NetworkID, + l2NetworkID, + value: utils.formatUnits(amount, nativeCurrency.decimals), + parentChainId: Number(l1NetworkID), + childChainId: Number(l2NetworkID), + direction: 'deposit', + type: 'deposit-l1', + source: 'local_storage_cache', + timestampCreated: String(parentChainBlockTimestamp), + nonce: tx.nonce + }) + + const receipt = await tx.wait() + + if (txLifecycle?.onTxConfirm) { + txLifecycle.onTxConfirm(receipt) + } + + updateEthBalances() + + if (nativeCurrency.isCustom) { + updateErc20L1Balance([nativeCurrency.address]) + } + } - try { - const gasLimit = await parentChainProvider.estimateGas( - depositRequest.txRequest - ) + const withdrawEth: ArbTokenBridgeEth['withdraw'] = async ({ + amount, + l2Signer, + txLifecycle + }) => { + if (!walletAddress) { + return + } + + try { + const ethBridger = await EthBridger.fromProvider(l2.provider) + + const withdrawalRequest = await ethBridger.getWithdrawalRequest({ + from: walletAddress, + destinationAddress: walletAddress, + amount + }) - tx = await ethBridger.deposit({ - amount, - l1Signer, - overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(5)) } - }) + const gasLimit = await l2.provider.estimateGas( + withdrawalRequest.txRequest + ) - if (txLifecycle?.onTxSubmit) { - txLifecycle.onTxSubmit(tx) - } - } catch (error: any) { - if (txLifecycle?.onTxError) { - txLifecycle.onTxError(error) - } - return error.message + const tx = await ethBridger.withdraw({ + ...withdrawalRequest, + l2Signer, + overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(30)) } + }) + + if (txLifecycle?.onTxSubmit) { + txLifecycle.onTxSubmit(tx) } addPendingTransaction({ sender: walletAddress, destination: walletAddress, - direction: 'deposit-l1', - status: 'pending', - createdAt: parentChainBlockTimestamp * 1_000, + direction: 'withdraw', + status: WithdrawalStatus.UNCONFIRMED, + createdAt: dayjs().valueOf(), resolvedAt: null, txId: tx.hash, asset: nativeCurrency.symbol, assetType: AssetType.ETH, value: utils.formatUnits(amount, nativeCurrency.decimals), uniqueId: null, - isWithdrawal: false, + isWithdrawal: true, blockNum: null, tokenAddress: null, - depositStatus: DepositStatus.L1_PENDING, - parentChainId: parentChain.id, - childChainId: childChain.id - }) - - addDepositToCache({ - sender: walletAddress, - destination: walletAddress, - status: 'pending', - txID: tx.hash, - assetName: nativeCurrency.symbol, - assetType: AssetType.ETH, - l1NetworkID: parentChain.id.toString(), - l2NetworkID: childChain.id.toString(), - value: utils.formatUnits(amount, nativeCurrency.decimals), - parentChainId: parentChain.id, - childChainId: childChain.id, - direction: 'deposit', - type: 'deposit-l1', - source: 'local_storage_cache', - timestampCreated: String(parentChainBlockTimestamp), - nonce: tx.nonce + parentChainId: Number(l1NetworkID), + childChainId: Number(l2NetworkID) }) const receipt = await tx.wait() @@ -260,961 +310,689 @@ export const useArbTokenBridge = (): ArbTokenBridge => { updateEthBalances() - if (nativeCurrency.isCustom) { - updateErc20L1Balance([nativeCurrency.address]) - } - }, - [ - addPendingTransaction, - childChain.id, - childChainProvider, - nativeCurrency, - parentChain.id, - parentChainProvider, - updateErc20L1Balance, - updateEthBalances, - walletAddress - ] - ) - - const withdrawEth: ArbTokenBridgeEth['withdraw'] = useCallback( - async ({ amount, l2Signer, txLifecycle }) => { - if (!walletAddress) { - return + return receipt + } catch (error) { + if (txLifecycle?.onTxError) { + txLifecycle.onTxError(error) } + console.error('withdrawEth err', error) + } + } - try { - const ethBridger = await EthBridger.fromProvider(childChainProvider) - - const withdrawalRequest = await ethBridger.getWithdrawalRequest({ - from: walletAddress, - destinationAddress: walletAddress, - amount - }) - - const gasLimit = await childChainProvider.estimateGas( - withdrawalRequest.txRequest - ) - - const tx = await ethBridger.withdraw({ - ...withdrawalRequest, - l2Signer, - overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(30)) } - }) + const approveToken = async ({ + erc20L1Address, + l1Signer + }: { + erc20L1Address: string + l1Signer: Signer + }) => { + if (!walletAddress) { + return + } - if (txLifecycle?.onTxSubmit) { - txLifecycle.onTxSubmit(tx) - } + const erc20Bridger = await Erc20Bridger.fromProvider(l2.provider) - addPendingTransaction({ - sender: walletAddress, - destination: walletAddress, - direction: 'withdraw', - status: WithdrawalStatus.UNCONFIRMED, - createdAt: dayjs().valueOf(), - resolvedAt: null, - txId: tx.hash, - asset: nativeCurrency.symbol, - assetType: AssetType.ETH, - value: utils.formatUnits(amount, nativeCurrency.decimals), - uniqueId: null, - isWithdrawal: true, - blockNum: null, - tokenAddress: null, - parentChainId: parentChain.id, - childChainId: childChain.id - }) - - const receipt = await tx.wait() + const tx = await erc20Bridger.approveToken({ + erc20L1Address, + l1Signer + }) + + const { symbol } = await fetchErc20Data({ + address: erc20L1Address, + provider: l1.provider + }) + + addTransaction({ + type: 'approve', + status: 'pending', + value: null, + txID: tx.hash, + assetName: symbol, + assetType: AssetType.ERC20, + sender: walletAddress, + l1NetworkID + }) + + const receipt = await tx.wait() + + updateTransaction(receipt, tx) + updateTokenData(erc20L1Address) + } - if (txLifecycle?.onTxConfirm) { - txLifecycle.onTxConfirm(receipt) - } + const approveTokenL2 = async ({ + erc20L1Address, + l2Signer + }: { + erc20L1Address: string + l2Signer: Signer + }) => { + if (typeof bridgeTokens === 'undefined' || !walletAddress) { + return + } + const bridgeToken = bridgeTokens[erc20L1Address] + if (!bridgeToken) throw new Error('Bridge token not found') + const { l2Address } = bridgeToken + if (!l2Address) throw new Error('L2 address not found') + const gatewayAddress = await fetchErc20L2GatewayAddress({ + erc20L1Address, + l2Provider: l2.provider + }) + const contract = await ERC20__factory.connect(l2Address, l2Signer) + const tx = await contract.functions.approve(gatewayAddress, MaxUint256) + const { symbol } = await fetchErc20Data({ + address: erc20L1Address, + provider: l1.provider + }) + + addTransaction({ + type: 'approve-l2', + status: 'pending', + value: null, + txID: tx.hash, + assetName: symbol, + assetType: AssetType.ERC20, + sender: walletAddress, + blockNumber: tx.blockNumber, + l1NetworkID, + l2NetworkID + }) + + const receipt = await tx.wait() + updateTransaction(receipt, tx) + updateTokenData(erc20L1Address) + } - updateEthBalances() + async function depositToken({ + erc20L1Address, + amount, + l1Signer, + txLifecycle, + destinationAddress + }: { + erc20L1Address: string + amount: BigNumber + l1Signer: Signer + txLifecycle?: L1ContractCallTransactionLifecycle + destinationAddress?: string + }) { + if (!walletAddress) { + return + } + const erc20Bridger = await Erc20Bridger.fromProvider(l2.provider) + const parentChainBlockTimestamp = (await l1.provider.getBlock('latest')) + .timestamp + + try { + const { symbol, decimals } = await fetchErc20Data({ + address: erc20L1Address, + provider: l1.provider + }) - return receipt - } catch (error) { - if (txLifecycle?.onTxError) { - txLifecycle.onTxError(error) + const depositRequest = await erc20Bridger.getDepositRequest({ + l1Provider: l1.provider, + l2Provider: l2.provider, + from: walletAddress, + erc20L1Address, + destinationAddress, + amount, + retryableGasOverrides: { + // the gas limit may vary by about 20k due to SSTORE (zero vs nonzero) + // the 30% gas limit increase should cover the difference + gasLimit: { percentIncrease: BigNumber.from(30) } } - console.error('withdrawEth err', error) - } - }, - [ - addPendingTransaction, - childChain.id, - childChainProvider, - nativeCurrency.decimals, - nativeCurrency.symbol, - parentChain.id, - updateEthBalances, - walletAddress - ] - ) - - const updateTokenData = useCallback( - async (l1Address: string) => { - updateUSDCBalances(l1Address) - - if (typeof bridgeTokens === 'undefined') { - return - } - const l1AddressLowerCased = l1Address.toLowerCase() - const bridgeToken = bridgeTokens[l1AddressLowerCased] + }) - if (!bridgeToken) { - return - } + const gasLimit = await l1.provider.estimateGas(depositRequest.txRequest) - const newBridgeTokens = { [l1AddressLowerCased]: bridgeToken } - setBridgeTokens(oldBridgeTokens => { - return { ...oldBridgeTokens, ...newBridgeTokens } + const tx = await erc20Bridger.deposit({ + ...depositRequest, + l1Signer, + overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(5)) } }) - const { l2Address } = bridgeToken - updateErc20L1Balance([l1AddressLowerCased]) - if (l2Address) { - updateErc20L2Balance([l2Address]) - } - }, - [ - bridgeTokens, - setBridgeTokens, - updateErc20L1Balance, - updateErc20L2Balance, - updateUSDCBalances - ] - ) - const approveToken = useCallback( - async ({ - erc20L1Address, - l1Signer - }: { - erc20L1Address: string - l1Signer: Signer - }) => { - if (!walletAddress) { - return + if (txLifecycle?.onTxSubmit) { + txLifecycle.onTxSubmit(tx) } - const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) - - const tx = await erc20Bridger.approveToken({ - erc20L1Address, - l1Signer - }) - - const { symbol } = await fetchErc20Data({ - address: erc20L1Address, - provider: parentChainProvider + addPendingTransaction({ + sender: walletAddress, + destination: destinationAddress ?? walletAddress, + direction: 'deposit-l1', + status: 'pending', + createdAt: parentChainBlockTimestamp * 1_000, + resolvedAt: null, + txId: tx.hash, + asset: symbol, + assetType: AssetType.ERC20, + value: utils.formatUnits(amount, decimals), + depositStatus: DepositStatus.L1_PENDING, + uniqueId: null, + isWithdrawal: false, + blockNum: null, + tokenAddress: erc20L1Address, + parentChainId: Number(l1NetworkID), + childChainId: Number(l2NetworkID) }) - addTransaction({ - type: 'approve', + addDepositToCache({ + sender: walletAddress, + destination: destinationAddress ?? walletAddress, status: 'pending', - value: null, txID: tx.hash, assetName: symbol, assetType: AssetType.ERC20, - sender: walletAddress, - l1NetworkID: parentChain.id.toString() + l1NetworkID, + l2NetworkID, + value: utils.formatUnits(amount, decimals), + parentChainId: Number(l1NetworkID), + childChainId: Number(l2NetworkID), + direction: 'deposit', + type: 'deposit-l1', + source: 'local_storage_cache', + timestampCreated: String(parentChainBlockTimestamp), + nonce: tx.nonce }) const receipt = await tx.wait() - updateTransaction(receipt, tx) + if (txLifecycle?.onTxConfirm) { + txLifecycle.onTxConfirm(receipt) + } + updateTokenData(erc20L1Address) - }, - [ - addTransaction, - childChainProvider, - parentChain.id, - parentChainProvider, - updateTokenData, - updateTransaction, - walletAddress - ] - ) + updateEthBalances() - const approveTokenL2 = useCallback( - async ({ - erc20L1Address, - l2Signer - }: { - erc20L1Address: string - l2Signer: Signer - }) => { - if (typeof bridgeTokens === 'undefined' || !walletAddress) { - return + if (nativeCurrency.isCustom) { + updateErc20L1Balance([nativeCurrency.address]) } - const bridgeToken = bridgeTokens[erc20L1Address] - if (!bridgeToken) throw new Error('Bridge token not found') - const { l2Address } = bridgeToken - if (!l2Address) throw new Error('L2 address not found') - const gatewayAddress = await fetchErc20L2GatewayAddress({ - erc20L1Address, - l2Provider: childChainProvider - }) - const contract = await ERC20__factory.connect(l2Address, l2Signer) - const tx = await contract.functions.approve(gatewayAddress, MaxUint256) - const { symbol } = await fetchErc20Data({ - address: erc20L1Address, - provider: parentChainProvider - }) - addTransaction({ - type: 'approve-l2', - status: 'pending', - value: null, - txID: tx.hash, - assetName: symbol, - assetType: AssetType.ERC20, - sender: walletAddress, - blockNumber: tx.blockNumber, - l1NetworkID: parentChain.id.toString(), - l2NetworkID: childChain.id.toString() - }) + return receipt + } catch (error) { + if (txLifecycle?.onTxError) { + txLifecycle.onTxError(error) + } + } + } - const receipt = await tx.wait() - updateTransaction(receipt, tx) - updateTokenData(erc20L1Address) - }, - [ - addTransaction, - bridgeTokens, - childChain.id, - childChainProvider, - parentChain.id, - parentChainProvider, - updateTokenData, - updateTransaction, - walletAddress - ] - ) + const withdrawToken: ArbTokenBridgeToken['withdraw'] = async ({ + erc20L1Address, + amount, + l2Signer, + txLifecycle, + destinationAddress + }) => { + if (!walletAddress) { + return + } + + try { + const erc20Bridger = await Erc20Bridger.fromProvider(l2.provider) + const provider = l2Signer.provider + const isSmartContractAddress = + provider && (await provider.getCode(String(erc20L1Address))).length < 2 + if (isSmartContractAddress && !destinationAddress) { + throw new Error(`Missing destination address`) + } - const depositToken = useCallback( - async ({ - erc20L1Address, - amount, - l1Signer, - txLifecycle, - destinationAddress - }: { - erc20L1Address: string - amount: BigNumber - l1Signer: Signer - txLifecycle?: L1ContractCallTransactionLifecycle - destinationAddress?: string - }) => { - if (!walletAddress) { + if (typeof bridgeTokens === 'undefined') { return } - const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) - const parentChainBlockTimestamp = ( - await parentChainProvider.getBlock('latest') - ).timestamp + const bridgeToken = bridgeTokens[erc20L1Address] - try { + const { symbol, decimals } = await (async () => { + if (bridgeToken) { + const { symbol, decimals } = bridgeToken + return { symbol, decimals } + } const { symbol, decimals } = await fetchErc20Data({ address: erc20L1Address, - provider: parentChainProvider - }) - - const depositRequest = await erc20Bridger.getDepositRequest({ - l1Provider: parentChainProvider, - l2Provider: childChainProvider, - from: walletAddress, - erc20L1Address, - destinationAddress, - amount, - retryableGasOverrides: { - // the gas limit may vary by about 20k due to SSTORE (zero vs nonzero) - // the 30% gas limit increase should cover the difference - gasLimit: { percentIncrease: BigNumber.from(30) } - } + provider: l1.provider }) - const gasLimit = await parentChainProvider.estimateGas( - depositRequest.txRequest - ) - - const tx = await erc20Bridger.deposit({ - ...depositRequest, - l1Signer, - overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(5)) } - }) + addToken(erc20L1Address) + return { symbol, decimals } + })() - if (txLifecycle?.onTxSubmit) { - txLifecycle.onTxSubmit(tx) - } - - addPendingTransaction({ - sender: walletAddress, - destination: destinationAddress ?? walletAddress, - direction: 'deposit-l1', - status: 'pending', - createdAt: parentChainBlockTimestamp * 1_000, - resolvedAt: null, - txId: tx.hash, - asset: symbol, - assetType: AssetType.ERC20, - value: utils.formatUnits(amount, decimals), - depositStatus: DepositStatus.L1_PENDING, - uniqueId: null, - isWithdrawal: false, - blockNum: null, - tokenAddress: erc20L1Address, - parentChainId: parentChain.id, - childChainId: childChain.id - }) - - addDepositToCache({ - sender: walletAddress, - destination: destinationAddress ?? walletAddress, - status: 'pending', - txID: tx.hash, - assetName: symbol, - assetType: AssetType.ERC20, - l1NetworkID: parentChain.id.toString(), - l2NetworkID: childChain.id.toString(), - value: utils.formatUnits(amount, decimals), - parentChainId: parentChain.id, - childChainId: childChain.id, - direction: 'deposit', - type: 'deposit-l1', - source: 'local_storage_cache', - timestampCreated: String(parentChainBlockTimestamp), - nonce: tx.nonce - }) - - const receipt = await tx.wait() - - if (txLifecycle?.onTxConfirm) { - txLifecycle.onTxConfirm(receipt) - } - - updateTokenData(erc20L1Address) - updateEthBalances() - - if (nativeCurrency.isCustom) { - updateErc20L1Balance([nativeCurrency.address]) - } - - return receipt - } catch (error) { - if (txLifecycle?.onTxError) { - txLifecycle.onTxError(error) - } - } - }, - [ - addPendingTransaction, - childChain.id, - childChainProvider, - nativeCurrency, - parentChain.id, - parentChainProvider, - updateErc20L1Balance, - updateEthBalances, - updateTokenData, - walletAddress - ] - ) - - const addToken = useCallback( - async (erc20L1orL2Address: string) => { - let l1Address: string - let l2Address: string | undefined - - if (!walletAddress) { - return - } - - const lowercasedErc20L1orL2Address = erc20L1orL2Address.toLowerCase() - const maybeL1Address = await getL1ERC20Address({ - erc20L2Address: lowercasedErc20L1orL2Address, - l2Provider: childChainProvider + const withdrawalRequest = await erc20Bridger.getWithdrawalRequest({ + from: walletAddress, + erc20l1Address: erc20L1Address, + destinationAddress: destinationAddress ?? walletAddress, + amount }) - if (maybeL1Address) { - // looks like l2 address was provided - l1Address = maybeL1Address - l2Address = lowercasedErc20L1orL2Address - } else { - // looks like l1 address was provided - l1Address = lowercasedErc20L1orL2Address - l2Address = await getL2ERC20Address({ - erc20L1Address: l1Address, - l1Provider: parentChainProvider, - l2Provider: childChainProvider - }) - } + const gasLimit = await l2.provider.estimateGas( + withdrawalRequest.txRequest + ) - const bridgeTokensToAdd: ContractStorage = {} - const erc20Params = { address: l1Address, provider: parentChainProvider } + const tx = await erc20Bridger.withdraw({ + ...withdrawalRequest, + l2Signer, + overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(30)) } + }) - if (!(await isValidErc20(erc20Params))) { - throw new Error(`${l1Address} is not a valid ERC-20 token`) + if (txLifecycle?.onTxSubmit) { + txLifecycle.onTxSubmit(tx) } - const { name, symbol, decimals } = await fetchErc20Data(erc20Params) - - const isDisabled = await l1TokenIsDisabled({ - erc20L1Address: l1Address, - l1Provider: parentChainProvider, - l2Provider: childChainProvider + addPendingTransaction({ + sender: walletAddress, + destination: destinationAddress ?? walletAddress, + direction: 'withdraw', + status: WithdrawalStatus.UNCONFIRMED, + createdAt: dayjs().valueOf(), + resolvedAt: null, + txId: tx.hash, + asset: symbol, + assetType: AssetType.ERC20, + value: utils.formatUnits(amount, decimals), + uniqueId: null, + isWithdrawal: true, + blockNum: null, + tokenAddress: erc20L1Address, + parentChainId: Number(l1NetworkID), + childChainId: Number(l2NetworkID) }) - if (isDisabled) { - throw new TokenDisabledError('Token currently disabled') - } + const receipt = await tx.wait() - const l1AddressLowerCased = l1Address.toLowerCase() - bridgeTokensToAdd[l1AddressLowerCased] = { - name, - type: TokenType.ERC20, - symbol, - address: l1AddressLowerCased, - l2Address: l2Address?.toLowerCase(), - decimals, - listIds: new Set() + if (txLifecycle?.onTxConfirm) { + txLifecycle.onTxConfirm(receipt) } - setBridgeTokens(oldBridgeTokens => { - return { ...oldBridgeTokens, ...bridgeTokensToAdd } - }) - - updateErc20L1Balance([l1AddressLowerCased]) - if (l2Address) { - updateErc20L2Balance([l2Address]) + updateTokenData(erc20L1Address) + return receipt + } catch (error) { + if (txLifecycle?.onTxError) { + txLifecycle.onTxError(error) } - }, - [ - childChainProvider, - parentChainProvider, - setBridgeTokens, - updateErc20L1Balance, - updateErc20L2Balance, - walletAddress - ] - ) + console.warn('withdraw token err', error) + } + } - const withdrawToken: ArbTokenBridgeToken['withdraw'] = useCallback( - async ({ - erc20L1Address, - amount, - l2Signer, - txLifecycle, - destinationAddress - }) => { - if (!walletAddress) { - return - } + const removeTokensFromList = (listID: number) => { + setBridgeTokens(prevBridgeTokens => { + const newBridgeTokens = { ...prevBridgeTokens } + for (const address in bridgeTokens) { + const token = bridgeTokens[address] + if (!token) continue - try { - const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) - const provider = l2Signer.provider - const isSmartContractAddress = - provider && - (await provider.getCode(String(erc20L1Address))).length < 2 - if (isSmartContractAddress && !destinationAddress) { - throw new Error(`Missing destination address`) - } + token.listIds.delete(listID) - if (typeof bridgeTokens === 'undefined') { - return + if (token.listIds.size === 0) { + delete newBridgeTokens[address] } - const bridgeToken = bridgeTokens[erc20L1Address] + } + return newBridgeTokens + }) + } - const { symbol, decimals } = await (async () => { - if (bridgeToken) { - const { symbol, decimals } = bridgeToken - return { symbol, decimals } - } - const { symbol, decimals } = await fetchErc20Data({ - address: erc20L1Address, - provider: parentChainProvider - }) + const addTokensFromList = async (arbTokenList: TokenList, listId: number) => { + const l1ChainID = l1.network.id + const l2ChainID = l2.network.id - addToken(erc20L1Address) - return { symbol, decimals } - })() + const bridgeTokensToAdd: ContractStorage = {} - const withdrawalRequest = await erc20Bridger.getWithdrawalRequest({ - from: walletAddress, - erc20l1Address: erc20L1Address, - destinationAddress: destinationAddress ?? walletAddress, - amount - }) + const candidateUnbridgedTokensToAdd: ERC20BridgeToken[] = [] - const gasLimit = await childChainProvider.estimateGas( - withdrawalRequest.txRequest - ) + for (const tokenData of arbTokenList.tokens) { + const { address, name, symbol, extensions, decimals, logoURI, chainId } = + tokenData - const tx = await erc20Bridger.withdraw({ - ...withdrawalRequest, - l2Signer, - overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(30)) } - }) + if (![l1ChainID, l2ChainID].includes(chainId)) { + continue + } - if (txLifecycle?.onTxSubmit) { - txLifecycle.onTxSubmit(tx) + 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 + } + } } - - addPendingTransaction({ - sender: walletAddress, - destination: destinationAddress ?? walletAddress, - direction: 'withdraw', - status: WithdrawalStatus.UNCONFIRMED, - createdAt: dayjs().valueOf(), - resolvedAt: null, - txId: tx.hash, - asset: symbol, - assetType: AssetType.ERC20, - value: utils.formatUnits(amount, decimals), - uniqueId: null, - isWithdrawal: true, - blockNum: null, - tokenAddress: erc20L1Address, - parentChainId: parentChain.id, - childChainId: childChain.id - }) - - const receipt = await tx.wait() - - if (txLifecycle?.onTxConfirm) { - txLifecycle.onTxConfirm(receipt) + 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 + ) } - - updateTokenData(erc20L1Address) - return receipt - } catch (error) { - if (txLifecycle?.onTxError) { - txLifecycle.onTxError(error) + if (!isExtensions(extensions)) { + return null + } else { + return extensions.bridgeInfo } - console.warn('withdraw token err', error) - } - }, - [ - addPendingTransaction, - addToken, - bridgeTokens, - childChain.id, - childChainProvider, - parentChain.id, - parentChainProvider, - updateTokenData, - walletAddress - ] - ) + })() - const removeTokensFromList = useCallback( - (listID: number) => { - setBridgeTokens(prevBridgeTokens => { - const newBridgeTokens = { ...prevBridgeTokens } - for (const address in bridgeTokens) { - const token = bridgeTokens[address] - if (!token) continue + if (bridgeInfo) { + const l1Address = bridgeInfo[l1NetworkID]?.tokenAddress.toLowerCase() - token.listIds.delete(listID) - - if (token.listIds.size === 0) { - delete newBridgeTokens[address] - } + if (!l1Address) { + return } - return newBridgeTokens - }) - }, - [bridgeTokens, setBridgeTokens] - ) - const addTokensFromList = useCallback( - async (arbTokenList: TokenList, listId: number) => { - const bridgeTokensToAdd: ContractStorage = {} - - const candidateUnbridgedTokensToAdd: ERC20BridgeToken[] = [] - - for (const tokenData of arbTokenList.tokens) { - const { - address, + bridgeTokensToAdd[l1Address] = { name, + type: TokenType.ERC20, symbol, - extensions, + address: l1Address, + l2Address: address.toLowerCase(), decimals, logoURI, - chainId - } = tokenData - - if (![parentChain.id, childChain.id].includes(chainId)) { - continue - } - - 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 - } - })() - - if (bridgeInfo) { - const l1Address = - bridgeInfo[parentChain.id]?.tokenAddress.toLowerCase() - - if (!l1Address) { - return - } - - bridgeTokensToAdd[l1Address] = { - name, - type: TokenType.ERC20, - symbol, - address: l1Address, - l2Address: address.toLowerCase(), - decimals, - logoURI, - listIds: new Set([listId]) - } - } - // 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]) - }) + listIds: new Set([listId]) } } + // 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) */ - ) + // 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 - } + ) + for (const l1TokenData of candidateUnbridgedTokensToAdd) { + if (!l1AddressesOfBridgedTokens.has(l1TokenData.address.toLowerCase())) { + bridgeTokensToAdd[l1TokenData.address] = l1TokenData } + } - // 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[] = [] - - // 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) - } + // 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[] = [] - 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) - } + // 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) + } - // 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]) + for (const tokenAddress in bridgeTokensToAdd) { + const tokenToAdd = bridgeTokensToAdd[tokenAddress] + if (!tokenToAdd) { + return } - - updateErc20L1Balance(l1Addresses) - updateErc20L2Balance(l2Addresses) - - return { - ...oldBridgeTokens, - ...bridgeTokensToAdd + const { address, l2Address } = tokenToAdd + if (address) { + l1Addresses.push(address) } - }) - }, - [ - childChain.id, - parentChain.id, - setBridgeTokens, - updateErc20L1Balance, - updateErc20L2Balance - ] - ) - - const addBridgeTokenListToBridge = useCallback( - (bridgeTokenList: BridgeTokenList) => { - fetchTokenListFromURL(bridgeTokenList.url).then( - ({ isValid, data: tokenList }) => { - if (!isValid) return - - addTokensFromList(tokenList!, bridgeTokenList.id) + if (l2Address) { + l2Addresses.push(l2Address) } - ) - }, - [addTokensFromList] - ) - const addToExecutedMessagesCache = useCallback( - (events: L2ToL1EventResult[]) => { - const added: { [cacheKey: string]: boolean } = {} + // 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]) + } - events.forEach((event: L2ToL1EventResult) => { - const cacheKey = getExecutedMessagesCacheKey({ - event, - l2ChainId: childChain.id - }) + updateErc20L1Balance(l1Addresses) + updateErc20L2Balance(l2Addresses) - added[cacheKey] = true + return { + ...oldBridgeTokens, + ...bridgeTokensToAdd + } + }) + } + + 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 + 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]) + } + } - setExecutedMessagesCache({ ...executedMessagesCache, ...added }) - }, - [childChain.id, executedMessagesCache, setExecutedMessagesCache] - ) + const updateTokenData = useCallback( + async (l1Address: string) => { + updateUSDCBalances(l1Address) - const triggerOutboxToken = useCallback( - async ({ - event, - l1Signer - }: { - event: L2ToL1EventResultPlus - l1Signer: Signer - }) => { - // sanity check - if (!event) { - throw new Error('Outbox message not found') + if (typeof bridgeTokens === 'undefined') { + return } + const l1AddressLowerCased = l1Address.toLowerCase() + const bridgeToken = bridgeTokens[l1AddressLowerCased] - if (!walletAddress) { + if (!bridgeToken) { return } - const parentChainProvider = getProvider(event.parentChainId) - const childChainProvider = getProvider(event.childChainId) + const newBridgeTokens = { [l1AddressLowerCased]: bridgeToken } + setBridgeTokens(oldBridgeTokens => { + return { ...oldBridgeTokens, ...newBridgeTokens } + }) + const { l2Address } = bridgeToken + updateErc20L1Balance([l1AddressLowerCased]) + if (l2Address) { + updateErc20L2Balance([l2Address]) + } + }, + [bridgeTokens, setBridgeTokens, updateErc20L1Balance, updateErc20L2Balance] + ) - const messageWriter = L2ToL1Message.fromEvent( - l1Signer, - event, - parentChainProvider - ) - const res = await messageWriter.execute(childChainProvider) + const updateEthBalances = async () => { + Promise.all([updateEthL1Balance(), updateEthL2Balance()]) + } - const rec = await res.wait() + 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 = getProvider(event.parentChainId) + const childChainProvider = getProvider(event.childChainId) + + const messageWriter = L2ToL1Message.fromEvent( + l1Signer, + event, + parentChainProvider + ) + const res = await messageWriter.execute(childChainProvider) - if (rec.status === 1) { - addToExecutedMessagesCache([event]) - } + const rec = await res.wait() - return rec - }, - [addToExecutedMessagesCache, walletAddress] - ) + if (rec.status === 1) { + addToExecutedMessagesCache([event]) + } - const addL2NativeToken = useCallback( - (erc20L2Address: string) => { - const token = getL2NativeToken(erc20L2Address, childChain.id) + return rec + } - 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 - } + 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 } - }) - }, - [childChain.id, setBridgeTokens] - ) + } + }) + } - const triggerOutboxEth = useCallback( - async ({ + 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 = getProvider(event.parentChainId) + const childChainProvider = getProvider(event.childChainId) + + const messageWriter = L2ToL1Message.fromEvent( + l1Signer, event, - l1Signer - }: { - event: L2ToL1EventResultPlus - l1Signer: Signer - }) => { - // sanity check - if (!event) { - throw new Error('Outbox message not found') - } + parentChainProvider + ) - if (!walletAddress) { - return - } + const res = await messageWriter.execute(childChainProvider) - const parentChainProvider = getProvider(event.parentChainId) - const childChainProvider = getProvider(event.childChainId) + const rec = await res.wait() - const messageWriter = L2ToL1Message.fromEvent( - l1Signer, - event, - parentChainProvider - ) + if (rec.status === 1) { + addToExecutedMessagesCache([event]) + } - const res = await messageWriter.execute(childChainProvider) + return rec + } - const rec = await res.wait() + function addToExecutedMessagesCache(events: L2ToL1EventResult[]) { + const added: { [cacheKey: string]: boolean } = {} - if (rec.status === 1) { - addToExecutedMessagesCache([event]) - } + events.forEach((event: L2ToL1EventResult) => { + const cacheKey = getExecutedMessagesCacheKey({ + event, + l2ChainId: l2.network.id + }) - return rec - }, - [addToExecutedMessagesCache, walletAddress] - ) + added[cacheKey] = true + }) + + setExecutedMessagesCache({ ...executedMessagesCache, ...added }) + } - const eth = useMemo( - () => ({ + return { + bridgeTokens, + eth: { deposit: depositEth, withdraw: withdrawEth, triggerOutbox: triggerOutboxEth - }), - [depositEth, triggerOutboxEth, withdrawEth] - ) - const token = useMemo( - () => ({ + }, + token: { add: addToken, addL2NativeToken, addTokensFromList, removeTokensFromList, - addBridgeTokenListToBridge, updateTokenData, approve: approveToken, approveL2: approveTokenL2, deposit: depositToken, withdraw: withdrawToken, triggerOutbox: triggerOutboxToken - }), - [ - addBridgeTokenListToBridge, - addL2NativeToken, - addToken, - addTokensFromList, - approveToken, - approveTokenL2, - depositToken, - removeTokensFromList, - triggerOutboxToken, - updateTokenData, - withdrawToken - ] - ) - - // TODO: rename - const transactions2 = useMemo( - () => ({ + }, + transactions: { transactions, updateTransaction, addTransaction, fetchAndUpdateL1ToL2MsgStatus - }), - [ - addTransaction, - fetchAndUpdateL1ToL2MsgStatus, - transactions, - updateTransaction - ] - ) - - // useEffect(() => { - // console.log('addToken?') - // }, [addToken]) - - // useEffect(() => { - // console.log('addL2NativeToken?') - // }, [addL2NativeToken]) - - // useEffect(() => { - // console.log('addTokensFromList?') - // }, [addTokensFromList]) - - // useEffect(() => { - // console.log('removeTokensFromList?') - // }, [removeTokensFromList]) - - // useEffect(() => { - // console.log('addBridgeTokenListToBridge?') - // }, [addBridgeTokenListToBridge]) - - // useEffect(() => { - // console.log('updateTokenData?') - // }, [updateTokenData]) - - // useEffect(() => { - // console.log('approveToken?') - // }, [approveToken]) - - // useEffect(() => { - // console.log('approveTokenL2?') - // }, [approveTokenL2]) - - // useEffect(() => { - // console.log('depositToken?') - // }, [depositToken]) - - // useEffect(() => { - // console.log('withdrawToken?') - // }, [withdrawToken]) - - // useEffect(() => { - // console.log('triggerOutboxToken?') - // }, [triggerOutboxToken]) - - return { - bridgeTokens, - eth, - token, - transactions: transactions2 + } } } From 4cf85e21dd7dc316d810c5e1cc55644685d04b99 Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 29 Jan 2024 15:29:44 +0100 Subject: [PATCH 07/28] Add shared zustand store for bridgeTokens --- .../src/hooks/useArbTokenBridge.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index 33ffd3fa3e..96e1a0b60a 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -13,6 +13,7 @@ import { EventArgs } from '@arbitrum/sdk/dist/lib/dataEntities/event' import { L2ToL1TransactionEvent } from '@arbitrum/sdk/dist/lib/message/L2ToL1Message' import { L2ToL1TransactionEvent as ClassicL2ToL1TransactionEvent } from '@arbitrum/sdk/dist/lib/abi/ArbSys' import dayjs from 'dayjs' +import { create } from 'zustand' import useTransactions from './useTransactions' import { @@ -102,6 +103,20 @@ export interface TokenBridgeParams { l2: { provider: JsonRpcProvider; network: Chain } } +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 = ( params: TokenBridgeParams ): ArbTokenBridge => { From c02f0e5f019c736d80444346e541ac2401dc06e9 Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 29 Jan 2024 15:35:03 +0100 Subject: [PATCH 08/28] Replace params with useNetworks in useArbTokenBridge --- .../src/hooks/useArbTokenBridge.ts | 142 +++++++++--------- 1 file changed, 74 insertions(+), 68 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index 96e1a0b60a..d5089045e7 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -49,6 +49,8 @@ import { addDepositToCache, getProvider } from '../components/TransactionHistory/helpers' +import { useNetworks } from './useNetworks' +import { useNetworksRelationship } from './useNetworksRelationship' export const wait = (ms = 0) => { return new Promise(res => setTimeout(res, ms)) @@ -117,14 +119,12 @@ export const useBridgeTokensStore = create(set => ({ } })) -export const useArbTokenBridge = ( - params: TokenBridgeParams -): ArbTokenBridge => { - const { l1, l2 } = params +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 { addPendingTransaction } = useTransactionHistory(walletAddress) @@ -132,14 +132,14 @@ export const useArbTokenBridge = ( eth: [, updateEthL1Balance], erc20: [, updateErc20L1Balance] } = useBalance({ - provider: l1.provider, + provider: parentChainProvider, walletAddress }) const { eth: [, updateEthL2Balance], erc20: [, updateErc20L2Balance] } = useBalance({ - provider: l2.provider, + provider: childChainProvider, walletAddress }) @@ -147,7 +147,7 @@ export const useArbTokenBridge = ( [id: string]: boolean } - const nativeCurrency = useNativeCurrency({ provider: l2.provider }) + const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) const { updateUSDCBalances } = useUpdateUSDCBalances({ walletAddress @@ -163,9 +163,6 @@ export const useArbTokenBridge = ( React.Dispatch ] - const l1NetworkID = useMemo(() => String(l1.network.id), [l1.network.id]) - const l2NetworkID = useMemo(() => String(l2.network.id), [l2.network.id]) - const [ transactions, { addTransaction, updateTransaction, fetchAndUpdateL1ToL2MsgStatus } @@ -184,9 +181,10 @@ export const useArbTokenBridge = ( return } - const ethBridger = await EthBridger.fromProvider(l2.provider) - const parentChainBlockTimestamp = (await l1.provider.getBlock('latest')) - .timestamp + const ethBridger = await EthBridger.fromProvider(childChainProvider) + const parentChainBlockTimestamp = ( + await parentChainProvider.getBlock('latest') + ).timestamp const depositRequest = await ethBridger.getDepositRequest({ amount, @@ -196,7 +194,9 @@ export const useArbTokenBridge = ( let tx: L1EthDepositTransaction try { - const gasLimit = await l1.provider.estimateGas(depositRequest.txRequest) + const gasLimit = await parentChainProvider.estimateGas( + depositRequest.txRequest + ) tx = await ethBridger.deposit({ amount, @@ -230,8 +230,8 @@ export const useArbTokenBridge = ( blockNum: null, tokenAddress: null, depositStatus: DepositStatus.L1_PENDING, - parentChainId: Number(l1NetworkID), - childChainId: Number(l2NetworkID) + parentChainId: parentChain.id, + childChainId: childChain.id }) addDepositToCache({ @@ -241,11 +241,11 @@ export const useArbTokenBridge = ( txID: tx.hash, assetName: nativeCurrency.symbol, assetType: AssetType.ETH, - l1NetworkID, - l2NetworkID, + l1NetworkID: parentChain.id.toString(), + l2NetworkID: childChain.id.toString(), value: utils.formatUnits(amount, nativeCurrency.decimals), - parentChainId: Number(l1NetworkID), - childChainId: Number(l2NetworkID), + parentChainId: parentChain.id, + childChainId: childChain.id, direction: 'deposit', type: 'deposit-l1', source: 'local_storage_cache', @@ -276,7 +276,7 @@ export const useArbTokenBridge = ( } try { - const ethBridger = await EthBridger.fromProvider(l2.provider) + const ethBridger = await EthBridger.fromProvider(childChainProvider) const withdrawalRequest = await ethBridger.getWithdrawalRequest({ from: walletAddress, @@ -284,7 +284,7 @@ export const useArbTokenBridge = ( amount }) - const gasLimit = await l2.provider.estimateGas( + const gasLimit = await childChainProvider.estimateGas( withdrawalRequest.txRequest ) @@ -313,8 +313,8 @@ export const useArbTokenBridge = ( isWithdrawal: true, blockNum: null, tokenAddress: null, - parentChainId: Number(l1NetworkID), - childChainId: Number(l2NetworkID) + parentChainId: parentChain.id, + childChainId: childChain.id }) const receipt = await tx.wait() @@ -345,7 +345,7 @@ export const useArbTokenBridge = ( return } - const erc20Bridger = await Erc20Bridger.fromProvider(l2.provider) + const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) const tx = await erc20Bridger.approveToken({ erc20L1Address, @@ -354,7 +354,7 @@ export const useArbTokenBridge = ( const { symbol } = await fetchErc20Data({ address: erc20L1Address, - provider: l1.provider + provider: parentChainProvider }) addTransaction({ @@ -365,7 +365,7 @@ export const useArbTokenBridge = ( assetName: symbol, assetType: AssetType.ERC20, sender: walletAddress, - l1NetworkID + l1NetworkID: parentChain.id.toString() }) const receipt = await tx.wait() @@ -390,13 +390,13 @@ export const useArbTokenBridge = ( if (!l2Address) throw new Error('L2 address not found') const gatewayAddress = await fetchErc20L2GatewayAddress({ erc20L1Address, - l2Provider: l2.provider + l2Provider: childChainProvider }) const contract = await ERC20__factory.connect(l2Address, l2Signer) const tx = await contract.functions.approve(gatewayAddress, MaxUint256) const { symbol } = await fetchErc20Data({ address: erc20L1Address, - provider: l1.provider + provider: parentChainProvider }) addTransaction({ @@ -408,8 +408,8 @@ export const useArbTokenBridge = ( assetType: AssetType.ERC20, sender: walletAddress, blockNumber: tx.blockNumber, - l1NetworkID, - l2NetworkID + l1NetworkID: parentChain.id.toString(), + l2NetworkID: childChain.id.toString() }) const receipt = await tx.wait() @@ -433,19 +433,20 @@ export const useArbTokenBridge = ( if (!walletAddress) { return } - const erc20Bridger = await Erc20Bridger.fromProvider(l2.provider) - const parentChainBlockTimestamp = (await l1.provider.getBlock('latest')) - .timestamp + const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) + const parentChainBlockTimestamp = ( + await parentChainProvider.getBlock('latest') + ).timestamp try { const { symbol, decimals } = await fetchErc20Data({ address: erc20L1Address, - provider: l1.provider + provider: parentChainProvider }) const depositRequest = await erc20Bridger.getDepositRequest({ - l1Provider: l1.provider, - l2Provider: l2.provider, + l1Provider: parentChainProvider, + l2Provider: childChainProvider, from: walletAddress, erc20L1Address, destinationAddress, @@ -457,7 +458,9 @@ export const useArbTokenBridge = ( } }) - const gasLimit = await l1.provider.estimateGas(depositRequest.txRequest) + const gasLimit = await parentChainProvider.estimateGas( + depositRequest.txRequest + ) const tx = await erc20Bridger.deposit({ ...depositRequest, @@ -485,8 +488,8 @@ export const useArbTokenBridge = ( isWithdrawal: false, blockNum: null, tokenAddress: erc20L1Address, - parentChainId: Number(l1NetworkID), - childChainId: Number(l2NetworkID) + parentChainId: parentChain.id, + childChainId: childChain.id }) addDepositToCache({ @@ -496,11 +499,11 @@ export const useArbTokenBridge = ( txID: tx.hash, assetName: symbol, assetType: AssetType.ERC20, - l1NetworkID, - l2NetworkID, + l1NetworkID: parentChain.id.toString(), + l2NetworkID: childChain.id.toString(), value: utils.formatUnits(amount, decimals), - parentChainId: Number(l1NetworkID), - childChainId: Number(l2NetworkID), + parentChainId: parentChain.id, + childChainId: childChain.id, direction: 'deposit', type: 'deposit-l1', source: 'local_storage_cache', @@ -541,7 +544,7 @@ export const useArbTokenBridge = ( } try { - const erc20Bridger = await Erc20Bridger.fromProvider(l2.provider) + const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) const provider = l2Signer.provider const isSmartContractAddress = provider && (await provider.getCode(String(erc20L1Address))).length < 2 @@ -561,7 +564,7 @@ export const useArbTokenBridge = ( } const { symbol, decimals } = await fetchErc20Data({ address: erc20L1Address, - provider: l1.provider + provider: parentChainProvider }) addToken(erc20L1Address) @@ -575,7 +578,7 @@ export const useArbTokenBridge = ( amount }) - const gasLimit = await l2.provider.estimateGas( + const gasLimit = await childChainProvider.estimateGas( withdrawalRequest.txRequest ) @@ -604,8 +607,8 @@ export const useArbTokenBridge = ( isWithdrawal: true, blockNum: null, tokenAddress: erc20L1Address, - parentChainId: Number(l1NetworkID), - childChainId: Number(l2NetworkID) + parentChainId: parentChain.id, + childChainId: childChain.id }) const receipt = await tx.wait() @@ -642,9 +645,6 @@ export const useArbTokenBridge = ( } const addTokensFromList = async (arbTokenList: TokenList, listId: number) => { - const l1ChainID = l1.network.id - const l2ChainID = l2.network.id - const bridgeTokensToAdd: ContractStorage = {} const candidateUnbridgedTokensToAdd: ERC20BridgeToken[] = [] @@ -653,7 +653,7 @@ export const useArbTokenBridge = ( const { address, name, symbol, extensions, decimals, logoURI, chainId } = tokenData - if (![l1ChainID, l2ChainID].includes(chainId)) { + if (![parentChain.id, childChain.id].includes(chainId)) { continue } @@ -689,7 +689,7 @@ export const useArbTokenBridge = ( })() if (bridgeInfo) { - const l1Address = bridgeInfo[l1NetworkID]?.tokenAddress.toLowerCase() + const l1Address = bridgeInfo[parentChain.id]?.tokenAddress.toLowerCase() if (!l1Address) { return @@ -741,10 +741,10 @@ export const useArbTokenBridge = ( // 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) { + if (isNetwork(childChain.id).isArbitrumOne) { l2Addresses.push(CommonAddress.ArbitrumOne.USDC) } - if (isNetwork(l2ChainID).isArbitrumSepolia) { + if (isNetwork(childChain.id).isArbitrumSepolia) { l2Addresses.push(CommonAddress.ArbitrumSepolia.USDC) } @@ -789,7 +789,7 @@ export const useArbTokenBridge = ( const lowercasedErc20L1orL2Address = erc20L1orL2Address.toLowerCase() const maybeL1Address = await getL1ERC20Address({ erc20L2Address: lowercasedErc20L1orL2Address, - l2Provider: l2.provider + l2Provider: childChainProvider }) if (maybeL1Address) { @@ -801,13 +801,13 @@ export const useArbTokenBridge = ( l1Address = lowercasedErc20L1orL2Address l2Address = await getL2ERC20Address({ erc20L1Address: l1Address, - l1Provider: l1.provider, - l2Provider: l2.provider + l1Provider: parentChainProvider, + l2Provider: childChainProvider }) } const bridgeTokensToAdd: ContractStorage = {} - const erc20Params = { address: l1Address, provider: l1.provider } + const erc20Params = { address: l1Address, provider: parentChainProvider } if (!(await isValidErc20(erc20Params))) { throw new Error(`${l1Address} is not a valid ERC-20 token`) @@ -817,8 +817,8 @@ export const useArbTokenBridge = ( const isDisabled = await l1TokenIsDisabled({ erc20L1Address: l1Address, - l1Provider: l1.provider, - l2Provider: l2.provider + l1Provider: parentChainProvider, + l2Provider: childChainProvider }) if (isDisabled) { @@ -870,7 +870,13 @@ export const useArbTokenBridge = ( updateErc20L2Balance([l2Address]) } }, - [bridgeTokens, setBridgeTokens, updateErc20L1Balance, updateErc20L2Balance] + [ + bridgeTokens, + setBridgeTokens, + updateErc20L1Balance, + updateErc20L2Balance, + updateUSDCBalances + ] ) const updateEthBalances = async () => { @@ -913,7 +919,7 @@ export const useArbTokenBridge = ( } function addL2NativeToken(erc20L2Address: string) { - const token = getL2NativeToken(erc20L2Address, l2.network.id) + const token = getL2NativeToken(erc20L2Address, childChain.id) setBridgeTokens(oldBridgeTokens => { return { @@ -975,7 +981,7 @@ export const useArbTokenBridge = ( events.forEach((event: L2ToL1EventResult) => { const cacheKey = getExecutedMessagesCacheKey({ event, - l2ChainId: l2.network.id + l2ChainId: childChain.id }) added[cacheKey] = true From a96c5a64a2820cc6c5bf793b750586dee2472424 Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 29 Jan 2024 15:36:41 +0100 Subject: [PATCH 09/28] Add addBridgeTokenListToBridge to useArbTokenBridge --- .../src/hooks/useArbTokenBridge.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index d5089045e7..a729e999e7 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -51,6 +51,7 @@ import { } from '../components/TransactionHistory/helpers' import { useNetworks } from './useNetworks' import { useNetworksRelationship } from './useNetworksRelationship' +import { BridgeTokenList, fetchTokenListFromURL } from '../util/TokenListUtils' export const wait = (ms = 0) => { return new Promise(res => setTimeout(res, ms)) @@ -778,6 +779,16 @@ export const useArbTokenBridge = (): ArbTokenBridge => { }) } + const addBridgeTokenListToBridge = (bridgeTokenList: BridgeTokenList) => { + fetchTokenListFromURL(bridgeTokenList.url).then( + ({ isValid, data: tokenList }) => { + if (!isValid) return + + addTokensFromList(tokenList!, bridgeTokenList.id) + } + ) + } + async function addToken(erc20L1orL2Address: string) { let l1Address: string let l2Address: string | undefined @@ -1001,6 +1012,7 @@ export const useArbTokenBridge = (): ArbTokenBridge => { add: addToken, addL2NativeToken, addTokensFromList, + addBridgeTokenListToBridge, removeTokensFromList, updateTokenData, approve: approveToken, From d2e4846df8fb7c883ccaf6b938b52c24a58b96fd Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 29 Jan 2024 15:38:47 +0100 Subject: [PATCH 10/28] Move updateTokenData --- .../src/hooks/useArbTokenBridge.ts | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index a729e999e7..93d35389dd 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -335,6 +335,39 @@ export const useArbTokenBridge = (): ArbTokenBridge => { } } + const updateTokenData = useCallback( + async (l1Address: string) => { + updateUSDCBalances(l1Address) + + if (typeof bridgeTokens === 'undefined') { + return + } + const l1AddressLowerCased = l1Address.toLowerCase() + const bridgeToken = bridgeTokens[l1AddressLowerCased] + + if (!bridgeToken) { + return + } + + const newBridgeTokens = { [l1AddressLowerCased]: bridgeToken } + setBridgeTokens(oldBridgeTokens => { + return { ...oldBridgeTokens, ...newBridgeTokens } + }) + const { l2Address } = bridgeToken + updateErc20L1Balance([l1AddressLowerCased]) + if (l2Address) { + updateErc20L2Balance([l2Address]) + } + }, + [ + bridgeTokens, + setBridgeTokens, + updateErc20L1Balance, + updateErc20L2Balance, + updateUSDCBalances + ] + ) + const approveToken = async ({ erc20L1Address, l1Signer @@ -857,39 +890,6 @@ export const useArbTokenBridge = (): ArbTokenBridge => { } } - const updateTokenData = useCallback( - async (l1Address: string) => { - updateUSDCBalances(l1Address) - - if (typeof bridgeTokens === 'undefined') { - return - } - const l1AddressLowerCased = l1Address.toLowerCase() - const bridgeToken = bridgeTokens[l1AddressLowerCased] - - if (!bridgeToken) { - return - } - - const newBridgeTokens = { [l1AddressLowerCased]: bridgeToken } - setBridgeTokens(oldBridgeTokens => { - return { ...oldBridgeTokens, ...newBridgeTokens } - }) - const { l2Address } = bridgeToken - updateErc20L1Balance([l1AddressLowerCased]) - if (l2Address) { - updateErc20L2Balance([l2Address]) - } - }, - [ - bridgeTokens, - setBridgeTokens, - updateErc20L1Balance, - updateErc20L2Balance, - updateUSDCBalances - ] - ) - const updateEthBalances = async () => { Promise.all([updateEthL1Balance(), updateEthL2Balance()]) } From 1ffeabb536c379c94dfe60077180e08a09c55aba Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 29 Jan 2024 15:43:06 +0100 Subject: [PATCH 11/28] Move addToken --- .../src/hooks/useArbTokenBridge.ts | 136 +++++++++--------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index 93d35389dd..2b0e23c487 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -566,6 +566,74 @@ export const useArbTokenBridge = (): ArbTokenBridge => { } } + 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: childChainProvider + }) + + if (maybeL1Address) { + // looks like l2 address was provided + l1Address = maybeL1Address + l2Address = lowercasedErc20L1orL2Address + } else { + // looks like l1 address was provided + l1Address = lowercasedErc20L1orL2Address + l2Address = await getL2ERC20Address({ + erc20L1Address: l1Address, + l1Provider: parentChainProvider, + l2Provider: childChainProvider + }) + } + + const bridgeTokensToAdd: ContractStorage = {} + const erc20Params = { address: l1Address, provider: parentChainProvider } + + 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: 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]) + } + } + const withdrawToken: ArbTokenBridgeToken['withdraw'] = async ({ erc20L1Address, amount, @@ -822,74 +890,6 @@ export const useArbTokenBridge = (): ArbTokenBridge => { ) } - 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: childChainProvider - }) - - if (maybeL1Address) { - // looks like l2 address was provided - l1Address = maybeL1Address - l2Address = lowercasedErc20L1orL2Address - } else { - // looks like l1 address was provided - l1Address = lowercasedErc20L1orL2Address - l2Address = await getL2ERC20Address({ - erc20L1Address: l1Address, - l1Provider: parentChainProvider, - l2Provider: childChainProvider - }) - } - - const bridgeTokensToAdd: ContractStorage = {} - const erc20Params = { address: l1Address, provider: parentChainProvider } - - 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: 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]) - } - } - const updateEthBalances = async () => { Promise.all([updateEthL1Balance(), updateEthL2Balance()]) } From 65583ee0a3a4e57a9d089adbd6a7303dfdd0e135 Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 29 Jan 2024 15:44:41 +0100 Subject: [PATCH 12/28] Move addToExecutedMessagesCache --- .../src/hooks/useArbTokenBridge.ts | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index 2b0e23c487..f73fce998d 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -143,6 +143,9 @@ export const useArbTokenBridge = (): ArbTokenBridge => { provider: childChainProvider, walletAddress }) + const updateEthBalances = async () => { + Promise.all([updateEthL1Balance(), updateEthL2Balance()]) + } interface ExecutedMessagesCache { [id: string]: boolean @@ -890,8 +893,19 @@ export const useArbTokenBridge = (): ArbTokenBridge => { ) } - const updateEthBalances = async () => { - Promise.all([updateEthL1Balance(), updateEthL2Balance()]) + function addToExecutedMessagesCache(events: L2ToL1EventResult[]) { + const added: { [cacheKey: string]: boolean } = {} + + events.forEach((event: L2ToL1EventResult) => { + const cacheKey = getExecutedMessagesCacheKey({ + event, + l2ChainId: childChain.id + }) + + added[cacheKey] = true + }) + + setExecutedMessagesCache({ ...executedMessagesCache, ...added }) } async function triggerOutboxToken({ @@ -986,21 +1000,6 @@ export const useArbTokenBridge = (): ArbTokenBridge => { return rec } - function addToExecutedMessagesCache(events: L2ToL1EventResult[]) { - const added: { [cacheKey: string]: boolean } = {} - - events.forEach((event: L2ToL1EventResult) => { - const cacheKey = getExecutedMessagesCacheKey({ - event, - l2ChainId: childChain.id - }) - - added[cacheKey] = true - }) - - setExecutedMessagesCache({ ...executedMessagesCache, ...added }) - } - return { bridgeTokens, eth: { From b87dc6f6e7babdcb31f044e20694b867bac7c635 Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 29 Jan 2024 15:46:27 +0100 Subject: [PATCH 13/28] Add useCallback for updateEthBalances --- packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index f73fce998d..da0110e6d5 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -143,9 +143,9 @@ export const useArbTokenBridge = (): ArbTokenBridge => { provider: childChainProvider, walletAddress }) - const updateEthBalances = async () => { + const updateEthBalances = useCallback(async () => { Promise.all([updateEthL1Balance(), updateEthL2Balance()]) - } + }, [updateEthL1Balance, updateEthL2Balance]) interface ExecutedMessagesCache { [id: string]: boolean From b4da51b2b57f1b304ec4b57767eed87d51c70239 Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 29 Jan 2024 15:46:49 +0100 Subject: [PATCH 14/28] Add useCallback for useArbTokenBridge functions --- .../src/hooks/useArbTokenBridge.ts | 1408 +++++++++-------- 1 file changed, 764 insertions(+), 644 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index da0110e6d5..9a6243a6cd 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -172,155 +172,92 @@ export const useArbTokenBridge = (): ArbTokenBridge => { { addTransaction, updateTransaction, fetchAndUpdateL1ToL2MsgStatus } ] = useTransactions() - const depositEth = async ({ - amount, - l1Signer, - txLifecycle - }: { - amount: BigNumber - l1Signer: Signer - txLifecycle?: L1EthDepositTransactionLifecycle - }) => { - if (!walletAddress) { - return - } - - const ethBridger = await EthBridger.fromProvider(childChainProvider) - const parentChainBlockTimestamp = ( - await parentChainProvider.getBlock('latest') - ).timestamp - - const depositRequest = await ethBridger.getDepositRequest({ + const depositEth = useCallback( + async ({ amount, - from: walletAddress - }) - - let tx: L1EthDepositTransaction - - try { - const gasLimit = await parentChainProvider.estimateGas( - depositRequest.txRequest - ) - - tx = await ethBridger.deposit({ - amount, - l1Signer, - overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(5)) } - }) - - if (txLifecycle?.onTxSubmit) { - txLifecycle.onTxSubmit(tx) - } - } catch (error: any) { - if (txLifecycle?.onTxError) { - txLifecycle.onTxError(error) + l1Signer, + txLifecycle + }: { + amount: BigNumber + l1Signer: Signer + txLifecycle?: L1EthDepositTransactionLifecycle + }) => { + if (!walletAddress) { + return } - return error.message - } - - addPendingTransaction({ - sender: walletAddress, - destination: walletAddress, - direction: 'deposit-l1', - status: 'pending', - createdAt: parentChainBlockTimestamp * 1_000, - resolvedAt: null, - txId: tx.hash, - asset: nativeCurrency.symbol, - assetType: AssetType.ETH, - value: utils.formatUnits(amount, nativeCurrency.decimals), - uniqueId: null, - isWithdrawal: false, - blockNum: null, - tokenAddress: null, - depositStatus: DepositStatus.L1_PENDING, - parentChainId: parentChain.id, - childChainId: childChain.id - }) - - addDepositToCache({ - sender: walletAddress, - destination: walletAddress, - status: 'pending', - txID: tx.hash, - assetName: nativeCurrency.symbol, - assetType: AssetType.ETH, - l1NetworkID: parentChain.id.toString(), - l2NetworkID: childChain.id.toString(), - value: utils.formatUnits(amount, nativeCurrency.decimals), - parentChainId: parentChain.id, - childChainId: childChain.id, - direction: 'deposit', - type: 'deposit-l1', - source: 'local_storage_cache', - timestampCreated: String(parentChainBlockTimestamp), - nonce: tx.nonce - }) - - const receipt = await tx.wait() - - if (txLifecycle?.onTxConfirm) { - txLifecycle.onTxConfirm(receipt) - } - - updateEthBalances() - - if (nativeCurrency.isCustom) { - updateErc20L1Balance([nativeCurrency.address]) - } - } - - const withdrawEth: ArbTokenBridgeEth['withdraw'] = async ({ - amount, - l2Signer, - txLifecycle - }) => { - if (!walletAddress) { - return - } - try { const ethBridger = await EthBridger.fromProvider(childChainProvider) + const parentChainBlockTimestamp = ( + await parentChainProvider.getBlock('latest') + ).timestamp - const withdrawalRequest = await ethBridger.getWithdrawalRequest({ - from: walletAddress, - destinationAddress: walletAddress, - amount + const depositRequest = await ethBridger.getDepositRequest({ + amount, + from: walletAddress }) - const gasLimit = await childChainProvider.estimateGas( - withdrawalRequest.txRequest - ) + let tx: L1EthDepositTransaction - const tx = await ethBridger.withdraw({ - ...withdrawalRequest, - l2Signer, - overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(30)) } - }) + try { + const gasLimit = await parentChainProvider.estimateGas( + depositRequest.txRequest + ) + + tx = await ethBridger.deposit({ + amount, + l1Signer, + overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(5)) } + }) - if (txLifecycle?.onTxSubmit) { - txLifecycle.onTxSubmit(tx) + if (txLifecycle?.onTxSubmit) { + txLifecycle.onTxSubmit(tx) + } + } catch (error: any) { + if (txLifecycle?.onTxError) { + txLifecycle.onTxError(error) + } + return error.message } addPendingTransaction({ sender: walletAddress, destination: walletAddress, - direction: 'withdraw', - status: WithdrawalStatus.UNCONFIRMED, - createdAt: dayjs().valueOf(), + direction: 'deposit-l1', + status: 'pending', + createdAt: parentChainBlockTimestamp * 1_000, resolvedAt: null, txId: tx.hash, asset: nativeCurrency.symbol, assetType: AssetType.ETH, value: utils.formatUnits(amount, nativeCurrency.decimals), uniqueId: null, - isWithdrawal: true, + isWithdrawal: false, blockNum: null, tokenAddress: null, + depositStatus: DepositStatus.L1_PENDING, parentChainId: parentChain.id, childChainId: childChain.id }) + addDepositToCache({ + sender: walletAddress, + destination: walletAddress, + status: 'pending', + txID: tx.hash, + assetName: nativeCurrency.symbol, + assetType: AssetType.ETH, + l1NetworkID: parentChain.id.toString(), + l2NetworkID: childChain.id.toString(), + value: utils.formatUnits(amount, nativeCurrency.decimals), + parentChainId: parentChain.id, + childChainId: childChain.id, + direction: 'deposit', + type: 'deposit-l1', + source: 'local_storage_cache', + timestampCreated: String(parentChainBlockTimestamp), + nonce: tx.nonce + }) + const receipt = await tx.wait() if (txLifecycle?.onTxConfirm) { @@ -329,14 +266,98 @@ export const useArbTokenBridge = (): ArbTokenBridge => { updateEthBalances() - return receipt - } catch (error) { - if (txLifecycle?.onTxError) { - txLifecycle.onTxError(error) + if (nativeCurrency.isCustom) { + updateErc20L1Balance([nativeCurrency.address]) } - console.error('withdrawEth err', error) - } - } + }, + [ + addPendingTransaction, + childChain.id, + childChainProvider, + nativeCurrency, + parentChain.id, + parentChainProvider, + updateErc20L1Balance, + updateEthBalances, + walletAddress + ] + ) + + const withdrawEth: ArbTokenBridgeEth['withdraw'] = useCallback( + async ({ amount, l2Signer, txLifecycle }) => { + if (!walletAddress) { + return + } + + try { + const ethBridger = await EthBridger.fromProvider(childChainProvider) + + const withdrawalRequest = await ethBridger.getWithdrawalRequest({ + from: walletAddress, + destinationAddress: walletAddress, + amount + }) + + const gasLimit = await childChainProvider.estimateGas( + withdrawalRequest.txRequest + ) + + const tx = await ethBridger.withdraw({ + ...withdrawalRequest, + l2Signer, + overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(30)) } + }) + + if (txLifecycle?.onTxSubmit) { + txLifecycle.onTxSubmit(tx) + } + + addPendingTransaction({ + sender: walletAddress, + destination: walletAddress, + direction: 'withdraw', + status: WithdrawalStatus.UNCONFIRMED, + createdAt: dayjs().valueOf(), + resolvedAt: null, + txId: tx.hash, + asset: nativeCurrency.symbol, + assetType: AssetType.ETH, + value: utils.formatUnits(amount, nativeCurrency.decimals), + uniqueId: null, + isWithdrawal: true, + blockNum: null, + tokenAddress: null, + parentChainId: parentChain.id, + childChainId: childChain.id + }) + + const receipt = await tx.wait() + + if (txLifecycle?.onTxConfirm) { + txLifecycle.onTxConfirm(receipt) + } + + updateEthBalances() + + return receipt + } catch (error) { + if (txLifecycle?.onTxError) { + txLifecycle.onTxError(error) + } + console.error('withdrawEth err', error) + } + }, + [ + addPendingTransaction, + childChain.id, + childChainProvider, + nativeCurrency.decimals, + nativeCurrency.symbol, + parentChain.id, + updateEthBalances, + walletAddress + ] + ) const updateTokenData = useCallback( async (l1Address: string) => { @@ -371,634 +392,733 @@ export const useArbTokenBridge = (): ArbTokenBridge => { ] ) - const approveToken = async ({ - erc20L1Address, - l1Signer - }: { - erc20L1Address: string - l1Signer: Signer - }) => { - if (!walletAddress) { - return - } - - const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) - - const tx = await erc20Bridger.approveToken({ + const approveToken = useCallback( + async ({ erc20L1Address, l1Signer - }) - - const { symbol } = await fetchErc20Data({ - address: erc20L1Address, - provider: parentChainProvider - }) - - addTransaction({ - type: 'approve', - status: 'pending', - value: null, - txID: tx.hash, - assetName: symbol, - assetType: AssetType.ERC20, - sender: walletAddress, - l1NetworkID: parentChain.id.toString() - }) - - const receipt = await tx.wait() - - updateTransaction(receipt, tx) - updateTokenData(erc20L1Address) - } + }: { + erc20L1Address: string + l1Signer: Signer + }) => { + if (!walletAddress) { + return + } - const approveTokenL2 = async ({ - erc20L1Address, - l2Signer - }: { - erc20L1Address: string - l2Signer: Signer - }) => { - if (typeof bridgeTokens === 'undefined' || !walletAddress) { - return - } - const bridgeToken = bridgeTokens[erc20L1Address] - if (!bridgeToken) throw new Error('Bridge token not found') - const { l2Address } = bridgeToken - if (!l2Address) throw new Error('L2 address not found') - const gatewayAddress = await fetchErc20L2GatewayAddress({ - erc20L1Address, - l2Provider: childChainProvider - }) - const contract = await ERC20__factory.connect(l2Address, l2Signer) - const tx = await contract.functions.approve(gatewayAddress, MaxUint256) - const { symbol } = await fetchErc20Data({ - address: erc20L1Address, - provider: parentChainProvider - }) - - addTransaction({ - type: 'approve-l2', - status: 'pending', - value: null, - txID: tx.hash, - assetName: symbol, - assetType: AssetType.ERC20, - sender: walletAddress, - blockNumber: tx.blockNumber, - l1NetworkID: parentChain.id.toString(), - l2NetworkID: childChain.id.toString() - }) - - const receipt = await tx.wait() - updateTransaction(receipt, tx) - updateTokenData(erc20L1Address) - } + const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) - async function depositToken({ - erc20L1Address, - amount, - l1Signer, - txLifecycle, - destinationAddress - }: { - erc20L1Address: string - amount: BigNumber - l1Signer: Signer - txLifecycle?: L1ContractCallTransactionLifecycle - destinationAddress?: string - }) { - if (!walletAddress) { - return - } - const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) - const parentChainBlockTimestamp = ( - await parentChainProvider.getBlock('latest') - ).timestamp + const tx = await erc20Bridger.approveToken({ + erc20L1Address, + l1Signer + }) - try { - const { symbol, decimals } = await fetchErc20Data({ + const { symbol } = await fetchErc20Data({ address: erc20L1Address, provider: parentChainProvider }) - const depositRequest = await erc20Bridger.getDepositRequest({ - l1Provider: parentChainProvider, - l2Provider: childChainProvider, - from: walletAddress, - erc20L1Address, - destinationAddress, - amount, - retryableGasOverrides: { - // the gas limit may vary by about 20k due to SSTORE (zero vs nonzero) - // the 30% gas limit increase should cover the difference - gasLimit: { percentIncrease: BigNumber.from(30) } - } + addTransaction({ + type: 'approve', + status: 'pending', + value: null, + txID: tx.hash, + assetName: symbol, + assetType: AssetType.ERC20, + sender: walletAddress, + l1NetworkID: parentChain.id.toString() }) - const gasLimit = await parentChainProvider.estimateGas( - depositRequest.txRequest - ) + const receipt = await tx.wait() - const tx = await erc20Bridger.deposit({ - ...depositRequest, - l1Signer, - overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(5)) } - }) + updateTransaction(receipt, tx) + updateTokenData(erc20L1Address) + }, + [ + addTransaction, + childChainProvider, + parentChain.id, + parentChainProvider, + updateTokenData, + updateTransaction, + walletAddress + ] + ) - if (txLifecycle?.onTxSubmit) { - txLifecycle.onTxSubmit(tx) + const approveTokenL2 = useCallback( + async ({ + erc20L1Address, + l2Signer + }: { + erc20L1Address: string + l2Signer: Signer + }) => { + if (typeof bridgeTokens === 'undefined' || !walletAddress) { + return } - - addPendingTransaction({ - sender: walletAddress, - destination: destinationAddress ?? walletAddress, - direction: 'deposit-l1', - status: 'pending', - createdAt: parentChainBlockTimestamp * 1_000, - resolvedAt: null, - txId: tx.hash, - asset: symbol, - assetType: AssetType.ERC20, - value: utils.formatUnits(amount, decimals), - depositStatus: DepositStatus.L1_PENDING, - uniqueId: null, - isWithdrawal: false, - blockNum: null, - tokenAddress: erc20L1Address, - parentChainId: parentChain.id, - childChainId: childChain.id + const bridgeToken = bridgeTokens[erc20L1Address] + if (!bridgeToken) throw new Error('Bridge token not found') + const { l2Address } = bridgeToken + if (!l2Address) throw new Error('L2 address not found') + const gatewayAddress = await fetchErc20L2GatewayAddress({ + erc20L1Address, + l2Provider: childChainProvider + }) + const contract = await ERC20__factory.connect(l2Address, l2Signer) + const tx = await contract.functions.approve(gatewayAddress, MaxUint256) + const { symbol } = await fetchErc20Data({ + address: erc20L1Address, + provider: parentChainProvider }) - addDepositToCache({ - sender: walletAddress, - destination: destinationAddress ?? walletAddress, + addTransaction({ + type: 'approve-l2', status: 'pending', + value: null, txID: tx.hash, assetName: symbol, assetType: AssetType.ERC20, + sender: walletAddress, + blockNumber: tx.blockNumber, l1NetworkID: parentChain.id.toString(), - l2NetworkID: childChain.id.toString(), - value: utils.formatUnits(amount, decimals), - parentChainId: parentChain.id, - childChainId: childChain.id, - direction: 'deposit', - type: 'deposit-l1', - source: 'local_storage_cache', - timestampCreated: String(parentChainBlockTimestamp), - nonce: tx.nonce + l2NetworkID: childChain.id.toString() }) const receipt = await tx.wait() - - if (txLifecycle?.onTxConfirm) { - txLifecycle.onTxConfirm(receipt) - } - + updateTransaction(receipt, tx) updateTokenData(erc20L1Address) - updateEthBalances() - - if (nativeCurrency.isCustom) { - updateErc20L1Balance([nativeCurrency.address]) - } + }, + [ + addTransaction, + bridgeTokens, + childChain.id, + childChainProvider, + parentChain.id, + parentChainProvider, + updateTokenData, + updateTransaction, + walletAddress + ] + ) - return receipt - } catch (error) { - if (txLifecycle?.onTxError) { - txLifecycle.onTxError(error) + const depositToken = useCallback( + async ({ + erc20L1Address, + amount, + l1Signer, + txLifecycle, + destinationAddress + }: { + erc20L1Address: string + amount: BigNumber + l1Signer: Signer + txLifecycle?: L1ContractCallTransactionLifecycle + destinationAddress?: string + }) => { + if (!walletAddress) { + return } - } - } - - async function addToken(erc20L1orL2Address: string) { - let l1Address: string - let l2Address: string | undefined + const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) + const parentChainBlockTimestamp = ( + await parentChainProvider.getBlock('latest') + ).timestamp - if (!walletAddress) { - return - } + try { + const { symbol, decimals } = await fetchErc20Data({ + address: erc20L1Address, + provider: parentChainProvider + }) - 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 - l2Address = await getL2ERC20Address({ - erc20L1Address: l1Address, - l1Provider: parentChainProvider, - l2Provider: childChainProvider - }) - } + const depositRequest = await erc20Bridger.getDepositRequest({ + l1Provider: parentChainProvider, + l2Provider: childChainProvider, + from: walletAddress, + erc20L1Address, + destinationAddress, + amount, + retryableGasOverrides: { + // the gas limit may vary by about 20k due to SSTORE (zero vs nonzero) + // the 30% gas limit increase should cover the difference + gasLimit: { percentIncrease: BigNumber.from(30) } + } + }) - const bridgeTokensToAdd: ContractStorage = {} - const erc20Params = { address: l1Address, provider: parentChainProvider } + const gasLimit = await parentChainProvider.estimateGas( + depositRequest.txRequest + ) - if (!(await isValidErc20(erc20Params))) { - throw new Error(`${l1Address} is not a valid ERC-20 token`) - } + const tx = await erc20Bridger.deposit({ + ...depositRequest, + l1Signer, + overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(5)) } + }) - const { name, symbol, decimals } = await fetchErc20Data(erc20Params) + if (txLifecycle?.onTxSubmit) { + txLifecycle.onTxSubmit(tx) + } - const isDisabled = await l1TokenIsDisabled({ - erc20L1Address: l1Address, - l1Provider: parentChainProvider, - l2Provider: childChainProvider - }) + addPendingTransaction({ + sender: walletAddress, + destination: destinationAddress ?? walletAddress, + direction: 'deposit-l1', + status: 'pending', + createdAt: parentChainBlockTimestamp * 1_000, + resolvedAt: null, + txId: tx.hash, + asset: symbol, + assetType: AssetType.ERC20, + value: utils.formatUnits(amount, decimals), + depositStatus: DepositStatus.L1_PENDING, + uniqueId: null, + isWithdrawal: false, + blockNum: null, + tokenAddress: erc20L1Address, + parentChainId: parentChain.id, + childChainId: childChain.id + }) - if (isDisabled) { - throw new TokenDisabledError('Token currently disabled') - } + addDepositToCache({ + sender: walletAddress, + destination: destinationAddress ?? walletAddress, + status: 'pending', + txID: tx.hash, + assetName: symbol, + assetType: AssetType.ERC20, + l1NetworkID: parentChain.id.toString(), + l2NetworkID: childChain.id.toString(), + value: utils.formatUnits(amount, decimals), + parentChainId: parentChain.id, + childChainId: childChain.id, + direction: 'deposit', + type: 'deposit-l1', + source: 'local_storage_cache', + timestampCreated: String(parentChainBlockTimestamp), + nonce: tx.nonce + }) - const l1AddressLowerCased = l1Address.toLowerCase() - bridgeTokensToAdd[l1AddressLowerCased] = { - name, - type: TokenType.ERC20, - symbol, - address: l1AddressLowerCased, - l2Address: l2Address?.toLowerCase(), - decimals, - listIds: new Set() - } + const receipt = await tx.wait() - setBridgeTokens(oldBridgeTokens => { - return { ...oldBridgeTokens, ...bridgeTokensToAdd } - }) + if (txLifecycle?.onTxConfirm) { + txLifecycle.onTxConfirm(receipt) + } - updateErc20L1Balance([l1AddressLowerCased]) - if (l2Address) { - updateErc20L2Balance([l2Address]) - } - } + updateTokenData(erc20L1Address) + updateEthBalances() - const withdrawToken: ArbTokenBridgeToken['withdraw'] = async ({ - erc20L1Address, - amount, - l2Signer, - txLifecycle, - destinationAddress - }) => { - if (!walletAddress) { - return - } + if (nativeCurrency.isCustom) { + updateErc20L1Balance([nativeCurrency.address]) + } - try { - const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) - const provider = l2Signer.provider - const isSmartContractAddress = - provider && (await provider.getCode(String(erc20L1Address))).length < 2 - if (isSmartContractAddress && !destinationAddress) { - throw new Error(`Missing destination address`) + return receipt + } catch (error) { + if (txLifecycle?.onTxError) { + txLifecycle.onTxError(error) + } } + }, + [ + addPendingTransaction, + childChain.id, + childChainProvider, + nativeCurrency, + parentChain.id, + parentChainProvider, + updateErc20L1Balance, + updateEthBalances, + updateTokenData, + walletAddress + ] + ) - if (typeof bridgeTokens === 'undefined') { + const addToken = useCallback( + async (erc20L1orL2Address: string) => { + let l1Address: string + let l2Address: string | undefined + + if (!walletAddress) { return } - const bridgeToken = bridgeTokens[erc20L1Address] - const { symbol, decimals } = await (async () => { - if (bridgeToken) { - const { symbol, decimals } = bridgeToken - return { symbol, decimals } - } - const { symbol, decimals } = await fetchErc20Data({ - address: erc20L1Address, - provider: parentChainProvider + 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 + l2Address = await getL2ERC20Address({ + erc20L1Address: l1Address, + l1Provider: parentChainProvider, + l2Provider: childChainProvider }) + } - addToken(erc20L1Address) - return { symbol, decimals } - })() + const bridgeTokensToAdd: ContractStorage = {} + const erc20Params = { address: l1Address, provider: parentChainProvider } - const withdrawalRequest = await erc20Bridger.getWithdrawalRequest({ - from: walletAddress, - erc20l1Address: erc20L1Address, - destinationAddress: destinationAddress ?? walletAddress, - amount - }) + if (!(await isValidErc20(erc20Params))) { + throw new Error(`${l1Address} is not a valid ERC-20 token`) + } - const gasLimit = await childChainProvider.estimateGas( - withdrawalRequest.txRequest - ) + const { name, symbol, decimals } = await fetchErc20Data(erc20Params) - const tx = await erc20Bridger.withdraw({ - ...withdrawalRequest, - l2Signer, - overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(30)) } + const isDisabled = await l1TokenIsDisabled({ + erc20L1Address: l1Address, + l1Provider: parentChainProvider, + l2Provider: childChainProvider }) - if (txLifecycle?.onTxSubmit) { - txLifecycle.onTxSubmit(tx) + if (isDisabled) { + throw new TokenDisabledError('Token currently disabled') } - addPendingTransaction({ - sender: walletAddress, - destination: destinationAddress ?? walletAddress, - direction: 'withdraw', - status: WithdrawalStatus.UNCONFIRMED, - createdAt: dayjs().valueOf(), - resolvedAt: null, - txId: tx.hash, - asset: symbol, - assetType: AssetType.ERC20, - value: utils.formatUnits(amount, decimals), - uniqueId: null, - isWithdrawal: true, - blockNum: null, - tokenAddress: erc20L1Address, - parentChainId: parentChain.id, - childChainId: childChain.id - }) + const l1AddressLowerCased = l1Address.toLowerCase() + bridgeTokensToAdd[l1AddressLowerCased] = { + name, + type: TokenType.ERC20, + symbol, + address: l1AddressLowerCased, + l2Address: l2Address?.toLowerCase(), + decimals, + listIds: new Set() + } - const receipt = await tx.wait() + setBridgeTokens(oldBridgeTokens => { + return { ...oldBridgeTokens, ...bridgeTokensToAdd } + }) - if (txLifecycle?.onTxConfirm) { - txLifecycle.onTxConfirm(receipt) + updateErc20L1Balance([l1AddressLowerCased]) + if (l2Address) { + updateErc20L2Balance([l2Address]) } + }, + [ + childChainProvider, + parentChainProvider, + setBridgeTokens, + updateErc20L1Balance, + updateErc20L2Balance, + walletAddress + ] + ) - updateTokenData(erc20L1Address) - return receipt - } catch (error) { - if (txLifecycle?.onTxError) { - txLifecycle.onTxError(error) + const withdrawToken: ArbTokenBridgeToken['withdraw'] = useCallback( + async ({ + erc20L1Address, + amount, + l2Signer, + txLifecycle, + destinationAddress + }) => { + if (!walletAddress) { + return } - console.warn('withdraw token err', error) - } - } - const removeTokensFromList = (listID: number) => { - setBridgeTokens(prevBridgeTokens => { - const newBridgeTokens = { ...prevBridgeTokens } - for (const address in bridgeTokens) { - const token = bridgeTokens[address] - if (!token) continue - - token.listIds.delete(listID) + try { + const erc20Bridger = await Erc20Bridger.fromProvider(childChainProvider) + const provider = l2Signer.provider + const isSmartContractAddress = + provider && + (await provider.getCode(String(erc20L1Address))).length < 2 + if (isSmartContractAddress && !destinationAddress) { + throw new Error(`Missing destination address`) + } - if (token.listIds.size === 0) { - delete newBridgeTokens[address] + if (typeof bridgeTokens === 'undefined') { + return } - } - return newBridgeTokens - }) - } + const bridgeToken = bridgeTokens[erc20L1Address] - const addTokensFromList = async (arbTokenList: TokenList, listId: number) => { - const bridgeTokensToAdd: ContractStorage = {} + const { symbol, decimals } = await (async () => { + if (bridgeToken) { + const { symbol, decimals } = bridgeToken + return { symbol, decimals } + } + const { symbol, decimals } = await fetchErc20Data({ + address: erc20L1Address, + provider: parentChainProvider + }) - const candidateUnbridgedTokensToAdd: ERC20BridgeToken[] = [] + addToken(erc20L1Address) + return { symbol, decimals } + })() - for (const tokenData of arbTokenList.tokens) { - const { address, name, symbol, extensions, decimals, logoURI, chainId } = - tokenData + const withdrawalRequest = await erc20Bridger.getWithdrawalRequest({ + from: walletAddress, + erc20l1Address: erc20L1Address, + destinationAddress: destinationAddress ?? walletAddress, + amount + }) - if (![parentChain.id, childChain.id].includes(chainId)) { - continue - } + const gasLimit = await childChainProvider.estimateGas( + withdrawalRequest.txRequest + ) - 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 tx = await erc20Bridger.withdraw({ + ...withdrawalRequest, + l2Signer, + overrides: { gasLimit: percentIncrease(gasLimit, BigNumber.from(30)) } + }) + + if (txLifecycle?.onTxSubmit) { + txLifecycle.onTxSubmit(tx) } - 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 - ) + + addPendingTransaction({ + sender: walletAddress, + destination: destinationAddress ?? walletAddress, + direction: 'withdraw', + status: WithdrawalStatus.UNCONFIRMED, + createdAt: dayjs().valueOf(), + resolvedAt: null, + txId: tx.hash, + asset: symbol, + assetType: AssetType.ERC20, + value: utils.formatUnits(amount, decimals), + uniqueId: null, + isWithdrawal: true, + blockNum: null, + tokenAddress: erc20L1Address, + parentChainId: parentChain.id, + childChainId: childChain.id + }) + + const receipt = await tx.wait() + + if (txLifecycle?.onTxConfirm) { + txLifecycle.onTxConfirm(receipt) } - if (!isExtensions(extensions)) { - return null - } else { - return extensions.bridgeInfo + + updateTokenData(erc20L1Address) + return receipt + } catch (error) { + if (txLifecycle?.onTxError) { + txLifecycle.onTxError(error) } - })() + console.warn('withdraw token err', error) + } + }, + [ + addPendingTransaction, + addToken, + bridgeTokens, + childChain.id, + childChainProvider, + parentChain.id, + parentChainProvider, + updateTokenData, + walletAddress + ] + ) - if (bridgeInfo) { - const l1Address = bridgeInfo[parentChain.id]?.tokenAddress.toLowerCase() + const removeTokensFromList = useCallback( + (listID: number) => { + setBridgeTokens(prevBridgeTokens => { + const newBridgeTokens = { ...prevBridgeTokens } + for (const address in bridgeTokens) { + const token = bridgeTokens[address] + if (!token) continue - if (!l1Address) { - return + token.listIds.delete(listID) + + if (token.listIds.size === 0) { + delete newBridgeTokens[address] + } } + return newBridgeTokens + }) + }, + [bridgeTokens, setBridgeTokens] + ) + + const addTokensFromList = useCallback( + async (arbTokenList: TokenList, listId: number) => { + const bridgeTokensToAdd: ContractStorage = {} - bridgeTokensToAdd[l1Address] = { + const candidateUnbridgedTokensToAdd: ERC20BridgeToken[] = [] + + 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(childChain.id).isArbitrumOne) { - l2Addresses.push(CommonAddress.ArbitrumOne.USDC) - } - if (isNetwork(childChain.id).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) + } - const addBridgeTokenListToBridge = (bridgeTokenList: BridgeTokenList) => { - fetchTokenListFromURL(bridgeTokenList.url).then( - ({ isValid, data: tokenList }) => { - if (!isValid) return + 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) + } - addTokensFromList(tokenList!, bridgeTokenList.id) - } - ) - } + // 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]) + } - function addToExecutedMessagesCache(events: L2ToL1EventResult[]) { - const added: { [cacheKey: string]: boolean } = {} + updateErc20L1Balance(l1Addresses) + updateErc20L2Balance(l2Addresses) - events.forEach((event: L2ToL1EventResult) => { - const cacheKey = getExecutedMessagesCacheKey({ - event, - l2ChainId: childChain.id + return { + ...oldBridgeTokens, + ...bridgeTokensToAdd + } }) + }, + [ + childChain.id, + parentChain.id, + setBridgeTokens, + updateErc20L1Balance, + updateErc20L2Balance + ] + ) - added[cacheKey] = true - }) + const addBridgeTokenListToBridge = useCallback( + (bridgeTokenList: BridgeTokenList) => { + fetchTokenListFromURL(bridgeTokenList.url).then( + ({ isValid, data: tokenList }) => { + if (!isValid) return - setExecutedMessagesCache({ ...executedMessagesCache, ...added }) - } + addTokensFromList(tokenList!, bridgeTokenList.id) + } + ) + }, + [addTokensFromList] + ) - async function triggerOutboxToken({ - event, - l1Signer - }: { - event: L2ToL1EventResultPlus - l1Signer: Signer - }) { - // sanity check - if (!event) { - throw new Error('Outbox message not found') - } + const addToExecutedMessagesCache = useCallback( + (events: L2ToL1EventResult[]) => { + const added: { [cacheKey: string]: boolean } = {} - if (!walletAddress) { - return - } + events.forEach((event: L2ToL1EventResult) => { + const cacheKey = getExecutedMessagesCacheKey({ + event, + l2ChainId: childChain.id + }) - const parentChainProvider = getProvider(event.parentChainId) - const childChainProvider = getProvider(event.childChainId) + added[cacheKey] = true + }) - const messageWriter = L2ToL1Message.fromEvent( - l1Signer, + 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 = getProvider(event.parentChainId) + const childChainProvider = getProvider(event.childChainId) - return rec - } + const messageWriter = L2ToL1Message.fromEvent( + l1Signer, + event, + parentChainProvider + ) + const res = await messageWriter.execute(childChainProvider) - function addL2NativeToken(erc20L2Address: string) { - const token = getL2NativeToken(erc20L2Address, childChain.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') - } + return rec + }, + [addToExecutedMessagesCache, walletAddress] + ) - if (!walletAddress) { - return - } + const addL2NativeToken = useCallback( + (erc20L2Address: string) => { + const token = getL2NativeToken(erc20L2Address, childChain.id) - const parentChainProvider = getProvider(event.parentChainId) - const childChainProvider = getProvider(event.childChainId) + 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] + ) - const messageWriter = L2ToL1Message.fromEvent( - l1Signer, + const triggerOutboxEth = useCallback( + async ({ event, - parentChainProvider - ) + l1Signer + }: { + event: L2ToL1EventResultPlus + l1Signer: Signer + }) => { + // sanity check + if (!event) { + throw new Error('Outbox message not found') + } + + if (!walletAddress) { + return + } - const res = await messageWriter.execute(childChainProvider) + const parentChainProvider = getProvider(event.parentChainId) + const childChainProvider = getProvider(event.childChainId) - const rec = await res.wait() + const messageWriter = L2ToL1Message.fromEvent( + l1Signer, + event, + parentChainProvider + ) - if (rec.status === 1) { - addToExecutedMessagesCache([event]) - } + const res = await messageWriter.execute(childChainProvider) - return rec - } + const rec = await res.wait() + + if (rec.status === 1) { + addToExecutedMessagesCache([event]) + } + + return rec + }, + [addToExecutedMessagesCache, walletAddress] + ) return { bridgeTokens, From 207c6f902009be1f882e0fd2a57d3254f24f8679 Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 29 Jan 2024 16:00:25 +0100 Subject: [PATCH 15/28] Memoize returned object in useArbTokenBridge --- .../src/hooks/useArbTokenBridge.ts | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index 9a6243a6cd..4152927d92 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -1120,31 +1120,62 @@ export const useArbTokenBridge = (): ArbTokenBridge => { [addToExecutedMessagesCache, walletAddress] ) - return { - bridgeTokens, - eth: { + const ethFunctions = useMemo( + () => ({ deposit: depositEth, withdraw: withdrawEth, triggerOutbox: triggerOutboxEth - }, - token: { + }), + [depositEth, triggerOutboxEth, withdrawEth] + ) + const tokenFunctions = useMemo( + () => ({ add: addToken, addL2NativeToken, addTokensFromList, - addBridgeTokenListToBridge, removeTokensFromList, + addBridgeTokenListToBridge, updateTokenData, approve: approveToken, approveL2: approveTokenL2, deposit: depositToken, withdraw: withdrawToken, triggerOutbox: triggerOutboxToken - }, - transactions: { + }), + [ + addBridgeTokenListToBridge, + addL2NativeToken, + addToken, + addTokensFromList, + approveToken, + approveTokenL2, + depositToken, + removeTokensFromList, + triggerOutboxToken, + updateTokenData, + withdrawToken + ] + ) + + const transactionsFunctions = useMemo( + () => ({ transactions, updateTransaction, addTransaction, fetchAndUpdateL1ToL2MsgStatus - } + }), + [ + addTransaction, + fetchAndUpdateL1ToL2MsgStatus, + transactions, + updateTransaction + ] + ) + + return { + bridgeTokens, + eth: ethFunctions, + token: tokenFunctions, + transactions: transactionsFunctions } } From 8cef3445ae9720db3c483f5191270742c863ca0d Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 29 Jan 2024 16:00:36 +0100 Subject: [PATCH 16/28] Remove unused import --- packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index 4152927d92..ea9f5c89eb 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, utils } from 'ethers' import { Signer } from '@ethersproject/abstract-signer' From aa1b575b5aafd6c246e41bffba8d0ec636cf5ae5 Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 29 Jan 2024 17:08:52 +0100 Subject: [PATCH 17/28] Rework call to useArbTokenBridge --- .../components/TransferPanel/TokenButton.tsx | 4 +-- .../TransferPanel/TokenImportDialog.tsx | 16 +++++++---- .../src/components/TransferPanel/TokenRow.tsx | 4 +-- .../components/TransferPanel/TokenSearch.tsx | 16 +++++++---- .../TransferPanel/TokenSearchUtils.ts | 4 +-- .../TransferPanel/TransferPanel.tsx | 28 ++++++++++++++----- .../TransferPanel/TransferPanelMain.tsx | 15 ++++------ .../TransferPanel/TransferPanelSummary.tsx | 7 +---- .../src/hooks/useClaimWithdrawal.ts | 10 ++++--- .../src/hooks/useRedeemRetryable.ts | 7 ++--- 10 files changed, 65 insertions(+), 46 deletions(-) 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 9cba93daf6..9ca42ddd6f 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx @@ -9,14 +9,14 @@ import { sanitizeTokenSymbol } from '../../util/TokenUtils' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' -import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' +import { useBridgeTokensStore } from '../../hooks/useArbTokenBridge' export function TokenButton(): JSX.Element { const { app: { selectedToken } } = useAppState() const [networks] = useNetworks() - const { bridgeTokens } = useArbTokenBridge() + const { bridgeTokens } = useBridgeTokensStore() const { childChain, childChainProvider, parentChain, isDepositMode } = useNetworksRelationship(networks) 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 94710fbfc5..f7e54bc856 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx @@ -29,7 +29,10 @@ import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { isWithdrawOnlyToken } from '../../util/WithdrawOnlyUtils' import { isTransferDisabledToken } from '../../util/TokenTransferDisabledUtils' import { useTransferDisabledDialogStore } from './TransferDisabledDialog' -import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' +import { + useArbTokenBridge, + useBridgeTokensStore +} from '../../hooks/useArbTokenBridge' enum ImportStatus { LOADING, @@ -75,7 +78,10 @@ export function TokenImportDialog({ const { app: { selectedToken } } = useAppState() - const { bridgeTokens, token } = useArbTokenBridge() + const { bridgeTokens } = useBridgeTokensStore() + const { + token: { add: addToken, updateTokenData } + } = useArbTokenBridge() const [networks] = useNetworks() const { childChain, @@ -176,10 +182,10 @@ export function TokenImportDialog({ const selectToken = useCallback( async (_token: ERC20BridgeToken) => { - await token.updateTokenData(_token.address) + await updateTokenData(_token.address) actions.app.setSelectedToken(_token) }, - [token, actions] + [updateTokenData, actions] ) useEffect(() => { @@ -263,7 +269,7 @@ export function TokenImportDialog({ ]) 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') { 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 12fa5fb6de..60b5f8226a 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx @@ -32,7 +32,7 @@ import { useAccountType } from '../../hooks/useAccountType' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' -import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' +import { useBridgeTokensStore } from '../../hooks/useArbTokenBridge' function tokenListIdsToNames(ids: number[]): string { return ids @@ -82,7 +82,7 @@ export function TokenRow({ token }: TokenRowProps): JSX.Element { const { address: walletAddress } = useAccount() - const { bridgeTokens } = useArbTokenBridge() + const { bridgeTokens } = useBridgeTokensStore() const { isLoading: isLoadingAccountType } = useAccountType() const [networks] = useNetworks() const { 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 0eb54f5f8f..2c8297104f 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx @@ -156,7 +156,10 @@ function TokensPanel({ onTokenSelected: (token: ERC20BridgeToken | null) => void }): JSX.Element { const { address: walletAddress } = useAccount() - const { token, bridgeTokens } = useArbTokenBridge() + const { bridgeTokens } = useBridgeTokensStore() + const { + token: { addL2NativeToken, add: addToken } + } = useArbTokenBridge() const [networks] = useNetworks() const { childChain, childChainProvider, parentChainProvider, isDepositMode } = useNetworksRelationship(networks) @@ -345,7 +348,7 @@ function TokensPanel({ try { // Try to add the token as an L2-native token - token.addL2NativeToken(newToken) + addL2NativeToken(newToken) isSuccessful = true } catch (error) { // @@ -353,7 +356,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') { @@ -471,7 +474,10 @@ function TokensPanel({ export function TokenSearch({ close }: { close: () => void }) { const { address: walletAddress } = useAccount() - const { token, bridgeTokens } = useArbTokenBridge() + const { bridgeTokens } = useBridgeTokensStore() + const { + token: { updateTokenData } + } = useArbTokenBridge() const { app: { setSelectedToken } } = useActions() @@ -543,7 +549,7 @@ export function TokenSearch({ close }: { close: () => void }) { }) 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 5fddd44f08..018ecaafbd 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearchUtils.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearchUtils.ts @@ -8,7 +8,7 @@ import { useTokenLists } from '../../hooks/useTokenLists' import { TokenListWithId } from '../../util/TokenListUtils' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { useNetworks } from '../../hooks/useNetworks' -import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' +import { useBridgeTokensStore } from '../../hooks/useArbTokenBridge' export function useTokensFromLists(): ContractStorage { const [networks] = useNetworks() @@ -25,7 +25,7 @@ export function useTokensFromLists(): ContractStorage { } export function useTokensFromUser(): ContractStorage { - const { bridgeTokens } = useArbTokenBridge() + 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 fd4618629d..9c99cb6ee9 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -121,7 +121,15 @@ export function TransferPanel() { const { app: { connectionState, selectedToken, warningTokens } } = useAppState() - const { eth, token } = useArbTokenBridge() + const { + eth: { withdraw: ethWithdraw, deposit: ethDeposit }, + token: { + withdraw: erc20Withdraw, + deposit: erc20Deposit, + approve: erc20Approve, + approveL2: erc20ApproveL2 + } + } = useArbTokenBridge() const { layout } = useAppContextState() const { isTransferring } = layout const { address: walletAddress, isConnected } = useAccount() @@ -154,8 +162,16 @@ export function TransferPanel() { const { isArbitrumNova } = isNetwork(childChain.id) - const latestEth = useLatest(eth) - const latestToken = useLatest(token) + const latestEth = useLatest({ + withdraw: ethWithdraw, + deposit: ethDeposit + }) + const latestToken = useLatest({ + withdraw: erc20Withdraw, + deposit: erc20Deposit, + approve: erc20Approve, + approveL2: erc20ApproveL2 + }) const isConnectedToArbitrum = useLatest(useIsConnectedToArbitrum()) const isConnectedToOrbitChain = useLatest(useIsConnectedToOrbitChain()) @@ -767,8 +783,7 @@ export function TransferPanel() { while ( (isConnectedToArbitrum.current && isParentChainEthereum) || isConnectedToOrbitChain.current || - !latestEth.current || - !arbTokenBridgeLoaded + !latestEth.current ) { await new Promise(r => setTimeout(r, 100)) } @@ -944,8 +959,7 @@ export function TransferPanel() { (!isConnectedToArbitrum.current && !isConnectedToOrbitChain.current) || (isConnectedToArbitrum.current && isOrbitChain) || - !latestEth.current || - !arbTokenBridgeLoaded + !latestEth.current ) { await new Promise(r => setTimeout(r, 100)) } diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx index a7f5758015..e5cf6a2a0e 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx @@ -363,7 +363,9 @@ export function TransferPanelMain({ const { app } = useAppState() const { address: walletAddress } = useAccount() const { selectedToken } = app - const { token } = useArbTokenBridge() + const { + token: { updateTokenData } + } = useArbTokenBridge() const { destinationAddress, setDestinationAddress } = useDestinationAddressStore() @@ -737,11 +739,6 @@ export function TransferPanelMain({ return } - // When switching network, token might be undefined - if (!token) { - return - } - const commonUSDC = { name: 'USD Coin', type: TokenType.ERC20, @@ -750,21 +747,21 @@ export function TransferPanelMain({ listIds: new Set() } if (isArbOneUSDC) { - token.updateTokenData(CommonAddress.Ethereum.USDC) + updateTokenData(CommonAddress.Ethereum.USDC) actions.app.setSelectedToken({ ...commonUSDC, address: CommonAddress.Ethereum.USDC, l2Address: CommonAddress.ArbitrumOne['USDC.e'] }) } else if (isArbSepoliaUSDC) { - token.updateTokenData(CommonAddress.Sepolia.USDC) + updateTokenData(CommonAddress.Sepolia.USDC) actions.app.setSelectedToken({ ...commonUSDC, address: CommonAddress.Sepolia.USDC, l2Address: CommonAddress.ArbitrumSepolia['USDC.e'] }) } - }, [actions.app, isDepositMode, selectedToken, token]) + }, [actions.app, isDepositMode, selectedToken, updateTokenData]) type NetworkListboxesProps = { from: Omit 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 a84262a947..0e2f21ef80 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelSummary.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelSummary.tsx @@ -4,7 +4,6 @@ import { InformationCircleIcon } from '@heroicons/react/24/outline' import { useAccount } from 'wagmi' import { Tooltip } from '../common/Tooltip' -import { useAppState } from '../../state' import { useETHPrice } from '../../hooks/useETHPrice' import { useDebouncedValue } from '../../hooks/useDebouncedValue' import { formatAmount, formatUSD } from '../../util/NumberUtils' @@ -22,7 +21,6 @@ import { ChainLayer, useChainLayers } from '../../hooks/useChainLayers' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' -import { useArbTokenBridge } from '../../hooks/useArbTokenBridge' export type GasEstimationStatus = | 'idle' @@ -64,7 +62,6 @@ export function useGasSummary( token: TransferPanelSummaryToken | null, shouldRunGasEstimation: boolean ): UseGasSummaryResult { - const arbTokenBridge = useArbTokenBridge() const [networks] = useNetworks() const { childChainProvider, parentChainProvider, isDepositMode } = useNetworksRelationship(networks) @@ -213,9 +210,7 @@ export function useGasSummary( } } - if (arbTokenBridge && arbTokenBridge.eth && arbTokenBridge.token) { - estimateGas() - } + estimateGas() // eslint-disable-next-line react-hooks/exhaustive-deps }, [ // Re-run gas estimation when: diff --git a/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts b/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts index d29780f737..21a7079288 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts @@ -2,7 +2,6 @@ import { useState } from 'react' import * as Sentry from '@sentry/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' @@ -25,7 +24,10 @@ export type UseClaimWithdrawalResult = { } export function useClaimWithdrawal(): UseClaimWithdrawalResult { - const arbTokenBridge = useArbTokenBridge() + const { + eth: { triggerOutbox: ethTriggerOutbox }, + token: { triggerOutbox: erc20TriggerOutbox } + } = useArbTokenBridge() const { address } = useAccount() const { data: signer } = useSigner() const { updatePendingTransaction } = useTransactionHistory(address) @@ -84,12 +86,12 @@ export function useClaimWithdrawal(): UseClaimWithdrawalResult { 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 }) diff --git a/packages/arb-token-bridge-ui/src/hooks/useRedeemRetryable.ts b/packages/arb-token-bridge-ui/src/hooks/useRedeemRetryable.ts index e624802835..258d1fe406 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useRedeemRetryable.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useRedeemRetryable.ts @@ -5,7 +5,6 @@ import { } from '@arbitrum/sdk' import { useSigner } from 'wagmi' -import { useAppState } from '../state' import { MergedTransaction } from '../state/app/state' import { getRetryableTicket } from '../util/RetryableUtils' import { shouldTrackAnalytics, trackEvent } from '../util/AnalyticsUtils' @@ -15,7 +14,7 @@ import { errorToast } from '../components/common/atoms/Toast' import { AssetType } from './arbTokenBridge.types' import { useNetworks } from './useNetworks' import { useNetworksRelationship } from './useNetworksRelationship' -import { useArbTokenBridge } from './useArbTokenBridge' +import useTransactions from './useTransactions' export type UseRedeemRetryableResult = { redeem: (tx: MergedTransaction) => void @@ -23,7 +22,7 @@ export type UseRedeemRetryableResult = { } export function useRedeemRetryable(): UseRedeemRetryableResult { - const arbTokenBridge = useArbTokenBridge() + const [, { fetchAndUpdateL1ToL2MsgStatus }] = useTransactions() const [networks] = useNetworks() const { childChain, parentChainProvider } = useNetworksRelationship(networks) const { data: signer } = useSigner() @@ -79,7 +78,7 @@ export function useRedeemRetryable(): UseRedeemRetryableResult { } // update in store - arbTokenBridge.transactions.fetchAndUpdateL1ToL2MsgStatus( + fetchAndUpdateL1ToL2MsgStatus( tx.txId, retryableTicket, tx.assetType === AssetType.ETH, From 59f56e481e09c0ae781d80d9c938f04ae3635420 Mon Sep 17 00:00:00 2001 From: Christophe Date: Tue, 30 Jan 2024 19:33:48 +0100 Subject: [PATCH 18/28] Keep track of source/destination chain in state --- .../arb-token-bridge-ui/src/components/App/App.tsx | 14 +++++++------- .../arb-token-bridge-ui/src/state/app/actions.ts | 10 +++++----- .../arb-token-bridge-ui/src/state/app/state.ts | 10 +++++----- 3 files changed, 17 insertions(+), 17 deletions(-) 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 44a24745f8..2d6a857db4 100644 --- a/packages/arb-token-bridge-ui/src/components/App/App.tsx +++ b/packages/arb-token-bridge-ui/src/components/App/App.tsx @@ -175,6 +175,10 @@ const Injector = ({ children }: { children: React.ReactNode }): JSX.Element => { ).isEthereumMainnetOrTestnet actions.app.reset(networks.sourceChain.id) + actions.app.setChainIds({ + sourceChainId: networks.sourceChain.id, + destinationChainId: networks.destinationChain.id + }) if ( (isParentChainEthereum && isConnectedToArbitrum) || @@ -190,13 +194,9 @@ const Injector = ({ children }: { children: React.ReactNode }): JSX.Element => { isConnected, address, networks.sourceChain.id, - parentChain.id, - childChain.id, - parentChain, - childChain, - parentChainProvider, - childChainProvider, - actions.app + networks.destinationChain.id, + actions.app, + parentChain.id ]) useEffect(() => { 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 38504b7976..6642643e62 100644 --- a/packages/arb-token-bridge-ui/src/state/app/actions.ts +++ b/packages/arb-token-bridge-ui/src/state/app/actions.ts @@ -12,10 +12,10 @@ export const setConnectionState = ( export const setChainIds = ( { state }: Context, - payload: { l1NetworkChainId: number; l2NetworkChainId: number } + payload: { sourceChainId: number; destinationChainId: number } ) => { - state.app.l1NetworkChainId = payload.l1NetworkChainId - state.app.l2NetworkChainId = payload.l2NetworkChainId + state.app.sourceChainId = payload.sourceChainId + state.app.destinationChainId = payload.destinationChainId } export const setSelectedToken = ( @@ -27,8 +27,8 @@ export const setSelectedToken = ( export const reset = ({ state }: Context, newChainId: number) => { if ( - state.app.l1NetworkChainId !== newChainId && - state.app.l2NetworkChainId !== newChainId + state.app.sourceChainId !== newChainId && + state.app.destinationChainId !== 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 Goerli to Arbitrum Goerli for example 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 5d916a1b2e..97f68425b9 100644 --- a/packages/arb-token-bridge-ui/src/state/app/state.ts +++ b/packages/arb-token-bridge-ui/src/state/app/state.ts @@ -82,15 +82,15 @@ export type AppState = { connectionState: number selectedToken: ERC20BridgeToken | null verifying: WhiteListState - l1NetworkChainId: number | null - l2NetworkChainId: number | null + sourceChainId: number | null + destinationChainId: number | null } -export const defaultState: AppState = { +const defaultState: AppState = { warningTokens: {} as WarningTokens, connectionState: ConnectionState.LOADING, - l1NetworkChainId: null, - l2NetworkChainId: null, + sourceChainId: null, + destinationChainId: null, verifying: WhiteListState.ALLOWED, selectedToken: null } From 5354099806ecf0f944f9584e2ca45c1eb8985598 Mon Sep 17 00:00:00 2001 From: Christophe Date: Tue, 30 Jan 2024 19:45:18 +0100 Subject: [PATCH 19/28] Fix warning with missing autoresizer style --- .../src/components/TransferPanel/TokenSearch.tsx | 1 + 1 file changed, 1 insertion(+) 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 2c8297104f..0397c3db6f 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx @@ -439,6 +439,7 @@ function TokensPanel({ return ( onTokenSelected(null)} token={null} /> From 22b6448ad51b56bb6859135ed32db51ef8b80c6a Mon Sep 17 00:00:00 2001 From: Christophe Date: Tue, 27 Feb 2024 16:10:28 +0100 Subject: [PATCH 20/28] Remove ledger from yarn.lock --- yarn.lock | 5 ----- 1 file changed, 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 45a0759d14..77f3062697 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1284,11 +1284,6 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@ledgerhq/connect-kit-loader@^1.0.1": - version "1.1.8" - resolved "https://registry.yarnpkg.com/@ledgerhq/connect-kit-loader/-/connect-kit-loader-1.1.8.tgz#6cc32191660dd9d6e8f89047af09b0f201e30190" - integrity sha512-mDJsOucVW8m3Lk2fdQst+P74SgiKebvq1iBk4sXLbADQOwhL9bWGaArvO+tW7jPJZwEfSPWBdHcHoYi11XAwZw== - "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" From 8be94787aa14e5006f620df7a817f7a0b967217e Mon Sep 17 00:00:00 2001 From: Fionna <13184582+fionnachan@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:34:18 +0100 Subject: [PATCH 21/28] fix import dialog --- .../components/TransferPanel/TransferPanel.tsx | 3 +-- .../hooks/TransferPanel/useImportTokenModal.ts | 15 ++++----------- 2 files changed, 5 insertions(+), 13 deletions(-) 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 e38d3a4b29..5f1798c87b 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -199,8 +199,7 @@ export function TransferPanel() { } useImportTokenModal({ - importTokenModalStatus, - connectionState + importTokenModalStatus }) const isBridgingANewStandardToken = useMemo(() => { 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]) } From 801d145abfa9013887ba2e251a2dc5e8302b5b12 Mon Sep 17 00:00:00 2001 From: Christophe Date: Thu, 1 Aug 2024 14:37:23 +0000 Subject: [PATCH 22/28] Fix import token --- .../TransferPanel/TokenImportDialog.tsx | 49 +++++-------------- 1 file changed, 12 insertions(+), 37 deletions(-) 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 fe777ae8e7..365a18dbb7 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx @@ -69,9 +69,6 @@ export function TokenImportDialog({ tokenAddress }: TokenImportDialogProps): JSX.Element { const { address: walletAddress } = useAccount() - const { - app: { selectedToken } - } = useAppState() const { bridgeTokens } = useBridgeTokensStore() const { token: { add: addToken, updateTokenData } @@ -238,37 +235,6 @@ 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 addToken(newToken).catch((ex: Error) => { setStatus(ImportStatus.ERROR) @@ -300,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 From d8e12da5731530d1140ff7bc7b715fa7258d0948 Mon Sep 17 00:00:00 2001 From: Christophe Date: Fri, 2 Aug 2024 13:07:58 +0000 Subject: [PATCH 23/28] Add missing hooks in App --- .../src/components/App/App.tsx | 6 ++++ .../components/TransferPanel/TokenButton.tsx | 12 ------- .../TransferPanel/TransferPanel.tsx | 2 +- .../src/hooks/useWarningTokensList.ts | 17 ++++++++++ .../src/state/app/actions.ts | 32 +------------------ 5 files changed, 25 insertions(+), 44 deletions(-) create mode 100644 packages/arb-token-bridge-ui/src/hooks/useWarningTokensList.ts 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 0660c8e490..1c3f270e49 100644 --- a/packages/arb-token-bridge-ui/src/components/App/App.tsx +++ b/packages/arb-token-bridge-ui/src/components/App/App.tsx @@ -37,6 +37,8 @@ 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 { @@ -94,6 +96,10 @@ function AppContent() { 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 } 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 41f9f6dc00..c058ae913d 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx @@ -47,18 +47,6 @@ export function TokenButton(): JSX.Element { onClick={onPopoverButtonClick} >
- {/* 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} 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 5f1798c87b..feabc3d73e 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -101,7 +101,7 @@ export function TransferPanel() { useState(false) const { - app: { connectionState, selectedToken, warningTokens } + app: { selectedToken, warningTokens } } = useAppState() const { token: { updateTokenData } 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 668461f67b..9812e50880 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,7 @@ import { ERC20BridgeToken } from '../../hooks/arbTokenBridge.types' import { Context } from '..' import { ConnectionState } from '../../util' -import { WhiteListState, WarningTokens } from './state' +import { WarningTokens } from './state' export const setConnectionState = ( { state }: Context, @@ -10,14 +10,6 @@ export const setConnectionState = ( state.app.connectionState = connectionState } -export const setChainIds = ( - { state }: Context, - payload: { sourceChainId: number; destinationChainId: number } -) => { - state.app.sourceChainId = payload.sourceChainId - state.app.destinationChainId = payload.destinationChainId -} - export const setSelectedToken = ( { state }: Context, token: ERC20BridgeToken | null @@ -25,31 +17,9 @@ export const setSelectedToken = ( state.app.selectedToken = token ? { ...token } : null } -export const reset = ({ state }: Context, newChainId: number) => { - if ( - state.app.sourceChainId !== newChainId && - state.app.destinationChainId !== 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.verifying = WhiteListState.ALLOWED - state.app.connectionState = ConnectionState.LOADING -} - export const setWarningTokens = ( { state }: Context, warningTokens: WarningTokens ) => { state.app.warningTokens = warningTokens } - -export const setWhitelistState = ( - { state }: Context, - verifying: WhiteListState -) => { - state.app.verifying = verifying -} From 303d33deb3895d5c5598d83b7899ff01e5bad037 Mon Sep 17 00:00:00 2001 From: Christophe Date: Fri, 2 Aug 2024 18:20:53 +0000 Subject: [PATCH 24/28] Remove connectionState --- packages/arb-token-bridge-ui/src/state/app/actions.ts | 8 -------- packages/arb-token-bridge-ui/src/state/app/state.ts | 3 --- packages/arb-token-bridge-ui/src/util/index.ts | 7 ------- 3 files changed, 18 deletions(-) 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 9812e50880..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,15 +1,7 @@ import { ERC20BridgeToken } from '../../hooks/arbTokenBridge.types' import { Context } from '..' -import { ConnectionState } from '../../util' import { WarningTokens } from './state' -export const setConnectionState = ( - { state }: Context, - connectionState: ConnectionState -) => { - state.app.connectionState = connectionState -} - export const setSelectedToken = ( { state }: Context, token: ERC20BridgeToken | null 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 5ea8ae1b2c..f4c15a4ec8 100644 --- a/packages/arb-token-bridge-ui/src/state/app/state.ts +++ b/packages/arb-token-bridge-ui/src/state/app/state.ts @@ -10,7 +10,6 @@ import { L2ToL3MessageData, TxnType } from '../../hooks/useTransactions' -import { ConnectionState } from '../../util' import { CCTPSupportedChainId } from '../cctpState' import { Address } from '../../util/AddressUtils' @@ -88,7 +87,6 @@ export interface WarningTokens { export type AppState = { warningTokens: WarningTokens - connectionState: number selectedToken: ERC20BridgeToken | null verifying: WhiteListState sourceChainId: number | null @@ -97,7 +95,6 @@ export type AppState = { const defaultState: AppState = { warningTokens: {} as WarningTokens, - connectionState: ConnectionState.LOADING, sourceChainId: null, destinationChainId: null, verifying: WhiteListState.ALLOWED, diff --git a/packages/arb-token-bridge-ui/src/util/index.ts b/packages/arb-token-bridge-ui/src/util/index.ts index d4ee03f1be..6e8aa8ca44 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)}` From ce1ade6972f7e59fae96c0c185654c772d3403d1 Mon Sep 17 00:00:00 2001 From: Christophe Date: Fri, 2 Aug 2024 18:21:34 +0000 Subject: [PATCH 25/28] Remove unused import --- .../src/components/TransferPanel/TokenImportDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 365a18dbb7..cd2b8b4ef6 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 { useActions } from '../../state' import { erc20DataToErc20BridgeToken, fetchErc20Data, From b868e0efe0ec67ef14d0da3a7ce90c0c288ba1d4 Mon Sep 17 00:00:00 2001 From: Christophe Date: Fri, 2 Aug 2024 18:23:48 +0000 Subject: [PATCH 26/28] Add lost code after merge --- .../src/hooks/useArbTokenBridge.ts | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index 1de79db8d2..dad9d2c6b5 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -28,7 +28,8 @@ import { getL1ERC20Address, getL2ERC20Address, l1TokenIsDisabled, - isValidErc20 + isValidErc20, + getL3ERC20Address } from '../util/TokenUtils' import { getL2NativeToken } from '../util/L2NativeUtils' import { CommonAddress } from '../util/CommonAddressUtils' @@ -38,6 +39,7 @@ import { useNetworksRelationship } from './useNetworksRelationship' import { BridgeTokenList, fetchTokenListFromURL } from '../util/TokenListUtils' import { useDestinationAddressStore } from '../components/TransferPanel/AdvancedSettings' import { getProviderForChainId } from '@/token-bridge-sdk/utils' +import { isTeleport } from '@/token-bridge-sdk/teleport' export const wait = (ms = 0) => { return new Promise(res => setTimeout(res, ms)) @@ -213,12 +215,36 @@ export const useArbTokenBridge = (): ArbTokenBridge => { l2Address = lowercasedErc20L1orL2Address } else { // looks like l1 address was provided + // l1Address = lowercasedErc20L1orL2Address + // l2Address = await getL2ERC20Address({ + // erc20L1Address: l1Address, + // l1Provider: parentChainProvider, + // l2Provider: childChainProvider + // }) l1Address = lowercasedErc20L1orL2Address - l2Address = await getL2ERC20Address({ - erc20L1Address: l1Address, - l1Provider: parentChainProvider, - l2Provider: childChainProvider - }) + + // 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 ( + isTeleport({ + 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 + }) + } } const bridgeTokensToAdd: ContractStorage = {} @@ -261,7 +287,9 @@ export const useArbTokenBridge = (): ArbTokenBridge => { } }, [ + childChain.id, childChainProvider, + parentChain.id, parentChainProvider, setBridgeTokens, updateErc20L1Balance, From a99a910089cd56f8d6068bcdaf3408d1c0c868af Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 5 Aug 2024 15:15:44 +0000 Subject: [PATCH 27/28] Remove commented code --- packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index dad9d2c6b5..dcbb9e0e88 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -215,12 +215,6 @@ export const useArbTokenBridge = (): ArbTokenBridge => { l2Address = lowercasedErc20L1orL2Address } else { // looks like l1 address was provided - // l1Address = lowercasedErc20L1orL2Address - // l2Address = await getL2ERC20Address({ - // erc20L1Address: l1Address, - // l1Provider: parentChainProvider, - // l2Provider: childChainProvider - // }) l1Address = lowercasedErc20L1orL2Address // while deriving the child-chain address, it can be a teleport transfer too, in that case derive L3 address from L1 address From 30462e6f79b741423f8e4dc9237adc1f436bda0d Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 9 Sep 2024 13:06:43 +0000 Subject: [PATCH 28/28] Remove extra token button --- .../src/components/TransferPanel/TokenButton.tsx | 3 --- 1 file changed, 3 deletions(-) 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 c9479ea641..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,9 +63,6 @@ export function TokenButton({ disabled={disabled} >
- - {tokenSymbol} - {tokenSymbol} {!disabled && (