Skip to content

Commit ccd708e

Browse files
committed
wip: qr implementation
1 parent 7525e99 commit ccd708e

13 files changed

+485
-7
lines changed

packages/keyring-eth-qr/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Changelog
2+

packages/keyring-eth-qr/LICENSE

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
ISC License
2+
3+
Copyright (c) 2020 MetaMask
4+
5+
Permission to use, copy, modify, and/or distribute this software for any
6+
purpose with or without fee is hereby granted, provided that the above
7+
copyright notice and this permission notice appear in all copies.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

packages/keyring-eth-qr/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# QR Keyring
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
// An array of regexp pattern strings used to skip coverage collection
18+
coveragePathIgnorePatterns: ['./src/tests'],
19+
20+
// An object that configures minimum threshold enforcement for coverage results
21+
coverageThreshold: {
22+
global: {
23+
branches: 100,
24+
functions: 100,
25+
lines: 100,
26+
statements: 100,
27+
},
28+
},
29+
});

packages/keyring-eth-qr/package.json

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{
2+
"name": "@metamask/eth-qr-keyring",
3+
"version": "1.0.0",
4+
"description": "A simple standard interface for a series of Ethereum private keys.",
5+
"keywords": [
6+
"ethereum",
7+
"keyring",
8+
"qr",
9+
"metamask"
10+
],
11+
"homepage": "https://github.com/MetaMask/accounts/packages/keyring-eth-qr#readme",
12+
"bugs": {
13+
"url": "https://github.com/MetaMask/accounts/issues"
14+
},
15+
"repository": {
16+
"type": "git",
17+
"url": "https://github.com/MetaMask/accounts.git"
18+
},
19+
"main": "dist/index.js",
20+
"types": "dist/index.d.ts",
21+
"files": [
22+
"dist/"
23+
],
24+
"scripts": {
25+
"build": "tsc --build tsconfig.build.json",
26+
"build:clean": "rimraf dist && yarn build",
27+
"build:docs": "typedoc",
28+
"build:force": "tsc --build tsconfig.build.json --force",
29+
"changelog:update": "../../scripts/update-changelog.sh @metamask/eth-simple-keyring",
30+
"changelog:validate": "../../scripts/validate-changelog.sh @metamask/eth-simple-keyring",
31+
"publish:preview": "yarn npm publish --tag preview",
32+
"sample": "ts-node src/sample.ts",
33+
"test": "jest",
34+
"test:clean": "jest --clearCache",
35+
"test:verbose": "jest --verbose",
36+
"test:watch": "jest --watch"
37+
},
38+
"dependencies": {
39+
"@ethereumjs/util": "^9.1.0",
40+
"@keystonehq/bc-ur-registry-eth": "^0.21.0",
41+
"@keystonehq/ur-decoder": "^0.12.2",
42+
"@metamask/utils": "^9.2.1",
43+
"@ngraveio/bc-ur": "^1.1.13",
44+
"hdkey": "^2.1.0"
45+
},
46+
"devDependencies": {
47+
"@lavamoat/allow-scripts": "^3.2.1",
48+
"@metamask/auto-changelog": "^3.4.4",
49+
"@types/hdkey": "^2.0.1",
50+
"@types/jest": "^29.5.12",
51+
"@types/node": "^20.12.12",
52+
"deepmerge": "^4.2.2",
53+
"depcheck": "^1.4.7",
54+
"jest": "^29.5.0",
55+
"ts-jest": "^29.0.5",
56+
"ts-node": "^10.9.2",
57+
"typedoc": "^0.25.13",
58+
"typescript": "~4.8.4"
59+
},
60+
"engines": {
61+
"node": "^18.18 || >=20"
62+
},
63+
"publishConfig": {
64+
"access": "public",
65+
"registry": "https://registry.npmjs.org/"
66+
},
67+
"lavamoat": {
68+
"allowScripts": {
69+
"keccak": true,
70+
"secp256k1": true,
71+
"@lavamoat/preinstall-always-fail": false,
72+
"ethereumjs-tx>ethereumjs-util>ethereum-cryptography>keccak": false,
73+
"ethereumjs-tx>ethereumjs-util>ethereum-cryptography>secp256k1": false
74+
}
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { pubToAddress } from '@ethereumjs/util';
2+
import {
3+
CryptoAccount,
4+
type CryptoHDKey,
5+
type CryptoOutput,
6+
} from '@keystonehq/bc-ur-registry-eth';
7+
import { add0x, getChecksumAddress, type Hex } from '@metamask/utils';
8+
import HDKey from 'hdkey';
9+
10+
export type RootAccount = { fingerprint: Hex } & (
11+
| {
12+
type: 'hd';
13+
hdPath: string;
14+
childrenPath: string;
15+
bip32xPub: string;
16+
}
17+
| {
18+
type: 'account';
19+
addressPaths: Record<Hex, string>;
20+
}
21+
);
22+
23+
export class AccountDeriver {
24+
#root?: RootAccount;
25+
26+
init(source: CryptoAccount | CryptoHDKey) {
27+
const fingerprint = this.#getFingerprintFromSource(source);
28+
if (source instanceof CryptoAccount) {
29+
this.#root = {
30+
fingerprint,
31+
type: 'account',
32+
addressPaths: this.#getPathsFromCryptoOutputDescriptors(
33+
source.getOutputDescriptors(),
34+
),
35+
};
36+
} else {
37+
this.#root = {
38+
fingerprint,
39+
type: 'hd',
40+
hdPath: `m/${source.getOrigin().getPath()}`,
41+
childrenPath: source.getChildren().getPath(),
42+
bip32xPub: source.getBip32Key(),
43+
};
44+
}
45+
}
46+
47+
deriveIndex(index: number): Hex {
48+
if (!this.#root) {
49+
throw new Error('AccountDeriver not initialized');
50+
}
51+
52+
if (this.#root.type === 'account') {
53+
const address = Object.keys(this.#root.addressPaths)[index];
54+
if (!address) {
55+
throw new Error(`Address not found for index ${index}`);
56+
}
57+
return add0x(address);
58+
} else {
59+
const hdKey = HDKey.fromExtendedKey(this.#root.bip32xPub);
60+
hdKey.derive(`m/${index}`);
61+
const address = getChecksumAddress(
62+
add0x(Buffer.from(pubToAddress(hdKey.publicKey, true)).toString('hex')),
63+
);
64+
return add0x(address);
65+
}
66+
}
67+
68+
#getFingerprintFromSource(source: CryptoAccount | CryptoHDKey): Hex {
69+
return source instanceof CryptoAccount
70+
? add0x(source.getMasterFingerprint().toString('hex'))
71+
: add0x(source.getOrigin().getSourceFingerprint().toString('hex'));
72+
}
73+
74+
#getPathsFromCryptoOutputDescriptors(
75+
descriptors: CryptoOutput[],
76+
): Record<Hex, string> {
77+
return descriptors.reduce((paths: Record<Hex, string>, current) => {
78+
const hdKey = current.getHDKey();
79+
if (hdKey) {
80+
const path = `M/${hdKey.getOrigin().getPath()}`;
81+
const address = getChecksumAddress(
82+
add0x(
83+
Buffer.from(pubToAddress(hdKey.getKey(), true)).toString('hex'),
84+
),
85+
);
86+
paths[address] = path;
87+
}
88+
return paths;
89+
}, {});
90+
}
91+
}

