Skip to content

Commit dd48931

Browse files
authored
feat(account-api): add Account{Group,Wallet} support (#307)
Introducing a new package that will hold most new accounts-related classes/concepts. For now, it contains 2 new concepts (which are already in-use in the `account-tree-controller`): - `AccountWallet`: A wallet that can hold multiple account groups - `AccountGroup`: A group of accounts. Those 2 will be used as a base for the new work regarding multichain accounts.
1 parent 82bcfc5 commit dd48931

16 files changed

Lines changed: 383 additions & 1 deletion

.github/workflows/validate-pr-title.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ jobs:
5050
keyring-snap-client
5151
keyring-snap-sdk
5252
keyring-utils
53+
account-api
5354
subjectPattern: '^(?![A-Z]).+$'
5455
env:
5556
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This repository contains the following packages [^fn1]:
1212

1313
<!-- start package list -->
1414

15+
- [`@metamask/account-api`](packages/account-api)
1516
- [`@metamask/eth-hd-keyring`](packages/keyring-eth-hd)
1617
- [`@metamask/eth-ledger-bridge-keyring`](packages/keyring-eth-ledger-bridge)
1718
- [`@metamask/eth-simple-keyring`](packages/keyring-eth-simple)
@@ -34,6 +35,7 @@ Or, in graph form [^fn1]:
3435
%%{ init: { 'flowchart': { 'curve': 'bumpX' } } }%%
3536
graph LR;
3637
linkStyle default opacity:0.5
38+
account_api(["@metamask/account-api"]);
3739
keyring_api(["@metamask/keyring-api"]);
3840
eth_hd_keyring(["@metamask/eth-hd-keyring"]);
3941
eth_ledger_bridge_keyring(["@metamask/eth-ledger-bridge-keyring"]);
@@ -45,6 +47,8 @@ linkStyle default opacity:0.5
4547
keyring_snap_client(["@metamask/keyring-snap-client"]);
4648
keyring_snap_sdk(["@metamask/keyring-snap-sdk"]);
4749
keyring_utils(["@metamask/keyring-utils"]);
50+
account_api --> keyring_api;
51+
account_api --> keyring_utils;
4852
keyring_api --> keyring_utils;
4953
eth_hd_keyring --> keyring_utils;
5054
eth_ledger_bridge_keyring --> keyring_utils;

packages/account-api/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
[Unreleased]: https://github.com/MetaMask/accounts/

packages/account-api/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# MetaMask Account API
2+
3+
API for the MetaMask account model.
4+
5+
## Installation
6+
7+
`yarn add @metamask/account-api`
8+
9+
or
10+
11+
`npm install @metamask/account-api`
12+
13+
## Contributing
14+
15+
This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/accounts#readme).
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* For a detailed explanation regarding each configuration property and type check, visit:
3+
* https://jestjs.io/docs/configuration
4+
*/
5+
6+
const merge = require('deepmerge');
7+
const path = require('path');
8+
9+
const baseConfig = require('../../jest.config.packages');
10+
11+
const displayName = path.basename(__dirname);
12+
13+
module.exports = merge(baseConfig, {
14+
// The display name when running multiple projects
15+
displayName,
16+
17+
coveragePathIgnorePatterns: ['./src/tests'],
18+
19+
// An object that configures minimum threshold enforcement for coverage results
20+
coverageThreshold: {
21+
global: {
22+
branches: 100,
23+
functions: 100,
24+
lines: 100,
25+
statements: 100,
26+
},
27+
},
28+
});

packages/account-api/package.json

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
{
2+
"name": "@metamask/account-api",
3+
"version": "0.0.0",
4+
"description": "MetaMask Account API",
5+
"keywords": [
6+
"metamask",
7+
"account",
8+
"multichain"
9+
],
10+
"homepage": "https://github.com/MetaMask/accounts/tree/main/packages/account-api#readme",
11+
"bugs": {
12+
"url": "https://github.com/MetaMask/accounts/issues"
13+
},
14+
"repository": {
15+
"type": "git",
16+
"url": "https://github.com/MetaMask/accounts.git"
17+
},
18+
"exports": {
19+
".": {
20+
"import": {
21+
"types": "./dist/index.d.mts",
22+
"default": "./dist/index.mjs"
23+
},
24+
"require": {
25+
"types": "./dist/index.d.cts",
26+
"default": "./dist/index.cjs"
27+
}
28+
}
29+
},
30+
"main": "./dist/index.cjs",
31+
"types": "./dist/index.d.cts",
32+
"files": [
33+
"dist/"
34+
],
35+
"scripts": {
36+
"build": "ts-bridge --project tsconfig.build.json --no-references",
37+
"build:clean": "yarn build --clean",
38+
"build:docs": "typedoc",
39+
"changelog:update": "../../scripts/update-changelog.sh @metamask/account-api",
40+
"changelog:validate": "../../scripts/validate-changelog.sh @metamask/account-api",
41+
"publish:preview": "yarn npm publish --tag preview",
42+
"test": "yarn test:source && yarn test:types",
43+
"test:clean": "jest --clearCache",
44+
"test:source": "jest && jest-it-up",
45+
"test:types": "../../scripts/tsd-test.sh ./src",
46+
"test:watch": "jest --watch"
47+
},
48+
"dependencies": {
49+
"@metamask/keyring-api": "workspace:^",
50+
"@metamask/keyring-utils": "workspace:^"
51+
},
52+
"devDependencies": {
53+
"@lavamoat/allow-scripts": "^3.2.1",
54+
"@lavamoat/preinstall-always-fail": "^2.1.0",
55+
"@metamask/auto-changelog": "^3.4.4",
56+
"@ts-bridge/cli": "^0.6.3",
57+
"@types/jest": "^29.5.12",
58+
"@types/node": "^20.12.12",
59+
"deepmerge": "^4.2.2",
60+
"depcheck": "^1.4.7",
61+
"jest": "^29.5.0",
62+
"jest-it-up": "^3.1.0",
63+
"rimraf": "^5.0.7",
64+
"ts-jest": "^29.0.5",
65+
"ts-node": "^10.9.2",
66+
"tsd": "^0.31.0",
67+
"typedoc": "^0.25.13",
68+
"typescript": "~5.6.3"
69+
},
70+
"engines": {
71+
"node": "^18.18 || >=20"
72+
},
73+
"publishConfig": {
74+
"access": "public",
75+
"registry": "https://registry.npmjs.org/"
76+
},
77+
"lavamoat": {
78+
"allowScripts": {
79+
"@lavamoat/preinstall-always-fail": false
80+
}
81+
},
82+
"tsd": {
83+
"directory": "src",
84+
"compilerOptions": {
85+
"composite": "false"
86+
}
87+
}
88+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { KeyringAccount } from '@metamask/keyring-api';
2+
import type { AccountId } from '@metamask/keyring-utils';
3+
4+
// Circular import are allowed when using `import type`.
5+
import type { AccountWallet, AccountWalletId } from './wallet';
6+
7+
/**
8+
* Default account group unique ID.
9+
*
10+
* This constant can be used to reference the "default" group from
11+
* an account wallet.
12+
*/
13+
export const DEFAULT_ACCOUNT_GROUP_UNIQUE_ID: string = 'default';
14+
15+
/**
16+
* Account group ID.
17+
*/
18+
export type AccountGroupId = `${AccountWalletId}/${string}`;
19+
20+
/**
21+
* Account group that can hold multiple accounts.
22+
*/
23+
export type AccountGroup<Account extends KeyringAccount> = {
24+
/**
25+
* Account group ID.
26+
*/
27+
get id(): AccountGroupId;
28+
29+
/**
30+
* Account wallet (parent).
31+
*/
32+
get wallet(): AccountWallet<Account>;
33+
34+
/**
35+
* Gets the "blockchain" accounts for this account group.
36+
*
37+
* @param id - Account ID.
38+
* @returns The "blockchain" accounts.
39+
*/
40+
getAccounts(): Account[];
41+
42+
/**
43+
* Gets the "blockchain" account for a given account ID.
44+
*
45+
* @param id - Account ID.
46+
* @returns The "blockchain" account or undefined if not found.
47+
*/
48+
getAccount(id: AccountId): Account | undefined;
49+
};
50+
51+
/**
52+
* Convert a wallet ID and a unique ID, to a group ID.
53+
*
54+
* @param walletId - A wallet ID.
55+
* @param id - A unique ID.
56+
* @returns A group ID.
57+
*/
58+
export function toAccountGroupId(
59+
walletId: AccountWalletId,
60+
id: string,
61+
): AccountGroupId {
62+
return `${walletId}/${id}`;
63+
}
64+
65+
/**
66+
* Convert a wallet ID to the default group ID.
67+
*
68+
* @param walletId - A wallet ID.
69+
* @returns The default group ID.
70+
*/
71+
export function toDefaultAccountGroupId(
72+
walletId: AccountWalletId,
73+
): AccountGroupId {
74+
return toAccountGroupId(walletId, DEFAULT_ACCOUNT_GROUP_UNIQUE_ID);
75+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { toAccountGroupId, toDefaultAccountGroupId } from './group';
2+
import { AccountWalletCategory, toAccountWalletId } from './wallet';
3+
4+
describe('toAccountGroupId', () => {
5+
it('converts a account wallet id and a unique id to a group id', () => {
6+
const walletId = toAccountWalletId(AccountWalletCategory.Keyring, 'test');
7+
const groupId = toAccountGroupId(walletId, 'test');
8+
9+
expect(groupId.startsWith(walletId)).toBe(true);
10+
});
11+
});
12+
13+
describe('toDefaultAccountGroupId', () => {
14+
it('converts a account wallet id and to the default group id', () => {
15+
const walletId = toAccountWalletId(AccountWalletCategory.Keyring, 'test');
16+
const groupId = toDefaultAccountGroupId(walletId);
17+
18+
expect(groupId.startsWith(walletId)).toBe(true);
19+
});
20+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './group';
2+
export * from './wallet';
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { KeyringAccount } from '@metamask/keyring-api';
2+
3+
// Circular import are allowed when using `import type`.
4+
import type { AccountGroup, AccountGroupId } from './group';
5+
6+
export enum AccountWalletCategory {
7+
/**
8+
* Category for wallets that group accounts based on their
9+
* entropy source.
10+
*/
11+
Entropy = 'entropy',
12+
13+
/**
14+
* Category for wallets that group accounts based on their
15+
* keyring's type.
16+
*/
17+
Keyring = 'keyring',
18+
19+
/**
20+
* Category for wallets that group accounts associated with an
21+
* account management Snap.
22+
*/
23+
Snap = 'snap',
24+
}
25+
26+
/**
27+
* Account wallet ID.
28+
*/
29+
export type AccountWalletId = `${AccountWalletCategory}:${string}`;
30+
31+
/**
32+
* Account wallet that can hold multiple account groups.
33+
*/
34+
export type AccountWallet<Account extends KeyringAccount> = {
35+
/**
36+
* Account wallet ID.
37+
*/
38+
get id(): AccountWalletId;
39+
40+
/**
41+
* Account wallet category.
42+
*/
43+
get category(): AccountWalletCategory;
44+
45+
/**
46+
* Gets account group for a given ID.
47+
*
48+
* @returns Account group.
49+
*/
50+
getAccountGroup(id: AccountGroupId): AccountGroup<Account> | undefined;
51+
52+
/**
53+
* Gets all account groups.
54+
*
55+
* @returns Account groups.
56+
*/
57+
getAccountGroups(): AccountGroup<Account>[];
58+
};
59+
60+
/**
61+
* Convert a unique ID to a wallet ID for a given category.
62+
*
63+
* @param category - A wallet category.
64+
* @param id - A unique ID.
65+
* @returns A wallet ID.
66+
*/
67+
export function toAccountWalletId(
68+
category: AccountWalletCategory,
69+
id: string,
70+
): AccountWalletId {
71+
return `${category}:${id}`;
72+
}

0 commit comments

Comments
 (0)