From 6591450b88a9e51939fcd4d9e0e2a1f647384a7c Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 27 Mar 2026 00:30:33 +0000 Subject: [PATCH] feat(transaction-controller): add configurable atomic option to batch transactions --- packages/transaction-controller/CHANGELOG.md | 4 ++ packages/transaction-controller/src/types.ts | 8 +++ .../src/utils/batch.test.ts | 50 +++++++++++++ .../transaction-controller/src/utils/batch.ts | 9 ++- .../src/utils/eip7702.test.ts | 71 +++++++++++++++++++ .../src/utils/eip7702.ts | 9 ++- 6 files changed, 148 insertions(+), 3 deletions(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 055fdc1f8e2..fb8f2db342f 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add optional `atomic` property to `TransactionBatchRequest` to configure whether EIP-7702 batch calls revert together or can fail independently ([#8320](https://github.com/MetaMask/core/pull/8320)) + ## [63.3.1] ### Changed diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 7b35c5b7b03..e64d5dc1004 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -1805,6 +1805,14 @@ export type TransactionBatchSingleRequest = { * Currently only atomic batches are supported via EIP-7702. */ export type TransactionBatchRequest = { + /** + * Whether the EIP-7702 batch transaction should be executed atomically. + * When `true` (default), all calls in the batch either succeed or revert together. + * When `false`, calls are independent — individual calls can fail without + * reverting the entire batch. + */ + atomic?: boolean; + batchId?: Hex; /** Whether to disable batch transaction processing via an EIP-7702 upgraded account. */ diff --git a/packages/transaction-controller/src/utils/batch.test.ts b/packages/transaction-controller/src/utils/batch.test.ts index da077713bc8..391c82b9dcc 100644 --- a/packages/transaction-controller/src/utils/batch.test.ts +++ b/packages/transaction-controller/src/utils/batch.test.ts @@ -865,6 +865,56 @@ describe('Batch Utils', () => { ); }); + it('passes atomic option to generateEIP7702BatchTransaction', async () => { + isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({ + delegationAddress: undefined, + isSupported: true, + }); + + addTransactionMock.mockResolvedValueOnce({ + transactionMeta: TRANSACTION_META_MOCK, + result: Promise.resolve(''), + }); + + generateEIP7702BatchTransactionMock.mockReturnValueOnce( + TRANSACTION_BATCH_PARAMS_MOCK, + ); + + request.request.atomic = false; + + await addTransactionBatch(request); + + expect(generateEIP7702BatchTransactionMock).toHaveBeenCalledWith( + FROM_MOCK, + expect.any(Array), + { atomic: false }, + ); + }); + + it('passes atomic as undefined to generateEIP7702BatchTransaction by default', async () => { + isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({ + delegationAddress: undefined, + isSupported: true, + }); + + addTransactionMock.mockResolvedValueOnce({ + transactionMeta: TRANSACTION_META_MOCK, + result: Promise.resolve(''), + }); + + generateEIP7702BatchTransactionMock.mockReturnValueOnce( + TRANSACTION_BATCH_PARAMS_MOCK, + ); + + await addTransactionBatch(request); + + expect(generateEIP7702BatchTransactionMock).toHaveBeenCalledWith( + FROM_MOCK, + expect.any(Array), + { atomic: undefined }, + ); + }); + it('throws if chain not supported', async () => { doesChainSupportEIP7702Mock.mockReturnValue(false); diff --git a/packages/transaction-controller/src/utils/batch.ts b/packages/transaction-controller/src/utils/batch.ts index eb75a97e200..60376508eeb 100644 --- a/packages/transaction-controller/src/utils/batch.ts +++ b/packages/transaction-controller/src/utils/batch.ts @@ -294,6 +294,7 @@ async function addTransactionBatchWith7702( } = request; const { + atomic, batchId: batchIdOverride, disableUpgrade, from, @@ -357,7 +358,13 @@ async function addTransactionBatchWith7702( ), ); - const batchParams = generateEIP7702BatchTransaction(from, nestedTransactions); + const batchParams = generateEIP7702BatchTransaction( + from, + nestedTransactions, + { + atomic, + }, + ); const txParams: TransactionParams = { ...batchParams, diff --git a/packages/transaction-controller/src/utils/eip7702.test.ts b/packages/transaction-controller/src/utils/eip7702.test.ts index 6b769d4b159..cc40635db62 100644 --- a/packages/transaction-controller/src/utils/eip7702.test.ts +++ b/packages/transaction-controller/src/utils/eip7702.test.ts @@ -45,6 +45,9 @@ const ETH_QUERY_MOCK = {} as EthQuery; const DATA_MOCK = '0xe9ae5c530100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000009876543210987654321098765432109876543210000000000000000000000000000000000000000000000000000000000005678000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd000000000000000000000000000000000000000000000000000000000000def0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000029abc000000000000000000000000000000000000000000000000000000000000'; +const DATA_NON_ATOMIC_MOCK = + '0xe9ae5c530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000009876543210987654321098765432109876543210000000000000000000000000000000000000000000000000000000000005678000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd000000000000000000000000000000000000000000000000000000000000def0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000029abc000000000000000000000000000000000000000000000000000000000000'; + const DATA_EMPTY_MOCK = '0xe9ae5c5301000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000'; @@ -432,6 +435,74 @@ describe('EIP-7702 Utils', () => { to: ADDRESS_MOCK, }); }); + + it('uses atomic mode by default', () => { + const result = generateEIP7702BatchTransaction(ADDRESS_MOCK, [ + { + data: '0x1234', + to: ADDRESS_2_MOCK, + value: '0x5678', + }, + { + data: '0x9abc', + to: ADDRESS_3_MOCK, + value: '0xdef0', + }, + ]); + + expect(result).toStrictEqual({ + data: DATA_MOCK, + to: ADDRESS_MOCK, + }); + }); + + it('uses atomic mode when atomic is true', () => { + const result = generateEIP7702BatchTransaction( + ADDRESS_MOCK, + [ + { + data: '0x1234', + to: ADDRESS_2_MOCK, + value: '0x5678', + }, + { + data: '0x9abc', + to: ADDRESS_3_MOCK, + value: '0xdef0', + }, + ], + { atomic: true }, + ); + + expect(result).toStrictEqual({ + data: DATA_MOCK, + to: ADDRESS_MOCK, + }); + }); + + it('uses non-atomic mode when atomic is false', () => { + const result = generateEIP7702BatchTransaction( + ADDRESS_MOCK, + [ + { + data: '0x1234', + to: ADDRESS_2_MOCK, + value: '0x5678', + }, + { + data: '0x9abc', + to: ADDRESS_3_MOCK, + value: '0xdef0', + }, + ], + { atomic: false }, + ); + + expect(result).toStrictEqual({ + data: DATA_NON_ATOMIC_MOCK, + to: ADDRESS_MOCK, + }); + }); }); describe('getDelegationAddress', () => { diff --git a/packages/transaction-controller/src/utils/eip7702.ts b/packages/transaction-controller/src/utils/eip7702.ts index ce526f74292..da3a7f1614f 100644 --- a/packages/transaction-controller/src/utils/eip7702.ts +++ b/packages/transaction-controller/src/utils/eip7702.ts @@ -114,12 +114,18 @@ export async function isAccountUpgradedToEIP7702( * * @param from - The sender address. * @param transactions - The transactions to batch. + * @param options - Options bag. + * @param options.atomic - Whether the batch should be atomic. Defaults to `true`. + * When `true`, mode `0x01` is used and all calls revert together. + * When `false`, mode `0x00` is used and individual calls can fail independently. * @returns The batch transaction. */ export function generateEIP7702BatchTransaction( from: Hex, transactions: BatchTransactionParams[], + options?: { atomic?: boolean }, ): BatchTransactionParams { + const atomic = options?.atomic ?? true; const erc7821Contract = Contract.getInterface(ABI_IERC7821); const calls = transactions.map((transaction) => { @@ -132,8 +138,7 @@ export function generateEIP7702BatchTransaction( ]; }); - // Single batch mode, no opData. - const mode = '0x01'.padEnd(66, '0'); + const mode = (atomic ? '0x01' : '0x00').padEnd(66, '0'); const callData = defaultAbiCoder.encode([CALLS_SIGNATURE], [calls]);