From 7fcbed159ad29e6479afa5fffda0863fd6dd3635 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 16 Mar 2026 13:20:23 +0000 Subject: [PATCH 01/29] wip --- packages/cash-account-service/CHANGELOG.md | 14 ++ packages/cash-account-service/LICENSE | 21 +++ packages/cash-account-service/README.md | 17 +++ packages/cash-account-service/jest.config.js | 26 ++++ packages/cash-account-service/package.json | 74 +++++++++ .../CashAccountService-method-action-types.ts | 9 ++ .../src/CashAccountService.test.ts | 144 ++++++++++++++++++ .../src/CashAccountService.ts | 66 ++++++++ packages/cash-account-service/src/index.ts | 9 ++ packages/cash-account-service/src/types.ts | 24 +++ .../cash-account-service/tsconfig.build.json | 14 ++ packages/cash-account-service/tsconfig.json | 12 ++ packages/cash-account-service/typedoc.json | 7 + packages/keyring-controller/package.json | 1 + .../src/KeyringController.ts | 3 + tsconfig.build.json | 3 + yarn.lock | 66 ++++++++ 17 files changed, 510 insertions(+) create mode 100644 packages/cash-account-service/CHANGELOG.md create mode 100644 packages/cash-account-service/LICENSE create mode 100644 packages/cash-account-service/README.md create mode 100644 packages/cash-account-service/jest.config.js create mode 100644 packages/cash-account-service/package.json create mode 100644 packages/cash-account-service/src/CashAccountService-method-action-types.ts create mode 100644 packages/cash-account-service/src/CashAccountService.test.ts create mode 100644 packages/cash-account-service/src/CashAccountService.ts create mode 100644 packages/cash-account-service/src/index.ts create mode 100644 packages/cash-account-service/src/types.ts create mode 100644 packages/cash-account-service/tsconfig.build.json create mode 100644 packages/cash-account-service/tsconfig.json create mode 100644 packages/cash-account-service/typedoc.json diff --git a/packages/cash-account-service/CHANGELOG.md b/packages/cash-account-service/CHANGELOG.md new file mode 100644 index 00000000000..8fcf72c699c --- /dev/null +++ b/packages/cash-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 + +- Initial release + +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/cash-account-service/LICENSE b/packages/cash-account-service/LICENSE new file mode 100644 index 00000000000..c259cd7ebcf --- /dev/null +++ b/packages/cash-account-service/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/cash-account-service/README.md b/packages/cash-account-service/README.md new file mode 100644 index 00000000000..66994ad4cd6 --- /dev/null +++ b/packages/cash-account-service/README.md @@ -0,0 +1,17 @@ +# `@metamask/cash-account-service` + +Cash account service. + +This service provides operations for creating and managing Cash accounts derived from HD keyrings. + +## Installation + +`yarn add @metamask/cash-account-service` + +or + +`npm install @metamask/cash-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/cash-account-service/jest.config.js b/packages/cash-account-service/jest.config.js new file mode 100644 index 00000000000..ca084133399 --- /dev/null +++ b/packages/cash-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/cash-account-service/package.json b/packages/cash-account-service/package.json new file mode 100644 index 00000000000..5d924c899cc --- /dev/null +++ b/packages/cash-account-service/package.json @@ -0,0 +1,74 @@ +{ + "name": "@metamask/cash-account-service", + "version": "0.0.0", + "description": "Service to manage cash accounts", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/cash-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/cash-account-service", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/cash-account-service", + "since-latest-release": "../../scripts/since-latest-release.sh", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", + "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "^9.0.0", + "@metamask/keyring-controller": "^25.1.0", + "@metamask/messenger": "^0.3.0", + "@metamask/utils": "^11.9.0" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.4.4", + "@metamask/eth-hd-keyring": "^13.0.0", + "@ts-bridge/cli": "^0.6.4", + "@types/jest": "^29.5.14", + "deepmerge": "^4.2.2", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "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/cash-account-service/src/CashAccountService-method-action-types.ts b/packages/cash-account-service/src/CashAccountService-method-action-types.ts new file mode 100644 index 00000000000..cec0dacad8b --- /dev/null +++ b/packages/cash-account-service/src/CashAccountService-method-action-types.ts @@ -0,0 +1,9 @@ +import type { CashAccountService } from './CashAccountService'; + +export type CashAccountServiceCreateCashAccountAction = { + type: `CashAccountService:createCashAccount`; + handler: CashAccountService['createCashAccount']; +}; + +export type CashAccountServiceMethodActions = + CashAccountServiceCreateCashAccountAction; diff --git a/packages/cash-account-service/src/CashAccountService.test.ts b/packages/cash-account-service/src/CashAccountService.test.ts new file mode 100644 index 00000000000..d0de4f9cd64 --- /dev/null +++ b/packages/cash-account-service/src/CashAccountService.test.ts @@ -0,0 +1,144 @@ +import type { HdKeyring } from '@metamask/eth-hd-keyring'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { KeyringObject } from '@metamask/keyring-controller'; +import type { + MessengerActions, + MessengerEvents, + MockAnyNamespace, +} from '@metamask/messenger'; +import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; + +import { CashAccountService, serviceName } from './CashAccountService'; +import type { CashAccountServiceMessenger } from './types'; + +type AllActions = MessengerActions; +type AllEvents = MessengerEvents; + +type RootMessenger = Messenger; + +const MOCK_MNEMONIC = new Uint8Array([ + 116, 101, 115, 116, 32, 116, 101, 115, 116, 32, 116, 101, 115, 116, 32, 116, + 101, 115, 116, 32, 116, 101, 115, 116, 32, 116, 101, 115, 116, 32, 116, 101, + 115, 116, 32, 116, 101, 115, 116, 32, 116, 101, 115, 116, 32, 116, 101, 115, + 116, 32, 116, 101, 115, 116, 32, 106, 117, 110, 107, +]); + +const MOCK_ENTROPY_SOURCE = 'mock-entropy-source-id'; + +const MOCK_HD_KEYRING: KeyringObject = { + type: KeyringTypes.hd, + accounts: ['0x1234'], + metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, +}; + +function setup(): { + service: CashAccountService; + rootMessenger: RootMessenger; + mocks: { + getState: jest.Mock; + getKeyringsByType: jest.Mock; + addNewKeyring: jest.Mock; + }; +} { + const rootMessenger: RootMessenger = new Messenger({ + namespace: MOCK_ANY_NAMESPACE, + captureException: jest.fn(), + }); + + const messenger: CashAccountServiceMessenger = new Messenger({ + namespace: serviceName, + parent: rootMessenger, + }); + + rootMessenger.delegate({ + messenger, + actions: [ + 'KeyringController:getState', + 'KeyringController:getKeyringsByType', + 'KeyringController:addNewKeyring', + ], + events: [], + }); + + const mocks = { + getState: jest.fn().mockReturnValue({ + isUnlocked: true, + keyrings: [MOCK_HD_KEYRING], + }), + getKeyringsByType: jest + .fn() + .mockReturnValue([{ mnemonic: MOCK_MNEMONIC } as unknown as HdKeyring]), + addNewKeyring: jest.fn().mockResolvedValue({ + id: 'new-cash-keyring-id', + name: '', + }), + }; + + rootMessenger.registerActionHandler( + 'KeyringController:getState', + mocks.getState, + ); + rootMessenger.registerActionHandler( + 'KeyringController:getKeyringsByType', + mocks.getKeyringsByType, + ); + rootMessenger.registerActionHandler( + 'KeyringController:addNewKeyring', + mocks.addNewKeyring, + ); + + const service = new CashAccountService({ messenger }); + + return { service, rootMessenger, mocks }; +} + +describe('CashAccountService', () => { + describe('createCashAccount', () => { + it('creates a Cash keyring from the HD keyring mnemonic', async () => { + const { service, mocks } = setup(); + + const result = await service.createCashAccount(MOCK_ENTROPY_SOURCE); + + expect(mocks.getKeyringsByType).toHaveBeenCalledWith(KeyringTypes.hd); + expect(mocks.addNewKeyring).toHaveBeenCalledWith(KeyringTypes.cash, { + mnemonic: MOCK_MNEMONIC, + }); + expect(result).toStrictEqual({ id: 'new-cash-keyring-id', name: '' }); + }); + + it('is callable via the messenger', async () => { + const { rootMessenger } = setup(); + + const result = await rootMessenger.call( + 'CashAccountService:createCashAccount', + MOCK_ENTROPY_SOURCE, + ); + + expect(result).toStrictEqual({ id: 'new-cash-keyring-id', name: '' }); + }); + + it('throws if no HD keyring matches the entropy source', async () => { + const { service } = setup(); + + await expect( + service.createCashAccount('nonexistent-entropy-source'), + ).rejects.toThrow( + 'No HD keyring found for entropy source: nonexistent-entropy-source', + ); + }); + + it('throws if the HD keyring has no mnemonic', async () => { + const { service, mocks } = setup(); + + mocks.getKeyringsByType.mockReturnValue([ + { mnemonic: null } as unknown as HdKeyring, + ]); + + await expect( + service.createCashAccount(MOCK_ENTROPY_SOURCE), + ).rejects.toThrow( + 'HD keyring does not have a mnemonic for the given entropy source.', + ); + }); + }); +}); diff --git a/packages/cash-account-service/src/CashAccountService.ts b/packages/cash-account-service/src/CashAccountService.ts new file mode 100644 index 00000000000..a1a1d312111 --- /dev/null +++ b/packages/cash-account-service/src/CashAccountService.ts @@ -0,0 +1,66 @@ +import type { HdKeyring } from '@metamask/eth-hd-keyring'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { + KeyringMetadata, + KeyringObject, +} from '@metamask/keyring-controller'; + +import type { CashAccountServiceMessenger } from './types'; + +export const serviceName = 'CashAccountService'; + +const MESSENGER_EXPOSED_METHODS = ['createCashAccount'] as const; + +export class CashAccountService { + readonly #messenger: CashAccountServiceMessenger; + + name: typeof serviceName = serviceName; + + constructor({ messenger }: { messenger: CashAccountServiceMessenger }) { + this.#messenger = messenger; + + this.#messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, + ); + } + + /** + * Creates a Cash keyring derived from the HD keyring identified by + * the given entropy source, and returns the new keyring's metadata. + * + * @param entropySource - The metadata id of the HD keyring to derive from. + * @returns The metadata of the newly created Cash keyring. + */ + async createCashAccount(entropySource: string): Promise { + const { keyrings } = this.#messenger.call('KeyringController:getState'); + + const hdKeyringIndex = keyrings.findIndex( + (kr: KeyringObject) => + kr.type === KeyringTypes.hd && kr.metadata.id === entropySource, + ); + if (hdKeyringIndex === -1) { + throw new Error( + `No HD keyring found for entropy source: ${entropySource}`, + ); + } + + const hdKeyrings = this.#messenger.call( + 'KeyringController:getKeyringsByType', + KeyringTypes.hd, + ) as HdKeyring[]; + + const hdKeyring = hdKeyrings[hdKeyringIndex]; + if (!hdKeyring?.mnemonic) { + throw new Error( + 'HD keyring does not have a mnemonic for the given entropy source.', + ); + } + + return await this.#messenger.call( + 'KeyringController:addNewKeyring', + KeyringTypes.cash, + { mnemonic: hdKeyring.mnemonic }, + ); + } +} diff --git a/packages/cash-account-service/src/index.ts b/packages/cash-account-service/src/index.ts new file mode 100644 index 00000000000..46d1826ad6e --- /dev/null +++ b/packages/cash-account-service/src/index.ts @@ -0,0 +1,9 @@ +export type { + CashAccountServiceActions, + CashAccountServiceMessenger, +} from './types'; +export type { + CashAccountServiceCreateCashAccountAction, + CashAccountServiceMethodActions, +} from './CashAccountService-method-action-types'; +export { CashAccountService } from './CashAccountService'; diff --git a/packages/cash-account-service/src/types.ts b/packages/cash-account-service/src/types.ts new file mode 100644 index 00000000000..cc1a3ded936 --- /dev/null +++ b/packages/cash-account-service/src/types.ts @@ -0,0 +1,24 @@ +import type { + KeyringControllerAddNewKeyringAction, + KeyringControllerGetKeyringsByTypeAction, + KeyringControllerGetStateAction, +} from '@metamask/keyring-controller'; +import type { Messenger } from '@metamask/messenger'; + +import type { serviceName } from './CashAccountService'; +import type { CashAccountServiceMethodActions } from './CashAccountService-method-action-types'; + +export type CashAccountServiceActions = CashAccountServiceMethodActions; + +type AllowedActions = + | KeyringControllerGetStateAction + | KeyringControllerGetKeyringsByTypeAction + | KeyringControllerAddNewKeyringAction; + +type AllowedEvents = never; + +export type CashAccountServiceMessenger = Messenger< + typeof serviceName, + CashAccountServiceActions | AllowedActions, + AllowedEvents +>; diff --git a/packages/cash-account-service/tsconfig.build.json b/packages/cash-account-service/tsconfig.build.json new file mode 100644 index 00000000000..b16ce7cfcda --- /dev/null +++ b/packages/cash-account-service/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../keyring-controller/tsconfig.build.json" }, + { "path": "../messenger/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/cash-account-service/tsconfig.json b/packages/cash-account-service/tsconfig.json new file mode 100644 index 00000000000..775a9ba69b6 --- /dev/null +++ b/packages/cash-account-service/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../keyring-controller" }, + { "path": "../messenger" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/cash-account-service/typedoc.json b/packages/cash-account-service/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/cash-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/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index 413009e83a7..d470ee909e1 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -48,6 +48,7 @@ }, "dependencies": { "@ethereumjs/util": "^9.1.0", + "@metamask-previews/eth-cash-keyring": "0.0.0-25e4b24", "@metamask/base-controller": "^9.0.0", "@metamask/browser-passworder": "^6.0.0", "@metamask/eth-hd-keyring": "^13.0.0", diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index add11e8183b..788ec8a88b5 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -28,6 +28,7 @@ import { isValidJson, remove0x, } from '@metamask/utils'; +import { CashKeyring } from '@metamask-previews/eth-cash-keyring'; import { Mutex } from 'async-mutex'; import type { MutexInterface } from 'async-mutex'; import Wallet, { thirdparty as importers } from 'ethereumjs-wallet'; @@ -55,6 +56,7 @@ export enum KeyringTypes { /* eslint-disable @typescript-eslint/naming-convention */ simple = 'Simple Key Pair', hd = 'HD Key Tree', + cash = 'Cash Keyring', qr = 'QR Hardware Wallet Device', trezor = 'Trezor Hardware', oneKey = 'OneKey Hardware', @@ -558,6 +560,7 @@ const defaultKeyringBuilders = [ // @ts-expect-error keyring types are mismatched keyringBuilderFactory(SimpleKeyring), keyringBuilderFactory(HdKeyring), + keyringBuilderFactory(CashKeyring), ]; export const getDefaultKeyringState = (): KeyringControllerState => { diff --git a/tsconfig.build.json b/tsconfig.build.json index 0b4716238fb..546267cfba1 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -55,6 +55,9 @@ { "path": "./packages/build-utils/tsconfig.build.json" }, + { + "path": "./packages/cash-account-service/tsconfig.build.json" + }, { "path": "./packages/chain-agnostic-permission/tsconfig.build.json" }, diff --git a/yarn.lock b/yarn.lock index 529185d3373..ec1ff5d708f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2485,6 +2485,15 @@ __metadata: languageName: node linkType: hard +"@metamask-previews/eth-cash-keyring@npm:0.0.0-25e4b24": + version: 0.0.0-25e4b24 + resolution: "@metamask-previews/eth-cash-keyring@npm:0.0.0-25e4b24" + dependencies: + "@metamask/eth-hd-keyring": "npm:13.1.0" + checksum: 10/468cd1afb1a498406cd2b4dd01f5059fc7313739b30c9ed066789f779dc784b7dc5baee982565da334671700a4f0a0bf2c45818040b7b656452700ff36b1a07b + languageName: node + linkType: hard + "@metamask/7715-permission-types@npm:^0.5.0": version: 0.5.0 resolution: "@metamask/7715-permission-types@npm:0.5.0" @@ -3104,6 +3113,27 @@ __metadata: languageName: unknown linkType: soft +"@metamask/cash-account-service@workspace:packages/cash-account-service": + version: 0.0.0-use.local + resolution: "@metamask/cash-account-service@workspace:packages/cash-account-service" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^9.0.0" + "@metamask/eth-hd-keyring": "npm:^13.0.0" + "@metamask/keyring-controller": "npm:^25.1.0" + "@metamask/messenger": "npm:^0.3.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" + jest: "npm:^29.7.0" + ts-jest: "npm:^29.2.5" + typedoc: "npm:^0.25.13" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.3.3" + languageName: unknown + linkType: soft + "@metamask/chain-agnostic-permission@npm:^1.4.0, @metamask/chain-agnostic-permission@workspace:packages/chain-agnostic-permission": version: 0.0.0-use.local resolution: "@metamask/chain-agnostic-permission@workspace:packages/chain-agnostic-permission" @@ -3673,6 +3703,24 @@ __metadata: languageName: unknown linkType: soft +"@metamask/eth-hd-keyring@npm:13.1.0": + version: 13.1.0 + resolution: "@metamask/eth-hd-keyring@npm:13.1.0" + dependencies: + "@ethereumjs/tx": "npm:^5.4.0" + "@ethereumjs/util": "npm:^9.1.0" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/key-tree": "npm:^10.0.2" + "@metamask/keyring-api": "npm:^21.3.0" + "@metamask/keyring-utils": "npm:^3.1.0" + "@metamask/scure-bip39": "npm:^2.1.1" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.1.0" + ethereum-cryptography: "npm:^2.1.2" + checksum: 10/7d67c29c6387ffe871995e67e4802b9a6f6eb2f14b556e43690509b342ef66b72765477b27e4b669fe8a00606e219e00991f94da3a74fcedcf339ab765215ae6 + languageName: node + linkType: hard + "@metamask/eth-hd-keyring@npm:^13.0.0": version: 13.0.0 resolution: "@metamask/eth-hd-keyring@npm:13.0.0" @@ -4157,6 +4205,23 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-api@npm:^21.3.0": + version: 21.5.0 + resolution: "@metamask/keyring-api@npm:21.5.0" + dependencies: + "@ethereumjs/tx": "npm:^5.4.0" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/keyring-utils": "npm:^3.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.1.0" + "@types/uuid": "npm:^9.0.8" + async-mutex: "npm:^0.5.0" + bitcoin-address-validation: "npm:^2.2.3" + uuid: "npm:^9.0.1" + checksum: 10/a7f2a8c66bc76edabde15b66b80904208b71fd62406ebb91579db4c65d74a8f66db6c610afe265823313df7423b6727a4de03cb1f43e41d840d51a5039f9ef4d + languageName: node + linkType: hard + "@metamask/keyring-api@npm:^21.4.0, @metamask/keyring-api@npm:^21.6.0": version: 21.6.0 resolution: "@metamask/keyring-api@npm:21.6.0" @@ -4183,6 +4248,7 @@ __metadata: "@ethereumjs/util": "npm:^9.1.0" "@lavamoat/allow-scripts": "npm:^3.0.4" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" + "@metamask-previews/eth-cash-keyring": "npm:0.0.0-25e4b24" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^9.0.0" "@metamask/browser-passworder": "npm:^6.0.0" From b960c58608d30d358b93aaa25aeda6b6dbb4af00 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 16 Mar 2026 15:44:55 +0000 Subject: [PATCH 02/29] fix: add codeowner --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 331f1487491..db34828ff76 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -38,6 +38,7 @@ ## Earn Team /packages/earn-controller @MetaMask/metamask-earn +/packages/cash-account-service @MetaMask/metamask-earn ## Social AI Team /packages/ai-controllers @MetaMask/social-ai From 890ecc1280ff045f9048053583d55c7f4ab785ac Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 17 Mar 2026 12:04:48 +0000 Subject: [PATCH 03/29] chore: add missing code owner rules --- .github/CODEOWNERS | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index db34828ff76..3e10a5b7016 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -38,7 +38,7 @@ ## Earn Team /packages/earn-controller @MetaMask/metamask-earn -/packages/cash-account-service @MetaMask/metamask-earn +/packages/cash-account-service @MetaMask/metamask-earn ## Social AI Team /packages/ai-controllers @MetaMask/social-ai @@ -137,6 +137,9 @@ /packages/assets-controllers/CHANGELOG.md @MetaMask/metamask-assets @MetaMask/core-platform /packages/assets-controller/package.json @MetaMask/metamask-assets @MetaMask/core-platform /packages/assets-controller/CHANGELOG.md @MetaMask/metamask-assets @MetaMask/core-platform + +/packages/cash-account-service/package.json @MetaMask/metamask-earn @MetaMask/core-platform +/packages/cash-account-service/CHANGELOG.md @MetaMask/metamask-earn @MetaMask/core-platform /packages/chain-agnostic-permission/package.json @MetaMask/wallet-integrations @MetaMask/core-platform /packages/chain-agnostic-permission/CHANGELOG.md @MetaMask/wallet-integrations @MetaMask/core-platform /packages/config-registry-controller/CHANGELOG.md @MetaMask/networks @MetaMask/core-platform From 69183918077a0f1fdb9a0b54b9fea6c732f31ba1 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 17 Mar 2026 12:13:10 +0000 Subject: [PATCH 04/29] chore: dedupe deps --- yarn.lock | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index ec1ff5d708f..b6a6e7a59a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3703,7 +3703,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/eth-hd-keyring@npm:13.1.0": +"@metamask/eth-hd-keyring@npm:13.1.0, @metamask/eth-hd-keyring@npm:^13.0.0": version: 13.1.0 resolution: "@metamask/eth-hd-keyring@npm:13.1.0" dependencies: @@ -3721,21 +3721,6 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-hd-keyring@npm:^13.0.0": - version: 13.0.0 - resolution: "@metamask/eth-hd-keyring@npm:13.0.0" - dependencies: - "@ethereumjs/util": "npm:^9.1.0" - "@metamask/eth-sig-util": "npm:^8.2.0" - "@metamask/key-tree": "npm:^10.0.2" - "@metamask/keyring-utils": "npm:^3.1.0" - "@metamask/scure-bip39": "npm:^2.1.1" - "@metamask/utils": "npm:^11.1.0" - ethereum-cryptography: "npm:^2.1.2" - checksum: 10/fe955a4e0331090df8110dbd8f46ea6286c2ad20e6677ecf535361ea9d0008194b2043eddd692cd7ceac2e033a54e4e340caa7d302bd5211826cb252b526f6bc - languageName: node - linkType: hard - "@metamask/eth-json-rpc-filters@npm:^9.0.0": version: 9.0.0 resolution: "@metamask/eth-json-rpc-filters@npm:9.0.0" From 8f6c75d5c30899086fb8567d516c76609b2933d7 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 17 Mar 2026 13:19:31 +0000 Subject: [PATCH 05/29] fix: handle non-hd keyrings in index check --- .../src/CashAccountService.test.ts | 23 +++++++++++++++++++ .../src/CashAccountService.ts | 8 ++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/cash-account-service/src/CashAccountService.test.ts b/packages/cash-account-service/src/CashAccountService.test.ts index d0de4f9cd64..041eb846507 100644 --- a/packages/cash-account-service/src/CashAccountService.test.ts +++ b/packages/cash-account-service/src/CashAccountService.test.ts @@ -117,6 +117,29 @@ describe('CashAccountService', () => { expect(result).toStrictEqual({ id: 'new-cash-keyring-id', name: '' }); }); + it('finds the correct HD keyring when non-HD keyrings precede it', async () => { + const { service, mocks } = setup(); + mocks.getState.mockReturnValue({ + isUnlocked: true, + keyrings: [ + { + type: KeyringTypes.simple, + accounts: ['0xabc'], + metadata: { id: 'simple-1', name: '' }, + }, + MOCK_HD_KEYRING, + ], + }); + + const result = await service.createCashAccount(MOCK_ENTROPY_SOURCE); + + expect(mocks.getKeyringsByType).toHaveBeenCalledWith(KeyringTypes.hd); + expect(mocks.addNewKeyring).toHaveBeenCalledWith(KeyringTypes.cash, { + mnemonic: MOCK_MNEMONIC, + }); + expect(result).toStrictEqual({ id: 'new-cash-keyring-id', name: '' }); + }); + it('throws if no HD keyring matches the entropy source', async () => { const { service } = setup(); diff --git a/packages/cash-account-service/src/CashAccountService.ts b/packages/cash-account-service/src/CashAccountService.ts index a1a1d312111..f3dd1cd57c3 100644 --- a/packages/cash-account-service/src/CashAccountService.ts +++ b/packages/cash-account-service/src/CashAccountService.ts @@ -35,9 +35,11 @@ export class CashAccountService { async createCashAccount(entropySource: string): Promise { const { keyrings } = this.#messenger.call('KeyringController:getState'); - const hdKeyringIndex = keyrings.findIndex( - (kr: KeyringObject) => - kr.type === KeyringTypes.hd && kr.metadata.id === entropySource, + const hdKeyringsFromState = keyrings.filter( + (kr: KeyringObject) => kr.type === KeyringTypes.hd, + ); + const hdKeyringIndex = hdKeyringsFromState.findIndex( + (kr: KeyringObject) => kr.metadata.id === entropySource, ); if (hdKeyringIndex === -1) { throw new Error( From 882cff2c36b6dc487de146a0b5db503acae1563d Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 17 Mar 2026 13:27:47 +0000 Subject: [PATCH 06/29] chore: use withKeyRing --- .../src/CashAccountService.test.ts | 79 +++++-------------- .../src/CashAccountService.ts | 45 ++++------- packages/cash-account-service/src/types.ts | 6 +- 3 files changed, 36 insertions(+), 94 deletions(-) diff --git a/packages/cash-account-service/src/CashAccountService.test.ts b/packages/cash-account-service/src/CashAccountService.test.ts index 041eb846507..d98089f04d2 100644 --- a/packages/cash-account-service/src/CashAccountService.test.ts +++ b/packages/cash-account-service/src/CashAccountService.test.ts @@ -1,6 +1,5 @@ import type { HdKeyring } from '@metamask/eth-hd-keyring'; import { KeyringTypes } from '@metamask/keyring-controller'; -import type { KeyringObject } from '@metamask/keyring-controller'; import type { MessengerActions, MessengerEvents, @@ -25,18 +24,11 @@ const MOCK_MNEMONIC = new Uint8Array([ const MOCK_ENTROPY_SOURCE = 'mock-entropy-source-id'; -const MOCK_HD_KEYRING: KeyringObject = { - type: KeyringTypes.hd, - accounts: ['0x1234'], - metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, -}; - function setup(): { service: CashAccountService; rootMessenger: RootMessenger; mocks: { - getState: jest.Mock; - getKeyringsByType: jest.Mock; + withKeyring: jest.Mock; addNewKeyring: jest.Mock; }; } { @@ -53,21 +45,19 @@ function setup(): { rootMessenger.delegate({ messenger, actions: [ - 'KeyringController:getState', - 'KeyringController:getKeyringsByType', + 'KeyringController:withKeyring', 'KeyringController:addNewKeyring', ], events: [], }); const mocks = { - getState: jest.fn().mockReturnValue({ - isUnlocked: true, - keyrings: [MOCK_HD_KEYRING], + withKeyring: jest.fn().mockImplementation(async (_selector, operation) => { + return operation({ + keyring: { mnemonic: MOCK_MNEMONIC } as unknown as HdKeyring, + metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, + }); }), - getKeyringsByType: jest - .fn() - .mockReturnValue([{ mnemonic: MOCK_MNEMONIC } as unknown as HdKeyring]), addNewKeyring: jest.fn().mockResolvedValue({ id: 'new-cash-keyring-id', name: '', @@ -75,12 +65,8 @@ function setup(): { }; rootMessenger.registerActionHandler( - 'KeyringController:getState', - mocks.getState, - ); - rootMessenger.registerActionHandler( - 'KeyringController:getKeyringsByType', - mocks.getKeyringsByType, + 'KeyringController:withKeyring', + mocks.withKeyring, ); rootMessenger.registerActionHandler( 'KeyringController:addNewKeyring', @@ -99,7 +85,10 @@ describe('CashAccountService', () => { const result = await service.createCashAccount(MOCK_ENTROPY_SOURCE); - expect(mocks.getKeyringsByType).toHaveBeenCalledWith(KeyringTypes.hd); + expect(mocks.withKeyring).toHaveBeenCalledWith( + { id: MOCK_ENTROPY_SOURCE }, + expect.any(Function), + ); expect(mocks.addNewKeyring).toHaveBeenCalledWith(KeyringTypes.cash, { mnemonic: MOCK_MNEMONIC, }); @@ -117,45 +106,15 @@ describe('CashAccountService', () => { expect(result).toStrictEqual({ id: 'new-cash-keyring-id', name: '' }); }); - it('finds the correct HD keyring when non-HD keyrings precede it', async () => { - const { service, mocks } = setup(); - mocks.getState.mockReturnValue({ - isUnlocked: true, - keyrings: [ - { - type: KeyringTypes.simple, - accounts: ['0xabc'], - metadata: { id: 'simple-1', name: '' }, - }, - MOCK_HD_KEYRING, - ], - }); - - const result = await service.createCashAccount(MOCK_ENTROPY_SOURCE); - - expect(mocks.getKeyringsByType).toHaveBeenCalledWith(KeyringTypes.hd); - expect(mocks.addNewKeyring).toHaveBeenCalledWith(KeyringTypes.cash, { - mnemonic: MOCK_MNEMONIC, - }); - expect(result).toStrictEqual({ id: 'new-cash-keyring-id', name: '' }); - }); - - it('throws if no HD keyring matches the entropy source', async () => { - const { service } = setup(); - - await expect( - service.createCashAccount('nonexistent-entropy-source'), - ).rejects.toThrow( - 'No HD keyring found for entropy source: nonexistent-entropy-source', - ); - }); - it('throws if the HD keyring has no mnemonic', async () => { const { service, mocks } = setup(); - mocks.getKeyringsByType.mockReturnValue([ - { mnemonic: null } as unknown as HdKeyring, - ]); + mocks.withKeyring.mockImplementation(async (_selector, operation) => { + return operation({ + keyring: { mnemonic: null } as unknown as HdKeyring, + metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, + }); + }); await expect( service.createCashAccount(MOCK_ENTROPY_SOURCE), diff --git a/packages/cash-account-service/src/CashAccountService.ts b/packages/cash-account-service/src/CashAccountService.ts index f3dd1cd57c3..661a2693b22 100644 --- a/packages/cash-account-service/src/CashAccountService.ts +++ b/packages/cash-account-service/src/CashAccountService.ts @@ -1,9 +1,6 @@ import type { HdKeyring } from '@metamask/eth-hd-keyring'; import { KeyringTypes } from '@metamask/keyring-controller'; -import type { - KeyringMetadata, - KeyringObject, -} from '@metamask/keyring-controller'; +import type { KeyringMetadata } from '@metamask/keyring-controller'; import type { CashAccountServiceMessenger } from './types'; @@ -33,36 +30,24 @@ export class CashAccountService { * @returns The metadata of the newly created Cash keyring. */ async createCashAccount(entropySource: string): Promise { - const { keyrings } = this.#messenger.call('KeyringController:getState'); - - const hdKeyringsFromState = keyrings.filter( - (kr: KeyringObject) => kr.type === KeyringTypes.hd, - ); - const hdKeyringIndex = hdKeyringsFromState.findIndex( - (kr: KeyringObject) => kr.metadata.id === entropySource, - ); - if (hdKeyringIndex === -1) { - throw new Error( - `No HD keyring found for entropy source: ${entropySource}`, - ); - } - - const hdKeyrings = this.#messenger.call( - 'KeyringController:getKeyringsByType', - KeyringTypes.hd, - ) as HdKeyring[]; - - const hdKeyring = hdKeyrings[hdKeyringIndex]; - if (!hdKeyring?.mnemonic) { - throw new Error( - 'HD keyring does not have a mnemonic for the given entropy source.', - ); - } + const mnemonic = (await this.#messenger.call( + 'KeyringController:withKeyring', + { id: entropySource }, + async ({ keyring }) => { + const hdKeyring = keyring as unknown as HdKeyring; + if (!hdKeyring.mnemonic) { + throw new Error( + 'HD keyring does not have a mnemonic for the given entropy source.', + ); + } + return hdKeyring.mnemonic; + }, + )) as Uint8Array; return await this.#messenger.call( 'KeyringController:addNewKeyring', KeyringTypes.cash, - { mnemonic: hdKeyring.mnemonic }, + { mnemonic }, ); } } diff --git a/packages/cash-account-service/src/types.ts b/packages/cash-account-service/src/types.ts index cc1a3ded936..741bd737a51 100644 --- a/packages/cash-account-service/src/types.ts +++ b/packages/cash-account-service/src/types.ts @@ -1,7 +1,6 @@ import type { KeyringControllerAddNewKeyringAction, - KeyringControllerGetKeyringsByTypeAction, - KeyringControllerGetStateAction, + KeyringControllerWithKeyringAction, } from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; @@ -11,8 +10,7 @@ import type { CashAccountServiceMethodActions } from './CashAccountService-metho export type CashAccountServiceActions = CashAccountServiceMethodActions; type AllowedActions = - | KeyringControllerGetStateAction - | KeyringControllerGetKeyringsByTypeAction + | KeyringControllerWithKeyringAction | KeyringControllerAddNewKeyringAction; type AllowedEvents = never; From c4a966af438eeda32b0503906c79c5146be4d6d5 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 17 Mar 2026 13:35:27 +0000 Subject: [PATCH 07/29] chore: update team.json --- teams.json | 1 + 1 file changed, 1 insertion(+) diff --git a/teams.json b/teams.json index 4a3f6902d14..f5292410e08 100644 --- a/teams.json +++ b/teams.json @@ -40,6 +40,7 @@ "metamask/base-controller": "team-core-platform", "metamask/base-data-service": "team-core-platform", "metamask/build-utils": "team-core-platform", + "metamask/cash-account-service": "team-earn", "metamask/composable-controller": "team-core-platform", "metamask/connectivity-controller": "team-core-platform", "metamask/geolocation-controller": "team-core-platform", From 9a8489e85dded2ba679b6ac356378f70480b35f9 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 19 Mar 2026 13:16:01 +0000 Subject: [PATCH 08/29] chore: add the cash keyring to the accounts controller switch --- packages/accounts-controller/src/AccountsController.test.ts | 1 + packages/accounts-controller/src/utils.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 2b56924135a..a8f2e49c3f5 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -2726,6 +2726,7 @@ describe('AccountsController', () => { KeyringTypes.ledger, KeyringTypes.lattice, KeyringTypes.qr, + KeyringTypes.cash, ])('should add accounts for %s type', async (keyringType) => { mockUUIDWithNormalAccounts([mockAccount]); diff --git a/packages/accounts-controller/src/utils.ts b/packages/accounts-controller/src/utils.ts index 5620031d37b..8c4f882f48e 100644 --- a/packages/accounts-controller/src/utils.ts +++ b/packages/accounts-controller/src/utils.ts @@ -39,6 +39,9 @@ export function keyringTypeToName(keyringType: string): string { case KeyringTypes.qr: { return 'QR'; } + case KeyringTypes.cash: { + return 'Cash Account'; + } case KeyringTypes.snap: { return 'Snap Account'; } From ccc6c2c8cdf2792925602111733bb89f3f4bfcfc Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 19 Mar 2026 13:56:46 +0000 Subject: [PATCH 09/29] fix: make createAccount idempotent --- packages/cash-account-service/package.json | 3 +- .../src/CashAccountService.test.ts | 41 +++++++++++++++++-- .../src/CashAccountService.ts | 25 ++++++++++- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/packages/cash-account-service/package.json b/packages/cash-account-service/package.json index 5d924c899cc..8bb5088d782 100644 --- a/packages/cash-account-service/package.json +++ b/packages/cash-account-service/package.json @@ -49,8 +49,7 @@ "dependencies": { "@metamask/base-controller": "^9.0.0", "@metamask/keyring-controller": "^25.1.0", - "@metamask/messenger": "^0.3.0", - "@metamask/utils": "^11.9.0" + "@metamask/messenger": "^0.3.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", diff --git a/packages/cash-account-service/src/CashAccountService.test.ts b/packages/cash-account-service/src/CashAccountService.test.ts index d98089f04d2..f7a1bf4c4dc 100644 --- a/packages/cash-account-service/src/CashAccountService.test.ts +++ b/packages/cash-account-service/src/CashAccountService.test.ts @@ -52,9 +52,15 @@ function setup(): { }); const mocks = { - withKeyring: jest.fn().mockImplementation(async (_selector, operation) => { + withKeyring: jest.fn().mockImplementation(async (selector, operation) => { + if ('type' in selector) { + throw new Error('Keyring not found'); + } return operation({ - keyring: { mnemonic: MOCK_MNEMONIC } as unknown as HdKeyring, + keyring: { + type: 'HD Key Tree', + mnemonic: MOCK_MNEMONIC, + } as unknown as HdKeyring, metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, }); }), @@ -106,12 +112,41 @@ describe('CashAccountService', () => { expect(result).toStrictEqual({ id: 'new-cash-keyring-id', name: '' }); }); + it('returns existing cash keyring metadata if a cash account already exists', async () => { + const { service, mocks } = setup(); + const MOCK_CASH_METADATA = { id: 'existing-cash-keyring-id', name: '' }; + const MOCK_CASH_ADDRESS = '0x1234567890abcdef1234567890abcdef12345678'; + + mocks.withKeyring.mockImplementation(async (selector, operation) => { + if ('type' in selector && selector.type === KeyringTypes.cash) { + return operation({ + keyring: { + getAccounts: jest.fn().mockResolvedValue([MOCK_CASH_ADDRESS]), + }, + metadata: MOCK_CASH_METADATA, + }); + } + return operation({ + keyring: { + type: 'HD Key Tree', + mnemonic: MOCK_MNEMONIC, + } as unknown as HdKeyring, + metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, + }); + }); + + const result = await service.createCashAccount(MOCK_ENTROPY_SOURCE); + + expect(result).toStrictEqual(MOCK_CASH_METADATA); + expect(mocks.addNewKeyring).not.toHaveBeenCalled(); + }); + it('throws if the HD keyring has no mnemonic', async () => { const { service, mocks } = setup(); mocks.withKeyring.mockImplementation(async (_selector, operation) => { return operation({ - keyring: { mnemonic: null } as unknown as HdKeyring, + keyring: { type: 'HD Key Tree', mnemonic: null } as unknown as HdKeyring, metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, }); }); diff --git a/packages/cash-account-service/src/CashAccountService.ts b/packages/cash-account-service/src/CashAccountService.ts index 661a2693b22..87cebf7c888 100644 --- a/packages/cash-account-service/src/CashAccountService.ts +++ b/packages/cash-account-service/src/CashAccountService.ts @@ -1,4 +1,4 @@ -import type { HdKeyring } from '@metamask/eth-hd-keyring'; +import { HdKeyring } from '@metamask/eth-hd-keyring'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { KeyringMetadata } from '@metamask/keyring-controller'; @@ -34,7 +34,13 @@ export class CashAccountService { 'KeyringController:withKeyring', { id: entropySource }, async ({ keyring }) => { - const hdKeyring = keyring as unknown as HdKeyring; + if (keyring.type !== HdKeyring.type) { + throw new Error( + 'HD keyring not have a mnemonic for the given entropy source.', + ); + } + const hdKeyring = keyring as HdKeyring; + if (!hdKeyring.mnemonic) { throw new Error( 'HD keyring does not have a mnemonic for the given entropy source.', @@ -44,6 +50,21 @@ export class CashAccountService { }, )) as Uint8Array; + const existingCashMetadata = (await this.#messenger + .call( + 'KeyringController:withKeyring', + { type: KeyringTypes.cash }, + async ({ keyring, metadata }) => { + const accounts = await keyring.getAccounts(); + return accounts.length > 0 ? metadata : null; + }, + ) + .catch(() => null)) as KeyringMetadata | null; + + if (existingCashMetadata) { + return existingCashMetadata; + } + return await this.#messenger.call( 'KeyringController:addNewKeyring', KeyringTypes.cash, From b6529384fc2bc6327afa659e245db12c60aefbce Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 19 Mar 2026 13:57:15 +0000 Subject: [PATCH 10/29] chore: remove uneeded guard --- packages/cash-account-service/src/CashAccountService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cash-account-service/src/CashAccountService.ts b/packages/cash-account-service/src/CashAccountService.ts index 87cebf7c888..b0a292b9769 100644 --- a/packages/cash-account-service/src/CashAccountService.ts +++ b/packages/cash-account-service/src/CashAccountService.ts @@ -30,7 +30,7 @@ export class CashAccountService { * @returns The metadata of the newly created Cash keyring. */ async createCashAccount(entropySource: string): Promise { - const mnemonic = (await this.#messenger.call( + const mnemonic = await this.#messenger.call( 'KeyringController:withKeyring', { id: entropySource }, async ({ keyring }) => { @@ -48,7 +48,7 @@ export class CashAccountService { } return hdKeyring.mnemonic; }, - )) as Uint8Array; + ); const existingCashMetadata = (await this.#messenger .call( From 7ac9b62ea0dcfb1c63783eb9e97fd16f065da44f Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 19 Mar 2026 13:57:49 +0000 Subject: [PATCH 11/29] chore: update teams.json --- teams.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teams.json b/teams.json index f5292410e08..470f0a3e2cb 100644 --- a/teams.json +++ b/teams.json @@ -40,7 +40,7 @@ "metamask/base-controller": "team-core-platform", "metamask/base-data-service": "team-core-platform", "metamask/build-utils": "team-core-platform", - "metamask/cash-account-service": "team-earn", + "metamask/cash-account-service": "team-accounts-framework,team-earn", "metamask/composable-controller": "team-core-platform", "metamask/connectivity-controller": "team-core-platform", "metamask/geolocation-controller": "team-core-platform", From 9b5b9b21e9a6145c353c0e2b49d9ae17d5e904d0 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 19 Mar 2026 13:59:41 +0000 Subject: [PATCH 12/29] chore: update codeowners to make new service shared --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3e10a5b7016..473fe1eb33d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -117,6 +117,7 @@ /packages/remote-feature-flag-controller @MetaMask/extension-platform @MetaMask/mobile-platform @MetaMask/core-platform /packages/storage-service @MetaMask/extension-platform @MetaMask/mobile-platform @MetaMask/core-platform /packages/client-controller @MetaMask/core-platform @MetaMask/extension-platform @MetaMask/mobile-platform +/packages/cash-account-service @MetaMask/accounts-engineers @MetaMask/metamask-earn ## Package Release related /packages/account-tree-controller/package.json @MetaMask/accounts-engineers @MetaMask/core-platform From 99bc537d839f4d8d9a8f421b02fcf054baef2283 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 19 Mar 2026 14:03:50 +0000 Subject: [PATCH 13/29] chore: updaet changelogs --- packages/accounts-controller/CHANGELOG.md | 1 + packages/cash-account-service/CHANGELOG.md | 2 +- packages/keyring-controller/CHANGELOG.md | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index 6b7a652a219..99ea932288a 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add support for `KeyringTypes.cash` in `keyringTypeToName`, mapping it to `'Cash Account'` ([#8204](https://github.com/MetaMask/core/pull/8204)) - Add `:accounts{Added,Removed}` batch events ([#8151](https://github.com/MetaMask/core/pull/8151)) - Those new events can be used instead of single `:accountAdded` and `:accountRemoved` events to reduce the number of events emitted during batch operations (e.g. `KeyringController` state re-synchronization). diff --git a/packages/cash-account-service/CHANGELOG.md b/packages/cash-account-service/CHANGELOG.md index 8fcf72c699c..955ecf2e9f9 100644 --- a/packages/cash-account-service/CHANGELOG.md +++ b/packages/cash-account-service/CHANGELOG.md @@ -9,6 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Initial release +- Initial release ([#8204](https://github.com/MetaMask/core/pull/8204)) [Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 073f80166da..6230b47674b 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `KeyringTypes.cash` (`'Cash Keyring'`) to the `KeyringTypes` enum ([#8204](https://github.com/MetaMask/core/pull/8204)) +- Add `CashKeyring` (from `@metamask-previews/eth-cash-keyring`) as a built-in default keyring ([#8204](https://github.com/MetaMask/core/pull/8204)) + ### Changed - Bump `@metamask/keyring-api` from `^21.0.0` to `^21.6.0` ([#7857](https://github.com/MetaMask/core/pull/7857), [#8259](https://github.com/MetaMask/core/pull/8259)) From 49cab71105684e9149d9d83f53cfdad23bf39230 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 19 Mar 2026 14:06:53 +0000 Subject: [PATCH 14/29] chore: regenerate lockfile --- yarn.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index b6a6e7a59a9..4f15a285aa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3122,7 +3122,6 @@ __metadata: "@metamask/eth-hd-keyring": "npm:^13.0.0" "@metamask/keyring-controller": "npm:^25.1.0" "@metamask/messenger": "npm:^0.3.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" From 686bc3a4ebcb67da48c5f41b969229e147380aa7 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 19 Mar 2026 14:44:26 +0000 Subject: [PATCH 15/29] chore: fix formatting --- packages/cash-account-service/src/CashAccountService.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cash-account-service/src/CashAccountService.test.ts b/packages/cash-account-service/src/CashAccountService.test.ts index f7a1bf4c4dc..96da178c8f6 100644 --- a/packages/cash-account-service/src/CashAccountService.test.ts +++ b/packages/cash-account-service/src/CashAccountService.test.ts @@ -146,7 +146,10 @@ describe('CashAccountService', () => { mocks.withKeyring.mockImplementation(async (_selector, operation) => { return operation({ - keyring: { type: 'HD Key Tree', mnemonic: null } as unknown as HdKeyring, + keyring: { + type: 'HD Key Tree', + mnemonic: null, + } as unknown as HdKeyring, metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, }); }); From 31dcb5b8d0c3dbf2b5e6328b6fdefd6e77f7a5c6 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 19 Mar 2026 15:00:29 +0000 Subject: [PATCH 16/29] chore: fix misleading error message --- packages/cash-account-service/src/CashAccountService.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/cash-account-service/src/CashAccountService.ts b/packages/cash-account-service/src/CashAccountService.ts index b0a292b9769..0a3c92b9c11 100644 --- a/packages/cash-account-service/src/CashAccountService.ts +++ b/packages/cash-account-service/src/CashAccountService.ts @@ -35,9 +35,7 @@ export class CashAccountService { { id: entropySource }, async ({ keyring }) => { if (keyring.type !== HdKeyring.type) { - throw new Error( - 'HD keyring not have a mnemonic for the given entropy source.', - ); + throw new Error('Got keyring without HD Keyring type'); } const hdKeyring = keyring as HdKeyring; From 377d17900bd251587ea77417e104c4a542652b10 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 19 Mar 2026 16:12:34 +0000 Subject: [PATCH 17/29] chore: properly mark dependency as runtime --- packages/cash-account-service/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cash-account-service/package.json b/packages/cash-account-service/package.json index 8bb5088d782..c7a74238623 100644 --- a/packages/cash-account-service/package.json +++ b/packages/cash-account-service/package.json @@ -48,12 +48,12 @@ }, "dependencies": { "@metamask/base-controller": "^9.0.0", + "@metamask/eth-hd-keyring": "^13.0.0", "@metamask/keyring-controller": "^25.1.0", "@metamask/messenger": "^0.3.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/eth-hd-keyring": "^13.0.0", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^29.5.14", "deepmerge": "^4.2.2", From e257bae1a5e82dcecdd00a4dd33a83f726c06e35 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 20 Mar 2026 10:00:18 +0000 Subject: [PATCH 18/29] chore: change cash to money --- .github/CODEOWNERS | 8 +- packages/accounts-controller/CHANGELOG.md | 2 +- .../src/AccountsController.test.ts | 2 +- packages/accounts-controller/src/utils.ts | 4 +- packages/cash-account-service/README.md | 17 ---- .../CashAccountService-method-action-types.ts | 9 -- packages/cash-account-service/src/index.ts | 9 -- packages/keyring-controller/CHANGELOG.md | 4 +- packages/keyring-controller/package.json | 1 + .../src/KeyringController.ts | 6 +- .../CHANGELOG.md | 0 .../LICENSE | 0 packages/money-account-service/README.md | 17 ++++ .../jest.config.js | 0 .../package.json | 10 +- ...MoneyAccountService-method-action-types.ts | 9 ++ .../src/MoneyAccountService.test.ts} | 95 ++++++++++++++----- .../src/MoneyAccountService.ts} | 28 +++--- packages/money-account-service/src/index.ts | 9 ++ .../src/types.ts | 10 +- .../tsconfig.build.json | 0 .../tsconfig.json | 0 .../typedoc.json | 0 teams.json | 2 +- tsconfig.build.json | 2 +- yarn.lock | 53 +++++++---- 26 files changed, 177 insertions(+), 120 deletions(-) delete mode 100644 packages/cash-account-service/README.md delete mode 100644 packages/cash-account-service/src/CashAccountService-method-action-types.ts delete mode 100644 packages/cash-account-service/src/index.ts rename packages/{cash-account-service => money-account-service}/CHANGELOG.md (100%) rename packages/{cash-account-service => money-account-service}/LICENSE (100%) create mode 100644 packages/money-account-service/README.md rename packages/{cash-account-service => money-account-service}/jest.config.js (100%) rename packages/{cash-account-service => money-account-service}/package.json (91%) create mode 100644 packages/money-account-service/src/MoneyAccountService-method-action-types.ts rename packages/{cash-account-service/src/CashAccountService.test.ts => money-account-service/src/MoneyAccountService.test.ts} (55%) rename packages/{cash-account-service/src/CashAccountService.ts => money-account-service/src/MoneyAccountService.ts} (66%) create mode 100644 packages/money-account-service/src/index.ts rename packages/{cash-account-service => money-account-service}/src/types.ts (52%) rename packages/{cash-account-service => money-account-service}/tsconfig.build.json (100%) rename packages/{cash-account-service => money-account-service}/tsconfig.json (100%) rename packages/{cash-account-service => money-account-service}/typedoc.json (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 473fe1eb33d..3ba251cca2c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -38,7 +38,7 @@ ## Earn Team /packages/earn-controller @MetaMask/metamask-earn -/packages/cash-account-service @MetaMask/metamask-earn +/packages/money-account-service @MetaMask/metamask-earn ## Social AI Team /packages/ai-controllers @MetaMask/social-ai @@ -117,7 +117,7 @@ /packages/remote-feature-flag-controller @MetaMask/extension-platform @MetaMask/mobile-platform @MetaMask/core-platform /packages/storage-service @MetaMask/extension-platform @MetaMask/mobile-platform @MetaMask/core-platform /packages/client-controller @MetaMask/core-platform @MetaMask/extension-platform @MetaMask/mobile-platform -/packages/cash-account-service @MetaMask/accounts-engineers @MetaMask/metamask-earn +/packages/money-account-service @MetaMask/accounts-engineers @MetaMask/metamask-earn ## Package Release related /packages/account-tree-controller/package.json @MetaMask/accounts-engineers @MetaMask/core-platform @@ -139,8 +139,8 @@ /packages/assets-controller/package.json @MetaMask/metamask-assets @MetaMask/core-platform /packages/assets-controller/CHANGELOG.md @MetaMask/metamask-assets @MetaMask/core-platform -/packages/cash-account-service/package.json @MetaMask/metamask-earn @MetaMask/core-platform -/packages/cash-account-service/CHANGELOG.md @MetaMask/metamask-earn @MetaMask/core-platform +/packages/money-account-service/package.json @MetaMask/metamask-earn @MetaMask/core-platform +/packages/money-account-service/CHANGELOG.md @MetaMask/metamask-earn @MetaMask/core-platform /packages/chain-agnostic-permission/package.json @MetaMask/wallet-integrations @MetaMask/core-platform /packages/chain-agnostic-permission/CHANGELOG.md @MetaMask/wallet-integrations @MetaMask/core-platform /packages/config-registry-controller/CHANGELOG.md @MetaMask/networks @MetaMask/core-platform diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index 99ea932288a..3dad8680186 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add support for `KeyringTypes.cash` in `keyringTypeToName`, mapping it to `'Cash Account'` ([#8204](https://github.com/MetaMask/core/pull/8204)) +- Add support for `KeyringTypes.money` in `keyringTypeToName`, mapping it to `'Money Account'` ([#8204](https://github.com/MetaMask/core/pull/8204)) - Add `:accounts{Added,Removed}` batch events ([#8151](https://github.com/MetaMask/core/pull/8151)) - Those new events can be used instead of single `:accountAdded` and `:accountRemoved` events to reduce the number of events emitted during batch operations (e.g. `KeyringController` state re-synchronization). diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index a8f2e49c3f5..f71452ad9d4 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -2726,7 +2726,7 @@ describe('AccountsController', () => { KeyringTypes.ledger, KeyringTypes.lattice, KeyringTypes.qr, - KeyringTypes.cash, + KeyringTypes.money, ])('should add accounts for %s type', async (keyringType) => { mockUUIDWithNormalAccounts([mockAccount]); diff --git a/packages/accounts-controller/src/utils.ts b/packages/accounts-controller/src/utils.ts index 8c4f882f48e..fc0ffde28f9 100644 --- a/packages/accounts-controller/src/utils.ts +++ b/packages/accounts-controller/src/utils.ts @@ -39,8 +39,8 @@ export function keyringTypeToName(keyringType: string): string { case KeyringTypes.qr: { return 'QR'; } - case KeyringTypes.cash: { - return 'Cash Account'; + case KeyringTypes.money: { + return 'Money Account'; } case KeyringTypes.snap: { return 'Snap Account'; diff --git a/packages/cash-account-service/README.md b/packages/cash-account-service/README.md deleted file mode 100644 index 66994ad4cd6..00000000000 --- a/packages/cash-account-service/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# `@metamask/cash-account-service` - -Cash account service. - -This service provides operations for creating and managing Cash accounts derived from HD keyrings. - -## Installation - -`yarn add @metamask/cash-account-service` - -or - -`npm install @metamask/cash-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/cash-account-service/src/CashAccountService-method-action-types.ts b/packages/cash-account-service/src/CashAccountService-method-action-types.ts deleted file mode 100644 index cec0dacad8b..00000000000 --- a/packages/cash-account-service/src/CashAccountService-method-action-types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { CashAccountService } from './CashAccountService'; - -export type CashAccountServiceCreateCashAccountAction = { - type: `CashAccountService:createCashAccount`; - handler: CashAccountService['createCashAccount']; -}; - -export type CashAccountServiceMethodActions = - CashAccountServiceCreateCashAccountAction; diff --git a/packages/cash-account-service/src/index.ts b/packages/cash-account-service/src/index.ts deleted file mode 100644 index 46d1826ad6e..00000000000 --- a/packages/cash-account-service/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type { - CashAccountServiceActions, - CashAccountServiceMessenger, -} from './types'; -export type { - CashAccountServiceCreateCashAccountAction, - CashAccountServiceMethodActions, -} from './CashAccountService-method-action-types'; -export { CashAccountService } from './CashAccountService'; diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 6230b47674b..997e8807406 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `KeyringTypes.cash` (`'Cash Keyring'`) to the `KeyringTypes` enum ([#8204](https://github.com/MetaMask/core/pull/8204)) -- Add `CashKeyring` (from `@metamask-previews/eth-cash-keyring`) as a built-in default keyring ([#8204](https://github.com/MetaMask/core/pull/8204)) +- Add `KeyringTypes.money` (`'Money Keyring'`) to the `KeyringTypes` enum ([#8204](https://github.com/MetaMask/core/pull/8204)) +- Add `MoneyKeyring` (from `@metamask/eth-money-keyring`) as a built-in default keyring ([#8204](https://github.com/MetaMask/core/pull/8204)) ### Changed diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index d470ee909e1..2b7659e841e 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -52,6 +52,7 @@ "@metamask/base-controller": "^9.0.0", "@metamask/browser-passworder": "^6.0.0", "@metamask/eth-hd-keyring": "^13.0.0", + "@metamask/eth-money-keyring": "1.0.0", "@metamask/eth-sig-util": "^8.2.0", "@metamask/eth-simple-keyring": "^11.0.0", "@metamask/keyring-api": "^21.6.0", diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index 788ec8a88b5..50807fa7ba0 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -3,6 +3,7 @@ import { isValidPrivate, getBinarySize } from '@ethereumjs/util'; import { BaseController } from '@metamask/base-controller'; import type * as encryptorUtils from '@metamask/browser-passworder'; import { HdKeyring } from '@metamask/eth-hd-keyring'; +import { MoneyKeyring } from '@metamask/eth-money-keyring'; import { normalize as ethNormalize } from '@metamask/eth-sig-util'; import SimpleKeyring from '@metamask/eth-simple-keyring'; import type { @@ -28,7 +29,6 @@ import { isValidJson, remove0x, } from '@metamask/utils'; -import { CashKeyring } from '@metamask-previews/eth-cash-keyring'; import { Mutex } from 'async-mutex'; import type { MutexInterface } from 'async-mutex'; import Wallet, { thirdparty as importers } from 'ethereumjs-wallet'; @@ -56,7 +56,7 @@ export enum KeyringTypes { /* eslint-disable @typescript-eslint/naming-convention */ simple = 'Simple Key Pair', hd = 'HD Key Tree', - cash = 'Cash Keyring', + money = 'Money Keyring', qr = 'QR Hardware Wallet Device', trezor = 'Trezor Hardware', oneKey = 'OneKey Hardware', @@ -560,7 +560,7 @@ const defaultKeyringBuilders = [ // @ts-expect-error keyring types are mismatched keyringBuilderFactory(SimpleKeyring), keyringBuilderFactory(HdKeyring), - keyringBuilderFactory(CashKeyring), + keyringBuilderFactory(MoneyKeyring), ]; export const getDefaultKeyringState = (): KeyringControllerState => { diff --git a/packages/cash-account-service/CHANGELOG.md b/packages/money-account-service/CHANGELOG.md similarity index 100% rename from packages/cash-account-service/CHANGELOG.md rename to packages/money-account-service/CHANGELOG.md diff --git a/packages/cash-account-service/LICENSE b/packages/money-account-service/LICENSE similarity index 100% rename from packages/cash-account-service/LICENSE rename to packages/money-account-service/LICENSE diff --git a/packages/money-account-service/README.md b/packages/money-account-service/README.md new file mode 100644 index 00000000000..cab3461be6d --- /dev/null +++ b/packages/money-account-service/README.md @@ -0,0 +1,17 @@ +# `@metamask/money-account-service` + +Money account service. + +This service provides operations for creating and managing Money accounts derived from HD keyrings. + +## Installation + +`yarn add @metamask/money-account-service` + +or + +`npm install @metamask/money-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/cash-account-service/jest.config.js b/packages/money-account-service/jest.config.js similarity index 100% rename from packages/cash-account-service/jest.config.js rename to packages/money-account-service/jest.config.js diff --git a/packages/cash-account-service/package.json b/packages/money-account-service/package.json similarity index 91% rename from packages/cash-account-service/package.json rename to packages/money-account-service/package.json index c7a74238623..ac075555bfc 100644 --- a/packages/cash-account-service/package.json +++ b/packages/money-account-service/package.json @@ -1,12 +1,12 @@ { - "name": "@metamask/cash-account-service", + "name": "@metamask/money-account-service", "version": "0.0.0", - "description": "Service to manage cash accounts", + "description": "Service to manage money accounts", "keywords": [ "MetaMask", "Ethereum" ], - "homepage": "https://github.com/MetaMask/core/tree/main/packages/cash-account-service#readme", + "homepage": "https://github.com/MetaMask/core/tree/main/packages/money-account-service#readme", "bugs": { "url": "https://github.com/MetaMask/core/issues" }, @@ -38,8 +38,8 @@ "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/cash-account-service", - "changelog:validate": "../../scripts/validate-changelog.sh @metamask/cash-account-service", + "changelog:update": "../../scripts/update-changelog.sh @metamask/money-account-service", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/money-account-service", "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", diff --git a/packages/money-account-service/src/MoneyAccountService-method-action-types.ts b/packages/money-account-service/src/MoneyAccountService-method-action-types.ts new file mode 100644 index 00000000000..e2a6d8baea5 --- /dev/null +++ b/packages/money-account-service/src/MoneyAccountService-method-action-types.ts @@ -0,0 +1,9 @@ +import type { MoneyAccountService } from './MoneyAccountService'; + +export type MoneyAccountServiceCreateMoneyAccountAction = { + type: `MoneyAccountService:createMoneyAccount`; + handler: MoneyAccountService['createMoneyAccount']; +}; + +export type MoneyAccountServiceMethodActions = + MoneyAccountServiceCreateMoneyAccountAction; diff --git a/packages/cash-account-service/src/CashAccountService.test.ts b/packages/money-account-service/src/MoneyAccountService.test.ts similarity index 55% rename from packages/cash-account-service/src/CashAccountService.test.ts rename to packages/money-account-service/src/MoneyAccountService.test.ts index 96da178c8f6..16d2ad32097 100644 --- a/packages/cash-account-service/src/CashAccountService.test.ts +++ b/packages/money-account-service/src/MoneyAccountService.test.ts @@ -7,11 +7,11 @@ import type { } from '@metamask/messenger'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; -import { CashAccountService, serviceName } from './CashAccountService'; -import type { CashAccountServiceMessenger } from './types'; +import { MoneyAccountService, serviceName } from './MoneyAccountService'; +import type { MoneyAccountServiceMessenger } from './types'; -type AllActions = MessengerActions; -type AllEvents = MessengerEvents; +type AllActions = MessengerActions; +type AllEvents = MessengerEvents; type RootMessenger = Messenger; @@ -25,7 +25,7 @@ const MOCK_MNEMONIC = new Uint8Array([ const MOCK_ENTROPY_SOURCE = 'mock-entropy-source-id'; function setup(): { - service: CashAccountService; + service: MoneyAccountService; rootMessenger: RootMessenger; mocks: { withKeyring: jest.Mock; @@ -37,7 +37,7 @@ function setup(): { captureException: jest.fn(), }); - const messenger: CashAccountServiceMessenger = new Messenger({ + const messenger: MoneyAccountServiceMessenger = new Messenger({ namespace: serviceName, parent: rootMessenger, }); @@ -65,7 +65,7 @@ function setup(): { }); }), addNewKeyring: jest.fn().mockResolvedValue({ - id: 'new-cash-keyring-id', + id: 'new-money-keyring-id', name: '', }), }; @@ -79,51 +79,51 @@ function setup(): { mocks.addNewKeyring, ); - const service = new CashAccountService({ messenger }); + const service = new MoneyAccountService({ messenger }); return { service, rootMessenger, mocks }; } -describe('CashAccountService', () => { - describe('createCashAccount', () => { - it('creates a Cash keyring from the HD keyring mnemonic', async () => { +describe('MoneyAccountService', () => { + describe('createMoneyAccount', () => { + it('creates a Money keyring from the HD keyring mnemonic', async () => { const { service, mocks } = setup(); - const result = await service.createCashAccount(MOCK_ENTROPY_SOURCE); + const result = await service.createMoneyAccount(MOCK_ENTROPY_SOURCE); expect(mocks.withKeyring).toHaveBeenCalledWith( { id: MOCK_ENTROPY_SOURCE }, expect.any(Function), ); - expect(mocks.addNewKeyring).toHaveBeenCalledWith(KeyringTypes.cash, { + expect(mocks.addNewKeyring).toHaveBeenCalledWith(KeyringTypes.money, { mnemonic: MOCK_MNEMONIC, }); - expect(result).toStrictEqual({ id: 'new-cash-keyring-id', name: '' }); + expect(result).toStrictEqual({ id: 'new-money-keyring-id', name: '' }); }); it('is callable via the messenger', async () => { const { rootMessenger } = setup(); const result = await rootMessenger.call( - 'CashAccountService:createCashAccount', + 'MoneyAccountService:createMoneyAccount', MOCK_ENTROPY_SOURCE, ); - expect(result).toStrictEqual({ id: 'new-cash-keyring-id', name: '' }); + expect(result).toStrictEqual({ id: 'new-money-keyring-id', name: '' }); }); - it('returns existing cash keyring metadata if a cash account already exists', async () => { + it('returns existing money keyring metadata if a money account already exists', async () => { const { service, mocks } = setup(); - const MOCK_CASH_METADATA = { id: 'existing-cash-keyring-id', name: '' }; - const MOCK_CASH_ADDRESS = '0x1234567890abcdef1234567890abcdef12345678'; + const MOCK_MONEY_METADATA = { id: 'existing-money-keyring-id', name: '' }; + const MOCK_MONEY_ADDRESS = '0x1234567890abcdef1234567890abcdef12345678'; mocks.withKeyring.mockImplementation(async (selector, operation) => { - if ('type' in selector && selector.type === KeyringTypes.cash) { + if ('type' in selector && selector.type === KeyringTypes.money) { return operation({ keyring: { - getAccounts: jest.fn().mockResolvedValue([MOCK_CASH_ADDRESS]), + getAccounts: jest.fn().mockResolvedValue([MOCK_MONEY_ADDRESS]), }, - metadata: MOCK_CASH_METADATA, + metadata: MOCK_MONEY_METADATA, }); } return operation({ @@ -135,12 +135,57 @@ describe('CashAccountService', () => { }); }); - const result = await service.createCashAccount(MOCK_ENTROPY_SOURCE); + const result = await service.createMoneyAccount(MOCK_ENTROPY_SOURCE); - expect(result).toStrictEqual(MOCK_CASH_METADATA); + expect(result).toStrictEqual(MOCK_MONEY_METADATA); expect(mocks.addNewKeyring).not.toHaveBeenCalled(); }); + it('creates a new money keyring if an existing one has no accounts', async () => { + const { service, mocks } = setup(); + + mocks.withKeyring.mockImplementation(async (selector, operation) => { + if ('type' in selector && selector.type === KeyringTypes.money) { + return operation({ + keyring: { + getAccounts: jest.fn().mockResolvedValue([]), + }, + metadata: { id: 'empty-money-keyring-id', name: '' }, + }); + } + return operation({ + keyring: { + type: 'HD Key Tree', + mnemonic: MOCK_MNEMONIC, + } as unknown as HdKeyring, + metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, + }); + }); + + await service.createMoneyAccount(MOCK_ENTROPY_SOURCE); + + expect(mocks.addNewKeyring).toHaveBeenCalledWith(KeyringTypes.money, { + mnemonic: MOCK_MNEMONIC, + }); + }); + + it('throws if the keyring is not an HD keyring', async () => { + const { service, mocks } = setup(); + + mocks.withKeyring.mockImplementation(async (_selector, operation) => { + return operation({ + keyring: { + type: 'Simple Key Pair', + } as unknown as HdKeyring, + metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, + }); + }); + + await expect( + service.createMoneyAccount(MOCK_ENTROPY_SOURCE), + ).rejects.toThrow('Got keyring without HD Keyring type'); + }); + it('throws if the HD keyring has no mnemonic', async () => { const { service, mocks } = setup(); @@ -155,7 +200,7 @@ describe('CashAccountService', () => { }); await expect( - service.createCashAccount(MOCK_ENTROPY_SOURCE), + service.createMoneyAccount(MOCK_ENTROPY_SOURCE), ).rejects.toThrow( 'HD keyring does not have a mnemonic for the given entropy source.', ); diff --git a/packages/cash-account-service/src/CashAccountService.ts b/packages/money-account-service/src/MoneyAccountService.ts similarity index 66% rename from packages/cash-account-service/src/CashAccountService.ts rename to packages/money-account-service/src/MoneyAccountService.ts index 0a3c92b9c11..74aac33a5a0 100644 --- a/packages/cash-account-service/src/CashAccountService.ts +++ b/packages/money-account-service/src/MoneyAccountService.ts @@ -2,18 +2,18 @@ import { HdKeyring } from '@metamask/eth-hd-keyring'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { KeyringMetadata } from '@metamask/keyring-controller'; -import type { CashAccountServiceMessenger } from './types'; +import type { MoneyAccountServiceMessenger } from './types'; -export const serviceName = 'CashAccountService'; +export const serviceName = 'MoneyAccountService'; -const MESSENGER_EXPOSED_METHODS = ['createCashAccount'] as const; +const MESSENGER_EXPOSED_METHODS = ['createMoneyAccount'] as const; -export class CashAccountService { - readonly #messenger: CashAccountServiceMessenger; +export class MoneyAccountService { + readonly #messenger: MoneyAccountServiceMessenger; name: typeof serviceName = serviceName; - constructor({ messenger }: { messenger: CashAccountServiceMessenger }) { + constructor({ messenger }: { messenger: MoneyAccountServiceMessenger }) { this.#messenger = messenger; this.#messenger.registerMethodActionHandlers( @@ -23,13 +23,13 @@ export class CashAccountService { } /** - * Creates a Cash keyring derived from the HD keyring identified by + * Creates a Money keyring derived from the HD keyring identified by * the given entropy source, and returns the new keyring's metadata. * * @param entropySource - The metadata id of the HD keyring to derive from. - * @returns The metadata of the newly created Cash keyring. + * @returns The metadata of the newly created Money keyring. */ - async createCashAccount(entropySource: string): Promise { + async createMoneyAccount(entropySource: string): Promise { const mnemonic = await this.#messenger.call( 'KeyringController:withKeyring', { id: entropySource }, @@ -48,10 +48,10 @@ export class CashAccountService { }, ); - const existingCashMetadata = (await this.#messenger + const existingMoneyMetadata = (await this.#messenger .call( 'KeyringController:withKeyring', - { type: KeyringTypes.cash }, + { type: KeyringTypes.money }, async ({ keyring, metadata }) => { const accounts = await keyring.getAccounts(); return accounts.length > 0 ? metadata : null; @@ -59,13 +59,13 @@ export class CashAccountService { ) .catch(() => null)) as KeyringMetadata | null; - if (existingCashMetadata) { - return existingCashMetadata; + if (existingMoneyMetadata) { + return existingMoneyMetadata; } return await this.#messenger.call( 'KeyringController:addNewKeyring', - KeyringTypes.cash, + KeyringTypes.money, { mnemonic }, ); } diff --git a/packages/money-account-service/src/index.ts b/packages/money-account-service/src/index.ts new file mode 100644 index 00000000000..a2a7b70590b --- /dev/null +++ b/packages/money-account-service/src/index.ts @@ -0,0 +1,9 @@ +export type { + MoneyAccountServiceActions, + MoneyAccountServiceMessenger, +} from './types'; +export type { + MoneyAccountServiceCreateMoneyAccountAction, + MoneyAccountServiceMethodActions, +} from './MoneyAccountService-method-action-types'; +export { MoneyAccountService } from './MoneyAccountService'; diff --git a/packages/cash-account-service/src/types.ts b/packages/money-account-service/src/types.ts similarity index 52% rename from packages/cash-account-service/src/types.ts rename to packages/money-account-service/src/types.ts index 741bd737a51..4b88d643203 100644 --- a/packages/cash-account-service/src/types.ts +++ b/packages/money-account-service/src/types.ts @@ -4,10 +4,10 @@ import type { } from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; -import type { serviceName } from './CashAccountService'; -import type { CashAccountServiceMethodActions } from './CashAccountService-method-action-types'; +import type { serviceName } from './MoneyAccountService'; +import type { MoneyAccountServiceMethodActions } from './MoneyAccountService-method-action-types'; -export type CashAccountServiceActions = CashAccountServiceMethodActions; +export type MoneyAccountServiceActions = MoneyAccountServiceMethodActions; type AllowedActions = | KeyringControllerWithKeyringAction @@ -15,8 +15,8 @@ type AllowedActions = type AllowedEvents = never; -export type CashAccountServiceMessenger = Messenger< +export type MoneyAccountServiceMessenger = Messenger< typeof serviceName, - CashAccountServiceActions | AllowedActions, + MoneyAccountServiceActions | AllowedActions, AllowedEvents >; diff --git a/packages/cash-account-service/tsconfig.build.json b/packages/money-account-service/tsconfig.build.json similarity index 100% rename from packages/cash-account-service/tsconfig.build.json rename to packages/money-account-service/tsconfig.build.json diff --git a/packages/cash-account-service/tsconfig.json b/packages/money-account-service/tsconfig.json similarity index 100% rename from packages/cash-account-service/tsconfig.json rename to packages/money-account-service/tsconfig.json diff --git a/packages/cash-account-service/typedoc.json b/packages/money-account-service/typedoc.json similarity index 100% rename from packages/cash-account-service/typedoc.json rename to packages/money-account-service/typedoc.json diff --git a/teams.json b/teams.json index 470f0a3e2cb..b46adc304a1 100644 --- a/teams.json +++ b/teams.json @@ -40,7 +40,7 @@ "metamask/base-controller": "team-core-platform", "metamask/base-data-service": "team-core-platform", "metamask/build-utils": "team-core-platform", - "metamask/cash-account-service": "team-accounts-framework,team-earn", + "metamask/money-account-service": "team-accounts-framework,team-earn", "metamask/composable-controller": "team-core-platform", "metamask/connectivity-controller": "team-core-platform", "metamask/geolocation-controller": "team-core-platform", diff --git a/tsconfig.build.json b/tsconfig.build.json index 546267cfba1..e3894a87f0d 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -56,7 +56,7 @@ "path": "./packages/build-utils/tsconfig.build.json" }, { - "path": "./packages/cash-account-service/tsconfig.build.json" + "path": "./packages/money-account-service/tsconfig.build.json" }, { "path": "./packages/chain-agnostic-permission/tsconfig.build.json" diff --git a/yarn.lock b/yarn.lock index 4f15a285aa3..779ba9c3aee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3113,26 +3113,6 @@ __metadata: languageName: unknown linkType: soft -"@metamask/cash-account-service@workspace:packages/cash-account-service": - version: 0.0.0-use.local - resolution: "@metamask/cash-account-service@workspace:packages/cash-account-service" - dependencies: - "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^9.0.0" - "@metamask/eth-hd-keyring": "npm:^13.0.0" - "@metamask/keyring-controller": "npm:^25.1.0" - "@metamask/messenger": "npm:^0.3.0" - "@ts-bridge/cli": "npm:^0.6.4" - "@types/jest": "npm:^29.5.14" - deepmerge: "npm:^4.2.2" - jest: "npm:^29.7.0" - ts-jest: "npm:^29.2.5" - typedoc: "npm:^0.25.13" - typedoc-plugin-missing-exports: "npm:^2.0.0" - typescript: "npm:~5.3.3" - languageName: unknown - linkType: soft - "@metamask/chain-agnostic-permission@npm:^1.4.0, @metamask/chain-agnostic-permission@workspace:packages/chain-agnostic-permission": version: 0.0.0-use.local resolution: "@metamask/chain-agnostic-permission@workspace:packages/chain-agnostic-permission" @@ -3702,7 +3682,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/eth-hd-keyring@npm:13.1.0, @metamask/eth-hd-keyring@npm:^13.0.0": +"@metamask/eth-hd-keyring@npm:13.1.0, @metamask/eth-hd-keyring@npm:^13.0.0, @metamask/eth-hd-keyring@npm:^13.1.0": version: 13.1.0 resolution: "@metamask/eth-hd-keyring@npm:13.1.0" dependencies: @@ -3812,6 +3792,16 @@ __metadata: languageName: unknown linkType: soft +"@metamask/eth-money-keyring@npm:1.0.0": + version: 1.0.0 + resolution: "@metamask/eth-money-keyring@npm:1.0.0" + dependencies: + "@metamask/eth-hd-keyring": "npm:^13.1.0" + "@metamask/superstruct": "npm:^3.1.0" + checksum: 10/244caa4cba12550bf0cadca4923a5540d40391e9dee940d7fe980bf77d4d08d329825745c97f89fe4909763d6978921f38b6c3f925fab9e0acd969165d5da718 + languageName: node + linkType: hard + "@metamask/eth-query@npm:^4.0.0": version: 4.0.0 resolution: "@metamask/eth-query@npm:4.0.0" @@ -4237,6 +4227,7 @@ __metadata: "@metamask/base-controller": "npm:^9.0.0" "@metamask/browser-passworder": "npm:^6.0.0" "@metamask/eth-hd-keyring": "npm:^13.0.0" + "@metamask/eth-money-keyring": "npm:1.0.0" "@metamask/eth-sig-util": "npm:^8.2.0" "@metamask/eth-simple-keyring": "npm:^11.0.0" "@metamask/keyring-api": "npm:^21.6.0" @@ -4400,6 +4391,26 @@ __metadata: languageName: node linkType: hard +"@metamask/money-account-service@workspace:packages/money-account-service": + version: 0.0.0-use.local + resolution: "@metamask/money-account-service@workspace:packages/money-account-service" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^9.0.0" + "@metamask/eth-hd-keyring": "npm:^13.0.0" + "@metamask/keyring-controller": "npm:^25.1.0" + "@metamask/messenger": "npm:^0.3.0" + "@ts-bridge/cli": "npm:^0.6.4" + "@types/jest": "npm:^29.5.14" + deepmerge: "npm:^4.2.2" + jest: "npm:^29.7.0" + ts-jest: "npm:^29.2.5" + typedoc: "npm:^0.25.13" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.3.3" + languageName: unknown + linkType: soft + "@metamask/multichain-account-service@npm:^7.1.0, @metamask/multichain-account-service@workspace:packages/multichain-account-service": version: 0.0.0-use.local resolution: "@metamask/multichain-account-service@workspace:packages/multichain-account-service" From c53287500213fa5d33861101e3d2bda75c9103b6 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 20 Mar 2026 10:38:05 +0000 Subject: [PATCH 19/29] feat: check that we see a not found error else rethrow --- packages/keyring-controller/CHANGELOG.md | 1 + packages/keyring-controller/src/index.ts | 1 + packages/money-account-service/CHANGELOG.md | 1 + .../src/MoneyAccountService.test.ts | 32 +++++++++++++++++-- .../src/MoneyAccountService.ts | 16 ++++++++-- 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 997e8807406..5373aacec64 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `KeyringTypes.money` (`'Money Keyring'`) to the `KeyringTypes` enum ([#8204](https://github.com/MetaMask/core/pull/8204)) - Add `MoneyKeyring` (from `@metamask/eth-money-keyring`) as a built-in default keyring ([#8204](https://github.com/MetaMask/core/pull/8204)) +- Export `KeyringControllerErrorMessage` enum to allow consumers to distinguish specific controller error cases ([#8204](https://github.com/MetaMask/core/pull/8204)) ### Changed diff --git a/packages/keyring-controller/src/index.ts b/packages/keyring-controller/src/index.ts index 341ad18bac3..68f49dad764 100644 --- a/packages/keyring-controller/src/index.ts +++ b/packages/keyring-controller/src/index.ts @@ -1,3 +1,4 @@ export * from './KeyringController'; export type * from './types'; export * from './errors'; +export { KeyringControllerErrorMessage } from './constants'; diff --git a/packages/money-account-service/CHANGELOG.md b/packages/money-account-service/CHANGELOG.md index 955ecf2e9f9..f9428bc7cd1 100644 --- a/packages/money-account-service/CHANGELOG.md +++ b/packages/money-account-service/CHANGELOG.md @@ -11,4 +11,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#8204](https://github.com/MetaMask/core/pull/8204)) +### Fixed [Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/money-account-service/src/MoneyAccountService.test.ts b/packages/money-account-service/src/MoneyAccountService.test.ts index 16d2ad32097..772f9ec6a41 100644 --- a/packages/money-account-service/src/MoneyAccountService.test.ts +++ b/packages/money-account-service/src/MoneyAccountService.test.ts @@ -1,5 +1,9 @@ import type { HdKeyring } from '@metamask/eth-hd-keyring'; -import { KeyringTypes } from '@metamask/keyring-controller'; +import { + KeyringControllerError, + KeyringControllerErrorMessage, + KeyringTypes, +} from '@metamask/keyring-controller'; import type { MessengerActions, MessengerEvents, @@ -54,7 +58,9 @@ function setup(): { const mocks = { withKeyring: jest.fn().mockImplementation(async (selector, operation) => { if ('type' in selector) { - throw new Error('Keyring not found'); + throw new KeyringControllerError( + KeyringControllerErrorMessage.KeyringNotFound, + ); } return operation({ keyring: { @@ -169,6 +175,28 @@ describe('MoneyAccountService', () => { }); }); + it('re-throws errors other than KeyringNotFound when checking for an existing money keyring', async () => { + const { service, mocks } = setup(); + const unexpectedError = new KeyringControllerError('Unexpected error'); + + mocks.withKeyring.mockImplementation(async (selector, operation) => { + if ('type' in selector && selector.type === KeyringTypes.money) { + throw unexpectedError; + } + return operation({ + keyring: { + type: 'HD Key Tree', + mnemonic: MOCK_MNEMONIC, + } as unknown as HdKeyring, + metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, + }); + }); + + await expect( + service.createMoneyAccount(MOCK_ENTROPY_SOURCE), + ).rejects.toThrow(unexpectedError); + }); + it('throws if the keyring is not an HD keyring', async () => { const { service, mocks } = setup(); diff --git a/packages/money-account-service/src/MoneyAccountService.ts b/packages/money-account-service/src/MoneyAccountService.ts index 74aac33a5a0..ab8da119e02 100644 --- a/packages/money-account-service/src/MoneyAccountService.ts +++ b/packages/money-account-service/src/MoneyAccountService.ts @@ -1,5 +1,9 @@ import { HdKeyring } from '@metamask/eth-hd-keyring'; -import { KeyringTypes } from '@metamask/keyring-controller'; +import { + KeyringControllerError, + KeyringControllerErrorMessage, + KeyringTypes, +} from '@metamask/keyring-controller'; import type { KeyringMetadata } from '@metamask/keyring-controller'; import type { MoneyAccountServiceMessenger } from './types'; @@ -57,7 +61,15 @@ export class MoneyAccountService { return accounts.length > 0 ? metadata : null; }, ) - .catch(() => null)) as KeyringMetadata | null; + .catch((error: unknown) => { + if ( + error instanceof KeyringControllerError && + error.message === KeyringControllerErrorMessage.KeyringNotFound + ) { + return null; + } + throw error; + })) as KeyringMetadata | null; if (existingMoneyMetadata) { return existingMoneyMetadata; From ad6b7e1316cf5f0698d763426fe3ed4dff1b32cc Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 20 Mar 2026 11:25:35 +0000 Subject: [PATCH 20/29] fix: only ever create one money account --- .../src/MoneyAccountService.test.ts | 41 ++----------------- .../src/MoneyAccountService.ts | 21 ++++++---- 2 files changed, 15 insertions(+), 47 deletions(-) diff --git a/packages/money-account-service/src/MoneyAccountService.test.ts b/packages/money-account-service/src/MoneyAccountService.test.ts index 772f9ec6a41..fec0e108e50 100644 --- a/packages/money-account-service/src/MoneyAccountService.test.ts +++ b/packages/money-account-service/src/MoneyAccountService.test.ts @@ -118,19 +118,12 @@ describe('MoneyAccountService', () => { expect(result).toStrictEqual({ id: 'new-money-keyring-id', name: '' }); }); - it('returns existing money keyring metadata if a money account already exists', async () => { + it('returns null if a money account already exists', async () => { const { service, mocks } = setup(); - const MOCK_MONEY_METADATA = { id: 'existing-money-keyring-id', name: '' }; - const MOCK_MONEY_ADDRESS = '0x1234567890abcdef1234567890abcdef12345678'; mocks.withKeyring.mockImplementation(async (selector, operation) => { if ('type' in selector && selector.type === KeyringTypes.money) { - return operation({ - keyring: { - getAccounts: jest.fn().mockResolvedValue([MOCK_MONEY_ADDRESS]), - }, - metadata: MOCK_MONEY_METADATA, - }); + return operation(); } return operation({ keyring: { @@ -143,38 +136,10 @@ describe('MoneyAccountService', () => { const result = await service.createMoneyAccount(MOCK_ENTROPY_SOURCE); - expect(result).toStrictEqual(MOCK_MONEY_METADATA); + expect(result).toBeNull(); expect(mocks.addNewKeyring).not.toHaveBeenCalled(); }); - it('creates a new money keyring if an existing one has no accounts', async () => { - const { service, mocks } = setup(); - - mocks.withKeyring.mockImplementation(async (selector, operation) => { - if ('type' in selector && selector.type === KeyringTypes.money) { - return operation({ - keyring: { - getAccounts: jest.fn().mockResolvedValue([]), - }, - metadata: { id: 'empty-money-keyring-id', name: '' }, - }); - } - return operation({ - keyring: { - type: 'HD Key Tree', - mnemonic: MOCK_MNEMONIC, - } as unknown as HdKeyring, - metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, - }); - }); - - await service.createMoneyAccount(MOCK_ENTROPY_SOURCE); - - expect(mocks.addNewKeyring).toHaveBeenCalledWith(KeyringTypes.money, { - mnemonic: MOCK_MNEMONIC, - }); - }); - it('re-throws errors other than KeyringNotFound when checking for an existing money keyring', async () => { const { service, mocks } = setup(); const unexpectedError = new KeyringControllerError('Unexpected error'); diff --git a/packages/money-account-service/src/MoneyAccountService.ts b/packages/money-account-service/src/MoneyAccountService.ts index ab8da119e02..d91b2e28c33 100644 --- a/packages/money-account-service/src/MoneyAccountService.ts +++ b/packages/money-account-service/src/MoneyAccountService.ts @@ -30,10 +30,14 @@ export class MoneyAccountService { * Creates a Money keyring derived from the HD keyring identified by * the given entropy source, and returns the new keyring's metadata. * + * If a keyring already existed, just returns null + * * @param entropySource - The metadata id of the HD keyring to derive from. * @returns The metadata of the newly created Money keyring. */ - async createMoneyAccount(entropySource: string): Promise { + async createMoneyAccount( + entropySource: string, + ): Promise { const mnemonic = await this.#messenger.call( 'KeyringController:withKeyring', { id: entropySource }, @@ -52,13 +56,12 @@ export class MoneyAccountService { }, ); - const existingMoneyMetadata = (await this.#messenger + const moneyAccountExists = await this.#messenger .call( 'KeyringController:withKeyring', { type: KeyringTypes.money }, - async ({ keyring, metadata }) => { - const accounts = await keyring.getAccounts(); - return accounts.length > 0 ? metadata : null; + async () => { + return true; }, ) .catch((error: unknown) => { @@ -66,13 +69,13 @@ export class MoneyAccountService { error instanceof KeyringControllerError && error.message === KeyringControllerErrorMessage.KeyringNotFound ) { - return null; + return false; } throw error; - })) as KeyringMetadata | null; + }); - if (existingMoneyMetadata) { - return existingMoneyMetadata; + if (moneyAccountExists) { + return null; } return await this.#messenger.call( From b8bdf65f00bf255f22a3eb42f13263582bc24529 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 20 Mar 2026 11:30:13 +0000 Subject: [PATCH 21/29] feat: add getMoneyAccount method to the money service --- ...MoneyAccountService-method-action-types.ts | 8 +- .../src/MoneyAccountService.test.ts | 75 +++++++++++++++++++ .../src/MoneyAccountService.ts | 28 ++++++- 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/packages/money-account-service/src/MoneyAccountService-method-action-types.ts b/packages/money-account-service/src/MoneyAccountService-method-action-types.ts index e2a6d8baea5..9e08b81937b 100644 --- a/packages/money-account-service/src/MoneyAccountService-method-action-types.ts +++ b/packages/money-account-service/src/MoneyAccountService-method-action-types.ts @@ -5,5 +5,11 @@ export type MoneyAccountServiceCreateMoneyAccountAction = { handler: MoneyAccountService['createMoneyAccount']; }; +export type MoneyAccountServiceGetMoneyAccountAction = { + type: `MoneyAccountService:getMoneyAccount`; + handler: MoneyAccountService['getMoneyAccount']; +}; + export type MoneyAccountServiceMethodActions = - MoneyAccountServiceCreateMoneyAccountAction; + | MoneyAccountServiceCreateMoneyAccountAction + | MoneyAccountServiceGetMoneyAccountAction; diff --git a/packages/money-account-service/src/MoneyAccountService.test.ts b/packages/money-account-service/src/MoneyAccountService.test.ts index fec0e108e50..d8b8e075412 100644 --- a/packages/money-account-service/src/MoneyAccountService.test.ts +++ b/packages/money-account-service/src/MoneyAccountService.test.ts @@ -199,4 +199,79 @@ describe('MoneyAccountService', () => { ); }); }); + + describe('getMoneyAccount', () => { + it('returns the money keyring metadata if one exists', async () => { + const { service, mocks } = setup(); + const MOCK_MONEY_METADATA = { id: 'existing-money-keyring-id', name: '' }; + + mocks.withKeyring.mockImplementation(async (selector, operation) => { + if ('type' in selector && selector.type === KeyringTypes.money) { + return operation({ + keyring: {}, + metadata: MOCK_MONEY_METADATA, + }); + } + return operation({ + keyring: { + type: 'HD Key Tree', + mnemonic: MOCK_MNEMONIC, + } as unknown as HdKeyring, + metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, + }); + }); + + const result = await service.getMoneyAccount(); + + expect(result).toStrictEqual(MOCK_MONEY_METADATA); + }); + + it('returns null if no money account exists', async () => { + const { service } = setup(); + + const result = await service.getMoneyAccount(); + + expect(result).toBeNull(); + }); + + it('is callable via the messenger', async () => { + const { rootMessenger, mocks } = setup(); + const MOCK_MONEY_METADATA = { id: 'existing-money-keyring-id', name: '' }; + + mocks.withKeyring.mockImplementation(async (selector, operation) => { + if ('type' in selector && selector.type === KeyringTypes.money) { + return operation({ + keyring: {}, + metadata: MOCK_MONEY_METADATA, + }); + } + return operation({ + keyring: { + type: 'HD Key Tree', + mnemonic: MOCK_MNEMONIC, + } as unknown as HdKeyring, + metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, + }); + }); + + const result = await rootMessenger.call( + 'MoneyAccountService:getMoneyAccount', + ); + + expect(result).toStrictEqual(MOCK_MONEY_METADATA); + }); + + it('re-throws errors other than KeyringNotFound', async () => { + const { service, mocks } = setup(); + const unexpectedError = new KeyringControllerError('Unexpected error'); + + mocks.withKeyring.mockImplementation(async (selector) => { + if ('type' in selector && selector.type === KeyringTypes.money) { + throw unexpectedError; + } + }); + + await expect(service.getMoneyAccount()).rejects.toThrow(unexpectedError); + }); + }); }); diff --git a/packages/money-account-service/src/MoneyAccountService.ts b/packages/money-account-service/src/MoneyAccountService.ts index d91b2e28c33..7fa97c59f74 100644 --- a/packages/money-account-service/src/MoneyAccountService.ts +++ b/packages/money-account-service/src/MoneyAccountService.ts @@ -10,7 +10,10 @@ import type { MoneyAccountServiceMessenger } from './types'; export const serviceName = 'MoneyAccountService'; -const MESSENGER_EXPOSED_METHODS = ['createMoneyAccount'] as const; +const MESSENGER_EXPOSED_METHODS = [ + 'createMoneyAccount', + 'getMoneyAccount', +] as const; export class MoneyAccountService { readonly #messenger: MoneyAccountServiceMessenger; @@ -84,4 +87,27 @@ export class MoneyAccountService { { mnemonic }, ); } + + /** + * Returns the Money keyring metadata if one exists, otherwise null. + * + * @returns The metadata of the Money keyring, or null if none exists. + */ + async getMoneyAccount(): Promise { + return (await this.#messenger + .call( + 'KeyringController:withKeyring', + { type: KeyringTypes.money }, + async ({ metadata }) => metadata, + ) + .catch((error: unknown) => { + if ( + error instanceof KeyringControllerError && + error.message === KeyringControllerErrorMessage.KeyringNotFound + ) { + return null; + } + throw error; + })) as KeyringMetadata | null; + } } From 355208d94bd3c6bfc3514f96983df93e7a9aee38 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 20 Mar 2026 11:36:59 +0000 Subject: [PATCH 22/29] fix: lock file and depenendecies after rebase --- packages/keyring-controller/package.json | 1 - yarn.lock | 12 +----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index 2b7659e841e..b7d998edf4b 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -48,7 +48,6 @@ }, "dependencies": { "@ethereumjs/util": "^9.1.0", - "@metamask-previews/eth-cash-keyring": "0.0.0-25e4b24", "@metamask/base-controller": "^9.0.0", "@metamask/browser-passworder": "^6.0.0", "@metamask/eth-hd-keyring": "^13.0.0", diff --git a/yarn.lock b/yarn.lock index 779ba9c3aee..6583de74b47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2485,15 +2485,6 @@ __metadata: languageName: node linkType: hard -"@metamask-previews/eth-cash-keyring@npm:0.0.0-25e4b24": - version: 0.0.0-25e4b24 - resolution: "@metamask-previews/eth-cash-keyring@npm:0.0.0-25e4b24" - dependencies: - "@metamask/eth-hd-keyring": "npm:13.1.0" - checksum: 10/468cd1afb1a498406cd2b4dd01f5059fc7313739b30c9ed066789f779dc784b7dc5baee982565da334671700a4f0a0bf2c45818040b7b656452700ff36b1a07b - languageName: node - linkType: hard - "@metamask/7715-permission-types@npm:^0.5.0": version: 0.5.0 resolution: "@metamask/7715-permission-types@npm:0.5.0" @@ -3682,7 +3673,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/eth-hd-keyring@npm:13.1.0, @metamask/eth-hd-keyring@npm:^13.0.0, @metamask/eth-hd-keyring@npm:^13.1.0": +"@metamask/eth-hd-keyring@npm:^13.0.0, @metamask/eth-hd-keyring@npm:^13.1.0": version: 13.1.0 resolution: "@metamask/eth-hd-keyring@npm:13.1.0" dependencies: @@ -4222,7 +4213,6 @@ __metadata: "@ethereumjs/util": "npm:^9.1.0" "@lavamoat/allow-scripts": "npm:^3.0.4" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" - "@metamask-previews/eth-cash-keyring": "npm:0.0.0-25e4b24" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^9.0.0" "@metamask/browser-passworder": "npm:^6.0.0" From 522a7ec3bd33775bd401f121280492896f76f5b7 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 20 Mar 2026 11:42:25 +0000 Subject: [PATCH 23/29] chore: lint money package --- packages/money-account-service/CHANGELOG.md | 1 + yarn.lock | 19 +------------------ 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/money-account-service/CHANGELOG.md b/packages/money-account-service/CHANGELOG.md index f9428bc7cd1..eeca08c5cb0 100644 --- a/packages/money-account-service/CHANGELOG.md +++ b/packages/money-account-service/CHANGELOG.md @@ -12,4 +12,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#8204](https://github.com/MetaMask/core/pull/8204)) ### Fixed + [Unreleased]: https://github.com/MetaMask/core/ diff --git a/yarn.lock b/yarn.lock index 6583de74b47..467fc4d123e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4170,24 +4170,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^21.3.0": - version: 21.5.0 - resolution: "@metamask/keyring-api@npm:21.5.0" - dependencies: - "@ethereumjs/tx": "npm:^5.4.0" - "@metamask/eth-sig-util": "npm:^8.2.0" - "@metamask/keyring-utils": "npm:^3.2.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.1.0" - "@types/uuid": "npm:^9.0.8" - async-mutex: "npm:^0.5.0" - bitcoin-address-validation: "npm:^2.2.3" - uuid: "npm:^9.0.1" - checksum: 10/a7f2a8c66bc76edabde15b66b80904208b71fd62406ebb91579db4c65d74a8f66db6c610afe265823313df7423b6727a4de03cb1f43e41d840d51a5039f9ef4d - languageName: node - linkType: hard - -"@metamask/keyring-api@npm:^21.4.0, @metamask/keyring-api@npm:^21.6.0": +"@metamask/keyring-api@npm:^21.3.0, @metamask/keyring-api@npm:^21.4.0, @metamask/keyring-api@npm:^21.6.0": version: 21.6.0 resolution: "@metamask/keyring-api@npm:21.6.0" dependencies: From a23ba718877d628b37783513bc18a0c00b207108 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 20 Mar 2026 12:13:45 +0000 Subject: [PATCH 24/29] chore: fix changelog --- packages/money-account-service/CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/money-account-service/CHANGELOG.md b/packages/money-account-service/CHANGELOG.md index eeca08c5cb0..955ecf2e9f9 100644 --- a/packages/money-account-service/CHANGELOG.md +++ b/packages/money-account-service/CHANGELOG.md @@ -11,6 +11,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#8204](https://github.com/MetaMask/core/pull/8204)) -### Fixed - [Unreleased]: https://github.com/MetaMask/core/ From e9620906a9de4fe8d449d8b447a6f0256440876c Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 20 Mar 2026 12:55:25 +0000 Subject: [PATCH 25/29] fix: avoid race condition when creating money keyring --- .../src/MoneyAccountService.test.ts | 42 ++++--------------- .../src/MoneyAccountService.ts | 24 +++-------- packages/money-account-service/src/types.ts | 4 +- 3 files changed, 18 insertions(+), 52 deletions(-) diff --git a/packages/money-account-service/src/MoneyAccountService.test.ts b/packages/money-account-service/src/MoneyAccountService.test.ts index d8b8e075412..df26854e150 100644 --- a/packages/money-account-service/src/MoneyAccountService.test.ts +++ b/packages/money-account-service/src/MoneyAccountService.test.ts @@ -34,6 +34,7 @@ function setup(): { mocks: { withKeyring: jest.Mock; addNewKeyring: jest.Mock; + getState: jest.Mock; }; } { const rootMessenger: RootMessenger = new Messenger({ @@ -51,6 +52,7 @@ function setup(): { actions: [ 'KeyringController:withKeyring', 'KeyringController:addNewKeyring', + 'KeyringController:getState', ], events: [], }); @@ -74,6 +76,7 @@ function setup(): { id: 'new-money-keyring-id', name: '', }), + getState: jest.fn().mockReturnValue({ keyrings: [] }), }; rootMessenger.registerActionHandler( @@ -84,6 +87,10 @@ function setup(): { 'KeyringController:addNewKeyring', mocks.addNewKeyring, ); + rootMessenger.registerActionHandler( + 'KeyringController:getState', + mocks.getState, + ); const service = new MoneyAccountService({ messenger }); @@ -121,17 +128,8 @@ describe('MoneyAccountService', () => { it('returns null if a money account already exists', async () => { const { service, mocks } = setup(); - mocks.withKeyring.mockImplementation(async (selector, operation) => { - if ('type' in selector && selector.type === KeyringTypes.money) { - return operation(); - } - return operation({ - keyring: { - type: 'HD Key Tree', - mnemonic: MOCK_MNEMONIC, - } as unknown as HdKeyring, - metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, - }); + mocks.getState.mockReturnValue({ + keyrings: [{ type: KeyringTypes.money }], }); const result = await service.createMoneyAccount(MOCK_ENTROPY_SOURCE); @@ -140,28 +138,6 @@ describe('MoneyAccountService', () => { expect(mocks.addNewKeyring).not.toHaveBeenCalled(); }); - it('re-throws errors other than KeyringNotFound when checking for an existing money keyring', async () => { - const { service, mocks } = setup(); - const unexpectedError = new KeyringControllerError('Unexpected error'); - - mocks.withKeyring.mockImplementation(async (selector, operation) => { - if ('type' in selector && selector.type === KeyringTypes.money) { - throw unexpectedError; - } - return operation({ - keyring: { - type: 'HD Key Tree', - mnemonic: MOCK_MNEMONIC, - } as unknown as HdKeyring, - metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, - }); - }); - - await expect( - service.createMoneyAccount(MOCK_ENTROPY_SOURCE), - ).rejects.toThrow(unexpectedError); - }); - it('throws if the keyring is not an HD keyring', async () => { const { service, mocks } = setup(); diff --git a/packages/money-account-service/src/MoneyAccountService.ts b/packages/money-account-service/src/MoneyAccountService.ts index 7fa97c59f74..1e7101a7c37 100644 --- a/packages/money-account-service/src/MoneyAccountService.ts +++ b/packages/money-account-service/src/MoneyAccountService.ts @@ -59,25 +59,13 @@ export class MoneyAccountService { }, ); - const moneyAccountExists = await this.#messenger - .call( - 'KeyringController:withKeyring', - { type: KeyringTypes.money }, - async () => { - return true; - }, - ) - .catch((error: unknown) => { - if ( - error instanceof KeyringControllerError && - error.message === KeyringControllerErrorMessage.KeyringNotFound - ) { - return false; - } - throw error; - }); + const { keyrings } = this.#messenger.call('KeyringController:getState'); + + const moneyKeyringExists = keyrings.some( + (keyring) => keyring.type === KeyringTypes.money, + ); - if (moneyAccountExists) { + if (moneyKeyringExists) { return null; } diff --git a/packages/money-account-service/src/types.ts b/packages/money-account-service/src/types.ts index 4b88d643203..fba68670247 100644 --- a/packages/money-account-service/src/types.ts +++ b/packages/money-account-service/src/types.ts @@ -1,6 +1,7 @@ import type { KeyringControllerAddNewKeyringAction, KeyringControllerWithKeyringAction, + KeyringControllerGetStateAction, } from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; @@ -11,7 +12,8 @@ export type MoneyAccountServiceActions = MoneyAccountServiceMethodActions; type AllowedActions = | KeyringControllerWithKeyringAction - | KeyringControllerAddNewKeyringAction; + | KeyringControllerAddNewKeyringAction + | KeyringControllerGetStateAction; type AllowedEvents = never; From 7d53b871b2c2a4ba6780c6a3c1fd57c497ad4a62 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 20 Mar 2026 14:41:51 +0000 Subject: [PATCH 26/29] fix: deserialize mnemonic in money account service --- .../money-account-service/src/MoneyAccountService.test.ts | 1 + packages/money-account-service/src/MoneyAccountService.ts | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/money-account-service/src/MoneyAccountService.test.ts b/packages/money-account-service/src/MoneyAccountService.test.ts index df26854e150..eb12d5dfd6e 100644 --- a/packages/money-account-service/src/MoneyAccountService.test.ts +++ b/packages/money-account-service/src/MoneyAccountService.test.ts @@ -68,6 +68,7 @@ function setup(): { keyring: { type: 'HD Key Tree', mnemonic: MOCK_MNEMONIC, + serialize: jest.fn().mockResolvedValue({ mnemonic: MOCK_MNEMONIC }), } as unknown as HdKeyring, metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, }); diff --git a/packages/money-account-service/src/MoneyAccountService.ts b/packages/money-account-service/src/MoneyAccountService.ts index 1e7101a7c37..2d9f426be3d 100644 --- a/packages/money-account-service/src/MoneyAccountService.ts +++ b/packages/money-account-service/src/MoneyAccountService.ts @@ -41,7 +41,7 @@ export class MoneyAccountService { async createMoneyAccount( entropySource: string, ): Promise { - const mnemonic = await this.#messenger.call( + const mnemonic = (await this.#messenger.call( 'KeyringController:withKeyring', { id: entropySource }, async ({ keyring }) => { @@ -55,9 +55,10 @@ export class MoneyAccountService { 'HD keyring does not have a mnemonic for the given entropy source.', ); } - return hdKeyring.mnemonic; + + return (await hdKeyring.serialize()).mnemonic; }, - ); + )) as number[]; const { keyrings } = this.#messenger.call('KeyringController:getState'); From f342399faf8d1380227c38d67d55bbbc2396191e Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 20 Mar 2026 15:38:48 +0000 Subject: [PATCH 27/29] feat: add money account service test that interacts with real money keyring --- packages/money-account-service/package.json | 1 + .../src/MoneyAccountService.test.ts | 47 +++++++++++++++++++ .../src/MoneyAccountService.ts | 3 +- yarn.lock | 3 +- 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/packages/money-account-service/package.json b/packages/money-account-service/package.json index ac075555bfc..f65d57ef098 100644 --- a/packages/money-account-service/package.json +++ b/packages/money-account-service/package.json @@ -49,6 +49,7 @@ "dependencies": { "@metamask/base-controller": "^9.0.0", "@metamask/eth-hd-keyring": "^13.0.0", + "@metamask/eth-money-keyring": "^1.0.0", "@metamask/keyring-controller": "^25.1.0", "@metamask/messenger": "^0.3.0" }, diff --git a/packages/money-account-service/src/MoneyAccountService.test.ts b/packages/money-account-service/src/MoneyAccountService.test.ts index eb12d5dfd6e..b7e3374c7e9 100644 --- a/packages/money-account-service/src/MoneyAccountService.test.ts +++ b/packages/money-account-service/src/MoneyAccountService.test.ts @@ -1,4 +1,9 @@ import type { HdKeyring } from '@metamask/eth-hd-keyring'; +import { + MoneyKeyring, + MONEY_DERIVATION_PATH, +} from '@metamask/eth-money-keyring'; +import type { MoneyKeyringSerializedState } from '@metamask/eth-money-keyring'; import { KeyringControllerError, KeyringControllerErrorMessage, @@ -111,6 +116,8 @@ describe('MoneyAccountService', () => { ); expect(mocks.addNewKeyring).toHaveBeenCalledWith(KeyringTypes.money, { mnemonic: MOCK_MNEMONIC, + numberOfAccounts: 1, + hdPath: MONEY_DERIVATION_PATH, }); expect(result).toStrictEqual({ id: 'new-money-keyring-id', name: '' }); }); @@ -156,6 +163,46 @@ describe('MoneyAccountService', () => { ).rejects.toThrow('Got keyring without HD Keyring type'); }); + it('passes params to addNewKeyring that produce a correctly initialized MoneyKeyring', async () => { + const { service, mocks } = setup(); + + // The real HdKeyring.serialize() returns mnemonic as number[] (via Array.from), + // not Uint8Array, so we match that format here for MoneyKeyring.deserialize to accept it. + mocks.withKeyring.mockImplementation(async (selector, operation) => { + if ('type' in selector) { + throw new KeyringControllerError( + KeyringControllerErrorMessage.KeyringNotFound, + ); + } + return operation({ + keyring: { + type: 'HD Key Tree', + mnemonic: MOCK_MNEMONIC, + serialize: jest + .fn() + .mockResolvedValue({ mnemonic: Array.from(MOCK_MNEMONIC) }), + } as unknown as HdKeyring, + metadata: { id: MOCK_ENTROPY_SOURCE, name: '' }, + }); + }); + + let capturedOpts: unknown; + mocks.addNewKeyring.mockImplementation(async (_type, opts) => { + capturedOpts = opts; + return { id: 'new-money-keyring-id', name: '' }; + }); + + await service.createMoneyAccount(MOCK_ENTROPY_SOURCE); + + const moneyKeyring = new MoneyKeyring(); + await moneyKeyring.deserialize( + capturedOpts as MoneyKeyringSerializedState, + ); + + expect(moneyKeyring.hdPath).toBe(MONEY_DERIVATION_PATH); + expect(await moneyKeyring.getAccounts()).toHaveLength(1); + }); + it('throws if the HD keyring has no mnemonic', async () => { const { service, mocks } = setup(); diff --git a/packages/money-account-service/src/MoneyAccountService.ts b/packages/money-account-service/src/MoneyAccountService.ts index 2d9f426be3d..e68fdd7e3fe 100644 --- a/packages/money-account-service/src/MoneyAccountService.ts +++ b/packages/money-account-service/src/MoneyAccountService.ts @@ -1,4 +1,5 @@ import { HdKeyring } from '@metamask/eth-hd-keyring'; +import { MONEY_DERIVATION_PATH } from '@metamask/eth-money-keyring'; import { KeyringControllerError, KeyringControllerErrorMessage, @@ -73,7 +74,7 @@ export class MoneyAccountService { return await this.#messenger.call( 'KeyringController:addNewKeyring', KeyringTypes.money, - { mnemonic }, + { mnemonic, numberOfAccounts: 1, hdPath: MONEY_DERIVATION_PATH }, ); } diff --git a/yarn.lock b/yarn.lock index 467fc4d123e..3a4681519ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3783,7 +3783,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/eth-money-keyring@npm:1.0.0": +"@metamask/eth-money-keyring@npm:1.0.0, @metamask/eth-money-keyring@npm:^1.0.0": version: 1.0.0 resolution: "@metamask/eth-money-keyring@npm:1.0.0" dependencies: @@ -4371,6 +4371,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^9.0.0" "@metamask/eth-hd-keyring": "npm:^13.0.0" + "@metamask/eth-money-keyring": "npm:^1.0.0" "@metamask/keyring-controller": "npm:^25.1.0" "@metamask/messenger": "npm:^0.3.0" "@ts-bridge/cli": "npm:^0.6.4" From 04f063ba1836abc05f8dfcad2343127d5c8203f1 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 20 Mar 2026 16:19:23 +0000 Subject: [PATCH 28/29] chore: add a carat to keyring version in package.json --- packages/keyring-controller/package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index b7d998edf4b..5cf26876dd0 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -51,7 +51,7 @@ "@metamask/base-controller": "^9.0.0", "@metamask/browser-passworder": "^6.0.0", "@metamask/eth-hd-keyring": "^13.0.0", - "@metamask/eth-money-keyring": "1.0.0", + "@metamask/eth-money-keyring": "^1.0.0", "@metamask/eth-sig-util": "^8.2.0", "@metamask/eth-simple-keyring": "^11.0.0", "@metamask/keyring-api": "^21.6.0", diff --git a/yarn.lock b/yarn.lock index 3a4681519ec..da160fb29a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3783,7 +3783,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/eth-money-keyring@npm:1.0.0, @metamask/eth-money-keyring@npm:^1.0.0": +"@metamask/eth-money-keyring@npm:^1.0.0": version: 1.0.0 resolution: "@metamask/eth-money-keyring@npm:1.0.0" dependencies: @@ -4200,7 +4200,7 @@ __metadata: "@metamask/base-controller": "npm:^9.0.0" "@metamask/browser-passworder": "npm:^6.0.0" "@metamask/eth-hd-keyring": "npm:^13.0.0" - "@metamask/eth-money-keyring": "npm:1.0.0" + "@metamask/eth-money-keyring": "npm:^1.0.0" "@metamask/eth-sig-util": "npm:^8.2.0" "@metamask/eth-simple-keyring": "npm:^11.0.0" "@metamask/keyring-api": "npm:^21.6.0" From 638782ec77271c77af7c6bd5ec5b7daf50cfd81c Mon Sep 17 00:00:00 2001 From: John Whiles Date: Fri, 20 Mar 2026 16:33:20 +0000 Subject: [PATCH 29/29] =?UTF-8?q?fix:=20don=E2=80=99t=20export=20method=20?= =?UTF-8?q?action=20type=20from=20money=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/money-account-service/src/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/money-account-service/src/index.ts b/packages/money-account-service/src/index.ts index a2a7b70590b..02f76ce0381 100644 --- a/packages/money-account-service/src/index.ts +++ b/packages/money-account-service/src/index.ts @@ -2,8 +2,5 @@ export type { MoneyAccountServiceActions, MoneyAccountServiceMessenger, } from './types'; -export type { - MoneyAccountServiceCreateMoneyAccountAction, - MoneyAccountServiceMethodActions, -} from './MoneyAccountService-method-action-types'; +export type { MoneyAccountServiceCreateMoneyAccountAction } from './MoneyAccountService-method-action-types'; export { MoneyAccountService } from './MoneyAccountService';