From 647e7d8c5f62188da0ee16eceefeefc6408dd2f8 Mon Sep 17 00:00:00 2001 From: Bartek Date: Mon, 19 Aug 2024 17:02:43 +0200 Subject: [PATCH 1/2] feat: send ETH with batched transfers (#1847) --- .../TransferPanel/TransferPanel.tsx | 41 ++++++++++++++++--- .../TransferPanelMain/SourceNetworkBox.tsx | 34 +++++++-------- .../useIsBatchTransferSupported.ts | 35 ++++++++++++++++ .../token-bridge-sdk/BridgeTransferStarter.ts | 6 +++ .../token-bridge-sdk/Erc20DepositStarter.ts | 10 ++++- 5 files changed, 99 insertions(+), 27 deletions(-) create mode 100644 packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts 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 fc1005a8f6..ed274b56d9 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -1,7 +1,7 @@ import dayjs from 'dayjs' import { useState, useMemo } from 'react' import Tippy from '@tippyjs/react' -import { constants, utils } from 'ethers' +import { BigNumber, constants, utils } from 'ethers' import { useLatest } from 'react-use' import { useAccount, useChainId, useSigner } from 'wagmi' import { TransactionResponse } from '@ethersproject/providers' @@ -49,7 +49,10 @@ import { isUserRejectedError } from '../../util/isUserRejectedError' import { getUsdcTokenAddressFromSourceChainId } from '../../state/cctpState' import { DepositStatus, MergedTransaction } from '../../state/app/state' import { useNativeCurrency } from '../../hooks/useNativeCurrency' -import { AssetType } from '../../hooks/arbTokenBridge.types' +import { + AssetType, + DepositGasEstimates +} from '../../hooks/arbTokenBridge.types' import { ImportTokenModalStatus, getWarningTokenDescription, @@ -63,7 +66,10 @@ import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { CctpTransferStarter } from '@/token-bridge-sdk/CctpTransferStarter' import { BridgeTransferStarterFactory } from '@/token-bridge-sdk/BridgeTransferStarterFactory' -import { BridgeTransfer } from '@/token-bridge-sdk/BridgeTransferStarter' +import { + BridgeTransfer, + TransferOverrides +} from '@/token-bridge-sdk/BridgeTransferStarter' import { addDepositToCache } from '../TransactionHistory/helpers' import { convertBridgeSdkToMergedTransaction, @@ -74,6 +80,7 @@ import { useSetInputAmount } from '../../hooks/TransferPanel/useSetInputAmount' import { getSmartContractWalletTeleportTransfersNotSupportedErrorMessage } from './useTransferReadinessUtils' import { useBalances } from '../../hooks/useBalances' import { captureSentryErrorWithExtraData } from '../../util/SentryUtils' +import { useIsBatchTransferSupported } from '../../hooks/TransferPanel/useIsBatchTransferSupported' const networkConnectionWarningToast = () => warningToast( @@ -124,6 +131,7 @@ export function TransferPanel() { isTeleportMode } = useNetworksRelationship(networks) const latestNetworks = useLatest(networks) + const isBatchTransferSupported = useIsBatchTransferSupported() const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) @@ -149,7 +157,7 @@ export function TransferPanel() { // Link the amount state directly to the amount in query params - no need of useState // Both `amount` getter and setter will internally be using `useArbQueryParams` functions - const [{ amount }] = useArbQueryParams() + const [{ amount, amount2 }] = useArbQueryParams() const { setAmount, setAmount2 } = useSetInputAmount() @@ -856,11 +864,34 @@ export function TransferPanel() { ) } + const overrides: TransferOverrides = {} + + const isBatchTransfer = isBatchTransferSupported && Number(amount2) > 0 + + if (isBatchTransfer) { + // when sending additional ETH with ERC-20, we add the additional ETH value as maxSubmissionCost + const gasEstimates = (await bridgeTransferStarter.transferEstimateGas({ + amount: amountBigNumber, + signer + })) as DepositGasEstimates + + if (!gasEstimates.estimatedChildChainSubmissionCost) { + errorToast('Failed to estimate deposit maxSubmissionCost') + throw 'Failed to estimate deposit maxSubmissionCost' + } + + overrides.maxSubmissionCost = utils + .parseEther(amount2) + .add(gasEstimates.estimatedChildChainSubmissionCost) + overrides.excessFeeRefundAddress = destinationAddress + } + // finally, call the transfer function const transfer = await bridgeTransferStarter.transfer({ amount: amountBigNumber, signer, - destinationAddress + destinationAddress, + overrides: Object.keys(overrides).length > 0 ? overrides : undefined }) // transaction submitted callback 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 db050e34bd..237c001d13 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 @@ -36,9 +36,9 @@ import { } from '../../../hooks/useArbQueryParams' import { useMaxAmount } from './useMaxAmount' import { useSetInputAmount } from '../../../hooks/TransferPanel/useSetInputAmount' -import { isExperimentalFeatureEnabled } from '../../../util' import { useDialog } from '../../common/Dialog' import { useTransferReadiness } from '../useTransferReadiness' +import { useIsBatchTransferSupported } from '../../../hooks/TransferPanel/useIsBatchTransferSupported' export function SourceNetworkBox({ customFeeTokenBalances, @@ -63,6 +63,7 @@ export function SourceNetworkBox({ }) const [sourceNetworkSelectionDialogProps, openSourceNetworkSelectionDialog] = useDialog() + const isBatchTransferSupported = useIsBatchTransferSupported() const { errorMessages } = useTransferReadiness() @@ -161,25 +162,18 @@ export function SourceNetworkBox({ onChange={e => setAmount(e.target.value)} /> - {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 - }) && - isDepositMode && - selectedToken && ( - setAmount2(e.target.value)} - tokenButtonOptions={{ - symbol: nativeCurrency.symbol, - disabled: true - }} - /> - )} + {isBatchTransferSupported && ( + setAmount2(e.target.value)} + tokenButtonOptions={{ + symbol: nativeCurrency.symbol, + disabled: true + }} + /> + )} {showUsdcSpecificInfo && (

diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts new file mode 100644 index 0000000000..13c592d0cb --- /dev/null +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts @@ -0,0 +1,35 @@ +import { useAppState } from '../../state' +import { isExperimentalFeatureEnabled } from '../../util' +import { useNativeCurrency } from '../useNativeCurrency' +import { useNetworks } from '../useNetworks' +import { useNetworksRelationship } from '../useNetworksRelationship' + +export const useIsBatchTransferSupported = () => { + const [networks] = useNetworks() + const { isDepositMode, isTeleportMode, childChainProvider } = + useNetworksRelationship(networks) + const { + app: { selectedToken } + } = useAppState() + const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) + + if (!isExperimentalFeatureEnabled('batch')) { + return false + } + if (!selectedToken) { + return false + } + if (!isDepositMode) { + return false + } + // TODO: teleport is disabled for now but it needs to be looked into more to check whether it is or can be supported + if (isTeleportMode) { + return false + } + // TODO: disable custom native currency for now, check if this works + if (nativeCurrency.isCustom) { + return false + } + + return true +} diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/BridgeTransferStarter.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/BridgeTransferStarter.ts index 3efd4df181..7dcbafa9ef 100644 --- a/packages/arb-token-bridge-ui/src/token-bridge-sdk/BridgeTransferStarter.ts +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/BridgeTransferStarter.ts @@ -48,10 +48,16 @@ export type TransferEstimateGas = { signer: Signer } +export type TransferOverrides = { + maxSubmissionCost?: BigNumber + excessFeeRefundAddress?: string +} + export type TransferProps = { amount: BigNumber signer: Signer destinationAddress?: string + overrides?: TransferOverrides } export type RequiresNativeCurrencyApprovalProps = { diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20DepositStarter.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20DepositStarter.ts index 7d90d2b4e9..d11dcbdcf3 100644 --- a/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20DepositStarter.ts +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20DepositStarter.ts @@ -273,7 +273,12 @@ export class Erc20DepositStarter extends BridgeTransferStarter { }) } - public async transfer({ amount, signer, destinationAddress }: TransferProps) { + public async transfer({ + amount, + signer, + destinationAddress, + overrides + }: TransferProps) { if (!this.sourceChainErc20Address) { throw Error('Erc20 token address not found') } @@ -292,7 +297,8 @@ export class Erc20DepositStarter extends BridgeTransferStarter { // the gas limit may vary by about 20k due to SSTORE (zero vs nonzero) // the 30% gas limit increase should cover the difference gasLimit: { percentIncrease: BigNumber.from(30) } - } + }, + ...overrides }) const gasLimit = await this.sourceChainProvider.estimateGas( From 377c1914502e4d71f3e4a6f857fd84581ddaaf5c Mon Sep 17 00:00:00 2001 From: Doug <4741454+douglance@users.noreply.github.com> Date: Mon, 19 Aug 2024 13:39:29 -0400 Subject: [PATCH 2/2] chore: rename transaction history helpers (#1821) Co-authored-by: Fionna Chan <13184582+fionnachan@users.noreply.github.com> --- .../components/TransactionHistory/helpers.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/helpers.ts b/packages/arb-token-bridge-ui/src/components/TransactionHistory/helpers.ts index d0daa4cb13..b5007f77a7 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/helpers.ts +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/helpers.ts @@ -280,7 +280,7 @@ export async function getUpdatedEthDeposit( return tx } - const { parentToChildMsg: l1ToL2Msg } = + const { parentToChildMsg } = await getParentToChildMessageDataFromParentTxHash({ depositTxId: tx.txId, isEthDeposit: true, @@ -288,7 +288,7 @@ export async function getUpdatedEthDeposit( childProvider: getProviderForChainId(tx.childChainId) }) - if (!l1ToL2Msg) { + if (!parentToChildMsg) { const receipt = await getTxReceipt(tx) if (!receipt || receipt.status !== 0) { @@ -300,7 +300,7 @@ export async function getUpdatedEthDeposit( return { ...tx, status: 'failure', depositStatus: DepositStatus.L1_FAILURE } } - const status = await l1ToL2Msg?.status() + const status = await parentToChildMsg?.status() const isDeposited = status === EthDepositMessageStatus.DEPOSITED const newDeposit: MergedTransaction = { @@ -312,10 +312,11 @@ export async function getUpdatedEthDeposit( status: isDeposited ? ParentToChildMessageStatus.FUNDS_DEPOSITED_ON_CHILD : ParentToChildMessageStatus.NOT_YET_CREATED, - retryableCreationTxID: (l1ToL2Msg as EthDepositMessage).childTxHash, + retryableCreationTxID: (parentToChildMsg as EthDepositMessage) + .childTxHash, // Only show `childTxId` after the deposit is confirmed childTxId: isDeposited - ? (l1ToL2Msg as EthDepositMessage).childTxHash + ? (parentToChildMsg as EthDepositMessage).childTxHash : undefined } } @@ -338,16 +339,15 @@ export async function getUpdatedTokenDeposit( return tx } - const { parentToChildMsg: l1ToL2Msg } = + const { parentToChildMsg } = await getParentToChildMessageDataFromParentTxHash({ depositTxId: tx.txId, isEthDeposit: false, parentProvider: getProviderForChainId(tx.parentChainId), childProvider: getProviderForChainId(tx.childChainId) }) - const _l1ToL2Msg = l1ToL2Msg as ParentToChildMessageReader - if (!l1ToL2Msg) { + if (!parentToChildMsg) { const receipt = await getTxReceipt(tx) if (!receipt || receipt.status !== 0) { @@ -359,7 +359,8 @@ export async function getUpdatedTokenDeposit( return { ...tx, status: 'failure', depositStatus: DepositStatus.L1_FAILURE } } - const res = await _l1ToL2Msg.getSuccessfulRedeem() + const _parentToChildMsg = parentToChildMsg as ParentToChildMessageReader + const res = await _parentToChildMsg.getSuccessfulRedeem() const childTxId = (() => { if (res.status === ParentToChildMessageStatus.REDEEMED) { @@ -371,7 +372,7 @@ export async function getUpdatedTokenDeposit( const newDeposit: MergedTransaction = { ...tx, - status: _l1ToL2Msg.retryableCreationId ? 'success' : tx.status, + status: _parentToChildMsg.retryableCreationId ? 'success' : tx.status, resolvedAt: res.status === ParentToChildMessageStatus.REDEEMED ? dayjs().valueOf() @@ -380,7 +381,7 @@ export async function getUpdatedTokenDeposit( status: res.status, childTxId, fetchingUpdate: false, - retryableCreationTxID: _l1ToL2Msg.retryableCreationId + retryableCreationTxID: _parentToChildMsg.retryableCreationId } }