@alephium/ledger-app 0.4.0 → 0.5.1

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.
@@ -1,21 +1,2 @@
1
- /// <reference types="node" />
2
- import { Account, KeyType } 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
- SIGN_TX = 3
10
- }
11
- export declare const GROUP_NUM = 4;
12
- export declare const HASH_LEN = 32;
13
- export default class AlephiumApp {
14
- readonly transport: Transport;
15
- constructor(transport: Transport);
16
- close(): Promise<void>;
17
- getVersion(): Promise<string>;
18
- getAccount(startPath: string, targetGroup?: number, keyType?: KeyType, display?: boolean): Promise<readonly [Account, number]>;
19
- signHash(path: string, hash: Buffer): Promise<string>;
20
- signUnsignedTx(path: string, unsignedTx: Buffer): Promise<string>;
21
- }
1
+ export * from './types';
2
+ export * from './ledger-app';
package/dist/src/index.js CHANGED
@@ -10,103 +10,9 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
10
10
  if (k2 === undefined) k2 = k;
11
11
  o[k2] = m[k];
12
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;
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
24
15
  };
25
16
  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 ec = new elliptic_1.ec('secp256k1');
32
- exports.CLA = 0x80;
33
- var INS;
34
- (function (INS) {
35
- INS[INS["GET_VERSION"] = 0] = "GET_VERSION";
36
- INS[INS["GET_PUBLIC_KEY"] = 1] = "GET_PUBLIC_KEY";
37
- INS[INS["SIGN_HASH"] = 2] = "SIGN_HASH";
38
- INS[INS["SIGN_TX"] = 3] = "SIGN_TX";
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 close() {
47
- await this.transport.close();
48
- }
49
- async getVersion() {
50
- const response = await this.transport.send(exports.CLA, INS.GET_VERSION, 0x00, 0x00);
51
- console.log(`response ${response.length} - ${response.toString('hex')}`);
52
- return `${response[0]}.${response[1]}.${response[2]}`;
53
- }
54
- async getAccount(startPath, targetGroup, keyType, display = false) {
55
- if ((targetGroup ?? 0) >= exports.GROUP_NUM) {
56
- throw Error(`Invalid targetGroup: ${targetGroup}`);
57
- }
58
- if (keyType === 'bip340-schnorr') {
59
- throw Error('BIP340-Schnorr is not supported yet');
60
- }
61
- const p1 = targetGroup === undefined ? 0x00 : exports.GROUP_NUM;
62
- const p2 = targetGroup === undefined ? 0x00 : targetGroup;
63
- const payload = Buffer.concat([serde.serializePath(startPath), Buffer.from([display ? 1 : 0])]);
64
- const response = await this.transport.send(exports.CLA, INS.GET_PUBLIC_KEY, p1, p2, payload);
65
- const publicKey = ec.keyFromPublic(response.slice(0, 65)).getPublic(true, 'hex');
66
- const address = (0, web3_1.addressFromPublicKey)(publicKey);
67
- const group = (0, web3_1.groupOfAddress)(address);
68
- const hdIndex = response.slice(65, 69).readUInt32BE(0);
69
- return [{ publicKey: publicKey, address: address, group: group, keyType: keyType ?? 'default' }, hdIndex];
70
- }
71
- async signHash(path, hash) {
72
- if (hash.length !== exports.HASH_LEN) {
73
- throw new Error('Invalid hash length');
74
- }
75
- const data = Buffer.concat([serde.serializePath(path), hash]);
76
- console.log(`data ${data.length}`);
77
- const response = await this.transport.send(exports.CLA, INS.SIGN_HASH, 0x00, 0x00, data, [hw_transport_1.StatusCodes.OK]);
78
- console.log(`response ${response.length} - ${response.toString('hex')}`);
79
- return decodeSignature(response);
80
- }
81
- async signUnsignedTx(path, unsignedTx) {
82
- console.log(`unsigned tx size: ${unsignedTx.length}`);
83
- const encodedPath = serde.serializePath(path);
84
- const firstFrameTxLength = 256 - 25;
85
- const txData = unsignedTx.slice(0, unsignedTx.length > firstFrameTxLength ? firstFrameTxLength : unsignedTx.length);
86
- const data = Buffer.concat([encodedPath, txData]);
87
- let response = await this.transport.send(exports.CLA, INS.SIGN_TX, 0x00, 0x00, data, [hw_transport_1.StatusCodes.OK]);
88
- if (unsignedTx.length <= firstFrameTxLength) {
89
- return decodeSignature(response);
90
- }
91
- const frameLength = 256 - 5;
92
- let fromIndex = firstFrameTxLength;
93
- while (fromIndex < unsignedTx.length) {
94
- const remain = unsignedTx.length - fromIndex;
95
- const toIndex = remain > frameLength ? (fromIndex + frameLength) : unsignedTx.length;
96
- const data = unsignedTx.slice(fromIndex, toIndex);
97
- response = await this.transport.send(exports.CLA, INS.SIGN_TX, 0x01, 0x00, data, [hw_transport_1.StatusCodes.OK]);
98
- fromIndex = toIndex;
99
- }
100
- return decodeSignature(response);
101
- }
102
- }
103
- exports.default = AlephiumApp;
104
- function decodeSignature(response) {
105
- // Decode signature: https://bitcoin.stackexchange.com/a/12556
106
- const rLen = response.slice(3, 4)[0];
107
- const r = response.slice(4, 4 + rLen);
108
- const sLen = response.slice(5 + rLen, 6 + rLen)[0];
109
- const s = response.slice(6 + rLen, 6 + rLen + sLen);
110
- console.log(`${rLen} - ${r.toString('hex')}\n${sLen} - ${s.toString('hex')}`);
111
- return (0, web3_1.encodeHexSignature)(r.toString('hex'), s.toString('hex'));
112
- }
17
+ __exportStar(require("./types"), exports);
18
+ __exportStar(require("./ledger-app"), exports);
@@ -0,0 +1,22 @@
1
+ /// <reference types="node" />
2
+ import { Account, KeyType } from '@alephium/web3';
3
+ import Transport from '@ledgerhq/hw-transport';
4
+ import { TokenMetadata } from './types';
5
+ export declare const CLA = 128;
6
+ export declare enum INS {
7
+ GET_VERSION = 0,
8
+ GET_PUBLIC_KEY = 1,
9
+ SIGN_HASH = 2,
10
+ SIGN_TX = 3
11
+ }
12
+ export declare const GROUP_NUM = 4;
13
+ export declare const HASH_LEN = 32;
14
+ export default class AlephiumApp {
15
+ readonly transport: Transport;
16
+ constructor(transport: Transport);
17
+ close(): Promise<void>;
18
+ getVersion(): Promise<string>;
19
+ getAccount(startPath: string, targetGroup?: number, keyType?: KeyType, display?: boolean): Promise<readonly [Account, number]>;
20
+ signHash(path: string, hash: Buffer): Promise<string>;
21
+ signUnsignedTx(path: string, unsignedTx: Buffer, tokenMetadata?: TokenMetadata[]): Promise<string>;
22
+ }
@@ -0,0 +1,115 @@
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 ec = new elliptic_1.ec('secp256k1');
32
+ exports.CLA = 0x80;
33
+ var INS;
34
+ (function (INS) {
35
+ INS[INS["GET_VERSION"] = 0] = "GET_VERSION";
36
+ INS[INS["GET_PUBLIC_KEY"] = 1] = "GET_PUBLIC_KEY";
37
+ INS[INS["SIGN_HASH"] = 2] = "SIGN_HASH";
38
+ INS[INS["SIGN_TX"] = 3] = "SIGN_TX";
39
+ })(INS = exports.INS || (exports.INS = {}));
40
+ exports.GROUP_NUM = 4;
41
+ exports.HASH_LEN = 32;
42
+ // The maximum payload size is 255: https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/hw-transport/src/Transport.ts#L261
43
+ const MAX_PAYLOAD_SIZE = 255;
44
+ class AlephiumApp {
45
+ constructor(transport) {
46
+ this.transport = transport;
47
+ }
48
+ async close() {
49
+ await this.transport.close();
50
+ }
51
+ async getVersion() {
52
+ const response = await this.transport.send(exports.CLA, INS.GET_VERSION, 0x00, 0x00);
53
+ console.log(`response ${response.length} - ${response.toString('hex')}`);
54
+ return `${response[0]}.${response[1]}.${response[2]}`;
55
+ }
56
+ async getAccount(startPath, targetGroup, keyType, display = false) {
57
+ if ((targetGroup ?? 0) >= exports.GROUP_NUM) {
58
+ throw Error(`Invalid targetGroup: ${targetGroup}`);
59
+ }
60
+ if (keyType === 'bip340-schnorr') {
61
+ throw Error('BIP340-Schnorr is not supported yet');
62
+ }
63
+ const p1 = targetGroup === undefined ? 0x00 : exports.GROUP_NUM;
64
+ const p2 = targetGroup === undefined ? 0x00 : targetGroup;
65
+ const payload = Buffer.concat([serde.serializePath(startPath), Buffer.from([display ? 1 : 0])]);
66
+ const response = await this.transport.send(exports.CLA, INS.GET_PUBLIC_KEY, p1, p2, payload);
67
+ const publicKey = ec.keyFromPublic(response.slice(0, 65)).getPublic(true, 'hex');
68
+ const address = (0, web3_1.addressFromPublicKey)(publicKey);
69
+ const group = (0, web3_1.groupOfAddress)(address);
70
+ const hdIndex = response.slice(65, 69).readUInt32BE(0);
71
+ return [{ publicKey: publicKey, address: address, group: group, keyType: keyType ?? 'default' }, hdIndex];
72
+ }
73
+ async signHash(path, hash) {
74
+ if (hash.length !== exports.HASH_LEN) {
75
+ throw new Error('Invalid hash length');
76
+ }
77
+ const data = Buffer.concat([serde.serializePath(path), hash]);
78
+ console.log(`data ${data.length}`);
79
+ const response = await this.transport.send(exports.CLA, INS.SIGN_HASH, 0x00, 0x00, data, [hw_transport_1.StatusCodes.OK]);
80
+ console.log(`response ${response.length} - ${response.toString('hex')}`);
81
+ return decodeSignature(response);
82
+ }
83
+ async signUnsignedTx(path, unsignedTx, tokenMetadata = []) {
84
+ console.log(`unsigned tx size: ${unsignedTx.length}`);
85
+ const encodedPath = serde.serializePath(path);
86
+ const encodedTokenMetadata = serde.serializeTokenMetadata(tokenMetadata);
87
+ const firstFrameTxLength = MAX_PAYLOAD_SIZE - 20 - encodedTokenMetadata.length;
88
+ const txData = unsignedTx.slice(0, unsignedTx.length > firstFrameTxLength ? firstFrameTxLength : unsignedTx.length);
89
+ const data = Buffer.concat([encodedPath, encodedTokenMetadata, txData]);
90
+ let response = await this.transport.send(exports.CLA, INS.SIGN_TX, 0x00, 0x00, data, [hw_transport_1.StatusCodes.OK]);
91
+ if (unsignedTx.length <= firstFrameTxLength) {
92
+ return decodeSignature(response);
93
+ }
94
+ const frameLength = MAX_PAYLOAD_SIZE;
95
+ let fromIndex = firstFrameTxLength;
96
+ while (fromIndex < unsignedTx.length) {
97
+ const remain = unsignedTx.length - fromIndex;
98
+ const toIndex = remain > frameLength ? (fromIndex + frameLength) : unsignedTx.length;
99
+ const data = unsignedTx.slice(fromIndex, toIndex);
100
+ response = await this.transport.send(exports.CLA, INS.SIGN_TX, 0x01, 0x00, data, [hw_transport_1.StatusCodes.OK]);
101
+ fromIndex = toIndex;
102
+ }
103
+ return decodeSignature(response);
104
+ }
105
+ }
106
+ exports.default = AlephiumApp;
107
+ function decodeSignature(response) {
108
+ // Decode signature: https://bitcoin.stackexchange.com/a/12556
109
+ const rLen = response.slice(3, 4)[0];
110
+ const r = response.slice(4, 4 + rLen);
111
+ const sLen = response.slice(5 + rLen, 6 + rLen)[0];
112
+ const s = response.slice(6 + rLen, 6 + rLen + sLen);
113
+ console.log(`${rLen} - ${r.toString('hex')}\n${sLen} - ${s.toString('hex')}`);
114
+ return (0, web3_1.encodeHexSignature)(r.toString('hex'), s.toString('hex'));
115
+ }
@@ -1,5 +1,7 @@
1
1
  /// <reference types="node" />
2
+ import { TokenMetadata } from "./types";
2
3
  export declare const TRUE = 16;
3
4
  export declare const FALSE = 0;
4
5
  export declare function splitPath(path: string): number[];
5
6
  export declare function serializePath(path: string): Buffer;
7
+ export declare function serializeTokenMetadata(tokens: TokenMetadata[]): Buffer;
package/dist/src/serde.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.serializePath = exports.splitPath = exports.FALSE = exports.TRUE = void 0;
3
+ exports.serializeTokenMetadata = exports.serializePath = exports.splitPath = exports.FALSE = exports.TRUE = void 0;
4
+ const web3_1 = require("@alephium/web3");
5
+ const types_1 = require("./types");
4
6
  exports.TRUE = 0x10;
5
7
  exports.FALSE = 0x00;
6
8
  function splitPath(path) {
@@ -30,3 +32,46 @@ function serializePath(path) {
30
32
  return buffer;
31
33
  }
32
34
  exports.serializePath = serializePath;
35
+ function symbolToBytes(symbol) {
36
+ const buffer = Buffer.alloc(types_1.MAX_TOKEN_SYMBOL_LENGTH, 0);
37
+ for (let i = 0; i < symbol.length; i++) {
38
+ buffer[i] = symbol.charCodeAt(i) & 0xFF;
39
+ }
40
+ return buffer;
41
+ }
42
+ function check(tokens) {
43
+ const hasDuplicate = tokens.some((token, index) => index !== tokens.findIndex((t) => t.tokenId === token.tokenId));
44
+ if (hasDuplicate) {
45
+ throw new Error(`There are duplicate tokens`);
46
+ }
47
+ tokens.forEach((token) => {
48
+ if (!((0, web3_1.isHexString)(token.tokenId) && token.tokenId.length === 64)) {
49
+ throw new Error(`Invalid token id: ${token.tokenId}`);
50
+ }
51
+ if (token.symbol.length > types_1.MAX_TOKEN_SYMBOL_LENGTH) {
52
+ throw new Error(`The token symbol is too long: ${token.symbol}`);
53
+ }
54
+ });
55
+ if (tokens.length > types_1.MAX_TOKEN_SIZE) {
56
+ throw new Error(`The token size exceeds maximum size`);
57
+ }
58
+ }
59
+ function serializeTokenMetadata(tokens) {
60
+ check(tokens);
61
+ const array = tokens
62
+ .map((metadata) => {
63
+ const symbolBytes = symbolToBytes(metadata.symbol);
64
+ const buffer = Buffer.concat([
65
+ Buffer.from([metadata.version]),
66
+ Buffer.from(metadata.tokenId, 'hex'),
67
+ symbolBytes,
68
+ Buffer.from([metadata.decimals])
69
+ ]);
70
+ if (buffer.length !== types_1.TOKEN_METADATA_SIZE) {
71
+ throw new Error(`Invalid token metadata: ${metadata}`);
72
+ }
73
+ return buffer;
74
+ });
75
+ return Buffer.concat([Buffer.from([array.length]), ...array]);
76
+ }
77
+ exports.serializeTokenMetadata = serializeTokenMetadata;
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const web3_1 = require("@alephium/web3");
3
4
  const serde_1 = require("./serde");
5
+ const crypto_1 = require("crypto");
6
+ const types_1 = require("./types");
4
7
  describe('serde', () => {
5
8
  it('should split path', () => {
6
9
  expect((0, serde_1.splitPath)(`m/44'/1234'/0'/0/0`)).toStrictEqual([44 + 0x80000000, 1234 + 0x80000000, 0 + 0x80000000, 0, 0]);
@@ -10,4 +13,68 @@ describe('serde', () => {
10
13
  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
14
  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
15
  });
16
+ it('should encode token metadata', () => {
17
+ const token0 = {
18
+ version: 0,
19
+ tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
20
+ symbol: 'Token0',
21
+ decimals: 8
22
+ };
23
+ const token1 = {
24
+ version: 1,
25
+ tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
26
+ symbol: 'Token1',
27
+ decimals: 18
28
+ };
29
+ const token2 = {
30
+ version: 2,
31
+ tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
32
+ symbol: 'Token2',
33
+ decimals: 6
34
+ };
35
+ const token3 = {
36
+ version: 3,
37
+ tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
38
+ symbol: 'Token3',
39
+ decimals: 0
40
+ };
41
+ const token4 = {
42
+ version: 4,
43
+ tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
44
+ symbol: 'Token4',
45
+ decimals: 12
46
+ };
47
+ const encodeSymbol = (symbol) => {
48
+ return (0, web3_1.binToHex)(Buffer.from(symbol, 'ascii')).padEnd(types_1.MAX_TOKEN_SYMBOL_LENGTH * 2, '0');
49
+ };
50
+ expect((0, web3_1.binToHex)((0, serde_1.serializeTokenMetadata)([]))).toEqual('00');
51
+ expect((0, web3_1.binToHex)((0, serde_1.serializeTokenMetadata)([token0]))).toEqual('01' + '00' + token0.tokenId + encodeSymbol(token0.symbol) + '08');
52
+ expect((0, web3_1.binToHex)((0, serde_1.serializeTokenMetadata)([token1]))).toEqual('01' + '01' + token1.tokenId + encodeSymbol(token1.symbol) + '12');
53
+ expect((0, web3_1.binToHex)((0, serde_1.serializeTokenMetadata)([token0, token1]))).toEqual('02' + '00' + token0.tokenId + encodeSymbol(token0.symbol) + '08' +
54
+ '01' + token1.tokenId + encodeSymbol(token1.symbol) + '12');
55
+ expect((0, web3_1.binToHex)((0, serde_1.serializeTokenMetadata)([token0, token1, token2, token3, token4]))).toEqual('05' + '00' + token0.tokenId + encodeSymbol(token0.symbol) + '08' +
56
+ '01' + token1.tokenId + encodeSymbol(token1.symbol) + '12' +
57
+ '02' + token2.tokenId + encodeSymbol(token2.symbol) + '06' +
58
+ '03' + token3.tokenId + encodeSymbol(token3.symbol) + '00' +
59
+ '04' + token4.tokenId + encodeSymbol(token4.symbol) + '0c');
60
+ expect(() => (0, serde_1.serializeTokenMetadata)([token0, token1, token0])).toThrow('There are duplicate tokens');
61
+ const token5 = {
62
+ version: 5,
63
+ tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
64
+ symbol: 'Token5',
65
+ decimals: 18
66
+ };
67
+ expect(() => (0, serde_1.serializeTokenMetadata)([token0, token1, token2, token3, token4, token5])).toThrow('The token size exceeds maximum size');
68
+ const invalidToken = {
69
+ ...token0,
70
+ tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(33))
71
+ };
72
+ expect(() => (0, serde_1.serializeTokenMetadata)([token0, invalidToken])).toThrow('Invalid token id');
73
+ const longSymbolToken = {
74
+ ...token0,
75
+ tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
76
+ symbol: 'LongSymbolToken'
77
+ };
78
+ expect(() => (0, serde_1.serializeTokenMetadata)([token0, longSymbolToken, token1])).toThrow('The token symbol is too long');
79
+ });
13
80
  });
@@ -0,0 +1,9 @@
1
+ export declare const MAX_TOKEN_SIZE = 5;
2
+ export declare const MAX_TOKEN_SYMBOL_LENGTH = 12;
3
+ export declare const TOKEN_METADATA_SIZE = 46;
4
+ export interface TokenMetadata {
5
+ version: number;
6
+ tokenId: string;
7
+ symbol: string;
8
+ decimals: number;
9
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TOKEN_METADATA_SIZE = exports.MAX_TOKEN_SYMBOL_LENGTH = exports.MAX_TOKEN_SIZE = void 0;
4
+ exports.MAX_TOKEN_SIZE = 5;
5
+ exports.MAX_TOKEN_SYMBOL_LENGTH = 12;
6
+ exports.TOKEN_METADATA_SIZE = 46;
@@ -3,7 +3,8 @@ export declare enum OutputType {
3
3
  Base = 0,
4
4
  Multisig = 1,
5
5
  Token = 2,
6
- MultisigAndToken = 3
6
+ BaseAndToken = 3,
7
+ MultisigAndToken = 4
7
8
  }
8
9
  export declare function staxFlexApproveOnce(): Promise<void>;
9
10
  export declare function approveTx(outputs: OutputType[], hasExternalInputs?: boolean): Promise<void>;
@@ -21,33 +21,41 @@ async function clickAndApprove(times) {
21
21
  }
22
22
  await pressButton('both');
23
23
  }
24
+ function getModel() {
25
+ const model = process.env.MODEL;
26
+ return model ? model : 'nanos';
27
+ }
24
28
  var OutputType;
25
29
  (function (OutputType) {
26
30
  OutputType[OutputType["Base"] = 0] = "Base";
27
31
  OutputType[OutputType["Multisig"] = 1] = "Multisig";
28
32
  OutputType[OutputType["Token"] = 2] = "Token";
29
- OutputType[OutputType["MultisigAndToken"] = 3] = "MultisigAndToken";
33
+ OutputType[OutputType["BaseAndToken"] = 3] = "BaseAndToken";
34
+ OutputType[OutputType["MultisigAndToken"] = 4] = "MultisigAndToken";
30
35
  })(OutputType = exports.OutputType || (exports.OutputType = {}));
31
36
  const NanosClickTable = new Map([
32
37
  [OutputType.Base, 5],
33
38
  [OutputType.Multisig, 10],
34
39
  [OutputType.Token, 11],
40
+ [OutputType.BaseAndToken, 12],
35
41
  [OutputType.MultisigAndToken, 16],
36
42
  ]);
37
43
  const NanospClickTable = new Map([
38
44
  [OutputType.Base, 3],
39
45
  [OutputType.Multisig, 5],
40
46
  [OutputType.Token, 6],
47
+ [OutputType.BaseAndToken, 6],
41
48
  [OutputType.MultisigAndToken, 8],
42
49
  ]);
43
50
  const StaxClickTable = new Map([
44
51
  [OutputType.Base, 2],
45
52
  [OutputType.Multisig, 3],
46
53
  [OutputType.Token, 3],
54
+ [OutputType.BaseAndToken, 3],
47
55
  [OutputType.MultisigAndToken, 4],
48
56
  ]);
49
57
  function getOutputClickSize(outputType) {
50
- const model = process.env.MODEL;
58
+ const model = getModel();
51
59
  switch (model) {
52
60
  case 'nanos': return NanosClickTable.get(outputType);
53
61
  case 'nanosp':
@@ -85,15 +93,16 @@ async function touchPosition(pos) {
85
93
  });
86
94
  }
87
95
  async function _touch(times) {
88
- let continuePos = process.env.MODEL === 'stax' ? STAX_CONTINUE_POSITION : FLEX_CONTINUE_POSITION;
96
+ const model = getModel();
97
+ const continuePos = model === 'stax' ? STAX_CONTINUE_POSITION : FLEX_CONTINUE_POSITION;
89
98
  for (let i = 0; i < times; i += 1) {
90
99
  await touchPosition(continuePos);
91
100
  }
92
- let approvePos = process.env.MODEL === 'stax' ? STAX_APPROVE_POSITION : FLEX_APPROVE_POSITION;
101
+ const approvePos = model === 'stax' ? STAX_APPROVE_POSITION : FLEX_APPROVE_POSITION;
93
102
  await touchPosition(approvePos);
94
103
  }
95
104
  async function staxFlexApproveOnce() {
96
- if (process.env.MODEL === 'stax') {
105
+ if (getModel() === 'stax') {
97
106
  await touchPosition(STAX_APPROVE_POSITION);
98
107
  }
99
108
  else {
@@ -138,7 +147,7 @@ async function approveHash() {
138
147
  if (isStaxOrFlex()) {
139
148
  return await _touch(3);
140
149
  }
141
- if (process.env.MODEL === 'nanos') {
150
+ if (getModel() === 'nanos') {
142
151
  await clickAndApprove(5);
143
152
  }
144
153
  else {
@@ -152,7 +161,7 @@ async function approveAddress() {
152
161
  if (isStaxOrFlex()) {
153
162
  return await _touch(2);
154
163
  }
155
- if (process.env.MODEL === 'nanos') {
164
+ if (getModel() === 'nanos') {
156
165
  await clickAndApprove(4);
157
166
  }
158
167
  else {
@@ -161,13 +170,13 @@ async function approveAddress() {
161
170
  }
162
171
  exports.approveAddress = approveAddress;
163
172
  function isStaxOrFlex() {
164
- return !process.env.MODEL.startsWith('nano');
173
+ return !getModel().startsWith('nano');
165
174
  }
166
175
  function skipBlindSigningWarning() {
167
176
  if (!needToAutoApprove())
168
177
  return;
169
178
  if (isStaxOrFlex()) {
170
- const rejectPos = process.env.MODEL === 'stax' ? STAX_REJECT_POSITION : FLEX_REJECT_POSITION;
179
+ const rejectPos = getModel() === 'stax' ? STAX_REJECT_POSITION : FLEX_REJECT_POSITION;
171
180
  touchPosition(rejectPos);
172
181
  }
173
182
  else {
@@ -179,8 +188,9 @@ async function enableBlindSigning() {
179
188
  if (!needToAutoApprove())
180
189
  return;
181
190
  if (isStaxOrFlex()) {
182
- const settingsPos = process.env.MODEL === 'stax' ? STAX_SETTINGS_POSITION : FLEX_SETTINGS_POSITION;
183
- const blindSettingPos = process.env.MODEL === 'stax' ? STAX_BLIND_SETTING_POSITION : FLEX_BLIND_SETTING_POSITION;
191
+ const model = getModel();
192
+ const settingsPos = model === 'stax' ? STAX_SETTINGS_POSITION : FLEX_SETTINGS_POSITION;
193
+ const blindSettingPos = model === 'stax' ? STAX_BLIND_SETTING_POSITION : FLEX_BLIND_SETTING_POSITION;
184
194
  await touchPosition(settingsPos);
185
195
  await touchPosition(blindSettingPos);
186
196
  await touchPosition(settingsPos);
@@ -26,12 +26,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
26
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
27
  };
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
- const src_1 = __importStar(require("../src"));
29
+ const ledger_app_1 = __importStar(require("../src/ledger-app"));
30
30
  const web3_1 = require("@alephium/web3");
31
31
  const web3_test_1 = require("@alephium/web3-test");
32
32
  const web3_wallet_1 = require("@alephium/web3-wallet");
33
33
  const blakejs_1 = __importDefault(require("blakejs"));
34
34
  const utils_1 = require("./utils");
35
+ const crypto_1 = require("crypto");
35
36
  describe('ledger wallet', () => {
36
37
  const nodeProvider = new web3_1.NodeProvider("http://127.0.0.1:22973");
37
38
  web3_1.web3.setCurrentNodeProvider(nodeProvider);
@@ -55,14 +56,14 @@ describe('ledger wallet', () => {
55
56
  }
56
57
  it('should get version', async () => {
57
58
  const transport = await (0, utils_1.createTransport)();
58
- const app = new src_1.default(transport);
59
+ const app = new ledger_app_1.default(transport);
59
60
  const version = await app.getVersion();
60
61
  expect(version).toBe('0.4.0');
61
62
  await app.close();
62
63
  });
63
64
  it('should get public key', async () => {
64
65
  const transport = await (0, utils_1.createTransport)();
65
- const app = new src_1.default(transport);
66
+ const app = new ledger_app_1.default(transport);
66
67
  const [account, hdIndex] = await app.getAccount(path);
67
68
  expect(hdIndex).toBe(pathIndex);
68
69
  console.log(account);
@@ -70,7 +71,7 @@ describe('ledger wallet', () => {
70
71
  });
71
72
  it('should get public key and confirm address', async () => {
72
73
  const transport = await (0, utils_1.createTransport)();
73
- const app = new src_1.default(transport);
74
+ const app = new ledger_app_1.default(transport);
74
75
  (0, utils_1.approveAddress)();
75
76
  const [account, hdIndex] = await app.getAccount(path, undefined, undefined, true);
76
77
  expect(hdIndex).toBe(pathIndex);
@@ -79,8 +80,8 @@ describe('ledger wallet', () => {
79
80
  }, 30000);
80
81
  it('should get public key for group', async () => {
81
82
  const transport = await (0, utils_1.createTransport)();
82
- const app = new src_1.default(transport);
83
- for (let group = 0; group < src_1.GROUP_NUM; group++) {
83
+ const app = new ledger_app_1.default(transport);
84
+ for (let group = 0; group < ledger_app_1.GROUP_NUM; group++) {
84
85
  const [account, hdIndex] = await app.getAccount(path, group);
85
86
  expect(hdIndex >= pathIndex).toBe(true);
86
87
  expect((0, web3_1.groupOfAddress)(account.address)).toBe(group);
@@ -90,15 +91,15 @@ describe('ledger wallet', () => {
90
91
  });
91
92
  it('should get public key for group for Schnorr signature', async () => {
92
93
  const transport = await (0, utils_1.createTransport)();
93
- const app = new src_1.default(transport);
94
- for (let group = 0; group < src_1.GROUP_NUM; group++) {
94
+ const app = new ledger_app_1.default(transport);
95
+ for (let group = 0; group < ledger_app_1.GROUP_NUM; group++) {
95
96
  await expect(app.getAccount(path, group, 'bip340-schnorr')).rejects.toThrow('BIP340-Schnorr is not supported yet');
96
97
  }
97
98
  await app.close();
98
99
  });
99
100
  it('should sign hash', async () => {
100
101
  const transport = await (0, utils_1.createTransport)();
101
- const app = new src_1.default(transport);
102
+ const app = new ledger_app_1.default(transport);
102
103
  const [account] = await app.getAccount(path);
103
104
  console.log(account);
104
105
  const hash = Buffer.from(blakejs_1.default.blake2b(Buffer.from([0, 1, 2, 3, 4]), undefined, 32));
@@ -108,9 +109,9 @@ describe('ledger wallet', () => {
108
109
  await app.close();
109
110
  expect((0, web3_1.transactionVerifySignature)(hash.toString('hex'), account.publicKey, signature)).toBe(true);
110
111
  }, 10000);
111
- it('shoudl transfer alph to one address', async () => {
112
+ it('should transfer alph to one address', async () => {
112
113
  const transport = await (0, utils_1.createTransport)();
113
- const app = new src_1.default(transport);
114
+ const app = new ledger_app_1.default(transport);
114
115
  const [testAccount] = await app.getAccount(path);
115
116
  await transferToAddress(testAccount.address);
116
117
  const buildTxResult = await nodeProvider.transactions.postTransactionsBuild({
@@ -136,7 +137,7 @@ describe('ledger wallet', () => {
136
137
  }, 120000);
137
138
  it('should transfer alph to multiple addresses', async () => {
138
139
  const transport = await (0, utils_1.createTransport)();
139
- const app = new src_1.default(transport);
140
+ const app = new ledger_app_1.default(transport);
140
141
  const [testAccount] = await app.getAccount(path);
141
142
  await transferToAddress(testAccount.address);
142
143
  const buildTxResult = await nodeProvider.transactions.postTransactionsBuild({
@@ -166,7 +167,7 @@ describe('ledger wallet', () => {
166
167
  }, 120000);
167
168
  it('should transfer alph to multisig address', async () => {
168
169
  const transport = await (0, utils_1.createTransport)();
169
- const app = new src_1.default(transport);
170
+ const app = new ledger_app_1.default(transport);
170
171
  const [testAccount] = await app.getAccount(path);
171
172
  await transferToAddress(testAccount.address);
172
173
  const multiSigAddress = 'X3KYVteDjsKuUP1F68Nv9iEUecnnkMuwjbC985AnA6MvciDFJ5bAUEso2Sd7sGrwZ5rfNLj7Rp4n9XjcyzDiZsrPxfhNkPYcDm3ce8pQ9QasNFByEufMi3QJ3cS9Vk6cTpqNcq';
@@ -193,7 +194,7 @@ describe('ledger wallet', () => {
193
194
  }, 120000);
194
195
  it('should transfer token to multisig address', async () => {
195
196
  const transport = await (0, utils_1.createTransport)();
196
- const app = new src_1.default(transport);
197
+ const app = new ledger_app_1.default(transport);
197
198
  const [testAccount] = await app.getAccount(path);
198
199
  await transferToAddress(testAccount.address);
199
200
  const tokenInfo = await (0, web3_test_1.mintToken)(testAccount.address, 2222222222222222222222222n);
@@ -230,9 +231,82 @@ describe('ledger wallet', () => {
230
231
  expect(token.amount).toEqual('1111111111111111111111111');
231
232
  await app.close();
232
233
  }, 120000);
234
+ async function genTokensAndDestinations(fromAddress, toAddress, mintAmount, transferAmount) {
235
+ const tokens = [];
236
+ const tokenSymbol = 'TestTokenABC';
237
+ const destinations = [];
238
+ for (let i = 0; i < 5; i += 1) {
239
+ const tokenInfo = await (0, web3_test_1.mintToken)(fromAddress, mintAmount);
240
+ const tokenMetadata = {
241
+ version: 0,
242
+ tokenId: tokenInfo.contractId,
243
+ symbol: tokenSymbol.slice(0, tokenSymbol.length - i),
244
+ decimals: 18 - i
245
+ };
246
+ tokens.push(tokenMetadata);
247
+ destinations.push({
248
+ address: toAddress,
249
+ attoAlphAmount: web3_1.DUST_AMOUNT.toString(),
250
+ tokens: [
251
+ {
252
+ id: tokenMetadata.tokenId,
253
+ amount: transferAmount.toString()
254
+ }
255
+ ]
256
+ });
257
+ }
258
+ return { tokens, destinations };
259
+ }
260
+ it('should transfer token with metadata', async () => {
261
+ const transport = await (0, utils_1.createTransport)();
262
+ const app = new ledger_app_1.default(transport);
263
+ const [testAccount] = await app.getAccount(path);
264
+ await transferToAddress(testAccount.address);
265
+ const toAddress = '1BmVCLrjttchZMW7i6df7mTdCKzHpy38bgDbVL1GqV6P7';
266
+ const transferAmount = 1234567890123456789012345n;
267
+ const mintAmount = 2222222222222222222222222n;
268
+ const { tokens, destinations } = await genTokensAndDestinations(testAccount.address, toAddress, mintAmount, transferAmount);
269
+ const randomOrderTokens = tokens.sort((a, b) => b.tokenId.localeCompare(a.tokenId));
270
+ const buildTxResult = await nodeProvider.transactions.postTransactionsBuild({
271
+ fromPublicKey: testAccount.publicKey,
272
+ destinations: destinations
273
+ });
274
+ (0, utils_1.approveTx)(Array(5).fill(utils_1.OutputType.BaseAndToken));
275
+ const signature = await app.signUnsignedTx(path, Buffer.from(buildTxResult.unsignedTx, 'hex'), randomOrderTokens);
276
+ expect((0, web3_1.transactionVerifySignature)(buildTxResult.txId, testAccount.publicKey, signature)).toBe(true);
277
+ const submitResult = await nodeProvider.transactions.postTransactionsSubmit({
278
+ unsignedTx: buildTxResult.unsignedTx,
279
+ signature: signature
280
+ });
281
+ await (0, web3_1.waitForTxConfirmation)(submitResult.txId, 1, 1000);
282
+ const balances = await nodeProvider.addresses.getAddressesAddressBalance(toAddress);
283
+ tokens.forEach((metadata) => {
284
+ const tokenBalance = balances.tokenBalances.find((t) => t.id === metadata.tokenId);
285
+ expect(BigInt(tokenBalance.amount)).toEqual(transferAmount);
286
+ });
287
+ await app.close();
288
+ }, 120000);
289
+ it('should reject tx if the metadata version is invalid', async () => {
290
+ const transport = await (0, utils_1.createTransport)();
291
+ const app = new ledger_app_1.default(transport);
292
+ const [testAccount] = await app.getAccount(path);
293
+ await transferToAddress(testAccount.address);
294
+ const toAddress = '1BmVCLrjttchZMW7i6df7mTdCKzHpy38bgDbVL1GqV6P7';
295
+ const transferAmount = 1234567890123456789012345n;
296
+ const mintAmount = 2222222222222222222222222n;
297
+ const { tokens, destinations } = await genTokensAndDestinations(testAccount.address, toAddress, mintAmount, transferAmount);
298
+ const invalidTokenIndex = (0, crypto_1.randomInt)(5);
299
+ tokens[invalidTokenIndex] = { ...tokens[invalidTokenIndex], version: 1 };
300
+ const buildTxResult = await nodeProvider.transactions.postTransactionsBuild({
301
+ fromPublicKey: testAccount.publicKey,
302
+ destinations: destinations
303
+ });
304
+ await expect(app.signUnsignedTx(path, Buffer.from(buildTxResult.unsignedTx, 'hex'), tokens)).rejects.toThrow();
305
+ await app.close();
306
+ }, 120000);
233
307
  it('should transfer from multiple inputs', async () => {
234
308
  const transport = await (0, utils_1.createTransport)();
235
- const app = new src_1.default(transport);
309
+ const app = new ledger_app_1.default(transport);
236
310
  const [testAccount] = await app.getAccount(path);
237
311
  for (let i = 0; i < 20; i += 1) {
238
312
  await transferToAddress(testAccount.address, web3_1.ONE_ALPH);
@@ -270,7 +344,7 @@ describe('ledger wallet', () => {
270
344
  }
271
345
  it('should test external inputs', async () => {
272
346
  const transport = await (0, utils_1.createTransport)();
273
- const app = new src_1.default(transport);
347
+ const app = new ledger_app_1.default(transport);
274
348
  const [testAccount] = await app.getAccount(path);
275
349
  const { account: newAccount, unlockScript: unlockScript0 } = getAccount(testAccount.group);
276
350
  for (let i = 0; i < 2; i += 1) {
@@ -327,7 +401,7 @@ describe('ledger wallet', () => {
327
401
  }, 120000);
328
402
  it('should test self transfer tx', async () => {
329
403
  const transport = await (0, utils_1.createTransport)();
330
- const app = new src_1.default(transport);
404
+ const app = new ledger_app_1.default(transport);
331
405
  const [testAccount] = await app.getAccount(path);
332
406
  await transferToAddress(testAccount.address);
333
407
  const buildTxResult = await nodeProvider.transactions.postTransactionsBuild({
@@ -353,7 +427,7 @@ describe('ledger wallet', () => {
353
427
  }, 12000);
354
428
  it('should test script execution tx', async () => {
355
429
  const transport = await (0, utils_1.createTransport)();
356
- const app = new src_1.default(transport);
430
+ const app = new ledger_app_1.default(transport);
357
431
  const [testAccount] = await app.getAccount(path);
358
432
  await transferToAddress(testAccount.address);
359
433
  const buildTxResult = await nodeProvider.contracts.postContractsUnsignedTxDeployContract({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alephium/ledger-app",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "license": "GPL",
5
5
  "types": "dist/src/index.d.ts",
6
6
  "exports": {
package/src/index.ts CHANGED
@@ -1,103 +1,2 @@
1
- import { Account, KeyType, 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
-
6
- const ec = new EC('secp256k1')
7
-
8
- export const CLA = 0x80
9
- export enum INS {
10
- GET_VERSION = 0x00,
11
- GET_PUBLIC_KEY = 0x01,
12
- SIGN_HASH = 0x02,
13
- SIGN_TX = 0x03
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 close(): Promise<void> {
27
- await this.transport.close()
28
- }
29
-
30
- async getVersion(): Promise<string> {
31
- const response = await this.transport.send(CLA, INS.GET_VERSION, 0x00, 0x00)
32
- console.log(`response ${response.length} - ${response.toString('hex')}`)
33
- return `${response[0]}.${response[1]}.${response[2]}`
34
- }
35
-
36
- async getAccount(startPath: string, targetGroup?: number, keyType?: KeyType, display = false): Promise<readonly [Account, number]> {
37
- if ((targetGroup ?? 0) >= GROUP_NUM) {
38
- throw Error(`Invalid targetGroup: ${targetGroup}`)
39
- }
40
-
41
- if (keyType === 'bip340-schnorr') {
42
- throw Error('BIP340-Schnorr is not supported yet')
43
- }
44
-
45
- const p1 = targetGroup === undefined ? 0x00 : GROUP_NUM
46
- const p2 = targetGroup === undefined ? 0x00 : targetGroup
47
- const payload = Buffer.concat([serde.serializePath(startPath), Buffer.from([display ? 1 : 0])]);
48
- const response = await this.transport.send(CLA, INS.GET_PUBLIC_KEY, p1, p2, payload)
49
- const publicKey = ec.keyFromPublic(response.slice(0, 65)).getPublic(true, 'hex')
50
- const address = addressFromPublicKey(publicKey)
51
- const group = groupOfAddress(address)
52
- const hdIndex = response.slice(65, 69).readUInt32BE(0)
53
-
54
- return [{ publicKey: publicKey, address: address, group: group, keyType: keyType ?? 'default' }, hdIndex] as const
55
- }
56
-
57
- async signHash(path: string, hash: Buffer): Promise<string> {
58
- if (hash.length !== HASH_LEN) {
59
- throw new Error('Invalid hash length')
60
- }
61
-
62
- const data = Buffer.concat([serde.serializePath(path), hash])
63
- console.log(`data ${data.length}`)
64
- const response = await this.transport.send(CLA, INS.SIGN_HASH, 0x00, 0x00, data, [StatusCodes.OK])
65
- console.log(`response ${response.length} - ${response.toString('hex')}`)
66
-
67
- return decodeSignature(response)
68
- }
69
-
70
- async signUnsignedTx(path: string, unsignedTx: Buffer): Promise<string> {
71
- console.log(`unsigned tx size: ${unsignedTx.length}`)
72
- const encodedPath = serde.serializePath(path)
73
- const firstFrameTxLength = 256 - 25;
74
- const txData = unsignedTx.slice(0, unsignedTx.length > firstFrameTxLength ? firstFrameTxLength : unsignedTx.length)
75
- const data = Buffer.concat([encodedPath, txData])
76
- let response = await this.transport.send(CLA, INS.SIGN_TX, 0x00, 0x00, data, [StatusCodes.OK])
77
- if (unsignedTx.length <= firstFrameTxLength) {
78
- return decodeSignature(response)
79
- }
80
-
81
- const frameLength = 256 - 5
82
- let fromIndex = firstFrameTxLength
83
- while (fromIndex < unsignedTx.length) {
84
- const remain = unsignedTx.length - fromIndex
85
- const toIndex = remain > frameLength ? (fromIndex + frameLength) : unsignedTx.length
86
- const data = unsignedTx.slice(fromIndex, toIndex)
87
- response = await this.transport.send(CLA, INS.SIGN_TX, 0x01, 0x00, data, [StatusCodes.OK])
88
- fromIndex = toIndex
89
- }
90
-
91
- return decodeSignature(response)
92
- }
93
- }
94
-
95
- function decodeSignature(response: Buffer): string {
96
- // Decode signature: https://bitcoin.stackexchange.com/a/12556
97
- const rLen = response.slice(3, 4)[0]
98
- const r = response.slice(4, 4 + rLen)
99
- const sLen = response.slice(5 + rLen, 6 + rLen)[0]
100
- const s = response.slice(6 + rLen, 6 + rLen + sLen)
101
- console.log(`${rLen} - ${r.toString('hex')}\n${sLen} - ${s.toString('hex')}`)
102
- return encodeHexSignature(r.toString('hex'), s.toString('hex'))
103
- }
1
+ export * from './types'
2
+ export * from './ledger-app'
@@ -0,0 +1,112 @@
1
+ import { Account, KeyType, 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 { TokenMetadata } from './types'
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
+ SIGN_TX = 0x03
15
+ }
16
+
17
+ export const GROUP_NUM = 4
18
+ export const HASH_LEN = 32
19
+
20
+ // The maximum payload size is 255: https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/hw-transport/src/Transport.ts#L261
21
+ const MAX_PAYLOAD_SIZE = 255
22
+
23
+ export default class AlephiumApp {
24
+ readonly transport: Transport
25
+
26
+ constructor(transport: Transport) {
27
+ this.transport = transport
28
+ }
29
+
30
+ async close(): Promise<void> {
31
+ await this.transport.close()
32
+ }
33
+
34
+ async getVersion(): Promise<string> {
35
+ const response = await this.transport.send(CLA, INS.GET_VERSION, 0x00, 0x00)
36
+ console.log(`response ${response.length} - ${response.toString('hex')}`)
37
+ return `${response[0]}.${response[1]}.${response[2]}`
38
+ }
39
+
40
+ async getAccount(startPath: string, targetGroup?: number, keyType?: KeyType, display = false): Promise<readonly [Account, number]> {
41
+ if ((targetGroup ?? 0) >= GROUP_NUM) {
42
+ throw Error(`Invalid targetGroup: ${targetGroup}`)
43
+ }
44
+
45
+ if (keyType === 'bip340-schnorr') {
46
+ throw Error('BIP340-Schnorr is not supported yet')
47
+ }
48
+
49
+ const p1 = targetGroup === undefined ? 0x00 : GROUP_NUM
50
+ const p2 = targetGroup === undefined ? 0x00 : targetGroup
51
+ const payload = Buffer.concat([serde.serializePath(startPath), Buffer.from([display ? 1 : 0])]);
52
+ const response = await this.transport.send(CLA, INS.GET_PUBLIC_KEY, p1, p2, payload)
53
+ const publicKey = ec.keyFromPublic(response.slice(0, 65)).getPublic(true, 'hex')
54
+ const address = addressFromPublicKey(publicKey)
55
+ const group = groupOfAddress(address)
56
+ const hdIndex = response.slice(65, 69).readUInt32BE(0)
57
+
58
+ return [{ publicKey: publicKey, address: address, group: group, keyType: keyType ?? 'default' }, hdIndex] as const
59
+ }
60
+
61
+ async signHash(path: string, hash: Buffer): Promise<string> {
62
+ if (hash.length !== HASH_LEN) {
63
+ throw new Error('Invalid hash length')
64
+ }
65
+
66
+ const data = Buffer.concat([serde.serializePath(path), hash])
67
+ console.log(`data ${data.length}`)
68
+ const response = await this.transport.send(CLA, INS.SIGN_HASH, 0x00, 0x00, data, [StatusCodes.OK])
69
+ console.log(`response ${response.length} - ${response.toString('hex')}`)
70
+
71
+ return decodeSignature(response)
72
+ }
73
+
74
+ async signUnsignedTx(
75
+ path: string,
76
+ unsignedTx: Buffer,
77
+ tokenMetadata: TokenMetadata[] = []
78
+ ): Promise<string> {
79
+ console.log(`unsigned tx size: ${unsignedTx.length}`)
80
+ const encodedPath = serde.serializePath(path)
81
+ const encodedTokenMetadata = serde.serializeTokenMetadata(tokenMetadata)
82
+ const firstFrameTxLength = MAX_PAYLOAD_SIZE - 20 - encodedTokenMetadata.length;
83
+ const txData = unsignedTx.slice(0, unsignedTx.length > firstFrameTxLength ? firstFrameTxLength : unsignedTx.length)
84
+ const data = Buffer.concat([encodedPath, encodedTokenMetadata, txData])
85
+ let response = await this.transport.send(CLA, INS.SIGN_TX, 0x00, 0x00, data, [StatusCodes.OK])
86
+ if (unsignedTx.length <= firstFrameTxLength) {
87
+ return decodeSignature(response)
88
+ }
89
+
90
+ const frameLength = MAX_PAYLOAD_SIZE
91
+ let fromIndex = firstFrameTxLength
92
+ while (fromIndex < unsignedTx.length) {
93
+ const remain = unsignedTx.length - fromIndex
94
+ const toIndex = remain > frameLength ? (fromIndex + frameLength) : unsignedTx.length
95
+ const data = unsignedTx.slice(fromIndex, toIndex)
96
+ response = await this.transport.send(CLA, INS.SIGN_TX, 0x01, 0x00, data, [StatusCodes.OK])
97
+ fromIndex = toIndex
98
+ }
99
+
100
+ return decodeSignature(response)
101
+ }
102
+ }
103
+
104
+ function decodeSignature(response: Buffer): string {
105
+ // Decode signature: https://bitcoin.stackexchange.com/a/12556
106
+ const rLen = response.slice(3, 4)[0]
107
+ const r = response.slice(4, 4 + rLen)
108
+ const sLen = response.slice(5 + rLen, 6 + rLen)[0]
109
+ const s = response.slice(6 + rLen, 6 + rLen + sLen)
110
+ console.log(`${rLen} - ${r.toString('hex')}\n${sLen} - ${s.toString('hex')}`)
111
+ return encodeHexSignature(r.toString('hex'), s.toString('hex'))
112
+ }
package/src/serde.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { isHexString } from "@alephium/web3"
2
+ import { MAX_TOKEN_SIZE, MAX_TOKEN_SYMBOL_LENGTH, TOKEN_METADATA_SIZE, TokenMetadata } from "./types"
3
+
1
4
  export const TRUE = 0x10
2
5
  export const FALSE = 0x00
3
6
 
@@ -28,3 +31,50 @@ export function serializePath(path: string): Buffer {
28
31
  nodes.forEach((element, index) => buffer.writeUInt32BE(element, 4 * index))
29
32
  return buffer
30
33
  }
34
+
35
+ function symbolToBytes(symbol: string): Buffer {
36
+ const buffer = Buffer.alloc(MAX_TOKEN_SYMBOL_LENGTH, 0)
37
+ for (let i = 0; i < symbol.length; i++) {
38
+ buffer[i] = symbol.charCodeAt(i) & 0xFF
39
+ }
40
+ return buffer
41
+ }
42
+
43
+ function check(tokens: TokenMetadata[]) {
44
+ const hasDuplicate = tokens.some((token, index) => index !== tokens.findIndex((t) => t.tokenId === token.tokenId))
45
+ if (hasDuplicate) {
46
+ throw new Error(`There are duplicate tokens`)
47
+ }
48
+
49
+ tokens.forEach((token) => {
50
+ if (!(isHexString(token.tokenId) && token.tokenId.length === 64)) {
51
+ throw new Error(`Invalid token id: ${token.tokenId}`)
52
+ }
53
+ if (token.symbol.length > MAX_TOKEN_SYMBOL_LENGTH) {
54
+ throw new Error(`The token symbol is too long: ${token.symbol}`)
55
+ }
56
+ })
57
+
58
+ if (tokens.length > MAX_TOKEN_SIZE) {
59
+ throw new Error(`The token size exceeds maximum size`)
60
+ }
61
+ }
62
+
63
+ export function serializeTokenMetadata(tokens: TokenMetadata[]): Buffer {
64
+ check(tokens)
65
+ const array = tokens
66
+ .map((metadata) => {
67
+ const symbolBytes = symbolToBytes(metadata.symbol)
68
+ const buffer = Buffer.concat([
69
+ Buffer.from([metadata.version]),
70
+ Buffer.from(metadata.tokenId, 'hex'),
71
+ symbolBytes,
72
+ Buffer.from([metadata.decimals])
73
+ ])
74
+ if (buffer.length !== TOKEN_METADATA_SIZE) {
75
+ throw new Error(`Invalid token metadata: ${metadata}`)
76
+ }
77
+ return buffer
78
+ })
79
+ return Buffer.concat([Buffer.from([array.length]), ...array])
80
+ }
package/src/types.ts ADDED
@@ -0,0 +1,10 @@
1
+ export const MAX_TOKEN_SIZE = 5
2
+ export const MAX_TOKEN_SYMBOL_LENGTH = 12
3
+ export const TOKEN_METADATA_SIZE = 46
4
+
5
+ export interface TokenMetadata {
6
+ version: number,
7
+ tokenId: string,
8
+ symbol: string,
9
+ decimals: number
10
+ }
package/test/utils.ts CHANGED
@@ -19,10 +19,16 @@ async function clickAndApprove(times: number) {
19
19
  await pressButton('both')
20
20
  }
21
21
 
22
+ function getModel(): string {
23
+ const model = process.env.MODEL
24
+ return model ? model as string : 'nanos'
25
+ }
26
+
22
27
  export enum OutputType {
23
28
  Base,
24
29
  Multisig,
25
30
  Token,
31
+ BaseAndToken,
26
32
  MultisigAndToken
27
33
  }
28
34
 
@@ -30,6 +36,7 @@ const NanosClickTable = new Map([
30
36
  [OutputType.Base, 5],
31
37
  [OutputType.Multisig, 10],
32
38
  [OutputType.Token, 11],
39
+ [OutputType.BaseAndToken, 12],
33
40
  [OutputType.MultisigAndToken, 16],
34
41
  ])
35
42
 
@@ -37,6 +44,7 @@ const NanospClickTable = new Map([
37
44
  [OutputType.Base, 3],
38
45
  [OutputType.Multisig, 5],
39
46
  [OutputType.Token, 6],
47
+ [OutputType.BaseAndToken, 6],
40
48
  [OutputType.MultisigAndToken, 8],
41
49
  ])
42
50
 
@@ -44,11 +52,12 @@ const StaxClickTable = new Map([
44
52
  [OutputType.Base, 2],
45
53
  [OutputType.Multisig, 3],
46
54
  [OutputType.Token, 3],
55
+ [OutputType.BaseAndToken, 3],
47
56
  [OutputType.MultisigAndToken, 4],
48
57
  ])
49
58
 
50
59
  function getOutputClickSize(outputType: OutputType) {
51
- const model = process.env.MODEL
60
+ const model = getModel()
52
61
  switch (model) {
53
62
  case 'nanos': return NanosClickTable.get(outputType)!
54
63
  case 'nanosp':
@@ -98,16 +107,17 @@ async function touchPosition(pos: Position) {
98
107
  }
99
108
 
100
109
  async function _touch(times: number) {
101
- let continuePos = process.env.MODEL === 'stax' ? STAX_CONTINUE_POSITION : FLEX_CONTINUE_POSITION
110
+ const model = getModel()
111
+ const continuePos = model === 'stax' ? STAX_CONTINUE_POSITION : FLEX_CONTINUE_POSITION
102
112
  for (let i = 0; i < times; i += 1) {
103
113
  await touchPosition(continuePos)
104
114
  }
105
- let approvePos = process.env.MODEL === 'stax' ? STAX_APPROVE_POSITION : FLEX_APPROVE_POSITION
115
+ const approvePos = model === 'stax' ? STAX_APPROVE_POSITION : FLEX_APPROVE_POSITION
106
116
  await touchPosition(approvePos)
107
117
  }
108
118
 
109
119
  export async function staxFlexApproveOnce() {
110
- if (process.env.MODEL === 'stax') {
120
+ if (getModel() === 'stax') {
111
121
  await touchPosition(STAX_APPROVE_POSITION)
112
122
  } else {
113
123
  await touchPosition(FLEX_APPROVE_POSITION)
@@ -151,7 +161,7 @@ export async function approveHash() {
151
161
  if (isStaxOrFlex()) {
152
162
  return await _touch(3)
153
163
  }
154
- if (process.env.MODEL === 'nanos') {
164
+ if (getModel() === 'nanos') {
155
165
  await clickAndApprove(5)
156
166
  } else {
157
167
  await clickAndApprove(3)
@@ -163,7 +173,7 @@ export async function approveAddress() {
163
173
  if (isStaxOrFlex()) {
164
174
  return await _touch(2)
165
175
  }
166
- if (process.env.MODEL === 'nanos') {
176
+ if (getModel() === 'nanos') {
167
177
  await clickAndApprove(4)
168
178
  } else {
169
179
  await clickAndApprove(2)
@@ -171,13 +181,13 @@ export async function approveAddress() {
171
181
  }
172
182
 
173
183
  function isStaxOrFlex(): boolean {
174
- return !process.env.MODEL!.startsWith('nano')
184
+ return !getModel().startsWith('nano')
175
185
  }
176
186
 
177
187
  export function skipBlindSigningWarning() {
178
188
  if (!needToAutoApprove()) return
179
189
  if (isStaxOrFlex()) {
180
- const rejectPos = process.env.MODEL === 'stax' ? STAX_REJECT_POSITION : FLEX_REJECT_POSITION
190
+ const rejectPos = getModel() === 'stax' ? STAX_REJECT_POSITION : FLEX_REJECT_POSITION
181
191
  touchPosition(rejectPos)
182
192
  } else {
183
193
  clickAndApprove(3)
@@ -187,8 +197,9 @@ export function skipBlindSigningWarning() {
187
197
  export async function enableBlindSigning() {
188
198
  if (!needToAutoApprove()) return
189
199
  if (isStaxOrFlex()) {
190
- const settingsPos = process.env.MODEL === 'stax' ? STAX_SETTINGS_POSITION : FLEX_SETTINGS_POSITION
191
- const blindSettingPos = process.env.MODEL === 'stax' ? STAX_BLIND_SETTING_POSITION : FLEX_BLIND_SETTING_POSITION
200
+ const model = getModel()
201
+ const settingsPos = model === 'stax' ? STAX_SETTINGS_POSITION : FLEX_SETTINGS_POSITION
202
+ const blindSettingPos = model === 'stax' ? STAX_BLIND_SETTING_POSITION : FLEX_BLIND_SETTING_POSITION
192
203
  await touchPosition(settingsPos)
193
204
  await touchPosition(blindSettingPos)
194
205
  await touchPosition(settingsPos)
@@ -1,10 +1,11 @@
1
- import SpeculosTransport from '@ledgerhq/hw-transport-node-speculos'
2
- import AlephiumApp, { GROUP_NUM } from '../src'
3
- import { ALPH_TOKEN_ID, Address, NodeProvider, ONE_ALPH, binToHex, codec, groupOfAddress, node, sleep, transactionVerifySignature, waitForTxConfirmation, web3 } from '@alephium/web3'
1
+ import AlephiumApp, { GROUP_NUM } from '../src/ledger-app'
2
+ import { ALPH_TOKEN_ID, Address, DUST_AMOUNT, NodeProvider, ONE_ALPH, binToHex, codec, groupOfAddress, node, sleep, transactionVerifySignature, waitForTxConfirmation, web3 } from '@alephium/web3'
4
3
  import { getSigner, mintToken, transfer } from '@alephium/web3-test'
5
4
  import { PrivateKeyWallet } from '@alephium/web3-wallet'
6
5
  import blake from 'blakejs'
7
6
  import { approveAddress, approveHash, approveTx, createTransport, enableBlindSigning, getRandomInt, needToAutoApprove, OutputType, skipBlindSigningWarning, staxFlexApproveOnce } from './utils'
7
+ import { TokenMetadata } from '../src/types'
8
+ import { randomInt } from 'crypto'
8
9
 
9
10
  describe('ledger wallet', () => {
10
11
  const nodeProvider = new NodeProvider("http://127.0.0.1:22973")
@@ -95,7 +96,7 @@ describe('ledger wallet', () => {
95
96
  expect(transactionVerifySignature(hash.toString('hex'), account.publicKey, signature)).toBe(true)
96
97
  }, 10000)
97
98
 
98
- it('shoudl transfer alph to one address', async () => {
99
+ it('should transfer alph to one address', async () => {
99
100
  const transport = await createTransport()
100
101
  const app = new AlephiumApp(transport)
101
102
  const [testAccount] = await app.getAccount(path)
@@ -239,6 +240,96 @@ describe('ledger wallet', () => {
239
240
  await app.close()
240
241
  }, 120000)
241
242
 
243
+ async function genTokensAndDestinations(
244
+ fromAddress: string,
245
+ toAddress: string,
246
+ mintAmount: bigint,
247
+ transferAmount: bigint
248
+ ) {
249
+ const tokens: TokenMetadata[] = []
250
+ const tokenSymbol = 'TestTokenABC'
251
+ const destinations: node.Destination[] = []
252
+ for (let i = 0; i < 5; i += 1) {
253
+ const tokenInfo = await mintToken(fromAddress, mintAmount);
254
+ const tokenMetadata: TokenMetadata = {
255
+ version: 0,
256
+ tokenId: tokenInfo.contractId,
257
+ symbol: tokenSymbol.slice(0, tokenSymbol.length - i),
258
+ decimals: 18 - i
259
+ }
260
+ tokens.push(tokenMetadata)
261
+ destinations.push({
262
+ address: toAddress,
263
+ attoAlphAmount: DUST_AMOUNT.toString(),
264
+ tokens: [
265
+ {
266
+ id: tokenMetadata.tokenId,
267
+ amount: transferAmount.toString()
268
+ }
269
+ ]
270
+ })
271
+ }
272
+ return { tokens, destinations }
273
+ }
274
+
275
+ it('should transfer token with metadata', async () => {
276
+ const transport = await createTransport()
277
+ const app = new AlephiumApp(transport)
278
+ const [testAccount] = await app.getAccount(path)
279
+ await transferToAddress(testAccount.address)
280
+
281
+ const toAddress = '1BmVCLrjttchZMW7i6df7mTdCKzHpy38bgDbVL1GqV6P7';
282
+ const transferAmount = 1234567890123456789012345n
283
+ const mintAmount = 2222222222222222222222222n
284
+ const { tokens, destinations } = await genTokensAndDestinations(testAccount.address, toAddress, mintAmount, transferAmount)
285
+
286
+ const randomOrderTokens = tokens.sort((a, b) => b.tokenId.localeCompare(a.tokenId))
287
+ const buildTxResult = await nodeProvider.transactions.postTransactionsBuild({
288
+ fromPublicKey: testAccount.publicKey,
289
+ destinations: destinations
290
+ })
291
+
292
+ approveTx(Array(5).fill(OutputType.BaseAndToken))
293
+ const signature = await app.signUnsignedTx(path, Buffer.from(buildTxResult.unsignedTx, 'hex'), randomOrderTokens)
294
+ expect(transactionVerifySignature(buildTxResult.txId, testAccount.publicKey, signature)).toBe(true)
295
+
296
+ const submitResult = await nodeProvider.transactions.postTransactionsSubmit({
297
+ unsignedTx: buildTxResult.unsignedTx,
298
+ signature: signature
299
+ })
300
+ await waitForTxConfirmation(submitResult.txId, 1, 1000)
301
+ const balances = await nodeProvider.addresses.getAddressesAddressBalance(toAddress)
302
+ tokens.forEach((metadata) => {
303
+ const tokenBalance = balances.tokenBalances!.find((t) => t.id === metadata.tokenId)!
304
+ expect(BigInt(tokenBalance.amount)).toEqual(transferAmount)
305
+ })
306
+
307
+ await app.close()
308
+ }, 120000)
309
+
310
+ it('should reject tx if the metadata version is invalid', async () => {
311
+ const transport = await createTransport()
312
+ const app = new AlephiumApp(transport)
313
+ const [testAccount] = await app.getAccount(path)
314
+ await transferToAddress(testAccount.address)
315
+
316
+ const toAddress = '1BmVCLrjttchZMW7i6df7mTdCKzHpy38bgDbVL1GqV6P7';
317
+ const transferAmount = 1234567890123456789012345n
318
+ const mintAmount = 2222222222222222222222222n
319
+ const { tokens, destinations } = await genTokensAndDestinations(testAccount.address, toAddress, mintAmount, transferAmount)
320
+
321
+ const invalidTokenIndex = randomInt(5)
322
+ tokens[invalidTokenIndex] = { ...tokens[invalidTokenIndex], version: 1 }
323
+ const buildTxResult = await nodeProvider.transactions.postTransactionsBuild({
324
+ fromPublicKey: testAccount.publicKey,
325
+ destinations: destinations
326
+ })
327
+
328
+ await expect(app.signUnsignedTx(path, Buffer.from(buildTxResult.unsignedTx, 'hex'), tokens)).rejects.toThrow()
329
+
330
+ await app.close()
331
+ }, 120000)
332
+
242
333
  it('should transfer from multiple inputs', async () => {
243
334
  const transport = await createTransport()
244
335
  const app = new AlephiumApp(transport)