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 6d1425657c..26853b602c 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -392,6 +392,33 @@ export function TransferPanel() { case 'dialog': { return confirmDialog(step.payload) } + + case 'scw_tooltip': { + showDelayedSmartContractTxRequest() + return + } + + case 'tx': { + try { + const tx = await signer!.sendTransaction(step.payload.txRequest) + const txReceipt = await tx.wait() + + return { data: txReceipt } + } catch (error) { + // capture error and show toast for anything that's not user rejecting error + if (!isUserRejectedError(error)) { + handleError({ + error, + label: step.payload.txRequestLabel, + category: 'transaction_signing' + }) + + errorToast(`${(error as Error)?.message ?? error}`) + } + + return { error: error as unknown as Error } + } + } } } @@ -399,6 +426,9 @@ export function TransferPanel() { if (!selectedToken) { return } + if (!walletAddress) { + throw new Error(`walletAddress is undefined`) + } if (!signer) { throw new Error(signerUndefinedError) } @@ -412,11 +442,18 @@ export function TransferPanel() { const { sourceChainProvider, destinationChainProvider, sourceChain } = latestNetworks.current + const cctpTransferStarter = new CctpTransferStarter({ + sourceChainProvider, + destinationChainProvider + }) + const returnEarly = await drive(stepGeneratorForCctp, stepExecutor, { + amountBigNumber, isDepositMode, isSmartContractWallet, walletAddress, - destinationAddress + destinationAddress, + transferStarter: cctpTransferStarter }) // this is only necessary while we are migrating to the ui driver @@ -427,49 +464,6 @@ export function TransferPanel() { return } - const cctpTransferStarter = new CctpTransferStarter({ - sourceChainProvider, - destinationChainProvider - }) - - const isTokenApprovalRequired = - await cctpTransferStarter.requiresTokenApproval({ - amount: amountBigNumber, - owner: await signer.getAddress() - }) - - if (isTokenApprovalRequired) { - const userConfirmation = await confirmDialog('approve_token') - if (!userConfirmation) return false - - if (isSmartContractWallet) { - showDelayedSmartContractTxRequest() - } - try { - const tx = await cctpTransferStarter.approveToken({ - signer, - amount: amountBigNumber - }) - - await tx.wait() - } catch (error) { - if (isUserRejectedError(error)) { - return - } - handleError({ - error, - label: 'cctp_approve_token', - category: 'token_approval' - }) - errorToast( - `USDC approval transaction failed: ${ - (error as Error)?.message ?? error - }` - ) - return - } - } - let depositForBurnTx try { 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 af4b1538a1..80dc0eb0ba 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 @@ -1,4 +1,4 @@ -import { Provider } from '@ethersproject/providers' +import { Provider, TransactionRequest } from '@ethersproject/providers' import { BigNumber, ContractTransaction, Signer } from 'ethers' import { Config } from 'wagmi' @@ -99,6 +99,10 @@ export type RequiresTokenApprovalProps = { destinationAddress?: string } +export type ApproveTokenPrepareTxRequestProps = { + amount?: BigNumber +} + export type ApproveTokenProps = { signer: Signer amount?: BigNumber @@ -147,6 +151,16 @@ export abstract class BridgeTransferStarter { props: RequiresTokenApprovalProps ): Promise + // not marking this as abstract for now, as we need a dummy implementation for every class + // only cctp is going to override it for now, and we'll do the same for others one by one + // finally, once we have all implementations we'll mark it as abstract + public async approveTokenPrepareTxRequest( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + props?: ApproveTokenPrepareTxRequestProps + ): Promise { + return {} as TransactionRequest + } + public abstract approveTokenEstimateGas( props: ApproveTokenProps ): Promise diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/CctpTransferStarter.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/CctpTransferStarter.ts index f769cf10b2..ae651eac7f 100644 --- a/packages/arb-token-bridge-ui/src/token-bridge-sdk/CctpTransferStarter.ts +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/CctpTransferStarter.ts @@ -4,6 +4,7 @@ import { TransactionRequest } from '@ethersproject/providers' import { ERC20__factory } from '@arbitrum/sdk/dist/lib/abi/factories/ERC20__factory' import { + ApproveTokenPrepareTxRequestProps, ApproveTokenProps, BridgeTransferStarter, RequiresTokenApprovalProps, @@ -50,9 +51,9 @@ export class CctpTransferStarter extends BridgeTransferStarter { return allowance.lt(amount) } - public async approveTokenPrepareTxRequest(params?: { - amount: BigNumber | undefined - }): Promise { + public async approveTokenPrepareTxRequest( + props?: ApproveTokenPrepareTxRequestProps + ): Promise { const { // usdcContractAddress, @@ -63,7 +64,7 @@ export class CctpTransferStarter extends BridgeTransferStarter { to: usdcContractAddress, data: ERC20__factory.createInterface().encodeFunctionData('approve', [ tokenMessengerContractAddress, - params?.amount ?? constants.MaxUint256 + props?.amount ?? constants.MaxUint256 ]), value: BigNumber.from(0) } 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 69f86b9d2c..5642c8aa8f 100644 --- a/packages/arb-token-bridge-ui/src/ui-driver/UiDriver.ts +++ b/packages/arb-token-bridge-ui/src/ui-driver/UiDriver.ts @@ -1,3 +1,6 @@ +import { BigNumber, providers } from 'ethers' +import { BridgeTransferStarter } from '@/token-bridge-sdk/BridgeTransferStarter' + import { DialogType } from '../components/common/Dialog2' export type Dialog = Extract< @@ -5,39 +8,64 @@ export type Dialog = Extract< | 'confirm_cctp_deposit' | 'confirm_cctp_withdrawal' | 'scw_custom_destination_address' + | 'approve_token' > export type UiDriverContext = { + amountBigNumber: BigNumber isDepositMode: boolean isSmartContractWallet: boolean - walletAddress?: string + walletAddress: string destinationAddress?: string + transferStarter: BridgeTransferStarter } export type UiDriverStep = | { type: 'start' } // | { type: 'return' } | { type: 'dialog'; payload: Dialog } + | { type: 'scw_tooltip' } + | { + type: 'tx' + payload: { + txRequest: providers.TransactionRequest + txRequestLabel: string + } + } -export type UiDriverStepResultFor = // - TStep extends { type: 'start' } +export type UiDriverStepType = UiDriverStep['type'] + +export type UiDriverStepPayloadFor = + Extract extends { + payload: infer TPayload + } + ? TPayload + : never + +type Result = + | { data: T; error?: undefined } + | { data?: undefined; error: Error } + +export type UiDriverStepResultFor = + TStepType extends 'start' ? void - : // - TStep extends { type: 'return' } + : TStepType extends 'return' ? void - : // - TStep extends { type: 'dialog' } + : TStepType extends 'dialog' ? boolean - : // - never + : TStepType extends 'scw_tooltip' + ? void + : TStepType extends 'tx' + ? Result + : never export type UiDriverStepGenerator = ( context: UiDriverContext -) => AsyncGenerator> +) => AsyncGenerator> export type UiDriverStepExecutor = ( step: TStep -) => Promise> +) => Promise> // TypeScript doesn't to the greatest job with generators // This 2nd generator helps with types both for params and result when yielding a step @@ -45,8 +73,8 @@ export async function* step( step: TStep ): AsyncGenerator< TStep, - UiDriverStepResultFor, - UiDriverStepResultFor + UiDriverStepResultFor, + UiDriverStepResultFor > { return yield step } diff --git a/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCctp.test.ts b/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCctp.test.ts index 6a0ed2f169..4eee4ca6cc 100644 --- a/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCctp.test.ts +++ b/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCctp.test.ts @@ -1,8 +1,150 @@ import { it } from 'vitest' +import { BigNumber } from 'ethers' +import { + TransactionRequest, + TransactionReceipt +} from '@ethersproject/providers' +import { BridgeTransferStarter } from '@/token-bridge-sdk/BridgeTransferStarter' +import { UiDriverContext, UiDriverStep } from './UiDriver' import { stepGeneratorForCctp } from './UiDriverCctp' import { nextStep, expectStep } from './UiDriverTestUtils' +const mockedApproveTokenTxRequest = { + to: '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238', + data: '0x095ea7b30000000000000000000000009f3b8679c73c2fef8b59b4f3444d4e156fb70aa5ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + value: BigNumber.from(0) +} + +function approveTokenPayload(txRequest: TransactionRequest) { + return { + txRequest, + txRequestLabel: 'stepGeneratorForCctp.approveToken' + } +} + +type UiDriverTestCaseStep = { + description: string + userInput?: any + expectedStep: UiDriverStep | undefined +} + +type UiDriverTestCase = { + name: string + context: UiDriverContext + sequence: UiDriverTestCaseStep[] +} + +const dialog = { + confirm: () => [true], + reject: () => [false] +} + +const testCases: UiDriverTestCase[] = [ + { + name: 'eoa :: deposit :: user rejects "confirm_cctp_deposit" dialog', + context: { + isDepositMode: true, + isSmartContractWallet: false + } as UiDriverContext, + sequence: [ + { + description: '"confirm_cctp_deposit" dialog is opened', + expectedStep: { + type: 'dialog', + payload: 'confirm_cctp_deposit' + } + }, + { + description: 'user rejects dialog', + userInput: dialog.reject(), + expectedStep: { + type: 'return' + } + } + ] + }, + { + name: 'scw :: deposit :: user confirms all dialogs and token approval succeeds', + context: { + amountBigNumber: BigNumber.from(1), + isDepositMode: true, + isSmartContractWallet: true, + walletAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + destinationAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + transferStarter: { + requiresTokenApproval: () => true, + approveTokenPrepareTxRequest: () => mockedApproveTokenTxRequest + } as unknown as BridgeTransferStarter + } as UiDriverContext, + sequence: [ + { + description: '"confirm_cctp_deposit" dialog is opened', + expectedStep: { type: 'dialog', payload: 'confirm_cctp_deposit' } + }, + { + description: 'user confirms deposit dialog', + userInput: dialog.confirm(), + expectedStep: { + type: 'dialog', + payload: 'scw_custom_destination_address' + } + }, + { + description: 'user confirms scw destination address dialog', + userInput: dialog.confirm(), + expectedStep: { type: 'dialog', payload: 'approve_token' } + }, + { + description: 'user confirms approve token dialog', + userInput: dialog.confirm(), + expectedStep: { type: 'scw_tooltip' } + }, + { + description: 'token approval transaction is prepared', + expectedStep: { + type: 'tx', + payload: approveTokenPayload(mockedApproveTokenTxRequest) + } + }, + { + description: 'token approval transaction succeeds', + userInput: [{ data: {} as TransactionReceipt }], + expectedStep: undefined + } + ] + } +] + +testCases.forEach(({ name, context, sequence }) => { + it(name, async () => { + const generator = stepGeneratorForCctp(context) + + expectStep(await nextStep(generator)) + // + .hasType('start') + + sequence.forEach(async ({ userInput, expectedStep }) => { + if (typeof expectedStep === 'undefined') { + expectStep(await nextStep(generator, userInput)) + // + .doesNotExist() + return + } + + if ('payload' in expectedStep) { + expectStep(await nextStep(generator, userInput)) + .hasType(expectedStep.type) + .hasPayload(expectedStep.payload) + } else { + expectStep(await nextStep(generator, userInput)) + // + .hasType(expectedStep.type) + } + }) + }) +}) + it(` context: isDepositMode=true @@ -11,17 +153,22 @@ it(` user actions: 1. user rejects "confirm_cctp_deposit" dialog `, async () => { - const generator = stepGeneratorForCctp({ + const context: UiDriverContext = { isDepositMode: true, isSmartContractWallet: false - }) + } as UiDriverContext + + const generator = stepGeneratorForCctp(context) - expectStep(await nextStep(generator)).hasType('start') + expectStep(await nextStep(generator)) + // + .hasType('start') expectStep(await nextStep(generator)) .hasType('dialog') .hasPayload('confirm_cctp_deposit') - expectStep(await nextStep(generator, [false])).hasType('return') - expectStep(await nextStep(generator)).doesNotExist() + expectStep(await nextStep(generator, [false])) + // + .hasType('return') }) it(` @@ -32,17 +179,22 @@ it(` user actions: 1. user rejects "confirm_cctp_withdrawal" dialog `, async () => { - const generator = stepGeneratorForCctp({ + const context: UiDriverContext = { isDepositMode: false, isSmartContractWallet: false - }) + } as UiDriverContext - expectStep(await nextStep(generator)).hasType('start') + const generator = stepGeneratorForCctp(context) + + expectStep(await nextStep(generator)) + // + .hasType('start') expectStep(await nextStep(generator)) .hasType('dialog') .hasPayload('confirm_cctp_withdrawal') - expectStep(await nextStep(generator, [false])).hasType('return') - expectStep(await nextStep(generator)).doesNotExist() + expectStep(await nextStep(generator, [false])) + // + .hasType('return') }) it(` @@ -52,21 +204,168 @@ it(` walletAddress=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 destinationAddress=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 + additional context: + 1. token requires approval + user actions: 1. user confirms "confirm_cctp_deposit" dialog + 2. user rejects "approve_token" dialog `, async () => { - const generator = stepGeneratorForCctp({ + const context: UiDriverContext = { + amountBigNumber: BigNumber.from(1), isDepositMode: true, isSmartContractWallet: false, walletAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - destinationAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' - }) + destinationAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + transferStarter: { + requiresTokenApproval: () => true + } as unknown as BridgeTransferStarter + } + + const generator = stepGeneratorForCctp(context) + + expectStep(await nextStep(generator)) + // + .hasType('start') + expectStep(await nextStep(generator)) + .hasType('dialog') + .hasPayload('confirm_cctp_deposit') + expectStep(await nextStep(generator, [true])) + .hasType('dialog') + .hasPayload('approve_token') + expectStep(await nextStep(generator, [false])) + // + .hasType('return') +}) + +it(` + context: + isDepositMode=true + isSmartContractWallet=false + walletAddress=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 + destinationAddress=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 + + additional context: + 1. token requires approval + + user actions: + 1. user confirms "confirm_cctp_deposit" dialog + 2. user confirms "approve_token" dialog + 3. token approval tx fails +`, async () => { + const context: UiDriverContext = { + amountBigNumber: BigNumber.from(1), + isDepositMode: true, + isSmartContractWallet: false, + walletAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + destinationAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + transferStarter: { + requiresTokenApproval: () => true, + approveTokenPrepareTxRequest: () => mockedApproveTokenTxRequest + } as unknown as BridgeTransferStarter + } + + const generator = stepGeneratorForCctp(context) + + expectStep(await nextStep(generator)) + // + .hasType('start') + expectStep(await nextStep(generator)) + .hasType('dialog') + .hasPayload('confirm_cctp_deposit') + expectStep(await nextStep(generator, [true])) + .hasType('dialog') + .hasPayload('approve_token') + expectStep(await nextStep(generator, [true])) + .hasType('tx') + .hasPayload(approveTokenPayload(mockedApproveTokenTxRequest)) + expectStep(await nextStep(generator, [{ error: new Error() }])) + // + .hasType('return') +}) + +it(` + context: + isDepositMode=true + isSmartContractWallet=false + walletAddress=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 + destinationAddress=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 + + additional context: + 1. token requires approval + + user actions: + 1. user confirms "confirm_cctp_deposit" dialog + 2. user confirms "approve_token" dialog + 3. token approval tx is successful +`, async () => { + const context: UiDriverContext = { + amountBigNumber: BigNumber.from(1), + isDepositMode: true, + isSmartContractWallet: false, + walletAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + destinationAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + transferStarter: { + requiresTokenApproval: () => true, + approveTokenPrepareTxRequest: () => mockedApproveTokenTxRequest + } as unknown as BridgeTransferStarter + } + + const generator = stepGeneratorForCctp(context) - expectStep(await nextStep(generator)).hasType('start') + expectStep(await nextStep(generator)) + // + .hasType('start') expectStep(await nextStep(generator)) .hasType('dialog') .hasPayload('confirm_cctp_deposit') - expectStep(await nextStep(generator, [true])).doesNotExist() + expectStep(await nextStep(generator, [true])) + .hasType('dialog') + .hasPayload('approve_token') + expectStep(await nextStep(generator, [true])) + .hasType('tx') + .hasPayload(approveTokenPayload(mockedApproveTokenTxRequest)) + expectStep(await nextStep(generator, [{ data: {} as TransactionReceipt }])) + // + .doesNotExist() +}) + +it(` + context: + isDepositMode=true + isSmartContractWallet=false + walletAddress=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 + destinationAddress=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 + + additional context: + 1. token does not require approval + + user actions: + 1. user confirms "confirm_cctp_deposit" dialog +`, async () => { + const context: UiDriverContext = { + amountBigNumber: BigNumber.from(1), + isDepositMode: true, + isSmartContractWallet: false, + walletAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + destinationAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + transferStarter: { + requiresTokenApproval: () => false, + approveTokenPrepareTxRequest: () => mockedApproveTokenTxRequest + } as unknown as BridgeTransferStarter + } + + const generator = stepGeneratorForCctp(context) + + expectStep(await nextStep(generator)) + // + .hasType('start') + expectStep(await nextStep(generator)) + .hasType('dialog') + .hasPayload('confirm_cctp_deposit') + expectStep(await nextStep(generator, [true])) + // + .doesNotExist() }) it(` @@ -80,22 +379,27 @@ it(` 1. user confirms "confirm_cctp_deposit" dialog 2. user rejects "scw_custom_destination_address" dialog `, async () => { - const generator = stepGeneratorForCctp({ + const context: UiDriverContext = { isDepositMode: true, isSmartContractWallet: true, walletAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', destinationAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' - }) + } as UiDriverContext - expectStep(await nextStep(generator)).hasType('start') + const generator = stepGeneratorForCctp(context) + + expectStep(await nextStep(generator)) + // + .hasType('start') expectStep(await nextStep(generator)) .hasType('dialog') .hasPayload('confirm_cctp_deposit') expectStep(await nextStep(generator, [true])) .hasType('dialog') .hasPayload('scw_custom_destination_address') - expectStep(await nextStep(generator, [false])).hasType('return') - expectStep(await nextStep(generator)).doesNotExist() + expectStep(await nextStep(generator, [false])) + // + .hasType('return') }) it(` @@ -105,23 +409,48 @@ it(` walletAddress=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 destinationAddress=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 + additional context: + 1. token requires approval + user actions: 1. user confirms "confirm_cctp_deposit" dialog 2. user confirms "scw_custom_destination_address" dialog + 3. user confirms "approve_token" dialog + 4. token approval tx is successful `, async () => { - const generator = stepGeneratorForCctp({ + const context: UiDriverContext = { + amountBigNumber: BigNumber.from(1), isDepositMode: true, isSmartContractWallet: true, walletAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - destinationAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' - }) + destinationAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + transferStarter: { + requiresTokenApproval: () => true, + approveTokenPrepareTxRequest: () => mockedApproveTokenTxRequest + } as unknown as BridgeTransferStarter + } - expectStep(await nextStep(generator)).hasType('start') + const generator = stepGeneratorForCctp(context) + + expectStep(await nextStep(generator)) + // + .hasType('start') expectStep(await nextStep(generator)) .hasType('dialog') .hasPayload('confirm_cctp_deposit') expectStep(await nextStep(generator, [true])) .hasType('dialog') .hasPayload('scw_custom_destination_address') - expectStep(await nextStep(generator, [true])).doesNotExist() + expectStep(await nextStep(generator, [true])) + .hasType('dialog') + .hasPayload('approve_token') + expectStep(await nextStep(generator, [true])) + // + .hasType('scw_tooltip') + expectStep(await nextStep(generator)) + .hasType('tx') + .hasPayload(approveTokenPayload(mockedApproveTokenTxRequest)) + expectStep(await nextStep(generator, [{ data: {} as TransactionReceipt }])) + // + .doesNotExist() }) 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 885470e4b0..2a330e6423 100644 --- a/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCctp.ts +++ b/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCctp.ts @@ -1,7 +1,8 @@ import { step, UiDriverStepGenerator } from './UiDriver' import { stepGeneratorForDialog, - stepGeneratorForSmartContractWalletDestinationDialog + stepGeneratorForSmartContractWalletDestinationDialog, + stepGeneratorForTransaction } from './UiDriverCommon' export const stepGeneratorForCctp: UiDriverStepGenerator = async function* ( @@ -13,4 +14,22 @@ export const stepGeneratorForCctp: UiDriverStepGenerator = async function* ( yield* step({ type: 'start' }) yield* stepGeneratorForDialog(dialog) yield* stepGeneratorForSmartContractWalletDestinationDialog(context) + + const approval = await context.transferStarter.requiresTokenApproval({ + amount: context.amountBigNumber, + owner: context.walletAddress + }) + + if (approval) { + yield* stepGeneratorForDialog('approve_token') + + const request = await context.transferStarter.approveTokenPrepareTxRequest({ + amount: context.amountBigNumber + }) + + yield* stepGeneratorForTransaction(context, { + txRequest: request, + txRequestLabel: 'stepGeneratorForCctp.approveToken' + }) + } } diff --git a/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCommon.ts b/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCommon.ts index ef7e5dd1b1..3273931bb6 100644 --- a/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCommon.ts +++ b/packages/arb-token-bridge-ui/src/ui-driver/UiDriverCommon.ts @@ -1,8 +1,12 @@ +import { providers } from 'ethers' + import { step, UiDriverStep, + UiDriverStepPayloadFor, UiDriverStepResultFor, UiDriverStepGenerator, + UiDriverContext, Dialog } from './UiDriver' import { addressesEqual } from '../util/AddressUtils' @@ -11,7 +15,7 @@ export type UiDriverStepGeneratorForDialog< TStep extends UiDriverStep = UiDriverStep > = ( dialog: Dialog -) => AsyncGenerator> +) => AsyncGenerator> export const stepGeneratorForDialog: UiDriverStepGeneratorForDialog = async function* (payload: Dialog) { @@ -31,3 +35,29 @@ export const stepGeneratorForSmartContractWalletDestinationDialog: UiDriverStepG yield* stepGeneratorForDialog('scw_custom_destination_address') } } + +export type UiDriverStepGeneratorForTransaction< + TStep extends UiDriverStep = UiDriverStep +> = ( + context: UiDriverContext, + payload: UiDriverStepPayloadFor<'tx'> +) => AsyncGenerator< + TStep, + providers.TransactionReceipt | void, + UiDriverStepResultFor +> + +export const stepGeneratorForTransaction: UiDriverStepGeneratorForTransaction = + async function* (context, payload) { + if (context.isSmartContractWallet) { + yield* step({ type: 'scw_tooltip' }) + } + + const { error, data } = yield* step({ type: 'tx', payload }) + + if (typeof error !== 'undefined') { + yield* step({ type: 'return' }) + } else { + return data + } + } diff --git a/packages/arb-token-bridge-ui/src/ui-driver/UiDriverTestUtils.ts b/packages/arb-token-bridge-ui/src/ui-driver/UiDriverTestUtils.ts index 69d3f5e07c..3daf7b5ef8 100644 --- a/packages/arb-token-bridge-ui/src/ui-driver/UiDriverTestUtils.ts +++ b/packages/arb-token-bridge-ui/src/ui-driver/UiDriverTestUtils.ts @@ -3,8 +3,8 @@ import { expect } from 'vitest' import { UiDriverStep, UiDriverStepResultFor } from './UiDriver' export async function nextStep( - generator: AsyncGenerator>, - nextStepInputs: [] | [UiDriverStepResultFor] = [] + generator: AsyncGenerator>, + nextStepInputs: [] | [UiDriverStepResultFor] = [] ) { return (await generator.next(...nextStepInputs)).value } @@ -13,7 +13,7 @@ export function expectStep(step: TStep | void) { return { hasType(expectedStepType: TStepType) { expect(step).toBeDefined() - expect(step!.type).toBe(expectedStepType) + expect(step!.type).toEqual(expectedStepType) return expectStep(step as Extract) }, @@ -24,7 +24,7 @@ export function expectStep(step: TStep | void) { throw new Error(`Step of type "${step!.type}" does not have a payload.`) } - expect(step.payload).toBe(expectedStepPayload) + expect(step.payload).toEqual(expectedStepPayload) return this },