Skip to content

Commit dbe5097

Browse files
authored
feat: add sequential batch support (#5762)
## Explanation The `TransactionController` currently lacks support for sequential batch transactions, which are required for the stablecoin lending feature. Specifically, there is no mechanism to execute multiple transactions (e.g., approval + token deposit) sequentially while ensuring confirmation for each transaction. This limitation prevents efficient batch processing to support new features. **What solution do these changes offer?** The `SequentialPublishBatchHook` introduces a default mechanism for handling batch transactions when no custom `publishBatchHook` is provided. It ensures transactions are published sequentially, waiting for confirmation before proceeding to the next. If any transaction fails to publish or confirm, the batch process halts and throws an error. **Key Features:** - Sequential Execution: Publishes transactions one at a time and waits for confirmation. - Error Handling: Halts the batch if any transaction fails to publish or confirm. - Polling for Confirmation: Retries confirmation checks up to a maximum number of attempts. <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> Fixes MetaMask/MetaMask-planning#4695 ## Changelog <!-- THIS SECTION IS NO LONGER NEEDED. The process for updating changelogs has changed. Please consult the "Updating changelogs" section of the Contributing doc for more. --> ## 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 - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
1 parent c767a57 commit dbe5097

File tree

8 files changed

+953
-16
lines changed

8 files changed

+953
-16
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 sequential batch support when `publishBatchHook` is not defined ([#5762](https://github.com/MetaMask/core/pull/5762))
13+
1014
### Fixed
1115

1216
- Fix `userFeeLevel` as `dappSuggested` initially when dApp suggested gas values for legacy transactions ([#5821](https://github.com/MetaMask/core/pull/5821))

packages/transaction-controller/src/TransactionController.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,11 @@ export class TransactionController extends BaseController<
10241024
async addTransactionBatch(
10251025
request: TransactionBatchRequest,
10261026
): Promise<TransactionBatchResult> {
1027+
const { blockTracker } = this.messagingSystem.call(
1028+
`NetworkController:getNetworkClientById`,
1029+
request.networkClientId,
1030+
);
1031+
10271032
return await addTransactionBatch({
10281033
addTransaction: this.addTransaction.bind(this),
10291034
getChainId: this.#getChainId.bind(this),
@@ -1036,6 +1041,17 @@ export class TransactionController extends BaseController<
10361041
publicKeyEIP7702: this.#publicKeyEIP7702,
10371042
request,
10381043
updateTransaction: this.#updateTransactionInternal.bind(this),
1044+
publishTransaction: (
1045+
ethQuery: EthQuery,
1046+
transactionMeta: TransactionMeta,
1047+
) => this.#publishTransaction(ethQuery, transactionMeta) as Promise<Hex>,
1048+
getPendingTransactionTracker: (networkClientId: NetworkClientId) =>
1049+
this.#createPendingTransactionTracker({
1050+
provider: this.#getProvider({ networkClientId }),
1051+
blockTracker,
1052+
chainId: this.#getChainId(networkClientId),
1053+
networkClientId,
1054+
}),
10391055
});
10401056
}
10411057

packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,4 +1151,53 @@ describe('PendingTransactionTracker', () => {
11511151
expect(transactionMeta.txReceipt).toBeUndefined();
11521152
});
11531153
});
1154+
1155+
describe('addTransactionToPoll', () => {
1156+
it('adds a transaction to poll and sets #transactionToForcePoll', () => {
1157+
pendingTransactionTracker = new PendingTransactionTracker(options);
1158+
1159+
pendingTransactionTracker.addTransactionToPoll(
1160+
TRANSACTION_SUBMITTED_MOCK,
1161+
);
1162+
1163+
expect(transactionPoller.setPendingTransactions).toHaveBeenCalledWith([
1164+
TRANSACTION_SUBMITTED_MOCK,
1165+
]);
1166+
expect(transactionPoller.start).toHaveBeenCalledTimes(1);
1167+
});
1168+
1169+
describe('emits confirm event and clean transactionToForcePoll', () => {
1170+
it('if receipt has success status', async () => {
1171+
const transaction = { ...TRANSACTION_SUBMITTED_MOCK };
1172+
const getTransactions = jest
1173+
.fn()
1174+
.mockReturnValue(freeze([transaction], true));
1175+
1176+
pendingTransactionTracker = new PendingTransactionTracker({
1177+
...options,
1178+
getTransactions,
1179+
});
1180+
1181+
pendingTransactionTracker.addTransactionToPoll(
1182+
TRANSACTION_SUBMITTED_MOCK,
1183+
);
1184+
1185+
const listener = jest.fn();
1186+
pendingTransactionTracker.hub.addListener(
1187+
'transaction-confirmed',
1188+
listener,
1189+
);
1190+
1191+
queryMock.mockResolvedValueOnce(RECEIPT_MOCK);
1192+
queryMock.mockResolvedValueOnce(BLOCK_MOCK);
1193+
1194+
await onPoll();
1195+
1196+
expect(listener).toHaveBeenCalledTimes(1);
1197+
expect(listener).toHaveBeenCalledWith(
1198+
expect.objectContaining(TRANSACTION_SUBMITTED_MOCK),
1199+
);
1200+
});
1201+
});
1202+
});
11541203
});

