diff --git a/package.json b/package.json index 1d5f64a75ac..796627fb992 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "215.0.0", + "version": "218.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 39c5761c2a9..aaa520ad4f9 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [38.3.0] + +### Changed + +- The `includeDuplicateSymbolAssets` param is removed from our api call to TokenApi ([#4768](https://github.com/MetaMask/core/pull/4768)) + ## [38.2.0] ### Changed @@ -1136,7 +1142,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Use Ethers for AssetsContractController ([#845](https://github.com/MetaMask/core/pull/845)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@38.2.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@38.3.0...HEAD +[38.3.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@38.2.0...@metamask/assets-controllers@38.3.0 [38.2.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@38.1.0...@metamask/assets-controllers@38.2.0 [38.1.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@38.0.1...@metamask/assets-controllers@38.1.0 [38.0.1]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@38.0.0...@metamask/assets-controllers@38.0.1 diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 7d2498f7a67..e0463472891 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/assets-controllers", - "version": "38.2.0", + "version": "38.3.0", "description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)", "keywords": [ "MetaMask", diff --git a/packages/assets-controllers/src/AccountTrackerController.test.ts b/packages/assets-controllers/src/AccountTrackerController.test.ts index 707b7a906e6..4c475f0b755 100644 --- a/packages/assets-controllers/src/AccountTrackerController.test.ts +++ b/packages/assets-controllers/src/AccountTrackerController.test.ts @@ -505,7 +505,9 @@ describe('AccountTrackerController', () => { .spyOn(controller, 'refresh') .mockResolvedValue(); - controller.startPollingByNetworkClientId(networkClientId1); + controller.startPolling({ + networkClientId: networkClientId1, + }); await advanceTime({ clock, duration: 0 }); expect(refreshSpy).toHaveBeenNthCalledWith(1, networkClientId1); @@ -516,8 +518,9 @@ describe('AccountTrackerController', () => { expect(refreshSpy).toHaveBeenNthCalledWith(2, networkClientId1); expect(refreshSpy).toHaveBeenCalledTimes(2); - const pollToken = - controller.startPollingByNetworkClientId(networkClientId2); + const pollToken = controller.startPolling({ + networkClientId: networkClientId2, + }); await advanceTime({ clock, duration: 0 }); expect(refreshSpy).toHaveBeenNthCalledWith(3, networkClientId2); diff --git a/packages/assets-controllers/src/AccountTrackerController.ts b/packages/assets-controllers/src/AccountTrackerController.ts index fb1f131cc71..e58bac61fc3 100644 --- a/packages/assets-controllers/src/AccountTrackerController.ts +++ b/packages/assets-controllers/src/AccountTrackerController.ts @@ -120,10 +120,15 @@ export type AccountTrackerControllerMessenger = RestrictedControllerMessenger< AllowedEvents['type'] >; +/** The input to start polling for the {@link AccountTrackerController} */ +type AccountTrackerPollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that tracks the network balances for all user accounts. */ -export class AccountTrackerController extends StaticIntervalPollingController< +export class AccountTrackerController extends StaticIntervalPollingController()< typeof controllerName, AccountTrackerControllerState, AccountTrackerControllerMessenger @@ -309,9 +314,12 @@ export class AccountTrackerController extends StaticIntervalPollingController< /** * Refreshes the balances of the accounts using the networkClientId * - * @param networkClientId - The network client ID used to get balances. + * @param input - The input for the poll. + * @param input.networkClientId - The network client ID used to get balances. */ - async _executePoll(networkClientId: string): Promise { + async _executePoll({ + networkClientId, + }: AccountTrackerPollingInput): Promise { // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.refresh(networkClientId); diff --git a/packages/assets-controllers/src/CurrencyRateController.test.ts b/packages/assets-controllers/src/CurrencyRateController.test.ts index c719301cbe4..4731e026bfc 100644 --- a/packages/assets-controllers/src/CurrencyRateController.test.ts +++ b/packages/assets-controllers/src/CurrencyRateController.test.ts @@ -155,7 +155,7 @@ describe('CurrencyRateController', () => { messenger, }); - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); await advanceTime({ clock, duration: 0 }); expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1); expect(controller.state.currencyRates).toStrictEqual({ @@ -192,7 +192,7 @@ describe('CurrencyRateController', () => { messenger, }); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); await advanceTime({ clock, duration: 0 }); @@ -217,7 +217,7 @@ describe('CurrencyRateController', () => { fetchExchangeRate: fetchExchangeRateStub, messenger, }); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); await advanceTime({ clock, duration: 0 }); controller.stopAllPolling(); @@ -225,7 +225,7 @@ describe('CurrencyRateController', () => { // called once upon initial start expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); await advanceTime({ clock, duration: 0 }); expect(fetchExchangeRateStub).toHaveBeenCalledTimes(2); diff --git a/packages/assets-controllers/src/CurrencyRateController.ts b/packages/assets-controllers/src/CurrencyRateController.ts index 319e819818f..badc1925323 100644 --- a/packages/assets-controllers/src/CurrencyRateController.ts +++ b/packages/assets-controllers/src/CurrencyRateController.ts @@ -78,11 +78,16 @@ const defaultState = { }, }; +/** The input to start polling for the {@link CurrencyRateController} */ +type CurrencyRatePollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that passively polls on a set interval for an exchange rate from the current network * asset to the user's preferred currency. */ -export class CurrencyRateController extends StaticIntervalPollingController< +export class CurrencyRateController extends StaticIntervalPollingController()< typeof name, CurrencyRateState, CurrencyRateMessenger @@ -237,10 +242,12 @@ export class CurrencyRateController extends StaticIntervalPollingController< /** * Updates exchange rate for the current currency. * - * @param networkClientId - The network client ID used to get a ticker value. - * @returns The controller state. + * @param input - The input for the poll. + * @param input.networkClientId - The network client ID used to get a ticker value. */ - async _executePoll(networkClientId: NetworkClientId): Promise { + async _executePoll({ + networkClientId, + }: CurrencyRatePollingInput): Promise { const networkClient = this.messagingSystem.call( 'NetworkController:getNetworkClientById', networkClientId, diff --git a/packages/assets-controllers/src/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts index dd0fd476c77..72af3018be3 100644 --- a/packages/assets-controllers/src/TokenDetectionController.test.ts +++ b/packages/assets-controllers/src/TokenDetectionController.test.ts @@ -1855,7 +1855,7 @@ describe('TokenDetectionController', () => { }); }); - describe('startPollingByNetworkClientId', () => { + describe('startPolling', () => { let clock: sinon.SinonFakeTimers; beforeEach(() => { clock = sinon.useFakeTimers(); @@ -1904,13 +1904,16 @@ describe('TokenDetectionController', () => { return Promise.resolve(); }); - controller.startPollingByNetworkClientId('mainnet', { + controller.startPolling({ + networkClientId: 'mainnet', address: '0x1', }); - controller.startPollingByNetworkClientId('sepolia', { + controller.startPolling({ + networkClientId: 'sepolia', address: '0xdeadbeef', }); - controller.startPollingByNetworkClientId('goerli', { + controller.startPolling({ + networkClientId: 'goerli', address: '0x3', }); await advanceTime({ clock, duration: 0 }); diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index 8e483d8f37b..2459baea38f 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -138,6 +138,12 @@ export type TokenDetectionControllerMessenger = RestrictedControllerMessenger< AllowedEvents['type'] >; +/** The input to start polling for the {@link TokenDetectionController} */ +type TokenDetectionPollingInput = { + networkClientId: NetworkClientId; + address: string; +}; + /** * Controller that passively polls on a set interval for Tokens auto detection * @property intervalId - Polling interval used to fetch new token rates @@ -148,7 +154,7 @@ export type TokenDetectionControllerMessenger = RestrictedControllerMessenger< * @property isDetectionEnabledFromPreferences - Boolean to track if detection is enabled from PreferencesController * @property isDetectionEnabledForNetwork - Boolean to track if detected is enabled for current network */ -export class TokenDetectionController extends StaticIntervalPollingController< +export class TokenDetectionController extends StaticIntervalPollingController()< typeof controllerName, TokenDetectionState, TokenDetectionControllerMessenger @@ -432,16 +438,16 @@ export class TokenDetectionController extends StaticIntervalPollingController< }; } - async _executePoll( - networkClientId: NetworkClientId, - options: { address: string }, - ): Promise { + async _executePoll({ + networkClientId, + address, + }: TokenDetectionPollingInput): Promise { if (!this.isActive) { return; } await this.detectTokens({ networkClientId, - selectedAddress: options.address, + selectedAddress: address, }); } diff --git a/packages/assets-controllers/src/TokenListController.test.ts b/packages/assets-controllers/src/TokenListController.test.ts index 5cd327112e4..317fc166570 100644 --- a/packages/assets-controllers/src/TokenListController.test.ts +++ b/packages/assets-controllers/src/TokenListController.test.ts @@ -1157,7 +1157,7 @@ describe('TokenListController', () => { }); }); - describe('startPollingByNetworkClient', () => { + describe('startPolling', () => { let clock: sinon.SinonFakeTimers; const pollingIntervalTime = 1000; beforeEach(() => { @@ -1200,7 +1200,7 @@ describe('TokenListController', () => { expiredCacheExistingState.tokenList, ); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); await advanceTime({ clock, duration: 0 }); expect(fetchTokenListByChainIdSpy.mock.calls[0]).toStrictEqual( @@ -1236,7 +1236,7 @@ describe('TokenListController', () => { expiredCacheExistingState.tokenList, ); - controller.startPollingByNetworkClientId('goerli'); + controller.startPolling({ networkClientId: 'goerli' }); await advanceTime({ clock, duration: 0 }); expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(1); @@ -1306,7 +1306,9 @@ describe('TokenListController', () => { expect(controller.state).toStrictEqual(startingState); // start polling for sepolia - const pollingToken = controller.startPollingByNetworkClientId('sepolia'); + const pollingToken = controller.startPolling({ + networkClientId: 'sepolia', + }); // wait a polling interval await advanceTime({ clock, duration: pollingIntervalTime }); @@ -1324,7 +1326,9 @@ describe('TokenListController', () => { controller.stopPollingByPollingToken(pollingToken); // start polling for binance - controller.startPollingByNetworkClientId('binance-network-client-id'); + controller.startPolling({ + networkClientId: 'binance-network-client-id', + }); await advanceTime({ clock, duration: pollingIntervalTime }); // expect fetchTokenListByChain to be called for binance, but not for sepolia diff --git a/packages/assets-controllers/src/TokenListController.ts b/packages/assets-controllers/src/TokenListController.ts index d4290e6a7d2..e4504ec0e94 100644 --- a/packages/assets-controllers/src/TokenListController.ts +++ b/packages/assets-controllers/src/TokenListController.ts @@ -92,10 +92,15 @@ export const getDefaultTokenListState = (): TokenListState => { }; }; +/** The input to start polling for the {@link TokenListController} */ +type TokenListPollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that passively polls on a set interval for the list of tokens from metaswaps api */ -export class TokenListController extends StaticIntervalPollingController< +export class TokenListController extends StaticIntervalPollingController()< typeof name, TokenListState, TokenListControllerMessenger @@ -211,7 +216,7 @@ export class TokenListController extends StaticIntervalPollingController< if (!isTokenListSupportedForNetwork(this.chainId)) { return; } - await this.startPolling(); + await this.#startPolling(); } /** @@ -219,7 +224,7 @@ export class TokenListController extends StaticIntervalPollingController< */ async restart() { this.stopPolling(); - await this.startPolling(); + await this.#startPolling(); } /** @@ -248,7 +253,7 @@ export class TokenListController extends StaticIntervalPollingController< /** * Starts a new polling interval. */ - private async startPolling(): Promise { + async #startPolling(): Promise { await safelyExecute(() => this.fetchTokenList()); // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-misused-promises @@ -261,10 +266,13 @@ export class TokenListController extends StaticIntervalPollingController< * Fetching token list from the Token Service API. * * @private - * @param networkClientId - The ID of the network client triggering the fetch. + * @param input - The input for the poll. + * @param input.networkClientId - The ID of the network client triggering the fetch. * @returns A promise that resolves when this operation completes. */ - async _executePoll(networkClientId: string): Promise { + async _executePoll({ + networkClientId, + }: TokenListPollingInput): Promise { return this.fetchTokenList(networkClientId); } diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index dbfcffc0f39..ea51853d46a 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 aeddfbfcb0e..6632e3635d1 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 b8b09177143..4b4bf928b3f 100644 --- a/packages/gas-fee-controller/src/GasFeeController.test.ts +++ b/packages/gas-fee-controller/src/GasFeeController.test.ts @@ -1199,7 +1199,9 @@ describe('GasFeeController', () => { interval: pollingInterval, }); - gasFeeController.startPollingByNetworkClientId('goerli'); + gasFeeController.startPolling({ + networkClientId: 'goerli', + }); await clock.tickAsync(0); expect(mockedDetermineGasFeeCalculations).toHaveBeenNthCalledWith( 1, @@ -1228,7 +1230,9 @@ describe('GasFeeController', () => { gasFeeController.state.gasFeeEstimatesByChainId?.['0x5'], ).toStrictEqual(buildMockGasFeeStateFeeMarket()); - gasFeeController.startPollingByNetworkClientId('sepolia'); + gasFeeController.startPolling({ + networkClientId: 'sepolia', + }); await clock.tickAsync(pollingInterval); expect(mockedDetermineGasFeeCalculations).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/gas-fee-controller/src/GasFeeController.ts b/packages/gas-fee-controller/src/GasFeeController.ts index 9e7b30515ef..13587418a33 100644 --- a/packages/gas-fee-controller/src/GasFeeController.ts +++ b/packages/gas-fee-controller/src/GasFeeController.ts @@ -256,10 +256,15 @@ const defaultState: GasFeeState = { nonRPCGasFeeApisDisabled: false, }; +/** The input to start polling for the {@link GasFeeController} */ +type GasFeePollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that retrieves gas fee estimate data and polls for updated data on a set interval */ -export class GasFeeController extends StaticIntervalPollingController< +export class GasFeeController extends StaticIntervalPollingController()< typeof name, GasFeeState, GasFeeMessenger @@ -560,10 +565,11 @@ export class GasFeeController extends StaticIntervalPollingController< * Fetching token list from the Token Service API. * * @private - * @param networkClientId - The ID of the network client triggering the fetch. + * @param input - The input for the poll. + * @param input.networkClientId - The ID of the network client triggering the fetch. * @returns A promise that resolves when this operation completes. */ - async _executePoll(networkClientId: string): Promise { + async _executePoll({ networkClientId }: GasFeePollingInput): Promise { await this._fetchGasFeeEstimateData({ networkClientId }); } diff --git a/packages/notification-services-controller/CHANGELOG.md b/packages/notification-services-controller/CHANGELOG.md index 46cbf0c6c56..1ddb18c066d 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 57c7372ec45..18c491858da 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", @@ -111,7 +111,7 @@ "@lavamoat/allow-scripts": "^3.0.4", "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^17.2.2", - "@metamask/profile-sync-controller": "^0.9.6", + "@metamask/profile-sync-controller": "^0.9.7", "@types/jest": "^27.4.1", "@types/readable-stream": "^2.3.0", "contentful": "^10.15.0", 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 9d0163b5b53..57591334f75 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 0e7d3a064ef..f5df92a052f 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 c37c8fca780..82ec90ff459 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 71dea69d348..59a2359ec4b 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/schema.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/schema.ts @@ -5,8 +5,21 @@ * Script: `npx openapi-typescript -o ./schema.d.ts` */ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + export type paths = { '/api/v1/notifications': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; /** List all notifications ordered by most recent */ post: { parameters: { @@ -16,6 +29,9 @@ export type paths = { /** @description Number of notifications per page for pagination */ per_page?: number; }; + header?: never; + path?: never; + cookie?: never; }; requestBody?: { content: { @@ -30,16 +46,38 @@ export type paths = { responses: { /** @description Successfully fetched a list of notifications */ 200: { + headers: { + [name: string]: unknown; + }; content: { 'application/json': components['schemas']['Notification'][]; }; }; }; }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/v1/notifications/mark-as-read': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; /** Mark notifications as read */ post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': { @@ -50,17 +88,290 @@ export type paths = { responses: { /** @description Successfully marked notifications as read */ 200: { - content: never; + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/internal/v1/topics': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all topics created (internal) */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully fetched all topics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Topic'][]; + }; + }; + }; + }; + put?: never; + /** Create a new topic (internal) */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name: string; + desc?: string; + }; + }; + }; + responses: { + /** @description Successfully created a new topic */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/internal/v1/subtopics': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all sub-topics created (internal) */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully fetched all subtopics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SubTopic'][]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/internal/v1/global-notifications': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Insert a new Global Notification (internal) */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['GlobalNotificationWrite']; + }; + }; + responses: { + /** @description Successfully created a new global notification */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/global-notifications': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all Global Notifications for a UserID */ + get: { + parameters: { + query: { + /** @description Platform(s) to filter notifications by */ + platform: ('portfolio' | 'extension' | 'mobile')[]; + /** @description Delivery channel(s) to filter notifications by */ + deliveryChannel: ('inbox' | 'push')[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully fetched global notifications */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GlobalNotification'][]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/user-preferences': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all preferences for a UserID */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully fetched preferences */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Topic'][]; + }; + }; + /** @description User not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + /** Update Preferences for a UserID */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + topics: string[]; + }; + }; + }; + responses: { + /** @description Successfully updated topics preferences */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; }; }; }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; }; - export type webhooks = Record; - export type components = { schemas: { + GlobalNotification: { + title: string; + body: string; + /** Format: date-time */ + created_at: string; + }; + GlobalNotificationWrite: { + title: string; + body: string; + 'sub-topic': string; + platforms: ('portfolio' | 'extension' | 'mobile')[]; + delivery_channels: ('inbox' | 'push')[]; + }; + Topic: { + name: string; + description?: string; + /** Format: date-time */ + created_at?: string; + }; + SubTopic: { + name: string; + /** Format: date-time */ + created_at?: string; + }; Notification: { /** Format: uuid */ id: string; @@ -69,14 +380,13 @@ export type components = { /** @example 1 */ chain_id: number; /** @example 17485840 */ - block_number: number; - block_timestamp: string; + block_number?: number; + block_timestamp?: string; /** * Format: address - * * @example 0x881D40237659C251811CEC9c364ef91dC08D300C */ - tx_hash: string; + tx_hash?: string; /** @example false */ unread: boolean; /** Format: date-time */ @@ -98,7 +408,13 @@ export type components = { | components['schemas']['Data_ERC721Sent'] | components['schemas']['Data_ERC721Received'] | components['schemas']['Data_ERC1155Sent'] - | components['schemas']['Data_ERC1155Received']; + | components['schemas']['Data_ERC1155Received'] + | components['schemas']['Data_AaveV3HealthFactor'] + | components['schemas']['Data_EnsExpiration'] + | components['schemas']['Data_LidoStakingRewards'] + | components['schemas']['Data_RocketpoolStakingRewards'] + | components['schemas']['Data_NotionalLoanExpiration'] + | components['schemas']['Data_SparkFiHealthFactor']; }; Data_MetamaskSwapCompleted: { /** @enum {string} */ @@ -241,6 +557,79 @@ export type components = { to: string; nft?: components['schemas']['NFT']; }; + Data_AaveV3HealthFactor: { + /** @enum {string} */ + kind: 'aave_v3_health_factor'; + /** @example 1 */ + chainId: number; + /** Format: decimal */ + healthFactor: string; + /** Format: decimal */ + threshold: string; + }; + Data_EnsExpiration: { + /** @enum {string} */ + kind: 'ens_expiration'; + chainId: number; + reverseEnsName: string; + /** Format: date-time */ + expirationDateIso: string; + /** @example 86400 */ + reminderDelayInSeconds: number; + }; + Data_LidoStakingRewards: { + /** @enum {string} */ + kind: 'lido_staking_rewards'; + chainId: number; + /** Format: decimal */ + currentStethBalance: string; + /** Format: decimal */ + currentEthValue: string; + /** Format: decimal */ + estimatedTotalRewardInPeriod: string; + /** @example 1 */ + daysSinceLastNotification: number; + /** @example 1 */ + notificationIntervalDays: number; + }; + Data_NotionalLoanExpiration: { + /** @enum {string} */ + kind: 'notional_loan_expiration'; + chainId: number; + loans: { + /** Format: decimal */ + amount: string; + symbol: string; + /** Format: date-time */ + maturityDateIso: string; + }[]; + /** @example 86400 */ + reminderDelayInSeconds: number; + }; + Data_RocketpoolStakingRewards: { + /** @enum {string} */ + kind: 'rocketpool_staking_rewards'; + chainId: number; + /** Format: decimal */ + currentRethBalance: string; + /** Format: decimal */ + currentEthValue: string; + /** Format: decimal */ + estimatedTotalRewardInPeriod: string; + /** @example 1 */ + daysSinceLastNotification: number; + /** @example 1 */ + notificationIntervalDays: number; + }; + Data_SparkFiHealthFactor: { + /** @enum {string} */ + kind: 'spark_fi_health_factor'; + chainId: number; + /** Format: decimal */ + healthFactor: string; + /** Format: decimal */ + threshold: string; + }; NetworkFee: { /** Format: decimal */ gas_price: string; @@ -299,6 +688,4 @@ export type components = { export type $defs = Record; -export type external = Record; - export type operations = Record; diff --git a/packages/phishing-controller/package.json b/packages/phishing-controller/package.json index 04af46d7dbd..ee46f85113a 100644 --- a/packages/phishing-controller/package.json +++ b/packages/phishing-controller/package.json @@ -51,7 +51,6 @@ "@metamask/controller-utils": "^11.3.0", "@noble/hashes": "^1.4.0", "@types/punycode": "^2.1.0", - "eth-phishing-detect": "^1.2.0", "ethereum-cryptography": "^2.1.2", "fastest-levenshtein": "^1.0.16", "punycode": "^2.1.1" diff --git a/packages/polling-controller/src/AbstractPollingController.ts b/packages/polling-controller/src/AbstractPollingController.ts index 87945d56cd5..d52aab938a0 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 2ddba4edab3..90192e50493 100644 --- a/packages/polling-controller/src/BlockTrackerPollingController.test.ts +++ b/packages/polling-controller/src/BlockTrackerPollingController.test.ts @@ -3,6 +3,7 @@ import type { NetworkClient } from '@metamask/network-controller'; import EventEmitter from 'events'; import { useFakeTimers } from 'sinon'; +import type { BlockTrackerPollingInput } from './BlockTrackerPollingController'; import { BlockTrackerPollingController } from './BlockTrackerPollingController'; const createExecutePollMock = () => { @@ -13,7 +14,7 @@ const createExecutePollMock = () => { }; let getNetworkClientByIdStub: jest.Mock; -class ChildBlockTrackerPollingController extends BlockTrackerPollingController< +class ChildBlockTrackerPollingController extends BlockTrackerPollingController()< // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any any, @@ -45,9 +46,7 @@ describe('BlockTrackerPollingController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockMessenger: any; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let controller: any; + let controller: ChildBlockTrackerPollingController; let mainnetBlockTracker: TestBlockTracker; let goerliBlockTracker: TestBlockTracker; let sepoliaBlockTracker: TestBlockTracker; @@ -92,29 +91,30 @@ describe('BlockTrackerPollingController', () => { clock.restore(); }); - describe('startPollingByNetworkClientId', () => { - it('should call _executePoll on "latest" block events emitted by blockTrackers for each networkClientId passed to startPollingByNetworkClientId', async () => { - controller.startPollingByNetworkClientId('mainnet'); - controller.startPollingByNetworkClientId('goerli'); + describe('startPolling', () => { + it('should call _executePoll on "latest" block events emitted by blockTrackers for each networkClientId passed to startPolling', async () => { + controller.startPolling({ networkClientId: 'mainnet' }); + controller.startPolling({ networkClientId: 'goerli' }); // await advanceTime({ clock, duration: 5 }); mainnetBlockTracker.emitBlockEvent(); expect(controller._executePoll).toHaveBeenCalledTimes(1); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll).toHaveBeenCalledWith( + { networkClientId: 'mainnet' }, + 1, + ); mainnetBlockTracker.emitBlockEvent(); goerliBlockTracker.emitBlockEvent(); expect(controller._executePoll).toHaveBeenNthCalledWith( 2, - 'mainnet', - {}, + { networkClientId: 'mainnet' }, 2, // 2nd block for mainnet ); expect(controller._executePoll).toHaveBeenNthCalledWith( 3, - 'goerli', - {}, + { networkClientId: 'goerli' }, 1, // 1st block for goerli ); @@ -126,32 +126,28 @@ describe('BlockTrackerPollingController', () => { expect(controller._executePoll).toHaveBeenNthCalledWith( 4, - 'mainnet', - {}, + { networkClientId: 'mainnet' }, 3, ); expect(controller._executePoll).toHaveBeenNthCalledWith( 5, - 'goerli', - {}, + { networkClientId: 'goerli' }, 2, ); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); mainnetBlockTracker.emitBlockEvent(); sepoliaBlockTracker.emitBlockEvent(); expect(controller._executePoll).toHaveBeenNthCalledWith( 6, - 'mainnet', - {}, + { networkClientId: 'mainnet' }, 4, ); expect(controller._executePoll).toHaveBeenNthCalledWith( 7, - 'sepolia', - {}, + { networkClientId: 'sepolia' }, 2, ); @@ -161,21 +157,28 @@ describe('BlockTrackerPollingController', () => { describe('stopPollingByPollingToken', () => { it('should should stop polling when all polling tokens for a networkClientId are deleted', async () => { - const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken1 = controller.startPolling({ + networkClientId: 'mainnet', + }); // await advanceTime({ clock, duration: 5 }); mainnetBlockTracker.emitBlockEvent(); expect(controller._executePoll).toHaveBeenCalledTimes(1); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll).toHaveBeenCalledWith( + { networkClientId: 'mainnet' }, + 1, + ); - const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken2 = controller.startPolling({ + networkClientId: 'mainnet', + }); mainnetBlockTracker.emitBlockEvent(); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], ]); controller.stopPollingByPollingToken(pollingToken1); @@ -184,9 +187,9 @@ describe('BlockTrackerPollingController', () => { // polling is still active for mainnet because pollingToken2 is still active expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], ]); controller.stopPollingByPollingToken(pollingToken2); @@ -198,37 +201,44 @@ describe('BlockTrackerPollingController', () => { // no further polling should occur regardless of how many blocks are emitted // because all pollingTokens for mainnet have been deleted expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], ]); }); it('should should stop polling for one networkClientId when all polling tokens for that networkClientId are deleted, without stopping polling for networkClientIds with active pollingTokens', async () => { - const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken1 = controller.startPolling({ + networkClientId: 'mainnet', + }); mainnetBlockTracker.emitBlockEvent(); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll).toHaveBeenCalledWith( + { networkClientId: 'mainnet' }, + 1, + ); - const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken2 = controller.startPolling({ + networkClientId: 'mainnet', + }); mainnetBlockTracker.emitBlockEvent(); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], ]); - controller.startPollingByNetworkClientId('goerli'); + controller.startPolling({ networkClientId: 'goerli' }); mainnetBlockTracker.emitBlockEvent(); // we are polling for mainnet and goerli but goerli has not emitted any blocks yet expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], ]); controller.stopPollingByPollingToken(pollingToken1); @@ -237,11 +247,11 @@ describe('BlockTrackerPollingController', () => { goerliBlockTracker.emitBlockEvent(); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 1], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], + [{ networkClientId: 'mainnet' }, 4], + [{ networkClientId: 'goerli' }, 1], ]); controller.stopPollingByPollingToken(pollingToken2); @@ -254,13 +264,13 @@ describe('BlockTrackerPollingController', () => { // no further polling for mainnet should occur expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 1], - ['goerli', {}, 2], - ['goerli', {}, 3], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], + [{ networkClientId: 'mainnet' }, 4], + [{ networkClientId: 'goerli' }, 1], + [{ networkClientId: 'goerli' }, 2], + [{ networkClientId: 'goerli' }, 3], ]); controller.stopAllPolling(); @@ -272,11 +282,18 @@ describe('BlockTrackerPollingController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const pollingComplete: any = jest.fn(); - controller.onPollingCompleteByNetworkClientId('mainnet', pollingComplete); - const pollingToken = controller.startPollingByNetworkClientId('mainnet'); + controller.onPollingComplete( + { networkClientId: 'mainnet' }, + pollingComplete, + ); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); controller.stopPollingByPollingToken(pollingToken); expect(pollingComplete).toHaveBeenCalledTimes(1); - expect(pollingComplete).toHaveBeenCalledWith('mainnet:{}'); + expect(pollingComplete).toHaveBeenCalledWith({ + networkClientId: 'mainnet', + }); }); }); }); diff --git a/packages/polling-controller/src/BlockTrackerPollingController.ts b/packages/polling-controller/src/BlockTrackerPollingController.ts index 60f6e1fdccf..cb97c5511ef 100644 --- a/packages/polling-controller/src/BlockTrackerPollingController.ts +++ b/packages/polling-controller/src/BlockTrackerPollingController.ts @@ -11,6 +11,14 @@ import { } from './AbstractPollingController'; import type { Constructor, PollingTokenSetId } from './types'; +/** + * The minimum input required to start polling for a {@link BlockTrackerPollingController}. + * Implementing classes may provide additional properties. + */ +export type BlockTrackerPollingInput = { + networkClientId: NetworkClientId; +}; + /** * BlockTrackerPollingControllerMixin * A polling controller that polls using a block tracker. @@ -20,35 +28,30 @@ import type { Constructor, PollingTokenSetId } from './types'; */ // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/naming-convention -function BlockTrackerPollingControllerMixin( - Base: TBase, -) { - abstract class BlockTrackerPollingController extends AbstractPollingControllerBaseMixin( - Base, - ) { +function BlockTrackerPollingControllerMixin< + TBase extends Constructor, + PollingInput extends BlockTrackerPollingInput, +>(Base: TBase) { + abstract class BlockTrackerPollingController extends AbstractPollingControllerBaseMixin< + TBase, + PollingInput + >(Base) { #activeListeners: Record Promise> = {}; abstract _getNetworkClientById( networkClientId: NetworkClientId, ): NetworkClient | undefined; - _startPollingByNetworkClientId( - networkClientId: NetworkClientId, - options: Json, - ) { - const key = getKey(networkClientId, options); + _startPolling(input: PollingInput) { + const key = getKey(input); if (this.#activeListeners[key]) { return; } - const networkClient = this._getNetworkClientById(networkClientId); + const networkClient = this._getNetworkClientById(input.networkClientId); if (networkClient) { - const updateOnNewBlock = this._executePoll.bind( - this, - networkClientId, - options, - ); + const updateOnNewBlock = this._executePoll.bind(this, input); // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-misused-promises networkClient.blockTracker.addListener('latest', updateOnNewBlock); @@ -57,13 +60,13 @@ function BlockTrackerPollingControllerMixin( throw new Error( // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Unable to retrieve blockTracker for networkClientId ${networkClientId}`, + `Unable to retrieve blockTracker for networkClientId ${input.networkClientId}`, ); } } _stopPollingByPollingTokenSetId(key: PollingTokenSetId) { - const [networkClientId] = key.split(':'); + const { networkClientId } = JSON.parse(key); const networkClient = this._getNetworkClientById( networkClientId as NetworkClientId, ); @@ -85,9 +88,20 @@ function BlockTrackerPollingControllerMixin( class Empty {} -export const BlockTrackerPollingControllerOnly = - BlockTrackerPollingControllerMixin(Empty); -export const BlockTrackerPollingController = - BlockTrackerPollingControllerMixin(BaseController); -export const BlockTrackerPollingControllerV1 = - BlockTrackerPollingControllerMixin(BaseControllerV1); +export const BlockTrackerPollingControllerOnly = < + PollingInput extends BlockTrackerPollingInput, +>() => BlockTrackerPollingControllerMixin(Empty); + +export const BlockTrackerPollingController = < + PollingInput extends BlockTrackerPollingInput, +>() => + BlockTrackerPollingControllerMixin( + BaseController, + ); + +export const BlockTrackerPollingControllerV1 = < + PollingInput extends BlockTrackerPollingInput, +>() => + BlockTrackerPollingControllerMixin( + BaseControllerV1, + ); diff --git a/packages/polling-controller/src/StaticIntervalPollingController.test.ts b/packages/polling-controller/src/StaticIntervalPollingController.test.ts index 2238fbc1115..b166b90a795 100644 --- a/packages/polling-controller/src/StaticIntervalPollingController.test.ts +++ b/packages/polling-controller/src/StaticIntervalPollingController.test.ts @@ -7,7 +7,12 @@ import { StaticIntervalPollingController } from './StaticIntervalPollingControll const TICK_TIME = 5; -class ChildBlockTrackerPollingController extends StaticIntervalPollingController< +type PollingInput = { + networkClientId: string; + address?: string; +}; + +class ChildBlockTrackerPollingController extends StaticIntervalPollingController()< // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any any, @@ -37,9 +42,7 @@ describe('StaticIntervalPollingController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockMessenger: any; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let controller: any; + let controller: ChildBlockTrackerPollingController; beforeEach(() => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -58,9 +61,9 @@ describe('StaticIntervalPollingController', () => { clock.restore(); }); - describe('startPollingByNetworkClientId', () => { + describe('startPolling', () => { it('should start polling if not already polling', async () => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); controller.executePollPromises[0].resolve(); @@ -70,10 +73,10 @@ describe('StaticIntervalPollingController', () => { }); it('should call _executePoll immediately once and continue calling _executePoll on interval when called again with the same networkClientId', async () => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); @@ -89,15 +92,19 @@ describe('StaticIntervalPollingController', () => { describe('multiple networkClientIds', () => { it('should poll for each networkClientId', async () => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('rinkeby'); + controller.startPolling({ + networkClientId: 'rinkeby', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['rinkeby', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], ]); controller.executePollPromises[0].resolve(); @@ -105,10 +112,10 @@ describe('StaticIntervalPollingController', () => { await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['rinkeby', {}], - ['mainnet', {}], - ['rinkeby', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], ]); controller.executePollPromises[2].resolve(); @@ -116,75 +123,79 @@ describe('StaticIntervalPollingController', () => { await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['rinkeby', {}], - ['mainnet', {}], - ['rinkeby', {}], - ['mainnet', {}], - ['rinkeby', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], ]); controller.stopAllPolling(); }); it('should poll multiple networkClientIds when setting interval length', async () => { controller.setIntervalLength(TICK_TIME * 2); - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], + [{ networkClientId: 'mainnet' }], ]); controller.executePollPromises[0].resolve(); await advanceTime({ clock, duration: TICK_TIME }); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ + networkClientId: 'sepolia', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], ]); controller.executePollPromises[1].resolve(); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], ]); controller.executePollPromises[2].resolve(); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], - ['sepolia', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], ]); controller.executePollPromises[3].resolve(); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], ]); controller.executePollPromises[4].resolve(); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], - ['sepolia', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], ]); }); }); @@ -192,7 +203,9 @@ describe('StaticIntervalPollingController', () => { describe('stopPollingByPollingToken', () => { it('should stop polling when called with a valid polling that was the only active pollingToken for a given networkClient', async () => { - const pollingToken = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); controller.executePollPromises[0].resolve(); @@ -204,10 +217,12 @@ describe('StaticIntervalPollingController', () => { }); it('should not stop polling if called with one of multiple active polling tokens for a given networkClient', async () => { - const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken1 = controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); expect(controller._executePoll).toHaveBeenCalledTimes(1); controller.executePollPromises[0].resolve(); await advanceTime({ clock, duration: TICK_TIME }); @@ -219,28 +234,35 @@ describe('StaticIntervalPollingController', () => { }); it('should error if no pollingToken is passed', () => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); expect(() => { - controller.stopPollingByPollingToken(); + controller.stopPollingByPollingToken(''); }).toThrow('pollingToken required'); controller.stopAllPolling(); }); it('should start and stop polling sessions for different networkClientIds with the same options', async () => { - const pollToken1 = controller.startPollingByNetworkClientId('mainnet', { + const pollToken1 = controller.startPolling({ + networkClientId: 'mainnet', address: '0x1', }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('mainnet', { address: '0x2' }); + controller.startPolling({ + networkClientId: 'mainnet', + address: '0x2', + }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('sepolia', { address: '0x2' }); + controller.startPolling({ + networkClientId: 'sepolia', + address: '0x2', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], ]); controller.executePollPromises[0].resolve(); @@ -249,12 +271,12 @@ describe('StaticIntervalPollingController', () => { await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], ]); controller.stopPollingByPollingToken(pollToken1); controller.executePollPromises[3].resolve(); @@ -263,19 +285,21 @@ describe('StaticIntervalPollingController', () => { await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], ]); }); it('should stop polling session after current iteration if stop is requested while current iteration is still executing', async () => { - const pollingToken = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); controller.stopPollingByPollingToken(pollingToken); @@ -293,11 +317,18 @@ describe('StaticIntervalPollingController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const pollingComplete: any = jest.fn(); - controller.onPollingCompleteByNetworkClientId('mainnet', pollingComplete); - const pollingToken = controller.startPollingByNetworkClientId('mainnet'); + controller.onPollingComplete( + { networkClientId: 'mainnet' }, + pollingComplete, + ); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); controller.stopPollingByPollingToken(pollingToken); expect(pollingComplete).toHaveBeenCalledTimes(1); - expect(pollingComplete).toHaveBeenCalledWith('mainnet:{}'); + expect(pollingComplete).toHaveBeenCalledWith({ + networkClientId: 'mainnet', + }); }); }); }); diff --git a/packages/polling-controller/src/StaticIntervalPollingController.ts b/packages/polling-controller/src/StaticIntervalPollingController.ts index a4e4fd2e84e..53493601fa9 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 c7848658ca2..2a1f88476da 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/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index 7d1384c7459..2df2099129a 100644 --- a/packages/profile-sync-controller/CHANGELOG.md +++ b/packages/profile-sync-controller/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.7] + +### Added + +- add support for DELETE ONE endpoint ([#4776](https://github.com/MetaMask/core/pull/4776)) + +### Fixed + +- imported accounts won't be synced anymore by account syncing ([#4777](https://github.com/MetaMask/core/pull/4777)) + ## [0.9.6] ### Added @@ -268,7 +278,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/profile-sync-controller@0.9.6...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@0.9.7...HEAD +[0.9.7]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@0.9.6...@metamask/profile-sync-controller@0.9.7 [0.9.6]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@0.9.5...@metamask/profile-sync-controller@0.9.6 [0.9.5]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@0.9.4...@metamask/profile-sync-controller@0.9.5 [0.9.4]: https://github.com/MetaMask/core/compare/@metamask/profile-sync-controller@0.9.3...@metamask/profile-sync-controller@0.9.4 diff --git a/packages/profile-sync-controller/package.json b/packages/profile-sync-controller/package.json index c519cfe53da..11cf9d248b2 100644 --- a/packages/profile-sync-controller/package.json +++ b/packages/profile-sync-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/profile-sync-controller", - "version": "0.9.6", + "version": "0.9.7", "description": "The profile sync helps developers synchronize data across multiple clients and devices in a privacy-preserving way. All data saved in the user storage database is encrypted client-side to preserve privacy. The user storage provides a modular design, giving developers the flexibility to construct and manage their storage spaces in a way that best suits their needs", "keywords": [ "MetaMask", diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts index 0a265267ab1..1a753acd992 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts @@ -19,6 +19,7 @@ import { mockEndpointGetUserStorageAllFeatureEntries, mockEndpointUpsertUserStorage, mockEndpointDeleteUserStorageAllFeatureEntries, + mockEndpointDeleteUserStorage, } from './__fixtures__/mockServices'; import { MOCK_STORAGE_DATA, @@ -405,6 +406,98 @@ describe('user-storage/user-storage-controller - performBatchSetStorage() tests' }); }); +describe('user-storage/user-storage-controller - performDeleteStorage() tests', () => { + const arrangeMocks = async (mockResponseStatus?: number) => { + return { + messengerMocks: mockUserStorageMessenger(), + mockAPI: mockEndpointDeleteUserStorage( + 'notifications.notification_settings', + mockResponseStatus ? { status: mockResponseStatus } : undefined, + ), + }; + }; + + it('deletes a user storage entry', async () => { + const { messengerMocks, mockAPI } = await arrangeMocks(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + getMetaMetricsState: () => true, + }); + + await controller.performDeleteStorage( + 'notifications.notification_settings', + ); + mockAPI.done(); + + expect(mockAPI.isDone()).toBe(true); + }); + + it('rejects if UserStorage is not enabled', async () => { + const { messengerMocks } = await arrangeMocks(); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + getMetaMetricsState: () => true, + state: { + isProfileSyncingEnabled: false, + isProfileSyncingUpdateLoading: false, + }, + }); + + await expect( + controller.performDeleteStorage('notifications.notification_settings'), + ).rejects.toThrow(expect.any(Error)); + }); + + it.each([ + [ + 'fails when no bearer token is found (auth errors)', + (messengerMocks: ReturnType) => + messengerMocks.mockAuthGetBearerToken.mockRejectedValue( + new Error('MOCK FAILURE'), + ), + ], + [ + 'fails when no session identifier is found (auth errors)', + (messengerMocks: ReturnType) => + messengerMocks.mockAuthGetSessionProfile.mockRejectedValue( + new Error('MOCK FAILURE'), + ), + ], + ])( + 'rejects on auth failure - %s', + async ( + _: string, + arrangeFailureCase: ( + messengerMocks: ReturnType, + ) => void, + ) => { + const { messengerMocks } = await arrangeMocks(); + arrangeFailureCase(messengerMocks); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + getMetaMetricsState: () => true, + }); + + await expect( + controller.performDeleteStorage('notifications.notification_settings'), + ).rejects.toThrow(expect.any(Error)); + }, + ); + + it('rejects if api call fails', async () => { + const { messengerMocks, mockAPI } = await arrangeMocks(500); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + getMetaMetricsState: () => true, + }); + + await expect( + controller.performDeleteStorage('notifications.notification_settings'), + ).rejects.toThrow(expect.any(Error)); + mockAPI.done(); + }); +}); + describe('user-storage/user-storage-controller - performDeleteStorageAllFeatureEntries() tests', () => { const arrangeMocks = async (mockResponseStatus?: number) => { return { diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts index 5a4deb1a5a4..9020e24ccad 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts @@ -45,6 +45,7 @@ import { import { startNetworkSyncing } from './network-syncing/controller-integration'; import { batchUpsertUserStorage, + deleteUserStorage, deleteUserStorageAllFeatureEntries, getUserStorage, getUserStorageAllFeatureEntries, @@ -314,10 +315,7 @@ export default class UserStorageController extends BaseController< ); }, doesInternalAccountHaveCorrectKeyringType: (account: InternalAccount) => { - return ( - account.metadata.keyring.type === KeyringTypes.hd || - account.metadata.keyring.type === KeyringTypes.simple - ); + return account.metadata.keyring.type === KeyringTypes.hd; }, getInternalAccountsList: async (): Promise => { // eslint-disable-next-line @typescript-eslint/await-thenable @@ -678,6 +676,27 @@ export default class UserStorageController extends BaseController< }); } + /** + * Allows deletion of user data. Developers can extend the entry path and entry name through the `schema.ts` file. + * + * @param path - string in the form of `${feature}.${key}` that matches schema + * @returns nothing. NOTE that an error is thrown if fails to delete data. + */ + public async performDeleteStorage( + path: UserStoragePathWithFeatureAndKey, + ): Promise { + this.#assertProfileSyncingEnabled(); + + const { bearerToken, storageKey } = + await this.#getStorageKeyAndBearerToken(); + + await deleteUserStorage({ + path, + bearerToken, + storageKey, + }); + } + /** * Allows deletion of all user data entries for a specific feature. * Developers can extend the entry path through the `schema.ts` file. diff --git a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockResponses.ts b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockResponses.ts index e2de8f71324..a3149f8dbe2 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockResponses.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockResponses.ts @@ -118,6 +118,16 @@ export const getMockUserStorageBatchPutResponse = ( } satisfies MockResponse; }; +export const deleteMockUserStorageResponse = ( + path: UserStoragePathWithFeatureAndKey = 'notifications.notification_settings', +) => { + return { + url: getMockUserStorageEndpoint(path), + requestMethod: 'DELETE', + response: null, + } satisfies MockResponse; +}; + export const deleteMockUserStorageAllFeatureEntriesResponse = ( path: UserStoragePathWithFeatureOnly = 'notifications', ) => { diff --git a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockServices.ts b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockServices.ts index 1613868760f..f5f12a1b7e6 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockServices.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockServices.ts @@ -10,6 +10,7 @@ import { getMockUserStorageAllFeatureEntriesResponse, getMockUserStorageBatchPutResponse, deleteMockUserStorageAllFeatureEntriesResponse, + deleteMockUserStorageResponse, } from './mockResponses'; type MockReply = { @@ -79,6 +80,20 @@ export const mockEndpointBatchUpsertUserStorage = ( return mockEndpoint; }; +export const mockEndpointDeleteUserStorage = ( + path: UserStoragePathWithFeatureAndKey = 'notifications.notification_settings', + mockReply?: MockReply, +) => { + const mockResponse = deleteMockUserStorageResponse(path); + const reply = mockReply ?? { + status: 200, + }; + + const mockEndpoint = nock(mockResponse.url).delete('').reply(reply.status); + + return mockEndpoint; +}; + export const mockEndpointDeleteUserStorageAllFeatureEntries = ( path: UserStoragePathWithFeatureOnly = 'notifications', mockReply?: MockReply, diff --git a/packages/profile-sync-controller/src/controllers/user-storage/services.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/services.test.ts index c545fcf825c..ef14dc2c7cb 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/services.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/services.test.ts @@ -7,6 +7,7 @@ import { mockEndpointGetUserStorageAllFeatureEntries, mockEndpointBatchUpsertUserStorage, mockEndpointDeleteUserStorageAllFeatureEntries, + mockEndpointDeleteUserStorage, } from './__fixtures__/mockServices'; import { MOCK_STORAGE_DATA, @@ -19,6 +20,7 @@ import { getUserStorageAllFeatureEntries, upsertUserStorage, deleteUserStorageAllFeatureEntries, + deleteUserStorage, } from './services'; describe('user-storage/services.ts - getUserStorage() tests', () => { @@ -244,6 +246,60 @@ describe('user-storage/services.ts - batchUpsertUserStorage() tests', () => { }); }); +describe('user-storage/services.ts - deleteUserStorage() tests', () => { + const actCallDeleteUserStorage = async () => { + return await deleteUserStorage({ + path: 'notifications.notification_settings', + bearerToken: 'MOCK_BEARER_TOKEN', + storageKey: MOCK_STORAGE_KEY, + }); + }; + + it('invokes delete endpoint with no errors', async () => { + const mockDeleteUserStorage = mockEndpointDeleteUserStorage( + 'notifications.notification_settings', + ); + + await actCallDeleteUserStorage(); + + expect(mockDeleteUserStorage.isDone()).toBe(true); + }); + + it('throws error if unable to delete user storage', async () => { + const mockDeleteUserStorage = mockEndpointDeleteUserStorage( + 'notifications.notification_settings', + { status: 500 }, + ); + + await expect(actCallDeleteUserStorage()).rejects.toThrow(expect.any(Error)); + mockDeleteUserStorage.done(); + }); + + it('throws error if feature not found', async () => { + const mockDeleteUserStorage = mockEndpointDeleteUserStorage( + 'notifications.notification_settings', + { status: 404 }, + ); + + await expect(actCallDeleteUserStorage()).rejects.toThrow( + 'user-storage - feature/entry not found', + ); + mockDeleteUserStorage.done(); + }); + + it('throws error if unable to get user storage', async () => { + const mockDeleteUserStorage = mockEndpointDeleteUserStorage( + 'notifications.notification_settings', + { status: 400 }, + ); + + await expect(actCallDeleteUserStorage()).rejects.toThrow( + 'user-storage - unable to delete data', + ); + mockDeleteUserStorage.done(); + }); +}); + describe('user-storage/services.ts - deleteUserStorageAllFeatureEntries() tests', () => { const actCallDeleteUserStorageAllFeatureEntries = async () => { return await deleteUserStorageAllFeatureEntries({ diff --git a/packages/profile-sync-controller/src/controllers/user-storage/services.ts b/packages/profile-sync-controller/src/controllers/user-storage/services.ts index 9794824e5e5..1345ec20346 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/services.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/services.ts @@ -240,6 +240,35 @@ export async function batchUpsertUserStorage( } } +/** + * User Storage Service - Delete Storage Entry. + * + * @param opts - User Storage Options + */ +export async function deleteUserStorage( + opts: UserStorageOptions, +): Promise { + const { bearerToken, path, storageKey } = opts; + const encryptedPath = createEntryPath(path, storageKey); + const url = new URL(`${USER_STORAGE_ENDPOINT}/${encryptedPath}`); + + const userStorageResponse = await fetch(url.toString(), { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${bearerToken}`, + }, + }); + + if (userStorageResponse.status === 404) { + throw new Error('user-storage - feature/entry not found'); + } + + if (!userStorageResponse.ok) { + throw new Error('user-storage - unable to delete data'); + } +} + /** * User Storage Service - Delete all storage entries for a specific feature. * @@ -259,12 +288,11 @@ export async function deleteUserStorageAllFeatureEntries( }, }); - // Acceptable error - since indicates feature does not exist. if (userStorageResponse.status === 404) { throw new Error('user-storage - feature not found'); } - if (userStorageResponse.status !== 200 || !userStorageResponse.ok) { + if (!userStorageResponse.ok) { throw new Error('user-storage - unable to delete data'); } } diff --git a/packages/profile-sync-controller/src/sdk/__fixtures__/mock-userstorage.ts b/packages/profile-sync-controller/src/sdk/__fixtures__/mock-userstorage.ts index 30da94b89ca..af861e8bae2 100644 --- a/packages/profile-sync-controller/src/sdk/__fixtures__/mock-userstorage.ts +++ b/packages/profile-sync-controller/src/sdk/__fixtures__/mock-userstorage.ts @@ -77,6 +77,16 @@ export const handleMockUserStoragePut = ( return mockEndpoint; }; +export const handleMockUserStorageDelete = async (mockReply?: MockReply) => { + const reply = mockReply ?? { status: 204 }; + const mockEndpoint = nock(MOCK_STORAGE_URL) + .persist() + .delete(/.*/u) + .reply(reply.status); + + return mockEndpoint; +}; + export const handleMockUserStorageDeleteAllFeatureEntries = async ( mockReply?: MockReply, ) => { diff --git a/packages/profile-sync-controller/src/sdk/user-storage.test.ts b/packages/profile-sync-controller/src/sdk/user-storage.test.ts index b308fe868ea..9eb303beabd 100644 --- a/packages/profile-sync-controller/src/sdk/user-storage.test.ts +++ b/packages/profile-sync-controller/src/sdk/user-storage.test.ts @@ -10,6 +10,7 @@ import { handleMockUserStoragePut, handleMockUserStorageGetAllFeatureEntries, handleMockUserStorageDeleteAllFeatureEntries, + handleMockUserStorageDelete, } from './__fixtures__/mock-userstorage'; import { arrangeAuth, typedMockFn } from './__fixtures__/test-utils'; import type { IBaseAuth } from './authentication-jwt-bearer/types'; @@ -133,6 +134,33 @@ describe('User Storage', () => { expect(mockPut.isDone()).toBe(true); }); + it('user storage: delete one feature entry', async () => { + const { auth } = arrangeAuth('SRP', MOCK_SRP); + const { userStorage } = arrangeUserStorage(auth); + + const mockDelete = await handleMockUserStorageDelete(); + + await userStorage.deleteItem('notifications.notification_settings'); + expect(mockDelete.isDone()).toBe(true); + }); + + it('user storage: failed to delete one feature entry', async () => { + const { auth } = arrangeAuth('SRP', MOCK_SRP); + const { userStorage } = arrangeUserStorage(auth); + + await handleMockUserStorageDelete({ + status: 401, + body: { + message: 'failed to delete storage entry', + error: 'generic-error', + }, + }); + + await expect( + userStorage.deleteItem('notifications.notification_settings'), + ).rejects.toThrow(UserStorageError); + }); + it('user storage: delete all feature entries', async () => { const { auth } = arrangeAuth('SRP', MOCK_SRP); const { userStorage } = arrangeUserStorage(auth); diff --git a/packages/profile-sync-controller/src/sdk/user-storage.ts b/packages/profile-sync-controller/src/sdk/user-storage.ts index 83c4f50eff2..d527982f76f 100644 --- a/packages/profile-sync-controller/src/sdk/user-storage.ts +++ b/packages/profile-sync-controller/src/sdk/user-storage.ts @@ -76,6 +76,10 @@ export class UserStorage { return this.#getUserStorageAllFeatureEntries(path); } + async deleteItem(path: UserStoragePathWithFeatureAndKey): Promise { + return this.#deleteUserStorage(path); + } + async deleteAllFeatureItems( path: UserStoragePathWithFeatureOnly, ): Promise { @@ -295,6 +299,51 @@ export class UserStorage { } } + async #deleteUserStorage( + path: UserStoragePathWithFeatureAndKey, + ): Promise { + try { + const headers = await this.#getAuthorizationHeader(); + const storageKey = await this.getStorageKey(); + const encryptedPath = createEntryPath(path, storageKey); + + const url = new URL(STORAGE_URL(this.env, encryptedPath)); + + const response = await fetch(url.toString(), { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + }); + + if (response.status === 404) { + throw new NotFoundError( + `feature/key set not found for path '${path}'.`, + ); + } + + if (!response.ok) { + const responseBody = (await response.json()) as ErrorMessage; + throw new Error( + `HTTP error message: ${responseBody.message}, error: ${responseBody.error}`, + ); + } + } catch (e) { + if (e instanceof NotFoundError) { + throw e; + } + + /* istanbul ignore next */ + const errorMessage = + e instanceof Error ? e.message : JSON.stringify(e ?? ''); + + throw new UserStorageError( + `failed to delete user storage for path '${path}'. ${errorMessage}`, + ); + } + } + async #deleteUserStorageAllFeatureEntries( path: UserStoragePathWithFeatureOnly, ): Promise { diff --git a/packages/user-operation-controller/src/UserOperationController.test.ts b/packages/user-operation-controller/src/UserOperationController.test.ts index e41af0e540f..027fbb1fcd2 100644 --- a/packages/user-operation-controller/src/UserOperationController.test.ts +++ b/packages/user-operation-controller/src/UserOperationController.test.ts @@ -137,7 +137,7 @@ function createBundlerMock() { */ function createPendingUserOperationTrackerMock() { return { - startPollingByNetworkClientId: jest.fn(), + startPolling: jest.fn(), setIntervalLength: jest.fn(), hub: new EventEmitter(), } as unknown as jest.Mocked; @@ -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 3a5a187849d..492233d33c4 100644 --- a/packages/user-operation-controller/src/UserOperationController.ts +++ b/packages/user-operation-controller/src/UserOperationController.ts @@ -302,9 +302,9 @@ export class UserOperationController extends BaseController< } startPollingByNetworkClientId(networkClientId: string): string { - return this.#pendingUserOperationTracker.startPollingByNetworkClientId( + return this.#pendingUserOperationTracker.startPolling({ networkClientId, - ); + }); } async #addUserOperation( diff --git a/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.test.ts b/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.test.ts index 2285f2cd90e..3291e07fdb8 100644 --- a/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.test.ts +++ b/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.test.ts @@ -93,7 +93,9 @@ describe('PendingUserOperationTracker', () => { beforeCallback?.(pendingUserOperationTracker); - await pendingUserOperationTracker._executePoll(NETWORK_CLIENT_ID_MOCK, {}); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); } /** @@ -117,7 +119,9 @@ describe('PendingUserOperationTracker', () => { beforeCallback?.(pendingUserOperationTracker); - await pendingUserOperationTracker._executePoll(NETWORK_CLIENT_ID_MOCK, {}); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); } beforeEach(() => { @@ -147,10 +151,9 @@ describe('PendingUserOperationTracker', () => { messenger: messengerMock, }); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); expect(bundlerMock.getUserOperationReceipt).not.toHaveBeenCalled(); expect(queryMock).not.toHaveBeenCalled(); @@ -173,10 +176,9 @@ describe('PendingUserOperationTracker', () => { messenger: messengerMock, }); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); expect(bundlerMock.getUserOperationReceipt).not.toHaveBeenCalled(); expect(queryMock).not.toHaveBeenCalled(); @@ -197,10 +199,9 @@ describe('PendingUserOperationTracker', () => { new Error('Test Error'), ); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); }); // eslint-disable-next-line jest/expect-expect @@ -216,10 +217,9 @@ describe('PendingUserOperationTracker', () => { bundlerMock.getUserOperationReceipt.mockResolvedValueOnce(undefined); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); }); it('queries bundler using eth_getUserOperationReceipt RPC method', async () => { @@ -232,10 +232,9 @@ describe('PendingUserOperationTracker', () => { messenger: messengerMock, }); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); expect(bundlerMock.getUserOperationReceipt).toHaveBeenCalledTimes(1); expect(bundlerMock.getUserOperationReceipt).toHaveBeenCalledWith( diff --git a/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.ts b/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.ts index 26c58cc3423..2d5c1ca366d 100644 --- a/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.ts +++ b/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.ts @@ -1,8 +1,11 @@ import { query, toHex } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; -import type { NetworkClient, Provider } from '@metamask/network-controller'; +import type { + NetworkClient, + NetworkClientId, + Provider, +} from '@metamask/network-controller'; import { BlockTrackerPollingControllerOnly } from '@metamask/polling-controller'; -import type { Json } from '@metamask/utils'; import { createModuleLogger, type Hex } from '@metamask/utils'; import EventEmitter from 'events'; @@ -40,11 +43,16 @@ export type PendingUserOperationTrackerEventEmitter = EventEmitter & { emit(eventName: T, ...args: Events[T]): boolean; }; +/** The input to start polling for the {@link PendingUserOperationTracker} */ +type PendingUserOperationPollingInput = { + networkClientId: NetworkClientId; +}; + /** * A helper class to periodically query the bundlers * and update the status of any submitted user operations. */ -export class PendingUserOperationTracker extends BlockTrackerPollingControllerOnly { +export class PendingUserOperationTracker extends BlockTrackerPollingControllerOnly() { hub: PendingUserOperationTrackerEventEmitter; #getUserOperations: () => UserOperationMetadata[]; @@ -66,7 +74,7 @@ export class PendingUserOperationTracker extends BlockTrackerPollingControllerOn this.#messenger = messenger; } - async _executePoll(networkClientId: string, _options: Json) { + async _executePoll({ networkClientId }: PendingUserOperationPollingInput) { try { const { blockTracker, configuration, provider } = this._getNetworkClientById(networkClientId) as NetworkClient; 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 6943346451f..00000000000 --- 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 cab272fdde9..00000000000 --- 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 29c85445f6d..02fc53459bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3108,7 +3108,7 @@ __metadata: "@metamask/base-controller": "npm:^7.0.1" "@metamask/controller-utils": "npm:^11.3.0" "@metamask/keyring-controller": "npm:^17.2.2" - "@metamask/profile-sync-controller": "npm:^0.9.6" + "@metamask/profile-sync-controller": "npm:^0.9.7" "@types/jest": "npm:^27.4.1" "@types/readable-stream": "npm:^2.3.0" bignumber.js: "npm:^4.1.0" @@ -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" @@ -3289,7 +3288,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/profile-sync-controller@npm:^0.9.6, @metamask/profile-sync-controller@workspace:packages/profile-sync-controller": +"@metamask/profile-sync-controller@npm:^0.9.7, @metamask/profile-sync-controller@workspace:packages/profile-sync-controller": version: 0.0.0-use.local resolution: "@metamask/profile-sync-controller@workspace:packages/profile-sync-controller" dependencies: @@ -7016,15 +7015,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"