From 9333e1b22069febb38eda63d8cfc4cd6f07ced79 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 16 Jul 2026 13:01:58 +0200 Subject: [PATCH 1/5] feat: add entropy-controller --- .github/CODEOWNERS | 3 + README.md | 2 + packages/entropy-controller/CHANGELOG.md | 10 + packages/entropy-controller/LICENSE | 6 + packages/entropy-controller/LICENSE.APACHE2 | 201 ++++++++++++++++++ packages/entropy-controller/LICENSE.MIT | 21 ++ packages/entropy-controller/README.md | 15 ++ packages/entropy-controller/jest.config.js | 26 +++ packages/entropy-controller/package.json | 74 +++++++ .../src/entropy-controller.test.ts | 180 ++++++++++++++++ .../src/entropy-controller.ts | 192 +++++++++++++++++ packages/entropy-controller/src/index.ts | 13 ++ .../entropy-controller/tsconfig.build.json | 17 ++ packages/entropy-controller/tsconfig.json | 15 ++ packages/entropy-controller/typedoc.json | 7 + tsconfig.build.json | 3 + yarn.lock | 19 ++ 17 files changed, 804 insertions(+) create mode 100644 packages/entropy-controller/CHANGELOG.md create mode 100644 packages/entropy-controller/LICENSE create mode 100644 packages/entropy-controller/LICENSE.APACHE2 create mode 100644 packages/entropy-controller/LICENSE.MIT create mode 100644 packages/entropy-controller/README.md create mode 100644 packages/entropy-controller/jest.config.js create mode 100644 packages/entropy-controller/package.json create mode 100644 packages/entropy-controller/src/entropy-controller.test.ts create mode 100644 packages/entropy-controller/src/entropy-controller.ts create mode 100644 packages/entropy-controller/src/index.ts create mode 100644 packages/entropy-controller/tsconfig.build.json create mode 100644 packages/entropy-controller/tsconfig.json create mode 100644 packages/entropy-controller/typedoc.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 49f39f1ce08..09f2af550df 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,6 +14,7 @@ /packages/profile-sync-controller @MetaMask/accounts-engineers /packages/money-account-controller @MetaMask/accounts-engineers /packages/snap-account-service @MetaMask/accounts-engineers +/packages/entropy-controller @MetaMask/accounts-engineers ## Auth Team /packages/authenticated-user-storage @MetaMask/auth-engineers @@ -280,3 +281,5 @@ /packages/money-account-upgrade-controller/CHANGELOG.md @MetaMask/earn @MetaMask/delegation @MetaMask/core-platform /packages/snap-account-service/package.json @MetaMask/accounts-engineers @MetaMask/core-platform /packages/snap-account-service/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform +/packages/entropy-controller/package.json @MetaMask/accounts-engineers @MetaMask/core-platform +/packages/entropy-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform diff --git a/README.md b/README.md index b8b3a2d8ca8..561b8fb9969 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ yarn skills --reset # clear saved local selection - [`@metamask/eip-7702-internal-rpc-middleware`](packages/eip-7702-internal-rpc-middleware) - [`@metamask/eip1193-permission-middleware`](packages/eip1193-permission-middleware) - [`@metamask/ens-controller`](packages/ens-controller) +- [`@metamask/entropy-controller`](packages/entropy-controller) - [`@metamask/eth-block-tracker`](packages/eth-block-tracker) - [`@metamask/eth-json-rpc-middleware`](packages/eth-json-rpc-middleware) - [`@metamask/eth-json-rpc-provider`](packages/eth-json-rpc-provider) @@ -180,6 +181,7 @@ linkStyle default opacity:0.5 eip_7702_internal_rpc_middleware(["@metamask/eip-7702-internal-rpc-middleware"]); eip1193_permission_middleware(["@metamask/eip1193-permission-middleware"]); ens_controller(["@metamask/ens-controller"]); + entropy_controller(["@metamask/entropy-controller"]); eth_block_tracker(["@metamask/eth-block-tracker"]); eth_json_rpc_middleware(["@metamask/eth-json-rpc-middleware"]); eth_json_rpc_provider(["@metamask/eth-json-rpc-provider"]); diff --git a/packages/entropy-controller/CHANGELOG.md b/packages/entropy-controller/CHANGELOG.md new file mode 100644 index 00000000000..b518709c7b8 --- /dev/null +++ b/packages/entropy-controller/CHANGELOG.md @@ -0,0 +1,10 @@ +# 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/entropy-controller/LICENSE b/packages/entropy-controller/LICENSE new file mode 100644 index 00000000000..9ec4f4514ea --- /dev/null +++ b/packages/entropy-controller/LICENSE @@ -0,0 +1,6 @@ +This project is licensed under either of + + * MIT license ([LICENSE.MIT](LICENSE.MIT)) + * Apache License, Version 2.0 ([LICENSE.APACHE2](LICENSE.APACHE2)) + +at your option. diff --git a/packages/entropy-controller/LICENSE.APACHE2 b/packages/entropy-controller/LICENSE.APACHE2 new file mode 100644 index 00000000000..e6e77b08909 --- /dev/null +++ b/packages/entropy-controller/LICENSE.APACHE2 @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/entropy-controller/LICENSE.MIT b/packages/entropy-controller/LICENSE.MIT new file mode 100644 index 00000000000..fe29e78e0fe --- /dev/null +++ b/packages/entropy-controller/LICENSE.MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 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 +SOFTWARE. diff --git a/packages/entropy-controller/README.md b/packages/entropy-controller/README.md new file mode 100644 index 00000000000..484bf57230e --- /dev/null +++ b/packages/entropy-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/entropy-controller` + +Controller that manages/owns various entropies (SRPs/mnemonic, private keys, hardware wallets, etc.) + +## Installation + +`yarn add @metamask/entropy-controller` + +or + +`npm install @metamask/entropy-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/entropy-controller/jest.config.js b/packages/entropy-controller/jest.config.js new file mode 100644 index 00000000000..ca084133399 --- /dev/null +++ b/packages/entropy-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: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/entropy-controller/package.json b/packages/entropy-controller/package.json new file mode 100644 index 00000000000..ac9894466bf --- /dev/null +++ b/packages/entropy-controller/package.json @@ -0,0 +1,74 @@ +{ + "name": "@metamask/entropy-controller", + "version": "0.0.0", + "description": "Controller that manages/owns various entropies (SRPs/mnemonic, private keys, hardware wallets, etc.)", + "keywords": [ + "Ethereum", + "MetaMask" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/entropy-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "license": "(MIT OR Apache-2.0)", + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "files": [ + "dist/" + ], + "sideEffects": false, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", + "build:all": "ts-bridge --project tsconfig.build.json --verbose --clean", + "build:docs": "typedoc", + "changelog:update": "../../scripts/update-changelog.sh @metamask/entropy-controller", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/entropy-controller", + "messenger-action-types:check": "tsx ../../packages/messenger-cli/src/cli.ts --formatter oxfmt --check", + "messenger-action-types:generate": "tsx ../../packages/messenger-cli/src/cli.ts --formatter oxfmt --generate", + "since-latest-release": "../../scripts/since-latest-release.sh", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", + "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "^9.1.0", + "@metamask/messenger": "^2.0.0" + }, + "devDependencies": { + "@metamask/auto-changelog": "^6.1.0", + "@ts-bridge/cli": "^0.6.4", + "@types/jest": "^29.5.14", + "deepmerge": "^4.2.2", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "tsx": "^4.20.5", + "typedoc": "^0.25.13", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~5.3.3" + }, + "engines": { + "node": "^18.18 || >=20" + } +} diff --git a/packages/entropy-controller/src/entropy-controller.test.ts b/packages/entropy-controller/src/entropy-controller.test.ts new file mode 100644 index 00000000000..3ddd6c24440 --- /dev/null +++ b/packages/entropy-controller/src/entropy-controller.test.ts @@ -0,0 +1,180 @@ +import { deriveStateFromMetadata } from '@metamask/base-controller'; +import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; +import type { + MockAnyNamespace, + MessengerActions, + MessengerEvents, +} from '@metamask/messenger'; + +import type { EntropyControllerMessenger } from './entropy-controller'; +import { EntropyController } from './entropy-controller'; + +describe('EntropyController', () => { + describe('constructor', () => { + it('accepts initial state', () => { + const givenState = { + entropySources: { + 'entropy-1': { + type: 'bip44:srp' as const, + metadata: {}, + }, + }, + }; + + withController({ options: { state: givenState } }, ({ controller }) => { + expect(controller.state).toStrictEqual(givenState); + }); + }); + + it('fills in missing initial state with defaults', () => { + withController(({ controller }) => { + expect(controller.state).toMatchInlineSnapshot(` + { + "entropySources": {}, + } + `); + }); + }); + }); + + describe('metadata', () => { + it('does not include entropy sources in debug snapshots', () => { + withController(({ controller }) => { + expect( + deriveStateFromMetadata( + controller.state, + controller.metadata, + 'includeInDebugSnapshot', + ), + ).toMatchInlineSnapshot(`{}`); + }); + }); + + it('includes entropy sources in state logs', () => { + withController(({ controller }) => { + expect( + deriveStateFromMetadata( + controller.state, + controller.metadata, + 'includeInStateLogs', + ), + ).toMatchInlineSnapshot(` + { + "entropySources": {}, + } + `); + }); + }); + + it('persists entropy sources', () => { + withController(({ controller }) => { + expect( + deriveStateFromMetadata( + controller.state, + controller.metadata, + 'persist', + ), + ).toMatchInlineSnapshot(` + { + "entropySources": {}, + } + `); + }); + }); + + it('exposes entropy sources to UI', () => { + withController(({ controller }) => { + expect( + deriveStateFromMetadata( + controller.state, + controller.metadata, + 'usedInUi', + ), + ).toMatchInlineSnapshot(` + { + "entropySources": {}, + } + `); + }); + }); + }); +}); + +/** + * The type of the messenger populated with all external actions and events + * required by the controller under test. + */ +type RootMessenger = Messenger< + MockAnyNamespace, + MessengerActions, + MessengerEvents +>; + +/** + * The callback that `withController` calls. + */ +type WithControllerCallback = (payload: { + controller: EntropyController; + rootMessenger: RootMessenger; + controllerMessenger: EntropyControllerMessenger; +}) => ReturnValue; + +/** + * The options that `withController` takes. + */ +type WithControllerOptions = { + options: Partial[0]>; +}; + +/** + * Constructs the messenger populated with all external actions and events + * required by the controller under test. + * + * @returns The root messenger. + */ +function getRootMessenger(): RootMessenger { + return new Messenger({ namespace: MOCK_ANY_NAMESPACE }); +} + +/** + * Constructs the messenger for the controller under test. + * + * @param rootMessenger - The root messenger, with all external actions and + * events required by the controller's messenger. + * @returns The controller-specific messenger. + */ +function getMessenger( + rootMessenger: RootMessenger, +): EntropyControllerMessenger { + return new Messenger({ + namespace: 'EntropyController', + parent: rootMessenger, + }); +} + +/** + * Wrap tests for the controller under test by ensuring that the controller is + * created ahead of time with the given options. + * + * @param args - Either a function, or an options bag + a function. The options + * bag contains arguments for the controller constructor. All constructor + * arguments are optional and will be filled in with defaults as needed + * (including `messenger`). The function is called with the instantiated + * controller, root messenger, and controller messenger. + * @returns The same return value as the given function. + */ +function withController( + ...args: + | [WithControllerCallback] + | [WithControllerOptions, WithControllerCallback] +): ReturnValue { + const [{ options = {} }, testFunction] = + args.length === 2 ? args : [{}, args[0]]; + const rootMessenger = getRootMessenger(); + const controllerMessenger = getMessenger(rootMessenger); + const controller = new EntropyController({ + messenger: controllerMessenger, + ...options, + }); + return testFunction({ controller, rootMessenger, controllerMessenger }); +} diff --git a/packages/entropy-controller/src/entropy-controller.ts b/packages/entropy-controller/src/entropy-controller.ts new file mode 100644 index 00000000000..b4b173f39f8 --- /dev/null +++ b/packages/entropy-controller/src/entropy-controller.ts @@ -0,0 +1,192 @@ +import type { + ControllerGetStateAction, + ControllerStateChangeEvent, + StateMetadata, +} from '@metamask/base-controller'; +import { BaseController } from '@metamask/base-controller'; +import type { Messenger } from '@metamask/messenger'; + +// === GENERAL === + +/** + * The name of the {@link EntropyController}, used to namespace the + * controller's actions and events and to namespace the controller's state data + * when composed with other controllers. + */ +const CONTROLLER_NAME = 'EntropyController'; + +// === TYPES === + +/** + * The category of an entropy source. + * + * - `'bip44'` — Entropy that uses BIP-44 to derive signers (e.g. SRP, hardware + * wallets). + * - `'raw'` — Entropy that exposes a single key directly, without derivation + * (e.g. imported private keys, MPC). + */ +export type EntropyCategory = 'bip44' | 'raw'; + +/** + * The type of an entropy source, expressed as `category:implementation`. + * + * @example `'bip44:srp'`, `'bip44:ledger'`, `'raw:private-key'`, `'raw:mpc'` + */ +export type EntropyType = `${EntropyCategory}:${string}`; + +/** + * Unique identifier for an entropy source. + */ +export type EntropyId = string; + +/** + * Metadata associated with an entropy source. + * + * Currently a placeholder — fields will be added as requirements emerge. + */ +export type EntropyMetadata = Record; + +// === STATE === + +/** + * Describes the shape of the state object for {@link EntropyController}. + */ +export type EntropyControllerState = { + /** + * The registry of entropy sources, keyed by their unique identifier. + * Each entry records the type and metadata of the entropy source. + */ + entropySources: { + [entropyId: EntropyId]: { + /** + * The type of the entropy source, expressed as `category:implementation` + * (e.g. `'bip44:srp'`, `'bip44:ledger'`). + */ + type: EntropyType; + + /** + * Metadata associated with the entropy source. + */ + metadata: EntropyMetadata; + }; + }; +}; + +/** + * The metadata for each property in {@link EntropyControllerState}. + */ +const entropyControllerMetadata = { + entropySources: { + includeInDebugSnapshot: false, + includeInStateLogs: true, + persist: true, + usedInUi: true, + }, +} satisfies StateMetadata; + +/** + * Constructs the default {@link EntropyController} state. This allows + * consumers to provide a partial state object when initializing the controller + * and also helps in constructing complete state objects for this controller in + * tests. + * + * @returns The default {@link EntropyController} state. + */ +export function getDefaultEntropyControllerState(): EntropyControllerState { + return { + entropySources: {}, + }; +} + +// === MESSENGER === + +/** + * Retrieves the state of the {@link EntropyController}. + */ +export type EntropyControllerGetStateAction = ControllerGetStateAction< + typeof CONTROLLER_NAME, + EntropyControllerState +>; + +/** + * Actions that {@link EntropyControllerMessenger} exposes to other consumers. + */ +export type EntropyControllerActions = EntropyControllerGetStateAction; + +/** + * Actions from other messengers that {@link EntropyControllerMessenger} calls. + */ +type AllowedActions = never; + +/** + * Published when the state of {@link EntropyController} changes. + */ +export type EntropyControllerStateChangeEvent = ControllerStateChangeEvent< + typeof CONTROLLER_NAME, + EntropyControllerState +>; + +/** + * Events that {@link EntropyControllerMessenger} exposes to other consumers. + */ +export type EntropyControllerEvents = EntropyControllerStateChangeEvent; + +/** + * Events from other messengers that {@link EntropyControllerMessenger} + * subscribes to. + */ +type AllowedEvents = never; + +/** + * The messenger restricted to actions and events accessed by + * {@link EntropyController}. + */ +export type EntropyControllerMessenger = Messenger< + typeof CONTROLLER_NAME, + EntropyControllerActions | AllowedActions, + EntropyControllerEvents | AllowedEvents +>; + +// === CONTROLLER DEFINITION === + +/** + * `EntropyController` maintains a registry that maps entropy source identifiers + * to their type and metadata. + * + * An entropy source is any provider of key material: a Secret Recovery Phrase + * (SRP), a hardware wallet (Ledger, Trezor), or an imported private key. The + * registry is the single source of truth for which entropy sources exist and + * what kind they are. Actual key material and signing operations are left to + * entropy source implementations and chain-specific keyrings. + */ +export class EntropyController extends BaseController< + typeof CONTROLLER_NAME, + EntropyControllerState, + EntropyControllerMessenger +> { + /** + * Constructs a new {@link EntropyController}. + * + * @param args - The arguments to this controller. + * @param args.messenger - The messenger suited for this controller. + * @param args.state - The desired state with which to initialize this + * controller. Missing properties will be filled in with defaults. + */ + constructor({ + messenger, + state, + }: { + messenger: EntropyControllerMessenger; + state?: Partial; + }) { + super({ + messenger, + metadata: entropyControllerMetadata, + name: CONTROLLER_NAME, + state: { + ...getDefaultEntropyControllerState(), + ...state, + }, + }); + } +} diff --git a/packages/entropy-controller/src/index.ts b/packages/entropy-controller/src/index.ts new file mode 100644 index 00000000000..3b144ac6ef6 --- /dev/null +++ b/packages/entropy-controller/src/index.ts @@ -0,0 +1,13 @@ +export { EntropyController } from './entropy-controller'; +export type { + EntropyCategory, + EntropyControllerActions, + EntropyControllerEvents, + EntropyControllerGetStateAction, + EntropyControllerMessenger, + EntropyControllerState, + EntropyControllerStateChangeEvent, + EntropyId, + EntropyMetadata, + EntropyType, +} from './entropy-controller'; diff --git a/packages/entropy-controller/tsconfig.build.json b/packages/entropy-controller/tsconfig.build.json new file mode 100644 index 00000000000..249f327913d --- /dev/null +++ b/packages/entropy-controller/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { + "path": "../base-controller/tsconfig.build.json" + }, + { + "path": "../messenger/tsconfig.build.json" + } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/entropy-controller/tsconfig.json b/packages/entropy-controller/tsconfig.json new file mode 100644 index 00000000000..cb296895b28 --- /dev/null +++ b/packages/entropy-controller/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { + "path": "../base-controller" + }, + { + "path": "../messenger" + } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/entropy-controller/typedoc.json b/packages/entropy-controller/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/entropy-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 5e805550062..6aa2c96d9a2 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -112,6 +112,9 @@ { "path": "./packages/ens-controller/tsconfig.build.json" }, + { + "path": "./packages/entropy-controller/tsconfig.build.json" + }, { "path": "./packages/eth-block-tracker/tsconfig.build.json" }, diff --git a/yarn.lock b/yarn.lock index 1e4085d18fb..d8c3167a78f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6593,6 +6593,25 @@ __metadata: languageName: unknown linkType: soft +"@metamask/entropy-controller@workspace:packages/entropy-controller": + version: 0.0.0-use.local + resolution: "@metamask/entropy-controller@workspace:packages/entropy-controller" + dependencies: + "@metamask/auto-changelog": "npm:^6.1.0" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/messenger": "npm:^2.0.0" + "@ts-bridge/cli": "npm:^0.6.4" + "@types/jest": "npm:^29.5.14" + deepmerge: "npm:^4.2.2" + jest: "npm:^29.7.0" + ts-jest: "npm:^29.2.5" + tsx: "npm:^4.20.5" + typedoc: "npm:^0.25.13" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.3.3" + languageName: unknown + linkType: soft + "@metamask/eslint-config-jest@npm:^15.0.0": version: 15.0.0 resolution: "@metamask/eslint-config-jest@npm:15.0.0" From a63764539bd561b0372ced179e3c585213a2427b Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 16 Jul 2026 13:08:48 +0200 Subject: [PATCH 2/5] chore: cosmetic --- packages/entropy-controller/src/entropy-controller.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/entropy-controller/src/entropy-controller.ts b/packages/entropy-controller/src/entropy-controller.ts index b4b173f39f8..2d76d0cb41a 100644 --- a/packages/entropy-controller/src/entropy-controller.ts +++ b/packages/entropy-controller/src/entropy-controller.ts @@ -6,8 +6,6 @@ import type { import { BaseController } from '@metamask/base-controller'; import type { Messenger } from '@metamask/messenger'; -// === GENERAL === - /** * The name of the {@link EntropyController}, used to namespace the * controller's actions and events and to namespace the controller's state data @@ -15,8 +13,6 @@ import type { Messenger } from '@metamask/messenger'; */ const CONTROLLER_NAME = 'EntropyController'; -// === TYPES === - /** * The category of an entropy source. * @@ -46,8 +42,6 @@ export type EntropyId = string; */ export type EntropyMetadata = Record; -// === STATE === - /** * Describes the shape of the state object for {@link EntropyController}. */ @@ -98,8 +92,6 @@ export function getDefaultEntropyControllerState(): EntropyControllerState { }; } -// === MESSENGER === - /** * Retrieves the state of the {@link EntropyController}. */ @@ -147,8 +139,6 @@ export type EntropyControllerMessenger = Messenger< EntropyControllerEvents | AllowedEvents >; -// === CONTROLLER DEFINITION === - /** * `EntropyController` maintains a registry that maps entropy source identifiers * to their type and metadata. From 406f6260cce3a26de65cafb68f366637c1ebec7c Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 16 Jul 2026 13:09:42 +0200 Subject: [PATCH 3/5] chore: changelog --- packages/entropy-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/entropy-controller/CHANGELOG.md b/packages/entropy-controller/CHANGELOG.md index b518709c7b8..366bd0dca54 100644 --- a/packages/entropy-controller/CHANGELOG.md +++ b/packages/entropy-controller/CHANGELOG.md @@ -7,4 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## Added + +- Add `EntropyController` ([#9538](https://github.com/MetaMask/core/pull/9538)) + [Unreleased]: https://github.com/MetaMask/core/ From 0a5bb19e490234ebc2b7593199e078b8e88efced Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 16 Jul 2026 13:49:17 +0200 Subject: [PATCH 4/5] chore: add toEntropy{Id,Fingerprint,FingerprintBytes} helpers --- packages/entropy-controller/package.json | 4 +- .../src/entropy-controller.ts | 31 +------- packages/entropy-controller/src/index.ts | 7 +- packages/entropy-controller/src/types.ts | 28 +++++++ packages/entropy-controller/src/utils.test.ts | 75 +++++++++++++++++++ packages/entropy-controller/src/utils.ts | 75 +++++++++++++++++++ yarn.lock | 2 + 7 files changed, 190 insertions(+), 32 deletions(-) create mode 100644 packages/entropy-controller/src/types.ts create mode 100644 packages/entropy-controller/src/utils.test.ts create mode 100644 packages/entropy-controller/src/utils.ts diff --git a/packages/entropy-controller/package.json b/packages/entropy-controller/package.json index ac9894466bf..d012e2b0a60 100644 --- a/packages/entropy-controller/package.json +++ b/packages/entropy-controller/package.json @@ -54,7 +54,9 @@ }, "dependencies": { "@metamask/base-controller": "^9.1.0", - "@metamask/messenger": "^2.0.0" + "@metamask/messenger": "^2.0.0", + "@noble/hashes": "^1.8.0", + "uuid": "^8.3.2" }, "devDependencies": { "@metamask/auto-changelog": "^6.1.0", diff --git a/packages/entropy-controller/src/entropy-controller.ts b/packages/entropy-controller/src/entropy-controller.ts index 2d76d0cb41a..3b4e504c11d 100644 --- a/packages/entropy-controller/src/entropy-controller.ts +++ b/packages/entropy-controller/src/entropy-controller.ts @@ -6,6 +6,8 @@ import type { import { BaseController } from '@metamask/base-controller'; import type { Messenger } from '@metamask/messenger'; +import type { EntropyId, EntropyMetadata, EntropyType } from './types'; + /** * The name of the {@link EntropyController}, used to namespace the * controller's actions and events and to namespace the controller's state data @@ -13,35 +15,6 @@ import type { Messenger } from '@metamask/messenger'; */ const CONTROLLER_NAME = 'EntropyController'; -/** - * The category of an entropy source. - * - * - `'bip44'` — Entropy that uses BIP-44 to derive signers (e.g. SRP, hardware - * wallets). - * - `'raw'` — Entropy that exposes a single key directly, without derivation - * (e.g. imported private keys, MPC). - */ -export type EntropyCategory = 'bip44' | 'raw'; - -/** - * The type of an entropy source, expressed as `category:implementation`. - * - * @example `'bip44:srp'`, `'bip44:ledger'`, `'raw:private-key'`, `'raw:mpc'` - */ -export type EntropyType = `${EntropyCategory}:${string}`; - -/** - * Unique identifier for an entropy source. - */ -export type EntropyId = string; - -/** - * Metadata associated with an entropy source. - * - * Currently a placeholder — fields will be added as requirements emerge. - */ -export type EntropyMetadata = Record; - /** * Describes the shape of the state object for {@link EntropyController}. */ diff --git a/packages/entropy-controller/src/index.ts b/packages/entropy-controller/src/index.ts index 3b144ac6ef6..6c9b590df22 100644 --- a/packages/entropy-controller/src/index.ts +++ b/packages/entropy-controller/src/index.ts @@ -1,13 +1,16 @@ export { EntropyController } from './entropy-controller'; export type { - EntropyCategory, EntropyControllerActions, EntropyControllerEvents, EntropyControllerGetStateAction, EntropyControllerMessenger, EntropyControllerState, EntropyControllerStateChangeEvent, +} from './entropy-controller'; +export type { + EntropyCategory, EntropyId, EntropyMetadata, EntropyType, -} from './entropy-controller'; +} from './types'; +export { toEntropyFingerprint, toEntropyId } from './utils'; diff --git a/packages/entropy-controller/src/types.ts b/packages/entropy-controller/src/types.ts new file mode 100644 index 00000000000..4d50a4f8e05 --- /dev/null +++ b/packages/entropy-controller/src/types.ts @@ -0,0 +1,28 @@ +/** + * The category of an entropy source. + * + * - `'bip44'` — Entropy that uses BIP-44 to derive signers (e.g. SRP, hardware + * wallets). + * - `'raw'` — Entropy that exposes a single key directly, without derivation + * (e.g. imported private keys, MPC). + */ +export type EntropyCategory = 'bip44' | 'raw'; + +/** + * The type of an entropy source, expressed as `category:implementation`. + * + * @example `'bip44:srp'`, `'bip44:ledger'`, `'raw:private-key'`, `'raw:mpc'` + */ +export type EntropyType = `${EntropyCategory}:${string}`; + +/** + * Unique identifier for an entropy source. + */ +export type EntropyId = string; + +/** + * Metadata associated with an entropy source. + * + * Currently a placeholder — fields will be added as requirements emerge. + */ +export type EntropyMetadata = Record; diff --git a/packages/entropy-controller/src/utils.test.ts b/packages/entropy-controller/src/utils.test.ts new file mode 100644 index 00000000000..54cfffabf0c --- /dev/null +++ b/packages/entropy-controller/src/utils.test.ts @@ -0,0 +1,75 @@ +import { toEntropyFingerprint, toEntropyId } from './utils'; + +const SECRET = new Uint8Array(32).fill(1); + +describe('toEntropyFingerprint', () => { + it('returns a 64-character lowercase hex string', async () => { + const fingerprint = await toEntropyFingerprint(SECRET, 'bip44:srp'); + expect(fingerprint).toHaveLength(64); + expect(fingerprint).toMatch(/^[0-9a-f]+$/u); + }); + + it('is deterministic — same inputs always produce the same fingerprint', async () => { + const first = await toEntropyFingerprint(SECRET, 'bip44:srp'); + const second = await toEntropyFingerprint(SECRET, 'bip44:srp'); + expect(first).toBe(second); + }); + + it('is type-scoped — different entropy types produce different fingerprints for the same secret', async () => { + const srpFingerprint = await toEntropyFingerprint(SECRET, 'bip44:srp'); + const pkFingerprint = await toEntropyFingerprint( + SECRET, + 'raw:private-key', + ); + expect(srpFingerprint).not.toBe(pkFingerprint); + }); + + it('produces different fingerprints for different secrets of the same type', async () => { + const otherSecret = new Uint8Array(32).fill(2); + const first = await toEntropyFingerprint(SECRET, 'bip44:srp'); + const second = await toEntropyFingerprint(otherSecret, 'bip44:srp'); + expect(first).not.toBe(second); + }); + + it('matches a known value to guard against accidental algorithm changes', async () => { + // hex( HMAC-SHA256( key=Uint8Array(32).fill(1), msg='metamask:bip44:srp:fingerprint' ) ) + const fingerprint = await toEntropyFingerprint(SECRET, 'bip44:srp'); + expect(fingerprint).toBe( + '10d37bf4886303a200fe2e8147ded8e89458e37576a799ddaaacb33c89c07599', + ); + }); +}); + +describe('toEntropyId', () => { + it('returns a valid UUID v4 string', async () => { + const id = await toEntropyId(SECRET, 'bip44:srp'); + expect(id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u, + ); + }); + + it('is deterministic — same inputs always produce the same ID', async () => { + const first = await toEntropyId(SECRET, 'bip44:srp'); + const second = await toEntropyId(SECRET, 'bip44:srp'); + expect(first).toBe(second); + }); + + it('is type-scoped — different entropy types produce different IDs for the same secret', async () => { + const srpId = await toEntropyId(SECRET, 'bip44:srp'); + const pkId = await toEntropyId(SECRET, 'raw:private-key'); + expect(srpId).not.toBe(pkId); + }); + + it('produces different IDs for different secrets of the same type', async () => { + const otherSecret = new Uint8Array(32).fill(2); + const first = await toEntropyId(SECRET, 'bip44:srp'); + const second = await toEntropyId(otherSecret, 'bip44:srp'); + expect(first).not.toBe(second); + }); + + it('matches a known value to guard against accidental algorithm changes', async () => { + // uuid({ random: HMAC-SHA256( key=Uint8Array(32).fill(1), msg='metamask:bip44:srp:fingerprint' ).slice(0, 16) }) + const id = await toEntropyId(SECRET, 'bip44:srp'); + expect(id).toBe('10d37bf4-8863-43a2-80fe-2e8147ded8e8'); + }); +}); diff --git a/packages/entropy-controller/src/utils.ts b/packages/entropy-controller/src/utils.ts new file mode 100644 index 00000000000..57bed674106 --- /dev/null +++ b/packages/entropy-controller/src/utils.ts @@ -0,0 +1,75 @@ +import { hmac } from '@noble/hashes/hmac'; +import { sha256 } from '@noble/hashes/sha256'; +import { bytesToHex } from '@noble/hashes/utils'; +import { v4 as uuid } from 'uuid'; + +import type { EntropyId, EntropyType } from './types'; + +/** + * Computes the raw HMAC-SHA256 digest for a piece of entropy — the shared + * primitive underlying both {@link toEntropyFingerprint} and + * {@link toEntropyId}. + * + * Using the secret as the HMAC key and the `EntropyType` as the domain + * separator ensures non-reversibility and type-scoping. + * + * @param secret - The raw entropy bytes. + * @param entropyType - The type of entropy source. + * @returns The 32-byte HMAC-SHA256 digest. + */ +async function toEntropyFingerprintBytes( + secret: Uint8Array, + entropyType: EntropyType, +): Promise { + const message = new TextEncoder().encode( + `metamask:${entropyType}:fingerprint`, + ); + return hmac(sha256, secret, message); +} + +/** + * Computes a deterministic, non-reversible hex fingerprint for a piece of + * entropy. + * + * The fingerprint is `hex(HMAC-SHA256(key=secret, msg='metamask:{entropyType}:fingerprint'))`. + * It is suitable for comparison and auditing — e.g. detecting that two + * separate components hold the same underlying secret. + * + * The return type is `Promise` to allow a future migration to the Web + * Crypto API without breaking callers. + * + * @param secret - The raw entropy bytes (e.g. BIP-39 mnemonic bytes, a 32-byte + * private key). + * @param entropyType - The type of entropy source, expressed as + * `category:implementation` (e.g. `'bip44:srp'`, `'raw:private-key'`). + * @returns The lowercase hex-encoded 32-byte HMAC-SHA256 digest. + */ +export async function toEntropyFingerprint( + secret: Uint8Array, + entropyType: EntropyType, +): Promise { + return bytesToHex(await toEntropyFingerprintBytes(secret, entropyType)); +} + +/** + * Derives a deterministic UUID v4 to use as an {@link EntropyId}. + * + * The UUID is built from the first 16 bytes of + * `HMAC-SHA256(key=secret, msg='metamask:{entropyType}:fingerprint')`, + * formatted as a standard UUID v4 (with the version and variant bits fixed by + * the UUID spec). Same secret + same type always produces the same ID. + * + * @param secret - The raw entropy bytes (e.g. BIP-39 mnemonic bytes, a 32-byte + * private key). + * @param entropyType - The type of entropy source, expressed as + * `category:implementation` (e.g. `'bip44:srp'`, `'raw:private-key'`). + * @returns A deterministic UUID v4 string suitable for use as an + * {@link EntropyId}. + */ +export async function toEntropyId( + secret: Uint8Array, + entropyType: EntropyType, +): Promise { + const bytes = await toEntropyFingerprintBytes(secret, entropyType); + return uuid({ random: bytes.slice(0, 16) }); +} diff --git a/yarn.lock b/yarn.lock index d8c3167a78f..dfbe6b8f4b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6600,6 +6600,7 @@ __metadata: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" "@metamask/messenger": "npm:^2.0.0" + "@noble/hashes": "npm:^1.8.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" deepmerge: "npm:^4.2.2" @@ -6609,6 +6610,7 @@ __metadata: typedoc: "npm:^0.25.13" typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.3.3" + uuid: "npm:^8.3.2" languageName: unknown linkType: soft From 46ec1328f31f4dec990c3a48300480e5bb23a9c0 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 16 Jul 2026 13:49:41 +0200 Subject: [PATCH 5/5] fix: fix yarn constraints --- packages/entropy-controller/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/entropy-controller/package.json b/packages/entropy-controller/package.json index d012e2b0a60..456d26625cc 100644 --- a/packages/entropy-controller/package.json +++ b/packages/entropy-controller/package.json @@ -50,7 +50,9 @@ "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", - "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", + "lint:tsconfigs": "tsx ../../scripts/lint-tsconfigs/lint-tsconfigs.mts", + "lint:tsconfigs:fix": "tsx ../../scripts/lint-tsconfigs/lint-tsconfigs.mts --fix" }, "dependencies": { "@metamask/base-controller": "^9.1.0",