Skip to content

feat: add sequential batch support #5762

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add sequential batch support when `publishBatchHook` is not defined ([#5762](https://github.com/MetaMask/core/pull/5762))

### Fixed

- Fix `addTransaction` function to correctly identify a transaction as a `simpleSend` type when the recipient is a smart account ([#5822](https://github.com/MetaMask/core/pull/5822))
Expand Down
18 changes: 18 additions & 0 deletions packages/transaction-controller/src/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,11 @@ export class TransactionController extends BaseController<
async addTransactionBatch(
request: TransactionBatchRequest,
): Promise<TransactionBatchResult> {
const { blockTracker } = this.messagingSystem.call(
`NetworkController:getNetworkClientById`,
request.networkClientId,
);

return await addTransactionBatch({
addTransaction: this.addTransaction.bind(this),
getChainId: this.#getChainId.bind(this),
Expand All @@ -1036,6 +1041,19 @@ export class TransactionController extends BaseController<
publicKeyEIP7702: this.#publicKeyEIP7702,
request,
updateTransaction: this.#updateTransactionInternal.bind(this),
publishTransaction: (
ethQuery: EthQuery,
transactionMeta: TransactionMeta,
) => this.#publishTransaction(ethQuery, transactionMeta) as Promise<Hex>,
getPendingTransactionTrackerByChainId: (
networkClientId: NetworkClientId,
) =>
this.#createPendingTransactionTracker({
provider: this.#getProvider({ networkClientId }),
blockTracker,
chainId: this.#getChainId(networkClientId),
networkClientId,
}),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1151,4 +1151,53 @@ describe('PendingTransactionTracker', () => {
expect(transactionMeta.txReceipt).toBeUndefined();
});
});

describe('addTransactionToPoll', () => {
it('adds a transaction to poll and sets #transactionToForcePoll', () => {
pendingTransactionTracker = new PendingTransactionTracker(options);

pendingTransactionTracker.addTransactionToPoll(
TRANSACTION_SUBMITTED_MOCK,
);

expect(transactionPoller.setPendingTransactions).toHaveBeenCalledWith([
TRANSACTION_SUBMITTED_MOCK,
]);
expect(transactionPoller.start).toHaveBeenCalledTimes(1);
});

describe('emits confirm event and clean transactionToForcePoll', () => {
it('if receipt has success status', async () => {
const transaction = { ...TRANSACTION_SUBMITTED_MOCK };
const getTransactions = jest
.fn()
.mockReturnValue(freeze([transaction], true));

pendingTransactionTracker = new PendingTransactionTracker({
...options,
getTransactions,
});

pendingTransactionTracker.addTransactionToPoll(
TRANSACTION_SUBMITTED_MOCK,
);

const listener = jest.fn();
pendingTransactionTracker.hub.addListener(
'transaction-confirmed',
listener,
);

queryMock.mockResolvedValueOnce(RECEIPT_MOCK);
queryMock.mockResolvedValueOnce(BLOCK_MOCK);

await onPoll();

expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith(
expect.objectContaining(TRANSACTION_SUBMITTED_MOCK),
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ export class PendingTransactionTracker {

readonly #transactionPoller: TransactionPoller;

#transactionToForcePoll: TransactionMeta | undefined;

readonly #beforeCheckPendingTransaction: (
transactionMeta: TransactionMeta,
) => Promise<boolean>;
Expand Down Expand Up @@ -139,6 +141,7 @@ export class PendingTransactionTracker {
this.#getGlobalLock = getGlobalLock;
this.#publishTransaction = publishTransaction;
this.#running = false;
this.#transactionToForcePoll = undefined;

this.#transactionPoller = new TransactionPoller({
blockTracker,
Expand Down Expand Up @@ -167,6 +170,22 @@ export class PendingTransactionTracker {
}
};

/**
* Adds a transaction to the polling mechanism for monitoring its status.
*
* This method forcefully adds a single transaction to the list of transactions
* being polled, ensuring that its status is checked, event emitted but no update is performed.
* It overrides the default behavior by prioritizing the given transaction for polling.
*
* @param transactionMeta - The transaction metadata to be added for polling.
*
* The transaction will now be monitored for updates, such as confirmation or failure.
*/
addTransactionToPoll = (transactionMeta: TransactionMeta) => {
this.#start([transactionMeta]);
this.#transactionToForcePoll = transactionMeta;
};

/**
* Force checks the network if the given transaction is confirmed and updates it's status.
*
Expand Down Expand Up @@ -232,7 +251,14 @@ export class PendingTransactionTracker {
async #checkTransactions() {
this.#log('Checking transactions');

const pendingTransactions = this.#getPendingTransactions();
const pendingTransactions: TransactionMeta[] = [
...new Set(
[
...this.#getPendingTransactions(),
this.#transactionToForcePoll,
].filter((tx): tx is TransactionMeta => tx !== undefined),
),
];

if (!pendingTransactions.length) {
this.#log('No pending transactions to check');
Expand Down Expand Up @@ -353,6 +379,12 @@ export class PendingTransactionTracker {
return blocksSinceFirstRetry >= requiredBlocksSinceFirstRetry;
}

#cleanTransactionToForcePoll(txMeta: TransactionMeta) {
if (this.#transactionToForcePoll?.id === txMeta.id) {
this.#transactionToForcePoll = undefined;
}
}

async #checkTransaction(txMeta: TransactionMeta) {
const { hash, id } = txMeta;

Expand Down Expand Up @@ -429,6 +461,12 @@ export class PendingTransactionTracker {

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

if (this.#transactionToForcePoll) {
this.#cleanTransactionToForcePoll(txMeta);
this.hub.emit('transaction-confirmed', txMeta);
return;
}

const { baseFeePerGas, timestamp: blockTimestamp } =
await this.#getBlockByHash(blockHash, false);

Expand Down Expand Up @@ -525,11 +563,13 @@ export class PendingTransactionTracker {

#failTransaction(txMeta: TransactionMeta, error: Error) {
this.#log('Transaction failed', txMeta.id, error);
this.#cleanTransactionToForcePoll(txMeta);
this.hub.emit('transaction-failed', txMeta, error);
}

#dropTransaction(txMeta: TransactionMeta) {
this.#log('Transaction dropped', txMeta.id);
this.#cleanTransactionToForcePoll(txMeta);
this.hub.emit('transaction-dropped', txMeta);
}

Expand Down
Loading
Loading