Skip to content

Commit 5d0e61d

Browse files
feat: update nonce of existing transaction in batch (#6528)
## Explanation When dynamically converting an existing transaction to a transaction batch via the `batchTransactions` property, automatically update the nonce and re-sign the existing transaction if it is not the first transaction in the batch. ## References ## 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
1 parent dd0a7d4 commit 5d0e61d

File tree

6 files changed

+294
-49
lines changed

6 files changed

+294
-49
lines changed

packages/transaction-controller/CHANGELOG.md

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

1010
### Changed
1111

12+
- Update nonce of existing transaction if converted to batch via `batchTransactions` but not first transaction ([#6528](https://github.com/MetaMask/core/pull/6528))
1213
- Bump `@metamask/base-controller` from `^8.2.0` to `^8.3.0` ([#6465](https://github.com/MetaMask/core/pull/6465))
1314

1415
## [60.2.0]

packages/transaction-controller/src/TransactionController.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,7 @@ export class TransactionController extends BaseController<
10901090
transactionMeta: TransactionMeta,
10911091
) => this.#publishTransaction(ethQuery, transactionMeta) as Promise<Hex>,
10921092
request,
1093+
signTransaction: this.#signTransaction.bind(this),
10931094
update: this.update.bind(this),
10941095
updateTransaction: this.#updateTransactionInternal.bind(this),
10951096
});

packages/transaction-controller/src/hooks/CollectPublishHook.test.ts

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { noop } from 'lodash';
2+
13
import { CollectPublishHook } from './CollectPublishHook';
24
import type { TransactionMeta } from '..';
35
import { flushPromises } from '../../../../tests/helpers';
@@ -10,21 +12,43 @@ const ERROR_MESSAGE_MOCK = 'Test error';
1012

1113
const TRANSACTION_META_MOCK = {
1214
id: '123-456',
15+
txParams: {
16+
nonce: '0x1',
17+
},
18+
} as TransactionMeta;
19+
20+
const TRANSACTION_META_2_MOCK = {
21+
id: '123-457',
22+
txParams: {
23+
nonce: '0x2',
24+
},
1325
} as TransactionMeta;
1426

1527
describe('CollectPublishHook', () => {
1628
describe('getHook', () => {
17-
it('returns function that resolves ready promise', async () => {
29+
it('resolves ready promise', async () => {
1830
const collectHook = new CollectPublishHook(2);
1931
const publishHook = collectHook.getHook();
2032

21-
publishHook(TRANSACTION_META_MOCK, SIGNED_TX_MOCK).catch(() => {
22-
// Intentionally empty
23-
});
33+
publishHook(TRANSACTION_META_MOCK, SIGNED_TX_MOCK).catch(noop);
34+
publishHook(TRANSACTION_META_2_MOCK, SIGNED_TX_2_MOCK).catch(noop);
2435

25-
publishHook(TRANSACTION_META_MOCK, SIGNED_TX_2_MOCK).catch(() => {
26-
// Intentionally empty
27-
});
36+
await flushPromises();
37+
38+
const result = await collectHook.ready();
39+
40+
expect(result.signedTransactions).toStrictEqual([
41+
SIGNED_TX_MOCK,
42+
SIGNED_TX_2_MOCK,
43+
]);
44+
});
45+
46+
it('resolves ready promise with signatures in nonce order', async () => {
47+
const collectHook = new CollectPublishHook(2);
48+
const publishHook = collectHook.getHook();
49+
50+
publishHook(TRANSACTION_META_2_MOCK, SIGNED_TX_2_MOCK).catch(noop);
51+
publishHook(TRANSACTION_META_MOCK, SIGNED_TX_MOCK).catch(noop);
2852

2953
await flushPromises();
3054

@@ -48,10 +72,33 @@ describe('CollectPublishHook', () => {
4872
);
4973

5074
const publishPromise2 = publishHook(
51-
TRANSACTION_META_MOCK,
75+
TRANSACTION_META_2_MOCK,
76+
SIGNED_TX_2_MOCK,
77+
);
78+
79+
collectHook.success([TRANSACTION_HASH_MOCK, TRANSACTION_HASH_2_MOCK]);
80+
81+
const result1 = await publishPromise1;
82+
const result2 = await publishPromise2;
83+
84+
expect(result1.transactionHash).toBe(TRANSACTION_HASH_MOCK);
85+
expect(result2.transactionHash).toBe(TRANSACTION_HASH_2_MOCK);
86+
});
87+
88+
it('resolves all publish promises in nonce order', async () => {
89+
const collectHook = new CollectPublishHook(2);
90+
const publishHook = collectHook.getHook();
91+
92+
const publishPromise2 = publishHook(
93+
TRANSACTION_META_2_MOCK,
5294
SIGNED_TX_2_MOCK,
5395
);
5496

97+
const publishPromise1 = publishHook(
98+
TRANSACTION_META_MOCK,
99+
SIGNED_TX_MOCK,
100+
);
101+
55102
collectHook.success([TRANSACTION_HASH_MOCK, TRANSACTION_HASH_2_MOCK]);
56103

57104
const result1 = await publishPromise1;

packages/transaction-controller/src/hooks/CollectPublishHook.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { DeferredPromise, Hex } from '@metamask/utils';
22
import { createDeferredPromise, createModuleLogger } from '@metamask/utils';
3+
import { sortBy } from 'lodash';
34

45
import { projectLogger } from '../logger';
56
import type { PublishHook, PublishHookResult, TransactionMeta } from '../types';
@@ -15,18 +16,19 @@ export type CollectPublishHookResult = {
1516
* Used by batch transactions to publish multiple transactions at once.
1617
*/
1718
export class CollectPublishHook {
18-
readonly #publishPromises: DeferredPromise<PublishHookResult>[];
19-
20-
readonly #signedTransactions: Hex[];
19+
#results: {
20+
nonce: number;
21+
promise: DeferredPromise<PublishHookResult>;
22+
signedTransaction: Hex;
23+
}[];
2124

2225
readonly #transactionCount: number;
2326

2427
readonly #readyPromise: DeferredPromise<CollectPublishHookResult>;
2528

2629
constructor(transactionCount: number) {
27-
this.#publishPromises = [];
2830
this.#readyPromise = createDeferredPromise();
29-
this.#signedTransactions = [];
31+
this.#results = [];
3032
this.#transactionCount = transactionCount;
3133
}
3234

@@ -56,39 +58,54 @@ export class CollectPublishHook {
5658
throw new Error('Transaction hash count mismatch');
5759
}
5860

59-
for (let i = 0; i < this.#publishPromises.length; i++) {
60-
const publishPromise = this.#publishPromises[i];
61+
for (let i = 0; i < this.#results.length; i++) {
62+
const result = this.#results[i];
6163
const transactionHash = transactionHashes[i];
6264

63-
publishPromise.resolve({ transactionHash });
65+
result.promise.resolve({ transactionHash });
6466
}
6567
}
6668

6769
error(error: unknown) {
6870
log('Error', { error });
6971

70-
for (const publishPromise of this.#publishPromises) {
71-
publishPromise.reject(error);
72+
for (const result of this.#results) {
73+
result.promise.reject(error);
7274
}
7375
}
7476

7577
#hook(
7678
transactionMeta: TransactionMeta,
7779
signedTx: string,
7880
): Promise<PublishHookResult> {
79-
this.#signedTransactions.push(signedTx as Hex);
81+
const nonceHex = transactionMeta.txParams.nonce ?? '0x0';
82+
const nonceDecimal = parseInt(nonceHex, 16);
8083

81-
log('Processing transaction', { transactionMeta, signedTx });
84+
log('Processing transaction', {
85+
nonce: nonceDecimal,
86+
signedTx,
87+
transactionMeta,
88+
});
8289

8390
const publishPromise = createDeferredPromise<PublishHookResult>();
8491

85-
this.#publishPromises.push(publishPromise);
92+
this.#results.push({
93+
nonce: nonceDecimal,
94+
promise: publishPromise,
95+
signedTransaction: signedTx as Hex,
96+
});
97+
98+
this.#results = sortBy(this.#results, (r) => r.nonce);
8699

87-
if (this.#signedTransactions.length === this.#transactionCount) {
100+
if (this.#results.length === this.#transactionCount) {
88101
log('All transactions signed');
89102

103+
const signedTransactions = this.#results.map(
104+
(result) => result.signedTransaction,
105+
);
106+
90107
this.#readyPromise.resolve({
91-
signedTransactions: this.#signedTransactions,
108+
signedTransactions,
92109
});
93110
}
94111

0 commit comments

Comments
 (0)