@ledgerhq/hw-app-btc 6.20.0 → 6.25.1-alpha.3
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/README.md +4 -4
- package/lib/newops/clientCommands.js +4 -4
- package/lib/newops/clientCommands.js.map +1 -1
- package/lib-es/Btc.js.flow +280 -0
- package/lib-es/bip32.js.flow +13 -0
- package/lib-es/compressPublicKey.js.flow +8 -0
- package/lib-es/constants.js.flow +13 -0
- package/lib-es/createTransaction.js.flow +419 -0
- package/lib-es/debug.js.flow +41 -0
- package/lib-es/finalizeInput.js.flow +38 -0
- package/lib-es/getAppAndVersion.js.flow +19 -0
- package/lib-es/getTrustedInput.js.flow +163 -0
- package/lib-es/getTrustedInputBIP143.js.flow +37 -0
- package/lib-es/getWalletPublicKey.js.flow +51 -0
- package/lib-es/hashPublicKey.js.flow +8 -0
- package/lib-es/newops/clientCommands.js +4 -4
- package/lib-es/newops/clientCommands.js.map +1 -1
- package/lib-es/serializeTransaction.js.flow +77 -0
- package/lib-es/shouldUseTrustedInputForSegwit.js.flow +15 -0
- package/lib-es/signMessage.js.flow +67 -0
- package/lib-es/signP2SHTransaction.js.flow +170 -0
- package/lib-es/signTransaction.js.flow +40 -0
- package/lib-es/splitTransaction.js.flow +145 -0
- package/lib-es/startUntrustedHashTransactionInput.js.flow +136 -0
- package/lib-es/types.js.flow +31 -0
- package/lib-es/varint.js.flow +43 -0
- package/package.json +3 -3
- package/src/newops/clientCommands.ts +4 -4
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import { log } from "@ledgerhq/logs";
|
|
4
|
+
import type Transport from "@ledgerhq/hw-transport";
|
|
5
|
+
import { hashPublicKey } from "./hashPublicKey";
|
|
6
|
+
import { getWalletPublicKey } from "./getWalletPublicKey";
|
|
7
|
+
import type { AddressFormat } from "./getWalletPublicKey";
|
|
8
|
+
import { getTrustedInput } from "./getTrustedInput";
|
|
9
|
+
import { startUntrustedHashTransactionInput } from "./startUntrustedHashTransactionInput";
|
|
10
|
+
import { serializeTransaction } from "./serializeTransaction";
|
|
11
|
+
import { getTrustedInputBIP143 } from "./getTrustedInputBIP143";
|
|
12
|
+
import { compressPublicKey } from "./compressPublicKey";
|
|
13
|
+
import { signTransaction } from "./signTransaction";
|
|
14
|
+
import { hashOutputFull, provideOutputFullChangePath } from "./finalizeInput";
|
|
15
|
+
import { getAppAndVersion } from "./getAppAndVersion";
|
|
16
|
+
import type { TransactionOutput, Transaction } from "./types";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_LOCKTIME,
|
|
19
|
+
DEFAULT_SEQUENCE,
|
|
20
|
+
SIGHASH_ALL,
|
|
21
|
+
OP_DUP,
|
|
22
|
+
OP_HASH160,
|
|
23
|
+
HASH_SIZE,
|
|
24
|
+
OP_EQUALVERIFY,
|
|
25
|
+
OP_CHECKSIG,
|
|
26
|
+
} from "./constants";
|
|
27
|
+
import { shouldUseTrustedInputForSegwit } from "./shouldUseTrustedInputForSegwit";
|
|
28
|
+
|
|
29
|
+
export type { AddressFormat };
|
|
30
|
+
|
|
31
|
+
const defaultsSignTransaction = {
|
|
32
|
+
lockTime: DEFAULT_LOCKTIME,
|
|
33
|
+
sigHashType: SIGHASH_ALL,
|
|
34
|
+
segwit: false,
|
|
35
|
+
additionals: [],
|
|
36
|
+
onDeviceStreaming: (_e) => {},
|
|
37
|
+
onDeviceSignatureGranted: () => {},
|
|
38
|
+
onDeviceSignatureRequested: () => {},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
*
|
|
43
|
+
*/
|
|
44
|
+
export type CreateTransactionArg = {
|
|
45
|
+
inputs: Array<[Transaction, number, ?string, ?number]>,
|
|
46
|
+
associatedKeysets: string[],
|
|
47
|
+
changePath?: string,
|
|
48
|
+
outputScriptHex: string,
|
|
49
|
+
lockTime?: number,
|
|
50
|
+
sigHashType?: number,
|
|
51
|
+
segwit?: boolean,
|
|
52
|
+
initialTimestamp?: number,
|
|
53
|
+
additionals: Array<string>,
|
|
54
|
+
expiryHeight?: Buffer,
|
|
55
|
+
useTrustedInputForSegwit?: boolean,
|
|
56
|
+
onDeviceStreaming?: ({
|
|
57
|
+
progress: number,
|
|
58
|
+
total: number,
|
|
59
|
+
index: number,
|
|
60
|
+
}) => void,
|
|
61
|
+
onDeviceSignatureRequested?: () => void,
|
|
62
|
+
onDeviceSignatureGranted?: () => void,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export async function createTransaction(
|
|
66
|
+
transport: Transport<*>,
|
|
67
|
+
arg: CreateTransactionArg
|
|
68
|
+
) {
|
|
69
|
+
let {
|
|
70
|
+
inputs,
|
|
71
|
+
associatedKeysets,
|
|
72
|
+
changePath,
|
|
73
|
+
outputScriptHex,
|
|
74
|
+
lockTime,
|
|
75
|
+
sigHashType,
|
|
76
|
+
segwit,
|
|
77
|
+
initialTimestamp,
|
|
78
|
+
additionals,
|
|
79
|
+
expiryHeight,
|
|
80
|
+
useTrustedInputForSegwit,
|
|
81
|
+
onDeviceStreaming,
|
|
82
|
+
onDeviceSignatureGranted,
|
|
83
|
+
onDeviceSignatureRequested,
|
|
84
|
+
} = {
|
|
85
|
+
...defaultsSignTransaction,
|
|
86
|
+
...arg,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (useTrustedInputForSegwit === undefined) {
|
|
90
|
+
try {
|
|
91
|
+
const a = await getAppAndVersion(transport);
|
|
92
|
+
useTrustedInputForSegwit = shouldUseTrustedInputForSegwit(a);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (e.statusCode === 0x6d00) {
|
|
95
|
+
useTrustedInputForSegwit = false;
|
|
96
|
+
} else {
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// loop: 0 or 1 (before and after)
|
|
103
|
+
// i: index of the input being streamed
|
|
104
|
+
// i goes on 0...n, inluding n. in order for the progress value to go to 1
|
|
105
|
+
// we normalize the 2 loops to make a global percentage
|
|
106
|
+
const notify = (loop, i) => {
|
|
107
|
+
const { length } = inputs;
|
|
108
|
+
if (length < 3) return; // there is not enough significant event to worth notifying (aka just use a spinner)
|
|
109
|
+
const index = length * loop + i;
|
|
110
|
+
const total = 2 * length;
|
|
111
|
+
const progress = index / total;
|
|
112
|
+
onDeviceStreaming({ progress, total, index });
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const isDecred = additionals.includes("decred");
|
|
116
|
+
const isXST = additionals.includes("stealthcoin");
|
|
117
|
+
let startTime = Date.now();
|
|
118
|
+
const sapling = additionals.includes("sapling");
|
|
119
|
+
const bech32 = segwit && additionals.includes("bech32");
|
|
120
|
+
let useBip143 =
|
|
121
|
+
segwit ||
|
|
122
|
+
(!!additionals &&
|
|
123
|
+
(additionals.includes("abc") ||
|
|
124
|
+
additionals.includes("gold") ||
|
|
125
|
+
additionals.includes("bip143"))) ||
|
|
126
|
+
(!!expiryHeight && !isDecred);
|
|
127
|
+
// Inputs are provided as arrays of [transaction, output_index, optional redeem script, optional sequence]
|
|
128
|
+
// associatedKeysets are provided as arrays of [path]
|
|
129
|
+
const nullScript = Buffer.alloc(0);
|
|
130
|
+
const nullPrevout = Buffer.alloc(0);
|
|
131
|
+
const defaultVersion = Buffer.alloc(4);
|
|
132
|
+
!!expiryHeight && !isDecred
|
|
133
|
+
? defaultVersion.writeUInt32LE(sapling ? 0x80000004 : 0x80000003, 0)
|
|
134
|
+
: isXST
|
|
135
|
+
? defaultVersion.writeUInt32LE(2, 0)
|
|
136
|
+
: defaultVersion.writeUInt32LE(1, 0); // Default version to 2 for XST not to have timestamp
|
|
137
|
+
const trustedInputs: Array<*> = [];
|
|
138
|
+
const regularOutputs: Array<TransactionOutput> = [];
|
|
139
|
+
const signatures = [];
|
|
140
|
+
const publicKeys = [];
|
|
141
|
+
let firstRun = true;
|
|
142
|
+
const resuming = false;
|
|
143
|
+
const targetTransaction: Transaction = {
|
|
144
|
+
inputs: [],
|
|
145
|
+
version: defaultVersion,
|
|
146
|
+
timestamp: Buffer.alloc(0),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const getTrustedInputCall =
|
|
150
|
+
useBip143 && !useTrustedInputForSegwit
|
|
151
|
+
? getTrustedInputBIP143
|
|
152
|
+
: getTrustedInput;
|
|
153
|
+
const outputScript = Buffer.from(outputScriptHex, "hex");
|
|
154
|
+
|
|
155
|
+
notify(0, 0);
|
|
156
|
+
|
|
157
|
+
// first pass on inputs to get trusted inputs
|
|
158
|
+
for (let input of inputs) {
|
|
159
|
+
if (!resuming) {
|
|
160
|
+
const trustedInput = await getTrustedInputCall(
|
|
161
|
+
transport,
|
|
162
|
+
input[1],
|
|
163
|
+
input[0],
|
|
164
|
+
additionals
|
|
165
|
+
);
|
|
166
|
+
log("hw", "got trustedInput=" + trustedInput);
|
|
167
|
+
let sequence = Buffer.alloc(4);
|
|
168
|
+
sequence.writeUInt32LE(
|
|
169
|
+
input.length >= 4 && typeof input[3] === "number"
|
|
170
|
+
? input[3]
|
|
171
|
+
: DEFAULT_SEQUENCE,
|
|
172
|
+
0
|
|
173
|
+
);
|
|
174
|
+
trustedInputs.push({
|
|
175
|
+
trustedInput: true,
|
|
176
|
+
value: Buffer.from(trustedInput, "hex"),
|
|
177
|
+
sequence,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const { outputs } = input[0];
|
|
182
|
+
const index = input[1];
|
|
183
|
+
if (outputs && index <= outputs.length - 1) {
|
|
184
|
+
regularOutputs.push(outputs[index]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (expiryHeight && !isDecred) {
|
|
188
|
+
targetTransaction.nVersionGroupId = Buffer.from(
|
|
189
|
+
sapling ? [0x85, 0x20, 0x2f, 0x89] : [0x70, 0x82, 0xc4, 0x03]
|
|
190
|
+
);
|
|
191
|
+
targetTransaction.nExpiryHeight = expiryHeight;
|
|
192
|
+
// For sapling : valueBalance (8), nShieldedSpend (1), nShieldedOutput (1), nJoinSplit (1)
|
|
193
|
+
// Overwinter : use nJoinSplit (1)
|
|
194
|
+
targetTransaction.extraData = Buffer.from(
|
|
195
|
+
sapling
|
|
196
|
+
? [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
|
197
|
+
: [0x00]
|
|
198
|
+
);
|
|
199
|
+
} else if (isDecred) {
|
|
200
|
+
targetTransaction.nExpiryHeight = expiryHeight;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
targetTransaction.inputs = inputs.map((input) => {
|
|
205
|
+
let sequence = Buffer.alloc(4);
|
|
206
|
+
sequence.writeUInt32LE(
|
|
207
|
+
input.length >= 4 && typeof input[3] === "number"
|
|
208
|
+
? input[3]
|
|
209
|
+
: DEFAULT_SEQUENCE,
|
|
210
|
+
0
|
|
211
|
+
);
|
|
212
|
+
return {
|
|
213
|
+
script: nullScript,
|
|
214
|
+
prevout: nullPrevout,
|
|
215
|
+
sequence,
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (!resuming) {
|
|
220
|
+
// Collect public keys
|
|
221
|
+
const result = [];
|
|
222
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
223
|
+
const r = await getWalletPublicKey(transport, {
|
|
224
|
+
path: associatedKeysets[i],
|
|
225
|
+
});
|
|
226
|
+
notify(0, i + 1);
|
|
227
|
+
result.push(r);
|
|
228
|
+
}
|
|
229
|
+
for (let i = 0; i < result.length; i++) {
|
|
230
|
+
publicKeys.push(
|
|
231
|
+
compressPublicKey(Buffer.from(result[i].publicKey, "hex"))
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (initialTimestamp !== undefined) {
|
|
237
|
+
targetTransaction.timestamp = Buffer.alloc(4);
|
|
238
|
+
targetTransaction.timestamp.writeUInt32LE(
|
|
239
|
+
Math.floor(initialTimestamp + (Date.now() - startTime) / 1000),
|
|
240
|
+
0
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
onDeviceSignatureRequested();
|
|
245
|
+
|
|
246
|
+
if (useBip143) {
|
|
247
|
+
// Do the first run with all inputs
|
|
248
|
+
await startUntrustedHashTransactionInput(
|
|
249
|
+
transport,
|
|
250
|
+
true,
|
|
251
|
+
targetTransaction,
|
|
252
|
+
trustedInputs,
|
|
253
|
+
true,
|
|
254
|
+
!!expiryHeight,
|
|
255
|
+
additionals,
|
|
256
|
+
useTrustedInputForSegwit
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (!resuming && changePath) {
|
|
260
|
+
await provideOutputFullChangePath(transport, changePath);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
await hashOutputFull(transport, outputScript);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!!expiryHeight && !isDecred) {
|
|
267
|
+
await signTransaction(transport, "", lockTime, SIGHASH_ALL, expiryHeight);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Do the second run with the individual transaction
|
|
271
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
272
|
+
const input = inputs[i];
|
|
273
|
+
let script =
|
|
274
|
+
inputs[i].length >= 3 && typeof input[2] === "string"
|
|
275
|
+
? Buffer.from(input[2], "hex")
|
|
276
|
+
: !segwit
|
|
277
|
+
? regularOutputs[i].script
|
|
278
|
+
: Buffer.concat([
|
|
279
|
+
Buffer.from([OP_DUP, OP_HASH160, HASH_SIZE]),
|
|
280
|
+
hashPublicKey(publicKeys[i]),
|
|
281
|
+
Buffer.from([OP_EQUALVERIFY, OP_CHECKSIG]),
|
|
282
|
+
]);
|
|
283
|
+
let pseudoTX = Object.assign({}, targetTransaction);
|
|
284
|
+
let pseudoTrustedInputs = useBip143 ? [trustedInputs[i]] : trustedInputs;
|
|
285
|
+
if (useBip143) {
|
|
286
|
+
pseudoTX.inputs = [{ ...pseudoTX.inputs[i], script }];
|
|
287
|
+
} else {
|
|
288
|
+
pseudoTX.inputs[i].script = script;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await startUntrustedHashTransactionInput(
|
|
292
|
+
transport,
|
|
293
|
+
!useBip143 && firstRun,
|
|
294
|
+
pseudoTX,
|
|
295
|
+
pseudoTrustedInputs,
|
|
296
|
+
useBip143,
|
|
297
|
+
!!expiryHeight && !isDecred,
|
|
298
|
+
additionals,
|
|
299
|
+
useTrustedInputForSegwit
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
if (!useBip143) {
|
|
303
|
+
if (!resuming && changePath) {
|
|
304
|
+
await provideOutputFullChangePath(transport, changePath);
|
|
305
|
+
}
|
|
306
|
+
await hashOutputFull(transport, outputScript, additionals);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (firstRun) {
|
|
310
|
+
onDeviceSignatureGranted();
|
|
311
|
+
notify(1, 0);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const signature = await signTransaction(
|
|
315
|
+
transport,
|
|
316
|
+
associatedKeysets[i],
|
|
317
|
+
lockTime,
|
|
318
|
+
sigHashType,
|
|
319
|
+
expiryHeight,
|
|
320
|
+
additionals
|
|
321
|
+
);
|
|
322
|
+
notify(1, i + 1);
|
|
323
|
+
|
|
324
|
+
signatures.push(signature);
|
|
325
|
+
targetTransaction.inputs[i].script = nullScript;
|
|
326
|
+
if (firstRun) {
|
|
327
|
+
firstRun = false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Populate the final input scripts
|
|
332
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
333
|
+
if (segwit) {
|
|
334
|
+
targetTransaction.witness = Buffer.alloc(0);
|
|
335
|
+
if (!bech32) {
|
|
336
|
+
targetTransaction.inputs[i].script = Buffer.concat([
|
|
337
|
+
Buffer.from("160014", "hex"),
|
|
338
|
+
hashPublicKey(publicKeys[i]),
|
|
339
|
+
]);
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
const signatureSize = Buffer.alloc(1);
|
|
343
|
+
const keySize = Buffer.alloc(1);
|
|
344
|
+
signatureSize[0] = signatures[i].length;
|
|
345
|
+
keySize[0] = publicKeys[i].length;
|
|
346
|
+
targetTransaction.inputs[i].script = Buffer.concat([
|
|
347
|
+
signatureSize,
|
|
348
|
+
signatures[i],
|
|
349
|
+
keySize,
|
|
350
|
+
publicKeys[i],
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
let offset = useBip143 && !useTrustedInputForSegwit ? 0 : 4;
|
|
354
|
+
targetTransaction.inputs[i].prevout = trustedInputs[i].value.slice(
|
|
355
|
+
offset,
|
|
356
|
+
offset + 0x24
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const lockTimeBuffer = Buffer.alloc(4);
|
|
361
|
+
lockTimeBuffer.writeUInt32LE(lockTime, 0);
|
|
362
|
+
|
|
363
|
+
var result = Buffer.concat([
|
|
364
|
+
serializeTransaction(
|
|
365
|
+
targetTransaction,
|
|
366
|
+
false,
|
|
367
|
+
targetTransaction.timestamp,
|
|
368
|
+
additionals
|
|
369
|
+
),
|
|
370
|
+
outputScript,
|
|
371
|
+
]);
|
|
372
|
+
|
|
373
|
+
if (segwit && !isDecred) {
|
|
374
|
+
var witness = Buffer.alloc(0);
|
|
375
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
376
|
+
var tmpScriptData = Buffer.concat([
|
|
377
|
+
Buffer.from("02", "hex"),
|
|
378
|
+
Buffer.from([signatures[i].length]),
|
|
379
|
+
signatures[i],
|
|
380
|
+
Buffer.from([publicKeys[i].length]),
|
|
381
|
+
publicKeys[i],
|
|
382
|
+
]);
|
|
383
|
+
witness = Buffer.concat([witness, tmpScriptData]);
|
|
384
|
+
}
|
|
385
|
+
result = Buffer.concat([result, witness]);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// FIXME: In ZEC or KMD sapling lockTime is serialized before expiryHeight.
|
|
389
|
+
// expiryHeight is used only in overwinter/sapling so I moved lockTimeBuffer here
|
|
390
|
+
// and it should not break other coins because expiryHeight is false for them.
|
|
391
|
+
// Don't know about Decred though.
|
|
392
|
+
result = Buffer.concat([result, lockTimeBuffer]);
|
|
393
|
+
|
|
394
|
+
if (expiryHeight) {
|
|
395
|
+
result = Buffer.concat([
|
|
396
|
+
result,
|
|
397
|
+
targetTransaction.nExpiryHeight || Buffer.alloc(0),
|
|
398
|
+
targetTransaction.extraData || Buffer.alloc(0),
|
|
399
|
+
]);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (isDecred) {
|
|
403
|
+
let decredWitness = Buffer.from([targetTransaction.inputs.length]);
|
|
404
|
+
inputs.forEach((input, inputIndex) => {
|
|
405
|
+
decredWitness = Buffer.concat([
|
|
406
|
+
decredWitness,
|
|
407
|
+
Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
|
408
|
+
Buffer.from([0x00, 0x00, 0x00, 0x00]), //Block height
|
|
409
|
+
Buffer.from([0xff, 0xff, 0xff, 0xff]), //Block index
|
|
410
|
+
Buffer.from([targetTransaction.inputs[inputIndex].script.length]),
|
|
411
|
+
targetTransaction.inputs[inputIndex].script,
|
|
412
|
+
]);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
result = Buffer.concat([result, decredWitness]);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return result.toString("hex");
|
|
419
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import type { Transaction } from "./types";
|
|
3
|
+
|
|
4
|
+
export function formatTransactionDebug(transaction: Transaction) {
|
|
5
|
+
let str = "TX";
|
|
6
|
+
str += " version " + transaction.version.toString("hex");
|
|
7
|
+
if (transaction.locktime) {
|
|
8
|
+
str += " locktime " + transaction.locktime.toString("hex");
|
|
9
|
+
}
|
|
10
|
+
if (transaction.witness) {
|
|
11
|
+
str += " witness " + transaction.witness.toString("hex");
|
|
12
|
+
}
|
|
13
|
+
if (transaction.timestamp) {
|
|
14
|
+
str += " timestamp " + transaction.timestamp.toString("hex");
|
|
15
|
+
}
|
|
16
|
+
if (transaction.nVersionGroupId) {
|
|
17
|
+
str += " nVersionGroupId " + transaction.nVersionGroupId.toString("hex");
|
|
18
|
+
}
|
|
19
|
+
if (transaction.nExpiryHeight) {
|
|
20
|
+
str += " nExpiryHeight " + transaction.nExpiryHeight.toString("hex");
|
|
21
|
+
}
|
|
22
|
+
if (transaction.extraData) {
|
|
23
|
+
str += " extraData " + transaction.extraData.toString("hex");
|
|
24
|
+
}
|
|
25
|
+
transaction.inputs.forEach(({ prevout, script, sequence }, i) => {
|
|
26
|
+
str += `\ninput ${i}:`;
|
|
27
|
+
str += ` prevout ${prevout.toString("hex")}`;
|
|
28
|
+
str += ` script ${script.toString("hex")}`;
|
|
29
|
+
str += ` sequence ${sequence.toString("hex")}`;
|
|
30
|
+
});
|
|
31
|
+
(transaction.outputs || []).forEach(({ amount, script }, i) => {
|
|
32
|
+
str += `\noutput ${i}:`;
|
|
33
|
+
str += ` amount ${amount.toString("hex")}`;
|
|
34
|
+
str += ` script ${script.toString("hex")}`;
|
|
35
|
+
});
|
|
36
|
+
return str;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function displayTransactionDebug(transaction: Transaction) {
|
|
40
|
+
console.log(formatTransactionDebug(transaction));
|
|
41
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import Transport from "@ledgerhq/hw-transport";
|
|
3
|
+
import { bip32asBuffer } from "./bip32";
|
|
4
|
+
import { MAX_SCRIPT_BLOCK } from "./constants";
|
|
5
|
+
|
|
6
|
+
export function provideOutputFullChangePath(
|
|
7
|
+
transport: Transport<*>,
|
|
8
|
+
path: string
|
|
9
|
+
): Promise<string> {
|
|
10
|
+
let buffer = bip32asBuffer(path);
|
|
11
|
+
return transport.send(0xe0, 0x4a, 0xff, 0x00, buffer);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function hashOutputFull(
|
|
15
|
+
transport: Transport<*>,
|
|
16
|
+
outputScript: Buffer,
|
|
17
|
+
additionals: Array<string> = []
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
let offset = 0;
|
|
20
|
+
let p1 = 0x80;
|
|
21
|
+
const isDecred = additionals.includes("decred");
|
|
22
|
+
///WARNING: Decred works only with one call (without chunking)
|
|
23
|
+
//TODO: test without this for Decred
|
|
24
|
+
if (isDecred) {
|
|
25
|
+
return transport.send(0xe0, 0x4a, p1, 0x00, outputScript);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
while (offset < outputScript.length) {
|
|
29
|
+
let blockSize =
|
|
30
|
+
offset + MAX_SCRIPT_BLOCK >= outputScript.length
|
|
31
|
+
? outputScript.length - offset
|
|
32
|
+
: MAX_SCRIPT_BLOCK;
|
|
33
|
+
let p1 = offset + blockSize === outputScript.length ? 0x80 : 0x00;
|
|
34
|
+
let data = outputScript.slice(offset, offset + blockSize);
|
|
35
|
+
await transport.send(0xe0, 0x4a, p1, 0x00, data);
|
|
36
|
+
offset += blockSize;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import invariant from "invariant";
|
|
3
|
+
import Transport from "@ledgerhq/hw-transport";
|
|
4
|
+
|
|
5
|
+
export const getAppAndVersion = async (
|
|
6
|
+
transport: Transport<*>
|
|
7
|
+
): Promise<{ name: string, version: string, flags: number }> => {
|
|
8
|
+
const r = await transport.send(0xb0, 0x01, 0x00, 0x00);
|
|
9
|
+
let i = 0;
|
|
10
|
+
const format = r[i++];
|
|
11
|
+
invariant(format === 1, "getAppAndVersion: format not supported");
|
|
12
|
+
const nameLength = r[i++];
|
|
13
|
+
const name = r.slice(i, (i += nameLength)).toString("ascii");
|
|
14
|
+
const versionLength = r[i++];
|
|
15
|
+
const version = r.slice(i, (i += versionLength)).toString("ascii");
|
|
16
|
+
const flagLength = r[i++];
|
|
17
|
+
const flags = r.slice(i, (i += flagLength));
|
|
18
|
+
return { name, version, flags };
|
|
19
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import invariant from "invariant";
|
|
3
|
+
import type Transport from "@ledgerhq/hw-transport";
|
|
4
|
+
import type { Transaction } from "./types";
|
|
5
|
+
import { MAX_SCRIPT_BLOCK } from "./constants";
|
|
6
|
+
import { createVarint } from "./varint";
|
|
7
|
+
|
|
8
|
+
export async function getTrustedInputRaw(
|
|
9
|
+
transport: Transport<*>,
|
|
10
|
+
transactionData: Buffer,
|
|
11
|
+
indexLookup: ?number
|
|
12
|
+
): Promise<string> {
|
|
13
|
+
let data;
|
|
14
|
+
let firstRound = false;
|
|
15
|
+
if (typeof indexLookup === "number") {
|
|
16
|
+
firstRound = true;
|
|
17
|
+
const prefix = Buffer.alloc(4);
|
|
18
|
+
prefix.writeUInt32BE(indexLookup, 0);
|
|
19
|
+
data = Buffer.concat([prefix, transactionData], transactionData.length + 4);
|
|
20
|
+
} else {
|
|
21
|
+
data = transactionData;
|
|
22
|
+
}
|
|
23
|
+
const trustedInput = await transport.send(
|
|
24
|
+
0xe0,
|
|
25
|
+
0x42,
|
|
26
|
+
firstRound ? 0x00 : 0x80,
|
|
27
|
+
0x00,
|
|
28
|
+
data
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const res = trustedInput.slice(0, trustedInput.length - 2).toString("hex");
|
|
32
|
+
return res;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getTrustedInput(
|
|
36
|
+
transport: Transport<*>,
|
|
37
|
+
indexLookup: number,
|
|
38
|
+
transaction: Transaction,
|
|
39
|
+
additionals: Array<string> = []
|
|
40
|
+
): Promise<string> {
|
|
41
|
+
const {
|
|
42
|
+
version,
|
|
43
|
+
inputs,
|
|
44
|
+
outputs,
|
|
45
|
+
locktime,
|
|
46
|
+
nExpiryHeight,
|
|
47
|
+
extraData,
|
|
48
|
+
} = transaction;
|
|
49
|
+
if (!outputs || !locktime) {
|
|
50
|
+
throw new Error("getTrustedInput: locktime & outputs is expected");
|
|
51
|
+
}
|
|
52
|
+
const isDecred = additionals.includes("decred");
|
|
53
|
+
const isXST = additionals.includes("stealthcoin");
|
|
54
|
+
|
|
55
|
+
const processScriptBlocks = async (script, sequence) => {
|
|
56
|
+
const seq = sequence || Buffer.alloc(0);
|
|
57
|
+
const scriptBlocks = [];
|
|
58
|
+
let offset = 0;
|
|
59
|
+
while (offset !== script.length) {
|
|
60
|
+
let blockSize =
|
|
61
|
+
script.length - offset > MAX_SCRIPT_BLOCK
|
|
62
|
+
? MAX_SCRIPT_BLOCK
|
|
63
|
+
: script.length - offset;
|
|
64
|
+
if (offset + blockSize !== script.length) {
|
|
65
|
+
scriptBlocks.push(script.slice(offset, offset + blockSize));
|
|
66
|
+
} else {
|
|
67
|
+
scriptBlocks.push(
|
|
68
|
+
Buffer.concat([script.slice(offset, offset + blockSize), seq])
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
offset += blockSize;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle case when no script length: we still want to pass the sequence
|
|
75
|
+
// relatable: https://github.com/LedgerHQ/ledger-live-desktop/issues/1386
|
|
76
|
+
if (script.length === 0) {
|
|
77
|
+
scriptBlocks.push(seq);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let res;
|
|
81
|
+
for (let scriptBlock of scriptBlocks) {
|
|
82
|
+
res = await getTrustedInputRaw(transport, scriptBlock);
|
|
83
|
+
}
|
|
84
|
+
return res;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const processWholeScriptBlock = (block) =>
|
|
88
|
+
getTrustedInputRaw(transport, block);
|
|
89
|
+
|
|
90
|
+
await getTrustedInputRaw(
|
|
91
|
+
transport,
|
|
92
|
+
Buffer.concat([
|
|
93
|
+
transaction.version,
|
|
94
|
+
transaction.timestamp || Buffer.alloc(0),
|
|
95
|
+
transaction.nVersionGroupId || Buffer.alloc(0),
|
|
96
|
+
createVarint(inputs.length),
|
|
97
|
+
]),
|
|
98
|
+
indexLookup
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
for (let input of inputs) {
|
|
102
|
+
const isXSTV2 =
|
|
103
|
+
isXST &&
|
|
104
|
+
Buffer.compare(version, Buffer.from([0x02, 0x00, 0x00, 0x00])) === 0;
|
|
105
|
+
const treeField = isDecred
|
|
106
|
+
? input.tree || Buffer.from([0x00])
|
|
107
|
+
: Buffer.alloc(0);
|
|
108
|
+
const data = Buffer.concat([
|
|
109
|
+
input.prevout,
|
|
110
|
+
treeField,
|
|
111
|
+
isXSTV2 ? Buffer.from([0x00]) : createVarint(input.script.length),
|
|
112
|
+
]);
|
|
113
|
+
await getTrustedInputRaw(transport, data);
|
|
114
|
+
|
|
115
|
+
// iteration (eachSeries) ended
|
|
116
|
+
// TODO notify progress
|
|
117
|
+
// deferred.notify("input");
|
|
118
|
+
// Reference: https://github.com/StealthSend/Stealth/commit/5be35d6c2c500b32ed82e5d6913d66d18a4b0a7f#diff-e8db9b851adc2422aadfffca88f14c91R566
|
|
119
|
+
await (isDecred
|
|
120
|
+
? processWholeScriptBlock(Buffer.concat([input.script, input.sequence]))
|
|
121
|
+
: isXSTV2
|
|
122
|
+
? processWholeScriptBlock(input.sequence)
|
|
123
|
+
: processScriptBlocks(input.script, input.sequence));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await getTrustedInputRaw(transport, createVarint(outputs.length));
|
|
127
|
+
|
|
128
|
+
for (let output of outputs) {
|
|
129
|
+
const data = Buffer.concat([
|
|
130
|
+
output.amount,
|
|
131
|
+
isDecred ? Buffer.from([0x00, 0x00]) : Buffer.alloc(0), //Version script
|
|
132
|
+
createVarint(output.script.length),
|
|
133
|
+
output.script,
|
|
134
|
+
]);
|
|
135
|
+
await getTrustedInputRaw(transport, data);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const endData = [];
|
|
139
|
+
|
|
140
|
+
if (nExpiryHeight && nExpiryHeight.length > 0) {
|
|
141
|
+
endData.push(nExpiryHeight);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (extraData && extraData.length > 0) {
|
|
145
|
+
endData.push(extraData);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let extraPart;
|
|
149
|
+
if (endData.length) {
|
|
150
|
+
const data = Buffer.concat(endData);
|
|
151
|
+
extraPart = isDecred
|
|
152
|
+
? data
|
|
153
|
+
: Buffer.concat([createVarint(data.length), data]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const res = await processScriptBlocks(
|
|
157
|
+
Buffer.concat([locktime, extraPart || Buffer.alloc(0)])
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
invariant(res, "missing result in processScriptBlocks");
|
|
161
|
+
|
|
162
|
+
return res;
|
|
163
|
+
}
|