diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistoryTable.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistoryTable.tsx
index 191223f331..114fa3950e 100644
--- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistoryTable.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistoryTable.tsx
@@ -31,6 +31,14 @@ import { TransactionsTableRow } from './TransactionsTableRow'
import { EmptyTransactionHistory } from './EmptyTransactionHistory'
import { Address } from '../../util/AddressUtils'
+export const BatchTransferEthTooltip = ({ children }: PropsWithChildren) => {
+ return (
+
+ {children}
+
+ )
+}
+
export const ContentWrapper = ({
children,
className = ''
diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetails.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetails.tsx
index 196d46fedc..755a7a1475 100644
--- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetails.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableDetails.tsx
@@ -6,6 +6,7 @@ import Image from 'next/image'
import dayjs from 'dayjs'
import CctpLogoColor from '@/images/CctpLogoColor.svg'
import ArbitrumLogo from '@/images/ArbitrumLogo.svg'
+import EthereumLogoRoundLight from '@/images/EthereumLogoRoundLight.svg'
import { useTxDetailsStore } from './TransactionHistory'
import { getExplorerUrl, getNetworkName, isNetwork } from '../../util/networks'
@@ -22,6 +23,8 @@ import { shortenAddress } from '../../util/CommonUtils'
import { isTxCompleted } from './helpers'
import { Address } from '../../util/AddressUtils'
import { sanitizeTokenSymbol } from '../../util/TokenUtils'
+import { isBatchTransfer } from '../../util/TokenDepositUtils'
+import { BatchTransferEthTooltip } from './TransactionHistoryTable'
const DetailsBox = ({
children,
@@ -135,17 +138,41 @@ export const TransactionsTableDetails = ({
{dayjs(tx.createdAt).format('MMMM DD, YYYY')}
{dayjs(tx.createdAt).format('h:mma')}
-
-
-
-
- {formatAmount(Number(tx.value), {
- symbol: tokenSymbol
- })}
-
-
+
+
+
+
+
+ {formatAmount(Number(tx.value), {
+ symbol: tokenSymbol
+ })}
+
+
+
+ {isBatchTransfer(tx) && (
+
+
+
+
+ {formatAmount(Number(tx.value2), {
+ symbol: ether.symbol
+ })}
+
+
+
+ )}
0
+
const timestampCreated = Math.floor(Date.now() / 1000).toString()
const txHistoryCompatibleObject = convertBridgeSdkToMergedTransaction({
@@ -941,6 +943,7 @@ export function TransferPanel() {
destinationAddress,
nativeCurrency,
amount: amountBigNumber,
+ amount2: isBatchTransfer ? utils.parseEther(amount2) : undefined,
timestampCreated
})
@@ -959,6 +962,7 @@ export function TransferPanel() {
destinationAddress,
nativeCurrency,
amount: amountBigNumber,
+ amount2: isBatchTransfer ? utils.parseEther(amount2) : undefined,
timestampCreated
})
)
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/bridgeSdkConversionUtils.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/bridgeSdkConversionUtils.ts
index 1e33ea38f5..de4174c18c 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/bridgeSdkConversionUtils.ts
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/bridgeSdkConversionUtils.ts
@@ -23,6 +23,7 @@ type SdkToUiConversionProps = {
destinationAddress?: string
nativeCurrency: NativeCurrency
amount: BigNumber
+ amount2?: BigNumber
timestampCreated: string
}
@@ -34,7 +35,8 @@ export const convertBridgeSdkToMergedTransaction = ({
walletAddress,
destinationAddress,
nativeCurrency,
- amount
+ amount,
+ amount2
}: SdkToUiConversionProps): MergedTransaction => {
const { transferType } = bridgeTransfer
const isDeposit =
@@ -57,6 +59,7 @@ export const convertBridgeSdkToMergedTransaction = ({
amount,
selectedToken ? selectedToken.decimals : nativeCurrency.decimals
),
+ value2: amount2 ? utils.formatEther(amount2) : undefined,
depositStatus: isDeposit ? DepositStatus.L1_PENDING : undefined,
uniqueId: null,
isWithdrawal: !isDeposit,
@@ -78,6 +81,7 @@ export const convertBridgeSdkToPendingDepositTransaction = ({
nativeCurrency,
destinationAddress,
amount,
+ amount2,
timestampCreated
}: SdkToUiConversionProps): Deposit => {
const transaction =
@@ -95,6 +99,7 @@ export const convertBridgeSdkToPendingDepositTransaction = ({
amount,
selectedToken ? selectedToken.decimals : nativeCurrency.decimals
),
+ value2: amount2 ? utils.formatEther(amount2) : undefined,
parentChainId,
childChainId,
direction: 'deposit',
diff --git a/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts b/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts
index bcfe7d054c..b2fd7b83b6 100644
--- a/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts
+++ b/packages/arb-token-bridge-ui/src/hooks/useTransactions.ts
@@ -89,6 +89,7 @@ type TransactionBase = {
type: TxnType
status?: TxnStatus
value: string | null
+ value2?: string
txID?: string
assetName: string
assetType: AssetType
diff --git a/packages/arb-token-bridge-ui/src/state/app/state.ts b/packages/arb-token-bridge-ui/src/state/app/state.ts
index 3c832e064b..3e42176cea 100644
--- a/packages/arb-token-bridge-ui/src/state/app/state.ts
+++ b/packages/arb-token-bridge-ui/src/state/app/state.ts
@@ -53,6 +53,7 @@ export interface MergedTransaction {
asset: string
assetType: AssetType
value: string | null
+ value2?: string
uniqueId: BigNumber | null
isWithdrawal: boolean
blockNum: number | null
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 ab48e5ca39..e6e9580fc6 100644
--- a/packages/arb-token-bridge-ui/src/state/app/utils.ts
+++ b/packages/arb-token-bridge-ui/src/state/app/utils.ts
@@ -153,6 +153,7 @@ export const transformDeposit = (
asset: tx.assetName || '',
assetType: tx.assetType,
value: tx.value,
+ value2: tx.value2,
uniqueId: null, // not needed
isWithdrawal: false,
blockNum: tx.blockNumber || null,
diff --git a/packages/arb-token-bridge-ui/src/util/TokenDepositUtils.ts b/packages/arb-token-bridge-ui/src/util/TokenDepositUtils.ts
index c6ab9300f3..39ecfd63b8 100644
--- a/packages/arb-token-bridge-ui/src/util/TokenDepositUtils.ts
+++ b/packages/arb-token-bridge-ui/src/util/TokenDepositUtils.ts
@@ -8,10 +8,12 @@ import {
fetchErc20ParentChainGatewayAddress,
getL2ERC20Address
} from './TokenUtils'
-import { DepositGasEstimates } from '../hooks/arbTokenBridge.types'
+import { AssetType, DepositGasEstimates } from '../hooks/arbTokenBridge.types'
import { addressIsSmartContract } from './AddressUtils'
import { getChainIdFromProvider } from '../token-bridge-sdk/utils'
import { captureSentryErrorWithExtraData } from './SentryUtils'
+import { MergedTransaction } from '../state/app/state'
+import { isExperimentalFeatureEnabled } from '.'
async function fetchTokenFallbackGasEstimates({
inboxAddress,
@@ -216,3 +218,14 @@ async function addressIsCustomGatewayToken({
childChainNetwork.tokenBridge?.parentCustomGateway.toLowerCase()
)
}
+
+export function isBatchTransfer(tx: MergedTransaction) {
+ return (
+ isExperimentalFeatureEnabled('batch') &&
+ !tx.isCctp &&
+ !tx.isWithdrawal &&
+ tx.assetType === AssetType.ERC20 &&
+ typeof tx.value2 !== 'undefined' &&
+ Number(tx.value2) > 0
+ )
+}
diff --git a/packages/arb-token-bridge-ui/src/util/deposits/helpers.ts b/packages/arb-token-bridge-ui/src/util/deposits/helpers.ts
index 185cb08479..52586e5e27 100644
--- a/packages/arb-token-bridge-ui/src/util/deposits/helpers.ts
+++ b/packages/arb-token-bridge-ui/src/util/deposits/helpers.ts
@@ -8,8 +8,9 @@ import {
EthL1L3DepositStatus,
Erc20L1L3DepositStatus
} from '@arbitrum/sdk'
+import { utils } from 'ethers'
-import { Provider } from '@ethersproject/providers'
+import { Provider, TransactionReceipt } from '@ethersproject/providers'
import { AssetType } from '../../hooks/arbTokenBridge.types'
import {
ParentToChildMessageData,
@@ -110,14 +111,125 @@ export const updateAdditionalDepositData = async ({
})
}
- // finally, else if the transaction is not ETH ie. it's a ERC20 token deposit
- return updateTokenDepositStatusData({
+ // ERC-20 deposit
+ const tokenDeposit = await updateTokenDepositStatusData({
depositTx,
l1ToL2Msg: l1ToL2Msg as ParentToChildMessageReader,
timestampCreated,
l1Provider,
l2Provider
})
+
+ // check local storage first, fallback to fetching on chain
+ if (depositTx.value2) {
+ return { ...tokenDeposit, value2: depositTx.value2 }
+ }
+
+ const { value2 } = await getBatchTransferDepositData({
+ l1ToL2Msg: l1ToL2Msg as ParentToChildMessageReader,
+ depositStatus: tokenDeposit.status
+ })
+
+ return {
+ ...tokenDeposit,
+ value2
+ }
+}
+
+const getBatchTransferDepositData = async ({
+ l1ToL2Msg,
+ depositStatus
+}: {
+ l1ToL2Msg: ParentToChildMessageReader
+ depositStatus: TxnStatus | undefined
+}): Promise<{
+ value2: Transaction['value2']
+}> => {
+ if (!isPotentialBatchTransfer({ l1ToL2Msg })) {
+ return { value2: undefined }
+ }
+
+ // get maxSubmissionCost, which is the amount of ETH sent in batched ERC-20 deposit + max gas cost
+ const maxSubmissionCost = Number(
+ utils.formatEther(l1ToL2Msg.messageData.maxSubmissionFee.toString())
+ )
+
+ // we deduct gas cost from max submission fee, which leaves us with amount2 (extra ETH sent with ERC-20)
+ if (depositStatus === 'success') {
+ // if success, we use the actual gas cost
+ const retryableFee = await getRetryableFee({
+ l1ToL2Msg
+ })
+
+ if (!retryableFee) {
+ return { value2: undefined }
+ }
+
+ return { value2: String(Number(maxSubmissionCost) - Number(retryableFee)) }
+ }
+
+ // when not success, we don't know the final gas cost yet so we use estimates
+ const estimatedRetryableFee = utils.formatEther(
+ l1ToL2Msg.messageData.gasLimit.mul(l1ToL2Msg.messageData.maxFeePerGas)
+ )
+
+ return {
+ value2: String(Number(maxSubmissionCost) - Number(estimatedRetryableFee))
+ }
+}
+
+const isPotentialBatchTransfer = ({
+ l1ToL2Msg
+}: {
+ l1ToL2Msg: ParentToChildMessageReader
+}) => {
+ const { maxSubmissionFee, gasLimit, maxFeePerGas } = l1ToL2Msg.messageData
+
+ const estimatedGas = gasLimit.mul(maxFeePerGas)
+
+ const maxSubmissionFeeNumber = Number(utils.formatEther(maxSubmissionFee))
+ const estimatedGasNumber = Number(utils.formatEther(estimatedGas))
+
+ const excessGasFee = maxSubmissionFeeNumber - estimatedGasNumber
+ const percentageGasUsed = (estimatedGasNumber / maxSubmissionFeeNumber) * 100
+
+ // heuristic for determining if it's a batch transfer (based on maxSubmissionFee)
+ return excessGasFee >= 0.001 && percentageGasUsed < 10
+}
+
+const getRetryableFee = async ({
+ l1ToL2Msg
+}: {
+ l1ToL2Msg: ParentToChildMessageReader
+}) => {
+ const autoRedeemReceipt = (
+ (await l1ToL2Msg.getSuccessfulRedeem()) as {
+ status: ParentToChildMessageStatus.REDEEMED
+ childTxReceipt: TransactionReceipt
+ }
+ ).childTxReceipt
+
+ if (!autoRedeemReceipt) {
+ return undefined
+ }
+
+ const autoRedeemGas = autoRedeemReceipt.gasUsed.mul(
+ autoRedeemReceipt.effectiveGasPrice
+ )
+
+ const retryableCreationReceipt = await l1ToL2Msg.getRetryableCreationReceipt()
+
+ if (!retryableCreationReceipt) {
+ return undefined
+ }
+
+ const retryableCreationGas = retryableCreationReceipt.gasUsed.mul(
+ retryableCreationReceipt.effectiveGasPrice
+ )
+
+ const gasUsed = autoRedeemGas.add(retryableCreationGas)
+
+ return utils.formatEther(gasUsed)
}
const updateETHDepositStatusData = async ({