packages/keyring-eth-qr/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './qr-keyring';
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { CryptoAccount, CryptoHDKey } from '@keystonehq/bc-ur-registry-eth';
2+
import { URRegistryDecoder } from '@keystonehq/ur-decoder';
3+
import type { Hex, Keyring } from '@metamask/utils';
4+
import { UR } from '@ngraveio/bc-ur';
5+
import { AccountDeriver } from './account-deriver';
6+
7+
export const QR_KEYRING_TYPE = 'QrKeyring' as const;
8+
9+
export const SUPPORTED_UR_TYPE = {
10+
CRYPTO_HDKEY: 'crypto-hdkey',
11+
CRYPTO_ACCOUNT: 'crypto-account',
12+
ETH_SIGNATURE: 'eth-signature',
13+
};
14+
15+
/**
16+
* The state of the QrKeyring
17+
*
18+
* @property accounts - The accounts in the QrKeyring
19+
*/
20+
export type QrKeyringState = {
21+
accounts: Record<number, Hex>;
22+
cbor?: Hex;
23+
};
24+
25+
export class QrKeyring implements Keyring<QrKeyringState> {
26+
type = QR_KEYRING_TYPE;
27+
28+
#accounts: Record<number, Hex> = {};
29+
30+
#deriver: AccountDeriver = new AccountDeriver();
31+
32+
async serialize(): Promise<QrKeyringState> {
33+
return {
34+
accounts: Object.values(this.#accounts),
35+
cbor: '', // TODO: Serialize deriver
36+
};
37+
}
38+
39+
async deserialize(state: QrKeyringState): Promise<void> {
40+
this.#accounts = state.accounts;
41+
if (state.cbor) {
42+
this.submitCBOR(state.cbor);
43+
}
44+
}
45+
46+
async addAccounts(_accountsToAdd: number): Promise<Hex[]> {
47+
// TODO: Implement
48+
}
49+
50+
async getAccounts(): Promise<Hex[]> {
51+
return Object.values(this.#accounts);
52+
}
53+
54+
submitCBOR(cbor: Hex) {
55+
const ur = URRegistryDecoder.decode(cbor);
56+
const bufferedCbor = Buffer.from(cbor, 'hex');
57+
let derivationSource: CryptoHDKey | CryptoAccount;
58+
59+
switch (ur.type) {
60+
case SUPPORTED_UR_TYPE.CRYPTO_HDKEY:
61+
derivationSource = CryptoHDKey.fromCBOR(bufferedCbor);
62+
break;
63+
case SUPPORTED_UR_TYPE.CRYPTO_ACCOUNT:
64+
derivationSource = CryptoAccount.fromCBOR(bufferedCbor);
65+
break;
66+
default:
67+
throw new Error('Unsupported UR type');
68+
}
69+
70+
this.#deriver.init(derivationSource);
71+
}
72+
73+
async setAccountToUnlock(index: number): Promise<void> {
74+
// TODO: Implement
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": "../../tsconfig.packages.build.json",
3+
"compilerOptions": {
4+
"baseUrl": "./",
5+
"outDir": "dist",
6+
"rootDir": "src",
7+
"target": "es2020"
8+
},
9+
"include": ["./src/**/*.ts"],
10+
"exclude": ["./src/**/*.test.ts", "./src/sample.ts"]
11+
}

packages/keyring-eth-qr/tsconfig.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "../../tsconfig.packages.json",
3+
"compilerOptions": {
4+
"baseUrl": "./"
5+
},
6+
"include": ["./src"],
7+
"exclude": ["./dist/**/*"]
8+
}

packages/keyring-eth-qr/typedoc.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"entryPoints": ["./src/index.ts"],
3+
"excludePrivate": true,
4+
"hideGenerator": true,
5+
"out": "docs"
6+
}

tsconfig.build.json

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"references": [
33
{ "path": "./packages/keyring-api/tsconfig.build.json" },
44
{ "path": "./packages/keyring-eth-ledger-bridge/tsconfig.build.json" },
5+
{ "path": "./packages/keyring-eth-qr/tsconfig.build.json" },
56
{ "path": "./packages/keyring-eth-simple/tsconfig.build.json" },
67
{ "path": "./packages/keyring-eth-trezor/tsconfig.build.json" },
78
{ "path": "./packages/keyring-snap-bridge/tsconfig.build.json" }

0 commit comments

Comments
 (0)