Skip to content

Commit b9cb503

Browse files
feat: support atomic batch transactions (#5306)
## Explanation Support atomic batch transactions via EIP-7702, and ERC-7821. Specifically: - Add `addTransactionBatch` method with `TransactionBatchRequest` and `TransactionBatchResult` types. - Encode multiple transactions into single `execute` call using ERC-7821 ABI. - Automatically upgrade account via `setCode` transaction if needed. - Add `isAtomicBatchSupported` method to identify which chains support atomic batch for a given account. - Add new `batch` `TransactionType`. - Add `batch` utils to encapsulate all batch-related logic. - Add `feature-flags` utils to encapsulate retrieval and fallback of LaunchDarkly configuration. - Currently EIP-7702 chains and contract addresses. - Validate `to` of external transaction is not an internal account unless `transactionType` is `batch`. ## References Fixes [#4096](MetaMask/MetaMask-planning#4096) ## Changelog See `CHANGELOG.md`. ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
1 parent 0223bed commit b9cb503

21 files changed

+1411
-74
lines changed

eslint-warning-thresholds.json

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -552,18 +552,12 @@
552552
},
553553
"packages/transaction-controller/src/TransactionController.test.ts": {
554554
"import-x/namespace": 1,
555-
"import-x/order": 4,
556-
"jsdoc/tag-lines": 1,
557555
"promise/always-return": 2
558556
},
559557
"packages/transaction-controller/src/TransactionController.ts": {
560558
"jsdoc/check-tag-names": 35,
561559
"jsdoc/require-returns": 5
562560
},
563-
"packages/transaction-controller/src/TransactionControllerIntegration.test.ts": {
564-
"import-x/order": 4,
565-
"jsdoc/tag-lines": 1
566-
},
567561
"packages/transaction-controller/src/api/accounts-api.test.ts": {
568562
"import-x/order": 1,
569563
"jsdoc/tag-lines": 1

packages/transaction-controller/CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- Support atomic batch transactions ([#5306](https://github.com/MetaMask/core/pull/5306))
13+
- Add methods:
14+
- `addTransactionBatch`
15+
- `isAtomicBatchSupported`
16+
- Add `batch` to `TransactionType`.
17+
- Add `nestedTransactions` to `TransactionMeta`.
18+
- Add new types:
19+
- `BatchTransactionParams`
20+
- `TransactionBatchSingleRequest`
21+
- `TransactionBatchRequest`
22+
- `TransactionBatchResult`
23+
- Add dependency on `@metamask/remote-feature-flag-controller:^1.4.0`.
24+
25+
### Changed
26+
27+
- **BREAKING:** Support atomic batch transactions ([#5306](https://github.com/MetaMask/core/pull/5306))
28+
- Require `AccountsController:getState` action permission in messenger.
29+
- Require `RemoteFeatureFlagController:getState` action permission in messenger.
30+
1031
## [46.0.0]
1132

1233
### Added

packages/transaction-controller/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ module.exports = merge(baseConfig, {
1818
coverageThreshold: {
1919
global: {
2020
branches: 91.76,
21-
functions: 94.57,
21+
functions: 93.69,
2222
lines: 96.83,
2323
statements: 96.82,
2424
},

packages/transaction-controller/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@metamask/eth-query": "^4.0.0",
5959
"@metamask/metamask-eth-abis": "^3.1.1",
6060
"@metamask/nonce-tracker": "^6.0.0",
61+
"@metamask/remote-feature-flag-controller": "^1.4.0",
6162
"@metamask/rpc-errors": "^7.0.2",
6263
"@metamask/utils": "^11.1.0",
6364
"async-mutex": "^0.5.0",
@@ -96,7 +97,8 @@
9697
"@metamask/approval-controller": "^7.0.0",
9798
"@metamask/eth-block-tracker": ">=9",
9899
"@metamask/gas-fee-controller": "^22.0.0",
99-
"@metamask/network-controller": "^22.0.0"
100+
"@metamask/network-controller": "^22.0.0",
101+
"@metamask/remote-feature-flag-controller": "^1.3.0"
100102
},
101103
"engines": {
102104
"node": "^18.18 || >=20"

packages/transaction-controller/src/TransactionController.test.ts

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,6 @@ import { createDeferredPromise } from '@metamask/utils';
3333
import assert from 'assert';
3434
import * as uuidModule from 'uuid';
3535

36-
import { FakeBlockTracker } from '../../../tests/fake-block-tracker';
37-
import { FakeProvider } from '../../../tests/fake-provider';
38-
import { flushPromises } from '../../../tests/helpers';
39-
import {
40-
buildCustomNetworkClientConfiguration,
41-
buildMockGetNetworkClientById,
42-
} from '../../network-controller/tests/helpers';
4336
import { getAccountAddressRelationship } from './api/accounts-api';
4437
import { CHAIN_IDS } from './constants';
4538
import { DefaultGasFeeFlow } from './gas-flows/DefaultGasFeeFlow';
@@ -80,6 +73,7 @@ import {
8073
TransactionType,
8174
WalletDevice,
8275
} from './types';
76+
import { addTransactionBatch } from './utils/batch';
8377
import { addGasBuffer, estimateGas, updateGas } from './utils/gas';
8478
import { updateGasFees } from './utils/gas-fees';
8579
import { getGasFeeFlow } from './utils/gas-flow';
@@ -92,6 +86,13 @@ import {
9286
updatePostTransactionBalance,
9387
updateSwapsTransaction,
9488
} from './utils/swaps';
89+
import { FakeBlockTracker } from '../../../tests/fake-block-tracker';
90+
import { FakeProvider } from '../../../tests/fake-provider';
91+
import { flushPromises } from '../../../tests/helpers';
92+
import {
93+
buildCustomNetworkClientConfiguration,
94+
buildMockGetNetworkClientById,
95+
} from '../../network-controller/tests/helpers';
9596

9697
type UnrestrictedMessenger = Messenger<
9798
TransactionControllerActions | AllowedActions,
@@ -111,6 +112,7 @@ jest.mock('./helpers/IncomingTransactionHelper');
111112
jest.mock('./helpers/MethodDataHelper');
112113
jest.mock('./helpers/MultichainTrackingHelper');
113114
jest.mock('./helpers/PendingTransactionTracker');
115+
jest.mock('./utils/batch');
114116
jest.mock('./utils/gas');
115117
jest.mock('./utils/gas-fees');
116118
jest.mock('./utils/gas-flow');
@@ -276,6 +278,7 @@ function buildMockBlockTracker(
276278

277279
/**
278280
* Builds a mock gas fee flow.
281+
*
279282
* @returns The mocked gas fee flow.
280283
*/
281284
function buildMockGasFeeFlow(): jest.Mocked<GasFeeFlow> {
@@ -488,6 +491,7 @@ describe('TransactionController', () => {
488491
const getAccountAddressRelationshipMock = jest.mocked(
489492
getAccountAddressRelationship,
490493
);
494+
const addTransactionBatchMock = jest.mocked(addTransactionBatch);
491495
const methodDataHelperClassMock = jest.mocked(MethodDataHelper);
492496

493497
let mockEthQuery: EthQuery;
@@ -638,6 +642,7 @@ describe('TransactionController', () => {
638642
'NetworkController:getNetworkClientById',
639643
'NetworkController:findNetworkClientIdByChainId',
640644
'AccountsController:getSelectedAccount',
645+
'AccountsController:getState',
641646
],
642647
allowedEvents: [],
643648
});
@@ -648,6 +653,11 @@ describe('TransactionController', () => {
648653
mockGetSelectedAccount,
649654
);
650655

656+
unrestrictedMessenger.registerActionHandler(
657+
'AccountsController:getState',
658+
() => ({}) as never,
659+
);
660+
651661
const controller = new TransactionController({
652662
...otherOptions,
653663
messenger: restrictedMessenger,
@@ -1371,8 +1381,6 @@ describe('TransactionController', () => {
13711381
const mockDeviceConfirmedOn = WalletDevice.OTHER;
13721382
const mockOrigin = 'origin';
13731383
const mockSecurityAlertResponse = {
1374-
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
1375-
// eslint-disable-next-line @typescript-eslint/naming-convention
13761384
result_type: 'Malicious',
13771385
reason: 'blur_farming',
13781386
description:
@@ -1571,6 +1579,7 @@ describe('TransactionController', () => {
15711579
deviceConfirmedOn: undefined,
15721580
id: expect.any(String),
15731581
isFirstTimeInteraction: undefined,
1582+
nestedTransactions: undefined,
15741583
networkClientId: NETWORK_CLIENT_ID_MOCK,
15751584
origin: undefined,
15761585
securityAlertResponse: undefined,
@@ -4166,8 +4175,6 @@ describe('TransactionController', () => {
41664175
const key = 'testKey';
41674176
const value = 123;
41684177

4169-
// TODO: Replace `any` with type
4170-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41714178
incomingTransactionHelperClassMock.mock.calls[0][0].updateCache(
41724179
(cache) => {
41734180
cache[key] = value;
@@ -4467,24 +4474,18 @@ describe('TransactionController', () => {
44674474
txParams: { ...TRANSACTION_META_MOCK.txParams, nonce: '0x1' },
44684475
};
44694476

4470-
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
4471-
// eslint-disable-next-line @typescript-eslint/naming-convention
44724477
const duplicate_1 = {
44734478
...confirmed,
44744479
id: 'testId2',
44754480
status: TransactionStatus.submitted,
44764481
};
44774482

4478-
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
4479-
// eslint-disable-next-line @typescript-eslint/naming-convention
44804483
const duplicate_2 = {
44814484
...duplicate_1,
44824485
id: 'testId3',
44834486
status: TransactionStatus.approved,
44844487
};
44854488

4486-
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
4487-
// eslint-disable-next-line @typescript-eslint/naming-convention
44884489
const duplicate_3 = {
44894490
...duplicate_1,
44904491
id: 'testId4',
@@ -5106,8 +5107,6 @@ describe('TransactionController', () => {
51065107

51075108
controller.updateSecurityAlertResponse(transactionMeta.id, {
51085109
reason: 'NA',
5109-
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
5110-
// eslint-disable-next-line @typescript-eslint/naming-convention
51115110
result_type: 'Benign',
51125111
});
51135112

@@ -5129,8 +5128,6 @@ describe('TransactionController', () => {
51295128
// @ts-expect-error Intentionally passing invalid input
51305129
controller.updateSecurityAlertResponse(undefined, {
51315130
reason: 'NA',
5132-
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
5133-
// eslint-disable-next-line @typescript-eslint/naming-convention
51345131
result_type: 'Benign',
51355132
}),
51365133
).toThrow(
@@ -5197,8 +5194,6 @@ describe('TransactionController', () => {
51975194
expect(() =>
51985195
controller.updateSecurityAlertResponse('456', {
51995196
reason: 'NA',
5200-
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
5201-
// eslint-disable-next-line @typescript-eslint/naming-convention
52025197
result_type: 'Benign',
52035198
}),
52045199
).toThrow(
@@ -6115,4 +6110,26 @@ describe('TransactionController', () => {
61156110
expect(transaction?.isActive).toBe(true);
61166111
});
61176112
});
6113+
6114+
describe('addTransactionBatch', () => {
6115+
it('invokes util', async () => {
6116+
const { controller } = setupController();
6117+
6118+
await controller.addTransactionBatch({
6119+
from: ACCOUNT_MOCK,
6120+
networkClientId: NETWORK_CLIENT_ID_MOCK,
6121+
transactions: [
6122+
{
6123+
params: {
6124+
to: ACCOUNT_2_MOCK,
6125+
data: '0x123456',
6126+
value: '0x123',
6127+
},
6128+
},
6129+
],
6130+
});
6131+
6132+
expect(addTransactionBatchMock).toHaveBeenCalledTimes(1);
6133+
});
6134+
});
61186135
});

0 commit comments

Comments
 (0)