diff --git a/package.json b/package.json index de89d058bc..796627fb99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "217.0.0", + "version": "218.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/assets-controllers/src/AccountTrackerController.test.ts b/packages/assets-controllers/src/AccountTrackerController.test.ts index 707b7a906e..4c475f0b75 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 fb1f131cc7..e58bac61fc 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 c719301cbe..4731e026bf 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 319e819818..badc192532 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 dd0fd476c7..72af3018be 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 8e483d8f37..2459baea38 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 5cd327112e..317fc16657 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 d4290e6a7d..e4504ec0e9 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 dbfcffc0f3..ea51853d46 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 aeddfbfcb0..6632e3635d 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 @@ -594,10 +599,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 b8b0917714..4b4bf928b3 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 9e7b30515e..13587418a3 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/notification-services-controller/CHANGELOG.md b/packages/notification-services-controller/CHANGELOG.md index 46cbf0c6c5..1ddb18c066 100644 --- a/packages/notification-services-controller/CHANGELOG.md +++ b/packages/notification-services-controller/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.0] + +### Added + +- Add new functions to create mock notifications ([#4780](https://github.com/MetaMask/core/pull/4780)) + - `createMockNotificationAaveV3HealthFactor`: this function generates a mock notification related to the health factor of an Aave V3 position + - `createMockNotificationEnsExpiration`: this function creates a mock notification for the expiration of an ENS (Ethereum Name Service) domain + - `createMockNotificationLidoStakingRewards`: this function produces a mock notification for Lido staking rewards + - `createMockNotificationNotionalLoanExpiration`: this function generates a mock notification for the expiration of a Notional loan + - `createMockNotificationSparkFiHealthFactor`: This function produces a mock notification related to the health factor of a SparkFi position + ## [0.8.2] ### Added @@ -179,7 +190,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.8.2...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.9.0...HEAD +[0.9.0]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.8.2...@metamask/notification-services-controller@0.9.0 [0.8.2]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.8.1...@metamask/notification-services-controller@0.8.2 [0.8.1]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.8.0...@metamask/notification-services-controller@0.8.1 [0.8.0]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.7.0...@metamask/notification-services-controller@0.8.0 diff --git a/packages/notification-services-controller/package.json b/packages/notification-services-controller/package.json index a22fe6693c..18c491858d 100644 --- a/packages/notification-services-controller/package.json +++ b/packages/notification-services-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/notification-services-controller", - "version": "0.8.2", + "version": "0.9.0", "description": "Manages New MetaMask decentralized Notification system", "keywords": [ "MetaMask", diff --git a/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mock-raw-notifications.ts b/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mock-raw-notifications.ts index 9d0163b5b5..57591334f7 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mock-raw-notifications.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mock-raw-notifications.ts @@ -9,8 +9,8 @@ import type { OnChainRawNotification } from '../types/on-chain-notification/on-c export function createMockNotificationEthSent(): OnChainRawNotification { const mockNotification: OnChainRawNotification = { type: TRIGGER_TYPES.ETH_SENT, - id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', + id: '3fa85f64-5717-4562-b3fc-2c963f66afa7', + trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa7', chain_id: 1, block_number: 17485840, block_timestamp: '2022-03-01T00:00:00Z', @@ -44,8 +44,8 @@ export function createMockNotificationEthSent(): OnChainRawNotification { export function createMockNotificationEthReceived(): OnChainRawNotification { const mockNotification: OnChainRawNotification = { type: TRIGGER_TYPES.ETH_RECEIVED, - id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', + id: '3fa85f64-5717-4562-b3fc-2c963f66afa8', + trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa8', chain_id: 1, block_number: 17485840, block_timestamp: '2022-03-01T00:00:00Z', @@ -79,8 +79,8 @@ export function createMockNotificationEthReceived(): OnChainRawNotification { export function createMockNotificationERC20Sent(): OnChainRawNotification { const mockNotification: OnChainRawNotification = { type: TRIGGER_TYPES.ERC20_SENT, - id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', + id: '3fa85f64-5717-4562-b3fc-2c963f66afa9', + trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa9', chain_id: 1, block_number: 17485840, block_timestamp: '2022-03-01T00:00:00Z', @@ -468,7 +468,7 @@ export function createMockNotificationRocketPoolUnStakeCompleted(): OnChainRawNo native_token_price_in_usd: '1553.75', }, }, - id: 'd8c246e7-a0a4-5f1d-b079-2b1707665fbc', + id: '291ec897-f569-4837-b6c0-21001b198dff', trigger_id: '291ec897-f569-4837-b6c0-21001b198dff', tx_hash: '0xc7972a7e409abfc62590ec90e633acd70b9b74e76ad02305be8bf133a0e22d5f', @@ -517,7 +517,7 @@ export function createMockNotificationLidoStakeCompleted(): OnChainRawNotificati native_token_price_in_usd: '1806.33', }, }, - id: '9d9b1467-b3ee-5492-8ca2-22382657b690', + id: 'ec10d66a-f78f-461f-83c9-609aada8cc50', trigger_id: 'ec10d66a-f78f-461f-83c9-609aada8cc50', tx_hash: '0x8cc0fa805f7c3b1743b14f3b91c6b824113b094f26d4ccaf6a71ad8547ce6a0f', @@ -566,8 +566,8 @@ export function createMockNotificationLidoWithdrawalRequested(): OnChainRawNotif native_token_price_in_usd: '1576.73', }, }, - id: '29ddc718-78c6-5f91-936f-2bef13a605f0', - trigger_id: 'ef003925-3379-4ba7-9e2d-8218690cadc8', + id: 'ef003925-3379-4ba7-9e2d-8218690cadc9', + trigger_id: 'ef003925-3379-4ba7-9e2d-8218690cadc9', tx_hash: '0x58b5f82e084cb750ea174e02b20fbdfd2ba8d78053deac787f34fc38e5d427aa', unread: true, @@ -615,8 +615,8 @@ export function createMockNotificationLidoWithdrawalCompleted(): OnChainRawNotif native_token_price_in_usd: '1571.74', }, }, - id: 'f4ef0b7f-5612-537f-9144-0b5c63ae5391', - trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042c', + id: 'd73df14d-ce73-4f38-bad3-ab028154042f', + trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042f', tx_hash: '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', unread: true, @@ -651,6 +651,63 @@ export function createMockNotificationLidoReadyToBeWithdrawn(): OnChainRawNotifi usd: '10000.00', }, }, + id: 'd73df14d-ce73-4f38-bad3-ab028154042e', + trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042e', + tx_hash: + '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', + unread: true, + }; + + return mockNotification; +} + +/** + * Mocking Utility - create a mock Aave V3 Health Factor notification + * @returns Mock raw Aave V3 Health Factor notification + */ +export function createMockNotificationAaveV3HealthFactor(): OnChainRawNotification { + const mockNotification: OnChainRawNotification = { + type: TRIGGER_TYPES.AAVE_V3_HEALTH_FACTOR, + block_number: 18378208, + block_timestamp: '1697643851', + chain_id: 1, + created_at: '2023-10-18T16:35:03.147606Z', + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'aave_v3_health_factor', + chainId: 1, + healthFactor: '3.4', + threshold: '5.5', + }, + id: 'd73df14d-ce73-4f38-bad3-ab028154042b', + trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042b', + tx_hash: + '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', + unread: true, + }; + + return mockNotification; +} + +/** + * Mocking Utility - create a mock ENS Expiration notification + * @returns Mock raw ENS Expiration notification + */ +export function createMockNotificationEnsExpiration(): OnChainRawNotification { + const mockNotification: OnChainRawNotification = { + type: TRIGGER_TYPES.ENS_EXPIRATION, + block_number: 18378208, + block_timestamp: '1697643851', + chain_id: 1, + created_at: '2023-10-18T16:35:03.147606Z', + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'ens_expiration', + chainId: 1, + reverseEnsName: 'example.eth', + expirationDateIso: '2024-01-01T00:00:00Z', + reminderDelayInSeconds: 86400, + }, id: 'f4ef0b7f-5612-537f-9144-0b5c63ae5391', trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042c', tx_hash: @@ -661,6 +718,130 @@ export function createMockNotificationLidoReadyToBeWithdrawn(): OnChainRawNotifi return mockNotification; } +/** + * Mocking Utility - create a mock Lido Staking Rewards notification + * @returns Mock raw Lido Staking Rewards notification + */ +export function createMockNotificationLidoStakingRewards(): OnChainRawNotification { + const mockNotification: OnChainRawNotification = { + type: TRIGGER_TYPES.LIDO_STAKING_REWARDS, + block_number: 18378208, + block_timestamp: '1697643851', + chain_id: 1, + created_at: '2023-10-18T16:35:03.147606Z', + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'lido_staking_rewards', + chainId: 1, + currentStethBalance: '100', + currentEthValue: '10000.00', + estimatedTotalRewardInPeriod: '10000.00', + daysSinceLastNotification: 1, + notificationIntervalDays: 1, + }, + id: 'd73df14d-ce73-4f38-bad3-ab028154042l', + trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042l', + tx_hash: + '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', + unread: true, + }; + + return mockNotification; +} + +/** + * Mocking Utility - create a mock Notional Loan Expiration notification + * @returns Mock raw Notional Loan Expiration notification + */ +export function createMockNotificationNotionalLoanExpiration(): OnChainRawNotification { + const mockNotification: OnChainRawNotification = { + type: TRIGGER_TYPES.NOTIONAL_LOAN_EXPIRATION, + block_number: 18378208, + block_timestamp: '1697643851', + chain_id: 1, + created_at: '2023-10-18T16:35:03.147606Z', + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'notional_loan_expiration', + chainId: 1, + loans: [ + { + amount: '100', + symbol: 'ETH', + maturityDateIso: '2024-01-01T00:00:00Z', + }, + ], + reminderDelayInSeconds: 86400, + }, + id: 'd73df14d-ce73-4f38-bad3-ab028154042n', + trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042n', + tx_hash: + '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', + unread: true, + }; + + return mockNotification; +} + +/** + * Mocking Utility - create a mock Rocketpool Staking Rewards notification + * @returns Mock raw Rocketpool Staking Rewards notification + */ +export function createMockNotificationRocketpoolStakingRewards(): OnChainRawNotification { + const mockNotification: OnChainRawNotification = { + type: TRIGGER_TYPES.ROCKETPOOL_STAKING_REWARDS, + block_number: 18378208, + block_timestamp: '1697643851', + chain_id: 1, + created_at: '2023-10-18T16:35:03.147606Z', + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'rocketpool_staking_rewards', + chainId: 1, + currentRethBalance: '100', + currentEthValue: '10000.00', + estimatedTotalRewardInPeriod: '10000.00', + daysSinceLastNotification: 1, + notificationIntervalDays: 1, + }, + id: 'd73df14d-ce73-4f38-bad3-ab028154042r', + trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042r', + tx_hash: + '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', + unread: true, + }; + + return mockNotification; +} + +/** + * Mocking Utility - create a mock SparkFi Health Factor notification + * @returns Mock raw SparkFi Health Factor notification + */ +export function createMockNotificationSparkFiHealthFactor(): OnChainRawNotification { + const mockNotification: OnChainRawNotification = { + type: TRIGGER_TYPES.SPARK_FI_HEALTH_FACTOR, + block_number: 18378208, + block_timestamp: '1697643851', + chain_id: 1, + created_at: '2023-10-18T16:35:03.147606Z', + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'spark_fi_health_factor', + chainId: 1, + healthFactor: '3.4', + threshold: '5.5', + }, + id: 'd73df14d-ce73-4f38-bad3-ab028154042s', + trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042s', + tx_hash: + '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', + unread: true, + }; + + return mockNotification; +} + /** * Mocking Utility - creates an array of raw on-chain notifications * @returns Array of raw on-chain notifications diff --git a/packages/notification-services-controller/src/NotificationServicesController/constants/notification-schema.ts b/packages/notification-services-controller/src/NotificationServicesController/constants/notification-schema.ts index 0e7d3a064e..f5df92a052 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/constants/notification-schema.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/constants/notification-schema.ts @@ -18,6 +18,12 @@ export enum TRIGGER_TYPES { ERC721_RECEIVED = 'erc721_received', ERC1155_SENT = 'erc1155_sent', ERC1155_RECEIVED = 'erc1155_received', + AAVE_V3_HEALTH_FACTOR = 'aave_v3_health_factor', + ENS_EXPIRATION = 'ens_expiration', + LIDO_STAKING_REWARDS = 'lido_staking_rewards', + ROCKETPOOL_STAKING_REWARDS = 'rocketpool_staking_rewards', + NOTIONAL_LOAN_EXPIRATION = 'notional_loan_expiration', + SPARK_FI_HEALTH_FACTOR = 'spark_fi_health_factor', } export const TRIGGER_TYPES_WALLET_SET: Set = new Set([ diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/on-chain-notification.ts b/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/on-chain-notification.ts index c37c8fca78..82ec90ff45 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/on-chain-notification.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/on-chain-notification.ts @@ -24,6 +24,19 @@ export type Data_ERC20Received = components['schemas']['Data_ERC20Received']; export type Data_ERC721Sent = components['schemas']['Data_ERC721Sent']; export type Data_ERC721Received = components['schemas']['Data_ERC721Received']; +// Web3Notifications +export type Data_AaveV3HealthFactor = + components['schemas']['Data_AaveV3HealthFactor']; +export type Data_EnsExpiration = components['schemas']['Data_EnsExpiration']; +export type Data_LidoStakingRewards = + components['schemas']['Data_LidoStakingRewards']; +export type Data_RocketpoolStakingRewards = + components['schemas']['Data_RocketpoolStakingRewards']; +export type Data_NotionalLoanExpiration = + components['schemas']['Data_NotionalLoanExpiration']; +export type Data_SparkFiHealthFactor = + components['schemas']['Data_SparkFiHealthFactor']; + type Notification = components['schemas']['Notification']; type NotificationDataKinds = NonNullable['kind']; type ConvertToEnum = { diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/schema.ts b/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/schema.ts index 71dea69d34..59a2359ec4 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/schema.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/schema.ts @@ -5,8 +5,21 @@ * Script: `npx openapi-typescript -o ./schema.d.ts` */ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + export type paths = { '/api/v1/notifications': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; /** List all notifications ordered by most recent */ post: { parameters: { @@ -16,6 +29,9 @@ export type paths = { /** @description Number of notifications per page for pagination */ per_page?: number; }; + header?: never; + path?: never; + cookie?: never; }; requestBody?: { content: { @@ -30,16 +46,38 @@ export type paths = { responses: { /** @description Successfully fetched a list of notifications */ 200: { + headers: { + [name: string]: unknown; + }; content: { 'application/json': components['schemas']['Notification'][]; }; }; }; }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/v1/notifications/mark-as-read': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; /** Mark notifications as read */ post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': { @@ -50,17 +88,290 @@ export type paths = { responses: { /** @description Successfully marked notifications as read */ 200: { - content: never; + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/internal/v1/topics': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all topics created (internal) */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully fetched all topics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Topic'][]; + }; + }; + }; + }; + put?: never; + /** Create a new topic (internal) */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name: string; + desc?: string; + }; + }; + }; + responses: { + /** @description Successfully created a new topic */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/internal/v1/subtopics': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all sub-topics created (internal) */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully fetched all subtopics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SubTopic'][]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/internal/v1/global-notifications': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Insert a new Global Notification (internal) */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['GlobalNotificationWrite']; + }; + }; + responses: { + /** @description Successfully created a new global notification */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/global-notifications': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all Global Notifications for a UserID */ + get: { + parameters: { + query: { + /** @description Platform(s) to filter notifications by */ + platform: ('portfolio' | 'extension' | 'mobile')[]; + /** @description Delivery channel(s) to filter notifications by */ + deliveryChannel: ('inbox' | 'push')[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully fetched global notifications */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GlobalNotification'][]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/user-preferences': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all preferences for a UserID */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully fetched preferences */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Topic'][]; + }; + }; + /** @description User not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + /** Update Preferences for a UserID */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + topics: string[]; + }; + }; + }; + responses: { + /** @description Successfully updated topics preferences */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; }; }; }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; }; - export type webhooks = Record; - export type components = { schemas: { + GlobalNotification: { + title: string; + body: string; + /** Format: date-time */ + created_at: string; + }; + GlobalNotificationWrite: { + title: string; + body: string; + 'sub-topic': string; + platforms: ('portfolio' | 'extension' | 'mobile')[]; + delivery_channels: ('inbox' | 'push')[]; + }; + Topic: { + name: string; + description?: string; + /** Format: date-time */ + created_at?: string; + }; + SubTopic: { + name: string; + /** Format: date-time */ + created_at?: string; + }; Notification: { /** Format: uuid */ id: string; @@ -69,14 +380,13 @@ export type components = { /** @example 1 */ chain_id: number; /** @example 17485840 */ - block_number: number; - block_timestamp: string; + block_number?: number; + block_timestamp?: string; /** * Format: address - * * @example 0x881D40237659C251811CEC9c364ef91dC08D300C */ - tx_hash: string; + tx_hash?: string; /** @example false */ unread: boolean; /** Format: date-time */ @@ -98,7 +408,13 @@ export type components = { | components['schemas']['Data_ERC721Sent'] | components['schemas']['Data_ERC721Received'] | components['schemas']['Data_ERC1155Sent'] - | components['schemas']['Data_ERC1155Received']; + | components['schemas']['Data_ERC1155Received'] + | components['schemas']['Data_AaveV3HealthFactor'] + | components['schemas']['Data_EnsExpiration'] + | components['schemas']['Data_LidoStakingRewards'] + | components['schemas']['Data_RocketpoolStakingRewards'] + | components['schemas']['Data_NotionalLoanExpiration'] + | components['schemas']['Data_SparkFiHealthFactor']; }; Data_MetamaskSwapCompleted: { /** @enum {string} */ @@ -241,6 +557,79 @@ export type components = { to: string; nft?: components['schemas']['NFT']; }; + Data_AaveV3HealthFactor: { + /** @enum {string} */ + kind: 'aave_v3_health_factor'; + /** @example 1 */ + chainId: number; + /** Format: decimal */ + healthFactor: string; + /** Format: decimal */ + threshold: string; + }; + Data_EnsExpiration: { + /** @enum {string} */ + kind: 'ens_expiration'; + chainId: number; + reverseEnsName: string; + /** Format: date-time */ + expirationDateIso: string; + /** @example 86400 */ + reminderDelayInSeconds: number; + }; + Data_LidoStakingRewards: { + /** @enum {string} */ + kind: 'lido_staking_rewards'; + chainId: number; + /** Format: decimal */ + currentStethBalance: string; + /** Format: decimal */ + currentEthValue: string; + /** Format: decimal */ + estimatedTotalRewardInPeriod: string; + /** @example 1 */ + daysSinceLastNotification: number; + /** @example 1 */ + notificationIntervalDays: number; + }; + Data_NotionalLoanExpiration: { + /** @enum {string} */ + kind: 'notional_loan_expiration'; + chainId: number; + loans: { + /** Format: decimal */ + amount: string; + symbol: string; + /** Format: date-time */ + maturityDateIso: string; + }[]; + /** @example 86400 */ + reminderDelayInSeconds: number; + }; + Data_RocketpoolStakingRewards: { + /** @enum {string} */ + kind: 'rocketpool_staking_rewards'; + chainId: number; + /** Format: decimal */ + currentRethBalance: string; + /** Format: decimal */ + currentEthValue: string; + /** Format: decimal */ + estimatedTotalRewardInPeriod: string; + /** @example 1 */ + daysSinceLastNotification: number; + /** @example 1 */ + notificationIntervalDays: number; + }; + Data_SparkFiHealthFactor: { + /** @enum {string} */ + kind: 'spark_fi_health_factor'; + chainId: number; + /** Format: decimal */ + healthFactor: string; + /** Format: decimal */ + threshold: string; + }; NetworkFee: { /** Format: decimal */ gas_price: string; @@ -299,6 +688,4 @@ export type components = { export type $defs = Record; -export type external = Record; - export type operations = Record; diff --git a/packages/phishing-controller/package.json b/packages/phishing-controller/package.json index 04af46d7db..ee46f85113 100644 --- a/packages/phishing-controller/package.json +++ b/packages/phishing-controller/package.json @@ -51,7 +51,6 @@ "@metamask/controller-utils": "^11.3.0", "@noble/hashes": "^1.4.0", "@types/punycode": "^2.1.0", - "eth-phishing-detect": "^1.2.0", "ethereum-cryptography": "^2.1.2", "fastest-levenshtein": "^1.0.16", "punycode": "^2.1.1" diff --git a/packages/polling-controller/src/AbstractPollingController.ts b/packages/polling-controller/src/AbstractPollingController.ts index 87945d56cd..d52aab938a 100644 --- a/packages/polling-controller/src/AbstractPollingController.ts +++ b/packages/polling-controller/src/AbstractPollingController.ts @@ -1,4 +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 +8,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 +19,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 extends Json, +>(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 +83,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 2ddba4edab..90192e5049 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 60f6e1fdcc..cb97c5511e 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 2238fbc111..b166b90a79 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 a4e4fd2e84..53493601fa 100644 --- a/packages/polling-controller/src/StaticIntervalPollingController.ts +++ b/packages/polling-controller/src/StaticIntervalPollingController.ts @@ -1,5 +1,4 @@ import { BaseController, BaseControllerV1 } from '@metamask/base-controller'; -import type { NetworkClientId } from '@metamask/network-controller'; import type { Json } from '@metamask/utils'; import { @@ -21,12 +20,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 extends Json, +>(Base: TBase) { abstract class StaticIntervalPollingController - extends AbstractPollingControllerBaseMixin(Base) - implements IPollingController + extends AbstractPollingControllerBaseMixin(Base) + implements IPollingController { readonly #intervalIds: Record = {}; @@ -40,15 +40,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 +55,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 +81,18 @@ function StaticIntervalPollingControllerMixin( class Empty {} -export const StaticIntervalPollingControllerOnly = - StaticIntervalPollingControllerMixin(Empty); -export const StaticIntervalPollingController = - StaticIntervalPollingControllerMixin(BaseController); -export const StaticIntervalPollingControllerV1 = - StaticIntervalPollingControllerMixin(BaseControllerV1); +export const StaticIntervalPollingControllerOnly = < + PollingInput extends Json, +>() => StaticIntervalPollingControllerMixin(Empty); + +export const StaticIntervalPollingController = () => + StaticIntervalPollingControllerMixin( + BaseController, + ); + +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 c7848658ca..2a1f88476d 100644 --- a/packages/polling-controller/src/types.ts +++ b/packages/polling-controller/src/types.ts @@ -1,29 +1,21 @@ -import type { NetworkClientId } from '@metamask/network-controller'; import type { Json } from '@metamask/utils'; -export type PollingTokenSetId = `${NetworkClientId}:${string}`; +export type PollingTokenSetId = 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 e41af0e540..027fbb1fcd 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; @@ -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( + { networkClientId: NETWORK_CLIENT_ID_MOCK }, + ); }); }); diff --git a/packages/user-operation-controller/src/UserOperationController.ts b/packages/user-operation-controller/src/UserOperationController.ts index 3a5a187849..492233d33c 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 2285f2cd90..3291e07fdb 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 26c58cc342..2d5c1ca366 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; diff --git a/types/eth-phishing-detect/src/config.json.d.ts b/types/eth-phishing-detect/src/config.json.d.ts deleted file mode 100644 index 6943346451..0000000000 --- a/types/eth-phishing-detect/src/config.json.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'eth-phishing-detect/src/config.json'; diff --git a/types/eth-phishing-detect/src/detector.d.ts b/types/eth-phishing-detect/src/detector.d.ts deleted file mode 100644 index cab272fdde..0000000000 --- a/types/eth-phishing-detect/src/detector.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'eth-phishing-detect/src/detector'; diff --git a/yarn.lock b/yarn.lock index 92e7d49e54..06a9e70928 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3219,7 +3219,6 @@ __metadata: "@types/jest": "npm:^27.4.1" "@types/punycode": "npm:^2.1.0" deepmerge: "npm:^4.2.2" - eth-phishing-detect: "npm:^1.2.0" ethereum-cryptography: "npm:^2.1.2" fastest-levenshtein: "npm:^1.0.16" jest: "npm:^27.5.1" @@ -7006,15 +7005,6 @@ __metadata: languageName: node linkType: hard -"eth-phishing-detect@npm:^1.2.0": - version: 1.2.0 - resolution: "eth-phishing-detect@npm:1.2.0" - dependencies: - fast-levenshtein: "npm:^2.0.6" - checksum: 10/e396c83a5678a227e76b8e2019d4307e060233c0c088d4b18cf9992e08233b58072ca1d9cdce0886f101c63395e3c134ca7ea6be02bc8522a41ac7e21c9ee05f - languageName: node - linkType: hard - "ethereum-cryptography@npm:^0.1.3": version: 0.1.3 resolution: "ethereum-cryptography@npm:0.1.3"