From 5ce2ab7c5d410889d6a569cd2b770e5cfb5d6a89 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 1 Jun 2026 16:45:15 +0200 Subject: [PATCH 01/10] feat(wallet): wire ApprovalController into default initialization Adds an `ApprovalController` initialization configuration to the default wallet ensemble, following the new-design recipe (namespaced `getMessenger` + `init` returning the instance). The controller owns only its own actions/events, so no external messenger delegation is required. Consumers can supply the `showApprovalRequest` callback (which surfaces a pending approval request to the user) via the new `instanceOptions.approvalController` slot; it defaults to a no-op so the controller works headlessly. EVM signing/transaction approval types are excluded from per-origin rate limiting. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/wallet/CHANGELOG.md | 5 ++ packages/wallet/package.json | 2 + packages/wallet/src/Wallet.test.ts | 25 +++++++ .../instances/approval-controller.test.ts | 75 +++++++++++++++++++ .../instances/approval-controller.ts | 43 +++++++++++ .../src/initialization/instances/index.ts | 1 + packages/wallet/src/types.ts | 4 + packages/wallet/tsconfig.build.json | 2 + packages/wallet/tsconfig.json | 2 + yarn.lock | 2 + 10 files changed, 161 insertions(+) create mode 100644 packages/wallet/src/initialization/instances/approval-controller.test.ts create mode 100644 packages/wallet/src/initialization/instances/approval-controller.ts diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index 77cb0f75b1..59d9b8e8a4 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Wire `ApprovalController` into the default wallet initialization ([#8953](https://github.com/MetaMask/core/pull/8953)) + - Adds an `approvalController.showApprovalRequest` slot to `instanceOptions` for supplying the callback that surfaces pending approval requests to the user. + ## [1.0.1] ### Changed diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 2f75bb0c42..15a0883066 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -53,8 +53,10 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { + "@metamask/approval-controller": "^9.0.1", "@metamask/base-controller": "^9.1.0", "@metamask/browser-passworder": "^6.0.0", + "@metamask/controller-utils": "^12.1.0", "@metamask/keyring-controller": "^26.0.0", "@metamask/messenger": "^1.2.0", "@metamask/scure-bip39": "^2.1.1", diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index a3a8dca781..812f3847f7 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -225,4 +225,29 @@ describe('Wallet', () => { }); }); }); + + describe('ApprovalController', () => { + it('is wired into the default wallet', () => { + const wallet = new Wallet({}); + + expect(wallet.state.ApprovalController).toStrictEqual({ + pendingApprovals: {}, + pendingApprovalCount: 0, + approvalFlows: [], + }); + }); + + it('invokes the provided showApprovalRequest when a flow starts', () => { + const showApprovalRequest = jest.fn(); + const wallet = new Wallet({ + instanceOptions: { + approvalController: { showApprovalRequest }, + }, + }); + + wallet.messenger.call('ApprovalController:startFlow'); + + expect(showApprovalRequest).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/wallet/src/initialization/instances/approval-controller.test.ts b/packages/wallet/src/initialization/instances/approval-controller.test.ts new file mode 100644 index 0000000000..125c88b5a6 --- /dev/null +++ b/packages/wallet/src/initialization/instances/approval-controller.test.ts @@ -0,0 +1,75 @@ +import { ApprovalController } from '@metamask/approval-controller'; +import { Messenger } from '@metamask/messenger'; + +import { approvalController } from './approval-controller'; +import type { DefaultActions, DefaultEvents, RootMessenger } from '../defaults'; + +/** + * Creates a root messenger for use in tests. + * + * @returns A root messenger. + */ +function getRootMessenger(): RootMessenger { + return new Messenger({ namespace: 'Root' }); +} + +describe('approvalController', () => { + it('initializes an ApprovalController with default state', () => { + const messenger = approvalController.getMessenger(getRootMessenger()); + + const instance = approvalController.init({ + state: undefined, + messenger, + options: {}, + }); + + expect(instance).toBeInstanceOf(ApprovalController); + expect(instance.state).toStrictEqual({ + pendingApprovals: {}, + pendingApprovalCount: 0, + approvalFlows: [], + }); + }); + + it('uses the provided showApprovalRequest callback', () => { + const messenger = approvalController.getMessenger(getRootMessenger()); + const showApprovalRequest = jest.fn(); + + const instance = approvalController.init({ + state: undefined, + messenger, + options: { showApprovalRequest }, + }); + + instance.startFlow(); + + expect(showApprovalRequest).toHaveBeenCalledTimes(1); + }); + + it('defaults showApprovalRequest to a no-op when omitted', () => { + const messenger = approvalController.getMessenger(getRootMessenger()); + + const instance = approvalController.init({ + state: undefined, + messenger, + options: {}, + }); + + expect(() => instance.startFlow()).not.toThrow(); + }); + + it('exposes its actions through the root messenger', () => { + const rootMessenger = getRootMessenger(); + const messenger = approvalController.getMessenger(rootMessenger); + + approvalController.init({ state: undefined, messenger, options: {} }); + + expect( + rootMessenger.call('ApprovalController:getState'), + ).toStrictEqual({ + pendingApprovals: {}, + pendingApprovalCount: 0, + approvalFlows: [], + }); + }); +}); diff --git a/packages/wallet/src/initialization/instances/approval-controller.ts b/packages/wallet/src/initialization/instances/approval-controller.ts new file mode 100644 index 0000000000..6dff23f016 --- /dev/null +++ b/packages/wallet/src/initialization/instances/approval-controller.ts @@ -0,0 +1,43 @@ +import { + ApprovalController, + ApprovalControllerMessenger, +} from '@metamask/approval-controller'; +import { ApprovalType } from '@metamask/controller-utils'; +import { Messenger } from '@metamask/messenger'; + +import { InitializationConfiguration } from '../types'; + +/** + * Approval types whose pending requests are exempt from per-origin rate + * limiting, allowing multiple requests of these EVM signing/transaction types + * to be queued from the same origin. + */ +const typesExcludedFromRateLimiting = [ + ApprovalType.PersonalSign, + ApprovalType.EthSignTypedData, + ApprovalType.Transaction, + ApprovalType.WatchAsset, + ApprovalType.EthGetEncryptionPublicKey, + ApprovalType.EthDecrypt, +]; + +export const approvalController: InitializationConfiguration< + ApprovalController, + ApprovalControllerMessenger +> = { + name: 'ApprovalController', + init: ({ state, messenger, options }) => + new ApprovalController({ + state, + messenger, + // The consumer supplies the callback that surfaces a pending request to + // the user; default to a no-op so the controller works headlessly. + showApprovalRequest: options.showApprovalRequest ?? ((): void => undefined), + typesExcludedFromRateLimiting, + }), + getMessenger: (parent) => + new Messenger({ + namespace: 'ApprovalController', + parent, + }), +}; diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts index 28a3bf2f23..b10bcaacd4 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -1 +1,2 @@ +export { approvalController } from './approval-controller'; export { keyringController } from './keyring-controller'; diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 4d385c85a9..92e064e0b7 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -1,3 +1,4 @@ +import type { ShowApprovalRequest } from '@metamask/approval-controller'; import { KeyringControllerOptions } from '@metamask/keyring-controller'; import type { Json } from '@metamask/utils'; @@ -20,6 +21,9 @@ export type WalletOptions = { }; export type InstanceSpecificOptions = { + approvalController?: { + showApprovalRequest?: ShowApprovalRequest; + }; keyringController?: { encryptor?: GenericEncryptor; keyringBuilders?: KeyringControllerOptions['keyringBuilders']; diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json index b16ce7cfcd..65b8062a0b 100644 --- a/packages/wallet/tsconfig.build.json +++ b/packages/wallet/tsconfig.build.json @@ -6,7 +6,9 @@ "rootDir": "./src" }, "references": [ + { "path": "../approval-controller/tsconfig.build.json" }, { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" }, { "path": "../keyring-controller/tsconfig.build.json" }, { "path": "../messenger/tsconfig.build.json" } ], diff --git a/packages/wallet/tsconfig.json b/packages/wallet/tsconfig.json index 1b4b6d0a5f..26bcd2ab54 100644 --- a/packages/wallet/tsconfig.json +++ b/packages/wallet/tsconfig.json @@ -4,7 +4,9 @@ "baseUrl": "./" }, "references": [ + { "path": "../approval-controller/tsconfig.json" }, { "path": "../base-controller/tsconfig.json" }, + { "path": "../controller-utils/tsconfig.json" }, { "path": "../keyring-controller/tsconfig.json" }, { "path": "../messenger/tsconfig.json" } ], diff --git a/yarn.lock b/yarn.lock index c9697f19f6..e72c0a4f24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5883,9 +5883,11 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/wallet@workspace:packages/wallet" dependencies: + "@metamask/approval-controller": "npm:^9.0.1" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" "@metamask/browser-passworder": "npm:^6.0.0" + "@metamask/controller-utils": "npm:^12.1.0" "@metamask/keyring-controller": "npm:^26.0.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/scure-bip39": "npm:^2.1.1" From 5db5ae582eb6c101fb3c22d41002f5fce619dae6 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 1 Jun 2026 16:54:14 +0200 Subject: [PATCH 02/10] feat(wallet): make ApprovalController options universal across platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the wiring usable by extension and mobile, not just wallet-cli: expose `typesExcludedFromRateLimiting` as an injectable option alongside `showApprovalRequest`, defaulting to the platform-agnostic EVM signing/transaction baseline. The rate-limiting exclusion set genuinely differs per platform — the extension and mobile each append client-specific types (their smart-transaction status page and `snap_dialog`) and even use different string values for the same concept — so no single hardcoded list is correct. Clients now pass their exact list; consumers that omit it get the EVM default. `showApprovalRequest` continues to default to a no-op (matching mobile; the extension injects its own). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/wallet/CHANGELOG.md | 2 +- .../instances/approval-controller.test.ts | 42 +++++++++++++++++++ .../instances/approval-controller.ts | 23 +++++++--- packages/wallet/src/types.ts | 1 + 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index 59d9b8e8a4..39115c7e33 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Wire `ApprovalController` into the default wallet initialization ([#8953](https://github.com/MetaMask/core/pull/8953)) - - Adds an `approvalController.showApprovalRequest` slot to `instanceOptions` for supplying the callback that surfaces pending approval requests to the user. + - Adds an `approvalController` slot to `instanceOptions` with `showApprovalRequest` (the callback that surfaces pending approval requests to the user; defaults to a no-op) and `typesExcludedFromRateLimiting` (the approval types exempt from per-origin rate limiting; defaults to the EVM signing/transaction types). Both let consumers (extension, mobile, wallet-cli) inject their platform-specific values. ## [1.0.1] diff --git a/packages/wallet/src/initialization/instances/approval-controller.test.ts b/packages/wallet/src/initialization/instances/approval-controller.test.ts index 125c88b5a6..bf07bc7249 100644 --- a/packages/wallet/src/initialization/instances/approval-controller.test.ts +++ b/packages/wallet/src/initialization/instances/approval-controller.test.ts @@ -1,4 +1,5 @@ import { ApprovalController } from '@metamask/approval-controller'; +import { ApprovalType } from '@metamask/controller-utils'; import { Messenger } from '@metamask/messenger'; import { approvalController } from './approval-controller'; @@ -58,6 +59,47 @@ describe('approvalController', () => { expect(() => instance.startFlow()).not.toThrow(); }); + it('excludes EVM signing types from rate limiting by default', () => { + const messenger = approvalController.getMessenger(getRootMessenger()); + + const instance = approvalController.init({ + state: undefined, + messenger, + options: {}, + }); + + // An excluded type allows multiple pending requests from the same origin. + // The pending promises never settle here; `.catch` only marks them handled. + expect(() => { + instance + .add({ origin: 'metamask.io', type: ApprovalType.Transaction }) + .catch(() => undefined); + instance + .add({ origin: 'metamask.io', type: ApprovalType.Transaction }) + .catch(() => undefined); + }).not.toThrow(); + }); + + it('honors a custom typesExcludedFromRateLimiting list that overrides the default', () => { + const messenger = approvalController.getMessenger(getRootMessenger()); + + const instance = approvalController.init({ + state: undefined, + messenger, + // Empty override: nothing is excluded, not even the default EVM types. + options: { typesExcludedFromRateLimiting: [] }, + }); + + instance + .add({ origin: 'metamask.io', type: ApprovalType.Transaction }) + .catch(() => undefined); + + // A second request of the same origin and type is now rate-limited. + expect(() => + instance.add({ origin: 'metamask.io', type: ApprovalType.Transaction }), + ).toThrow('already pending'); + }); + it('exposes its actions through the root messenger', () => { const rootMessenger = getRootMessenger(); const messenger = approvalController.getMessenger(rootMessenger); diff --git a/packages/wallet/src/initialization/instances/approval-controller.ts b/packages/wallet/src/initialization/instances/approval-controller.ts index 6dff23f016..bd63c5cf37 100644 --- a/packages/wallet/src/initialization/instances/approval-controller.ts +++ b/packages/wallet/src/initialization/instances/approval-controller.ts @@ -8,11 +8,18 @@ import { Messenger } from '@metamask/messenger'; import { InitializationConfiguration } from '../types'; /** - * Approval types whose pending requests are exempt from per-origin rate - * limiting, allowing multiple requests of these EVM signing/transaction types - * to be queued from the same origin. + * The platform-agnostic baseline of approval types whose pending requests are + * exempt from per-origin rate limiting, allowing multiple requests of these EVM + * signing/transaction types to be queued from the same origin. + * + * Clients can override this entirely via + * `instanceOptions.approvalController.typesExcludedFromRateLimiting`. The set + * differs per platform — the extension and mobile each append client-specific + * types (e.g. their smart-transaction status page and `snap_dialog`), and even + * use different string values for the same concept — so there is no single + * correct list to hardcode here. */ -const typesExcludedFromRateLimiting = [ +const DEFAULT_TYPES_EXCLUDED_FROM_RATE_LIMITING = [ ApprovalType.PersonalSign, ApprovalType.EthSignTypedData, ApprovalType.Transaction, @@ -31,9 +38,13 @@ export const approvalController: InitializationConfiguration< state, messenger, // The consumer supplies the callback that surfaces a pending request to - // the user; default to a no-op so the controller works headlessly. + // the user; default to a no-op so the controller works headlessly (this + // matches mobile, which drives approvals through state; the extension + // injects its own). showApprovalRequest: options.showApprovalRequest ?? ((): void => undefined), - typesExcludedFromRateLimiting, + typesExcludedFromRateLimiting: + options.typesExcludedFromRateLimiting ?? + DEFAULT_TYPES_EXCLUDED_FROM_RATE_LIMITING, }), getMessenger: (parent) => new Messenger({ diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 92e064e0b7..2b5e726296 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -23,6 +23,7 @@ export type WalletOptions = { export type InstanceSpecificOptions = { approvalController?: { showApprovalRequest?: ShowApprovalRequest; + typesExcludedFromRateLimiting?: string[]; }; keyringController?: { encryptor?: GenericEncryptor; From 5f9fd209c7d9825a7a382c393d378b543ea2838e Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 1 Jun 2026 18:56:23 +0200 Subject: [PATCH 03/10] =?UTF-8?q?chore(wallet):=20fix=20CI=20=E2=80=94=20r?= =?UTF-8?q?egenerate=20README=20graph=20and=20apply=20oxfmt=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Regenerate the root README dependency graph for the new `@metamask/approval-controller` and `@metamask/controller-utils` deps (`readme-content:check`). - Apply `oxfmt` formatting to the approval-controller files (`lint:misc:check`). Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 ++ .../initialization/instances/approval-controller.test.ts | 6 ++---- .../src/initialization/instances/approval-controller.ts | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 125343e1c9..10b7c254c2 100644 --- a/README.md +++ b/README.md @@ -565,7 +565,9 @@ linkStyle default opacity:0.5 user_operation_controller --> polling_controller; user_operation_controller --> transaction_controller; user_operation_controller --> eth_block_tracker; + wallet --> approval_controller; wallet --> base_controller; + wallet --> controller_utils; wallet --> keyring_controller; wallet --> messenger; ``` diff --git a/packages/wallet/src/initialization/instances/approval-controller.test.ts b/packages/wallet/src/initialization/instances/approval-controller.test.ts index bf07bc7249..df32a5904c 100644 --- a/packages/wallet/src/initialization/instances/approval-controller.test.ts +++ b/packages/wallet/src/initialization/instances/approval-controller.test.ts @@ -2,8 +2,8 @@ import { ApprovalController } from '@metamask/approval-controller'; import { ApprovalType } from '@metamask/controller-utils'; import { Messenger } from '@metamask/messenger'; -import { approvalController } from './approval-controller'; import type { DefaultActions, DefaultEvents, RootMessenger } from '../defaults'; +import { approvalController } from './approval-controller'; /** * Creates a root messenger for use in tests. @@ -106,9 +106,7 @@ describe('approvalController', () => { approvalController.init({ state: undefined, messenger, options: {} }); - expect( - rootMessenger.call('ApprovalController:getState'), - ).toStrictEqual({ + expect(rootMessenger.call('ApprovalController:getState')).toStrictEqual({ pendingApprovals: {}, pendingApprovalCount: 0, approvalFlows: [], diff --git a/packages/wallet/src/initialization/instances/approval-controller.ts b/packages/wallet/src/initialization/instances/approval-controller.ts index bd63c5cf37..2423c4ac5d 100644 --- a/packages/wallet/src/initialization/instances/approval-controller.ts +++ b/packages/wallet/src/initialization/instances/approval-controller.ts @@ -41,7 +41,8 @@ export const approvalController: InitializationConfiguration< // the user; default to a no-op so the controller works headlessly (this // matches mobile, which drives approvals through state; the extension // injects its own). - showApprovalRequest: options.showApprovalRequest ?? ((): void => undefined), + showApprovalRequest: + options.showApprovalRequest ?? ((): void => undefined), typesExcludedFromRateLimiting: options.typesExcludedFromRateLimiting ?? DEFAULT_TYPES_EXCLUDED_FROM_RATE_LIMITING, From dd0c5efb37dc48ee6e7fd86fb19234e30cf5c2e3 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 1 Jun 2026 21:11:07 +0200 Subject: [PATCH 04/10] test(wallet): verify approval wiring without constructing a Wallet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anticipate #8946 (which makes `instanceOptions` — including a required `storageService.storage` — mandatory on the `Wallet` constructor): drop the `new Wallet(...)` integration tests from `Wallet.test.ts` and instead assert the controller is in the default configuration set from the colocated unit test. This keeps the PR independent of the constructor-options shape (no `new Wallet()` to break) and leaves `Wallet.test.ts` untouched so it merges cleanly with #8946 in either order. Coverage stays at 100%. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/wallet/src/Wallet.test.ts | 25 ------------------- .../instances/approval-controller.test.ts | 8 ++++++ 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index 812f3847f7..a3a8dca781 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -225,29 +225,4 @@ describe('Wallet', () => { }); }); }); - - describe('ApprovalController', () => { - it('is wired into the default wallet', () => { - const wallet = new Wallet({}); - - expect(wallet.state.ApprovalController).toStrictEqual({ - pendingApprovals: {}, - pendingApprovalCount: 0, - approvalFlows: [], - }); - }); - - it('invokes the provided showApprovalRequest when a flow starts', () => { - const showApprovalRequest = jest.fn(); - const wallet = new Wallet({ - instanceOptions: { - approvalController: { showApprovalRequest }, - }, - }); - - wallet.messenger.call('ApprovalController:startFlow'); - - expect(showApprovalRequest).toHaveBeenCalledTimes(1); - }); - }); }); diff --git a/packages/wallet/src/initialization/instances/approval-controller.test.ts b/packages/wallet/src/initialization/instances/approval-controller.test.ts index df32a5904c..f110296058 100644 --- a/packages/wallet/src/initialization/instances/approval-controller.test.ts +++ b/packages/wallet/src/initialization/instances/approval-controller.test.ts @@ -2,6 +2,7 @@ import { ApprovalController } from '@metamask/approval-controller'; import { ApprovalType } from '@metamask/controller-utils'; import { Messenger } from '@metamask/messenger'; +import { defaultConfigurations } from '../defaults'; import type { DefaultActions, DefaultEvents, RootMessenger } from '../defaults'; import { approvalController } from './approval-controller'; @@ -15,6 +16,13 @@ function getRootMessenger(): RootMessenger { } describe('approvalController', () => { + it('is registered as a default initialization configuration', () => { + // Proves the controller is part of the default ensemble that `initialize()` + // wires, without constructing a `Wallet` (which keeps this PR independent of + // the constructor-options shape). + expect(Object.values(defaultConfigurations)).toContain(approvalController); + }); + it('initializes an ApprovalController with default state', () => { const messenger = approvalController.getMessenger(getRootMessenger()); From 53fc74a705a4e58ba264396dcf5ceda6bc75f7e3 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 1 Jun 2026 21:19:40 +0200 Subject: [PATCH 05/10] chore: add CODEOWNERS entry for approval-controller initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assign `packages/wallet/src/initialization/instances/approval-controller.ts` to @MetaMask/confirmations (the approval-controller domain team), alongside @MetaMask/core-platform (the wallet package owner) — mirroring the existing keyring-controller initialization entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f855e4a66c..41ca25d6c6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -129,6 +129,7 @@ /packages/profile-metrics-controller @MetaMask/mobile-platform @MetaMask/extension-platform ## Initialization +/packages/wallet/src/initialization/instances/approval-controller.ts @MetaMask/confirmations @MetaMask/core-platform /packages/wallet/src/initialization/instances/keyring-controller.ts @MetaMask/accounts-engineers @MetaMask/core-platform ## Package Release related From 5e1577363bcc5bd9c5402c97c7a35bf95dc59b0e Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 2 Jun 2026 11:48:31 +0200 Subject: [PATCH 06/10] chore(wallet): address review on approval-controller wiring - CODEOWNERS: mirror the approval-controller package owner (@MetaMask/confirmations only) for the instance file, per review. - Reword the default rate-limiting-exclusion JSDoc: the list also covers asset-watch and encryption approval types, not only signing/transaction (flagged by Cursor Bugbot). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/CODEOWNERS | 2 +- .../initialization/instances/approval-controller.ts | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 41ca25d6c6..cfdd598d82 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -129,7 +129,7 @@ /packages/profile-metrics-controller @MetaMask/mobile-platform @MetaMask/extension-platform ## Initialization -/packages/wallet/src/initialization/instances/approval-controller.ts @MetaMask/confirmations @MetaMask/core-platform +/packages/wallet/src/initialization/instances/approval-controller.ts @MetaMask/confirmations /packages/wallet/src/initialization/instances/keyring-controller.ts @MetaMask/accounts-engineers @MetaMask/core-platform ## Package Release related diff --git a/packages/wallet/src/initialization/instances/approval-controller.ts b/packages/wallet/src/initialization/instances/approval-controller.ts index 2423c4ac5d..5e7304cf33 100644 --- a/packages/wallet/src/initialization/instances/approval-controller.ts +++ b/packages/wallet/src/initialization/instances/approval-controller.ts @@ -8,13 +8,16 @@ import { Messenger } from '@metamask/messenger'; import { InitializationConfiguration } from '../types'; /** - * The platform-agnostic baseline of approval types whose pending requests are - * exempt from per-origin rate limiting, allowing multiple requests of these EVM - * signing/transaction types to be queued from the same origin. + * The platform-agnostic baseline of EVM approval types whose pending requests + * are exempt from per-origin rate limiting, so multiple requests of the same + * type can be queued from one origin. Covers the common EVM signing + * (`personal_sign`, `eth_signTypedData`), transaction, asset-watch + * (`wallet_watchAsset`), and encryption (`eth_getEncryptionPublicKey`, + * `eth_decrypt`) approval types — the set the clients exempt today. * * Clients can override this entirely via - * `instanceOptions.approvalController.typesExcludedFromRateLimiting`. The set - * differs per platform — the extension and mobile each append client-specific + * `instanceOptions.approvalController.typesExcludedFromRateLimiting`. The full + * set differs per platform — the extension and mobile each append client-specific * types (e.g. their smart-transaction status page and `snap_dialog`), and even * use different string values for the same concept — so there is no single * correct list to hardcode here. From d816a605a4e92bce88f84140815d0fb676057949 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 2 Jun 2026 12:05:13 +0200 Subject: [PATCH 07/10] fix(wallet): keep ApprovalController entry in Unreleased + apply review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changelog: move the ApprovalController entry to [Unreleased] (main cut a 2.0.0 release that absorbed the StorageService / importSecretRecoveryPhrase entries) — fixes the "Validate changelog diffs" CI job. - Type `typesExcludedFromRateLimiting` via the upstream `ApprovalControllerOptions` indexed type, matching the keyring slot convention (review: type-design). - Correct the default-exclusion JSDoc: it mirrors the extension baseline; mobile exempts only a subset (review: comment accuracy). - Tests: pin the full default exclusion set (parametrized, with a positive pendingApprovalCount assertion) and verify init forwards `state` (review: test coverage). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/wallet/CHANGELOG.md | 7 ++- .../instances/approval-controller.test.ts | 44 ++++++++++++++----- .../instances/approval-controller.ts | 5 ++- packages/wallet/src/types.ts | 9 +++- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index 6933e79464..e115995e30 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Wire `ApprovalController` into the default wallet initialization ([#8953](https://github.com/MetaMask/core/pull/8953)) + - Adds an `approvalController` slot to `instanceOptions` with `showApprovalRequest` (the callback that surfaces pending approval requests to the user; defaults to a no-op) and `typesExcludedFromRateLimiting` (the approval types exempt from per-origin rate limiting; defaults to a baseline of EVM approval types). Both let consumers (extension, mobile, wallet-cli) inject their platform-specific values. + ## [2.0.0] ### Added @@ -15,8 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Add `StorageService` initialization ([#8946](https://github.com/MetaMask/core/pull/8946)) - Passing `instanceOptions.storageService.storage` is now required. - Export `importSecretRecoveryPhrase` from the package root ([#8952](https://github.com/MetaMask/core/pull/8952)) -- Wire `ApprovalController` into the default wallet initialization ([#8953](https://github.com/MetaMask/core/pull/8953)) - - Adds an `approvalController` slot to `instanceOptions` with `showApprovalRequest` (the callback that surfaces pending approval requests to the user; defaults to a no-op) and `typesExcludedFromRateLimiting` (the approval types exempt from per-origin rate limiting; defaults to the EVM signing/transaction types). Both let consumers (extension, mobile, wallet-cli) inject their platform-specific values. ## [1.0.1] diff --git a/packages/wallet/src/initialization/instances/approval-controller.test.ts b/packages/wallet/src/initialization/instances/approval-controller.test.ts index f110296058..6fe1a9b777 100644 --- a/packages/wallet/src/initialization/instances/approval-controller.test.ts +++ b/packages/wallet/src/initialization/instances/approval-controller.test.ts @@ -40,6 +40,22 @@ describe('approvalController', () => { }); }); + it('forwards the provided state to the controller', () => { + const messenger = approvalController.getMessenger(getRootMessenger()); + + const instance = approvalController.init({ + state: { + pendingApprovals: {}, + pendingApprovalCount: 3, + approvalFlows: [], + }, + messenger, + options: {}, + }); + + expect(instance.state.pendingApprovalCount).toBe(3); + }); + it('uses the provided showApprovalRequest callback', () => { const messenger = approvalController.getMessenger(getRootMessenger()); const showApprovalRequest = jest.fn(); @@ -67,7 +83,18 @@ describe('approvalController', () => { expect(() => instance.startFlow()).not.toThrow(); }); - it('excludes EVM signing types from rate limiting by default', () => { + // Pins the exact default exclusion set (independent of the source constant): + // each of these EVM types must allow multiple pending requests from one + // origin. The pending promises never settle here; `.catch` only marks them + // handled. + it.each([ + ApprovalType.PersonalSign, + ApprovalType.EthSignTypedData, + ApprovalType.Transaction, + ApprovalType.WatchAsset, + ApprovalType.EthGetEncryptionPublicKey, + ApprovalType.EthDecrypt, + ])('excludes %s from rate limiting by default', (type) => { const messenger = approvalController.getMessenger(getRootMessenger()); const instance = approvalController.init({ @@ -76,16 +103,11 @@ describe('approvalController', () => { options: {}, }); - // An excluded type allows multiple pending requests from the same origin. - // The pending promises never settle here; `.catch` only marks them handled. - expect(() => { - instance - .add({ origin: 'metamask.io', type: ApprovalType.Transaction }) - .catch(() => undefined); - instance - .add({ origin: 'metamask.io', type: ApprovalType.Transaction }) - .catch(() => undefined); - }).not.toThrow(); + instance.add({ origin: 'metamask.io', type }).catch(() => undefined); + instance.add({ origin: 'metamask.io', type }).catch(() => undefined); + + // Both requests are queued rather than the second being rejected. + expect(instance.state.pendingApprovalCount).toBe(2); }); it('honors a custom typesExcludedFromRateLimiting list that overrides the default', () => { diff --git a/packages/wallet/src/initialization/instances/approval-controller.ts b/packages/wallet/src/initialization/instances/approval-controller.ts index 5e7304cf33..88e1cfa59a 100644 --- a/packages/wallet/src/initialization/instances/approval-controller.ts +++ b/packages/wallet/src/initialization/instances/approval-controller.ts @@ -10,10 +10,11 @@ import { InitializationConfiguration } from '../types'; /** * The platform-agnostic baseline of EVM approval types whose pending requests * are exempt from per-origin rate limiting, so multiple requests of the same - * type can be queued from one origin. Covers the common EVM signing + * type can be queued from one origin. Covers the EVM signing * (`personal_sign`, `eth_signTypedData`), transaction, asset-watch * (`wallet_watchAsset`), and encryption (`eth_getEncryptionPublicKey`, - * `eth_decrypt`) approval types — the set the clients exempt today. + * `eth_decrypt`) approval types. This mirrors the extension's baseline; mobile + * exempts only a subset (transaction and asset-watch). * * Clients can override this entirely via * `instanceOptions.approvalController.typesExcludedFromRateLimiting`. The full diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 85d28c618c..8dc50ac692 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -1,4 +1,7 @@ -import type { ShowApprovalRequest } from '@metamask/approval-controller'; +import type { + ApprovalControllerOptions, + ShowApprovalRequest, +} from '@metamask/approval-controller'; import { KeyringControllerOptions } from '@metamask/keyring-controller'; import { StorageAdapter } from '@metamask/storage-service'; import type { Json } from '@metamask/utils'; @@ -24,7 +27,9 @@ export type WalletOptions = { export type InstanceSpecificOptions = { approvalController?: { showApprovalRequest?: ShowApprovalRequest; - typesExcludedFromRateLimiting?: string[]; + typesExcludedFromRateLimiting?: NonNullable< + ApprovalControllerOptions['typesExcludedFromRateLimiting'] + >; }; keyringController?: { encryptor?: GenericEncryptor; From 343634d9d90337cd70e7b021ae8cbb97d85cef3c Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 2 Jun 2026 16:55:21 +0200 Subject: [PATCH 08/10] refactor(wallet): address review on ApprovalController wiring - Restructure into a per-controller directory (`instances/approval-controller/{approval-controller,types}.ts`) so it can hold a dedicated type + future utils (review: @matthewwalsh0). - Extract an `ApprovalControllerInstanceOptions` type and reference it from `InstanceSpecificOptions` (review: @matthewwalsh0). - Add `snap_dialog` to the default rate-limit-exclusion set (superset baseline the clients share); the smart-tx status type stays client-injected since its string differs per platform (review: @FrederikBolding, @matthewwalsh0). - Mark the changelog entry BREAKING: the default Wallet now registers `ApprovalController:*`, so consumers wiring their own must remove it on upgrade to avoid a messenger collision (review: @FrederikBolding). - Point the CODEOWNERS entry at the new directory. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/CODEOWNERS | 2 +- packages/wallet/CHANGELOG.md | 5 ++-- .../approval-controller.test.ts | 16 +++++++---- .../approval-controller.ts | 28 ++++++++++--------- .../instances/approval-controller/types.ts | 23 +++++++++++++++ .../src/initialization/instances/index.ts | 2 +- packages/wallet/src/types.ts | 12 ++------ 7 files changed, 55 insertions(+), 33 deletions(-) rename packages/wallet/src/initialization/instances/{ => approval-controller}/approval-controller.test.ts (92%) rename packages/wallet/src/initialization/instances/{ => approval-controller}/approval-controller.ts (60%) create mode 100644 packages/wallet/src/initialization/instances/approval-controller/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cfdd598d82..e228d8fb4b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -129,7 +129,7 @@ /packages/profile-metrics-controller @MetaMask/mobile-platform @MetaMask/extension-platform ## Initialization -/packages/wallet/src/initialization/instances/approval-controller.ts @MetaMask/confirmations +/packages/wallet/src/initialization/instances/approval-controller/ @MetaMask/confirmations /packages/wallet/src/initialization/instances/keyring-controller.ts @MetaMask/accounts-engineers @MetaMask/core-platform ## Package Release related diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index e115995e30..d012607b12 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -9,8 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Wire `ApprovalController` into the default wallet initialization ([#8953](https://github.com/MetaMask/core/pull/8953)) - - Adds an `approvalController` slot to `instanceOptions` with `showApprovalRequest` (the callback that surfaces pending approval requests to the user; defaults to a no-op) and `typesExcludedFromRateLimiting` (the approval types exempt from per-origin rate limiting; defaults to a baseline of EVM approval types). Both let consumers (extension, mobile, wallet-cli) inject their platform-specific values. +- **BREAKING:** Wire `ApprovalController` into the default wallet initialization ([#8953](https://github.com/MetaMask/core/pull/8953)) + - The default `Wallet` now constructs an `ApprovalController` and registers its `ApprovalController:*` messenger actions. Consumers that pass their own `messenger` and already wire an `ApprovalController` must remove their own before upgrading, or the duplicate registration will collide. + - Adds an `approvalController` slot to `instanceOptions` with `showApprovalRequest` (the callback that surfaces pending approval requests to the user; defaults to a no-op) and `typesExcludedFromRateLimiting` (the approval types exempt from per-origin rate limiting; defaults to a baseline of EVM approval types plus `snap_dialog`). Both let consumers (extension, mobile, wallet-cli) inject their platform-specific values. ## [2.0.0] diff --git a/packages/wallet/src/initialization/instances/approval-controller.test.ts b/packages/wallet/src/initialization/instances/approval-controller/approval-controller.test.ts similarity index 92% rename from packages/wallet/src/initialization/instances/approval-controller.test.ts rename to packages/wallet/src/initialization/instances/approval-controller/approval-controller.test.ts index 6fe1a9b777..e1298baeaa 100644 --- a/packages/wallet/src/initialization/instances/approval-controller.test.ts +++ b/packages/wallet/src/initialization/instances/approval-controller/approval-controller.test.ts @@ -2,8 +2,12 @@ import { ApprovalController } from '@metamask/approval-controller'; import { ApprovalType } from '@metamask/controller-utils'; import { Messenger } from '@metamask/messenger'; -import { defaultConfigurations } from '../defaults'; -import type { DefaultActions, DefaultEvents, RootMessenger } from '../defaults'; +import { defaultConfigurations } from '../../defaults'; +import type { + DefaultActions, + DefaultEvents, + RootMessenger, +} from '../../defaults'; import { approvalController } from './approval-controller'; /** @@ -84,9 +88,8 @@ describe('approvalController', () => { }); // Pins the exact default exclusion set (independent of the source constant): - // each of these EVM types must allow multiple pending requests from one - // origin. The pending promises never settle here; `.catch` only marks them - // handled. + // each of these types must allow multiple pending requests from one origin. + // The pending promises never settle here; `.catch` only marks them handled. it.each([ ApprovalType.PersonalSign, ApprovalType.EthSignTypedData, @@ -94,6 +97,7 @@ describe('approvalController', () => { ApprovalType.WatchAsset, ApprovalType.EthGetEncryptionPublicKey, ApprovalType.EthDecrypt, + 'snap_dialog', ])('excludes %s from rate limiting by default', (type) => { const messenger = approvalController.getMessenger(getRootMessenger()); @@ -116,7 +120,7 @@ describe('approvalController', () => { const instance = approvalController.init({ state: undefined, messenger, - // Empty override: nothing is excluded, not even the default EVM types. + // Empty override: nothing is excluded, not even the default types. options: { typesExcludedFromRateLimiting: [] }, }); diff --git a/packages/wallet/src/initialization/instances/approval-controller.ts b/packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts similarity index 60% rename from packages/wallet/src/initialization/instances/approval-controller.ts rename to packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts index 88e1cfa59a..5a5cfab7c6 100644 --- a/packages/wallet/src/initialization/instances/approval-controller.ts +++ b/packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts @@ -5,23 +5,20 @@ import { import { ApprovalType } from '@metamask/controller-utils'; import { Messenger } from '@metamask/messenger'; -import { InitializationConfiguration } from '../types'; +import { InitializationConfiguration } from '../../types'; /** - * The platform-agnostic baseline of EVM approval types whose pending requests - * are exempt from per-origin rate limiting, so multiple requests of the same - * type can be queued from one origin. Covers the EVM signing - * (`personal_sign`, `eth_signTypedData`), transaction, asset-watch - * (`wallet_watchAsset`), and encryption (`eth_getEncryptionPublicKey`, - * `eth_decrypt`) approval types. This mirrors the extension's baseline; mobile - * exempts only a subset (transaction and asset-watch). + * The baseline of approval types whose pending requests are exempt from + * per-origin rate limiting, so multiple requests of the same type can be queued + * from one origin. Covers the EVM signing (`personal_sign`, + * `eth_signTypedData`), transaction, asset-watch (`wallet_watchAsset`), and + * encryption (`eth_getEncryptionPublicKey`, `eth_decrypt`) approval types, plus + * `snap_dialog` — the union of what the extension and mobile exempt that is + * stable across both. Their smart-transaction status type is intentionally left + * out: its string differs per platform, so clients inject it themselves. * * Clients can override this entirely via - * `instanceOptions.approvalController.typesExcludedFromRateLimiting`. The full - * set differs per platform — the extension and mobile each append client-specific - * types (e.g. their smart-transaction status page and `snap_dialog`), and even - * use different string values for the same concept — so there is no single - * correct list to hardcode here. + * `instanceOptions.approvalController.typesExcludedFromRateLimiting`. */ const DEFAULT_TYPES_EXCLUDED_FROM_RATE_LIMITING = [ ApprovalType.PersonalSign, @@ -30,6 +27,11 @@ const DEFAULT_TYPES_EXCLUDED_FROM_RATE_LIMITING = [ ApprovalType.WatchAsset, ApprovalType.EthGetEncryptionPublicKey, ApprovalType.EthDecrypt, + // `snap_dialog` (the `DIALOG_APPROVAL_TYPES.default` value from + // `@metamask/snaps-rpc-methods`); hardcoded as a literal to avoid depending on + // the snaps package for a single constant while `SnapController` isn't wired + // into the wallet yet. + 'snap_dialog', ]; export const approvalController: InitializationConfiguration< diff --git a/packages/wallet/src/initialization/instances/approval-controller/types.ts b/packages/wallet/src/initialization/instances/approval-controller/types.ts new file mode 100644 index 0000000000..f0ab918873 --- /dev/null +++ b/packages/wallet/src/initialization/instances/approval-controller/types.ts @@ -0,0 +1,23 @@ +import type { + ApprovalControllerOptions, + ShowApprovalRequest, +} from '@metamask/approval-controller'; + +/** + * Per-instance options for the wallet's `ApprovalController`. Both fields are + * optional; see the controller's `init` for the defaults applied when omitted. + */ +export type ApprovalControllerInstanceOptions = { + /** + * Callback that surfaces a pending approval request to the user. Defaults to + * a no-op so the controller works headlessly. + */ + showApprovalRequest?: ShowApprovalRequest; + /** + * Approval types exempt from per-origin rate limiting. Defaults to a baseline + * of EVM approval types plus `snap_dialog`. + */ + typesExcludedFromRateLimiting?: NonNullable< + ApprovalControllerOptions['typesExcludedFromRateLimiting'] + >; +}; diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts index daef170b9c..56cb78a012 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -1,3 +1,3 @@ -export { approvalController } from './approval-controller'; +export { approvalController } from './approval-controller/approval-controller'; export { keyringController } from './keyring-controller'; export { storageService } from './storage-service'; diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 8dc50ac692..a95699f8ef 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -1,7 +1,3 @@ -import type { - ApprovalControllerOptions, - ShowApprovalRequest, -} from '@metamask/approval-controller'; import { KeyringControllerOptions } from '@metamask/keyring-controller'; import { StorageAdapter } from '@metamask/storage-service'; import type { Json } from '@metamask/utils'; @@ -11,6 +7,7 @@ import type { DefaultEvents, RootMessenger, } from './initialization/defaults'; +import type { ApprovalControllerInstanceOptions } from './initialization/instances/approval-controller/types'; import { GenericEncryptor } from './initialization/instances/keyring-controller'; import { InitializationConfiguration } from './initialization/types'; @@ -25,12 +22,7 @@ export type WalletOptions = { }; export type InstanceSpecificOptions = { - approvalController?: { - showApprovalRequest?: ShowApprovalRequest; - typesExcludedFromRateLimiting?: NonNullable< - ApprovalControllerOptions['typesExcludedFromRateLimiting'] - >; - }; + approvalController?: ApprovalControllerInstanceOptions; keyringController?: { encryptor?: GenericEncryptor; keyringBuilders?: KeyringControllerOptions['keyringBuilders']; From b9f3b1abe4a877f1d9c0774b047f51c0217f0346 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 2 Jun 2026 17:06:26 +0200 Subject: [PATCH 09/10] refactor(wallet): keep approval rate-limit default as the EVM baseline Drop `snap_dialog` from the default exclusion list. The default isn't the mechanism that aligns the clients (they each pass their own full list via `instanceOptions`), so a superset default earns nothing and can't be a true superset anyway (the smart-tx status string differs per platform). Keep the default as the EVM baseline; `snap_dialog` joins it when `SnapController` is wired. Simplify the explaining comment. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/wallet/CHANGELOG.md | 2 +- .../approval-controller.test.ts | 1 - .../approval-controller.ts | 22 ++++++------------- .../instances/approval-controller/types.ts | 2 +- 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index d012607b12..787705fe66 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Wire `ApprovalController` into the default wallet initialization ([#8953](https://github.com/MetaMask/core/pull/8953)) - The default `Wallet` now constructs an `ApprovalController` and registers its `ApprovalController:*` messenger actions. Consumers that pass their own `messenger` and already wire an `ApprovalController` must remove their own before upgrading, or the duplicate registration will collide. - - Adds an `approvalController` slot to `instanceOptions` with `showApprovalRequest` (the callback that surfaces pending approval requests to the user; defaults to a no-op) and `typesExcludedFromRateLimiting` (the approval types exempt from per-origin rate limiting; defaults to a baseline of EVM approval types plus `snap_dialog`). Both let consumers (extension, mobile, wallet-cli) inject their platform-specific values. + - Adds an `approvalController` slot to `instanceOptions` with `showApprovalRequest` (the callback that surfaces pending approval requests to the user; defaults to a no-op) and `typesExcludedFromRateLimiting` (the approval types exempt from per-origin rate limiting; defaults to a baseline of EVM approval types). Both let consumers (extension, mobile, wallet-cli) inject their platform-specific values. ## [2.0.0] diff --git a/packages/wallet/src/initialization/instances/approval-controller/approval-controller.test.ts b/packages/wallet/src/initialization/instances/approval-controller/approval-controller.test.ts index e1298baeaa..de5a517e69 100644 --- a/packages/wallet/src/initialization/instances/approval-controller/approval-controller.test.ts +++ b/packages/wallet/src/initialization/instances/approval-controller/approval-controller.test.ts @@ -97,7 +97,6 @@ describe('approvalController', () => { ApprovalType.WatchAsset, ApprovalType.EthGetEncryptionPublicKey, ApprovalType.EthDecrypt, - 'snap_dialog', ])('excludes %s from rate limiting by default', (type) => { const messenger = approvalController.getMessenger(getRootMessenger()); diff --git a/packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts b/packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts index 5a5cfab7c6..ca846b4ab5 100644 --- a/packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts +++ b/packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts @@ -8,17 +8,14 @@ import { Messenger } from '@metamask/messenger'; import { InitializationConfiguration } from '../../types'; /** - * The baseline of approval types whose pending requests are exempt from - * per-origin rate limiting, so multiple requests of the same type can be queued - * from one origin. Covers the EVM signing (`personal_sign`, - * `eth_signTypedData`), transaction, asset-watch (`wallet_watchAsset`), and - * encryption (`eth_getEncryptionPublicKey`, `eth_decrypt`) approval types, plus - * `snap_dialog` — the union of what the extension and mobile exempt that is - * stable across both. Their smart-transaction status type is intentionally left - * out: its string differs per platform, so clients inject it themselves. + * Approval types that are exempt from per-origin rate limiting, so more than one + * request of the same type can be pending at once. These are the common EVM + * types: signing, transactions, watch-asset, and encryption. * - * Clients can override this entirely via - * `instanceOptions.approvalController.typesExcludedFromRateLimiting`. + * Clients can replace this list via + * `instanceOptions.approvalController.typesExcludedFromRateLimiting` — the + * extension and mobile pass their own. `snap_dialog` will be added here once the + * wallet wires `SnapController`. */ const DEFAULT_TYPES_EXCLUDED_FROM_RATE_LIMITING = [ ApprovalType.PersonalSign, @@ -27,11 +24,6 @@ const DEFAULT_TYPES_EXCLUDED_FROM_RATE_LIMITING = [ ApprovalType.WatchAsset, ApprovalType.EthGetEncryptionPublicKey, ApprovalType.EthDecrypt, - // `snap_dialog` (the `DIALOG_APPROVAL_TYPES.default` value from - // `@metamask/snaps-rpc-methods`); hardcoded as a literal to avoid depending on - // the snaps package for a single constant while `SnapController` isn't wired - // into the wallet yet. - 'snap_dialog', ]; export const approvalController: InitializationConfiguration< diff --git a/packages/wallet/src/initialization/instances/approval-controller/types.ts b/packages/wallet/src/initialization/instances/approval-controller/types.ts index f0ab918873..842750f636 100644 --- a/packages/wallet/src/initialization/instances/approval-controller/types.ts +++ b/packages/wallet/src/initialization/instances/approval-controller/types.ts @@ -15,7 +15,7 @@ export type ApprovalControllerInstanceOptions = { showApprovalRequest?: ShowApprovalRequest; /** * Approval types exempt from per-origin rate limiting. Defaults to a baseline - * of EVM approval types plus `snap_dialog`. + * of EVM approval types. */ typesExcludedFromRateLimiting?: NonNullable< ApprovalControllerOptions['typesExcludedFromRateLimiting'] From e533d67bcafa377c454a9c53317b44b910537f3c Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 2 Jun 2026 17:38:02 +0200 Subject: [PATCH 10/10] Apply suggestion from @FrederikBolding Co-authored-by: Frederik Bolding --- .../instances/approval-controller/approval-controller.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts b/packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts index ca846b4ab5..728c002008 100644 --- a/packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts +++ b/packages/wallet/src/initialization/instances/approval-controller/approval-controller.ts @@ -35,10 +35,6 @@ export const approvalController: InitializationConfiguration< new ApprovalController({ state, messenger, - // The consumer supplies the callback that surfaces a pending request to - // the user; default to a no-op so the controller works headlessly (this - // matches mobile, which drives approvals through state; the extension - // injects its own). showApprovalRequest: options.showApprovalRequest ?? ((): void => undefined), typesExcludedFromRateLimiting: