Skip to content

Commit c68741e

Browse files
committed
feat(transaction-controller): add configurable atomic option to batch transactions
1 parent 4ed9795 commit c68741e

6 files changed

Lines changed: 148 additions & 3 deletions

File tree

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
13+
1014
## [63.3.1]
1115

1216
### Changed

packages/transaction-controller/src/types.ts

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

18101818
/** 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
@@ -865,6 +865,56 @@ describe('Batch Utils', () => {
865865
);
866866
});
867867

868+
it('passes atomic option to generateEIP7702BatchTransaction', async () => {
869+
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
870+
delegationAddress: undefined,
871+
isSupported: true,
872+
});
873+
874+
addTransactionMock.mockResolvedValueOnce({
875+
transactionMeta: TRANSACTION_META_MOCK,
876+
result: Promise.resolve(''),
877+
});
878+
879+
generateEIP7702BatchTransactionMock.mockReturnValueOnce(
880+
TRANSACTION_BATCH_PARAMS_MOCK,
881+
);
882+
883+
request.request.atomic = false;
884+
885+
await addTransactionBatch(request);
886+
887+
expect(generateEIP7702BatchTransactionMock).toHaveBeenCalledWith(
888+
FROM_MOCK,
889+
expect.any(Array),
890+
{ atomic: false },
891+
);
892+
});
893+
894+
it('passes atomic as undefined to generateEIP7702BatchTransaction by default', async () => {
895+
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
896+
delegationAddress: undefined,
897+
isSupported: true,
898+
});
899+
900+
addTransactionMock.mockResolvedValueOnce({
901+
transactionMeta: TRANSACTION_META_MOCK,
902+
result: Promise.resolve(''),
903+
});
904+
905+
generateEIP7702BatchTransactionMock.mockReturnValueOnce(
906+
TRANSACTION_BATCH_PARAMS_MOCK,
907+
);
908+
909+
await addTransactionBatch(request);
910+
911+
expect(generateEIP7702BatchTransactionMock).toHaveBeenCalledWith(
912+
FROM_MOCK,
913+
expect.any(Array),
914+
{ atomic: undefined },
915+
);
916+
});
917+
868918
it('throws if chain not supported', async () => {
869919
doesChainSupportEIP7702Mock.mockReturnValue(false);
870920

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

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

296296
const {
297+
atomic,
297298
batchId: batchIdOverride,
298299
disableUpgrade,
299300
from,
@@ -357,7 +358,13 @@ async function addTransactionBatchWith7702(
357358
),
358359
);
359360

360-
const batchParams = generateEIP7702BatchTransaction(from, nestedTransactions);
361+
const batchParams = generateEIP7702BatchTransaction(
362+
from,
363+
nestedTransactions,
364+
{
365+
atomic,
366+
},
367+
);
361368

362369
const txParams: TransactionParams = {
363370
...batchParams,

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ const ETH_QUERY_MOCK = {} as EthQuery;
4545
const DATA_MOCK =
4646
'0xe9ae5c530100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000009876543210987654321098765432109876543210000000000000000000000000000000000000000000000000000000000005678000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd000000000000000000000000000000000000000000000000000000000000def0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000029abc000000000000000000000000000000000000000000000000000000000000';
4747

48+
const DATA_NON_ATOMIC_MOCK =
49+
'0xe9ae5c530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000009876543210987654321098765432109876543210000000000000000000000000000000000000000000000000000000000005678000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd000000000000000000000000000000000000000000000000000000000000def0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000029abc000000000000000000000000000000000000000000000000000000000000';
50+
4851
const DATA_EMPTY_MOCK =
4952
'0xe9ae5c5301000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000';
5053

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

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

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,18 @@ export async function isAccountUpgradedToEIP7702(
114114
*
115115
* @param from - The sender address.
116116
* @param transactions - The transactions to batch.
117+
* @param options - Options bag.
118+
* @param options.atomic - Whether the batch should be atomic. Defaults to `true`.
119+
* When `true`, mode `0x01` is used and all calls revert together.
120+
* When `false`, mode `0x00` is used and individual calls can fail independently.
117121
* @returns The batch transaction.
118122
*/
119123
export function generateEIP7702BatchTransaction(
120124
from: Hex,
121125
transactions: BatchTransactionParams[],
126+
options?: { atomic?: boolean },
122127
): BatchTransactionParams {
128+
const atomic = options?.atomic ?? true;
123129
const erc7821Contract = Contract.getInterface(ABI_IERC7821);
124130

125131
const calls = transactions.map((transaction) => {
@@ -132,8 +138,7 @@ export function generateEIP7702BatchTransaction(
132138
];
133139
});
134140

135-
// Single batch mode, no opData.
136-
const mode = '0x01'.padEnd(66, '0');
141+
const mode = (atomic ? '0x01' : '0x00').padEnd(66, '0');
137142

138143
const callData = defaultAbiCoder.encode([CALLS_SIGNATURE], [calls]);
139144

0 commit comments

Comments
 (0)