From 222314beec8e3add660fcbcd45f254d72bd389bc Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Tue, 1 Oct 2024 16:42:21 -0700 Subject: [PATCH 1/3] make polling input generic --- .../src/AccountTrackerController.test.ts | 9 +- .../src/AccountTrackerController.ts | 14 +- .../src/CurrencyRateController.test.ts | 8 +- .../src/CurrencyRateController.ts | 15 +- .../src/TokenDetectionController.test.ts | 11 +- .../src/TokenDetectionController.ts | 18 +- .../src/TokenListController.test.ts | 14 +- .../src/TokenListController.ts | 20 +- .../src/TokenRatesController.test.ts | 21 +- .../src/TokenRatesController.ts | 15 +- .../src/GasFeeController.test.ts | 8 +- .../src/GasFeeController.ts | 12 +- .../src/AbstractPollingController.ts | 55 ++---- .../src/BlockTrackerPollingController.test.ts | 131 +++++++------ .../src/BlockTrackerPollingController.ts | 64 ++++--- .../StaticIntervalPollingController.test.ts | 179 ++++++++++-------- .../src/StaticIntervalPollingController.ts | 42 ++-- packages/polling-controller/src/types.ts | 26 +-- .../src/UserOperationController.test.ts | 10 +- .../src/UserOperationController.ts | 4 +- .../PendingUserOperationTracker.test.ts | 43 ++--- .../helpers/PendingUserOperationTracker.ts | 16 +- 22 files changed, 422 insertions(+), 313 deletions(-) diff --git a/packages/assets-controllers/src/AccountTrackerController.test.ts b/packages/assets-controllers/src/AccountTrackerController.test.ts index 707b7a906e6..4c475f0b755 100644 --- a/packages/assets-controllers/src/AccountTrackerController.test.ts +++ b/packages/assets-controllers/src/AccountTrackerController.test.ts @@ -505,7 +505,9 @@ describe('AccountTrackerController', () => { .spyOn(controller, 'refresh') .mockResolvedValue(); - controller.startPollingByNetworkClientId(networkClientId1); + controller.startPolling({ + networkClientId: networkClientId1, + }); await advanceTime({ clock, duration: 0 }); expect(refreshSpy).toHaveBeenNthCalledWith(1, networkClientId1); @@ -516,8 +518,9 @@ describe('AccountTrackerController', () => { expect(refreshSpy).toHaveBeenNthCalledWith(2, networkClientId1); expect(refreshSpy).toHaveBeenCalledTimes(2); - const pollToken = - controller.startPollingByNetworkClientId(networkClientId2); + const pollToken = controller.startPolling({ + networkClientId: networkClientId2, + }); await advanceTime({ clock, duration: 0 }); expect(refreshSpy).toHaveBeenNthCalledWith(3, networkClientId2); diff --git a/packages/assets-controllers/src/AccountTrackerController.ts b/packages/assets-controllers/src/AccountTrackerController.ts index fb1f131cc71..e58bac61fc3 100644 --- a/packages/assets-controllers/src/AccountTrackerController.ts +++ b/packages/assets-controllers/src/AccountTrackerController.ts @@ -120,10 +120,15 @@ export type AccountTrackerControllerMessenger = RestrictedControllerMessenger< AllowedEvents['type'] >; +/** The input to start polling for the {@link AccountTrackerController} */ +type AccountTrackerPollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that tracks the network balances for all user accounts. */ -export class AccountTrackerController extends StaticIntervalPollingController< +export class AccountTrackerController extends StaticIntervalPollingController()< typeof controllerName, AccountTrackerControllerState, AccountTrackerControllerMessenger @@ -309,9 +314,12 @@ export class AccountTrackerController extends StaticIntervalPollingController< /** * Refreshes the balances of the accounts using the networkClientId * - * @param networkClientId - The network client ID used to get balances. + * @param input - The input for the poll. + * @param input.networkClientId - The network client ID used to get balances. */ - async _executePoll(networkClientId: string): Promise { + async _executePoll({ + networkClientId, + }: AccountTrackerPollingInput): Promise { // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.refresh(networkClientId); diff --git a/packages/assets-controllers/src/CurrencyRateController.test.ts b/packages/assets-controllers/src/CurrencyRateController.test.ts index c719301cbe4..4731e026bfc 100644 --- a/packages/assets-controllers/src/CurrencyRateController.test.ts +++ b/packages/assets-controllers/src/CurrencyRateController.test.ts @@ -155,7 +155,7 @@ describe('CurrencyRateController', () => { messenger, }); - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); await advanceTime({ clock, duration: 0 }); expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1); expect(controller.state.currencyRates).toStrictEqual({ @@ -192,7 +192,7 @@ describe('CurrencyRateController', () => { messenger, }); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); await advanceTime({ clock, duration: 0 }); @@ -217,7 +217,7 @@ describe('CurrencyRateController', () => { fetchExchangeRate: fetchExchangeRateStub, messenger, }); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); await advanceTime({ clock, duration: 0 }); controller.stopAllPolling(); @@ -225,7 +225,7 @@ describe('CurrencyRateController', () => { // called once upon initial start expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); await advanceTime({ clock, duration: 0 }); expect(fetchExchangeRateStub).toHaveBeenCalledTimes(2); diff --git a/packages/assets-controllers/src/CurrencyRateController.ts b/packages/assets-controllers/src/CurrencyRateController.ts index 319e819818f..badc1925323 100644 --- a/packages/assets-controllers/src/CurrencyRateController.ts +++ b/packages/assets-controllers/src/CurrencyRateController.ts @@ -78,11 +78,16 @@ const defaultState = { }, }; +/** The input to start polling for the {@link CurrencyRateController} */ +type CurrencyRatePollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that passively polls on a set interval for an exchange rate from the current network * asset to the user's preferred currency. */ -export class CurrencyRateController extends StaticIntervalPollingController< +export class CurrencyRateController extends StaticIntervalPollingController()< typeof name, CurrencyRateState, CurrencyRateMessenger @@ -237,10 +242,12 @@ export class CurrencyRateController extends StaticIntervalPollingController< /** * Updates exchange rate for the current currency. * - * @param networkClientId - The network client ID used to get a ticker value. - * @returns The controller state. + * @param input - The input for the poll. + * @param input.networkClientId - The network client ID used to get a ticker value. */ - async _executePoll(networkClientId: NetworkClientId): Promise { + async _executePoll({ + networkClientId, + }: CurrencyRatePollingInput): Promise { const networkClient = this.messagingSystem.call( 'NetworkController:getNetworkClientById', networkClientId, diff --git a/packages/assets-controllers/src/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts index 21ae2029dd0..72af50d7bcb 100644 --- a/packages/assets-controllers/src/TokenDetectionController.test.ts +++ b/packages/assets-controllers/src/TokenDetectionController.test.ts @@ -1855,7 +1855,7 @@ describe('TokenDetectionController', () => { }); }); - describe('startPollingByNetworkClientId', () => { + describe('startPolling', () => { let clock: sinon.SinonFakeTimers; beforeEach(() => { clock = sinon.useFakeTimers(); @@ -1904,13 +1904,16 @@ describe('TokenDetectionController', () => { return Promise.resolve(); }); - controller.startPollingByNetworkClientId('mainnet', { + controller.startPolling({ + networkClientId: 'mainnet', address: '0x1', }); - controller.startPollingByNetworkClientId('sepolia', { + controller.startPolling({ + networkClientId: 'sepolia', address: '0xdeadbeef', }); - controller.startPollingByNetworkClientId('goerli', { + controller.startPolling({ + networkClientId: 'goerli', address: '0x3', }); await advanceTime({ clock, duration: 0 }); diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index 8e483d8f37b..2459baea38f 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -138,6 +138,12 @@ export type TokenDetectionControllerMessenger = RestrictedControllerMessenger< AllowedEvents['type'] >; +/** The input to start polling for the {@link TokenDetectionController} */ +type TokenDetectionPollingInput = { + networkClientId: NetworkClientId; + address: string; +}; + /** * Controller that passively polls on a set interval for Tokens auto detection * @property intervalId - Polling interval used to fetch new token rates @@ -148,7 +154,7 @@ export type TokenDetectionControllerMessenger = RestrictedControllerMessenger< * @property isDetectionEnabledFromPreferences - Boolean to track if detection is enabled from PreferencesController * @property isDetectionEnabledForNetwork - Boolean to track if detected is enabled for current network */ -export class TokenDetectionController extends StaticIntervalPollingController< +export class TokenDetectionController extends StaticIntervalPollingController()< typeof controllerName, TokenDetectionState, TokenDetectionControllerMessenger @@ -432,16 +438,16 @@ export class TokenDetectionController extends StaticIntervalPollingController< }; } - async _executePoll( - networkClientId: NetworkClientId, - options: { address: string }, - ): Promise { + async _executePoll({ + networkClientId, + address, + }: TokenDetectionPollingInput): Promise { if (!this.isActive) { return; } await this.detectTokens({ networkClientId, - selectedAddress: options.address, + selectedAddress: address, }); } diff --git a/packages/assets-controllers/src/TokenListController.test.ts b/packages/assets-controllers/src/TokenListController.test.ts index cb02140a806..05eeb167873 100644 --- a/packages/assets-controllers/src/TokenListController.test.ts +++ b/packages/assets-controllers/src/TokenListController.test.ts @@ -1157,7 +1157,7 @@ describe('TokenListController', () => { }); }); - describe('startPollingByNetworkClient', () => { + describe('startPolling', () => { let clock: sinon.SinonFakeTimers; const pollingIntervalTime = 1000; beforeEach(() => { @@ -1200,7 +1200,7 @@ describe('TokenListController', () => { expiredCacheExistingState.tokenList, ); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); await advanceTime({ clock, duration: 0 }); expect(fetchTokenListByChainIdSpy.mock.calls[0]).toStrictEqual( @@ -1236,7 +1236,7 @@ describe('TokenListController', () => { expiredCacheExistingState.tokenList, ); - controller.startPollingByNetworkClientId('goerli'); + controller.startPolling({ networkClientId: 'goerli' }); await advanceTime({ clock, duration: 0 }); expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(1); @@ -1306,7 +1306,9 @@ describe('TokenListController', () => { expect(controller.state).toStrictEqual(startingState); // start polling for sepolia - const pollingToken = controller.startPollingByNetworkClientId('sepolia'); + const pollingToken = controller.startPolling({ + networkClientId: 'sepolia', + }); // wait a polling interval await advanceTime({ clock, duration: pollingIntervalTime }); @@ -1324,7 +1326,9 @@ describe('TokenListController', () => { controller.stopPollingByPollingToken(pollingToken); // start polling for binance - controller.startPollingByNetworkClientId('binance-network-client-id'); + controller.startPolling({ + networkClientId: 'binance-network-client-id', + }); await advanceTime({ clock, duration: pollingIntervalTime }); // expect fetchTokenListByChain to be called for binance, but not for sepolia diff --git a/packages/assets-controllers/src/TokenListController.ts b/packages/assets-controllers/src/TokenListController.ts index d4290e6a7d2..e4504ec0e94 100644 --- a/packages/assets-controllers/src/TokenListController.ts +++ b/packages/assets-controllers/src/TokenListController.ts @@ -92,10 +92,15 @@ export const getDefaultTokenListState = (): TokenListState => { }; }; +/** The input to start polling for the {@link TokenListController} */ +type TokenListPollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that passively polls on a set interval for the list of tokens from metaswaps api */ -export class TokenListController extends StaticIntervalPollingController< +export class TokenListController extends StaticIntervalPollingController()< typeof name, TokenListState, TokenListControllerMessenger @@ -211,7 +216,7 @@ export class TokenListController extends StaticIntervalPollingController< if (!isTokenListSupportedForNetwork(this.chainId)) { return; } - await this.startPolling(); + await this.#startPolling(); } /** @@ -219,7 +224,7 @@ export class TokenListController extends StaticIntervalPollingController< */ async restart() { this.stopPolling(); - await this.startPolling(); + await this.#startPolling(); } /** @@ -248,7 +253,7 @@ export class TokenListController extends StaticIntervalPollingController< /** * Starts a new polling interval. */ - private async startPolling(): Promise { + async #startPolling(): Promise { await safelyExecute(() => this.fetchTokenList()); // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-misused-promises @@ -261,10 +266,13 @@ export class TokenListController extends StaticIntervalPollingController< * Fetching token list from the Token Service API. * * @private - * @param networkClientId - The ID of the network client triggering the fetch. + * @param input - The input for the poll. + * @param input.networkClientId - The ID of the network client triggering the fetch. * @returns A promise that resolves when this operation completes. */ - async _executePoll(networkClientId: string): Promise { + async _executePoll({ + networkClientId, + }: TokenListPollingInput): Promise { return this.fetchTokenList(networkClientId); } diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index 2fcfdb46e68..8a48fe941a4 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -1216,7 +1216,9 @@ describe('TokenRatesController', () => { }, }, async ({ controller }) => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); @@ -1268,7 +1270,9 @@ describe('TokenRatesController', () => { }, }, async ({ controller }) => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(controller.state).toStrictEqual({ @@ -1372,7 +1376,9 @@ describe('TokenRatesController', () => { }, }, async ({ controller }) => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); // flush promises and advance setTimeouts they enqueue 3 times // needed because fetch() doesn't resolve immediately, so any // downstream promises aren't flushed until the next advanceTime loop @@ -1472,7 +1478,9 @@ describe('TokenRatesController', () => { }, }, async ({ controller }) => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); // flush promises and advance setTimeouts they enqueue 3 times // needed because fetch() doesn't resolve immediately, so any // downstream promises aren't flushed until the next advanceTime loop @@ -1513,8 +1521,9 @@ describe('TokenRatesController', () => { }, }, async ({ controller }) => { - const pollingToken = - controller.startPollingByNetworkClientId('mainnet'); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( 1, diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index 102e52a9e24..631b0a40e6e 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -221,11 +221,16 @@ export const getDefaultTokenRatesControllerState = }; }; +/** The input to start polling for the {@link TokenRatesController} */ +export type TokenRatesPollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that passively polls on a set interval for token-to-fiat exchange rates * for tokens stored in the TokensController */ -export class TokenRatesController extends StaticIntervalPollingController< +export class TokenRatesController extends StaticIntervalPollingController()< typeof controllerName, TokenRatesControllerState, TokenRatesControllerMessenger @@ -619,10 +624,12 @@ export class TokenRatesController extends StaticIntervalPollingController< /** * Updates token rates for the given networkClientId * - * @param networkClientId - The network client ID used to get a ticker value. - * @returns The controller state. + * @param input - The input for the poll. + * @param input.networkClientId - The network client ID used to get a ticker value. */ - async _executePoll(networkClientId: NetworkClientId): Promise { + async _executePoll({ + networkClientId, + }: TokenRatesPollingInput): Promise { const networkClient = this.messagingSystem.call( 'NetworkController:getNetworkClientById', networkClientId, diff --git a/packages/gas-fee-controller/src/GasFeeController.test.ts b/packages/gas-fee-controller/src/GasFeeController.test.ts index b8b09177143..4b4bf928b3f 100644 --- a/packages/gas-fee-controller/src/GasFeeController.test.ts +++ b/packages/gas-fee-controller/src/GasFeeController.test.ts @@ -1199,7 +1199,9 @@ describe('GasFeeController', () => { interval: pollingInterval, }); - gasFeeController.startPollingByNetworkClientId('goerli'); + gasFeeController.startPolling({ + networkClientId: 'goerli', + }); await clock.tickAsync(0); expect(mockedDetermineGasFeeCalculations).toHaveBeenNthCalledWith( 1, @@ -1228,7 +1230,9 @@ describe('GasFeeController', () => { gasFeeController.state.gasFeeEstimatesByChainId?.['0x5'], ).toStrictEqual(buildMockGasFeeStateFeeMarket()); - gasFeeController.startPollingByNetworkClientId('sepolia'); + gasFeeController.startPolling({ + networkClientId: 'sepolia', + }); await clock.tickAsync(pollingInterval); expect(mockedDetermineGasFeeCalculations).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/gas-fee-controller/src/GasFeeController.ts b/packages/gas-fee-controller/src/GasFeeController.ts index 9e7b30515ef..13587418a33 100644 --- a/packages/gas-fee-controller/src/GasFeeController.ts +++ b/packages/gas-fee-controller/src/GasFeeController.ts @@ -256,10 +256,15 @@ const defaultState: GasFeeState = { nonRPCGasFeeApisDisabled: false, }; +/** The input to start polling for the {@link GasFeeController} */ +type GasFeePollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that retrieves gas fee estimate data and polls for updated data on a set interval */ -export class GasFeeController extends StaticIntervalPollingController< +export class GasFeeController extends StaticIntervalPollingController()< typeof name, GasFeeState, GasFeeMessenger @@ -560,10 +565,11 @@ export class GasFeeController extends StaticIntervalPollingController< * Fetching token list from the Token Service API. * * @private - * @param networkClientId - The ID of the network client triggering the fetch. + * @param input - The input for the poll. + * @param input.networkClientId - The ID of the network client triggering the fetch. * @returns A promise that resolves when this operation completes. */ - async _executePoll(networkClientId: string): Promise { + async _executePoll({ networkClientId }: GasFeePollingInput): Promise { await this._fetchGasFeeEstimateData({ networkClientId }); } diff --git a/packages/polling-controller/src/AbstractPollingController.ts b/packages/polling-controller/src/AbstractPollingController.ts index 87945d56cd5..565a3351798 100644 --- a/packages/polling-controller/src/AbstractPollingController.ts +++ b/packages/polling-controller/src/AbstractPollingController.ts @@ -1,5 +1,3 @@ -import type { NetworkClientId } from '@metamask/network-controller'; -import type { Json } from '@metamask/utils'; import stringify from 'fast-json-stable-stringify'; import { v4 as random } from 'uuid'; @@ -9,12 +7,8 @@ import type { IPollingController, } from './types'; -export const getKey = ( - networkClientId: NetworkClientId, - options: Json, - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions -): PollingTokenSetId => `${networkClientId}:${stringify(options)}`; +export const getKey = (input: PollingInput): PollingTokenSetId => + stringify(input); /** * AbstractPollingControllerBaseMixin @@ -24,45 +18,35 @@ export const getKey = ( */ // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/naming-convention -export function AbstractPollingControllerBaseMixin( - Base: TBase, -) { +export function AbstractPollingControllerBaseMixin< + TBase extends Constructor, + PollingInput, +>(Base: TBase) { abstract class AbstractPollingControllerBase extends Base - implements IPollingController + implements IPollingController { readonly #pollingTokenSets: Map> = new Map(); - #callbacks: Map< - PollingTokenSetId, - Set<(PollingTokenSetId: PollingTokenSetId) => void> - > = new Map(); + #callbacks: Map void>> = + new Map(); - abstract _executePoll( - networkClientId: NetworkClientId, - options: Json, - ): Promise; + abstract _executePoll(input: PollingInput): Promise; - abstract _startPollingByNetworkClientId( - networkClientId: NetworkClientId, - options: Json, - ): void; + abstract _startPolling(input: PollingInput): void; abstract _stopPollingByPollingTokenSetId(key: PollingTokenSetId): void; - startPollingByNetworkClientId( - networkClientId: NetworkClientId, - options: Json = {}, - ): string { + startPolling(input: PollingInput): string { const pollToken = random(); - const key = getKey(networkClientId, options); + const key = getKey(input); const pollingTokenSet = this.#pollingTokenSets.get(key) ?? new Set(); pollingTokenSet.add(pollToken); this.#pollingTokenSets.set(key, pollingTokenSet); if (pollingTokenSet.size === 1) { - this._startPollingByNetworkClientId(networkClientId, options); + this._startPolling(input); } return pollToken; @@ -98,19 +82,18 @@ export function AbstractPollingControllerBaseMixin( if (callbacks) { for (const callback of callbacks) { // eslint-disable-next-line n/callback-return - callback(keyToDelete); + callback(JSON.parse(keyToDelete)); } callbacks.clear(); } } } - onPollingCompleteByNetworkClientId( - networkClientId: NetworkClientId, - callback: (networkClientId: NetworkClientId) => void, - options: Json = {}, + onPollingComplete( + input: PollingInput, + callback: (input: PollingInput) => void, ) { - const key = getKey(networkClientId, options); + const key = getKey(input); const callbacks = this.#callbacks.get(key) ?? new Set(); callbacks.add(callback); this.#callbacks.set(key, callbacks); diff --git a/packages/polling-controller/src/BlockTrackerPollingController.test.ts b/packages/polling-controller/src/BlockTrackerPollingController.test.ts index 2ddba4edab3..90192e50493 100644 --- a/packages/polling-controller/src/BlockTrackerPollingController.test.ts +++ b/packages/polling-controller/src/BlockTrackerPollingController.test.ts @@ -3,6 +3,7 @@ import type { NetworkClient } from '@metamask/network-controller'; import EventEmitter from 'events'; import { useFakeTimers } from 'sinon'; +import type { BlockTrackerPollingInput } from './BlockTrackerPollingController'; import { BlockTrackerPollingController } from './BlockTrackerPollingController'; const createExecutePollMock = () => { @@ -13,7 +14,7 @@ const createExecutePollMock = () => { }; let getNetworkClientByIdStub: jest.Mock; -class ChildBlockTrackerPollingController extends BlockTrackerPollingController< +class ChildBlockTrackerPollingController extends BlockTrackerPollingController()< // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any any, @@ -45,9 +46,7 @@ describe('BlockTrackerPollingController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockMessenger: any; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let controller: any; + let controller: ChildBlockTrackerPollingController; let mainnetBlockTracker: TestBlockTracker; let goerliBlockTracker: TestBlockTracker; let sepoliaBlockTracker: TestBlockTracker; @@ -92,29 +91,30 @@ describe('BlockTrackerPollingController', () => { clock.restore(); }); - describe('startPollingByNetworkClientId', () => { - it('should call _executePoll on "latest" block events emitted by blockTrackers for each networkClientId passed to startPollingByNetworkClientId', async () => { - controller.startPollingByNetworkClientId('mainnet'); - controller.startPollingByNetworkClientId('goerli'); + describe('startPolling', () => { + it('should call _executePoll on "latest" block events emitted by blockTrackers for each networkClientId passed to startPolling', async () => { + controller.startPolling({ networkClientId: 'mainnet' }); + controller.startPolling({ networkClientId: 'goerli' }); // await advanceTime({ clock, duration: 5 }); mainnetBlockTracker.emitBlockEvent(); expect(controller._executePoll).toHaveBeenCalledTimes(1); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll).toHaveBeenCalledWith( + { networkClientId: 'mainnet' }, + 1, + ); mainnetBlockTracker.emitBlockEvent(); goerliBlockTracker.emitBlockEvent(); expect(controller._executePoll).toHaveBeenNthCalledWith( 2, - 'mainnet', - {}, + { networkClientId: 'mainnet' }, 2, // 2nd block for mainnet ); expect(controller._executePoll).toHaveBeenNthCalledWith( 3, - 'goerli', - {}, + { networkClientId: 'goerli' }, 1, // 1st block for goerli ); @@ -126,32 +126,28 @@ describe('BlockTrackerPollingController', () => { expect(controller._executePoll).toHaveBeenNthCalledWith( 4, - 'mainnet', - {}, + { networkClientId: 'mainnet' }, 3, ); expect(controller._executePoll).toHaveBeenNthCalledWith( 5, - 'goerli', - {}, + { networkClientId: 'goerli' }, 2, ); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); mainnetBlockTracker.emitBlockEvent(); sepoliaBlockTracker.emitBlockEvent(); expect(controller._executePoll).toHaveBeenNthCalledWith( 6, - 'mainnet', - {}, + { networkClientId: 'mainnet' }, 4, ); expect(controller._executePoll).toHaveBeenNthCalledWith( 7, - 'sepolia', - {}, + { networkClientId: 'sepolia' }, 2, ); @@ -161,21 +157,28 @@ describe('BlockTrackerPollingController', () => { describe('stopPollingByPollingToken', () => { it('should should stop polling when all polling tokens for a networkClientId are deleted', async () => { - const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken1 = controller.startPolling({ + networkClientId: 'mainnet', + }); // await advanceTime({ clock, duration: 5 }); mainnetBlockTracker.emitBlockEvent(); expect(controller._executePoll).toHaveBeenCalledTimes(1); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll).toHaveBeenCalledWith( + { networkClientId: 'mainnet' }, + 1, + ); - const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken2 = controller.startPolling({ + networkClientId: 'mainnet', + }); mainnetBlockTracker.emitBlockEvent(); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], ]); controller.stopPollingByPollingToken(pollingToken1); @@ -184,9 +187,9 @@ describe('BlockTrackerPollingController', () => { // polling is still active for mainnet because pollingToken2 is still active expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], ]); controller.stopPollingByPollingToken(pollingToken2); @@ -198,37 +201,44 @@ describe('BlockTrackerPollingController', () => { // no further polling should occur regardless of how many blocks are emitted // because all pollingTokens for mainnet have been deleted expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], ]); }); it('should should stop polling for one networkClientId when all polling tokens for that networkClientId are deleted, without stopping polling for networkClientIds with active pollingTokens', async () => { - const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken1 = controller.startPolling({ + networkClientId: 'mainnet', + }); mainnetBlockTracker.emitBlockEvent(); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll).toHaveBeenCalledWith( + { networkClientId: 'mainnet' }, + 1, + ); - const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken2 = controller.startPolling({ + networkClientId: 'mainnet', + }); mainnetBlockTracker.emitBlockEvent(); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], ]); - controller.startPollingByNetworkClientId('goerli'); + controller.startPolling({ networkClientId: 'goerli' }); mainnetBlockTracker.emitBlockEvent(); // we are polling for mainnet and goerli but goerli has not emitted any blocks yet expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], ]); controller.stopPollingByPollingToken(pollingToken1); @@ -237,11 +247,11 @@ describe('BlockTrackerPollingController', () => { goerliBlockTracker.emitBlockEvent(); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 1], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], + [{ networkClientId: 'mainnet' }, 4], + [{ networkClientId: 'goerli' }, 1], ]); controller.stopPollingByPollingToken(pollingToken2); @@ -254,13 +264,13 @@ describe('BlockTrackerPollingController', () => { // no further polling for mainnet should occur expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 1], - ['goerli', {}, 2], - ['goerli', {}, 3], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], + [{ networkClientId: 'mainnet' }, 4], + [{ networkClientId: 'goerli' }, 1], + [{ networkClientId: 'goerli' }, 2], + [{ networkClientId: 'goerli' }, 3], ]); controller.stopAllPolling(); @@ -272,11 +282,18 @@ describe('BlockTrackerPollingController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const pollingComplete: any = jest.fn(); - controller.onPollingCompleteByNetworkClientId('mainnet', pollingComplete); - const pollingToken = controller.startPollingByNetworkClientId('mainnet'); + controller.onPollingComplete( + { networkClientId: 'mainnet' }, + pollingComplete, + ); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); controller.stopPollingByPollingToken(pollingToken); expect(pollingComplete).toHaveBeenCalledTimes(1); - expect(pollingComplete).toHaveBeenCalledWith('mainnet:{}'); + expect(pollingComplete).toHaveBeenCalledWith({ + networkClientId: 'mainnet', + }); }); }); }); diff --git a/packages/polling-controller/src/BlockTrackerPollingController.ts b/packages/polling-controller/src/BlockTrackerPollingController.ts index 60f6e1fdccf..cb97c5511ef 100644 --- a/packages/polling-controller/src/BlockTrackerPollingController.ts +++ b/packages/polling-controller/src/BlockTrackerPollingController.ts @@ -11,6 +11,14 @@ import { } from './AbstractPollingController'; import type { Constructor, PollingTokenSetId } from './types'; +/** + * The minimum input required to start polling for a {@link BlockTrackerPollingController}. + * Implementing classes may provide additional properties. + */ +export type BlockTrackerPollingInput = { + networkClientId: NetworkClientId; +}; + /** * BlockTrackerPollingControllerMixin * A polling controller that polls using a block tracker. @@ -20,35 +28,30 @@ import type { Constructor, PollingTokenSetId } from './types'; */ // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/naming-convention -function BlockTrackerPollingControllerMixin( - Base: TBase, -) { - abstract class BlockTrackerPollingController extends AbstractPollingControllerBaseMixin( - Base, - ) { +function BlockTrackerPollingControllerMixin< + TBase extends Constructor, + PollingInput extends BlockTrackerPollingInput, +>(Base: TBase) { + abstract class BlockTrackerPollingController extends AbstractPollingControllerBaseMixin< + TBase, + PollingInput + >(Base) { #activeListeners: Record Promise> = {}; abstract _getNetworkClientById( networkClientId: NetworkClientId, ): NetworkClient | undefined; - _startPollingByNetworkClientId( - networkClientId: NetworkClientId, - options: Json, - ) { - const key = getKey(networkClientId, options); + _startPolling(input: PollingInput) { + const key = getKey(input); if (this.#activeListeners[key]) { return; } - const networkClient = this._getNetworkClientById(networkClientId); + const networkClient = this._getNetworkClientById(input.networkClientId); if (networkClient) { - const updateOnNewBlock = this._executePoll.bind( - this, - networkClientId, - options, - ); + const updateOnNewBlock = this._executePoll.bind(this, input); // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-misused-promises networkClient.blockTracker.addListener('latest', updateOnNewBlock); @@ -57,13 +60,13 @@ function BlockTrackerPollingControllerMixin( throw new Error( // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Unable to retrieve blockTracker for networkClientId ${networkClientId}`, + `Unable to retrieve blockTracker for networkClientId ${input.networkClientId}`, ); } } _stopPollingByPollingTokenSetId(key: PollingTokenSetId) { - const [networkClientId] = key.split(':'); + const { networkClientId } = JSON.parse(key); const networkClient = this._getNetworkClientById( networkClientId as NetworkClientId, ); @@ -85,9 +88,20 @@ function BlockTrackerPollingControllerMixin( class Empty {} -export const BlockTrackerPollingControllerOnly = - BlockTrackerPollingControllerMixin(Empty); -export const BlockTrackerPollingController = - BlockTrackerPollingControllerMixin(BaseController); -export const BlockTrackerPollingControllerV1 = - BlockTrackerPollingControllerMixin(BaseControllerV1); +export const BlockTrackerPollingControllerOnly = < + PollingInput extends BlockTrackerPollingInput, +>() => BlockTrackerPollingControllerMixin(Empty); + +export const BlockTrackerPollingController = < + PollingInput extends BlockTrackerPollingInput, +>() => + BlockTrackerPollingControllerMixin( + BaseController, + ); + +export const BlockTrackerPollingControllerV1 = < + PollingInput extends BlockTrackerPollingInput, +>() => + BlockTrackerPollingControllerMixin( + BaseControllerV1, + ); diff --git a/packages/polling-controller/src/StaticIntervalPollingController.test.ts b/packages/polling-controller/src/StaticIntervalPollingController.test.ts index 2238fbc1115..b166b90a795 100644 --- a/packages/polling-controller/src/StaticIntervalPollingController.test.ts +++ b/packages/polling-controller/src/StaticIntervalPollingController.test.ts @@ -7,7 +7,12 @@ import { StaticIntervalPollingController } from './StaticIntervalPollingControll const TICK_TIME = 5; -class ChildBlockTrackerPollingController extends StaticIntervalPollingController< +type PollingInput = { + networkClientId: string; + address?: string; +}; + +class ChildBlockTrackerPollingController extends StaticIntervalPollingController()< // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any any, @@ -37,9 +42,7 @@ describe('StaticIntervalPollingController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockMessenger: any; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let controller: any; + let controller: ChildBlockTrackerPollingController; beforeEach(() => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -58,9 +61,9 @@ describe('StaticIntervalPollingController', () => { clock.restore(); }); - describe('startPollingByNetworkClientId', () => { + describe('startPolling', () => { it('should start polling if not already polling', async () => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); controller.executePollPromises[0].resolve(); @@ -70,10 +73,10 @@ describe('StaticIntervalPollingController', () => { }); it('should call _executePoll immediately once and continue calling _executePoll on interval when called again with the same networkClientId', async () => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); @@ -89,15 +92,19 @@ describe('StaticIntervalPollingController', () => { describe('multiple networkClientIds', () => { it('should poll for each networkClientId', async () => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('rinkeby'); + controller.startPolling({ + networkClientId: 'rinkeby', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['rinkeby', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], ]); controller.executePollPromises[0].resolve(); @@ -105,10 +112,10 @@ describe('StaticIntervalPollingController', () => { await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['rinkeby', {}], - ['mainnet', {}], - ['rinkeby', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], ]); controller.executePollPromises[2].resolve(); @@ -116,75 +123,79 @@ describe('StaticIntervalPollingController', () => { await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['rinkeby', {}], - ['mainnet', {}], - ['rinkeby', {}], - ['mainnet', {}], - ['rinkeby', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], ]); controller.stopAllPolling(); }); it('should poll multiple networkClientIds when setting interval length', async () => { controller.setIntervalLength(TICK_TIME * 2); - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], + [{ networkClientId: 'mainnet' }], ]); controller.executePollPromises[0].resolve(); await advanceTime({ clock, duration: TICK_TIME }); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ + networkClientId: 'sepolia', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], ]); controller.executePollPromises[1].resolve(); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], ]); controller.executePollPromises[2].resolve(); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], - ['sepolia', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], ]); controller.executePollPromises[3].resolve(); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], ]); controller.executePollPromises[4].resolve(); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], - ['sepolia', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], ]); }); }); @@ -192,7 +203,9 @@ describe('StaticIntervalPollingController', () => { describe('stopPollingByPollingToken', () => { it('should stop polling when called with a valid polling that was the only active pollingToken for a given networkClient', async () => { - const pollingToken = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); controller.executePollPromises[0].resolve(); @@ -204,10 +217,12 @@ describe('StaticIntervalPollingController', () => { }); it('should not stop polling if called with one of multiple active polling tokens for a given networkClient', async () => { - const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken1 = controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); expect(controller._executePoll).toHaveBeenCalledTimes(1); controller.executePollPromises[0].resolve(); await advanceTime({ clock, duration: TICK_TIME }); @@ -219,28 +234,35 @@ describe('StaticIntervalPollingController', () => { }); it('should error if no pollingToken is passed', () => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); expect(() => { - controller.stopPollingByPollingToken(); + controller.stopPollingByPollingToken(''); }).toThrow('pollingToken required'); controller.stopAllPolling(); }); it('should start and stop polling sessions for different networkClientIds with the same options', async () => { - const pollToken1 = controller.startPollingByNetworkClientId('mainnet', { + const pollToken1 = controller.startPolling({ + networkClientId: 'mainnet', address: '0x1', }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('mainnet', { address: '0x2' }); + controller.startPolling({ + networkClientId: 'mainnet', + address: '0x2', + }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('sepolia', { address: '0x2' }); + controller.startPolling({ + networkClientId: 'sepolia', + address: '0x2', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], ]); controller.executePollPromises[0].resolve(); @@ -249,12 +271,12 @@ describe('StaticIntervalPollingController', () => { await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], ]); controller.stopPollingByPollingToken(pollToken1); controller.executePollPromises[3].resolve(); @@ -263,19 +285,21 @@ describe('StaticIntervalPollingController', () => { await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], ]); }); it('should stop polling session after current iteration if stop is requested while current iteration is still executing', async () => { - const pollingToken = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); controller.stopPollingByPollingToken(pollingToken); @@ -293,11 +317,18 @@ describe('StaticIntervalPollingController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const pollingComplete: any = jest.fn(); - controller.onPollingCompleteByNetworkClientId('mainnet', pollingComplete); - const pollingToken = controller.startPollingByNetworkClientId('mainnet'); + controller.onPollingComplete( + { networkClientId: 'mainnet' }, + pollingComplete, + ); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); controller.stopPollingByPollingToken(pollingToken); expect(pollingComplete).toHaveBeenCalledTimes(1); - expect(pollingComplete).toHaveBeenCalledWith('mainnet:{}'); + expect(pollingComplete).toHaveBeenCalledWith({ + networkClientId: 'mainnet', + }); }); }); }); diff --git a/packages/polling-controller/src/StaticIntervalPollingController.ts b/packages/polling-controller/src/StaticIntervalPollingController.ts index a4e4fd2e84e..d7eb6067dcc 100644 --- a/packages/polling-controller/src/StaticIntervalPollingController.ts +++ b/packages/polling-controller/src/StaticIntervalPollingController.ts @@ -1,6 +1,4 @@ import { BaseController, BaseControllerV1 } from '@metamask/base-controller'; -import type { NetworkClientId } from '@metamask/network-controller'; -import type { Json } from '@metamask/utils'; import { AbstractPollingControllerBaseMixin, @@ -21,12 +19,13 @@ import type { */ // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/naming-convention -function StaticIntervalPollingControllerMixin( - Base: TBase, -) { +function StaticIntervalPollingControllerMixin< + TBase extends Constructor, + PollingInput, +>(Base: TBase) { abstract class StaticIntervalPollingController - extends AbstractPollingControllerBaseMixin(Base) - implements IPollingController + extends AbstractPollingControllerBaseMixin(Base) + implements IPollingController { readonly #intervalIds: Record = {}; @@ -40,15 +39,12 @@ function StaticIntervalPollingControllerMixin( return this.#intervalLength; } - _startPollingByNetworkClientId( - networkClientId: NetworkClientId, - options: Json, - ) { + _startPolling(input: PollingInput) { if (!this.#intervalLength) { throw new Error('intervalLength must be defined and greater than 0'); } - const key = getKey(networkClientId, options); + const key = getKey(input); const existingInterval = this.#intervalIds[key]; this._stopPollingByPollingTokenSetId(key); @@ -58,12 +54,12 @@ function StaticIntervalPollingControllerMixin( // eslint-disable-next-line @typescript-eslint/no-misused-promises async () => { try { - await this._executePoll(networkClientId, options); + await this._executePoll(input); } catch (error) { console.error(error); } if (intervalId === this.#intervalIds[key]) { - this._startPollingByNetworkClientId(networkClientId, options); + this._startPolling(input); } }, existingInterval ? this.#intervalLength : 0, @@ -84,9 +80,15 @@ function StaticIntervalPollingControllerMixin( class Empty {} -export const StaticIntervalPollingControllerOnly = - StaticIntervalPollingControllerMixin(Empty); -export const StaticIntervalPollingController = - StaticIntervalPollingControllerMixin(BaseController); -export const StaticIntervalPollingControllerV1 = - StaticIntervalPollingControllerMixin(BaseControllerV1); +export const StaticIntervalPollingControllerOnly = () => + StaticIntervalPollingControllerMixin(Empty); + +export const StaticIntervalPollingController = () => + StaticIntervalPollingControllerMixin( + BaseController, + ); + +export const StaticIntervalPollingControllerV1 = () => + StaticIntervalPollingControllerMixin( + BaseControllerV1, + ); diff --git a/packages/polling-controller/src/types.ts b/packages/polling-controller/src/types.ts index c7848658ca2..a18122f6002 100644 --- a/packages/polling-controller/src/types.ts +++ b/packages/polling-controller/src/types.ts @@ -1,29 +1,19 @@ -import type { NetworkClientId } from '@metamask/network-controller'; -import type { Json } from '@metamask/utils'; +export type PollingTokenSetId = string; -export type PollingTokenSetId = `${NetworkClientId}:${string}`; - -export type IPollingController = { - startPollingByNetworkClientId( - networkClientId: NetworkClientId, - options: Json, - ): string; +export type IPollingController = { + startPolling(input: PollingInput): string; stopAllPolling(): void; stopPollingByPollingToken(pollingToken: string): void; - onPollingCompleteByNetworkClientId( - networkClientId: NetworkClientId, - callback: (networkClientId: NetworkClientId) => void, - options: Json, + onPollingComplete( + input: PollingInput, + callback: (input: PollingInput) => void, ): void; - _executePoll(networkClientId: NetworkClientId, options: Json): Promise; - _startPollingByNetworkClientId( - networkClientId: NetworkClientId, - options: Json, - ): void; + _executePoll(input: PollingInput): Promise; + _startPolling(input: PollingInput): void; _stopPollingByPollingTokenSetId(key: PollingTokenSetId): void; }; diff --git a/packages/user-operation-controller/src/UserOperationController.test.ts b/packages/user-operation-controller/src/UserOperationController.test.ts index e41af0e540f..0a0b921584a 100644 --- a/packages/user-operation-controller/src/UserOperationController.test.ts +++ b/packages/user-operation-controller/src/UserOperationController.test.ts @@ -1308,18 +1308,18 @@ describe('UserOperationController', () => { } }); - describe('startPollingByNetworkClientId', () => { + describe('startPolling', () => { it('starts polling in PendingUserOperationTracker', async () => { const controller = new UserOperationController(optionsMock); controller.startPollingByNetworkClientId(NETWORK_CLIENT_ID_MOCK); expect( - pendingUserOperationTrackerMock.startPollingByNetworkClientId, + pendingUserOperationTrackerMock.startPolling, ).toHaveBeenCalledTimes(1); - expect( - pendingUserOperationTrackerMock.startPollingByNetworkClientId, - ).toHaveBeenCalledWith(NETWORK_CLIENT_ID_MOCK); + expect(pendingUserOperationTrackerMock.startPolling).toHaveBeenCalledWith( + NETWORK_CLIENT_ID_MOCK, + ); }); }); diff --git a/packages/user-operation-controller/src/UserOperationController.ts b/packages/user-operation-controller/src/UserOperationController.ts index 3a5a187849d..492233d33c4 100644 --- a/packages/user-operation-controller/src/UserOperationController.ts +++ b/packages/user-operation-controller/src/UserOperationController.ts @@ -302,9 +302,9 @@ export class UserOperationController extends BaseController< } startPollingByNetworkClientId(networkClientId: string): string { - return this.#pendingUserOperationTracker.startPollingByNetworkClientId( + return this.#pendingUserOperationTracker.startPolling({ networkClientId, - ); + }); } async #addUserOperation( diff --git a/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.test.ts b/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.test.ts index 2285f2cd90e..3291e07fdb8 100644 --- a/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.test.ts +++ b/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.test.ts @@ -93,7 +93,9 @@ describe('PendingUserOperationTracker', () => { beforeCallback?.(pendingUserOperationTracker); - await pendingUserOperationTracker._executePoll(NETWORK_CLIENT_ID_MOCK, {}); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); } /** @@ -117,7 +119,9 @@ describe('PendingUserOperationTracker', () => { beforeCallback?.(pendingUserOperationTracker); - await pendingUserOperationTracker._executePoll(NETWORK_CLIENT_ID_MOCK, {}); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); } beforeEach(() => { @@ -147,10 +151,9 @@ describe('PendingUserOperationTracker', () => { messenger: messengerMock, }); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); expect(bundlerMock.getUserOperationReceipt).not.toHaveBeenCalled(); expect(queryMock).not.toHaveBeenCalled(); @@ -173,10 +176,9 @@ describe('PendingUserOperationTracker', () => { messenger: messengerMock, }); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); expect(bundlerMock.getUserOperationReceipt).not.toHaveBeenCalled(); expect(queryMock).not.toHaveBeenCalled(); @@ -197,10 +199,9 @@ describe('PendingUserOperationTracker', () => { new Error('Test Error'), ); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); }); // eslint-disable-next-line jest/expect-expect @@ -216,10 +217,9 @@ describe('PendingUserOperationTracker', () => { bundlerMock.getUserOperationReceipt.mockResolvedValueOnce(undefined); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); }); it('queries bundler using eth_getUserOperationReceipt RPC method', async () => { @@ -232,10 +232,9 @@ describe('PendingUserOperationTracker', () => { messenger: messengerMock, }); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); expect(bundlerMock.getUserOperationReceipt).toHaveBeenCalledTimes(1); expect(bundlerMock.getUserOperationReceipt).toHaveBeenCalledWith( diff --git a/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.ts b/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.ts index 26c58cc3423..2d5c1ca366d 100644 --- a/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.ts +++ b/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.ts @@ -1,8 +1,11 @@ import { query, toHex } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; -import type { NetworkClient, Provider } from '@metamask/network-controller'; +import type { + NetworkClient, + NetworkClientId, + Provider, +} from '@metamask/network-controller'; import { BlockTrackerPollingControllerOnly } from '@metamask/polling-controller'; -import type { Json } from '@metamask/utils'; import { createModuleLogger, type Hex } from '@metamask/utils'; import EventEmitter from 'events'; @@ -40,11 +43,16 @@ export type PendingUserOperationTrackerEventEmitter = EventEmitter & { emit(eventName: T, ...args: Events[T]): boolean; }; +/** The input to start polling for the {@link PendingUserOperationTracker} */ +type PendingUserOperationPollingInput = { + networkClientId: NetworkClientId; +}; + /** * A helper class to periodically query the bundlers * and update the status of any submitted user operations. */ -export class PendingUserOperationTracker extends BlockTrackerPollingControllerOnly { +export class PendingUserOperationTracker extends BlockTrackerPollingControllerOnly() { hub: PendingUserOperationTrackerEventEmitter; #getUserOperations: () => UserOperationMetadata[]; @@ -66,7 +74,7 @@ export class PendingUserOperationTracker extends BlockTrackerPollingControllerOn this.#messenger = messenger; } - async _executePoll(networkClientId: string, _options: Json) { + async _executePoll({ networkClientId }: PendingUserOperationPollingInput) { try { const { blockTracker, configuration, provider } = this._getNetworkClientById(networkClientId) as NetworkClient; From 0531fd4794b92d97f7399f39f5ed892ea4cf5812 Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Tue, 1 Oct 2024 17:45:27 -0700 Subject: [PATCH 2/3] fix unit test --- .../src/UserOperationController.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/user-operation-controller/src/UserOperationController.test.ts b/packages/user-operation-controller/src/UserOperationController.test.ts index 0a0b921584a..027fbb1fcd2 100644 --- a/packages/user-operation-controller/src/UserOperationController.test.ts +++ b/packages/user-operation-controller/src/UserOperationController.test.ts @@ -137,7 +137,7 @@ function createBundlerMock() { */ function createPendingUserOperationTrackerMock() { return { - startPollingByNetworkClientId: jest.fn(), + startPolling: jest.fn(), setIntervalLength: jest.fn(), hub: new EventEmitter(), } as unknown as jest.Mocked; @@ -1318,7 +1318,7 @@ describe('UserOperationController', () => { pendingUserOperationTrackerMock.startPolling, ).toHaveBeenCalledTimes(1); expect(pendingUserOperationTrackerMock.startPolling).toHaveBeenCalledWith( - NETWORK_CLIENT_ID_MOCK, + { networkClientId: NETWORK_CLIENT_ID_MOCK }, ); }); }); From b6302c5d0878eec788546b9bb667820fdb336b40 Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Thu, 3 Oct 2024 08:07:29 -0700 Subject: [PATCH 3/3] `extends Json` --- .../src/AbstractPollingController.ts | 3 ++- .../src/StaticIntervalPollingController.ts | 14 +++++++++----- packages/polling-controller/src/types.ts | 4 +++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/polling-controller/src/AbstractPollingController.ts b/packages/polling-controller/src/AbstractPollingController.ts index 565a3351798..d52aab938a0 100644 --- a/packages/polling-controller/src/AbstractPollingController.ts +++ b/packages/polling-controller/src/AbstractPollingController.ts @@ -1,3 +1,4 @@ +import type { Json } from '@metamask/utils'; import stringify from 'fast-json-stable-stringify'; import { v4 as random } from 'uuid'; @@ -20,7 +21,7 @@ export const getKey = (input: PollingInput): PollingTokenSetId => // eslint-disable-next-line @typescript-eslint/naming-convention export function AbstractPollingControllerBaseMixin< TBase extends Constructor, - PollingInput, + PollingInput extends Json, >(Base: TBase) { abstract class AbstractPollingControllerBase extends Base diff --git a/packages/polling-controller/src/StaticIntervalPollingController.ts b/packages/polling-controller/src/StaticIntervalPollingController.ts index d7eb6067dcc..53493601fa9 100644 --- a/packages/polling-controller/src/StaticIntervalPollingController.ts +++ b/packages/polling-controller/src/StaticIntervalPollingController.ts @@ -1,4 +1,5 @@ import { BaseController, BaseControllerV1 } from '@metamask/base-controller'; +import type { Json } from '@metamask/utils'; import { AbstractPollingControllerBaseMixin, @@ -21,7 +22,7 @@ import type { // eslint-disable-next-line @typescript-eslint/naming-convention function StaticIntervalPollingControllerMixin< TBase extends Constructor, - PollingInput, + PollingInput extends Json, >(Base: TBase) { abstract class StaticIntervalPollingController extends AbstractPollingControllerBaseMixin(Base) @@ -80,15 +81,18 @@ function StaticIntervalPollingControllerMixin< class Empty {} -export const StaticIntervalPollingControllerOnly = () => - StaticIntervalPollingControllerMixin(Empty); +export const StaticIntervalPollingControllerOnly = < + PollingInput extends Json, +>() => StaticIntervalPollingControllerMixin(Empty); -export const StaticIntervalPollingController = () => +export const StaticIntervalPollingController = () => StaticIntervalPollingControllerMixin( BaseController, ); -export const StaticIntervalPollingControllerV1 = () => +export const StaticIntervalPollingControllerV1 = < + PollingInput extends Json, +>() => StaticIntervalPollingControllerMixin( BaseControllerV1, ); diff --git a/packages/polling-controller/src/types.ts b/packages/polling-controller/src/types.ts index a18122f6002..2a1f88476da 100644 --- a/packages/polling-controller/src/types.ts +++ b/packages/polling-controller/src/types.ts @@ -1,6 +1,8 @@ +import type { Json } from '@metamask/utils'; + export type PollingTokenSetId = string; -export type IPollingController = { +export type IPollingController = { startPolling(input: PollingInput): string; stopAllPolling(): void;