@alephium/ledger-app 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +7 -4
- package/dist/src/serde.d.ts +10 -0
- package/dist/src/serde.js +48 -1
- package/dist/src/serde.test.js +66 -0
- package/dist/test/utils.d.ts +2 -1
- package/dist/test/utils.js +21 -11
- package/dist/test/wallet.test.js +75 -1
- package/package.json +1 -1
- package/src/index.ts +12 -4
- package/src/serde.ts +59 -0
- package/test/utils.ts +21 -10
- package/test/wallet.test.ts +94 -3
package/dist/src/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { Account, KeyType } from '@alephium/web3';
|
|
3
3
|
import Transport from '@ledgerhq/hw-transport';
|
|
4
|
+
import * as serde from './serde';
|
|
4
5
|
export declare const CLA = 128;
|
|
5
6
|
export declare enum INS {
|
|
6
7
|
GET_VERSION = 0,
|
|
@@ -17,5 +18,5 @@ export default class AlephiumApp {
|
|
|
17
18
|
getVersion(): Promise<string>;
|
|
18
19
|
getAccount(startPath: string, targetGroup?: number, keyType?: KeyType, display?: boolean): Promise<readonly [Account, number]>;
|
|
19
20
|
signHash(path: string, hash: Buffer): Promise<string>;
|
|
20
|
-
signUnsignedTx(path: string, unsignedTx: Buffer): Promise<string>;
|
|
21
|
+
signUnsignedTx(path: string, unsignedTx: Buffer, tokenMetadata?: serde.TokenMetadata[]): Promise<string>;
|
|
21
22
|
}
|
package/dist/src/index.js
CHANGED
|
@@ -39,6 +39,8 @@ var INS;
|
|
|
39
39
|
})(INS = exports.INS || (exports.INS = {}));
|
|
40
40
|
exports.GROUP_NUM = 4;
|
|
41
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;
|
|
42
44
|
class AlephiumApp {
|
|
43
45
|
constructor(transport) {
|
|
44
46
|
this.transport = transport;
|
|
@@ -78,17 +80,18 @@ class AlephiumApp {
|
|
|
78
80
|
console.log(`response ${response.length} - ${response.toString('hex')}`);
|
|
79
81
|
return decodeSignature(response);
|
|
80
82
|
}
|
|
81
|
-
async signUnsignedTx(path, unsignedTx) {
|
|
83
|
+
async signUnsignedTx(path, unsignedTx, tokenMetadata = []) {
|
|
82
84
|
console.log(`unsigned tx size: ${unsignedTx.length}`);
|
|
83
85
|
const encodedPath = serde.serializePath(path);
|
|
84
|
-
const
|
|
86
|
+
const encodedTokenMetadata = serde.serializeTokenMetadata(tokenMetadata);
|
|
87
|
+
const firstFrameTxLength = MAX_PAYLOAD_SIZE - 20 - encodedTokenMetadata.length;
|
|
85
88
|
const txData = unsignedTx.slice(0, unsignedTx.length > firstFrameTxLength ? firstFrameTxLength : unsignedTx.length);
|
|
86
|
-
const data = Buffer.concat([encodedPath, txData]);
|
|
89
|
+
const data = Buffer.concat([encodedPath, encodedTokenMetadata, txData]);
|
|
87
90
|
let response = await this.transport.send(exports.CLA, INS.SIGN_TX, 0x00, 0x00, data, [hw_transport_1.StatusCodes.OK]);
|
|
88
91
|
if (unsignedTx.length <= firstFrameTxLength) {
|
|
89
92
|
return decodeSignature(response);
|
|
90
93
|
}
|
|
91
|
-
const frameLength =
|
|
94
|
+
const frameLength = MAX_PAYLOAD_SIZE;
|
|
92
95
|
let fromIndex = firstFrameTxLength;
|
|
93
96
|
while (fromIndex < unsignedTx.length) {
|
|
94
97
|
const remain = unsignedTx.length - fromIndex;
|
package/dist/src/serde.d.ts
CHANGED
|
@@ -3,3 +3,13 @@ export declare const TRUE = 16;
|
|
|
3
3
|
export declare const FALSE = 0;
|
|
4
4
|
export declare function splitPath(path: string): number[];
|
|
5
5
|
export declare function serializePath(path: string): Buffer;
|
|
6
|
+
export declare const MAX_TOKEN_SIZE = 5;
|
|
7
|
+
export declare const MAX_TOKEN_SYMBOL_LENGTH = 12;
|
|
8
|
+
export declare const TOKEN_METADATA_SIZE = 46;
|
|
9
|
+
export interface TokenMetadata {
|
|
10
|
+
version: number;
|
|
11
|
+
tokenId: string;
|
|
12
|
+
symbol: string;
|
|
13
|
+
decimals: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function serializeTokenMetadata(tokens: TokenMetadata[]): Buffer;
|
package/dist/src/serde.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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.TOKEN_METADATA_SIZE = exports.MAX_TOKEN_SYMBOL_LENGTH = exports.MAX_TOKEN_SIZE = exports.serializePath = exports.splitPath = exports.FALSE = exports.TRUE = void 0;
|
|
4
|
+
const web3_1 = require("@alephium/web3");
|
|
4
5
|
exports.TRUE = 0x10;
|
|
5
6
|
exports.FALSE = 0x00;
|
|
6
7
|
function splitPath(path) {
|
|
@@ -30,3 +31,49 @@ function serializePath(path) {
|
|
|
30
31
|
return buffer;
|
|
31
32
|
}
|
|
32
33
|
exports.serializePath = serializePath;
|
|
34
|
+
exports.MAX_TOKEN_SIZE = 5;
|
|
35
|
+
exports.MAX_TOKEN_SYMBOL_LENGTH = 12;
|
|
36
|
+
exports.TOKEN_METADATA_SIZE = 46;
|
|
37
|
+
function symbolToBytes(symbol) {
|
|
38
|
+
const buffer = Buffer.alloc(exports.MAX_TOKEN_SYMBOL_LENGTH, 0);
|
|
39
|
+
for (let i = 0; i < symbol.length; i++) {
|
|
40
|
+
buffer[i] = symbol.charCodeAt(i) & 0xFF;
|
|
41
|
+
}
|
|
42
|
+
return buffer;
|
|
43
|
+
}
|
|
44
|
+
function check(tokens) {
|
|
45
|
+
const hasDuplicate = tokens.some((token, index) => index !== tokens.findIndex((t) => t.tokenId === token.tokenId));
|
|
46
|
+
if (hasDuplicate) {
|
|
47
|
+
throw new Error(`There are duplicate tokens`);
|
|
48
|
+
}
|
|
49
|
+
tokens.forEach((token) => {
|
|
50
|
+
if (!((0, web3_1.isHexString)(token.tokenId) && token.tokenId.length === 64)) {
|
|
51
|
+
throw new Error(`Invalid token id: ${token.tokenId}`);
|
|
52
|
+
}
|
|
53
|
+
if (token.symbol.length > exports.MAX_TOKEN_SYMBOL_LENGTH) {
|
|
54
|
+
throw new Error(`The token symbol is too long: ${token.symbol}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (tokens.length > exports.MAX_TOKEN_SIZE) {
|
|
58
|
+
throw new Error(`The token size exceeds maximum size`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function serializeTokenMetadata(tokens) {
|
|
62
|
+
check(tokens);
|
|
63
|
+
const array = tokens
|
|
64
|
+
.map((metadata) => {
|
|
65
|
+
const symbolBytes = symbolToBytes(metadata.symbol);
|
|
66
|
+
const buffer = Buffer.concat([
|
|
67
|
+
Buffer.from([metadata.version]),
|
|
68
|
+
Buffer.from(metadata.tokenId, 'hex'),
|
|
69
|
+
symbolBytes,
|
|
70
|
+
Buffer.from([metadata.decimals])
|
|
71
|
+
]);
|
|
72
|
+
if (buffer.length !== exports.TOKEN_METADATA_SIZE) {
|
|
73
|
+
throw new Error(`Invalid token metadata: ${metadata}`);
|
|
74
|
+
}
|
|
75
|
+
return buffer;
|
|
76
|
+
});
|
|
77
|
+
return Buffer.concat([Buffer.from([array.length]), ...array]);
|
|
78
|
+
}
|
|
79
|
+
exports.serializeTokenMetadata = serializeTokenMetadata;
|
package/dist/src/serde.test.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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");
|
|
4
6
|
describe('serde', () => {
|
|
5
7
|
it('should split path', () => {
|
|
6
8
|
expect((0, serde_1.splitPath)(`m/44'/1234'/0'/0/0`)).toStrictEqual([44 + 0x80000000, 1234 + 0x80000000, 0 + 0x80000000, 0, 0]);
|
|
@@ -10,4 +12,68 @@ describe('serde', () => {
|
|
|
10
12
|
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
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]));
|
|
12
14
|
});
|
|
15
|
+
it('should encode token metadata', () => {
|
|
16
|
+
const token0 = {
|
|
17
|
+
version: 0,
|
|
18
|
+
tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
|
|
19
|
+
symbol: 'Token0',
|
|
20
|
+
decimals: 8
|
|
21
|
+
};
|
|
22
|
+
const token1 = {
|
|
23
|
+
version: 1,
|
|
24
|
+
tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
|
|
25
|
+
symbol: 'Token1',
|
|
26
|
+
decimals: 18
|
|
27
|
+
};
|
|
28
|
+
const token2 = {
|
|
29
|
+
version: 2,
|
|
30
|
+
tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
|
|
31
|
+
symbol: 'Token2',
|
|
32
|
+
decimals: 6
|
|
33
|
+
};
|
|
34
|
+
const token3 = {
|
|
35
|
+
version: 3,
|
|
36
|
+
tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
|
|
37
|
+
symbol: 'Token3',
|
|
38
|
+
decimals: 0
|
|
39
|
+
};
|
|
40
|
+
const token4 = {
|
|
41
|
+
version: 4,
|
|
42
|
+
tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
|
|
43
|
+
symbol: 'Token4',
|
|
44
|
+
decimals: 12
|
|
45
|
+
};
|
|
46
|
+
const encodeSymbol = (symbol) => {
|
|
47
|
+
return (0, web3_1.binToHex)(Buffer.from(symbol, 'ascii')).padEnd(serde_1.MAX_TOKEN_SYMBOL_LENGTH * 2, '0');
|
|
48
|
+
};
|
|
49
|
+
expect((0, web3_1.binToHex)((0, serde_1.serializeTokenMetadata)([]))).toEqual('00');
|
|
50
|
+
expect((0, web3_1.binToHex)((0, serde_1.serializeTokenMetadata)([token0]))).toEqual('01' + '00' + token0.tokenId + encodeSymbol(token0.symbol) + '08');
|
|
51
|
+
expect((0, web3_1.binToHex)((0, serde_1.serializeTokenMetadata)([token1]))).toEqual('01' + '01' + token1.tokenId + encodeSymbol(token1.symbol) + '12');
|
|
52
|
+
expect((0, web3_1.binToHex)((0, serde_1.serializeTokenMetadata)([token0, token1]))).toEqual('02' + '00' + token0.tokenId + encodeSymbol(token0.symbol) + '08' +
|
|
53
|
+
'01' + token1.tokenId + encodeSymbol(token1.symbol) + '12');
|
|
54
|
+
expect((0, web3_1.binToHex)((0, serde_1.serializeTokenMetadata)([token0, token1, token2, token3, token4]))).toEqual('05' + '00' + token0.tokenId + encodeSymbol(token0.symbol) + '08' +
|
|
55
|
+
'01' + token1.tokenId + encodeSymbol(token1.symbol) + '12' +
|
|
56
|
+
'02' + token2.tokenId + encodeSymbol(token2.symbol) + '06' +
|
|
57
|
+
'03' + token3.tokenId + encodeSymbol(token3.symbol) + '00' +
|
|
58
|
+
'04' + token4.tokenId + encodeSymbol(token4.symbol) + '0c');
|
|
59
|
+
expect(() => (0, serde_1.serializeTokenMetadata)([token0, token1, token0])).toThrow('There are duplicate tokens');
|
|
60
|
+
const token5 = {
|
|
61
|
+
version: 5,
|
|
62
|
+
tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
|
|
63
|
+
symbol: 'Token5',
|
|
64
|
+
decimals: 18
|
|
65
|
+
};
|
|
66
|
+
expect(() => (0, serde_1.serializeTokenMetadata)([token0, token1, token2, token3, token4, token5])).toThrow('The token size exceeds maximum size');
|
|
67
|
+
const invalidToken = {
|
|
68
|
+
...token0,
|
|
69
|
+
tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(33))
|
|
70
|
+
};
|
|
71
|
+
expect(() => (0, serde_1.serializeTokenMetadata)([token0, invalidToken])).toThrow('Invalid token id');
|
|
72
|
+
const longSymbolToken = {
|
|
73
|
+
...token0,
|
|
74
|
+
tokenId: (0, web3_1.binToHex)((0, crypto_1.randomBytes)(32)),
|
|
75
|
+
symbol: 'LongSymbolToken'
|
|
76
|
+
};
|
|
77
|
+
expect(() => (0, serde_1.serializeTokenMetadata)([token0, longSymbolToken, token1])).toThrow('The token symbol is too long');
|
|
78
|
+
});
|
|
13
79
|
});
|
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
|
@@ -32,6 +32,7 @@ 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);
|
|
@@ -108,7 +109,7 @@ 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
114
|
const app = new src_1.default(transport);
|
|
114
115
|
const [testAccount] = await app.getAccount(path);
|
|
@@ -230,6 +231,79 @@ 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 src_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 src_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
309
|
const app = new src_1.default(transport);
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -16,6 +16,9 @@ export enum INS {
|
|
|
16
16
|
export const GROUP_NUM = 4
|
|
17
17
|
export const HASH_LEN = 32
|
|
18
18
|
|
|
19
|
+
// The maximum payload size is 255: https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/hw-transport/src/Transport.ts#L261
|
|
20
|
+
const MAX_PAYLOAD_SIZE = 255
|
|
21
|
+
|
|
19
22
|
export default class AlephiumApp {
|
|
20
23
|
readonly transport: Transport
|
|
21
24
|
|
|
@@ -67,18 +70,23 @@ export default class AlephiumApp {
|
|
|
67
70
|
return decodeSignature(response)
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
async signUnsignedTx(
|
|
73
|
+
async signUnsignedTx(
|
|
74
|
+
path: string,
|
|
75
|
+
unsignedTx: Buffer,
|
|
76
|
+
tokenMetadata: serde.TokenMetadata[] = []
|
|
77
|
+
): Promise<string> {
|
|
71
78
|
console.log(`unsigned tx size: ${unsignedTx.length}`)
|
|
72
79
|
const encodedPath = serde.serializePath(path)
|
|
73
|
-
const
|
|
80
|
+
const encodedTokenMetadata = serde.serializeTokenMetadata(tokenMetadata)
|
|
81
|
+
const firstFrameTxLength = MAX_PAYLOAD_SIZE - 20 - encodedTokenMetadata.length;
|
|
74
82
|
const txData = unsignedTx.slice(0, unsignedTx.length > firstFrameTxLength ? firstFrameTxLength : unsignedTx.length)
|
|
75
|
-
const data = Buffer.concat([encodedPath, txData])
|
|
83
|
+
const data = Buffer.concat([encodedPath, encodedTokenMetadata, txData])
|
|
76
84
|
let response = await this.transport.send(CLA, INS.SIGN_TX, 0x00, 0x00, data, [StatusCodes.OK])
|
|
77
85
|
if (unsignedTx.length <= firstFrameTxLength) {
|
|
78
86
|
return decodeSignature(response)
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
const frameLength =
|
|
89
|
+
const frameLength = MAX_PAYLOAD_SIZE
|
|
82
90
|
let fromIndex = firstFrameTxLength
|
|
83
91
|
while (fromIndex < unsignedTx.length) {
|
|
84
92
|
const remain = unsignedTx.length - fromIndex
|
package/src/serde.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isHexString } from "@alephium/web3"
|
|
2
|
+
|
|
1
3
|
export const TRUE = 0x10
|
|
2
4
|
export const FALSE = 0x00
|
|
3
5
|
|
|
@@ -28,3 +30,60 @@ export function serializePath(path: string): Buffer {
|
|
|
28
30
|
nodes.forEach((element, index) => buffer.writeUInt32BE(element, 4 * index))
|
|
29
31
|
return buffer
|
|
30
32
|
}
|
|
33
|
+
|
|
34
|
+
export const MAX_TOKEN_SIZE = 5
|
|
35
|
+
export const MAX_TOKEN_SYMBOL_LENGTH = 12
|
|
36
|
+
export const TOKEN_METADATA_SIZE = 46
|
|
37
|
+
export interface TokenMetadata {
|
|
38
|
+
version: number,
|
|
39
|
+
tokenId: string,
|
|
40
|
+
symbol: string,
|
|
41
|
+
decimals: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function symbolToBytes(symbol: string): Buffer {
|
|
45
|
+
const buffer = Buffer.alloc(MAX_TOKEN_SYMBOL_LENGTH, 0)
|
|
46
|
+
for (let i = 0; i < symbol.length; i++) {
|
|
47
|
+
buffer[i] = symbol.charCodeAt(i) & 0xFF
|
|
48
|
+
}
|
|
49
|
+
return buffer
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function check(tokens: TokenMetadata[]) {
|
|
53
|
+
const hasDuplicate = tokens.some((token, index) => index !== tokens.findIndex((t) => t.tokenId === token.tokenId))
|
|
54
|
+
if (hasDuplicate) {
|
|
55
|
+
throw new Error(`There are duplicate tokens`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
tokens.forEach((token) => {
|
|
59
|
+
if (!(isHexString(token.tokenId) && token.tokenId.length === 64)) {
|
|
60
|
+
throw new Error(`Invalid token id: ${token.tokenId}`)
|
|
61
|
+
}
|
|
62
|
+
if (token.symbol.length > MAX_TOKEN_SYMBOL_LENGTH) {
|
|
63
|
+
throw new Error(`The token symbol is too long: ${token.symbol}`)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (tokens.length > MAX_TOKEN_SIZE) {
|
|
68
|
+
throw new Error(`The token size exceeds maximum size`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function serializeTokenMetadata(tokens: TokenMetadata[]): Buffer {
|
|
73
|
+
check(tokens)
|
|
74
|
+
const array = tokens
|
|
75
|
+
.map((metadata) => {
|
|
76
|
+
const symbolBytes = symbolToBytes(metadata.symbol)
|
|
77
|
+
const buffer = Buffer.concat([
|
|
78
|
+
Buffer.from([metadata.version]),
|
|
79
|
+
Buffer.from(metadata.tokenId, 'hex'),
|
|
80
|
+
symbolBytes,
|
|
81
|
+
Buffer.from([metadata.decimals])
|
|
82
|
+
])
|
|
83
|
+
if (buffer.length !== TOKEN_METADATA_SIZE) {
|
|
84
|
+
throw new Error(`Invalid token metadata: ${metadata}`)
|
|
85
|
+
}
|
|
86
|
+
return buffer
|
|
87
|
+
})
|
|
88
|
+
return Buffer.concat([Buffer.from([array.length]), ...array])
|
|
89
|
+
}
|
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 SpeculosTransport from '@ledgerhq/hw-transport-node-speculos'
|
|
2
1
|
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'
|
|
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/serde'
|
|
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)
|