From 17a17e87bd78f405f786f68128b7d5cecb81e507 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Wed, 20 Mar 2024 09:24:49 -0230 Subject: [PATCH] feat: Add `@metamask/wallet` package The `@metamask/wallet` package is a library that is intended for building MetaMask wallets. It will provide an API for all functionality supported across all MetaMask wallts, minus the UI. The first release of this package will not support all MetaMask wallet functionality. The initial version will support keyrings, accounts, approvals, preferences, network access, fiat currency rates, gas estimates, and transactions. We will add additional features one-by-one until this library includes all common functionality. This MVP has not yet been implemented. I've written a type declaration and some non-functional unit tests to demonstrate roughly what the API would look like, to get early feedback. This will be written up as an architecture decision record before proceeding with the implementation. --- README.md | 33 ++- packages/wallet/CHANGELOG.md | 10 + packages/wallet/LICENSE | 20 ++ packages/wallet/README.md | 15 ++ packages/wallet/jest.config.js | 26 +++ packages/wallet/package.json | 73 +++++++ packages/wallet/src/Wallet.d.ts | 308 +++++++++++++++++++++++++++ packages/wallet/src/Wallet.test.ts | 191 +++++++++++++++++ packages/wallet/src/index.ts | 11 + packages/wallet/src/permissions.d.ts | 64 ++++++ packages/wallet/tsconfig.build.json | 22 ++ packages/wallet/tsconfig.json | 20 ++ packages/wallet/typedoc.json | 7 + tsconfig.build.json | 3 +- tsconfig.json | 3 +- yarn.lock | 141 +++++++++++- 16 files changed, 940 insertions(+), 7 deletions(-) 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/Wallet.d.ts create mode 100644 packages/wallet/src/Wallet.test.ts create mode 100644 packages/wallet/src/index.ts create mode 100644 packages/wallet/src/permissions.d.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 e0a425c8b86..50b5fd67982 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ This repository contains the following packages [^fn1]: - [`@metamask/selected-network-controller`](packages/selected-network-controller) - [`@metamask/signature-controller`](packages/signature-controller) - [`@metamask/transaction-controller`](packages/transaction-controller) +- [`@metamask/user-operation-controller`](packages/user-operation-controller) +- [`@metamask/wallet`](packages/wallet) @@ -79,19 +81,24 @@ linkStyle default opacity:0.5 selected_network_controller(["@metamask/selected-network-controller"]); signature_controller(["@metamask/signature-controller"]); transaction_controller(["@metamask/transaction-controller"]); + user_operation_controller(["@metamask/user-operation-controller"]); + wallet(["@metamask/wallet"]); accounts_controller --> base_controller; accounts_controller --> keyring_controller; address_book_controller --> base_controller; address_book_controller --> controller_utils; announcement_controller --> base_controller; approval_controller --> base_controller; + assets_controllers --> accounts_controller; assets_controllers --> approval_controller; assets_controllers --> base_controller; assets_controllers --> controller_utils; + assets_controllers --> keyring_controller; assets_controllers --> network_controller; assets_controllers --> polling_controller; assets_controllers --> preferences_controller; composable_controller --> base_controller; + composable_controller --> json_rpc_engine; ens_controller --> base_controller; ens_controller --> controller_utils; ens_controller --> network_controller; @@ -103,21 +110,21 @@ linkStyle default opacity:0.5 json_rpc_middleware_stream --> json_rpc_engine; keyring_controller --> base_controller; keyring_controller --> message_manager; - keyring_controller --> preferences_controller; logging_controller --> base_controller; logging_controller --> controller_utils; message_manager --> base_controller; message_manager --> controller_utils; name_controller --> base_controller; + name_controller --> controller_utils; network_controller --> base_controller; network_controller --> controller_utils; network_controller --> eth_json_rpc_provider; network_controller --> json_rpc_engine; notification_controller --> base_controller; - permission_controller --> approval_controller; permission_controller --> base_controller; permission_controller --> controller_utils; permission_controller --> json_rpc_engine; + permission_controller --> approval_controller; permission_log_controller --> base_controller; permission_log_controller --> json_rpc_engine; phishing_controller --> base_controller; @@ -127,7 +134,7 @@ linkStyle default opacity:0.5 polling_controller --> network_controller; preferences_controller --> base_controller; preferences_controller --> controller_utils; - queued_request_controller --> approval_controller; + preferences_controller --> keyring_controller; queued_request_controller --> base_controller; queued_request_controller --> controller_utils; queued_request_controller --> json_rpc_engine; @@ -137,6 +144,7 @@ linkStyle default opacity:0.5 selected_network_controller --> base_controller; selected_network_controller --> json_rpc_engine; selected_network_controller --> network_controller; + selected_network_controller --> permission_controller; signature_controller --> approval_controller; signature_controller --> base_controller; signature_controller --> controller_utils; @@ -148,6 +156,25 @@ linkStyle default opacity:0.5 transaction_controller --> controller_utils; transaction_controller --> gas_fee_controller; transaction_controller --> network_controller; + user_operation_controller --> approval_controller; + user_operation_controller --> base_controller; + user_operation_controller --> controller_utils; + user_operation_controller --> gas_fee_controller; + user_operation_controller --> keyring_controller; + user_operation_controller --> network_controller; + user_operation_controller --> polling_controller; + user_operation_controller --> transaction_controller; + wallet --> accounts_controller; + wallet --> approval_controller; + wallet --> assets_controllers; + wallet --> base_controller; + wallet --> gas_fee_controller; + wallet --> json_rpc_engine; + wallet --> keyring_controller; + wallet --> network_controller; + wallet --> preferences_controller; + wallet --> transaction_controller; + wallet --> controller_utils; ``` 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..6f8bff03fc4 --- /dev/null +++ b/packages/wallet/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2024 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..7e788b21dd4 --- /dev/null +++ b/packages/wallet/README.md @@ -0,0 +1,15 @@ +# `@metamask/wallet` + +MetaMask wallet library + +## 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..e50d97c10b0 --- /dev/null +++ b/packages/wallet/package.json @@ -0,0 +1,73 @@ +{ + "name": "@metamask/wallet", + "version": "0.0.0", + "description": "MetaMask wallet library", + "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": "./dist/index.mjs", + "require": "./dist/index.js", + "types": "./dist/types/index.d.ts" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/wallet", + "publish:preview": "yarn npm publish --tag preview", + "test": "jest --reporters=jest-silent-reporter", + "test:clean": "jest --clearCache", + "test:verbose": "jest --verbose", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/accounts-controller": "^12.0.1", + "@metamask/approval-controller": "^6.0.1", + "@metamask/assets-controllers": "^27.2.0", + "@metamask/base-controller": "^5.0.1", + "@metamask/gas-fee-controller": "^14.0.1", + "@metamask/json-rpc-engine": "^8.0.1", + "@metamask/keyring-api": "^5.1.0", + "@metamask/keyring-controller": "^14.0.1", + "@metamask/network-controller": "^18.0.1", + "@metamask/preferences-controller": "^9.0.1", + "@metamask/transaction-controller": "^25.3.0", + "@metamask/utils": "^8.4.0" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.4.4", + "@metamask/controller-utils": "^9.0.2", + "@types/jest": "^27.4.1", + "deepmerge": "^4.2.2", + "jest": "^27.5.1", + "ts-jest": "^27.1.4", + "typedoc": "^0.24.8", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~4.8.4" + }, + "engines": { + "node": ">=16.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/wallet/src/Wallet.d.ts b/packages/wallet/src/Wallet.d.ts new file mode 100644 index 00000000000..2dcd17d0a8a --- /dev/null +++ b/packages/wallet/src/Wallet.d.ts @@ -0,0 +1,308 @@ +import type { + AccountsController, + AccountsControllerActions, + AccountsControllerEvents, + AccountsControllerState, +} from '@metamask/accounts-controller'; +import type { + ApprovalController, + ApprovalControllerActions, + ApprovalControllerEvents, + ApprovalControllerState, +} from '@metamask/approval-controller'; +import type { + CurrencyRateController, + CurrencyRateControllerActions, + CurrencyRateControllerEvents, + CurrencyRateState, +} from '@metamask/assets-controllers'; +import type { + ControllerGetStateAction, + ControllerMessenger, + ControllerStateChangeEvent, +} from '@metamask/base-controller'; +import type { + GasFeeController, + GasFeeControllerActions, + GasFeeControllerEvents, + GasFeeState, +} from '@metamask/gas-fee-controller'; +import type { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import type { + KeyringController, + KeyringControllerActions, + KeyringControllerEvents, + KeyringControllerState, +} from '@metamask/keyring-controller'; +import type { + NetworkController, + NetworkControllerActions, + NetworkControllerEvents, + NetworkState, +} from '@metamask/network-controller'; +import type { + CaveatSpecificationConstraint, + ExtractPermission, + PermissionController, + PermissionControllerActions, + PermissionControllerEvents, + PermissionControllerState, + PermissionSpecificationConstraint, + SubjectType, +} from '@metamask/permission-controller'; +import type { + PreferencesController, + PreferencesControllerActions, + PreferencesControllerEvents, + PreferencesState, +} from '@metamask/preferences-controller'; +import type { + TransactionController, + TransactionControllerActions, + TransactionControllerEvents, + TransactionControllerState, +} from '@metamask/transaction-controller'; + +import type { + InternalCaveatSpecification, + InternalPermissionSpecification, +} from './permissions'; + +/** + * MetaMask wallet state. + */ +export type MetamaskState< + ControllerPermissionSpecification extends PermissionSpecificationConstraint & + InternalPermissionSpecification = InternalPermissionSpecification, + ControllerCaveatSpecification extends CaveatSpecificationConstraint & + InternalCaveatSpecification = InternalCaveatSpecification, +> = { + accountsController: AccountsControllerState; + approvalController: ApprovalControllerState; + currencyRateController: CurrencyRateState; + gasFeeController: GasFeeState; + keyringController: KeyringControllerState; + networkController: NetworkState; + permissionController: PermissionControllerState< + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification & InternalCaveatSpecification + > + >; + preferencesController: PreferencesState; + transactionController: TransactionControllerState; +}; + +export type WalletGetState< + ControllerPermissionSpecification extends PermissionSpecificationConstraint & + InternalPermissionSpecification = InternalPermissionSpecification, + ControllerCaveatSpecification extends CaveatSpecificationConstraint & + InternalCaveatSpecification = InternalCaveatSpecification, +> = ControllerGetStateAction< + 'Wallet', + MetamaskState< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > +>; + +export type WalletActions< + ControllerPermissionSpecification extends PermissionSpecificationConstraint & + InternalPermissionSpecification = InternalPermissionSpecification, + ControllerCaveatSpecification extends CaveatSpecificationConstraint & + InternalCaveatSpecification = InternalCaveatSpecification, +> = WalletGetState< + ControllerPermissionSpecification, + ControllerCaveatSpecification +>; + +/** + * All wallet actions. + */ +export type AllWalletActions< + ControllerPermissionSpecification extends PermissionSpecificationConstraint & + InternalPermissionSpecification = InternalPermissionSpecification, + ControllerCaveatSpecification extends CaveatSpecificationConstraint & + InternalCaveatSpecification = InternalCaveatSpecification, +> = + | AccountsControllerActions + | ApprovalControllerActions + | CurrencyRateControllerActions + | GasFeeControllerActions + | KeyringControllerActions + | NetworkControllerActions + | PermissionControllerActions + | PreferencesControllerActions + | TransactionControllerActions + | WalletActions< + ControllerPermissionSpecification, + ControllerCaveatSpecification + >; + +export type WalletStateChange< + ControllerPermissionSpecification extends PermissionSpecificationConstraint & + InternalPermissionSpecification = InternalPermissionSpecification, + ControllerCaveatSpecification extends CaveatSpecificationConstraint & + InternalCaveatSpecification = InternalCaveatSpecification, +> = ControllerStateChangeEvent< + 'Wallet', + MetamaskState< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > +>; + +export type WalletEvents< + ControllerPermissionSpecification extends PermissionSpecificationConstraint & + InternalPermissionSpecification = InternalPermissionSpecification, + ControllerCaveatSpecification extends CaveatSpecificationConstraint & + InternalCaveatSpecification = InternalCaveatSpecification, +> = WalletStateChange< + ControllerPermissionSpecification, + ControllerCaveatSpecification +>; + +/** + * All wallet events. + */ +export type AllWalletEvents< + ControllerPermissionSpecification extends PermissionSpecificationConstraint & + InternalPermissionSpecification = InternalPermissionSpecification, + ControllerCaveatSpecification extends CaveatSpecificationConstraint & + InternalCaveatSpecification = InternalCaveatSpecification, +> = + | AccountsControllerEvents + | ApprovalControllerEvents + | CurrencyRateControllerEvents + | GasFeeControllerEvents + | KeyringControllerEvents + | NetworkControllerEvents + | PermissionControllerEvents + | PreferencesControllerEvents + | TransactionControllerEvents + | WalletEvents< + ControllerPermissionSpecification, + ControllerCaveatSpecification + >; + +/** + * A MetaMask wallet. + * + * @template ControllerPermissionSpecification - A union of the types of all + * permission specifications available to the controller. Any referenced caveats + * must be included in the controller's caveat specifications. + * @template ControllerCaveatSpecification - A union of the types of all + * caveat specifications available to the controller. + */ +export class MetamaskWallet< + ControllerPermissionSpecification extends PermissionSpecificationConstraint & + InternalPermissionSpecification = InternalPermissionSpecification, + ControllerCaveatSpecification extends CaveatSpecificationConstraint & + InternalCaveatSpecification = InternalCaveatSpecification, +> { + #controllerMessenger: ControllerMessenger; + + #controllers: { + accountsController: AccountsController; + approvalController: ApprovalController; + currencyRateController: CurrencyRateController; + gasFeeController: GasFeeController; + keyringController: KeyringController; + networkController: NetworkController; + permissionController: PermissionController< + ControllerPermissionSpecification, + ControllerCaveatSpecification + >; + preferencesController: PreferencesController; + transactionController: TransactionController; + }; + + #services: { + // TODO: Create EVM RPC request service, extracting from fetch middleware + evmRpcRequest: () => void; + // TODO: Create Etherscan service, extracting from transaction controller + }; + + /** + * Construct a MetaMask wallet. + * + * @param options - Options. + * @param options.controllerMessenger - An unrestricted global messenger, used as the primary + * message broker for the wallet. + * @param options.state - The initial wallet state, broken down by controller. + */ + constructor({ + controllerMessenger, + state = {}, + }: { + controllerMessenger: ControllerMessenger; + state?: Partial< + MetamaskState< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > + >; + }); + + /** + * Initialize the wallet. + * + * This step is for asynchronous operations that should be performed after the wallet is + * initially constructed. For example, status checks, remote configuration updates, or preemptive + * caching. + * + * Note that this initialziation may not occur right away after wallet construction. For new + * wallet installations, this initialization will not be run until after onboarding. + */ + initialize(): Promise; + + // TODO: Consider adding state reset to base controller + + /** + * Reset all transient wallet state. + * + * This method is meant for applications that expect to automatically restart during typical + * operation (e.g. a wallet running in a service worker). Such applications can persist all + * wallet state, including transient state, to ensure caches are not cleared during these routine + * restarts. But after a fresh application start, we still want to have the ability to clear + * transient data that is not intended to be persisted. + * + * This method will erase all transient wallet state, leaving only persistent state. It should be + * called before initialization during a new application session. + */ + resetState(): void; + + /** + * Create a "provider engine" for the given subject. + * + * A provider engine is a JSON-RPC request handler for provider JSON-RPC requests. It can be + * used to construct a provider, or to implement a wallet API. + * + * @param options - Options + * @param options.origin - The origin of the subject. + * @param options.subjectType - The type of the subject. + */ + createProviderEngine({ + origin, + subjectType, + }: { + origin: string; + subjectType: SubjectType; + }): JsonRpcEngine; + + // TODO: Add start/resume, pause, stop methods to control all polling/services + + // Additional jotnotes: + // + // Step0: Rename controller messenger, write ADR about services and selectors, refactor guts of ComposableController into compose function + // Step1: Create wallet package + // Step2: Update options to take the root controller messenger, no controllers and no restricted controller + // Step3: Add controllers and services one-by-one, starting with keyring + // Step4: After adding the network controller, add `createProviderEngine` method for RPC pipeline + // Step5: Handle state reset + // + // The wallet API is called through the messenger, same as for a controller + // Actions and events + // We would also expose actions and events from internal controllers/services + // Leave the controller API to the clients +} diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts new file mode 100644 index 00000000000..fb48cc927b0 --- /dev/null +++ b/packages/wallet/src/Wallet.test.ts @@ -0,0 +1,191 @@ +import { ControllerMessenger } from '@metamask/base-controller'; +import { NetworkType } from '@metamask/controller-utils'; +import { + defaultState as defaultNetworkState, + NetworkStatus, +} from '@metamask/network-controller'; + +import { MetamaskWallet, type WalletActions, type WalletEvents } from '.'; +import { SubjectType } from '../../permission-controller/src/SubjectMetadataController'; + +describe('Wallet', () => { + describe('constructor', () => { + it('initializes with default state', () => { + const controllerMessenger = new ControllerMessenger< + WalletActions, + WalletEvents + >(); + new MetamaskWallet({ controllerMessenger }); + + // This snapshot is too large for inline + // eslint-disable-next-line jest/no-restricted-matchers + expect(controllerMessenger.call('Wallet:getState')).toMatchSnapshot(); + }); + + it('initializes with persistent state', () => { + const controllerMessenger = new ControllerMessenger< + WalletActions, + WalletEvents + >(); + new MetamaskWallet({ + controllerMessenger, + state: { + networkController: { + ...defaultNetworkState, + // This state is persisted + selectedNetworkClientId: NetworkType.sepolia, + }, + }, + }); + + // This snapshot is too large for inline + // eslint-disable-next-line jest/no-restricted-matchers + expect(controllerMessenger.call('Wallet:getState')).toMatchSnapshot(); + }); + + it('initializes with persistent and transient state', () => { + const controllerMessenger = new ControllerMessenger< + WalletActions, + WalletEvents + >(); + new MetamaskWallet({ + controllerMessenger, + state: { + // The ApprovalController state is transient + approvalController: { + approvalFlows: [], + pendingApprovalCount: 1, + pendingApprovals: { + '123': { + id: '123', + origin: 'metamask.test', + time: 1, + type: 'Example', + requestData: {}, + requestState: null, + expectsResult: false, + }, + }, + }, + networkController: { + ...defaultNetworkState, + // This state is persisted + selectedNetworkClientId: NetworkType.sepolia, + }, + }, + }); + + // This snapshot is too large for inline + // eslint-disable-next-line jest/no-restricted-matchers + expect(controllerMessenger.call('Wallet:getState')).toMatchSnapshot(); + }); + }); + + describe('initialize', () => { + it('initializes network status', async () => { + const controllerMessenger = new ControllerMessenger< + WalletActions, + WalletEvents + >(); + const wallet = new MetamaskWallet({ controllerMessenger }); + + await wallet.initialize(); + + expect( + controllerMessenger.call('Wallet:getState').networkController + .networksMetadata[NetworkType.mainnet].status, + ).toBe(NetworkStatus.Available); + }); + }); + + describe('resetState', () => { + it('clears transient state', () => { + const controllerMessenger = new ControllerMessenger< + WalletActions, + WalletEvents + >(); + const wallet = new MetamaskWallet({ + controllerMessenger, + state: { + // The ApprovalController state is transient + approvalController: { + approvalFlows: [], + pendingApprovalCount: 1, + pendingApprovals: { + '123': { + id: '123', + origin: 'metamask.test', + time: 1, + type: 'Example', + requestData: {}, + requestState: null, + expectsResult: false, + }, + }, + }, + }, + }); + + wallet.resetState(); + + // This snapshot is too large for inline + // eslint-disable-next-line jest/no-restricted-matchers + expect( + controllerMessenger.call('Wallet:getState').approvalController + .pendingApprovalCount, + ).toBe(0); + }); + + it('does not clear persistent state', () => { + const controllerMessenger = new ControllerMessenger< + WalletActions, + WalletEvents + >(); + const wallet = new MetamaskWallet({ + controllerMessenger, + state: { + networkController: { + ...defaultNetworkState, + // This state is persisted + selectedNetworkClientId: NetworkType.sepolia, + }, + }, + }); + + wallet.resetState(); + + // This snapshot is too large for inline + // eslint-disable-next-line jest/no-restricted-matchers + expect( + controllerMessenger.call('Wallet:getState').networkController + .selectedNetworkClientId, + ).toBe(NetworkType.sepolia); + }); + }); + + describe('createProviderEngine', () => { + it('creates working provider engine', async () => { + const controllerMessenger = new ControllerMessenger< + WalletActions, + WalletEvents + >(); + const wallet = new MetamaskWallet({ + controllerMessenger, + state: {}, + }); + await wallet.initialize(); + + const providerEngine = wallet.createProviderEngine({ + origin: 'metamask', + subjectType: SubjectType.Internal, + }); + + const accounts = await providerEngine.handle({ + id: 1, + jsonrpc: '2.0', + method: 'eth_accounts', + }); + expect(accounts).toBe(['0x1']); + }); + }); +}); diff --git a/packages/wallet/src/index.ts b/packages/wallet/src/index.ts new file mode 100644 index 00000000000..96379bb7f73 --- /dev/null +++ b/packages/wallet/src/index.ts @@ -0,0 +1,11 @@ +export { MetamaskWallet } from './Wallet'; +export type { + AllWalletActions, + AllWalletEvents, + WalletActions, + WalletEvents, +} from './Wallet'; +export type { + InternalCaveatSpecification, + InternalPermissionSpecification, +} from './permissions'; diff --git a/packages/wallet/src/permissions.d.ts b/packages/wallet/src/permissions.d.ts new file mode 100644 index 00000000000..fc8941bd7aa --- /dev/null +++ b/packages/wallet/src/permissions.d.ts @@ -0,0 +1,64 @@ +import type { + CaveatValidator, + PermissionConstraint, + PermissionFactory, + PermissionType, + RestrictedMethod, +} from '@metamask/permission-controller'; +import type { Hex } from '@metamask/utils'; + +declare const CaveatTypes: { + readonly restrictReturnedAccounts: 'restrictReturnedAccounts'; +}; + +declare const RestrictedMethods: { + // These properties match RPC method names, which follow a different naming convention + /* eslint-disable @typescript-eslint/naming-convention */ + readonly eth_accounts: 'eth_accounts'; + /* eslint-enable @typescript-eslint/naming-convention */ +}; + +type InternalCaveats = { + restrictReturnedAccounts: { + type: typeof CaveatTypes.restrictReturnedAccounts; + + value: Hex[]; + }; +}; + +export type InternalCaveatSpecification = { + type: typeof CaveatTypes.restrictReturnedAccounts; + + decorator: ( + method: string, + caveat: InternalCaveats['restrictReturnedAccounts'], + ) => Hex[]; + + validator: CaveatValidator; +}; + +type InternalPermissions = { + [RestrictedMethods.eth_accounts]: { + caveats: [InternalCaveats['restrictReturnedAccounts']]; + date: number; + id: string; + invoker: string; + parentCapability: typeof RestrictedMethods.eth_accounts; + }; +}; + +export type InternalPermissionSpecification = { + permissionType: PermissionType.RestrictedMethod; + targetName: typeof RestrictedMethods.eth_accounts; + allowedCaveats: [typeof CaveatTypes.restrictReturnedAccounts]; + factory: PermissionFactory< + InternalPermissions[typeof RestrictedMethods.eth_accounts], + Record + >; + methodImplementation: RestrictedMethod<[], Hex[]>; + validator: ( + permission: PermissionConstraint, + origin?: string, + target?: string, + ) => void; +}; diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json new file mode 100644 index 00000000000..6d8541b98ef --- /dev/null +++ b/packages/wallet/tsconfig.build.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/types", + "rootDir": "./src" + }, + "references": [ + { "path": "../accounts-controller/tsconfig.build.json" }, + { "path": "../approval-controller/tsconfig.build.json" }, + { "path": "../assets-controllers/tsconfig.build.json" }, + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" }, + { "path": "../json-rpc-engine/tsconfig.build.json" }, + { "path": "../keyring-controller/tsconfig.build.json" }, + { "path": "../network-controller/tsconfig.build.json" }, + { "path": "../permission-controller/tsconfig.build.json" }, + { "path": "../preferences-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..7f566d7fc7a --- /dev/null +++ b/packages/wallet/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../accounts-controller" }, + { "path": "../approval-controller" }, + { "path": "../assets-controllers" }, + { "path": "../base-controller" }, + { "path": "../controller-utils" }, + { "path": "../json-rpc-engine" }, + { "path": "../keyring-controller" }, + { "path": "../network-controller" }, + { "path": "../permission-controller" }, + { "path": "../preferences-controller" }, + { "path": "../transaction-controller" } + ], + "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 96be50e5b38..9932ecacf73 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -29,7 +29,8 @@ { "path": "./packages/selected-network-controller/tsconfig.build.json" }, { "path": "./packages/signature-controller/tsconfig.build.json" }, { "path": "./packages/transaction-controller/tsconfig.build.json" }, - { "path": "./packages/user-operation-controller/tsconfig.build.json" } + { "path": "./packages/user-operation-controller/tsconfig.build.json" }, + { "path": "./packages/wallet" } ], "files": [], "include": [] diff --git a/tsconfig.json b/tsconfig.json index d32a4c26ff2..6ad93902134 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,7 +29,8 @@ { "path": "./packages/selected-network-controller" }, { "path": "./packages/signature-controller" }, { "path": "./packages/transaction-controller" }, - { "path": "./packages/user-operation-controller" } + { "path": "./packages/user-operation-controller" }, + { "path": "./packages/wallet" } ], "files": [], "include": ["./types"] diff --git a/yarn.lock b/yarn.lock index b94ce729691..d49361fd073 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1746,7 +1746,7 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@workspace:packages/assets-controllers": +"@metamask/assets-controllers@^27.2.0, @metamask/assets-controllers@workspace:packages/assets-controllers": version: 0.0.0-use.local resolution: "@metamask/assets-controllers@workspace:packages/assets-controllers" dependencies: @@ -2384,6 +2384,18 @@ __metadata: languageName: node linkType: hard +"@metamask/json-rpc-middleware-stream@npm:^6.0.2": + version: 6.0.2 + resolution: "@metamask/json-rpc-middleware-stream@npm:6.0.2" + dependencies: + "@metamask/json-rpc-engine": ^7.3.2 + "@metamask/safe-event-emitter": ^3.0.0 + "@metamask/utils": ^8.3.0 + readable-stream: ^3.6.2 + checksum: e831041b03e9f48f584f4425188f72b58974f95b60429c9fe8b5561da69c6bbfad2f2b2199acdff06ee718967214b65c05604d4f85f3287186619683487f1060 + languageName: node + linkType: hard + "@metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream": version: 0.0.0-use.local resolution: "@metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream" @@ -2451,6 +2463,21 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-api@npm:^5.1.0": + version: 5.1.0 + resolution: "@metamask/keyring-api@npm:5.1.0" + dependencies: + "@metamask/snaps-sdk": ^3.1.1 + "@metamask/utils": ^8.3.0 + "@types/uuid": ^9.0.1 + superstruct: ^1.0.3 + uuid: ^9.0.0 + peerDependencies: + "@metamask/providers": ">=15 <17" + checksum: 37cc4d35116285762be7167f294a31add9a978eca7652af014355ce691c9b90cfeb52ab8732ebc59d413aeb7ac87e785e4a2a49fd44d3c1563f3796e4ac1bb8f + languageName: node + linkType: hard + "@metamask/keyring-controller@^14.0.1, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" @@ -2815,6 +2842,26 @@ __metadata: languageName: node linkType: hard +"@metamask/providers@npm:^15.0.0": + version: 15.0.0 + resolution: "@metamask/providers@npm:15.0.0" + dependencies: + "@metamask/json-rpc-engine": ^7.3.2 + "@metamask/json-rpc-middleware-stream": ^6.0.2 + "@metamask/object-multiplex": ^2.0.0 + "@metamask/rpc-errors": ^6.2.1 + "@metamask/safe-event-emitter": ^3.0.0 + "@metamask/utils": ^8.3.0 + detect-browser: ^5.2.0 + extension-port-stream: ^3.0.0 + fast-deep-equal: ^3.1.3 + is-stream: ^2.0.0 + readable-stream: ^3.6.2 + webextension-polyfill: ^0.10.0 + checksum: 42571450e79d69d943384f557f6a61e0f73101d49804fb6e8075d791959f76c42b8ff626f711d434674792d77aead6cb8a32b04a3dcd53598c8aff24cbb1ad25 + languageName: node + linkType: hard + "@metamask/queued-request-controller@workspace:packages/queued-request-controller": version: 0.0.0-use.local resolution: "@metamask/queued-request-controller@workspace:packages/queued-request-controller" @@ -3090,6 +3137,20 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-sdk@npm:^3.1.1": + version: 3.1.1 + resolution: "@metamask/snaps-sdk@npm:3.1.1" + dependencies: + "@metamask/key-tree": ^9.0.0 + "@metamask/providers": ^15.0.0 + "@metamask/rpc-errors": ^6.2.1 + "@metamask/utils": ^8.3.0 + fast-xml-parser: ^4.3.4 + superstruct: ^1.0.3 + checksum: dbedd7c331bbe7900f7fec96a43bf90ec8637db8ae30b181d6a9f53ed5b14e8b48d7ffe83f5821d97a93530e91f0e0262731e48938c872cb000eb5ad45382d68 + languageName: node + linkType: hard + "@metamask/snaps-utils@npm:^5.0.0, @metamask/snaps-utils@npm:^5.1.1, @metamask/snaps-utils@npm:^5.1.2, @metamask/snaps-utils@npm:^5.2.0": version: 5.2.0 resolution: "@metamask/snaps-utils@npm:5.2.0" @@ -3254,6 +3315,51 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^8.4.0": + version: 8.4.0 + resolution: "@metamask/utils@npm:8.4.0" + dependencies: + "@ethereumjs/tx": ^4.2.0 + "@noble/hashes": ^1.3.1 + "@scure/base": ^1.1.3 + "@types/debug": ^4.1.7 + debug: ^4.3.4 + pony-cause: ^2.1.10 + semver: ^7.5.4 + superstruct: ^1.0.3 + uuid: ^9.0.1 + checksum: b0397e97bac7192f6189a8625a2dfcb56d3c2cf4dd2cb3d4e012a7e9786f04f59f6917805544bc131a6dacd2c8344e237ae43ad47429bb5eb35c6cf1248440b4 + 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": ^12.0.1 + "@metamask/approval-controller": ^6.0.1 + "@metamask/assets-controllers": ^27.2.0 + "@metamask/auto-changelog": ^3.4.4 + "@metamask/base-controller": ^5.0.1 + "@metamask/controller-utils": ^9.0.2 + "@metamask/gas-fee-controller": ^14.0.1 + "@metamask/json-rpc-engine": ^8.0.1 + "@metamask/keyring-api": ^5.1.0 + "@metamask/keyring-controller": ^14.0.1 + "@metamask/network-controller": ^18.0.1 + "@metamask/preferences-controller": ^9.0.1 + "@metamask/transaction-controller": ^25.3.0 + "@metamask/utils": ^8.4.0 + "@types/jest": ^27.4.1 + deepmerge: ^4.2.2 + jest: ^27.5.1 + ts-jest: ^27.1.4 + typedoc: ^0.24.8 + typedoc-plugin-missing-exports: ^2.0.0 + typescript: ~4.8.4 + languageName: unknown + linkType: soft + "@ngraveio/bc-ur@npm:^1.1.5": version: 1.1.6 resolution: "@ngraveio/bc-ur@npm:1.1.6" @@ -6746,6 +6852,17 @@ __metadata: languageName: node linkType: hard +"fast-xml-parser@npm:^4.3.4": + version: 4.3.6 + resolution: "fast-xml-parser@npm:4.3.6" + dependencies: + strnum: ^1.0.5 + bin: + fxparser: src/cli/cli.js + checksum: 12795c55f4564699c3cee13f7e892423244ac1125775e9b85bf948a1d4b65352da8f688d334bad530972288bb7ee0cf3d2605088d475123fce40d95003f045fa + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.15.0 resolution: "fastq@npm:1.15.0" @@ -11738,6 +11855,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:~4.8.4": + version: 4.8.4 + resolution: "typescript@npm:4.8.4" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 3e4f061658e0c8f36c820802fa809e0fd812b85687a9a2f5430bc3d0368e37d1c9605c3ce9b39df9a05af2ece67b1d844f9f6ea8ff42819f13bcb80f85629af0 + languageName: node + linkType: hard + "typescript@npm:~4.9.5": version: 4.9.5 resolution: "typescript@npm:4.9.5" @@ -11748,6 +11875,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@~4.8.4#~builtin": + version: 4.8.4 + resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin::version=4.8.4&hash=0102e9" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 301459fc3eb3b1a38fe91bf96d98eb55da88a9cb17b4ef80b4d105d620f4d547ba776cc27b44cc2ef58b66eda23fe0a74142feb5e79a6fb99f54fc018a696afa + languageName: node + linkType: hard + "typescript@patch:typescript@~4.9.5#~builtin": version: 4.9.5 resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=d73830" @@ -11871,7 +12008,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.0": +"uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: