Skip to content

Commit de55719

Browse files
committed
feat(transaction-controller): add configurable atomic option to batch transactions
1 parent 054432d commit de55719

File tree

6 files changed

+148
-3
lines changed

6 files changed

+148
-3
lines changed

packages/transaction-controller/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- 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))
13+
1014
### Changed
1115

1216
- Bump `@metamask/accounts-controller` from `^37.1.0` to `^37.1.1` ([#8325](https://github.com/MetaMask/core/pull/8325))

packages/transaction-controller/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1801,6 +1801,14 @@ export type TransactionBatchSingleRequest = {
18011801
* Currently only atomic batches are supported via EIP-7702.
18021802
*/
18031803
export type TransactionBatchRequest = {
1804+
/**
1805+
* Whether the EIP-7702 batch transaction should be executed atomically.
1806+
* When `true` (default), all calls in the batch either succeed or revert together.
1807+
* When `false`, calls are independent — individual calls can fail without
1808+
* reverting the entire batch.
1809+
*/
1810+
atomic?: boolean;
1811+
18041812
batchId?: Hex;
18051813

18061814
/** Whether to disable batch transaction processing via an EIP-7702 upgraded account. */

packages/transaction-controller/src/utils/batch.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,56 @@ describe('Batch Utils', () => {
894894
);
895895
});
896896

897+
it('passes atomic option to generateEIP7702BatchTransaction', async () => {
898+
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
899+
delegationAddress: undefined,
900+
isSupported: true,
901+
});
902+
903+
addTransactionMock.mockResolvedValueOnce({
904+
transactionMeta: TRANSACTION_META_MOCK,
905+
result: Promise.resolve(''),
906+
});
907+
908+
generateEIP7702BatchTransactionMock.mockReturnValueOnce(
909+
TRANSACTION_BATCH_PARAMS_MOCK,
910+
);
911+
912+
request.request.atomic = false;
913+
914+
await addTransactionBatch(request);
915+
916+
expect(generateEIP7702BatchTransactionMock).toHaveBeenCalledWith(
917+
FROM_MOCK,
918+
expect.any(Array),
919+
{ atomic: false },
920+
);
921+
});
922+
923+
it('passes atomic as undefined to generateEIP7702BatchTransaction by default', async () => {
924+
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
925+
delegationAddress: undefined,
926+
isSupported: true,
927+
});
928+
929+
addTransactionMock.mockResolvedValueOnce({
930+
transactionMeta: TRANSACTION_META_MOCK,
931+
result: Promise.resolve(''),
932+
});
933+
934+
generateEIP7702BatchTransactionMock.mockReturnValueOnce(
935+
TRANSACTION_BATCH_PARAMS_MOCK,
936+
);
937+
938+
await addTransactionBatch(request);
939+
940+
expect(generateEIP7702BatchTransactionMock).toHaveBeenCalledWith(
941+
FROM_MOCK,
942+
expect.any(Array),
943+
{ atomic: undefined },
944+
);
945+
});
946+
897947
it('throws if chain not supported', async () => {
898948
doesChainSupportEIP7702Mock.mockReturnValue(false);
899949

packages/transaction-controller/src/utils/batch.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ async function addTransactionBatchWith7702(
284284
} = request;
285285

286286
const {
287+
atomic,
287288
batchId: batchIdOverride,
288289
disableUpgrade,
289290
from,
@@ -346,7 +347,13 @@ async function addTransactionBatchWith7702(
346347
),
347348
);
348349

349-
const batchParams = generateEIP7702BatchTransaction(from, nestedTransactions);
350+
const batchParams = generateEIP7702BatchTransaction(
351+
from,
352+
nestedTransactions,
353+
{
354+
atomic,
355+
},
356+
);
350357

351358
const txParams: TransactionParams = {
352359
...batchParams,

packages/transaction-controller/src/utils/eip7702.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ const NETWORK_CLIENT_ID_MOCK = 'testNetworkClientId' as NetworkClientId;
4444
const DATA_MOCK =
4545
'0xe9ae5c530100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000009876543210987654321098765432109876543210000000000000000000000000000000000000000000000000000000000005678000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd000000000000000000000000000000000000000000000000000000000000def0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000029abc000000000000000000000000000000000000000000000000000000000000';
4646

47+
const DATA_NON_ATOMIC_MOCK =
48+
'0xe9ae5c530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000009876543210987654321098765432109876543210000000000000000000000000000000000000000000000000000000000005678000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd000000000000000000000000000000000000000000000000000000000000def0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000029abc000000000000000000000000000000000000000000000000000000000000';
49+
4750
const DATA_EMPTY_MOCK =
4851
'0xe9ae5c5301000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000';
4952

@@ -431,6 +434,74 @@ describe('EIP-7702 Utils', () => {
431434
to: ADDRESS_MOCK,
432435
});
433436
});
437+
438+
it('uses atomic mode by default', () => {
439+
const result = generateEIP7702BatchTransaction(ADDRESS_MOCK, [
440+
{
441+
data: '0x1234',
442+
to: ADDRESS_2_MOCK,
443+
value: '0x5678',
444+
},
445+
{
446+
data: '0x9abc',
447+
to: ADDRESS_3_MOCK,
448+
value: '0xdef0',
449+
},
450+
]);
451+
452+
expect(result).toStrictEqual({
453+
data: DATA_MOCK,
454+
to: ADDRESS_MOCK,
455+
});
456+
});
457+
458+
it('uses atomic mode when atomic is true', () => {
459+
const result = generateEIP7702BatchTransaction(
460+
ADDRESS_MOCK,
461+
[
462+
{
463+
data: '0x1234',
464+
to: ADDRESS_2_MOCK,
465+
value: '0x5678',
466+
},
467+
{
468+
data: '0x9abc',
469+
to: ADDRESS_3_MOCK,
470+
value: '0xdef0',
471+
},
472+
],
473+
{ atomic: true },
474+
);
475+
476+
expect(result).toStrictEqual({
477+
data: DATA_MOCK,
478+
to: ADDRESS_MOCK,
479+
});
480+
});
481+
482+
it('uses non-atomic mode when atomic is false', () => {
483+
const result = generateEIP7702BatchTransaction(
484+
ADDRESS_MOCK,
485+
[
486+
{
487+
data: '0x1234',
488+
to: ADDRESS_2_MOCK,
489+
value: '0x5678',
490+
},
491+
{
492+
data: '0x9abc',
493+
to: ADDRESS_3_MOCK,
494+
value: '0xdef0',
495+
},
496+
],
497+
{ atomic: false },
498+
);
499+
500+
expect(result).toStrictEqual({
501+
data: DATA_NON_ATOMIC_MOCK,
502+
to: ADDRESS_MOCK,
503+
});
504+
});
434505
});
435506