packages/transaction-controller/src/helpers/PendingTransactionTracker.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ export class PendingTransactionTracker {
9393

9494
readonly #transactionPoller: TransactionPoller;
9595

96+
#transactionToForcePoll: TransactionMeta | undefined;
97+
9698
readonly #beforeCheckPendingTransaction: (
9799
transactionMeta: TransactionMeta,
98100
) => Promise<boolean>;
@@ -139,6 +141,7 @@ export class PendingTransactionTracker {
139141
this.#getGlobalLock = getGlobalLock;
140142
this.#publishTransaction = publishTransaction;
141143
this.#running = false;
144+
this.#transactionToForcePoll = undefined;
142145

143146
this.#transactionPoller = new TransactionPoller({
144147
blockTracker,
@@ -167,6 +170,22 @@ export class PendingTransactionTracker {
167170
}
168171
};
169172

173+
/**
174+
* Adds a transaction to the polling mechanism for monitoring its status.
175+
*
176+
* This method forcefully adds a single transaction to the list of transactions
177+
* being polled, ensuring that its status is checked, event emitted but no update is performed.
178+
* It overrides the default behavior by prioritizing the given transaction for polling.
179+
*
180+
* @param transactionMeta - The transaction metadata to be added for polling.
181+
*
182+
* The transaction will now be monitored for updates, such as confirmation or failure.
183+
*/
184+
addTransactionToPoll(transactionMeta: TransactionMeta): void {
185+
this.#start([transactionMeta]);
186+
this.#transactionToForcePoll = transactionMeta;
187+
}
188+
170189
/**
171190
* Force checks the network if the given transaction is confirmed and updates it's status.
172191
*
@@ -232,7 +251,10 @@ export class PendingTransactionTracker {
232251
async #checkTransactions() {
233252
this.#log('Checking transactions');
234253

235-
const pendingTransactions = this.#getPendingTransactions();
254+
const pendingTransactions: TransactionMeta[] = [
255+
...this.#getPendingTransactions(),
256+
...(this.#transactionToForcePoll ? [this.#transactionToForcePoll] : []),
257+
];
236258

237259
if (!pendingTransactions.length) {
238260
this.#log('No pending transactions to check');
@@ -353,6 +375,12 @@ export class PendingTransactionTracker {
353375
return blocksSinceFirstRetry >= requiredBlocksSinceFirstRetry;
354376
}
355377

378+
#cleanTransactionToForcePoll(transactionId: string) {
379+
if (this.#transactionToForcePoll?.id === transactionId) {
380+
this.#transactionToForcePoll = undefined;
381+
}
382+
}
383+
356384
async #checkTransaction(txMeta: TransactionMeta) {
357385
const { hash, id } = txMeta;
358386

@@ -429,6 +457,12 @@ export class PendingTransactionTracker {
429457

430458
this.#log('Transaction confirmed', id);
431459

460+
if (this.#transactionToForcePoll) {
461+
this.#cleanTransactionToForcePoll(txMeta.id);
462+
this.hub.emit('transaction-confirmed', txMeta);
463+
return;
464+
}
465+
432466
const { baseFeePerGas, timestamp: blockTimestamp } =
433467
await this.#getBlockByHash(blockHash, false);
434468

@@ -525,11 +559,13 @@ export class PendingTransactionTracker {
525559

526560
#failTransaction(txMeta: TransactionMeta, error: Error) {
527561
this.#log('Transaction failed', txMeta.id, error);
562+
this.#cleanTransactionToForcePoll(txMeta.id);
528563
this.hub.emit('transaction-failed', txMeta, error);
529564
}
530565

531566
#dropTransaction(txMeta: TransactionMeta) {
532567
this.#log('Transaction dropped', txMeta.id);
568+
this.#cleanTransactionToForcePoll(txMeta.id);
533569
this.hub.emit('transaction-dropped', txMeta);
534570
}
535571

0 commit comments

Comments
 (0)