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/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/.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/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..f9f85c6d4ec --- /dev/null +++ b/packages/wallet/LICENSE @@ -0,0 +1,6 @@ +This project is licensed under either of + + * MIT license ([LICENSE.MIT](LICENSE.MIT)) + * Apache License, Version 2.0 ([LICENSE.APACHE2](LICENSE.APACHE2)) + +at your option. diff --git a/packages/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 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..e0b6d9792e8 --- /dev/null +++ b/packages/wallet/jest.config.js @@ -0,0 +1,29 @@ +/* + * 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, + + // Load dotenv before tests + setupFiles: [path.resolve(__dirname, 'test/setup.ts')], + + // 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..3f11bbed70b --- /dev/null +++ b/packages/wallet/package.json @@ -0,0 +1,86 @@ +{ + "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" + }, + "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/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", + "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/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts new file mode 100644 index 00000000000..ba8387ed270 --- /dev/null +++ b/packages/wallet/src/Wallet.test.ts @@ -0,0 +1,113 @@ +import { + ClientConfigApiService, + ClientType, + DistributionType, + EnvironmentType, +} from '@metamask/remote-feature-flag-controller'; +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'; + +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: process.env.INFURA_PROJECT_KEY, + clientVersion: '1.0.0', + showApprovalRequest: (): undefined => undefined, + clientConfigApiService: new ClientConfigApiService({ + fetch: globalThis.fetch, + config: { + client: ClientType.Extension, + distribution: DistributionType.Main, + environment: EnvironmentType.Production, + }, + }), + getMetaMetricsId: (): string => 'fake-metrics-id', + }, + }); + + await importSecretRecoveryPhrase(wallet, TEST_PASSWORD, TEST_PHRASE); + + return wallet; +} + +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 () => { + wallet = await setupWallet(); + const { messenger } = wallet; + + expect( + messenger + .call('AccountsController:listAccounts') + .map((account) => account.address), + ).toStrictEqual(['0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf']); + }); + + it('signs transactions', async () => { + enableNetConnect(); + + 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: 'sepolia' }, + ); + + // 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( + expect.objectContaining({ + txParams: expect.objectContaining({ + from: addresses[0], + to: addresses[0], + data: '0x00', + value: '0x0', + type: '0x2', + }), + }), + ); + }, 10_000); + + it('exposes state', async () => { + wallet = await setupWallet(); + const { state } = wallet; + + expect(state.KeyringController).toStrictEqual({ + 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 new file mode 100644 index 00000000000..69eb108f04e --- /dev/null +++ b/packages/wallet/src/Wallet.ts @@ -0,0 +1,55 @@ +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 type { WalletOptions } from './types'; + +export type WalletConstructorArgs = { + state?: Record; + options: WalletOptions; +}; + +export class Wallet { + // TODO: Expand types when passing additionalConfigurations. + public readonly messenger: RootMessenger; + + readonly #instances: DefaultInstances; + + constructor({ state = {}, options }: WalletConstructorArgs) { + this.messenger = new Messenger({ + namespace: 'Root', + }); + + this.#instances = initialize({ state, messenger: this.messenger, options }); + } + + get state(): DefaultState { + return Object.entries(this.#instances).reduce>( + (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/index.ts b/packages/wallet/src/index.ts new file mode 100644 index 00000000000..a3db3b1b449 --- /dev/null +++ b/packages/wallet/src/index.ts @@ -0,0 +1 @@ +export { Wallet } from './Wallet'; 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 new file mode 100644 index 00000000000..5d17e1a18fa --- /dev/null +++ b/packages/wallet/src/initialization/index.ts @@ -0,0 +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 new file mode 100644 index 00000000000..22fd3bbc8dd --- /dev/null +++ b/packages/wallet/src/initialization/initialization.ts @@ -0,0 +1,53 @@ +import { Json } from '@metamask/utils'; + +import type { DefaultInstances } from './defaults'; +import { defaultConfigurations, RootMessenger } from './defaults'; +import { InitializationConfiguration } from './types'; +import { WalletOptions } from '../types'; + +export type InitializeArgs = { + state: Record; + messenger: RootMessenger; + initializationConfigurations?: InitializationConfiguration< + unknown, + unknown + >[]; + options: WalletOptions; +}; + +export function initialize({ + state, + messenger, + initializationConfigurations = [], + options, +}: InitializeArgs): DefaultInstances { + const overriddenConfiguration = initializationConfigurations.map( + (config) => config.name, + ); + + const configurationEntries = initializationConfigurations.concat( + Object.values(defaultConfigurations).filter( + (config) => !overriddenConfiguration.includes(config.name), + ), + ); + + const instances: Record = {}; + + 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, + options, + }); + + instances[name] = instance as Record; + } + + return instances as DefaultInstances; +} 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..ebe6b7847ed --- /dev/null +++ b/packages/wallet/src/initialization/instances/accounts-controller.ts @@ -0,0 +1,61 @@ +import { + AccountsController, + AccountsControllerMessenger, +} from '@metamask/accounts-controller'; +import { + Messenger, + MessengerActions, + MessengerEvents, +} from '@metamask/messenger'; + +import { InitializationConfiguration } from '../types'; + +type AllowedActions = MessengerActions; + +type AllowedEvents = MessengerEvents; + +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/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 new file mode 100644 index 00000000000..4f869053f11 --- /dev/null +++ b/packages/wallet/src/initialization/instances/index.ts @@ -0,0 +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/keyring-controller.ts b/packages/wallet/src/initialization/instances/keyring-controller.ts new file mode 100644 index 00000000000..7f3a356864c --- /dev/null +++ b/packages/wallet/src/initialization/instances/keyring-controller.ts @@ -0,0 +1,162 @@ +import type { + DetailedEncryptionResult, + EncryptionKey, + KeyDerivationOptions, +} from '@metamask/browser-passworder'; +import { + encrypt, + encryptWithDetail, + encryptWithKey, + decrypt, + decryptWithDetail, + decryptWithKey, + isVaultUpdated, + keyFromPassword, + importKey, + exportKey, + generateSalt, +} from '@metamask/browser-passworder'; +import type { Encryptor } from '@metamask/keyring-controller'; +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, + ): Promise => + 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, + ): Promise => + 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, + ): Promise => + 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): boolean => + 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): Encryptor => ({ + 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/instances/network-controller.ts b/packages/wallet/src/initialization/instances/network-controller.ts new file mode 100644 index 00000000000..aa23185d83c --- /dev/null +++ b/packages/wallet/src/initialization/instances/network-controller.ts @@ -0,0 +1,92 @@ +import { CONNECTIVITY_STATUSES } from '@metamask/connectivity-controller'; +import { DEFAULT_MAX_RETRIES } from '@metamask/controller-utils'; +import { + Messenger, + MessengerActions, + MessengerEvents, +} from '@metamask/messenger'; +import type { NetworkControllerOptions } from '@metamask/network-controller'; +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: NetworkControllerOptions['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..4b5aaf3ca76 --- /dev/null +++ b/packages/wallet/src/initialization/instances/remote-feature-flag-controller.ts @@ -0,0 +1,33 @@ +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, + clientConfigApiService: options.clientConfigApiService, + getMetaMetricsId: options.getMetaMetricsId, + }); + + 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..3768e5d195c --- /dev/null +++ b/packages/wallet/src/initialization/instances/transaction-controller.ts @@ -0,0 +1,118 @@ +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 { bindMessengerAction, InitializationConfiguration } from '../types'; + +type InitActions = + | NetworkControllerGetNetworkClientRegistryAction + | NetworkControllerGetEIP1559CompatibilityAction + | NetworkControllerGetStateAction + | KeyringControllerSignTransactionAction; + +type AllowedActions = + | MessengerActions + | InitActions; + +type AllowedEvents = MessengerEvents; + +type WalletTransactionControllerMessenger = Messenger< + 'TransactionController', + AllowedActions, + AllowedEvents +>; + +export const transactionController: InitializationConfiguration< + TransactionController, + WalletTransactionControllerMessenger +> = { + name: 'TransactionController', + init: ({ state, messenger }) => { + // TODO: Add the rest of the arguments. + const instance = new TransactionController({ + state, + messenger: messenger as unknown as TransactionControllerMessenger, + disableHistory: true, + disableSendFlowHistory: true, + disableSwaps: false, + hooks: {}, + getNetworkClientRegistry: bindMessengerAction( + messenger, + 'NetworkController:getNetworkClientRegistry', + ), + getCurrentNetworkEIP1559Compatibility: bindMessengerAction( + messenger, + 'NetworkController:getEIP1559Compatibility', + ) as () => Promise, + getNetworkState: bindMessengerAction( + messenger, + 'NetworkController:getState', + ), + // 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 { + 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', + 'NetworkController:getState', + '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 new file mode 100644 index 00000000000..24a88988f6d --- /dev/null +++ b/packages/wallet/src/initialization/types.ts @@ -0,0 +1,58 @@ +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'] + : unknown; + +export type InitFunctionArguments = { + state: InstanceState; + messenger: InstanceMessenger; + options: WalletOptions; +}; + +/** + * 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; + // 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 new file mode 100644 index 00000000000..e808637e474 --- /dev/null +++ b/packages/wallet/src/types.ts @@ -0,0 +1,9 @@ +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 new file mode 100644 index 00000000000..edd281f968b --- /dev/null +++ b/packages/wallet/src/utilities.ts @@ -0,0 +1,75 @@ +// TODO: Determine if these should be available directly on Wallet. +import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; +import type { + AddTransactionOptions, + TransactionMeta, + TransactionParams, +} from '@metamask/transaction-controller'; + +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, +): Promise { + 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, + ); +} + +/** + * 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, +): Promise { + // 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, +): Promise<{ transactionMeta: TransactionMeta; result: Promise }> { + 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/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 new file mode 100644 index 00000000000..a5e012287d5 --- /dev/null +++ b/packages/wallet/tsconfig.build.json @@ -0,0 +1,38 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { + "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 new file mode 100644 index 00000000000..8f0b0c57883 --- /dev/null +++ b/packages/wallet/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { + "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", "./test"] +} 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..60655666277 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5677,6 +5677,37 @@ __metadata: languageName: node linkType: hard +"@metamask/wallet@workspace:packages/wallet": + 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/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" + 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" @@ -8511,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"