Skip to content

Commit

Permalink
feat: support atomic batch transactions (#5306)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
matthewwalsh0 authored Feb 17, 2025
1 parent 0223bed commit b9cb503
Show file tree
Hide file tree
Showing 21 changed files with 1,411 additions and 74 deletions.
6 changes: 0 additions & 6 deletions eslint-warning-thresholds.json
Original file line number Diff line number Diff line change
Expand Up @@ -552,18 +552,12 @@
},
"packages/transaction-controller/src/TransactionController.test.ts": {
"import-x/namespace": 1,
"import-x/order": 4,
"jsdoc/tag-lines": 1,
"promise/always-return": 2
},
"packages/transaction-controller/src/TransactionController.ts": {
"jsdoc/check-tag-names": 35,
"jsdoc/require-returns": 5
},
"packages/transaction-controller/src/TransactionControllerIntegration.test.ts": {
"import-x/order": 4,
"jsdoc/tag-lines": 1
},
"packages/transaction-controller/src/api/accounts-api.test.ts": {
"import-x/order": 1,
"jsdoc/tag-lines": 1
Expand Down
21 changes: 21 additions & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Support atomic batch transactions ([#5306](https://github.com/MetaMask/core/pull/5306))
- Add methods:
- `addTransactionBatch`
- `isAtomicBatchSupported`
- Add `batch` to `TransactionType`.
- Add `nestedTransactions` to `TransactionMeta`.
- Add new types:
- `BatchTransactionParams`
- `TransactionBatchSingleRequest`
- `TransactionBatchRequest`
- `TransactionBatchResult`
- Add dependency on `@metamask/remote-feature-flag-controller:^1.4.0`.

### Changed

- **BREAKING:** Support atomic batch transactions ([#5306](https://github.com/MetaMask/core/pull/5306))
- Require `AccountsController:getState` action permission in messenger.
- Require `RemoteFeatureFlagController:getState` action permission in messenger.

## [46.0.0]

### Added
Expand Down
2 changes: 1 addition & 1 deletion packages/transaction-controller/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = merge(baseConfig, {
coverageThreshold: {
global: {
branches: 91.76,
functions: 94.57,
functions: 93.69,
lines: 96.83,
statements: 96.82,
},
Expand Down
4 changes: 3 additions & 1 deletion packages/transaction-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@metamask/eth-query": "^4.0.0",
"@metamask/metamask-eth-abis": "^3.1.1",
"@metamask/nonce-tracker": "^6.0.0",
"@metamask/remote-feature-flag-controller": "^1.4.0",
"@metamask/rpc-errors": "^7.0.2",
"@metamask/utils": "^11.1.0",
"async-mutex": "^0.5.0",
Expand Down Expand Up @@ -96,7 +97,8 @@
"@metamask/approval-controller": "^7.0.0",
"@metamask/eth-block-tracker": ">=9",
"@metamask/gas-fee-controller": "^22.0.0",
"@metamask/network-controller": "^22.0.0"
"@metamask/network-controller": "^22.0.0",
"@metamask/remote-feature-flag-controller": "^1.3.0"
},
"engines": {
"node": "^18.18 || >=20"
Expand Down
63 changes: 40 additions & 23 deletions packages/transaction-controller/src/TransactionController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,6 @@ import { createDeferredPromise } from '@metamask/utils';
import assert from 'assert';
import * as uuidModule from 'uuid';

import { FakeBlockTracker } from '../../../tests/fake-block-tracker';
import { FakeProvider } from '../../../tests/fake-provider';
import { flushPromises } from '../../../tests/helpers';
import {
buildCustomNetworkClientConfiguration,
buildMockGetNetworkClientById,
} from '../../network-controller/tests/helpers';
import { getAccountAddressRelationship } from './api/accounts-api';
import { CHAIN_IDS } from './constants';
import { DefaultGasFeeFlow } from './gas-flows/DefaultGasFeeFlow';
Expand Down Expand Up @@ -80,6 +73,7 @@ import {
TransactionType,
WalletDevice,
} from './types';
import { addTransactionBatch } from './utils/batch';
import { addGasBuffer, estimateGas, updateGas } from './utils/gas';
import { updateGasFees } from './utils/gas-fees';
import { getGasFeeFlow } from './utils/gas-flow';
Expand All @@ -92,6 +86,13 @@ import {
updatePostTransactionBalance,
updateSwapsTransaction,
} from './utils/swaps';
import { FakeBlockTracker } from '../../../tests/fake-block-tracker';
import { FakeProvider } from '../../../tests/fake-provider';
import { flushPromises } from '../../../tests/helpers';
import {
buildCustomNetworkClientConfiguration,
buildMockGetNetworkClientById,
} from '../../network-controller/tests/helpers';

type UnrestrictedMessenger = Messenger<
TransactionControllerActions | AllowedActions,
Expand All @@ -111,6 +112,7 @@ jest.mock('./helpers/IncomingTransactionHelper');
jest.mock('./helpers/MethodDataHelper');
jest.mock('./helpers/MultichainTrackingHelper');
jest.mock('./helpers/PendingTransactionTracker');
jest.mock('./utils/batch');
jest.mock('./utils/gas');
jest.mock('./utils/gas-fees');
jest.mock('./utils/gas-flow');
Expand Down Expand Up @@ -276,6 +278,7 @@ function buildMockBlockTracker(

/**
* Builds a mock gas fee flow.
*
* @returns The mocked gas fee flow.
*/
function buildMockGasFeeFlow(): jest.Mocked<GasFeeFlow> {
Expand Down Expand Up @@ -488,6 +491,7 @@ describe('TransactionController', () => {
const getAccountAddressRelationshipMock = jest.mocked(
getAccountAddressRelationship,
);
const addTransactionBatchMock = jest.mocked(addTransactionBatch);
const methodDataHelperClassMock = jest.mocked(MethodDataHelper);

let mockEthQuery: EthQuery;
Expand Down Expand Up @@ -638,6 +642,7 @@ describe('TransactionController', () => {
'NetworkController:getNetworkClientById',
'NetworkController:findNetworkClientIdByChainId',
'AccountsController:getSelectedAccount',
'AccountsController:getState',
],
allowedEvents: [],
});
Expand All @@ -648,6 +653,11 @@ describe('TransactionController', () => {
mockGetSelectedAccount,
);

unrestrictedMessenger.registerActionHandler(
'AccountsController:getState',
() => ({}) as never,
);

const controller = new TransactionController({
...otherOptions,
messenger: restrictedMessenger,
Expand Down Expand Up @@ -1371,8 +1381,6 @@ describe('TransactionController', () => {
const mockDeviceConfirmedOn = WalletDevice.OTHER;
const mockOrigin = 'origin';
const mockSecurityAlertResponse = {
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/naming-convention
result_type: 'Malicious',
reason: 'blur_farming',
description:
Expand Down Expand Up @@ -1571,6 +1579,7 @@ describe('TransactionController', () => {
deviceConfirmedOn: undefined,
id: expect.any(String),
isFirstTimeInteraction: undefined,
nestedTransactions: undefined,
networkClientId: NETWORK_CLIENT_ID_MOCK,
origin: undefined,
securityAlertResponse: undefined,
Expand Down Expand Up @@ -4166,8 +4175,6 @@ describe('TransactionController', () => {
const key = 'testKey';
const value = 123;

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

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

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

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

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

Expand All @@ -5129,8 +5128,6 @@ describe('TransactionController', () => {
// @ts-expect-error Intentionally passing invalid input
controller.updateSecurityAlertResponse(undefined, {
reason: 'NA',
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/naming-convention
result_type: 'Benign',
}),
).toThrow(
Expand Down Expand Up @@ -5197,8 +5194,6 @@ describe('TransactionController', () => {
expect(() =>
controller.updateSecurityAlertResponse('456', {
reason: 'NA',
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/naming-convention
result_type: 'Benign',
}),
).toThrow(
Expand Down Expand Up @@ -6115,4 +6110,26 @@ describe('TransactionController', () => {
expect(transaction?.isActive).toBe(true);
});
});

describe('addTransactionBatch', () => {
it('invokes util', async () => {
const { controller } = setupController();

await controller.addTransactionBatch({
from: ACCOUNT_MOCK,
networkClientId: NETWORK_CLIENT_ID_MOCK,
transactions: [
{
params: {
to: ACCOUNT_2_MOCK,
data: '0x123456',
value: '0x123',
},
},
],
});

expect(addTransactionBatchMock).toHaveBeenCalledTimes(1);
});
});
});
Loading

0 comments on commit b9cb503

Please sign in to comment.