@alephium/ledger-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +13 -0
- package/.eslintignore +2 -0
- package/.eslintrc.json +16 -0
- package/.gitattributes +1 -0
- package/README.md +1 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +79 -0
- package/dist/src/serde.d.ts +5 -0
- package/dist/src/serde.js +32 -0
- package/dist/src/serde.test.d.ts +1 -0
- package/dist/src/serde.test.js +13 -0
- package/dist/test/release.test.d.ts +1 -0
- package/dist/test/release.test.js +29 -0
- package/dist/test/speculos.test.d.ts +1 -0
- package/dist/test/speculos.test.js +95 -0
- package/jest-config.json +11 -0
- package/package.json +56 -0
- package/src/index.ts +63 -0
- package/src/serde.ts +30 -0
- package/test/release.test.ts +32 -0
- package/test/speculos.test.ts +79 -0
- package/tsconfig.json +20 -0
package/.editorconfig
ADDED
package/.eslintignore
ADDED
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"prettier",
|
|
4
|
+
"plugin:prettier/recommended",
|
|
5
|
+
"plugin:@typescript-eslint/recommended"
|
|
6
|
+
],
|
|
7
|
+
"rules": {
|
|
8
|
+
"header/header": ["off"]
|
|
9
|
+
},
|
|
10
|
+
"parserOptions": {
|
|
11
|
+
"project": "tsconfig.json",
|
|
12
|
+
"ecmaVersion": 2020,
|
|
13
|
+
"sourceType": "module"
|
|
14
|
+
},
|
|
15
|
+
"parser": "@typescript-eslint/parser"
|
|
16
|
+
}
|
package/.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.ral linguist-language=Rust
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# TS package for ledger integration
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { Account } from '@alephium/web3';
|
|
3
|
+
import Transport from '@ledgerhq/hw-transport';
|
|
4
|
+
export declare const CLA = 128;
|
|
5
|
+
export declare enum INS {
|
|
6
|
+
GET_VERSION = 0,
|
|
7
|
+
GET_PUBLIC_KEY = 1,
|
|
8
|
+
SIGN_HASH = 2
|
|
9
|
+
}
|
|
10
|
+
export declare const GROUP_NUM = 4;
|
|
11
|
+
export declare const HASH_LEN = 32;
|
|
12
|
+
export default class AlephiumApp {
|
|
13
|
+
readonly transport: Transport;
|
|
14
|
+
constructor(transport: Transport);
|
|
15
|
+
getVersion(): Promise<string>;
|
|
16
|
+
getAccount(startPath: string, targetGroup?: number): Promise<Account>;
|
|
17
|
+
signHash(path: string, hash: Buffer): Promise<string>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.HASH_LEN = exports.GROUP_NUM = exports.INS = exports.CLA = void 0;
|
|
27
|
+
const web3_1 = require("@alephium/web3");
|
|
28
|
+
const hw_transport_1 = require("@ledgerhq/hw-transport");
|
|
29
|
+
const serde = __importStar(require("./serde"));
|
|
30
|
+
const elliptic_1 = require("elliptic");
|
|
31
|
+
const console_1 = require("console");
|
|
32
|
+
const ec = new elliptic_1.ec('secp256k1');
|
|
33
|
+
exports.CLA = 0x80;
|
|
34
|
+
var INS;
|
|
35
|
+
(function (INS) {
|
|
36
|
+
INS[INS["GET_VERSION"] = 0] = "GET_VERSION";
|
|
37
|
+
INS[INS["GET_PUBLIC_KEY"] = 1] = "GET_PUBLIC_KEY";
|
|
38
|
+
INS[INS["SIGN_HASH"] = 2] = "SIGN_HASH";
|
|
39
|
+
})(INS = exports.INS || (exports.INS = {}));
|
|
40
|
+
exports.GROUP_NUM = 4;
|
|
41
|
+
exports.HASH_LEN = 32;
|
|
42
|
+
class AlephiumApp {
|
|
43
|
+
constructor(transport) {
|
|
44
|
+
this.transport = transport;
|
|
45
|
+
}
|
|
46
|
+
async getVersion() {
|
|
47
|
+
const response = await this.transport.send(exports.CLA, INS.GET_VERSION, 0x00, 0x00);
|
|
48
|
+
console.log(`response ${response.length} - ${response.toString('hex')}`);
|
|
49
|
+
return `${response[0]}.${response[1]}.${response[2]}`;
|
|
50
|
+
}
|
|
51
|
+
// TODO: make address display optional
|
|
52
|
+
async getAccount(startPath, targetGroup) {
|
|
53
|
+
(0, console_1.assert)((targetGroup ?? 0) < exports.GROUP_NUM);
|
|
54
|
+
const p1 = targetGroup === undefined ? 0x00 : exports.GROUP_NUM;
|
|
55
|
+
const p2 = targetGroup === undefined ? 0x00 : targetGroup;
|
|
56
|
+
const response = await this.transport.send(exports.CLA, INS.GET_PUBLIC_KEY, p1, p2, serde.serializePath(startPath));
|
|
57
|
+
const publicKey = ec.keyFromPublic(response.slice(0, 65)).getPublic(true, 'hex');
|
|
58
|
+
const address = (0, web3_1.addressFromPublicKey)(publicKey);
|
|
59
|
+
const group = (0, web3_1.groupOfAddress)(address);
|
|
60
|
+
return { publicKey: publicKey, address: address, group: group };
|
|
61
|
+
}
|
|
62
|
+
async signHash(path, hash) {
|
|
63
|
+
if (hash.length !== exports.HASH_LEN) {
|
|
64
|
+
throw new Error('Invalid hash length');
|
|
65
|
+
}
|
|
66
|
+
const data = Buffer.concat([serde.serializePath(path), hash]);
|
|
67
|
+
console.log(`data ${data.length}`);
|
|
68
|
+
const response = await this.transport.send(exports.CLA, INS.SIGN_HASH, 0x00, 0x00, data, [hw_transport_1.StatusCodes.OK]);
|
|
69
|
+
console.log(`response ${response.length} - ${response.toString('hex')}`);
|
|
70
|
+
// Decode signature: https://bitcoin.stackexchange.com/a/12556
|
|
71
|
+
const rLen = response.slice(3, 4)[0];
|
|
72
|
+
const r = response.slice(4, 4 + rLen);
|
|
73
|
+
const sLen = response.slice(5 + rLen, 6 + rLen)[0];
|
|
74
|
+
const s = response.slice(6 + rLen, 6 + rLen + sLen);
|
|
75
|
+
console.log(`${rLen} - ${r.toString('hex')}\n${sLen} - ${s.toString('hex')}`);
|
|
76
|
+
return (0, web3_1.encodeHexSignature)(r.toString('hex'), s.toString('hex'));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.default = AlephiumApp;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serializePath = exports.splitPath = exports.FALSE = exports.TRUE = void 0;
|
|
4
|
+
exports.TRUE = 0x10;
|
|
5
|
+
exports.FALSE = 0x00;
|
|
6
|
+
function splitPath(path) {
|
|
7
|
+
const result = [];
|
|
8
|
+
const allComponents = path.trim().split('/');
|
|
9
|
+
const components = allComponents.length > 0 && allComponents[0] == 'm' ? allComponents.slice(1) : allComponents;
|
|
10
|
+
components.forEach((element) => {
|
|
11
|
+
let number = parseInt(element, 10);
|
|
12
|
+
if (isNaN(number)) {
|
|
13
|
+
throw Error(`Invalid bip32 path: ${path}`);
|
|
14
|
+
}
|
|
15
|
+
if (element.length > 1 && element[element.length - 1] === "'") {
|
|
16
|
+
number += 0x80000000;
|
|
17
|
+
}
|
|
18
|
+
result.push(number);
|
|
19
|
+
});
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
exports.splitPath = splitPath;
|
|
23
|
+
function serializePath(path) {
|
|
24
|
+
const nodes = splitPath(path);
|
|
25
|
+
if (nodes.length != 5) {
|
|
26
|
+
throw Error('Invalid BIP32 path length');
|
|
27
|
+
}
|
|
28
|
+
const buffer = Buffer.alloc(nodes.length * 4);
|
|
29
|
+
nodes.forEach((element, index) => buffer.writeUInt32BE(element, 4 * index));
|
|
30
|
+
return buffer;
|
|
31
|
+
}
|
|
32
|
+
exports.serializePath = serializePath;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const serde_1 = require("./serde");
|
|
4
|
+
describe('serde', () => {
|
|
5
|
+
it('should split path', () => {
|
|
6
|
+
expect((0, serde_1.splitPath)(`m/44'/1234'/0'/0/0`)).toStrictEqual([44 + 0x80000000, 1234 + 0x80000000, 0 + 0x80000000, 0, 0]);
|
|
7
|
+
expect((0, serde_1.splitPath)(`44'/1234'/0'/0/0`)).toStrictEqual([44 + 0x80000000, 1234 + 0x80000000, 0 + 0x80000000, 0, 0]);
|
|
8
|
+
});
|
|
9
|
+
it('should encode path', () => {
|
|
10
|
+
expect((0, serde_1.serializePath)(`m/1'/2'/0'/0/0`)).toStrictEqual(Buffer.from([0x80, 0, 0, 1, 0x80, 0, 0, 2, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]));
|
|
11
|
+
expect((0, serde_1.serializePath)(`m/1'/2'/0'/0/0`)).toStrictEqual(Buffer.from([0x80, 0, 0, 1, 0x80, 0, 0, 2, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]));
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const hw_transport_node_hid_1 = __importDefault(require("@ledgerhq/hw-transport-node-hid"));
|
|
7
|
+
const logs_1 = require("@ledgerhq/logs");
|
|
8
|
+
const blakejs_1 = __importDefault(require("blakejs"));
|
|
9
|
+
const web3_1 = require("@alephium/web3");
|
|
10
|
+
function sleep(ms) {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
const src_1 = __importDefault(require("../src"));
|
|
14
|
+
describe('Integration', () => {
|
|
15
|
+
const path = `m/44'/1234'/0'/0/0`;
|
|
16
|
+
// enable this for integration test
|
|
17
|
+
it('should test node', async () => {
|
|
18
|
+
const transport = await hw_transport_node_hid_1.default.open('');
|
|
19
|
+
(0, logs_1.listen)((log) => console.log(log));
|
|
20
|
+
const app = new src_1.default(transport);
|
|
21
|
+
const account = await app.getAccount(path);
|
|
22
|
+
console.log(`${JSON.stringify(account)}`);
|
|
23
|
+
const hash = Buffer.from(blakejs_1.default.blake2b(Buffer.from([0, 1, 2, 3, 4]), undefined, 32));
|
|
24
|
+
const signature = await app.signHash(path, hash);
|
|
25
|
+
console.log(signature);
|
|
26
|
+
expect((0, web3_1.transactionVerifySignature)(hash.toString('hex'), account.publicKey, signature)).toBe(true);
|
|
27
|
+
await transport.close();
|
|
28
|
+
}, 100000);
|
|
29
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
const hw_transport_node_speculos_1 = __importDefault(require("@ledgerhq/hw-transport-node-speculos"));
|
|
30
|
+
const src_1 = __importStar(require("../src"));
|
|
31
|
+
const blakejs_1 = __importDefault(require("blakejs"));
|
|
32
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
33
|
+
const web3_1 = require("@alephium/web3");
|
|
34
|
+
function sleep(ms) {
|
|
35
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
}
|
|
37
|
+
async function pressButton(button) {
|
|
38
|
+
await sleep(500);
|
|
39
|
+
return (0, node_fetch_1.default)(`http://localhost:25000/button/${button}`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
body: JSON.stringify({ action: 'press-and-release' })
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function getRandomInt(min, max) {
|
|
45
|
+
min = Math.ceil(min);
|
|
46
|
+
max = Math.floor(max);
|
|
47
|
+
return Math.floor(Math.random() * (max - min) + min); // The maximum is exclusive and the minimum is inclusive
|
|
48
|
+
}
|
|
49
|
+
describe('sdk', () => {
|
|
50
|
+
const apduPort = 9999;
|
|
51
|
+
let path;
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
path = `m/44'/1234'/0'/0/` + getRandomInt(0, 1000000);
|
|
54
|
+
});
|
|
55
|
+
it('should get version', async () => {
|
|
56
|
+
const transport = await hw_transport_node_speculos_1.default.open({ apduPort });
|
|
57
|
+
const app = new src_1.default(transport);
|
|
58
|
+
const version = await app.getVersion();
|
|
59
|
+
expect(version).toBe('0.1.0');
|
|
60
|
+
await transport.close();
|
|
61
|
+
});
|
|
62
|
+
it('should get public key', async () => {
|
|
63
|
+
const transport = await hw_transport_node_speculos_1.default.open({ apduPort });
|
|
64
|
+
const app = new src_1.default(transport);
|
|
65
|
+
const account = await app.getAccount(path);
|
|
66
|
+
console.log(account);
|
|
67
|
+
await transport.close();
|
|
68
|
+
});
|
|
69
|
+
it('should get public key for group', async () => {
|
|
70
|
+
const transport = await hw_transport_node_speculos_1.default.open({ apduPort });
|
|
71
|
+
const app = new src_1.default(transport);
|
|
72
|
+
Array(src_1.GROUP_NUM).forEach(async (_, group) => {
|
|
73
|
+
const account = await app.getAccount(path, group);
|
|
74
|
+
expect((0, web3_1.groupOfAddress)(account.address)).toBe(group);
|
|
75
|
+
});
|
|
76
|
+
await transport.close();
|
|
77
|
+
});
|
|
78
|
+
it('should sign hash', async () => {
|
|
79
|
+
const transport = await hw_transport_node_speculos_1.default.open({ apduPort });
|
|
80
|
+
const app = new src_1.default(transport);
|
|
81
|
+
const account = await app.getAccount(path);
|
|
82
|
+
console.log(account);
|
|
83
|
+
const hash = Buffer.from(blakejs_1.default.blake2b(Buffer.from([0, 1, 2, 3, 4]), undefined, 32));
|
|
84
|
+
setTimeout(async () => {
|
|
85
|
+
await pressButton('both'); // review message
|
|
86
|
+
await pressButton('both'); // done review
|
|
87
|
+
await pressButton('right'); // select signing
|
|
88
|
+
await pressButton('both'); // done selection
|
|
89
|
+
}, 1000);
|
|
90
|
+
const signature = await app.signHash(path, hash);
|
|
91
|
+
console.log(signature);
|
|
92
|
+
await transport.close();
|
|
93
|
+
expect((0, web3_1.transactionVerifySignature)(hash.toString('hex'), account.publicKey, signature)).toBe(true);
|
|
94
|
+
}, 10000);
|
|
95
|
+
});
|
package/jest-config.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"testPathIgnorePatterns": [".*/node_modules/"],
|
|
3
|
+
"transform": {
|
|
4
|
+
"^.+\\.(t|j)sx?$": "ts-jest"
|
|
5
|
+
},
|
|
6
|
+
"testMatch": ["**/*.test.ts"],
|
|
7
|
+
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
|
|
8
|
+
"collectCoverage": true,
|
|
9
|
+
"coverageDirectory": "./coverage/",
|
|
10
|
+
"collectCoverageFrom": ["src/**/*.ts"]
|
|
11
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alephium/ledger-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "GPL",
|
|
5
|
+
"types": "dist/src/index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist/src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "npm run clean:windows && npm run clean:unix && npx --yes tsc --build .",
|
|
11
|
+
"clean:unix": "node -e \"if (process.platform !== 'win32') process.exit(1)\" || rm -rf dist",
|
|
12
|
+
"clean:windows": "node -e \"if (process.platform === 'win32') process.exit(1)\" || , if exist dist rmdir /Q /S dist",
|
|
13
|
+
"lint": "eslint . --ext ts",
|
|
14
|
+
"lint:fix": "eslint . --fix --ext ts",
|
|
15
|
+
"test": "jest -i --config ./jest-config.json",
|
|
16
|
+
"publish": "npm run build && npm publish --access public"
|
|
17
|
+
},
|
|
18
|
+
"prettier": {
|
|
19
|
+
"printWidth": 120,
|
|
20
|
+
"tabWidth": 2,
|
|
21
|
+
"useTabs": false,
|
|
22
|
+
"semi": false,
|
|
23
|
+
"singleQuote": true,
|
|
24
|
+
"bracketSameLine": false,
|
|
25
|
+
"trailingComma": "none"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@alephium/cli": "0.3.0-rc.4",
|
|
29
|
+
"@alephium/web3": "0.3.0-rc.4",
|
|
30
|
+
"@alephium/web3-test": "0.3.0-rc.4",
|
|
31
|
+
"@alephium/web3-wallet": "0.3.0-rc.4",
|
|
32
|
+
"@ledgerhq/hw-transport": "6.27.10"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@ledgerhq/hw-transport-node-hid": "^6.27.9",
|
|
36
|
+
"@ledgerhq/hw-transport-node-speculos": "^6.27.9",
|
|
37
|
+
"@ledgerhq/logs": "^6.10.1",
|
|
38
|
+
"@types/elliptic": "^6.4.13",
|
|
39
|
+
"@types/jest": "^27.5.1",
|
|
40
|
+
"@types/node": "^16.7.8",
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
|
42
|
+
"@typescript-eslint/parser": "^4.30.0",
|
|
43
|
+
"eslint": "^7.32.0",
|
|
44
|
+
"eslint-config-prettier": "^8.5.0",
|
|
45
|
+
"eslint-plugin-prettier": "^4.0.0",
|
|
46
|
+
"jest": "^28.1.0",
|
|
47
|
+
"node-fetch": "^2.6.7",
|
|
48
|
+
"ts-jest": "^28.0.2",
|
|
49
|
+
"ts-node": "^10.7.0",
|
|
50
|
+
"typescript": "^4.4.2"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=14.0.0",
|
|
54
|
+
"npm": ">=7.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Account, addressFromPublicKey, encodeHexSignature, groupOfAddress } from '@alephium/web3'
|
|
2
|
+
import Transport, { StatusCodes } from '@ledgerhq/hw-transport'
|
|
3
|
+
import * as serde from './serde'
|
|
4
|
+
import { ec as EC } from 'elliptic'
|
|
5
|
+
import { assert } from 'console'
|
|
6
|
+
|
|
7
|
+
const ec = new EC('secp256k1')
|
|
8
|
+
|
|
9
|
+
export const CLA = 0x80
|
|
10
|
+
export enum INS {
|
|
11
|
+
GET_VERSION = 0x00,
|
|
12
|
+
GET_PUBLIC_KEY = 0x01,
|
|
13
|
+
SIGN_HASH = 0x02
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const GROUP_NUM = 4
|
|
17
|
+
export const HASH_LEN = 32
|
|
18
|
+
|
|
19
|
+
export default class AlephiumApp {
|
|
20
|
+
readonly transport: Transport
|
|
21
|
+
|
|
22
|
+
constructor(transport: Transport) {
|
|
23
|
+
this.transport = transport
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getVersion(): Promise<string> {
|
|
27
|
+
const response = await this.transport.send(CLA, INS.GET_VERSION, 0x00, 0x00)
|
|
28
|
+
console.log(`response ${response.length} - ${response.toString('hex')}`)
|
|
29
|
+
return `${response[0]}.${response[1]}.${response[2]}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// TODO: make address display optional
|
|
33
|
+
async getAccount(startPath: string, targetGroup?: number): Promise<Account> {
|
|
34
|
+
assert((targetGroup ?? 0) < GROUP_NUM)
|
|
35
|
+
const p1 = targetGroup === undefined ? 0x00 : GROUP_NUM
|
|
36
|
+
const p2 = targetGroup === undefined ? 0x00 : targetGroup
|
|
37
|
+
const response = await this.transport.send(CLA, INS.GET_PUBLIC_KEY, p1, p2, serde.serializePath(startPath))
|
|
38
|
+
const publicKey = ec.keyFromPublic(response.slice(0, 65)).getPublic(true, 'hex')
|
|
39
|
+
const address = addressFromPublicKey(publicKey)
|
|
40
|
+
const group = groupOfAddress(address)
|
|
41
|
+
|
|
42
|
+
return { publicKey: publicKey, address: address, group: group }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async signHash(path: string, hash: Buffer): Promise<string> {
|
|
46
|
+
if (hash.length !== HASH_LEN) {
|
|
47
|
+
throw new Error('Invalid hash length')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data = Buffer.concat([serde.serializePath(path), hash])
|
|
51
|
+
console.log(`data ${data.length}`)
|
|
52
|
+
const response = await this.transport.send(CLA, INS.SIGN_HASH, 0x00, 0x00, data, [StatusCodes.OK])
|
|
53
|
+
console.log(`response ${response.length} - ${response.toString('hex')}`)
|
|
54
|
+
|
|
55
|
+
// Decode signature: https://bitcoin.stackexchange.com/a/12556
|
|
56
|
+
const rLen = response.slice(3, 4)[0]
|
|
57
|
+
const r = response.slice(4, 4 + rLen)
|
|
58
|
+
const sLen = response.slice(5 + rLen, 6 + rLen)[0]
|
|
59
|
+
const s = response.slice(6 + rLen, 6 + rLen + sLen)
|
|
60
|
+
console.log(`${rLen} - ${r.toString('hex')}\n${sLen} - ${s.toString('hex')}`)
|
|
61
|
+
return encodeHexSignature(r.toString('hex'), s.toString('hex'))
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/serde.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const TRUE = 0x10
|
|
2
|
+
export const FALSE = 0x00
|
|
3
|
+
|
|
4
|
+
export function splitPath(path: string): number[] {
|
|
5
|
+
const result: number[] = []
|
|
6
|
+
const allComponents = path.trim().split('/')
|
|
7
|
+
const components = allComponents.length > 0 && allComponents[0] == 'm' ? allComponents.slice(1) : allComponents
|
|
8
|
+
components.forEach((element) => {
|
|
9
|
+
let number = parseInt(element, 10)
|
|
10
|
+
if (isNaN(number)) {
|
|
11
|
+
throw Error(`Invalid bip32 path: ${path}`)
|
|
12
|
+
}
|
|
13
|
+
if (element.length > 1 && element[element.length - 1] === "'") {
|
|
14
|
+
number += 0x80000000
|
|
15
|
+
}
|
|
16
|
+
result.push(number)
|
|
17
|
+
})
|
|
18
|
+
return result
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function serializePath(path: string): Buffer {
|
|
22
|
+
const nodes = splitPath(path)
|
|
23
|
+
|
|
24
|
+
if (nodes.length != 5) {
|
|
25
|
+
throw Error('Invalid BIP32 path length')
|
|
26
|
+
}
|
|
27
|
+
const buffer = Buffer.alloc(nodes.length * 4)
|
|
28
|
+
nodes.forEach((element, index) => buffer.writeUInt32BE(element, 4 * index))
|
|
29
|
+
return buffer
|
|
30
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import NodeTransport from '@ledgerhq/hw-transport-node-hid'
|
|
2
|
+
import { listen } from '@ledgerhq/logs'
|
|
3
|
+
import blake from 'blakejs'
|
|
4
|
+
|
|
5
|
+
import { transactionVerifySignature } from '@alephium/web3'
|
|
6
|
+
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
import AlephiumApp, { GROUP_NUM } from '../src'
|
|
12
|
+
|
|
13
|
+
describe('Integration', () => {
|
|
14
|
+
const path = `m/44'/1234'/0'/0/0`
|
|
15
|
+
|
|
16
|
+
// enable this for integration test
|
|
17
|
+
it('should test node', async () => {
|
|
18
|
+
const transport = await NodeTransport.open('')
|
|
19
|
+
listen((log) => console.log(log))
|
|
20
|
+
const app = new AlephiumApp(transport)
|
|
21
|
+
|
|
22
|
+
const account = await app.getAccount(path)
|
|
23
|
+
console.log(`${JSON.stringify(account)}`)
|
|
24
|
+
|
|
25
|
+
const hash = Buffer.from(blake.blake2b(Buffer.from([0, 1, 2, 3, 4]), undefined, 32))
|
|
26
|
+
const signature = await app.signHash(path, hash)
|
|
27
|
+
console.log(signature)
|
|
28
|
+
expect(transactionVerifySignature(hash.toString('hex'), account.publicKey, signature)).toBe(true)
|
|
29
|
+
|
|
30
|
+
await transport.close()
|
|
31
|
+
}, 100000)
|
|
32
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import SpeculosTransport from '@ledgerhq/hw-transport-node-speculos'
|
|
2
|
+
import AlephiumApp, { GROUP_NUM } from '../src'
|
|
3
|
+
import blake from 'blakejs'
|
|
4
|
+
import fetch from 'node-fetch'
|
|
5
|
+
import { groupOfAddress, transactionVerifySignature } from '@alephium/web3'
|
|
6
|
+
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function pressButton(button: 'left' | 'right' | 'both') {
|
|
12
|
+
await sleep(500)
|
|
13
|
+
return fetch(`http://localhost:25000/button/${button}`, {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
body: JSON.stringify({ action: 'press-and-release' })
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getRandomInt(min, max) {
|
|
20
|
+
min = Math.ceil(min)
|
|
21
|
+
max = Math.floor(max)
|
|
22
|
+
return Math.floor(Math.random() * (max - min) + min) // The maximum is exclusive and the minimum is inclusive
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('sdk', () => {
|
|
26
|
+
const apduPort = 9999
|
|
27
|
+
let path: string
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
path = `m/44'/1234'/0'/0/` + getRandomInt(0, 1000000)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should get version', async () => {
|
|
34
|
+
const transport = await SpeculosTransport.open({ apduPort })
|
|
35
|
+
const app = new AlephiumApp(transport)
|
|
36
|
+
const version = await app.getVersion()
|
|
37
|
+
expect(version).toBe('0.1.0')
|
|
38
|
+
await transport.close()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should get public key', async () => {
|
|
42
|
+
const transport = await SpeculosTransport.open({ apduPort })
|
|
43
|
+
const app = new AlephiumApp(transport)
|
|
44
|
+
const account = await app.getAccount(path)
|
|
45
|
+
console.log(account)
|
|
46
|
+
await transport.close()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should get public key for group', async () => {
|
|
50
|
+
const transport = await SpeculosTransport.open({ apduPort })
|
|
51
|
+
const app = new AlephiumApp(transport)
|
|
52
|
+
Array(GROUP_NUM).forEach(async (_, group) => {
|
|
53
|
+
const account = await app.getAccount(path, group)
|
|
54
|
+
expect(groupOfAddress(account.address)).toBe(group)
|
|
55
|
+
})
|
|
56
|
+
await transport.close()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should sign hash', async () => {
|
|
60
|
+
const transport = await SpeculosTransport.open({ apduPort })
|
|
61
|
+
const app = new AlephiumApp(transport)
|
|
62
|
+
|
|
63
|
+
const account = await app.getAccount(path)
|
|
64
|
+
console.log(account)
|
|
65
|
+
|
|
66
|
+
const hash = Buffer.from(blake.blake2b(Buffer.from([0, 1, 2, 3, 4]), undefined, 32))
|
|
67
|
+
setTimeout(async () => {
|
|
68
|
+
await pressButton('both') // review message
|
|
69
|
+
await pressButton('both') // done review
|
|
70
|
+
await pressButton('right') // select signing
|
|
71
|
+
await pressButton('both') // done selection
|
|
72
|
+
}, 1000)
|
|
73
|
+
const signature = await app.signHash(path, hash)
|
|
74
|
+
console.log(signature)
|
|
75
|
+
await transport.close()
|
|
76
|
+
|
|
77
|
+
expect(transactionVerifySignature(hash.toString('hex'), account.publicKey, signature)).toBe(true)
|
|
78
|
+
}, 10000)
|
|
79
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"outDir": "dist",
|
|
4
|
+
"target": "es2020",
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noImplicitAny": false,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"module": "commonjs",
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"experimentalDecorators": true,
|
|
16
|
+
"noImplicitOverride": true
|
|
17
|
+
},
|
|
18
|
+
"exclude": ["node_modules"],
|
|
19
|
+
"include": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts", "alephium.config.ts"]
|
|
20
|
+
}
|