Skip to content

Commit 6e10739

Browse files
Support custom nonce value (#3579)
Encapsulate all nonce logic in a new file with additional logging. Update transaction metadata after approval if the approval result includes the value.txMeta property. Add type to incoming transactions.
1 parent 41b0eca commit 6e10739

File tree

10 files changed

+349
-159
lines changed

10 files changed

+349
-159
lines changed

packages/transaction-controller/jest.config.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ module.exports = merge(baseConfig, {
1717
// An object that configures minimum threshold enforcement for coverage results
1818
coverageThreshold: {
1919
global: {
20-
branches: 88.78,
21-
functions: 94.16,
22-
lines: 98.07,
23-
statements: 98.03,
20+
branches: 89.18,
21+
functions: 94.18,
22+
lines: 98.09,
23+
statements: 98.05,
2424
},
2525
},
2626

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ function buildMockMessenger({
213213

214214
if (delay) {
215215
promise = new Promise((res, rej) => {
216-
approve = () => res({ resultCallbacks });
216+
approve = (value?: any) => res({ resultCallbacks, value });
217217
reject = rej;
218218
});
219219
}
@@ -237,7 +237,7 @@ function buildMockMessenger({
237237

238238
return {
239239
messenger,
240-
approve: approve as unknown as () => void,
240+
approve: approve as unknown as (value?: any) => void,
241241
reject: reject as unknown as (reason: unknown) => void,
242242
};
243243
}
@@ -454,7 +454,7 @@ describe('TransactionController', () => {
454454
let messengerMock: TransactionControllerMessenger;
455455
let rejectMessengerMock: TransactionControllerMessenger;
456456
let delayMessengerMock: TransactionControllerMessenger;
457-
let approveTransaction: () => void;
457+
let approveTransaction: (value?: any) => void;
458458
let getNonceLockSpy: jest.Mock;
459459
let incomingTransactionHelperMock: jest.Mocked<IncomingTransactionHelper>;
460460
let pendingTransactionTrackerMock: jest.Mocked<PendingTransactionTracker>;
@@ -1342,6 +1342,29 @@ describe('TransactionController', () => {
13421342
expect(resultCallbacksMock.error).toHaveBeenCalledTimes(1);
13431343
});
13441344

1345+
it('updates transaction if approval result includes updated metadata', async () => {
1346+
const controller = newController();
1347+
1348+
const { result } = await controller.addTransaction({
1349+
from: ACCOUNT_MOCK,
1350+
to: ACCOUNT_MOCK,
1351+
});
1352+
1353+
const transaction = controller.state.transactions[0];
1354+
1355+
approveTransaction({
1356+
txMeta: { ...transaction, customNonceValue: '123' },
1357+
});
1358+
1359+
await result;
1360+
1361+
expect(controller.state.transactions).toStrictEqual([
1362+
expect.objectContaining({
1363+
customNonceValue: '123',
1364+
}),
1365+
]);
1366+
});
1367+
13451368
describe('fails', () => {
13461369
/**
13471370
* Test template to assert adding and submitting a transaction fails.

packages/transaction-controller/src/TransactionController.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,16 @@ import {
6666
addInitialHistorySnapshot,
6767
updateTransactionHistory,
6868
} from './utils/history';
69+
import {
70+
getAndFormatTransactionsForNonceTracker,
71+
getNextNonce,
72+
} from './utils/nonce';
6973
import {
7074
updatePostTransactionBalance,
7175
updateSwapsTransaction,
7276
} from './utils/swaps';
7377
import { determineTransactionType } from './utils/transaction-type';
7478
import {
75-
getAndFormatTransactionsForNonceTracker,
7679
getIncreasedPriceFromExisting,
7780
normalizeTxParams,
7881
isEIP1559Transaction,
@@ -1882,6 +1885,26 @@ export class TransactionController extends BaseControllerV1<
18821885
resultCallbacks = undefined;
18831886
});
18841887
}
1888+
1889+
const approvalValue = acceptResult.value as
1890+
| {
1891+
txMeta?: TransactionMeta;
1892+
}
1893+
| undefined;
1894+
1895+
const updatedTransaction = approvalValue?.txMeta;
1896+
1897+
if (updatedTransaction) {
1898+
log('Updating transaction with approval data', {
1899+
customNonce: updatedTransaction.customNonceValue,
1900+
params: updatedTransaction.txParams,
1901+
});
1902+
1903+
this.updateTransaction(
1904+
updatedTransaction,
1905+
'TransactionController#processApproval - Updated with approval data',
1906+
);
1907+
}
18851908
}
18861909

18871910
const { isCompleted: isTxCompleted } =
@@ -1951,10 +1974,13 @@ export class TransactionController extends BaseControllerV1<
19511974
const chainId = this.getChainId();
19521975
const index = transactions.findIndex(({ id }) => transactionId === id);
19531976
const transactionMeta = transactions[index];
1977+
19541978
const {
1955-
txParams: { nonce, from },
1979+
txParams: { from },
19561980
} = transactionMeta;
1957-
let nonceLock;
1981+
1982+
let releaseNonceLock: (() => void) | undefined;
1983+
19581984
try {
19591985
if (!this.sign) {
19601986
releaseLock();
@@ -1974,16 +2000,15 @@ export class TransactionController extends BaseControllerV1<
19742000
return;
19752001
}
19762002

1977-
let nonceToUse = nonce;
1978-
// if a nonce already exists on the transactionMeta it means this is a speedup or cancel transaction
1979-
// so we want to reuse that nonce and hope that it beats the previous attempt to chain. Otherwise use a new locked nonce
1980-
if (!nonceToUse) {
1981-
nonceLock = await this.nonceTracker.getNonceLock(from);
1982-
nonceToUse = addHexPrefix(nonceLock.nextNonce.toString(16));
1983-
}
2003+
const [nonce, releaseNonce] = await getNextNonce(
2004+
transactionMeta,
2005+
this.nonceTracker,
2006+
);
2007+
2008+
releaseNonceLock = releaseNonce;
19842009

19852010
transactionMeta.status = TransactionStatus.approved;
1986-
transactionMeta.txParams.nonce = nonceToUse;
2011+
transactionMeta.txParams.nonce = nonce;
19872012
transactionMeta.txParams.chainId = chainId;
19882013

19892014
const baseTxParams = {
@@ -2057,9 +2082,7 @@ export class TransactionController extends BaseControllerV1<
20572082
} finally {
20582083
this.inProcessOfSigning.delete(transactionId);
20592084
// must set transaction to submitted/failed before releasing lock
2060-
if (nonceLock) {
2061-
nonceLock.releaseLock();
2062-
}
2085+
releaseNonceLock?.();
20632086
releaseLock();
20642087
}
20652088
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { v1 as random } from 'uuid';
22

33
import { CHAIN_IDS } from '../constants';
4-
import { TransactionStatus } from '../types';
4+
import { TransactionStatus, TransactionType } from '../types';
55
import type {
66
EtherscanTokenTransactionMeta,
77
EtherscanTransactionMeta,
@@ -116,6 +116,7 @@ const EXPECTED_NORMALISED_TRANSACTION_BASE = {
116116
to: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.to,
117117
value: '0xb1a2bc2ec50000',
118118
},
119+
type: TransactionType.incoming,
119120
verifiedOnBlockchain: false,
120121
};
121122

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
RemoteTransactionSourceRequest,
1111
TransactionMeta,
1212
} from '../types';
13-
import { TransactionStatus } from '../types';
13+
import { TransactionStatus, TransactionType } from '../types';
1414
import {
1515
fetchEtherscanTokenTransactions,
1616
fetchEtherscanTransactions,
@@ -177,6 +177,7 @@ export class EtherscanRemoteTransactionSource
177177
to: txMeta.to,
178178
value: BNToHex(new BN(txMeta.value)),
179179
},
180+
type: TransactionType.incoming,
180181
verifiedOnBlockchain: false,
181182
};
182183
}

packages/transaction-controller/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ type TransactionMetaBase = {
105105
*/
106106
custodyStatus?: string;
107107

108+
/** The optional custom nonce override as a decimal string. */
109+
customNonceValue?: string;
110+
108111
/**
109112
* The custom token amount is the amount set by the user.
110113
*/
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import type {
2+
NonceTracker,
3+
Transaction as NonceTrackerTransaction,
4+
} from 'nonce-tracker';
5+
6+
import type { TransactionMeta } from '../types';
7+
import { TransactionStatus } from '../types';
8+
import { getAndFormatTransactionsForNonceTracker, getNextNonce } from './nonce';
9+
10+
const TRANSACTION_META_MOCK: TransactionMeta = {
11+
chainId: '0x1',
12+
id: 'testId1',
13+
status: TransactionStatus.unapproved,
14+
time: 1,
15+
txParams: {
16+
from: '0x1',
17+
},
18+
};
19+
20+
/**
21+
* Creates a mock instance of a nonce tracker.
22+
* @returns The mock instance.
23+
*/
24+
function createNonceTrackerMock(): jest.Mocked<NonceTracker> {
25+
return { getNonceLock: jest.fn() } as any;
26+
}
27+
28+
describe('nonce', () => {
29+
describe('getNextNonce', () => {
30+
it('returns custom nonce if provided', async () => {
31+
const transactionMeta = {
32+
...TRANSACTION_META_MOCK,
33+
customNonceValue: '123',
34+
};
35+
36+
const nonceTracker = createNonceTrackerMock();
37+
38+
const [nonce, releaseLock] = await getNextNonce(
39+
transactionMeta,
40+
nonceTracker,
41+
);
42+
43+
expect(nonce).toBe('0x7b');
44+
expect(releaseLock).toBeUndefined();
45+
});
46+
47+
it('returns existing nonce if provided and no custom nonce', async () => {
48+
const transactionMeta = {
49+
...TRANSACTION_META_MOCK,
50+
txParams: {
51+
...TRANSACTION_META_MOCK.txParams,
52+
nonce: '0x123',
53+
},
54+
};
55+
56+
const nonceTracker = createNonceTrackerMock();
57+
58+
const [nonce, releaseLock] = await getNextNonce(
59+
transactionMeta,
60+
nonceTracker,
61+
);
62+
63+
expect(nonce).toBe('0x123');
64+
expect(releaseLock).toBeUndefined();
65+
});
66+
67+
it('returns next nonce from tracker if no custom nonce and no existing nonce', async () => {
68+
const transactionMeta = {
69+
...TRANSACTION_META_MOCK,
70+
txParams: {
71+
...TRANSACTION_META_MOCK.txParams,
72+
},
73+
};
74+
75+
const nonceTracker = createNonceTrackerMock();
76+
const releaseLock = jest.fn();
77+
78+
nonceTracker.getNonceLock.mockResolvedValueOnce({
79+
nextNonce: 456,
80+
releaseLock,
81+
} as any);
82+
83+
const [nonce, resultReleaseLock] = await getNextNonce(
84+
transactionMeta,
85+
nonceTracker,
86+
);
87+
88+
expect(nonce).toBe('0x1c8');
89+
90+
resultReleaseLock?.();
91+
92+
expect(releaseLock).toHaveBeenCalledTimes(1);
93+
});
94+
});
95+
96+
describe('getAndFormatTransactionsForNonceTracker', () => {
97+
it('returns formatted transactions filtered by chain, from, isTransfer, and status', () => {
98+
const fromAddress = '0x123';
99+
const inputTransactions: TransactionMeta[] = [
100+
{
101+
id: '1',
102+
chainId: '0x1',
103+
time: 123456,
104+
txParams: {
105+
from: fromAddress,
106+
gas: '0x100',
107+
value: '0x200',
108+
nonce: '0x1',
109+
},
110+
status: TransactionStatus.confirmed,
111+
},
112+
{
113+
id: '2',
114+
chainId: '0x1',
115+
time: 123457,
116+
txParams: {
117+
from: '0x124',
118+
gas: '0x101',
119+
value: '0x201',
120+
nonce: '0x2',
121+
},
122+
status: TransactionStatus.submitted,
123+
},
124+
{
125+
id: '3',
126+
chainId: '0x1',
127+
time: 123458,
128+
txParams: {
129+
from: fromAddress,
130+
gas: '0x102',
131+
value: '0x202',
132+
nonce: '0x3',
133+
},
134+
status: TransactionStatus.approved,
135+
},
136+
{
137+
id: '4',
138+
chainId: '0x2',
139+
time: 123459,
140+
txParams: {
141+
from: fromAddress,
142+
gas: '0x103',
143+
value: '0x203',
144+
nonce: '0x4',
145+
},
146+
status: TransactionStatus.confirmed,
147+
},
148+
{
149+
id: '5',
150+
chainId: '0x2',
151+
isTransfer: true,
152+
time: 123460,
153+
txParams: {
154+
from: fromAddress,
155+
gas: '0x104',
156+
value: '0x204',
157+
nonce: '0x5',
158+
},
159+
status: TransactionStatus.confirmed,
160+
},
161+
];
162+
163+
const expectedResult: NonceTrackerTransaction[] = [
164+
{
165+
status: TransactionStatus.confirmed,
166+
history: [{}],
167+
txParams: {
168+
from: fromAddress,
169+
gas: '0x103',
170+
value: '0x203',
171+
nonce: '0x4',
172+
},
173+
},
174+
];
175+
176+
const result = getAndFormatTransactionsForNonceTracker(
177+
'0x2',
178+
fromAddress,
179+
TransactionStatus.confirmed,
180+
inputTransactions,
181+
);
182+
183+
expect(result).toStrictEqual(expectedResult);
184+
});
185+
});
186+
});

0 commit comments

Comments
 (0)