diff --git a/package.json b/package.json index 9c76630a028..7432babc2a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "77.0.0", + "version": "78.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index d88d084412f..504571a4496 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -6,9 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.0.0] +### Changed +- **BREAKING:** Bump peer dependency on `@metamask/keyring-controller` to ^8.0.0 + ## [1.0.0] ### Added - Initial release ([#1637](https://github.com/MetaMask/core/pull/1637)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@1.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@2.0.0...HEAD +[2.0.0]: https://github.com/MetaMask/core/compare/@metamask/accounts-controller@1.0.0...@metamask/accounts-controller@2.0.0 [1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/accounts-controller@1.0.0 diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 5d36c7404a4..8442387438f 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/accounts-controller", - "version": "1.0.0", + "version": "2.0.0", "description": "Manages internal accounts", "keywords": [ "MetaMask", @@ -42,18 +42,18 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.1.0", - "@metamask/keyring-controller": "^7.5.0", + "@metamask/keyring-controller": "^8.0.0", "@metamask/snaps-controllers": "^1.0.1", "@types/jest": "^27.4.1", "@types/readable-stream": "^2.3.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "peerDependencies": { - "@metamask/keyring-controller": "^7.5.0" + "@metamask/keyring-controller": "^8.0.0" }, "engines": { "node": ">=16.0.0" diff --git a/packages/address-book-controller/CHANGELOG.md b/packages/address-book-controller/CHANGELOG.md index afa2822fec6..6fa4af9722b 100644 --- a/packages/address-book-controller/CHANGELOG.md +++ b/packages/address-book-controller/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.1.2] +### Changed +- Bump dependency on `@metamask/controller-utils` to ^5.0.0 + ## [3.1.1] ### Changed - Bump dependency on `@metamask/base-controller` to ^3.2.1 @@ -32,7 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Add optional `addressType` property to address book entries ([#828](https://github.com/MetaMask/controllers/pull/828), [#1068](https://github.com/MetaMask/core/pull/1068)) - Rename this repository to `core` ([#1031](https://github.com/MetaMask/controllers/pull/1031)) -- Update `@metamask/controller-utils` package ([#1041](https://github.com/MetaMask/controllers/pull/1041)) +- Update `@metamask/controller-utils` package ([#1041](https://github.com/MetaMask/controllers/pull/1041)) ## [1.0.1] ### Changed @@ -47,7 +51,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/address-book-controller@3.1.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/address-book-controller@3.1.2...HEAD +[3.1.2]: https://github.com/MetaMask/core/compare/@metamask/address-book-controller@3.1.1...@metamask/address-book-controller@3.1.2 [3.1.1]: https://github.com/MetaMask/core/compare/@metamask/address-book-controller@3.1.0...@metamask/address-book-controller@3.1.1 [3.1.0]: https://github.com/MetaMask/core/compare/@metamask/address-book-controller@3.0.0...@metamask/address-book-controller@3.1.0 [3.0.0]: https://github.com/MetaMask/core/compare/@metamask/address-book-controller@2.0.0...@metamask/address-book-controller@3.0.0 diff --git a/packages/address-book-controller/package.json b/packages/address-book-controller/package.json index 57c83c4d3fe..9364550f58f 100644 --- a/packages/address-book-controller/package.json +++ b/packages/address-book-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/address-book-controller", - "version": "3.1.1", + "version": "3.1.2", "description": "Manages a list of recipient addresses associated with nicknames", "keywords": [ "MetaMask", @@ -29,7 +29,7 @@ }, "dependencies": { "@metamask/base-controller": "^3.2.1", - "@metamask/controller-utils": "^4.3.2", + "@metamask/controller-utils": "^5.0.0", "@metamask/utils": "^8.1.0" }, "devDependencies": { @@ -38,8 +38,8 @@ "deepmerge": "^4.2.2", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/announcement-controller/package.json b/packages/announcement-controller/package.json index a8f3a456a5e..8ad9778e1d5 100644 --- a/packages/announcement-controller/package.json +++ b/packages/announcement-controller/package.json @@ -37,8 +37,8 @@ "immer": "^9.0.6", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/approval-controller/package.json b/packages/approval-controller/package.json index 018e76901a1..fc1f74a4b7d 100644 --- a/packages/approval-controller/package.json +++ b/packages/approval-controller/package.json @@ -29,8 +29,8 @@ }, "dependencies": { "@metamask/base-controller": "^3.2.1", + "@metamask/rpc-errors": "^6.0.0", "@metamask/utils": "^8.1.0", - "eth-rpc-errors": "^4.0.2", "immer": "^9.0.6", "nanoid": "^3.1.31" }, @@ -41,8 +41,8 @@ "jest": "^27.5.1", "sinon": "^9.2.4", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/approval-controller/src/ApprovalController.test.ts b/packages/approval-controller/src/ApprovalController.test.ts index 94db8cbc8b4..d9901406156 100644 --- a/packages/approval-controller/src/ApprovalController.test.ts +++ b/packages/approval-controller/src/ApprovalController.test.ts @@ -1,7 +1,7 @@ /* eslint-disable jest/expect-expect */ import { ControllerMessenger } from '@metamask/base-controller'; -import { errorCodes, EthereumRpcError } from 'eth-rpc-errors'; +import { errorCodes, JsonRpcError } from '@metamask/rpc-errors'; import type { ApprovalControllerActions, @@ -642,7 +642,7 @@ describe('approval controller', () => { approvalController.reject('2', new Error('foo')); expect(approvalController.getTotalApprovalCount()).toBe(2); - approvalController.clear(new EthereumRpcError(1, 'clear')); + approvalController.clear(new JsonRpcError(1, 'clear')); expect(approvalController.getTotalApprovalCount()).toBe(0); }); @@ -666,7 +666,7 @@ describe('approval controller', () => { approvalController.reject('2', new Error('foo')); expect(approvalController.getTotalApprovalCount()).toBe(1); - approvalController.clear(new EthereumRpcError(1, 'clear')); + approvalController.clear(new JsonRpcError(1, 'clear')); expect(approvalController.getTotalApprovalCount()).toBe(0); }); }); @@ -1025,7 +1025,7 @@ describe('approval controller', () => { describe('clear', () => { it('does nothing if state is already empty', () => { expect(() => - approvalController.clear(new EthereumRpcError(1, 'clear')), + approvalController.clear(new JsonRpcError(1, 'clear')), ).not.toThrow(); }); @@ -1040,7 +1040,7 @@ describe('approval controller', () => { .add({ id: 'foo3', origin: 'fizz.buzz', type: 'myType' }) .catch((_error) => undefined); - approvalController.clear(new EthereumRpcError(1, 'clear')); + approvalController.clear(new JsonRpcError(1, 'clear')); expect( approvalController.state[PENDING_APPROVALS_STORE_KEY], @@ -1055,16 +1055,16 @@ describe('approval controller', () => { type: 'myType', }); - approvalController.clear(new EthereumRpcError(1000, 'foo')); + approvalController.clear(new JsonRpcError(1000, 'foo')); await expect(rejectPromise).rejects.toThrow( - new EthereumRpcError(1000, 'foo'), + new JsonRpcError(1000, 'foo'), ); }); it('does not clear approval flows', async () => { approvalController.startFlow(); - approvalController.clear(new EthereumRpcError(1, 'clear')); + approvalController.clear(new JsonRpcError(1, 'clear')); expect(approvalController.state[APPROVAL_FLOWS_STORE_KEY]).toHaveLength( 1, diff --git a/packages/approval-controller/src/ApprovalController.ts b/packages/approval-controller/src/ApprovalController.ts index 1c2096164fe..4e3259e892d 100644 --- a/packages/approval-controller/src/ApprovalController.ts +++ b/packages/approval-controller/src/ApprovalController.ts @@ -1,8 +1,8 @@ import type { RestrictedControllerMessenger } from '@metamask/base-controller'; import { BaseControllerV2 } from '@metamask/base-controller'; +import type { JsonRpcError, DataWithOptionalCause } from '@metamask/rpc-errors'; +import { rpcErrors } from '@metamask/rpc-errors'; import type { Json, OptionalField } from '@metamask/utils'; -import type { EthereumRpcError } from 'eth-rpc-errors'; -import { ethErrors } from 'eth-rpc-errors'; import type { Patch } from 'immer'; import { nanoid } from 'nanoid'; @@ -256,7 +256,7 @@ export type GetApprovalsState = { export type ClearApprovalRequests = { type: `${typeof controllerName}:clearRequests`; - handler: (error: EthereumRpcError) => void; + handler: (error: JsonRpcError) => void; }; export type AddApprovalRequest = { @@ -719,10 +719,10 @@ export class ApprovalController extends BaseControllerV2< /** * Rejects and deletes all approval requests. * - * @param rejectionError - The EthereumRpcError to reject the approval + * @param rejectionError - The JsonRpcError to reject the approval * requests with. */ - clear(rejectionError: EthereumRpcError): void { + clear(rejectionError: JsonRpcError): void { for (const id of this.#approvals.keys()) { this.reject(id, rejectionError); } @@ -878,7 +878,7 @@ export class ApprovalController extends BaseControllerV2< !this.#typesExcludedFromRateLimiting.includes(type) && this.has({ origin, type }) ) { - throw ethErrors.rpc.resourceUnavailable( + throw rpcErrors.resourceUnavailable( getAlreadyPendingMessage(origin, type), ); } @@ -937,7 +937,7 @@ export class ApprovalController extends BaseControllerV2< } if (errorMessage) { - throw ethErrors.rpc.internal(errorMessage); + throw rpcErrors.internal(errorMessage); } } diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 4dcb8bf9f4f..103fa7ed375 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -6,6 +6,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [13.0.0] +### Changed +- **BREAKING**: `TokensController` now expects `getNetworkClientById` in constructor options ([#1676](https://github.com/MetaMask/core/pull/1676)) +- **BREAKING**: `TokensController.addToken` now accepts a single options object ([#1676](https://github.com/MetaMask/core/pull/1676)) + ``` + { + address: string; + symbol: string; + decimals: number; + name?: string; + image?: string; + interactingAddress?: string; + networkClientId?: NetworkClientId; + } + ``` +- **BREAKING**: Bump peer dependency on `@metamask/network-controller` to ^13.0.0 ([#1633](https://github.com/MetaMask/core/pull/1633)) +- **CHANGED**: `TokensController.addToken` will use the chain ID value derived from state for `networkClientId` if provided ([#1676](https://github.com/MetaMask/core/pull/1676)) +- **CHANGED**: `TokensController.addTokens` now accepts an optional `networkClientId` as the last parameter ([#1676](https://github.com/MetaMask/core/pull/1676)) +- **CHANGED**: `TokensController.addTokens` will use the chain ID value derived from state for `networkClientId` if provided ([#1676](https://github.com/MetaMask/core/pull/1676)) +- **CHANGED**: `TokensController.watchAsset` options now accepts optional `networkClientId` which is used to get the ERC-20 token name if provided ([#1676](https://github.com/MetaMask/core/pull/1676)) +- Bump dependency on `@metamask/controller-utils` to ^5.0.0 ([#1633](https://github.com/MetaMask/core/pull/1633)) +- Bump dependency on `@metamask/preferences-controller` to ^4.4.1 ([#1676](https://github.com/MetaMask/core/pull/1676)) + ## [12.0.0] ### Added - Add `AssetsContractController` methods `getProvider`, `getChainId`, `getERC721Standard`, and `getERC1155Standard` ([#1638](https://github.com/MetaMask/core/pull/1638)) @@ -78,7 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `getERC20TokenName` method is used to get the token name for tokens added via `wallet_watchAsset` - The `onTokenListStateChange` method is used to trigger a name update when the token list changes. On each change, token names are copied from the token list if they're missing from token controller state. - **BREAKING:** The signature of the tokens controller method `addToken` has changed - - The fourth and fifth positional parameters (`image` and `interactingAddress`) have been replaced by an `options` object + - The fourth and fifth positional parameters (`image` and `interactingAddress`) have been replaced by an `options` object - The new options parameter includes the `image` and `interactingAddress` properties, and a new `name` property - The token detection controller now sets the token name when new tokens are detected ([#1127](https://github.com/MetaMask/core/pull/1127)) - The `Token` type now includes an optional `name` field ([#1127](https://github.com/MetaMask/core/pull/1127)) @@ -157,7 +180,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Remove the `networkType` configuration option from the NFT detection controller, NFT controller, and tokens controller ([#1360](https://github.com/MetaMask/core/pull/1360), [#1359](https://github.com/MetaMask/core/pull/1359)) - **BREAKING:** Remove the `SuggestedAssetMeta` and `SuggestedAssetMetaBase` types from the token controller ([#1268](https://github.com/MetaMask/core/pull/1268)) - **BREAKING:** Remove the `acceptWatchAsset` and `rejectWatchAsset` methods from the token controller ([#1268](https://github.com/MetaMask/core/pull/1268)) - - Suggested assets can be accepted or rejected using the approval controller instead + - Suggested assets can be accepted or rejected using the approval controller instead ## [7.0.0] ### Changed @@ -248,7 +271,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Use Ethers for AssetsContractController ([#845](https://github.com/MetaMask/core/pull/845)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@12.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@13.0.0...HEAD +[13.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@12.0.0...@metamask/assets-controllers@13.0.0 [12.0.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@11.1.0...@metamask/assets-controllers@12.0.0 [11.1.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@11.0.1...@metamask/assets-controllers@11.1.0 [11.0.1]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@11.0.0...@metamask/assets-controllers@11.0.1 diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index 7bbb7cf09af..40073f0584d 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/assets-controllers", - "version": "12.0.0", + "version": "13.0.0", "description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)", "keywords": [ "MetaMask", @@ -36,15 +36,14 @@ "@metamask/approval-controller": "^3.5.1", "@metamask/base-controller": "^3.2.1", "@metamask/contract-metadata": "^2.3.1", - "@metamask/controller-utils": "^4.3.2", + "@metamask/controller-utils": "^5.0.0", "@metamask/eth-query": "^3.0.1", "@metamask/metamask-eth-abis": "3.0.0", - "@metamask/network-controller": "^12.2.0", - "@metamask/preferences-controller": "^4.4.0", - "@metamask/rpc-errors": "^5.1.1", + "@metamask/network-controller": "^13.0.0", + "@metamask/preferences-controller": "^4.4.1", + "@metamask/rpc-errors": "^6.0.0", "@metamask/utils": "^8.1.0", "@types/uuid": "^8.3.0", - "abort-controller": "^3.0.0", "async-mutex": "^0.2.6", "ethereumjs-util": "^7.0.10", "immer": "^9.0.6", @@ -63,14 +62,14 @@ "nock": "^13.3.1", "sinon": "^9.2.4", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "peerDependencies": { "@metamask/approval-controller": "^3.5.1", - "@metamask/network-controller": "^12.2.0", - "@metamask/preferences-controller": "^4.4.0" + "@metamask/network-controller": "^13.0.0", + "@metamask/preferences-controller": "^4.4.1" }, "engines": { "node": ">=16.0.0" diff --git a/packages/assets-controllers/src/TokenListController.ts b/packages/assets-controllers/src/TokenListController.ts index 8edde3e2a68..eef38badaa4 100644 --- a/packages/assets-controllers/src/TokenListController.ts +++ b/packages/assets-controllers/src/TokenListController.ts @@ -6,7 +6,6 @@ import type { NetworkState, } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; -import { AbortController as WhatwgAbortController } from 'abort-controller'; import { Mutex } from 'async-mutex'; import type { Patch } from 'immer'; @@ -96,7 +95,7 @@ export class TokenListController extends BaseControllerV2< private chainId: Hex; - private abortController: WhatwgAbortController; + private abortController: AbortController; /** * Creates a TokenListController instance. @@ -139,7 +138,7 @@ export class TokenListController extends BaseControllerV2< this.cacheRefreshThreshold = cacheRefreshThreshold; this.chainId = chainId; this.updatePreventPollingOnNetworkRestart(preventPollingOnNetworkRestart); - this.abortController = new WhatwgAbortController(); + this.abortController = new AbortController(); if (onNetworkStateChange) { onNetworkStateChange(async (networkControllerState) => { await this.#onNetworkControllerStateChange(networkControllerState); @@ -163,7 +162,7 @@ export class TokenListController extends BaseControllerV2< async #onNetworkControllerStateChange(networkControllerState: NetworkState) { if (this.chainId !== networkControllerState.providerConfig.chainId) { this.abortController.abort(); - this.abortController = new WhatwgAbortController(); + this.abortController = new AbortController(); this.chainId = networkControllerState.providerConfig.chainId; if (this.state.preventPollingOnNetworkRestart) { this.clearingTokenListData(); diff --git a/packages/assets-controllers/src/TokensController.ts b/packages/assets-controllers/src/TokensController.ts index 33c1ee9c5d8..1f04ecccc23 100644 --- a/packages/assets-controllers/src/TokensController.ts +++ b/packages/assets-controllers/src/TokensController.ts @@ -23,7 +23,6 @@ import type { } from '@metamask/network-controller'; import type { PreferencesState } from '@metamask/preferences-controller'; import type { Hex } from '@metamask/utils'; -import { AbortController as WhatwgAbortController } from 'abort-controller'; import { Mutex } from 'async-mutex'; import { EventEmitter } from 'events'; import { v1 as random } from 'uuid'; @@ -125,7 +124,7 @@ export class TokensController extends BaseController< > { private readonly mutex = new Mutex(); - private abortController: WhatwgAbortController; + private abortController: AbortController; private readonly messagingSystem: TokensControllerMessenger; @@ -231,7 +230,7 @@ export class TokensController extends BaseController< }; this.initialize(); - this.abortController = new WhatwgAbortController(); + this.abortController = new AbortController(); this.getERC20TokenName = getERC20TokenName; this.getNetworkClientById = getNetworkClientById; @@ -253,7 +252,7 @@ export class TokensController extends BaseController< const { selectedAddress } = this.config; const { chainId } = providerConfig; this.abortController.abort(); - this.abortController = new WhatwgAbortController(); + this.abortController = new AbortController(); this.configure({ chainId }); this.update({ tokens: allTokens[chainId]?.[selectedAddress] || [], diff --git a/packages/assets-controllers/src/token-service.test.ts b/packages/assets-controllers/src/token-service.test.ts index 7fa3c083b07..7e168dd6cc6 100644 --- a/packages/assets-controllers/src/token-service.test.ts +++ b/packages/assets-controllers/src/token-service.test.ts @@ -1,5 +1,4 @@ import { toHex } from '@metamask/controller-utils'; -import { AbortController as WhatwgAbortController } from 'abort-controller'; import nock from 'nock'; import { @@ -140,7 +139,7 @@ const sampleChainId = toHex(sampleDecimalChainId); describe('Token service', () => { describe('fetchTokenList', () => { it('should call the tokens api and return the list of tokens', async () => { - const { signal } = new WhatwgAbortController(); + const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get(`/tokens/${sampleDecimalChainId}`) .reply(200, sampleTokenList) @@ -152,7 +151,7 @@ describe('Token service', () => { }); it('should return undefined if the fetch is aborted', async () => { - const abortController = new WhatwgAbortController(); + const abortController = new AbortController(); nock(TOKEN_END_POINT_API) .get(`/tokens/${sampleDecimalChainId}`) // well beyond time it will take to abort @@ -170,7 +169,7 @@ describe('Token service', () => { }); it('should return undefined if the fetch fails with a network error', async () => { - const { signal } = new WhatwgAbortController(); + const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get(`/tokens/${sampleDecimalChainId}`) .replyWithError('Example network error') @@ -182,7 +181,7 @@ describe('Token service', () => { }); it('should return undefined if the fetch fails with an unsuccessful status code', async () => { - const { signal } = new WhatwgAbortController(); + const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get(`/tokens/${sampleDecimalChainId}`) .reply(500) @@ -194,7 +193,7 @@ describe('Token service', () => { }); it('should return undefined if the fetch fails with a timeout', async () => { - const { signal } = new WhatwgAbortController(); + const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get(`/tokens/${sampleDecimalChainId}`) // well beyond timeout @@ -212,7 +211,7 @@ describe('Token service', () => { describe('fetchTokenMetadata', () => { it('should call the api to return the token metadata for eth address provided', async () => { - const { signal } = new WhatwgAbortController(); + const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get( `/token/${sampleDecimalChainId}?address=0x514910771af9ca656af840dff83e8264ecf986ca`, @@ -230,7 +229,7 @@ describe('Token service', () => { }); it('should return undefined if the fetch is aborted', async () => { - const abortController = new WhatwgAbortController(); + const abortController = new AbortController(); nock(TOKEN_END_POINT_API) .get(`/tokens/${sampleDecimalChainId}`) // well beyond time it will take to abort @@ -249,7 +248,7 @@ describe('Token service', () => { }); it('should return undefined if the fetch fails with a network error', async () => { - const { signal } = new WhatwgAbortController(); + const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get(`/tokens/${sampleDecimalChainId}`) .replyWithError('Example network error') @@ -265,7 +264,7 @@ describe('Token service', () => { }); it('should return undefined if the fetch fails with an unsuccessful status code', async () => { - const { signal } = new WhatwgAbortController(); + const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get(`/tokens/${sampleDecimalChainId}`) .reply(500) @@ -281,7 +280,7 @@ describe('Token service', () => { }); it('should return undefined if the fetch fails with a timeout', async () => { - const { signal } = new WhatwgAbortController(); + const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get(`/tokens/${sampleDecimalChainId}`) // well beyond timeout @@ -300,7 +299,7 @@ describe('Token service', () => { }); it('should throw error if fetching from non supported network', async () => { - const { signal } = new WhatwgAbortController(); + const { signal } = new AbortController(); await expect( fetchTokenMetadata( toHex(5), @@ -312,7 +311,7 @@ describe('Token service', () => { }); it('should call the tokens api and return undefined', async () => { - const { signal } = new WhatwgAbortController(); + const { signal } = new AbortController(); nock(TOKEN_END_POINT_API) .get(`/tokens/${sampleDecimalChainId}`) .reply(404, undefined) diff --git a/packages/base-controller/package.json b/packages/base-controller/package.json index b4ff32a936e..82c07cfc0a0 100644 --- a/packages/base-controller/package.json +++ b/packages/base-controller/package.json @@ -39,8 +39,8 @@ "jest": "^27.5.1", "sinon": "^9.2.4", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/composable-controller/package.json b/packages/composable-controller/package.json index ae25fcc010b..c62d5fb7799 100644 --- a/packages/composable-controller/package.json +++ b/packages/composable-controller/package.json @@ -38,8 +38,8 @@ "jest": "^27.5.1", "sinon": "^9.2.4", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/controller-utils/CHANGELOG.md b/packages/controller-utils/CHANGELOG.md index 865a9f96e0e..cbc4b9d7a6c 100644 --- a/packages/controller-utils/CHANGELOG.md +++ b/packages/controller-utils/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.0.0] +### Changed +- **BREAKING**: Rename `NETWORK_ID_TO_ETHERS_NETWORK_NAME_MAP` to `CHAIN_ID_TO_ETHERS_NETWORK_NAME_MAP` ([#1633](https://github.com/MetaMask/core/pull/1633)) + - Change it to a map of `Hex` chain ID to `BuiltInNetworkName` + +### Removed +- **BREAKING**: Remove `NetworkId` constant and type ([#1633](https://github.com/MetaMask/core/pull/1633)) + ## [4.3.2] ### Changed - There are no consumer-facing changes to this package. This version is a part of a synchronized release across all packages in our monorepo. @@ -147,7 +155,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@4.3.2...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@5.0.0...HEAD +[5.0.0]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@4.3.2...@metamask/controller-utils@5.0.0 [4.3.2]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@4.3.1...@metamask/controller-utils@4.3.2 [4.3.1]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@4.3.0...@metamask/controller-utils@4.3.1 [4.3.0]: https://github.com/MetaMask/core/compare/@metamask/controller-utils@4.2.0...@metamask/controller-utils@4.3.0 diff --git a/packages/controller-utils/package.json b/packages/controller-utils/package.json index eea9f6ec123..33c48d0b00f 100644 --- a/packages/controller-utils/package.json +++ b/packages/controller-utils/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/controller-utils", - "version": "4.3.2", + "version": "5.0.0", "description": "Data and convenience functions shared by multiple packages", "keywords": [ "MetaMask", @@ -32,7 +32,6 @@ "@metamask/utils": "^8.1.0", "@spruceid/siwe-parser": "1.1.3", "eth-ens-namehash": "^2.0.8", - "eth-rpc-errors": "^4.0.2", "ethereumjs-util": "^7.0.10", "ethjs-unit": "^0.1.6", "fast-deep-equal": "^3.1.3" @@ -40,13 +39,12 @@ "devDependencies": { "@metamask/auto-changelog": "^3.1.0", "@types/jest": "^27.4.1", - "abort-controller": "^3.0.0", "deepmerge": "^4.2.2", "jest": "^27.5.1", "nock": "^13.3.1", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/ens-controller/CHANGELOG.md b/packages/ens-controller/CHANGELOG.md index e11a3136e03..f152f18b351 100644 --- a/packages/ens-controller/CHANGELOG.md +++ b/packages/ens-controller/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.0.0] +### Changed +- **BREAKING**: Bump peer dependency on `@metamask/network-controller` to ^13.0.0 ([#1633](https://github.com/MetaMask/core/pull/1633)) +- Use `providerConfig.chainId` instead of `providerConfig.networkId` to determine ENS compatability ([#1633](https://github.com/MetaMask/core/pull/1633)) +- Bump dependency on `@metamask/controller-utils` to ^5.0.0 ([#1633](https://github.com/MetaMask/core/pull/1633)) + ## [4.1.1] ### Changed - Bump dependency on `@metamask/base-controller` to ^3.2.1 @@ -60,7 +66,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/ens-controller@4.1.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/ens-controller@5.0.0...HEAD +[5.0.0]: https://github.com/MetaMask/core/compare/@metamask/ens-controller@4.1.1...@metamask/ens-controller@5.0.0 [4.1.1]: https://github.com/MetaMask/core/compare/@metamask/ens-controller@4.1.0...@metamask/ens-controller@4.1.1 [4.1.0]: https://github.com/MetaMask/core/compare/@metamask/ens-controller@4.0.0...@metamask/ens-controller@4.1.0 [4.0.0]: https://github.com/MetaMask/core/compare/@metamask/ens-controller@3.1.0...@metamask/ens-controller@4.0.0 diff --git a/packages/ens-controller/package.json b/packages/ens-controller/package.json index 2bca780a5e2..4f72422a7e7 100644 --- a/packages/ens-controller/package.json +++ b/packages/ens-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/ens-controller", - "version": "4.1.1", + "version": "5.0.0", "description": "Maps ENS names to their resolved addresses by chain id", "keywords": [ "MetaMask", @@ -30,8 +30,8 @@ "dependencies": { "@ethersproject/providers": "^5.7.0", "@metamask/base-controller": "^3.2.1", - "@metamask/controller-utils": "^4.3.2", - "@metamask/network-controller": "^12.2.0", + "@metamask/controller-utils": "^5.0.0", + "@metamask/network-controller": "^13.0.0", "@metamask/utils": "^8.1.0", "ethereum-ens-network-map": "^1.0.2", "punycode": "^2.1.1" @@ -42,12 +42,12 @@ "deepmerge": "^4.2.2", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "peerDependencies": { - "@metamask/network-controller": "^12.2.0" + "@metamask/network-controller": "^13.0.0" }, "engines": { "node": ">=16.0.0" diff --git a/packages/gas-fee-controller/CHANGELOG.md b/packages/gas-fee-controller/CHANGELOG.md index 259e259304c..3a3708f1de6 100644 --- a/packages/gas-fee-controller/CHANGELOG.md +++ b/packages/gas-fee-controller/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.0.0] +### Changed +- **BREAKING**: Bump peer dependency on `@metamask/network-controller` to ^13.0.0 ([#1633](https://github.com/MetaMask/core/pull/1633)) +- Bump dependency on `@metamask/controller-utils` to ^5.0.0 ([#1633](https://github.com/MetaMask/core/pull/1633)) + ## [6.1.2] ### Changed - Bump dependency on `@metamask/base-controller` to ^3.2.1 @@ -77,7 +82,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/gas-fee-controller@6.1.2...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/gas-fee-controller@7.0.0...HEAD +[7.0.0]: https://github.com/MetaMask/core/compare/@metamask/gas-fee-controller@6.1.2...@metamask/gas-fee-controller@7.0.0 [6.1.2]: https://github.com/MetaMask/core/compare/@metamask/gas-fee-controller@6.1.1...@metamask/gas-fee-controller@6.1.2 [6.1.1]: https://github.com/MetaMask/core/compare/@metamask/gas-fee-controller@6.1.0...@metamask/gas-fee-controller@6.1.1 [6.1.0]: https://github.com/MetaMask/core/compare/@metamask/gas-fee-controller@6.0.1...@metamask/gas-fee-controller@6.1.0 diff --git a/packages/gas-fee-controller/package.json b/packages/gas-fee-controller/package.json index 96475aadda7..105a9dd7436 100644 --- a/packages/gas-fee-controller/package.json +++ b/packages/gas-fee-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/gas-fee-controller", - "version": "6.1.2", + "version": "7.0.0", "description": "Periodically calculates gas fee estimates based on various gas limits as well as other data displayed on transaction confirm screens", "keywords": [ "MetaMask", @@ -29,9 +29,9 @@ }, "dependencies": { "@metamask/base-controller": "^3.2.1", - "@metamask/controller-utils": "^4.3.2", + "@metamask/controller-utils": "^5.0.0", "@metamask/eth-query": "^3.0.1", - "@metamask/network-controller": "^12.2.0", + "@metamask/network-controller": "^13.0.0", "@metamask/utils": "^8.1.0", "@types/uuid": "^8.3.0", "ethereumjs-util": "^7.0.10", @@ -49,12 +49,12 @@ "nock": "^13.3.1", "sinon": "^9.2.4", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "peerDependencies": { - "@metamask/network-controller": "^12.2.0" + "@metamask/network-controller": "^13.0.0" }, "engines": { "node": ">=16.0.0" diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 0ea041ab2ac..5b7992cde3b 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [8.0.0] +### Added +- Add `getQRKeyring(): QRKeyring | undefined` method +- Add `KeyringController:qrKeyringStateChange` messenger event +- The event emits updates from the internal `QRKeyring` instance, if there's one + +### Changed +- **BREAKING:** addNewKeyring(type) return type changed from Promise> to Promise + - When calling with QRKeyring type the keyring instance is retrieved or created (no multiple QRKeyring instances possible) +- Bump dependency on `@metamask/message-manager` to ^7.3.3 +- Bump dependency on `@metamask/preferences-controller` to ^4.4.1 + +### Fixed +- Fix `addNewAccountForKeyring` for `CustodyKeyring` ([#1694](https://github.com/MetaMask/core/pull/1694)) + ## [7.5.0] ### Added - Add `KeyringController` messenger actions ([#1691](https://github.com/MetaMask/core/pull/1691)) @@ -20,7 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `KeyringController` messenger actions ([#1654](https://github.com/MetaMask/core/pull/1654)) - `KeyringController:signMessage` - - `KeyringController:signPersonalMessage` + - `KeyringController:signPersonalMessage` - `KeyringController:signTypedMessage` - `KeyringController:decryptMessage` - `KeyringController:getEncryptionPublicKey` @@ -181,7 +196,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@7.5.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@8.0.0...HEAD +[8.0.0]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@7.5.0...@metamask/keyring-controller@8.0.0 [7.5.0]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@7.4.0...@metamask/keyring-controller@7.5.0 [7.4.0]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@7.3.0...@metamask/keyring-controller@7.4.0 [7.3.0]: https://github.com/MetaMask/core/compare/@metamask/keyring-controller@7.2.0...@metamask/keyring-controller@7.3.0 diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index 0ead512a957..96ccfebacf5 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/keyring-controller", - "version": "7.5.0", + "version": "8.0.0", "description": "Stores identities seen in the wallet and manages interactions such as signing", "keywords": [ "MetaMask", @@ -31,8 +31,8 @@ "@keystonehq/metamask-airgapped-keyring": "^0.13.1", "@metamask/base-controller": "^3.2.1", "@metamask/eth-keyring-controller": "^13.0.1", - "@metamask/message-manager": "^7.3.2", - "@metamask/preferences-controller": "^4.4.0", + "@metamask/message-manager": "^7.3.3", + "@metamask/preferences-controller": "^4.4.1", "@metamask/utils": "^8.1.0", "async-mutex": "^0.2.6", "ethereumjs-util": "^7.0.10", @@ -51,13 +51,13 @@ "jest": "^27.5.1", "sinon": "^9.2.4", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3", "uuid": "^8.3.2" }, "peerDependencies": { - "@metamask/preferences-controller": "^4.4.0" + "@metamask/preferences-controller": "^4.4.1" }, "engines": { "node": ">=16.0.0" diff --git a/packages/logging-controller/CHANGELOG.md b/packages/logging-controller/CHANGELOG.md index cde21f3cf17..fa98f02a60d 100644 --- a/packages/logging-controller/CHANGELOG.md +++ b/packages/logging-controller/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.2] +### Changed +- Bump dependency on `@metamask/controller-utils` to ^5.0.0 + ## [1.0.1] ### Changed - Bump dependency on `@metamask/base-controller` to ^3.2.1 @@ -16,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial Release - Add logging controller ([#1089](https://github.com/MetaMask/core.git/pull/1089)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/logging-controller@1.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/logging-controller@1.0.2...HEAD +[1.0.2]: https://github.com/MetaMask/core/compare/@metamask/logging-controller@1.0.1...@metamask/logging-controller@1.0.2 [1.0.1]: https://github.com/MetaMask/core/compare/@metamask/logging-controller@1.0.0...@metamask/logging-controller@1.0.1 [1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/logging-controller@1.0.0 diff --git a/packages/logging-controller/package.json b/packages/logging-controller/package.json index 4371043418e..a02635b1680 100644 --- a/packages/logging-controller/package.json +++ b/packages/logging-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/logging-controller", - "version": "1.0.1", + "version": "1.0.2", "description": "Manages logging data to assist users and support staff", "keywords": [ "MetaMask", @@ -29,7 +29,7 @@ }, "dependencies": { "@metamask/base-controller": "^3.2.1", - "@metamask/controller-utils": "^4.3.2", + "@metamask/controller-utils": "^5.0.0", "uuid": "^8.3.2" }, "devDependencies": { @@ -38,8 +38,8 @@ "deepmerge": "^4.2.2", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/message-manager/CHANGELOG.md b/packages/message-manager/CHANGELOG.md index dc3dd7dc8c8..9d626514e6c 100644 --- a/packages/message-manager/CHANGELOG.md +++ b/packages/message-manager/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.3.3] +### Changed +- Bump dependency on `@metamask/controller-utils` to ^5.0.0 + ## [7.3.2] ### Changed - Bump @metamask/eth-sig-util from 6.0.0 to 7.0.0 ([#1669](https://github.com/MetaMask/core/pull/1669)) @@ -109,7 +113,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/message-manager@7.3.2...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/message-manager@7.3.3...HEAD +[7.3.3]: https://github.com/MetaMask/core/compare/@metamask/message-manager@7.3.2...@metamask/message-manager@7.3.3 [7.3.2]: https://github.com/MetaMask/core/compare/@metamask/message-manager@7.3.1...@metamask/message-manager@7.3.2 [7.3.1]: https://github.com/MetaMask/core/compare/@metamask/message-manager@7.3.0...@metamask/message-manager@7.3.1 [7.3.0]: https://github.com/MetaMask/core/compare/@metamask/message-manager@7.2.0...@metamask/message-manager@7.3.0 diff --git a/packages/message-manager/package.json b/packages/message-manager/package.json index 39d16912329..f60ea399e3a 100644 --- a/packages/message-manager/package.json +++ b/packages/message-manager/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/message-manager", - "version": "7.3.2", + "version": "7.3.3", "description": "Stores and manages interactions with signing requests", "keywords": [ "MetaMask", @@ -29,7 +29,7 @@ }, "dependencies": { "@metamask/base-controller": "^3.2.1", - "@metamask/controller-utils": "^4.3.2", + "@metamask/controller-utils": "^5.0.0", "@metamask/eth-sig-util": "^7.0.0", "@metamask/utils": "^8.1.0", "@types/uuid": "^8.3.0", @@ -43,8 +43,8 @@ "deepmerge": "^4.2.2", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/name-controller/package.json b/packages/name-controller/package.json index 84d793d9289..a8cc3e9f6d2 100644 --- a/packages/name-controller/package.json +++ b/packages/name-controller/package.json @@ -30,6 +30,8 @@ }, "dependencies": { "@metamask/base-controller": "^3.2.1", + "@metamask/utils": "^8.1.0", + "async-mutex": "^0.2.6", "immer": "^9.0.6" }, "devDependencies": { @@ -38,8 +40,8 @@ "deepmerge": "^4.2.2", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/name-controller/src/NameController.test.ts b/packages/name-controller/src/NameController.test.ts index de557e14492..802216fe90a 100644 --- a/packages/name-controller/src/NameController.test.ts +++ b/packages/name-controller/src/NameController.test.ts @@ -33,9 +33,14 @@ Date.now = jest.fn().mockReturnValue(TIME_MOCK * 1000); * Creates a mock name provider. * * @param index - Index of the provider used to generate unique values. + * @param options - Additional options to configure the mock provider. + * @param options.updateDelay - Optional update delay to return. * @returns Mock instance of a name provider. */ -function createMockProvider(index: number): jest.Mocked { +function createMockProvider( + index: number, + { updateDelay }: { updateDelay?: number } = {}, +): jest.Mocked { return { getMetadata: jest.fn().mockReturnValue({ sourceIds: { @@ -52,6 +57,7 @@ function createMockProvider(index: number): jest.Mocked { PROPOSED_NAME_MOCK + String(index), `${PROPOSED_NAME_MOCK + String(index)}_2`, ], + updateDelay, }, }, }), @@ -81,7 +87,6 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: NAME_MOCK, sourceId: `${SOURCE_ID_MOCK}1`, - proposedNamesLastUpdated: null, proposedNames: {}, }, }, @@ -103,9 +108,12 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: null, proposedNames: { - [SOURCE_ID_MOCK]: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + lastRequestTime: null, + updateDelay: null, + }, }, }, }, @@ -125,9 +133,12 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: NAME_MOCK, sourceId: `${SOURCE_ID_MOCK}1`, - proposedNamesLastUpdated: null, proposedNames: { - [SOURCE_ID_MOCK]: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + lastRequestTime: null, + updateDelay: null, + }, }, }, }, @@ -144,9 +155,12 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: SOURCE_ID_MOCK, - proposedNamesLastUpdated: null, proposedNames: { - [SOURCE_ID_MOCK]: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + lastRequestTime: null, + updateDelay: null, + }, }, }, }, @@ -165,9 +179,12 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: NAME_MOCK, sourceId: null, - proposedNamesLastUpdated: null, proposedNames: { - [SOURCE_ID_MOCK]: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + lastRequestTime: null, + updateDelay: null, + }, }, }, }, @@ -190,9 +207,12 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: NAME_MOCK, sourceId: SOURCE_ID_MOCK, - proposedNamesLastUpdated: TIME_MOCK, proposedNames: { - [SOURCE_ID_MOCK]: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, }, }, }, @@ -211,15 +231,17 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: NAME_MOCK, sourceId: SOURCE_ID_MOCK, - proposedNamesLastUpdated: TIME_MOCK, proposedNames: { - [SOURCE_ID_MOCK]: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, }, }, [alternateChainId]: { name: alternateName, sourceId: null, - proposedNamesLastUpdated: null, proposedNames: {}, }, }, @@ -241,9 +263,12 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: NAME_MOCK, sourceId: SOURCE_ID_MOCK, - proposedNamesLastUpdated: null, proposedNames: { - [SOURCE_ID_MOCK]: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + lastRequestTime: null, + updateDelay: null, + }, }, }, }, @@ -262,9 +287,12 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: null, proposedNames: { - [SOURCE_ID_MOCK]: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + [SOURCE_ID_MOCK]: { + proposedNames: [PROPOSED_NAME_MOCK, PROPOSED_NAME_2_MOCK], + lastRequestTime: null, + updateDelay: null, + }, }, }, }, @@ -382,7 +410,7 @@ describe('NameController', () => { 'creates entry with proposed names if value is new%s', async (_, getExistingState) => { const provider1 = createMockProvider(1); - const provider2 = createMockProvider(2); + const provider2 = createMockProvider(2, { updateDelay: 3 }); const controller = new NameController({ ...CONTROLLER_ARGS_MOCK, @@ -402,16 +430,23 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: TIME_MOCK, proposedNames: { - [`${SOURCE_ID_MOCK}1`]: [ - `${PROPOSED_NAME_MOCK}1`, - `${PROPOSED_NAME_MOCK}1_2`, - ], - [`${SOURCE_ID_MOCK}2`]: [ - `${PROPOSED_NAME_MOCK}2`, - `${PROPOSED_NAME_MOCK}2_2`, - ], + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: 3, + }, }, }, }, @@ -454,10 +489,17 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: 12, proposedNames: { - [`${SOURCE_ID_MOCK}1`]: ['ShouldBeDeleted1'], - [`${SOURCE_ID_MOCK}2`]: ['ShouldBeDeleted2'], + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldBeDeleted1'], + lastRequestTime: 12, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: ['ShouldBeDeleted2'], + lastRequestTime: 12, + updateDelay: null, + }, }, }, }, @@ -475,16 +517,23 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: TIME_MOCK, proposedNames: { - [`${SOURCE_ID_MOCK}1`]: [ - `${PROPOSED_NAME_MOCK}1`, - `${PROPOSED_NAME_MOCK}1_2`, - ], - [`${SOURCE_ID_MOCK}2`]: [ - `${PROPOSED_NAME_MOCK}2`, - `${PROPOSED_NAME_MOCK}2_2`, - ], + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, }, }, }, @@ -526,9 +575,12 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: 12, proposedNames: { - [`${SOURCE_ID_MOCK}3`]: ['ShouldBeDeleted3'], + [`${SOURCE_ID_MOCK}3`]: { + proposedNames: ['ShouldBeDeleted3'], + lastRequestTime: 12, + updateDelay: null, + }, }, }, }, @@ -546,16 +598,23 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: TIME_MOCK, proposedNames: { - [`${SOURCE_ID_MOCK}1`]: [ - `${PROPOSED_NAME_MOCK}1`, - `${PROPOSED_NAME_MOCK}1_2`, - ], - [`${SOURCE_ID_MOCK}2`]: [ - `${PROPOSED_NAME_MOCK}2`, - `${PROPOSED_NAME_MOCK}2_2`, - ], + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, }, }, }, @@ -592,72 +651,20 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: TIME_MOCK, - proposedNames: { - [`${SOURCE_ID_MOCK}1`]: [], - [`${SOURCE_ID_MOCK}2`]: [ - `${PROPOSED_NAME_MOCK}2`, - `${PROPOSED_NAME_MOCK}2_2`, - ], - }, - }, - }, - }, - }); - - expect(result).toStrictEqual({ - results: { - [`${SOURCE_ID_MOCK}1`]: { - proposedNames: [], - error: undefined, - }, - [`${SOURCE_ID_MOCK}2`]: { - proposedNames: [ - `${PROPOSED_NAME_MOCK}2`, - `${PROPOSED_NAME_MOCK}2_2`, - ], - error: undefined, - }, - }, - }); - }); - - it('stores empty array if proposed names is undefined and no error', async () => { - const provider1 = createMockProvider(1); - const provider2 = createMockProvider(2); - - provider1.getProposedNames.mockResolvedValue({ - results: { - [`${SOURCE_ID_MOCK}1`]: { - proposedNames: undefined, - error: undefined, - }, - }, - }); - - const controller = new NameController({ - ...CONTROLLER_ARGS_MOCK, - providers: [provider1, provider2], - }); - - const result = await controller.updateProposedNames({ - value: VALUE_MOCK, - type: NameType.ETHEREUM_ADDRESS, - }); - - expect(controller.state.names).toStrictEqual({ - [NameType.ETHEREUM_ADDRESS]: { - [VALUE_MOCK]: { - [CHAIN_ID_MOCK]: { - name: null, - sourceId: null, - proposedNamesLastUpdated: TIME_MOCK, proposedNames: { - [`${SOURCE_ID_MOCK}1`]: [], - [`${SOURCE_ID_MOCK}2`]: [ - `${PROPOSED_NAME_MOCK}2`, - `${PROPOSED_NAME_MOCK}2_2`, - ], + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, }, }, }, @@ -681,7 +688,7 @@ describe('NameController', () => { }); }); - it('updates provider state', async () => { + it('updates source state', async () => { const provider1 = createMockProvider(1); const provider2 = createMockProvider(2); @@ -737,11 +744,22 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: 12, proposedNames: { - [`${SOURCE_ID_MOCK}1`]: ['ShouldNotBeDeleted1'], - [`${SOURCE_ID_MOCK}2`]: ['ShouldNotBeDeleted2'], - [`${SOURCE_ID_MOCK}3`]: ['ShouldNotBeDeleted3'], + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeDeleted1'], + lastRequestTime: 12, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: ['ShouldNotBeDeleted2'], + lastRequestTime: 12, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}3`]: { + proposedNames: ['ShouldNotBeDeleted3'], + lastRequestTime: 12, + updateDelay: null, + }, }, }, }, @@ -759,26 +777,44 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: 12, proposedNames: { - [`${SOURCE_ID_MOCK}1`]: ['ShouldNotBeDeleted1'], - [`${SOURCE_ID_MOCK}2`]: ['ShouldNotBeDeleted2'], - [`${SOURCE_ID_MOCK}3`]: ['ShouldNotBeDeleted3'], + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeDeleted1'], + lastRequestTime: 12, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: ['ShouldNotBeDeleted2'], + lastRequestTime: 12, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}3`]: { + proposedNames: ['ShouldNotBeDeleted3'], + lastRequestTime: 12, + updateDelay: null, + }, }, }, [alternateChainId]: { name: null, sourceId: null, - proposedNamesLastUpdated: TIME_MOCK, proposedNames: { - [`${SOURCE_ID_MOCK}1`]: [ - `${PROPOSED_NAME_MOCK}1`, - `${PROPOSED_NAME_MOCK}1_2`, - ], - [`${SOURCE_ID_MOCK}2`]: [ - `${PROPOSED_NAME_MOCK}2`, - `${PROPOSED_NAME_MOCK}2_2`, - ], + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, }, }, }, @@ -786,67 +822,144 @@ describe('NameController', () => { }); }); - describe('with error', () => { - it('returns result errors if unhandled error while getting proposed name using provider', async () => { - const provider1 = createMockProvider(1); - const provider2 = createMockProvider(2); - const error = new Error('TestError'); + it('ignores source IDs in response if not in metadata', async () => { + const provider1 = createMockProvider(1); - provider1.getProposedNames.mockRejectedValue(error); + provider1.getProposedNames.mockResolvedValue({ + results: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + }, + }, + }); - const controller = new NameController({ - ...CONTROLLER_ARGS_MOCK, - providers: [provider1, provider2], - }); + const controller = new NameController({ + ...CONTROLLER_ARGS_MOCK, + providers: [provider1], + }); - const result = await controller.updateProposedNames({ - value: VALUE_MOCK, - type: NameType.ETHEREUM_ADDRESS, - }); + const result = await controller.updateProposedNames({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); - expect(controller.state.names).toStrictEqual({ - [NameType.ETHEREUM_ADDRESS]: { - [VALUE_MOCK]: { - [CHAIN_ID_MOCK]: { - name: null, - sourceId: null, - proposedNamesLastUpdated: TIME_MOCK, - proposedNames: { - [`${SOURCE_ID_MOCK}2`]: [ - `${PROPOSED_NAME_MOCK}2`, - `${PROPOSED_NAME_MOCK}2_2`, + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, ], + lastRequestTime: TIME_MOCK, + updateDelay: null, }, }, }, }, - }); + }, + }); - expect(result).toStrictEqual({ - results: { - [`${SOURCE_ID_MOCK}1`]: { - proposedNames: undefined, - error, - }, - [`${SOURCE_ID_MOCK}2`]: { - proposedNames: [ - `${PROPOSED_NAME_MOCK}2`, - `${PROPOSED_NAME_MOCK}2_2`, - ], - error: undefined, + expect(result).toStrictEqual({ + results: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + error: undefined, + }, + }, + }); + }); + + it('ignores empty or undefined proposed names', async () => { + const provider1 = createMockProvider(1); + + provider1.getProposedNames.mockResolvedValue({ + results: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + undefined, + null, + '', + `${PROPOSED_NAME_MOCK}1_2`, + ], + }, + }, + } as any); + + const controller = new NameController({ + ...CONTROLLER_ARGS_MOCK, + providers: [provider1], + }); + + const result = await controller.updateProposedNames({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + }, }, }, - }); + }, }); - it('returns result errors if response error while getting proposed name using provider', async () => { + expect(result).toStrictEqual({ + results: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + error: undefined, + }, + }, + }); + }); + + describe('does not update existing proposed names if', () => { + it('new value is undefined', async () => { const provider1 = createMockProvider(1); const provider2 = createMockProvider(2); - const error = new Error('TestError'); provider1.getProposedNames.mockResolvedValue({ + results: { + [`${SOURCE_ID_MOCK}1`]: { proposedNames: undefined }, + }, + }); + + provider2.getProposedNames.mockResolvedValue({ results: {}, - error, }); const controller = new NameController({ @@ -854,28 +967,283 @@ describe('NameController', () => { providers: [provider1, provider2], }); - const result = await controller.updateProposedNames({ - value: VALUE_MOCK, - type: NameType.ETHEREUM_ADDRESS, - }); - - expect(controller.state.names).toStrictEqual({ + controller.state.names = { [NameType.ETHEREUM_ADDRESS]: { [VALUE_MOCK]: { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: TIME_MOCK, proposedNames: { - [`${SOURCE_ID_MOCK}2`]: [ - `${PROPOSED_NAME_MOCK}2`, - `${PROPOSED_NAME_MOCK}2_2`, - ], + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeUpdated1'], + lastRequestTime: 11, + updateDelay: 1, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: ['ShouldNotBeUpdated2'], + lastRequestTime: 12, + updateDelay: 2, + }, }, }, }, }, - }); + }; + + await controller.updateProposedNames({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeUpdated1'], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: ['ShouldNotBeUpdated2'], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + }, + }, + }, + }, + }); + }); + + it('result error', async () => { + const provider1 = createMockProvider(1); + + provider1.getProposedNames.mockResolvedValue({ + results: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [PROPOSED_NAME_MOCK], + error: new Error('TestError'), + }, + }, + }); + + const controller = new NameController({ + ...CONTROLLER_ARGS_MOCK, + providers: [provider1], + }); + + controller.state.names = { + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeUpdated1'], + lastRequestTime: 11, + updateDelay: 1, + }, + }, + }, + }, + }, + }; + + await controller.updateProposedNames({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeUpdated1'], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + }, + }, + }, + }, + }); + }); + + it('response error', async () => { + const provider1 = createMockProvider(1); + + provider1.getProposedNames.mockResolvedValue({ + results: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [PROPOSED_NAME_MOCK], + }, + }, + error: new Error('TestError'), + }); + + const controller = new NameController({ + ...CONTROLLER_ARGS_MOCK, + providers: [provider1], + }); + + controller.state.names = { + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeUpdated1'], + lastRequestTime: 11, + updateDelay: 1, + }, + }, + }, + }, + }, + }; + + await controller.updateProposedNames({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeUpdated1'], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + }, + }, + }, + }, + }); + }); + }); + + describe('with error', () => { + it('returns result errors if unhandled error while getting proposed name using provider', async () => { + const provider1 = createMockProvider(1); + const provider2 = createMockProvider(2); + const error = new Error('TestError'); + + provider1.getProposedNames.mockRejectedValue(error); + + const controller = new NameController({ + ...CONTROLLER_ARGS_MOCK, + providers: [provider1, provider2], + }); + + const result = await controller.updateProposedNames({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + }, + }, + }, + }, + }); + + expect(result).toStrictEqual({ + results: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: undefined, + error, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + error: undefined, + }, + }, + }); + }); + + it('returns result errors if response error while getting proposed name using provider', async () => { + const provider1 = createMockProvider(1); + const provider2 = createMockProvider(2); + const error = new Error('TestError'); + + provider1.getProposedNames.mockResolvedValue({ + results: {}, + error, + }); + + const controller = new NameController({ + ...CONTROLLER_ARGS_MOCK, + providers: [provider1, provider2], + }); + + const result = await controller.updateProposedNames({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + }, + }, + }, + }, + }); expect(result).toStrictEqual({ results: { @@ -894,7 +1262,7 @@ describe('NameController', () => { }); }); - it('stores null if result error while getting proposed name using provider', async () => { + it('stores emtpy array if result error while getting proposed name using provider', async () => { const provider1 = createMockProvider(1); const provider2 = createMockProvider(2); const error = new Error('TestError'); @@ -923,13 +1291,20 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: TIME_MOCK, proposedNames: { - [`${SOURCE_ID_MOCK}1`]: null, - [`${SOURCE_ID_MOCK}2`]: [ - `${PROPOSED_NAME_MOCK}2`, - `${PROPOSED_NAME_MOCK}2_2`, - ], + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, }, }, }, @@ -971,11 +1346,22 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: 12, proposedNames: { - [`${SOURCE_ID_MOCK}1`]: ['ShouldNotBeDeleted1'], - [`${SOURCE_ID_MOCK}2`]: ['ShouldBeDeleted2'], - [`${SOURCE_ID_MOCK}3`]: ['ShouldNotBeDeleted3'], + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeDeleted1'], + lastRequestTime: 12, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: ['ShouldBeDeleted2'], + lastRequestTime: 12, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}3`]: { + proposedNames: ['ShouldNotBeDeleted3'], + lastRequestTime: 12, + updateDelay: null, + }, }, }, }, @@ -994,14 +1380,25 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: TIME_MOCK, proposedNames: { - [`${SOURCE_ID_MOCK}1`]: [`ShouldNotBeDeleted1`], - [`${SOURCE_ID_MOCK}2`]: [ - `${PROPOSED_NAME_MOCK}2`, - `${PROPOSED_NAME_MOCK}2_2`, - ], - [`${SOURCE_ID_MOCK}3`]: ['ShouldNotBeDeleted3'], + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [`ShouldNotBeDeleted1`], + lastRequestTime: 12, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}3`]: { + proposedNames: ['ShouldNotBeDeleted3'], + lastRequestTime: 12, + updateDelay: null, + }, }, }, }, @@ -1099,12 +1496,15 @@ describe('NameController', () => { [CHAIN_ID_MOCK]: { name: null, sourceId: null, - proposedNamesLastUpdated: TIME_MOCK, proposedNames: { - [`${SOURCE_ID_MOCK}1`]: [ - `${PROPOSED_NAME_MOCK}1`, - `${PROPOSED_NAME_MOCK}1_2`, - ], + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, }, }, }, @@ -1212,5 +1612,381 @@ describe('NameController', () => { ); }); }); + + describe('with onlyUpdateAfterDelay', () => { + it('does not update if no updateDelay and controller delay not elapsed', async () => { + const provider1 = createMockProvider(1); + const provider2 = createMockProvider(2); + + const controller = new NameController({ + ...CONTROLLER_ARGS_MOCK, + providers: [provider1, provider2], + updateDelay: 123, + }); + + controller.state.names = { + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeUpdated1'], + lastRequestTime: TIME_MOCK - 122, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: ['ShouldNotBeUpdated2'], + lastRequestTime: TIME_MOCK - 121, + updateDelay: null, + }, + }, + }, + }, + }, + }; + + const result = await controller.updateProposedNames({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + onlyUpdateAfterDelay: true, + }); + + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeUpdated1'], + lastRequestTime: TIME_MOCK - 122, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: ['ShouldNotBeUpdated2'], + lastRequestTime: TIME_MOCK - 121, + updateDelay: null, + }, + }, + }, + }, + }, + }); + + expect(result).toStrictEqual({ + results: {}, + }); + }); + + it('does not update if updateDelay not elapsed', async () => { + const provider1 = createMockProvider(1); + const provider2 = createMockProvider(2); + + const controller = new NameController({ + ...CONTROLLER_ARGS_MOCK, + providers: [provider1, provider2], + }); + + controller.state.names = { + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeUpdated1'], + lastRequestTime: TIME_MOCK - 9, + updateDelay: 10, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: ['ShouldNotBeUpdated2'], + lastRequestTime: TIME_MOCK - 6, + updateDelay: 7, + }, + }, + }, + }, + }, + }; + + const result = await controller.updateProposedNames({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + onlyUpdateAfterDelay: true, + }); + + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeUpdated1'], + lastRequestTime: TIME_MOCK - 9, + updateDelay: 10, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: ['ShouldNotBeUpdated2'], + lastRequestTime: TIME_MOCK - 6, + updateDelay: 7, + }, + }, + }, + }, + }, + }); + + expect(result).toStrictEqual({ + results: {}, + }); + }); + + it('updates if controller delay elapsed', async () => { + const provider1 = createMockProvider(1); + const provider2 = createMockProvider(2); + + const controller = new NameController({ + ...CONTROLLER_ARGS_MOCK, + providers: [provider1, provider2], + updateDelay: 123, + }); + + controller.state.names = { + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeUpdated1'], + lastRequestTime: TIME_MOCK - 123, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: ['ShouldNotBeUpdated2'], + lastRequestTime: TIME_MOCK - 124, + updateDelay: null, + }, + }, + }, + }, + }, + }; + + const result = await controller.updateProposedNames({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + onlyUpdateAfterDelay: true, + }); + + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + }, + }, + }, + }, + }); + + expect(result).toStrictEqual({ + results: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + error: undefined, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + error: undefined, + }, + }, + }); + }); + + it('updates if updateDelay elapsed', async () => { + const provider1 = createMockProvider(1); + const provider2 = createMockProvider(2); + + const controller = new NameController({ + ...CONTROLLER_ARGS_MOCK, + providers: [provider1, provider2], + }); + + controller.state.names = { + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: ['ShouldNotBeUpdated1'], + lastRequestTime: TIME_MOCK - 10, + updateDelay: 10, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: ['ShouldNotBeUpdated2'], + lastRequestTime: TIME_MOCK - 16, + updateDelay: 15, + }, + }, + }, + }, + }, + }; + + const result = await controller.updateProposedNames({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + onlyUpdateAfterDelay: true, + }); + + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + }, + }, + }, + }, + }); + + expect(result).toStrictEqual({ + results: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + error: undefined, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + error: undefined, + }, + }, + }); + }); + + it('updates if no proposed name entry', async () => { + const provider1 = createMockProvider(1); + const provider2 = createMockProvider(2); + + const controller = new NameController({ + ...CONTROLLER_ARGS_MOCK, + providers: [provider1, provider2], + }); + + controller.state.names = {} as any; + + const result = await controller.updateProposedNames({ + value: VALUE_MOCK, + type: NameType.ETHEREUM_ADDRESS, + onlyUpdateAfterDelay: true, + }); + + expect(controller.state.names).toStrictEqual({ + [NameType.ETHEREUM_ADDRESS]: { + [VALUE_MOCK]: { + [CHAIN_ID_MOCK]: { + name: null, + sourceId: null, + proposedNames: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + lastRequestTime: TIME_MOCK, + updateDelay: null, + }, + }, + }, + }, + }, + }); + + expect(result).toStrictEqual({ + results: { + [`${SOURCE_ID_MOCK}1`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}1`, + `${PROPOSED_NAME_MOCK}1_2`, + ], + error: undefined, + }, + [`${SOURCE_ID_MOCK}2`]: { + proposedNames: [ + `${PROPOSED_NAME_MOCK}2`, + `${PROPOSED_NAME_MOCK}2_2`, + ], + error: undefined, + }, + }, + }); + }); + }); }); }); diff --git a/packages/name-controller/src/NameController.ts b/packages/name-controller/src/NameController.ts index f4475a178bb..cb357dcf080 100644 --- a/packages/name-controller/src/NameController.ts +++ b/packages/name-controller/src/NameController.ts @@ -6,9 +6,12 @@ import type { NameProvider, NameProviderRequest, NameProviderResult, + NameProviderSourceResult, } from './types'; import { NameType } from './types'; +const DEFAULT_UPDATE_DELAY = 60 * 2; // 2 Minutes + const controllerName = 'NameController'; const stateMetadata = { @@ -23,11 +26,16 @@ const getDefaultState = () => ({ nameSources: {}, }); +export type ProposedNamesEntry = { + proposedNames: string[]; + lastRequestTime: number | null; + updateDelay: number | null; +}; + export type NameEntry = { name: string | null; sourceId: string | null; - proposedNames: Record; - proposedNamesLastUpdated: number | null; + proposedNames: Record; }; export type SourceEntry = { @@ -67,12 +75,14 @@ export type NameControllerOptions = { messenger: NameControllerMessenger; providers: NameProvider[]; state?: Partial; + updateDelay?: number; }; export type UpdateProposedNamesRequest = { value: string; type: NameType; sourceIds?: string[]; + onlyUpdateAfterDelay?: boolean; }; export type UpdateProposedNamesResult = { @@ -98,6 +108,8 @@ export class NameController extends BaseControllerV2< #providers: NameProvider[]; + #updateDelay: number; + /** * Construct a Name controller. * @@ -106,12 +118,14 @@ export class NameController extends BaseControllerV2< * @param options.messenger - Restricted controller messenger for the name controller. * @param options.providers - Array of name provider instances to propose names. * @param options.state - Initial state to set on the controller. + * @param options.updateDelay - The delay in seconds before a new request to a source should be made. */ constructor({ getChainId, messenger, providers, state, + updateDelay, }: NameControllerOptions) { super({ name: controllerName, @@ -122,6 +136,7 @@ export class NameController extends BaseControllerV2< this.#getChainId = getChainId; this.#providers = providers; + this.#updateDelay = updateDelay ?? DEFAULT_UPDATE_DELAY; } /** @@ -139,7 +154,10 @@ export class NameController extends BaseControllerV2< const { value, type, name, sourceId: requestSourceId } = request; const sourceId = requestSourceId ?? null; - this.#updateEntry(value, type, { name, sourceId }); + this.#updateEntry(value, type, (entry: NameEntry) => { + entry.name = name; + entry.sourceId = sourceId; + }); } /** @@ -177,46 +195,35 @@ export class NameController extends BaseControllerV2< providerResponses: NameProviderResult[], ) { const { value, type } = request; - const newProposedNames: { [sourceId: string]: string[] | null } = {}; - - for (const providerResponse of providerResponses) { - const { results, error: responseError } = providerResponse; + const currentTime = this.#getCurrentTimeSeconds(); - if (responseError) { - continue; - } + this.#updateEntry(value, type, (entry: NameEntry) => { + this.#removeDormantProposedNames(entry.proposedNames, type); - for (const sourceId of Object.keys(providerResponse.results)) { - const result = results[sourceId]; - const { proposedNames } = result; - let finalProposedNames = result.error ? null : proposedNames ?? []; - - if (finalProposedNames) { - finalProposedNames = finalProposedNames.filter( - (proposedName) => proposedName?.length, - ); - } - - newProposedNames[sourceId] = finalProposedNames; - } - } - - const variationKey = this.#getTypeVariationKey(type); + for (const providerResponse of providerResponses) { + const { results } = providerResponse; - const existingProposedNames = - this.state.names[type]?.[value]?.[variationKey]?.proposedNames; + for (const sourceId of Object.keys(providerResponse.results)) { + const result = results[sourceId]; + const { proposedNames, updateDelay } = result; - const existingProposedNamesWithoutDormant = - this.#removeDormantProposedNames(existingProposedNames, type); + const proposedNameEntry = entry.proposedNames[sourceId] ?? { + proposedNames: [], + lastRequestTime: null, + updateDelay: null, + }; - const proposedNames = { - ...existingProposedNamesWithoutDormant, - ...newProposedNames, - }; + entry.proposedNames[sourceId] = proposedNameEntry; - const proposedNamesLastUpdated = this.#getCurrentTimeSeconds(); + if (proposedNames) { + proposedNameEntry.proposedNames = proposedNames; + } - this.#updateEntry(value, type, { proposedNames, proposedNamesLastUpdated }); + proposedNameEntry.lastRequestTime = currentTime; + proposedNameEntry.updateDelay = updateDelay ?? null; + } + } + }); } #updateSourceState(providers: NameProvider[]) { @@ -245,22 +252,11 @@ export class NameController extends BaseControllerV2< const { results } = providerResponse; for (const sourceId of Object.keys(results)) { - const { proposedNames: resultProposedNames, error: resultError } = - results[sourceId]; - - let proposedNames = resultError - ? undefined - : resultProposedNames ?? []; - - if (proposedNames) { - proposedNames = proposedNames.filter( - (proposedName) => proposedName?.length, - ); - } + const { proposedNames, error } = results[sourceId]; acc.results[sourceId] = { proposedNames, - error: resultError, + error, }; } @@ -275,15 +271,37 @@ export class NameController extends BaseControllerV2< chainId: string, provider: NameProvider, ): Promise { - const { value, type, sourceIds: requestedSourceIds } = request; + const { + value, + type, + sourceIds: requestedSourceIds, + onlyUpdateAfterDelay, + } = request; + + const variationKey = this.#getTypeVariationKey(type); const supportedSourceIds = this.#getSourceIds(provider, type); + const currentTime = this.#getCurrentTimeSeconds(); + + const matchingSourceIds = supportedSourceIds.filter((sourceId) => { + if (requestedSourceIds && !requestedSourceIds.includes(sourceId)) { + return false; + } + + if (onlyUpdateAfterDelay) { + const entry = this.state.names[type]?.[value]?.[variationKey] ?? {}; + const proposedNamesEntry = entry.proposedNames?.[sourceId] ?? {}; + const lastRequestTime = proposedNamesEntry.lastRequestTime ?? 0; + const updateDelay = proposedNamesEntry.updateDelay ?? this.#updateDelay; + + if (currentTime - lastRequestTime < updateDelay) { + return false; + } + } - const matchingSourceIds = - requestedSourceIds?.filter((sourceId) => - supportedSourceIds.includes(sourceId), - ) ?? supportedSourceIds; + return true; + }); - if (requestedSourceIds && !matchingSourceIds.length) { + if (!matchingSourceIds.length) { return undefined; } @@ -304,35 +322,63 @@ export class NameController extends BaseControllerV2< responseError = error; } - let results = {}; + return this.#normalizeProviderResult( + response, + responseError, + matchingSourceIds, + ); + } - if (response?.results) { - results = Object.keys(response.results).reduce( - (acc: NameProviderResult['results'], sourceId) => { - if (!requestedSourceIds || requestedSourceIds.includes(sourceId)) { - acc[sourceId] = (response as NameProviderResult).results[sourceId]; - } + #normalizeProviderResult( + result: NameProviderResult | undefined, + responseError: unknown, + matchingSourceIds: string[], + ): NameProviderResult { + const error = responseError ?? undefined; + + const results = matchingSourceIds.reduce((acc, sourceId) => { + const sourceResult = result?.results?.[sourceId]; - return acc; - }, - {}, + const normalizedSourceResult = this.#normalizeProviderSourceResult( + sourceResult, + responseError, ); - } - if (responseError) { - results = supportedSourceIds.reduce( - (acc: NameProviderResult['results'], sourceId) => { - acc[sourceId] = { proposedNames: [], error: responseError }; - return acc; - }, - {}, + return { + ...acc, + [sourceId]: normalizedSourceResult, + }; + }, {}); + + return { results, error }; + } + + #normalizeProviderSourceResult( + result: NameProviderSourceResult | undefined, + responseError: unknown, + ): NameProviderSourceResult | undefined { + const error = result?.error ?? responseError ?? undefined; + const updateDelay = result?.updateDelay ?? undefined; + let proposedNames = error ? undefined : result?.proposedNames ?? undefined; + + if (proposedNames) { + proposedNames = proposedNames.filter( + (proposedName) => proposedName?.length, ); } - return { results, error: responseError }; + return { + proposedNames, + error, + updateDelay, + }; } - #updateEntry(value: string, type: NameType, data: Partial) { + #updateEntry( + value: string, + type: NameType, + callback: (entry: NameEntry) => void, + ) { const variationKey = this.#getTypeVariationKey(type); this.update((state) => { @@ -342,16 +388,14 @@ export class NameController extends BaseControllerV2< const variationEntries = typeEntries[value] || {}; typeEntries[value] = variationEntries; - const currentEntry = variationEntries[variationKey] ?? { + const entry = variationEntries[variationKey] ?? { proposedNames: {}, - proposedNamesLastUpdated: null, name: null, sourceId: null, }; + variationEntries[variationKey] = entry; - const updatedEntry = { ...currentEntry, ...data }; - - variationEntries[variationKey] = updatedEntry; + callback(entry); }); } @@ -506,23 +550,21 @@ export class NameController extends BaseControllerV2< } #removeDormantProposedNames( - proposedNames: Record, + proposedNames: Record, type: NameType, - ): Record { - if (!proposedNames || Object.keys(proposedNames).length === 0) { - return proposedNames; + ) { + if (Object.keys(proposedNames).length === 0) { + return; } const typeSourceIds = this.#getAllSourceIds(type); - return Object.keys(proposedNames) - .filter((sourceId) => typeSourceIds.includes(sourceId)) - .reduce( - (acc, sourceId) => ({ - ...acc, - [sourceId]: proposedNames[sourceId], - }), - {}, - ); + const dormantSourceIds = Object.keys(proposedNames).filter( + (sourceId) => !typeSourceIds.includes(sourceId), + ); + + for (const dormantSourceId of dormantSourceIds) { + delete proposedNames[dormantSourceId]; + } } } diff --git a/packages/name-controller/src/logger.ts b/packages/name-controller/src/logger.ts new file mode 100644 index 00000000000..4e7415f91e4 --- /dev/null +++ b/packages/name-controller/src/logger.ts @@ -0,0 +1,5 @@ +import { createProjectLogger, createModuleLogger } from '@metamask/utils'; + +export const projectLogger = createProjectLogger('name-controller'); + +export { createModuleLogger }; diff --git a/packages/name-controller/src/providers/ens.test.ts b/packages/name-controller/src/providers/ens.test.ts index 33419a8c37b..7e28f069a3c 100644 --- a/packages/name-controller/src/providers/ens.test.ts +++ b/packages/name-controller/src/providers/ens.test.ts @@ -66,5 +66,40 @@ describe('ENSNameProvider', () => { expect(reverseLookupMock).toHaveBeenCalledTimes(1); expect(reverseLookupMock).toHaveBeenCalledWith(VALUE_MOCK, CHAIN_ID_MOCK); }); + + it('returns empty result if disabled', async () => { + const provider = new ENSNameProvider({ + ...CONSTRUCTOR_ARGS_MOCK, + isEnabled: () => false, + }); + + const response = await provider.getProposedNames({ + value: VALUE_MOCK, + chainId: CHAIN_ID_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + expect(response).toStrictEqual({ + results: { [SOURCE_ID]: { proposedNames: [] } }, + }); + }); + + it('throws if callback fails', async () => { + const reverseLookupMock = jest.fn().mockImplementation(() => { + throw new Error('TestError'); + }); + + const provider = new ENSNameProvider({ + reverseLookup: reverseLookupMock, + }); + + await expect( + provider.getProposedNames({ + value: VALUE_MOCK, + chainId: CHAIN_ID_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }), + ).rejects.toThrow('TestError'); + }); }); }); diff --git a/packages/name-controller/src/providers/ens.ts b/packages/name-controller/src/providers/ens.ts index 5ca36ca4548..4614b4963c8 100644 --- a/packages/name-controller/src/providers/ens.ts +++ b/packages/name-controller/src/providers/ens.ts @@ -1,3 +1,4 @@ +import { projectLogger, createModuleLogger } from '../logger'; import type { NameProvider, NameProviderMetadata, @@ -14,10 +15,21 @@ export type ReverseLookupCallback = ( const ID = 'ens'; const LABEL = 'Ethereum Name Service (ENS)'; +const log = createModuleLogger(projectLogger, 'ens'); + export class ENSNameProvider implements NameProvider { + #isEnabled: () => boolean; + #reverseLookup: ReverseLookupCallback; - constructor({ reverseLookup }: { reverseLookup: ReverseLookupCallback }) { + constructor({ + isEnabled, + reverseLookup, + }: { + isEnabled?: () => boolean; + reverseLookup: ReverseLookupCallback; + }) { + this.#isEnabled = isEnabled || (() => true); this.#reverseLookup = reverseLookup; } @@ -31,13 +43,36 @@ export class ENSNameProvider implements NameProvider { async getProposedNames( request: NameProviderRequest, ): Promise { + if (!this.#isEnabled()) { + log('Skipping request as disabled'); + + return { + results: { + [ID]: { + proposedNames: [], + }, + }, + }; + } + const { value, chainId } = request; - const proposedName = await this.#reverseLookup(value, chainId); - return { - results: { - [ID]: { proposedNames: [proposedName] }, - }, - }; + log('Invoking callback', { value, chainId }); + + try { + const proposedName = await this.#reverseLookup(value, chainId); + const proposedNames = proposedName ? [proposedName] : []; + + log('New proposed names', proposedNames); + + return { + results: { + [ID]: { proposedNames }, + }, + }; + } catch (error) { + log('Request failed', error); + throw error; + } } } diff --git a/packages/name-controller/src/providers/etherscan.test.ts b/packages/name-controller/src/providers/etherscan.test.ts index 5ec87ad97b5..eef1dbf87ad 100644 --- a/packages/name-controller/src/providers/etherscan.test.ts +++ b/packages/name-controller/src/providers/etherscan.test.ts @@ -10,7 +10,6 @@ const CHAIN_ID_MOCK = '0x1'; const SOURCE_ID = 'etherscan'; const CONTRACT_NAME_MOCK = 'TestContractName'; const CONTRACT_NAME_2_MOCK = 'TestContractName2'; -const API_KEY_MOCK = 'TestApiKey'; describe('EtherscanNameProvider', () => { const handleFetchMock = jest.mocked(handleFetch); @@ -95,8 +94,8 @@ describe('EtherscanNameProvider', () => { }, ); - it('includes API key in requested URL if provided', async () => { - const provider = new EtherscanNameProvider({ apiKey: API_KEY_MOCK }); + it('requests alternate URL based on chain ID', async () => { + const provider = new EtherscanNameProvider(); handleFetchMock.mockResolvedValueOnce({ result: [ @@ -111,55 +110,107 @@ describe('EtherscanNameProvider', () => { await provider.getProposedNames({ value: VALUE_MOCK, - chainId: CHAIN_ID_MOCK, + chainId: CHAIN_IDS.LINEA_GOERLI, type: NameType.ETHEREUM_ADDRESS, }); expect(handleFetchMock).toHaveBeenCalledTimes(1); expect(handleFetchMock).toHaveBeenCalledWith( - `https://api.etherscan.io/api?module=contract&action=getsourcecode&address=${VALUE_MOCK}&apikey=${API_KEY_MOCK}`, + `https://goerli.lineascan.build/api?module=contract&action=getsourcecode&address=${VALUE_MOCK}`, ); }); - it('requests alternate URL based on chain ID', async () => { + it('throws if chain ID not supported', async () => { + const invalidChainId = '0x0'; const provider = new EtherscanNameProvider(); - handleFetchMock.mockResolvedValueOnce({ - result: [ - { - ContractName: CONTRACT_NAME_MOCK, + await expect( + provider.getProposedNames({ + value: VALUE_MOCK, + chainId: invalidChainId, + type: NameType.ETHEREUM_ADDRESS, + }), + ).rejects.toThrow( + `Etherscan does not support chain with ID: ${invalidChainId}`, + ); + }); + + it('returns delay only if within rate limit interval', async () => { + const provider = new EtherscanNameProvider(); + + await provider.getProposedNames({ + value: VALUE_MOCK, + chainId: CHAIN_ID_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + const result = await provider.getProposedNames({ + value: VALUE_MOCK, + chainId: CHAIN_ID_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + expect(result).toStrictEqual({ + results: { + [SOURCE_ID]: { + updateDelay: 5, }, - { - ContractName: CONTRACT_NAME_2_MOCK, + }, + }); + }); + + it('returns delay only if request has warning', async () => { + const provider = new EtherscanNameProvider(); + + handleFetchMock.mockResolvedValueOnce({ + message: 'NOTOK', + }); + + const result = await provider.getProposedNames({ + value: VALUE_MOCK, + chainId: CHAIN_ID_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + expect(result).toStrictEqual({ + results: { + [SOURCE_ID]: { + updateDelay: 5, }, - ], + }, }); + }); - await provider.getProposedNames({ + it('returns empty result if disabled', async () => { + const provider = new EtherscanNameProvider({ + isEnabled: () => false, + }); + + const response = await provider.getProposedNames({ value: VALUE_MOCK, - chainId: CHAIN_IDS.LINEA_GOERLI, + chainId: CHAIN_ID_MOCK, type: NameType.ETHEREUM_ADDRESS, }); - expect(handleFetchMock).toHaveBeenCalledTimes(1); - expect(handleFetchMock).toHaveBeenCalledWith( - `https://goerli.lineascan.build/api?module=contract&action=getsourcecode&address=${VALUE_MOCK}`, - ); + expect(response).toStrictEqual({ + results: { [SOURCE_ID]: { proposedNames: [] } }, + }); }); - it('throws if chain ID not supported', async () => { - const invalidChainId = '0x0'; + it('throws if request fails', async () => { const provider = new EtherscanNameProvider(); + handleFetchMock.mockImplementation(() => { + throw new Error('TestError'); + }); + await expect( provider.getProposedNames({ value: VALUE_MOCK, - chainId: invalidChainId, + chainId: CHAIN_ID_MOCK, type: NameType.ETHEREUM_ADDRESS, }), - ).rejects.toThrow( - `Etherscan does not support chain with ID: ${invalidChainId}`, - ); + ).rejects.toThrow('TestError'); }); }); }); diff --git a/packages/name-controller/src/providers/etherscan.ts b/packages/name-controller/src/providers/etherscan.ts index b4b23736602..aa048922422 100644 --- a/packages/name-controller/src/providers/etherscan.ts +++ b/packages/name-controller/src/providers/etherscan.ts @@ -1,4 +1,7 @@ +import { Mutex } from 'async-mutex'; + import { ETHERSCAN_SUPPORTED_NETWORKS } from '../constants'; +import { createModuleLogger, projectLogger } from '../logger'; import type { NameProvider, NameProviderMetadata, @@ -10,6 +13,10 @@ import { handleFetch } from '../util'; const ID = 'etherscan'; const LABEL = 'Etherscan (Verified Contract Name)'; +const RATE_LIMIT_UPDATE_DELAY = 5; // 5 Seconds +const RATE_LIMIT_INTERVAL = RATE_LIMIT_UPDATE_DELAY * 1000; + +const log = createModuleLogger(projectLogger, 'etherscan'); type EtherscanGetSourceCodeResponse = { status: '1' | '0'; @@ -33,10 +40,14 @@ type EtherscanGetSourceCodeResponse = { }; export class EtherscanNameProvider implements NameProvider { - #apiKey?: string; + #isEnabled: () => boolean; + + #lastRequestTime = 0; + + #mutex = new Mutex(); - constructor({ apiKey }: { apiKey?: string } = {}) { - this.#apiKey = apiKey; + constructor({ isEnabled }: { isEnabled?: () => boolean } = {}) { + this.#isEnabled = isEnabled || (() => true); } getMetadata(): NameProviderMetadata { @@ -49,29 +60,94 @@ export class EtherscanNameProvider implements NameProvider { async getProposedNames( request: NameProviderRequest, ): Promise { - const { value, chainId } = request; + if (!this.#isEnabled()) { + log('Skipping request as disabled'); + + return { + results: { + [ID]: { + proposedNames: [], + }, + }, + }; + } - const url = this.#getUrl(chainId, { - module: 'contract', - action: 'getsourcecode', - address: value, - apikey: this.#apiKey, - }); + const releaseLock = await this.#mutex.acquire(); - const responseData = (await handleFetch( - url, - )) as EtherscanGetSourceCodeResponse; + try { + const { value, chainId } = request; - const results = responseData?.result ?? []; - const proposedNames = results.map((result) => result.ContractName); + const time = Date.now(); + const timeSinceLastRequest = time - this.#lastRequestTime; - return { - results: { - [ID]: { - proposedNames, + if (timeSinceLastRequest < RATE_LIMIT_INTERVAL) { + log('Skipping request to avoid rate limit'); + + return { + results: { + [ID]: { + updateDelay: RATE_LIMIT_UPDATE_DELAY, + }, + }, + }; + } + + const url = this.#getUrl(chainId, { + module: 'contract', + action: 'getsourcecode', + address: value, + }); + + const { responseData, error } = await this.#sendRequest(url); + + if (error) { + log('Request failed', error); + throw error; + } + + if (responseData?.message === 'NOTOK') { + log('Request warning', responseData.result); + + return { + results: { + [ID]: { + updateDelay: RATE_LIMIT_UPDATE_DELAY, + }, + }, + }; + } + + const results = responseData?.result ?? []; + const proposedNames = results.map((result) => result.ContractName); + + log('New proposed names', proposedNames); + + return { + results: { + [ID]: { + proposedNames, + }, }, - }, - }; + }; + } finally { + releaseLock(); + } + } + + async #sendRequest(url: string) { + try { + log('Sending request', url); + + const responseData = (await handleFetch( + url, + )) as EtherscanGetSourceCodeResponse; + + return { responseData }; + } catch (error) { + return { error }; + } finally { + this.#lastRequestTime = Date.now(); + } } #getUrl(chainId: string, params: Record): string { @@ -88,11 +164,6 @@ export class EtherscanNameProvider implements NameProvider { Object.keys(params).forEach((key, index) => { const value = params[key]; - - if (!value) { - return; - } - url += `${index === 0 ? '?' : '&'}${key}=${value}`; }); diff --git a/packages/name-controller/src/providers/lens.test.ts b/packages/name-controller/src/providers/lens.test.ts index c22141121ac..05821a6d272 100644 --- a/packages/name-controller/src/providers/lens.test.ts +++ b/packages/name-controller/src/providers/lens.test.ts @@ -86,5 +86,37 @@ describe('LensNameProvider', () => { }, }); }); + + it('returns empty result if disabled', async () => { + const provider = new LensNameProvider({ + isEnabled: () => false, + }); + + const response = await provider.getProposedNames({ + value: VALUE_MOCK, + chainId: CHAIN_ID_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + expect(response).toStrictEqual({ + results: { [SOURCE_ID]: { proposedNames: [] } }, + }); + }); + + it('throws if request fails', async () => { + graphqlMock.mockImplementation(() => { + throw new Error('TestError'); + }); + + const provider = new LensNameProvider(); + + await expect( + provider.getProposedNames({ + value: VALUE_MOCK, + chainId: CHAIN_ID_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }), + ).rejects.toThrow('TestError'); + }); }); }); diff --git a/packages/name-controller/src/providers/lens.ts b/packages/name-controller/src/providers/lens.ts index 939499bed86..36e05744806 100644 --- a/packages/name-controller/src/providers/lens.ts +++ b/packages/name-controller/src/providers/lens.ts @@ -1,3 +1,4 @@ +import { createModuleLogger, projectLogger } from '../logger'; import type { NameProvider, NameProviderMetadata, @@ -20,6 +21,8 @@ query HandlesForAddress($address: EthereumAddress!) { } }`; +const log = createModuleLogger(projectLogger, 'lens'); + type LensResponse = { profiles: { items: [ @@ -31,6 +34,12 @@ type LensResponse = { }; export class LensNameProvider implements NameProvider { + #isEnabled: () => boolean; + + constructor({ isEnabled }: { isEnabled?: () => boolean } = {}) { + this.#isEnabled = isEnabled || (() => true); + } + getMetadata(): NameProviderMetadata { return { sourceIds: { [NameType.ETHEREUM_ADDRESS]: [ID] }, @@ -41,21 +50,45 @@ export class LensNameProvider implements NameProvider { async getProposedNames( request: NameProviderRequest, ): Promise { + if (!this.#isEnabled()) { + log('Skipping request as disabled'); + + return { + results: { + [ID]: { + proposedNames: [], + }, + }, + }; + } + const { value } = request; + const variables = { address: value }; - const responseData = await graphQL(LENS_URL, QUERY, { - address: value, - }); + log('Sending request', { variables }); - const profiles = responseData?.profiles?.items ?? []; - const proposedNames = profiles.map((profile) => profile.handle); + try { + const responseData = await graphQL( + LENS_URL, + QUERY, + variables, + ); - return { - results: { - [ID]: { - proposedNames, + const profiles = responseData?.profiles?.items ?? []; + const proposedNames = profiles.map((profile) => profile.handle); + + log('New proposed names', proposedNames); + + return { + results: { + [ID]: { + proposedNames, + }, }, - }, - }; + }; + } catch (error) { + log('Request failed', error); + throw error; + } } } diff --git a/packages/name-controller/src/providers/token.test.ts b/packages/name-controller/src/providers/token.test.ts index 421bf03c690..6e17f581d0e 100644 --- a/packages/name-controller/src/providers/token.test.ts +++ b/packages/name-controller/src/providers/token.test.ts @@ -54,5 +54,55 @@ describe('TokenNameProvider', () => { `https://token-api.metaswap.codefi.network/token/${CHAIN_ID_MOCK}?address=${VALUE_MOCK}`, ); }); + + it('returns empty array if no token name in infura response', async () => { + const provider = new TokenNameProvider(); + + handleFetchMock.mockResolvedValueOnce({ name: undefined }); + + const response = await provider.getProposedNames({ + value: VALUE_MOCK, + chainId: CHAIN_ID_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + expect(response).toStrictEqual({ + results: { + [SOURCE_ID]: { proposedNames: [] }, + }, + }); + }); + + it('returns empty result if disabled', async () => { + const provider = new TokenNameProvider({ + isEnabled: () => false, + }); + + const response = await provider.getProposedNames({ + value: VALUE_MOCK, + chainId: CHAIN_ID_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }); + + expect(response).toStrictEqual({ + results: { [SOURCE_ID]: { proposedNames: [] } }, + }); + }); + + it('throws if request fails', async () => { + handleFetchMock.mockImplementation(() => { + throw new Error('TestError'); + }); + + const provider = new TokenNameProvider(); + + await expect( + provider.getProposedNames({ + value: VALUE_MOCK, + chainId: CHAIN_ID_MOCK, + type: NameType.ETHEREUM_ADDRESS, + }), + ).rejects.toThrow('TestError'); + }); }); }); diff --git a/packages/name-controller/src/providers/token.ts b/packages/name-controller/src/providers/token.ts index 0d55f3372ba..f52ad19ed7a 100644 --- a/packages/name-controller/src/providers/token.ts +++ b/packages/name-controller/src/providers/token.ts @@ -1,3 +1,4 @@ +import { createModuleLogger, projectLogger } from '../logger'; import type { NameProvider, NameProviderMetadata, @@ -10,7 +11,15 @@ import { handleFetch } from '../util'; const ID = 'token'; const LABEL = 'Blockchain (Token Name)'; +const log = createModuleLogger(projectLogger, 'token'); + export class TokenNameProvider implements NameProvider { + #isEnabled: () => boolean; + + constructor({ isEnabled }: { isEnabled?: () => boolean } = {}) { + this.#isEnabled = isEnabled || (() => true); + } + getMetadata(): NameProviderMetadata { return { sourceIds: { [NameType.ETHEREUM_ADDRESS]: [ID] }, @@ -21,17 +30,40 @@ export class TokenNameProvider implements NameProvider { async getProposedNames( request: NameProviderRequest, ): Promise { + if (!this.#isEnabled()) { + log('Skipping request as disabled'); + + return { + results: { + [ID]: { + proposedNames: [], + }, + }, + }; + } + const { value, chainId } = request; const url = `https://token-api.metaswap.codefi.network/token/${chainId}?address=${value}`; - const responseData = await handleFetch(url); - const proposedName = responseData.name; - return { - results: { - [ID]: { - proposedNames: [proposedName], + log('Sending request', url); + + try { + const responseData = await handleFetch(url); + const proposedName = responseData.name; + const proposedNames = proposedName ? [proposedName] : []; + + log('New proposed names', proposedNames); + + return { + results: { + [ID]: { + proposedNames, + }, }, - }, - }; + }; + } catch (error) { + log('Request failed', error); + throw error; + } } } diff --git a/packages/name-controller/src/types.ts b/packages/name-controller/src/types.ts index 24c506f93ac..0adba9c136f 100644 --- a/packages/name-controller/src/types.ts +++ b/packages/name-controller/src/types.ts @@ -42,6 +42,12 @@ export type NameProviderSourceResult = { */ proposedNames?: string[]; + /** + * The delay in seconds before the next request to the source should be made. + * Can be used to avoid rate limiting for example. + */ + updateDelay?: number; + /** * An error that occurred while fetching the proposed names from the source. * Undefined if there was no error. diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 70e8b2b5867..a244a7ff6e1 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [13.0.0] +### Changed +- **BREAKING**: Remove `NetworkId` type ([#1633](https://github.com/MetaMask/core/pull/1633)) +- **BREAKING**: Remove `networkId` property from `NetworkState` type ([#1633](https://github.com/MetaMask/core/pull/1633)) +- Update scaffold RPC middleware for built-in Infura networks to no longer resolve `net_version` locally ([#1633](https://github.com/MetaMask/core/pull/1633)) +- Stop making `net_version` request to determine network status ([#1633](https://github.com/MetaMask/core/pull/1633)) +- Bump dependency on `@metamask/controller-utils` to ^5.0.0 + ## [12.2.0] ### Added - Add `NetworkController:getNetworkClientById` action ([#1638](https://github.com/MetaMask/core/pull/1638)) @@ -253,7 +261,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/network-controller@12.2.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/network-controller@13.0.0...HEAD +[13.0.0]: https://github.com/MetaMask/core/compare/@metamask/network-controller@12.2.0...@metamask/network-controller@13.0.0 [12.2.0]: https://github.com/MetaMask/core/compare/@metamask/network-controller@12.1.2...@metamask/network-controller@12.2.0 [12.1.2]: https://github.com/MetaMask/core/compare/@metamask/network-controller@12.1.1...@metamask/network-controller@12.1.2 [12.1.1]: https://github.com/MetaMask/core/compare/@metamask/network-controller@12.1.0...@metamask/network-controller@12.1.1 diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index 43035c18f61..ebdd2a9a627 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/network-controller", - "version": "12.2.0", + "version": "13.0.0", "description": "Provides an interface to the currently selected network via a MetaMask-compatible provider object", "keywords": [ "MetaMask", @@ -29,7 +29,7 @@ }, "dependencies": { "@metamask/base-controller": "^3.2.1", - "@metamask/controller-utils": "^4.3.2", + "@metamask/controller-utils": "^5.0.0", "@metamask/eth-json-rpc-infura": "^8.1.1", "@metamask/eth-json-rpc-middleware": "^11.0.2", "@metamask/eth-json-rpc-provider": "^1.0.0", @@ -56,8 +56,8 @@ "nock": "^13.3.1", "sinon": "^9.2.4", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/notification-controller/package.json b/packages/notification-controller/package.json index 935e5b8a8e7..32cabd5c1a2 100644 --- a/packages/notification-controller/package.json +++ b/packages/notification-controller/package.json @@ -39,8 +39,8 @@ "deepmerge": "^4.2.2", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/permission-controller/CHANGELOG.md b/packages/permission-controller/CHANGELOG.md index d56114e70fa..7e11e7f4316 100644 --- a/packages/permission-controller/CHANGELOG.md +++ b/packages/permission-controller/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [4.1.2] +### Changed +- Bump dependency on `@metamask/controller-utils` to ^5.0.0 + ## [4.1.1] ### Changed - Bump dependency and peer dependency on `@metamask/approval-controller` to ^3.5.1 @@ -79,7 +83,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@4.1.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@4.1.2...HEAD +[4.1.2]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@4.1.1...@metamask/permission-controller@4.1.2 [4.1.1]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@4.1.0...@metamask/permission-controller@4.1.1 [4.1.0]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@4.0.1...@metamask/permission-controller@4.1.0 [4.0.1]: https://github.com/MetaMask/core/compare/@metamask/permission-controller@4.0.0...@metamask/permission-controller@4.0.1 diff --git a/packages/permission-controller/README.md b/packages/permission-controller/README.md index b05c2240073..9d3e8f0edd9 100644 --- a/packages/permission-controller/README.md +++ b/packages/permission-controller/README.md @@ -1,6 +1,6 @@ # `@metamask/permission-controller` -Mediates access to JSON-RPC methods, used to interact with pieces of the MetaMask stack, via middleware for `json-rpc-engine`. +Mediates access to JSON-RPC methods, used to interact with pieces of the MetaMask stack, via middleware for `@metamask/json-rpc-engine`. ## Installation diff --git a/packages/permission-controller/package.json b/packages/permission-controller/package.json index 9a96e8cf5d9..93532f56115 100644 --- a/packages/permission-controller/package.json +++ b/packages/permission-controller/package.json @@ -30,12 +30,12 @@ "dependencies": { "@metamask/approval-controller": "^3.5.1", "@metamask/base-controller": "^3.2.1", - "@metamask/controller-utils": "^4.3.2", + "@metamask/controller-utils": "^5.0.0", "@metamask/json-rpc-engine": "^7.1.1", + "@metamask/rpc-errors": "^6.0.0", "@metamask/utils": "^8.1.0", "@types/deep-freeze-strict": "^1.1.0", "deep-freeze-strict": "^1.1.1", - "eth-rpc-errors": "^4.0.2", "immer": "^9.0.6", "nanoid": "^3.1.31" }, @@ -45,8 +45,8 @@ "deepmerge": "^4.2.2", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "peerDependencies": { diff --git a/packages/permission-controller/src/Permission.ts b/packages/permission-controller/src/Permission.ts index 36cd03ff025..c9951630f0e 100644 --- a/packages/permission-controller/src/Permission.ts +++ b/packages/permission-controller/src/Permission.ts @@ -3,7 +3,7 @@ import type { EventConstraint, } from '@metamask/base-controller'; import type { NonEmptyArray } from '@metamask/controller-utils'; -import type { Json, JsonRpcParams } from '@metamask/utils'; +import type { Json } from '@metamask/utils'; import { nanoid } from 'nanoid'; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -210,19 +210,20 @@ type RestrictedMethodContext = Readonly<{ [key: string]: any; }>; -export type RestrictedMethodParameters = JsonRpcParams; +export type RestrictedMethodParameters = Json[] | Record; /** * The arguments passed to a restricted method implementation. * * @template Params - The JSON-RPC parameters of the restricted method. */ -export type RestrictedMethodOptions = - { - method: TargetName; - params?: Params; - context: RestrictedMethodContext; - }; +export type RestrictedMethodOptions< + Params extends RestrictedMethodParameters | null, +> = { + method: TargetName; + params?: Params; + context: RestrictedMethodContext; +}; /** * A synchronous restricted method implementation. diff --git a/packages/permission-controller/src/PermissionController.test.ts b/packages/permission-controller/src/PermissionController.test.ts index 841315b7193..d32f3fd22a9 100644 --- a/packages/permission-controller/src/PermissionController.test.ts +++ b/packages/permission-controller/src/PermissionController.test.ts @@ -7,12 +7,8 @@ import type { import { ControllerMessenger } from '@metamask/base-controller'; import { isPlainObject } from '@metamask/controller-utils'; import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import type { Json, PendingJsonRpcResponse } from '@metamask/utils'; import { hasProperty } from '@metamask/utils'; -import type { - Json, - PendingJsonRpcResponse, - JsonRpcParams, -} from '@metamask/utils'; import assert from 'assert'; import type { @@ -254,7 +250,7 @@ function getDefaultPermissionSpecifications() { CaveatTypes.filterArrayResponse, CaveatTypes.reverseArrayResponse, ], - methodImplementation: (_args: RestrictedMethodOptions) => { + methodImplementation: (_args: RestrictedMethodOptions) => { return ['a', 'b', 'c']; }, }, @@ -265,7 +261,9 @@ function getDefaultPermissionSpecifications() { CaveatTypes.filterObjectResponse, CaveatTypes.noopCaveat, ], - methodImplementation: (_args: RestrictedMethodOptions) => { + methodImplementation: ( + _args: RestrictedMethodOptions>, + ) => { return { a: 'x', b: 'y', c: 'z' }; }, validator: (permission: PermissionConstraint) => { @@ -295,7 +293,7 @@ function getDefaultPermissionSpecifications() { permissionType: PermissionType.RestrictedMethod, targetName: PermissionKeys.wallet_noop, allowedCaveats: null, - methodImplementation: (_args: RestrictedMethodOptions) => { + methodImplementation: (_args: RestrictedMethodOptions) => { return null; }, }, @@ -303,7 +301,7 @@ function getDefaultPermissionSpecifications() { permissionType: PermissionType.RestrictedMethod, targetName: PermissionKeys.wallet_noopWithPermittedAndFailureSideEffects, allowedCaveats: null, - methodImplementation: (_args: RestrictedMethodOptions) => { + methodImplementation: (_args: RestrictedMethodOptions) => { return null; }, sideEffect: { @@ -315,7 +313,7 @@ function getDefaultPermissionSpecifications() { permissionType: PermissionType.RestrictedMethod, targetName: PermissionKeys.wallet_noopWithPermittedAndFailureSideEffects2, allowedCaveats: null, - methodImplementation: (_args: RestrictedMethodOptions) => { + methodImplementation: (_args: RestrictedMethodOptions) => { return null; }, sideEffect: { @@ -327,7 +325,7 @@ function getDefaultPermissionSpecifications() { permissionType: PermissionType.RestrictedMethod, targetName: PermissionKeys.wallet_noopWithPermittedSideEffects, allowedCaveats: null, - methodImplementation: (_args: RestrictedMethodOptions) => { + methodImplementation: (_args: RestrictedMethodOptions) => { return null; }, sideEffect: { @@ -338,7 +336,7 @@ function getDefaultPermissionSpecifications() { [PermissionKeys.wallet_noopWithValidator]: { permissionType: PermissionType.RestrictedMethod, targetName: PermissionKeys.wallet_noopWithValidator, - methodImplementation: (_args: RestrictedMethodOptions) => { + methodImplementation: (_args: RestrictedMethodOptions) => { return null; }, allowedCaveats: [CaveatTypes.noopCaveat, CaveatTypes.filterArrayResponse], @@ -355,7 +353,7 @@ function getDefaultPermissionSpecifications() { [PermissionKeys.wallet_noopWithRequiredCaveat]: { permissionType: PermissionType.RestrictedMethod, targetName: PermissionKeys.wallet_noopWithRequiredCaveat, - methodImplementation: (_args: RestrictedMethodOptions) => { + methodImplementation: (_args: RestrictedMethodOptions) => { return null; }, allowedCaveats: [CaveatTypes.noopCaveat], @@ -391,7 +389,7 @@ function getDefaultPermissionSpecifications() { [PermissionKeys.wallet_noopWithFactory]: { permissionType: PermissionType.RestrictedMethod, targetName: PermissionKeys.wallet_noopWithFactory, - methodImplementation: (_args: RestrictedMethodOptions) => { + methodImplementation: (_args: RestrictedMethodOptions) => { return null; }, allowedCaveats: [CaveatTypes.filterArrayResponse], @@ -418,7 +416,7 @@ function getDefaultPermissionSpecifications() { permissionType: PermissionType.RestrictedMethod, targetName: PermissionKeys.snap_foo, allowedCaveats: null, - methodImplementation: (_args: RestrictedMethodOptions) => { + methodImplementation: (_args: RestrictedMethodOptions) => { return null; }, subjectTypes: [SubjectType.Snap], @@ -1853,7 +1851,7 @@ describe('PermissionController', () => { expect(() => controller.removeCaveat( origin, - PermissionNames.wallet_noopWithRequiredCaveat, + PermissionNames.wallet_noopWithRequiredCaveat as any, CaveatTypes.noopCaveat, ), ).toThrow( @@ -5273,7 +5271,13 @@ describe('PermissionController', () => { }; const expectedError = errors.unauthorized({ - data: { origin, method: PermissionNames.wallet_getSecretArray }, + data: { + origin, + method: PermissionNames.wallet_getSecretArray, + cause: null, + }, + message: + 'Unauthorized to perform action. Try requesting the required permission(s) first. For more information, see: https://docs.metamask.io/guide/rpc-api.html#permissions', }); const { error }: any = await engine.handle(request); @@ -5296,6 +5300,11 @@ describe('PermissionController', () => { const expectedError = errors.methodNotFound('wallet_foo', { origin }); const { error }: any = await engine.handle(request); + + expect(error.message).toStrictEqual(expectedError.message); + expect(error.data.cause).toBeNull(); + delete error.message; + delete error.data.cause; expect(error).toMatchObject(expect.objectContaining(expectedError)); }); @@ -5337,6 +5346,10 @@ describe('PermissionController', () => { ); const { error }: any = await engine.handle(request); + expect(error.message).toStrictEqual(expectedError.message); + expect(error.data.cause).toBeNull(); + delete error.message; + delete error.data.cause; expect(error).toMatchObject(expect.objectContaining(expectedError)); }); }); diff --git a/packages/permission-controller/src/PermissionController.ts b/packages/permission-controller/src/PermissionController.ts index db282cda67c..5bebdb970a8 100644 --- a/packages/permission-controller/src/PermissionController.ts +++ b/packages/permission-controller/src/PermissionController.ts @@ -18,10 +18,10 @@ import { isPlainObject, isValidJson, } from '@metamask/controller-utils'; +import { JsonRpcError } from '@metamask/rpc-errors'; import { hasProperty } from '@metamask/utils'; import type { Json, Mutable } from '@metamask/utils'; import deepFreeze from 'deep-freeze-strict'; -import { EthereumRpcError } from 'eth-rpc-errors'; import { castDraft } from 'immer'; import type { Draft, Patch } from 'immer'; import { nanoid } from 'nanoid'; @@ -1802,6 +1802,7 @@ export class PermissionController< target: string, ): void { if (!isPlainObject(caveat)) { + // eslint-disable-next-line @typescript-eslint/no-throw-literal throw new InvalidCaveatError(caveat, origin, target); } @@ -2149,7 +2150,7 @@ export class PermissionController< try { this.validateRequestedPermissions(origin, permissions); } catch (error) { - if (error instanceof EthereumRpcError) { + if (error instanceof JsonRpcError) { // Re-throw as an internal error; we should never receive invalid approved // permissions. throw internalError( diff --git a/packages/permission-controller/src/errors.ts b/packages/permission-controller/src/errors.ts index 04a41ba0538..43a80b7d87f 100644 --- a/packages/permission-controller/src/errors.ts +++ b/packages/permission-controller/src/errors.ts @@ -1,9 +1,16 @@ -import { errorCodes, ethErrors, EthereumRpcError } from 'eth-rpc-errors'; +import type { DataWithOptionalCause } from '@metamask/rpc-errors'; +import { + errorCodes, + providerErrors, + rpcErrors, + JsonRpcError, +} from '@metamask/rpc-errors'; import type { PermissionType } from './Permission'; type UnauthorizedArg = { data?: Record; + message?: string; }; /** @@ -13,7 +20,7 @@ type UnauthorizedArg = { * @returns The built error */ export function unauthorized(opts: UnauthorizedArg) { - return ethErrors.provider.unauthorized({ + return providerErrors.unauthorized({ message: 'Unauthorized to perform action. Try requesting the required permission(s) first. For more information, see: https://docs.metamask.io/guide/rpc-api.html#permissions', data: opts.data, @@ -27,19 +34,19 @@ export function unauthorized(opts: UnauthorizedArg) { * @param data - Optional data for context. * @returns The built error */ -export function methodNotFound(method: string, data?: unknown) { +export function methodNotFound(method: string, data?: DataWithOptionalCause) { const message = `The method "${method}" does not exist / is not available.`; - const opts: Parameters[0] = { message }; + const opts: Parameters[0] = { message }; if (data !== undefined) { opts.data = data; } - return ethErrors.rpc.methodNotFound(opts); + return rpcErrors.methodNotFound(opts); } type InvalidParamsArg = { message?: string; - data?: unknown; + data?: DataWithOptionalCause; }; /** @@ -49,7 +56,7 @@ type InvalidParamsArg = { * @returns The built error */ export function invalidParams(opts: InvalidParamsArg) { - return ethErrors.rpc.invalidParams({ + return rpcErrors.invalidParams({ data: opts.data, message: opts.message, }); @@ -63,8 +70,8 @@ export function invalidParams(opts: InvalidParamsArg) { */ export function userRejectedRequest>( data?: Data, -): EthereumRpcError { - return ethErrors.provider.userRejectedRequest({ data }); +): JsonRpcError { + return providerErrors.userRejectedRequest({ data }); } /** @@ -77,8 +84,8 @@ export function userRejectedRequest>( export function internalError>( message: string, data?: Data, -): EthereumRpcError { - return ethErrors.rpc.internal({ message, data }); +): JsonRpcError { + return rpcErrors.internal({ message, data }); } export class InvalidSubjectIdentifierError extends Error { @@ -183,7 +190,9 @@ export class CaveatAlreadyExistsError extends Error { } } -export class InvalidCaveatError extends EthereumRpcError { +export class InvalidCaveatError extends JsonRpcError< + DataWithOptionalCause | undefined +> { public override data: { origin: string; target: string }; constructor(receivedCaveat: unknown, origin: string, target: string) { diff --git a/packages/permission-controller/src/permission-middleware.ts b/packages/permission-controller/src/permission-middleware.ts index f618e1cfd65..c18b90c622b 100644 --- a/packages/permission-controller/src/permission-middleware.ts +++ b/packages/permission-controller/src/permission-middleware.ts @@ -7,8 +7,8 @@ import type { } from '@metamask/json-rpc-engine'; import type { Json, - JsonRpcRequest, PendingJsonRpcResponse, + JsonRpcRequest, } from '@metamask/utils'; import type { @@ -62,7 +62,7 @@ export function getPermissionMiddlewareFactory({ } const permissionsMiddleware = async ( - req: JsonRpcRequest, + req: JsonRpcRequest, res: PendingJsonRpcResponse, next: AsyncJsonRpcEngineNextCallback, ): Promise => { diff --git a/packages/permission-controller/src/rpc-methods/getPermissions.ts b/packages/permission-controller/src/rpc-methods/getPermissions.ts index 7e34f698d0d..6debc06c849 100644 --- a/packages/permission-controller/src/rpc-methods/getPermissions.ts +++ b/packages/permission-controller/src/rpc-methods/getPermissions.ts @@ -1,5 +1,5 @@ import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; -import type { JsonRpcParams, PendingJsonRpcResponse } from '@metamask/utils'; +import type { PendingJsonRpcResponse } from '@metamask/utils'; import type { PermissionConstraint } from '../Permission'; import type { SubjectPermissions } from '../PermissionController'; @@ -8,7 +8,7 @@ import { MethodNames } from '../utils'; export const getPermissionsHandler: PermittedHandlerExport< GetPermissionsHooks, - JsonRpcParams, + any, PermissionConstraint[] > = { methodNames: [MethodNames.getPermissions], diff --git a/packages/permission-controller/src/rpc-methods/requestPermissions.test.ts b/packages/permission-controller/src/rpc-methods/requestPermissions.test.ts index 45b346b9618..dfa18ef953e 100644 --- a/packages/permission-controller/src/rpc-methods/requestPermissions.test.ts +++ b/packages/permission-controller/src/rpc-methods/requestPermissions.test.ts @@ -2,7 +2,7 @@ import { JsonRpcEngine, createAsyncMiddleware, } from '@metamask/json-rpc-engine'; -import { ethErrors, serializeError } from 'eth-rpc-errors'; +import { rpcErrors, serializeError } from '@metamask/rpc-errors'; import { requestPermissionsHandler } from './requestPermissions'; @@ -70,7 +70,13 @@ describe('requestPermissions RPC method', () => { }); expect(response.result).toBeUndefined(); - expect(response.error).toStrictEqual(serializeError(new Error('foo'))); + delete response.error.stack; + delete response.error.data.cause.stack; + const expectedError = new Error('foo'); + delete expectedError.stack; + expect(response.error).toStrictEqual( + serializeError(expectedError, { shouldIncludeStack: false }), + ); expect(mockRequestPermissionsForOrigin).toHaveBeenCalledTimes(1); expect(mockRequestPermissionsForOrigin).toHaveBeenCalledWith({}, '1'); }); @@ -94,7 +100,7 @@ describe('requestPermissions RPC method', () => { params: [], // doesn't matter }; - const expectedError = ethErrors.rpc + const expectedError = rpcErrors .invalidRequest({ message: 'Invalid request: Must specify a valid id.', data: { request: { ...req } }, @@ -128,7 +134,7 @@ describe('requestPermissions RPC method', () => { params: invalidParams, }; - const expectedError = ethErrors.rpc + const expectedError = rpcErrors .invalidParams({ data: { request: { ...req } }, }) diff --git a/packages/permission-controller/src/rpc-methods/requestPermissions.ts b/packages/permission-controller/src/rpc-methods/requestPermissions.ts index b32b1a6bb78..1976eb2934d 100644 --- a/packages/permission-controller/src/rpc-methods/requestPermissions.ts +++ b/packages/permission-controller/src/rpc-methods/requestPermissions.ts @@ -1,7 +1,7 @@ import { isPlainObject } from '@metamask/controller-utils'; import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import { rpcErrors } from '@metamask/rpc-errors'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; -import { ethErrors } from 'eth-rpc-errors'; import { invalidParams } from '../errors'; import type { PermissionConstraint, RequestedPermissions } from '../Permission'; @@ -56,7 +56,7 @@ async function requestPermissionsImplementation( (typeof id === 'string' && !id) ) { return end( - ethErrors.rpc.invalidRequest({ + rpcErrors.invalidRequest({ message: 'Invalid request: Must specify a valid id.', data: { request: req }, }), diff --git a/packages/phishing-controller/CHANGELOG.md b/packages/phishing-controller/CHANGELOG.md index 8fe039a422e..6af40c68856 100644 --- a/packages/phishing-controller/CHANGELOG.md +++ b/packages/phishing-controller/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [6.0.2] +### Changed +- Bump dependency on `@metamask/controller-utils` to ^5.0.0 + ## [6.0.1] ### Changed - Bump dependency on `@metamask/base-controller` to ^3.2.1 @@ -61,7 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.1.1] ### Changed - Rename this repository to `core` ([#1031](https://github.com/MetaMask/controllers/pull/1031)) -- Update `@metamask/controller-utils` package ([#1041](https://github.com/MetaMask/controllers/pull/1041)) +- Update `@metamask/controller-utils` package ([#1041](https://github.com/MetaMask/controllers/pull/1041)) ## [1.1.0] ### Added @@ -80,7 +84,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/phishing-controller@6.0.1...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/phishing-controller@6.0.2...HEAD +[6.0.2]: https://github.com/MetaMask/core/compare/@metamask/phishing-controller@6.0.1...@metamask/phishing-controller@6.0.2 [6.0.1]: https://github.com/MetaMask/core/compare/@metamask/phishing-controller@6.0.0...@metamask/phishing-controller@6.0.1 [6.0.0]: https://github.com/MetaMask/core/compare/@metamask/phishing-controller@5.0.0...@metamask/phishing-controller@6.0.0 [5.0.0]: https://github.com/MetaMask/core/compare/@metamask/phishing-controller@4.0.0...@metamask/phishing-controller@5.0.0 diff --git a/packages/phishing-controller/package.json b/packages/phishing-controller/package.json index 96bc5e5ffe1..73db7a26336 100644 --- a/packages/phishing-controller/package.json +++ b/packages/phishing-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/phishing-controller", - "version": "6.0.1", + "version": "6.0.2", "description": "Maintains a periodically updated list of approved and unapproved website origins", "keywords": [ "MetaMask", @@ -29,7 +29,7 @@ }, "dependencies": { "@metamask/base-controller": "^3.2.1", - "@metamask/controller-utils": "^4.3.2", + "@metamask/controller-utils": "^5.0.0", "@types/punycode": "^2.1.0", "eth-phishing-detect": "^1.2.0", "punycode": "^2.1.1" @@ -42,8 +42,8 @@ "nock": "^13.3.1", "sinon": "^9.2.4", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/phishing-controller/src/PhishingController.test.ts b/packages/phishing-controller/src/PhishingController.test.ts index 0950c77381b..be3888ad208 100644 --- a/packages/phishing-controller/src/PhishingController.test.ts +++ b/packages/phishing-controller/src/PhishingController.test.ts @@ -1,3 +1,4 @@ +import { ControllerMessenger } from '@metamask/base-controller'; import { strict as assert } from 'assert'; import nock from 'nock'; import * as sinon from 'sinon'; @@ -8,10 +9,45 @@ import { METAMASK_STALELIST_FILE, PhishingController, PHISHING_CONFIG_BASE_URL, + type PhishingControllerActions, + type PhishingControllerOptions, } from './PhishingController'; -const defaultHotlistRefreshInterval = 30 * 60; -const defaultStalelistRefreshInterval = 4 * 24 * 60 * 60; +const controllerName = 'PhishingController'; + +/** + * Constructs a restricted controller messenger. + * + * @returns A restricted controller messenger. + */ +function getRestrictedMessenger() { + const controllerMessenger = new ControllerMessenger< + PhishingControllerActions, + never + >(); + + const messenger = controllerMessenger.getRestricted< + typeof controllerName, + never, + never + >({ + name: 'PhishingController', + }); + + return messenger; +} + +/** + * Contruct a Phishing Controller with the given options if any. + * @param options - The Phishing Controller options. + * @returns The contstructed Phishing Controller. + */ +function getPhishingController(options?: Partial) { + return new PhishingController({ + messenger: getRestrictedMessenger(), + ...options, + }); +} describe('PhishingController', () => { afterEach(() => { @@ -19,23 +55,15 @@ describe('PhishingController', () => { }); it('should have no default phishing lists', () => { - const controller = new PhishingController(); + const controller = getPhishingController(); expect(controller.state.phishingLists).toStrictEqual([]); }); it('should default to an empty whitelist', () => { - const controller = new PhishingController(); + const controller = getPhishingController(); expect(controller.state.whitelist).toStrictEqual([]); }); - it('should use default stalelist & hotlist refresh intervals', () => { - const controller = new PhishingController(); - expect(controller.config).toStrictEqual({ - stalelistRefreshInterval: defaultStalelistRefreshInterval, - hotlistRefreshInterval: defaultHotlistRefreshInterval, - }); - }); - it('does not call update stalelist or hotlist upon construction', async () => { const nockScope = nock(PHISHING_CONFIG_BASE_URL) .get(METAMASK_STALELIST_FILE) @@ -56,7 +84,7 @@ describe('PhishingController', () => { .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - new PhishingController({}); + getPhishingController(); expect(nockScope.isDone()).toBe(false); }); @@ -82,7 +110,7 @@ describe('PhishingController', () => { ], }); - const controller = new PhishingController({ + const controller = getPhishingController({ hotlistRefreshInterval: 10, }); clock.tick(1000 * 10); @@ -137,7 +165,7 @@ describe('PhishingController', () => { it('should not have stalelist be out of date immediately after maybeUpdateState is called', async () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ stalelistRefreshInterval: 10, }); clock.tick(1000 * 10); @@ -149,7 +177,7 @@ describe('PhishingController', () => { it('should not be out of date after maybeUpdateStalelist is called but before refresh interval has passed', async () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ stalelistRefreshInterval: 10, }); clock.tick(1000 * 10); @@ -162,7 +190,7 @@ describe('PhishingController', () => { it('should still be out of date while update is in progress', async () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ stalelistRefreshInterval: 10, }); clock.tick(1000 * 10); @@ -178,7 +206,7 @@ describe('PhishingController', () => { it('should call update only if it is out of date, otherwise it should not call update', async () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ stalelistRefreshInterval: 10, }); expect(controller.isStalelistOutOfDate()).toBe(false); @@ -236,7 +264,7 @@ describe('PhishingController', () => { ], }); const clock = sinon.useFakeTimers(50); - const controller = new PhishingController({ + const controller = getPhishingController({ hotlistRefreshInterval: 10, stalelistRefreshInterval: 50, }); @@ -250,7 +278,7 @@ describe('PhishingController', () => { describe('isStalelistOutOfDate', () => { it('should not be out of date upon construction', () => { sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ stalelistRefreshInterval: 10, }); @@ -259,7 +287,7 @@ describe('PhishingController', () => { it('should not be out of date after some of the refresh interval has passed', () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ stalelistRefreshInterval: 10, }); clock.tick(1000 * 5); @@ -269,7 +297,7 @@ describe('PhishingController', () => { it('should be out of date after the refresh interval has passed', () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ stalelistRefreshInterval: 10, }); clock.tick(1000 * 10); @@ -279,7 +307,7 @@ describe('PhishingController', () => { it('should be out of date if the refresh interval has passed and an update is in progress', async () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ stalelistRefreshInterval: 10, }); clock.tick(1000 * 10); @@ -293,7 +321,7 @@ describe('PhishingController', () => { it('should not be out of date if the phishing lists were just updated', async () => { sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ stalelistRefreshInterval: 10, }); await controller.updateStalelist(); @@ -303,18 +331,18 @@ describe('PhishingController', () => { it('should not be out of date if the phishing lists were recently updated', async () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ stalelistRefreshInterval: 10, }); await controller.updateStalelist(); - await clock.tick(1000 * 5); + clock.tick(1000 * 5); expect(controller.isStalelistOutOfDate()).toBe(false); }); it('should be out of date if the time elapsed since the last update equals the refresh interval', async () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ stalelistRefreshInterval: 10, }); await controller.updateStalelist(); @@ -327,7 +355,7 @@ describe('PhishingController', () => { describe('isHotlistOutOfDate', () => { it('should not be out of date upon construction', () => { sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ hotlistRefreshInterval: 10, }); @@ -336,7 +364,7 @@ describe('PhishingController', () => { it('should not be out of date after some of the refresh interval has passed', () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ hotlistRefreshInterval: 10, }); clock.tick(1000 * 5); @@ -346,7 +374,7 @@ describe('PhishingController', () => { it('should be out of date after the refresh interval has passed', () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ hotlistRefreshInterval: 10, }); clock.tick(1000 * 10); @@ -356,7 +384,7 @@ describe('PhishingController', () => { it('should be out of date if the refresh interval has passed and an update is in progress', async () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ hotlistRefreshInterval: 10, }); clock.tick(1000 * 10); @@ -370,7 +398,7 @@ describe('PhishingController', () => { it('should not be out of date if the phishing lists were just updated', async () => { sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ hotlistRefreshInterval: 10, }); await controller.updateHotlist(); @@ -380,18 +408,18 @@ describe('PhishingController', () => { it('should not be out of date if the phishing lists were recently updated', async () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ hotlistRefreshInterval: 10, }); await controller.updateHotlist(); - await clock.tick(1000 * 5); + clock.tick(1000 * 5); expect(controller.isHotlistOutOfDate()).toBe(false); }); it('should be out of date if the time elapsed since the last update equals the refresh interval', async () => { const clock = sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ hotlistRefreshInterval: 10, }); await controller.updateHotlist(); @@ -403,7 +431,7 @@ describe('PhishingController', () => { it('should be able to change the stalelistRefreshInterval', async () => { sinon.useFakeTimers(); - const controller = new PhishingController({ stalelistRefreshInterval: 10 }); + const controller = getPhishingController({ stalelistRefreshInterval: 10 }); controller.setStalelistRefreshInterval(0); expect(controller.isStalelistOutOfDate()).toBe(true); @@ -411,7 +439,7 @@ describe('PhishingController', () => { it('should be able to change the hotlistRefreshInterval', async () => { sinon.useFakeTimers(); - const controller = new PhishingController({ + const controller = getPhishingController({ hotlistRefreshInterval: 10, }); controller.setHotlistRefreshInterval(0); @@ -439,7 +467,7 @@ describe('PhishingController', () => { }) .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); expect(controller.test('metamask.io')).toMatchObject({ result: false, @@ -469,7 +497,7 @@ describe('PhishingController', () => { .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); expect(controller.test('i❤.ws')).toMatchObject({ result: false, @@ -498,7 +526,7 @@ describe('PhishingController', () => { .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); expect(controller.test('xn--i-7iq.ws')).toMatchObject({ result: false, @@ -527,7 +555,7 @@ describe('PhishingController', () => { .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); expect(controller.test('etnerscan.io')).toMatchObject({ result: true, @@ -556,7 +584,7 @@ describe('PhishingController', () => { }) .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); expect(controller.test('myetherẉalletṭ.com')).toMatchObject({ result: true, @@ -586,7 +614,7 @@ describe('PhishingController', () => { .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); expect(controller.test('xn--myetherallet-4k5fwn.com')).toMatchObject({ result: true, @@ -624,7 +652,7 @@ describe('PhishingController', () => { ], }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); expect( controller.test('e4d600ab9141b7a9859511c77e63b9b3.com'), @@ -656,7 +684,7 @@ describe('PhishingController', () => { .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(500); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); expect( controller.test('e4d600ab9141b7a9859511c77e63b9b3.com'), @@ -686,7 +714,7 @@ describe('PhishingController', () => { }) .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); expect(controller.test('opensea.io')).toMatchObject({ result: false, @@ -715,7 +743,7 @@ describe('PhishingController', () => { }) .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); expect(controller.test('ohpensea.io')).toMatchObject({ result: true, @@ -744,7 +772,7 @@ describe('PhishingController', () => { }) .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); expect( controller.test('this-is-the-official-website-of-opensea.io'), @@ -774,7 +802,7 @@ describe('PhishingController', () => { }) .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); const unsafeDomain = 'electrum.mx'; assert.equal( @@ -809,7 +837,7 @@ describe('PhishingController', () => { }) .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); const unsafeDomain = 'electrum.mx'; assert.equal( @@ -845,7 +873,7 @@ describe('PhishingController', () => { }) .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); const unsafeDomain = 'myetherẉalletṭ.com'; assert.equal( @@ -880,7 +908,7 @@ describe('PhishingController', () => { }) .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); const unsafeDomain = 'xn--myetherallet-4k5fwn.com'; assert.equal( @@ -930,7 +958,7 @@ describe('PhishingController', () => { ], }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); expect(controller.state.phishingLists).toStrictEqual([ @@ -993,7 +1021,7 @@ describe('PhishingController', () => { ], }); - const controller = new PhishingController(); + const controller = getPhishingController(); await controller.updateStalelist(); expect(controller.state.phishingLists).toStrictEqual([ @@ -1018,10 +1046,15 @@ describe('PhishingController', () => { ]); }); - it('should not update stale list if disabled', async () => { - const controller = new PhishingController( - { disabled: true }, - { + it('should not update phishing lists if fetch returns 304', async () => { + nock(PHISHING_CONFIG_BASE_URL) + .get(METAMASK_STALELIST_FILE) + .reply(304) + .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) + .reply(304); + + const controller = getPhishingController({ + state: { phishingLists: [ { allowlist: [], @@ -1034,7 +1067,7 @@ describe('PhishingController', () => { }, ], }, - ); + }); await controller.updateStalelist(); expect(controller.state.phishingLists).toStrictEqual([ @@ -1050,10 +1083,15 @@ describe('PhishingController', () => { ]); }); - it('should not update hotlist lists if disabled', async () => { - const controller = new PhishingController( - { disabled: true }, - { + it('should not update phishing lists if fetch returns 500', async () => { + nock(PHISHING_CONFIG_BASE_URL) + .get(METAMASK_STALELIST_FILE) + .reply(500) + .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) + .reply(500); + + const controller = getPhishingController({ + state: { phishingLists: [ { allowlist: [], @@ -1066,76 +1104,6 @@ describe('PhishingController', () => { }, ], }, - ); - await controller.updateHotlist(); - - expect(controller.state.phishingLists).toStrictEqual([ - { - allowlist: [], - blocklist: [], - fuzzylist: [], - tolerance: 3, - version: 1, - name: ListNames.MetaMask, - lastUpdated: 0, - }, - ]); - }); - - it('should not update phishing lists if fetch returns 304', async () => { - nock(PHISHING_CONFIG_BASE_URL) - .get(METAMASK_STALELIST_FILE) - .reply(304) - .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) - .reply(304); - - const controller = new PhishingController(undefined, { - phishingLists: [ - { - allowlist: [], - blocklist: [], - fuzzylist: [], - tolerance: 3, - version: 1, - name: ListNames.MetaMask, - lastUpdated: 0, - }, - ], - }); - await controller.updateStalelist(); - - expect(controller.state.phishingLists).toStrictEqual([ - { - allowlist: [], - blocklist: [], - fuzzylist: [], - tolerance: 3, - version: 1, - name: ListNames.MetaMask, - lastUpdated: 0, - }, - ]); - }); - - it('should not update phishing lists if fetch returns 500', async () => { - nock(PHISHING_CONFIG_BASE_URL) - .get(METAMASK_STALELIST_FILE) - .reply(500) - .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) - .reply(500); - - const controller = new PhishingController(undefined, { - phishingLists: [ - { - allowlist: [], - blocklist: [], - fuzzylist: [], - tolerance: 3, - version: 1, - name: ListNames.MetaMask, - lastUpdated: 0, - }, - ], }); await controller.updateStalelist(); @@ -1159,7 +1127,7 @@ describe('PhishingController', () => { .get(`${METAMASK_HOTLIST_DIFF_FILE}/${1}`) .replyWithError('network error'); - const controller = new PhishingController(); + const controller = getPhishingController(); expect(await controller.updateStalelist()).toBeUndefined(); }); @@ -1189,7 +1157,7 @@ describe('PhishingController', () => { .delay(100) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); const firstPromise = controller.updateStalelist(); const secondPromise = controller.updateStalelist(); @@ -1227,7 +1195,7 @@ describe('PhishingController', () => { .delay(100) .reply(200, { data: [] }); - const controller = new PhishingController(); + const controller = getPhishingController(); const firstPromise = controller.updateStalelist(); const secondPromise = controller.updateStalelist(); clock.tick(1000 * 99); @@ -1255,18 +1223,20 @@ describe('PhishingController', () => { ], }); - const controller = new PhishingController(undefined, { - phishingLists: [ - { - allowlist: [], - blocklist: [], - fuzzylist: [], - tolerance: 3, - version: 1, - name: ListNames.MetaMask, - lastUpdated: 0, - }, - ], + const controller = getPhishingController({ + state: { + phishingLists: [ + { + allowlist: [], + blocklist: [], + fuzzylist: [], + tolerance: 3, + version: 1, + name: ListNames.MetaMask, + lastUpdated: 0, + }, + ], + }, }); await controller.updateHotlist(); @@ -1287,18 +1257,20 @@ describe('PhishingController', () => { .get(`${METAMASK_HOTLIST_DIFF_FILE}/${0}`) .reply(404); - const controller = new PhishingController(undefined, { - phishingLists: [ - { - allowlist: [], - blocklist: [], - fuzzylist: [], - tolerance: 3, - version: 1, - name: ListNames.MetaMask, - lastUpdated: 0, - }, - ], + const controller = getPhishingController({ + state: { + phishingLists: [ + { + allowlist: [], + blocklist: [], + fuzzylist: [], + tolerance: 3, + version: 1, + name: ListNames.MetaMask, + lastUpdated: 0, + }, + ], + }, }); await controller.updateHotlist(); diff --git a/packages/phishing-controller/src/PhishingController.ts b/packages/phishing-controller/src/PhishingController.ts index 80ae1cd16c6..2c95e86587d 100644 --- a/packages/phishing-controller/src/PhishingController.ts +++ b/packages/phishing-controller/src/PhishingController.ts @@ -1,11 +1,24 @@ -import type { BaseConfig, BaseState } from '@metamask/base-controller'; -import { BaseController } from '@metamask/base-controller'; +import type { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { BaseControllerV2 as BaseController } from '@metamask/base-controller'; import { safelyExecute } from '@metamask/controller-utils'; import PhishingDetector from 'eth-phishing-detect/src/detector'; import { toASCII } from 'punycode/'; import { applyDiffs, fetchTimeNow } from './utils'; +export const PHISHING_CONFIG_BASE_URL = + 'https://phishing-detection.metafi.codefi.network'; + +export const METAMASK_STALELIST_FILE = '/v1/stalelist'; + +export const METAMASK_HOTLIST_DIFF_FILE = '/v1/diffsSince'; + +export const HOTLIST_REFRESH_INTERVAL = 30 * 60; // 30 mins in seconds +export const STALELIST_REFRESH_INTERVAL = 4 * 24 * 60 * 60; // 4 days in seconds + +export const METAMASK_STALELIST_URL = `${PHISHING_CONFIG_BASE_URL}${METAMASK_STALELIST_FILE}`; +export const METAMASK_HOTLIST_DIFF_URL = `${PHISHING_CONFIG_BASE_URL}${METAMASK_HOTLIST_DIFF_FILE}`; + /** * @type ListTypes * @@ -24,36 +37,36 @@ export type ListTypes = 'fuzzylist' | 'blocklist' | 'allowlist'; * @property version - Version number of this configuration * @property whitelist - List of approved origins */ -export interface EthPhishingResponse { +export type EthPhishingResponse = { blacklist: string[]; fuzzylist: string[]; tolerance: number; version: number; whitelist: string[]; -} +}; /** * @type PhishingStalelist * - * Interface defining expected type of the stalelist.json file. + * type defining expected type of the stalelist.json file. * @property eth_phishing_detect_config - Stale list sourced from eth-phishing-detect's config.json. * @property phishfort_hotlist - Stale list sourced from phishfort's hotlist.json. Only includes blocklist. Deduplicated entries from eth_phishing_detect_config. * @property tolerance - Fuzzy match tolerance level * @property lastUpdated - Timestamp of last update. * @property version - Stalelist data structure iteration. */ -export interface PhishingStalelist { +export type PhishingStalelist = { eth_phishing_detect_config: Record; phishfort_hotlist: Record; tolerance: number; version: number; lastUpdated: number; -} +}; /** * @type PhishingListState * - * Interface defining the persisted list state. This is the persisted state that is updated frequently with `this.maybeUpdateState()`. + * type defining the persisted list state. This is the persisted state that is updated frequently with `this.maybeUpdateState()`. * @property allowlist - List of approved origins (legacy naming "whitelist") * @property blocklist - List of unapproved origins (legacy naming "blacklist") * @property fuzzylist - List of fuzzy-matched unapproved origins @@ -62,7 +75,7 @@ export interface PhishingStalelist { * @property version - Version of the phishing list state. * @property name - Name of the list. Used for attribution. */ -export interface PhishingListState { +export type PhishingListState = { allowlist: string[]; blocklist: string[]; fuzzylist: string[]; @@ -70,45 +83,45 @@ export interface PhishingListState { version: number; lastUpdated: number; name: ListNames; -} +}; /** * @type EthPhishingDetectResult * - * Interface that describes the result of the `test` method. + * type that describes the result of the `test` method. * @property name - Name of the config on which a match was found. * @property version - Version of the config on which a match was found. * @property result - Whether a domain was detected as a phishing domain. True means an unsafe domain. * @property match - The matching fuzzylist origin when a fuzzylist match is found. Returned as undefined for non-fuzzy true results. * @property type - The field of the config on which a match was found. */ -export interface EthPhishingDetectResult { +export type EthPhishingDetectResult = { name?: string; version?: string; result: boolean; match?: string; // Returned as undefined for non-fuzzy true results. type: 'all' | 'fuzzy' | 'blocklist' | 'allowlist'; -} +}; /** * @type HotlistDiff * - * Interface defining the expected type of the diffs in hotlist.json file. + * type defining the expected type of the diffs in hotlist.json file. * @property url - Url of the diff entry. * @property timestamp - Timestamp at which the diff was identified. * @property targetList - The list name where the diff was identified. * @property isRemoval - Was the diff identified a removal type. */ -export interface HotlistDiff { +export type HotlistDiff = { url: string; timestamp: number; targetList: `${ListKeys}.${ListTypes}`; isRemoval?: boolean; -} +}; -export interface DataResultWrapper { +export type DataResultWrapper = { data: T; -} +}; /** * @type Hotlist @@ -121,45 +134,6 @@ export interface DataResultWrapper { */ export type Hotlist = HotlistDiff[]; -/** - * @type PhishingConfig - * - * Phishing controller configuration - * @property stalelistRefreshInterval - Polling interval used to fetch stale list. - * @property hotlistRefreshInterval - Polling interval used to fetch hotlist diff list. - */ -export interface PhishingConfig extends BaseConfig { - stalelistRefreshInterval: number; - hotlistRefreshInterval: number; -} - -/** - * @type PhishingState - * - * Phishing controller state - * @property phishing - eth-phishing-detect configuration - * @property whitelist - array of temporarily-approved origins - */ -export interface PhishingState extends BaseState { - phishingLists: PhishingListState[]; - whitelist: string[]; - hotlistLastFetched: number; - stalelistLastFetched: number; -} - -export const PHISHING_CONFIG_BASE_URL = - 'https://phishing-detection.metafi.codefi.network'; - -export const METAMASK_STALELIST_FILE = '/v1/stalelist'; - -export const METAMASK_HOTLIST_DIFF_FILE = '/v1/diffsSince'; - -export const HOTLIST_REFRESH_INTERVAL = 30 * 60; // 30 mins in seconds -export const STALELIST_REFRESH_INTERVAL = 4 * 24 * 60 * 60; // 4 days in seconds - -export const METAMASK_STALELIST_URL = `${PHISHING_CONFIG_BASE_URL}${METAMASK_STALELIST_FILE}`; -export const METAMASK_HOTLIST_DIFF_URL = `${PHISHING_CONFIG_BASE_URL}${METAMASK_HOTLIST_DIFF_FILE}`; - /** * Enum containing upstream data provider source list keys. * These are the keys denoting lists consumed by the upstream data provider. @@ -195,56 +169,147 @@ export const phishingListKeyNameMap = { [ListKeys.PhishfortHotlist]: ListNames.Phishfort, }; +const controllerName = 'PhishingController'; + +const metadata = { + phishingLists: { persist: true, anonymous: false }, + whitelist: { persist: true, anonymous: false }, + hotlistLastFetched: { persist: true, anonymous: false }, + stalelistLastFetched: { persist: true, anonymous: false }, +}; + +/** + * Get a default empty state for the controller. + * @returns The default empty state. + */ +const getDefaultState = (): PhishingControllerState => { + return { + phishingLists: [], + whitelist: [], + hotlistLastFetched: 0, + stalelistLastFetched: 0, + }; +}; + +/** + * @type PhishingControllerState + * + * Phishing controller state + * @property phishing - eth-phishing-detect configuration + * @property whitelist - array of temporarily-approved origins + */ +export type PhishingControllerState = { + phishingLists: PhishingListState[]; + whitelist: string[]; + hotlistLastFetched: number; + stalelistLastFetched: number; +}; + +/** + * @type PhishingControllerOptions + * + * Phishing controller options + * @property stalelistRefreshInterval - Polling interval used to fetch stale list. + * @property hotlistRefreshInterval - Polling interval used to fetch hotlist diff list. + */ +export type PhishingControllerOptions = { + stalelistRefreshInterval?: number; + hotlistRefreshInterval?: number; + messenger: PhishingControllerMessenger; + state?: Partial; +}; + +export type MaybeUpdateState = { + type: `${typeof controllerName}:maybeUpdateState`; + handler: PhishingController['maybeUpdateState']; +}; + +export type TestOrigin = { + type: `${typeof controllerName}:testOrigin`; + handler: PhishingController['test']; +}; + +export type PhishingControllerActions = MaybeUpdateState | TestOrigin; + +export type PhishingControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + PhishingControllerActions, + never, + never, + never +>; + /** * Controller that manages community-maintained lists of approved and unapproved website origins. */ export class PhishingController extends BaseController< - PhishingConfig, - PhishingState + typeof controllerName, + PhishingControllerState, + PhishingControllerMessenger > { - private detector: any; + #detector: any; - #inProgressHotlistUpdate: Promise | undefined; + #stalelistRefreshInterval: number; - #inProgressStalelistUpdate: Promise | undefined; + #hotlistRefreshInterval: number; - /** - * Name of this controller used during composition - */ - override name = 'PhishingController'; + #inProgressHotlistUpdate?: Promise; + + #inProgressStalelistUpdate?: Promise; /** - * Creates a PhishingController instance. + * Construct a Phishing Controller. * * @param config - Initial options used to configure this controller. - * @param state - Initial state to set on this controller. + * @param config.stalelistRefreshInterval - Polling interval used to fetch stale list. + * @param config.hotlistRefreshInterval - Polling interval used to fetch hotlist diff list. + * @param config.messenger - The controller restricted messenger. + * @param config.state - Initial state to set on this controller. */ - constructor( - config?: Partial, - state?: Partial, - ) { - super(config, state); - this.defaultConfig = { - stalelistRefreshInterval: STALELIST_REFRESH_INTERVAL, - hotlistRefreshInterval: HOTLIST_REFRESH_INTERVAL, - }; + constructor({ + stalelistRefreshInterval = STALELIST_REFRESH_INTERVAL, + hotlistRefreshInterval = HOTLIST_REFRESH_INTERVAL, + messenger, + state = {}, + }: PhishingControllerOptions) { + super({ + name: controllerName, + metadata, + messenger, + state: { + ...getDefaultState(), + ...state, + }, + }); - this.defaultState = { - phishingLists: [], - whitelist: [], - hotlistLastFetched: 0, - stalelistLastFetched: 0, - }; + this.#stalelistRefreshInterval = stalelistRefreshInterval; + this.#hotlistRefreshInterval = hotlistRefreshInterval; + this.#registerMessageHandlers(); - this.initialize(); this.updatePhishingDetector(); } + /** + * Constructor helper for registering this controller's messaging system + * actions. + */ + #registerMessageHandlers(): void { + this.messagingSystem.registerActionHandler( + `${controllerName}:maybeUpdateState` as const, + this.maybeUpdateState.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `${controllerName}:testOrigin` as const, + this.test.bind(this), + ); + } + /** * Updates this.detector with an instance of PhishingDetector using the current state. */ updatePhishingDetector() { - this.detector = new PhishingDetector(this.state.phishingLists); + this.#detector = new PhishingDetector(this.state.phishingLists); } /** @@ -255,7 +320,7 @@ export class PhishingController extends BaseController< * @param interval - the new interval, in ms. */ setStalelistRefreshInterval(interval: number) { - this.configure({ stalelistRefreshInterval: interval }, false, false); + this.#stalelistRefreshInterval = interval; } /** @@ -266,7 +331,7 @@ export class PhishingController extends BaseController< * @param interval - the new interval, in ms. */ setHotlistRefreshInterval(interval: number) { - this.configure({ hotlistRefreshInterval: interval }, false, false); + this.#hotlistRefreshInterval = interval; } /** @@ -277,7 +342,7 @@ export class PhishingController extends BaseController< isStalelistOutOfDate() { return ( fetchTimeNow() - this.state.stalelistLastFetched >= - this.config.stalelistRefreshInterval + this.#stalelistRefreshInterval ); } @@ -289,7 +354,7 @@ export class PhishingController extends BaseController< isHotlistOutOfDate() { return ( fetchTimeNow() - this.state.hotlistLastFetched >= - this.config.hotlistRefreshInterval + this.#hotlistRefreshInterval ); } @@ -328,7 +393,7 @@ export class PhishingController extends BaseController< if (this.state.whitelist.includes(punycodeOrigin)) { return { result: false, type: 'all' }; // Same as whitelisted match returned by detector.check(...). } - return this.detector.check(punycodeOrigin); + return this.#detector.check(punycodeOrigin); } /** @@ -342,7 +407,9 @@ export class PhishingController extends BaseController< if (whitelist.includes(punycodeOrigin)) { return; } - this.update({ whitelist: [...whitelist, punycodeOrigin] }); + this.update((draftState) => { + draftState.whitelist.push(punycodeOrigin); + }); } /** @@ -392,21 +459,17 @@ export class PhishingController extends BaseController< * this function that prevents redundant configuration updates. */ async #updateStalelist() { - if (this.disabled) { - return; - } - let stalelistResponse; let hotlistDiffsResponse; try { - stalelistResponse = await this.queryConfig< + stalelistResponse = await this.#queryConfig< DataResultWrapper >(METAMASK_STALELIST_URL).then((d) => d); // Fetching hotlist diffs relies on having a lastUpdated timestamp to do `GET /v1/diffsSince/:timestamp`, // so it doesn't make sense to call if there is not a timestamp to begin with. if (stalelistResponse?.data && stalelistResponse.data.lastUpdated > 0) { - hotlistDiffsResponse = await this.queryConfig< + hotlistDiffsResponse = await this.#queryConfig< DataResultWrapper >(`${METAMASK_HOTLIST_DIFF_URL}/${stalelistResponse.data.lastUpdated}`); } @@ -414,9 +477,9 @@ export class PhishingController extends BaseController< // Set `stalelistLastFetched` and `hotlistLastFetched` even for failed requests to prevent server // from being overwhelmed with traffic after a network disruption. const timeNow = fetchTimeNow(); - this.update({ - stalelistLastFetched: timeNow, - hotlistLastFetched: timeNow, + this.update((draftState) => { + draftState.stalelistLastFetched = timeNow; + draftState.hotlistLastFetched = timeNow; }); } @@ -451,8 +514,8 @@ export class PhishingController extends BaseController< ListKeys.EthPhishingDetectConfig, ); - this.update({ - phishingLists: [newMetaMaskListState, newPhishfortListState], + this.update((draftState) => { + draftState.phishingLists = [newMetaMaskListState, newPhishfortListState]; }); this.updatePhishingDetector(); } @@ -464,23 +527,20 @@ export class PhishingController extends BaseController< * this function that prevents redundant configuration updates. */ async #updateHotlist() { - if (this.disabled) { - return; - } const lastDiffTimestamp = Math.max( ...this.state.phishingLists.map(({ lastUpdated }) => lastUpdated), ); let hotlistResponse: DataResultWrapper | null; try { - hotlistResponse = await this.queryConfig>( + hotlistResponse = await this.#queryConfig>( `${METAMASK_HOTLIST_DIFF_URL}/${lastDiffTimestamp}`, ); } finally { // Set `hotlistLastFetched` even for failed requests to prevent server from being overwhelmed with // traffic after a network disruption. - this.update({ - hotlistLastFetched: fetchTimeNow(), + this.update((draftState) => { + draftState.hotlistLastFetched = fetchTimeNow(); }); } @@ -496,13 +556,13 @@ export class PhishingController extends BaseController< ), ); - this.update({ - phishingLists: newPhishingLists, + this.update((draftState) => { + draftState.phishingLists = newPhishingLists; }); this.updatePhishingDetector(); } - private async queryConfig( + async #queryConfig( input: RequestInfo, ): Promise { const response = await safelyExecute( diff --git a/packages/polling-controller/CHANGELOG.md b/packages/polling-controller/CHANGELOG.md new file mode 100644 index 00000000000..27eb830b8c5 --- /dev/null +++ b/packages/polling-controller/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/polling-controller/LICENSE b/packages/polling-controller/LICENSE new file mode 100644 index 00000000000..ddfbecf9020 --- /dev/null +++ b/packages/polling-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2018 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/polling-controller/README.md b/packages/polling-controller/README.md new file mode 100644 index 00000000000..bade0a3e03d --- /dev/null +++ b/packages/polling-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/polling-controller` + +PollingController is used as the base for all controllers that need to poll for updates based on `networkClientId`. + +## Installation + +`yarn add @metamask/polling-controller` + +or + +`npm install @metamask/polling-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/packages/polling-controller/jest.config.js b/packages/polling-controller/jest.config.js new file mode 100644 index 00000000000..17db4cd31b6 --- /dev/null +++ b/packages/polling-controller/jest.config.js @@ -0,0 +1,26 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const merge = require('deepmerge'); +const path = require('path'); + +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 65.31, + functions: 76.59, + lines: 75.83, + statements: 75.91, + }, + }, +}); diff --git a/packages/polling-controller/package.json b/packages/polling-controller/package.json new file mode 100644 index 00000000000..ab1be76a786 --- /dev/null +++ b/packages/polling-controller/package.json @@ -0,0 +1,58 @@ +{ + "name": "@metamask/polling-controller", + "version": "0.0.0", + "description": "Polling Controller is the base for controllers that polling by networkClientId", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/polling-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/polling-controller", + "publish:preview": "yarn npm publish --tag preview", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "^3.2.1", + "@metamask/controller-utils": "^5.0.0", + "@metamask/network-controller": "^13.0.0", + "@metamask/utils": "^8.1.0", + "@types/uuid": "^8.3.0", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.1.0", + "@types/jest": "^27.4.1", + "deepmerge": "^4.2.2", + "jest": "^27.5.1", + "ts-jest": "^27.1.4", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", + "typescript": "~4.6.3" + }, + "peerDependencies": { + "@metamask/network-controller": "^13.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/polling-controller/src/PollingController.test.ts b/packages/polling-controller/src/PollingController.test.ts new file mode 100644 index 00000000000..26c583d2c68 --- /dev/null +++ b/packages/polling-controller/src/PollingController.test.ts @@ -0,0 +1,308 @@ +import { ControllerMessenger } from '@metamask/base-controller'; + +import type { PollingCompleteType } from './PollingController'; +import PollingController from './PollingController'; + +const TICK_TIME = 1000; + +const createExecutePollMock = () => { + const executePollMock = jest.fn().mockImplementation(async () => { + return true; + }); + return executePollMock; +}; + +describe('PollingController', () => { + describe('start', () => { + it('should start polling if not polling', () => { + jest.useFakeTimers(); + + class MyGasFeeController extends PollingController { + executePoll = createExecutePollMock(); + } + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + pollingIntervalLength: TICK_TIME, + }); + controller.start('mainnet'); + jest.advanceTimersByTime(TICK_TIME); + controller.stopAll(); + expect(controller.executePoll).toHaveBeenCalledTimes(1); + }); + }); + describe('stop', () => { + it('should stop polling when called with a valid polling that was the only active pollingToken for a given networkClient', () => { + jest.useFakeTimers(); + class MyGasFeeController extends PollingController { + executePoll = createExecutePollMock(); + } + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + pollingIntervalLength: TICK_TIME, + }); + const pollingToken = controller.start('mainnet'); + jest.advanceTimersByTime(TICK_TIME); + controller.stop(pollingToken); + jest.advanceTimersByTime(TICK_TIME); + expect(controller.executePoll).toHaveBeenCalledTimes(1); + controller.stopAll(); + }); + it('should not stop polling if called with one of multiple active polling tokens for a given networkClient', async () => { + jest.useFakeTimers(); + class MyGasFeeController extends PollingController { + executePoll = createExecutePollMock(); + } + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + pollingIntervalLength: TICK_TIME, + }); + const pollingToken1 = controller.start('mainnet'); + controller.start('mainnet'); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + controller.stop(pollingToken1); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + expect(controller.executePoll).toHaveBeenCalledTimes(2); + controller.stopAll(); + }); + it('should error if no pollingToken is passed', () => { + jest.useFakeTimers(); + class MyGasFeeController extends PollingController { + executePoll = createExecutePollMock(); + } + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + pollingIntervalLength: TICK_TIME, + }); + controller.start('mainnet'); + expect(() => { + controller.stop(undefined as unknown as any); + }).toThrow('pollingToken required'); + controller.stopAll(); + }); + it('should error if no matching pollingToken is found', () => { + jest.useFakeTimers(); + class MyGasFeeController extends PollingController { + executePoll = createExecutePollMock(); + } + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + pollingIntervalLength: TICK_TIME, + }); + controller.start('mainnet'); + expect(() => { + controller.stop('potato'); + }).toThrow('pollingToken not found'); + controller.stopAll(); + }); + }); + describe('poll', () => { + it('should call executePoll if polling', async () => { + jest.useFakeTimers(); + + class MyGasFeeController extends PollingController { + executePoll = createExecutePollMock(); + } + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + pollingIntervalLength: TICK_TIME, + }); + controller.start('mainnet'); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + expect(controller.executePoll).toHaveBeenCalledTimes(2); + }); + it('should continue calling executePoll when start is called again with the same networkClientId', async () => { + jest.useFakeTimers(); + + class MyGasFeeController extends PollingController { + executePoll = createExecutePollMock(); + } + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + pollingIntervalLength: TICK_TIME, + }); + controller.start('mainnet'); + controller.start('mainnet'); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + expect(controller.executePoll).toHaveBeenCalledTimes(2); + controller.stopAll(); + }); + it('should publish "pollingComplete" when stop is called', async () => { + jest.useFakeTimers(); + const pollingComplete: any = jest.fn(); + class MyGasFeeController extends PollingController { + executePoll = createExecutePollMock(); + } + const name = 'PollingController'; + + const mockMessenger = new ControllerMessenger< + any, + PollingCompleteType + >(); + + mockMessenger.subscribe(`${name}:pollingComplete`, pollingComplete); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name, + state: { foo: 'bar' }, + pollingIntervalLength: TICK_TIME, + }); + const pollingToken = controller.start('mainnet'); + controller.stop(pollingToken); + expect(pollingComplete).toHaveBeenCalledTimes(1); + }); + it('should poll at the interval length passed via the constructor', async () => { + jest.useFakeTimers(); + + class MyGasFeeController extends PollingController { + executePoll = createExecutePollMock(); + } + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + pollingIntervalLength: TICK_TIME * 3, + }); + controller.start('mainnet'); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + expect(controller.executePoll).not.toHaveBeenCalled(); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + expect(controller.executePoll).not.toHaveBeenCalled(); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + expect(controller.executePoll).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(TICK_TIME * 3); + await Promise.resolve(); + expect(controller.executePoll).toHaveBeenCalledTimes(2); + }); + }); + describe('multiple networkClientIds', () => { + it('should poll for each networkClientId', async () => { + jest.useFakeTimers(); + class MyGasFeeController extends PollingController { + executePoll = createExecutePollMock(); + } + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + pollingIntervalLength: TICK_TIME, + }); + controller.start('mainnet'); + controller.start('rinkeby'); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + expect(controller.executePoll.mock.calls).toMatchObject([ + ['mainnet'], + ['rinkeby'], + ]); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + expect(controller.executePoll.mock.calls).toMatchObject([ + ['mainnet'], + ['rinkeby'], + ['mainnet'], + ['rinkeby'], + ]); + controller.stopAll(); + }); + + it('should poll multiple networkClientIds at the interval length passed via the constructor', async () => { + jest.useFakeTimers(); + + class MyGasFeeController extends PollingController { + executePoll = createExecutePollMock(); + } + const mockMessenger = new ControllerMessenger(); + + const controller = new MyGasFeeController({ + messenger: mockMessenger, + metadata: {}, + name: 'PollingController', + state: { foo: 'bar' }, + pollingIntervalLength: TICK_TIME * 2, + }); + controller.start('mainnet'); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + controller.start('sepolia'); + expect(controller.executePoll.mock.calls).toMatchObject([]); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + expect(controller.executePoll.mock.calls).toMatchObject([['mainnet']]); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + expect(controller.executePoll.mock.calls).toMatchObject([ + ['mainnet'], + ['sepolia'], + ]); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + expect(controller.executePoll.mock.calls).toMatchObject([ + ['mainnet'], + ['sepolia'], + ['mainnet'], + ]); + jest.advanceTimersByTime(TICK_TIME); + await Promise.resolve(); + expect(controller.executePoll.mock.calls).toMatchObject([ + ['mainnet'], + ['sepolia'], + ['mainnet'], + ['sepolia'], + ]); + }); + }); +}); diff --git a/packages/polling-controller/src/PollingController.ts b/packages/polling-controller/src/PollingController.ts new file mode 100644 index 00000000000..a9595cc4e84 --- /dev/null +++ b/packages/polling-controller/src/PollingController.ts @@ -0,0 +1,150 @@ +import { BaseControllerV2 } from '@metamask/base-controller'; +import type { + RestrictedControllerMessenger, + StateMetadata, +} from '@metamask/base-controller'; +import type { NetworkClientId } from '@metamask/network-controller'; +import type { Json } from '@metamask/utils'; +import { v4 as random } from 'uuid'; + +export type PollingCompleteType = { + type: `${N}:pollingComplete`; + payload: [string]; +}; + +/** + * PollingController is an abstract class that implements the polling + * functionality for a controller. It is meant to be extended by a controller + * that needs to poll for data by networkClientId. + * + */ +export default abstract class PollingController< + Name extends string, + State extends Record, + messenger extends RestrictedControllerMessenger< + Name, + any, + PollingCompleteType | any, + string, + string + >, +> extends BaseControllerV2 { + readonly #intervalLength: number; + + private readonly networkClientIdTokensMap: Map> = + new Map(); + + private readonly intervalIds: Record = {}; + + constructor({ + name, + state, + messenger, + metadata, + pollingIntervalLength, + }: { + name: Name; + state: State; + metadata: StateMetadata; + messenger: messenger; + pollingIntervalLength: number; + }) { + super({ + name, + state, + messenger, + metadata, + }); + + if (!pollingIntervalLength) { + throw new Error('pollingIntervalLength required for PollingController'); + } + + this.#intervalLength = pollingIntervalLength; + } + + /** + * Starts polling for a networkClientId + * + * @param networkClientId - The networkClientId to start polling for + * @returns void + */ + start(networkClientId: NetworkClientId) { + const innerPollToken = random(); + if (this.networkClientIdTokensMap.has(networkClientId)) { + const set = this.networkClientIdTokensMap.get(networkClientId); + set?.add(innerPollToken); + } else { + const set = new Set(); + set.add(innerPollToken); + this.networkClientIdTokensMap.set(networkClientId, set); + } + this.#poll(networkClientId); + return innerPollToken; + } + + /** + * Stops polling for all networkClientIds + */ + stopAll() { + this.networkClientIdTokensMap.forEach((tokens, _networkClientId) => { + tokens.forEach((token) => { + this.stop(token); + }); + }); + } + + /** + * Stops polling for a networkClientId + * + * @param pollingToken - The polling token to stop polling for + */ + stop(pollingToken: string) { + if (!pollingToken) { + throw new Error('pollingToken required'); + } + let found = false; + this.networkClientIdTokensMap.forEach((tokens, networkClientId) => { + if (tokens.has(pollingToken)) { + found = true; + this.networkClientIdTokensMap + .get(networkClientId) + ?.delete(pollingToken); + if (this.networkClientIdTokensMap.get(networkClientId)?.size === 0) { + clearTimeout(this.intervalIds[networkClientId]); + delete this.intervalIds[networkClientId]; + this.networkClientIdTokensMap.delete(networkClientId); + this.messagingSystem.publish( + `${this.name}:pollingComplete`, + networkClientId, + ); + } + } + }); + if (!found) { + throw new Error('pollingToken not found'); + } + } + + /** + * Executes the poll for a networkClientId + * + * @param networkClientId - The networkClientId to execute the poll for + */ + abstract executePoll(networkClientId: NetworkClientId): Promise; + + #poll(networkClientId: NetworkClientId) { + if (this.intervalIds[networkClientId]) { + clearTimeout(this.intervalIds[networkClientId]); + delete this.intervalIds[networkClientId]; + } + this.intervalIds[networkClientId] = setTimeout(async () => { + try { + await this.executePoll(networkClientId); + } catch (error) { + console.error(error); + } + this.#poll(networkClientId); + }, this.#intervalLength); + } +} diff --git a/packages/polling-controller/src/index.ts b/packages/polling-controller/src/index.ts new file mode 100644 index 00000000000..1458c0cfe9b --- /dev/null +++ b/packages/polling-controller/src/index.ts @@ -0,0 +1 @@ +export { default as PollingController } from './PollingController'; diff --git a/packages/polling-controller/tsconfig.build.json b/packages/polling-controller/tsconfig.build.json new file mode 100644 index 00000000000..ac0df4920c6 --- /dev/null +++ b/packages/polling-controller/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" }, + { "path": "../network-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/polling-controller/tsconfig.json b/packages/polling-controller/tsconfig.json new file mode 100644 index 00000000000..4bbb0be81b1 --- /dev/null +++ b/packages/polling-controller/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../controller-utils" }, + { "path": "../network-controller" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/polling-controller/typedoc.json b/packages/polling-controller/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/polling-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/preferences-controller/CHANGELOG.md b/packages/preferences-controller/CHANGELOG.md index a8cad049d94..2f246bf4bea 100644 --- a/packages/preferences-controller/CHANGELOG.md +++ b/packages/preferences-controller/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [4.4.1] +### Changed +- Bump dependency on `@metamask/controller-utils` to ^5.0.0 + ## [4.4.0] ### Added - Add `isIpfsGatewayEnabled` property to PreferencesController state ([#1577](https://github.com/MetaMask/core/pull/1577)) @@ -48,7 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.2] ### Changed - Rename this repository to `core` ([#1031](https://github.com/MetaMask/controllers/pull/1031)) -- Update `@metamask/controller-utils` package ([#1041](https://github.com/MetaMask/controllers/pull/1041)) +- Update `@metamask/controller-utils` package ([#1041](https://github.com/MetaMask/controllers/pull/1041)) ## [1.0.1] ### Changed @@ -63,7 +67,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@4.4.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@4.4.1...HEAD +[4.4.1]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@4.4.0...@metamask/preferences-controller@4.4.1 [4.4.0]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@4.3.0...@metamask/preferences-controller@4.4.0 [4.3.0]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@4.2.0...@metamask/preferences-controller@4.3.0 [4.2.0]: https://github.com/MetaMask/core/compare/@metamask/preferences-controller@4.1.0...@metamask/preferences-controller@4.2.0 diff --git a/packages/preferences-controller/package.json b/packages/preferences-controller/package.json index 578a923c10d..e72014e400f 100644 --- a/packages/preferences-controller/package.json +++ b/packages/preferences-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/preferences-controller", - "version": "4.4.0", + "version": "4.4.1", "description": "Manages user-configurable settings for MetaMask", "keywords": [ "MetaMask", @@ -29,7 +29,7 @@ }, "dependencies": { "@metamask/base-controller": "^3.2.1", - "@metamask/controller-utils": "^4.3.2" + "@metamask/controller-utils": "^5.0.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.1.0", @@ -37,8 +37,8 @@ "deepmerge": "^4.2.2", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/rate-limit-controller/package.json b/packages/rate-limit-controller/package.json index a666b383fc2..d929f6f53d4 100644 --- a/packages/rate-limit-controller/package.json +++ b/packages/rate-limit-controller/package.json @@ -38,8 +38,8 @@ "deepmerge": "^4.2.2", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "engines": { diff --git a/packages/selected-network-controller/CHANGELOG.md b/packages/selected-network-controller/CHANGELOG.md index dc454350299..1e8b0225edd 100644 --- a/packages/selected-network-controller/CHANGELOG.md +++ b/packages/selected-network-controller/CHANGELOG.md @@ -6,9 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.0.0] +### Changed +- **BREAKING**: Bump peer dependency on `@metamask/network-controller` to ^13.0.0 ([#1633](https://github.com/MetaMask/core/pull/1633)) + ## [1.0.0] ### Added - Initial Release ([#1643](https://github.com/MetaMask/core/pull/1643)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/selected-network-controller@1.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/selected-network-controller@2.0.0...HEAD +[2.0.0]: https://github.com/MetaMask/core/compare/@metamask/selected-network-controller@1.0.0...@metamask/selected-network-controller@2.0.0 [1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/selected-network-controller@1.0.0 diff --git a/packages/selected-network-controller/package.json b/packages/selected-network-controller/package.json index e46b9dd7250..e49bc8a9b05 100644 --- a/packages/selected-network-controller/package.json +++ b/packages/selected-network-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/selected-network-controller", - "version": "1.0.0", + "version": "2.0.0", "description": "Provides an interface to the currently selected networkClientId for a given domain", "keywords": [ "MetaMask", @@ -29,7 +29,7 @@ }, "dependencies": { "@metamask/base-controller": "^3.2.1", - "@metamask/network-controller": "^12.2.0", + "@metamask/network-controller": "^13.0.0", "json-rpc-engine": "^6.1.0" }, "devDependencies": { @@ -42,12 +42,12 @@ "nock": "^13.3.1", "sinon": "^9.2.4", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "peerDependencies": { - "@metamask/network-controller": "^12.2.0" + "@metamask/network-controller": "^13.0.0" }, "engines": { "node": ">=16.0.0" diff --git a/packages/signature-controller/CHANGELOG.md b/packages/signature-controller/CHANGELOG.md index 3ae7f84e7de..9879eb8cc09 100644 --- a/packages/signature-controller/CHANGELOG.md +++ b/packages/signature-controller/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [6.1.0] +### Changed +- Add `LoggingController` logs on signature operation stages ([#1692](https://github.com/MetaMask/core/pull/1692)) +- Bump dependency on `@metamask/controller-utils` to ^5.0.0 +- Bump dependency on `@metamask/keyring-controller` to ^8.0.0 +- Bump dependency on `@metamask/logging-controller` to ^1.0.2 +- Bump dependency on `@metamask/message-manager` to ^7.3.3 + ## [6.0.0] ### Changed - **BREAKING**: Removed `keyringController` property from constructor option ([#1593](https://github.com/MetaMask/core/pull/1593)) @@ -70,7 +78,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial release ([#1214](https://github.com/MetaMask/core/pull/1214)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@6.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@6.1.0...HEAD +[6.1.0]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@6.0.0...@metamask/signature-controller@6.1.0 [6.0.0]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@5.3.1...@metamask/signature-controller@6.0.0 [5.3.1]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@5.3.0...@metamask/signature-controller@5.3.1 [5.3.0]: https://github.com/MetaMask/core/compare/@metamask/signature-controller@5.2.0...@metamask/signature-controller@5.3.0 diff --git a/packages/signature-controller/package.json b/packages/signature-controller/package.json index 9898fb91b72..16bda55af62 100644 --- a/packages/signature-controller/package.json +++ b/packages/signature-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/signature-controller", - "version": "6.0.0", + "version": "6.1.0", "description": "Processes signing requests in order to sign arbitrary and typed data", "keywords": [ "MetaMask", @@ -30,9 +30,9 @@ "dependencies": { "@metamask/approval-controller": "^3.5.1", "@metamask/base-controller": "^3.2.1", - "@metamask/controller-utils": "^4.3.2", - "@metamask/logging-controller": "^1.0.1", - "@metamask/message-manager": "^7.3.2", + "@metamask/controller-utils": "^5.0.0", + "@metamask/logging-controller": "^1.0.2", + "@metamask/message-manager": "^7.3.3", "@metamask/utils": "^8.1.0", "eth-rpc-errors": "^4.0.2", "ethereumjs-util": "^7.0.10", @@ -41,18 +41,18 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.1.0", - "@metamask/keyring-controller": "^7.5.0", + "@metamask/keyring-controller": "^8.0.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "peerDependencies": { "@metamask/approval-controller": "^3.5.1", - "@metamask/logging-controller": "^1.0.1" + "@metamask/logging-controller": "^1.0.2" }, "engines": { "node": ">=16.0.0" diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 9b59c07c434..0f63f6265ba 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -6,6 +6,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [12.0.0] +### Changed +- **BREAKING**: Use only `chainId` to determine if a transaction belongs to the current network ([#1633](https://github.com/MetaMask/core/pull/1633)) + - No longer uses `networkID` as a fallback if `chainId` is missing +- **BREAKING**: Change `TransactionMeta.chainId` to be required ([#1633](https://github.com/MetaMask/core/pull/1633)) +- **BREAKING**: Bump peer dependency on `@metamask/network-controller` to ^13.0.0 ([#1633](https://github.com/MetaMask/core/pull/1633)) +- Update `TransactionMeta.networkID` as deprecated ([#1633](https://github.com/MetaMask/core/pull/1633)) +- Change `TransactionMeta.networkID` to be readonly ([#1633](https://github.com/MetaMask/core/pull/1633)) +- Bump dependency on `@metamask/controller-utils` to ^5.0.0 ([#1633](https://github.com/MetaMask/core/pull/1633)) + +### Removed +- Remove `networkId` param from `RemoteTransactionSource.isSupportedNetwork()` interface ([#1633](https://github.com/MetaMask/core/pull/1633)) +- Remove `currentNetworkId` property from `RemoteTransactionSourceRequest` ([#1633](https://github.com/MetaMask/core/pull/1633)) + +## [11.1.0] +### Added +- Add `type` property to the transaction metadata ([#1670](https://github.com/MetaMask/core/pull/1670)) + ## [11.0.0] ### Added - Add optional `getLastBlockVariations` method to `RemoteTransactionSource` type ([#1668](https://github.com/MetaMask/core/pull/1668)) @@ -70,7 +88,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add incoming transaction methods ([#1579](https://github.com/MetaMask/core/pull/1579)) - `startIncomingTransactionPolling` - `stopIncomingTransactionPolling` - - `updateIncomingTransactions` + - `updateIncomingTransactions` - Add `requireApproval` option to `addTransaction` method options ([#1580](https://github.com/MetaMask/core/pull/1580)) - Add `address` argument to `wipeTransactions` method ([#1573](https://github.com/MetaMask/core/pull/1573)) @@ -110,7 +128,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [7.0.0] ### Changed - **BREAKING**: Change the approveTransaction and cancelTransaction methods to private ([#1435](https://github.com/MetaMask/core/pull/1435)) - - Consumers should migrate from use of these methods to use of `processApproval`. + - Consumers should migrate from use of these methods to use of `processApproval`. - Update the TransactionController to await the approval request promise before automatically performing the relevant logic, either signing and submitting the transaction, or cancelling it ([#1435](https://github.com/MetaMask/core/pull/1435)) ## [6.1.0] @@ -123,7 +141,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update transaction controller to automatically initiate, finalize, and cancel approval requests as transactions move through states ([#1241](https://github.com/MetaMask/core/pull/1241)) - The `ApprovalController:addRequest` action will be called when a new transaction is initiated - The `ApprovalController:rejectRequest` action will be called if a transaction fails - - The `ApprovalController:acceptRequest` action will be called when a transaction is approved + - The `ApprovalController:acceptRequest` action will be called when a transaction is approved ### Changed - **BREAKING:** Bump to Node 16 ([#1262](https://github.com/MetaMask/core/pull/1262)) @@ -185,7 +203,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 All changes listed after this point were applied to this package following the monorepo conversion. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@11.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@12.0.0...HEAD +[12.0.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@11.1.0...@metamask/transaction-controller@12.0.0 +[11.1.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@11.0.0...@metamask/transaction-controller@11.1.0 [11.0.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@10.0.0...@metamask/transaction-controller@11.0.0 [10.0.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@9.2.0...@metamask/transaction-controller@10.0.0 [9.2.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-controller@9.1.0...@metamask/transaction-controller@9.2.0 diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 81a3190b72f..9b8812b90be 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/transaction-controller", - "version": "11.0.0", + "version": "12.0.0", "description": "Stores transactions alongside their periodically updated statuses and manages interactions such as approval and cancellation", "keywords": [ "MetaMask", @@ -33,10 +33,10 @@ "@ethersproject/abi": "^5.7.0", "@metamask/approval-controller": "^3.5.1", "@metamask/base-controller": "^3.2.1", - "@metamask/controller-utils": "^4.3.2", + "@metamask/controller-utils": "^5.0.0", "@metamask/eth-query": "^3.0.1", "@metamask/metamask-eth-abis": "^3.0.0", - "@metamask/network-controller": "^12.2.0", + "@metamask/network-controller": "^13.0.0", "@metamask/utils": "^8.1.0", "async-mutex": "^0.2.6", "eth-method-registry": "1.1.0", @@ -57,13 +57,13 @@ "jest": "^27.5.1", "sinon": "^9.2.4", "ts-jest": "^27.1.4", - "typedoc": "^0.22.15", - "typedoc-plugin-missing-exports": "^0.22.6", + "typedoc": "^0.23.15", + "typedoc-plugin-missing-exports": "^0.23.0", "typescript": "~4.6.3" }, "peerDependencies": { "@metamask/approval-controller": "^3.5.1", - "@metamask/network-controller": "^12.2.0", + "@metamask/network-controller": "^13.0.0", "babel-runtime": "^6.26.0" }, "engines": { diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 5cf4544d57c..e6a29685607 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -430,6 +430,12 @@ export interface TransactionParams { * Value associated with this transaction. */ value?: string; + + /** + * Type of transaction. + * 0x0 indicates a legacy transaction. + */ + type?: string; } /** diff --git a/packages/transaction-controller/src/utils.test.ts b/packages/transaction-controller/src/utils.test.ts index d0932826a18..d54d47ddc8d 100644 --- a/packages/transaction-controller/src/utils.test.ts +++ b/packages/transaction-controller/src/utils.test.ts @@ -19,8 +19,8 @@ describe('utils', () => { jest.clearAllMocks(); }); - it('normalizeTxParams', () => { - const normalized = util.normalizeTxParams({ + describe('normalizeTxParams', () => { + const commonInput = { data: 'data', from: 'FROM', gas: 'gas', @@ -31,18 +31,43 @@ describe('utils', () => { maxFeePerGas: 'maxFeePerGas', maxPriorityFeePerGas: 'maxPriorityFeePerGas', estimatedBaseFee: 'estimatedBaseFee', + }; + + it('normalizeTransaction', () => { + const normalized = util.normalizeTxParams({ + ...commonInput, + }); + expect(normalized).toStrictEqual({ + data: '0xdata', + from: '0xfrom', + gas: '0xgas', + gasPrice: '0xgasPrice', + nonce: '0xnonce', + to: '0xto', + value: '0xvalue', + maxFeePerGas: '0xmaxFeePerGas', + maxPriorityFeePerGas: '0xmaxPriorityFeePerGas', + estimatedBaseFee: '0xestimatedBaseFee', + }); }); - expect(normalized).toStrictEqual({ - data: '0xdata', - from: '0xfrom', - gas: '0xgas', - gasPrice: '0xgasPrice', - nonce: '0xnonce', - to: '0xto', - value: '0xvalue', - maxFeePerGas: '0xmaxFeePerGas', - maxPriorityFeePerGas: '0xmaxPriorityFeePerGas', - estimatedBaseFee: '0xestimatedBaseFee', + it('normalizeTransaction if type is zero', () => { + const normalized = util.normalizeTxParams({ + ...commonInput, + type: '0x0', + }); + expect(normalized).toStrictEqual({ + data: '0xdata', + from: '0xfrom', + gas: '0xgas', + gasPrice: '0xgasPrice', + nonce: '0xnonce', + to: '0xto', + value: '0xvalue', + maxFeePerGas: '0xmaxFeePerGas', + maxPriorityFeePerGas: '0xmaxPriorityFeePerGas', + estimatedBaseFee: '0xestimatedBaseFee', + type: '0x0', + }); }); }); diff --git a/packages/transaction-controller/src/utils.ts b/packages/transaction-controller/src/utils.ts index 7bab7d91e44..13dd009818d 100644 --- a/packages/transaction-controller/src/utils.ts +++ b/packages/transaction-controller/src/utils.ts @@ -28,6 +28,7 @@ const NORMALIZERS: { [param in keyof TransactionParams]: any } = { addHexPrefix(maxPriorityFeePerGas), estimatedBaseFee: (maxPriorityFeePerGas: string) => addHexPrefix(maxPriorityFeePerGas), + type: (type: string) => (type === '0x0' ? '0x0' : undefined), }; /** diff --git a/yarn.lock b/yarn.lock index cba01517c1d..45dbc1826b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1295,7 +1295,7 @@ __metadata: "@metamask/base-controller": ^3.2.1 "@metamask/eth-snap-keyring": ^0.2.2 "@metamask/keyring-api": ^0.2.5 - "@metamask/keyring-controller": ^7.5.0 + "@metamask/keyring-controller": ^8.0.0 "@metamask/snaps-controllers": ^1.0.1 "@metamask/snaps-utils": ^1.0.1 "@metamask/utils": ^8.1.0 @@ -1308,12 +1308,12 @@ __metadata: jest: ^27.5.1 nanoid: ^3.1.31 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 uuid: ^8.3.2 peerDependencies: - "@metamask/keyring-controller": ^7.5.0 + "@metamask/keyring-controller": ^8.0.0 languageName: unknown linkType: soft @@ -1334,14 +1334,14 @@ __metadata: dependencies: "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 - "@metamask/controller-utils": ^4.3.2 + "@metamask/controller-utils": ^5.0.0 "@metamask/utils": ^8.1.0 "@types/jest": ^27.4.1 deepmerge: ^4.2.2 jest: ^27.5.1 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 languageName: unknown linkType: soft @@ -1357,8 +1357,8 @@ __metadata: immer: ^9.0.6 jest: ^27.5.1 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 languageName: unknown linkType: soft @@ -1369,17 +1369,17 @@ __metadata: dependencies: "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 + "@metamask/rpc-errors": ^6.0.0 "@metamask/utils": ^8.1.0 "@types/jest": ^27.4.1 deepmerge: ^4.2.2 - eth-rpc-errors: ^4.0.2 immer: ^9.0.6 jest: ^27.5.1 nanoid: ^3.1.31 sinon: ^9.2.4 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 languageName: unknown linkType: soft @@ -1397,17 +1397,16 @@ __metadata: "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 "@metamask/contract-metadata": ^2.3.1 - "@metamask/controller-utils": ^4.3.2 + "@metamask/controller-utils": ^5.0.0 "@metamask/eth-query": ^3.0.1 "@metamask/metamask-eth-abis": 3.0.0 - "@metamask/network-controller": ^12.2.0 - "@metamask/preferences-controller": ^4.4.0 - "@metamask/rpc-errors": ^5.1.1 + "@metamask/network-controller": ^13.0.0 + "@metamask/preferences-controller": ^4.4.1 + "@metamask/rpc-errors": ^6.0.0 "@metamask/utils": ^8.1.0 "@types/jest": ^27.4.1 "@types/node": ^16.18.24 "@types/uuid": ^8.3.0 - abort-controller: ^3.0.0 async-mutex: ^0.2.6 deepmerge: ^4.2.2 ethereumjs-util: ^7.0.10 @@ -1420,14 +1419,14 @@ __metadata: single-call-balance-checker-abi: ^1.0.0 sinon: ^9.2.4 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 uuid: ^8.3.2 peerDependencies: "@metamask/approval-controller": ^3.5.1 - "@metamask/network-controller": ^12.2.0 - "@metamask/preferences-controller": ^4.4.0 + "@metamask/network-controller": ^13.0.0 + "@metamask/preferences-controller": ^4.4.1 languageName: unknown linkType: soft @@ -1458,8 +1457,8 @@ __metadata: jest: ^27.5.1 sinon: ^9.2.4 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 languageName: unknown linkType: soft @@ -1483,8 +1482,8 @@ __metadata: jest: ^27.5.1 sinon: ^9.2.4 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 languageName: unknown linkType: soft @@ -1496,7 +1495,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@^4.3.2, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@^5.0.0, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -1505,18 +1504,16 @@ __metadata: "@metamask/utils": ^8.1.0 "@spruceid/siwe-parser": 1.1.3 "@types/jest": ^27.4.1 - abort-controller: ^3.0.0 deepmerge: ^4.2.2 eth-ens-namehash: ^2.0.8 - eth-rpc-errors: ^4.0.2 ethereumjs-util: ^7.0.10 ethjs-unit: ^0.1.6 fast-deep-equal: ^3.1.3 jest: ^27.5.1 nock: ^13.3.1 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 languageName: unknown linkType: soft @@ -1586,8 +1583,8 @@ __metadata: "@ethersproject/providers": ^5.7.0 "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 - "@metamask/controller-utils": ^4.3.2 - "@metamask/network-controller": ^12.2.0 + "@metamask/controller-utils": ^5.0.0 + "@metamask/network-controller": ^13.0.0 "@metamask/utils": ^8.1.0 "@types/jest": ^27.4.1 deepmerge: ^4.2.2 @@ -1595,11 +1592,11 @@ __metadata: jest: ^27.5.1 punycode: ^2.1.1 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 peerDependencies: - "@metamask/network-controller": ^12.2.0 + "@metamask/network-controller": ^13.0.0 languageName: unknown linkType: soft @@ -1807,9 +1804,9 @@ __metadata: dependencies: "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 - "@metamask/controller-utils": ^4.3.2 + "@metamask/controller-utils": ^5.0.0 "@metamask/eth-query": ^3.0.1 - "@metamask/network-controller": ^12.2.0 + "@metamask/network-controller": ^13.0.0 "@metamask/utils": ^8.1.0 "@types/jest": ^27.4.1 "@types/jest-when": ^2.7.3 @@ -1823,12 +1820,12 @@ __metadata: nock: ^13.3.1 sinon: ^9.2.4 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 uuid: ^8.3.2 peerDependencies: - "@metamask/network-controller": ^12.2.0 + "@metamask/network-controller": ^13.0.0 languageName: unknown linkType: soft @@ -1887,7 +1884,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@^7.5.0, @metamask/keyring-controller@workspace:packages/keyring-controller": +"@metamask/keyring-controller@^8.0.0, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" dependencies: @@ -1899,8 +1896,8 @@ __metadata: "@metamask/base-controller": ^3.2.1 "@metamask/eth-keyring-controller": ^13.0.1 "@metamask/eth-sig-util": ^7.0.0 - "@metamask/message-manager": ^7.3.2 - "@metamask/preferences-controller": ^4.4.0 + "@metamask/message-manager": ^7.3.3 + "@metamask/preferences-controller": ^4.4.1 "@metamask/scure-bip39": ^2.1.0 "@metamask/utils": ^8.1.0 "@types/jest": ^27.4.1 @@ -1912,40 +1909,40 @@ __metadata: jest: ^27.5.1 sinon: ^9.2.4 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 uuid: ^8.3.2 peerDependencies: - "@metamask/preferences-controller": ^4.4.0 + "@metamask/preferences-controller": ^4.4.1 languageName: unknown linkType: soft -"@metamask/logging-controller@^1.0.1, @metamask/logging-controller@workspace:packages/logging-controller": +"@metamask/logging-controller@^1.0.2, @metamask/logging-controller@workspace:packages/logging-controller": version: 0.0.0-use.local resolution: "@metamask/logging-controller@workspace:packages/logging-controller" dependencies: "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 - "@metamask/controller-utils": ^4.3.2 + "@metamask/controller-utils": ^5.0.0 "@types/jest": ^27.4.1 deepmerge: ^4.2.2 jest: ^27.5.1 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 uuid: ^8.3.2 languageName: unknown linkType: soft -"@metamask/message-manager@^7.3.2, @metamask/message-manager@workspace:packages/message-manager": +"@metamask/message-manager@^7.3.3, @metamask/message-manager@workspace:packages/message-manager": version: 0.0.0-use.local resolution: "@metamask/message-manager@workspace:packages/message-manager" dependencies: "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 - "@metamask/controller-utils": ^4.3.2 + "@metamask/controller-utils": ^5.0.0 "@metamask/eth-sig-util": ^7.0.0 "@metamask/utils": ^8.1.0 "@types/jest": ^27.4.1 @@ -1955,8 +1952,8 @@ __metadata: jest: ^27.5.1 jsonschema: ^1.2.4 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 uuid: ^8.3.2 languageName: unknown @@ -1975,25 +1972,27 @@ __metadata: dependencies: "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 + "@metamask/utils": ^8.1.0 "@types/jest": ^27.4.1 + async-mutex: ^0.2.6 deepmerge: ^4.2.2 immer: ^9.0.6 jest: ^27.5.1 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 languageName: unknown linkType: soft -"@metamask/network-controller@^12.2.0, @metamask/network-controller@workspace:packages/network-controller": +"@metamask/network-controller@^13.0.0, @metamask/network-controller@workspace:packages/network-controller": version: 0.0.0-use.local resolution: "@metamask/network-controller@workspace:packages/network-controller" dependencies: "@json-rpc-specification/meta-schema": ^1.0.6 "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 - "@metamask/controller-utils": ^4.3.2 + "@metamask/controller-utils": ^5.0.0 "@metamask/eth-json-rpc-infura": ^8.1.1 "@metamask/eth-json-rpc-middleware": ^11.0.2 "@metamask/eth-json-rpc-provider": ^1.0.0 @@ -2015,8 +2014,8 @@ __metadata: nock: ^13.3.1 sinon: ^9.2.4 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 uuid: ^8.3.2 languageName: unknown @@ -2035,8 +2034,8 @@ __metadata: jest: ^27.5.1 nanoid: ^3.1.31 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 languageName: unknown linkType: soft @@ -2079,20 +2078,20 @@ __metadata: "@metamask/approval-controller": ^3.5.1 "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 - "@metamask/controller-utils": ^4.3.2 + "@metamask/controller-utils": ^5.0.0 "@metamask/json-rpc-engine": ^7.1.1 + "@metamask/rpc-errors": ^6.0.0 "@metamask/utils": ^8.1.0 "@types/deep-freeze-strict": ^1.1.0 "@types/jest": ^27.4.1 deep-freeze-strict: ^1.1.1 deepmerge: ^4.2.2 - eth-rpc-errors: ^4.0.2 immer: ^9.0.6 jest: ^27.5.1 nanoid: ^3.1.31 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 peerDependencies: "@metamask/approval-controller": ^3.5.1 @@ -2105,7 +2104,7 @@ __metadata: dependencies: "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 - "@metamask/controller-utils": ^4.3.2 + "@metamask/controller-utils": ^5.0.0 "@types/jest": ^27.4.1 "@types/punycode": ^2.1.0 deepmerge: ^4.2.2 @@ -2115,9 +2114,32 @@ __metadata: punycode: ^2.1.1 sinon: ^9.2.4 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 + typescript: ~4.6.3 + languageName: unknown + linkType: soft + +"@metamask/polling-controller@workspace:packages/polling-controller": + version: 0.0.0-use.local + resolution: "@metamask/polling-controller@workspace:packages/polling-controller" + dependencies: + "@metamask/auto-changelog": ^3.1.0 + "@metamask/base-controller": ^3.2.1 + "@metamask/controller-utils": ^5.0.0 + "@metamask/network-controller": ^13.0.0 + "@metamask/utils": ^8.1.0 + "@types/jest": ^27.4.1 + "@types/uuid": ^8.3.0 + deepmerge: ^4.2.2 + jest: ^27.5.1 + ts-jest: ^27.1.4 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 + uuid: ^8.3.2 + peerDependencies: + "@metamask/network-controller": ^13.0.0 languageName: unknown linkType: soft @@ -2131,19 +2153,19 @@ __metadata: languageName: node linkType: hard -"@metamask/preferences-controller@^4.4.0, @metamask/preferences-controller@workspace:packages/preferences-controller": +"@metamask/preferences-controller@^4.4.1, @metamask/preferences-controller@workspace:packages/preferences-controller": version: 0.0.0-use.local resolution: "@metamask/preferences-controller@workspace:packages/preferences-controller" dependencies: "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 - "@metamask/controller-utils": ^4.3.2 + "@metamask/controller-utils": ^5.0.0 "@types/jest": ^27.4.1 deepmerge: ^4.2.2 jest: ^27.5.1 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 languageName: unknown linkType: soft @@ -2218,22 +2240,12 @@ __metadata: immer: ^9.0.6 jest: ^27.5.1 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 languageName: unknown linkType: soft -"@metamask/rpc-errors@npm:^5.1.1": - version: 5.1.1 - resolution: "@metamask/rpc-errors@npm:5.1.1" - dependencies: - "@metamask/utils": ^5.0.0 - fast-safe-stringify: ^2.0.6 - checksum: ccd1b24da66af3ae63960b79c04b86efb8b96acb89ca6f7e0bbfe636d23ba5cddeba533c0692eafb87c44ec6f840085372d0f21b39e05df9a80700ff61538a30 - languageName: node - linkType: hard - "@metamask/rpc-errors@npm:^6.0.0": version: 6.0.0 resolution: "@metamask/rpc-errors@npm:6.0.0" @@ -2308,7 +2320,7 @@ __metadata: dependencies: "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 - "@metamask/network-controller": ^12.2.0 + "@metamask/network-controller": ^13.0.0 "@types/jest": ^27.4.1 deepmerge: ^4.2.2 immer: ^9.0.6 @@ -2318,11 +2330,11 @@ __metadata: nock: ^13.3.1 sinon: ^9.2.4 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 peerDependencies: - "@metamask/network-controller": ^12.2.0 + "@metamask/network-controller": ^13.0.0 languageName: unknown linkType: soft @@ -2333,10 +2345,10 @@ __metadata: "@metamask/approval-controller": ^3.5.1 "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 - "@metamask/controller-utils": ^4.3.2 - "@metamask/keyring-controller": ^7.5.0 - "@metamask/logging-controller": ^1.0.1 - "@metamask/message-manager": ^7.3.2 + "@metamask/controller-utils": ^5.0.0 + "@metamask/keyring-controller": ^8.0.0 + "@metamask/logging-controller": ^1.0.2 + "@metamask/message-manager": ^7.3.3 "@metamask/utils": ^8.1.0 "@types/jest": ^27.4.1 deepmerge: ^4.2.2 @@ -2346,12 +2358,12 @@ __metadata: jest: ^27.5.1 lodash: ^4.17.21 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 peerDependencies: "@metamask/approval-controller": ^3.5.1 - "@metamask/logging-controller": ^1.0.1 + "@metamask/logging-controller": ^1.0.2 languageName: unknown linkType: soft @@ -2558,10 +2570,10 @@ __metadata: "@metamask/approval-controller": ^3.5.1 "@metamask/auto-changelog": ^3.1.0 "@metamask/base-controller": ^3.2.1 - "@metamask/controller-utils": ^4.3.2 + "@metamask/controller-utils": ^5.0.0 "@metamask/eth-query": ^3.0.1 "@metamask/metamask-eth-abis": ^3.0.0 - "@metamask/network-controller": ^12.2.0 + "@metamask/network-controller": ^13.0.0 "@metamask/utils": ^8.1.0 "@types/jest": ^27.4.1 "@types/node": ^16.18.24 @@ -2578,13 +2590,13 @@ __metadata: nonce-tracker: ^1.1.0 sinon: ^9.2.4 ts-jest: ^27.1.4 - typedoc: ^0.22.15 - typedoc-plugin-missing-exports: ^0.22.6 + typedoc: ^0.23.15 + typedoc-plugin-missing-exports: ^0.23.0 typescript: ~4.6.3 uuid: ^8.3.2 peerDependencies: "@metamask/approval-controller": ^3.5.1 - "@metamask/network-controller": ^12.2.0 + "@metamask/network-controller": ^13.0.0 babel-runtime: ^6.26.0 languageName: unknown linkType: soft @@ -3424,15 +3436,6 @@ __metadata: languageName: node linkType: hard -"abort-controller@npm:^3.0.0": - version: 3.0.0 - resolution: "abort-controller@npm:3.0.0" - dependencies: - event-target-shim: ^5.0.0 - checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75 - languageName: node - linkType: hard - "acorn-globals@npm:^6.0.0": version: 6.0.0 resolution: "acorn-globals@npm:6.0.0" @@ -3556,6 +3559,13 @@ __metadata: languageName: node linkType: hard +"ansi-sequence-parser@npm:^1.1.0": + version: 1.1.1 + resolution: "ansi-sequence-parser@npm:1.1.1" + checksum: ead5b15c596e8e85ca02951a844366c6776769dcc9fd1bd3a0db11bb21364554822c6a439877fb599e7e1ffa0b5f039f1e5501423950457f3dcb2f480c30b188 + languageName: node + linkType: hard + "ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -5564,13 +5574,6 @@ __metadata: languageName: node linkType: hard -"event-target-shim@npm:^5.0.0": - version: 5.0.1 - resolution: "event-target-shim@npm:5.0.1" - checksum: 1ffe3bb22a6d51bdeb6bf6f7cf97d2ff4a74b017ad12284cc9e6a279e727dc30a5de6bb613e5596ff4dc3e517841339ad09a7eec44266eccb1aa201a30448166 - languageName: node - linkType: hard - "evp_bytestokey@npm:^1.0.3": version: 1.0.3 resolution: "evp_bytestokey@npm:1.0.3" @@ -6045,19 +6048,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.3": - version: 8.1.0 - resolution: "glob@npm:8.1.0" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^5.0.1 - once: ^1.3.0 - checksum: 92fbea3221a7d12075f26f0227abac435de868dd0736a17170663783296d0dd8d3d532a5672b4488a439bf5d7fb85cdd07c11185d6cd39184f0385cbdfb86a47 - languageName: node - linkType: hard - "globals@npm:^11.1.0": version: 11.12.0 resolution: "globals@npm:11.12.0" @@ -7648,7 +7638,7 @@ __metadata: languageName: node linkType: hard -"jsonc-parser@npm:^3.0.0": +"jsonc-parser@npm:^3.2.0": version: 3.2.0 resolution: "jsonc-parser@npm:3.2.0" checksum: 946dd9a5f326b745aa326d48a7257e3f4a4b62c5e98ec8e49fa2bdd8d96cef7e6febf1399f5c7016114fd1f68a1c62c6138826d5d90bc650448e3cf0951c53c7 @@ -7852,7 +7842,7 @@ __metadata: languageName: node linkType: hard -"marked@npm:^4.0.16": +"marked@npm:^4.2.12": version: 4.3.0 resolution: "marked@npm:4.3.0" bin: @@ -7956,12 +7946,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": - version: 5.1.6 - resolution: "minimatch@npm:5.1.6" +"minimatch@npm:^7.1.3": + version: 7.4.6 + resolution: "minimatch@npm:7.4.6" dependencies: brace-expansion: ^2.0.1 - checksum: 7564208ef81d7065a370f788d337cd80a689e981042cb9a1d0e6580b6c6a8c9279eba80010516e258835a988363f99f54a6f711a315089b8b42694f5da9d0d77 + checksum: 1a6c8d22618df9d2a88aabeef1de5622eb7b558e9f8010be791cb6b0fa6e102d39b11c28d75b855a1e377b12edc7db8ff12a99c20353441caa6a05e78deb5da9 languageName: node linkType: hard @@ -9226,14 +9216,15 @@ __metadata: languageName: node linkType: hard -"shiki@npm:^0.10.1": - version: 0.10.1 - resolution: "shiki@npm:0.10.1" +"shiki@npm:^0.14.1": + version: 0.14.4 + resolution: "shiki@npm:0.14.4" dependencies: - jsonc-parser: ^3.0.0 - vscode-oniguruma: ^1.6.1 - vscode-textmate: 5.2.0 - checksum: fb746f3cb3de7e545e3b10a6cb658d3938f840e4ccc9a3c90ceb7e69a8f89dbb432171faac1e9f02a03f103684dad88ee5e54b5c4964fa6b579fca6e8e26424d + ansi-sequence-parser: ^1.1.0 + jsonc-parser: ^3.2.0 + vscode-oniguruma: ^1.7.0 + vscode-textmate: ^8.0.0 + checksum: 1173f6fa9531690a8cd4bf1d8e28c9eb9295af38a4c150cba6546e95f6e32bc96c7dd98826e39e688f1ca9d36b683a9a02ef77d51ce6495900b3a46ada64f828 languageName: node linkType: hard @@ -10029,29 +10020,28 @@ __metadata: languageName: node linkType: hard -"typedoc-plugin-missing-exports@npm:^0.22.6": - version: 0.22.6 - resolution: "typedoc-plugin-missing-exports@npm:0.22.6" +"typedoc-plugin-missing-exports@npm:^0.23.0": + version: 0.23.0 + resolution: "typedoc-plugin-missing-exports@npm:0.23.0" peerDependencies: - typedoc: 0.22.x - checksum: 012f44beaac05731b4d37c26bfba2d972ac48344eab6be793a95982712a207416e7dc3451279be4e4cebc8b6b99a16764d2d4aaa06e041fac60ce21cc22cf796 + typedoc: 0.22.x || 0.23.x + checksum: b3fc9eccca88a9ffb686d1e9ba923178c54b4bb7e496823b7b971b6f6baa957263f7ccff058f5b0e579fee49c93da09dbdc3a4dafd713960d93b2832de8094e1 languageName: node linkType: hard -"typedoc@npm:^0.22.15": - version: 0.22.18 - resolution: "typedoc@npm:0.22.18" +"typedoc@npm:^0.23.15": + version: 0.23.28 + resolution: "typedoc@npm:0.23.28" dependencies: - glob: ^8.0.3 lunr: ^2.3.9 - marked: ^4.0.16 - minimatch: ^5.1.0 - shiki: ^0.10.1 + marked: ^4.2.12 + minimatch: ^7.1.3 + shiki: ^0.14.1 peerDependencies: - typescript: 4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x || 4.7.x + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x bin: typedoc: bin/typedoc - checksum: b813d8129682f6ed5a4e96bacaf019e4da1d2744ca89fef850d6bb4c034616567ce67e6a7f5cfc5f00aac573f0b45d44b1427aafa262ab88dce6b460cb9e744c + checksum: 40eb4e207aac1b734e09400cf03f543642cc7b11000895198dd5a0d3166315759ccf4ac30a2915153597c5c186101c72bac2f1fc12b428184a9274d3a0e44c5e languageName: node linkType: hard @@ -10233,17 +10223,17 @@ __metadata: languageName: node linkType: hard -"vscode-oniguruma@npm:^1.6.1": +"vscode-oniguruma@npm:^1.7.0": version: 1.7.0 resolution: "vscode-oniguruma@npm:1.7.0" checksum: 53519d91d90593e6fb080260892e87d447e9b200c4964d766772b5053f5699066539d92100f77f1302c91e8fc5d9c772fbe40fe4c90f3d411a96d5a9b1e63f42 languageName: node linkType: hard -"vscode-textmate@npm:5.2.0": - version: 5.2.0 - resolution: "vscode-textmate@npm:5.2.0" - checksum: 5449b42d451080f6f3649b66948f4b5ee4643c4e88cfe3558a3b31c84c78060cfdd288c4958c1690eaa5cd65d09992fa6b7c3bef9d4aa72b3651054a04624d20 +"vscode-textmate@npm:^8.0.0": + version: 8.0.0 + resolution: "vscode-textmate@npm:8.0.0" + checksum: 127780dfea89559d70b8326df6ec344cfd701312dd7f3f591a718693812b7852c30b6715e3cfc8b3200a4e2515b4c96f0843c0eacc0a3020969b5de262c2a4bb languageName: node linkType: hard