diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts index 8c83422642..2f2c69c764 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useArbTokenBridge.ts @@ -135,245 +135,279 @@ export const useArbTokenBridge = ( const l1NetworkID = useMemo(() => String(l1.network.id), [l1.network.id]) - 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) - - if (token.listIds.size === 0) { - delete newBridgeTokens[address] + const removeTokensFromList = useCallback( + function removeTokensFromList(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 - }) - } + return newBridgeTokens + }) + }, + [bridgeTokens, setBridgeTokens] + ) - const addTokensFromList = async (arbTokenList: TokenList, listId: number) => { - const l1ChainID = l1.network.id - const l2ChainID = l2.network.id + const addTokensFromList = useCallback( + function (arbTokenList: TokenList, listId: number) { + const l1ChainID = l1.network.id + const l2ChainID = l2.network.id - const bridgeTokensToAdd: ContractStorage = {} + const bridgeTokensToAdd: ContractStorage = {} - const candidateUnbridgedTokensToAdd: ERC20BridgeToken[] = [] + const candidateUnbridgedTokensToAdd: ERC20BridgeToken[] = [] - for (const tokenData of arbTokenList.tokens) { - const { address, name, symbol, extensions, decimals, logoURI, chainId } = - tokenData + for (const tokenData of arbTokenList.tokens) { + const { + address, + name, + symbol, + extensions, + decimals, + logoURI, + chainId + } = tokenData - if (![l1ChainID, l2ChainID].includes(chainId)) { - continue - } + if (![l1ChainID, l2ChainID].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 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 - } - })() + 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[l1NetworkID]?.tokenAddress.toLowerCase() + if (bridgeInfo) { + const l1Address = bridgeInfo[l1NetworkID]?.tokenAddress.toLowerCase() - if (!l1Address) { - return - } + if (!l1Address) { + return + } - bridgeTokensToAdd[l1Address] = { - name, - type: TokenType.ERC20, - symbol, - address: l1Address, - l2Address: address.toLowerCase(), - decimals, - logoURI, - listIds: new Set([listId]) + 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]) + }) } } - // 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[] = [] + // 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(l2ChainID).isArbitrumOne) { - l2Addresses.push(CommonAddress.ArbitrumOne.USDC) - } - if (isNetwork(l2ChainID).isArbitrumSepolia) { - l2Addresses.push(CommonAddress.ArbitrumSepolia.USDC) - } + // 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) + } - for (const tokenAddress in bridgeTokensToAdd) { - const tokenToAdd = bridgeTokensToAdd[tokenAddress] - if (!tokenToAdd) { - 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) + } + + // 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 { address, l2Address } = tokenToAdd - if (address) { - l1Addresses.push(address) + + updateErc20L1Balance(l1Addresses) + updateErc20L2Balance(l2Addresses) + + return { + ...oldBridgeTokens, + ...bridgeTokensToAdd } - if (l2Address) { - l2Addresses.push(l2Address) + }) + }, + [ + l1.network.id, + l1NetworkID, + l2.network.id, + setBridgeTokens, + updateErc20L1Balance, + updateErc20L2Balance + ] + ) + + const addToken = useCallback( + async function addToken(erc20L1orL2Address: string) { + let l1Address: string + let l2Address: string | undefined + + if (!walletAddress) { + return + } + + const lowercasedErc20L1orL2Address = erc20L1orL2Address.toLowerCase() + const maybeL1Address = await getL1ERC20Address({ + erc20L2Address: lowercasedErc20L1orL2Address, + l2Provider: l2.provider + }) + + if (maybeL1Address) { + // looks like l2 address was provided + l1Address = maybeL1Address + l2Address = lowercasedErc20L1orL2Address + } else { + // looks like l1 address was provided + l1Address = lowercasedErc20L1orL2Address + + // while deriving the child-chain address, it can be a teleport transfer too, in that case derive L3 address from L1 address + // else, derive the L2 address from L1 address OR L3 address from L2 address + if ( + isValidTeleportChainPair({ + sourceChainId: l1.network.id, + destinationChainId: l2.network.id + }) + ) { + // this can be a bit hard to follow, but it will resolve when we have code-wide better naming for variables + // here `l2Address` actually means `childChainAddress`, and `l2.provider` is actually being used as a child-chain-provider, which in this case will be L3 + l2Address = await getL3ERC20Address({ + erc20L1Address: l1Address, + l1Provider: l1.provider, + l3Provider: l2.provider // in case of teleport transfer, the l2.provider being used here is actually the l3 provider + }) + } else { + l2Address = await getL2ERC20Address({ + erc20L1Address: l1Address, + l1Provider: l1.provider, + l2Provider: l2.provider + }) } + } - // 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 bridgeTokensToAdd: ContractStorage = {} + const erc20Params = { address: l1Address, provider: l1.provider } + + if (!(await isValidErc20(erc20Params))) { + throw new Error(`${l1Address} is not a valid ERC-20 token`) } - updateErc20L1Balance(l1Addresses) - updateErc20L2Balance(l2Addresses) + const { name, symbol, decimals } = await fetchErc20Data(erc20Params) + + const isDisabled = await l1TokenIsDisabled({ + erc20L1Address: l1Address, + l1Provider: l1.provider, + l2Provider: l2.provider + }) - return { - ...oldBridgeTokens, - ...bridgeTokensToAdd + if (isDisabled) { + throw new TokenDisabledError('Token currently disabled') } - }) - } - async function addToken(erc20L1orL2Address: string) { - let l1Address: string - let l2Address: string | undefined - - if (!walletAddress) { - return - } - - const lowercasedErc20L1orL2Address = erc20L1orL2Address.toLowerCase() - const maybeL1Address = await getL1ERC20Address({ - erc20L2Address: lowercasedErc20L1orL2Address, - l2Provider: l2.provider - }) - - if (maybeL1Address) { - // looks like l2 address was provided - l1Address = maybeL1Address - l2Address = lowercasedErc20L1orL2Address - } else { - // looks like l1 address was provided - l1Address = lowercasedErc20L1orL2Address - - // while deriving the child-chain address, it can be a teleport transfer too, in that case derive L3 address from L1 address - // else, derive the L2 address from L1 address OR L3 address from L2 address - if ( - isValidTeleportChainPair({ - sourceChainId: l1.network.id, - destinationChainId: l2.network.id - }) - ) { - // this can be a bit hard to follow, but it will resolve when we have code-wide better naming for variables - // here `l2Address` actually means `childChainAddress`, and `l2.provider` is actually being used as a child-chain-provider, which in this case will be L3 - l2Address = await getL3ERC20Address({ - erc20L1Address: l1Address, - l1Provider: l1.provider, - l3Provider: l2.provider // in case of teleport transfer, the l2.provider being used here is actually the l3 provider - }) - } else { - l2Address = await getL2ERC20Address({ - erc20L1Address: l1Address, - l1Provider: l1.provider, - l2Provider: l2.provider - }) + const l1AddressLowerCased = l1Address.toLowerCase() + bridgeTokensToAdd[l1AddressLowerCased] = { + name, + type: TokenType.ERC20, + symbol, + address: l1AddressLowerCased, + l2Address: l2Address?.toLowerCase(), + decimals, + listIds: new Set() } - } - - 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]) - } - } + + setBridgeTokens(oldBridgeTokens => { + return { ...oldBridgeTokens, ...bridgeTokensToAdd } + }) + + updateErc20L1Balance([l1AddressLowerCased]) + if (l2Address) { + updateErc20L2Balance([l2Address]) + } + }, + [ + l1.network.id, + l1.provider, + l2.network.id, + l2.provider, + setBridgeTokens, + updateErc20L1Balance, + updateErc20L2Balance, + walletAddress + ] + ) const updateTokenData = useCallback( async (l1Address: string) => { @@ -409,129 +443,154 @@ export const useArbTokenBridge = ( updateErc20L1Balance, updateErc20L2Balance, updateErc20L1CustomDestinationBalance, - updateErc20CustomDestinationL2Balance + updateErc20CustomDestinationL2Balance, + destinationAddress ] ) - async function triggerOutboxToken({ - event, - l1Signer - }: { - event: L2ToL1EventResultPlus - l1Signer: Signer - }) { - // sanity check - if (!event) { - throw new Error('Outbox message not found') - } - - if (!walletAddress) { - return - } - - const parentChainProvider = getProviderForChainId(event.parentChainId) - const childChainProvider = getProviderForChainId(event.childChainId) - - const messageWriter = ChildToParentMessage.fromEvent( - l1Signer, + const addToExecutedMessagesCache = useCallback( + function addToExecutedMessagesCache(events: L2ToL1EventResult[]) { + const added: { [cacheKey: string]: boolean } = {} + + events.forEach((event: L2ToL1EventResult) => { + const cacheKey = getExecutedMessagesCacheKey({ + event, + l2ChainId: l2.network.id + }) + + added[cacheKey] = true + }) + + setExecutedMessagesCache({ ...executedMessagesCache, ...added }) + }, + [executedMessagesCache, l2.network.id, setExecutedMessagesCache] + ) + + const triggerOutboxToken = useCallback( + async function triggerOutboxToken({ event, - parentChainProvider - ) - const res = await messageWriter.execute(childChainProvider) + l1Signer + }: { + event: L2ToL1EventResultPlus + l1Signer: Signer + }) { + // sanity check + if (!event) { + throw new Error('Outbox message not found') + } - const rec = await res.wait() + if (!walletAddress) { + return + } - if (rec.status === 1) { - addToExecutedMessagesCache([event]) - } + const parentChainProvider = getProviderForChainId(event.parentChainId) + const childChainProvider = getProviderForChainId(event.childChainId) - return rec - } + const messageWriter = ChildToParentMessage.fromEvent( + l1Signer, + event, + parentChainProvider + ) + const res = await messageWriter.execute(childChainProvider) - function addL2NativeToken(erc20L2Address: string) { - const token = getL2NativeToken(erc20L2Address, l2.network.id) - - setBridgeTokens(oldBridgeTokens => { - return { - ...oldBridgeTokens, - [`L2-NATIVE:${token.address}`]: { - name: token.name, - type: TokenType.ERC20, - symbol: token.symbol, - address: token.address, - l2Address: token.address, - decimals: token.decimals, - logoURI: token.logoURI, - listIds: new Set(), - isL2Native: true - } + const rec = await res.wait() + + if (rec.status === 1) { + addToExecutedMessagesCache([event]) } - }) - } - async function triggerOutboxEth({ - event, - l1Signer - }: { - event: L2ToL1EventResultPlus - l1Signer: Signer - }) { - // sanity check - if (!event) { - throw new Error('Outbox message not found') - } - - if (!walletAddress) { - return - } - - const parentChainProvider = getProviderForChainId(event.parentChainId) - const childChainProvider = getProviderForChainId(event.childChainId) - - const messageWriter = ChildToParentMessage.fromEvent( - l1Signer, - event, - parentChainProvider - ) + return rec + }, + [addToExecutedMessagesCache, walletAddress] + ) - const res = await messageWriter.execute(childChainProvider) + const addL2NativeToken = useCallback( + function addL2NativeToken(erc20L2Address: string) { + const token = getL2NativeToken(erc20L2Address, l2.network.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 + } + } + }) + }, + [l2.network.id, setBridgeTokens] + ) - if (rec.status === 1) { - addToExecutedMessagesCache([event]) - } + const triggerOutboxEth = useCallback( + async function triggerOutboxEth({ + event, + l1Signer + }: { + event: L2ToL1EventResultPlus + l1Signer: Signer + }) { + // sanity check + if (!event) { + throw new Error('Outbox message not found') + } - return rec - } + if (!walletAddress) { + return + } - function addToExecutedMessagesCache(events: L2ToL1EventResult[]) { - const added: { [cacheKey: string]: boolean } = {} + const parentChainProvider = getProviderForChainId(event.parentChainId) + const childChainProvider = getProviderForChainId(event.childChainId) - events.forEach((event: L2ToL1EventResult) => { - const cacheKey = getExecutedMessagesCacheKey({ + const messageWriter = ChildToParentMessage.fromEvent( + l1Signer, event, - l2ChainId: l2.network.id - }) + parentChainProvider + ) - added[cacheKey] = true - }) + const res = await messageWriter.execute(childChainProvider) - setExecutedMessagesCache({ ...executedMessagesCache, ...added }) - } + const rec = await res.wait() + + if (rec.status === 1) { + addToExecutedMessagesCache([event]) + } - return { - bridgeTokens, - eth: { - triggerOutbox: triggerOutboxEth + return rec }, - token: { - add: addToken, + [addToExecutedMessagesCache, walletAddress] + ) + + return useMemo( + () => ({ + bridgeTokens, + eth: { + triggerOutbox: triggerOutboxEth + }, + token: { + add: addToken, + addL2NativeToken, + addTokensFromList, + removeTokensFromList, + updateTokenData, + triggerOutbox: triggerOutboxToken + } + }), + [ + bridgeTokens, + triggerOutboxEth, + addToken, addL2NativeToken, addTokensFromList, removeTokensFromList, updateTokenData, - triggerOutbox: triggerOutboxToken - } - } + triggerOutboxToken + ] + ) }