From 0ce8040cce95af4304ae5c59efdbc9ac580acb43 Mon Sep 17 00:00:00 2001 From: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:32:22 +0200 Subject: [PATCH 1/4] chore: remove `eth-phishing-detect` (#4681) --- packages/phishing-controller/package.json | 1 - types/eth-phishing-detect/src/config.json.d.ts | 1 - types/eth-phishing-detect/src/detector.d.ts | 1 - yarn.lock | 10 ---------- 4 files changed, 13 deletions(-) delete mode 100644 types/eth-phishing-detect/src/config.json.d.ts delete mode 100644 types/eth-phishing-detect/src/detector.d.ts 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/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" From c29b8a09ecbbae90f8b0190c335ddde151353ee9 Mon Sep 17 00:00:00 2001 From: Matteo Scurati Date: Thu, 10 Oct 2024 16:00:07 +0200 Subject: [PATCH 2/4] Feat/new mock web3 notifications (#4780) ## Explanation This PR introduces new functions to generate mock notifications. The goal is to ensure the correct implementation of new notifications on the relevant clients by providing developers with the ability to mock the information they will soon receive from the notification service. ## References N/A ## Changelog ### `@metamask/notification-services-controller` - **ADDED**: New functions added to mock the notifications received by clients (extension and mobile) ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../__fixtures__/mock-raw-notifications.ts | 205 ++++++++- .../constants/notification-schema.ts | 6 + .../on-chain-notification.ts | 13 + .../types/on-chain-notification/schema.ts | 407 +++++++++++++++++- 4 files changed, 609 insertions(+), 22 deletions(-) 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; From b1f5475b6105914ff73c418649b4d75702dd2d04 Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Thu, 10 Oct 2024 08:24:05 -0700 Subject: [PATCH 3/4] feat: make polling input generic (#4752) ## Explanation Allow the polling controllers to accept generic input when starting a poll. Today they accept: ``` networkClientId: NetworkClientId, options: Json ``` But controllers may want unique polling loops over other data. This PR allows controllers to use any serializable type as their polling input. Controllers pass their input type as a generic argument to the base class, making it more type safe than then previous `options: Json`. An example of how this would be used in the future, is how `CurrencyRateController` only needs a polling loop for each unique native currency. So it would define: ``` type CurrencyRatePollingInput = { nativeCurrency: string; }; ``` And you would initiate polling with: ``` const currencyRateController = new(...); const token1 = currencyRateController.startPolling({nativeCurrency: 'ETH'}); const token2 = currencyRateController.startPolling({nativeCurrency: 'POL'}); ``` ## References ## Changelog ### `@metamask/polling-controller` - **BREAKING**: The input to start polling is now a generic type that can be any input, instead of requiring a network client id like before. The functions interfaces are updated to accommodate this. `startPollingByNetworkClientId` is now `startPolling`. And `onPollingComplete` now returns the entire input object, instead of a network client id. ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../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 | 54 ++---- .../src/BlockTrackerPollingController.test.ts | 131 +++++++------ .../src/BlockTrackerPollingController.ts | 64 ++++--- .../StaticIntervalPollingController.test.ts | 179 ++++++++++-------- .../src/StaticIntervalPollingController.ts | 44 +++-- packages/polling-controller/src/types.ts | 24 +-- .../src/UserOperationController.test.ts | 12 +- .../src/UserOperationController.ts | 4 +- .../PendingUserOperationTracker.test.ts | 43 ++--- .../helpers/PendingUserOperationTracker.ts | 16 +- 22 files changed, 426 insertions(+), 310 deletions(-) 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/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; From a63a4ea46c2872ba5a1e212d093442f6eef43fb2 Mon Sep 17 00:00:00 2001 From: Matteo Scurati Date: Thu, 10 Oct 2024 19:10:43 +0200 Subject: [PATCH 4/4] Release 218.0.0 (#4783) ## Explanation This is a RC for v218.0.0. See changelog for more details - @metamask/notification-services-controller:0.9.0 ## References https://consensyssoftware.atlassian.net/browse/NOTIFY-1202 ## Changelog ``` ## [0.9.0] ### Added - Add new functions for creating new mock notifications ([#4780](https://github.com/MetaMask/core/pull/4780)) ``` ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- package.json | 2 +- .../notification-services-controller/CHANGELOG.md | 14 +++++++++++++- .../notification-services-controller/package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) 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/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",