Skip to content

Commit 216b0a6

Browse files
perf: speed up adding transactions (#7205)
## Explanation Minor optimisations to `addTransaction` and `addTransactionBatch` methods: - Don't await initial gas limit and gas fee estimation if `skipInitialGasEstimate` set. - Skip EIP-7702 request and upgrade check if `disableUpgrade` set. - Do not wait for `delegationAddress` and instead update asynchronously. - Skip unnecessary `eth_getCode` requests when determining transaction type. - Only decode transfer recipient for first time interaction validation if transaction type suitable. ## References Related to [#6165](MetaMask/MetaMask-planning#6165) ## 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 communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Speeds up `addTransaction`/`addTransactionBatch` by allowing async gas estimation and skipping EIP-7702 upgrade checks, plus tighter type handling and first‑time interaction optimizations. > > - **Transaction add flow (performance)**: > - Add `skipInitialGasEstimate` to `addTransaction` and `addTransactionBatch`; when set, skip blocking gas calc and update gas fields asynchronously. > - Resolve `delegationAddress` asynchronously post-add; remove from initial snapshot, update later. > - **Batch (EIP-7702) changes**: > - Add `disableUpgrade` to `TransactionBatchRequest`; when set, bypass upgrade check and avoid type `0x4` (setCode) path. > - Propagate `skipInitialGasEstimate` from batch to underlying `addTransaction`. > - Use provided nested transaction `type` directly; only infer when not supplied. > - **First-time interaction**: > - Only decode recipient from `data` when `type` is `tokenMethodTransfer` or `tokenMethodTransferFrom`; otherwise fall back to `to`. > - **Misc**: > - Import `noop` to safely swallow async errors; minor test updates and slight coverage threshold tweak. > - Changelog updated accordingly. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4ed214f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 2785995 commit 216b0a6

File tree

9 files changed

+149
-29
lines changed

9 files changed

+149
-29
lines changed

packages/transaction-controller/CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Performance optimisations in `addTransaction` and `addTransactionBatch` methods ([#7205](https://github.com/MetaMask/core/pull/7205))
13+
- Add `skipInitialGasEstimate` option to `addTransaction` and `addTransactionBatch` methods.
14+
- Add `disableUpgrade` option to `addTransactionBatch` method.
15+
1016
## [62.0.0]
1117

1218
### 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: 92.76,
21+
functions: 92.46,
2222
lines: 96.83,
2323
statements: 96.82,
2424
},

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1686,7 +1686,6 @@ describe('TransactionController', () => {
16861686
batchId: undefined,
16871687
chainId: expect.any(String),
16881688
dappSuggestedGasFees: undefined,
1689-
delegationAddress: undefined,
16901689
deviceConfirmedOn: undefined,
16911690
disableGasBuffer: undefined,
16921691
id: expect.any(String),

packages/transaction-controller/src/TransactionController.ts

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import { add0x } from '@metamask/utils';
5656
// This package purposefully relies on Node's EventEmitter module.
5757
// eslint-disable-next-line import-x/no-nodejs-modules
5858
import { EventEmitter } from 'events';
59-
import { cloneDeep, mapValues, merge, pickBy, sortBy } from 'lodash';
59+
import { cloneDeep, mapValues, merge, noop, pickBy, sortBy } from 'lodash';
6060
import { v1 as random } from 'uuid';
6161

6262
import { DefaultGasFeeFlow } from './gas-flows/DefaultGasFeeFlow';
@@ -1239,6 +1239,7 @@ export class TransactionController extends BaseController<
12391239
requireApproval,
12401240
securityAlertResponse,
12411241
sendFlowHistory,
1242+
skipInitialGasEstimate,
12421243
swaps = {},
12431244
traceContext,
12441245
type,
@@ -1311,8 +1312,6 @@ export class TransactionController extends BaseController<
13111312
const transactionType =
13121313
type ?? (await determineTransactionType(txParams, ethQuery)).type;
13131314

1314-
const delegationAddress = await delegationAddressPromise;
1315-
13161315
const existingTransactionMeta = this.#getTransactionWithActionId(actionId);
13171316

13181317
// If a request to add a transaction with the same actionId is submitted again, a new transaction will not be created for it.
@@ -1325,7 +1324,6 @@ export class TransactionController extends BaseController<
13251324
batchId,
13261325
chainId,
13271326
dappSuggestedGasFees,
1328-
delegationAddress,
13291327
deviceConfirmedOn,
13301328
disableGasBuffer,
13311329
id: random(),
@@ -1360,13 +1358,40 @@ export class TransactionController extends BaseController<
13601358
updateTransaction(addedTransactionMeta);
13611359
}
13621360

1363-
await this.#trace(
1364-
{ name: 'Estimate Gas Properties', parentContext: traceContext },
1365-
(context) =>
1366-
this.#updateGasProperties(addedTransactionMeta, {
1367-
traceContext: context,
1368-
}),
1369-
);
1361+
if (!skipInitialGasEstimate) {
1362+
await this.#trace(
1363+
{ name: 'Estimate Gas Properties', parentContext: traceContext },
1364+
(context) =>
1365+
this.#updateGasProperties(addedTransactionMeta, {
1366+
traceContext: context,
1367+
}),
1368+
);
1369+
} else {
1370+
const newTransactionMeta = cloneDeep(addedTransactionMeta);
1371+
1372+
this.#updateGasProperties(newTransactionMeta)
1373+
.then(() => {
1374+
this.#updateTransactionInternal(
1375+
{
1376+
transactionId: newTransactionMeta.id,
1377+
skipHistory: true,
1378+
skipResimulateCheck: true,
1379+
skipValidation: true,
1380+
},
1381+
(tx) => {
1382+
tx.txParams.gas = newTransactionMeta.txParams.gas;
1383+
tx.txParams.gasPrice = newTransactionMeta.txParams.gasPrice;
1384+
tx.txParams.maxFeePerGas =
1385+
newTransactionMeta.txParams.maxFeePerGas;
1386+
tx.txParams.maxPriorityFeePerGas =
1387+
newTransactionMeta.txParams.maxPriorityFeePerGas;
1388+
},
1389+
);
1390+
1391+
return undefined;
1392+
})
1393+
.catch(noop);
1394+
}
13701395

