From cde2bc7eecd65f72bc02542c75104ae8d2e933a4 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 8 Apr 2026 09:23:48 +0200 Subject: [PATCH 01/11] Initialize wallet package --- README.md | 2 + packages/wallet/CHANGELOG.md | 10 +++++ packages/wallet/LICENSE | 20 +++++++++ packages/wallet/README.md | 15 +++++++ packages/wallet/jest.config.js | 26 +++++++++++ packages/wallet/package.json | 70 +++++++++++++++++++++++++++++ packages/wallet/src/index.test.ts | 9 ++++ packages/wallet/src/index.ts | 9 ++++ packages/wallet/tsconfig.build.json | 10 +++++ packages/wallet/tsconfig.json | 8 ++++ packages/wallet/typedoc.json | 7 +++ tsconfig.build.json | 3 ++ tsconfig.json | 3 ++ yarn.lock | 17 +++++++ 14 files changed, 209 insertions(+) create mode 100644 packages/wallet/CHANGELOG.md create mode 100644 packages/wallet/LICENSE create mode 100644 packages/wallet/README.md create mode 100644 packages/wallet/jest.config.js create mode 100644 packages/wallet/package.json create mode 100644 packages/wallet/src/index.test.ts create mode 100644 packages/wallet/src/index.ts create mode 100644 packages/wallet/tsconfig.build.json create mode 100644 packages/wallet/tsconfig.json create mode 100644 packages/wallet/typedoc.json diff --git a/README.md b/README.md index 352fec4e6ec..13dae34ef03 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ Each package in this repository has its own README where you can find installati - [`@metamask/transaction-controller`](packages/transaction-controller) - [`@metamask/transaction-pay-controller`](packages/transaction-pay-controller) - [`@metamask/user-operation-controller`](packages/user-operation-controller) +- [`@metamask/wallet`](packages/wallet) @@ -183,6 +184,7 @@ linkStyle default opacity:0.5 transaction_controller(["@metamask/transaction-controller"]); transaction_pay_controller(["@metamask/transaction-pay-controller"]); user_operation_controller(["@metamask/user-operation-controller"]); + wallet(["@metamask/wallet"]); account_tree_controller --> accounts_controller; account_tree_controller --> base_controller; account_tree_controller --> keyring_controller; diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md new file mode 100644 index 00000000000..b518709c7b8 --- /dev/null +++ b/packages/wallet/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/wallet/LICENSE b/packages/wallet/LICENSE new file mode 100644 index 00000000000..c8a0ff6be3a --- /dev/null +++ b/packages/wallet/LICENSE @@ -0,0 +1,20 @@ +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 diff --git a/packages/wallet/README.md b/packages/wallet/README.md new file mode 100644 index 00000000000..da275a947df --- /dev/null +++ b/packages/wallet/README.md @@ -0,0 +1,15 @@ +# `@metamask/wallet` + +Provides a shared framework for building MetaMask wallets + +## Installation + +`yarn add @metamask/wallet` + +or + +`npm install @metamask/wallet` + +## 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/wallet/jest.config.js b/packages/wallet/jest.config.js new file mode 100644 index 00000000000..ca084133399 --- /dev/null +++ b/packages/wallet/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/wallet/package.json b/packages/wallet/package.json new file mode 100644 index 00000000000..92af1946d7a --- /dev/null +++ b/packages/wallet/package.json @@ -0,0 +1,70 @@ +{ + "name": "@metamask/wallet", + "version": "0.0.0", + "description": "Provides a shared framework for building MetaMask wallets", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/wallet#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "license": "MIT", + "sideEffects": false, + "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" + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "files": [ + "dist/" + ], + "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/wallet", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/wallet", + "messenger-action-types:check": "tsx ../../packages/messenger-cli/src/cli.ts --check", + "messenger-action-types:generate": "tsx ../../packages/messenger-cli/src/cli.ts --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" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.4.4", + "@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" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/wallet/src/index.test.ts b/packages/wallet/src/index.test.ts new file mode 100644 index 00000000000..bc062d3694a --- /dev/null +++ b/packages/wallet/src/index.test.ts @@ -0,0 +1,9 @@ +import greeter from '.'; + +describe('Test', () => { + it('greets', () => { + const name = 'Huey'; + const result = greeter(name); + expect(result).toBe('Hello, Huey!'); + }); +}); diff --git a/packages/wallet/src/index.ts b/packages/wallet/src/index.ts new file mode 100644 index 00000000000..6972c117292 --- /dev/null +++ b/packages/wallet/src/index.ts @@ -0,0 +1,9 @@ +/** + * Example function that returns a greeting for the given name. + * + * @param name - The name to greet. + * @returns The greeting. + */ +export default function greeter(name: string): string { + return `Hello, ${name}!`; +} diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json new file mode 100644 index 00000000000..02a0eea03fe --- /dev/null +++ b/packages/wallet/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [], + "include": ["../../types", "./src"] +} diff --git a/packages/wallet/tsconfig.json b/packages/wallet/tsconfig.json new file mode 100644 index 00000000000..025ba2ef7f4 --- /dev/null +++ b/packages/wallet/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [], + "include": ["../../types", "./src"] +} diff --git a/packages/wallet/typedoc.json b/packages/wallet/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/wallet/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 b02f3a882f5..af5eef1e9a2 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -237,6 +237,9 @@ }, { "path": "./packages/user-operation-controller/tsconfig.build.json" + }, + { + "path": "./packages/wallet/tsconfig.build.json" } ], "files": [], diff --git a/tsconfig.json b/tsconfig.json index 7eb3aadc457..cd7ce6f2538 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -226,6 +226,9 @@ }, { "path": "./packages/user-operation-controller" + }, + { + "path": "./packages/wallet" } ], "files": [], diff --git a/yarn.lock b/yarn.lock index 52edcac51b3..4dc6362877c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5677,6 +5677,23 @@ __metadata: languageName: node linkType: hard +"@metamask/wallet@workspace:packages/wallet": + version: 0.0.0-use.local + resolution: "@metamask/wallet@workspace:packages/wallet" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@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 + "@myx-trade/sdk@npm:^0.1.265": version: 0.1.265 resolution: "@myx-trade/sdk@npm:0.1.265" From 73ba89768d80dcc0feab83e87f17b3639a0d4781 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 8 Apr 2026 09:25:26 +0200 Subject: [PATCH 02/11] Default licensing --- packages/wallet/LICENSE | 22 +--- packages/wallet/LICENSE.APACHE2 | 201 ++++++++++++++++++++++++++++++++ packages/wallet/LICENSE.MIT | 21 ++++ 3 files changed, 226 insertions(+), 18 deletions(-) create mode 100644 packages/wallet/LICENSE.APACHE2 create mode 100644 packages/wallet/LICENSE.MIT diff --git a/packages/wallet/LICENSE b/packages/wallet/LICENSE index c8a0ff6be3a..f9f85c6d4ec 100644 --- a/packages/wallet/LICENSE +++ b/packages/wallet/LICENSE @@ -1,20 +1,6 @@ -MIT License +This project is licensed under either of -Copyright (c) 2026 MetaMask + * MIT license ([LICENSE.MIT](LICENSE.MIT)) + * Apache License, Version 2.0 ([LICENSE.APACHE2](LICENSE.APACHE2)) -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 +at your option. diff --git a/packages/wallet/LICENSE.APACHE2 b/packages/wallet/LICENSE.APACHE2 new file mode 100644 index 00000000000..18002eac9ae --- /dev/null +++ b/packages/wallet/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 2026 MetaMask + + 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. \ No newline at end of file diff --git a/packages/wallet/LICENSE.MIT b/packages/wallet/LICENSE.MIT new file mode 100644 index 00000000000..e0278643409 --- /dev/null +++ b/packages/wallet/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. \ No newline at end of file From 26111408e648d0fd71a1ca1111e8260010ac505f Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 8 Apr 2026 12:34:24 +0200 Subject: [PATCH 03/11] Add basic support KeyringController and AccountsController --- packages/wallet/package.json | 7 + packages/wallet/src/Wallet.test.ts | 31 ++++ packages/wallet/src/Wallet.ts | 33 ++++ packages/wallet/src/index.test.ts | 9 -- packages/wallet/src/index.ts | 10 +- packages/wallet/src/initialization/index.ts | 1 + .../src/initialization/initialization.ts | 40 +++++ .../instances/accounts-controller.ts | 57 +++++++ .../src/initialization/instances/index.ts | 2 + .../instances/keyring-controller.ts | 152 ++++++++++++++++++ packages/wallet/src/initialization/types.ts | 24 +++ packages/wallet/src/types.ts | 3 + packages/wallet/src/utilities.ts | 29 ++++ packages/wallet/tsconfig.build.json | 9 +- packages/wallet/tsconfig.json | 9 +- yarn.lock | 24 +++ 16 files changed, 420 insertions(+), 20 deletions(-) create mode 100644 packages/wallet/src/Wallet.test.ts create mode 100644 packages/wallet/src/Wallet.ts delete mode 100644 packages/wallet/src/index.test.ts create mode 100644 packages/wallet/src/initialization/index.ts create mode 100644 packages/wallet/src/initialization/initialization.ts create mode 100644 packages/wallet/src/initialization/instances/accounts-controller.ts create mode 100644 packages/wallet/src/initialization/instances/index.ts create mode 100644 packages/wallet/src/initialization/instances/keyring-controller.ts create mode 100644 packages/wallet/src/initialization/types.ts create mode 100644 packages/wallet/src/types.ts create mode 100644 packages/wallet/src/utilities.ts diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 92af1946d7a..c08321d0f64 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -48,6 +48,13 @@ "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, + "dependencies": { + "@metamask/browser-passworder": "^6.0.0", + "@metamask/keyring-controller": "^25.2.0", + "@metamask/messenger": "^1.1.1", + "@metamask/scure-bip39": "^2.1.1", + "@metamask/utils": "^11.11.0" + }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@ts-bridge/cli": "^0.6.4", diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts new file mode 100644 index 00000000000..91bf3023806 --- /dev/null +++ b/packages/wallet/src/Wallet.test.ts @@ -0,0 +1,31 @@ +import { importSecretRecoveryPhrase } from './utilities'; +import { Wallet } from './Wallet'; + +const TEST_PHRASE = + 'test test test test test test test test test test test ball'; +const TEST_PASSWORD = 'testpass'; + +describe('Wallet', () => { + it('can unlock and populate accounts', async () => { + const wallet = new Wallet(); + + await importSecretRecoveryPhrase(wallet, TEST_PASSWORD, TEST_PHRASE); + + const { messenger } = wallet; + + expect( + messenger + .call('AccountsController:listAccounts') + .map((account) => account.address), + ).toStrictEqual(['0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf']); + }); + + it('exposes state', () => { + const { state } = new Wallet(); + + expect(state.KeyringController).toStrictEqual({ + isUnlocked: false, + keyrings: [], + }); + }); +}); diff --git a/packages/wallet/src/Wallet.ts b/packages/wallet/src/Wallet.ts new file mode 100644 index 00000000000..7e82bdcdb6f --- /dev/null +++ b/packages/wallet/src/Wallet.ts @@ -0,0 +1,33 @@ +import { Messenger } from '@metamask/messenger'; +import type { Json } from '@metamask/utils'; + +import { initialize } from './initialization'; +import { RootMessenger } from './types'; + +export type WalletArgs = { + state: Json; +}; + +export class Wallet { + public messenger: RootMessenger; + + readonly #instances; + + constructor({ state = {} } = {}) { + this.messenger = new Messenger({ + namespace: 'Root', + }); + + this.#instances = initialize({ state, messenger: this.messenger }); + } + + get state(): Record { + return Object.entries(this.#instances).reduce>( + (accumulator, [key, instance]) => { + accumulator[key] = instance.state ?? null; + return accumulator; + }, + {}, + ); + } +} diff --git a/packages/wallet/src/index.test.ts b/packages/wallet/src/index.test.ts deleted file mode 100644 index bc062d3694a..00000000000 --- a/packages/wallet/src/index.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import greeter from '.'; - -describe('Test', () => { - it('greets', () => { - const name = 'Huey'; - const result = greeter(name); - expect(result).toBe('Hello, Huey!'); - }); -}); diff --git a/packages/wallet/src/index.ts b/packages/wallet/src/index.ts index 6972c117292..a3db3b1b449 100644 --- a/packages/wallet/src/index.ts +++ b/packages/wallet/src/index.ts @@ -1,9 +1 @@ -/** - * Example function that returns a greeting for the given name. - * - * @param name - The name to greet. - * @returns The greeting. - */ -export default function greeter(name: string): string { - return `Hello, ${name}!`; -} +export { Wallet } from './Wallet'; diff --git a/packages/wallet/src/initialization/index.ts b/packages/wallet/src/initialization/index.ts new file mode 100644 index 00000000000..5f1b4048d67 --- /dev/null +++ b/packages/wallet/src/initialization/index.ts @@ -0,0 +1 @@ +export { initialize } from './initialization'; diff --git a/packages/wallet/src/initialization/initialization.ts b/packages/wallet/src/initialization/initialization.ts new file mode 100644 index 00000000000..734f39a59ca --- /dev/null +++ b/packages/wallet/src/initialization/initialization.ts @@ -0,0 +1,40 @@ +import { Json } from '@metamask/utils'; + +import * as defaultConfigurations from './instances'; +import { InitializationConfiguration } from './types'; +import { RootMessenger } from '../types'; + +export type InitializeArgs = { + state: Record; + messenger: RootMessenger; + initializationConfigurations?: InitializationConfiguration[]; +}; + +export function initialize({ + state, + messenger, + initializationConfigurations = [], +}: InitializeArgs) { + const configurationEntries = initializationConfigurations.concat( + Object.values(defaultConfigurations), + ); + + const instances = {}; + + for (const config of configurationEntries) { + const { name } = config; + + const instanceState = state[name]; + + const instanceMessenger = config.messenger(messenger); + + const { instance } = config.init({ + state: instanceState, + messenger: instanceMessenger, + }); + + instances[name] = instance; + } + + return instances; +} diff --git a/packages/wallet/src/initialization/instances/accounts-controller.ts b/packages/wallet/src/initialization/instances/accounts-controller.ts new file mode 100644 index 00000000000..ce37ba2df57 --- /dev/null +++ b/packages/wallet/src/initialization/instances/accounts-controller.ts @@ -0,0 +1,57 @@ +import { + AccountsController, + AccountsControllerMessenger, +} from '@metamask/accounts-controller'; +import type { + AllowedActions, + AllowedEvents, +} from '@metamask/accounts-controller'; +import { Messenger } from '@metamask/messenger'; + +import { InitializationConfiguration } from '../types'; + +export const accountsController: InitializationConfiguration< + AccountsController, + AccountsControllerMessenger +> = { + name: 'AccountsController', + init: ({ state, messenger }) => { + const instance = new AccountsController({ + state, + messenger, + }); + + return { + instance, + }; + }, + messenger: (parent) => { + const accountsControllerMessenger = new Messenger< + 'AccountsController', + AllowedActions, + AllowedEvents, + typeof parent + >({ + namespace: 'AccountsController', + parent, + }); + + parent.delegate({ + messenger: accountsControllerMessenger, + actions: [ + 'KeyringController:getState', + 'KeyringController:getKeyringsByType', + ], + events: [ + 'SnapController:stateChange', + 'KeyringController:stateChange', + 'SnapKeyring:accountAssetListUpdated', + 'SnapKeyring:accountBalancesUpdated', + 'SnapKeyring:accountTransactionsUpdated', + 'MultichainNetworkController:networkDidChange', + ], + }); + + return accountsControllerMessenger; + }, +}; diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts new file mode 100644 index 00000000000..840e2e416af --- /dev/null +++ b/packages/wallet/src/initialization/instances/index.ts @@ -0,0 +1,2 @@ +export * from './accounts-controller'; +export * from './keyring-controller'; diff --git a/packages/wallet/src/initialization/instances/keyring-controller.ts b/packages/wallet/src/initialization/instances/keyring-controller.ts new file mode 100644 index 00000000000..8348de7e1be --- /dev/null +++ b/packages/wallet/src/initialization/instances/keyring-controller.ts @@ -0,0 +1,152 @@ +import { + encrypt, + encryptWithDetail, + encryptWithKey, + decrypt, + decryptWithDetail, + decryptWithKey, + isVaultUpdated, + keyFromPassword, + importKey, + exportKey, + generateSalt, + EncryptionKey, + KeyDerivationOptions, +} from '@metamask/browser-passworder'; +import { + KeyringController, + KeyringControllerMessenger, +} from '@metamask/keyring-controller'; +import { Messenger } from '@metamask/messenger'; + +import { InitializationConfiguration } from '../types'; + +/** + * A factory function for the encrypt method of the browser-passworder library, + * that encrypts with a given number of iterations. + * + * @param iterations - The number of iterations to use for the PBKDF2 algorithm. + * @returns A function that encrypts with the given number of iterations. + */ +const encryptFactory = + (iterations: number) => + async ( + password: string, + data: unknown, + key?: EncryptionKey | CryptoKey, + salt?: string, + ) => + encrypt(password, data, key, salt, { + algorithm: 'PBKDF2', + params: { + iterations, + }, + }); + +/** + * A factory function for the encryptWithDetail method of the browser-passworder library, + * that encrypts with a given number of iterations. + * + * @param iterations - The number of iterations to use for the PBKDF2 algorithm. + * @returns A function that encrypts with the given number of iterations. + */ +const encryptWithDetailFactory = + (iterations: number) => + async (password: string, object: unknown, salt?: string) => + encryptWithDetail(password, object, salt, { + algorithm: 'PBKDF2', + params: { + iterations, + }, + }); + +/** + * A factory function for the keyFromPassword method of the browser-passworder library, + * that generates a key from a password and a salt. + * + * This factory function overrides the default key derivation options with the specified + * number of iterations, unless existing key derivation options are passed in. + * + * @param iterations - The number of iterations to use for the PBKDF2 algorithm. + * @returns A function that generates a key with a potentially overriden number of iterations. + */ +const keyFromPasswordFactory = + (iterations: number) => + async ( + password: string, + salt: string, + exportable?: boolean, + opts?: KeyDerivationOptions, + ) => + keyFromPassword( + password, + salt, + exportable, + opts ?? { + algorithm: 'PBKDF2', + params: { + iterations, + }, + }, + ); + +/** + * A factory function for the isVaultUpdated method of the browser-passworder library, + * that checks if the given vault was encrypted with the given number of iterations. + * + * @param iterations - The number of iterations to use for the PBKDF2 algorithm. + * @returns A function that checks if the vault was encrypted with the given number of iterations. + */ +const isVaultUpdatedFactory = (iterations: number) => (vault: string) => + isVaultUpdated(vault, { + algorithm: 'PBKDF2', + params: { + iterations, + }, + }); + +/** + * A factory function that returns an encryptor with the given number of iterations. + * + * The returned encryptor is a wrapper around the browser-passworder library, that + * calls the encrypt and encryptWithDetail methods with the given number of iterations. + * + * @param iterations - The number of iterations to use for the PBKDF2 algorithm. + * @returns An encryptor set with the given number of iterations. + */ +const encryptorFactory = (iterations: number) => ({ + encrypt: encryptFactory(iterations), + encryptWithKey, + encryptWithDetail: encryptWithDetailFactory(iterations), + decrypt, + decryptWithKey, + decryptWithDetail, + keyFromPassword: keyFromPasswordFactory(iterations), + isVaultUpdated: isVaultUpdatedFactory(iterations), + importKey, + exportKey, + generateSalt, +}); + +export const keyringController: InitializationConfiguration< + KeyringController, + KeyringControllerMessenger +> = { + name: 'KeyringController', + init: ({ state, messenger }) => { + const instance = new KeyringController({ + state, + messenger, + encryptor: encryptorFactory(600_000), + }); + + return { + instance, + }; + }, + messenger: (parent) => + new Messenger<'KeyringController', never, never, typeof parent>({ + namespace: 'KeyringController', + parent, + }), +}; diff --git a/packages/wallet/src/initialization/types.ts b/packages/wallet/src/initialization/types.ts new file mode 100644 index 00000000000..07a07f6a581 --- /dev/null +++ b/packages/wallet/src/initialization/types.ts @@ -0,0 +1,24 @@ +import { RootMessenger } from '../types'; + +export type InstanceState = Instance extends { state: unknown } + ? Instance['state'] + : null; + +export type InitFunctionArguments = { + state: InstanceState; + messenger: InstanceMessenger; +}; + +export type InitFunction = ( + args: InitFunctionArguments, +) => { instance: Instance }; + +export type MessengerInitFunction = ( + parent: RootMessenger, +) => NarrowedMessenger; + +export type InitializationConfiguration = { + name: string; + init: InitFunction; + messenger: MessengerInitFunction; +}; diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts new file mode 100644 index 00000000000..2a6cc3f26df --- /dev/null +++ b/packages/wallet/src/types.ts @@ -0,0 +1,3 @@ +import { Messenger } from '@metamask/messenger'; + +export type RootMessenger = Messenger<'Root'>; diff --git a/packages/wallet/src/utilities.ts b/packages/wallet/src/utilities.ts new file mode 100644 index 00000000000..45a480171ef --- /dev/null +++ b/packages/wallet/src/utilities.ts @@ -0,0 +1,29 @@ +// TODO: Determine if these should be available directly on Wallet. +import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; + +import { Wallet } from './Wallet'; + +/** + * Import a secret recovery phrase using the wallet object. + * + * @param wallet - The wallet object. + * @param password - The password to the MetaMask wallet (not the SRP). + * @param phrase - The SRP as a string. + */ +export async function importSecretRecoveryPhrase( + wallet: Wallet, + password: string, + phrase: string, +) { + const { messenger } = wallet; + + const indices = phrase.split(' ').map((word) => wordlist.indexOf(word)); + const mnemonic = new Uint8Array(new Uint16Array(indices).buffer); + + // TODO: This should use the new MultichainAccountService. + await messenger.call( + 'KeyringController:createNewVaultAndRestore', + password, + mnemonic, + ); +} diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json index 02a0eea03fe..2657e084ed0 100644 --- a/packages/wallet/tsconfig.build.json +++ b/packages/wallet/tsconfig.build.json @@ -5,6 +5,13 @@ "outDir": "./dist", "rootDir": "./src" }, - "references": [], + "references": [ + { + "path": "../messenger/tsconfig.build.json" + }, + { + "path": "../keyring-controller/tsconfig.build.json" + } + ], "include": ["../../types", "./src"] } diff --git a/packages/wallet/tsconfig.json b/packages/wallet/tsconfig.json index 025ba2ef7f4..cdbf9854ac4 100644 --- a/packages/wallet/tsconfig.json +++ b/packages/wallet/tsconfig.json @@ -3,6 +3,13 @@ "compilerOptions": { "baseUrl": "./" }, - "references": [], + "references": [ + { + "path": "../messenger/tsconfig.json" + }, + { + "path": "../keyring-controller/tsconfig.json" + } + ], "include": ["../../types", "./src"] } diff --git a/yarn.lock b/yarn.lock index 4dc6362877c..2eb72eda13a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5660,6 +5660,25 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^11.11.0": + version: 11.11.0 + resolution: "@metamask/utils@npm:11.11.0" + dependencies: + "@ethereumjs/tx": "npm:^4.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.3" + "@types/debug": "npm:^4.1.7" + "@types/lodash": "npm:^4.17.20" + debug: "npm:^4.3.4" + lodash: "npm:^4.17.21" + pony-cause: "npm:^2.1.10" + semver: "npm:^7.5.4" + uuid: "npm:^9.0.1" + checksum: 10/c4381b9e451a9616bde84ac659bc0d1848ef06b6e605f877bfa065b78c8ed5015706683ea88a3387de5eaeb3a50d1af9af0994f04f9e06258d992598fe2be3bf + languageName: node + linkType: hard + "@metamask/utils@npm:^9.0.0": version: 9.3.0 resolution: "@metamask/utils@npm:9.3.0" @@ -5682,6 +5701,11 @@ __metadata: resolution: "@metamask/wallet@workspace:packages/wallet" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/browser-passworder": "npm:^6.0.0" + "@metamask/keyring-controller": "npm:^25.2.0" + "@metamask/messenger": "npm:^1.1.1" + "@metamask/scure-bip39": "npm:^2.1.1" + "@metamask/utils": "npm:^11.11.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" deepmerge: "npm:^4.2.2" From 51661608232e979309a945d56f368cc6212825ef Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 8 Apr 2026 12:39:58 +0200 Subject: [PATCH 04/11] Filter out overridden configuration --- packages/wallet/src/initialization/initialization.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/wallet/src/initialization/initialization.ts b/packages/wallet/src/initialization/initialization.ts index 734f39a59ca..f6ea3944cde 100644 --- a/packages/wallet/src/initialization/initialization.ts +++ b/packages/wallet/src/initialization/initialization.ts @@ -15,8 +15,14 @@ export function initialize({ messenger, initializationConfigurations = [], }: InitializeArgs) { + const overriddenConfiguration = initializationConfigurations.map( + (config) => config.name, + ); + const configurationEntries = initializationConfigurations.concat( - Object.values(defaultConfigurations), + Object.values(defaultConfigurations).filter( + (config) => !overriddenConfiguration.includes(config.name), + ), ); const instances = {}; From 33f89341e92e5508795841cd99b9dbe1502fa2e9 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 8 Apr 2026 15:25:13 +0200 Subject: [PATCH 05/11] Add controllers required for signing and submitting transactions --- packages/wallet/package.json | 1 + packages/wallet/src/Wallet.test.ts | 56 +++++++++--- packages/wallet/src/Wallet.ts | 11 +-- .../src/initialization/initialization.ts | 10 ++- .../instances/accounts-controller.ts | 14 +-- .../instances/approval-controller.ts | 46 ++++++++++ .../instances/connectivity-controller.ts | 47 ++++++++++ .../src/initialization/instances/index.ts | 5 ++ .../instances/network-controller.ts | 89 +++++++++++++++++++ .../remote-feature-flag-controller.ts | 31 +++++++ .../instances/transaction-controller.ts | 79 ++++++++++++++++ packages/wallet/src/initialization/types.ts | 3 +- packages/wallet/src/types.ts | 17 +++- packages/wallet/src/utilities.ts | 51 ++++++++++- yarn.lock | 1 + 15 files changed, 433 insertions(+), 28 deletions(-) create mode 100644 packages/wallet/src/initialization/instances/approval-controller.ts create mode 100644 packages/wallet/src/initialization/instances/connectivity-controller.ts create mode 100644 packages/wallet/src/initialization/instances/network-controller.ts create mode 100644 packages/wallet/src/initialization/instances/remote-feature-flag-controller.ts create mode 100644 packages/wallet/src/initialization/instances/transaction-controller.ts diff --git a/packages/wallet/package.json b/packages/wallet/package.json index c08321d0f64..0ad514fb7c7 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -50,6 +50,7 @@ }, "dependencies": { "@metamask/browser-passworder": "^6.0.0", + "@metamask/controller-utils": "^11.20.0", "@metamask/keyring-controller": "^25.2.0", "@metamask/messenger": "^1.1.1", "@metamask/scure-bip39": "^2.1.1", diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index 91bf3023806..05a55a2b1d8 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -1,17 +1,29 @@ -import { importSecretRecoveryPhrase } from './utilities'; +import { enableNetConnect } from 'nock'; + +import { importSecretRecoveryPhrase, sendTransaction } from './utilities'; import { Wallet } from './Wallet'; const TEST_PHRASE = 'test test test test test test test test test test test ball'; const TEST_PASSWORD = 'testpass'; -describe('Wallet', () => { - it('can unlock and populate accounts', async () => { - const wallet = new Wallet(); +async function setupWallet() { + const wallet = new Wallet({ + options: { + infuraProjectId: 'infura-project-id', + clientVersion: '1.0.0', + showApprovalRequest: () => undefined, + }, + }); - await importSecretRecoveryPhrase(wallet, TEST_PASSWORD, TEST_PHRASE); + await importSecretRecoveryPhrase(wallet, TEST_PASSWORD, TEST_PHRASE); - const { messenger } = wallet; + return wallet; +} + +describe('Wallet', () => { + it('can unlock and populate accounts', async () => { + const { messenger } = await setupWallet(); expect( messenger @@ -20,12 +32,36 @@ describe('Wallet', () => { ).toStrictEqual(['0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf']); }); - it('exposes state', () => { - const { state } = new Wallet(); + it('signs transactions', async () => { + enableNetConnect(); + + const wallet = await setupWallet(); + + const addresses = wallet.messenger + .call('AccountsController:listAccounts') + .map((account) => account.address); + + const { result, transactionMeta } = await sendTransaction( + wallet, + { from: addresses[0], to: addresses[0], data: '0x00' }, + { networkClientId: 'mainnet' }, + ); + + await result; + + expect(result).toStrictEqual({}); + expect(transactionMeta).toStrictEqual({}); + }); + + it('exposes state', async () => { + const { state } = await setupWallet(); expect(state.KeyringController).toStrictEqual({ - isUnlocked: false, - keyrings: [], + isUnlocked: true, + keyrings: expect.any(Array), + encryptionKey: expect.any(String), + encryptionSalt: expect.any(String), + vault: expect.any(String), }); }); }); diff --git a/packages/wallet/src/Wallet.ts b/packages/wallet/src/Wallet.ts index 7e82bdcdb6f..3b378f81803 100644 --- a/packages/wallet/src/Wallet.ts +++ b/packages/wallet/src/Wallet.ts @@ -2,10 +2,11 @@ import { Messenger } from '@metamask/messenger'; import type { Json } from '@metamask/utils'; import { initialize } from './initialization'; -import { RootMessenger } from './types'; +import { RootMessenger, WalletOptions } from './types'; -export type WalletArgs = { - state: Json; +export type WalletConstructorArgs = { + state?: Record; + options: WalletOptions; }; export class Wallet { @@ -13,12 +14,12 @@ export class Wallet { readonly #instances; - constructor({ state = {} } = {}) { + constructor({ state = {}, options }: WalletConstructorArgs) { this.messenger = new Messenger({ namespace: 'Root', }); - this.#instances = initialize({ state, messenger: this.messenger }); + this.#instances = initialize({ state, messenger: this.messenger, options }); } get state(): Record { diff --git a/packages/wallet/src/initialization/initialization.ts b/packages/wallet/src/initialization/initialization.ts index f6ea3944cde..abcf6034ef8 100644 --- a/packages/wallet/src/initialization/initialization.ts +++ b/packages/wallet/src/initialization/initialization.ts @@ -2,18 +2,23 @@ import { Json } from '@metamask/utils'; import * as defaultConfigurations from './instances'; import { InitializationConfiguration } from './types'; -import { RootMessenger } from '../types'; +import { RootMessenger, WalletOptions } from '../types'; export type InitializeArgs = { state: Record; messenger: RootMessenger; - initializationConfigurations?: InitializationConfiguration[]; + initializationConfigurations?: InitializationConfiguration< + unknown, + unknown + >[]; + options: WalletOptions; }; export function initialize({ state, messenger, initializationConfigurations = [], + options, }: InitializeArgs) { const overriddenConfiguration = initializationConfigurations.map( (config) => config.name, @@ -37,6 +42,7 @@ export function initialize({ const { instance } = config.init({ state: instanceState, messenger: instanceMessenger, + options, }); instances[name] = instance; diff --git a/packages/wallet/src/initialization/instances/accounts-controller.ts b/packages/wallet/src/initialization/instances/accounts-controller.ts index ce37ba2df57..ebe6b7847ed 100644 --- a/packages/wallet/src/initialization/instances/accounts-controller.ts +++ b/packages/wallet/src/initialization/instances/accounts-controller.ts @@ -2,14 +2,18 @@ import { AccountsController, AccountsControllerMessenger, } from '@metamask/accounts-controller'; -import type { - AllowedActions, - AllowedEvents, -} from '@metamask/accounts-controller'; -import { Messenger } from '@metamask/messenger'; +import { + Messenger, + MessengerActions, + MessengerEvents, +} from '@metamask/messenger'; import { InitializationConfiguration } from '../types'; +type AllowedActions = MessengerActions; + +type AllowedEvents = MessengerEvents; + export const accountsController: InitializationConfiguration< AccountsController, AccountsControllerMessenger 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 00000000000..1e7b7b24ba2 --- /dev/null +++ b/packages/wallet/src/initialization/instances/approval-controller.ts @@ -0,0 +1,46 @@ +import { + ApprovalController, + ApprovalControllerMessenger, +} from '@metamask/approval-controller'; +import { ApprovalType } from '@metamask/controller-utils'; +import { Messenger } from '@metamask/messenger'; + +import { InitializationConfiguration } from '../types'; + +export const approvalController: InitializationConfiguration< + ApprovalController, + ApprovalControllerMessenger +> = { + name: 'ApprovalController', + init: ({ state, messenger, options }) => { + const instance = new ApprovalController({ + state, + messenger, + showApprovalRequest: options.showApprovalRequest, + typesExcludedFromRateLimiting: [ + ApprovalType.PersonalSign, + ApprovalType.EthSignTypedData, + ApprovalType.Transaction, + ApprovalType.WatchAsset, + ApprovalType.EthGetEncryptionPublicKey, + ApprovalType.EthDecrypt, + + // Exclude Smart TX Status Page from rate limiting to allow sequential + // transactions. + 'smartTransaction:showSmartTransactionStatusPage', + + // Allow one flavor of snap_dialog to be queued. + 'snap_dialog', + ], + }); + + return { + instance, + }; + }, + messenger: (parent) => + new Messenger<'ApprovalController', never, never, typeof parent>({ + namespace: 'ApprovalController', + parent, + }), +}; diff --git a/packages/wallet/src/initialization/instances/connectivity-controller.ts b/packages/wallet/src/initialization/instances/connectivity-controller.ts new file mode 100644 index 00000000000..98abc2c228c --- /dev/null +++ b/packages/wallet/src/initialization/instances/connectivity-controller.ts @@ -0,0 +1,47 @@ +import { + CONNECTIVITY_STATUSES, + ConnectivityAdapter, + ConnectivityController, + ConnectivityControllerMessenger, + ConnectivityStatus, +} from '@metamask/connectivity-controller'; +import { Messenger } from '@metamask/messenger'; + +import { InitializationConfiguration } from '../types'; + +// TODO: For now, we assume we are always online. +class AlwaysOnlineAdapter implements ConnectivityAdapter { + async getStatus(): Promise { + return CONNECTIVITY_STATUSES.Online; + } + + onConnectivityChange(_callback: (status: ConnectivityStatus) => void): void { + // no-op + } + + destroy(): void { + // no-op + } +} + +export const connectivityController: InitializationConfiguration< + ConnectivityController, + ConnectivityControllerMessenger +> = { + name: 'ConnectivityController', + init: ({ messenger }) => { + const instance = new ConnectivityController({ + messenger, + connectivityAdapter: new AlwaysOnlineAdapter(), + }); + + return { + instance, + }; + }, + messenger: (parent) => + new Messenger<'ConnectivityController', never, never, typeof parent>({ + namespace: 'ConnectivityController', + parent, + }), +}; diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts index 840e2e416af..4f869053f11 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -1,2 +1,7 @@ export * from './accounts-controller'; +export * from './approval-controller'; +export * from './connectivity-controller'; export * from './keyring-controller'; +export * from './network-controller'; +export * from './remote-feature-flag-controller'; +export * from './transaction-controller'; diff --git a/packages/wallet/src/initialization/instances/network-controller.ts b/packages/wallet/src/initialization/instances/network-controller.ts new file mode 100644 index 00000000000..47d762955a7 --- /dev/null +++ b/packages/wallet/src/initialization/instances/network-controller.ts @@ -0,0 +1,89 @@ +import { CONNECTIVITY_STATUSES } from '@metamask/connectivity-controller'; +import { DEFAULT_MAX_RETRIES } from '@metamask/controller-utils'; +import { + Messenger, + MessengerActions, + MessengerEvents, +} from '@metamask/messenger'; +import { + NetworkController, + NetworkControllerMessenger, +} from '@metamask/network-controller'; +import { Duration, inMilliseconds } from '@metamask/utils'; + +import { InitializationConfiguration } from '../types'; + +type AllowedActions = MessengerActions; + +type AllowedEvents = MessengerEvents; + +export const networkController: InitializationConfiguration< + NetworkController, + NetworkControllerMessenger +> = { + name: 'NetworkController', + init: ({ state, messenger, options }) => { + // TODO: This was gutted to simplify implementation for now. + const getRpcServiceOptions = () => { + const maxRetries = DEFAULT_MAX_RETRIES; + + const isOffline = (): boolean => { + const connectivityState = messenger.call( + 'ConnectivityController:getState', + ); + return ( + connectivityState.connectivityStatus === CONNECTIVITY_STATUSES.Offline + ); + }; + + return { + fetch: globalThis.fetch.bind(globalThis), + btoa: globalThis.btoa.bind(globalThis), + isOffline, + policyOptions: { + // Ensure that the "cooldown" period after breaking the circuit is short. + circuitBreakDuration: inMilliseconds(30, Duration.Second), + maxRetries, + // Ensure that if the endpoint continually responds with errors, we + // break the circuit relatively fast (but not prematurely). + // + // Note that the circuit will break much faster if the errors are + // retriable (e.g. 503) than if not (e.g. 500), so we attempt to strike + // a balance here. + maxConsecutiveFailures: (maxRetries + 1) * 3, + }, + }; + }; + + // TODO: Add the rest of the arguments. + const instance = new NetworkController({ + state, + messenger, + getRpcServiceOptions, + infuraProjectId: options.infuraProjectId, + }); + + return { + instance, + }; + }, + messenger: (parent) => { + const networkControllerMessenger = new Messenger< + 'NetworkController', + AllowedActions, + AllowedEvents, + typeof parent + >({ + namespace: 'NetworkController', + parent, + }); + + parent.delegate({ + messenger: networkControllerMessenger, + actions: ['ConnectivityController:getState'], + events: [], + }); + + return networkControllerMessenger; + }, +}; diff --git a/packages/wallet/src/initialization/instances/remote-feature-flag-controller.ts b/packages/wallet/src/initialization/instances/remote-feature-flag-controller.ts new file mode 100644 index 00000000000..56885f4466a --- /dev/null +++ b/packages/wallet/src/initialization/instances/remote-feature-flag-controller.ts @@ -0,0 +1,31 @@ +import { Messenger } from '@metamask/messenger'; +import { + RemoteFeatureFlagController, + RemoteFeatureFlagControllerMessenger, +} from '@metamask/remote-feature-flag-controller'; + +import { InitializationConfiguration } from '../types'; + +export const remoteFeatureFlagController: InitializationConfiguration< + RemoteFeatureFlagController, + RemoteFeatureFlagControllerMessenger +> = { + name: 'RemoteFeatureFlagController', + init: ({ state, messenger, options }) => { + // TODO: Add the rest of the arguments. + const instance = new RemoteFeatureFlagController({ + state, + messenger, + clientVersion: options.clientVersion, + }); + + return { + instance, + }; + }, + messenger: (parent) => + new Messenger<'RemoteFeatureFlagController', never, never, typeof parent>({ + namespace: 'RemoteFeatureFlagController', + parent, + }), +}; diff --git a/packages/wallet/src/initialization/instances/transaction-controller.ts b/packages/wallet/src/initialization/instances/transaction-controller.ts new file mode 100644 index 00000000000..e5ad99f1cd5 --- /dev/null +++ b/packages/wallet/src/initialization/instances/transaction-controller.ts @@ -0,0 +1,79 @@ +import { + Messenger, + MessengerActions, + MessengerEvents, +} from '@metamask/messenger'; +import { + TransactionController, + TransactionControllerMessenger, +} from '@metamask/transaction-controller'; + +import { InitializationConfiguration } from '../types'; + +type AllowedActions = MessengerActions; + +type AllowedEvents = MessengerEvents; + +export const transactionController: InitializationConfiguration< + TransactionController, + TransactionControllerMessenger +> = { + name: 'TransactionController', + init: ({ state, messenger }) => { + // TODO: Add the rest of the arguments. + const instance = new TransactionController({ + state, + messenger, + getNetworkClientRegistry: messenger.call.bind( + messenger, + 'NetworkController:getNetworkClientRegistry', + ), + getCurrentNetworkEIP1559Compatibility: messenger.call.bind( + messenger, + 'NetworkController:getEIP1559Compatibility', + ), + sign: messenger.call.bind(messenger, 'KeyringController:signTransaction'), + }); + + return { + instance, + }; + }, + messenger: (parent) => { + const transactionControllerMessenger = new Messenger< + 'TransactionController', + AllowedActions, + AllowedEvents, + typeof parent + >({ + namespace: 'TransactionController', + parent, + }); + + parent.delegate({ + messenger: transactionControllerMessenger, + actions: [ + 'AccountsController:getSelectedAccount', + 'AccountsController:getState', + `ApprovalController:addRequest`, + 'KeyringController:signEip7702Authorization', + 'NetworkController:findNetworkClientIdByChainId', + 'NetworkController:getNetworkClientById', + 'RemoteFeatureFlagController:getState', + // TODO: These are added for use in the constructor, in the extension this uses the "init messenger" concept. + 'NetworkController:getNetworkClientRegistry', + 'NetworkController:getEIP1559Compatibility', + 'KeyringController:signTransaction', + ], + events: [ + 'AccountActivityService:transactionUpdated', + 'AccountActivityService:statusChanged', + 'AccountsController:selectedAccountChange', + 'BackendWebSocketService:connectionStateChanged', + 'NetworkController:stateChange', + ], + }); + + return transactionControllerMessenger; + }, +}; diff --git a/packages/wallet/src/initialization/types.ts b/packages/wallet/src/initialization/types.ts index 07a07f6a581..a04826d9765 100644 --- a/packages/wallet/src/initialization/types.ts +++ b/packages/wallet/src/initialization/types.ts @@ -1,4 +1,4 @@ -import { RootMessenger } from '../types'; +import { RootMessenger, WalletOptions } from '../types'; export type InstanceState = Instance extends { state: unknown } ? Instance['state'] @@ -7,6 +7,7 @@ export type InstanceState = Instance extends { state: unknown } export type InitFunctionArguments = { state: InstanceState; messenger: InstanceMessenger; + options: WalletOptions; }; export type InitFunction = ( diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 2a6cc3f26df..865163da5da 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -1,3 +1,16 @@ -import { Messenger } from '@metamask/messenger'; +import { + ActionConstraint, + EventConstraint, + Messenger, +} from '@metamask/messenger'; -export type RootMessenger = Messenger<'Root'>; +export type RootMessenger< + AllowedActions extends ActionConstraint = ActionConstraint, + AllowedEvents extends EventConstraint = EventConstraint, +> = Messenger<'Root', AllowedActions, AllowedEvents>; + +export type WalletOptions = { + infuraProjectId: string; + clientVersion: string; + showApprovalRequest: () => void; +}; diff --git a/packages/wallet/src/utilities.ts b/packages/wallet/src/utilities.ts index 45a480171ef..3cd4c624d0f 100644 --- a/packages/wallet/src/utilities.ts +++ b/packages/wallet/src/utilities.ts @@ -1,5 +1,9 @@ // TODO: Determine if these should be available directly on Wallet. import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; +import { + AddTransactionOptions, + TransactionParams, +} from '@metamask/transaction-controller'; import { Wallet } from './Wallet'; @@ -15,15 +19,56 @@ export async function importSecretRecoveryPhrase( password: string, phrase: string, ) { - const { messenger } = wallet; - const indices = phrase.split(' ').map((word) => wordlist.indexOf(word)); const mnemonic = new Uint8Array(new Uint16Array(indices).buffer); // TODO: This should use the new MultichainAccountService. - await messenger.call( + await wallet.messenger.call( 'KeyringController:createNewVaultAndRestore', password, mnemonic, ); } + +/** + * Initialize the wallet object with a randomly generated secret recovery phrase. + * + * @param wallet - The wallet object. + * @param password - The password to the MetaMask wallet (not the SRP). + */ +export async function createSecretRecoveryPhrase( + wallet: Wallet, + password: string, +) { + // TODO: This should use the new MultichainAccountService. + await wallet.messenger.call( + 'KeyringController:createNewVaultAndKeychain', + password, + ); +} + +/** + * Sign a transaction using the wallet and submit it to the blockchain. + * + * @param wallet - The wallet object. + * @param transaction - The transaction. + * @param options - The transaction options (including which network to use). + * @returns The result. + */ +export async function sendTransaction( + wallet: Wallet, + transaction: TransactionParams, + options: AddTransactionOptions, +) { + const { transactionMeta, result } = await wallet.messenger.call( + 'TransactionController:addTransaction', + transaction, + options, + ); + + const approvalId = transactionMeta.id; + + await wallet.messenger.call('ApprovalController:acceptRequest', approvalId); + + return { transactionMeta, result }; +} diff --git a/yarn.lock b/yarn.lock index 2eb72eda13a..bc3452ae7ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5702,6 +5702,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/browser-passworder": "npm:^6.0.0" + "@metamask/controller-utils": "npm:^11.20.0" "@metamask/keyring-controller": "npm:^25.2.0" "@metamask/messenger": "npm:^1.1.1" "@metamask/scure-bip39": "npm:^2.1.1" From 38d0877d5156015878c3c7c8a334a53254633e40 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 8 Apr 2026 16:51:20 +0200 Subject: [PATCH 06/11] Run tests on Sepolia --- packages/wallet/src/Wallet.test.ts | 18 ++++++++++++++---- .../instances/transaction-controller.ts | 5 +++++ packages/wallet/src/utilities.ts | 2 +- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index 05a55a2b1d8..b73b5ec7238 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -44,13 +44,23 @@ describe('Wallet', () => { const { result, transactionMeta } = await sendTransaction( wallet, { from: addresses[0], to: addresses[0], data: '0x00' }, - { networkClientId: 'mainnet' }, + { networkClientId: 'sepolia' }, ); - await result; + const hash = await result; - expect(result).toStrictEqual({}); - expect(transactionMeta).toStrictEqual({}); + expect(hash).toStrictEqual(expect.any(String)); + expect(transactionMeta).toStrictEqual( + expect.objectContaining({ + txParams: expect.objectContaining({ + from: addresses[0], + to: addresses[0], + data: '0x00', + value: '0x0', + type: '0x2', + }), + }), + ); }); it('exposes state', async () => { diff --git a/packages/wallet/src/initialization/instances/transaction-controller.ts b/packages/wallet/src/initialization/instances/transaction-controller.ts index e5ad99f1cd5..c2b89afce53 100644 --- a/packages/wallet/src/initialization/instances/transaction-controller.ts +++ b/packages/wallet/src/initialization/instances/transaction-controller.ts @@ -32,6 +32,10 @@ export const transactionController: InitializationConfiguration< messenger, 'NetworkController:getEIP1559Compatibility', ), + getNetworkState: messenger.call.bind( + messenger, + 'NetworkController:getState', + ), sign: messenger.call.bind(messenger, 'KeyringController:signTransaction'), }); @@ -63,6 +67,7 @@ export const transactionController: InitializationConfiguration< // TODO: These are added for use in the constructor, in the extension this uses the "init messenger" concept. 'NetworkController:getNetworkClientRegistry', 'NetworkController:getEIP1559Compatibility', + 'NetworkController:getState', 'KeyringController:signTransaction', ], events: [ diff --git a/packages/wallet/src/utilities.ts b/packages/wallet/src/utilities.ts index 3cd4c624d0f..936d01e5965 100644 --- a/packages/wallet/src/utilities.ts +++ b/packages/wallet/src/utilities.ts @@ -49,7 +49,7 @@ export async function createSecretRecoveryPhrase( /** * Sign a transaction using the wallet and submit it to the blockchain. - * + * * @param wallet - The wallet object. * @param transaction - The transaction. * @param options - The transaction options (including which network to use). From 0c6a18af411478506f8d8fb9e60d9c52e82e2ca5 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 14 Apr 2026 03:50:05 -0700 Subject: [PATCH 07/11] fix(wallet): Fix builds and most lint issues (#8420) Builds and tests pass. All lint issues are fixed except a handful that are deferred pending future changes. --- > [!NOTE] > **Medium Risk** > Moderate risk due to new controller dependencies and changes to initialization wiring/typing that could affect runtime messaging and controller lifecycle. Test behavior now depends on external `INFURA_PROJECT_KEY` and mocked timers, which may introduce CI/environment sensitivity. > > **Overview** > **Stabilizes wallet builds/tests by wiring in missing controllers and config.** The wallet package now depends on additional controllers (accounts/approval/connectivity/network/remote feature flags/transaction) and updates TS project references accordingly. > > **Improves runtime/test ergonomics.** Jest loads a local `.env` (with `.env.example` added and `.env` gitignored), `Wallet` exposes stronger typed `messenger`/`state` and adds `destroy()` to clean up controller instances; tests are updated to require `INFURA_PROJECT_KEY`, use fake timers, and properly teardown the wallet. > > **Tightens initialization typing and controller wiring.** Adds `initialization/defaults.ts` for inferred `DefaultInstances`/`DefaultActions`/`DefaultEvents`, introduces `bindMessengerAction` to preserve action typings, and updates controller initializers (notably `TransactionController` and `RemoteFeatureFlagController`) to pass required options and bind messenger actions safely. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a6529337b2daf27d06b4a9e4645ab88939576880. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Frederik Bolding --- .gitignore | 7 ++- packages/wallet/.env.example | 2 + packages/wallet/jest.config.js | 3 + packages/wallet/package.json | 10 +++- packages/wallet/src/Wallet.test.ts | 52 ++++++++++++++--- packages/wallet/src/Wallet.ts | 35 +++++++++--- .../wallet/src/initialization/defaults.ts | 50 ++++++++++++++++ packages/wallet/src/initialization/index.ts | 7 +++ .../src/initialization/initialization.ts | 13 +++-- .../instances/keyring-controller.ts | 36 +++++++----- .../instances/network-controller.ts | 57 ++++++++++--------- .../remote-feature-flag-controller.ts | 2 + .../instances/transaction-controller.ts | 52 ++++++++++++++--- packages/wallet/src/initialization/types.ts | 55 ++++++++++++++---- packages/wallet/src/types.ts | 13 +---- packages/wallet/src/utilities.ts | 9 +-- packages/wallet/test/setup.ts | 4 ++ packages/wallet/tsconfig.build.json | 23 +++++++- packages/wallet/tsconfig.json | 25 +++++++- yarn.lock | 36 ++++++------ 20 files changed, 371 insertions(+), 120 deletions(-) create mode 100644 packages/wallet/.env.example create mode 100644 packages/wallet/src/initialization/defaults.ts create mode 100644 packages/wallet/test/setup.ts diff --git a/.gitignore b/.gitignore index 2f1de082398..78e1324684b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,9 @@ scripts/coverage packages/*/*.tsbuildinfo # AI -.sisyphus/ \ No newline at end of file +.sisyphus/ + +# Wallet +.claude/ +.env +!.env.example diff --git a/packages/wallet/.env.example b/packages/wallet/.env.example new file mode 100644 index 00000000000..ba9556adb02 --- /dev/null +++ b/packages/wallet/.env.example @@ -0,0 +1,2 @@ +INFURA_PROJECT_KEY= + diff --git a/packages/wallet/jest.config.js b/packages/wallet/jest.config.js index ca084133399..e0b6d9792e8 100644 --- a/packages/wallet/jest.config.js +++ b/packages/wallet/jest.config.js @@ -14,6 +14,9 @@ module.exports = merge(baseConfig, { // The display name when running multiple projects displayName, + // Load dotenv before tests + setupFiles: [path.resolve(__dirname, 'test/setup.ts')], + // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 0ad514fb7c7..3f11bbed70b 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -49,19 +49,27 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { + "@metamask/accounts-controller": "^37.2.0", + "@metamask/approval-controller": "^9.0.1", "@metamask/browser-passworder": "^6.0.0", + "@metamask/connectivity-controller": "^0.2.0", "@metamask/controller-utils": "^11.20.0", "@metamask/keyring-controller": "^25.2.0", "@metamask/messenger": "^1.1.1", + "@metamask/network-controller": "^30.0.1", + "@metamask/remote-feature-flag-controller": "^4.2.0", "@metamask/scure-bip39": "^2.1.1", - "@metamask/utils": "^11.11.0" + "@metamask/transaction-controller": "^64.0.0", + "@metamask/utils": "^11.9.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^29.5.14", "deepmerge": "^4.2.2", + "dotenv": "^16.4.7", "jest": "^29.7.0", + "nock": "^13.3.1", "ts-jest": "^29.2.5", "tsx": "^4.20.5", "typedoc": "^0.25.13", diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index b73b5ec7238..ba8387ed270 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -1,3 +1,9 @@ +import { + ClientConfigApiService, + ClientType, + DistributionType, + EnvironmentType, +} from '@metamask/remote-feature-flag-controller'; import { enableNetConnect } from 'nock'; import { importSecretRecoveryPhrase, sendTransaction } from './utilities'; @@ -7,12 +13,27 @@ const TEST_PHRASE = 'test test test test test test test test test test test ball'; const TEST_PASSWORD = 'testpass'; -async function setupWallet() { +async function setupWallet(): Promise { + if (!process.env.INFURA_PROJECT_KEY) { + throw new Error( + 'INFURA_PROJECT_KEY is not set. Copy .env.example to .env and fill in your key.', + ); + } + const wallet = new Wallet({ options: { - infuraProjectId: 'infura-project-id', + infuraProjectId: process.env.INFURA_PROJECT_KEY, clientVersion: '1.0.0', - showApprovalRequest: () => undefined, + showApprovalRequest: (): undefined => undefined, + clientConfigApiService: new ClientConfigApiService({ + fetch: globalThis.fetch, + config: { + client: ClientType.Extension, + distribution: DistributionType.Main, + environment: EnvironmentType.Production, + }, + }), + getMetaMetricsId: (): string => 'fake-metrics-id', }, }); @@ -22,8 +43,21 @@ async function setupWallet() { } describe('Wallet', () => { + let wallet: Wallet; + + beforeEach(() => { + jest.useFakeTimers({ doNotFake: ['nextTick', 'queueMicrotask'] }); + }); + + afterEach(async () => { + await wallet?.destroy(); + enableNetConnect(); + jest.useRealTimers(); + }); + it('can unlock and populate accounts', async () => { - const { messenger } = await setupWallet(); + wallet = await setupWallet(); + const { messenger } = wallet; expect( messenger @@ -35,7 +69,7 @@ describe('Wallet', () => { it('signs transactions', async () => { enableNetConnect(); - const wallet = await setupWallet(); + wallet = await setupWallet(); const addresses = wallet.messenger .call('AccountsController:listAccounts') @@ -47,7 +81,8 @@ describe('Wallet', () => { { networkClientId: 'sepolia' }, ); - const hash = await result; + // Advance timers by an arbitrary value to trigger downstream timer logic. + const hash = await jest.advanceTimersByTimeAsync(60_000).then(() => result); expect(hash).toStrictEqual(expect.any(String)); expect(transactionMeta).toStrictEqual( @@ -61,10 +96,11 @@ describe('Wallet', () => { }), }), ); - }); + }, 10_000); it('exposes state', async () => { - const { state } = await setupWallet(); + wallet = await setupWallet(); + const { state } = wallet; expect(state.KeyringController).toStrictEqual({ isUnlocked: true, diff --git a/packages/wallet/src/Wallet.ts b/packages/wallet/src/Wallet.ts index 3b378f81803..69eb108f04e 100644 --- a/packages/wallet/src/Wallet.ts +++ b/packages/wallet/src/Wallet.ts @@ -1,8 +1,15 @@ import { Messenger } from '@metamask/messenger'; import type { Json } from '@metamask/utils'; +import type { + DefaultActions, + DefaultEvents, + DefaultInstances, + DefaultState, + RootMessenger, +} from './initialization'; import { initialize } from './initialization'; -import { RootMessenger, WalletOptions } from './types'; +import type { WalletOptions } from './types'; export type WalletConstructorArgs = { state?: Record; @@ -10,9 +17,10 @@ export type WalletConstructorArgs = { }; export class Wallet { - public messenger: RootMessenger; + // TODO: Expand types when passing additionalConfigurations. + public readonly messenger: RootMessenger; - readonly #instances; + readonly #instances: DefaultInstances; constructor({ state = {}, options }: WalletConstructorArgs) { this.messenger = new Messenger({ @@ -22,13 +30,26 @@ export class Wallet { this.#instances = initialize({ state, messenger: this.messenger, options }); } - get state(): Record { + get state(): DefaultState { return Object.entries(this.#instances).reduce>( - (accumulator, [key, instance]) => { - accumulator[key] = instance.state ?? null; - return accumulator; + (totalState, [name, instance]) => { + totalState[name] = instance.state ?? null; + return totalState; }, {}, + ) as DefaultState; + } + + async destroy(): Promise { + await Promise.all( + Object.values(this.#instances).map((instance) => { + // @ts-expect-error Accessing protected property. + if (typeof instance.destroy === 'function') { + // @ts-expect-error Accessing protected property. + return instance.destroy(); + } + return undefined; + }), ); } } diff --git a/packages/wallet/src/initialization/defaults.ts b/packages/wallet/src/initialization/defaults.ts new file mode 100644 index 00000000000..b3b8a463553 --- /dev/null +++ b/packages/wallet/src/initialization/defaults.ts @@ -0,0 +1,50 @@ +import type { + ActionConstraint, + EventConstraint, + Messenger, + MessengerActions, + MessengerEvents, +} from '@metamask/messenger'; + +import * as defaultConfigurations from './instances'; +import type { InitializationConfiguration, InstanceState } from './types'; + +export { defaultConfigurations }; + +type ExtractInstance = + Config extends InitializationConfiguration + ? Instance + : never; + +type ExtractInstanceMessenger = + Config extends InitializationConfiguration + ? InferredMessenger + : never; + +type ExtractName = + ExtractInstance extends { name: infer Name extends string } + ? Name + : never; + +type Configs = typeof defaultConfigurations; + +type AllMessengers = ExtractInstanceMessenger; + +export type DefaultInstances = { + [Key in keyof Configs as ExtractName]: ExtractInstance< + Configs[Key] + >; +}; + +export type DefaultActions = MessengerActions; + +export type DefaultEvents = MessengerEvents; + +export type RootMessenger< + AllowedActions extends ActionConstraint = ActionConstraint, + AllowedEvents extends EventConstraint = EventConstraint, +> = Messenger<'Root', AllowedActions, AllowedEvents>; + +export type DefaultState = { + [Key in keyof DefaultInstances]: InstanceState; +}; diff --git a/packages/wallet/src/initialization/index.ts b/packages/wallet/src/initialization/index.ts index 5f1b4048d67..5d17e1a18fa 100644 --- a/packages/wallet/src/initialization/index.ts +++ b/packages/wallet/src/initialization/index.ts @@ -1 +1,8 @@ +export type { + DefaultActions, + DefaultEvents, + DefaultInstances, + DefaultState, + RootMessenger, +} from './defaults'; export { initialize } from './initialization'; diff --git a/packages/wallet/src/initialization/initialization.ts b/packages/wallet/src/initialization/initialization.ts index abcf6034ef8..22fd3bbc8dd 100644 --- a/packages/wallet/src/initialization/initialization.ts +++ b/packages/wallet/src/initialization/initialization.ts @@ -1,8 +1,9 @@ import { Json } from '@metamask/utils'; -import * as defaultConfigurations from './instances'; +import type { DefaultInstances } from './defaults'; +import { defaultConfigurations, RootMessenger } from './defaults'; import { InitializationConfiguration } from './types'; -import { RootMessenger, WalletOptions } from '../types'; +import { WalletOptions } from '../types'; export type InitializeArgs = { state: Record; @@ -19,7 +20,7 @@ export function initialize({ messenger, initializationConfigurations = [], options, -}: InitializeArgs) { +}: InitializeArgs): DefaultInstances { const overriddenConfiguration = initializationConfigurations.map( (config) => config.name, ); @@ -30,7 +31,7 @@ export function initialize({ ), ); - const instances = {}; + const instances: Record = {}; for (const config of configurationEntries) { const { name } = config; @@ -45,8 +46,8 @@ export function initialize({ options, }); - instances[name] = instance; + instances[name] = instance as Record; } - return instances; + return instances as DefaultInstances; } diff --git a/packages/wallet/src/initialization/instances/keyring-controller.ts b/packages/wallet/src/initialization/instances/keyring-controller.ts index 8348de7e1be..7f3a356864c 100644 --- a/packages/wallet/src/initialization/instances/keyring-controller.ts +++ b/packages/wallet/src/initialization/instances/keyring-controller.ts @@ -1,3 +1,8 @@ +import type { + DetailedEncryptionResult, + EncryptionKey, + KeyDerivationOptions, +} from '@metamask/browser-passworder'; import { encrypt, encryptWithDetail, @@ -10,9 +15,8 @@ import { importKey, exportKey, generateSalt, - EncryptionKey, - KeyDerivationOptions, } from '@metamask/browser-passworder'; +import type { Encryptor } from '@metamask/keyring-controller'; import { KeyringController, KeyringControllerMessenger, @@ -35,7 +39,7 @@ const encryptFactory = data: unknown, key?: EncryptionKey | CryptoKey, salt?: string, - ) => + ): Promise => encrypt(password, data, key, salt, { algorithm: 'PBKDF2', params: { @@ -52,7 +56,11 @@ const encryptFactory = */ const encryptWithDetailFactory = (iterations: number) => - async (password: string, object: unknown, salt?: string) => + async ( + password: string, + object: unknown, + salt?: string, + ): Promise => encryptWithDetail(password, object, salt, { algorithm: 'PBKDF2', params: { @@ -77,7 +85,7 @@ const keyFromPasswordFactory = salt: string, exportable?: boolean, opts?: KeyDerivationOptions, - ) => + ): Promise => keyFromPassword( password, salt, @@ -97,13 +105,15 @@ const keyFromPasswordFactory = * @param iterations - The number of iterations to use for the PBKDF2 algorithm. * @returns A function that checks if the vault was encrypted with the given number of iterations. */ -const isVaultUpdatedFactory = (iterations: number) => (vault: string) => - isVaultUpdated(vault, { - algorithm: 'PBKDF2', - params: { - iterations, - }, - }); +const isVaultUpdatedFactory = + (iterations: number) => + (vault: string): boolean => + isVaultUpdated(vault, { + algorithm: 'PBKDF2', + params: { + iterations, + }, + }); /** * A factory function that returns an encryptor with the given number of iterations. @@ -114,7 +124,7 @@ const isVaultUpdatedFactory = (iterations: number) => (vault: string) => * @param iterations - The number of iterations to use for the PBKDF2 algorithm. * @returns An encryptor set with the given number of iterations. */ -const encryptorFactory = (iterations: number) => ({ +const encryptorFactory = (iterations: number): Encryptor => ({ encrypt: encryptFactory(iterations), encryptWithKey, encryptWithDetail: encryptWithDetailFactory(iterations), diff --git a/packages/wallet/src/initialization/instances/network-controller.ts b/packages/wallet/src/initialization/instances/network-controller.ts index 47d762955a7..aa23185d83c 100644 --- a/packages/wallet/src/initialization/instances/network-controller.ts +++ b/packages/wallet/src/initialization/instances/network-controller.ts @@ -5,6 +5,7 @@ import { MessengerActions, MessengerEvents, } from '@metamask/messenger'; +import type { NetworkControllerOptions } from '@metamask/network-controller'; import { NetworkController, NetworkControllerMessenger, @@ -24,36 +25,38 @@ export const networkController: InitializationConfiguration< name: 'NetworkController', init: ({ state, messenger, options }) => { // TODO: This was gutted to simplify implementation for now. - const getRpcServiceOptions = () => { - const maxRetries = DEFAULT_MAX_RETRIES; + const getRpcServiceOptions: NetworkControllerOptions['getRpcServiceOptions'] = + () => { + const maxRetries = DEFAULT_MAX_RETRIES; - const isOffline = (): boolean => { - const connectivityState = messenger.call( - 'ConnectivityController:getState', - ); - return ( - connectivityState.connectivityStatus === CONNECTIVITY_STATUSES.Offline - ); - }; + const isOffline = (): boolean => { + const connectivityState = messenger.call( + 'ConnectivityController:getState', + ); + return ( + connectivityState.connectivityStatus === + CONNECTIVITY_STATUSES.Offline + ); + }; - return { - fetch: globalThis.fetch.bind(globalThis), - btoa: globalThis.btoa.bind(globalThis), - isOffline, - policyOptions: { - // Ensure that the "cooldown" period after breaking the circuit is short. - circuitBreakDuration: inMilliseconds(30, Duration.Second), - maxRetries, - // Ensure that if the endpoint continually responds with errors, we - // break the circuit relatively fast (but not prematurely). - // - // Note that the circuit will break much faster if the errors are - // retriable (e.g. 503) than if not (e.g. 500), so we attempt to strike - // a balance here. - maxConsecutiveFailures: (maxRetries + 1) * 3, - }, + return { + fetch: globalThis.fetch.bind(globalThis), + btoa: globalThis.btoa.bind(globalThis), + isOffline, + policyOptions: { + // Ensure that the "cooldown" period after breaking the circuit is short. + circuitBreakDuration: inMilliseconds(30, Duration.Second), + maxRetries, + // Ensure that if the endpoint continually responds with errors, we + // break the circuit relatively fast (but not prematurely). + // + // Note that the circuit will break much faster if the errors are + // retriable (e.g. 503) than if not (e.g. 500), so we attempt to strike + // a balance here. + maxConsecutiveFailures: (maxRetries + 1) * 3, + }, + }; }; - }; // TODO: Add the rest of the arguments. const instance = new NetworkController({ diff --git a/packages/wallet/src/initialization/instances/remote-feature-flag-controller.ts b/packages/wallet/src/initialization/instances/remote-feature-flag-controller.ts index 56885f4466a..4b5aaf3ca76 100644 --- a/packages/wallet/src/initialization/instances/remote-feature-flag-controller.ts +++ b/packages/wallet/src/initialization/instances/remote-feature-flag-controller.ts @@ -17,6 +17,8 @@ export const remoteFeatureFlagController: InitializationConfiguration< state, messenger, clientVersion: options.clientVersion, + clientConfigApiService: options.clientConfigApiService, + getMetaMetricsId: options.getMetaMetricsId, }); return { diff --git a/packages/wallet/src/initialization/instances/transaction-controller.ts b/packages/wallet/src/initialization/instances/transaction-controller.ts index c2b89afce53..3768e5d195c 100644 --- a/packages/wallet/src/initialization/instances/transaction-controller.ts +++ b/packages/wallet/src/initialization/instances/transaction-controller.ts @@ -1,42 +1,76 @@ +import type { KeyringControllerSignTransactionAction } from '@metamask/keyring-controller'; import { Messenger, MessengerActions, MessengerEvents, } from '@metamask/messenger'; +import type { + NetworkControllerGetEIP1559CompatibilityAction, + NetworkControllerGetNetworkClientRegistryAction, + NetworkControllerGetStateAction, +} from '@metamask/network-controller'; +import type { TransactionControllerOptions } from '@metamask/transaction-controller'; import { TransactionController, TransactionControllerMessenger, } from '@metamask/transaction-controller'; -import { InitializationConfiguration } from '../types'; +import { bindMessengerAction, InitializationConfiguration } from '../types'; -type AllowedActions = MessengerActions; +type InitActions = + | NetworkControllerGetNetworkClientRegistryAction + | NetworkControllerGetEIP1559CompatibilityAction + | NetworkControllerGetStateAction + | KeyringControllerSignTransactionAction; + +type AllowedActions = + | MessengerActions + | InitActions; type AllowedEvents = MessengerEvents; +type WalletTransactionControllerMessenger = Messenger< + 'TransactionController', + AllowedActions, + AllowedEvents +>; + export const transactionController: InitializationConfiguration< TransactionController, - TransactionControllerMessenger + WalletTransactionControllerMessenger > = { name: 'TransactionController', init: ({ state, messenger }) => { // TODO: Add the rest of the arguments. const instance = new TransactionController({ state, - messenger, - getNetworkClientRegistry: messenger.call.bind( + messenger: messenger as unknown as TransactionControllerMessenger, + disableHistory: true, + disableSendFlowHistory: true, + disableSwaps: false, + hooks: {}, + getNetworkClientRegistry: bindMessengerAction( messenger, 'NetworkController:getNetworkClientRegistry', ), - getCurrentNetworkEIP1559Compatibility: messenger.call.bind( + getCurrentNetworkEIP1559Compatibility: bindMessengerAction( messenger, 'NetworkController:getEIP1559Compatibility', - ), - getNetworkState: messenger.call.bind( + ) as () => Promise, + getNetworkState: bindMessengerAction( messenger, 'NetworkController:getState', ), - sign: messenger.call.bind(messenger, 'KeyringController:signTransaction'), + // KeyringController.signTransaction is typed as returning + // Promise (a plain data object), but the actual keyring + // implementations return the full TypedTransaction class instance. + // TransactionController expects Promise here. The + // cast bridges a stale return-type declaration in KeyringController, + // not a real runtime mismatch. + sign: bindMessengerAction( + messenger, + 'KeyringController:signTransaction', + ) as unknown as TransactionControllerOptions['sign'], }); return { diff --git a/packages/wallet/src/initialization/types.ts b/packages/wallet/src/initialization/types.ts index a04826d9765..24a88988f6d 100644 --- a/packages/wallet/src/initialization/types.ts +++ b/packages/wallet/src/initialization/types.ts @@ -1,8 +1,18 @@ -import { RootMessenger, WalletOptions } from '../types'; +import type { + ActionConstraint, + EventConstraint, + ExtractActionParameters, + ExtractActionResponse, + Messenger, + MessengerActions, +} from '@metamask/messenger'; + +import type { RootMessenger } from './defaults'; +import type { WalletOptions } from '../types'; export type InstanceState = Instance extends { state: unknown } ? Instance['state'] - : null; + : unknown; export type InitFunctionArguments = { state: InstanceState; @@ -10,16 +20,39 @@ export type InitFunctionArguments = { options: WalletOptions; }; -export type InitFunction = ( - args: InitFunctionArguments, -) => { instance: Instance }; - -export type MessengerInitFunction = ( - parent: RootMessenger, -) => NarrowedMessenger; +/** + * Typed wrapper around `messenger.call.bind(messenger, actionType)`. + * + * TypeScript's `Function.prototype.bind` loses generic inference on + * `Messenger.call`, so the bound function's parameters and return type + * collapse to a union of every action. This helper restores the correct + * per-action types via an explicit cast that is safe because `bind` + * preserves the runtime behavior exactly. + * + * @param messenger - The messenger instance. + * @param actionType - The action to bind. + * @returns A function that calls the action with the correct types. + */ +export function bindMessengerAction< + Msgr extends Messenger, + ActionType extends MessengerActions['type'], +>( + messenger: Msgr, + actionType: ActionType, +): ( + ...args: ExtractActionParameters, ActionType> +) => ExtractActionResponse, ActionType> { + return messenger.call.bind(messenger, actionType) as ( + ...args: ExtractActionParameters, ActionType> + ) => ExtractActionResponse, ActionType>; +} export type InitializationConfiguration = { name: string; - init: InitFunction; - messenger: MessengerInitFunction; + // This is a method as opposed to function property in order to collect + // heterogeneous InitializationConfiguration values in a single array. + init(args: InitFunctionArguments): { + instance: Instance; + }; + messenger(parent: RootMessenger): InstanceMessenger; }; diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 865163da5da..e808637e474 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -1,16 +1,9 @@ -import { - ActionConstraint, - EventConstraint, - Messenger, -} from '@metamask/messenger'; - -export type RootMessenger< - AllowedActions extends ActionConstraint = ActionConstraint, - AllowedEvents extends EventConstraint = EventConstraint, -> = Messenger<'Root', AllowedActions, AllowedEvents>; +import type { ClientConfigApiService } from '@metamask/remote-feature-flag-controller'; export type WalletOptions = { infuraProjectId: string; clientVersion: string; showApprovalRequest: () => void; + clientConfigApiService: ClientConfigApiService; + getMetaMetricsId: () => string; }; diff --git a/packages/wallet/src/utilities.ts b/packages/wallet/src/utilities.ts index 936d01e5965..edd281f968b 100644 --- a/packages/wallet/src/utilities.ts +++ b/packages/wallet/src/utilities.ts @@ -1,7 +1,8 @@ // TODO: Determine if these should be available directly on Wallet. import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; -import { +import type { AddTransactionOptions, + TransactionMeta, TransactionParams, } from '@metamask/transaction-controller'; @@ -18,7 +19,7 @@ export async function importSecretRecoveryPhrase( wallet: Wallet, password: string, phrase: string, -) { +): Promise { const indices = phrase.split(' ').map((word) => wordlist.indexOf(word)); const mnemonic = new Uint8Array(new Uint16Array(indices).buffer); @@ -39,7 +40,7 @@ export async function importSecretRecoveryPhrase( export async function createSecretRecoveryPhrase( wallet: Wallet, password: string, -) { +): Promise { // TODO: This should use the new MultichainAccountService. await wallet.messenger.call( 'KeyringController:createNewVaultAndKeychain', @@ -59,7 +60,7 @@ export async function sendTransaction( wallet: Wallet, transaction: TransactionParams, options: AddTransactionOptions, -) { +): Promise<{ transactionMeta: TransactionMeta; result: Promise }> { const { transactionMeta, result } = await wallet.messenger.call( 'TransactionController:addTransaction', transaction, diff --git a/packages/wallet/test/setup.ts b/packages/wallet/test/setup.ts new file mode 100644 index 00000000000..192571b40bc --- /dev/null +++ b/packages/wallet/test/setup.ts @@ -0,0 +1,4 @@ +import { config } from 'dotenv'; +import path from 'path'; + +config({ path: path.resolve(__dirname, '../.env') }); diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json index 2657e084ed0..a5e012287d5 100644 --- a/packages/wallet/tsconfig.build.json +++ b/packages/wallet/tsconfig.build.json @@ -7,10 +7,31 @@ }, "references": [ { - "path": "../messenger/tsconfig.build.json" + "path": "../accounts-controller/tsconfig.build.json" + }, + { + "path": "../approval-controller/tsconfig.build.json" + }, + { + "path": "../connectivity-controller/tsconfig.build.json" + }, + { + "path": "../controller-utils/tsconfig.build.json" }, { "path": "../keyring-controller/tsconfig.build.json" + }, + { + "path": "../messenger/tsconfig.build.json" + }, + { + "path": "../network-controller/tsconfig.build.json" + }, + { + "path": "../remote-feature-flag-controller/tsconfig.build.json" + }, + { + "path": "../transaction-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] diff --git a/packages/wallet/tsconfig.json b/packages/wallet/tsconfig.json index cdbf9854ac4..8f0b0c57883 100644 --- a/packages/wallet/tsconfig.json +++ b/packages/wallet/tsconfig.json @@ -5,11 +5,32 @@ }, "references": [ { - "path": "../messenger/tsconfig.json" + "path": "../accounts-controller/tsconfig.json" + }, + { + "path": "../approval-controller/tsconfig.json" + }, + { + "path": "../connectivity-controller/tsconfig.json" + }, + { + "path": "../controller-utils/tsconfig.json" }, { "path": "../keyring-controller/tsconfig.json" + }, + { + "path": "../messenger/tsconfig.json" + }, + { + "path": "../network-controller/tsconfig.json" + }, + { + "path": "../remote-feature-flag-controller/tsconfig.json" + }, + { + "path": "../transaction-controller/tsconfig.json" } ], - "include": ["../../types", "./src"] + "include": ["../../types", "./src", "./test"] } diff --git a/yarn.lock b/yarn.lock index bc3452ae7ae..60655666277 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5660,25 +5660,6 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^11.11.0": - version: 11.11.0 - resolution: "@metamask/utils@npm:11.11.0" - dependencies: - "@ethereumjs/tx": "npm:^4.2.0" - "@metamask/superstruct": "npm:^3.1.0" - "@noble/hashes": "npm:^1.3.1" - "@scure/base": "npm:^1.1.3" - "@types/debug": "npm:^4.1.7" - "@types/lodash": "npm:^4.17.20" - debug: "npm:^4.3.4" - lodash: "npm:^4.17.21" - pony-cause: "npm:^2.1.10" - semver: "npm:^7.5.4" - uuid: "npm:^9.0.1" - checksum: 10/c4381b9e451a9616bde84ac659bc0d1848ef06b6e605f877bfa065b78c8ed5015706683ea88a3387de5eaeb3a50d1af9af0994f04f9e06258d992598fe2be3bf - languageName: node - linkType: hard - "@metamask/utils@npm:^9.0.0": version: 9.3.0 resolution: "@metamask/utils@npm:9.3.0" @@ -5700,17 +5681,25 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/wallet@workspace:packages/wallet" dependencies: + "@metamask/accounts-controller": "npm:^37.2.0" + "@metamask/approval-controller": "npm:^9.0.1" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/browser-passworder": "npm:^6.0.0" + "@metamask/connectivity-controller": "npm:^0.2.0" "@metamask/controller-utils": "npm:^11.20.0" "@metamask/keyring-controller": "npm:^25.2.0" "@metamask/messenger": "npm:^1.1.1" + "@metamask/network-controller": "npm:^30.0.1" + "@metamask/remote-feature-flag-controller": "npm:^4.2.0" "@metamask/scure-bip39": "npm:^2.1.1" - "@metamask/utils": "npm:^11.11.0" + "@metamask/transaction-controller": "npm:^64.0.0" + "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" deepmerge: "npm:^4.2.2" + dotenv: "npm:^16.4.7" jest: "npm:^29.7.0" + nock: "npm:^13.3.1" ts-jest: "npm:^29.2.5" tsx: "npm:^4.20.5" typedoc: "npm:^0.25.13" @@ -8553,6 +8542,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.4.7": + version: 16.6.1 + resolution: "dotenv@npm:16.6.1" + checksum: 10/1d1897144344447ffe62aa1a6d664f4cd2e0784e0aff787eeeec1940ded32f8e4b5b506d665134fc87157baa086fce07ec6383970a2b6d2e7985beaed6a4cc14 + languageName: node + linkType: hard + "dunder-proto@npm:^1.0.1": version: 1.0.1 resolution: "dunder-proto@npm:1.0.1" From 2057e736db5c67d2e3b44d352b8abc7316abc330 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 15 Apr 2026 15:11:32 +0200 Subject: [PATCH 08/11] feat(wallet): Add MultichainAccountService --- packages/wallet/package.json | 2 + packages/wallet/src/Wallet.test.ts | 2 +- .../src/initialization/instances/index.ts | 2 + .../instances/multichain-account-service.ts | 61 +++++++++ .../instances/snap-controller.ts | 26 ++++ packages/wallet/src/utilities.ts | 11 +- yarn.lock | 128 +++++++++++++++++- 7 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 packages/wallet/src/initialization/instances/multichain-account-service.ts create mode 100644 packages/wallet/src/initialization/instances/snap-controller.ts diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 3f11bbed70b..fe14ae0c370 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -56,9 +56,11 @@ "@metamask/controller-utils": "^11.20.0", "@metamask/keyring-controller": "^25.2.0", "@metamask/messenger": "^1.1.1", + "@metamask/multichain-account-service": "workspace:^", "@metamask/network-controller": "^30.0.1", "@metamask/remote-feature-flag-controller": "^4.2.0", "@metamask/scure-bip39": "^2.1.1", + "@metamask/snaps-controllers": "^20.0.1", "@metamask/transaction-controller": "^64.0.0", "@metamask/utils": "^11.9.0" }, diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index ba8387ed270..4ae77050778 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -66,7 +66,7 @@ describe('Wallet', () => { ).toStrictEqual(['0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf']); }); - it('signs transactions', async () => { + it.skip('signs transactions', async () => { enableNetConnect(); wallet = await setupWallet(); diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts index 4f869053f11..84442bacb0a 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -5,3 +5,5 @@ export * from './keyring-controller'; export * from './network-controller'; export * from './remote-feature-flag-controller'; export * from './transaction-controller'; +export * from './snap-controller'; +export * from './multichain-account-service'; diff --git a/packages/wallet/src/initialization/instances/multichain-account-service.ts b/packages/wallet/src/initialization/instances/multichain-account-service.ts new file mode 100644 index 00000000000..bcb21a57072 --- /dev/null +++ b/packages/wallet/src/initialization/instances/multichain-account-service.ts @@ -0,0 +1,61 @@ +import { + MultichainAccountService, + SOL_ACCOUNT_PROVIDER_NAME, + TRX_ACCOUNT_PROVIDER_NAME, + BTC_ACCOUNT_PROVIDER_NAME, + MultichainAccountServiceMessenger, +} from '@metamask/multichain-account-service'; +import { Messenger } from '@metamask/messenger'; + +import { InitializationConfiguration } from '../types'; + + +export const multichainAccountService: InitializationConfiguration< + MultichainAccountService, + MultichainAccountServiceMessenger +> = { + name: 'MultichainAccountService', + init: ({ messenger }) => { + const instance = new MultichainAccountService({ + messenger, + }); + + return { + instance, + }; + }, + messenger: (parent) => { + const serviceMessenger: MultichainAccountServiceMessenger = new Messenger({ + namespace: 'MultichainAccountService', + parent, + }); + + parent.delegate({ + messenger: serviceMessenger, + events: [ + 'KeyringController:stateChange', + 'SnapController:stateChange', + 'AccountsController:accountAdded', + 'AccountsController:accountRemoved', + ], + actions: [ + 'AccountsController:listMultichainAccounts', + 'AccountsController:getAccountByAddress', + 'AccountsController:getAccount', + 'AccountsController:getAccounts', + 'SnapController:getState', + 'SnapController:handleRequest', + 'KeyringController:getState', + 'KeyringController:withKeyring', + 'KeyringController:addNewKeyring', + 'KeyringController:getKeyringsByType', + 'KeyringController:createNewVaultAndKeychain', + 'KeyringController:createNewVaultAndRestore', + 'NetworkController:getNetworkClientById', + 'NetworkController:findNetworkClientIdByChainId', + ], + }); + + return serviceMessenger; + } +}; diff --git a/packages/wallet/src/initialization/instances/snap-controller.ts b/packages/wallet/src/initialization/instances/snap-controller.ts new file mode 100644 index 00000000000..c0162f403a2 --- /dev/null +++ b/packages/wallet/src/initialization/instances/snap-controller.ts @@ -0,0 +1,26 @@ +import { Messenger } from '@metamask/messenger'; + +import { InitializationConfiguration } from '../types'; +import { SnapController, SnapControllerMessenger } from '@metamask/snaps-controllers'; + + +export const snapController: InitializationConfiguration< + SnapController, + SnapControllerMessenger +> = { + name: 'SnapController', + init: ({ messenger }) => { + const instance = new SnapController({ + messenger, + }); + + return { + instance, + }; + }, + messenger: (parent) => + new Messenger<'SnapController', never, never, typeof parent>({ + namespace: 'SnapController', + parent, + }), +}; diff --git a/packages/wallet/src/utilities.ts b/packages/wallet/src/utilities.ts index edd281f968b..b0ee261d729 100644 --- a/packages/wallet/src/utilities.ts +++ b/packages/wallet/src/utilities.ts @@ -23,11 +23,9 @@ export async function importSecretRecoveryPhrase( const indices = phrase.split(' ').map((word) => wordlist.indexOf(word)); const mnemonic = new Uint8Array(new Uint16Array(indices).buffer); - // TODO: This should use the new MultichainAccountService. await wallet.messenger.call( - 'KeyringController:createNewVaultAndRestore', - password, - mnemonic, + 'MultichainAccountService:createMultichainAccountWallet', + { type: 'restore', password, mnemonic } ); } @@ -41,10 +39,9 @@ export async function createSecretRecoveryPhrase( wallet: Wallet, password: string, ): Promise { - // TODO: This should use the new MultichainAccountService. await wallet.messenger.call( - 'KeyringController:createNewVaultAndKeychain', - password, + 'MultichainAccountService:createMultichainAccountWallet', + { type: 'create', password }, ); } diff --git a/yarn.lock b/yarn.lock index 60655666277..4f3d2bb917f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4429,7 +4429,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/multichain-account-service@npm:^8.0.1, @metamask/multichain-account-service@workspace:packages/multichain-account-service": +"@metamask/multichain-account-service@npm:^8.0.1, @metamask/multichain-account-service@workspace:^, @metamask/multichain-account-service@workspace:packages/multichain-account-service": version: 0.0.0-use.local resolution: "@metamask/multichain-account-service@workspace:packages/multichain-account-service" dependencies: @@ -5316,6 +5316,49 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-controllers@npm:^20.0.1": + version: 20.0.1 + resolution: "@metamask/snaps-controllers@npm:20.0.1" + dependencies: + "@metamask/approval-controller": "npm:^9.0.1" + "@metamask/base-controller": "npm:^9.0.1" + "@metamask/json-rpc-engine": "npm:^10.2.4" + "@metamask/json-rpc-middleware-stream": "npm:^8.0.8" + "@metamask/key-tree": "npm:^10.1.1" + "@metamask/messenger": "npm:^1.1.1" + "@metamask/object-multiplex": "npm:^2.1.0" + "@metamask/permission-controller": "npm:^12.3.0" + "@metamask/post-message-stream": "npm:^10.0.0" + "@metamask/rpc-errors": "npm:^7.0.3" + "@metamask/snaps-registry": "npm:^4.0.0" + "@metamask/snaps-rpc-methods": "npm:^15.1.1" + "@metamask/snaps-sdk": "npm:^11.1.0" + "@metamask/snaps-utils": "npm:^12.2.0" + "@metamask/storage-service": "npm:^1.0.1" + "@metamask/superstruct": "npm:^3.2.1" + "@metamask/utils": "npm:^11.11.0" + "@xstate/fsm": "npm:^2.0.0" + async-mutex: "npm:^0.5.0" + concat-stream: "npm:^2.0.0" + cron-parser: "npm:^4.5.0" + fast-deep-equal: "npm:^3.1.3" + get-npm-tarball-url: "npm:^2.0.3" + immer: "npm:^9.0.21" + luxon: "npm:^3.5.0" + nanoid: "npm:^3.3.10" + readable-stream: "npm:^3.6.2" + readable-web-to-node-stream: "npm:^3.0.2" + semver: "npm:^7.5.4" + tar-stream: "npm:^3.1.7" + peerDependencies: + "@metamask/snaps-execution-environments": ^11.0.2 + peerDependenciesMeta: + "@metamask/snaps-execution-environments": + optional: true + checksum: 10/ccacdffa8b630a777f7e95ad2a795a95663b0ae4da1fe78c35fa4b1e3590318687e5edb3cd769661e74fc03ee2e59bcd62098657e9b00163b216eb8424b8a135 + languageName: node + linkType: hard + "@metamask/snaps-registry@npm:^4.0.0": version: 4.0.0 resolution: "@metamask/snaps-registry@npm:4.0.0" @@ -5345,6 +5388,23 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-rpc-methods@npm:^15.1.1": + version: 15.1.1 + resolution: "@metamask/snaps-rpc-methods@npm:15.1.1" + dependencies: + "@metamask/key-tree": "npm:^10.1.1" + "@metamask/permission-controller": "npm:^12.3.0" + "@metamask/rpc-errors": "npm:^7.0.3" + "@metamask/snaps-sdk": "npm:^11.1.0" + "@metamask/snaps-utils": "npm:^12.2.0" + "@metamask/superstruct": "npm:^3.2.1" + "@metamask/utils": "npm:^11.11.0" + "@noble/hashes": "npm:^1.7.1" + async-mutex: "npm:^0.5.0" + checksum: 10/906eafa8a2d4944e50d73b8aa4d32a7070c33367ad3d58843e892e62b3bd8facb0a02e1d88a9245c18a281637435d97266212b7706ef67ee6a68db85a5991ea3 + languageName: node + linkType: hard + "@metamask/snaps-sdk@npm:^10.4.0": version: 10.4.0 resolution: "@metamask/snaps-sdk@npm:10.4.0" @@ -5373,6 +5433,20 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-sdk@npm:^11.1.0": + version: 11.1.0 + resolution: "@metamask/snaps-sdk@npm:11.1.0" + dependencies: + "@metamask/key-tree": "npm:^10.1.1" + "@metamask/providers": "npm:^22.1.1" + "@metamask/rpc-errors": "npm:^7.0.3" + "@metamask/superstruct": "npm:^3.2.1" + "@metamask/utils": "npm:^11.11.0" + luxon: "npm:^3.5.0" + checksum: 10/138c616584d537b9976ae48123090ab5731848d79d5d1f4e979c797dfdfe061329cbf18a5e84d8bd068fe36d5b9d169337f6d74efab0736f30c31ddf4088f70b + languageName: node + linkType: hard + "@metamask/snaps-utils@npm:^12.1.2": version: 12.1.2 resolution: "@metamask/snaps-utils@npm:12.1.2" @@ -5404,6 +5478,37 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-utils@npm:^12.2.0": + version: 12.2.0 + resolution: "@metamask/snaps-utils@npm:12.2.0" + dependencies: + "@babel/core": "npm:^7.23.2" + "@babel/types": "npm:^7.23.0" + "@metamask/key-tree": "npm:^10.1.1" + "@metamask/messenger": "npm:^1.1.1" + "@metamask/permission-controller": "npm:^12.3.0" + "@metamask/rpc-errors": "npm:^7.0.3" + "@metamask/slip44": "npm:^4.4.0" + "@metamask/snaps-registry": "npm:^4.0.0" + "@metamask/snaps-sdk": "npm:^11.1.0" + "@metamask/superstruct": "npm:^3.2.1" + "@metamask/utils": "npm:^11.11.0" + "@scure/base": "npm:^1.1.1" + chalk: "npm:^4.1.2" + cron-parser: "npm:^4.5.0" + fast-deep-equal: "npm:^3.1.3" + fast-json-stable-stringify: "npm:^2.1.0" + fast-xml-parser: "npm:^5.5.6" + luxon: "npm:^3.5.0" + marked: "npm:^12.0.1" + rfdc: "npm:^1.3.0" + semver: "npm:^7.5.4" + ses: "npm:^1.15.0" + validate-npm-package-name: "npm:^5.0.0" + checksum: 10/0e7cb5a4deebad3dc98404486b7767be049e9f86173195e9b1ae577f197e67cdd392f1e05ebb146a199ffc3cd52013636637a4d1ac7f9fec164f3189be97df55 + languageName: node + linkType: hard + "@metamask/social-controllers@workspace:packages/social-controllers": version: 0.0.0-use.local resolution: "@metamask/social-controllers@workspace:packages/social-controllers" @@ -5660,6 +5765,25 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^11.11.0": + version: 11.11.0 + resolution: "@metamask/utils@npm:11.11.0" + dependencies: + "@ethereumjs/tx": "npm:^4.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.3" + "@types/debug": "npm:^4.1.7" + "@types/lodash": "npm:^4.17.20" + debug: "npm:^4.3.4" + lodash: "npm:^4.17.21" + pony-cause: "npm:^2.1.10" + semver: "npm:^7.5.4" + uuid: "npm:^9.0.1" + checksum: 10/c4381b9e451a9616bde84ac659bc0d1848ef06b6e605f877bfa065b78c8ed5015706683ea88a3387de5eaeb3a50d1af9af0994f04f9e06258d992598fe2be3bf + languageName: node + linkType: hard + "@metamask/utils@npm:^9.0.0": version: 9.3.0 resolution: "@metamask/utils@npm:9.3.0" @@ -5689,9 +5813,11 @@ __metadata: "@metamask/controller-utils": "npm:^11.20.0" "@metamask/keyring-controller": "npm:^25.2.0" "@metamask/messenger": "npm:^1.1.1" + "@metamask/multichain-account-service": "workspace:^" "@metamask/network-controller": "npm:^30.0.1" "@metamask/remote-feature-flag-controller": "npm:^4.2.0" "@metamask/scure-bip39": "npm:^2.1.1" + "@metamask/snaps-controllers": "npm:^20.0.1" "@metamask/transaction-controller": "npm:^64.0.0" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" From 3458af7fa323c0302f56b5e7b56ac86f255ab277 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 15 Apr 2026 16:29:29 +0200 Subject: [PATCH 09/11] Partial SnapController implementation --- packages/wallet/package.json | 3 + packages/wallet/src/Wallet.test.ts | 1 + .../src/initialization/instances/index.ts | 3 +- .../instances/keyring-controller.ts | 144 ++++++++++++------ .../instances/multichain-account-service.ts | 35 ++++- .../instances/snap-controller.ts | 62 +++++++- .../instances/storage-service.ts | 24 +++ packages/wallet/src/types.ts | 1 + packages/wallet/src/utilities.ts | 8 +- packages/wallet/tsconfig.build.json | 3 + packages/wallet/tsconfig.json | 3 + yarn.lock | 24 +++ 12 files changed, 251 insertions(+), 60 deletions(-) create mode 100644 packages/wallet/src/initialization/instances/storage-service.ts diff --git a/packages/wallet/package.json b/packages/wallet/package.json index fe14ae0c370..94b59223e08 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -51,6 +51,7 @@ "dependencies": { "@metamask/accounts-controller": "^37.2.0", "@metamask/approval-controller": "^9.0.1", + "@metamask/bitcoin-wallet-snap": "^1.10.1", "@metamask/browser-passworder": "^6.0.0", "@metamask/connectivity-controller": "^0.2.0", "@metamask/controller-utils": "^11.20.0", @@ -61,7 +62,9 @@ "@metamask/remote-feature-flag-controller": "^4.2.0", "@metamask/scure-bip39": "^2.1.1", "@metamask/snaps-controllers": "^20.0.1", + "@metamask/solana-wallet-snap": "^2.8.0", "@metamask/transaction-controller": "^64.0.0", + "@metamask/tron-wallet-snap": "^1.25.2", "@metamask/utils": "^11.9.0" }, "devDependencies": { diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index 4ae77050778..f925f683fe6 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -34,6 +34,7 @@ async function setupWallet(): Promise { }, }), getMetaMetricsId: (): string => 'fake-metrics-id', + ensureOnboardingComplete: () => Promise.resolve(), }, }); diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts index 84442bacb0a..b780e254773 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -1,9 +1,10 @@ export * from './accounts-controller'; export * from './approval-controller'; export * from './connectivity-controller'; +export * from './storage-service'; +export * from './snap-controller'; export * from './keyring-controller'; export * from './network-controller'; export * from './remote-feature-flag-controller'; export * from './transaction-controller'; -export * from './snap-controller'; export * from './multichain-account-service'; diff --git a/packages/wallet/src/initialization/instances/keyring-controller.ts b/packages/wallet/src/initialization/instances/keyring-controller.ts index 7f3a356864c..89b16f8d8f4 100644 --- a/packages/wallet/src/initialization/instances/keyring-controller.ts +++ b/packages/wallet/src/initialization/instances/keyring-controller.ts @@ -20,8 +20,10 @@ import type { Encryptor } from '@metamask/keyring-controller'; import { KeyringController, KeyringControllerMessenger, + KeyringTypes, } from '@metamask/keyring-controller'; import { Messenger } from '@metamask/messenger'; +import { SnapKeyring } from '@metamask/eth-snap-keyring'; import { InitializationConfiguration } from '../types'; @@ -34,18 +36,18 @@ import { InitializationConfiguration } from '../types'; */ const encryptFactory = (iterations: number) => - async ( - password: string, - data: unknown, - key?: EncryptionKey | CryptoKey, - salt?: string, - ): Promise => - encrypt(password, data, key, salt, { - algorithm: 'PBKDF2', - params: { - iterations, - }, - }); + async ( + password: string, + data: unknown, + key?: EncryptionKey | CryptoKey, + salt?: string, + ): Promise => + encrypt(password, data, key, salt, { + algorithm: 'PBKDF2', + params: { + iterations, + }, + }); /** * A factory function for the encryptWithDetail method of the browser-passworder library, @@ -56,17 +58,17 @@ const encryptFactory = */ const encryptWithDetailFactory = (iterations: number) => - async ( - password: string, - object: unknown, - salt?: string, - ): Promise => - encryptWithDetail(password, object, salt, { - algorithm: 'PBKDF2', - params: { - iterations, - }, - }); + async ( + password: string, + object: unknown, + salt?: string, + ): Promise => + encryptWithDetail(password, object, salt, { + algorithm: 'PBKDF2', + params: { + iterations, + }, + }); /** * A factory function for the keyFromPassword method of the browser-passworder library, @@ -80,23 +82,23 @@ const encryptWithDetailFactory = */ const keyFromPasswordFactory = (iterations: number) => - async ( - password: string, - salt: string, - exportable?: boolean, - opts?: KeyDerivationOptions, - ): Promise => - keyFromPassword( - password, - salt, - exportable, - opts ?? { - algorithm: 'PBKDF2', - params: { - iterations, + async ( + password: string, + salt: string, + exportable?: boolean, + opts?: KeyDerivationOptions, + ): Promise => + keyFromPassword( + password, + salt, + exportable, + opts ?? { + algorithm: 'PBKDF2', + params: { + iterations, + }, }, - }, - ); + ); /** * A factory function for the isVaultUpdated method of the browser-passworder library, @@ -107,13 +109,13 @@ const keyFromPasswordFactory = */ const isVaultUpdatedFactory = (iterations: number) => - (vault: string): boolean => - isVaultUpdated(vault, { - algorithm: 'PBKDF2', - params: { - iterations, - }, - }); + (vault: string): boolean => + isVaultUpdated(vault, { + algorithm: 'PBKDF2', + params: { + iterations, + }, + }); /** * A factory function that returns an encryptor with the given number of iterations. @@ -138,6 +140,25 @@ const encryptorFactory = (iterations: number): Encryptor => ({ generateSalt, }); +const createSnapKeyringBuilder = (messenger: KeyringControllerMessenger) => { + const SnapKeyringBuilder = (() => { + return new SnapKeyring({ + messenger, + // callbacks: new SnapKeyringImpl(messenger, helpers), + isAnyAccountTypeAllowed: false, + }); + }) as { + (): SnapKeyring; + type: typeof SnapKeyring.type + state: null; + }; + + SnapKeyringBuilder.state = null; + SnapKeyringBuilder.type = SnapKeyring.type; + + return SnapKeyringBuilder; +} + export const keyringController: InitializationConfiguration< KeyringController, KeyringControllerMessenger @@ -148,15 +169,40 @@ export const keyringController: InitializationConfiguration< state, messenger, encryptor: encryptorFactory(600_000), + keyringBuilders: [createSnapKeyringBuilder(messenger)] }); + // Ensure the SnapKeyring has been added, this happens in different places in the clients. + messenger.subscribe('KeyringController:unlock', () => { + const [snapKeyring] = instance.getKeyringsByType( + KeyringTypes.snap, + ); + + if (!snapKeyring) { + instance.addNewKeyring(KeyringTypes.snap).catch(console.error); + } + }) + return { instance, }; }, - messenger: (parent) => - new Messenger<'KeyringController', never, never, typeof parent>({ + messenger: (parent) => { + const controllerMessenger: KeyringControllerMessenger = new Messenger({ namespace: 'KeyringController', parent, - }), + }); + + // TODO: We only need to delegate here for the SnapKeyring, decide if we wanna do that + parent.delegate({ + messenger: controllerMessenger, + events: [], + actions: [ + 'SnapController:handleRequest', + ], + }); + + return controllerMessenger; + } + }; diff --git a/packages/wallet/src/initialization/instances/multichain-account-service.ts b/packages/wallet/src/initialization/instances/multichain-account-service.ts index bcb21a57072..662317674e7 100644 --- a/packages/wallet/src/initialization/instances/multichain-account-service.ts +++ b/packages/wallet/src/initialization/instances/multichain-account-service.ts @@ -9,17 +9,50 @@ import { Messenger } from '@metamask/messenger'; import { InitializationConfiguration } from '../types'; +const snapAccountProviderConfig = { + // READ THIS CAREFULLY: + // We are using 1 to prevent any concurrent `keyring_createAccount` requests. This ensures + // we prevent any desync between Snap's accounts and Metamask's accounts. + maxConcurrency: 1, + // Re-use the default config for the rest: + discovery: { + timeoutMs: 2000, + maxAttempts: 3, + backOffMs: 1000, + }, + createAccounts: { + timeoutMs: 3000, + batched: false, + }, + resyncAccounts: { + autoRemoveExtraSnapAccounts: false, + }, +}; export const multichainAccountService: InitializationConfiguration< MultichainAccountService, MultichainAccountServiceMessenger > = { name: 'MultichainAccountService', - init: ({ messenger }) => { + init: ({ messenger, options }) => { const instance = new MultichainAccountService({ messenger, + providerConfigs: { + [SOL_ACCOUNT_PROVIDER_NAME]: { + ...snapAccountProviderConfig, + createAccounts: { + ...snapAccountProviderConfig.createAccounts, + batched: true, + }, + }, + [BTC_ACCOUNT_PROVIDER_NAME]: snapAccountProviderConfig, + [TRX_ACCOUNT_PROVIDER_NAME]: snapAccountProviderConfig, + }, + ensureOnboardingComplete: options.ensureOnboardingComplete, }); + // TODO: Basic Functionality triggers + return { instance, }; diff --git a/packages/wallet/src/initialization/instances/snap-controller.ts b/packages/wallet/src/initialization/instances/snap-controller.ts index c0162f403a2..f0aa6102d0a 100644 --- a/packages/wallet/src/initialization/instances/snap-controller.ts +++ b/packages/wallet/src/initialization/instances/snap-controller.ts @@ -1,26 +1,74 @@ import { Messenger } from '@metamask/messenger'; - -import { InitializationConfiguration } from '../types'; import { SnapController, SnapControllerMessenger } from '@metamask/snaps-controllers'; - +import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json'; +import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json'; +import TronWalletSnap from '@metamask/tron-wallet-snap/dist/preinstalled-snap.json'; +import { InitializationConfiguration } from '../types'; export const snapController: InitializationConfiguration< SnapController, SnapControllerMessenger > = { name: 'SnapController', - init: ({ messenger }) => { + init: ({ messenger, options }) => { const instance = new SnapController({ messenger, + ensureOnboardingComplete: options.ensureOnboardingComplete, + preinstalledSnaps: [SolanaWalletSnap, BitcoinWalletSnap, TronWalletSnap], }); + instance.init().catch(console.error); + return { instance, }; }, - messenger: (parent) => - new Messenger<'SnapController', never, never, typeof parent>({ + messenger: (parent) => { + const controllerMessenger: SnapControllerMessenger = new Messenger({ namespace: 'SnapController', parent, - }), + }); + + parent.delegate({ + messenger: controllerMessenger, + events: [ + 'ExecutionService:unhandledError', + 'ExecutionService:outboundRequest', + 'ExecutionService:outboundResponse', + 'KeyringController:lock', + 'SnapRegistryController:registryUpdated', + ], + actions: [ + 'PermissionController:getEndowments', + 'PermissionController:getPermissions', + 'PermissionController:hasPermission', + 'PermissionController:hasPermissions', + 'PermissionController:revokeAllPermissions', + 'PermissionController:revokePermissions', + 'PermissionController:revokePermissionForAllSubjects', + 'PermissionController:getSubjectNames', + 'PermissionController:updateCaveat', + 'ApprovalController:addRequest', + 'ApprovalController:updateRequestState', + 'PermissionController:grantPermissions', + 'SubjectMetadataController:getSubjectMetadata', + 'SubjectMetadataController:addSubjectMetadata', + 'ExecutionService:executeSnap', + 'ExecutionService:terminateSnap', + 'ExecutionService:handleRpcRequest', + 'SnapRegistryController:get', + 'SnapRegistryController:getMetadata', + 'SnapRegistryController:requestUpdate', + 'SnapRegistryController:resolveVersion', + 'SnapInterfaceController:createInterface', + 'SnapInterfaceController:getInterface', + 'SnapInterfaceController:setInterfaceDisplayed', + 'StorageService:setItem', + 'StorageService:getItem', + 'StorageService:removeItem', + 'StorageService:clear', + ], + }); + return controllerMessenger; + } }; diff --git a/packages/wallet/src/initialization/instances/storage-service.ts b/packages/wallet/src/initialization/instances/storage-service.ts new file mode 100644 index 00000000000..3b6b6c504f2 --- /dev/null +++ b/packages/wallet/src/initialization/instances/storage-service.ts @@ -0,0 +1,24 @@ +import { Messenger } from "@metamask/messenger"; +import { StorageService, StorageServiceMessenger } from "@metamask/storage-service"; +import { InitializationConfiguration } from "../types"; + +export const storageService: InitializationConfiguration< + StorageService, + StorageServiceMessenger +> = { + name: 'StorageService', + init: ({ messenger }) => { + const instance = new StorageService({ + messenger, + }); + + return { + instance, + }; + }, + messenger: (parent) => + new Messenger<'StorageService', never, never, typeof parent>({ + namespace: 'StorageService', + parent, + }), +}; diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index e808637e474..3897fcfb353 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -6,4 +6,5 @@ export type WalletOptions = { showApprovalRequest: () => void; clientConfigApiService: ClientConfigApiService; getMetaMetricsId: () => string; + ensureOnboardingComplete: () => Promise; }; diff --git a/packages/wallet/src/utilities.ts b/packages/wallet/src/utilities.ts index b0ee261d729..75496afa18d 100644 --- a/packages/wallet/src/utilities.ts +++ b/packages/wallet/src/utilities.ts @@ -23,10 +23,12 @@ export async function importSecretRecoveryPhrase( const indices = phrase.split(' ').map((word) => wordlist.indexOf(word)); const mnemonic = new Uint8Array(new Uint16Array(indices).buffer); - await wallet.messenger.call( + const walletGroup = await wallet.messenger.call( 'MultichainAccountService:createMultichainAccountWallet', { type: 'restore', password, mnemonic } ); + + await walletGroup.discoverAccounts(); } /** @@ -39,10 +41,12 @@ export async function createSecretRecoveryPhrase( wallet: Wallet, password: string, ): Promise { - await wallet.messenger.call( + const walletGroup = await wallet.messenger.call( 'MultichainAccountService:createMultichainAccountWallet', { type: 'create', password }, ); + + await walletGroup.discoverAccounts(); } /** diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json index a5e012287d5..87e6476f402 100644 --- a/packages/wallet/tsconfig.build.json +++ b/packages/wallet/tsconfig.build.json @@ -24,6 +24,9 @@ { "path": "../messenger/tsconfig.build.json" }, + { + "path": "../multichain-account-service/tsconfig.build.json" + }, { "path": "../network-controller/tsconfig.build.json" }, diff --git a/packages/wallet/tsconfig.json b/packages/wallet/tsconfig.json index 8f0b0c57883..91ec181232f 100644 --- a/packages/wallet/tsconfig.json +++ b/packages/wallet/tsconfig.json @@ -22,6 +22,9 @@ { "path": "../messenger/tsconfig.json" }, + { + "path": "../multichain-account-service/tsconfig.json" + }, { "path": "../network-controller/tsconfig.json" }, diff --git a/yarn.lock b/yarn.lock index 4f3d2bb917f..fbe436db14e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2993,6 +2993,13 @@ __metadata: languageName: unknown linkType: soft +"@metamask/bitcoin-wallet-snap@npm:^1.10.1": + version: 1.10.1 + resolution: "@metamask/bitcoin-wallet-snap@npm:1.10.1" + checksum: 10/647c0c6211011a54f97fe58f97e930693c8dd64a861969d3546486fdadc8f0efb7b39328469a07408c8ab76fc79d63a74d06c12de91e60204ce040f8ee89741d + languageName: node + linkType: hard + "@metamask/bridge-controller@npm:^70.0.1, @metamask/bridge-controller@workspace:packages/bridge-controller": version: 0.0.0-use.local resolution: "@metamask/bridge-controller@workspace:packages/bridge-controller" @@ -5531,6 +5538,13 @@ __metadata: languageName: unknown linkType: soft +"@metamask/solana-wallet-snap@npm:^2.8.0": + version: 2.8.0 + resolution: "@metamask/solana-wallet-snap@npm:2.8.0" + checksum: 10/5e26f28d585aa00c4da204a7311f15048de30ff336baa90df1ca82f0d151b25dcf7b3fc3daa8b9ab02914aca35f31eb7a2854bf167008f6e96f99e215f02767c + languageName: node + linkType: hard + "@metamask/stake-sdk@npm:^3.2.1": version: 3.2.1 resolution: "@metamask/stake-sdk@npm:3.2.1" @@ -5709,6 +5723,13 @@ __metadata: languageName: unknown linkType: soft +"@metamask/tron-wallet-snap@npm:^1.25.2": + version: 1.25.2 + resolution: "@metamask/tron-wallet-snap@npm:1.25.2" + checksum: 10/1f42a2d14b50895613dca01398b16c94b510ff55c4fa0a4292272d8820ca2664bf2dc8985d833317fa667a124a58565bd89d4be867c74e0f82792c43095b8620 + languageName: node + linkType: hard + "@metamask/user-operation-controller@workspace:packages/user-operation-controller": version: 0.0.0-use.local resolution: "@metamask/user-operation-controller@workspace:packages/user-operation-controller" @@ -5808,6 +5829,7 @@ __metadata: "@metamask/accounts-controller": "npm:^37.2.0" "@metamask/approval-controller": "npm:^9.0.1" "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/bitcoin-wallet-snap": "npm:^1.10.1" "@metamask/browser-passworder": "npm:^6.0.0" "@metamask/connectivity-controller": "npm:^0.2.0" "@metamask/controller-utils": "npm:^11.20.0" @@ -5818,7 +5840,9 @@ __metadata: "@metamask/remote-feature-flag-controller": "npm:^4.2.0" "@metamask/scure-bip39": "npm:^2.1.1" "@metamask/snaps-controllers": "npm:^20.0.1" + "@metamask/solana-wallet-snap": "npm:^2.8.0" "@metamask/transaction-controller": "npm:^64.0.0" + "@metamask/tron-wallet-snap": "npm:^1.25.2" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" From 942f00e5485215cd6e5e8e21719d4f9a28567f54 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 16 Apr 2026 11:29:07 +0200 Subject: [PATCH 10/11] Add SubjectMetadataController and PermissionController --- jest.config.packages.js | 4 + packages/wallet/package.json | 4 +- .../src/initialization/initialization.ts | 12 +- .../instances/execution-service.ts | 29 +++ .../src/initialization/instances/index.ts | 5 +- .../instances/keyring-controller.ts | 113 ++++++----- .../instances/multichain-account-service.ts | 4 +- .../instances/permission-controller/index.ts | 1 + .../permission-controller.ts | 53 +++++ .../permission-controller/specifications.ts | 181 ++++++++++++++++++ .../instances/snap-controller.ts | 17 +- .../instances/storage-service.ts | 10 +- .../instances/subject-metadata-controller.ts | 39 ++++ packages/wallet/src/utilities.ts | 2 +- yarn.lock | 6 +- 15 files changed, 403 insertions(+), 77 deletions(-) create mode 100644 packages/wallet/src/initialization/instances/execution-service.ts create mode 100644 packages/wallet/src/initialization/instances/permission-controller/index.ts create mode 100644 packages/wallet/src/initialization/instances/permission-controller/permission-controller.ts create mode 100644 packages/wallet/src/initialization/instances/permission-controller/specifications.ts create mode 100644 packages/wallet/src/initialization/instances/subject-metadata-controller.ts diff --git a/jest.config.packages.js b/jest.config.packages.js index 07d249241ce..36e7ac947fd 100644 --- a/jest.config.packages.js +++ b/jest.config.packages.js @@ -84,6 +84,10 @@ module.exports = { '/../json-rpc-engine/src/v2/index.ts', ], '^@metamask/utils/node$': require.resolve('@metamask/utils/node'), + '^@metamask/snaps-controllers/node$': ['@metamask/snaps-controllers/node'], + '^@metamask/post-message-stream/node$': [ + '@metamask/post-message-stream/node', + ], '^@metamask/(.+)$': [ '/../$1/src', // Some @metamask/* packages we are referencing aren't in this monorepo, diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 94b59223e08..873c0169994 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -55,10 +55,12 @@ "@metamask/browser-passworder": "^6.0.0", "@metamask/connectivity-controller": "^0.2.0", "@metamask/controller-utils": "^11.20.0", + "@metamask/json-rpc-engine": "^10.2.4", "@metamask/keyring-controller": "^25.2.0", "@metamask/messenger": "^1.1.1", - "@metamask/multichain-account-service": "workspace:^", + "@metamask/multichain-account-service": "^8.0.1", "@metamask/network-controller": "^30.0.1", + "@metamask/permission-controller": "^12.3.0", "@metamask/remote-feature-flag-controller": "^4.2.0", "@metamask/scure-bip39": "^2.1.1", "@metamask/snaps-controllers": "^20.0.1", diff --git a/packages/wallet/src/initialization/initialization.ts b/packages/wallet/src/initialization/initialization.ts index 22fd3bbc8dd..0ac85cc98c0 100644 --- a/packages/wallet/src/initialization/initialization.ts +++ b/packages/wallet/src/initialization/initialization.ts @@ -46,8 +46,16 @@ export function initialize({ options, }); - instances[name] = instance as Record; + instances[name] = instance; } - return instances as DefaultInstances; + const castInstances = instances as DefaultInstances; + + Object.values(castInstances).forEach((instance) => { + if ('init' in instance) { + instance.init().catch(console.error); + } + }); + + return castInstances; } diff --git a/packages/wallet/src/initialization/instances/execution-service.ts b/packages/wallet/src/initialization/instances/execution-service.ts new file mode 100644 index 00000000000..1c3d4078a9d --- /dev/null +++ b/packages/wallet/src/initialization/instances/execution-service.ts @@ -0,0 +1,29 @@ +import { Messenger } from '@metamask/messenger'; +import { + ExecutionService, + ExecutionServiceMessenger, + NodeThreadExecutionService, +} from '@metamask/snaps-controllers/node'; + +import { InitializationConfiguration } from '../types'; + +export const executionService: InitializationConfiguration< + ExecutionService, + ExecutionServiceMessenger +> = { + name: 'ExecutionService', + init: ({ messenger }) => { + const instance = new NodeThreadExecutionService({ + messenger, + }); + + return { + instance, + }; + }, + messenger: (parent) => + new Messenger<'ExecutionService', never, never, typeof parent>({ + namespace: 'ExecutionService', + parent, + }), +}; diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts index b780e254773..e2de77b8d9e 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -1,10 +1,13 @@ export * from './accounts-controller'; export * from './approval-controller'; export * from './connectivity-controller'; +export * from './execution-service'; export * from './storage-service'; export * from './snap-controller'; export * from './keyring-controller'; +export * from './multichain-account-service'; export * from './network-controller'; +export * from './permission-controller'; export * from './remote-feature-flag-controller'; +export * from './subject-metadata-controller'; export * from './transaction-controller'; -export * from './multichain-account-service'; diff --git a/packages/wallet/src/initialization/instances/keyring-controller.ts b/packages/wallet/src/initialization/instances/keyring-controller.ts index 89b16f8d8f4..2d4a8c5dba1 100644 --- a/packages/wallet/src/initialization/instances/keyring-controller.ts +++ b/packages/wallet/src/initialization/instances/keyring-controller.ts @@ -16,6 +16,7 @@ import { exportKey, generateSalt, } from '@metamask/browser-passworder'; +import { SnapKeyring } from '@metamask/eth-snap-keyring'; import type { Encryptor } from '@metamask/keyring-controller'; import { KeyringController, @@ -23,7 +24,6 @@ import { KeyringTypes, } from '@metamask/keyring-controller'; import { Messenger } from '@metamask/messenger'; -import { SnapKeyring } from '@metamask/eth-snap-keyring'; import { InitializationConfiguration } from '../types'; @@ -36,18 +36,18 @@ import { InitializationConfiguration } from '../types'; */ const encryptFactory = (iterations: number) => - async ( - password: string, - data: unknown, - key?: EncryptionKey | CryptoKey, - salt?: string, - ): Promise => - encrypt(password, data, key, salt, { - algorithm: 'PBKDF2', - params: { - iterations, - }, - }); + async ( + password: string, + data: unknown, + key?: EncryptionKey | CryptoKey, + salt?: string, + ): Promise => + encrypt(password, data, key, salt, { + algorithm: 'PBKDF2', + params: { + iterations, + }, + }); /** * A factory function for the encryptWithDetail method of the browser-passworder library, @@ -58,17 +58,17 @@ const encryptFactory = */ const encryptWithDetailFactory = (iterations: number) => - async ( - password: string, - object: unknown, - salt?: string, - ): Promise => - encryptWithDetail(password, object, salt, { - algorithm: 'PBKDF2', - params: { - iterations, - }, - }); + async ( + password: string, + object: unknown, + salt?: string, + ): Promise => + encryptWithDetail(password, object, salt, { + algorithm: 'PBKDF2', + params: { + iterations, + }, + }); /** * A factory function for the keyFromPassword method of the browser-passworder library, @@ -82,23 +82,23 @@ const encryptWithDetailFactory = */ const keyFromPasswordFactory = (iterations: number) => - async ( - password: string, - salt: string, - exportable?: boolean, - opts?: KeyDerivationOptions, - ): Promise => - keyFromPassword( - password, - salt, - exportable, - opts ?? { - algorithm: 'PBKDF2', - params: { - iterations, - }, + async ( + password: string, + salt: string, + exportable?: boolean, + opts?: KeyDerivationOptions, + ): Promise => + keyFromPassword( + password, + salt, + exportable, + opts ?? { + algorithm: 'PBKDF2', + params: { + iterations, }, - ); + }, + ); /** * A factory function for the isVaultUpdated method of the browser-passworder library, @@ -109,13 +109,13 @@ const keyFromPasswordFactory = */ const isVaultUpdatedFactory = (iterations: number) => - (vault: string): boolean => - isVaultUpdated(vault, { - algorithm: 'PBKDF2', - params: { - iterations, - }, - }); + (vault: string): boolean => + isVaultUpdated(vault, { + algorithm: 'PBKDF2', + params: { + iterations, + }, + }); /** * A factory function that returns an encryptor with the given number of iterations. @@ -149,7 +149,7 @@ const createSnapKeyringBuilder = (messenger: KeyringControllerMessenger) => { }); }) as { (): SnapKeyring; - type: typeof SnapKeyring.type + type: typeof SnapKeyring.type; state: null; }; @@ -157,7 +157,7 @@ const createSnapKeyringBuilder = (messenger: KeyringControllerMessenger) => { SnapKeyringBuilder.type = SnapKeyring.type; return SnapKeyringBuilder; -} +}; export const keyringController: InitializationConfiguration< KeyringController, @@ -169,19 +169,17 @@ export const keyringController: InitializationConfiguration< state, messenger, encryptor: encryptorFactory(600_000), - keyringBuilders: [createSnapKeyringBuilder(messenger)] + keyringBuilders: [createSnapKeyringBuilder(messenger)], }); // Ensure the SnapKeyring has been added, this happens in different places in the clients. messenger.subscribe('KeyringController:unlock', () => { - const [snapKeyring] = instance.getKeyringsByType( - KeyringTypes.snap, - ); + const [snapKeyring] = instance.getKeyringsByType(KeyringTypes.snap); if (!snapKeyring) { instance.addNewKeyring(KeyringTypes.snap).catch(console.error); } - }) + }); return { instance, @@ -197,12 +195,9 @@ export const keyringController: InitializationConfiguration< parent.delegate({ messenger: controllerMessenger, events: [], - actions: [ - 'SnapController:handleRequest', - ], + actions: ['SnapController:handleRequest'], }); return controllerMessenger; - } - + }, }; diff --git a/packages/wallet/src/initialization/instances/multichain-account-service.ts b/packages/wallet/src/initialization/instances/multichain-account-service.ts index 662317674e7..370c9e0fa9a 100644 --- a/packages/wallet/src/initialization/instances/multichain-account-service.ts +++ b/packages/wallet/src/initialization/instances/multichain-account-service.ts @@ -1,3 +1,4 @@ +import { Messenger } from '@metamask/messenger'; import { MultichainAccountService, SOL_ACCOUNT_PROVIDER_NAME, @@ -5,7 +6,6 @@ import { BTC_ACCOUNT_PROVIDER_NAME, MultichainAccountServiceMessenger, } from '@metamask/multichain-account-service'; -import { Messenger } from '@metamask/messenger'; import { InitializationConfiguration } from '../types'; @@ -90,5 +90,5 @@ export const multichainAccountService: InitializationConfiguration< }); return serviceMessenger; - } + }, }; diff --git a/packages/wallet/src/initialization/instances/permission-controller/index.ts b/packages/wallet/src/initialization/instances/permission-controller/index.ts new file mode 100644 index 00000000000..c62323452e3 --- /dev/null +++ b/packages/wallet/src/initialization/instances/permission-controller/index.ts @@ -0,0 +1 @@ +export * from './permission-controller'; diff --git a/packages/wallet/src/initialization/instances/permission-controller/permission-controller.ts b/packages/wallet/src/initialization/instances/permission-controller/permission-controller.ts new file mode 100644 index 00000000000..20494eb4311 --- /dev/null +++ b/packages/wallet/src/initialization/instances/permission-controller/permission-controller.ts @@ -0,0 +1,53 @@ +import { Messenger } from '@metamask/messenger'; +import { + PermissionController, + PermissionControllerMessenger, +} from '@metamask/permission-controller'; + +import { + getCaveatSpecifications, + getPermissionSpecifications, + unrestrictedMethods, +} from './specifications'; +import { InitializationConfiguration } from '../../types'; + +export const permissionController: InitializationConfiguration< + PermissionController, + PermissionControllerMessenger +> = { + name: 'PermissionController', + init: ({ messenger, state }) => { + const instance = new PermissionController({ + messenger, + state, + permissionSpecifications: getPermissionSpecifications({}), + caveatSpecifications: getCaveatSpecifications({}), + unrestrictedMethods, + }); + + return { + instance, + }; + }, + messenger: (parent) => { + const controllerMessenger: PermissionControllerMessenger = new Messenger({ + namespace: 'PermissionController', + parent, + }); + + parent.delegate({ + messenger: controllerMessenger, + actions: [ + 'ApprovalController:addRequest', + 'ApprovalController:hasRequest', + 'ApprovalController:acceptRequest', + 'ApprovalController:rejectRequest', + 'SnapController:getPermittedSnaps', + 'SnapController:installSnaps', + 'SubjectMetadataController:getSubjectMetadata', + ], + }); + + return controllerMessenger; + }, +}; diff --git a/packages/wallet/src/initialization/instances/permission-controller/specifications.ts b/packages/wallet/src/initialization/instances/permission-controller/specifications.ts new file mode 100644 index 00000000000..dcf7605d869 --- /dev/null +++ b/packages/wallet/src/initialization/instances/permission-controller/specifications.ts @@ -0,0 +1,181 @@ +import { + Caip25CaveatType, + caip25CaveatBuilder, + caip25EndowmentBuilder, +} from '@metamask/chain-agnostic-permission'; +import { + buildSnapEndowmentSpecifications, + buildSnapRestrictedMethodSpecifications, + caveatSpecifications as snapsCaveatsSpecifications, + endowmentCaveatSpecifications as snapsEndowmentCaveatSpecifications, +} from '@metamask/snaps-rpc-methods'; + +export const ExcludedSnapPermissions = Object.freeze({}); + +export const ExcludedSnapEndowments = Object.freeze({ + 'endowment:caip25': + 'eth_accounts is disabled. For more information please see https://github.com/MetaMask/snaps/issues/990.', +}); + +/** + * Gets the specifications for all permissions that will be recognized by the + * PermissionController. + * + * @returns the permission specifications to construct the PermissionController. + */ +export const getPermissionSpecifications = () => { + return { + [caip25EndowmentBuilder.targetName]: + caip25EndowmentBuilder.specificationBuilder({}), + ...buildSnapEndowmentSpecifications(Object.keys(ExcludedSnapEndowments)), + ...buildSnapRestrictedMethodSpecifications( + Object.keys(ExcludedSnapPermissions), + {}, + ), + }; +}; + +/** + * Gets the specifications for all caveats that will be recognized by the + * PermissionController. + * + * @param options - The options object. + * @param options.listAccounts - A function that returns the + * `AccountsController` internalAccount objects for all evm accounts. + * @param options.findNetworkClientIdByChainId - A function that + * returns the networkClientId given a chainId. + * @param options.isNonEvmScopeSupported - A function that returns true if + * a non-evm scope is supported. + * @param options.getNonEvmAccountAddresses - A function that returns the + * supported CAIP-10 account addresses for a non-evm scope. + * @returns the caveat specifications to construct the PermissionController. + */ +export const getCaveatSpecifications = ({ + listAccounts, + findNetworkClientIdByChainId, + isNonEvmScopeSupported, + getNonEvmAccountAddresses, +}: Parameters[0]) => { + return { + [Caip25CaveatType]: caip25CaveatBuilder({ + listAccounts, + findNetworkClientIdByChainId, + isNonEvmScopeSupported, + getNonEvmAccountAddresses, + }), + ...snapsCaveatsSpecifications, + ...snapsEndowmentCaveatSpecifications, + }; +}; + +/** + * All unrestricted methods recognized by the PermissionController. + * Unrestricted methods are ignored by the permission system, but every + * JSON-RPC request seen by the permission system must correspond to a + * restricted or unrestricted method, or the request will be rejected with a + * "method not found" error. + */ +export const unrestrictedMethods = Object.freeze([ + 'eth_blockNumber', + 'eth_call', + 'eth_chainId', + 'eth_coinbase', + 'eth_decrypt', + 'eth_estimateGas', + 'eth_feeHistory', + 'eth_gasPrice', + 'eth_getBalance', + 'eth_getBlockByHash', + 'eth_getBlockByNumber', + 'eth_getBlockTransactionCountByHash', + 'eth_getBlockTransactionCountByNumber', + 'eth_getCode', + 'eth_getEncryptionPublicKey', + 'eth_getFilterChanges', + 'eth_getFilterLogs', + 'eth_getLogs', + 'eth_getProof', + 'eth_getStorageAt', + 'eth_getTransactionByBlockHashAndIndex', + 'eth_getTransactionByBlockNumberAndIndex', + 'eth_getTransactionByHash', + 'eth_getTransactionCount', + 'eth_getTransactionReceipt', + 'eth_getUncleByBlockHashAndIndex', + 'eth_getUncleByBlockNumberAndIndex', + 'eth_getUncleCountByBlockHash', + 'eth_getUncleCountByBlockNumber', + 'eth_getWork', + 'eth_hashrate', + 'eth_mining', + 'eth_newBlockFilter', + 'eth_newFilter', + 'eth_newPendingTransactionFilter', + 'eth_protocolVersion', + 'eth_requestAccounts', + 'eth_sendRawTransaction', + 'eth_sendTransaction', + 'eth_signTypedData', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + 'eth_submitHashrate', + 'eth_submitWork', + 'eth_subscribe', + 'eth_syncing', + 'eth_uninstallFilter', + 'eth_unsubscribe', + 'metamask_getProviderState', + 'metamask_logWeb3ShimUsage', + 'metamask_sendDomainMetadata', + 'metamask_watchAsset', + 'net_listening', + 'net_peerCount', + 'net_version', + 'personal_ecRecover', + 'personal_sign', + 'wallet_requestExecutionPermissions', + 'wallet_getSupportedExecutionPermissions', + 'wallet_getGrantedExecutionPermissions', + 'wallet_addEthereumChain', + 'wallet_getCallsStatus', + 'wallet_getCapabilities', + 'wallet_getPermissions', + 'wallet_requestPermissions', + 'wallet_revokePermissions', + 'wallet_registerOnboarding', + 'wallet_sendCalls', + 'wallet_switchEthereumChain', + 'wallet_watchAsset', + 'wallet_upgradeAccount', + 'wallet_getAccountUpgradeStatus', + 'web3_clientVersion', + 'web3_sha3', + 'wallet_getAllSnaps', + 'wallet_getSnaps', + 'wallet_requestSnaps', + 'wallet_invokeSnap', + 'wallet_invokeKeyring', + 'snap_getClientStatus', + 'snap_clearState', + 'snap_getFile', + 'snap_getState', + 'snap_listEntropySources', + 'snap_createInterface', + 'snap_updateInterface', + 'snap_getInterfaceState', + 'snap_getInterfaceContext', + 'snap_resolveInterface', + 'snap_setState', + 'snap_scheduleBackgroundEvent', + 'snap_cancelBackgroundEvent', + 'snap_getBackgroundEvents', + 'snap_trackError', + 'snap_trackEvent', + 'snap_openWebSocket', + 'snap_sendWebSocketMessage', + 'snap_closeWebSocket', + 'snap_getWebSockets', + 'snap_startTrace', + 'snap_endTrace', +]); diff --git a/packages/wallet/src/initialization/instances/snap-controller.ts b/packages/wallet/src/initialization/instances/snap-controller.ts index f0aa6102d0a..ac4c30d4fe2 100644 --- a/packages/wallet/src/initialization/instances/snap-controller.ts +++ b/packages/wallet/src/initialization/instances/snap-controller.ts @@ -1,8 +1,13 @@ +import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json'; import { Messenger } from '@metamask/messenger'; -import { SnapController, SnapControllerMessenger } from '@metamask/snaps-controllers'; +import { + SnapController, + SnapControllerMessenger, + PersistedSnapControllerState, +} from '@metamask/snaps-controllers'; import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json'; -import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json'; import TronWalletSnap from '@metamask/tron-wallet-snap/dist/preinstalled-snap.json'; + import { InitializationConfiguration } from '../types'; export const snapController: InitializationConfiguration< @@ -10,15 +15,15 @@ export const snapController: InitializationConfiguration< SnapControllerMessenger > = { name: 'SnapController', - init: ({ messenger, options }) => { + init: ({ messenger, state, options }) => { const instance = new SnapController({ messenger, + // Persisted state is different from actual state, consider changing `state` inference type. + state: state as PersistedSnapControllerState, ensureOnboardingComplete: options.ensureOnboardingComplete, preinstalledSnaps: [SolanaWalletSnap, BitcoinWalletSnap, TronWalletSnap], }); - instance.init().catch(console.error); - return { instance, }; @@ -70,5 +75,5 @@ export const snapController: InitializationConfiguration< ], }); return controllerMessenger; - } + }, }; diff --git a/packages/wallet/src/initialization/instances/storage-service.ts b/packages/wallet/src/initialization/instances/storage-service.ts index 3b6b6c504f2..de2f1a09ee6 100644 --- a/packages/wallet/src/initialization/instances/storage-service.ts +++ b/packages/wallet/src/initialization/instances/storage-service.ts @@ -1,6 +1,10 @@ -import { Messenger } from "@metamask/messenger"; -import { StorageService, StorageServiceMessenger } from "@metamask/storage-service"; -import { InitializationConfiguration } from "../types"; +import { Messenger } from '@metamask/messenger'; +import { + StorageService, + StorageServiceMessenger, +} from '@metamask/storage-service'; + +import { InitializationConfiguration } from '../types'; export const storageService: InitializationConfiguration< StorageService, diff --git a/packages/wallet/src/initialization/instances/subject-metadata-controller.ts b/packages/wallet/src/initialization/instances/subject-metadata-controller.ts new file mode 100644 index 00000000000..3d59a340cf9 --- /dev/null +++ b/packages/wallet/src/initialization/instances/subject-metadata-controller.ts @@ -0,0 +1,39 @@ +import { Messenger } from '@metamask/messenger'; +import { + SubjectMetadataController, + SubjectMetadataControllerMessenger, +} from '@metamask/permission-controller'; + +import { InitializationConfiguration } from '../types'; + +export const subjectMetadataController: InitializationConfiguration< + SubjectMetadataController, + SubjectMetadataControllerMessenger +> = { + name: 'SubjectMetadataController', + init: ({ messenger, state }) => { + const instance = new SubjectMetadataController({ + messenger, + state, + subjectCacheLimit: 100, + }); + + return { + instance, + }; + }, + messenger: (parent) => { + const controllerMessenger: SubjectMetadataControllerMessenger = + new Messenger({ + namespace: 'SubjectMetadataController', + parent, + }); + + parent.delegate({ + messenger: controllerMessenger, + actions: ['PermissionController:hasPermissions'], + }); + + return controllerMessenger; + }, +}; diff --git a/packages/wallet/src/utilities.ts b/packages/wallet/src/utilities.ts index 75496afa18d..59a33647398 100644 --- a/packages/wallet/src/utilities.ts +++ b/packages/wallet/src/utilities.ts @@ -25,7 +25,7 @@ export async function importSecretRecoveryPhrase( const walletGroup = await wallet.messenger.call( 'MultichainAccountService:createMultichainAccountWallet', - { type: 'restore', password, mnemonic } + { type: 'restore', password, mnemonic }, ); await walletGroup.discoverAccounts(); diff --git a/yarn.lock b/yarn.lock index fbe436db14e..bb835ca331d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4436,7 +4436,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/multichain-account-service@npm:^8.0.1, @metamask/multichain-account-service@workspace:^, @metamask/multichain-account-service@workspace:packages/multichain-account-service": +"@metamask/multichain-account-service@npm:^8.0.1, @metamask/multichain-account-service@workspace:packages/multichain-account-service": version: 0.0.0-use.local resolution: "@metamask/multichain-account-service@workspace:packages/multichain-account-service" dependencies: @@ -5833,10 +5833,12 @@ __metadata: "@metamask/browser-passworder": "npm:^6.0.0" "@metamask/connectivity-controller": "npm:^0.2.0" "@metamask/controller-utils": "npm:^11.20.0" + "@metamask/json-rpc-engine": "npm:^10.2.4" "@metamask/keyring-controller": "npm:^25.2.0" "@metamask/messenger": "npm:^1.1.1" - "@metamask/multichain-account-service": "workspace:^" + "@metamask/multichain-account-service": "npm:^8.0.1" "@metamask/network-controller": "npm:^30.0.1" + "@metamask/permission-controller": "npm:^12.3.0" "@metamask/remote-feature-flag-controller": "npm:^4.2.0" "@metamask/scure-bip39": "npm:^2.1.1" "@metamask/snaps-controllers": "npm:^20.0.1" From 15c8efe6783e5b5586fa93ba1a936e4122f4b47a Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 16 Apr 2026 12:35:02 +0200 Subject: [PATCH 11/11] Non-functional JSON-RPC --- jest.config.packages.js | 3 ++ packages/wallet/package.json | 7 ++- packages/wallet/src/Wallet.test.ts | 9 ++-- packages/wallet/src/Wallet.ts | 13 +++++- .../src/initialization/initialization.ts | 2 + .../instances/execution-service.ts | 8 +++- .../permission-controller.ts | 4 +- .../instances/permission-controller/index.ts | 1 - .../instances/snap-controller.ts | 12 +++++ .../wallet/src/json-rpc/createProviderRpc.ts | 44 ++++++++++++++++++ .../specifications.ts | 18 ++++++++ yarn.lock | 45 +++++++++++++++++++ 12 files changed, 155 insertions(+), 11 deletions(-) rename packages/wallet/src/initialization/instances/{permission-controller => }/permission-controller.ts (93%) delete mode 100644 packages/wallet/src/initialization/instances/permission-controller/index.ts create mode 100644 packages/wallet/src/json-rpc/createProviderRpc.ts rename packages/wallet/src/{initialization/instances/permission-controller => permissions}/specifications.ts (86%) diff --git a/jest.config.packages.js b/jest.config.packages.js index 36e7ac947fd..c9d42297eb6 100644 --- a/jest.config.packages.js +++ b/jest.config.packages.js @@ -85,6 +85,9 @@ module.exports = { ], '^@metamask/utils/node$': require.resolve('@metamask/utils/node'), '^@metamask/snaps-controllers/node$': ['@metamask/snaps-controllers/node'], + '^@metamask/snaps-execution-environments/node-thread$': [ + '@metamask/snaps-execution-environments/node-thread', + ], '^@metamask/post-message-stream/node$': [ '@metamask/post-message-stream/node', ], diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 873c0169994..4822d708bd3 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -56,23 +56,28 @@ "@metamask/connectivity-controller": "^0.2.0", "@metamask/controller-utils": "^11.20.0", "@metamask/json-rpc-engine": "^10.2.4", + "@metamask/json-rpc-middleware-stream": "^8.0.8", "@metamask/keyring-controller": "^25.2.0", "@metamask/messenger": "^1.1.1", "@metamask/multichain-account-service": "^8.0.1", "@metamask/network-controller": "^30.0.1", + "@metamask/object-multiplex": "^2.1.0", "@metamask/permission-controller": "^12.3.0", "@metamask/remote-feature-flag-controller": "^4.2.0", "@metamask/scure-bip39": "^2.1.1", "@metamask/snaps-controllers": "^20.0.1", + "@metamask/snaps-execution-environments": "^11.0.2", "@metamask/solana-wallet-snap": "^2.8.0", "@metamask/transaction-controller": "^64.0.0", "@metamask/tron-wallet-snap": "^1.25.2", - "@metamask/utils": "^11.9.0" + "@metamask/utils": "^11.9.0", + "readable-stream": "^4.7.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^29.5.14", + "@types/readable-stream": "^4", "deepmerge": "^4.2.2", "dotenv": "^16.4.7", "jest": "^29.7.0", diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index f925f683fe6..57130d843d4 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -4,7 +4,7 @@ import { DistributionType, EnvironmentType, } from '@metamask/remote-feature-flag-controller'; -import { enableNetConnect } from 'nock'; +import { disableNetConnect, enableNetConnect } from 'nock'; import { importSecretRecoveryPhrase, sendTransaction } from './utilities'; import { Wallet } from './Wallet'; @@ -47,12 +47,13 @@ describe('Wallet', () => { let wallet: Wallet; beforeEach(() => { - jest.useFakeTimers({ doNotFake: ['nextTick', 'queueMicrotask'] }); + enableNetConnect(); + // jest.useFakeTimers({ doNotFake: ['nextTick', 'queueMicrotask'] }); }); afterEach(async () => { await wallet?.destroy(); - enableNetConnect(); + disableNetConnect(); jest.useRealTimers(); }); @@ -68,8 +69,6 @@ describe('Wallet', () => { }); it.skip('signs transactions', async () => { - enableNetConnect(); - wallet = await setupWallet(); const addresses = wallet.messenger diff --git a/packages/wallet/src/Wallet.ts b/packages/wallet/src/Wallet.ts index 69eb108f04e..0b11a6686bc 100644 --- a/packages/wallet/src/Wallet.ts +++ b/packages/wallet/src/Wallet.ts @@ -1,5 +1,6 @@ import { Messenger } from '@metamask/messenger'; import type { Json } from '@metamask/utils'; +import type { Duplex } from 'stream'; import type { DefaultActions, @@ -9,6 +10,7 @@ import type { RootMessenger, } from './initialization'; import { initialize } from './initialization'; +import { createProviderRpc } from './json-rpc/createProviderRpc'; import type { WalletOptions } from './types'; export type WalletConstructorArgs = { @@ -27,7 +29,12 @@ export class Wallet { namespace: 'Root', }); - this.#instances = initialize({ state, messenger: this.messenger, options }); + this.#instances = initialize({ + state, + messenger: this.messenger, + options, + createProviderRpc: this.createProviderRpc.bind(this), + }); } get state(): DefaultState { @@ -40,6 +47,10 @@ export class Wallet { ) as DefaultState; } + createProviderRpc(stream: Duplex) { + return createProviderRpc(stream); + } + async destroy(): Promise { await Promise.all( Object.values(this.#instances).map((instance) => { diff --git a/packages/wallet/src/initialization/initialization.ts b/packages/wallet/src/initialization/initialization.ts index 0ac85cc98c0..898a0a67eb6 100644 --- a/packages/wallet/src/initialization/initialization.ts +++ b/packages/wallet/src/initialization/initialization.ts @@ -20,6 +20,7 @@ export function initialize({ messenger, initializationConfigurations = [], options, + createProviderRpc, }: InitializeArgs): DefaultInstances { const overriddenConfiguration = initializationConfigurations.map( (config) => config.name, @@ -44,6 +45,7 @@ export function initialize({ state: instanceState, messenger: instanceMessenger, options, + createProviderRpc, }); instances[name] = instance; diff --git a/packages/wallet/src/initialization/instances/execution-service.ts b/packages/wallet/src/initialization/instances/execution-service.ts index 1c3d4078a9d..c0a3b3fde68 100644 --- a/packages/wallet/src/initialization/instances/execution-service.ts +++ b/packages/wallet/src/initialization/instances/execution-service.ts @@ -4,6 +4,7 @@ import { ExecutionServiceMessenger, NodeThreadExecutionService, } from '@metamask/snaps-controllers/node'; +import { Duplex } from 'stream'; import { InitializationConfiguration } from '../types'; @@ -12,9 +13,14 @@ export const executionService: InitializationConfiguration< ExecutionServiceMessenger > = { name: 'ExecutionService', - init: ({ messenger }) => { + init: ({ messenger, createProviderRpc }) => { + function setupSnapProvider(snapId: string, stream: Duplex) { + createProviderRpc(stream); + } + const instance = new NodeThreadExecutionService({ messenger, + setupSnapProvider, }); return { diff --git a/packages/wallet/src/initialization/instances/permission-controller/permission-controller.ts b/packages/wallet/src/initialization/instances/permission-controller.ts similarity index 93% rename from packages/wallet/src/initialization/instances/permission-controller/permission-controller.ts rename to packages/wallet/src/initialization/instances/permission-controller.ts index 20494eb4311..022f214cc67 100644 --- a/packages/wallet/src/initialization/instances/permission-controller/permission-controller.ts +++ b/packages/wallet/src/initialization/instances/permission-controller.ts @@ -8,8 +8,8 @@ import { getCaveatSpecifications, getPermissionSpecifications, unrestrictedMethods, -} from './specifications'; -import { InitializationConfiguration } from '../../types'; +} from '../../permissions/specifications'; +import { InitializationConfiguration } from '../types'; export const permissionController: InitializationConfiguration< PermissionController, diff --git a/packages/wallet/src/initialization/instances/permission-controller/index.ts b/packages/wallet/src/initialization/instances/permission-controller/index.ts deleted file mode 100644 index c62323452e3..00000000000 --- a/packages/wallet/src/initialization/instances/permission-controller/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './permission-controller'; diff --git a/packages/wallet/src/initialization/instances/snap-controller.ts b/packages/wallet/src/initialization/instances/snap-controller.ts index ac4c30d4fe2..22ef7c64c6a 100644 --- a/packages/wallet/src/initialization/instances/snap-controller.ts +++ b/packages/wallet/src/initialization/instances/snap-controller.ts @@ -8,6 +8,11 @@ import { import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json'; import TronWalletSnap from '@metamask/tron-wallet-snap/dist/preinstalled-snap.json'; +import { + EndowmentPermissions, + ExcludedSnapEndowments, + ExcludedSnapPermissions, +} from '../../permissions/specifications'; import { InitializationConfiguration } from '../types'; export const snapController: InitializationConfiguration< @@ -20,6 +25,13 @@ export const snapController: InitializationConfiguration< messenger, // Persisted state is different from actual state, consider changing `state` inference type. state: state as PersistedSnapControllerState, + + environmentEndowmentPermissions: Object.values(EndowmentPermissions), + excludedPermissions: { + ...ExcludedSnapPermissions, + ...ExcludedSnapEndowments, + }, + ensureOnboardingComplete: options.ensureOnboardingComplete, preinstalledSnaps: [SolanaWalletSnap, BitcoinWalletSnap, TronWalletSnap], }); diff --git a/packages/wallet/src/json-rpc/createProviderRpc.ts b/packages/wallet/src/json-rpc/createProviderRpc.ts new file mode 100644 index 00000000000..6903fb7dd4e --- /dev/null +++ b/packages/wallet/src/json-rpc/createProviderRpc.ts @@ -0,0 +1,44 @@ +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { createEngineStream } from '@metamask/json-rpc-middleware-stream'; +import ObjectMultiplex from '@metamask/object-multiplex'; +import { Duplex, pipeline } from 'readable-stream'; + +const METAMASK_EIP_1193_PROVIDER = 'metamask-provider'; +const METAMASK_CAIP_MULTICHAIN_PROVIDER = 'metamask-multichain-provider'; + +/** + * Sets up stream multiplexing for the given stream + * + * @param connectionStream - the stream to mux + * @returns the multiplexed stream + */ +export function setupMultiplex(connectionStream: Duplex): ObjectMultiplex { + const mux = new ObjectMultiplex(); + pipeline(connectionStream, mux, connectionStream, (err: Error | null) => { + if (err && !err.message?.match('Premature close')) { + console.error(err); + } + }); + return mux; +} + +export function createProviderRpc(stream: Duplex) { + const mux = setupMultiplex(stream); + + // TODO: Use V2, currently not compatible with createEngineStream. + const engine = new JsonRpcEngine(); + + // TODO: CAIP provider + const providerStream = mux.createStream(METAMASK_EIP_1193_PROVIDER); + + const engineStream = createEngineStream({ engine }); + + pipeline(providerStream, engineStream, providerStream, (error) => { + engine.destroy(); + if (error && !error.message?.match('Premature close')) { + console.error(error); + } + }); + + return { engine }; +} diff --git a/packages/wallet/src/initialization/instances/permission-controller/specifications.ts b/packages/wallet/src/permissions/specifications.ts similarity index 86% rename from packages/wallet/src/initialization/instances/permission-controller/specifications.ts rename to packages/wallet/src/permissions/specifications.ts index dcf7605d869..ebb3d5fec90 100644 --- a/packages/wallet/src/initialization/instances/permission-controller/specifications.ts +++ b/packages/wallet/src/permissions/specifications.ts @@ -10,6 +10,24 @@ import { endowmentCaveatSpecifications as snapsEndowmentCaveatSpecifications, } from '@metamask/snaps-rpc-methods'; +export const EndowmentPermissions = Object.freeze({ + 'endowment:network-access': 'endowment:network-access', + 'endowment:transaction-insight': 'endowment:transaction-insight', + 'endowment:cronjob': 'endowment:cronjob', + 'endowment:ethereum-provider': 'endowment:ethereum-provider', + 'endowment:rpc': 'endowment:rpc', + 'endowment:webassembly': 'endowment:webassembly', + 'endowment:lifecycle-hooks': 'endowment:lifecycle-hooks', + 'endowment:multichain-provider': 'endowment:multichain-provider', + 'endowment:page-home': 'endowment:page-home', + 'endowment:page-settings': 'endowment:page-settings', + 'endowment:signature-insight': 'endowment:signature-insight', + 'endowment:name-lookup': 'endowment:name-lookup', + 'endowment:assets': 'endowment:assets', + 'endowment:protocol': 'endowment:protocol', + 'endowment:keyring': 'endowment:keyring', +} as const); + export const ExcludedSnapPermissions = Object.freeze({}); export const ExcludedSnapEndowments = Object.freeze({ diff --git a/yarn.lock b/yarn.lock index bb835ca331d..f85ce8eb181 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5366,6 +5366,24 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-execution-environments@npm:^11.0.2": + version: 11.0.2 + resolution: "@metamask/snaps-execution-environments@npm:11.0.2" + dependencies: + "@metamask/json-rpc-engine": "npm:^10.2.3" + "@metamask/object-multiplex": "npm:^2.1.0" + "@metamask/post-message-stream": "npm:^10.0.0" + "@metamask/providers": "npm:^22.1.1" + "@metamask/rpc-errors": "npm:^7.0.3" + "@metamask/snaps-sdk": "npm:^11.0.0" + "@metamask/snaps-utils": "npm:^12.1.2" + "@metamask/superstruct": "npm:^3.2.1" + "@metamask/utils": "npm:^11.10.0" + readable-stream: "npm:^3.6.2" + checksum: 10/9faa7bba96688fe1430bbe7797df1693e7f3a67c7d993fdba2731fa6ce062910ab48eecb36188e7448d452d5642bafe42a5bf9ab6abeb3b0e31962fe24495993 + languageName: node + linkType: hard + "@metamask/snaps-registry@npm:^4.0.0": version: 4.0.0 resolution: "@metamask/snaps-registry@npm:4.0.0" @@ -5834,24 +5852,29 @@ __metadata: "@metamask/connectivity-controller": "npm:^0.2.0" "@metamask/controller-utils": "npm:^11.20.0" "@metamask/json-rpc-engine": "npm:^10.2.4" + "@metamask/json-rpc-middleware-stream": "npm:^8.0.8" "@metamask/keyring-controller": "npm:^25.2.0" "@metamask/messenger": "npm:^1.1.1" "@metamask/multichain-account-service": "npm:^8.0.1" "@metamask/network-controller": "npm:^30.0.1" + "@metamask/object-multiplex": "npm:^2.1.0" "@metamask/permission-controller": "npm:^12.3.0" "@metamask/remote-feature-flag-controller": "npm:^4.2.0" "@metamask/scure-bip39": "npm:^2.1.1" "@metamask/snaps-controllers": "npm:^20.0.1" + "@metamask/snaps-execution-environments": "npm:^11.0.2" "@metamask/solana-wallet-snap": "npm:^2.8.0" "@metamask/transaction-controller": "npm:^64.0.0" "@metamask/tron-wallet-snap": "npm:^1.25.2" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" + "@types/readable-stream": "npm:^4" deepmerge: "npm:^4.2.2" dotenv: "npm:^16.4.7" jest: "npm:^29.7.0" nock: "npm:^13.3.1" + readable-stream: "npm:^4.7.0" ts-jest: "npm:^29.2.5" tsx: "npm:^4.20.5" typedoc: "npm:^0.25.13" @@ -6882,6 +6905,15 @@ __metadata: languageName: node linkType: hard +"@types/readable-stream@npm:^4": + version: 4.0.23 + resolution: "@types/readable-stream@npm:4.0.23" + dependencies: + "@types/node": "npm:*" + checksum: 10/2798c083eb74c9c5910cdda2e8132e333e2435362cc811eb866871e6a2ea77cd5a665dd3085be0e45ccc90a6d7b533d3d221f3b5ccfcf3080f4133517105b3fd + languageName: node + linkType: hard + "@types/secp256k1@npm:^4.0.1": version: 4.0.6 resolution: "@types/secp256k1@npm:4.0.6" @@ -13302,6 +13334,19 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^4.7.0": + version: 4.7.0 + resolution: "readable-stream@npm:4.7.0" + dependencies: + abort-controller: "npm:^3.0.0" + buffer: "npm:^6.0.3" + events: "npm:^3.3.0" + process: "npm:^0.11.10" + string_decoder: "npm:^1.3.0" + checksum: 10/bdf096c8ff59452ce5d08f13da9597f9fcfe400b4facfaa88e74ec057e5ad1fdfa140ffe28e5ed806cf4d2055f0b812806e962bca91dce31bc4cef08e53be3a4 + languageName: node + linkType: hard + "readable-web-to-node-stream@npm:^3.0.2": version: 3.0.2 resolution: "readable-web-to-node-stream@npm:3.0.2"