diff --git a/.github/workflows/core-batch-poster-monitor.yml b/.github/workflows/core-batch-poster-monitor.yml index 033362f2b9..bdd0b6f876 100644 --- a/.github/workflows/core-batch-poster-monitor.yml +++ b/.github/workflows/core-batch-poster-monitor.yml @@ -23,11 +23,8 @@ jobs: with: repository: OffchainLabs/arbitrum-token-bridge - - name: Restore node_modules - uses: OffchainLabs/actions/node-modules/restore@main - - - name: Install dependencies - run: yarn install + - name: Install node_modules + uses: OffchainLabs/actions/node-modules/install@main - name: Generate chains JSON run: yarn workspace arb-token-bridge-ui generateCoreChainsToMonitor diff --git a/.github/workflows/orbit-batch-poster-monitor.yml b/.github/workflows/orbit-batch-poster-monitor.yml index 10c1c5565b..e72bbed120 100644 --- a/.github/workflows/orbit-batch-poster-monitor.yml +++ b/.github/workflows/orbit-batch-poster-monitor.yml @@ -21,11 +21,8 @@ jobs: with: repository: OffchainLabs/arbitrum-token-bridge - - name: Restore node_modules - uses: OffchainLabs/actions/node-modules/restore@main - - - name: Install dependencies - run: yarn install + - name: Install node_modules + uses: OffchainLabs/actions/node-modules/install@main - name: Generate chains JSON run: yarn workspace arb-token-bridge-ui generateOrbitChainsToMonitor diff --git a/packages/arb-token-bridge-ui/package.json b/packages/arb-token-bridge-ui/package.json index 57678dd84d..28a4f5ea79 100644 --- a/packages/arb-token-bridge-ui/package.json +++ b/packages/arb-token-bridge-ui/package.json @@ -10,7 +10,7 @@ "@headlessui/react": "^1.7.8", "@headlessui/tailwindcss": "^0.1.2", "@heroicons/react": "^2.0.18", - "@offchainlabs/cobalt": "^0.3.6", + "@offchainlabs/cobalt": "0.3.7", "@rainbow-me/rainbowkit": "^0.12.16", "@rehooks/local-storage": "^2.4.4", "@sentry/react": "^7.73.0", diff --git a/packages/arb-token-bridge-ui/src/components/App/WelcomeDialog.tsx b/packages/arb-token-bridge-ui/src/components/App/WelcomeDialog.tsx index c49607e85a..61a3c65f85 100644 --- a/packages/arb-token-bridge-ui/src/components/App/WelcomeDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/App/WelcomeDialog.tsx @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/react' import { useConnectModal } from '@rainbow-me/rainbowkit' import { useCallback } from 'react' import { useLocalStorage } from '@uidotdev/usehooks' @@ -7,6 +6,7 @@ import { ExternalLink } from '../common/ExternalLink' import { errorToast } from '../common/atoms/Toast' import { TOS_LOCALSTORAGE_KEY } from '../../constants' import { Button } from '../common/Button' +import { captureSentryErrorWithExtraData } from '../../util/SentryUtils' export function WelcomeDialog() { const [, setTosAccepted] = useLocalStorage( @@ -23,7 +23,10 @@ export function WelcomeDialog() { openConnectModal?.() } catch (error) { errorToast('Failed to open up RainbowKit Connect Modal') - Sentry.captureException(error) + captureSentryErrorWithExtraData({ + error, + originFunction: 'WelcomeDialog closeHandler' + }) } }, [openConnectModal, setTosAccepted]) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/NetworkListbox.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/NetworkListbox.tsx deleted file mode 100644 index b2ebf56d86..0000000000 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/NetworkListbox.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { Listbox } from '@headlessui/react' -import { ChevronDownIcon } from '@heroicons/react/24/outline' -import { twMerge } from 'tailwind-merge' -import { Chain } from 'wagmi' - -import { getNetworkName } from '../../util/networks' -import { getBridgeUiConfigForChain } from '../../util/bridgeUiConfig' -import { Transition } from '../common/Transition' -import { NetworkImage } from '../common/NetworkImage' - -export type NetworkListboxProps = { - disabled?: boolean - label: string - options: Chain[] - value: Chain - onChange: (value: Chain) => void -} - -export function NetworkListbox({ - disabled = false, - label, - options, - value, - onChange -}: NetworkListboxProps) { - const { color: backgroundColor } = getBridgeUiConfigForChain(value.id) - - return ( - - {({ open }) => ( - <> - - - {label} {getNetworkName(value.id)} - - {!disabled && ( - - )} - - - - - {options.map(option => { - return ( - -
- -
- - {getNetworkName(option.id)} - -
- ) - })} -
-
- - )} -
- ) -} diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/OneNovaTransferDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/OneNovaTransferDialog.tsx index ceaf080e43..44a8c540d5 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/OneNovaTransferDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/OneNovaTransferDialog.tsx @@ -7,34 +7,65 @@ import { BridgesTable } from '../common/BridgesTable' import { SecurityNotGuaranteed } from './SecurityLabels' import { Dialog, UseDialogProps } from '../common/Dialog' import { FastBridgeInfo, FastBridgeNames } from '../../util/fastBridges' -import { ChainId, getNetworkName } from '../../util/networks' +import { ChainId, getNetworkName, isNetwork } from '../../util/networks' import { ether } from '../../constants' +import { useArbQueryParams } from '../../hooks/useArbQueryParams' +import { useNetworks } from '../../hooks/useNetworks' -export function OneNovaTransferDialog( - props: UseDialogProps & { - destinationChainId: number | null - amount: string +/** + * On the UI, user can select the pair Arbitrum One/Arbitrum Nova with the network selection dropdowns. + * However, they are not valid pairs for transfer, so the latest selected chain will not be set as query param + * and useNetworks will not save it. + * + * This function will use the currently selected chain in the source & destination chain pair to determine + * which chain user has selected (but not stored in the query params or useNetworks). + */ +function getDialogSourceAndDestinationChains({ + sourceChainId, + destinationChainId +}: { + sourceChainId: ChainId + destinationChainId: ChainId +}) { + const { isArbitrumNova: isSourceChainNova } = isNetwork(sourceChainId) + const { isArbitrumOne: isDestinationChainArbOne } = + isNetwork(destinationChainId) + + if (isSourceChainNova || isDestinationChainArbOne) { + return { + selectedSourceChainId: ChainId.ArbitrumNova, + selectedDestinationChainId: ChainId.ArbitrumOne + } + } + // if source chain is Arbitrum One or + // if destination chain is Arbitrum Nova + return { + selectedSourceChainId: ChainId.ArbitrumOne, + selectedDestinationChainId: ChainId.ArbitrumNova } -) { +} + +export function OneNovaTransferDialog(props: UseDialogProps) { const { app: { selectedToken } } = useAppState() + const [{ amount }] = useArbQueryParams() + const [{ sourceChain, destinationChain }] = useNetworks() - const { destinationChainId } = props - - const sourceChainId = - destinationChainId === ChainId.ArbitrumNova - ? ChainId.ArbitrumOne - : ChainId.ArbitrumNova + const { selectedSourceChainId, selectedDestinationChainId } = + getDialogSourceAndDestinationChains({ + sourceChainId: sourceChain.id, + destinationChainId: destinationChain.id + }) const sourceNetworkSlug = - sourceChainId === ChainId.ArbitrumOne ? 'arbitrum' : 'nova' + selectedSourceChainId === ChainId.ArbitrumOne ? 'arbitrum' : 'nova' const destinationNetworkSlug = - destinationChainId === ChainId.ArbitrumOne ? 'arbitrum' : 'nova' + selectedDestinationChainId === ChainId.ArbitrumOne ? 'arbitrum' : 'nova' const bridgeDeepLink = `https://app.hop.exchange/#/send?sourceNetwork=${sourceNetworkSlug}&destNetwork=${destinationNetworkSlug}&token=${ selectedToken?.symbol || ether.symbol - }&amount=${props.amount}` + }&amount=${amount}` // only enable Hop for now const fastBridgeList: FastBridgeInfo[] = [ @@ -46,8 +77,8 @@ export function OneNovaTransferDialog( {...props} onClose={() => props.onClose(false)} title={`Move funds from ${getNetworkName( - sourceChainId - )} to ${getNetworkName(destinationChainId ?? 0)}`} + selectedSourceChainId + )} to ${getNetworkName(selectedDestinationChainId)}`} actionButtonProps={{ hidden: true }} className="max-w-[700px]" > 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 3aa216762a..640649a7ae 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx @@ -500,6 +500,7 @@ function TokensPanel({ onSubmit={addNewToken} SearchInputButton={AddButton} dataCy="tokenSearchList" + isDialog={false} > {({ height, width }) => ( 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 8d8b50e1fe..698d4d0222 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -3,7 +3,6 @@ import { useState, useMemo } from 'react' import Tippy from '@tippyjs/react' import { constants, utils } from 'ethers' import { useLatest } from 'react-use' -import * as Sentry from '@sentry/react' import { useAccount, useChainId, useSigner } from 'wagmi' import { TransactionResponse } from '@ethersproject/providers' import { twMerge } from 'tailwind-merge' @@ -75,6 +74,7 @@ import { getBridgeTransferProperties } from '../../token-bridge-sdk/utils' import { useSetInputAmount } from '../../hooks/TransferPanel/useSetInputAmount' import { getSmartContractWalletTeleportTransfersNotSupportedErrorMessage } from './useTransferReadinessUtils' import { useBalances } from '../../hooks/useBalances' +import { captureSentryErrorWithExtraData } from '../../util/SentryUtils' const networkConnectionWarningToast = () => warningToast( @@ -374,8 +374,11 @@ export function TransferPanel() { const switchTargetChainId = latestNetworks.current.sourceChain.id try { await switchNetworkAsync?.(switchTargetChainId) - } catch (e) { - Sentry.captureException(e) + } catch (error) { + captureSentryErrorWithExtraData({ + error, + originFunction: 'transferCctp switchNetworkAsync' + }) } } @@ -439,7 +442,10 @@ export function TransferPanel() { if (isUserRejectedError(error)) { return } - Sentry.captureException(error) + captureSentryErrorWithExtraData({ + error, + originFunction: 'cctpTransferStarter.approveToken' + }) errorToast( `USDC approval transaction failed: ${ (error as Error)?.message ?? error @@ -465,7 +471,10 @@ export function TransferPanel() { if (isUserRejectedError(error)) { return } - Sentry.captureException(error) + captureSentryErrorWithExtraData({ + error, + originFunction: 'cctpTransferStarter.transfer' + }) errorToast( `USDC ${ isDepositMode ? 'Deposit' : 'Withdrawal' @@ -861,8 +870,17 @@ export function TransferPanel() { // transaction submitted callback onTxSubmit(transfer) - } catch (ex) { - Sentry.captureException(ex) + } catch (error) { + captureSentryErrorWithExtraData({ + error, + originFunction: 'bridgeTransferStarter.transfer', + additionalData: selectedToken + ? { + erc20_address_on_parent_chain: selectedToken.address, + transfer_type: 'token' + } + : { transfer_type: 'native currency' } + }) } finally { setTransferring(false) } 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 36d1113526..80194a4568 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react' +import React, { useEffect, useMemo } from 'react' import { ArrowsUpDownIcon, ArrowDownIcon } from '@heroicons/react/24/outline' import { twMerge } from 'tailwind-merge' import { BigNumber, utils } from 'ethers' @@ -6,18 +6,11 @@ import { Chain, useAccount } from 'wagmi' import { useMedia } from 'react-use' import { Loader } from '../common/atoms/Loader' -import { useActions, useAppState } from '../../state' +import { useAppState } from '../../state' import { formatAmount } from '../../util/NumberUtils' -import { - ChainId, - getExplorerUrl, - getDestinationChainIds, - isNetwork -} from '../../util/networks' -import { getWagmiChain } from '../../util/wagmi/getWagmiChain' +import { getExplorerUrl, isNetwork } from '../../util/networks' import { useDestinationAddressStore } from './AdvancedSettings' import { ExternalLink } from '../common/ExternalLink' -import { useDialog } from '../common/Dialog' import { useAccountType } from '../../hooks/useAccountType' import { @@ -27,8 +20,6 @@ import { isTokenMainnetUSDC } from '../../util/TokenUtils' import { ether } from '../../constants' -import { NetworkListboxProps } from './NetworkListbox' -import { OneNovaTransferDialog } from './OneNovaTransferDialog' import { useUpdateUSDCBalances } from '../../hooks/CCTP/useUpdateUSDCBalances' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { TransferReadinessRichErrorMessage } from './useTransferReadinessUtils' @@ -266,13 +257,10 @@ export function TransferPanelMain({ amount: string errorMessage?: TransferReadinessRichErrorMessage | string }) { - const actions = useActions() - const [networks, setNetworks] = useNetworks() + const [networks] = useNetworks() const { childChain, childChainProvider, isTeleportMode } = useNetworksRelationship(networks) - const { isSmartContractWallet, isLoading: isLoadingAccountType } = - useAccountType() const { isArbitrumOne, isArbitrumSepolia } = isNetwork(childChain.id) const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) @@ -351,12 +339,6 @@ export function TransferPanelMain({ } }, [nativeCurrency, ethParentBalance, ethChildBalance, erc20ParentBalances]) - const [oneNovaTransferDialogProps, openOneNovaTransferDialog] = useDialog() - const [ - oneNovaTransferDestinationNetworkId, - setOneNovaTransferDestinationNetworkId - ] = useState(null) - const showUSDCSpecificInfo = !isTeleportMode && ((isTokenMainnetUSDC(selectedToken?.address) && isArbitrumOne) || @@ -371,74 +353,6 @@ export function TransferPanelMain({ useUpdateUSDCTokenData() - type NetworkListboxesProps = { - to: Omit - } - - const networkListboxProps: NetworkListboxesProps = useMemo(() => { - function getDestinationChains() { - const destinationChainIds = getDestinationChainIds( - networks.sourceChain.id - ) - - // if source chain is Arbitrum One, add Arbitrum Nova to destination - if (networks.sourceChain.id === ChainId.ArbitrumOne) { - destinationChainIds.push(ChainId.ArbitrumNova) - } - - // if source chain is Arbitrum Nova, add Arbitrum One to destination - if (networks.sourceChain.id === ChainId.ArbitrumNova) { - destinationChainIds.push(ChainId.ArbitrumOne) - } - - return ( - destinationChainIds - // remove self - .filter(chainId => chainId !== networks.destinationChain.id) - .map(getWagmiChain) - ) - } - - function shouldOpenOneNovaDialog(selectedChainIds: number[]) { - return [ChainId.ArbitrumOne, ChainId.ArbitrumNova].every(chainId => - selectedChainIds.includes(chainId) - ) - } - - const destinationChains = getDestinationChains() - - return { - to: { - disabled: - isSmartContractWallet || - isLoadingAccountType || - destinationChains.length === 0, - options: destinationChains, - value: networks.destinationChain, - onChange: async network => { - if (shouldOpenOneNovaDialog([network.id, networks.sourceChain.id])) { - setOneNovaTransferDestinationNetworkId(network.id) - openOneNovaTransferDialog() - return - } - - setNetworks({ - sourceChainId: networks.sourceChain.id, - destinationChainId: network.id - }) - actions.app.setSelectedToken(null) - } - } - } - }, [ - isSmartContractWallet, - isLoadingAccountType, - networks.sourceChain, - networks.destinationChain, - setNetworks, - openOneNovaTransferDialog - ]) - return (
-
) } diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx index 6e5f1a459e..8438f49b35 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx @@ -1,8 +1,8 @@ import { constants, utils } from 'ethers' +import { useAccount } from 'wagmi' import { useNetworks } from '../../../hooks/useNetworks' import { useDestinationAddressStore } from '../AdvancedSettings' -import { NetworkListbox, NetworkListboxProps } from '../NetworkListbox' import { BalancesContainer, ETHBalance, @@ -10,7 +10,6 @@ import { NetworkListboxPlusBalancesContainer } from '../TransferPanelMain' import { TokenBalance } from './TokenBalance' -import { useAccount } from 'wagmi' import { useNetworksRelationship } from '../../../hooks/useNetworksRelationship' import { NetworkType } from './utils' import { useAppState } from '../../../state' @@ -24,15 +23,18 @@ import { useSelectedTokenBalances } from '../../../hooks/TransferPanel/useSelectedTokenBalances' import { useNativeCurrency } from '../../../hooks/useNativeCurrency' +import { useDialog } from '../../common/Dialog' +import { + NetworkButton, + NetworkSelectionContainer +} from '../../common/NetworkSelectionContainer' export function DestinationNetworkBox({ customFeeTokenBalances, - showUsdcSpecificInfo, - destinationNetworkListboxProps + showUsdcSpecificInfo }: { customFeeTokenBalances: Balances showUsdcSpecificInfo: boolean - destinationNetworkListboxProps: Omit }) { const { address: walletAddress } = useAccount() const [networks] = useNetworks() @@ -48,101 +50,116 @@ export function DestinationNetworkBox({ const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) const { destinationAddress } = useDestinationAddressStore() const destinationAddressOrWalletAddress = destinationAddress || walletAddress + const [ + destinationNetworkSelectionDialogProps, + openDestinationNetworkSelectionDialog + ] = useDialog() return ( - - - - - {destinationAddressOrWalletAddress && - utils.isAddress(destinationAddressOrWalletAddress) && ( - <> - - {/* In deposit mode, when user selected USDC on mainnet, - the UI shows the Arb One balance of both USDC.e and native USDC */} - {isDepositMode && showUsdcSpecificInfo && ( + <> + + + + + {destinationAddressOrWalletAddress && + utils.isAddress(destinationAddressOrWalletAddress) && ( + <> - )} - {nativeCurrency.isCustom ? ( - <> + {/* In deposit mode, when user selected USDC on mainnet, + the UI shows the Arb One balance of both USDC.e and native USDC */} + {isDepositMode && showUsdcSpecificInfo && ( + )} + {nativeCurrency.isCustom ? ( + <> + + {!isDepositMode && ( + + )} + + ) : ( + - {!isDepositMode && ( - - )} - - ) : ( - - )} - - )} - - - - + )} + + )} + + + + + + ) } diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx index f7c6bce116..b892b0e97a 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx @@ -1,9 +1,10 @@ -import { twMerge } from 'tailwind-merge' -import { Chain } from 'wagmi' import { useCallback, useEffect } from 'react' import { getNetworkName } from '../../../util/networks' -import { NetworkSelectionContainer } from '../../common/NetworkSelectionContainer' +import { + NetworkButton, + NetworkSelectionContainer +} from '../../common/NetworkSelectionContainer' import { BalancesContainer, ETHBalance, @@ -12,7 +13,7 @@ import { } from '../TransferPanelMain' import { TokenBalance } from './TokenBalance' import { NetworkType } from './utils' -import { useActions, useAppState } from '../../../state' +import { useAppState } from '../../../state' import { useNetworks } from '../../../hooks/useNetworks' import { useNativeCurrency } from '../../../hooks/useNativeCurrency' import { useNetworksRelationship } from '../../../hooks/useNetworksRelationship' @@ -28,11 +29,11 @@ import { import { ExternalLink } from '../../common/ExternalLink' import { EstimatedGas } from '../EstimatedGas' import { TransferPanelMainInput } from '../TransferPanelMainInput' -import { getBridgeUiConfigForChain } from '../../../util/bridgeUiConfig' import { AmountQueryParamEnum } from '../../../hooks/useArbQueryParams' import { TransferReadinessRichErrorMessage } from '../useTransferReadinessUtils' import { useMaxAmount } from './useMaxAmount' import { useSetInputAmount } from '../../../hooks/TransferPanel/useSetInputAmount' +import { useDialog } from '../../common/Dialog' export function SourceNetworkBox({ amount, @@ -45,8 +46,7 @@ export function SourceNetworkBox({ customFeeTokenBalances: Balances showUsdcSpecificInfo: boolean }) { - const actions = useActions() - const [networks, setNetworks] = useNetworks() + const [networks] = useNetworks() const { childChain, childChainProvider, isDepositMode } = useNetworksRelationship(networks) const { @@ -59,6 +59,8 @@ export function SourceNetworkBox({ const { maxAmount } = useMaxAmount({ customFeeTokenBalances }) + const [sourceNetworkSelectionDialogProps, openSourceNetworkSelectionDialog] = + useDialog() const isMaxAmount = amount === AmountQueryParamEnum.MAX @@ -77,137 +79,107 @@ export function SourceNetworkBox({ } }, [maxAmount, setAmount]) - const buttonStyle = { - backgroundColor: getBridgeUiConfigForChain(networks.sourceChain.id).color - } - - const onChange = useCallback( - (network: Chain) => { - if (networks.destinationChain.id === network.id) { - setNetworks({ - sourceChainId: networks.destinationChain.id, - destinationChainId: networks.sourceChain.id - }) - return - } - - // if changing sourceChainId, let the destinationId be the same, and let the `setNetworks` func decide whether it's a valid or invalid chain pair - // this way, the destination doesn't reset to the default chain if the source chain is changed, and if both are valid - setNetworks({ - sourceChainId: network.id, - destinationChainId: networks.destinationChain.id - }) - - actions.app.setSelectedToken(null) - }, - [ - actions.app, - networks.destinationChain.id, - networks.sourceChain.id, - setNetworks - ] - ) - return ( - - - - - From: {getNetworkName(networks.sourceChain.id)} - - - - + + + - {nativeCurrency.isCustom ? ( - <> - + + {nativeCurrency.isCustom ? ( + <> + + {/* Only show ETH balance on parent chain */} + {isDepositMode && ( + + )} + + ) : ( + - {/* Only show ETH balance on parent chain */} - {isDepositMode && ( - - )} - - ) : ( - - )} - - + )} + + -
- +
+ - {showUsdcSpecificInfo && ( -

- Bridged USDC (USDC.e) will work but is different from Native USDC.{' '} - - Learn more - - . -

- )} + {showUsdcSpecificInfo && ( +

+ Bridged USDC (USDC.e) will work but is different from Native USDC.{' '} + + Learn more + + . +

+ )} - {isDepositMode && selectedToken && ( -

- Make sure you have {nativeCurrency.symbol} in your{' '} - {getNetworkName(childChain.id)} account, as you’ll need it to power - transactions. -
- - Learn more - - . -

- )} -
- - + {isDepositMode && selectedToken && ( +

+ Make sure you have {nativeCurrency.symbol} in your{' '} + {getNetworkName(childChain.id)} account, as you’ll need it to + power transactions. +
+ + Learn more + + . +

+ )} +
+ +
+ + ) } diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/utils.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/utils.ts index cff9adbb21..930bab1453 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/utils.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/utils.ts @@ -1,4 +1,12 @@ +import { ChainId } from '../../../util/networks' + export enum NetworkType { parentChain = 'parentChain', childChain = 'childChain' } + +export function shouldOpenOneNovaDialog(selectedChainIds: number[]) { + return [ChainId.ArbitrumOne, ChainId.ArbitrumNova].every(chainId => + selectedChainIds.includes(chainId) + ) +} diff --git a/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx b/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx index 2ba168c790..95994fa261 100644 --- a/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx @@ -1,4 +1,3 @@ -import { Popover } from '@headlessui/react' import { CSSProperties, useCallback, @@ -9,28 +8,29 @@ import { } from 'react' import { Chain } from 'wagmi' import { useDebounce } from '@uidotdev/usehooks' -import { ShieldExclamationIcon } from '@heroicons/react/24/outline' +import { + ChevronDownIcon, + ShieldExclamationIcon +} from '@heroicons/react/24/outline' import { twMerge } from 'tailwind-merge' import { AutoSizer, List, ListRowProps } from 'react-virtualized' -import { ChevronDownIcon } from '@heroicons/react/24/outline' -import { ChainId, getSupportedChainIds, isNetwork } from '../../util/networks' -import { useAccountType } from '../../hooks/useAccountType' +import { ChainId, isNetwork, getNetworkName } from '../../util/networks' import { useIsTestnetMode } from '../../hooks/useIsTestnetMode' import { SearchPanel } from './SearchPanel/SearchPanel' import { SearchPanelTable } from './SearchPanel/SearchPanelTable' import { TestnetToggle } from './TestnetToggle' import { useArbQueryParams } from '../../hooks/useArbQueryParams' -import { - panelWrapperClassnames, - onPopoverButtonClick, - onPopoverClose -} from './SearchPanel/SearchPanelUtils' import { getBridgeUiConfigForChain } from '../../util/bridgeUiConfig' import { getWagmiChain } from '../../util/wagmi/getWagmiChain' -import { useNetworks } from '../../hooks/useNetworks' -import { Transition } from './Transition' import { NetworkImage } from './NetworkImage' +import { Dialog, UseDialogProps, useDialog } from './Dialog' +import { useNetworks } from '../../hooks/useNetworks' +import { OneNovaTransferDialog } from '../TransferPanel/OneNovaTransferDialog' +import { shouldOpenOneNovaDialog } from '../TransferPanel/TransferPanelMain/utils' +import { useActions } from '../../state' +import { useChainIdsForNetworkSelection } from '../../hooks/TransferPanel/useChainIdsForNetworkSelection' +import { useAccountType } from '../../hooks/useAccountType' type NetworkType = 'core' | 'orbit' @@ -89,20 +89,62 @@ function ChainTypeInfoRow({ ) } +export function NetworkButton({ + type, + onClick +}: { + type: 'source' | 'destination' + onClick: () => void +}) { + const [networks] = useNetworks() + const { isSmartContractWallet, isLoading } = useAccountType() + const isSource = type === 'source' + const chains = useChainIdsForNetworkSelection({ isSource }) + + const selectedChainId = isSource + ? networks.sourceChain.id + : networks.destinationChain.id + + const hasOneOrLessChain = chains.length <= 1 + + const disabled = hasOneOrLessChain || isSmartContractWallet || isLoading + + const buttonStyle = { + backgroundColor: getBridgeUiConfigForChain(selectedChainId).color + } + + return ( + + ) +} + function NetworkRow({ chainId, + isSelected, style, onClick, close }: { chainId: ChainId + isSelected: boolean style: CSSProperties onClick: (value: Chain) => void close: (focusableElement?: HTMLElement) => void }) { const { network, nativeTokenData } = getBridgeUiConfigForChain(chainId) const chain = getWagmiChain(chainId) - const [{ sourceChain }] = useNetworks() function handleClick() { onClick(chain) @@ -118,7 +160,7 @@ function NetworkRow({ aria-label={`Switch to ${network.name}`} className={twMerge( 'flex h-[90px] w-full items-center gap-4 px-4 py-2 text-lg transition-[background] duration-200 hover:bg-white/10', - chainId === sourceChain.id && 'bg-white/10' // selected row + isSelected && 'bg-white/10' // selected row )} > void +}) { const [, setQueryParams] = useArbQueryParams() const [isTestnetMode] = useIsTestnetMode() - const openSettingsPanel = () => setQueryParams({ settingsOpen: true }) + const openSettingsPanel = () => { + setQueryParams({ settingsOpen: true }) + closeDialog() + } if (!isTestnetMode) { return null @@ -159,9 +208,13 @@ function AddCustomOrbitChainButton() { } function NetworksPanel({ + chainIds, + selectedChainId, onNetworkRowClick, close }: { + chainIds: ChainId[] + selectedChainId: ChainId onNetworkRowClick: (value: Chain) => void close: (focusableElement?: HTMLElement) => void }) { @@ -171,15 +224,6 @@ function NetworksPanel({ const listRef = useRef(null) const [isTestnetMode] = useIsTestnetMode() - const chainIds = useMemo( - () => - getSupportedChainIds({ - includeMainnets: !isTestnetMode, - includeTestnets: isTestnetMode - }), - [isTestnetMode] - ) - const networksToShow = useMemo(() => { const _networkSearched = debouncedNetworkSearched.trim().toLowerCase() @@ -206,17 +250,24 @@ function NetworksPanel({ const isNetworkSearchResult = Array.isArray(networksToShow) - const networkRowsWithChainInfoRows = useMemo(() => { - if (isNetworkSearchResult) { - return networksToShow - } - return [ - ChainGroupName.core, - ...networksToShow.core, - ChainGroupName.orbit, - ...networksToShow.orbit - ] - }, [isNetworkSearchResult, networksToShow]) + const networkRowsWithChainInfoRows: (ChainId | ChainGroupName)[] = + useMemo(() => { + if (isNetworkSearchResult) { + return networksToShow + } + + const groupedNetworks = [] + + if (networksToShow.core.length > 0) { + groupedNetworks.push(ChainGroupName.core, ...networksToShow.core) + } + + if (networksToShow.orbit.length > 0) { + groupedNetworks.push(ChainGroupName.orbit, ...networksToShow.orbit) + } + + return groupedNetworks + }, [isNetworkSearchResult, networksToShow]) function getRowHeight({ index }: { index: number }) { const rowItemOrChainId = networkRowsWithChainInfoRows[index] @@ -228,7 +279,7 @@ function NetworksPanel({ } const rowItem = getBridgeUiConfigForChain(rowItemOrChainId) if (rowItem.network.description) { - return 90 + return 95 } return 60 } @@ -262,12 +313,13 @@ function NetworksPanel({ key={networkOrChainTypeName} style={style} chainId={networkOrChainTypeName} + isSelected={networkOrChainTypeName === selectedChainId} onClick={onNetworkRowClick} close={close} /> ) }, - [close, networkRowsWithChainInfoRows, onNetworkRowClick] + [close, networkRowsWithChainInfoRows, onNetworkRowClick, selectedChainId] ) const onSearchInputChange = useCallback( @@ -285,6 +337,7 @@ function NetworksPanel({ searchInputValue={networkSearched} searchInputOnChange={onSearchInputChange} errorMessage={errorMessage} + isDialog > {({ height, width }) => ( @@ -302,77 +355,82 @@ function NetworksPanel({
- +
) } -export const NetworkSelectionContainer = ({ - children, - buttonClassName, - buttonStyle, - onChange -}: { - children: React.ReactNode - buttonClassName: string - buttonStyle?: CSSProperties - onChange: (value: Chain) => void -}) => { - const { isSmartContractWallet, isLoading: isLoadingAccountType } = - useAccountType() +export const NetworkSelectionContainer = ( + props: UseDialogProps & { + type: 'source' | 'destination' + } +) => { + const actions = useActions() + const [networks, setNetworks] = useNetworks() + const [oneNovaTransferDialogProps, openOneNovaTransferDialog] = useDialog() + + const isSource = props.type === 'source' + + const selectedChainId = isSource + ? networks.sourceChain.id + : networks.destinationChain.id + + const supportedChainIds = useChainIdsForNetworkSelection({ + isSource + }) + + const onNetworkRowClick = useCallback( + (value: Chain) => { + const pairedChain = isSource ? 'destinationChain' : 'sourceChain' + + if (shouldOpenOneNovaDialog([value.id, networks[pairedChain].id])) { + openOneNovaTransferDialog() + return + } + + if (networks[pairedChain].id === value.id) { + setNetworks({ + sourceChainId: networks.destinationChain.id, + destinationChainId: networks.sourceChain.id + }) + return + } + + // if changing sourceChainId, let the destinationId be the same, and let the `setNetworks` func decide whether it's a valid or invalid chain pair + // this way, the destination doesn't reset to the default chain if the source chain is changed, and if both are valid + setNetworks({ + sourceChainId: isSource ? value.id : networks.sourceChain.id, + destinationChainId: isSource ? networks.destinationChain.id : value.id + }) + + actions.app.setSelectedToken(null) + }, + [actions.app, isSource, networks, openOneNovaTransferDialog, setNetworks] + ) return ( - - {({ open }) => ( - <> - - {children} - {!isSmartContractWallet && ( - - )} - - - - - {({ close }) => { - function onClose() { - onPopoverClose() - close() - } - return ( - - - - - - - - - ) - }} - - - - )} - + <> + props.onClose(false)} + title={`Select ${isSource ? 'Source' : 'Destination'} Network`} + actionButtonProps={{ hidden: true }} + isFooterHidden={true} + className="h-screen overflow-hidden md:h-[calc(100vh_-_200px)] md:max-h-[900px] md:max-w-[500px]" + > + + + props.onClose(false)} + onNetworkRowClick={onNetworkRowClick} + /> + + + + + ) } diff --git a/packages/arb-token-bridge-ui/src/components/common/SearchPanel/SearchPanelTable.tsx b/packages/arb-token-bridge-ui/src/components/common/SearchPanel/SearchPanelTable.tsx index 73d1672c06..2c10587742 100644 --- a/packages/arb-token-bridge-ui/src/components/common/SearchPanel/SearchPanelTable.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/SearchPanel/SearchPanelTable.tsx @@ -1,5 +1,6 @@ import { MagnifyingGlassIcon } from '@heroicons/react/24/outline' import React, { PropsWithChildren } from 'react' +import { twMerge } from 'tailwind-merge' type SearchPanelTableProps = { searchInputPlaceholder: string @@ -9,6 +10,7 @@ type SearchPanelTableProps = { onSubmit?: React.FormEventHandler errorMessage: string dataCy?: string + isDialog: boolean } export const SearchPanelTable = ({ @@ -21,10 +23,11 @@ export const SearchPanelTable = ({ }, errorMessage, children, - dataCy + dataCy, + isDialog }: PropsWithChildren) => { return ( -
+
@@ -44,7 +47,10 @@ export const SearchPanelTable = ({ )}
{children} diff --git a/packages/arb-token-bridge-ui/src/components/common/TokenSymbolWithExplorerLink.tsx b/packages/arb-token-bridge-ui/src/components/common/TokenSymbolWithExplorerLink.tsx index 17296864e4..ac91c1935c 100644 --- a/packages/arb-token-bridge-ui/src/components/common/TokenSymbolWithExplorerLink.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/TokenSymbolWithExplorerLink.tsx @@ -1,6 +1,4 @@ import { useMemo } from 'react' -import * as Sentry from '@sentry/react' - import { ERC20BridgeToken } from '../../hooks/arbTokenBridge.types' import { NativeCurrencyErc20, @@ -10,6 +8,7 @@ import { isTokenNativeUSDC, sanitizeTokenSymbol } from '../../util/TokenUtils' import { ExternalLink } from './ExternalLink' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' +import { captureSentryErrorWithExtraData } from '../../util/SentryUtils' const createBlockExplorerUrlForToken = ({ explorerLink, @@ -29,7 +28,10 @@ const createBlockExplorerUrlForToken = ({ url.pathname += `token/${tokenAddress}` return url.toString() } catch (error) { - Sentry.captureException(error) + captureSentryErrorWithExtraData({ + error, + originFunction: 'createBlockExplorerUrlForToken' + }) return undefined } } diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useChainIdsForNetworkSelection.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useChainIdsForNetworkSelection.ts new file mode 100644 index 0000000000..6d139ed254 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useChainIdsForNetworkSelection.ts @@ -0,0 +1,36 @@ +import { + ChainId, + getDestinationChainIds, + getSupportedChainIds +} from '../../util/networks' +import { useIsTestnetMode } from '../useIsTestnetMode' +import { useNetworks } from '../useNetworks' + +export function useChainIdsForNetworkSelection({ + isSource +}: { + isSource: boolean +}) { + const [networks] = useNetworks() + const [isTestnetMode] = useIsTestnetMode() + + if (isSource) { + return getSupportedChainIds({ + includeMainnets: !isTestnetMode, + includeTestnets: isTestnetMode + }) + } + + const destinationChainIds = getDestinationChainIds(networks.sourceChain.id) + + // if source chain is Arbitrum One, add Arbitrum Nova to destination + if (networks.sourceChain.id === ChainId.ArbitrumOne) { + destinationChainIds.push(ChainId.ArbitrumNova) + } + + if (networks.sourceChain.id === ChainId.ArbitrumNova) { + destinationChainIds.push(ChainId.ArbitrumOne) + } + + return destinationChainIds +} diff --git a/packages/arb-token-bridge-ui/src/hooks/useBalance.ts b/packages/arb-token-bridge-ui/src/hooks/useBalance.ts index af983fa092..6090aa9630 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useBalance.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useBalance.ts @@ -7,8 +7,8 @@ import useSWR, { SWRHook } from 'swr' import { MultiCaller } from '@arbitrum/sdk' -import * as Sentry from '@sentry/react' import { getProviderForChainId } from '@/token-bridge-sdk/utils' +import { captureSentryErrorWithExtraData } from '../util/SentryUtils' type Erc20Balances = { [address: string]: BigNumber | undefined @@ -90,10 +90,13 @@ const useBalance = ({ chainId, walletAddress }: UseBalanceProps) => { return acc }, {} as Erc20Balances) } catch (error) { - // log some extra info on sentry in case multi-caller fails - Sentry.configureScope(function (scope) { - scope.setExtra('token_addresses', addresses) - Sentry.captureException(error) + captureSentryErrorWithExtraData({ + error, + originFunction: 'useBalance fetchErc20', + additionalData: { + token_addresses: addresses.toString(), + chain: chainId.toString() + } }) return {} } diff --git a/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts b/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts index 475cc0b000..f174dc8fb3 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useClaimWithdrawal.ts @@ -1,5 +1,4 @@ import { useCallback, useState } from 'react' -import * as Sentry from '@sentry/react' import { useAccount, useSigner } from 'wagmi' import { useAppState } from '../state' @@ -15,6 +14,7 @@ import dayjs from 'dayjs' import { fetchErc20Data } from '../util/TokenUtils' import { fetchNativeCurrency } from './useNativeCurrency' import { getProviderForChainId } from '@/token-bridge-sdk/utils' +import { captureSentryErrorWithExtraData } from '../util/SentryUtils' export type UseClaimWithdrawalResult = { claim: () => Promise @@ -41,7 +41,7 @@ export function useClaimWithdrawal( return errorToast("Can't find withdrawal transaction.") } - let res, err + let res, err: any setIsClaiming(true) @@ -106,7 +106,10 @@ export function useClaimWithdrawal( return } - Sentry.captureException(err) + captureSentryErrorWithExtraData({ + error: err, + originFunction: 'useClaimWithdrawal claim' + }) if (!res) { errorToast(`Can't claim withdrawal: ${err?.message ?? err}`) } diff --git a/packages/arb-token-bridge-ui/src/hooks/useSwitchNetworkWithConfig.tsx b/packages/arb-token-bridge-ui/src/hooks/useSwitchNetworkWithConfig.tsx index 2641ff60ca..f482f886c6 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useSwitchNetworkWithConfig.tsx +++ b/packages/arb-token-bridge-ui/src/hooks/useSwitchNetworkWithConfig.tsx @@ -1,10 +1,10 @@ import { useSwitchNetwork } from 'wagmi' import { SwitchNetworkArgs } from '@wagmi/core' -import * as Sentry from '@sentry/react' import { getNetworkName, isNetwork } from '../util/networks' import { isUserRejectedError } from '../util/isUserRejectedError' import { warningToast } from '../components/common/atoms/Toast' +import { captureSentryErrorWithExtraData } from '../util/SentryUtils' type SwitchNetworkConfig = { isSwitchingNetworkBeforeTx?: boolean @@ -46,7 +46,10 @@ function handleSwitchNetworkError( if (error.name === 'SwitchChainNotSupportedError') { handleSwitchNetworkNotSupported(chainId, isSwitchingNetworkBeforeTx) } else { - Sentry.captureException(error) + captureSentryErrorWithExtraData({ + error, + originFunction: 'handleSwitchNetworkError' + }) } } diff --git a/packages/arb-token-bridge-ui/src/state/app/utils.ts b/packages/arb-token-bridge-ui/src/state/app/utils.ts index 1083b8cf97..ab48e5ca39 100644 --- a/packages/arb-token-bridge-ui/src/state/app/utils.ts +++ b/packages/arb-token-bridge-ui/src/state/app/utils.ts @@ -62,24 +62,18 @@ export const getDepositStatus = ( return DepositStatus.L1_PENDING } - // for teleport txn - if ( - isTeleport({ - sourceChainId: tx.parentChainId, // we make sourceChain=parentChain assumption coz it's a deposit txn - destinationChainId: tx.childChainId - }) - ) { - const { l2ToL3MsgData, l1ToL2MsgData } = tx as TeleporterMergedTransaction + if (isTeleporterTransaction(tx)) { + const { l2ToL3MsgData, parentToChildMsgData } = tx // if any of the retryable info is missing, first fetch might be pending - if (!l1ToL2MsgData || !l2ToL3MsgData) return DepositStatus.L2_PENDING + if (!parentToChildMsgData || !l2ToL3MsgData) return DepositStatus.L2_PENDING // if we find `l2ForwarderRetryableTxID` then this tx will need to be redeemed if (l2ToL3MsgData.l2ForwarderRetryableTxID) return DepositStatus.L2_FAILURE // if we find first retryable leg failing, then no need to check for the second leg const firstLegDepositStatus = getDepositStatusFromL1ToL2MessageStatus( - l1ToL2MsgData.status + parentToChildMsgData.status ) if (firstLegDepositStatus !== DepositStatus.L2_SUCCESS) { return firstLegDepositStatus @@ -91,11 +85,13 @@ export const getDepositStatus = ( if (typeof secondLegDepositStatus !== 'undefined') { return secondLegDepositStatus } - switch (l1ToL2MsgData.status) { + switch (parentToChildMsgData.status) { case ParentToChildMessageStatus.REDEEMED: return DepositStatus.L2_PENDING // tx is still pending if `l1ToL2MsgData` is redeemed (but l2ToL3MsgData is not) default: - return getDepositStatusFromL1ToL2MessageStatus(l1ToL2MsgData.status) + return getDepositStatusFromL1ToL2MessageStatus( + parentToChildMsgData.status + ) } } diff --git a/packages/arb-token-bridge-ui/src/state/cctpState.ts b/packages/arb-token-bridge-ui/src/state/cctpState.ts index 07e57ffbe8..d2c094dc3e 100644 --- a/packages/arb-token-bridge-ui/src/state/cctpState.ts +++ b/packages/arb-token-bridge-ui/src/state/cctpState.ts @@ -2,7 +2,6 @@ import { BigNumber } from 'ethers' import { useCallback, useEffect, useMemo, useState } from 'react' import { create } from 'zustand' import useSWRImmutable from 'swr/immutable' -import * as Sentry from '@sentry/react' import { useInterval } from 'react-use' import { getCctpUtils } from '@/token-bridge-sdk/cctp' @@ -29,6 +28,7 @@ import { useAccountType } from '../hooks/useAccountType' import { AssetType } from '../hooks/arbTokenBridge.types' import { useTransactionHistory } from '../hooks/useTransactionHistory' import { Address } from '../util/AddressUtils' +import { captureSentryErrorWithExtraData } from '../util/SentryUtils' // see https://developers.circle.com/stablecoin/docs/cctp-technical-reference#block-confirmations-for-attestations // Blocks need to be awaited on the L1 whether it's a deposit or a withdrawal @@ -569,9 +569,12 @@ export function useClaimCctp(tx: MergedTransaction) { if (receiveReceiptTx.status === 0) { throw new Error('Transaction failed') } - } catch (e) { - Sentry.captureException(e) - throw e + } catch (error) { + captureSentryErrorWithExtraData({ + error, + originFunction: 'useClaimCctp claim' + }) + throw error } finally { setIsClaiming(false) } diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20WithdrawalStarter.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20WithdrawalStarter.ts index 021f73abee..8ee9f51ac2 100644 --- a/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20WithdrawalStarter.ts +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20WithdrawalStarter.ts @@ -101,9 +101,17 @@ export class Erc20WithdrawalStarter extends BridgeTransferStarter { const sourceChainId = await getChainIdFromProvider(this.sourceChainProvider) + const destinationChainId = await getChainIdFromProvider( + this.destinationChainProvider + ) + // check first if token is even eligible for allowance check on l2 if ( - tokenRequiresApprovalOnL2(destinationChainErc20Address, sourceChainId) && + (await tokenRequiresApprovalOnL2({ + tokenAddressOnParentChain: destinationChainErc20Address, + parentChainId: destinationChainId, + childChainId: sourceChainId + })) && this.sourceChainErc20Address ) { const token = ERC20__factory.connect( diff --git a/packages/arb-token-bridge-ui/src/util/L2ApprovalUtils.ts b/packages/arb-token-bridge-ui/src/util/L2ApprovalUtils.ts index 41c0e97bd1..08db846f7e 100644 --- a/packages/arb-token-bridge-ui/src/util/L2ApprovalUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/L2ApprovalUtils.ts @@ -1,4 +1,5 @@ import { ChainId } from '../util/networks' +import { xErc20RequiresApprovalOnChildChain } from './xErc20Utils' export type RequireL2ApproveToken = { symbol: string @@ -66,11 +67,22 @@ const L2ApproveTokens: { [chainId: number]: RequireL2ApproveToken[] } = { ] } -export function tokenRequiresApprovalOnL2( - erc20L1Address: string, - l2ChainId: number +export type TokenWithdrawalApprovalParams = { + tokenAddressOnParentChain: string + parentChainId: ChainId + childChainId: ChainId +} + +export async function tokenRequiresApprovalOnL2( + params: TokenWithdrawalApprovalParams ) { - return (L2ApproveTokens[l2ChainId] ?? []) + if (await xErc20RequiresApprovalOnChildChain(params)) { + return true + } + + const { tokenAddressOnParentChain, childChainId } = params + + return (L2ApproveTokens[childChainId] ?? []) .map(token => token.l1Address.toLowerCase()) - .includes(erc20L1Address.toLowerCase()) + .includes(tokenAddressOnParentChain.toLowerCase()) } diff --git a/packages/arb-token-bridge-ui/src/util/SentryUtils.ts b/packages/arb-token-bridge-ui/src/util/SentryUtils.ts new file mode 100644 index 0000000000..cd3a635305 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/util/SentryUtils.ts @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/react' + +export function captureSentryErrorWithExtraData({ + error, + originFunction, + additionalData +}: { + error: unknown + originFunction: string + additionalData?: Record +}) { + Sentry.configureScope(function (scope) { + // tags only allow primitive values + scope.setTag('origin function', originFunction) + if (additionalData) { + scope.setTags(additionalData) + } + Sentry.captureException(error, () => scope) + }) +} diff --git a/packages/arb-token-bridge-ui/src/util/TokenDepositUtils.ts b/packages/arb-token-bridge-ui/src/util/TokenDepositUtils.ts index fe587f409d..c6ab9300f3 100644 --- a/packages/arb-token-bridge-ui/src/util/TokenDepositUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/TokenDepositUtils.ts @@ -2,7 +2,6 @@ import { Erc20Bridger, getArbitrumNetwork } from '@arbitrum/sdk' import { Inbox__factory } from '@arbitrum/sdk/dist/lib/abi/factories/Inbox__factory' import { Provider } from '@ethersproject/providers' import { BigNumber } from 'ethers' -import * as Sentry from '@sentry/react' import { fetchErc20Allowance, @@ -12,6 +11,7 @@ import { import { DepositGasEstimates } from '../hooks/arbTokenBridge.types' import { addressIsSmartContract } from './AddressUtils' import { getChainIdFromProvider } from '../token-bridge-sdk/utils' +import { captureSentryErrorWithExtraData } from './SentryUtils' async function fetchTokenFallbackGasEstimates({ inboxAddress, @@ -182,7 +182,10 @@ export async function depositTokenEstimateGas( estimatedChildChainSubmissionCost: retryableData.maxSubmissionCost } } catch (error) { - Sentry.captureException(error) + captureSentryErrorWithExtraData({ + error, + originFunction: 'depositTokenEstimateGas' + }) return fetchTokenFallbackGasEstimates({ inboxAddress: erc20Bridger.childNetwork.ethBridge.inbox, diff --git a/packages/arb-token-bridge-ui/src/util/TokenUtils.ts b/packages/arb-token-bridge-ui/src/util/TokenUtils.ts index 937f34dfbf..54a8af215b 100644 --- a/packages/arb-token-bridge-ui/src/util/TokenUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/TokenUtils.ts @@ -9,7 +9,6 @@ import { getArbitrumNetwork } from '@arbitrum/sdk' import { ERC20__factory } from '@arbitrum/sdk/dist/lib/abi/factories/ERC20__factory' -import * as Sentry from '@sentry/react' import { CommonAddress } from './CommonAddressUtils' import { ChainId, isNetwork } from './networks' @@ -20,6 +19,7 @@ import { getL2ConfigForTeleport, isTeleport } from '../token-bridge-sdk/teleport' +import { captureSentryErrorWithExtraData } from './SentryUtils' export function getDefaultTokenName(address: string) { const lowercased = address.toLowerCase() @@ -156,10 +156,13 @@ export async function fetchErc20Data({ return erc20Data } catch (error) { - // log some extra info on sentry in case multi-caller fails - Sentry.configureScope(function (scope) { - scope.setExtra('token_address', address) - Sentry.captureException(error) + captureSentryErrorWithExtraData({ + error, + originFunction: 'fetchErc20Data', + additionalData: { + token_address_on_this_chain: address, + chain: chainId.toString() + } }) throw error } @@ -208,10 +211,14 @@ export async function fetchErc20Allowance(params: FetchErc20AllowanceParams) { }) return tokenData?.allowance ?? constants.Zero } catch (error) { - // log the issue on sentry, later, fall back if there is no multicall - Sentry.configureScope(function (scope) { - scope.setExtra('token_address', address) - Sentry.captureException(error) + const chainId = await getChainIdFromProvider(provider) + captureSentryErrorWithExtraData({ + error, + originFunction: 'fetchErc20Allowance', + additionalData: { + token_address_on_this_chain: address, + chain: chainId.toString() + } }) throw error } diff --git a/packages/arb-token-bridge-ui/src/util/WithdrawalUtils.ts b/packages/arb-token-bridge-ui/src/util/WithdrawalUtils.ts index f4aa78e0a5..2bf9334593 100644 --- a/packages/arb-token-bridge-ui/src/util/WithdrawalUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/WithdrawalUtils.ts @@ -5,10 +5,10 @@ import { } from '@arbitrum/sdk' import { Provider } from '@ethersproject/providers' import { BigNumber } from 'ethers' -import * as Sentry from '@sentry/react' import { GasEstimates } from '../hooks/arbTokenBridge.types' import { Address } from './AddressUtils' +import { captureSentryErrorWithExtraData } from './SentryUtils' export async function withdrawInitTxEstimateGas({ amount, @@ -63,7 +63,16 @@ export async function withdrawInitTxEstimateGas({ estimatedChildChainGas } } catch (error) { - Sentry.captureException(error) + captureSentryErrorWithExtraData({ + error, + originFunction: 'withdrawInitTxEstimateGas', + additionalData: isToken + ? { + erc20_address_on_parent_chain: erc20L1Address, + withdrawal_type: 'token' + } + : { withdrawal_type: 'native currency' } + }) return { estimatedParentChainGas, diff --git a/packages/arb-token-bridge-ui/src/util/xErc20Utils.ts b/packages/arb-token-bridge-ui/src/util/xErc20Utils.ts new file mode 100644 index 0000000000..267763edaf --- /dev/null +++ b/packages/arb-token-bridge-ui/src/util/xErc20Utils.ts @@ -0,0 +1,42 @@ +import { getProviderForChainId } from '@/token-bridge-sdk/utils' +import { fetchErc20L2GatewayAddress } from './TokenUtils' +import { ChainId } from './networks' +import { TokenWithdrawalApprovalParams } from './L2ApprovalUtils' + +export const xErc20Gateways: { + [chainId: number]: { + parentChainId: ChainId + parentGateway: string + childGateway: string + } +} = { + [ChainId.ArbitrumSepolia]: { + parentChainId: ChainId.Sepolia, + parentGateway: '0x30BEc9c7C2d102aF63F23712bEAc69cdF013f062', + childGateway: '0x30BEc9c7C2d102aF63F23712bEAc69cdF013f062' + } +} + +export async function xErc20RequiresApprovalOnChildChain({ + tokenAddressOnParentChain, + parentChainId, + childChainId +}: TokenWithdrawalApprovalParams): Promise { + const gatewayData = xErc20Gateways[childChainId] + + if (gatewayData?.parentChainId !== parentChainId) { + return false + } + + const childChainProvider = getProviderForChainId(childChainId) + + const childChainGatewayAddress = await fetchErc20L2GatewayAddress({ + erc20L1Address: tokenAddressOnParentChain, + l2Provider: childChainProvider + }) + + return ( + childChainGatewayAddress.toLowerCase() === + gatewayData.childGateway.toLowerCase() + ) +} diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/approveToken.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/approveToken.cy.ts index 12c4923ac3..da3ed22763 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/approveToken.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/approveToken.cy.ts @@ -24,18 +24,18 @@ describe('Approve token and deposit afterwards', () => { // ERC-20 token should be selected now and popup should be closed after selection cy.findSelectTokenButton(ERC20TokenSymbol) - cy.findByText('MAX') - .click() - .then(() => { - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) - }) + cy.findByText('MAX').click() + + cy.findGasFeeSummary(zeroToLessThanOneETH) + cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + cy.waitUntil(() => cy.findMoveFundsButton().should('not.be.disabled'), { errorMsg: 'move funds button is disabled (expected to be enabled)', timeout: 50000, interval: 500 - }).then(() => cy.findMoveFundsButton().click()) + }) + cy.findMoveFundsButton().click() cy.findByText(/pay a one-time approval fee/).click() cy.findByRole('button', { name: /Pay approval fee of/ diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/depositCctp.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/depositCctp.cy.ts index 4e7680c053..70b6d28bf0 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/depositCctp.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/depositCctp.cy.ts @@ -4,7 +4,6 @@ import { zeroToLessThanOneETH } from '../../support/common' import { CommonAddress } from '../../../src/util/CommonAddressUtils' -import { shortenAddress } from '../../../src/util/CommonUtils' // common function for this cctp deposit const confirmAndApproveCctpDeposit = () => { @@ -88,14 +87,11 @@ describe('Deposit USDC through CCTP', () => { context('should show summary', () => { cy.typeAmount(USDCAmountToSend) - // - .then(() => { - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain('Sepolia', zeroToLessThanOneETH) - cy.findGasFeeForChain( - /You'll have to pay Arbitrum Sepolia gas fee upon claiming./i - ) - }) + cy.findGasFeeSummary(zeroToLessThanOneETH) + cy.findGasFeeForChain('Sepolia', zeroToLessThanOneETH) + cy.findGasFeeForChain( + /You'll have to pay Arbitrum Sepolia gas fee upon claiming./i + ) }) }) @@ -106,19 +102,16 @@ describe('Deposit USDC through CCTP', () => { context('Should display CCTP modal', () => { confirmAndApproveCctpDeposit() - cy.confirmMetamaskPermissionToSpend(USDCAmountToSend.toString()).then( - () => { - // eslint-disable-next-line - cy.wait(40_000) - cy.confirmMetamaskTransaction().then(() => { - cy.findTransactionInTransactionHistory({ - duration: 'a minute', - amount: USDCAmountToSend, - symbol: 'USDC' - }) - }) - } - ) + cy.confirmMetamaskPermissionToSpend(USDCAmountToSend.toString()) + + // eslint-disable-next-line + cy.wait(40_000) + cy.confirmMetamaskTransaction() + cy.findTransactionInTransactionHistory({ + duration: 'a minute', + amount: USDCAmountToSend, + symbol: 'USDC' + }) }) }) @@ -133,22 +126,19 @@ describe('Deposit USDC through CCTP', () => { context('Should display CCTP modal', () => { confirmAndApproveCctpDeposit() - cy.confirmMetamaskPermissionToSpend(USDCAmountToSend.toString()).then( - () => { - // eslint-disable-next-line - cy.wait(40_000) - cy.confirmMetamaskTransaction().then(() => { - const txData = { amount: USDCAmountToSend, symbol: 'USDC' } - cy.findTransactionInTransactionHistory({ - duration: 'a minute', - ...txData - }) - cy.openTransactionDetails(txData) - cy.findTransactionDetailsCustomDestinationAddress( - Cypress.env('CUSTOM_DESTINATION_ADDRESS') - ) - }) - } + cy.confirmMetamaskPermissionToSpend(USDCAmountToSend.toString()) + + // eslint-disable-next-line + cy.wait(40_000) + cy.confirmMetamaskTransaction() + const txData = { amount: USDCAmountToSend, symbol: 'USDC' } + cy.findTransactionInTransactionHistory({ + duration: 'a minute', + ...txData + }) + cy.openTransactionDetails(txData) + cy.findTransactionDetailsCustomDestinationAddress( + Cypress.env('CUSTOM_DESTINATION_ADDRESS') ) }) }) diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/depositERC20.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/depositERC20.cy.ts index 3e65361e4e..3b1ec8dab4 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/depositERC20.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/depositERC20.cy.ts @@ -10,7 +10,6 @@ import { getL1NetworkName, getL2NetworkName } from '../../support/common' -import { shortenAddress } from '../../../src/util/CommonUtils' const moreThanZeroBalance = /0(\.\d+)/ @@ -64,12 +63,9 @@ describe('Deposit ERC20 Token', () => { context('should show gas estimations', () => { cy.typeAmount(ERC20AmountToSend) - // - .then(() => { - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) - }) + cy.findGasFeeSummary(zeroToLessThanOneETH) + cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) }) context('should deposit successfully', () => { @@ -96,12 +92,9 @@ describe('Deposit ERC20 Token', () => { context('should show summary', () => { cy.typeAmount(ERC20AmountToSend) - // - .then(() => { - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) - }) + cy.findGasFeeSummary(zeroToLessThanOneETH) + cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) }) context('should fill custom destination address successfully', () => { @@ -109,25 +102,21 @@ describe('Deposit ERC20 Token', () => { }) context('should deposit successfully', () => { - cy.findMoveFundsButton() - .click() - .then(() => { - cy.confirmMetamaskTransaction().then(() => { - const txData = { - amount: ERC20AmountToSend, - symbol: 'WETH' - } - cy.findTransactionInTransactionHistory({ - duration: depositTime, - ...txData - }) - cy.openTransactionDetails(txData) - cy.findTransactionDetailsCustomDestinationAddress( - Cypress.env('CUSTOM_DESTINATION_ADDRESS') - ) - cy.closeTransactionDetails() - }) - }) + cy.findMoveFundsButton().click() + cy.confirmMetamaskTransaction() + const txData = { + amount: ERC20AmountToSend, + symbol: 'WETH' + } + cy.findTransactionInTransactionHistory({ + duration: depositTime, + ...txData + }) + cy.openTransactionDetails(txData) + cy.findTransactionDetailsCustomDestinationAddress( + Cypress.env('CUSTOM_DESTINATION_ADDRESS') + ) + cy.closeTransactionDetails() }) context('deposit should complete successfully', () => { @@ -147,22 +136,21 @@ describe('Deposit ERC20 Token', () => { timeout: 60_000, interval: 500 } - ).then(() => { - // open the tx details popup - const txData = { - amount: ERC20AmountToSend, - symbol: 'WETH' - } - cy.findTransactionInTransactionHistory({ - duration: 'a few seconds ago', - ...txData - }) - cy.openTransactionDetails(txData) - cy.findTransactionDetailsCustomDestinationAddress( - Cypress.env('CUSTOM_DESTINATION_ADDRESS') - ) - cy.closeTransactionDetails() + ) + // open the tx details popup + const txData = { + amount: ERC20AmountToSend, + symbol: 'WETH' + } + cy.findTransactionInTransactionHistory({ + duration: 'a few seconds ago', + ...txData }) + cy.openTransactionDetails(txData) + cy.findTransactionDetailsCustomDestinationAddress( + Cypress.env('CUSTOM_DESTINATION_ADDRESS') + ) + cy.closeTransactionDetails() }) context('funds should reach destination account successfully', () => { diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/depositETH.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/depositETH.cy.ts index 9fdbda2562..484fe31784 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/depositETH.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/depositETH.cy.ts @@ -24,19 +24,15 @@ describe('Deposit ETH', () => { it('should show gas estimations and bridge successfully', () => { cy.login({ networkType: 'parentChain' }) cy.typeAmount(ETHAmountToDeposit) - // - .then(() => { - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) - }) + cy.findGasFeeSummary(zeroToLessThanOneETH) + cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) cy.findMoveFundsButton().click() - cy.confirmMetamaskTransaction().then(() => { - cy.findTransactionInTransactionHistory({ - duration: depositTime, - amount: ETHAmountToDeposit, - symbol: 'ETH' - }) + cy.confirmMetamaskTransaction() + cy.findTransactionInTransactionHistory({ + duration: depositTime, + amount: ETHAmountToDeposit, + symbol: 'ETH' }) }) diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/importToken.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/importToken.cy.ts index 728e81e147..3e856e97f3 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/importToken.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/importToken.cy.ts @@ -120,9 +120,7 @@ describe('Import token', () => { .typeRecursively('UNI') // flaky test can load data too slowly here - cy.wait(5000) - - cy.findByText('Uniswap').click() + cy.findByText('Uniswap', { timeout: 5_000 }).click() // UNI token should be selected now and popup should be closed after selection cy.findSelectTokenButton('UNI') @@ -194,14 +192,10 @@ describe('Import token', () => { .trigger('click', { force: true }) - .then(() => { - cy.findSelectTokenButton(ERC20TokenSymbol) + cy.findSelectTokenButton(ERC20TokenSymbol) - // Modal is closed - cy.findByRole('button', { name: 'Import token' }).should( - 'not.exist' - ) - }) + // Modal is closed + cy.findByRole('button', { name: 'Import token' }).should('not.exist') }) }) @@ -233,9 +227,7 @@ describe('Import token', () => { .trigger('click', { force: true }) - .then(() => { - cy.findSelectTokenButton(ERC20TokenSymbol) - }) + cy.findSelectTokenButton(ERC20TokenSymbol) // Modal is closed cy.findByRole('button', { name: 'Import token' }).should('not.exist') @@ -269,9 +261,7 @@ describe('Import token', () => { .trigger('click', { force: true }) - .then(() => { - cy.findSelectTokenButton('ETH') - }) + cy.findSelectTokenButton('ETH') // Modal is closed cy.findByRole('button', { name: 'Dialog Cancel' }).should('not.exist') diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/redeemRetryable.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/redeemRetryable.cy.ts index 5654ae0a39..8290f825b5 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/redeemRetryable.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/redeemRetryable.cy.ts @@ -84,32 +84,26 @@ describe('Redeem ERC20 Deposit', () => { .click() // approve redeem transaction - cy.confirmMetamaskTransaction().then(() => { - cy.wait(15_000).then(() => { - cy.selectTransactionsPanelTab('settled') - - // find the same transaction there redeemed successfully - cy.findTransactionInTransactionHistory({ - amount: wethAmountToDeposit, - symbol: 'WETH' - }) - - // close transaction history - cy.findByLabelText('Close side panel').click() - - // wait for the destination balance to update - cy.wait(5_000).then(() => { - // the balance on the destination chain should not be the same as before - cy.findByLabelText('WETH balance amount on childChain') - .should('be.visible') - .invoke('text') - .should( - 'eq', - formatAmount(Number(l2ERC20bal) + wethAmountToDeposit) - ) - }) - }) + cy.confirmMetamaskTransaction() + cy.wait(15_000) + cy.selectTransactionsPanelTab('settled') + + // find the same transaction there redeemed successfully + cy.findTransactionInTransactionHistory({ + amount: wethAmountToDeposit, + symbol: 'WETH' }) + + // close transaction history + cy.findByLabelText('Close side panel').click() + + // wait for the destination balance to update + cy.wait(5_000) + // the balance on the destination chain should not be the same as before + cy.findByLabelText('WETH balance amount on childChain') + .should('be.visible') + .invoke('text') + .should('eq', formatAmount(Number(l2ERC20bal) + wethAmountToDeposit)) }) }) }) diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts index 46e6851c0d..0124434723 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts @@ -35,29 +35,28 @@ describe('User enters site with query params on URL', () => { .should('be.visible') .should('not.have.text', 'max') .should('not.have.text', 'MAX') - // it's very hard to get the max amount separately - // so this test only asserts the amount set for the input field is less than user's balance - // but not the exact MAX AMOUNT set by the `setMaxAmount` function in `TransferPanelMain.tsx` - .then(() => { - cy.waitUntil( - () => - cy - .findByPlaceholderText(/Enter amount/i) - .then($el => Number($el.val()) > 0), - // optional timeouts and error messages - { - errorMsg: - 'was expecting a numerical input value greater than 0', - timeout: 5000, - interval: 500 - } - ).then(() => { - cy.findByPlaceholderText(/Enter amount/i) - .invoke('val') - .then(value => { - cy.wrap(Number(value)).should('be.lt', l1ETHbal) - }) - }) + // it's very hard to get the max amount separately + // so this test only asserts the amount set for the input field is less than user's balance + // but not the exact MAX AMOUNT set by the `setMaxAmount` function in `TransferPanelMain.tsx` + cy.waitUntil( + () => + cy + .findByPlaceholderText(/Enter amount/i) + .invoke('val') + .should($val => { + cy.wrap(Number($val)).should('be.gt', 0) + }), + // optional timeouts and error messages + { + errorMsg: 'was expecting a numerical input value greater than 0', + timeout: 5000, + interval: 500 + } + ) + cy.findByPlaceholderText(/Enter amount/i) + .invoke('val') + .should($val => { + cy.wrap(Number($val)).should('be.lt', l1ETHbal) }) } ) @@ -76,29 +75,25 @@ describe('User enters site with query params on URL', () => { .should('be.visible') .should('not.have.text', 'max') .should('not.have.text', 'MAX') - // it's very hard to get the max amount separately - // so this test only asserts the amount set for the input field is less than user's balance - // but not the exact MAX AMOUNT set by the `setMaxAmount` function in `TransferPanelMain.tsx` - .then(() => { - cy.waitUntil( - () => - cy - .findByPlaceholderText(/Enter amount/i) - .then($el => Number($el.val()) > 0), - // optional timeouts and error messages - { - errorMsg: - 'was expecting a numerical input value greater than 0', - timeout: 5000, - interval: 500 - } - ).then(() => { - cy.findByPlaceholderText(/Enter amount/i) - .invoke('val') - .then(value => { - cy.wrap(Number(value)).should('be.lt', l1ETHbal) - }) - }) + // it's very hard to get the max amount separately + // so this test only asserts the amount set for the input field is less than user's balance + // but not the exact MAX AMOUNT set by the `setMaxAmount` function in `TransferPanelMain.tsx` + cy.waitUntil( + () => + cy + .findByPlaceholderText(/Enter amount/i) + .then($el => Number($el.val()) > 0), + // optional timeouts and error messages + { + errorMsg: 'was expecting a numerical input value greater than 0', + timeout: 5000, + interval: 500 + } + ) + cy.findByPlaceholderText(/Enter amount/i) + .invoke('val') + .should($val => { + cy.wrap(Number($val)).should('be.lt', l1ETHbal) }) } ) @@ -118,29 +113,28 @@ describe('User enters site with query params on URL', () => { .should('not.have.text', 'max') .should('not.have.text', 'MAX') .should('not.have.text', 'MaX') - // it's very hard to get the max amount separately - // so this test only asserts the amount set for the input field is less than user's balance - // but not the exact MAX AMOUNT set by the `setMaxAmount` function in `TransferPanelMain.tsx` - .then(() => { - cy.waitUntil( - () => - cy - .findByPlaceholderText(/Enter amount/i) - .then($el => Number($el.val()) > 0), - // optional timeouts and error messages - { - errorMsg: - 'was expecting a numerical input value greater than 0', - timeout: 5000, - interval: 500 - } - ).then(() => { - cy.findByPlaceholderText(/Enter amount/i) - .invoke('val') - .then(value => { - cy.wrap(Number(value)).should('be.lt', l1ETHbal) - }) - }) + // it's very hard to get the max amount separately + // so this test only asserts the amount set for the input field is less than user's balance + // but not the exact MAX AMOUNT set by the `setMaxAmount` function in `TransferPanelMain.tsx` + cy.waitUntil( + () => + cy + .findByPlaceholderText(/Enter amount/i) + .invoke('val') + .should($val => { + cy.wrap(Number($val)).should('be.gt', 0) + }), + // optional timeouts and error messages + { + errorMsg: 'was expecting a numerical input value greater than 0', + timeout: 5000, + interval: 500 + } + ) + cy.findByPlaceholderText(/Enter amount/i) + .invoke('val') + .should($val => { + cy.wrap(Number($val)).should('be.lt', l1ETHbal) }) } ) diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawCctp.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawCctp.cy.ts index b040d61108..b7b772e4ad 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawCctp.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawCctp.cy.ts @@ -3,8 +3,6 @@ */ import { CommonAddress } from 'packages/arb-token-bridge-ui/src/util/CommonAddressUtils' -import { formatAmount } from '../../../src/util/NumberUtils' -import { shortenAddress } from '../../../src/util/CommonUtils' import { zeroToLessThanOneETH } from '../../support/common' // common function for this cctp withdrawal @@ -80,33 +78,28 @@ describe('Withdraw USDC through CCTP', () => { it('should initiate withdrawing USDC to the same address through CCTP successfully', () => { context('should show clickable withdraw button', () => { - cy.typeAmount(USDCAmountToSend).then(() => { - cy.findByText( - 'Gas estimates are not available for this action.' - ).should('be.visible') - cy.findGasFeeForChain('Arbitrum Sepolia', zeroToLessThanOneETH) - cy.findGasFeeForChain( - /You'll have to pay Sepolia gas fee upon claiming./i - ) - }) + cy.typeAmount(USDCAmountToSend) + cy.findByText( + 'Gas estimates are not available for this action.' + ).should('be.visible') + cy.findGasFeeForChain('Arbitrum Sepolia', zeroToLessThanOneETH) + cy.findGasFeeForChain( + /You'll have to pay Sepolia gas fee upon claiming./i + ) cy.findMoveFundsButton().click() }) context('Should display CCTP modal', () => { confirmAndApproveCctpWithdrawal() - cy.confirmMetamaskPermissionToSpend(USDCAmountToSend.toString()).then( - () => { - // eslint-disable-next-line - cy.wait(40_000) - cy.confirmMetamaskTransaction().then(() => { - cy.findTransactionInTransactionHistory({ - duration: 'a minute', - amount: USDCAmountToSend, - symbol: 'USDC' - }) - }) - } - ) + cy.confirmMetamaskPermissionToSpend(USDCAmountToSend.toString()) + // eslint-disable-next-line + cy.wait(40_000) + cy.confirmMetamaskTransaction() + cy.findTransactionInTransactionHistory({ + duration: 'a minute', + amount: USDCAmountToSend, + symbol: 'USDC' + }) }) }) @@ -125,25 +118,22 @@ describe('Withdraw USDC through CCTP', () => { context('Should display CCTP modal', () => { confirmAndApproveCctpWithdrawal() - cy.confirmMetamaskPermissionToSpend(USDCAmountToSend.toString()).then( - () => { - // eslint-disable-next-line - cy.wait(40_000) - cy.confirmMetamaskTransaction().then(() => { - const txData = { - amount: USDCAmountToSend, - symbol: 'USDC' - } - cy.findTransactionInTransactionHistory({ - duration: 'a minute', - ...txData - }) - cy.openTransactionDetails(txData) - cy.findTransactionDetailsCustomDestinationAddress( - Cypress.env('CUSTOM_DESTINATION_ADDRESS') - ) - }) - } + cy.confirmMetamaskPermissionToSpend(USDCAmountToSend.toString()) + + // eslint-disable-next-line + cy.wait(40_000) + cy.confirmMetamaskTransaction() + const txData = { + amount: USDCAmountToSend, + symbol: 'USDC' + } + cy.findTransactionInTransactionHistory({ + duration: 'a minute', + ...txData + }) + cy.openTransactionDetails(txData) + cy.findTransactionDetailsCustomDestinationAddress( + Cypress.env('CUSTOM_DESTINATION_ADDRESS') ) }) }) diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawERC20.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawERC20.cy.ts index f1cffa96e7..f16612d267 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawERC20.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawERC20.cy.ts @@ -2,7 +2,6 @@ * When user wants to bridge ERC20 from L2 to L1 */ -import { shortenAddress } from '../../../src/util/CommonUtils' import { formatAmount } from '../../../src/util/NumberUtils' import { getInitialERC20Balance, @@ -73,17 +72,14 @@ describe('Withdraw ERC20 Token', () => { context('should show summary', () => { cy.typeAmount(ERC20AmountToSend) - // - .then(() => { - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain( - new RegExp( - `You'll have to pay ${getL1NetworkName()} gas fee upon claiming.`, - 'i' - ) - ) - }) + cy.findGasFeeSummary(zeroToLessThanOneETH) + cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeForChain( + new RegExp( + `You'll have to pay ${getL1NetworkName()} gas fee upon claiming.`, + 'i' + ) + ) }) context('should show clickable withdraw button', () => { @@ -109,22 +105,20 @@ describe('Withdraw ERC20 Token', () => { }) .should('be.visible') .click() - .then(() => { - // the Continue withdrawal button should not be disabled now - cy.findByRole('button', { - name: /Continue/i - }) - .should('be.enabled') - .click() - - cy.confirmMetamaskTransaction() - - cy.findTransactionInTransactionHistory({ - duration: 'an hour', - amount: ERC20AmountToSend, - symbol: 'WETH' - }) - }) + // the Continue withdrawal button should not be disabled now + cy.findByRole('button', { + name: /Continue/i + }) + .should('be.enabled') + .click() + + cy.confirmMetamaskTransaction() + + cy.findTransactionInTransactionHistory({ + duration: 'an hour', + amount: ERC20AmountToSend, + symbol: 'WETH' + }) }) }) @@ -182,17 +176,14 @@ describe('Withdraw ERC20 Token', () => { context('should show summary', () => { cy.typeAmount(ERC20AmountToSend) - // - .then(() => { - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain( - new RegExp( - `You'll have to pay ${getL1NetworkName()} gas fee upon claiming.`, - 'i' - ) - ) - }) + cy.findGasFeeSummary(zeroToLessThanOneETH) + cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeForChain( + new RegExp( + `You'll have to pay ${getL1NetworkName()} gas fee upon claiming.`, + 'i' + ) + ) }) context('should fill custom destination address successfully', () => { @@ -222,39 +213,36 @@ describe('Withdraw ERC20 Token', () => { }) .should('be.visible') .click() - .then(() => { - // the Continue withdrawal button should not be disabled now - cy.findByRole('button', { - name: /Continue/i - }) - .should('be.enabled') - .click() - - cy.confirmMetamaskTransaction().then(() => { - const txData = { - amount: ERC20AmountToSend, - symbol: 'WETH' - } - cy.findTransactionInTransactionHistory({ - duration: 'an hour', - ...txData - }) - cy.openTransactionDetails(txData) - cy.findTransactionDetailsCustomDestinationAddress( - Cypress.env('CUSTOM_DESTINATION_ADDRESS') - ) - - // close popup - cy.closeTransactionDetails() - cy.findByLabelText('Close side panel').click() - - // the balance on the source chain should not be the same as before - cy.findByLabelText('WETH balance amount on childChain') - .should('be.visible') - .its('text') - .should('not.eq', l2ERC20bal) - }) - }) + // the Continue withdrawal button should not be disabled now + cy.findByRole('button', { + name: /Continue/i + }) + .should('be.enabled') + .click() + + cy.confirmMetamaskTransaction() + const txData = { + amount: ERC20AmountToSend, + symbol: 'WETH' + } + cy.findTransactionInTransactionHistory({ + duration: 'an hour', + ...txData + }) + cy.openTransactionDetails(txData) + cy.findTransactionDetailsCustomDestinationAddress( + Cypress.env('CUSTOM_DESTINATION_ADDRESS') + ) + + // close popup + cy.closeTransactionDetails() + cy.findByLabelText('Close side panel').click() + + // the balance on the source chain should not be the same as before + cy.findByLabelText('WETH balance amount on childChain') + .should('be.visible') + .its('text') + .should('not.eq', l2ERC20bal) }) }) diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawETH.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawETH.cy.ts index 3fc828dac3..3716249386 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawETH.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawETH.cy.ts @@ -39,66 +39,57 @@ describe('Withdraw ETH', () => { it('should show gas estimations', () => { cy.login({ networkType: 'childChain' }) cy.typeAmount(ETHToWithdraw) - // - .then(() => { - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain( - new RegExp( - `You'll have to pay ${getL1NetworkName()} gas fee upon claiming.`, - 'i' - ) - ) - }) + cy.findGasFeeSummary(zeroToLessThanOneETH) + cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeForChain( + new RegExp( + `You'll have to pay ${getL1NetworkName()} gas fee upon claiming.`, + 'i' + ) + ) }) it('should show withdrawal confirmation and withdraw', () => { ETHToWithdraw = Number((Math.random() * 0.001).toFixed(5)) // generate a new withdrawal amount for each test-run attempt so that findAllByText doesn't stall coz of prev transactions cy.login({ networkType: 'childChain' }) cy.typeAmount(ETHToWithdraw) - // - .then(() => { - cy.findMoveFundsButton().click() - cy.findByText(/Arbitrum’s bridge/i).should('be.visible') - - // the Continue withdrawal button should be disabled at first - cy.findByRole('button', { - name: /Continue/i - }).should('be.disabled') - - cy.findByRole('switch', { - name: /before I can claim my funds/i - }) - .should('be.visible') - .click() - - cy.findByRole('switch', { - name: /after claiming my funds/i - }) - .should('be.visible') - .click() - .then(() => { - // the Continue withdrawal button should not be disabled now - cy.findByRole('button', { - name: /Continue/i - }) - .should('be.enabled') - .click() - - cy.confirmMetamaskTransaction() - - cy.findTransactionInTransactionHistory({ - duration: 'an hour', - amount: ETHToWithdraw, - symbol: 'ETH' - }) - }) - }) + cy.findMoveFundsButton().click() + cy.findByText(/Arbitrum’s bridge/i).should('be.visible') + + // the Continue withdrawal button should be disabled at first + cy.findByRole('button', { + name: /Continue/i + }).should('be.disabled') + + cy.findByRole('switch', { + name: /before I can claim my funds/i + }) + .should('be.visible') + .click() + + cy.findByRole('switch', { + name: /after claiming my funds/i + }) + .should('be.visible') + .click() + // the Continue withdrawal button should not be disabled now + cy.findByRole('button', { + name: /Continue/i + }) + .should('be.enabled') + .click() + + cy.confirmMetamaskTransaction() + + cy.findTransactionInTransactionHistory({ + duration: 'an hour', + amount: ETHToWithdraw, + symbol: 'ETH' + }) }) it('should claim funds', { defaultCommandTimeout: 200_000 }, () => { // increase the timeout for this test as claim button can take ~(20 blocks *10 blocks/sec) to activate - cy.login({ networkType: 'parentChain' }) // login to L1 to claim the funds (otherwise would need to change network after clicking on claim) cy.findByLabelText('Open Transaction History') diff --git a/packages/arb-token-bridge-ui/tests/support/commands.ts b/packages/arb-token-bridge-ui/tests/support/commands.ts index f0d2f638c7..4d4a7d0666 100644 --- a/packages/arb-token-bridge-ui/tests/support/commands.ts +++ b/packages/arb-token-bridge-ui/tests/support/commands.ts @@ -103,14 +103,12 @@ Cypress.Commands.add( // once all assertions are run, before test exit, make sure web-app is reset to original export const logout = () => { - cy.disconnectMetamaskWalletFromAllDapps().then(() => { - cy.resetMetamaskAccount().then(() => { - // resetMetamaskAccount doesn't seem to remove the connected network in CI - // changeMetamaskNetwork fails if already connected to the desired network - // as a workaround we switch to another network after all the tests - cy.changeMetamaskNetwork('sepolia') - }) - }) + cy.disconnectMetamaskWalletFromAllDapps() + cy.resetMetamaskAccount() + // resetMetamaskAccount doesn't seem to remove the connected network in CI + // changeMetamaskNetwork fails if already connected to the desired network + // as a workaround we switch to another network after all the tests + cy.changeMetamaskNetwork('sepolia') } export const connectToApp = () => { @@ -246,18 +244,17 @@ export const searchAndSelectToken = ({ cy.findByPlaceholderText(/Search by token name/i) .typeRecursively(tokenAddress) .should('be.visible') - .then(() => { - // Click on the Add new token button - cy.findByRole('button', { name: 'Add New Token' }) - .should('be.visible') - .click() - // Select the USDC token - cy.findAllByText(tokenName).first().click() + // Click on the Add new token button + cy.findByRole('button', { name: 'Add New Token' }) + .should('be.visible') + .click() + + // Select the USDC token + cy.findAllByText(tokenName).first().click() - // USDC token should be selected now and popup should be closed after selection - cy.findSelectTokenButton(tokenName) - }) + // USDC token should be selected now and popup should be closed after selection + cy.findSelectTokenButton(tokenName) } export const fillCustomDestinationAddress = () => { diff --git a/yarn.lock b/yarn.lock index a84ce59fbe..4e027cffbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1537,10 +1537,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@offchainlabs/cobalt@^0.3.6": - version "0.3.6" - resolved "https://registry.yarnpkg.com/@offchainlabs/cobalt/-/cobalt-0.3.6.tgz#e7daa9b20a7ff5897cb57d39927ebbf1ede3ae5f" - integrity sha512-7WSP6Mme+pMAoIxO+PHHF1ICKc/htky8xd1QbkzOFQAyMSrkVxqICnKsLr8Pu1nzG597A6BApk//LYv/0EkUjg== +"@offchainlabs/cobalt@0.3.7": + version "0.3.7" + resolved "https://registry.yarnpkg.com/@offchainlabs/cobalt/-/cobalt-0.3.7.tgz#5c525a46d534dd48ee4b603fd1c72d121a95aa91" + integrity sha512-rJ2Trpa0P92WezI2icdPynHR5apDPnkQroJYFrxdxBVbJs0vjkU4gRz+hIKFAay4IN7xmovhsCLC4EdSwDGHhA== "@parcel/watcher-android-arm64@2.4.1": version "2.4.1"