13711396
// Checks if a transaction already exists with a given actionId
13721397
if (!existingTransactionMeta) {
@@ -1401,6 +1426,24 @@ export class TransactionController extends BaseController<
14011426

14021427
this.#addMetadata(addedTransactionMeta);
14031428

1429+
delegationAddressPromise
1430+
.then((delegationAddress) => {
1431+
this.#updateTransactionInternal(
1432+
{
1433+
transactionId: addedTransactionMeta.id,
1434+
skipHistory: true,
1435+
skipResimulateCheck: true,
1436+
skipValidation: true,
1437+
},
1438+
(tx) => {
1439+
tx.delegationAddress = delegationAddress;
1440+
},
1441+
);
1442+
1443+
return undefined;
1444+
})
1445+
.catch(noop);
1446+
14041447
if (requireApproval !== false) {
14051448
this.#updateSimulationData(addedTransactionMeta, {
14061449
traceContext,

packages/transaction-controller/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1723,6 +1723,9 @@ export type TransactionBatchRequest = {
17231723
/** Whether to disable batch transaction via sequential transactions. */
17241724
disableSequential?: boolean;
17251725

1726+
/** Whether to disable upgrading the account to an EIP-7702. */
1727+
disableUpgrade?: boolean;
1728+
17261729
/** Address of the account to submit the transaction batch. */
17271730
from: Hex;
17281731

@@ -1747,6 +1750,9 @@ export type TransactionBatchRequest = {
17471750
/** Security alert ID to persist on the transaction. */
17481751
securityAlertId?: string;
17491752

1753+
/** Whether to skip the initial gas calculation and rely only on the polling. */
1754+
skipInitialGasEstimate?: boolean;
1755+
17501756
/** Transactions to be submitted as part of the batch. */
17511757
transactions: TransactionBatchSingleRequest[];
17521758

@@ -2100,6 +2106,9 @@ export type AddTransactionOptions = {
21002106
/** Entries to add to the `sendFlowHistory`. */
21012107
sendFlowHistory?: SendFlowHistoryEntry[];
21022108

2109+
/** Whether to skip the initial gas calculation and rely only on the polling. */
2110+
skipInitialGasEstimate?: boolean;
2111+
21032112
/** Options for swaps transactions. */
21042113
swaps?: {
21052114
/** Whether the transaction has an approval transaction. */

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,44 @@ describe('Batch Utils', () => {
647647
);
648648
});
649649

650+
it('does not use type 4 if not upgraded but disableUpgrade set', async () => {
651+
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
652+
delegationAddress: undefined,
653+
isSupported: false,
654+
});
655+
656+
addTransactionMock.mockResolvedValueOnce({
657+
transactionMeta: TRANSACTION_META_MOCK,
658+
result: Promise.resolve(''),
659+
});
660+
661+
generateEIP7702BatchTransactionMock.mockReturnValueOnce(
662+
TRANSACTION_BATCH_PARAMS_MOCK,
663+
);
664+
665+
getEIP7702UpgradeContractAddressMock.mockReturnValueOnce(
666+
CONTRACT_ADDRESS_MOCK,
667+
);
668+
669+
request.request.disableUpgrade = true;
670+
671+
await addTransactionBatch(request);
672+
673+
expect(addTransactionMock).toHaveBeenCalledTimes(1);
674+
expect(addTransactionMock).toHaveBeenCalledWith(
675+
{
676+
from: FROM_MOCK,
677+
to: TO_MOCK,
678+
data: DATA_MOCK,
679+
value: VALUE_MOCK,
680+
},
681+
expect.objectContaining({
682+
networkClientId: NETWORK_CLIENT_ID_MOCK,
683+
requireApproval: true,
684+
}),
685+
);
686+
});
687+
650688
it('passes nested transactions to add transaction', async () => {
651689
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
652690
delegationAddress: undefined,

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

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -257,15 +257,21 @@ async function getNestedTransactionMeta(
257257
const { from } = request;
258258
const { params, type: requestedType } = singleRequest;
259259

260+
if (requestedType) {
261+
return {
262+
...params,
263+
type: requestedType,
264+
};
265+
}
266+
260267
const { type: determinedType } = await determineTransactionType(
261268
{ from, ...params },
262269
ethQuery,
263270
);
264271

265-
const type = requestedType ?? determinedType;
266272
return {
267273
...params,
268-
type,
274+
type: determinedType,
269275
};
270276
}
271277

@@ -288,12 +294,14 @@ async function addTransactionBatchWith7702(
288294

289295
const {
290296
batchId: batchIdOverride,
297+
disableUpgrade,
291298
from,
292299
gasFeeToken,
293300
networkClientId,
294301
origin,
295302
requireApproval,
296303
securityAlertId,
304+
skipInitialGasEstimate,
297305
transactions,
298306
validateSecurity,
299307
} = userRequest;
@@ -311,19 +319,25 @@ async function addTransactionBatchWith7702(
311319
throw rpcErrors.internal(ERROR_MESSGE_PUBLIC_KEY);
312320
}
313321

314-
const { delegationAddress, isSupported } = await isAccountUpgradedToEIP7702(
315-
from,
316-
chainId,
317-
publicKeyEIP7702,
318-
messenger,
319-
ethQuery,
320-
);
322+
let requiresUpgrade = false;
321323

322-
log('Account', { delegationAddress, isSupported });
324+
if (!disableUpgrade) {
325+
const { delegationAddress, isSupported } = await isAccountUpgradedToEIP7702(
326+
from,
327+
chainId,
328+
publicKeyEIP7702,
329+
messenger,
330+
ethQuery,
331+
);
332+
333+
log('Account', { delegationAddress, isSupported });
334+
335+
if (!isSupported && delegationAddress) {
336+
log('Account upgraded to unsupported contract', from, delegationAddress);
337+
throw rpcErrors.internal('Account upgraded to unsupported contract');
338+
}
323339

324-
if (!isSupported && delegationAddress) {
325-
log('Account upgraded to unsupported contract', from, delegationAddress);
326-
throw rpcErrors.internal('Account upgraded to unsupported contract');
340+
requiresUpgrade = !isSupported;
327341
}
328342

329343
const nestedTransactions = await Promise.all(
@@ -339,7 +353,7 @@ async function addTransactionBatchWith7702(
339353
...batchParams,
340354
};
341355

342-
if (!isSupported) {
356+
if (requiresUpgrade) {
343357
const upgradeContractAddress = getEIP7702UpgradeContractAddress(
344358
chainId,
345359
messenger,
@@ -409,6 +423,7 @@ async function addTransactionBatchWith7702(
409423
origin,
410424
requireApproval,
411425
securityAlertResponse,
426+
skipInitialGasEstimate,
412427
type: TransactionType.batch,
413428
});
414429

packages/transaction-controller/src/utils/first-time-interaction.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ describe('updateFirstTimeInteraction', () => {
106106
const transactionMetaWithData = {
107107
...mockTransactionMeta,
108108
txParams: { ...mockTransactionMeta.txParams, data: '0xabcdef' },
109+
type: TransactionType.tokenMethodTransfer,
109110
};
110111

111112
mockDecodeTransactionData.mockReturnValue({
@@ -136,6 +137,7 @@ describe('updateFirstTimeInteraction', () => {
136137
const transactionMetaWithData = {
137138
...mockTransactionMeta,
138139
txParams: { ...mockTransactionMeta.txParams, data: '0xabcdef' },
140+
type: TransactionType.tokenMethodTransfer,
139141
};
140142

141143
mockDecodeTransactionData.mockReturnValue({
@@ -165,6 +167,7 @@ describe('updateFirstTimeInteraction', () => {
165167
const transactionMetaWithData = {
166168
...mockTransactionMeta,
167169
txParams: { ...mockTransactionMeta.txParams, data: '0xabcdef' },
170+
type: TransactionType.tokenMethodTransferFrom,
168171
};
169172

170173
mockDecodeTransactionData.mockReturnValue({

packages/transaction-controller/src/utils/first-time-interaction.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
type GetAccountAddressRelationshipRequest,
1010
} from '../api/accounts-api';
1111
import { projectLogger as log } from '../logger';
12-
import type { TransactionMeta } from '../types';
12+
import { TransactionType, type TransactionMeta } from '../types';
1313

1414
type UpdateFirstTimeInteractionRequest = {
1515
existingTransactions: TransactionMeta[];
@@ -57,10 +57,17 @@ export async function updateFirstTimeInteraction({
5757
chainId,
5858
id: transactionId,
5959
txParams: { data, from, to },
60+
type,
6061
} = transactionMeta;
6162

6263
let recipient;
63-
if (data) {
64+
if (
65+
data &&
66+
[
67+
TransactionType.tokenMethodTransfer,
68+
TransactionType.tokenMethodTransferFrom,
69+
].includes(type as TransactionType)
70+
) {
6471
const parsedData = decodeTransactionData(data) as TransactionDescription;
6572
// _to is for ERC20, ERC721 and USDC
6673
// to is for ERC1155

0 commit comments

Comments
 (0)