436507
describe('getDelegationAddress', () => {

packages/transaction-controller/src/utils/eip7702.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,18 @@ export async function isAccountUpgradedToEIP7702(
126126
*
127127
* @param from - The sender address.
128128
* @param transactions - The transactions to batch.
129+
* @param options - Options bag.
130+
* @param options.atomic - Whether the batch should be atomic. Defaults to `true`.
131+
* When `true`, mode `0x01` is used and all calls revert together.
132+
* When `false`, mode `0x00` is used and individual calls can fail independently.
129133
* @returns The batch transaction.
130134
*/
131135
export function generateEIP7702BatchTransaction(
132136
from: Hex,
133137
transactions: BatchTransactionParams[],
138+
options?: { atomic?: boolean },
134139
): BatchTransactionParams {
140+
const atomic = options?.atomic ?? true;
135141
const erc7821Contract = Contract.getInterface(ABI_IERC7821);
136142

137143
const calls = transactions.map((transaction) => {
@@ -144,8 +150,7 @@ export function generateEIP7702BatchTransaction(
144150
];
145151
});
146152

147-
// Single batch mode, no opData.
148-
const mode = '0x01'.padEnd(66, '0');
153+
const mode = (atomic ? '0x01' : '0x00').padEnd(66, '0');
149154

150155
const callData = defaultAbiCoder.encode([CALLS_SIGNATURE], [calls]);
151156

0 commit comments

Comments
 (0)