@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.
- package/dist/src/index.d.ts +2 -21
- package/dist/src/index.js +4 -98
- package/dist/src/ledger-app.d.ts +22 -0
- package/dist/src/ledger-app.js +115 -0
- package/dist/src/serde.d.ts +2 -0
- package/dist/src/serde.js +46 -1
- package/dist/src/serde.test.js +67 -0
- package/dist/src/types.d.ts +9 -0
- package/dist/src/types.js +6 -0
- package/dist/test/utils.d.ts +2 -1
- package/dist/test/utils.js +21 -11
- package/dist/test/wallet.test.js +92 -18
- package/package.json +1 -1
- package/src/index.ts +2 -103
- package/src/ledger-app.ts +112 -0
- package/src/serde.ts +50 -0
- package/src/types.ts +10 -0
- package/test/utils.ts +21 -10
- package/test/wallet.test.ts +95 -4
package/dist/src/index.d.ts
CHANGED
|
@@ -1,21 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
+
}
|
package/dist/src/serde.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/serde.test.js
CHANGED
|
@@ -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,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;
|
package/dist/test/utils.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ export declare enum OutputType {
|
|
|
3
3
|
Base = 0,
|
|
4
4
|
Multisig = 1,
|
|
5
5
|
Token = 2,
|
|
6
|
-
|
|
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>;
|
package/dist/test/utils.js
CHANGED
|
@@ -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["
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 !
|
|
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 =
|
|
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
|
|
183
|
-
const
|
|
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);
|
package/dist/test/wallet.test.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
83
|
-
for (let group = 0; 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
|
|
94
|
-
for (let group = 0; 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
|
|
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('
|
|
112
|
+
it('should transfer alph to one address', async () => {
|
|
112
113
|
const transport = await (0, utils_1.createTransport)();
|
|
113
|
-
const app = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
package/src/index.ts
CHANGED
|
@@ -1,103 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 !
|
|
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 =
|
|
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
|
|
191
|
-
const
|
|
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)
|
package/test/wallet.test.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
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('
|
|
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)
|