diff --git a/package.json b/package.json index 1d5f64a75a..782c32954d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "215.0.0", + "version": "219.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 39c5761c2a..aaa520ad4f 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 7d2498f7a6..e046347289 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 707b7a906e..4c475f0b75 100644 --- a/packages/assets-controllers/src/AccountTrackerController.test.ts +++ b/packages/assets-controllers/src/AccountTrackerController.test.ts @@ -505,7 +505,9 @@ describe('AccountTrackerController', () => { .spyOn(controller, 'refresh') .mockResolvedValue(); - controller.startPollingByNetworkClientId(networkClientId1); + controller.startPolling({ + networkClientId: networkClientId1, + }); await advanceTime({ clock, duration: 0 }); expect(refreshSpy).toHaveBeenNthCalledWith(1, networkClientId1); @@ -516,8 +518,9 @@ describe('AccountTrackerController', () => { expect(refreshSpy).toHaveBeenNthCalledWith(2, networkClientId1); expect(refreshSpy).toHaveBeenCalledTimes(2); - const pollToken = - controller.startPollingByNetworkClientId(networkClientId2); + const pollToken = controller.startPolling({ + networkClientId: networkClientId2, + }); await advanceTime({ clock, duration: 0 }); expect(refreshSpy).toHaveBeenNthCalledWith(3, networkClientId2); diff --git a/packages/assets-controllers/src/AccountTrackerController.ts b/packages/assets-controllers/src/AccountTrackerController.ts index fb1f131cc7..e58bac61fc 100644 --- a/packages/assets-controllers/src/AccountTrackerController.ts +++ b/packages/assets-controllers/src/AccountTrackerController.ts @@ -120,10 +120,15 @@ export type AccountTrackerControllerMessenger = RestrictedControllerMessenger< AllowedEvents['type'] >; +/** The input to start polling for the {@link AccountTrackerController} */ +type AccountTrackerPollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that tracks the network balances for all user accounts. */ -export class AccountTrackerController extends StaticIntervalPollingController< +export class AccountTrackerController extends StaticIntervalPollingController()< typeof controllerName, AccountTrackerControllerState, AccountTrackerControllerMessenger @@ -309,9 +314,12 @@ export class AccountTrackerController extends StaticIntervalPollingController< /** * Refreshes the balances of the accounts using the networkClientId * - * @param networkClientId - The network client ID used to get balances. + * @param input - The input for the poll. + * @param input.networkClientId - The network client ID used to get balances. */ - async _executePoll(networkClientId: string): Promise { + async _executePoll({ + networkClientId, + }: AccountTrackerPollingInput): Promise { // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.refresh(networkClientId); diff --git a/packages/assets-controllers/src/CurrencyRateController.test.ts b/packages/assets-controllers/src/CurrencyRateController.test.ts index c719301cbe..4731e026bf 100644 --- a/packages/assets-controllers/src/CurrencyRateController.test.ts +++ b/packages/assets-controllers/src/CurrencyRateController.test.ts @@ -155,7 +155,7 @@ describe('CurrencyRateController', () => { messenger, }); - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); await advanceTime({ clock, duration: 0 }); expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1); expect(controller.state.currencyRates).toStrictEqual({ @@ -192,7 +192,7 @@ describe('CurrencyRateController', () => { messenger, }); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); await advanceTime({ clock, duration: 0 }); @@ -217,7 +217,7 @@ describe('CurrencyRateController', () => { fetchExchangeRate: fetchExchangeRateStub, messenger, }); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); await advanceTime({ clock, duration: 0 }); controller.stopAllPolling(); @@ -225,7 +225,7 @@ describe('CurrencyRateController', () => { // called once upon initial start expect(fetchExchangeRateStub).toHaveBeenCalledTimes(1); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); await advanceTime({ clock, duration: 0 }); expect(fetchExchangeRateStub).toHaveBeenCalledTimes(2); diff --git a/packages/assets-controllers/src/CurrencyRateController.ts b/packages/assets-controllers/src/CurrencyRateController.ts index 319e819818..badc192532 100644 --- a/packages/assets-controllers/src/CurrencyRateController.ts +++ b/packages/assets-controllers/src/CurrencyRateController.ts @@ -78,11 +78,16 @@ const defaultState = { }, }; +/** The input to start polling for the {@link CurrencyRateController} */ +type CurrencyRatePollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that passively polls on a set interval for an exchange rate from the current network * asset to the user's preferred currency. */ -export class CurrencyRateController extends StaticIntervalPollingController< +export class CurrencyRateController extends StaticIntervalPollingController()< typeof name, CurrencyRateState, CurrencyRateMessenger @@ -237,10 +242,12 @@ export class CurrencyRateController extends StaticIntervalPollingController< /** * Updates exchange rate for the current currency. * - * @param networkClientId - The network client ID used to get a ticker value. - * @returns The controller state. + * @param input - The input for the poll. + * @param input.networkClientId - The network client ID used to get a ticker value. */ - async _executePoll(networkClientId: NetworkClientId): Promise { + async _executePoll({ + networkClientId, + }: CurrencyRatePollingInput): Promise { const networkClient = this.messagingSystem.call( 'NetworkController:getNetworkClientById', networkClientId, diff --git a/packages/assets-controllers/src/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts index 21ae2029dd..72af3018be 100644 --- a/packages/assets-controllers/src/TokenDetectionController.test.ts +++ b/packages/assets-controllers/src/TokenDetectionController.test.ts @@ -1855,7 +1855,7 @@ describe('TokenDetectionController', () => { }); }); - describe('startPollingByNetworkClientId', () => { + describe('startPolling', () => { let clock: sinon.SinonFakeTimers; beforeEach(() => { clock = sinon.useFakeTimers(); @@ -1904,13 +1904,16 @@ describe('TokenDetectionController', () => { return Promise.resolve(); }); - controller.startPollingByNetworkClientId('mainnet', { + controller.startPolling({ + networkClientId: 'mainnet', address: '0x1', }); - controller.startPollingByNetworkClientId('sepolia', { + controller.startPolling({ + networkClientId: 'sepolia', address: '0xdeadbeef', }); - controller.startPollingByNetworkClientId('goerli', { + controller.startPolling({ + networkClientId: 'goerli', address: '0x3', }); await advanceTime({ clock, duration: 0 }); @@ -2247,7 +2250,7 @@ function getTokensPath(chainId: Hex) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `/tokens/${convertHexToDecimal( chainId, - )}?occurrenceFloor=3&includeNativeAssets=false&includeDuplicateSymbolAssets=false&includeTokenFees=false&includeAssetType=false`; + )}?occurrenceFloor=3&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false`; } type WithControllerCallback = ({ diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index 8e483d8f37..2459baea38 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -138,6 +138,12 @@ export type TokenDetectionControllerMessenger = RestrictedControllerMessenger< AllowedEvents['type'] >; +/** The input to start polling for the {@link TokenDetectionController} */ +type TokenDetectionPollingInput = { + networkClientId: NetworkClientId; + address: string; +}; + /** * Controller that passively polls on a set interval for Tokens auto detection * @property intervalId - Polling interval used to fetch new token rates @@ -148,7 +154,7 @@ export type TokenDetectionControllerMessenger = RestrictedControllerMessenger< * @property isDetectionEnabledFromPreferences - Boolean to track if detection is enabled from PreferencesController * @property isDetectionEnabledForNetwork - Boolean to track if detected is enabled for current network */ -export class TokenDetectionController extends StaticIntervalPollingController< +export class TokenDetectionController extends StaticIntervalPollingController()< typeof controllerName, TokenDetectionState, TokenDetectionControllerMessenger @@ -432,16 +438,16 @@ export class TokenDetectionController extends StaticIntervalPollingController< }; } - async _executePoll( - networkClientId: NetworkClientId, - options: { address: string }, - ): Promise { + async _executePoll({ + networkClientId, + address, + }: TokenDetectionPollingInput): Promise { if (!this.isActive) { return; } await this.detectTokens({ networkClientId, - selectedAddress: options.address, + selectedAddress: address, }); } diff --git a/packages/assets-controllers/src/TokenListController.test.ts b/packages/assets-controllers/src/TokenListController.test.ts index cb02140a80..317fc16657 100644 --- a/packages/assets-controllers/src/TokenListController.test.ts +++ b/packages/assets-controllers/src/TokenListController.test.ts @@ -1157,7 +1157,7 @@ describe('TokenListController', () => { }); }); - describe('startPollingByNetworkClient', () => { + describe('startPolling', () => { let clock: sinon.SinonFakeTimers; const pollingIntervalTime = 1000; beforeEach(() => { @@ -1200,7 +1200,7 @@ describe('TokenListController', () => { expiredCacheExistingState.tokenList, ); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); await advanceTime({ clock, duration: 0 }); expect(fetchTokenListByChainIdSpy.mock.calls[0]).toStrictEqual( @@ -1236,7 +1236,7 @@ describe('TokenListController', () => { expiredCacheExistingState.tokenList, ); - controller.startPollingByNetworkClientId('goerli'); + controller.startPolling({ networkClientId: 'goerli' }); await advanceTime({ clock, duration: 0 }); expect(fetchTokenListByChainIdSpy).toHaveBeenCalledTimes(1); @@ -1306,7 +1306,9 @@ describe('TokenListController', () => { expect(controller.state).toStrictEqual(startingState); // start polling for sepolia - const pollingToken = controller.startPollingByNetworkClientId('sepolia'); + const pollingToken = controller.startPolling({ + networkClientId: 'sepolia', + }); // wait a polling interval await advanceTime({ clock, duration: pollingIntervalTime }); @@ -1324,7 +1326,9 @@ describe('TokenListController', () => { controller.stopPollingByPollingToken(pollingToken); // start polling for binance - controller.startPollingByNetworkClientId('binance-network-client-id'); + controller.startPolling({ + networkClientId: 'binance-network-client-id', + }); await advanceTime({ clock, duration: pollingIntervalTime }); // expect fetchTokenListByChain to be called for binance, but not for sepolia @@ -1363,5 +1367,5 @@ function getTokensPath(chainId: Hex) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `/tokens/${convertHexToDecimal( chainId, - )}?occurrenceFloor=3&includeNativeAssets=false&includeDuplicateSymbolAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`; + )}?occurrenceFloor=3&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`; } diff --git a/packages/assets-controllers/src/TokenListController.ts b/packages/assets-controllers/src/TokenListController.ts index d4290e6a7d..e4504ec0e9 100644 --- a/packages/assets-controllers/src/TokenListController.ts +++ b/packages/assets-controllers/src/TokenListController.ts @@ -92,10 +92,15 @@ export const getDefaultTokenListState = (): TokenListState => { }; }; +/** The input to start polling for the {@link TokenListController} */ +type TokenListPollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that passively polls on a set interval for the list of tokens from metaswaps api */ -export class TokenListController extends StaticIntervalPollingController< +export class TokenListController extends StaticIntervalPollingController()< typeof name, TokenListState, TokenListControllerMessenger @@ -211,7 +216,7 @@ export class TokenListController extends StaticIntervalPollingController< if (!isTokenListSupportedForNetwork(this.chainId)) { return; } - await this.startPolling(); + await this.#startPolling(); } /** @@ -219,7 +224,7 @@ export class TokenListController extends StaticIntervalPollingController< */ async restart() { this.stopPolling(); - await this.startPolling(); + await this.#startPolling(); } /** @@ -248,7 +253,7 @@ export class TokenListController extends StaticIntervalPollingController< /** * Starts a new polling interval. */ - private async startPolling(): Promise { + async #startPolling(): Promise { await safelyExecute(() => this.fetchTokenList()); // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-misused-promises @@ -261,10 +266,13 @@ export class TokenListController extends StaticIntervalPollingController< * Fetching token list from the Token Service API. * * @private - * @param networkClientId - The ID of the network client triggering the fetch. + * @param input - The input for the poll. + * @param input.networkClientId - The ID of the network client triggering the fetch. * @returns A promise that resolves when this operation completes. */ - async _executePoll(networkClientId: string): Promise { + async _executePoll({ + networkClientId, + }: TokenListPollingInput): Promise { return this.fetchTokenList(networkClientId); } diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index dbfcffc0f3..ea51853d46 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -1216,7 +1216,9 @@ describe('TokenRatesController', () => { }, }, async ({ controller }) => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes(1); @@ -1268,7 +1270,9 @@ describe('TokenRatesController', () => { }, }, async ({ controller }) => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(controller.state).toStrictEqual({ @@ -1372,7 +1376,9 @@ describe('TokenRatesController', () => { }, }, async ({ controller }) => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); // flush promises and advance setTimeouts they enqueue 3 times // needed because fetch() doesn't resolve immediately, so any // downstream promises aren't flushed until the next advanceTime loop @@ -1472,7 +1478,9 @@ describe('TokenRatesController', () => { }, }, async ({ controller }) => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); // flush promises and advance setTimeouts they enqueue 3 times // needed because fetch() doesn't resolve immediately, so any // downstream promises aren't flushed until the next advanceTime loop @@ -1513,8 +1521,9 @@ describe('TokenRatesController', () => { }, }, async ({ controller }) => { - const pollingToken = - controller.startPollingByNetworkClientId('mainnet'); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(tokenPricesService.fetchTokenPrices).toHaveBeenCalledTimes( 1, diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index aeddfbfcb0..6632e3635d 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -221,11 +221,16 @@ export const getDefaultTokenRatesControllerState = }; }; +/** The input to start polling for the {@link TokenRatesController} */ +export type TokenRatesPollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that passively polls on a set interval for token-to-fiat exchange rates * for tokens stored in the TokensController */ -export class TokenRatesController extends StaticIntervalPollingController< +export class TokenRatesController extends StaticIntervalPollingController()< typeof controllerName, TokenRatesControllerState, TokenRatesControllerMessenger @@ -594,10 +599,12 @@ export class TokenRatesController extends StaticIntervalPollingController< /** * Updates token rates for the given networkClientId * - * @param networkClientId - The network client ID used to get a ticker value. - * @returns The controller state. + * @param input - The input for the poll. + * @param input.networkClientId - The network client ID used to get a ticker value. */ - async _executePoll(networkClientId: NetworkClientId): Promise { + async _executePoll({ + networkClientId, + }: TokenRatesPollingInput): Promise { const networkClient = this.messagingSystem.call( 'NetworkController:getNetworkClientById', networkClientId, diff --git a/packages/assets-controllers/src/token-service.test.ts b/packages/assets-controllers/src/token-service.test.ts index 26ff3aa8fb..05b4fa025e 100644 --- a/packages/assets-controllers/src/token-service.test.ts +++ b/packages/assets-controllers/src/token-service.test.ts @@ -243,7 +243,7 @@ describe('Token service', () => { const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get( - `/tokens/${sampleDecimalChainId}?occurrenceFloor=3&includeNativeAssets=false&includeDuplicateSymbolAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`, + `/tokens/${sampleDecimalChainId}?occurrenceFloor=3&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`, ) .reply(200, sampleTokenList) .persist(); @@ -260,7 +260,7 @@ describe('Token service', () => { nock(TOKEN_END_POINT_API) .get( - `/tokens/${lineaChainId}?occurrenceFloor=1&includeNativeAssets=false&includeDuplicateSymbolAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`, + `/tokens/${lineaChainId}?occurrenceFloor=1&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`, ) .reply(200, sampleTokenListLinea) .persist(); @@ -274,7 +274,7 @@ describe('Token service', () => { const abortController = new AbortController(); nock(TOKEN_END_POINT_API) .get( - `/tokens/${sampleDecimalChainId}?occurrenceFloor=3&includeNativeAssets=false&includeDuplicateSymbolAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`, + `/tokens/${sampleDecimalChainId}?occurrenceFloor=3&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`, ) // well beyond time it will take to abort .delay(ONE_SECOND_IN_MILLISECONDS) @@ -294,7 +294,7 @@ describe('Token service', () => { const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get( - `/tokens/${sampleDecimalChainId}?occurrenceFloor=3&includeNativeAssets=false&includeDuplicateSymbolAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`, + `/tokens/${sampleDecimalChainId}?occurrenceFloor=3&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`, ) .replyWithError('Example network error') .persist(); @@ -308,7 +308,7 @@ describe('Token service', () => { const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get( - `/tokens/${sampleDecimalChainId}?occurrenceFloor=3&includeNativeAssets=false&includeDuplicateSymbolAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`, + `/tokens/${sampleDecimalChainId}?occurrenceFloor=3&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`, ) .reply(500) .persist(); @@ -322,7 +322,7 @@ describe('Token service', () => { const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get( - `/tokens/${sampleDecimalChainId}?occurrenceFloor=3&includeNativeAssets=false&includeDuplicateSymbolAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`, + `/tokens/${sampleDecimalChainId}?occurrenceFloor=3&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`, ) // well beyond timeout .delay(ONE_SECOND_IN_MILLISECONDS) diff --git a/packages/assets-controllers/src/token-service.ts b/packages/assets-controllers/src/token-service.ts index ba6fa99e2b..b347079e02 100644 --- a/packages/assets-controllers/src/token-service.ts +++ b/packages/assets-controllers/src/token-service.ts @@ -23,7 +23,7 @@ function getTokensURL(chainId: Hex) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `${TOKEN_END_POINT_API}/tokens/${convertHexToDecimal( chainId, - )}?occurrenceFloor=${occurrenceFloor}&includeNativeAssets=false&includeDuplicateSymbolAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`; + )}?occurrenceFloor=${occurrenceFloor}&includeNativeAssets=false&includeTokenFees=false&includeAssetType=false&includeERC20Permit=false&includeStorage=false`; } /** diff --git a/packages/eth-json-rpc-provider/package.json b/packages/eth-json-rpc-provider/package.json index da014e3516..e117faf36d 100644 --- a/packages/eth-json-rpc-provider/package.json +++ b/packages/eth-json-rpc-provider/package.json @@ -53,7 +53,7 @@ }, "dependencies": { "@metamask/json-rpc-engine": "^9.0.3", - "@metamask/rpc-errors": "^6.3.1", + "@metamask/rpc-errors": "^7.0.0", "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^9.1.0", "uuid": "^8.3.2" diff --git a/packages/gas-fee-controller/src/GasFeeController.test.ts b/packages/gas-fee-controller/src/GasFeeController.test.ts index b8b0917714..4b4bf928b3 100644 --- a/packages/gas-fee-controller/src/GasFeeController.test.ts +++ b/packages/gas-fee-controller/src/GasFeeController.test.ts @@ -1199,7 +1199,9 @@ describe('GasFeeController', () => { interval: pollingInterval, }); - gasFeeController.startPollingByNetworkClientId('goerli'); + gasFeeController.startPolling({ + networkClientId: 'goerli', + }); await clock.tickAsync(0); expect(mockedDetermineGasFeeCalculations).toHaveBeenNthCalledWith( 1, @@ -1228,7 +1230,9 @@ describe('GasFeeController', () => { gasFeeController.state.gasFeeEstimatesByChainId?.['0x5'], ).toStrictEqual(buildMockGasFeeStateFeeMarket()); - gasFeeController.startPollingByNetworkClientId('sepolia'); + gasFeeController.startPolling({ + networkClientId: 'sepolia', + }); await clock.tickAsync(pollingInterval); expect(mockedDetermineGasFeeCalculations).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/gas-fee-controller/src/GasFeeController.ts b/packages/gas-fee-controller/src/GasFeeController.ts index 9e7b30515e..13587418a3 100644 --- a/packages/gas-fee-controller/src/GasFeeController.ts +++ b/packages/gas-fee-controller/src/GasFeeController.ts @@ -256,10 +256,15 @@ const defaultState: GasFeeState = { nonRPCGasFeeApisDisabled: false, }; +/** The input to start polling for the {@link GasFeeController} */ +type GasFeePollingInput = { + networkClientId: NetworkClientId; +}; + /** * Controller that retrieves gas fee estimate data and polls for updated data on a set interval */ -export class GasFeeController extends StaticIntervalPollingController< +export class GasFeeController extends StaticIntervalPollingController()< typeof name, GasFeeState, GasFeeMessenger @@ -560,10 +565,11 @@ export class GasFeeController extends StaticIntervalPollingController< * Fetching token list from the Token Service API. * * @private - * @param networkClientId - The ID of the network client triggering the fetch. + * @param input - The input for the poll. + * @param input.networkClientId - The ID of the network client triggering the fetch. * @returns A promise that resolves when this operation completes. */ - async _executePoll(networkClientId: string): Promise { + async _executePoll({ networkClientId }: GasFeePollingInput): Promise { await this._fetchGasFeeEstimateData({ networkClientId }); } diff --git a/packages/json-rpc-engine/package.json b/packages/json-rpc-engine/package.json index 93b4e77c92..5cd3044314 100644 --- a/packages/json-rpc-engine/package.json +++ b/packages/json-rpc-engine/package.json @@ -56,7 +56,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/rpc-errors": "^6.3.1", + "@metamask/rpc-errors": "^7.0.0", "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^9.1.0" }, diff --git a/packages/notification-services-controller/CHANGELOG.md b/packages/notification-services-controller/CHANGELOG.md index 46cbf0c6c5..0ae2e9bc0c 100644 --- a/packages/notification-services-controller/CHANGELOG.md +++ b/packages/notification-services-controller/CHANGELOG.md @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.10.0] + +### Added + +- added the ability for the `fetchFeatureAnnouncementNotifications` function, within the `notification-services-controller`, to fetch draft content from Contentful. This is made possible by passing a `previewToken` parameter ([#4790](https://github.com/MetaMask/core/pull/4790)) + +### Changed + +- update `createMockNotification` functions to provide more realistic data for use in tests and component rendering in Storybook ([#4791](https://github.com/MetaMask/core/pull/4791)) + +## [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 +200,9 @@ 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.10.0...HEAD +[0.10.0]: https://github.com/MetaMask/core/compare/@metamask/notification-services-controller@0.9.0...@metamask/notification-services-controller@0.10.0 +[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 57c7372ec4..0548cde4f4 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.10.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/NotificationServicesController.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts index 2ffc48d63c..f8f64613b7 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts @@ -1054,9 +1054,12 @@ export default class NotificationServicesController extends BaseController< * * **Action** - When a user views the notification list page/dropdown * + * @param previewToken - the preview token to use if needed * @throws {Error} Throws an error if unauthenticated or from other operations. */ - public async fetchAndUpdateMetamaskNotifications(): Promise { + public async fetchAndUpdateMetamaskNotifications( + previewToken?: string, + ): Promise { try { this.#setIsFetchingMetamaskNotifications(true); @@ -1065,6 +1068,7 @@ export default class NotificationServicesController extends BaseController< .isFeatureAnnouncementsEnabled ? await FeatureNotifications.getFeatureAnnouncementNotifications( this.#featureAnnouncementEnv, + previewToken, ).catch(() => []) : []; diff --git a/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mock-raw-notifications.ts b/packages/notification-services-controller/src/NotificationServicesController/__fixtures__/mock-raw-notifications.ts index 9d0163b5b5..7d41fa3e7a 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: 'vitalik.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: '10', + currentEthValue: '10.5', + estimatedTotalRewardInPeriod: '0.5', + daysSinceLastNotification: 30, + notificationIntervalDays: 30, + }, + 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: '1.1234', + 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: '10', + currentEthValue: '10.5', + estimatedTotalRewardInPeriod: '0.5', + daysSinceLastNotification: 30, + notificationIntervalDays: 30, + }, + id: 'd73df14d-ce73-4f38-bad3-ab028154042r', + trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042r', + tx_hash: + '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', + unread: true, + }; + + return mockNotification; +} + +/** + * Mocking Utility - create a mock SparkFi Health Factor notification + * @returns Mock raw SparkFi Health Factor notification + */ +export function createMockNotificationSparkFiHealthFactor(): OnChainRawNotification { + const mockNotification: OnChainRawNotification = { + type: TRIGGER_TYPES.SPARK_FI_HEALTH_FACTOR, + block_number: 18378208, + block_timestamp: '1697643851', + chain_id: 1, + created_at: '2023-10-18T16:35:03.147606Z', + address: '0x881D40237659C251811CEC9c364ef91dC08D300C', + data: { + kind: 'spark_fi_health_factor', + chainId: 1, + healthFactor: '3.4', + threshold: '5.5', + }, + id: 'd73df14d-ce73-4f38-bad3-ab028154042s', + trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042s', + tx_hash: + '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', + unread: true, + }; + + return mockNotification; +} + /** * Mocking Utility - creates an array of raw on-chain notifications * @returns Array of raw on-chain notifications diff --git a/packages/notification-services-controller/src/NotificationServicesController/constants/notification-schema.ts b/packages/notification-services-controller/src/NotificationServicesController/constants/notification-schema.ts index 0e7d3a064e..f5df92a052 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/constants/notification-schema.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/constants/notification-schema.ts @@ -18,6 +18,12 @@ export enum TRIGGER_TYPES { ERC721_RECEIVED = 'erc721_received', ERC1155_SENT = 'erc1155_sent', ERC1155_RECEIVED = 'erc1155_received', + AAVE_V3_HEALTH_FACTOR = 'aave_v3_health_factor', + ENS_EXPIRATION = 'ens_expiration', + LIDO_STAKING_REWARDS = 'lido_staking_rewards', + ROCKETPOOL_STAKING_REWARDS = 'rocketpool_staking_rewards', + NOTIONAL_LOAN_EXPIRATION = 'notional_loan_expiration', + SPARK_FI_HEALTH_FACTOR = 'spark_fi_health_factor', } export const TRIGGER_TYPES_WALLET_SET: Set = new Set([ diff --git a/packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.test.ts b/packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.test.ts index 55b1b85183..b866545205 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.test.ts @@ -1,7 +1,10 @@ import { createMockFeatureAnnouncementAPIResult } from '../__fixtures__/mock-feature-announcements'; import { mockFetchFeatureAnnouncementNotifications } from '../__fixtures__/mockServices'; import { TRIGGER_TYPES } from '../constants/notification-schema'; -import { getFeatureAnnouncementNotifications } from './feature-announcements'; +import { + getFeatureAnnouncementNotifications, + getFeatureAnnouncementUrl, +} from './feature-announcements'; // Mocked type for testing, allows overwriting TS to test erroneous values // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -95,3 +98,22 @@ describe('Feature Announcement Notifications', () => { expect(resultNotification.data).toBeDefined(); }); }); + +describe('getFeatureAnnouncementUrl', () => { + it('should construct the correct URL for the default domain', () => { + const url = getFeatureAnnouncementUrl(featureAnnouncementsEnv); + expect(url).toBe( + `https://cdn.contentful.com/spaces/:space_id/environments/master/entries?access_token=:access_token&content_type=productAnnouncement&include=10&fields.clients=extension`, + ); + }); + + it('should construct the correct URL for the preview domain', () => { + const url = getFeatureAnnouncementUrl( + featureAnnouncementsEnv, + ':preview_token', + ); + expect(url).toBe( + `https://preview.contentful.com/spaces/:space_id/environments/master/entries?access_token=:preview_token&content_type=productAnnouncement&include=10&fields.clients=extension`, + ); + }); +}); diff --git a/packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.ts b/packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.ts index fdfa40a824..8ed2963e7c 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/services/feature-announcements.ts @@ -19,7 +19,9 @@ import type { INotification } from '../types/notification/notification'; const DEFAULT_SPACE_ID = ':space_id'; const DEFAULT_ACCESS_TOKEN = ':access_token'; const DEFAULT_CLIENT_ID = ':client_id'; -export const FEATURE_ANNOUNCEMENT_API = `https://cdn.contentful.com/spaces/${DEFAULT_SPACE_ID}/environments/master/entries`; +const DEFAULT_DOMAIN = 'cdn.contentful.com'; +const PREVIEW_DOMAIN = 'preview.contentful.com'; +export const FEATURE_ANNOUNCEMENT_API = `https://${DEFAULT_DOMAIN}/spaces/${DEFAULT_SPACE_ID}/environments/master/entries`; export const FEATURE_ANNOUNCEMENT_URL = `${FEATURE_ANNOUNCEMENT_API}?access_token=${DEFAULT_ACCESS_TOKEN}&content_type=productAnnouncement&include=10&fields.clients=${DEFAULT_CLIENT_ID}`; type Env = { @@ -41,15 +43,19 @@ export type ContentfulResult = { items?: TypeFeatureAnnouncement[]; }; -const getFeatureAnnouncementUrl = (env: Env) => - FEATURE_ANNOUNCEMENT_URL.replace(DEFAULT_SPACE_ID, env.spaceId) - .replace(DEFAULT_ACCESS_TOKEN, env.accessToken) - .replace(DEFAULT_CLIENT_ID, env.platform); +export const getFeatureAnnouncementUrl = (env: Env, previewToken?: string) => { + const domain = previewToken ? PREVIEW_DOMAIN : DEFAULT_DOMAIN; + return FEATURE_ANNOUNCEMENT_URL.replace(DEFAULT_SPACE_ID, env.spaceId) + .replace(DEFAULT_ACCESS_TOKEN, previewToken || env.accessToken) + .replace(DEFAULT_CLIENT_ID, env.platform) + .replace(DEFAULT_DOMAIN, domain); +}; const fetchFeatureAnnouncementNotifications = async ( env: Env, + previewToken?: string, ): Promise => { - const url = getFeatureAnnouncementUrl(env); + const url = getFeatureAnnouncementUrl(env, previewToken); const data = await fetch(url) .then((r) => r.json()) @@ -144,13 +150,18 @@ const fetchFeatureAnnouncementNotifications = async ( /** * Gets Feature Announcement from our services * @param env - environment for feature announcements + * @param previewToken - the preview token to use if needed * @returns Raw Feature Announcements */ export async function getFeatureAnnouncementNotifications( env: Env, + previewToken?: string, ): Promise { if (env?.accessToken && env?.spaceId && env?.platform) { - const rawNotifications = await fetchFeatureAnnouncementNotifications(env); + const rawNotifications = await fetchFeatureAnnouncementNotifications( + env, + previewToken, + ); const notifications = rawNotifications.map((notification) => processFeatureAnnouncement(notification), ); diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/on-chain-notification.ts b/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/on-chain-notification.ts index c37c8fca78..82ec90ff45 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/on-chain-notification.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/on-chain-notification.ts @@ -24,6 +24,19 @@ export type Data_ERC20Received = components['schemas']['Data_ERC20Received']; export type Data_ERC721Sent = components['schemas']['Data_ERC721Sent']; export type Data_ERC721Received = components['schemas']['Data_ERC721Received']; +// Web3Notifications +export type Data_AaveV3HealthFactor = + components['schemas']['Data_AaveV3HealthFactor']; +export type Data_EnsExpiration = components['schemas']['Data_EnsExpiration']; +export type Data_LidoStakingRewards = + components['schemas']['Data_LidoStakingRewards']; +export type Data_RocketpoolStakingRewards = + components['schemas']['Data_RocketpoolStakingRewards']; +export type Data_NotionalLoanExpiration = + components['schemas']['Data_NotionalLoanExpiration']; +export type Data_SparkFiHealthFactor = + components['schemas']['Data_SparkFiHealthFactor']; + type Notification = components['schemas']['Notification']; type NotificationDataKinds = NonNullable['kind']; type ConvertToEnum = { diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/schema.ts b/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/schema.ts index 71dea69d34..59a2359ec4 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/schema.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/on-chain-notification/schema.ts @@ -5,8 +5,21 @@ * Script: `npx openapi-typescript -o ./schema.d.ts` */ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + export type paths = { '/api/v1/notifications': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; /** List all notifications ordered by most recent */ post: { parameters: { @@ -16,6 +29,9 @@ export type paths = { /** @description Number of notifications per page for pagination */ per_page?: number; }; + header?: never; + path?: never; + cookie?: never; }; requestBody?: { content: { @@ -30,16 +46,38 @@ export type paths = { responses: { /** @description Successfully fetched a list of notifications */ 200: { + headers: { + [name: string]: unknown; + }; content: { 'application/json': components['schemas']['Notification'][]; }; }; }; }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; '/api/v1/notifications/mark-as-read': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; /** Mark notifications as read */ post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; requestBody: { content: { 'application/json': { @@ -50,17 +88,290 @@ export type paths = { responses: { /** @description Successfully marked notifications as read */ 200: { - content: never; + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/internal/v1/topics': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all topics created (internal) */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully fetched all topics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Topic'][]; + }; + }; + }; + }; + put?: never; + /** Create a new topic (internal) */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name: string; + desc?: string; + }; + }; + }; + responses: { + /** @description Successfully created a new topic */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/internal/v1/subtopics': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all sub-topics created (internal) */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully fetched all subtopics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SubTopic'][]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/internal/v1/global-notifications': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Insert a new Global Notification (internal) */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['GlobalNotificationWrite']; + }; + }; + responses: { + /** @description Successfully created a new global notification */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/global-notifications': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all Global Notifications for a UserID */ + get: { + parameters: { + query: { + /** @description Platform(s) to filter notifications by */ + platform: ('portfolio' | 'extension' | 'mobile')[]; + /** @description Delivery channel(s) to filter notifications by */ + deliveryChannel: ('inbox' | 'push')[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully fetched global notifications */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GlobalNotification'][]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/user-preferences': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all preferences for a UserID */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully fetched preferences */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Topic'][]; + }; + }; + /** @description User not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + /** Update Preferences for a UserID */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + topics: string[]; + }; + }; + }; + responses: { + /** @description Successfully updated topics preferences */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; }; }; }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; }; - export type webhooks = Record; - export type components = { schemas: { + GlobalNotification: { + title: string; + body: string; + /** Format: date-time */ + created_at: string; + }; + GlobalNotificationWrite: { + title: string; + body: string; + 'sub-topic': string; + platforms: ('portfolio' | 'extension' | 'mobile')[]; + delivery_channels: ('inbox' | 'push')[]; + }; + Topic: { + name: string; + description?: string; + /** Format: date-time */ + created_at?: string; + }; + SubTopic: { + name: string; + /** Format: date-time */ + created_at?: string; + }; Notification: { /** Format: uuid */ id: string; @@ -69,14 +380,13 @@ export type components = { /** @example 1 */ chain_id: number; /** @example 17485840 */ - block_number: number; - block_timestamp: string; + block_number?: number; + block_timestamp?: string; /** * Format: address - * * @example 0x881D40237659C251811CEC9c364ef91dC08D300C */ - tx_hash: string; + tx_hash?: string; /** @example false */ unread: boolean; /** Format: date-time */ @@ -98,7 +408,13 @@ export type components = { | components['schemas']['Data_ERC721Sent'] | components['schemas']['Data_ERC721Received'] | components['schemas']['Data_ERC1155Sent'] - | components['schemas']['Data_ERC1155Received']; + | components['schemas']['Data_ERC1155Received'] + | components['schemas']['Data_AaveV3HealthFactor'] + | components['schemas']['Data_EnsExpiration'] + | components['schemas']['Data_LidoStakingRewards'] + | components['schemas']['Data_RocketpoolStakingRewards'] + | components['schemas']['Data_NotionalLoanExpiration'] + | components['schemas']['Data_SparkFiHealthFactor']; }; Data_MetamaskSwapCompleted: { /** @enum {string} */ @@ -241,6 +557,79 @@ export type components = { to: string; nft?: components['schemas']['NFT']; }; + Data_AaveV3HealthFactor: { + /** @enum {string} */ + kind: 'aave_v3_health_factor'; + /** @example 1 */ + chainId: number; + /** Format: decimal */ + healthFactor: string; + /** Format: decimal */ + threshold: string; + }; + Data_EnsExpiration: { + /** @enum {string} */ + kind: 'ens_expiration'; + chainId: number; + reverseEnsName: string; + /** Format: date-time */ + expirationDateIso: string; + /** @example 86400 */ + reminderDelayInSeconds: number; + }; + Data_LidoStakingRewards: { + /** @enum {string} */ + kind: 'lido_staking_rewards'; + chainId: number; + /** Format: decimal */ + currentStethBalance: string; + /** Format: decimal */ + currentEthValue: string; + /** Format: decimal */ + estimatedTotalRewardInPeriod: string; + /** @example 1 */ + daysSinceLastNotification: number; + /** @example 1 */ + notificationIntervalDays: number; + }; + Data_NotionalLoanExpiration: { + /** @enum {string} */ + kind: 'notional_loan_expiration'; + chainId: number; + loans: { + /** Format: decimal */ + amount: string; + symbol: string; + /** Format: date-time */ + maturityDateIso: string; + }[]; + /** @example 86400 */ + reminderDelayInSeconds: number; + }; + Data_RocketpoolStakingRewards: { + /** @enum {string} */ + kind: 'rocketpool_staking_rewards'; + chainId: number; + /** Format: decimal */ + currentRethBalance: string; + /** Format: decimal */ + currentEthValue: string; + /** Format: decimal */ + estimatedTotalRewardInPeriod: string; + /** @example 1 */ + daysSinceLastNotification: number; + /** @example 1 */ + notificationIntervalDays: number; + }; + Data_SparkFiHealthFactor: { + /** @enum {string} */ + kind: 'spark_fi_health_factor'; + chainId: number; + /** Format: decimal */ + healthFactor: string; + /** Format: decimal */ + threshold: string; + }; NetworkFee: { /** Format: decimal */ gas_price: string; @@ -299,6 +688,4 @@ export type components = { export type $defs = Record; -export type external = Record; - export type operations = Record; diff --git a/packages/phishing-controller/package.json b/packages/phishing-controller/package.json index 04af46d7db..ee46f85113 100644 --- a/packages/phishing-controller/package.json +++ b/packages/phishing-controller/package.json @@ -51,7 +51,6 @@ "@metamask/controller-utils": "^11.3.0", "@noble/hashes": "^1.4.0", "@types/punycode": "^2.1.0", - "eth-phishing-detect": "^1.2.0", "ethereum-cryptography": "^2.1.2", "fastest-levenshtein": "^1.0.16", "punycode": "^2.1.1" diff --git a/packages/polling-controller/src/AbstractPollingController.ts b/packages/polling-controller/src/AbstractPollingController.ts index 87945d56cd..d52aab938a 100644 --- a/packages/polling-controller/src/AbstractPollingController.ts +++ b/packages/polling-controller/src/AbstractPollingController.ts @@ -1,4 +1,3 @@ -import type { NetworkClientId } from '@metamask/network-controller'; import type { Json } from '@metamask/utils'; import stringify from 'fast-json-stable-stringify'; import { v4 as random } from 'uuid'; @@ -9,12 +8,8 @@ import type { IPollingController, } from './types'; -export const getKey = ( - networkClientId: NetworkClientId, - options: Json, - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions -): PollingTokenSetId => `${networkClientId}:${stringify(options)}`; +export const getKey = (input: PollingInput): PollingTokenSetId => + stringify(input); /** * AbstractPollingControllerBaseMixin @@ -24,45 +19,35 @@ export const getKey = ( */ // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/naming-convention -export function AbstractPollingControllerBaseMixin( - Base: TBase, -) { +export function AbstractPollingControllerBaseMixin< + TBase extends Constructor, + PollingInput extends Json, +>(Base: TBase) { abstract class AbstractPollingControllerBase extends Base - implements IPollingController + implements IPollingController { readonly #pollingTokenSets: Map> = new Map(); - #callbacks: Map< - PollingTokenSetId, - Set<(PollingTokenSetId: PollingTokenSetId) => void> - > = new Map(); + #callbacks: Map void>> = + new Map(); - abstract _executePoll( - networkClientId: NetworkClientId, - options: Json, - ): Promise; + abstract _executePoll(input: PollingInput): Promise; - abstract _startPollingByNetworkClientId( - networkClientId: NetworkClientId, - options: Json, - ): void; + abstract _startPolling(input: PollingInput): void; abstract _stopPollingByPollingTokenSetId(key: PollingTokenSetId): void; - startPollingByNetworkClientId( - networkClientId: NetworkClientId, - options: Json = {}, - ): string { + startPolling(input: PollingInput): string { const pollToken = random(); - const key = getKey(networkClientId, options); + const key = getKey(input); const pollingTokenSet = this.#pollingTokenSets.get(key) ?? new Set(); pollingTokenSet.add(pollToken); this.#pollingTokenSets.set(key, pollingTokenSet); if (pollingTokenSet.size === 1) { - this._startPollingByNetworkClientId(networkClientId, options); + this._startPolling(input); } return pollToken; @@ -98,19 +83,18 @@ export function AbstractPollingControllerBaseMixin( if (callbacks) { for (const callback of callbacks) { // eslint-disable-next-line n/callback-return - callback(keyToDelete); + callback(JSON.parse(keyToDelete)); } callbacks.clear(); } } } - onPollingCompleteByNetworkClientId( - networkClientId: NetworkClientId, - callback: (networkClientId: NetworkClientId) => void, - options: Json = {}, + onPollingComplete( + input: PollingInput, + callback: (input: PollingInput) => void, ) { - const key = getKey(networkClientId, options); + const key = getKey(input); const callbacks = this.#callbacks.get(key) ?? new Set(); callbacks.add(callback); this.#callbacks.set(key, callbacks); diff --git a/packages/polling-controller/src/BlockTrackerPollingController.test.ts b/packages/polling-controller/src/BlockTrackerPollingController.test.ts index 2ddba4edab..90192e5049 100644 --- a/packages/polling-controller/src/BlockTrackerPollingController.test.ts +++ b/packages/polling-controller/src/BlockTrackerPollingController.test.ts @@ -3,6 +3,7 @@ import type { NetworkClient } from '@metamask/network-controller'; import EventEmitter from 'events'; import { useFakeTimers } from 'sinon'; +import type { BlockTrackerPollingInput } from './BlockTrackerPollingController'; import { BlockTrackerPollingController } from './BlockTrackerPollingController'; const createExecutePollMock = () => { @@ -13,7 +14,7 @@ const createExecutePollMock = () => { }; let getNetworkClientByIdStub: jest.Mock; -class ChildBlockTrackerPollingController extends BlockTrackerPollingController< +class ChildBlockTrackerPollingController extends BlockTrackerPollingController()< // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any any, @@ -45,9 +46,7 @@ describe('BlockTrackerPollingController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockMessenger: any; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let controller: any; + let controller: ChildBlockTrackerPollingController; let mainnetBlockTracker: TestBlockTracker; let goerliBlockTracker: TestBlockTracker; let sepoliaBlockTracker: TestBlockTracker; @@ -92,29 +91,30 @@ describe('BlockTrackerPollingController', () => { clock.restore(); }); - describe('startPollingByNetworkClientId', () => { - it('should call _executePoll on "latest" block events emitted by blockTrackers for each networkClientId passed to startPollingByNetworkClientId', async () => { - controller.startPollingByNetworkClientId('mainnet'); - controller.startPollingByNetworkClientId('goerli'); + describe('startPolling', () => { + it('should call _executePoll on "latest" block events emitted by blockTrackers for each networkClientId passed to startPolling', async () => { + controller.startPolling({ networkClientId: 'mainnet' }); + controller.startPolling({ networkClientId: 'goerli' }); // await advanceTime({ clock, duration: 5 }); mainnetBlockTracker.emitBlockEvent(); expect(controller._executePoll).toHaveBeenCalledTimes(1); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll).toHaveBeenCalledWith( + { networkClientId: 'mainnet' }, + 1, + ); mainnetBlockTracker.emitBlockEvent(); goerliBlockTracker.emitBlockEvent(); expect(controller._executePoll).toHaveBeenNthCalledWith( 2, - 'mainnet', - {}, + { networkClientId: 'mainnet' }, 2, // 2nd block for mainnet ); expect(controller._executePoll).toHaveBeenNthCalledWith( 3, - 'goerli', - {}, + { networkClientId: 'goerli' }, 1, // 1st block for goerli ); @@ -126,32 +126,28 @@ describe('BlockTrackerPollingController', () => { expect(controller._executePoll).toHaveBeenNthCalledWith( 4, - 'mainnet', - {}, + { networkClientId: 'mainnet' }, 3, ); expect(controller._executePoll).toHaveBeenNthCalledWith( 5, - 'goerli', - {}, + { networkClientId: 'goerli' }, 2, ); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ networkClientId: 'sepolia' }); mainnetBlockTracker.emitBlockEvent(); sepoliaBlockTracker.emitBlockEvent(); expect(controller._executePoll).toHaveBeenNthCalledWith( 6, - 'mainnet', - {}, + { networkClientId: 'mainnet' }, 4, ); expect(controller._executePoll).toHaveBeenNthCalledWith( 7, - 'sepolia', - {}, + { networkClientId: 'sepolia' }, 2, ); @@ -161,21 +157,28 @@ describe('BlockTrackerPollingController', () => { describe('stopPollingByPollingToken', () => { it('should should stop polling when all polling tokens for a networkClientId are deleted', async () => { - const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken1 = controller.startPolling({ + networkClientId: 'mainnet', + }); // await advanceTime({ clock, duration: 5 }); mainnetBlockTracker.emitBlockEvent(); expect(controller._executePoll).toHaveBeenCalledTimes(1); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll).toHaveBeenCalledWith( + { networkClientId: 'mainnet' }, + 1, + ); - const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken2 = controller.startPolling({ + networkClientId: 'mainnet', + }); mainnetBlockTracker.emitBlockEvent(); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], ]); controller.stopPollingByPollingToken(pollingToken1); @@ -184,9 +187,9 @@ describe('BlockTrackerPollingController', () => { // polling is still active for mainnet because pollingToken2 is still active expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], ]); controller.stopPollingByPollingToken(pollingToken2); @@ -198,37 +201,44 @@ describe('BlockTrackerPollingController', () => { // no further polling should occur regardless of how many blocks are emitted // because all pollingTokens for mainnet have been deleted expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], ]); }); it('should should stop polling for one networkClientId when all polling tokens for that networkClientId are deleted, without stopping polling for networkClientIds with active pollingTokens', async () => { - const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken1 = controller.startPolling({ + networkClientId: 'mainnet', + }); mainnetBlockTracker.emitBlockEvent(); - expect(controller._executePoll).toHaveBeenCalledWith('mainnet', {}, 1); + expect(controller._executePoll).toHaveBeenCalledWith( + { networkClientId: 'mainnet' }, + 1, + ); - const pollingToken2 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken2 = controller.startPolling({ + networkClientId: 'mainnet', + }); mainnetBlockTracker.emitBlockEvent(); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], ]); - controller.startPollingByNetworkClientId('goerli'); + controller.startPolling({ networkClientId: 'goerli' }); mainnetBlockTracker.emitBlockEvent(); // we are polling for mainnet and goerli but goerli has not emitted any blocks yet expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], ]); controller.stopPollingByPollingToken(pollingToken1); @@ -237,11 +247,11 @@ describe('BlockTrackerPollingController', () => { goerliBlockTracker.emitBlockEvent(); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 1], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], + [{ networkClientId: 'mainnet' }, 4], + [{ networkClientId: 'goerli' }, 1], ]); controller.stopPollingByPollingToken(pollingToken2); @@ -254,13 +264,13 @@ describe('BlockTrackerPollingController', () => { // no further polling for mainnet should occur expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}, 1], - ['mainnet', {}, 2], - ['mainnet', {}, 3], - ['mainnet', {}, 4], - ['goerli', {}, 1], - ['goerli', {}, 2], - ['goerli', {}, 3], + [{ networkClientId: 'mainnet' }, 1], + [{ networkClientId: 'mainnet' }, 2], + [{ networkClientId: 'mainnet' }, 3], + [{ networkClientId: 'mainnet' }, 4], + [{ networkClientId: 'goerli' }, 1], + [{ networkClientId: 'goerli' }, 2], + [{ networkClientId: 'goerli' }, 3], ]); controller.stopAllPolling(); @@ -272,11 +282,18 @@ describe('BlockTrackerPollingController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const pollingComplete: any = jest.fn(); - controller.onPollingCompleteByNetworkClientId('mainnet', pollingComplete); - const pollingToken = controller.startPollingByNetworkClientId('mainnet'); + controller.onPollingComplete( + { networkClientId: 'mainnet' }, + pollingComplete, + ); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); controller.stopPollingByPollingToken(pollingToken); expect(pollingComplete).toHaveBeenCalledTimes(1); - expect(pollingComplete).toHaveBeenCalledWith('mainnet:{}'); + expect(pollingComplete).toHaveBeenCalledWith({ + networkClientId: 'mainnet', + }); }); }); }); diff --git a/packages/polling-controller/src/BlockTrackerPollingController.ts b/packages/polling-controller/src/BlockTrackerPollingController.ts index 60f6e1fdcc..cb97c5511e 100644 --- a/packages/polling-controller/src/BlockTrackerPollingController.ts +++ b/packages/polling-controller/src/BlockTrackerPollingController.ts @@ -11,6 +11,14 @@ import { } from './AbstractPollingController'; import type { Constructor, PollingTokenSetId } from './types'; +/** + * The minimum input required to start polling for a {@link BlockTrackerPollingController}. + * Implementing classes may provide additional properties. + */ +export type BlockTrackerPollingInput = { + networkClientId: NetworkClientId; +}; + /** * BlockTrackerPollingControllerMixin * A polling controller that polls using a block tracker. @@ -20,35 +28,30 @@ import type { Constructor, PollingTokenSetId } from './types'; */ // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/naming-convention -function BlockTrackerPollingControllerMixin( - Base: TBase, -) { - abstract class BlockTrackerPollingController extends AbstractPollingControllerBaseMixin( - Base, - ) { +function BlockTrackerPollingControllerMixin< + TBase extends Constructor, + PollingInput extends BlockTrackerPollingInput, +>(Base: TBase) { + abstract class BlockTrackerPollingController extends AbstractPollingControllerBaseMixin< + TBase, + PollingInput + >(Base) { #activeListeners: Record Promise> = {}; abstract _getNetworkClientById( networkClientId: NetworkClientId, ): NetworkClient | undefined; - _startPollingByNetworkClientId( - networkClientId: NetworkClientId, - options: Json, - ) { - const key = getKey(networkClientId, options); + _startPolling(input: PollingInput) { + const key = getKey(input); if (this.#activeListeners[key]) { return; } - const networkClient = this._getNetworkClientById(networkClientId); + const networkClient = this._getNetworkClientById(input.networkClientId); if (networkClient) { - const updateOnNewBlock = this._executePoll.bind( - this, - networkClientId, - options, - ); + const updateOnNewBlock = this._executePoll.bind(this, input); // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-misused-promises networkClient.blockTracker.addListener('latest', updateOnNewBlock); @@ -57,13 +60,13 @@ function BlockTrackerPollingControllerMixin( throw new Error( // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Unable to retrieve blockTracker for networkClientId ${networkClientId}`, + `Unable to retrieve blockTracker for networkClientId ${input.networkClientId}`, ); } } _stopPollingByPollingTokenSetId(key: PollingTokenSetId) { - const [networkClientId] = key.split(':'); + const { networkClientId } = JSON.parse(key); const networkClient = this._getNetworkClientById( networkClientId as NetworkClientId, ); @@ -85,9 +88,20 @@ function BlockTrackerPollingControllerMixin( class Empty {} -export const BlockTrackerPollingControllerOnly = - BlockTrackerPollingControllerMixin(Empty); -export const BlockTrackerPollingController = - BlockTrackerPollingControllerMixin(BaseController); -export const BlockTrackerPollingControllerV1 = - BlockTrackerPollingControllerMixin(BaseControllerV1); +export const BlockTrackerPollingControllerOnly = < + PollingInput extends BlockTrackerPollingInput, +>() => BlockTrackerPollingControllerMixin(Empty); + +export const BlockTrackerPollingController = < + PollingInput extends BlockTrackerPollingInput, +>() => + BlockTrackerPollingControllerMixin( + BaseController, + ); + +export const BlockTrackerPollingControllerV1 = < + PollingInput extends BlockTrackerPollingInput, +>() => + BlockTrackerPollingControllerMixin( + BaseControllerV1, + ); diff --git a/packages/polling-controller/src/StaticIntervalPollingController.test.ts b/packages/polling-controller/src/StaticIntervalPollingController.test.ts index 2238fbc111..b166b90a79 100644 --- a/packages/polling-controller/src/StaticIntervalPollingController.test.ts +++ b/packages/polling-controller/src/StaticIntervalPollingController.test.ts @@ -7,7 +7,12 @@ import { StaticIntervalPollingController } from './StaticIntervalPollingControll const TICK_TIME = 5; -class ChildBlockTrackerPollingController extends StaticIntervalPollingController< +type PollingInput = { + networkClientId: string; + address?: string; +}; + +class ChildBlockTrackerPollingController extends StaticIntervalPollingController()< // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any any, @@ -37,9 +42,7 @@ describe('StaticIntervalPollingController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockMessenger: any; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let controller: any; + let controller: ChildBlockTrackerPollingController; beforeEach(() => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -58,9 +61,9 @@ describe('StaticIntervalPollingController', () => { clock.restore(); }); - describe('startPollingByNetworkClientId', () => { + describe('startPolling', () => { it('should start polling if not already polling', async () => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); controller.executePollPromises[0].resolve(); @@ -70,10 +73,10 @@ describe('StaticIntervalPollingController', () => { }); it('should call _executePoll immediately once and continue calling _executePoll on interval when called again with the same networkClientId', async () => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); @@ -89,15 +92,19 @@ describe('StaticIntervalPollingController', () => { describe('multiple networkClientIds', () => { it('should poll for each networkClientId', async () => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('rinkeby'); + controller.startPolling({ + networkClientId: 'rinkeby', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['rinkeby', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], ]); controller.executePollPromises[0].resolve(); @@ -105,10 +112,10 @@ describe('StaticIntervalPollingController', () => { await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['rinkeby', {}], - ['mainnet', {}], - ['rinkeby', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], ]); controller.executePollPromises[2].resolve(); @@ -116,75 +123,79 @@ describe('StaticIntervalPollingController', () => { await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['rinkeby', {}], - ['mainnet', {}], - ['rinkeby', {}], - ['mainnet', {}], - ['rinkeby', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'rinkeby' }], ]); controller.stopAllPolling(); }); it('should poll multiple networkClientIds when setting interval length', async () => { controller.setIntervalLength(TICK_TIME * 2); - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], + [{ networkClientId: 'mainnet' }], ]); controller.executePollPromises[0].resolve(); await advanceTime({ clock, duration: TICK_TIME }); - controller.startPollingByNetworkClientId('sepolia'); + controller.startPolling({ + networkClientId: 'sepolia', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], ]); controller.executePollPromises[1].resolve(); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], ]); controller.executePollPromises[2].resolve(); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], - ['sepolia', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], ]); controller.executePollPromises[3].resolve(); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], ]); controller.executePollPromises[4].resolve(); await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], - ['sepolia', {}], - ['mainnet', {}], - ['sepolia', {}], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], + [{ networkClientId: 'mainnet' }], + [{ networkClientId: 'sepolia' }], ]); }); }); @@ -192,7 +203,9 @@ describe('StaticIntervalPollingController', () => { describe('stopPollingByPollingToken', () => { it('should stop polling when called with a valid polling that was the only active pollingToken for a given networkClient', async () => { - const pollingToken = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); controller.executePollPromises[0].resolve(); @@ -204,10 +217,12 @@ describe('StaticIntervalPollingController', () => { }); it('should not stop polling if called with one of multiple active polling tokens for a given networkClient', async () => { - const pollingToken1 = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken1 = controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); expect(controller._executePoll).toHaveBeenCalledTimes(1); controller.executePollPromises[0].resolve(); await advanceTime({ clock, duration: TICK_TIME }); @@ -219,28 +234,35 @@ describe('StaticIntervalPollingController', () => { }); it('should error if no pollingToken is passed', () => { - controller.startPollingByNetworkClientId('mainnet'); + controller.startPolling({ networkClientId: 'mainnet' }); expect(() => { - controller.stopPollingByPollingToken(); + controller.stopPollingByPollingToken(''); }).toThrow('pollingToken required'); controller.stopAllPolling(); }); it('should start and stop polling sessions for different networkClientIds with the same options', async () => { - const pollToken1 = controller.startPollingByNetworkClientId('mainnet', { + const pollToken1 = controller.startPolling({ + networkClientId: 'mainnet', address: '0x1', }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('mainnet', { address: '0x2' }); + controller.startPolling({ + networkClientId: 'mainnet', + address: '0x2', + }); await advanceTime({ clock, duration: 0 }); - controller.startPollingByNetworkClientId('sepolia', { address: '0x2' }); + controller.startPolling({ + networkClientId: 'sepolia', + address: '0x2', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], ]); controller.executePollPromises[0].resolve(); @@ -249,12 +271,12 @@ describe('StaticIntervalPollingController', () => { await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], ]); controller.stopPollingByPollingToken(pollToken1); controller.executePollPromises[3].resolve(); @@ -263,19 +285,21 @@ describe('StaticIntervalPollingController', () => { await advanceTime({ clock, duration: TICK_TIME }); expect(controller._executePoll.mock.calls).toMatchObject([ - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], - ['mainnet', { address: '0x1' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], - ['mainnet', { address: '0x2' }], - ['sepolia', { address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x1' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], + [{ networkClientId: 'mainnet', address: '0x2' }], + [{ networkClientId: 'sepolia', address: '0x2' }], ]); }); it('should stop polling session after current iteration if stop is requested while current iteration is still executing', async () => { - const pollingToken = controller.startPollingByNetworkClientId('mainnet'); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); await advanceTime({ clock, duration: 0 }); expect(controller._executePoll).toHaveBeenCalledTimes(1); controller.stopPollingByPollingToken(pollingToken); @@ -293,11 +317,18 @@ describe('StaticIntervalPollingController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const pollingComplete: any = jest.fn(); - controller.onPollingCompleteByNetworkClientId('mainnet', pollingComplete); - const pollingToken = controller.startPollingByNetworkClientId('mainnet'); + controller.onPollingComplete( + { networkClientId: 'mainnet' }, + pollingComplete, + ); + const pollingToken = controller.startPolling({ + networkClientId: 'mainnet', + }); controller.stopPollingByPollingToken(pollingToken); expect(pollingComplete).toHaveBeenCalledTimes(1); - expect(pollingComplete).toHaveBeenCalledWith('mainnet:{}'); + expect(pollingComplete).toHaveBeenCalledWith({ + networkClientId: 'mainnet', + }); }); }); }); diff --git a/packages/polling-controller/src/StaticIntervalPollingController.ts b/packages/polling-controller/src/StaticIntervalPollingController.ts index a4e4fd2e84..53493601fa 100644 --- a/packages/polling-controller/src/StaticIntervalPollingController.ts +++ b/packages/polling-controller/src/StaticIntervalPollingController.ts @@ -1,5 +1,4 @@ import { BaseController, BaseControllerV1 } from '@metamask/base-controller'; -import type { NetworkClientId } from '@metamask/network-controller'; import type { Json } from '@metamask/utils'; import { @@ -21,12 +20,13 @@ import type { */ // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/naming-convention -function StaticIntervalPollingControllerMixin( - Base: TBase, -) { +function StaticIntervalPollingControllerMixin< + TBase extends Constructor, + PollingInput extends Json, +>(Base: TBase) { abstract class StaticIntervalPollingController - extends AbstractPollingControllerBaseMixin(Base) - implements IPollingController + extends AbstractPollingControllerBaseMixin(Base) + implements IPollingController { readonly #intervalIds: Record = {}; @@ -40,15 +40,12 @@ function StaticIntervalPollingControllerMixin( return this.#intervalLength; } - _startPollingByNetworkClientId( - networkClientId: NetworkClientId, - options: Json, - ) { + _startPolling(input: PollingInput) { if (!this.#intervalLength) { throw new Error('intervalLength must be defined and greater than 0'); } - const key = getKey(networkClientId, options); + const key = getKey(input); const existingInterval = this.#intervalIds[key]; this._stopPollingByPollingTokenSetId(key); @@ -58,12 +55,12 @@ function StaticIntervalPollingControllerMixin( // eslint-disable-next-line @typescript-eslint/no-misused-promises async () => { try { - await this._executePoll(networkClientId, options); + await this._executePoll(input); } catch (error) { console.error(error); } if (intervalId === this.#intervalIds[key]) { - this._startPollingByNetworkClientId(networkClientId, options); + this._startPolling(input); } }, existingInterval ? this.#intervalLength : 0, @@ -84,9 +81,18 @@ function StaticIntervalPollingControllerMixin( class Empty {} -export const StaticIntervalPollingControllerOnly = - StaticIntervalPollingControllerMixin(Empty); -export const StaticIntervalPollingController = - StaticIntervalPollingControllerMixin(BaseController); -export const StaticIntervalPollingControllerV1 = - StaticIntervalPollingControllerMixin(BaseControllerV1); +export const StaticIntervalPollingControllerOnly = < + PollingInput extends Json, +>() => StaticIntervalPollingControllerMixin(Empty); + +export const StaticIntervalPollingController = () => + StaticIntervalPollingControllerMixin( + BaseController, + ); + +export const StaticIntervalPollingControllerV1 = < + PollingInput extends Json, +>() => + StaticIntervalPollingControllerMixin( + BaseControllerV1, + ); diff --git a/packages/polling-controller/src/types.ts b/packages/polling-controller/src/types.ts index c7848658ca..2a1f88476d 100644 --- a/packages/polling-controller/src/types.ts +++ b/packages/polling-controller/src/types.ts @@ -1,29 +1,21 @@ -import type { NetworkClientId } from '@metamask/network-controller'; import type { Json } from '@metamask/utils'; -export type PollingTokenSetId = `${NetworkClientId}:${string}`; +export type PollingTokenSetId = string; -export type IPollingController = { - startPollingByNetworkClientId( - networkClientId: NetworkClientId, - options: Json, - ): string; +export type IPollingController = { + startPolling(input: PollingInput): string; stopAllPolling(): void; stopPollingByPollingToken(pollingToken: string): void; - onPollingCompleteByNetworkClientId( - networkClientId: NetworkClientId, - callback: (networkClientId: NetworkClientId) => void, - options: Json, + onPollingComplete( + input: PollingInput, + callback: (input: PollingInput) => void, ): void; - _executePoll(networkClientId: NetworkClientId, options: Json): Promise; - _startPollingByNetworkClientId( - networkClientId: NetworkClientId, - options: Json, - ): void; + _executePoll(input: PollingInput): Promise; + _startPolling(input: PollingInput): void; _stopPollingByPollingTokenSetId(key: PollingTokenSetId): void; }; diff --git a/packages/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index 7d1384c745..2df2099129 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 c519cfe53d..11cf9d248b 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 0a265267ab..1a753acd99 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 5a4deb1a5a..9020e24cca 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 e2de8f7132..a3149f8dbe 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 1613868760..f5f12a1b7e 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 c545fcf825..ef14dc2c7c 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 9794824e5e..1345ec2034 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 30da94b89c..af861e8bae 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 b308fe868e..9eb303beab 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 83c4f50eff..d527982f76 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/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js index a7cd29f672..c618be1554 100644 --- a/packages/transaction-controller/jest.config.js +++ b/packages/transaction-controller/jest.config.js @@ -18,9 +18,9 @@ module.exports = merge(baseConfig, { coverageThreshold: { global: { branches: 94.42, - functions: 97.46, - lines: 98.44, - statements: 98.46, + functions: 97.45, + lines: 98.37, + statements: 98.38, }, }, diff --git a/packages/transaction-controller/src/utils/simulation.test.ts b/packages/transaction-controller/src/utils/simulation.test.ts index b07c3a75ea..1df0a186e8 100644 --- a/packages/transaction-controller/src/utils/simulation.test.ts +++ b/packages/transaction-controller/src/utils/simulation.test.ts @@ -1,5 +1,6 @@ import type { LogDescription } from '@ethersproject/abi'; import { Interface } from '@ethersproject/abi'; +import { type Hex } from '@metamask/utils'; import { SimulationInvalidResponseError, @@ -11,7 +12,10 @@ import { SupportedToken, type GetSimulationDataRequest, } from './simulation'; -import type { SimulationResponseLog } from './simulation-api'; +import type { + SimulationResponseLog, + SimulationResponseTransaction, +} from './simulation-api'; import { simulateTransactions, type SimulationResponse, @@ -19,19 +23,37 @@ import { jest.mock('./simulation-api'); -const USER_ADDRESS_MOCK = '0x123'; -const OTHER_ADDRESS_MOCK = '0x456'; -const CONTRACT_ADDRESS_1_MOCK = '0x789'; -const CONTRACT_ADDRESS_2_MOCK = '0xDEF'; -const BALANCE_1_MOCK = '0x0'; -const BALANCE_2_MOCK = '0x1'; -const DIFFERENCE_MOCK = '0x1'; -const VALUE_MOCK = '0x4'; -const TOKEN_ID_MOCK = '0x5'; -const OTHER_TOKEN_ID_MOCK = '0x6'; +// Utility function to encode addresses and values to 32-byte ABI format +const encodeTo32ByteHex = (value: string | number): Hex => { + // Pad to 32 bytes (64 characters) and add '0x' prefix + return `0x${BigInt(value).toString(16).padStart(64, '0')}`; +}; + +// getSimulationData returns values in hex format with leading zeros trimmed. +const trimLeadingZeros = (hexString: Hex): Hex => { + const trimmed = hexString.replace(/^0x0+/u, '0x') as Hex; + return trimmed === '0x' ? '0x0' : trimmed; +}; + +const USER_ADDRESS_MOCK = '0x1233333333333333333333333333333333333333' as Hex; +const OTHER_ADDRESS_MOCK = '0x4566666666666666666666666666666666666666' as Hex; +const CONTRACT_ADDRESS_1_MOCK = + '0x7899999999999999999999999999999999999999' as Hex; +const CONTRACT_ADDRESS_2_MOCK = + '0xDEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' as Hex; +const BALANCE_1_MOCK = '0x0' as Hex; +const BALANCE_2_MOCK = '0x1' as Hex; +const DIFFERENCE_MOCK = '0x1' as Hex; +const VALUE_MOCK = '0x4' as Hex; +const TOKEN_ID_MOCK = '0x5' as Hex; +const OTHER_TOKEN_ID_MOCK = '0x6' as Hex; const ERROR_CODE_MOCK = 123; const ERROR_MESSAGE_MOCK = 'Test Error'; +// Regression test – leading zero in user address +const USER_ADDRESS_WITH_LEADING_ZERO = + '0x0012333333333333333333333333333333333333' as Hex; + const REQUEST_MOCK: GetSimulationDataRequest = { chainId: '0x1', from: USER_ADDRESS_MOCK, @@ -87,10 +109,16 @@ const PARSED_WRAPPED_ERC20_DEPOSIT_EVENT_MOCK = { args: [USER_ADDRESS_MOCK, { toHexString: () => VALUE_MOCK }], } as unknown as LogDescription; +const defaultResponseTx: SimulationResponseTransaction = { + return: encodeTo32ByteHex('0x0'), + callTrace: { calls: [], logs: [] }, + stateDiff: { pre: {}, post: {} }, +}; + const RESPONSE_NESTED_LOGS_MOCK: SimulationResponse = { transactions: [ { - return: '0x1', + ...defaultResponseTx, callTrace: { calls: [ { @@ -105,10 +133,6 @@ const RESPONSE_NESTED_LOGS_MOCK: SimulationResponse = { ], logs: [], }, - stateDiff: { - pre: {}, - post: {}, - }, }, ], }; @@ -129,22 +153,12 @@ function createLogMock(contractAddress: string) { * @param logs - The logs. * @returns Mock API response. */ -function createEventResponseMock(logs: SimulationResponseLog[]) { +function createEventResponseMock( + logs: SimulationResponseLog[], +): SimulationResponse { return { - transactions: [ - { - return: '0x', - callTrace: { - calls: [], - logs, - }, - stateDiff: { - pre: {}, - post: {}, - }, - }, - ], - } as unknown as SimulationResponse; + transactions: [{ ...defaultResponseTx, callTrace: { calls: [], logs } }], + }; } /** @@ -160,20 +174,14 @@ function createNativeBalanceResponse( return { transactions: [ { - callTrace: { - calls: [], - logs: [], - }, + ...defaultResponseTx, + return: encodeTo32ByteHex(previousBalance), stateDiff: { pre: { - [USER_ADDRESS_MOCK]: { - balance: previousBalance, - }, + [USER_ADDRESS_MOCK]: { balance: previousBalance }, }, post: { - [USER_ADDRESS_MOCK]: { - balance: newBalance, - }, + [USER_ADDRESS_MOCK]: { balance: newBalance }, }, }, }, @@ -194,37 +202,13 @@ function createBalanceOfResponse( return { transactions: [ ...previousBalances.map((previousBalance) => ({ - return: previousBalance, - callTrace: { - calls: [], - logs: [], - }, - stateDiff: { - pre: {}, - post: {}, - }, + ...defaultResponseTx, + return: encodeTo32ByteHex(previousBalance), })), - { - return: '0xabc', - callTrace: { - calls: [], - logs: [], - }, - stateDiff: { - pre: {}, - post: {}, - }, - }, + defaultResponseTx, ...newBalances.map((newBalance) => ({ - return: newBalance, - callTrace: { - calls: [], - logs: [], - }, - stateDiff: { - pre: {}, - post: {}, - }, + ...defaultResponseTx, + return: encodeTo32ByteHex(newBalance), })), ], } as unknown as SimulationResponse; @@ -317,6 +301,7 @@ describe('Simulation Utils', () => { it.each([ { title: 'ERC-20 token', + from: USER_ADDRESS_MOCK, parsedEvent: PARSED_ERC20_TRANSFER_EVENT_MOCK, tokenType: SupportedToken.ERC20, tokenStandard: SimulationTokenStandard.erc20, @@ -326,6 +311,7 @@ describe('Simulation Utils', () => { }, { title: 'ERC-721 token', + from: USER_ADDRESS_MOCK, parsedEvent: PARSED_ERC721_TRANSFER_EVENT_MOCK, tokenType: SupportedToken.ERC721, tokenStandard: SimulationTokenStandard.erc721, @@ -333,8 +319,27 @@ describe('Simulation Utils', () => { previousBalances: [OTHER_ADDRESS_MOCK], newBalances: [USER_ADDRESS_MOCK], }, + { + // Regression test – leading zero in user address + title: 'ERC-721 token – where user address has leadding zero', + from: USER_ADDRESS_WITH_LEADING_ZERO, + parsedEvent: { + ...PARSED_ERC721_TRANSFER_EVENT_MOCK, + args: [ + OTHER_ADDRESS_MOCK, + USER_ADDRESS_WITH_LEADING_ZERO, + { toHexString: () => TOKEN_ID_MOCK }, + ], + }, + tokenType: SupportedToken.ERC721, + tokenStandard: SimulationTokenStandard.erc721, + tokenId: TOKEN_ID_MOCK, + previousBalances: [OTHER_ADDRESS_MOCK], + newBalances: [USER_ADDRESS_WITH_LEADING_ZERO], + }, { title: 'ERC-1155 token via single event', + from: USER_ADDRESS_MOCK, parsedEvent: PARSED_ERC1155_TRANSFER_SINGLE_EVENT_MOCK, tokenType: SupportedToken.ERC1155, tokenStandard: SimulationTokenStandard.erc1155, @@ -344,6 +349,7 @@ describe('Simulation Utils', () => { }, { title: 'ERC-1155 token via batch event', + from: USER_ADDRESS_MOCK, parsedEvent: PARSED_ERC1155_TRANSFER_BATCH_EVENT_MOCK, tokenType: SupportedToken.ERC1155, tokenStandard: SimulationTokenStandard.erc1155, @@ -353,6 +359,7 @@ describe('Simulation Utils', () => { }, { title: 'wrapped ERC-20 token', + from: USER_ADDRESS_MOCK, parsedEvent: PARSED_WRAPPED_ERC20_DEPOSIT_EVENT_MOCK, tokenType: SupportedToken.ERC20_WRAPPED, tokenStandard: SimulationTokenStandard.erc20, @@ -362,6 +369,7 @@ describe('Simulation Utils', () => { }, { title: 'legacy ERC-721 token', + from: USER_ADDRESS_MOCK, parsedEvent: PARSED_ERC721_TRANSFER_EVENT_MOCK, tokenType: SupportedToken.ERC721_LEGACY, tokenStandard: SimulationTokenStandard.erc721, @@ -372,6 +380,7 @@ describe('Simulation Utils', () => { ])( 'on $title', async ({ + from, parsedEvent, tokenStandard, tokenType, @@ -389,7 +398,10 @@ describe('Simulation Utils', () => { createBalanceOfResponse(previousBalances, newBalances), ); - const simulationData = await getSimulationData(REQUEST_MOCK); + const simulationData = await getSimulationData({ + chainId: '0x1', + from, + }); expect(simulationData).toStrictEqual({ nativeBalanceChange: undefined, @@ -398,8 +410,8 @@ describe('Simulation Utils', () => { standard: tokenStandard, address: CONTRACT_ADDRESS_1_MOCK, id: tokenId, - previousBalance: BALANCE_1_MOCK, - newBalance: BALANCE_2_MOCK, + previousBalance: trimLeadingZeros(BALANCE_1_MOCK), + newBalance: trimLeadingZeros(BALANCE_2_MOCK), difference: DIFFERENCE_MOCK, isDecrease: false, }, @@ -501,8 +513,8 @@ describe('Simulation Utils', () => { standard: SimulationTokenStandard.erc20, address: CONTRACT_ADDRESS_1_MOCK, id: undefined, - previousBalance: BALANCE_2_MOCK, - newBalance: BALANCE_1_MOCK, + previousBalance: trimLeadingZeros(BALANCE_2_MOCK), + newBalance: trimLeadingZeros(BALANCE_1_MOCK), difference: DIFFERENCE_MOCK, isDecrease: true, }, @@ -545,8 +557,8 @@ describe('Simulation Utils', () => { standard: SimulationTokenStandard.erc721, address: CONTRACT_ADDRESS_1_MOCK, id: TOKEN_ID_MOCK, - previousBalance: BALANCE_1_MOCK, - newBalance: BALANCE_2_MOCK, + previousBalance: trimLeadingZeros(BALANCE_1_MOCK), + newBalance: trimLeadingZeros(BALANCE_2_MOCK), difference: DIFFERENCE_MOCK, isDecrease: false, }, @@ -554,8 +566,8 @@ describe('Simulation Utils', () => { standard: SimulationTokenStandard.erc721, address: CONTRACT_ADDRESS_1_MOCK, id: OTHER_TOKEN_ID_MOCK, - previousBalance: BALANCE_1_MOCK, - newBalance: BALANCE_2_MOCK, + previousBalance: trimLeadingZeros(BALANCE_1_MOCK), + newBalance: trimLeadingZeros(BALANCE_2_MOCK), difference: DIFFERENCE_MOCK, isDecrease: false, }, @@ -738,9 +750,60 @@ describe('Simulation Utils', () => { standard: SimulationTokenStandard.erc20, address: CONTRACT_ADDRESS_1_MOCK, id: undefined, - previousBalance: BALANCE_1_MOCK, - newBalance: BALANCE_2_MOCK, - difference: DIFFERENCE_MOCK, + previousBalance: trimLeadingZeros(BALANCE_1_MOCK), + newBalance: trimLeadingZeros(BALANCE_2_MOCK), + difference: '0x1', + isDecrease: false, + }, + ], + }); + }); + + // Ensures no regression of bug https://github.com/MetaMask/metamask-extension/issues/26521 + it('decodes raw balanceOf output correctly for ERC20 token with extra zeros', async () => { + const DECODED_BALANCE_BEFORE = '0x134c31d252'; + const DECODED_BALANCE_AFTER = '0x134c31d257'; + const EXPECTED_BALANCE_CHANGE = '0x5'; + + // Contract returns 64 extra zeros in raw output of balanceOf. + // Abi decoding should ignore them. + const encodeOutputWith64ExtraZeros = (value: string) => + (encodeTo32ByteHex(value) + ''.padStart(64, '0')) as Hex; + const RAW_BALANCE_BEFORE = encodeOutputWith64ExtraZeros( + DECODED_BALANCE_BEFORE, + ); + const RAW_BALANCE_AFTER = encodeOutputWith64ExtraZeros( + DECODED_BALANCE_AFTER, + ); + + mockParseLog({ + erc20: PARSED_ERC20_TRANSFER_EVENT_MOCK, + }); + + simulateTransactionsMock + .mockResolvedValueOnce( + createEventResponseMock([createLogMock(CONTRACT_ADDRESS_2_MOCK)]), + ) + .mockResolvedValueOnce({ + transactions: [ + { ...defaultResponseTx, return: RAW_BALANCE_BEFORE }, + defaultResponseTx, + { ...defaultResponseTx, return: RAW_BALANCE_AFTER }, + ], + }); + + const simulationData = await getSimulationData(REQUEST_MOCK); + + expect(simulationData).toStrictEqual({ + nativeBalanceChange: undefined, + tokenBalanceChanges: [ + { + standard: SimulationTokenStandard.erc20, + address: CONTRACT_ADDRESS_2_MOCK, + id: undefined, + previousBalance: DECODED_BALANCE_BEFORE, + newBalance: DECODED_BALANCE_AFTER, + difference: EXPECTED_BALANCE_CHANGE, isDecrease: false, }, ], diff --git a/packages/transaction-controller/src/utils/simulation.ts b/packages/transaction-controller/src/utils/simulation.ts index 04ea5f66f4..cb6ff73f06 100644 --- a/packages/transaction-controller/src/utils/simulation.ts +++ b/packages/transaction-controller/src/utils/simulation.ts @@ -332,14 +332,14 @@ async function getTokenBalanceChanges( const previousBalanceCheckSkipped = !balanceTxs.before.get(token); const previousBalance = previousBalanceCheckSkipped ? '0x0' - : getValueFromBalanceTransaction( + : getAmountFromBalanceTransactionResult( request.from, token, // eslint-disable-next-line no-plusplus response.transactions[prevBalanceTxIndex++], ); - const newBalance = getValueFromBalanceTransaction( + const newBalance = getAmountFromBalanceTransactionResult( request.from, token, response.transactions[index + balanceTxs.before.size + 1], @@ -477,24 +477,54 @@ function getEventTokenIds(event: ParsedEvent): (Hex | undefined)[] { } /** - * Extract the value from a balance transaction response. + * Get the interface for a token standard. + * @param tokenStandard - The token standard. + * @returns The interface for the token standard. + */ +function getContractInterface( + tokenStandard: SimulationTokenStandard, +): Interface { + switch (tokenStandard) { + case SimulationTokenStandard.erc721: + return new Interface(abiERC721); + case SimulationTokenStandard.erc1155: + return new Interface(abiERC1155); + default: + return new Interface(abiERC20); + } +} + +/** + * Extract the value from a balance transaction response using the correct ABI. * @param from - The address to check the balance of. * @param token - The token to check the balance of. * @param response - The balance transaction response. - * @returns The value of the balance transaction. + * @returns The value of the balance transaction as Hex. */ -function getValueFromBalanceTransaction( +function getAmountFromBalanceTransactionResult( from: Hex, token: SimulationToken, response: SimulationResponseTransaction, ): Hex { - const normalizedReturn = normalizeReturnValue(response.return); + const contract = getContractInterface(token.standard); - if (token.standard === SimulationTokenStandard.erc721) { - return normalizedReturn === from ? '0x1' : '0x0'; - } + try { + if (token.standard === SimulationTokenStandard.erc721) { + const result = contract.decodeFunctionResult('ownerOf', response.return); + const owner = result[0]; + return owner.toLowerCase() === from.toLowerCase() ? '0x1' : '0x0'; + } - return normalizedReturn; + const result = contract.decodeFunctionResult('balanceOf', response.return); + return toHex(result[0]); + } catch (error) { + log('Failed to decode balance transaction', error, { token, response }); + throw new SimulationError( + `Failed to decode balance transaction for token ${ + token.address + }: ${String(error)}`, + ); + } } /** @@ -509,22 +539,16 @@ function getBalanceTransactionData( from: Hex, tokenId?: Hex, ): Hex { + const contract = getContractInterface(tokenStandard); switch (tokenStandard) { case SimulationTokenStandard.erc721: - return new Interface(abiERC721).encodeFunctionData('ownerOf', [ - tokenId, - ]) as Hex; + return contract.encodeFunctionData('ownerOf', [tokenId]) as Hex; case SimulationTokenStandard.erc1155: - return new Interface(abiERC1155).encodeFunctionData('balanceOf', [ - from, - tokenId, - ]) as Hex; + return contract.encodeFunctionData('balanceOf', [from, tokenId]) as Hex; default: - return new Interface(abiERC20).encodeFunctionData('balanceOf', [ - from, - ]) as Hex; + return contract.encodeFunctionData('balanceOf', [from]) as Hex; } } @@ -607,15 +631,6 @@ function getSimulationBalanceChange( }; } -/** - * Normalize a return value. - * @param value - The return value to normalize. - * @returns The normalized return value. - */ -function normalizeReturnValue(value: Hex): Hex { - return toHex(hexToBN(value)); -} - /** * Get the contract interfaces for all supported tokens. * @returns A map of supported tokens to their contract interfaces. diff --git a/packages/user-operation-controller/src/UserOperationController.test.ts b/packages/user-operation-controller/src/UserOperationController.test.ts index e41af0e540..027fbb1fcd 100644 --- a/packages/user-operation-controller/src/UserOperationController.test.ts +++ b/packages/user-operation-controller/src/UserOperationController.test.ts @@ -137,7 +137,7 @@ function createBundlerMock() { */ function createPendingUserOperationTrackerMock() { return { - startPollingByNetworkClientId: jest.fn(), + startPolling: jest.fn(), setIntervalLength: jest.fn(), hub: new EventEmitter(), } as unknown as jest.Mocked; @@ -1308,18 +1308,18 @@ describe('UserOperationController', () => { } }); - describe('startPollingByNetworkClientId', () => { + describe('startPolling', () => { it('starts polling in PendingUserOperationTracker', async () => { const controller = new UserOperationController(optionsMock); controller.startPollingByNetworkClientId(NETWORK_CLIENT_ID_MOCK); expect( - pendingUserOperationTrackerMock.startPollingByNetworkClientId, + pendingUserOperationTrackerMock.startPolling, ).toHaveBeenCalledTimes(1); - expect( - pendingUserOperationTrackerMock.startPollingByNetworkClientId, - ).toHaveBeenCalledWith(NETWORK_CLIENT_ID_MOCK); + expect(pendingUserOperationTrackerMock.startPolling).toHaveBeenCalledWith( + { networkClientId: NETWORK_CLIENT_ID_MOCK }, + ); }); }); diff --git a/packages/user-operation-controller/src/UserOperationController.ts b/packages/user-operation-controller/src/UserOperationController.ts index 3a5a187849..492233d33c 100644 --- a/packages/user-operation-controller/src/UserOperationController.ts +++ b/packages/user-operation-controller/src/UserOperationController.ts @@ -302,9 +302,9 @@ export class UserOperationController extends BaseController< } startPollingByNetworkClientId(networkClientId: string): string { - return this.#pendingUserOperationTracker.startPollingByNetworkClientId( + return this.#pendingUserOperationTracker.startPolling({ networkClientId, - ); + }); } async #addUserOperation( diff --git a/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.test.ts b/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.test.ts index 2285f2cd90..3291e07fdb 100644 --- a/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.test.ts +++ b/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.test.ts @@ -93,7 +93,9 @@ describe('PendingUserOperationTracker', () => { beforeCallback?.(pendingUserOperationTracker); - await pendingUserOperationTracker._executePoll(NETWORK_CLIENT_ID_MOCK, {}); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); } /** @@ -117,7 +119,9 @@ describe('PendingUserOperationTracker', () => { beforeCallback?.(pendingUserOperationTracker); - await pendingUserOperationTracker._executePoll(NETWORK_CLIENT_ID_MOCK, {}); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); } beforeEach(() => { @@ -147,10 +151,9 @@ describe('PendingUserOperationTracker', () => { messenger: messengerMock, }); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); expect(bundlerMock.getUserOperationReceipt).not.toHaveBeenCalled(); expect(queryMock).not.toHaveBeenCalled(); @@ -173,10 +176,9 @@ describe('PendingUserOperationTracker', () => { messenger: messengerMock, }); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); expect(bundlerMock.getUserOperationReceipt).not.toHaveBeenCalled(); expect(queryMock).not.toHaveBeenCalled(); @@ -197,10 +199,9 @@ describe('PendingUserOperationTracker', () => { new Error('Test Error'), ); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); }); // eslint-disable-next-line jest/expect-expect @@ -216,10 +217,9 @@ describe('PendingUserOperationTracker', () => { bundlerMock.getUserOperationReceipt.mockResolvedValueOnce(undefined); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); }); it('queries bundler using eth_getUserOperationReceipt RPC method', async () => { @@ -232,10 +232,9 @@ describe('PendingUserOperationTracker', () => { messenger: messengerMock, }); - await pendingUserOperationTracker._executePoll( - NETWORK_CLIENT_ID_MOCK, - {}, - ); + await pendingUserOperationTracker._executePoll({ + networkClientId: NETWORK_CLIENT_ID_MOCK, + }); expect(bundlerMock.getUserOperationReceipt).toHaveBeenCalledTimes(1); expect(bundlerMock.getUserOperationReceipt).toHaveBeenCalledWith( diff --git a/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.ts b/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.ts index 26c58cc342..2d5c1ca366 100644 --- a/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.ts +++ b/packages/user-operation-controller/src/helpers/PendingUserOperationTracker.ts @@ -1,8 +1,11 @@ import { query, toHex } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; -import type { NetworkClient, Provider } from '@metamask/network-controller'; +import type { + NetworkClient, + NetworkClientId, + Provider, +} from '@metamask/network-controller'; import { BlockTrackerPollingControllerOnly } from '@metamask/polling-controller'; -import type { Json } from '@metamask/utils'; import { createModuleLogger, type Hex } from '@metamask/utils'; import EventEmitter from 'events'; @@ -40,11 +43,16 @@ export type PendingUserOperationTrackerEventEmitter = EventEmitter & { emit(eventName: T, ...args: Events[T]): boolean; }; +/** The input to start polling for the {@link PendingUserOperationTracker} */ +type PendingUserOperationPollingInput = { + networkClientId: NetworkClientId; +}; + /** * A helper class to periodically query the bundlers * and update the status of any submitted user operations. */ -export class PendingUserOperationTracker extends BlockTrackerPollingControllerOnly { +export class PendingUserOperationTracker extends BlockTrackerPollingControllerOnly() { hub: PendingUserOperationTrackerEventEmitter; #getUserOperations: () => UserOperationMetadata[]; @@ -66,7 +74,7 @@ export class PendingUserOperationTracker extends BlockTrackerPollingControllerOn this.#messenger = messenger; } - async _executePoll(networkClientId: string, _options: Json) { + async _executePoll({ networkClientId }: PendingUserOperationPollingInput) { try { const { blockTracker, configuration, provider } = this._getNetworkClientById(networkClientId) as NetworkClient; diff --git a/types/eth-phishing-detect/src/config.json.d.ts b/types/eth-phishing-detect/src/config.json.d.ts deleted file mode 100644 index 6943346451..0000000000 --- a/types/eth-phishing-detect/src/config.json.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'eth-phishing-detect/src/config.json'; diff --git a/types/eth-phishing-detect/src/detector.d.ts b/types/eth-phishing-detect/src/detector.d.ts deleted file mode 100644 index cab272fdde..0000000000 --- a/types/eth-phishing-detect/src/detector.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'eth-phishing-detect/src/detector'; diff --git a/yarn.config.cjs b/yarn.config.cjs index a2e0428f09..eb78a316c6 100644 --- a/yarn.config.cjs +++ b/yarn.config.cjs @@ -12,6 +12,15 @@ const { basename, resolve } = require('path'); const semver = require('semver'); const { inspect } = require('util'); +/** + * These packages and ranges are allowed to mismatch expected consistency checks + * Only intended as temporary measures to faciliate upgrades and releases. + * This should trend towards empty. + */ +const ALLOWED_INCONSISTENT_DEPENDENCIES = { + '@metamask/rpc-errors': ['^7.0.0'], +}; + /** * Aliases for the Yarn type definitions, to make the code more readable. * @@ -594,6 +603,11 @@ function expectUpToDateWorkspaceDependenciesAndDevDependencies( dependencyWorkspace !== null && dependency.type !== 'peerDependencies' ) { + const ignoredRanges = ALLOWED_INCONSISTENT_DEPENDENCIES[dependency.ident]; + if (ignoredRanges?.includes(dependency.range)) { + continue; + } + dependency.update(`^${dependencyWorkspace.manifest.version}`); } } @@ -714,6 +728,28 @@ function expectControllerDependenciesListedAsPeerDependencies( } } +/** + * Filter out dependency ranges which are not to be considered in `expectConsistentDependenciesAndDevDependencies`. + * + * @param {string} dependencyIdent - The dependency being filtered for + * @param {Map} dependenciesByRange - Dependencies by range + * @returns {Map} The resulting map. + */ +function getInconsistentDependenciesAndDevDependencies( + dependencyIdent, + dependenciesByRange, +) { + const ignoredRanges = ALLOWED_INCONSISTENT_DEPENDENCIES[dependencyIdent]; + if (!ignoredRanges) { + return dependenciesByRange; + } + return new Map( + Object.entries(dependenciesByRange).filter( + ([range]) => !ignoredRanges.includes(range), + ), + ); +} + /** * Expect that all version ranges in `dependencies` and `devDependencies` for * the same dependency across the entire monorepo are the same. As it is @@ -732,18 +768,24 @@ function expectConsistentDependenciesAndDevDependencies(Yarn) { dependencyIdent, dependenciesByRange, ] of nonPeerDependenciesByIdent.entries()) { - const dependencyRanges = [...dependenciesByRange.keys()].sort(); - if (dependenciesByRange.size > 1) { - for (const dependencies of dependenciesByRange.values()) { - for (const dependency of dependencies) { - dependency.error( - `Expected version range for ${dependencyIdent} (in ${ - dependency.type - }) to be consistent across monorepo. Pick one: ${inspect( - dependencyRanges, - )}`, - ); - } + if (dependenciesByRange.size <= 1) { + continue; + } + const dependenciesToConsider = + getInconsistentDependenciesAndDevDependencies( + dependencyIdent, + dependenciesByRange, + ); + const dependencyRanges = [...dependenciesToConsider.keys()].sort(); + for (const dependencies of dependenciesToConsider.values()) { + for (const dependency of dependencies) { + dependency.error( + `Expected version range for ${dependencyIdent} (in ${ + dependency.type + }) to be consistent across monorepo. Pick one: ${inspect( + dependencyRanges, + )}`, + ); } } } diff --git a/yarn.lock b/yarn.lock index 201e4c43ce..037b06dcf9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2579,7 +2579,7 @@ __metadata: "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-query": "npm:^0.5.3" "@metamask/json-rpc-engine": "npm:^9.0.3" - "@metamask/rpc-errors": "npm:^6.3.1" + "@metamask/rpc-errors": "npm:^7.0.0" "@metamask/safe-event-emitter": "npm:^3.0.0" "@metamask/utils": "npm:^9.1.0" "@types/jest": "npm:^27.4.1" @@ -2862,7 +2862,7 @@ __metadata: dependencies: "@lavamoat/allow-scripts": "npm:^3.0.4" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/rpc-errors": "npm:^6.3.1" + "@metamask/rpc-errors": "npm:^7.0.0" "@metamask/safe-event-emitter": "npm:^3.0.0" "@metamask/utils": "npm:^9.1.0" "@types/jest": "npm:^27.4.1" @@ -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: @@ -3404,6 +3403,16 @@ __metadata: languageName: node linkType: hard +"@metamask/rpc-errors@npm:^7.0.0": + version: 7.0.0 + resolution: "@metamask/rpc-errors@npm:7.0.0" + dependencies: + "@metamask/utils": "npm:^9.0.0" + fast-safe-stringify: "npm:^2.0.6" + checksum: 10/f25e2a5506d4d0d6193c88aef8f035ec189a1177f8aee8fa01c9a33d73b1536ca7b5eea2fb33a477768bbd2abaf16529e68f0b3cf714387e5d6c9178225354fd + languageName: node + linkType: hard + "@metamask/safe-event-emitter@npm:^3.0.0, @metamask/safe-event-emitter@npm:^3.1.1": version: 3.1.1 resolution: "@metamask/safe-event-emitter@npm:3.1.1" @@ -7006,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"