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 b9542246b1..dff14021bb 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -452,6 +452,13 @@ export function TransferPanel() { return handleTxSigningError(error, step.payload.txRequestLabel) } } + + case 'tx_history_add': { + addPendingTransaction(step.payload) + switchToTransactionHistoryTab() + + return + } } } @@ -472,8 +479,11 @@ export function TransferPanel() { const destinationAddress = latestDestinationAddress.current try { - const { sourceChainProvider, destinationChainProvider, sourceChain } = - latestNetworks.current + const { + // + sourceChainProvider, + destinationChainProvider + } = latestNetworks.current const cctpTransferStarter = new CctpTransferStarter({ sourceChainProvider, @@ -481,12 +491,14 @@ export function TransferPanel() { }) const returnEarly = await drive(stepGeneratorForCctp, stepExecutor, { + amount, amountBigNumber, isDepositMode, isSmartContractWallet, walletAddress, destinationAddress, - transferStarter: cctpTransferStarter + transferStarter: cctpTransferStarter, + wagmiConfig }) // this is only necessary while we are migrating to the ui driver @@ -497,94 +509,6 @@ export function TransferPanel() { return } - let depositForBurnTx - - try { - if (isSmartContractWallet) { - showDelayedSmartContractTxRequest() - } - const transfer = await cctpTransferStarter.transfer({ - amount: amountBigNumber, - signer, - destinationAddress, - wagmiConfig - }) - depositForBurnTx = transfer.sourceChainTransaction - } catch (error) { - if (isUserRejectedError(error)) { - return - } - handleError({ - error, - label: 'cctp_transfer', - category: 'transaction_signing' - }) - errorToast( - `USDC ${ - isDepositMode ? 'Deposit' : 'Withdrawal' - } transaction failed: ${(error as Error)?.message ?? error}` - ) - } - - const childChainName = getNetworkName(childChain.id) - - if (isSmartContractWallet) { - // For SCW, we assume that the transaction went through - trackEvent(isDepositMode ? 'CCTP Deposit' : 'CCTP Withdrawal', { - accountType: 'Smart Contract', - network: childChainName, - amount: Number(amount), - complete: false, - version: 2 - }) - - return - } - - if (!depositForBurnTx) { - return - } - - trackEvent(isDepositMode ? 'CCTP Deposit' : 'CCTP Withdrawal', { - accountType: 'EOA', - network: childChainName, - amount: Number(amount), - complete: false, - version: 2 - }) - - const newTransfer: MergedTransaction = { - txId: depositForBurnTx.hash, - asset: 'USDC', - assetType: AssetType.ERC20, - blockNum: null, - createdAt: dayjs().valueOf(), - direction: isDepositMode ? 'deposit' : 'withdraw', - isWithdrawal: !isDepositMode, - resolvedAt: null, - status: 'pending', - uniqueId: null, - value: amount, - depositStatus: DepositStatus.CCTP_DEFAULT_STATE, - destination: destinationAddress ?? walletAddress, - sender: walletAddress, - isCctp: true, - tokenAddress: getUsdcTokenAddressFromSourceChainId(sourceChain.id), - cctpData: { - sourceChainId: sourceChain.id, - attestationHash: null, - messageBytes: null, - receiveMessageTransactionHash: null, - receiveMessageTimestamp: null - }, - parentChainId: parentChain.id, - childChainId: childChain.id, - sourceChainId: networks.sourceChain.id, - destinationChainId: networks.destinationChain.id - } - - addPendingTransaction(newTransfer) - switchToTransactionHistoryTab() setTransferring(false) clearAmountInput() clearRoute() diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts index d7c5f15f58..d058e50d6e 100644 --- a/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts @@ -1,5 +1,10 @@ import { BigNumber, Signer } from 'ethers' -import { Provider, StaticJsonRpcProvider } from '@ethersproject/providers' +import { + Provider, + StaticJsonRpcProvider, + TransactionRequest +} from '@ethersproject/providers' +import { SimulateContractReturnType } from 'viem' import { isNetwork, rpcURLs } from '../util/networks' import { ChainId } from '../types/ChainId' @@ -123,3 +128,9 @@ export function getProviderForChainId(chainId: ChainId): StaticJsonRpcProvider { return createProviderWithCache(chainId) } + +export function isSimulateContractReturnType( + value: TransactionRequest | SimulateContractReturnType +): value is SimulateContractReturnType { + return 'request' in value && 'result' in value +} diff --git a/packages/arb-token-bridge-ui/src/ui-driver/UiDriver.ts b/packages/arb-token-bridge-ui/src/ui-driver/UiDriver.ts index d2007af501..3ff9cfc1b5 100644 --- a/packages/arb-token-bridge-ui/src/ui-driver/UiDriver.ts +++ b/packages/arb-token-bridge-ui/src/ui-driver/UiDriver.ts @@ -1,8 +1,11 @@ import { BigNumber, providers } from 'ethers' import { BridgeTransferStarter } from '@/token-bridge-sdk/BridgeTransferStarter' -import { SimulateContractReturnType } from '@wagmi/core' +import { Config, SimulateContractReturnType } from '@wagmi/core' +import { Chain } from 'wagmi/chains' import { DialogType } from '../components/common/Dialog2' +import { trackEvent } from '../util/AnalyticsUtils' +import { MergedTransaction } from '../state/app/state' export type Dialog = Extract< DialogType, @@ -13,12 +16,14 @@ export type Dialog = Extract< > export type UiDriverContext = { + amount: string amountBigNumber: BigNumber isDepositMode: boolean isSmartContractWallet: boolean walletAddress: string destinationAddress?: string transferStarter: BridgeTransferStarter + wagmiConfig: Config } export type UiDriverStep = @@ -40,6 +45,17 @@ export type UiDriverStep = txRequestLabel: string } } + | { + type: 'analytics' + payload: { + event: Parameters[0] + properties?: Parameters[1] + } + } + | { + type: 'tx_history_add' + payload: MergedTransaction + } export type UiDriverStepType = UiDriverStep['type'] @@ -65,6 +81,10 @@ export type UiDriverStepResultFor = ? void : TStepType extends 'tx_ethers' | 'tx_wagmi' ? Result + : TStepType extends 'analytics' + ? void + : TStepType extends 'tx_history_add' + ? void : never export type UiDriverStepGenerator = ( diff --git a/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCctp.ts b/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCctp.ts index 4bad9150a7..deda48717e 100644 --- a/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCctp.ts +++ b/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCctp.ts @@ -1,13 +1,30 @@ -import { step, UiDriverStepGenerator } from './UiDriver' +import dayjs from 'dayjs' +import { + getChainIdFromProvider, + isSimulateContractReturnType +} from '@/token-bridge-sdk/utils' + +import { step, UiDriverStepGenerator, UiDriverContext } from './UiDriver' import { stepGeneratorForDialog, stepGeneratorForSmartContractWalletDestinationDialog, - stepGeneratorForTransactionEthers + stepGeneratorForTransactionEthers, + stepGeneratorForTransactionWagmi } from './UiDriverCommon' +import { getNetworkName } from '../util/networks' +import { DepositStatus, MergedTransaction } from '../state/app/state' +import { AssetType } from '../hooks/arbTokenBridge.types' +import { getUsdcTokenAddressFromSourceChainId } from '../state/cctpState' + export const stepGeneratorForCctp: UiDriverStepGenerator = async function* ( context ) { + const [sourceChainId, destinationChainId] = await Promise.all([ + getChainIdFromProvider(context.transferStarter.sourceChainProvider), + getChainIdFromProvider(context.transferStarter.destinationChainProvider) + ]) + const deposit = context.isDepositMode const dialog = `confirm_cctp_${deposit ? 'deposit' : 'withdrawal'}` as const @@ -32,4 +49,96 @@ export const stepGeneratorForCctp: UiDriverStepGenerator = async function* ( txRequestLabel: 'stepGeneratorForCctp.approveToken' }) } + + const request = await context.transferStarter.transferPrepareTxRequest({ + amount: context.amountBigNumber, + from: context.walletAddress, + destinationAddress: context.destinationAddress, + wagmiConfig: context.wagmiConfig + }) + + // narrow the union type + if (!isSimulateContractReturnType(request)) { + throw new Error( + `Expected "SimulateContractReturnType" for wagmi transaction` + ) + } + + const receipt = yield* stepGeneratorForTransactionWagmi(context, { + txRequest: request, + txRequestLabel: 'stepGeneratorForCctp.transfer' + }) + + if (typeof receipt === 'undefined') { + return + } + + yield* step({ + type: 'analytics', + payload: { + event: context.isDepositMode ? 'CCTP Deposit' : 'CCTP Withdrawal', + properties: { + accountType: context.isSmartContractWallet ? 'Smart Contract' : 'EOA', + network: getNetworkName( + context.isDepositMode ? destinationChainId : sourceChainId + ), + amount: Number(context.amount), + complete: false, + version: 2 + } + } + }) + + yield* step({ + type: 'tx_history_add', + payload: await createMergedTransaction( + { ...context, sourceChainId, destinationChainId }, + receipt.transactionHash + ) + }) +} + +async function createMergedTransaction( + { + isDepositMode, + walletAddress, + destinationAddress, + amount, + sourceChainId, + destinationChainId + }: UiDriverContext & { sourceChainId: number; destinationChainId: number }, + depositForBurnTxHash: string +): Promise { + const childChainId = isDepositMode ? destinationChainId : sourceChainId + const parentChainId = isDepositMode ? sourceChainId : destinationChainId + + return { + txId: depositForBurnTxHash, + asset: 'USDC', + assetType: AssetType.ERC20, + blockNum: null, + createdAt: dayjs().valueOf(), + direction: isDepositMode ? 'deposit' : 'withdraw', + isWithdrawal: !isDepositMode, + resolvedAt: null, + status: 'pending', + uniqueId: null, + value: amount, + depositStatus: DepositStatus.CCTP_DEFAULT_STATE, + destination: destinationAddress ?? walletAddress, + sender: walletAddress, + isCctp: true, + tokenAddress: getUsdcTokenAddressFromSourceChainId(sourceChainId), + cctpData: { + sourceChainId, + attestationHash: null, + messageBytes: null, + receiveMessageTransactionHash: null, + receiveMessageTimestamp: null + }, + parentChainId, + childChainId, + sourceChainId, + destinationChainId + } }