diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7da370ce3fe..830e5951fbf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,7 @@ /packages/account-tree-controller @MetaMask/accounts-engineers /packages/profile-sync-controller @MetaMask/accounts-engineers /packages/money-account-controller @MetaMask/accounts-engineers +/packages/snap-account-service @MetaMask/accounts-engineers ## Assets Team /packages/assets-controllers @MetaMask/metamask-assets @@ -226,3 +227,5 @@ /packages/social-controllers/CHANGELOG.md @MetaMask/social-ai @MetaMask/core-platform /packages/money-account-controller/package.json @MetaMask/accounts-engineers @MetaMask/core-platform /packages/money-account-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform +/packages/snap-account-service/package.json @MetaMask/accounts-engineers @MetaMask/core-platform +/packages/snap-account-service/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform diff --git a/README.md b/README.md index 352fec4e6ec..35fb3abaa68 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ Each package in this repository has its own README where you can find installati - [`@metamask/selected-network-controller`](packages/selected-network-controller) - [`@metamask/shield-controller`](packages/shield-controller) - [`@metamask/signature-controller`](packages/signature-controller) +- [`@metamask/snap-account-service`](packages/snap-account-service) - [`@metamask/social-controllers`](packages/social-controllers) - [`@metamask/storage-service`](packages/storage-service) - [`@metamask/subscription-controller`](packages/subscription-controller) @@ -177,6 +178,7 @@ linkStyle default opacity:0.5 selected_network_controller(["@metamask/selected-network-controller"]); shield_controller(["@metamask/shield-controller"]); signature_controller(["@metamask/signature-controller"]); + snap_account_service(["@metamask/snap-account-service"]); social_controllers(["@metamask/social-controllers"]); storage_service(["@metamask/storage-service"]); subscription_controller(["@metamask/subscription-controller"]); @@ -475,6 +477,7 @@ linkStyle default opacity:0.5 signature_controller --> logging_controller; signature_controller --> messenger; signature_controller --> network_controller; + snap_account_service --> messenger; social_controllers --> base_controller; social_controllers --> base_data_service; social_controllers --> controller_utils; diff --git a/packages/snap-account-service/CHANGELOG.md b/packages/snap-account-service/CHANGELOG.md new file mode 100644 index 00000000000..d52277b11f8 --- /dev/null +++ b/packages/snap-account-service/CHANGELOG.md @@ -0,0 +1,14 @@ +# 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] + +### Added + +- Add `SnapAccountService` ([#8414](https://github.com/MetaMask/core/pull/8414)) + +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/snap-account-service/LICENSE b/packages/snap-account-service/LICENSE new file mode 100644 index 00000000000..c8a0ff6be3a --- /dev/null +++ b/packages/snap-account-service/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2026 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/snap-account-service/README.md b/packages/snap-account-service/README.md new file mode 100644 index 00000000000..e7b5f9091ce --- /dev/null +++ b/packages/snap-account-service/README.md @@ -0,0 +1,15 @@ +# `@metamask/snap-account-service` + +Service for Account Management Snaps + +## Installation + +`yarn add @metamask/snap-account-service` + +or + +`npm install @metamask/snap-account-service` + +## 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/snap-account-service/jest.config.js b/packages/snap-account-service/jest.config.js new file mode 100644 index 00000000000..ca084133399 --- /dev/null +++ b/packages/snap-account-service/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/snap-account-service/package.json b/packages/snap-account-service/package.json new file mode 100644 index 00000000000..bfded021346 --- /dev/null +++ b/packages/snap-account-service/package.json @@ -0,0 +1,73 @@ +{ + "name": "@metamask/snap-account-service", + "version": "0.0.0", + "description": "Service for Account Management Snaps", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/snap-account-service#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/snap-account-service", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/snap-account-service", + "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/messenger": "^1.1.1" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.4.4", + "@ts-bridge/cli": "^0.6.4", + "@types/jest": "^29.5.14", + "deepmerge": "^4.2.2", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "tsx": "^4.20.5", + "typedoc": "^0.25.13", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~5.3.3" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/snap-account-service/src/SnapAccountService.test.ts b/packages/snap-account-service/src/SnapAccountService.test.ts new file mode 100644 index 00000000000..641e00d7f62 --- /dev/null +++ b/packages/snap-account-service/src/SnapAccountService.test.ts @@ -0,0 +1,76 @@ +import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; +import type { + MockAnyNamespace, + MessengerActions, + MessengerEvents, +} from '@metamask/messenger'; + +import type { SnapAccountServiceMessenger } from './SnapAccountService'; +import { SnapAccountService } from './SnapAccountService'; + +describe('SnapAccountService', () => { + describe('init', () => { + it('resolves without throwing', async () => { + const { service } = createService(); + + expect(await service.init()).toBeUndefined(); + }); + }); +}); + +/** + * The type of the messenger populated with all external actions and events + * required by the service under test. + */ +type RootMessenger = Messenger< + MockAnyNamespace, + MessengerActions, + MessengerEvents +>; + +/** + * Constructs the root messenger for the service under test. + * + * @returns The root messenger. + */ +function createRootMessenger(): RootMessenger { + return new Messenger({ namespace: MOCK_ANY_NAMESPACE }); +} + +/** + * Constructs the messenger for the service under test. + * + * @param rootMessenger - The root messenger. + * @returns The service-specific messenger. + */ +function createServiceMessenger( + rootMessenger: RootMessenger, +): SnapAccountServiceMessenger { + return new Messenger({ + namespace: 'SnapAccountService', + parent: rootMessenger, + }); +} + +/** + * Constructs the service under test with sensible defaults. + * + * @param args - The arguments to this function. + * @param args.options - The options that the service constructor takes. + * @returns The new service, root messenger, and service messenger. + */ +function createService({ + options = {}, +}: { + options?: Partial[0]>; +} = {}): { + service: SnapAccountService; + rootMessenger: RootMessenger; + messenger: SnapAccountServiceMessenger; +} { + const rootMessenger = createRootMessenger(); + const messenger = createServiceMessenger(rootMessenger); + const service = new SnapAccountService({ messenger, ...options }); + + return { service, rootMessenger, messenger }; +} diff --git a/packages/snap-account-service/src/SnapAccountService.ts b/packages/snap-account-service/src/SnapAccountService.ts new file mode 100644 index 00000000000..ad1be018ff3 --- /dev/null +++ b/packages/snap-account-service/src/SnapAccountService.ts @@ -0,0 +1,80 @@ +import type { Messenger } from '@metamask/messenger'; + +/** + * The name of the {@link SnapAccountService}, used to namespace the service's + * actions and events. + */ +export const serviceName = 'SnapAccountService'; + +// === MESSENGER === + +/** + * All of the methods within {@link SnapAccountService} that are exposed via + * the messenger. + */ +const MESSENGER_EXPOSED_METHODS = [] as const; + +/** + * Actions that {@link SnapAccountService} exposes to other consumers. + */ +export type SnapAccountServiceActions = never; + +/** + * Actions from other messengers that {@link SnapAccountService} calls. + */ +type AllowedActions = never; + +/** + * Events that {@link SnapAccountService} exposes to other consumers. + */ +export type SnapAccountServiceEvents = never; + +/** + * Events from other messengers that {@link SnapAccountService} subscribes to. + */ +type AllowedEvents = never; + +/** + * The messenger which is restricted to actions and events accessed by + * {@link SnapAccountService}. + */ +export type SnapAccountServiceMessenger = Messenger< + typeof serviceName, + SnapAccountServiceActions | AllowedActions, + SnapAccountServiceEvents | AllowedEvents +>; + +/** + * Service responsible for managing account management snaps. + */ +export class SnapAccountService { + /** + * The name of the service. + */ + readonly name: typeof serviceName; + + readonly #messenger: SnapAccountServiceMessenger; + + /** + * Constructs a new {@link SnapAccountService}. + * + * @param args - The constructor arguments. + * @param args.messenger - The messenger suited for this service. + */ + constructor({ messenger }: { messenger: SnapAccountServiceMessenger }) { + this.name = serviceName; + this.#messenger = messenger; + + this.#messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, + ); + } + + /** + * Initializes the snap account service. + */ + async init(): Promise { + // TODO: Add initialization logic here. + } +} diff --git a/packages/snap-account-service/src/index.ts b/packages/snap-account-service/src/index.ts new file mode 100644 index 00000000000..dabef0507ff --- /dev/null +++ b/packages/snap-account-service/src/index.ts @@ -0,0 +1,6 @@ +export { SnapAccountService } from './SnapAccountService'; +export type { + SnapAccountServiceActions, + SnapAccountServiceEvents, + SnapAccountServiceMessenger, +} from './SnapAccountService'; diff --git a/packages/snap-account-service/tsconfig.build.json b/packages/snap-account-service/tsconfig.build.json new file mode 100644 index 00000000000..57f3ffc0f9b --- /dev/null +++ b/packages/snap-account-service/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [{ "path": "../messenger/tsconfig.build.json" }], + "include": ["../../types", "./src"] +} diff --git a/packages/snap-account-service/tsconfig.json b/packages/snap-account-service/tsconfig.json new file mode 100644 index 00000000000..77e4d580465 --- /dev/null +++ b/packages/snap-account-service/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [{ "path": "../messenger" }], + "include": ["../../types", "./src"] +} diff --git a/packages/snap-account-service/typedoc.json b/packages/snap-account-service/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/snap-account-service/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/teams.json b/teams.json index b2648bc9e8d..71291ce8015 100644 --- a/teams.json +++ b/teams.json @@ -75,5 +75,6 @@ "metamask/remote-feature-flag-controller": "team-extension-platform,team-mobile-platform", "metamask/storage-service": "team-extension-platform,team-mobile-platform", "metamask/config-registry-controller": "team-networks", - "metamask/money-account-controller": "team-accounts-framework" + "metamask/money-account-controller": "team-accounts-framework", + "metamask/snap-account-service": "team-accounts-framework" } diff --git a/tsconfig.build.json b/tsconfig.build.json index b02f3a882f5..8f378cdfb1b 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -220,6 +220,9 @@ { "path": "./packages/signature-controller/tsconfig.build.json" }, + { + "path": "./packages/snap-account-service/tsconfig.build.json" + }, { "path": "./packages/social-controllers/tsconfig.build.json" }, diff --git a/tsconfig.json b/tsconfig.json index 7eb3aadc457..75ee50ee048 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -212,6 +212,9 @@ { "path": "./packages/signature-controller" }, + { + "path": "./packages/snap-account-service" + }, { "path": "./packages/social-controllers" }, diff --git a/yarn.lock b/yarn.lock index 6b43f443c03..9df1011f265 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5266,6 +5266,24 @@ __metadata: languageName: node linkType: hard +"@metamask/snap-account-service@workspace:packages/snap-account-service": + version: 0.0.0-use.local + resolution: "@metamask/snap-account-service@workspace:packages/snap-account-service" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/messenger": "npm:^1.1.1" + "@ts-bridge/cli": "npm:^0.6.4" + "@types/jest": "npm:^29.5.14" + deepmerge: "npm:^4.2.2" + jest: "npm:^29.7.0" + ts-jest: "npm:^29.2.5" + tsx: "npm:^4.20.5" + typedoc: "npm:^0.25.13" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.3.3" + languageName: unknown + linkType: soft + "@metamask/snaps-controllers@npm:^19.0.0": version: 19.0.0 resolution: "@metamask/snaps-controllers@npm:19.0.0"