From 2058bc74fa3904a33d6b7f165e108985c52b5a00 Mon Sep 17 00:00:00 2001 From: Bartek Date: Wed, 7 Aug 2024 18:06:19 +0200 Subject: [PATCH 01/11] feat: standardize deposit and withdrawal times (#1814) Co-authored-by: Fionna Chan <13184582+fionnachan@users.noreply.github.com> --- .../TransactionsTableDetailsSteps.tsx | 53 +-- ...ransactionsTableDetailsTeleporterSteps.tsx | 37 +- .../TransactionsTableRowAction.tsx | 15 +- .../USDCDepositConfirmationDialogCheckbox.tsx | 7 +- ...DCWithdrawalConfirmationDialogCheckbox.tsx | 6 +- .../WithdrawalConfirmationDialog.tsx | 6 +- .../components/common/DepositCountdown.tsx | 74 --- .../components/common/TransferCountdown.tsx | 60 +++ .../components/common/WithdrawalCountdown.tsx | 55 --- .../__tests__/useTransferDuration.test.ts | 438 ++++++++++++++++++ .../src/hooks/useTransferDuration.ts | 183 ++++++++ .../src/state/cctpState.ts | 15 +- .../tests/e2e/specs/depositERC20.cy.ts | 2 +- .../tests/e2e/specs/depositETH.cy.ts | 2 +- 14 files changed, 742 insertions(+), 211 deletions(-) delete mode 100644 packages/arb-token-bridge-ui/src/components/common/DepositCountdown.tsx create mode 100644 packages/arb-token-bridge-ui/src/components/common/TransferCountdown.tsx delete mode 100644 packages/arb-token-bridge-ui/src/components/common/WithdrawalCountdown.tsx create mode 100644 packages/arb-token-bridge-ui/src/hooks/__tests__/useTransferDuration.test.ts create mode 100644 packages/arb-token-bridge-ui/src/hooks/useTransferDuration.ts diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsSteps.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsSteps.tsx index f046a5106e..61a321c1ac 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsSteps.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsSteps.tsx @@ -1,5 +1,4 @@ import { ReactNode, useMemo } from 'react' -import dayjs from 'dayjs' import { twMerge } from 'tailwind-merge' import { ArrowTopRightOnSquareIcon, @@ -8,7 +7,7 @@ import { } from '@heroicons/react/24/outline' import { DepositStatus, MergedTransaction } from '../../state/app/state' -import { getExplorerUrl, getNetworkName, isNetwork } from '../../util/networks' +import { getExplorerUrl, getNetworkName } from '../../util/networks' import { getDestinationNetworkTxId, isTxClaimable, @@ -19,12 +18,7 @@ import { } from './helpers' import { TransactionsTableRowAction } from './TransactionsTableRowAction' import { ExternalLink } from '../common/ExternalLink' -import { - WithdrawalCountdown, - getTxConfirmationDate -} from '../common/WithdrawalCountdown' -import { DepositCountdown } from '../common/DepositCountdown' -import { useRemainingTime } from '../../state/cctpState' +import { TransferCountdown } from '../common/TransferCountdown' import { isDepositReadyToRedeem } from '../../state/app/utils' import { Address } from '../../util/AddressUtils' import { isTeleport } from '@/token-bridge-sdk/teleport' @@ -33,30 +27,12 @@ import { secondRetryableLegForTeleportRequiresRedeem } from '../../util/RetryableUtils' import { TransactionsTableDetailsTeleporterSteps } from './TransactionsTableDetailsTeleporterSteps' +import { + minutesToHumanReadableTime, + useTransferDuration +} from '../../hooks/useTransferDuration' import { isTeleporterTransaction } from '../../hooks/useTransactions' -function getTransferDurationText(tx: MergedTransaction) { - const { isTestnet, isOrbitChain } = isNetwork(tx.childChainId) - - if (tx.isCctp) { - return isTestnet ? 'a minute' : '10 minutes' - } - - if (!tx.isWithdrawal) { - if (isOrbitChain && !isTeleport(tx)) { - return 'a minute' - } - return isTestnet ? '10 minutes' : '15 minutes' - } - - // withdrawals - return getTxConfirmationDate({ - createdAt: dayjs(), - withdrawalFromChainId: tx.childChainId - // we set from to current time so that we get the full withdrawal confirmation time - }).from(dayjs(), true) -} - function needsToClaimTransfer(tx: MergedTransaction) { return tx.isCctp || tx.isWithdrawal } @@ -193,7 +169,7 @@ export const TransactionsTableDetailsSteps = ({ tx: MergedTransaction address: Address | undefined }) => { - const { remainingTime: cctpRemainingTime } = useRemainingTime(tx) + const { approximateDurationInMinutes } = useTransferDuration(tx) const { sourceChainId } = tx @@ -254,19 +230,12 @@ export const TransactionsTableDetailsSteps = ({ - {tx.isCctp && <>{cctpRemainingTime}} - {!tx.isCctp && - (tx.isWithdrawal ? ( - - ) : ( - - ))} - remaining - + ) } /> diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsTeleporterSteps.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsTeleporterSteps.tsx index bfb48745dd..0540259aa3 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsTeleporterSteps.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetailsTeleporterSteps.tsx @@ -20,7 +20,12 @@ import { Step, TransactionFailedOnNetwork } from './TransactionsTableDetailsSteps' -import { DepositCountdown } from '../common/DepositCountdown' +import { TransferCountdown } from '../common/TransferCountdown' +import { + getOrbitDepositDuration, + getStandardDepositDuration, + minutesToHumanReadableTime +} from '../../hooks/useTransferDuration' const TeleportMiddleStepFailureExplanationNote = ({ tx @@ -73,9 +78,7 @@ export const TransactionsTableDetailsTeleporterSteps = ({ tx: TeleporterMergedTransaction address: Address | undefined }) => { - const { isTestnet: isTestnetTx } = isNetwork(tx.childChainId) - - const l2TxID = tx.l1ToL2MsgData?.childTxId + const l2TxID = tx.parentToChildMsgData?.childTxId const isFirstRetryableLegSucceeded = typeof l2TxID !== 'undefined' && typeof tx.l2ToL3MsgData?.l2ForwarderRetryableTxID === 'undefined' @@ -83,6 +86,8 @@ export const TransactionsTableDetailsTeleporterSteps = ({ const isFirstRetryableLegFailed = firstRetryableLegRequiresRedeem(tx) const l2ForwarderRequiresRedeem = l2ForwarderRetryableRequiresRedeem(tx) + const { isTestnet } = isNetwork(tx.sourceChainId) + const isFirstRetryableLegResolved = isFirstRetryableLegSucceeded || isFirstRetryableLegFailed @@ -117,11 +122,8 @@ export const TransactionsTableDetailsTeleporterSteps = ({ ? firstRetryableRedeemButton : firstTransactionExternalLink - const firstRetryableWaitingDuration = isTestnetTx - ? '10 minutes' - : '15 minutes' - - const secondRetryableWaitingDuration = isTestnetTx ? '1 minute' : '5 minutes' + const firstRetryableWaitingDuration = getStandardDepositDuration(isTestnet) + const secondRetryableWaitingDuration = getOrbitDepositDuration(isTestnet) return ( <> @@ -129,13 +131,16 @@ export const TransactionsTableDetailsTeleporterSteps = ({ - - remaining - + ) } /> @@ -160,7 +165,9 @@ export const TransactionsTableDetailsTeleporterSteps = ({ ) diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRowAction.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRowAction.tsx index bec78d56a8..65da87e937 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRowAction.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRowAction.tsx @@ -1,12 +1,12 @@ import { useCallback } from 'react' import { GET_HELP_LINK } from '../../constants' import { useClaimWithdrawal } from '../../hooks/useClaimWithdrawal' +import { useClaimCctp } from '../../state/cctpState' import { DepositStatus, MergedTransaction, TeleporterMergedTransaction } from '../../state/app/state' -import { useClaimCctp, useRemainingTime } from '../../state/cctpState' import { trackEvent } from '../../util/AnalyticsUtils' import { isUserRejectedError } from '../../util/isUserRejectedError' import { getNetworkName } from '../../util/networks' @@ -16,8 +16,7 @@ import { useSwitchNetworkWithConfig } from '../../hooks/useSwitchNetworkWithConf import { useNetwork } from 'wagmi' import { isDepositReadyToRedeem } from '../../state/app/utils' import { useRedeemRetryable } from '../../hooks/useRedeemRetryable' -import { WithdrawalCountdown } from '../common/WithdrawalCountdown' -import { DepositCountdown } from '../common/DepositCountdown' +import { TransferCountdown } from '../common/TransferCountdown' import { Address } from '../../util/AddressUtils' import { getChainIdForRedeemingRetryable } from '../../util/RetryableUtils' import { isTeleport } from '@/token-bridge-sdk/teleport' @@ -54,8 +53,6 @@ export function TransactionsTableRowAction({ const { redeem: teleporterRedeem, isRedeeming: isTeleporterRedeeming } = useRedeemTeleporter(tx, address) - const { remainingTime: cctpRemainingTime } = useRemainingTime(tx) - const isRedeeming = isRetryableRedeeming || isTeleporterRedeeming const chainIdForRedeemingRetryable = getChainIdForRedeemingRetryable(tx) @@ -153,13 +150,7 @@ export function TransactionsTableRowAction({ return (
Time left: - {tx.isCctp && <>{cctpRemainingTime}} - {!tx.isCctp && - (tx.isWithdrawal ? ( - - ) : ( - - ))} +
) } diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/USDCDeposit/USDCDepositConfirmationDialogCheckbox.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/USDCDeposit/USDCDepositConfirmationDialogCheckbox.tsx index 2ac4ce34a3..0673db4217 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/USDCDeposit/USDCDepositConfirmationDialogCheckbox.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/USDCDeposit/USDCDepositConfirmationDialogCheckbox.tsx @@ -3,6 +3,8 @@ import { Checkbox } from '../../common/Checkbox' import { useEffect, useState } from 'react' import { isNetwork } from '../../../util/networks' import { useNetwork } from 'wagmi' +import { getCctpTransferDuration } from '../../../hooks/useTransferDuration' +import { minutesToHumanReadableTime } from '../../../hooks/useTransferDuration' export function USDCDepositConfirmationDialogCheckbox({ onChange, @@ -64,7 +66,10 @@ export function USDCDepositConfirmationDialogCheckbox({ I understand that it will take{' '} - {isTestnet ? '~1 minute' : '~15 minutes'} + ~ + {minutesToHumanReadableTime( + getCctpTransferDuration(isTestnet) + )} {' '} before I can claim my USDC on {destinationNetworkName}. diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/USDCWithdrawal/USDCWithdrawalConfirmationDialogCheckbox.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/USDCWithdrawal/USDCWithdrawalConfirmationDialogCheckbox.tsx index 463a23cb81..621dd21ff4 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/USDCWithdrawal/USDCWithdrawalConfirmationDialogCheckbox.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/USDCWithdrawal/USDCWithdrawalConfirmationDialogCheckbox.tsx @@ -3,6 +3,10 @@ import { useEffect, useState } from 'react' import { Checkbox } from '../../common/Checkbox' import { getNetworkName, isNetwork } from '../../../util/networks' import { useNetworks } from '../../../hooks/useNetworks' +import { + getCctpTransferDuration, + minutesToHumanReadableTime +} from '../../../hooks/useTransferDuration' export function USDCWithdrawalConfirmationDialogCheckbox({ onChange, @@ -50,7 +54,7 @@ export function USDCWithdrawalConfirmationDialogCheckbox({ I understand that it will take{' '} - {isTestnet ? '~1 minute' : '~15 minutes'} + ~{minutesToHumanReadableTime(getCctpTransferDuration(isTestnet))} {' '} before I can claim my USDC on {isTestnet ? 'Sepolia' : 'Ethereum'}. diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/WithdrawalConfirmationDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/WithdrawalConfirmationDialog.tsx index b8167f25df..5b57633497 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/WithdrawalConfirmationDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/WithdrawalConfirmationDialog.tsx @@ -16,8 +16,8 @@ import { CONFIRMATION_PERIOD_ARTICLE_LINK } from '../../constants' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' -import { getTxConfirmationDate } from '../common/WithdrawalCountdown' import { SecurityGuaranteed, SecurityNotGuaranteed } from './SecurityLabels' +import { getWithdrawalConfirmationDate } from '../../hooks/useTransferDuration' function getCalendarUrl( withdrawalDate: dayjs.Dayjs, @@ -67,8 +67,8 @@ export function WithdrawalConfirmationDialog( const { isArbitrumOne } = isNetwork(childChain.id) const bothCheckboxesChecked = checkbox1Checked && checkbox2Checked - const estimatedConfirmationDate = getTxConfirmationDate({ - createdAt: dayjs(new Date()), + const estimatedConfirmationDate = getWithdrawalConfirmationDate({ + createdAt: null, withdrawalFromChainId: childChain.id }) diff --git a/packages/arb-token-bridge-ui/src/components/common/DepositCountdown.tsx b/packages/arb-token-bridge-ui/src/components/common/DepositCountdown.tsx deleted file mode 100644 index 0138221e35..0000000000 --- a/packages/arb-token-bridge-ui/src/components/common/DepositCountdown.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import dayjs from 'dayjs' - -import { DepositStatus, MergedTransaction } from '../../state/app/state' -import { isNetwork } from '../../util/networks' -import { isTeleport } from '@/token-bridge-sdk/teleport' - -function getMinutesRemainingText(minutesRemaining: number): string { - if (minutesRemaining <= 1) { - return 'Less than a minute' - } - - return `${minutesRemaining} minutes` -} - -function getEstimatedDepositDurationInMinutes( - tx: MergedTransaction, - firstTxOnly?: boolean // teleport has 2 txns, this flag will give us estimate of only 1st tx, else it will give consolidated duration -) { - const { parentChainId, sourceChainId, destinationChainId } = tx - if (!parentChainId) { - return 15 - } - - const { isEthereumMainnetOrTestnet, isTestnet } = isNetwork(parentChainId) - - if ( - isTeleport({ - sourceChainId: sourceChainId, - destinationChainId: destinationChainId - }) - ) { - if (firstTxOnly) { - return isTestnet ? 10 : 15 - } - return isTestnet ? 11 : 20 // assuming 10 L2 + 1 Orbit, otherwise 15 L2 + 5 Orbit, - } - - // this covers orbit chains - if (!isEthereumMainnetOrTestnet) { - return 1 - } - - return isTestnet ? 10 : 15 -} - -export function DepositCountdown({ - tx, - firstTxOnly -}: { - tx: MergedTransaction - firstTxOnly?: boolean // teleport has 2 txns, this flag will give us estimate of only 1st tx, else it will give consolidated duration -}): JSX.Element | null { - const now = dayjs() - const createdAt = tx.createdAt - const depositStatus = tx.depositStatus - const whenCreated = dayjs(createdAt) - - if ( - depositStatus === DepositStatus.L1_PENDING || - depositStatus === DepositStatus.L2_PENDING - ) { - // Subtract the diff from the initial deposit time - const minutesRemaining = - getEstimatedDepositDurationInMinutes(tx, firstTxOnly) - - now.diff(whenCreated, 'minutes') - return ( - - {getMinutesRemainingText(minutesRemaining)} - - ) - } - - return null -} diff --git a/packages/arb-token-bridge-ui/src/components/common/TransferCountdown.tsx b/packages/arb-token-bridge-ui/src/components/common/TransferCountdown.tsx new file mode 100644 index 0000000000..81236de87c --- /dev/null +++ b/packages/arb-token-bridge-ui/src/components/common/TransferCountdown.tsx @@ -0,0 +1,60 @@ +import { DepositStatus, MergedTransaction } from '../../state/app/state' +import { + getOrbitDepositDuration, + minutesToHumanReadableTime, + useTransferDuration +} from '../../hooks/useTransferDuration' +import { isNetwork } from '../../util/networks' +import { isTeleporterTransaction } from '../../hooks/useTransactions' + +/** + * Displays a transfer countdown for a deposit, withdrawal, or cctp. + * + * @param {MergedTransaction} tx - The transaction object. + * @param {boolean} firstLegOnly - Teleport has 2 txns, this flag will give us estimate of only 1st tx, else it will give consolidated duration. + * @param {string} textAfterTime - Text to be displayed after the remaining time, e.g. if this was "remaining", it would result with e.g. "15 minutes remaining". + */ +export function TransferCountdown({ + tx, + firstLegOnly, + textAfterTime = '' +}: { + tx: MergedTransaction + firstLegOnly?: boolean + textAfterTime?: string +}) { + const { isTestnet } = isNetwork(tx.sourceChainId) + let { estimatedMinutesLeft } = useTransferDuration(tx) + + if (estimatedMinutesLeft === null) { + return Calculating... + } + + const isTeleport = isTeleporterTransaction(tx) + // To get the first retryable only, we subtract the Orbit deposit time (second retryable) + if (isTeleport && firstLegOnly) { + estimatedMinutesLeft -= getOrbitDepositDuration(isTestnet) + } + + const isStandardDeposit = !tx.isWithdrawal && !tx.isCctp && !isTeleport + + if (isStandardDeposit) { + const depositStatus = tx.depositStatus + + // Only show when status is Pending + if ( + !depositStatus || + ![DepositStatus.L1_PENDING, DepositStatus.L2_PENDING].includes( + depositStatus + ) + ) { + return null + } + } + + return ( + + {minutesToHumanReadableTime(estimatedMinutesLeft)} {textAfterTime} + + ) +} diff --git a/packages/arb-token-bridge-ui/src/components/common/WithdrawalCountdown.tsx b/packages/arb-token-bridge-ui/src/components/common/WithdrawalCountdown.tsx deleted file mode 100644 index 09012b177d..0000000000 --- a/packages/arb-token-bridge-ui/src/components/common/WithdrawalCountdown.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import dayjs, { Dayjs } from 'dayjs' - -import { - getBaseChainIdByChainId, - getL1BlockTime, - getConfirmPeriodBlocks -} from '../../util/networks' -import { MergedTransaction } from '../../state/app/state' - -/** - * Buffer for after a node is confirmable but isn't yet confirmed. - * A rollup block (RBlock) typically gets asserted every 30-60 minutes. - */ -const CONFIRMATION_BUFFER_MINUTES = 60 - -const SECONDS_IN_MIN = 60 - -export function getTxConfirmationDate({ - createdAt, - withdrawalFromChainId -}: { - createdAt: Dayjs - withdrawalFromChainId: number -}) { - const baseChainId = getBaseChainIdByChainId({ - chainId: withdrawalFromChainId - }) - // the block time is always base chain's block time regardless of withdrawing from L3 to L2 or from L2 to L1 - // and similarly, the confirm period blocks is always the number of blocks on the base chain - const confirmationSeconds = - getL1BlockTime(baseChainId) * - getConfirmPeriodBlocks(withdrawalFromChainId) + - CONFIRMATION_BUFFER_MINUTES * SECONDS_IN_MIN - return createdAt.add(confirmationSeconds, 'second') -} - -export function WithdrawalCountdown({ - tx -}: { - tx: MergedTransaction -}): JSX.Element | null { - // For new txs createdAt won't be defined yet, we default to the current time in that case - const createdAtDate = tx.createdAt ? dayjs(tx.createdAt) : dayjs() - const txConfirmationDate = getTxConfirmationDate({ - createdAt: createdAtDate, - withdrawalFromChainId: tx.childChainId - }) - - const minutesLeft = Math.max(txConfirmationDate.diff(dayjs(), 'minute'), 0) - - const timeLeftText = - minutesLeft === 0 ? 'less than a minute' : txConfirmationDate.fromNow(true) - - return {timeLeftText} -} diff --git a/packages/arb-token-bridge-ui/src/hooks/__tests__/useTransferDuration.test.ts b/packages/arb-token-bridge-ui/src/hooks/__tests__/useTransferDuration.test.ts new file mode 100644 index 0000000000..327474fafc --- /dev/null +++ b/packages/arb-token-bridge-ui/src/hooks/__tests__/useTransferDuration.test.ts @@ -0,0 +1,438 @@ +import dayjs from 'dayjs' +import { RenderHookResult, act, renderHook } from '@testing-library/react' +import { registerCustomArbitrumNetwork } from '@arbitrum/sdk' +import { MergedTransaction } from '../../state/app/state' +import { AssetType } from '../../hooks/arbTokenBridge.types' +import { + getCctpTransferDuration, + getOrbitDepositDuration, + getStandardDepositDuration, + useTransferDuration +} from '../../hooks/useTransferDuration' +import { getOrbitChains } from '../../util/orbitChainsList' + +const DAY_IN_MINUTES = 24 * 60 +const HOUR_IN_MINUTES = 60 + +function mockTransactionObject({ + minutesSinceStart, + isDeposit, + isCctp, + parentChainId, + childChainId +}: { + minutesSinceStart: number + isDeposit: boolean + isCctp: boolean + parentChainId: number + childChainId: number +}): MergedTransaction { + return { + sender: '', + destination: '', + direction: 'deposit', + status: 'pending', + createdAt: dayjs() + .subtract(minutesSinceStart, 'minutes') + // subtract extra 30 seconds to ensure returned minutes are always consistent + .subtract(30, 'seconds') + .valueOf(), + resolvedAt: 0, + txId: '', + asset: 'ETH', + assetType: AssetType.ETH, + value: '1', + uniqueId: null, + isWithdrawal: !isDeposit, + blockNum: 0, + tokenAddress: '', + isCctp, + childChainId, + parentChainId, + sourceChainId: isDeposit ? parentChainId : childChainId, + destinationChainId: isDeposit ? childChainId : parentChainId + } +} + +const renderHookAsyncUseTransferDuration = async (tx: MergedTransaction) => { + let hook: + | RenderHookResult< + ReturnType, + MergedTransaction + > + | undefined + + await act(async () => { + hook = renderHook(() => useTransferDuration(tx)) + }) + + if (!hook) { + throw new Error('Hook is not defined') + } + + return { result: hook.result } +} + +describe('useTransferDuration', () => { + const DEPOSIT_TIME_MINUTES_MAINNET = getStandardDepositDuration(false) + const DEPOSIT_TIME_MINUTES_TESTNET = getStandardDepositDuration(true) + const DEPOSIT_TIME_MINUTES_ORBIT_MAINNET = getOrbitDepositDuration(false) + const DEPOSIT_TIME_MINUTES_ORBIT_TESTNET = getOrbitDepositDuration(true) + const TRANSFER_TIME_MINUTES_CCTP_MAINNET = getCctpTransferDuration(false) + const TRANSFER_TIME_MINUTES_CCTP_TESTNET = getCctpTransferDuration(true) + + beforeAll(() => { + // register all chains so we can read `isTestnet` + getOrbitChains().forEach(chain => registerCustomArbitrumNetwork(chain)) + }) + + // ========= DEPOSITS ========= + + it('gets standard deposit duration for a new transfer on Mainnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 0, + isDeposit: true, + isCctp: false, + parentChainId: 1, + childChainId: 42161 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + DEPOSIT_TIME_MINUTES_MAINNET + ) + expect(result.current.estimatedMinutesLeft).toEqual(14) + }) + + it('gets standard deposit duration for an ongoing transfer on Mainnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 8, + isDeposit: true, + isCctp: false, + parentChainId: 1, + childChainId: 42161 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + DEPOSIT_TIME_MINUTES_MAINNET + ) + expect(result.current.estimatedMinutesLeft).toEqual(6) + }) + + it('gets standard deposit duration for a new transfer on Testnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 0, + isDeposit: true, + isCctp: false, + parentChainId: 11155111, + childChainId: 421614 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + DEPOSIT_TIME_MINUTES_TESTNET + ) + expect(result.current.estimatedMinutesLeft).toEqual(9) + }) + + it('gets standard deposit duration for an ongoing transfer on Testnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 2, + isDeposit: true, + isCctp: false, + parentChainId: 11155111, + childChainId: 421614 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + DEPOSIT_TIME_MINUTES_TESTNET + ) + expect(result.current.estimatedMinutesLeft).toEqual(7) + }) + + it('gets cctp deposit duration for a new transfer on Mainnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 0, + isDeposit: true, + isCctp: true, + parentChainId: 1, + childChainId: 42161 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + TRANSFER_TIME_MINUTES_CCTP_MAINNET + ) + }) + + it('gets cctp deposit duration for a new transfer on Testnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 0, + isDeposit: true, + isCctp: true, + parentChainId: 11155111, + childChainId: 421614 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + TRANSFER_TIME_MINUTES_CCTP_TESTNET + ) + }) + + it('gets orbit deposit duration for a new transfer on Mainnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 0, + isDeposit: true, + isCctp: false, + parentChainId: 42161, + childChainId: 660279 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + DEPOSIT_TIME_MINUTES_ORBIT_MAINNET + ) + expect(result.current.estimatedMinutesLeft).toEqual(4) + }) + + it('gets orbit deposit duration for an ongoing transfer on Mainnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 3, + isDeposit: true, + isCctp: false, + parentChainId: 42161, + childChainId: 660279 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + DEPOSIT_TIME_MINUTES_ORBIT_MAINNET + ) + expect(result.current.estimatedMinutesLeft).toEqual(1) + }) + + it('gets orbit deposit duration for a new transfer on Testnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 0, + isDeposit: true, + isCctp: false, + parentChainId: 421614, + childChainId: 37714555429 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + DEPOSIT_TIME_MINUTES_ORBIT_TESTNET + ) + expect(result.current.estimatedMinutesLeft).toEqual(0) + }) + + it('gets orbit deposit duration for an ongoing transfer on Testnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 1, + isDeposit: true, + isCctp: false, + parentChainId: 421614, + childChainId: 37714555429 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + DEPOSIT_TIME_MINUTES_ORBIT_TESTNET + ) + expect(result.current.estimatedMinutesLeft).toEqual(0) + }) + + it('gets teleport duration for a new transfer on Mainnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 0, + isDeposit: true, + isCctp: false, + parentChainId: 1, + childChainId: 1380012617 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + DEPOSIT_TIME_MINUTES_MAINNET + DEPOSIT_TIME_MINUTES_ORBIT_MAINNET + ) + expect(result.current.estimatedMinutesLeft).toEqual(19) + }) + + it('gets teleport duration for an ongoing transfer on Mainnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 11, + isDeposit: true, + isCctp: false, + parentChainId: 1, + childChainId: 1380012617 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + DEPOSIT_TIME_MINUTES_MAINNET + DEPOSIT_TIME_MINUTES_ORBIT_MAINNET + ) + expect(result.current.estimatedMinutesLeft).toEqual(8) + }) + + it('gets teleport duration for a new transfer on Testnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 0, + isDeposit: true, + isCctp: false, + parentChainId: 11155111, + childChainId: 1918988905 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + DEPOSIT_TIME_MINUTES_TESTNET + DEPOSIT_TIME_MINUTES_ORBIT_TESTNET + ) + expect(result.current.estimatedMinutesLeft).toEqual(10) + }) + + it('gets teleport duration for an ongoing transfer on Testnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 4, + isDeposit: true, + isCctp: false, + parentChainId: 11155111, + childChainId: 1918988905 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + DEPOSIT_TIME_MINUTES_TESTNET + DEPOSIT_TIME_MINUTES_ORBIT_TESTNET + ) + expect(result.current.estimatedMinutesLeft).toEqual(6) + }) + + // ========= WITHDRAWALS ========= + + it('gets standard withdrawal duration for a new transfer on Mainnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 0, + isDeposit: false, + isCctp: false, + parentChainId: 1, + childChainId: 42161 + }) + ) + + expect(result.current.approximateDurationInMinutes).toBeGreaterThan( + 6 * DAY_IN_MINUTES + ) + expect(result.current.estimatedMinutesLeft).toBeGreaterThan( + 6 * DAY_IN_MINUTES + ) + }) + + it('gets standard withdrawal duration for an ongoing transfer on Mainnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 3 * DAY_IN_MINUTES, + isDeposit: false, + isCctp: false, + parentChainId: 1, + childChainId: 42161 + }) + ) + + expect(result.current.approximateDurationInMinutes).toBeGreaterThan( + 6 * DAY_IN_MINUTES + ) + expect(result.current.estimatedMinutesLeft).toBeGreaterThan( + 3 * DAY_IN_MINUTES + ) + expect(result.current.estimatedMinutesLeft).toBeLessThan(4 * DAY_IN_MINUTES) + }) + + it('gets standard withdrawal duration for a new transfer on Testnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 0, + isDeposit: false, + isCctp: false, + parentChainId: 11155111, + childChainId: 421614 + }) + ) + + expect(result.current.approximateDurationInMinutes).toBeGreaterThan( + HOUR_IN_MINUTES + ) + expect(result.current.estimatedMinutesLeft).toBeGreaterThan(HOUR_IN_MINUTES) + }) + + it('gets standard withdrawal duration for an ongoing transfer on Testnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 0.5 * HOUR_IN_MINUTES, + isDeposit: false, + isCctp: false, + parentChainId: 11155111, + childChainId: 421614 + }) + ) + + expect(result.current.approximateDurationInMinutes).toBeGreaterThan( + HOUR_IN_MINUTES + ) + expect(result.current.estimatedMinutesLeft).toBeGreaterThan( + 0.3 * HOUR_IN_MINUTES + ) + expect(result.current.estimatedMinutesLeft).toBeLessThan( + 0.7 * HOUR_IN_MINUTES + ) + }) + + it('gets cctp withdrawal duration for a new transfer on Mainnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 0, + isDeposit: false, + isCctp: true, + parentChainId: 1, + childChainId: 42161 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + TRANSFER_TIME_MINUTES_CCTP_MAINNET + ) + }) + + it('gets cctp withdrawal duration for a new transfer on Testnet', async () => { + const { result } = await renderHookAsyncUseTransferDuration( + mockTransactionObject({ + minutesSinceStart: 0, + isDeposit: false, + isCctp: true, + parentChainId: 11155111, + childChainId: 421614 + }) + ) + + expect(result.current.approximateDurationInMinutes).toEqual( + TRANSFER_TIME_MINUTES_CCTP_TESTNET + ) + }) +}) diff --git a/packages/arb-token-bridge-ui/src/hooks/useTransferDuration.ts b/packages/arb-token-bridge-ui/src/hooks/useTransferDuration.ts new file mode 100644 index 0000000000..e5c0264625 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/hooks/useTransferDuration.ts @@ -0,0 +1,183 @@ +import dayjs from 'dayjs' +import { isTeleport } from '@/token-bridge-sdk/teleport' + +import { MergedTransaction } from '../state/app/state' +import { useRemainingTimeCctp } from '../state/cctpState' +import { + getBaseChainIdByChainId, + getConfirmPeriodBlocks, + getL1BlockTime, + isNetwork +} from '../util/networks' + +const DEPOSIT_TIME_MINUTES = { + mainnet: 15, + testnet: 10 +} + +const TRANSFER_TIME_MINUTES_CCTP = { + mainnet: 15, + testnet: 1 +} + +/** + * TODO: An assumption should be 15 minutes for mainnet orbit deposits + * We should default to 15 and allow custom deposit times in orbit config (e.g. Xai should be 1 min) + * For now set 5 minutes for mainnet, 1 minute for testnet + */ +const DEPOSIT_TIME_MINUTES_ORBIT = { + mainnet: 5, + testnet: 1 +} + +/** + * Buffer for after a node is confirmable but isn't yet confirmed. + * A rollup block (RBlock) typically gets asserted every 30-60 minutes. + */ +const CONFIRMATION_BUFFER_MINUTES = 60 +const SECONDS_IN_MIN = 60 + +type UseTransferDurationResult = { + approximateDurationInMinutes: number + estimatedMinutesLeft: number | null +} + +/** + * Calculates the transfer duration in minutes for a given transaction. + * + * @param {MergedTransaction} tx - The transaction object. + * @returns {UseTransferDurationResult} - An object containing the total duration, first leg duration, and remaining time. + * @property {number} approximateDurationInMinutes - The total duration of the transfer in minutes. + * @property {number | null} estimatedMinutesLeft - The remaining time for the transfer in minutes, or null if calculating or unavailable. + */ +export const useTransferDuration = ( + tx: MergedTransaction +): UseTransferDurationResult => { + const { estimatedMinutesLeftCctp } = useRemainingTimeCctp(tx) + + const { sourceChainId, destinationChainId, isCctp, childChainId } = tx + const { isTestnet, isOrbitChain } = isNetwork(childChainId) + + const standardDepositDuration = getStandardDepositDuration(isTestnet) + const orbitDepositDuration = getOrbitDepositDuration(isTestnet) + + if (isTeleport({ sourceChainId, destinationChainId })) { + // Deposit only + return { + approximateDurationInMinutes: + standardDepositDuration + orbitDepositDuration, + estimatedMinutesLeft: getRemainingMinutes({ + createdAt: tx.createdAt, + totalDuration: standardDepositDuration + orbitDepositDuration + }) + } + } + + if (isCctp) { + const cctpTransferDuration = getCctpTransferDuration(isTestnet) + return { + approximateDurationInMinutes: cctpTransferDuration, + estimatedMinutesLeft: estimatedMinutesLeftCctp + } + } + + if (tx.isWithdrawal) { + const withdrawalDuration = getWithdrawalDuration(tx) + return { + approximateDurationInMinutes: withdrawalDuration, + estimatedMinutesLeft: getRemainingMinutes({ + createdAt: tx.createdAt, + totalDuration: withdrawalDuration + }) + } + } + + if (isOrbitChain) { + return { + approximateDurationInMinutes: orbitDepositDuration, + estimatedMinutesLeft: getRemainingMinutes({ + createdAt: tx.createdAt, + totalDuration: orbitDepositDuration + }) + } + } + + return { + approximateDurationInMinutes: standardDepositDuration, + estimatedMinutesLeft: getRemainingMinutes({ + createdAt: tx.createdAt, + totalDuration: standardDepositDuration + }) + } +} + +export function getWithdrawalConfirmationDate({ + createdAt, + withdrawalFromChainId +}: { + createdAt: number | null + withdrawalFromChainId: number +}) { + // For new txs createdAt won't be defined yet, we default to the current time in that case + const createdAtDate = createdAt ? dayjs(createdAt) : dayjs() + + const baseChainId = getBaseChainIdByChainId({ + chainId: withdrawalFromChainId + }) + // the block time is always base chain's block time regardless of withdrawing from L3 to L2 or from L2 to L1 + // and similarly, the confirm period blocks is always the number of blocks on the base chain + const confirmationSeconds = + getL1BlockTime(baseChainId) * + getConfirmPeriodBlocks(withdrawalFromChainId) + + CONFIRMATION_BUFFER_MINUTES * SECONDS_IN_MIN + return createdAtDate.add(confirmationSeconds, 'second') +} + +function getWithdrawalDuration(tx: MergedTransaction) { + const confirmationDate = getWithdrawalConfirmationDate({ + createdAt: tx.createdAt, + withdrawalFromChainId: tx.sourceChainId + }) + return Math.max(confirmationDate.diff(tx.createdAt, 'minute'), 0) +} + +export function getStandardDepositDuration(testnet: boolean) { + return testnet ? DEPOSIT_TIME_MINUTES.testnet : DEPOSIT_TIME_MINUTES.mainnet +} + +export function getOrbitDepositDuration(testnet: boolean) { + return testnet + ? DEPOSIT_TIME_MINUTES_ORBIT.testnet + : DEPOSIT_TIME_MINUTES_ORBIT.mainnet +} + +export function getCctpTransferDuration(testnet: boolean) { + return testnet + ? TRANSFER_TIME_MINUTES_CCTP.testnet + : TRANSFER_TIME_MINUTES_CCTP.mainnet +} + +function getRemainingMinutes({ + createdAt, + totalDuration +}: { + createdAt: number | null + totalDuration: number +}): number { + // For new txs createdAt won't be defined yet, we default to the current time in that case + const createdAtDate = createdAt ? dayjs(createdAt) : dayjs() + const estimatedCompletionTime = createdAtDate.add(totalDuration, 'minutes') + + return Math.max(estimatedCompletionTime.diff(dayjs(), 'minute'), 0) +} + +export function minutesToHumanReadableTime(minutes: number | null) { + if (minutes === null) { + return 'Calculating...' + } + if (minutes <= 0) { + return 'Less than a minute' + } + // will convert number to '20 minutes', '1 hour', '7 days', etc + return dayjs().add(minutes, 'minutes').fromNow(true) +} diff --git a/packages/arb-token-bridge-ui/src/state/cctpState.ts b/packages/arb-token-bridge-ui/src/state/cctpState.ts index 7732eacfbe..07e57ffbe8 100644 --- a/packages/arb-token-bridge-ui/src/state/cctpState.ts +++ b/packages/arb-token-bridge-ui/src/state/cctpState.ts @@ -619,8 +619,10 @@ export function isTransferConfirmed(tx: MergedTransaction) { return dayjs().isAfter(getConfirmedDate(tx)) } -export function useRemainingTime(tx: MergedTransaction) { - const [remainingTime, setRemainingTime] = useState('Calculating...') +export function useRemainingTimeCctp(tx: MergedTransaction) { + const [estimatedMinutesLeftCctp, setEstimatedMinutesLeftCctp] = useState< + number | null + >(null) const [canBeClaimedDate, setCanBeClaimedDate] = useState() const [isConfirmed, setIsConfirmed] = useState( tx.status === 'Confirmed' || tx.status === 'Executed' @@ -628,9 +630,9 @@ export function useRemainingTime(tx: MergedTransaction) { useEffect(() => { if (tx.status === 'Failure') { - setRemainingTime('') + setEstimatedMinutesLeftCctp(null) } - }, [tx.status, setRemainingTime]) + }, [tx.status, setEstimatedMinutesLeftCctp]) useEffect(() => { if (!tx.createdAt || tx.status === 'Failure') { @@ -647,13 +649,14 @@ export function useRemainingTime(tx: MergedTransaction) { if (isTransferConfirmed(tx)) { setIsConfirmed(true) + setEstimatedMinutesLeftCctp(0) } else { - setRemainingTime(canBeClaimedDate.fromNow(true).toString()) + setEstimatedMinutesLeftCctp(canBeClaimedDate.diff(dayjs(), 'minutes')) } }, 2000) return { - remainingTime, + estimatedMinutesLeftCctp, isConfirmed } } 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 902b0e40e2..3e65361e4e 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 @@ -20,7 +20,7 @@ describe('Deposit ERC20 Token', () => { // because it is cleared between each `it` cypress test const isOrbitTest = Cypress.env('ORBIT_TEST') == '1' - const depositTime = isOrbitTest ? 'Less than a minute' : '10 minutes' + const depositTime = isOrbitTest ? 'Less than a minute' : '9 minutes' const l1WethAddress = Cypress.env('L1_WETH_ADDRESS') // Happy Path 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 3b74f4592a..9fdbda2562 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 @@ -12,7 +12,7 @@ describe('Deposit ETH', () => { const ETHAmountToDeposit = 0.0001 const isOrbitTest = Cypress.env('ORBIT_TEST') == '1' - const depositTime = isOrbitTest ? 'Less than a minute' : '10 minutes' + const depositTime = isOrbitTest ? 'Less than a minute' : '9 minutes' // Happy Path it('should show L1 and L2 chains correctly', () => { From 5ed08e10efc5151d3be1940a1a736d22f1f2c6e6 Mon Sep 17 00:00:00 2001 From: Christophe Deveaux Date: Thu, 8 Aug 2024 12:18:13 +0200 Subject: [PATCH 02/11] test: remove .then in e2e tests (#1810) --- .../tests/e2e/specs/approveToken.cy.ts | 16 +-- .../tests/e2e/specs/depositCctp.cy.ts | 66 ++++----- .../tests/e2e/specs/depositERC20.cy.ts | 82 +++++------ .../tests/e2e/specs/depositETH.cy.ts | 20 ++- .../tests/e2e/specs/importToken.cy.ts | 22 +-- .../tests/e2e/specs/redeemRetryable.cy.ts | 44 +++--- .../tests/e2e/specs/urlQueryParam.cy.ts | 132 +++++++++--------- .../tests/e2e/specs/withdrawCctp.cy.ts | 76 +++++----- .../tests/e2e/specs/withdrawERC20.cy.ts | 132 ++++++++---------- .../tests/e2e/specs/withdrawETH.cy.ts | 91 ++++++------ .../tests/support/commands.ts | 33 ++--- 11 files changed, 316 insertions(+), 398 deletions(-) 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 = () => { From 7b2b6b34b12173cc0d4b5570c011771756ee2e8a Mon Sep 17 00:00:00 2001 From: Bartek Date: Thu, 8 Aug 2024 13:49:30 +0200 Subject: [PATCH 03/11] fix: fetch correct teleport status on initial load (#1831) --- .../src/state/app/utils.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) 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 + ) } } From a05b0d1c9c0ee0a4682de2b532b4d6781564e71b Mon Sep 17 00:00:00 2001 From: Fionna Chan <13184582+fionnachan@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:03:45 +0100 Subject: [PATCH 04/11] chore: extra data for Sentry errors (#1830) --- .../src/components/App/WelcomeDialog.tsx | 7 ++-- .../TransferPanel/TransferPanel.tsx | 32 +++++++++++++++---- .../common/TokenSymbolWithExplorerLink.tsx | 8 +++-- .../src/hooks/useBalance.ts | 13 +++++--- .../src/hooks/useClaimWithdrawal.ts | 9 ++++-- .../src/hooks/useSwitchNetworkWithConfig.tsx | 7 ++-- .../src/state/cctpState.ts | 11 ++++--- .../src/util/SentryUtils.ts | 20 ++++++++++++ .../src/util/TokenDepositUtils.ts | 7 ++-- .../src/util/TokenUtils.ts | 25 +++++++++------ .../src/util/WithdrawalUtils.ts | 13 ++++++-- 11 files changed, 113 insertions(+), 39 deletions(-) create mode 100644 packages/arb-token-bridge-ui/src/util/SentryUtils.ts 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/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/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/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/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/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, From 72cf7bdb3684fce20ae2b34a66b7067fd36f7283 Mon Sep 17 00:00:00 2001 From: Fionna Chan <13184582+fionnachan@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:18:09 +0100 Subject: [PATCH 05/11] feat: prompt L2 approval for xerc20 gateway (#1820) --- .../Erc20WithdrawalStarter.ts | 10 ++++- .../src/util/L2ApprovalUtils.ts | 22 +++++++--- .../src/util/xErc20Utils.ts | 42 +++++++++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 packages/arb-token-bridge-ui/src/util/xErc20Utils.ts 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/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() + ) +} From 4c7dfab97b83ff89f78a7b17a4e79e4ff7769a1b Mon Sep 17 00:00:00 2001 From: Doug <4741454+douglance@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:38:59 -0400 Subject: [PATCH 06/11] build(deps): bump cobalt to 0.3.7 (#1833) --- packages/arb-token-bridge-ui/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) 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/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" From 8bd6aa018b402355937afacc6522969f41fb70a5 Mon Sep 17 00:00:00 2001 From: Bartek Date: Fri, 9 Aug 2024 16:42:06 +0200 Subject: [PATCH 07/11] feat: new destination chain dropdown (#1602) --- .../TransferPanel/NetworkListbox.tsx | 85 ------ .../TransferPanel/OneNovaTransferDialog.tsx | 65 +++-- .../components/TransferPanel/TokenSearch.tsx | 1 + .../TransferPanel/TransferPanelMain.tsx | 100 +------ .../DestinationNetworkBox.tsx | 191 +++++++------ .../TransferPanelMain/SourceNetworkBox.tsx | 228 +++++++-------- .../TransferPanel/TransferPanelMain/utils.ts | 8 + .../common/NetworkSelectionContainer.tsx | 264 +++++++++++------- .../common/SearchPanel/SearchPanelTable.tsx | 12 +- .../useChainIdsForNetworkSelection.ts | 36 +++ 10 files changed, 471 insertions(+), 519 deletions(-) delete mode 100644 packages/arb-token-bridge-ui/src/components/TransferPanel/NetworkListbox.tsx create mode 100644 packages/arb-token-bridge-ui/src/hooks/TransferPanel/useChainIdsForNetworkSelection.ts 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 3a63ca4bb7..a583a55d07 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx @@ -493,6 +493,7 @@ function TokensPanel({ onSubmit={addNewToken} SearchInputButton={AddButton} dataCy="tokenSearchList" + isDialog={false} > {({ height, width }) => ( 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/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 +} From bd1e24386fc33f42fdc855421608614e4d9326e0 Mon Sep 17 00:00:00 2001 From: Fionna Chan <13184582+fionnachan@users.noreply.github.com> Date: Fri, 9 Aug 2024 17:01:53 +0100 Subject: [PATCH 08/11] ci: replace restore action with install action (#1835) --- .github/workflows/core-batch-poster-monitor.yml | 7 ++----- .github/workflows/orbit-batch-poster-monitor.yml | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) 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 From e7cd50af16ed665ae13985a1246d8544bdb629a6 Mon Sep 17 00:00:00 2001 From: Bartek Date: Tue, 13 Aug 2024 15:34:16 +0200 Subject: [PATCH 09/11] ci: ignore axios advisory on yarn audit (#1839) --- audit-ci.jsonc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/audit-ci.jsonc b/audit-ci.jsonc index 7003d1ccb1..df74e90f0e 100644 --- a/audit-ci.jsonc +++ b/audit-ci.jsonc @@ -9,6 +9,9 @@ // https://github.com/advisories/GHSA-977x-g7h5-7qgw "GHSA-977x-g7h5-7qgw", // https://github.com/advisories/GHSA-f7q4-pwc6-w24p - "GHSA-f7q4-pwc6-w24p" + "GHSA-f7q4-pwc6-w24p", + // axios + // https://github.com/advisories/GHSA-8hc4-vh64-cxmj + "GHSA-8hc4-vh64-cxmj" ] } From 46d0a0138354974b60d399f4a833350d8fbbf229 Mon Sep 17 00:00:00 2001 From: Bartek Date: Wed, 14 Aug 2024 12:16:55 +0200 Subject: [PATCH 10/11] feat: show native currency input for batch transfers (#1832) --- .../components/TransferPanel/TokenButton.tsx | 62 ++++++++----------- .../TransferPanelMain/SourceNetworkBox.tsx | 23 +++++++ .../TransferPanel/TransferPanelMainInput.tsx | 7 ++- .../src/components/common/Header.tsx | 21 ++++++- .../arb-token-bridge-ui/src/pages/index.tsx | 27 ++++++-- .../arb-token-bridge-ui/src/util/index.ts | 46 ++++++++++++++ 6 files changed, 139 insertions(+), 47 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx index 1d3d4065e1..c090e311a3 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx @@ -4,7 +4,6 @@ import { ChevronDownIcon } from '@heroicons/react/24/outline' import { twMerge } from 'tailwind-merge' import { useAppState } from '../../state' -import { sanitizeImageSrc } from '../../util' import { TokenSearch } from '../TransferPanel/TokenSearch' import { sanitizeTokenSymbol } from '../../util/TokenUtils' import { useNativeCurrency } from '../../hooks/useNativeCurrency' @@ -17,43 +16,31 @@ import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { Transition } from '../common/Transition' -export function TokenButton(): JSX.Element { +export type TokenButtonOptions = { + symbol?: string + disabled?: boolean +} + +export function TokenButton({ + options +}: { + options?: TokenButtonOptions +}): JSX.Element { const { - app: { - selectedToken, - arbTokenBridge: { bridgeTokens }, - arbTokenBridgeLoaded - } + app: { selectedToken } } = useAppState() + const disabled = options?.disabled ?? false + const [networks] = useNetworks() const { childChainProvider } = useNetworksRelationship(networks) const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) - const tokenLogo = useMemo(() => { - const selectedAddress = selectedToken?.address - if (!selectedAddress) { - return nativeCurrency.logoUrl - } - if (!arbTokenBridgeLoaded) { - return undefined - } - if (typeof bridgeTokens === 'undefined') { - return undefined - } - const logo = bridgeTokens[selectedAddress]?.logoURI - if (logo) { - return sanitizeImageSrc(logo) + const tokenSymbol = useMemo(() => { + if (typeof options?.symbol !== 'undefined') { + return options.symbol } - return undefined - }, [ - nativeCurrency, - bridgeTokens, - selectedToken?.address, - arbTokenBridgeLoaded - ]) - const tokenSymbol = useMemo(() => { if (!selectedToken) { return nativeCurrency.symbol } @@ -62,7 +49,7 @@ export function TokenButton(): JSX.Element { erc20L1Address: selectedToken.address, chainId: networks.sourceChain.id }) - }, [selectedToken, networks.sourceChain.id, nativeCurrency.symbol]) + }, [selectedToken, networks.sourceChain.id, nativeCurrency.symbol, options]) return ( <> @@ -73,6 +60,7 @@ export function TokenButton(): JSX.Element { className="arb-hover h-full w-max rounded-bl rounded-tl px-3 py-3 text-white" aria-label="Select Token" onClick={onPopoverButtonClick} + disabled={disabled} >
{/* Commenting it out until we update the token image source files to be of better quality */} @@ -90,12 +78,14 @@ export function TokenButton(): JSX.Element { {tokenSymbol} - + {!disabled && ( + + )}
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 b892b0e97a..10865e3aa6 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,5 +1,6 @@ import { useCallback, useEffect } from 'react' +import { isTeleport } from '@/token-bridge-sdk/teleport' import { getNetworkName } from '../../../util/networks' import { NetworkButton, @@ -33,6 +34,7 @@ import { AmountQueryParamEnum } from '../../../hooks/useArbQueryParams' import { TransferReadinessRichErrorMessage } from '../useTransferReadinessUtils' import { useMaxAmount } from './useMaxAmount' import { useSetInputAmount } from '../../../hooks/TransferPanel/useSetInputAmount' +import { isExperimentalFeatureEnabled } from '../../../util' import { useDialog } from '../../common/Dialog' export function SourceNetworkBox({ @@ -145,6 +147,27 @@ export function SourceNetworkBox({ value={isMaxAmount ? '' : amount} /> + {isExperimentalFeatureEnabled('batch') && + // TODO: teleport is disabled for now but it needs to be looked into more to check whether it is or can be supported + !isTeleport({ + sourceChainId: networks.sourceChain.id, + destinationChainId: networks.destinationChain.id + }) && + selectedToken && ( + {}} + errorMessage={undefined} + value={''} + // eslint-disable-next-line + onChange={() => {}} + tokenButtonOptions={{ + symbol: nativeCurrency.symbol, + disabled: true + }} + /> + )} + {showUsdcSpecificInfo && (

Bridged USDC (USDC.e) will work but is different from Native USDC.{' '} diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx index 5eb2afdae3..9ebbee6a3d 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx @@ -1,7 +1,7 @@ import { twMerge } from 'tailwind-merge' import { useEffect, useMemo } from 'react' -import { TokenButton } from './TokenButton' +import { TokenButton, TokenButtonOptions } from './TokenButton' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { useSelectedTokenBalances } from '../../hooks/TransferPanel/useSelectedTokenBalances' @@ -156,10 +156,11 @@ export type TransferPanelMainInputProps = errorMessage?: string | TransferReadinessRichErrorMessage | undefined maxButtonOnClick: React.ButtonHTMLAttributes['onClick'] value: string + tokenButtonOptions?: TokenButtonOptions } export function TransferPanelMainInput(props: TransferPanelMainInputProps) { - const { errorMessage, maxButtonOnClick, ...rest } = props + const { errorMessage, maxButtonOnClick, tokenButtonOptions, ...rest } = props return ( <> @@ -171,7 +172,7 @@ export function TransferPanelMainInput(props: TransferPanelMainInputProps) { : 'border-white/30 text-white' )} > - +

Arbitrum - {isTestnet && TESTNET MODE} + {isTestnet && !isExperimentalMode && ( + TESTNET MODE + )} + {isExperimentalMode && ( + + EXPERIMENTAL MODE: features may be incomplete or not work properly + + )}
{children}
diff --git a/packages/arb-token-bridge-ui/src/pages/index.tsx b/packages/arb-token-bridge-ui/src/pages/index.tsx index 345101068f..1720c741f8 100644 --- a/packages/arb-token-bridge-ui/src/pages/index.tsx +++ b/packages/arb-token-bridge-ui/src/pages/index.tsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react' import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next' import dynamic from 'next/dynamic' +import { decodeString, encodeString } from 'use-query-params' import { registerCustomArbitrumNetwork } from '@arbitrum/sdk' import { Loader } from '../components/common/atoms/Loader' @@ -14,6 +15,7 @@ import { decodeChainQueryParam, encodeChainQueryParam } from '../hooks/useArbQueryParams' +import { sanitizeExperimentalFeaturesQueryParam } from '../util' const App = dynamic(() => import('../components/App/App'), { ssr: false, @@ -31,6 +33,7 @@ function getDestinationWithSanitizedQueryParams( sanitized: { sourceChainId: number destinationChainId: number + experiments: string | undefined }, query: GetServerSidePropsContext['query'] ) { @@ -38,7 +41,11 @@ function getDestinationWithSanitizedQueryParams( for (const key in query) { // don't copy "sourceChain" and "destinationChain" query params - if (key === 'sourceChain' || key === 'destinationChain') { + if ( + key === 'sourceChain' || + key === 'destinationChain' || + key === 'experiments' + ) { continue } @@ -52,6 +59,7 @@ function getDestinationWithSanitizedQueryParams( const encodedSource = encodeChainQueryParam(sanitized.sourceChainId) const encodedDestination = encodeChainQueryParam(sanitized.destinationChainId) + const encodedExperiments = encodeString(sanitized.experiments) if (encodedSource) { params.set('sourceChain', encodedSource) @@ -61,6 +69,10 @@ function getDestinationWithSanitizedQueryParams( } } + if (encodedExperiments) { + params.set('experiments', encodedExperiments) + } + return `/?${params.toString()}` } @@ -82,6 +94,7 @@ export function getServerSideProps({ }: GetServerSidePropsContext): GetServerSidePropsResult> { const sourceChainId = decodeChainQueryParam(query.sourceChain) const destinationChainId = decodeChainQueryParam(query.destinationChain) + const experiments = decodeString(query.experiments) // If both sourceChain and destinationChain are not present, let the client sync with Metamask if (!sourceChainId && !destinationChainId) { @@ -94,19 +107,23 @@ export function getServerSideProps({ addOrbitChainsToArbitrumSDK() // sanitize the query params - const sanitized = sanitizeQueryParams({ sourceChainId, destinationChainId }) + const sanitized = { + ...sanitizeQueryParams({ sourceChainId, destinationChainId }), + experiments: sanitizeExperimentalFeaturesQueryParam(experiments) + } // if the sanitized query params are different from the initial values, redirect to the url with sanitized query params if ( sourceChainId !== sanitized.sourceChainId || - destinationChainId !== sanitized.destinationChainId + destinationChainId !== sanitized.destinationChainId || + experiments !== sanitized.experiments ) { console.log(`[getServerSideProps] sanitizing query params`) console.log( - `[getServerSideProps] sourceChain=${sourceChainId}&destinationChain=${destinationChainId} (before)` + `[getServerSideProps] sourceChain=${sourceChainId}&destinationChain=${destinationChainId}&experiments=${experiments} (before)` ) console.log( - `[getServerSideProps] sourceChain=${sanitized.sourceChainId}&destinationChain=${sanitized.destinationChainId} (after)` + `[getServerSideProps] sourceChain=${sanitized.sourceChainId}&destinationChain=${sanitized.destinationChainId}&experiments=${sanitized.experiments} (after)` ) return { redirect: { diff --git a/packages/arb-token-bridge-ui/src/util/index.ts b/packages/arb-token-bridge-ui/src/util/index.ts index d4ee03f1be..8392214fa0 100644 --- a/packages/arb-token-bridge-ui/src/util/index.ts +++ b/packages/arb-token-bridge-ui/src/util/index.ts @@ -53,3 +53,49 @@ export const getAPIBaseUrl = () => { // Resolves: next-js-error-only-absolute-urls-are-supported in test:ci return process.env.NODE_ENV === 'test' ? 'http://localhost:3000' : '' } + +const featureFlags = ['batch'] as const + +type FeatureFlag = (typeof featureFlags)[number] + +export const isExperimentalFeatureEnabled = (flag: FeatureFlag) => { + const query = new URLSearchParams(window.location.search) + const flags = query.get('experiments') + + if (!flags) { + return false + } + + return flags.split(',').includes(flag) +} + +export const isExperimentalModeEnabled = () => { + const query = new URLSearchParams(window.location.search) + const flags = query.get('experiments') + + return flags !== null +} + +export const sanitizeExperimentalFeaturesQueryParam = ( + flags: string | null | undefined +) => { + if (!flags) { + return undefined + } + + const flagsArray = flags.split(',') + + if (flagsArray.length === 0) { + return undefined + } + + const validFlagsArray = flagsArray.filter(f => + featureFlags.includes(f as FeatureFlag) + ) + + if (validFlagsArray.length === 0) { + return undefined + } + + return validFlagsArray.join(',') +} From f5e4cd3a516588172b32b7cbb9ec700003fdf577 Mon Sep 17 00:00:00 2001 From: Dewansh Date: Wed, 14 Aug 2024 15:33:07 +0400 Subject: [PATCH 11/11] chore: upgrade posthog (#1842) --- packages/arb-token-bridge-ui/package.json | 2 +- yarn.lock | 55 +++++++++-------------- 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/packages/arb-token-bridge-ui/package.json b/packages/arb-token-bridge-ui/package.json index 28a4f5ea79..c7dc36f63f 100644 --- a/packages/arb-token-bridge-ui/package.json +++ b/packages/arb-token-bridge-ui/package.json @@ -34,7 +34,7 @@ "next-query-params": "^5.0.0", "overmind": "^28.0.1", "overmind-react": "^29.0.1", - "posthog-js": "^1.57.2", + "posthog-js": "^1.155.4", "query-string": "^8.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/yarn.lock b/yarn.lock index 4e027cffbb..d68198ac6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6696,7 +6696,7 @@ fetch-retry@^5.0.3: resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-5.0.6.tgz#17d0bc90423405b7a88b74355bf364acd2a7fa56" integrity sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ== -fflate@^0.4.1: +fflate@^0.4.8: version "0.4.8" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== @@ -10430,12 +10430,14 @@ postcss@8.4.31, postcss@^8.4.23, postcss@^8.4.31: picocolors "^1.0.0" source-map-js "^1.0.2" -posthog-js@^1.57.2: - version "1.68.1" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.68.1.tgz#eadca3db9e45287771fe3a8b4100bffe50891750" - integrity sha512-edwURtegKXIUEdjgLErJF8cfCuwj7kw8JDmomnVXp9cjwaJT8Y3BRLh5Lh81GnjwGuifp1vjOL0hD3AamtsmGg== +posthog-js@^1.155.4: + version "1.155.4" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.155.4.tgz#611a63cf95b8fa908b3b50b1043cbdcb4c2a712b" + integrity sha512-suxwAsmZGqMDXJe/RaCKI3PaDEHiuMDDhKcJklgGAg7eDnywieRkr5CoPcOOvnqTDMnuOPETr98jpYBXKUwGFQ== dependencies: - fflate "^0.4.1" + fflate "^0.4.8" + preact "^10.19.3" + web-vitals "^4.0.1" postinstall-postinstall@^2.1.0: version "2.1.0" @@ -10447,6 +10449,11 @@ preact@^10.12.0, preact@^10.5.9: resolved "https://registry.yarnpkg.com/preact/-/preact-10.15.1.tgz#a1de60c9fc0c79a522d969c65dcaddc5d994eede" integrity sha512-qs2ansoQEwzNiV5eAcRT1p1EC/dmEzaATVDJNiB3g2sRDWdA7b7MurXdJjB2+/WQktGWZwxvDrnuRFbWuIr64g== +preact@^10.19.3: + version "10.23.2" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.23.2.tgz#52deec92796ae0f0cc6b034d9c66e0fbc1b837dc" + integrity sha512-kKYfePf9rzKnxOAKDpsWhg/ysrHPqT+yQ7UW4JjdnqjFIeNUnNcEJvhuA8fDenxAGWzUqtd51DfVg7xp/8T9NA== + prebuild-install@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" @@ -11852,16 +11859,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -11961,7 +11959,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11975,13 +11973,6 @@ strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -12968,6 +12959,11 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" +web-vitals@^4.0.1: + version "4.2.3" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.3.tgz#270c4baecfbc6ec6fc15da1989e465e5f9b94fb7" + integrity sha512-/CFAm1mNxSmOj6i0Co+iGFJ58OS4NRGVP+AWS/l509uIK5a1bSoIVaHz/ZumpHTfHSZBpgrJ+wjfpAOrTHok5Q== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -13159,7 +13155,7 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -13177,15 +13173,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"