From 01e9b649bfb68a6e22e94cd7f66d587ce82d8de8 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Mon, 9 Mar 2026 13:53:06 +0000 Subject: [PATCH 01/12] feat: add cash account keyring based on hd-key-ring --- README.md | 3 + packages/keyring-api/CHANGELOG.md | 1 + .../keyring-api/src/api/v2/keyring-type.ts | 5 + .../keyring-api/src/api/v2/keyring.test-d.ts | 1 + .../keyring-eth-cash-account/CHANGELOG.md | 16 ++ packages/keyring-eth-cash-account/LICENSE | 15 ++ packages/keyring-eth-cash-account/README.md | 38 +++ .../keyring-eth-cash-account/jest.config.js | 19 ++ .../keyring-eth-cash-account/package.json | 71 +++++ .../src/cash-account-keyring.test.ts | 242 ++++++++++++++++++ .../src/cash-account-keyring.ts | 25 ++ .../keyring-eth-cash-account/src/index.ts | 1 + .../tsconfig.build.json | 16 ++ .../keyring-eth-cash-account/tsconfig.json | 11 + tsconfig.build.json | 1 + yarn.lock | 19 +- 16 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 packages/keyring-eth-cash-account/CHANGELOG.md create mode 100644 packages/keyring-eth-cash-account/LICENSE create mode 100644 packages/keyring-eth-cash-account/README.md create mode 100644 packages/keyring-eth-cash-account/jest.config.js create mode 100644 packages/keyring-eth-cash-account/package.json create mode 100644 packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts create mode 100644 packages/keyring-eth-cash-account/src/cash-account-keyring.ts create mode 100644 packages/keyring-eth-cash-account/src/index.ts create mode 100644 packages/keyring-eth-cash-account/tsconfig.build.json create mode 100644 packages/keyring-eth-cash-account/tsconfig.json diff --git a/README.md b/README.md index d3bba33aa..29db92314 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ This repository contains the following packages [^fn1]: - [`@metamask/account-api`](packages/account-api) +- [`@metamask/eth-cash-account-keyring`](packages/keyring-eth-cash-account) - [`@metamask/eth-hd-keyring`](packages/keyring-eth-hd) - [`@metamask/eth-ledger-bridge-keyring`](packages/keyring-eth-ledger-bridge) - [`@metamask/eth-qr-keyring`](packages/keyring-eth-qr) @@ -40,6 +41,7 @@ linkStyle default opacity:0.5 account_api(["@metamask/account-api"]); hw_wallet_sdk(["@metamask/hw-wallet-sdk"]); keyring_api(["@metamask/keyring-api"]); + eth_cash_account_keyring(["@metamask/eth-cash-account-keyring"]); eth_hd_keyring(["@metamask/eth-hd-keyring"]); eth_ledger_bridge_keyring(["@metamask/eth-ledger-bridge-keyring"]); eth_qr_keyring(["@metamask/eth-qr-keyring"]); @@ -54,6 +56,7 @@ linkStyle default opacity:0.5 account_api --> keyring_api; account_api --> keyring_utils; keyring_api --> keyring_utils; + eth_cash_account_keyring --> keyring_eth_hd; eth_hd_keyring --> keyring_api; eth_hd_keyring --> keyring_utils; eth_hd_keyring --> account_api; diff --git a/packages/keyring-api/CHANGELOG.md b/packages/keyring-api/CHANGELOG.md index 0d9df7907..27d5d8c02 100644 --- a/packages/keyring-api/CHANGELOG.md +++ b/packages/keyring-api/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `CashAccount` variant to `KeyringType` enum ([#472](https://github.com/MetaMask/accounts/pull/472)) - Add optional `details` field to `Transaction` type ([#445](https://github.com/MetaMask/accounts/pull/445)) - Add `SecurityAlertResponse` enum with values: `benign`, `warning`, `malicious` - Add optional `origin` field (string) to track transaction request source diff --git a/packages/keyring-api/src/api/v2/keyring-type.ts b/packages/keyring-api/src/api/v2/keyring-type.ts index aedd48d94..0ba79798e 100644 --- a/packages/keyring-api/src/api/v2/keyring-type.ts +++ b/packages/keyring-api/src/api/v2/keyring-type.ts @@ -43,4 +43,9 @@ export enum KeyringType { * Represents keyring backed by a OneKey hardware wallet. */ OneKey = 'onekey', + + /** + * A keyring for the cash account + */ + CashAccount = 'cash-account', } diff --git a/packages/keyring-api/src/api/v2/keyring.test-d.ts b/packages/keyring-api/src/api/v2/keyring.test-d.ts index 25fb82339..048697709 100644 --- a/packages/keyring-api/src/api/v2/keyring.test-d.ts +++ b/packages/keyring-api/src/api/v2/keyring.test-d.ts @@ -22,6 +22,7 @@ import type { ImportPrivateKeyFormat } from './private-key'; // Test KeyringType enum expectAssignable(KeyringType.Hd); +expectAssignable(KeyringType.CashAccount); expectAssignable(KeyringType.PrivateKey); expectAssignable(KeyringType.Qr); expectAssignable(KeyringType.Snap); diff --git a/packages/keyring-eth-cash-account/CHANGELOG.md b/packages/keyring-eth-cash-account/CHANGELOG.md new file mode 100644 index 000000000..2d00805c1 --- /dev/null +++ b/packages/keyring-eth-cash-account/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Add `CashAccountKeyring` class extending `HdKeyring` from `@metamask/eth-hd-keyring` ([#472](https://github.com/MetaMask/accounts/pull/472)) + - Uses keyring type `Cash Account Keyring` + - Uses derivation path `m/44'/4392018'/0'/0` + +[Unreleased]: https://github.com/MetaMask/accounts/ diff --git a/packages/keyring-eth-cash-account/LICENSE b/packages/keyring-eth-cash-account/LICENSE new file mode 100644 index 000000000..b5ed1b9c5 --- /dev/null +++ b/packages/keyring-eth-cash-account/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2020 MetaMask + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/packages/keyring-eth-cash-account/README.md b/packages/keyring-eth-cash-account/README.md new file mode 100644 index 000000000..95751d74b --- /dev/null +++ b/packages/keyring-eth-cash-account/README.md @@ -0,0 +1,38 @@ +# Cash Account Keyring + +An Ethereum keyring that extends [`@metamask/eth-hd-keyring`](../keyring-eth-hd) with a distinct keyring type and derivation path for cash accounts. + +Cash accounts use a separate HD derivation path to keep funds isolated from the primary HD keyring, while reusing the same seed phrase and signing infrastructure. + +## Installation + +`yarn add @metamask/eth-cash-account-keyring` + +or + +`npm install @metamask/eth-cash-account-keyring` + +## Usage + +```ts +import { CashAccountKeyring } from '@metamask/eth-cash-account-keyring'; + +const keyring = new CashAccountKeyring(); +``` + +The `CashAccountKeyring` class implements the same `Keyring` interface as `HdKeyring` — see the [HD Keyring README](../keyring-eth-hd/README.md) for full API documentation. + +## Contributing + +### Setup + +- Install [Node.js](https://nodejs.org) version 18 + - If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended) running `nvm use` will automatically choose the right node version for you. +- Install [Yarn v3](https://yarnpkg.com/getting-started/install) +- Run `yarn install` to install dependencies and run any required post-install scripts + +### Testing and Linting + +Run `yarn test` to run the tests once. + +Run `yarn lint` to run the linter, or run `yarn lint:fix` to run the linter and fix any automatically fixable issues. diff --git a/packages/keyring-eth-cash-account/jest.config.js b/packages/keyring-eth-cash-account/jest.config.js new file mode 100644 index 000000000..7eb53e602 --- /dev/null +++ b/packages/keyring-eth-cash-account/jest.config.js @@ -0,0 +1,19 @@ +/* + * 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, + + // The glob patterns Jest uses to detect test files + testMatch: ['**/*.test.[jt]s?(x)'], +}); diff --git a/packages/keyring-eth-cash-account/package.json b/packages/keyring-eth-cash-account/package.json new file mode 100644 index 000000000..e32fd438a --- /dev/null +++ b/packages/keyring-eth-cash-account/package.json @@ -0,0 +1,71 @@ +{ + "name": "@metamask/eth-cash-account-keyring", + "version": "1.0.0", + "description": "A cash account keyring that extends the HD keyring with a different keyring type and derivation path.", + "keywords": [ + "ethereum", + "keyring" + ], + "homepage": "https://github.com/MetaMask/accounts#readme", + "bugs": { + "url": "https://github.com/MetaMask/accounts/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/accounts.git" + }, + "license": "ISC", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --no-references", + "build:clean": "yarn build --clean", + "build:docs": "typedoc", + "changelog:update": "../../scripts/update-changelog.sh @metamask/eth-cash-account-keyring", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/eth-cash-account-keyring", + "publish:preview": "yarn npm publish --tag preview", + "test": "jest", + "test:clean": "jest --clearCache" + }, + "dependencies": { + "@metamask/eth-hd-keyring": "workspace:^" + }, + "devDependencies": { + "@lavamoat/allow-scripts": "^3.2.1", + "@lavamoat/preinstall-always-fail": "^2.1.0", + "@metamask/auto-changelog": "^3.4.4", + "@metamask/eth-sig-util": "^8.2.0", + "@metamask/utils": "^11.1.0", + "@ts-bridge/cli": "^0.6.3", + "@types/jest": "^29.5.12", + "deepmerge": "^4.2.2", + "jest": "^29.5.0" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "lavamoat": { + "allowScripts": { + "@lavamoat/preinstall-always-fail": false + } + } +} diff --git a/packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts b/packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts new file mode 100644 index 000000000..5e5d3c580 --- /dev/null +++ b/packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts @@ -0,0 +1,242 @@ +import { + recoverPersonalSignature, + recoverTypedSignature, + SignTypedDataVersion, + type MessageTypes, + type TypedMessage, +} from '@metamask/eth-sig-util'; +import { normalize } from '@metamask/eth-sig-util'; +import { assert, type Hex } from '@metamask/utils'; + +import { CashAccountKeyring } from './cash-account-keyring'; + +const sampleMnemonic = + 'finish oppose decorate face calm tragic certain desk hour urge dinosaur mango'; + +const cashAccountHdPath = `m/44'/4392018'/0'/0`; + +const getAddressAtIndex = async ( + keyring: CashAccountKeyring, + index: number, +): Promise => { + const accounts = await keyring.getAccounts(); + assert(accounts[index], `Account not found at index ${index}`); + return accounts[index]; +}; + +describe('CashAccountKeyring', () => { + describe('static properties', () => { + it('has the correct type', () => { + expect(CashAccountKeyring.type).toBe('Cash Account Keyring'); + }); + }); + + describe('#type', () => { + it('returns the correct value', () => { + const keyring = new CashAccountKeyring(); + expect(keyring.type).toBe('Cash Account Keyring'); + expect(keyring.type).toBe(CashAccountKeyring.type); + }); + }); + + describe('#hdPath', () => { + it('uses the cash account derivation path', () => { + const keyring = new CashAccountKeyring(); + expect(keyring.hdPath).toBe(cashAccountHdPath); + }); + }); + + describe('#deserialize', () => { + it('derives accounts using the cash account hd path', async () => { + const keyring = new CashAccountKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + + const serialized = await keyring.serialize(); + expect(serialized.hdPath).toBe(cashAccountHdPath); + }); + + it('derives different addresses than the standard HD keyring path', async () => { + const { HdKeyring } = await import('@metamask/eth-hd-keyring'); + + const cashKeyring = new CashAccountKeyring(); + await cashKeyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + const cashAccounts = await cashKeyring.getAccounts(); + + const hdKeyring = new HdKeyring(); + await hdKeyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + const hdAccounts = await hdKeyring.getAccounts(); + + expect(cashAccounts[0]).not.toBe(hdAccounts[0]); + }); + + it('uses the cash account hd path when no hdPath option is provided', async () => { + const keyring = new CashAccountKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + + const serialized = await keyring.serialize(); + expect(serialized.hdPath).toBe(cashAccountHdPath); + }); + + it('respects an explicitly provided hdPath', async () => { + const customPath = `m/44'/60'/0'/0`; + const keyring = new CashAccountKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + hdPath: customPath, + }); + + const serialized = await keyring.serialize(); + expect(serialized.hdPath).toBe(customPath); + }); + }); + + describe('#addAccounts', () => { + it('creates accounts', async () => { + const keyring = new CashAccountKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + + await keyring.addAccounts(1); + const accounts = await keyring.getAccounts(); + expect(accounts).toHaveLength(2); + }); + }); + + describe('#signPersonalMessage', () => { + it('signs and the signature can be recovered', async () => { + const keyring = new CashAccountKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + + const address = await getAddressAtIndex(keyring, 0); + const message = '0x68656c6c6f20776f726c64'; + const signature = await keyring.signPersonalMessage(address, message); + + const restored = recoverPersonalSignature({ + data: message, + signature, + }); + expect(restored).toStrictEqual(normalize(address)); + }); + }); + + describe('#signTypedData', () => { + it('signs V1 typed data and the signature can be recovered', async () => { + const keyring = new CashAccountKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + + const address = await getAddressAtIndex(keyring, 0); + const typedData = [ + { + type: 'string', + name: 'message', + value: 'Hi, Alice!', + }, + ]; + + const signature = await keyring.signTypedData(address, typedData); + const restored = recoverTypedSignature({ + data: typedData, + signature, + version: SignTypedDataVersion.V1, + }); + expect(restored).toStrictEqual(address); + }); + + it('signs V3 typed data and the signature can be recovered', async () => { + const keyring = new CashAccountKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + + const address = await getAddressAtIndex(keyring, 0); + const typedData: TypedMessage = { + types: { + EIP712Domain: [], + }, + domain: {}, + primaryType: 'EIP712Domain', + message: {}, + }; + + const signature = await keyring.signTypedData(address, typedData, { + version: SignTypedDataVersion.V3, + }); + const restored = recoverTypedSignature({ + data: typedData, + signature, + version: SignTypedDataVersion.V3, + }); + expect(restored).toStrictEqual(address); + }); + }); + + describe('#removeAccount', () => { + it('removes an existing account', async () => { + const keyring = new CashAccountKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + + const accounts = await keyring.getAccounts(); + expect(accounts).toHaveLength(1); + + keyring.removeAccount(await getAddressAtIndex(keyring, 0)); + const remaining = await keyring.getAccounts(); + expect(remaining).toHaveLength(0); + }); + + it('throws when removing a non-existent account', () => { + const keyring = new CashAccountKeyring(); + const fakeAddress = '0x0000000000000000000000000000000000000000'; + expect(() => keyring.removeAccount(fakeAddress)).toThrow( + `Address ${fakeAddress} not found in this keyring`, + ); + }); + }); + + describe('#serialize / #deserialize round-trip', () => { + it('serializes what it deserializes', async () => { + const keyring = new CashAccountKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 2, + }); + + const accounts = await keyring.getAccounts(); + const serialized = await keyring.serialize(); + + const restored = new CashAccountKeyring(); + await restored.deserialize(serialized); + + const restoredAccounts = await restored.getAccounts(); + expect(restoredAccounts).toStrictEqual(accounts); + + const restoredSerialized = await restored.serialize(); + expect(restoredSerialized.hdPath).toBe(cashAccountHdPath); + expect(restoredSerialized.numberOfAccounts).toBe(2); + }); + }); +}); diff --git a/packages/keyring-eth-cash-account/src/cash-account-keyring.ts b/packages/keyring-eth-cash-account/src/cash-account-keyring.ts new file mode 100644 index 000000000..105b873eb --- /dev/null +++ b/packages/keyring-eth-cash-account/src/cash-account-keyring.ts @@ -0,0 +1,25 @@ +import { + HdKeyring, + type DeserializableHDKeyringState, +} from '@metamask/eth-hd-keyring'; + +const hdPathString = `m/44'/4392018'/0'/0`; +const type = 'Cash Account Keyring'; + +export class CashAccountKeyring extends HdKeyring { + static override type: string = type; + + override type: string = type; + + override hdPath: string = hdPathString; + + // This override is required because the deserialize method in the cash-account-keyring falls back to it's own static value if no option is provided. + override async deserialize( + opts: Partial, + ): Promise { + return super.deserialize({ + ...opts, + hdPath: opts.hdPath ?? hdPathString, + }); + } +} diff --git a/packages/keyring-eth-cash-account/src/index.ts b/packages/keyring-eth-cash-account/src/index.ts new file mode 100644 index 000000000..e9878e3e1 --- /dev/null +++ b/packages/keyring-eth-cash-account/src/index.ts @@ -0,0 +1 @@ +export { CashAccountKeyring } from './cash-account-keyring'; diff --git a/packages/keyring-eth-cash-account/tsconfig.build.json b/packages/keyring-eth-cash-account/tsconfig.build.json new file mode 100644 index 000000000..b6de6895a --- /dev/null +++ b/packages/keyring-eth-cash-account/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "dist", + "rootDir": "src", + "exactOptionalPropertyTypes": false + }, + "references": [ + { + "path": "../keyring-eth-hd/tsconfig.build.json" + } + ], + "include": ["./src/**/*.ts"], + "exclude": ["./src/**/*.test.ts"] +} diff --git a/packages/keyring-eth-cash-account/tsconfig.json b/packages/keyring-eth-cash-account/tsconfig.json new file mode 100644 index 000000000..4dd9c7011 --- /dev/null +++ b/packages/keyring-eth-cash-account/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./", + "exactOptionalPropertyTypes": false, + "target": "es2017" + }, + "references": [{ "path": "../keyring-eth-hd" }], + "include": ["./src"], + "exclude": ["./dist/**/*"] +} diff --git a/tsconfig.build.json b/tsconfig.build.json index b7caf0488..e364b0224 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,6 +4,7 @@ { "path": "./packages/hw-wallet-sdk/tsconfig.build.json" }, { "path": "./packages/keyring-api/tsconfig.build.json" }, { "path": "./packages/keyring-eth-hd/tsconfig.build.json" }, + { "path": "./packages/keyring-eth-cash-account/tsconfig.build.json" }, { "path": "./packages/keyring-eth-ledger-bridge/tsconfig.build.json" }, { "path": "./packages/keyring-eth-qr/tsconfig.build.json" }, { "path": "./packages/keyring-eth-simple/tsconfig.build.json" }, diff --git a/yarn.lock b/yarn.lock index 0ddb96ba1..52d960a20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1678,7 +1678,24 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-hd-keyring@workspace:packages/keyring-eth-hd": +"@metamask/eth-cash-account-keyring@workspace:packages/keyring-eth-cash-account": + version: 0.0.0-use.local + resolution: "@metamask/eth-cash-account-keyring@workspace:packages/keyring-eth-cash-account" + dependencies: + "@lavamoat/allow-scripts": "npm:^3.2.1" + "@lavamoat/preinstall-always-fail": "npm:^2.1.0" + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/eth-hd-keyring": "workspace:^" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/utils": "npm:^11.1.0" + "@ts-bridge/cli": "npm:^0.6.3" + "@types/jest": "npm:^29.5.12" + deepmerge: "npm:^4.2.2" + jest: "npm:^29.5.0" + languageName: unknown + linkType: soft + +"@metamask/eth-hd-keyring@workspace:^, @metamask/eth-hd-keyring@workspace:packages/keyring-eth-hd": version: 0.0.0-use.local resolution: "@metamask/eth-hd-keyring@workspace:packages/keyring-eth-hd" dependencies: From 77eaae7ad2b4a05751e59f0359700d526eea4bd0 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 10 Mar 2026 10:05:08 +0000 Subject: [PATCH 02/12] chore: add comments explaining origin of deriation path --- .../src/cash-account-keyring.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/keyring-eth-cash-account/src/cash-account-keyring.ts b/packages/keyring-eth-cash-account/src/cash-account-keyring.ts index 105b873eb..3f2382a4f 100644 --- a/packages/keyring-eth-cash-account/src/cash-account-keyring.ts +++ b/packages/keyring-eth-cash-account/src/cash-account-keyring.ts @@ -3,7 +3,8 @@ import { type DeserializableHDKeyringState, } from '@metamask/eth-hd-keyring'; -const hdPathString = `m/44'/4392018'/0'/0`; +// Based on the coin type created in [this PR](https://github.com/satoshilabs/slips/pull/1983) +export const CASH_ACCOUNT_DERIVATION_PATH = `m/44'/4392018'/0'/0`; const type = 'Cash Account Keyring'; export class CashAccountKeyring extends HdKeyring { @@ -11,15 +12,17 @@ export class CashAccountKeyring extends HdKeyring { override type: string = type; - override hdPath: string = hdPathString; + override hdPath: string = CASH_ACCOUNT_DERIVATION_PATH; - // This override is required because the deserialize method in the cash-account-keyring falls back to it's own static value if no option is provided. + // This override is required because the deserialize method in the + // CashAccountKeyring falls back to it's own static value if no + // option is provided. override async deserialize( opts: Partial, ): Promise { return super.deserialize({ ...opts, - hdPath: opts.hdPath ?? hdPathString, + hdPath: opts.hdPath ?? CASH_ACCOUNT_DERIVATION_PATH, }); } } From fe2949a90f582820be1b320c56c098a83b514186 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 10 Mar 2026 10:10:05 +0000 Subject: [PATCH 03/12] chore: various small non-functional tweaks based on PR feedback --- packages/keyring-api/src/api/v2/keyring-type.ts | 2 +- packages/keyring-eth-cash-account/CHANGELOG.md | 7 ++++--- packages/keyring-eth-cash-account/README.md | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/keyring-api/src/api/v2/keyring-type.ts b/packages/keyring-api/src/api/v2/keyring-type.ts index 0ba79798e..097171cb1 100644 --- a/packages/keyring-api/src/api/v2/keyring-type.ts +++ b/packages/keyring-api/src/api/v2/keyring-type.ts @@ -45,7 +45,7 @@ export enum KeyringType { OneKey = 'onekey', /** - * A keyring for the cash account + * Represents keyring for cash accounts. */ CashAccount = 'cash-account', } diff --git a/packages/keyring-eth-cash-account/CHANGELOG.md b/packages/keyring-eth-cash-account/CHANGELOG.md index 2d00805c1..521052a70 100644 --- a/packages/keyring-eth-cash-account/CHANGELOG.md +++ b/packages/keyring-eth-cash-account/CHANGELOG.md @@ -9,8 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `CashAccountKeyring` class extending `HdKeyring` from `@metamask/eth-hd-keyring` ([#472](https://github.com/MetaMask/accounts/pull/472)) - - Uses keyring type `Cash Account Keyring` - - Uses derivation path `m/44'/4392018'/0'/0` +- Add initial implementation of `CashAccountKeyring` ([#472](https://github.com/MetaMask/accounts/pull/472)) + - Extends `HdKeyring` from `@metamask/eth-hd-keyring`. + - Uses keyring type `"Cash Account Keyring"`. + - Uses derivation path `"m/44'/4392018'/0'/0"`. [Unreleased]: https://github.com/MetaMask/accounts/ diff --git a/packages/keyring-eth-cash-account/README.md b/packages/keyring-eth-cash-account/README.md index 95751d74b..077980e58 100644 --- a/packages/keyring-eth-cash-account/README.md +++ b/packages/keyring-eth-cash-account/README.md @@ -28,7 +28,7 @@ The `CashAccountKeyring` class implements the same `Keyring` interface as `HdKey - Install [Node.js](https://nodejs.org) version 18 - If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended) running `nvm use` will automatically choose the right node version for you. -- Install [Yarn v3](https://yarnpkg.com/getting-started/install) +- Install [Yarn v4](https://yarnpkg.com/getting-started/install) - Run `yarn install` to install dependencies and run any required post-install scripts ### Testing and Linting From 3de607bbfe29a175d4605dc884f80c891a1249e2 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 10 Mar 2026 10:10:24 +0000 Subject: [PATCH 04/12] feat: only allow a single account to be added to cash keyring --- .../src/cash-account-keyring.test.ts | 22 +++++++++++++++---- .../src/cash-account-keyring.ts | 7 ++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts b/packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts index 5e5d3c580..d1c87b811 100644 --- a/packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts +++ b/packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts @@ -104,17 +104,31 @@ describe('CashAccountKeyring', () => { }); describe('#addAccounts', () => { - it('creates accounts', async () => { + it('adds a single account', async () => { const keyring = new CashAccountKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, numberOfAccounts: 1, }); - await keyring.addAccounts(1); + await keyring.addAccounts(); const accounts = await keyring.getAccounts(); expect(accounts).toHaveLength(2); }); + + it('only adds one account per call', async () => { + const keyring = new CashAccountKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + + await keyring.addAccounts(); + await keyring.addAccounts(); + await keyring.addAccounts(); + const accounts = await keyring.getAccounts(); + expect(accounts).toHaveLength(4); + }); }); describe('#signPersonalMessage', () => { @@ -222,7 +236,7 @@ describe('CashAccountKeyring', () => { const keyring = new CashAccountKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, - numberOfAccounts: 2, + numberOfAccounts: 1, }); const accounts = await keyring.getAccounts(); @@ -236,7 +250,7 @@ describe('CashAccountKeyring', () => { const restoredSerialized = await restored.serialize(); expect(restoredSerialized.hdPath).toBe(cashAccountHdPath); - expect(restoredSerialized.numberOfAccounts).toBe(2); + expect(restoredSerialized.numberOfAccounts).toBe(1); }); }); }); diff --git a/packages/keyring-eth-cash-account/src/cash-account-keyring.ts b/packages/keyring-eth-cash-account/src/cash-account-keyring.ts index 3f2382a4f..edfa61b67 100644 --- a/packages/keyring-eth-cash-account/src/cash-account-keyring.ts +++ b/packages/keyring-eth-cash-account/src/cash-account-keyring.ts @@ -2,6 +2,7 @@ import { HdKeyring, type DeserializableHDKeyringState, } from '@metamask/eth-hd-keyring'; +import { Hex } from '@metamask/utils'; // Based on the coin type created in [this PR](https://github.com/satoshilabs/slips/pull/1983) export const CASH_ACCOUNT_DERIVATION_PATH = `m/44'/4392018'/0'/0`; @@ -25,4 +26,10 @@ export class CashAccountKeyring extends HdKeyring { hdPath: opts.hdPath ?? CASH_ACCOUNT_DERIVATION_PATH, }); } + + // This override ensures that we can only ever add a + // single account to this keyring + override async addAccounts(): Promise { + return super.addAccounts(1); + } } From 8fb69f7a4bbc27e1e09e2ae8f46ae832803cf90f Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 10 Mar 2026 10:14:49 +0000 Subject: [PATCH 05/12] chore: update keyring-api changelog --- packages/keyring-api/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keyring-api/CHANGELOG.md b/packages/keyring-api/CHANGELOG.md index 27d5d8c02..001dee9c5 100644 --- a/packages/keyring-api/CHANGELOG.md +++ b/packages/keyring-api/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `CashAccount` variant to `KeyringType` enum ([#472](https://github.com/MetaMask/accounts/pull/472)) +- Add `KeyringType.CashAccount` variant ([#472](https://github.com/MetaMask/accounts/pull/472)) - Add optional `details` field to `Transaction` type ([#445](https://github.com/MetaMask/accounts/pull/445)) - Add `SecurityAlertResponse` enum with values: `benign`, `warning`, `malicious` - Add optional `origin` field (string) to track transaction request source From 8ebb609f7465c61c0cf977d8742d67e731fd02a3 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 10 Mar 2026 10:15:01 +0000 Subject: [PATCH 06/12] feat: limit options that can be passed to deserialize in cash keyring --- .../src/cash-account-keyring.test.ts | 31 +++++++++---------- .../src/cash-account-keyring.ts | 13 +++++--- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts b/packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts index d1c87b811..8176f3513 100644 --- a/packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts +++ b/packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts @@ -51,7 +51,6 @@ describe('CashAccountKeyring', () => { const keyring = new CashAccountKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, - numberOfAccounts: 1, }); const serialized = await keyring.serialize(); @@ -64,7 +63,6 @@ describe('CashAccountKeyring', () => { const cashKeyring = new CashAccountKeyring(); await cashKeyring.deserialize({ mnemonic: sampleMnemonic, - numberOfAccounts: 1, }); const cashAccounts = await cashKeyring.getAccounts(); @@ -82,24 +80,30 @@ describe('CashAccountKeyring', () => { const keyring = new CashAccountKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, - numberOfAccounts: 1, }); const serialized = await keyring.serialize(); expect(serialized.hdPath).toBe(cashAccountHdPath); }); - it('respects an explicitly provided hdPath', async () => { - const customPath = `m/44'/60'/0'/0`; + it('always uses the cash account hd path', async () => { const keyring = new CashAccountKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, - numberOfAccounts: 1, - hdPath: customPath, }); const serialized = await keyring.serialize(); - expect(serialized.hdPath).toBe(customPath); + expect(serialized.hdPath).toBe(cashAccountHdPath); + }); + + it('always deserializes exactly one account', async () => { + const keyring = new CashAccountKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + }); + + const accounts = await keyring.getAccounts(); + expect(accounts).toHaveLength(1); }); }); @@ -108,7 +112,6 @@ describe('CashAccountKeyring', () => { const keyring = new CashAccountKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, - numberOfAccounts: 1, }); await keyring.addAccounts(); @@ -120,7 +123,6 @@ describe('CashAccountKeyring', () => { const keyring = new CashAccountKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, - numberOfAccounts: 1, }); await keyring.addAccounts(); @@ -136,7 +138,6 @@ describe('CashAccountKeyring', () => { const keyring = new CashAccountKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, - numberOfAccounts: 1, }); const address = await getAddressAtIndex(keyring, 0); @@ -156,7 +157,6 @@ describe('CashAccountKeyring', () => { const keyring = new CashAccountKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, - numberOfAccounts: 1, }); const address = await getAddressAtIndex(keyring, 0); @@ -181,7 +181,6 @@ describe('CashAccountKeyring', () => { const keyring = new CashAccountKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, - numberOfAccounts: 1, }); const address = await getAddressAtIndex(keyring, 0); @@ -211,7 +210,6 @@ describe('CashAccountKeyring', () => { const keyring = new CashAccountKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, - numberOfAccounts: 1, }); const accounts = await keyring.getAccounts(); @@ -236,14 +234,15 @@ describe('CashAccountKeyring', () => { const keyring = new CashAccountKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, - numberOfAccounts: 1, }); const accounts = await keyring.getAccounts(); const serialized = await keyring.serialize(); const restored = new CashAccountKeyring(); - await restored.deserialize(serialized); + await restored.deserialize({ + mnemonic: serialized.mnemonic, + }); const restoredAccounts = await restored.getAccounts(); expect(restoredAccounts).toStrictEqual(accounts); diff --git a/packages/keyring-eth-cash-account/src/cash-account-keyring.ts b/packages/keyring-eth-cash-account/src/cash-account-keyring.ts index edfa61b67..1393d80c3 100644 --- a/packages/keyring-eth-cash-account/src/cash-account-keyring.ts +++ b/packages/keyring-eth-cash-account/src/cash-account-keyring.ts @@ -2,7 +2,7 @@ import { HdKeyring, type DeserializableHDKeyringState, } from '@metamask/eth-hd-keyring'; -import { Hex } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; // Based on the coin type created in [this PR](https://github.com/satoshilabs/slips/pull/1983) export const CASH_ACCOUNT_DERIVATION_PATH = `m/44'/4392018'/0'/0`; @@ -11,19 +11,22 @@ const type = 'Cash Account Keyring'; export class CashAccountKeyring extends HdKeyring { static override type: string = type; - override type: string = type; + override readonly type: string = type; - override hdPath: string = CASH_ACCOUNT_DERIVATION_PATH; + override readonly hdPath: string = CASH_ACCOUNT_DERIVATION_PATH; // This override is required because the deserialize method in the // CashAccountKeyring falls back to it's own static value if no // option is provided. override async deserialize( - opts: Partial, + opts: Partial< + Omit + >, ): Promise { return super.deserialize({ ...opts, - hdPath: opts.hdPath ?? CASH_ACCOUNT_DERIVATION_PATH, + numberOfAccounts: 1, + hdPath: CASH_ACCOUNT_DERIVATION_PATH, }); } From 5df4e9bb4cb9b94906710e5b218b97c6a3b41b6a Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 10 Mar 2026 10:20:36 +0000 Subject: [PATCH 07/12] chore: rename new keyring package to simply cash --- .github/workflows/validate-pr-title.yml | 1 + README.md | 6 +- packages/keyring-api/CHANGELOG.md | 2 +- .../keyring-api/src/api/v2/keyring-type.ts | 2 +- .../keyring-api/src/api/v2/keyring.test-d.ts | 2 +- .../keyring-eth-cash-account/src/index.ts | 1 - .../CHANGELOG.md | 4 +- .../LICENSE | 0 .../README.md | 12 ++-- .../jest.config.js | 0 .../package.json | 6 +- .../src/cash-keyring.test.ts} | 56 +++++++++---------- .../src/cash-keyring.ts} | 12 ++-- packages/keyring-eth-cash/src/index.ts | 1 + .../tsconfig.build.json | 0 .../tsconfig.json | 0 tsconfig.build.json | 2 +- yarn.lock | 4 +- 18 files changed, 56 insertions(+), 55 deletions(-) delete mode 100644 packages/keyring-eth-cash-account/src/index.ts rename packages/{keyring-eth-cash-account => keyring-eth-cash}/CHANGELOG.md (73%) rename packages/{keyring-eth-cash-account => keyring-eth-cash}/LICENSE (100%) rename packages/{keyring-eth-cash-account => keyring-eth-cash}/README.md (69%) rename packages/{keyring-eth-cash-account => keyring-eth-cash}/jest.config.js (100%) rename packages/{keyring-eth-cash-account => keyring-eth-cash}/package.json (94%) rename packages/{keyring-eth-cash-account/src/cash-account-keyring.test.ts => keyring-eth-cash/src/cash-keyring.test.ts} (81%) rename packages/{keyring-eth-cash-account/src/cash-account-keyring.ts => keyring-eth-cash/src/cash-keyring.ts} (71%) create mode 100644 packages/keyring-eth-cash/src/index.ts rename packages/{keyring-eth-cash-account => keyring-eth-cash}/tsconfig.build.json (100%) rename packages/{keyring-eth-cash-account => keyring-eth-cash}/tsconfig.json (100%) diff --git a/.github/workflows/validate-pr-title.yml b/.github/workflows/validate-pr-title.yml index 6589493be..03a0385fe 100644 --- a/.github/workflows/validate-pr-title.yml +++ b/.github/workflows/validate-pr-title.yml @@ -43,6 +43,7 @@ jobs: deps-dev keyring-api keyring-eth-hd + keyring-cash keyring-eth-ledger-bridge keyring-eth-simple keyring-eth-trezor diff --git a/README.md b/README.md index 29db92314..953b13cf8 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This repository contains the following packages [^fn1]: - [`@metamask/account-api`](packages/account-api) -- [`@metamask/eth-cash-account-keyring`](packages/keyring-eth-cash-account) +- [`@metamask/eth-cash-keyring`](packages/keyring-eth-cash) - [`@metamask/eth-hd-keyring`](packages/keyring-eth-hd) - [`@metamask/eth-ledger-bridge-keyring`](packages/keyring-eth-ledger-bridge) - [`@metamask/eth-qr-keyring`](packages/keyring-eth-qr) @@ -41,7 +41,7 @@ linkStyle default opacity:0.5 account_api(["@metamask/account-api"]); hw_wallet_sdk(["@metamask/hw-wallet-sdk"]); keyring_api(["@metamask/keyring-api"]); - eth_cash_account_keyring(["@metamask/eth-cash-account-keyring"]); + eth_cash_keyring(["@metamask/eth-cash-keyring"]); eth_hd_keyring(["@metamask/eth-hd-keyring"]); eth_ledger_bridge_keyring(["@metamask/eth-ledger-bridge-keyring"]); eth_qr_keyring(["@metamask/eth-qr-keyring"]); @@ -56,7 +56,7 @@ linkStyle default opacity:0.5 account_api --> keyring_api; account_api --> keyring_utils; keyring_api --> keyring_utils; - eth_cash_account_keyring --> keyring_eth_hd; + eth_cash_keyring --> keyring_eth_hd; eth_hd_keyring --> keyring_api; eth_hd_keyring --> keyring_utils; eth_hd_keyring --> account_api; diff --git a/packages/keyring-api/CHANGELOG.md b/packages/keyring-api/CHANGELOG.md index 001dee9c5..3c5b4d9d3 100644 --- a/packages/keyring-api/CHANGELOG.md +++ b/packages/keyring-api/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `KeyringType.CashAccount` variant ([#472](https://github.com/MetaMask/accounts/pull/472)) +- Add `KeyringType.Cash` variant ([#472](https://github.com/MetaMask/accounts/pull/472)) - Add optional `details` field to `Transaction` type ([#445](https://github.com/MetaMask/accounts/pull/445)) - Add `SecurityAlertResponse` enum with values: `benign`, `warning`, `malicious` - Add optional `origin` field (string) to track transaction request source diff --git a/packages/keyring-api/src/api/v2/keyring-type.ts b/packages/keyring-api/src/api/v2/keyring-type.ts index 097171cb1..6a464822d 100644 --- a/packages/keyring-api/src/api/v2/keyring-type.ts +++ b/packages/keyring-api/src/api/v2/keyring-type.ts @@ -47,5 +47,5 @@ export enum KeyringType { /** * Represents keyring for cash accounts. */ - CashAccount = 'cash-account', + Cash = 'cash', } diff --git a/packages/keyring-api/src/api/v2/keyring.test-d.ts b/packages/keyring-api/src/api/v2/keyring.test-d.ts index 048697709..47c38dde7 100644 --- a/packages/keyring-api/src/api/v2/keyring.test-d.ts +++ b/packages/keyring-api/src/api/v2/keyring.test-d.ts @@ -22,7 +22,7 @@ import type { ImportPrivateKeyFormat } from './private-key'; // Test KeyringType enum expectAssignable(KeyringType.Hd); -expectAssignable(KeyringType.CashAccount); +expectAssignable(KeyringType.Cash); expectAssignable(KeyringType.PrivateKey); expectAssignable(KeyringType.Qr); expectAssignable(KeyringType.Snap); diff --git a/packages/keyring-eth-cash-account/src/index.ts b/packages/keyring-eth-cash-account/src/index.ts deleted file mode 100644 index e9878e3e1..000000000 --- a/packages/keyring-eth-cash-account/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CashAccountKeyring } from './cash-account-keyring'; diff --git a/packages/keyring-eth-cash-account/CHANGELOG.md b/packages/keyring-eth-cash/CHANGELOG.md similarity index 73% rename from packages/keyring-eth-cash-account/CHANGELOG.md rename to packages/keyring-eth-cash/CHANGELOG.md index 521052a70..577aad03d 100644 --- a/packages/keyring-eth-cash-account/CHANGELOG.md +++ b/packages/keyring-eth-cash/CHANGELOG.md @@ -9,9 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add initial implementation of `CashAccountKeyring` ([#472](https://github.com/MetaMask/accounts/pull/472)) +- Add initial implementation of `CashKeyring` ([#472](https://github.com/MetaMask/accounts/pull/472)) - Extends `HdKeyring` from `@metamask/eth-hd-keyring`. - - Uses keyring type `"Cash Account Keyring"`. + - Uses keyring type `"Cash Keyring"`. - Uses derivation path `"m/44'/4392018'/0'/0"`. [Unreleased]: https://github.com/MetaMask/accounts/ diff --git a/packages/keyring-eth-cash-account/LICENSE b/packages/keyring-eth-cash/LICENSE similarity index 100% rename from packages/keyring-eth-cash-account/LICENSE rename to packages/keyring-eth-cash/LICENSE diff --git a/packages/keyring-eth-cash-account/README.md b/packages/keyring-eth-cash/README.md similarity index 69% rename from packages/keyring-eth-cash-account/README.md rename to packages/keyring-eth-cash/README.md index 077980e58..52713e8d0 100644 --- a/packages/keyring-eth-cash-account/README.md +++ b/packages/keyring-eth-cash/README.md @@ -1,4 +1,4 @@ -# Cash Account Keyring +# Cash Keyring An Ethereum keyring that extends [`@metamask/eth-hd-keyring`](../keyring-eth-hd) with a distinct keyring type and derivation path for cash accounts. @@ -6,21 +6,21 @@ Cash accounts use a separate HD derivation path to keep funds isolated from the ## Installation -`yarn add @metamask/eth-cash-account-keyring` +`yarn add @metamask/eth-cash-keyring` or -`npm install @metamask/eth-cash-account-keyring` +`npm install @metamask/eth-cash-keyring` ## Usage ```ts -import { CashAccountKeyring } from '@metamask/eth-cash-account-keyring'; +import { CashKeyring } from '@metamask/eth-cash-keyring'; -const keyring = new CashAccountKeyring(); +const keyring = new CashKeyring(); ``` -The `CashAccountKeyring` class implements the same `Keyring` interface as `HdKeyring` — see the [HD Keyring README](../keyring-eth-hd/README.md) for full API documentation. +The `CashKeyring` class implements the same `Keyring` interface as `HdKeyring` — see the [HD Keyring README](../keyring-eth-hd/README.md) for full API documentation. ## Contributing diff --git a/packages/keyring-eth-cash-account/jest.config.js b/packages/keyring-eth-cash/jest.config.js similarity index 100% rename from packages/keyring-eth-cash-account/jest.config.js rename to packages/keyring-eth-cash/jest.config.js diff --git a/packages/keyring-eth-cash-account/package.json b/packages/keyring-eth-cash/package.json similarity index 94% rename from packages/keyring-eth-cash-account/package.json rename to packages/keyring-eth-cash/package.json index e32fd438a..0312f9dc1 100644 --- a/packages/keyring-eth-cash-account/package.json +++ b/packages/keyring-eth-cash/package.json @@ -1,5 +1,5 @@ { - "name": "@metamask/eth-cash-account-keyring", + "name": "@metamask/eth-cash-keyring", "version": "1.0.0", "description": "A cash account keyring that extends the HD keyring with a different keyring type and derivation path.", "keywords": [ @@ -36,8 +36,8 @@ "build": "ts-bridge --project tsconfig.build.json --no-references", "build:clean": "yarn build --clean", "build:docs": "typedoc", - "changelog:update": "../../scripts/update-changelog.sh @metamask/eth-cash-account-keyring", - "changelog:validate": "../../scripts/validate-changelog.sh @metamask/eth-cash-account-keyring", + "changelog:update": "../../scripts/update-changelog.sh @metamask/eth-cash-keyring", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/eth-cash-keyring", "publish:preview": "yarn npm publish --tag preview", "test": "jest", "test:clean": "jest --clearCache" diff --git a/packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts b/packages/keyring-eth-cash/src/cash-keyring.test.ts similarity index 81% rename from packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts rename to packages/keyring-eth-cash/src/cash-keyring.test.ts index 8176f3513..8b5d5a128 100644 --- a/packages/keyring-eth-cash-account/src/cash-account-keyring.test.ts +++ b/packages/keyring-eth-cash/src/cash-keyring.test.ts @@ -8,15 +8,15 @@ import { import { normalize } from '@metamask/eth-sig-util'; import { assert, type Hex } from '@metamask/utils'; -import { CashAccountKeyring } from './cash-account-keyring'; +import { CashKeyring } from './cash-keyring'; const sampleMnemonic = 'finish oppose decorate face calm tragic certain desk hour urge dinosaur mango'; -const cashAccountHdPath = `m/44'/4392018'/0'/0`; +const cashHdPath = `m/44'/4392018'/0'/0`; const getAddressAtIndex = async ( - keyring: CashAccountKeyring, + keyring: CashKeyring, index: number, ): Promise => { const accounts = await keyring.getAccounts(); @@ -24,43 +24,43 @@ const getAddressAtIndex = async ( return accounts[index]; }; -describe('CashAccountKeyring', () => { +describe('CashKeyring', () => { describe('static properties', () => { it('has the correct type', () => { - expect(CashAccountKeyring.type).toBe('Cash Account Keyring'); + expect(CashKeyring.type).toBe('Cash Keyring'); }); }); describe('#type', () => { it('returns the correct value', () => { - const keyring = new CashAccountKeyring(); - expect(keyring.type).toBe('Cash Account Keyring'); - expect(keyring.type).toBe(CashAccountKeyring.type); + const keyring = new CashKeyring(); + expect(keyring.type).toBe('Cash Keyring'); + expect(keyring.type).toBe(CashKeyring.type); }); }); describe('#hdPath', () => { it('uses the cash account derivation path', () => { - const keyring = new CashAccountKeyring(); - expect(keyring.hdPath).toBe(cashAccountHdPath); + const keyring = new CashKeyring(); + expect(keyring.hdPath).toBe(cashHdPath); }); }); describe('#deserialize', () => { it('derives accounts using the cash account hd path', async () => { - const keyring = new CashAccountKeyring(); + const keyring = new CashKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, }); const serialized = await keyring.serialize(); - expect(serialized.hdPath).toBe(cashAccountHdPath); + expect(serialized.hdPath).toBe(cashHdPath); }); it('derives different addresses than the standard HD keyring path', async () => { const { HdKeyring } = await import('@metamask/eth-hd-keyring'); - const cashKeyring = new CashAccountKeyring(); + const cashKeyring = new CashKeyring(); await cashKeyring.deserialize({ mnemonic: sampleMnemonic, }); @@ -77,27 +77,27 @@ describe('CashAccountKeyring', () => { }); it('uses the cash account hd path when no hdPath option is provided', async () => { - const keyring = new CashAccountKeyring(); + const keyring = new CashKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, }); const serialized = await keyring.serialize(); - expect(serialized.hdPath).toBe(cashAccountHdPath); + expect(serialized.hdPath).toBe(cashHdPath); }); it('always uses the cash account hd path', async () => { - const keyring = new CashAccountKeyring(); + const keyring = new CashKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, }); const serialized = await keyring.serialize(); - expect(serialized.hdPath).toBe(cashAccountHdPath); + expect(serialized.hdPath).toBe(cashHdPath); }); it('always deserializes exactly one account', async () => { - const keyring = new CashAccountKeyring(); + const keyring = new CashKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, }); @@ -109,7 +109,7 @@ describe('CashAccountKeyring', () => { describe('#addAccounts', () => { it('adds a single account', async () => { - const keyring = new CashAccountKeyring(); + const keyring = new CashKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, }); @@ -120,7 +120,7 @@ describe('CashAccountKeyring', () => { }); it('only adds one account per call', async () => { - const keyring = new CashAccountKeyring(); + const keyring = new CashKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, }); @@ -135,7 +135,7 @@ describe('CashAccountKeyring', () => { describe('#signPersonalMessage', () => { it('signs and the signature can be recovered', async () => { - const keyring = new CashAccountKeyring(); + const keyring = new CashKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, }); @@ -154,7 +154,7 @@ describe('CashAccountKeyring', () => { describe('#signTypedData', () => { it('signs V1 typed data and the signature can be recovered', async () => { - const keyring = new CashAccountKeyring(); + const keyring = new CashKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, }); @@ -178,7 +178,7 @@ describe('CashAccountKeyring', () => { }); it('signs V3 typed data and the signature can be recovered', async () => { - const keyring = new CashAccountKeyring(); + const keyring = new CashKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, }); @@ -207,7 +207,7 @@ describe('CashAccountKeyring', () => { describe('#removeAccount', () => { it('removes an existing account', async () => { - const keyring = new CashAccountKeyring(); + const keyring = new CashKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, }); @@ -221,7 +221,7 @@ describe('CashAccountKeyring', () => { }); it('throws when removing a non-existent account', () => { - const keyring = new CashAccountKeyring(); + const keyring = new CashKeyring(); const fakeAddress = '0x0000000000000000000000000000000000000000'; expect(() => keyring.removeAccount(fakeAddress)).toThrow( `Address ${fakeAddress} not found in this keyring`, @@ -231,7 +231,7 @@ describe('CashAccountKeyring', () => { describe('#serialize / #deserialize round-trip', () => { it('serializes what it deserializes', async () => { - const keyring = new CashAccountKeyring(); + const keyring = new CashKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, }); @@ -239,7 +239,7 @@ describe('CashAccountKeyring', () => { const accounts = await keyring.getAccounts(); const serialized = await keyring.serialize(); - const restored = new CashAccountKeyring(); + const restored = new CashKeyring(); await restored.deserialize({ mnemonic: serialized.mnemonic, }); @@ -248,7 +248,7 @@ describe('CashAccountKeyring', () => { expect(restoredAccounts).toStrictEqual(accounts); const restoredSerialized = await restored.serialize(); - expect(restoredSerialized.hdPath).toBe(cashAccountHdPath); + expect(restoredSerialized.hdPath).toBe(cashHdPath); expect(restoredSerialized.numberOfAccounts).toBe(1); }); }); diff --git a/packages/keyring-eth-cash-account/src/cash-account-keyring.ts b/packages/keyring-eth-cash/src/cash-keyring.ts similarity index 71% rename from packages/keyring-eth-cash-account/src/cash-account-keyring.ts rename to packages/keyring-eth-cash/src/cash-keyring.ts index 1393d80c3..821398612 100644 --- a/packages/keyring-eth-cash-account/src/cash-account-keyring.ts +++ b/packages/keyring-eth-cash/src/cash-keyring.ts @@ -5,18 +5,18 @@ import { import type { Hex } from '@metamask/utils'; // Based on the coin type created in [this PR](https://github.com/satoshilabs/slips/pull/1983) -export const CASH_ACCOUNT_DERIVATION_PATH = `m/44'/4392018'/0'/0`; -const type = 'Cash Account Keyring'; +export const CASH_DERIVATION_PATH = `m/44'/4392018'/0'/0`; +const type = 'Cash Keyring'; -export class CashAccountKeyring extends HdKeyring { +export class CashKeyring extends HdKeyring { static override type: string = type; override readonly type: string = type; - override readonly hdPath: string = CASH_ACCOUNT_DERIVATION_PATH; + override readonly hdPath: string = CASH_DERIVATION_PATH; // This override is required because the deserialize method in the - // CashAccountKeyring falls back to it's own static value if no + // CashKeyring falls back to it's own static value if no // option is provided. override async deserialize( opts: Partial< @@ -26,7 +26,7 @@ export class CashAccountKeyring extends HdKeyring { return super.deserialize({ ...opts, numberOfAccounts: 1, - hdPath: CASH_ACCOUNT_DERIVATION_PATH, + hdPath: CASH_DERIVATION_PATH, }); } diff --git a/packages/keyring-eth-cash/src/index.ts b/packages/keyring-eth-cash/src/index.ts new file mode 100644 index 000000000..56f96bb5b --- /dev/null +++ b/packages/keyring-eth-cash/src/index.ts @@ -0,0 +1 @@ +export { CashKeyring } from './cash-keyring'; diff --git a/packages/keyring-eth-cash-account/tsconfig.build.json b/packages/keyring-eth-cash/tsconfig.build.json similarity index 100% rename from packages/keyring-eth-cash-account/tsconfig.build.json rename to packages/keyring-eth-cash/tsconfig.build.json diff --git a/packages/keyring-eth-cash-account/tsconfig.json b/packages/keyring-eth-cash/tsconfig.json similarity index 100% rename from packages/keyring-eth-cash-account/tsconfig.json rename to packages/keyring-eth-cash/tsconfig.json diff --git a/tsconfig.build.json b/tsconfig.build.json index e364b0224..331687caa 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,7 +4,7 @@ { "path": "./packages/hw-wallet-sdk/tsconfig.build.json" }, { "path": "./packages/keyring-api/tsconfig.build.json" }, { "path": "./packages/keyring-eth-hd/tsconfig.build.json" }, - { "path": "./packages/keyring-eth-cash-account/tsconfig.build.json" }, + { "path": "./packages/keyring-eth-cash/tsconfig.build.json" }, { "path": "./packages/keyring-eth-ledger-bridge/tsconfig.build.json" }, { "path": "./packages/keyring-eth-qr/tsconfig.build.json" }, { "path": "./packages/keyring-eth-simple/tsconfig.build.json" }, diff --git a/yarn.lock b/yarn.lock index 52d960a20..75a9f48e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1678,9 +1678,9 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-cash-account-keyring@workspace:packages/keyring-eth-cash-account": +"@metamask/eth-cash-keyring@workspace:packages/keyring-eth-cash": version: 0.0.0-use.local - resolution: "@metamask/eth-cash-account-keyring@workspace:packages/keyring-eth-cash-account" + resolution: "@metamask/eth-cash-keyring@workspace:packages/keyring-eth-cash" dependencies: "@lavamoat/allow-scripts": "npm:^3.2.1" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" From b26b34175d3e8b78b2ae90c06e326a41061716b0 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 10 Mar 2026 10:48:18 +0000 Subject: [PATCH 08/12] chore: change version of eth-cash to 0.0.0 --- packages/keyring-eth-cash/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keyring-eth-cash/package.json b/packages/keyring-eth-cash/package.json index 0312f9dc1..c99815a0f 100644 --- a/packages/keyring-eth-cash/package.json +++ b/packages/keyring-eth-cash/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/eth-cash-keyring", - "version": "1.0.0", + "version": "0.0.0", "description": "A cash account keyring that extends the HD keyring with a different keyring type and derivation path.", "keywords": [ "ethereum", From 980df04cb679f2af3075624e11995f56de277fb6 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 10 Mar 2026 14:14:17 +0000 Subject: [PATCH 09/12] fix: only allow one account to be added --- .../keyring-eth-cash/src/cash-keyring.test.ts | 18 ++++++++++-------- packages/keyring-eth-cash/src/cash-keyring.ts | 6 ++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/keyring-eth-cash/src/cash-keyring.test.ts b/packages/keyring-eth-cash/src/cash-keyring.test.ts index 8b5d5a128..d07a9478c 100644 --- a/packages/keyring-eth-cash/src/cash-keyring.test.ts +++ b/packages/keyring-eth-cash/src/cash-keyring.test.ts @@ -108,28 +108,30 @@ describe('CashKeyring', () => { }); describe('#addAccounts', () => { - it('adds a single account', async () => { + it('throws if an account already exists', async () => { const keyring = new CashKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, }); - await keyring.addAccounts(); - const accounts = await keyring.getAccounts(); - expect(accounts).toHaveLength(2); + await expect(keyring.addAccounts()).rejects.toThrow( + 'Cash keyring already has an account', + ); }); - it('only adds one account per call', async () => { + it('adds an account when none exist', async () => { const keyring = new CashKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, }); - await keyring.addAccounts(); - await keyring.addAccounts(); + keyring.removeAccount(await getAddressAtIndex(keyring, 0)); + const empty = await keyring.getAccounts(); + expect(empty).toHaveLength(0); + await keyring.addAccounts(); const accounts = await keyring.getAccounts(); - expect(accounts).toHaveLength(4); + expect(accounts).toHaveLength(1); }); }); diff --git a/packages/keyring-eth-cash/src/cash-keyring.ts b/packages/keyring-eth-cash/src/cash-keyring.ts index 821398612..78c9cde9a 100644 --- a/packages/keyring-eth-cash/src/cash-keyring.ts +++ b/packages/keyring-eth-cash/src/cash-keyring.ts @@ -30,9 +30,11 @@ export class CashKeyring extends HdKeyring { }); } - // This override ensures that we can only ever add a - // single account to this keyring override async addAccounts(): Promise { + const existing = await this.getAccounts(); + if (existing.length > 0) { + throw new Error('Cash keyring already has an account'); + } return super.addAccounts(1); } } From edd76fb4aba7b4f3f1791aadd824f42680883bf8 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 10 Mar 2026 14:19:23 +0000 Subject: [PATCH 10/12] chore: remove un-needed tests --- .../keyring-eth-cash/src/cash-keyring.test.ts | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/packages/keyring-eth-cash/src/cash-keyring.test.ts b/packages/keyring-eth-cash/src/cash-keyring.test.ts index d07a9478c..eea4727dd 100644 --- a/packages/keyring-eth-cash/src/cash-keyring.test.ts +++ b/packages/keyring-eth-cash/src/cash-keyring.test.ts @@ -85,26 +85,6 @@ describe('CashKeyring', () => { const serialized = await keyring.serialize(); expect(serialized.hdPath).toBe(cashHdPath); }); - - it('always uses the cash account hd path', async () => { - const keyring = new CashKeyring(); - await keyring.deserialize({ - mnemonic: sampleMnemonic, - }); - - const serialized = await keyring.serialize(); - expect(serialized.hdPath).toBe(cashHdPath); - }); - - it('always deserializes exactly one account', async () => { - const keyring = new CashKeyring(); - await keyring.deserialize({ - mnemonic: sampleMnemonic, - }); - - const accounts = await keyring.getAccounts(); - expect(accounts).toHaveLength(1); - }); }); describe('#addAccounts', () => { From 702efff10f514dff20910859ea7e334e8366ed59 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 10 Mar 2026 15:04:17 +0000 Subject: [PATCH 11/12] chore: fix pr tag --- .github/workflows/validate-pr-title.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-pr-title.yml b/.github/workflows/validate-pr-title.yml index 03a0385fe..7eba36c33 100644 --- a/.github/workflows/validate-pr-title.yml +++ b/.github/workflows/validate-pr-title.yml @@ -43,7 +43,7 @@ jobs: deps-dev keyring-api keyring-eth-hd - keyring-cash + keyring-eth-cash keyring-eth-ledger-bridge keyring-eth-simple keyring-eth-trezor From 25e4b24ee6874d49a29ba37ca43a4da1d41997bb Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 10 Mar 2026 15:19:14 +0000 Subject: [PATCH 12/12] chore: export the derivation path --- packages/keyring-eth-cash/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keyring-eth-cash/src/index.ts b/packages/keyring-eth-cash/src/index.ts index 56f96bb5b..4474660a7 100644 --- a/packages/keyring-eth-cash/src/index.ts +++ b/packages/keyring-eth-cash/src/index.ts @@ -1 +1 @@ -export { CashKeyring } from './cash-keyring'; +export { CashKeyring, CASH_DERIVATION_PATH } from './cash-keyring';