diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchBar.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchBar.tsx index f6486fcc35..2a0bdb165d 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchBar.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchBar.tsx @@ -1,5 +1,5 @@ import { create } from 'zustand' -import { isAddress } from 'ethers/lib/utils.js' +import { isAddress } from 'ethers/lib/utils' import { Address, useAccount } from 'wagmi' import { useCallback, useEffect } from 'react' import { MagnifyingGlassIcon } from '@heroicons/react/24/outline' 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 8d90a95caa..bae5436094 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx @@ -29,7 +29,7 @@ import { warningToast } from '../common/atoms/Toast' import { CommonAddress } from '../../util/CommonAddressUtils' import { ArbOneNativeUSDC } from '../../util/L2NativeUtils' import { getNetworkName, isNetwork } from '../../util/networks' -import { useUpdateUSDCBalances } from '../../hooks/CCTP/useUpdateUSDCBalances' +import { useUpdateUsdcBalances } from '../../hooks/CCTP/useUpdateUsdcBalances' import { useAccountType } from '../../hooks/useAccountType' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { SearchPanelTable } from '../common/SearchPanel/SearchPanelTable' @@ -538,7 +538,7 @@ export function TokenSearch({ parentChainProvider, isTeleportMode } = useNetworksRelationship(networks) - const { updateUSDCBalances } = useUpdateUSDCBalances({ walletAddress }) + const { updateUsdcBalances } = useUpdateUsdcBalances({ walletAddress }) const { isLoading: isLoadingAccountType } = useAccountType() const { openDialog: openTransferDisabledDialog } = useTransferDisabledDialogStore() @@ -574,7 +574,7 @@ export function TokenSearch({ return } - await updateUSDCBalances() + updateUsdcBalances() // if an Orbit chain is selected we need to fetch its USDC address let childChainUsdcAddress 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 463768f240..19ad772ef3 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx @@ -4,6 +4,7 @@ import { twMerge } from 'tailwind-merge' import { utils } from 'ethers' import { Chain, useAccount } from 'wagmi' import { useMedia } from 'react-use' +import { isAddress } from 'ethers/lib/utils' import { useAppState } from '../../state' import { getExplorerUrl } from '../../util/networks' @@ -16,7 +17,7 @@ import { isTokenSepoliaUSDC, isTokenMainnetUSDC } from '../../util/TokenUtils' -import { useUpdateUSDCBalances } from '../../hooks/CCTP/useUpdateUSDCBalances' +import { useUpdateUsdcBalances } from '../../hooks/CCTP/useUpdateUsdcBalances' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' @@ -236,8 +237,12 @@ export function TransferPanelMain() { const { updateErc20ParentBalances, updateErc20ChildBalances } = useBalances() - const { updateUSDCBalances } = useUpdateUSDCBalances({ - walletAddress: destinationAddressOrWalletAddress + const { updateUsdcBalances } = useUpdateUsdcBalances({ + walletAddress: + destinationAddressOrWalletAddress && + isAddress(destinationAddressOrWalletAddress) + ? destinationAddressOrWalletAddress + : undefined }) useEffect(() => { @@ -262,7 +267,7 @@ export function TransferPanelMain() { isTokenArbitrumOneNativeUSDC(selectedToken.address) || isTokenArbitrumSepoliaNativeUSDC(selectedToken.address)) ) { - updateUSDCBalances() + updateUsdcBalances() return } @@ -275,7 +280,7 @@ export function TransferPanelMain() { updateErc20ParentBalances, updateErc20ChildBalances, destinationAddressOrWalletAddress, - updateUSDCBalances, + updateUsdcBalances, isTeleportMode ]) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useDestinationAddressError.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useDestinationAddressError.ts index 890a735445..5d41f1bc50 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useDestinationAddressError.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useDestinationAddressError.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' import useSWRImmutable from 'swr/immutable' -import { isAddress } from 'ethers/lib/utils.js' +import { isAddress } from 'ethers/lib/utils' import { DestinationAddressErrors } from '../AdvancedSettings' import { addressIsDenylisted } from '../../../util/AddressUtils' diff --git a/packages/arb-token-bridge-ui/src/components/common/AddCustomChain.tsx b/packages/arb-token-bridge-ui/src/components/common/AddCustomChain.tsx index 9be3138a8d..f63ec399b3 100644 --- a/packages/arb-token-bridge-ui/src/components/common/AddCustomChain.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/AddCustomChain.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { isAddress } from 'ethers/lib/utils.js' +import { isAddress } from 'ethers/lib/utils' import { Popover } from '@headlessui/react' import { registerCustomArbitrumNetwork } from '@arbitrum/sdk' import { StaticJsonRpcProvider } from '@ethersproject/providers' diff --git a/packages/arb-token-bridge-ui/src/components/syncers/useBalanceUpdater.tsx b/packages/arb-token-bridge-ui/src/components/syncers/useBalanceUpdater.tsx index 728322ced2..7b20f2cfa7 100644 --- a/packages/arb-token-bridge-ui/src/components/syncers/useBalanceUpdater.tsx +++ b/packages/arb-token-bridge-ui/src/components/syncers/useBalanceUpdater.tsx @@ -2,7 +2,8 @@ import { useInterval, useLatest } from 'react-use' import { useAccount } from 'wagmi' import { useAppState } from '../../state' -import { useUpdateUSDCBalances } from '../../hooks/CCTP/useUpdateUSDCBalances' +import { useUpdateUsdcBalances } from '../../hooks/CCTP/useUpdateUsdcBalances' +import { isTokenNativeUSDC } from '../../util/TokenUtils' // Updates all balances periodically export function useBalanceUpdater() { @@ -12,14 +13,17 @@ export function useBalanceUpdater() { const { address: walletAddress } = useAccount() const latestTokenBridge = useLatest(arbTokenBridge) - const { updateUSDCBalances } = useUpdateUSDCBalances({ + const { updateUsdcBalances } = useUpdateUsdcBalances({ walletAddress }) useInterval(() => { - updateUSDCBalances() - if (selectedToken) { + if (isTokenNativeUSDC(selectedToken.address)) { + updateUsdcBalances() + return + } + latestTokenBridge?.current?.token?.updateTokenData(selectedToken.address) } }, 10000) diff --git a/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUSDCBalances.ts b/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUSDCBalances.ts deleted file mode 100644 index 48e1bca0db..0000000000 --- a/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUSDCBalances.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { useCallback } from 'react' -import { isAddress } from 'ethers/lib/utils.js' - -import { CommonAddress } from '../../util/CommonAddressUtils' -import { getL2ERC20Address } from '../../util/TokenUtils' -import { useNetworks } from '../useNetworks' -import { useNetworksRelationship } from '../useNetworksRelationship' -import { isNetwork } from '../../util/networks' -import { useBalances } from '../useBalances' -import { Address } from 'wagmi' - -export function useUpdateUSDCBalances({ - walletAddress -}: { - walletAddress: string | undefined -}) { - const [networks] = useNetworks() - const { parentChainProvider, parentChain, childChainProvider } = - useNetworksRelationship(networks) - - const _walletAddress: Address | undefined = - walletAddress && isAddress(walletAddress) ? walletAddress : undefined - const { - updateErc20ParentBalances: updateErc20ParentBalance, - updateErc20ChildBalances: updateErc20ChildBalance - } = useBalances({ - parentWalletAddress: _walletAddress, - childWalletAddress: _walletAddress - }) - - const updateUSDCBalances = useCallback(async () => { - const { isEthereumMainnet, isSepolia, isArbitrumOne, isArbitrumSepolia } = - isNetwork(parentChain.id) - - let parentChainUsdcAddress, childChainUsdcAddress: string | undefined - - if (isEthereumMainnet || isSepolia) { - parentChainUsdcAddress = isEthereumMainnet - ? CommonAddress.Ethereum.USDC - : CommonAddress.Sepolia.USDC - - childChainUsdcAddress = isEthereumMainnet - ? CommonAddress.ArbitrumOne.USDC - : CommonAddress.ArbitrumSepolia.USDC - } - - if (isArbitrumOne || isArbitrumSepolia) { - parentChainUsdcAddress = isArbitrumOne - ? CommonAddress.ArbitrumOne.USDC - : CommonAddress.ArbitrumSepolia.USDC - } - - // USDC is not native for the selected networks, do nothing - if (!parentChainUsdcAddress) { - return - } - - updateErc20ParentBalance([parentChainUsdcAddress]) - - // we don't have native USDC addresses for Orbit chains, we need to fetch it - if (!childChainUsdcAddress) { - try { - childChainUsdcAddress = ( - await getL2ERC20Address({ - erc20L1Address: parentChainUsdcAddress, - l1Provider: parentChainProvider, - l2Provider: childChainProvider - }) - ).toLowerCase() - } catch { - // could be never bridged before - return - } - } - - if (childChainUsdcAddress) { - updateErc20ChildBalance([childChainUsdcAddress]) - } - }, [ - childChainProvider, - parentChain.id, - parentChainProvider, - updateErc20ParentBalance, - updateErc20ChildBalance - ]) - - return { updateUSDCBalances } -} diff --git a/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUsdcBalances.ts b/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUsdcBalances.ts new file mode 100644 index 0000000000..dde96b1a69 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUsdcBalances.ts @@ -0,0 +1,127 @@ +import { useCallback } from 'react' +import { Address } from 'wagmi' +import useSWRImmutable from 'swr/immutable' + +import { CommonAddress } from '../../util/CommonAddressUtils' +import { getL2ERC20Address } from '../../util/TokenUtils' +import { useNetworks } from '../useNetworks' +import { useNetworksRelationship } from '../useNetworksRelationship' +import { isNetwork } from '../../util/networks' +import { useBalances } from '../useBalances' +import { getProviderForChainId } from '@/token-bridge-sdk/utils' + +export async function getChildUsdcAddress({ + parentChainId, + childChainId +}: { + parentChainId: number + childChainId: number +}) { + const { + isEthereumMainnet: isParentEthereumMainnet, + isSepolia: isParentSepolia + } = isNetwork(parentChainId) + + if (isParentEthereumMainnet) { + return CommonAddress.ArbitrumOne.USDC + } + + if (isParentSepolia) { + return CommonAddress.ArbitrumSepolia.USDC + } + + const parentUsdcAddress = getParentUsdcAddress(parentChainId) + const parentProvider = getProviderForChainId(parentChainId) + const childProvider = getProviderForChainId(childChainId) + + if (!parentUsdcAddress) { + return + } + + return getL2ERC20Address({ + erc20L1Address: parentUsdcAddress, + l1Provider: parentProvider, + l2Provider: childProvider + }) +} + +export function getParentUsdcAddress(parentChainId: number) { + const { + isEthereumMainnet: isParentEthereumMainnet, + isSepolia: isParentSepolia, + isArbitrumOne: isParentArbitrumOne, + isArbitrumSepolia: isParentArbitrumSepolia + } = isNetwork(parentChainId) + + if (isParentEthereumMainnet) { + return CommonAddress.Ethereum.USDC + } + + if (isParentSepolia) { + return CommonAddress.Sepolia.USDC + } + + if (isParentArbitrumOne) { + return CommonAddress.ArbitrumOne.USDC + } + + if (isParentArbitrumSepolia) { + return CommonAddress.ArbitrumSepolia.USDC + } +} + +export function useUpdateUsdcBalances({ + walletAddress +}: { + walletAddress: Address | undefined +}) { + const [networks] = useNetworks() + const { parentChain, childChain } = useNetworksRelationship(networks) + + const { + updateErc20ParentBalances: updateErc20ParentBalance, + updateErc20ChildBalances: updateErc20ChildBalance + } = useBalances({ + parentWalletAddress: walletAddress, + childWalletAddress: walletAddress + }) + + // we don't have native USDC addresses for Orbit chains, we need to fetch it + const { data: childUsdcAddress, isLoading } = useSWRImmutable( + [parentChain.id, childChain.id, 'getChildUsdcAddress'], + ([parentChainId, childChainId]) => + getChildUsdcAddress({ + parentChainId, + childChainId + }) + ) + + const updateUsdcBalances = useCallback(() => { + const parentUsdcAddress = getParentUsdcAddress(parentChain.id) + + // USDC is not native for the selected networks, do nothing + if (!parentUsdcAddress) { + return + } + + if (isLoading) { + return + } + + updateErc20ParentBalance([parentUsdcAddress.toLowerCase()]) + + if (childUsdcAddress) { + updateErc20ChildBalance([childUsdcAddress.toLowerCase()]) + } + }, [ + isLoading, + childUsdcAddress, + parentChain.id, + updateErc20ChildBalance, + updateErc20ParentBalance + ]) + + return { + updateUsdcBalances + } +} diff --git a/packages/arb-token-bridge-ui/src/hooks/__tests__/useUpdateUsdcBalances.test.ts b/packages/arb-token-bridge-ui/src/hooks/__tests__/useUpdateUsdcBalances.test.ts new file mode 100644 index 0000000000..ac3358d499 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/hooks/__tests__/useUpdateUsdcBalances.test.ts @@ -0,0 +1,79 @@ +import { CommonAddress } from '../../util/CommonAddressUtils' +import { + getChildUsdcAddress, + getParentUsdcAddress +} from '../CCTP/useUpdateUsdcBalances' +import { getL2ERC20Address } from '../../util/TokenUtils' +import { ChainId } from '../../types/ChainId' + +jest.mock('../../util/TokenUtils', () => ({ + getL2ERC20Address: jest.fn() +})) + +const xaiTestnetChainId = 37714555429 as ChainId + +describe('getParentUsdcAddress', () => { + it('should return native USDC address on Ethereum when parent chain is Ethereum (1)', () => { + const result = getParentUsdcAddress(ChainId.Ethereum) + expect(result).toEqual(CommonAddress.Ethereum.USDC) + }) + + it('should return native USDC address on Sepolia when parent chain is Sepolia (11155111)', () => { + const result = getParentUsdcAddress(ChainId.Sepolia) + expect(result).toEqual(CommonAddress.Sepolia.USDC) + }) + + it('should return native USDC address on Arbitrum One when parent chain is Arbitrum One (42161)', () => { + const result = getParentUsdcAddress(ChainId.ArbitrumOne) + expect(result).toEqual(CommonAddress.ArbitrumOne.USDC) + }) + + it('should return native USDC address on Arbitrum Sepolia when parent chain is Arbitrum Sepolia (421614)', () => { + const result = getParentUsdcAddress(ChainId.ArbitrumSepolia) + expect(result).toEqual(CommonAddress.ArbitrumSepolia.USDC) + }) + + it('should return undefined when parent chain is Base (8453)', () => { + const result = getParentUsdcAddress(ChainId.Base) + expect(result).toEqual(undefined) + }) + + it('should return undefined when parent chain is Base Sepolia (84532)', () => { + const result = getParentUsdcAddress(ChainId.BaseSepolia) + expect(result).toEqual(undefined) + }) +}) + +describe('getChildUsdcAddress', () => { + it('should return native USDC address on Arbitrum One when parent USDC address is native USDC on Ethereum, parent chain is Ethereum, and child chain is Arbitrum One', async () => { + const result = await getChildUsdcAddress({ + parentChainId: ChainId.Ethereum, + childChainId: ChainId.ArbitrumOne + }) + + expect(result).toEqual(CommonAddress.ArbitrumOne.USDC) + }) + + it('should return native USDC address on Arbitrum Sepolia when parent USDC address is native USDC on Sepolia, parent chain is Sepolia, and child chain is Arbitrum Sepolia', async () => { + const result = await getChildUsdcAddress({ + parentChainId: ChainId.Sepolia, + childChainId: ChainId.ArbitrumSepolia + }) + + expect(result).toEqual(CommonAddress.ArbitrumSepolia.USDC) + }) + + it('should return USDC address on Xai Testnet when parent USDC address is native USDC on Arbitrum Sepolia, parent chain is Arbitrum Sepolia, and child chain is Xai Testnet', async () => { + const mockedGetL2ERC20Address = jest + .mocked(getL2ERC20Address) + .mockResolvedValueOnce('0xBd8C9bFBB225bFF89C7884060338150dAA626Edb') + + const result = await getChildUsdcAddress({ + parentChainId: ChainId.ArbitrumSepolia, + childChainId: xaiTestnetChainId + }) + + expect(result).toEqual('0xBd8C9bFBB225bFF89C7884060338150dAA626Edb') + expect(mockedGetL2ERC20Address).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/arb-token-bridge-ui/src/util/networks.ts b/packages/arb-token-bridge-ui/src/util/networks.ts index bb3fa0dbaa..0512022568 100644 --- a/packages/arb-token-bridge-ui/src/util/networks.ts +++ b/packages/arb-token-bridge-ui/src/util/networks.ts @@ -1,4 +1,4 @@ -import { Provider, StaticJsonRpcProvider } from '@ethersproject/providers' +import { StaticJsonRpcProvider } from '@ethersproject/providers' import { ArbitrumNetwork, getChildrenForNetwork,