Skip to content

Commit 327dee1

Browse files
feat: Configure pending transaction polling intervals using remote fe… (#5549)
…ature flags ## Explanation - Defines feature flag schema for accelerated polling parameters. - Creates a new `getAcceleratedPollingParams` to read said feature flag params. - Leverages the new method inside the constructor of `TransactionPoller`. - Updates existing unit tests and covers `getAcceleratedPollingParams` <!-- 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 Fixes: MetaMask/MetaMask-planning#4499 <!-- 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 --> ## Changelog <!-- If you're making any consumer-facing changes, list those changes here as if you were updating a changelog, using the template below as a guide. (CATEGORY is one of BREAKING, ADDED, CHANGED, DEPRECATED, REMOVED, or FIXED. For security-related issues, follow the Security Advisory process.) Please take care to name the exact pieces of the API you've added or changed (e.g. types, interfaces, functions, or methods). If there are any breaking changes, make sure to offer a solution for consumers to follow once they upgrade to the changes. Finally, if you're only making changes to development scripts or tests, you may replace the template below with "None". --> ### `@metamask/transaction-controller` - **CHANGED**: Accelerated transaction polling now leverages remote feature flags ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
1 parent 580f94a commit 327dee1

File tree

7 files changed

+352
-36
lines changed

7 files changed

+352
-36
lines changed

packages/transaction-controller/src/TransactionController.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3629,6 +3629,7 @@ export class TransactionController extends BaseController<
36293629
this.#multichainTrackingHelper.acquireNonceLockForChainIdKey({
36303630
chainId,
36313631
}),
3632+
messenger: this.messagingSystem,
36323633
publishTransaction: (_ethQuery, transactionMeta) =>
36333634
this.publishTransaction(_ethQuery, transactionMeta, {
36343635
skipSubmitHistory: true,

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { freeze } from 'immer';
55

66
import { PendingTransactionTracker } from './PendingTransactionTracker';
77
import { TransactionPoller } from './TransactionPoller';
8+
import type { TransactionControllerMessenger } from '../TransactionController';
89
import type { TransactionMeta } from '../types';
910
import { TransactionStatus } from '../types';
1011

@@ -74,11 +75,25 @@ function createTransactionPollerMock(): jest.Mocked<TransactionPoller> {
7475
} as unknown as jest.Mocked<TransactionPoller>;
7576
}
7677

78+
/**
79+
* Creates a mock messenger instance.
80+
*
81+
* @returns The mock messenger instance.
82+
*/
83+
function createMessengerMock(): jest.Mocked<TransactionControllerMessenger> {
84+
return {
85+
call: jest.fn().mockReturnValue({
86+
remoteFeatureFlags: {},
87+
}),
88+
} as unknown as jest.Mocked<TransactionControllerMessenger>;
89+
}
90+
7791
describe('PendingTransactionTracker', () => {
7892
const queryMock = jest.mocked(query);
7993
let blockTracker: jest.Mocked<BlockTracker>;
8094
let pendingTransactionTracker: PendingTransactionTracker;
8195
let transactionPoller: jest.Mocked<TransactionPoller>;
96+
let messenger: jest.Mocked<TransactionControllerMessenger>;
8297

8398
let options: jest.Mocked<
8499
ConstructorParameters<typeof PendingTransactionTracker>[0]
@@ -112,6 +127,7 @@ describe('PendingTransactionTracker', () => {
112127
beforeEach(() => {
113128
blockTracker = createBlockTrackerMock();
114129
transactionPoller = createTransactionPollerMock();
130+
messenger = createMessengerMock();
115131

116132
jest.mocked(TransactionPoller).mockImplementation(() => transactionPoller);
117133

@@ -123,6 +139,7 @@ describe('PendingTransactionTracker', () => {
123139
getTransactions: jest.fn(),
124140
getGlobalLock: jest.fn(() => Promise.resolve(jest.fn())),
125141
publishTransaction: jest.fn(),
142+
messenger,
126143
};
127144
});
128145

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import type {
44
BlockTracker,
55
NetworkClientId,
66
} from '@metamask/network-controller';
7+
import type { Hex } from '@metamask/utils';
78
// This package purposefully relies on Node's EventEmitter module.
89
// eslint-disable-next-line import-x/no-nodejs-modules
910
import EventEmitter from 'events';
1011
import { cloneDeep, merge } from 'lodash';
1112

1213
import { TransactionPoller } from './TransactionPoller';
1314
import { createModuleLogger, projectLogger } from '../logger';
15+
import type { TransactionControllerMessenger } from '../TransactionController';
1416
import type { TransactionMeta, TransactionReceipt } from '../types';
1517
import { TransactionStatus, TransactionType } from '../types';
1618

@@ -101,15 +103,16 @@ export class PendingTransactionTracker {
101103
blockTracker,
102104
getChainId,
103105
getEthQuery,
106+
getGlobalLock,
104107
getNetworkClientId,
105108
getTransactions,
109+
hooks,
106110
isResubmitEnabled,
107-
getGlobalLock,
111+
messenger,
108112
publishTransaction,
109-
hooks,
110113
}: {
111114
blockTracker: BlockTracker;
112-
getChainId: () => string;
115+
getChainId: () => Hex;
113116
getEthQuery: (networkClientId?: NetworkClientId) => EthQuery;
114117
getNetworkClientId: () => string;
115118
getTransactions: () => TransactionMeta[];
@@ -125,6 +128,7 @@ export class PendingTransactionTracker {
125128
) => boolean;
126129
beforePublish?: (transactionMeta: TransactionMeta) => boolean;
127130
};
131+
messenger: TransactionControllerMessenger;
128132
}) {
129133
this.hub = new EventEmitter() as PendingTransactionTrackerEventEmitter;
130134

@@ -138,7 +142,11 @@ export class PendingTransactionTracker {
138142
this.#getGlobalLock = getGlobalLock;
139143
this.#publishTransaction = publishTransaction;
140144
this.#running = false;
141-
this.#transactionPoller = new TransactionPoller(blockTracker);
145+
this.#transactionPoller = new TransactionPoller({
146+
blockTracker,
147+
chainId: getChainId(),
148+
messenger,
149+
});
142150
this.#beforePublish = hooks?.beforePublish ?? (() => true);
143151
this.#beforeCheckPendingTransaction =
144152
hooks?.beforeCheckPendingTransaction ?? (() => true);

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

Lines changed: 95 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,37 @@
11
import type { BlockTracker } from '@metamask/network-controller';
22

3-
import { ACCELERATED_COUNT_MAX, TransactionPoller } from './TransactionPoller';
3+
import { TransactionPoller } from './TransactionPoller';
44
import { flushPromises } from '../../../../tests/helpers';
5+
import type { TransactionControllerMessenger } from '../TransactionController';
56
import type { TransactionMeta } from '../types';
67

78
jest.useFakeTimers();
89

910
const BLOCK_NUMBER_MOCK = '0x123';
11+
const CHAIN_ID_MOCK = '0x1';
12+
const DEFAULT_ACCELERATED_COUNT_MAX = 10;
13+
const DEFAULT_ACCELERATED_POLLING_INTERVAL_MS = 3000;
1014

1115
const BLOCK_TRACKER_MOCK = {
1216
getLatestBlock: jest.fn(),
1317
on: jest.fn(),
1418
removeListener: jest.fn(),
1519
} as unknown as jest.Mocked<BlockTracker>;
1620

21+
const MESSENGER_MOCK = {
22+
call: jest.fn().mockReturnValue({
23+
remoteFeatureFlags: {},
24+
}),
25+
} as unknown as jest.Mocked<TransactionControllerMessenger>;
26+
27+
jest.mock('../utils/feature-flags', () => ({
28+
getAcceleratedPollingParams: () => ({
29+
countMax: DEFAULT_ACCELERATED_COUNT_MAX,
30+
intervalMs: DEFAULT_ACCELERATED_POLLING_INTERVAL_MS,
31+
}),
32+
FEATURE_FLAG_TRANSACTIONS: 'confirmations_transactions',
33+
}));
34+
1735
/**
1836
* Creates a mock transaction metadata object.
1937
*
@@ -32,7 +50,11 @@ describe('TransactionPoller', () => {
3250

3351
describe('Accelerated Polling', () => {
3452
it('invokes listener after timeout', async () => {
35-
const poller = new TransactionPoller(BLOCK_TRACKER_MOCK);
53+
const poller = new TransactionPoller({
54+
blockTracker: BLOCK_TRACKER_MOCK,
55+
messenger: MESSENGER_MOCK,
56+
chainId: CHAIN_ID_MOCK,
57+
});
3658

3759
const listener = jest.fn();
3860
poller.start(listener);
@@ -46,21 +68,29 @@ describe('TransactionPoller', () => {
4668
});
4769

4870
it('stops creating timeouts after max reached', async () => {
49-
const poller = new TransactionPoller(BLOCK_TRACKER_MOCK);
71+
const poller = new TransactionPoller({
72+
blockTracker: BLOCK_TRACKER_MOCK,
73+
messenger: MESSENGER_MOCK,
74+
chainId: CHAIN_ID_MOCK,
75+
});
5076

5177
const listener = jest.fn();
5278
poller.start(listener);
5379

54-
for (let i = 0; i < ACCELERATED_COUNT_MAX * 3; i++) {
80+
for (let i = 0; i < DEFAULT_ACCELERATED_COUNT_MAX * 3; i++) {
5581
jest.runOnlyPendingTimers();
5682
await flushPromises();
5783
}
5884

59-
expect(listener).toHaveBeenCalledTimes(ACCELERATED_COUNT_MAX);
85+
expect(listener).toHaveBeenCalledTimes(DEFAULT_ACCELERATED_COUNT_MAX);
6086
});
6187

6288
it('invokes listener with latest block number from block tracker', async () => {
63-
const poller = new TransactionPoller(BLOCK_TRACKER_MOCK);
89+
const poller = new TransactionPoller({
90+
blockTracker: BLOCK_TRACKER_MOCK,
91+
messenger: MESSENGER_MOCK,
92+
chainId: CHAIN_ID_MOCK,
93+
});
6494

6595
BLOCK_TRACKER_MOCK.getLatestBlock.mockResolvedValue(BLOCK_NUMBER_MOCK);
6696

@@ -74,7 +104,11 @@ describe('TransactionPoller', () => {
74104
});
75105

76106
it('does not create timeout if stopped while listener being invoked', async () => {
77-
const poller = new TransactionPoller(BLOCK_TRACKER_MOCK);
107+
const poller = new TransactionPoller({
108+
blockTracker: BLOCK_TRACKER_MOCK,
109+
messenger: MESSENGER_MOCK,
110+
chainId: CHAIN_ID_MOCK,
111+
});
78112

79113
const listener = jest.fn();
80114
listener.mockImplementation(() => poller.stop());
@@ -90,12 +124,16 @@ describe('TransactionPoller', () => {
90124

91125
describe('Block Tracker Polling', () => {
92126
it('invokes listener on block tracker update after accelerated limit reached', async () => {
93-
const poller = new TransactionPoller(BLOCK_TRACKER_MOCK);
127+
const poller = new TransactionPoller({
128+
blockTracker: BLOCK_TRACKER_MOCK,
129+
messenger: MESSENGER_MOCK,
130+
chainId: CHAIN_ID_MOCK,
131+
});
94132

95133
const listener = jest.fn();
96134
poller.start(listener);
97135

98-
for (let i = 0; i < ACCELERATED_COUNT_MAX; i++) {
136+
for (let i = 0; i < DEFAULT_ACCELERATED_COUNT_MAX; i++) {
99137
jest.runOnlyPendingTimers();
100138
await flushPromises();
101139
}
@@ -106,16 +144,20 @@ describe('TransactionPoller', () => {
106144
BLOCK_TRACKER_MOCK.on.mock.calls[0][1]();
107145
await flushPromises();
108146

109-
expect(listener).toHaveBeenCalledTimes(ACCELERATED_COUNT_MAX + 2);
147+
expect(listener).toHaveBeenCalledTimes(DEFAULT_ACCELERATED_COUNT_MAX + 2);
110148
});
111149

112150
it('invokes listener with latest block number from event', async () => {
113-
const poller = new TransactionPoller(BLOCK_TRACKER_MOCK);
151+
const poller = new TransactionPoller({
152+
blockTracker: BLOCK_TRACKER_MOCK,
153+
messenger: MESSENGER_MOCK,
154+
chainId: CHAIN_ID_MOCK,
155+
});
114156

115157
const listener = jest.fn();
116158
poller.start(listener);
117159

118-
for (let i = 0; i < ACCELERATED_COUNT_MAX; i++) {
160+
for (let i = 0; i < DEFAULT_ACCELERATED_COUNT_MAX; i++) {
119161
jest.runOnlyPendingTimers();
120162
await flushPromises();
121163
}
@@ -129,7 +171,11 @@ describe('TransactionPoller', () => {
129171

130172
describe('start', () => {
131173
it('does nothing if already started', () => {
132-
const poller = new TransactionPoller(BLOCK_TRACKER_MOCK);
174+
const poller = new TransactionPoller({
175+
blockTracker: BLOCK_TRACKER_MOCK,
176+
messenger: MESSENGER_MOCK,
177+
chainId: CHAIN_ID_MOCK,
178+
});
133179

134180
poller.start(jest.fn());
135181
poller.start(jest.fn());
@@ -140,7 +186,11 @@ describe('TransactionPoller', () => {
140186

141187
describe('stop', () => {
142188
it('removes timeout', () => {
143-
const poller = new TransactionPoller(BLOCK_TRACKER_MOCK);
189+
const poller = new TransactionPoller({
190+
blockTracker: BLOCK_TRACKER_MOCK,
191+
messenger: MESSENGER_MOCK,
192+
chainId: CHAIN_ID_MOCK,
193+
});
144194

145195
const listener = jest.fn();
146196
poller.start(listener);
@@ -151,24 +201,32 @@ describe('TransactionPoller', () => {
151201
});
152202

153203
it('removes block tracker listener', async () => {
154-
const poller = new TransactionPoller(BLOCK_TRACKER_MOCK);
204+
const poller = new TransactionPoller({
205+
blockTracker: BLOCK_TRACKER_MOCK,
206+
messenger: MESSENGER_MOCK,
207+
chainId: CHAIN_ID_MOCK,
208+
});
155209

156210
const listener = jest.fn();
157211
poller.start(listener);
158212

159-
for (let i = 0; i < ACCELERATED_COUNT_MAX; i++) {
213+
for (let i = 0; i < DEFAULT_ACCELERATED_COUNT_MAX; i++) {
160214
jest.runOnlyPendingTimers();
161215
await flushPromises();
162216
}
163217

164218
poller.stop();
165219

166220
expect(BLOCK_TRACKER_MOCK.removeListener).toHaveBeenCalledTimes(1);
167-
expect(listener).toHaveBeenCalledTimes(ACCELERATED_COUNT_MAX);
221+
expect(listener).toHaveBeenCalledTimes(DEFAULT_ACCELERATED_COUNT_MAX);
168222
});
169223

170224
it('does nothing if not started', async () => {
171-
const poller = new TransactionPoller(BLOCK_TRACKER_MOCK);
225+
const poller = new TransactionPoller({
226+
blockTracker: BLOCK_TRACKER_MOCK,
227+
messenger: MESSENGER_MOCK,
228+
chainId: CHAIN_ID_MOCK,
229+
});
172230

173231
poller.stop();
174232

@@ -191,7 +249,11 @@ describe('TransactionPoller', () => {
191249
])(
192250
'resets accelerated count if transaction IDs %s',
193251
async (_title, newPendingTransactions) => {
194-
const poller = new TransactionPoller(BLOCK_TRACKER_MOCK);
252+
const poller = new TransactionPoller({
253+
blockTracker: BLOCK_TRACKER_MOCK,
254+
messenger: MESSENGER_MOCK,
255+
chainId: CHAIN_ID_MOCK,
256+
});
195257

196258
poller.setPendingTransactions([
197259
createTransactionMetaMock('1'),
@@ -208,12 +270,14 @@ describe('TransactionPoller', () => {
208270

209271
poller.setPendingTransactions(newPendingTransactions);
210272

211-
for (let i = 0; i < ACCELERATED_COUNT_MAX; i++) {
273+
for (let i = 0; i < DEFAULT_ACCELERATED_COUNT_MAX; i++) {
212274
jest.runOnlyPendingTimers();
213275
await flushPromises();
214276
}
215277

216-
expect(listener).toHaveBeenCalledTimes(ACCELERATED_COUNT_MAX + 3);
278+
expect(listener).toHaveBeenCalledTimes(
279+
DEFAULT_ACCELERATED_COUNT_MAX + 3,
280+
);
217281
},
218282
);
219283

@@ -230,7 +294,11 @@ describe('TransactionPoller', () => {
230294
])(
231295
'resets to accelerated polling if transaction IDs added',
232296
async (_title, newPendingTransactions) => {
233-
const poller = new TransactionPoller(BLOCK_TRACKER_MOCK);
297+
const poller = new TransactionPoller({
298+
blockTracker: BLOCK_TRACKER_MOCK,
299+
messenger: MESSENGER_MOCK,
300+
chainId: CHAIN_ID_MOCK,
301+
});
234302

235303
poller.setPendingTransactions([
236304
createTransactionMetaMock('1'),
@@ -240,7 +308,7 @@ describe('TransactionPoller', () => {
240308
const listener = jest.fn();
241309
poller.start(listener);
242310

243-
for (let i = 0; i < ACCELERATED_COUNT_MAX; i++) {
311+
for (let i = 0; i < DEFAULT_ACCELERATED_COUNT_MAX; i++) {
244312
jest.runOnlyPendingTimers();
245313
await flushPromises();
246314
}
@@ -253,12 +321,14 @@ describe('TransactionPoller', () => {
253321

254322
poller.setPendingTransactions(newPendingTransactions);
255323

256-
for (let i = 0; i < ACCELERATED_COUNT_MAX; i++) {
324+
for (let i = 0; i < DEFAULT_ACCELERATED_COUNT_MAX; i++) {
257325
jest.runOnlyPendingTimers();
258326
await flushPromises();
259327
}
260328

261-
expect(listener).toHaveBeenCalledTimes(ACCELERATED_COUNT_MAX * 2 + 2);
329+
expect(listener).toHaveBeenCalledTimes(
330+
DEFAULT_ACCELERATED_COUNT_MAX * 2 + 2,
331+
);
262332
},
263333
);
264334
});

0 commit comments

Comments
 (0)