@ledgerhq/hw-app-btc 6.10.0 → 6.12.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/README.md +384 -60
- package/lib/Btc.d.ts +9 -6
- package/lib/Btc.d.ts.map +1 -1
- package/lib/Btc.js +73 -8
- package/lib/Btc.js.map +1 -1
- package/lib/BtcNew.d.ts +79 -32
- package/lib/BtcNew.d.ts.map +1 -1
- package/lib/BtcNew.js +168 -207
- package/lib/BtcNew.js.map +1 -1
- package/lib/newops/accounttype.d.ts +110 -0
- package/lib/newops/accounttype.d.ts.map +1 -0
- package/lib/newops/accounttype.js +233 -0
- package/lib/newops/accounttype.js.map +1 -0
- package/lib/newops/appClient.d.ts +6 -2
- package/lib/newops/appClient.d.ts.map +1 -1
- package/lib/newops/appClient.js +8 -4
- package/lib/newops/appClient.js.map +1 -1
- package/lib/newops/clientCommands.d.ts +18 -2
- package/lib/newops/clientCommands.d.ts.map +1 -1
- package/lib/newops/clientCommands.js +34 -12
- package/lib/newops/clientCommands.js.map +1 -1
- package/lib/newops/merkelizedPsbt.d.ts +11 -0
- package/lib/newops/merkelizedPsbt.d.ts.map +1 -1
- package/lib/newops/merkelizedPsbt.js +11 -0
- package/lib/newops/merkelizedPsbt.js.map +1 -1
- package/lib/newops/merkle.d.ts +5 -0
- package/lib/newops/merkle.d.ts.map +1 -1
- package/lib/newops/merkle.js +5 -0
- package/lib/newops/merkle.js.map +1 -1
- package/lib/newops/merkleMap.d.ts +10 -0
- package/lib/newops/merkleMap.d.ts.map +1 -1
- package/lib/newops/merkleMap.js +10 -0
- package/lib/newops/merkleMap.js.map +1 -1
- package/lib/newops/policy.d.ts +8 -0
- package/lib/newops/policy.d.ts.map +1 -1
- package/lib/newops/policy.js +9 -1
- package/lib/newops/policy.js.map +1 -1
- package/lib/newops/psbtExtractor.d.ts +6 -0
- package/lib/newops/psbtExtractor.d.ts.map +1 -1
- package/lib/newops/psbtExtractor.js +6 -0
- package/lib/newops/psbtExtractor.js.map +1 -1
- package/lib/newops/psbtFinalizer.d.ts +11 -1
- package/lib/newops/psbtFinalizer.d.ts.map +1 -1
- package/lib/newops/psbtFinalizer.js +26 -1
- package/lib/newops/psbtFinalizer.js.map +1 -1
- package/lib/newops/psbtv2.d.ts +22 -2
- package/lib/newops/psbtv2.d.ts.map +1 -1
- package/lib/newops/psbtv2.js +33 -8
- package/lib/newops/psbtv2.js.map +1 -1
- package/lib-es/Btc.d.ts +9 -6
- package/lib-es/Btc.d.ts.map +1 -1
- package/lib-es/Btc.js +73 -8
- package/lib-es/Btc.js.map +1 -1
- package/lib-es/BtcNew.d.ts +79 -32
- package/lib-es/BtcNew.d.ts.map +1 -1
- package/lib-es/BtcNew.js +170 -209
- package/lib-es/BtcNew.js.map +1 -1
- package/lib-es/newops/accounttype.d.ts +110 -0
- package/lib-es/newops/accounttype.d.ts.map +1 -0
- package/lib-es/newops/accounttype.js +230 -0
- package/lib-es/newops/accounttype.js.map +1 -0
- package/lib-es/newops/appClient.d.ts +6 -2
- package/lib-es/newops/appClient.d.ts.map +1 -1
- package/lib-es/newops/appClient.js +8 -4
- package/lib-es/newops/appClient.js.map +1 -1
- package/lib-es/newops/clientCommands.d.ts +18 -2
- package/lib-es/newops/clientCommands.d.ts.map +1 -1
- package/lib-es/newops/clientCommands.js +34 -12
- package/lib-es/newops/clientCommands.js.map +1 -1
- package/lib-es/newops/merkelizedPsbt.d.ts +11 -0
- package/lib-es/newops/merkelizedPsbt.d.ts.map +1 -1
- package/lib-es/newops/merkelizedPsbt.js +11 -0
- package/lib-es/newops/merkelizedPsbt.js.map +1 -1
- package/lib-es/newops/merkle.d.ts +5 -0
- package/lib-es/newops/merkle.d.ts.map +1 -1
- package/lib-es/newops/merkle.js +5 -0
- package/lib-es/newops/merkle.js.map +1 -1
- package/lib-es/newops/merkleMap.d.ts +10 -0
- package/lib-es/newops/merkleMap.d.ts.map +1 -1
- package/lib-es/newops/merkleMap.js +10 -0
- package/lib-es/newops/merkleMap.js.map +1 -1
- package/lib-es/newops/policy.d.ts +8 -0
- package/lib-es/newops/policy.d.ts.map +1 -1
- package/lib-es/newops/policy.js +10 -2
- package/lib-es/newops/policy.js.map +1 -1
- package/lib-es/newops/psbtExtractor.d.ts +6 -0
- package/lib-es/newops/psbtExtractor.d.ts.map +1 -1
- package/lib-es/newops/psbtExtractor.js +6 -0
- package/lib-es/newops/psbtExtractor.js.map +1 -1
- package/lib-es/newops/psbtFinalizer.d.ts +11 -1
- package/lib-es/newops/psbtFinalizer.d.ts.map +1 -1
- package/lib-es/newops/psbtFinalizer.js +26 -1
- package/lib-es/newops/psbtFinalizer.js.map +1 -1
- package/lib-es/newops/psbtv2.d.ts +22 -2
- package/lib-es/newops/psbtv2.d.ts.map +1 -1
- package/lib-es/newops/psbtv2.js +33 -8
- package/lib-es/newops/psbtv2.js.map +1 -1
- package/package.json +3 -3
- package/src/Btc.ts +111 -9
- package/src/BtcNew.ts +204 -209
- package/src/newops/accounttype.ts +370 -0
- package/src/newops/appClient.ts +12 -4
- package/src/newops/clientCommands.ts +34 -12
- package/src/newops/merkelizedPsbt.ts +11 -0
- package/src/newops/merkle.ts +5 -0
- package/src/newops/merkleMap.ts +10 -0
- package/src/newops/policy.ts +10 -2
- package/src/newops/psbtExtractor.ts +6 -0
- package/src/newops/psbtFinalizer.ts +26 -1
- package/src/newops/psbtv2.ts +34 -14
- package/tests/Btc.test.ts +89 -0
- package/tests/newops/BtcNew.test.ts +48 -21
- package/tests/newops/integrationtools.ts +47 -36
- package/tests/newops/testtx.ts +0 -55
package/src/newops/psbtv2.ts
CHANGED
|
@@ -14,6 +14,7 @@ export enum psbtIn {
|
|
|
14
14
|
NON_WITNESS_UTXO = 0x00,
|
|
15
15
|
WITNESS_UTXO = 0x01,
|
|
16
16
|
PARTIAL_SIG = 0x02,
|
|
17
|
+
SIGHASH_TYPE = 0x03,
|
|
17
18
|
REDEEM_SCRIPT = 0x04,
|
|
18
19
|
BIP32_DERIVATION = 0x06,
|
|
19
20
|
FINAL_SCRIPTSIG = 0x07,
|
|
@@ -36,6 +37,24 @@ const PSBT_MAGIC_BYTES = Buffer.of(0x70, 0x73, 0x62, 0x74, 0xff);
|
|
|
36
37
|
|
|
37
38
|
export class NoSuchEntry extends Error {}
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Implements Partially Signed Bitcoin Transaction version 2, BIP370, as
|
|
42
|
+
* documented at https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki
|
|
43
|
+
* and https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
|
44
|
+
*
|
|
45
|
+
* A psbt is a data structure that can carry all relevant information about a
|
|
46
|
+
* transaction through all stages of the signing process. From constructing an
|
|
47
|
+
* unsigned transaction to extracting the final serialized transaction ready for
|
|
48
|
+
* broadcast.
|
|
49
|
+
*
|
|
50
|
+
* This implementation is limited to what's needed in ledgerjs to carry out its
|
|
51
|
+
* duties, which means that support for features like multisig or taproot script
|
|
52
|
+
* path spending are not implemented. Specifically, it supports p2pkh,
|
|
53
|
+
* p2wpkhWrappedInP2sh, p2wpkh and p2tr key path spending.
|
|
54
|
+
*
|
|
55
|
+
* This class is made purposefully dumb, so it's easy to add support for
|
|
56
|
+
* complemantary fields as needed in the future.
|
|
57
|
+
*/
|
|
39
58
|
export class PsbtV2 {
|
|
40
59
|
protected globalMap: Map<string, Buffer> = new Map();
|
|
41
60
|
protected inputMaps: Map<string, Buffer>[] = [];
|
|
@@ -110,6 +129,14 @@ export class PsbtV2 {
|
|
|
110
129
|
getInputPartialSig(inputIndex: number, pubkey: Buffer): Buffer | undefined {
|
|
111
130
|
return this.getInputOptional(inputIndex, psbtIn.PARTIAL_SIG, pubkey);
|
|
112
131
|
}
|
|
132
|
+
setInputSighashType(inputIndex: number, sigHashtype: number) {
|
|
133
|
+
this.setInput(inputIndex, psbtIn.SIGHASH_TYPE, b(), uint32LE(sigHashtype));
|
|
134
|
+
}
|
|
135
|
+
getInputSighashType(inputIndex: number): number | undefined {
|
|
136
|
+
const result = this.getInputOptional(inputIndex, psbtIn.SIGHASH_TYPE, b());
|
|
137
|
+
if (!result) return undefined;
|
|
138
|
+
return result.readUInt32LE(0);
|
|
139
|
+
}
|
|
113
140
|
setInputRedeemScript(inputIndex: number, redeemScript: Buffer) {
|
|
114
141
|
this.setInput(inputIndex, psbtIn.REDEEM_SCRIPT, b(), redeemScript);
|
|
115
142
|
}
|
|
@@ -361,15 +388,6 @@ export class PsbtV2 {
|
|
|
361
388
|
) {
|
|
362
389
|
set(this.getMap(index, this.inputMaps), keyType, keyData, value);
|
|
363
390
|
}
|
|
364
|
-
private getMap(
|
|
365
|
-
index: number,
|
|
366
|
-
maps: Map<string, Buffer>[]
|
|
367
|
-
): Map<string, Buffer> {
|
|
368
|
-
if (maps[index]) {
|
|
369
|
-
return maps[index];
|
|
370
|
-
}
|
|
371
|
-
return (maps[index] = new Map());
|
|
372
|
-
}
|
|
373
391
|
private getInput(index: number, keyType: KeyType, keyData: Buffer): Buffer {
|
|
374
392
|
return get(this.inputMaps[index], keyType, keyData, false)!;
|
|
375
393
|
}
|
|
@@ -391,12 +409,14 @@ export class PsbtV2 {
|
|
|
391
409
|
private getOutput(index: number, keyType: KeyType, keyData: Buffer): Buffer {
|
|
392
410
|
return get(this.outputMaps[index], keyType, keyData, false)!;
|
|
393
411
|
}
|
|
394
|
-
private
|
|
412
|
+
private getMap(
|
|
395
413
|
index: number,
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
414
|
+
maps: Map<string, Buffer>[]
|
|
415
|
+
): Map<string, Buffer> {
|
|
416
|
+
if (maps[index]) {
|
|
417
|
+
return maps[index];
|
|
418
|
+
}
|
|
419
|
+
return (maps[index] = new Map());
|
|
400
420
|
}
|
|
401
421
|
private encodeBip32Derivation(masterFingerprint: Buffer, path: number[]) {
|
|
402
422
|
const buf = new BufferWriter();
|
package/tests/Btc.test.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import Transport from "@ledgerhq/hw-transport";
|
|
1
2
|
import {
|
|
2
3
|
openTransportReplayer,
|
|
3
4
|
RecordStore,
|
|
4
5
|
} from "@ledgerhq/hw-transport-mocker";
|
|
5
6
|
import Btc from "../src/Btc";
|
|
7
|
+
import BtcNew from "../src/BtcNew";
|
|
8
|
+
import BtcOld, { AddressFormat } from "../src/BtcOld";
|
|
9
|
+
import { AppAndVersion, getAppAndVersion } from "../src/getAppAndVersion";
|
|
10
|
+
import { TestingClient } from "./newops/integrationtools";
|
|
6
11
|
|
|
7
12
|
test("btc.getWalletXpub", async () => {
|
|
8
13
|
/*
|
|
@@ -421,3 +426,87 @@ test("signMessage", async () => {
|
|
|
421
426
|
s: "385d83273c9d03c469596292fb354b07d193034f83c2633a4c1f057838e12a5b",
|
|
422
427
|
});
|
|
423
428
|
});
|
|
429
|
+
|
|
430
|
+
function testBackend(s: string): any {
|
|
431
|
+
return async () => {
|
|
432
|
+
return { publicKey: s, bitcoinAddress: "", chainCode: "" };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
class TestBtc extends Btc {
|
|
437
|
+
n: BtcNew;
|
|
438
|
+
o: BtcOld;
|
|
439
|
+
constructor(public tr: Transport) {
|
|
440
|
+
super(tr);
|
|
441
|
+
this.n = new BtcNew(new TestingClient(tr));
|
|
442
|
+
this.n.getWalletPublicKey = testBackend("new");
|
|
443
|
+
this.o = new BtcOld(tr);
|
|
444
|
+
this.o.getWalletPublicKey = testBackend("old");
|
|
445
|
+
}
|
|
446
|
+
protected new(): BtcNew {
|
|
447
|
+
return this.n;
|
|
448
|
+
}
|
|
449
|
+
protected old(): BtcOld {
|
|
450
|
+
return this.o;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// test.each`
|
|
455
|
+
// a | b | expected
|
|
456
|
+
// ${1} | ${1} | ${2}
|
|
457
|
+
// ${1} | ${2} | ${3}
|
|
458
|
+
// ${2} | ${1} | ${3}
|
|
459
|
+
// `('returns $expected when $a is added $c', ({ a, c, expected }) => {
|
|
460
|
+
// expect(a + c).toBe(expected);
|
|
461
|
+
// });
|
|
462
|
+
|
|
463
|
+
test.each`
|
|
464
|
+
app | ver | path | format | display | exp
|
|
465
|
+
${"Bitcoin"} | ${"1.99.99"} | ${"m/44'/0'/1'"} | ${"bech32m"} | ${false} | ${""}
|
|
466
|
+
${"Bitcoin"} | ${"1.99.99"} | ${"m/44'/0'"} | ${"bech32m"} | ${false} | ${""}
|
|
467
|
+
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'"} | ${"bech32m"} | ${false} | ${"new"}
|
|
468
|
+
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'"} | ${"bech32m"} | ${false} | ${"new"}
|
|
469
|
+
${"Bitcoin"} | ${"2.0.0-beta"} | ${"m/84'/1'/0'"} | ${"bech32"} | ${false} | ${"new"}
|
|
470
|
+
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'"} | ${"bech32"} | ${false} | ${"new"}
|
|
471
|
+
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'"} | ${"bech32"} | ${undefined} | ${"old"}
|
|
472
|
+
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'"} | ${"bech32"} | ${true} | ${"new"}
|
|
473
|
+
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'/0/0"} | ${"bech32"} | ${false} | ${"new"}
|
|
474
|
+
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'/1/0"} | ${"bech32"} | ${false} | ${"new"}
|
|
475
|
+
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'/1/0"} | ${"legacy"} | ${false} | ${"new"}
|
|
476
|
+
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'/1/0"} | ${"p2sh"} | ${false} | ${"new"}
|
|
477
|
+
${"Bitcoin"} | ${"2.0.0-alpha1"} | ${"m/44'/0'/1'/2/0"} | ${"bech32"} | ${false} | ${"old"}
|
|
478
|
+
`("dispatch $app $ver $path $format $display to $exp", async ({ app, ver, path, format, display, exp }) => {
|
|
479
|
+
const appName = Buffer.of(app.length)
|
|
480
|
+
.toString("hex")
|
|
481
|
+
.concat(Buffer.from(app, "ascii").toString("hex"));
|
|
482
|
+
const appVersion = Buffer.of(ver.length)
|
|
483
|
+
.toString("hex")
|
|
484
|
+
.concat(Buffer.from(ver, "ascii").toString("hex"));
|
|
485
|
+
const resp = `01${appName}${appVersion}01029000`;
|
|
486
|
+
const tr = await openTransportReplayer(RecordStore.fromString(`=> b001000000\n <= ${resp}`));
|
|
487
|
+
const btc = new TestBtc(tr);
|
|
488
|
+
try {
|
|
489
|
+
const key = await btc.getWalletPublicKey(path, { format: format, verify: display });
|
|
490
|
+
if (exp === "") {
|
|
491
|
+
expect(1).toEqual(0); // Allways fail. Don't know how to do that properly
|
|
492
|
+
}
|
|
493
|
+
expect(key.publicKey).toEqual(exp);
|
|
494
|
+
} catch (e: any) {
|
|
495
|
+
if (exp != "") {
|
|
496
|
+
throw e;
|
|
497
|
+
}
|
|
498
|
+
expect(exp).toEqual("");
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
// test("getWalletPublicKey compatibility for internal hardened keys", async () => {
|
|
503
|
+
// await testDispatch("Bitcoin", "1.99.99", "m/44'/0'/1'", "bech32m", "");
|
|
504
|
+
// await testDispatch("Bitcoin", "1.99.99", "m/44'/0'", "bech32m", "");
|
|
505
|
+
// await testDispatch("Bitcoin", "2.0.0-alpha1", "m/44'/0'/1'", "bech32m", "new");
|
|
506
|
+
// await testDispatch("Bitcoin", "2.0.0-alpha1", "m/44'/0'", "bech32m", "new");
|
|
507
|
+
// await testDispatch("Bitcoin", "2.0.0-alpha1", "m/44'/0'/1'", "bech32", "new");
|
|
508
|
+
// await testDispatch("Bitcoin", "2.0.0-alpha1", "m/44'/0'", "bech32", "old");
|
|
509
|
+
// });
|
|
510
|
+
|
|
511
|
+
async function testDispatch(name: string, version: string, path: string, addressFormat: AddressFormat | undefined, exp: string): Promise<void> {
|
|
512
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
-
/* eslint-disable prettier/prettier */
|
|
3
2
|
import { openTransportReplayer, RecordStore } from "@ledgerhq/hw-transport-mocker";
|
|
4
3
|
import { TransportReplayer } from "@ledgerhq/hw-transport-mocker/lib/openTransportReplayer";
|
|
5
4
|
import ecc from "tiny-secp256k1";
|
|
@@ -10,8 +9,8 @@ import {
|
|
|
10
9
|
WalletPolicy
|
|
11
10
|
} from "../../src/newops/policy";
|
|
12
11
|
import { PsbtV2 } from "../../src/newops/psbtv2";
|
|
13
|
-
import {
|
|
14
|
-
import { CoreTx, p2pkh, p2tr, p2wpkh,
|
|
12
|
+
import { StandardPurpose, addressFormatFromDescriptorTemplate, creatDummyXpub, masterFingerprint, runSignTransaction, TestingClient } from "./integrationtools";
|
|
13
|
+
import { CoreInput, CoreTx, p2pkh, p2tr, p2wpkh, wrappedP2wpkh, wrappedP2wpkhTwoInputs } from "./testtx";
|
|
15
14
|
|
|
16
15
|
test("getWalletPublicKey p2pkh", async () => {
|
|
17
16
|
await testGetWalletPublicKey("m/44'/1'/0'", "pkh(@0)");
|
|
@@ -41,7 +40,7 @@ test("getWalletXpub normal path", async () => {
|
|
|
41
40
|
await testGetWalletXpub("m/44'/0'/0'");
|
|
42
41
|
});
|
|
43
42
|
|
|
44
|
-
function testPaths(type:
|
|
43
|
+
function testPaths(type: StandardPurpose): { ins: string[], out?: string } {
|
|
45
44
|
const basePath = `m/${type}/1'/0'/`;
|
|
46
45
|
const ins = [
|
|
47
46
|
basePath + "0/0",
|
|
@@ -51,48 +50,64 @@ function testPaths(type: AccountType): {ins: string[], out?: string} {
|
|
|
51
50
|
basePath + "0/2",
|
|
52
51
|
basePath + "1/2",
|
|
53
52
|
];
|
|
54
|
-
return {ins};
|
|
53
|
+
return { ins };
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
test("Sign p2pkh", async () => {
|
|
58
57
|
const changePubkey = "037ed58c914720772c59f7a1e7e76fba0ef95d7c5667119798586301519b9ad2cf";
|
|
59
|
-
await runSignTransactionTest(p2pkh,
|
|
58
|
+
await runSignTransactionTest(p2pkh, StandardPurpose.p2pkh, changePubkey);
|
|
60
59
|
});
|
|
61
60
|
test("Sign p2wpkh wrapped", async () => {
|
|
62
61
|
let changePubkey = "03efc6b990c1626d08bd176aab0e545a4f55c627c7ddee878d12bbbc46a126177a";
|
|
63
|
-
await runSignTransactionTest(wrappedP2wpkh,
|
|
62
|
+
await runSignTransactionTest(wrappedP2wpkh, StandardPurpose.p2wpkhInP2sh, changePubkey);
|
|
64
63
|
changePubkey = "031175a985c56e310ce3496a819229b427a2172920fd20b5972dda62758c6def09";
|
|
65
|
-
await runSignTransactionTest(wrappedP2wpkhTwoInputs,
|
|
64
|
+
await runSignTransactionTest(wrappedP2wpkhTwoInputs, StandardPurpose.p2wpkhInP2sh, changePubkey);
|
|
66
65
|
});
|
|
67
66
|
test("Sign p2wpkh", async () => {
|
|
68
|
-
await runSignTransactionTest(p2wpkh,
|
|
69
|
-
await runSignTransactionTest(p2wpkhTwoInputs, AccountType.p2wpkh);
|
|
67
|
+
await runSignTransactionTest(p2wpkh, StandardPurpose.p2wpkh);
|
|
70
68
|
});
|
|
71
69
|
test("Sign p2tr", async () => {
|
|
72
|
-
|
|
70
|
+
// This tx uses locktime, so this test verifies that locktime is propagated to/from
|
|
71
|
+
// the psbt correctly.
|
|
72
|
+
await runSignTransactionTest(p2tr, StandardPurpose.p2tr);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
test("Sign p2tr with sigHashType", async () => {
|
|
76
|
+
const testTx = JSON.parse(JSON.stringify(p2tr));
|
|
77
|
+
testTx.vin.forEach((input: CoreInput, index: number) => {
|
|
78
|
+
// Test SIGHASH_SINGLE | SIGHASH_ANYONECANPAY, 0x83
|
|
79
|
+
const sig = input.txinwitness![0] + "83";
|
|
80
|
+
input.txinwitness = [sig];
|
|
81
|
+
})
|
|
82
|
+
const tx = await runSignTransactionNoVerification(testTx, StandardPurpose.p2tr);
|
|
83
|
+
// The verification of the sighashtype is done in MockClient.signPsbt
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
async function runSignTransactionTest(testTx: CoreTx, accountType: StandardPurpose, changePubkey?: string) {
|
|
87
|
+
const tx = await runSignTransactionNoVerification(testTx, accountType, changePubkey);
|
|
88
|
+
expect(tx).toEqual(testTx.hex);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function runSignTransactionNoVerification(testTx: CoreTx, accountType: StandardPurpose, changePubkey?: string): Promise<string> {
|
|
76
92
|
const [client, transport] = await createClient();
|
|
77
93
|
const accountXpub = "tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT";
|
|
78
94
|
client.mockGetPubkeyResponse(`m/${accountType}/1'/0'`, accountXpub);
|
|
79
95
|
const paths = testPaths(accountType);
|
|
80
96
|
if (changePubkey) {
|
|
81
|
-
paths.out =
|
|
97
|
+
paths.out = `m/${accountType}/1'/0'` + "/1/3";
|
|
82
98
|
client.mockGetPubkeyResponse(paths.out, creatDummyXpub(Buffer.from(changePubkey, "hex")));
|
|
83
99
|
}
|
|
84
100
|
const tx = await runSignTransaction(testTx, paths, client, transport);
|
|
85
|
-
expect(tx).toEqual(testTx.hex);
|
|
86
101
|
await transport.close();
|
|
102
|
+
return tx;
|
|
87
103
|
}
|
|
88
104
|
|
|
89
|
-
|
|
90
105
|
async function testGetWalletXpub(path: string, version = 0x043587cf) {
|
|
91
106
|
const [client] = await createClient();
|
|
92
107
|
const expectedXpub = "tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT";
|
|
93
108
|
client.mockGetPubkeyResponse(path, expectedXpub);
|
|
94
109
|
const btc = new BtcNew(client);
|
|
95
|
-
const result = await btc.getWalletXpub({path: path, xpubVersion: version});
|
|
110
|
+
const result = await btc.getWalletXpub({ path: path, xpubVersion: version });
|
|
96
111
|
expect(result).toEqual(expectedXpub);
|
|
97
112
|
}
|
|
98
113
|
async function testGetWalletPublicKey(
|
|
@@ -119,7 +134,7 @@ async function testGetWalletPublicKey(
|
|
|
119
134
|
|
|
120
135
|
const btcNew = new BtcNew(client);
|
|
121
136
|
const addressFormat = addressFormatFromDescriptorTemplate(expectedDescriptorTemplate);
|
|
122
|
-
const result = await btcNew.getWalletPublicKey(path, {format: addressFormat});
|
|
137
|
+
const result = await btcNew.getWalletPublicKey(path, { format: addressFormat });
|
|
123
138
|
verifyGetWalletPublicKeyResult(result, keyXpub, "testaddress");
|
|
124
139
|
|
|
125
140
|
const resultAccount = await btcNew.getWalletPublicKey(accountPath);
|
|
@@ -167,7 +182,7 @@ class MockClient extends TestingClient {
|
|
|
167
182
|
mockSignPsbt(yieldSigs: Map<number, Buffer>) {
|
|
168
183
|
this.yieldSigs.push(yieldSigs);
|
|
169
184
|
}
|
|
170
|
-
async
|
|
185
|
+
async getExtendedPubkey(display: boolean, pathElements: number[]): Promise<string> {
|
|
171
186
|
const path = pathArrayToString(pathElements);
|
|
172
187
|
const response = this.getPubkeyResponses.get(path);
|
|
173
188
|
if (!response) {
|
|
@@ -195,11 +210,23 @@ class MockClient extends TestingClient {
|
|
|
195
210
|
return masterFingerprint;
|
|
196
211
|
}
|
|
197
212
|
async signPsbt(
|
|
198
|
-
|
|
213
|
+
psbt: PsbtV2,
|
|
199
214
|
_walletPolicy: WalletPolicy,
|
|
200
|
-
_walletHMAC: Buffer | null
|
|
215
|
+
_walletHMAC: Buffer | null,
|
|
201
216
|
): Promise<Map<number, Buffer>> {
|
|
202
|
-
|
|
217
|
+
const sigs = this.yieldSigs.splice(0, 1)[0];
|
|
218
|
+
const sig0 = sigs.get(0)!;
|
|
219
|
+
if (sig0.length == 64) {
|
|
220
|
+
// Taproot may leave out sighash type, which defaults to 0x01 SIGHASH_ALL
|
|
221
|
+
return sigs;
|
|
222
|
+
}
|
|
223
|
+
const sigHashType = sig0.readUInt8(sig0.length - 1);
|
|
224
|
+
if (sigHashType != 0x01) {
|
|
225
|
+
for (let i = 0; i < psbt.getGlobalInputCount(); i++) {
|
|
226
|
+
expect(psbt.getInputSighashType(i)).toEqual(sigHashType);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return sigs;
|
|
203
230
|
}
|
|
204
231
|
private getWalletAddressKey(
|
|
205
232
|
walletPolicy: WalletPolicy,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
-
/* eslint-disable prettier/prettier */
|
|
3
2
|
import Transport from "@ledgerhq/hw-transport";
|
|
4
3
|
import bs58check from "bs58check";
|
|
5
4
|
import Btc from "../../src/Btc";
|
|
@@ -17,19 +16,19 @@ import { CoreInput, CoreTx, spentTxs } from "./testtx";
|
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
export async function runSignTransaction(
|
|
20
|
-
testTx: CoreTx,
|
|
21
|
-
testPaths: {ins: string[], out?: string},
|
|
22
|
-
client: TestingClient,
|
|
19
|
+
testTx: CoreTx,
|
|
20
|
+
testPaths: { ins: string[], out?: string },
|
|
21
|
+
client: TestingClient,
|
|
23
22
|
transport: Transport): Promise<string> {
|
|
24
23
|
const btcNew = new BtcNew(client);
|
|
25
24
|
// btc is needed to perform some functions like splitTransaction.
|
|
26
25
|
const btc = new Btc(transport);
|
|
27
26
|
const accountType = getAccountType(testTx.vin[0], btc);
|
|
28
27
|
const additionals: string[] = [];
|
|
29
|
-
if (accountType ==
|
|
28
|
+
if (accountType == StandardPurpose.p2wpkh) {
|
|
30
29
|
additionals.push("bech32");
|
|
31
30
|
}
|
|
32
|
-
if (accountType ==
|
|
31
|
+
if (accountType == StandardPurpose.p2tr) {
|
|
33
32
|
additionals.push("bech32m");
|
|
34
33
|
}
|
|
35
34
|
const associatedKeysets: string[] = [];
|
|
@@ -44,15 +43,23 @@ export async function runSignTransaction(
|
|
|
44
43
|
yieldSigs.set(index, getSignature(input, accountType));
|
|
45
44
|
return inputData;
|
|
46
45
|
});
|
|
46
|
+
const sig0 = yieldSigs.get(0)!;
|
|
47
|
+
let sigHashType: number | undefined = sig0.readUInt8(sig0.length - 1)
|
|
48
|
+
if (sigHashType == 0x01) {
|
|
49
|
+
sigHashType = undefined;
|
|
50
|
+
}
|
|
47
51
|
client.mockSignPsbt(yieldSigs);
|
|
48
52
|
const outputWriter = new BufferWriter();
|
|
49
53
|
outputWriter.writeVarInt(testTx.vout.length);
|
|
50
54
|
testTx.vout.forEach(output => {
|
|
51
55
|
outputWriter.writeUInt64(BigInt(Number.parseFloat((output.value * 100000000).toFixed(8))));
|
|
52
|
-
outputWriter.writeVarSlice(Buffer.from(output.scriptPubKey.hex, "hex"));
|
|
56
|
+
outputWriter.writeVarSlice(Buffer.from(output.scriptPubKey.hex, "hex"));
|
|
53
57
|
});
|
|
54
58
|
const outputScriptHex = outputWriter.buffer().toString("hex");
|
|
55
|
-
|
|
59
|
+
let callbacks = "";
|
|
60
|
+
function logCallback(message: string) {
|
|
61
|
+
callbacks += new Date().toISOString() + " " + message + "\n";
|
|
62
|
+
}
|
|
56
63
|
const arg: CreateTransactionArg = {
|
|
57
64
|
inputs,
|
|
58
65
|
additionals,
|
|
@@ -60,12 +67,21 @@ export async function runSignTransaction(
|
|
|
60
67
|
changePath: testPaths.out,
|
|
61
68
|
outputScriptHex,
|
|
62
69
|
lockTime: testTx.locktime,
|
|
63
|
-
|
|
70
|
+
sigHashType,
|
|
71
|
+
segwit: accountType != StandardPurpose.p2pkh,
|
|
72
|
+
onDeviceSignatureGranted: () => logCallback("CALLBACK: signature granted"),
|
|
73
|
+
onDeviceSignatureRequested: () => logCallback("CALLBACK: signature requested"),
|
|
74
|
+
onDeviceStreaming: (arg) => logCallback("CALLBACK: " + JSON.stringify(arg))
|
|
64
75
|
};
|
|
76
|
+
logCallback("Start createPaymentTransactionNew");
|
|
65
77
|
const tx = await btcNew.createPaymentTransactionNew(arg);
|
|
78
|
+
logCallback("Done createPaymentTransactionNew");
|
|
79
|
+
// console.log(callbacks);
|
|
66
80
|
return tx;
|
|
67
81
|
};
|
|
68
82
|
|
|
83
|
+
|
|
84
|
+
|
|
69
85
|
export function addressFormatFromDescriptorTemplate(descTemp: DefaultDescriptorTemplate): AddressFormat {
|
|
70
86
|
if (descTemp == "tr(@0)") return "bech32m";
|
|
71
87
|
if (descTemp == "pkh(@0)") return "legacy";
|
|
@@ -74,42 +90,42 @@ export function addressFormatFromDescriptorTemplate(descTemp: DefaultDescriptorT
|
|
|
74
90
|
throw new Error();
|
|
75
91
|
}
|
|
76
92
|
|
|
77
|
-
export enum
|
|
93
|
+
export enum StandardPurpose {
|
|
78
94
|
p2tr = "86'",
|
|
79
95
|
p2wpkh = "84'",
|
|
80
96
|
p2wpkhInP2sh = "49'",
|
|
81
97
|
p2pkh = "44'"
|
|
82
98
|
}
|
|
83
99
|
|
|
84
|
-
function getPubkey(inputIndex: number, accountType:
|
|
100
|
+
function getPubkey(inputIndex: number, accountType: StandardPurpose, testTx: CoreTx, spentTx: Transaction, spentOutputIndex: number): Buffer {
|
|
85
101
|
const scriptSig = Buffer.from(testTx.vin[inputIndex].scriptSig.hex, "hex");
|
|
86
|
-
if (accountType ==
|
|
87
|
-
return scriptSig.slice(scriptSig.length-33);
|
|
102
|
+
if (accountType == StandardPurpose.p2pkh) {
|
|
103
|
+
return scriptSig.slice(scriptSig.length - 33);
|
|
88
104
|
}
|
|
89
|
-
if (accountType ==
|
|
105
|
+
if (accountType == StandardPurpose.p2tr) {
|
|
90
106
|
return spentTx.outputs![spentOutputIndex].script.slice(2, 34); // 32 bytes x-only pubkey
|
|
91
107
|
}
|
|
92
|
-
if (accountType ==
|
|
108
|
+
if (accountType == StandardPurpose.p2wpkh || accountType == StandardPurpose.p2wpkhInP2sh) {
|
|
93
109
|
return Buffer.from(testTx.vin[inputIndex].txinwitness![1], "hex");
|
|
94
110
|
}
|
|
95
111
|
throw new Error();
|
|
96
112
|
}
|
|
97
113
|
|
|
98
|
-
function getSignature(testTxInput: CoreInput, accountType:
|
|
114
|
+
function getSignature(testTxInput: CoreInput, accountType: StandardPurpose): Buffer {
|
|
99
115
|
const scriptSig = Buffer.from(testTxInput.scriptSig.hex, "hex");
|
|
100
|
-
if (accountType ==
|
|
101
|
-
return scriptSig.slice(1, scriptSig.length-34);
|
|
116
|
+
if (accountType == StandardPurpose.p2pkh) {
|
|
117
|
+
return scriptSig.slice(1, scriptSig.length - 34);
|
|
102
118
|
}
|
|
103
|
-
if (accountType ==
|
|
119
|
+
if (accountType == StandardPurpose.p2tr) {
|
|
104
120
|
return Buffer.from(testTxInput.txinwitness![0], "hex");
|
|
105
121
|
}
|
|
106
|
-
if (accountType ==
|
|
122
|
+
if (accountType == StandardPurpose.p2wpkh || accountType == StandardPurpose.p2wpkhInP2sh) {
|
|
107
123
|
return Buffer.from(testTxInput.txinwitness![0], "hex");
|
|
108
124
|
}
|
|
109
125
|
throw new Error();
|
|
110
126
|
}
|
|
111
127
|
|
|
112
|
-
function getAccountType(coreInput: CoreInput, btc: Btc):
|
|
128
|
+
function getAccountType(coreInput: CoreInput, btc: Btc): StandardPurpose {
|
|
113
129
|
const spentTx = spentTxs[coreInput.txid];
|
|
114
130
|
if (!spentTx) {
|
|
115
131
|
throw new Error("Spent tx " + coreInput.txid + " unavailable.");
|
|
@@ -118,46 +134,41 @@ function getAccountType(coreInput: CoreInput, btc: Btc): AccountType {
|
|
|
118
134
|
const spentOutput = splitSpentTx.outputs![coreInput.vout];
|
|
119
135
|
const script = spentOutput.script;
|
|
120
136
|
if (script.length == 34 && script[0] == 0x51) {
|
|
121
|
-
return
|
|
137
|
+
return StandardPurpose.p2tr;
|
|
122
138
|
}
|
|
123
139
|
if (script.length == 22 && script[0] == 0x00) {
|
|
124
|
-
return
|
|
140
|
+
return StandardPurpose.p2wpkh;
|
|
125
141
|
}
|
|
126
142
|
if (script.length == 23) {
|
|
127
|
-
return
|
|
143
|
+
return StandardPurpose.p2wpkhInP2sh;
|
|
128
144
|
}
|
|
129
|
-
return
|
|
145
|
+
return StandardPurpose.p2pkh;
|
|
130
146
|
}
|
|
131
147
|
|
|
132
148
|
export function creatDummyXpub(pubkey: Buffer): string {
|
|
133
149
|
const xpubDecoded = bs58check.decode("tpubDHcN44A4UHqdHJZwBxgTbu8Cy87ZrZkN8tQnmJGhcijHqe4rztuvGcD4wo36XSviLmiqL5fUbDnekYaQ7LzAnaqauBb9RsyahsTTFHdeJGd");
|
|
134
150
|
const pubkey33 = pubkey.length == 33 ? pubkey : Buffer.concat([Buffer.of(2), pubkey]);
|
|
135
|
-
xpubDecoded.fill(pubkey33, xpubDecoded.length-33);
|
|
151
|
+
xpubDecoded.fill(pubkey33, xpubDecoded.length - 33);
|
|
136
152
|
return bs58check.encode(xpubDecoded);
|
|
137
153
|
}
|
|
138
154
|
|
|
139
|
-
function createInput(coreInput: CoreInput, btc: Btc): [Transaction, number, string, number] {
|
|
155
|
+
function createInput(coreInput: CoreInput, btc: Btc): [Transaction, number, string | null, number] {
|
|
140
156
|
const spentTx = spentTxs[coreInput.txid];
|
|
141
157
|
if (!spentTx) {
|
|
142
158
|
throw new Error("Spent tx " + coreInput.txid + " unavailable.");
|
|
143
159
|
}
|
|
144
160
|
const splitSpentTx = btc.splitTransaction(spentTx, true);
|
|
145
|
-
|
|
146
|
-
let redeemScript;
|
|
147
|
-
if (scriptSig?.hex && scriptSig.hex.startsWith("160014")) {
|
|
148
|
-
redeemScript = scriptSig.hex.substring(2);
|
|
149
|
-
}
|
|
150
|
-
return [splitSpentTx, coreInput.vout, redeemScript, coreInput.sequence];
|
|
161
|
+
return [splitSpentTx, coreInput.vout, null, coreInput.sequence];
|
|
151
162
|
}
|
|
152
163
|
|
|
153
164
|
export const masterFingerprint = Buffer.of(1, 2, 3, 4);
|
|
154
165
|
export class TestingClient extends AppClient {
|
|
155
|
-
mockGetPubkeyResponse(_pathElements: string, _response: string): void {};
|
|
166
|
+
mockGetPubkeyResponse(_pathElements: string, _response: string): void { };
|
|
156
167
|
mockGetWalletAddressResponse(
|
|
157
168
|
_walletPolicy: WalletPolicy,
|
|
158
169
|
_change: number,
|
|
159
170
|
_addressIndex: number,
|
|
160
171
|
_response: string
|
|
161
|
-
): void {};
|
|
162
|
-
mockSignPsbt(_yieldSigs: Map<number, Buffer>): void {};
|
|
172
|
+
): void { };
|
|
173
|
+
mockSignPsbt(_yieldSigs: Map<number, Buffer>): void { };
|
|
163
174
|
}
|
package/tests/newops/testtx.ts
CHANGED
|
@@ -237,61 +237,6 @@ export const p2pkh: CoreTx = {
|
|
|
237
237
|
"blocktime": 1633611385
|
|
238
238
|
};
|
|
239
239
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
240
|
-
export const p2wpkhTwoInputs = {
|
|
241
|
-
"txid": "1913b7b5ffdcb5f32b9aca1f5eec2a189e7c66650f82b560eae211265fc995b7",
|
|
242
|
-
"hash": "c3439dcd3489373c586c7aed48c32f2b5d9c71aad24acd765a61684d98690a3f",
|
|
243
|
-
"version": 2,
|
|
244
|
-
"size": 388,
|
|
245
|
-
"vsize": 226,
|
|
246
|
-
"weight": 904,
|
|
247
|
-
"locktime": 0,
|
|
248
|
-
"vin": [
|
|
249
|
-
{
|
|
250
|
-
"txid": "5512d5788d4c26117f093de91223ef384c3fb22799810a92e3304bb6f0819224",
|
|
251
|
-
"vout": 1,
|
|
252
|
-
"scriptSig": {
|
|
253
|
-
"asm": "0014c1ac0d63d0258ea1b6fe90ef72d0c35d8d773dd3",
|
|
254
|
-
"hex": "160014c1ac0d63d0258ea1b6fe90ef72d0c35d8d773dd3"
|
|
255
|
-
},
|
|
256
|
-
"txinwitness": [
|
|
257
|
-
"30440220543617c5f4504dc29d34d2d06d0d7733dac4ec418b77c67feefb29f3f82ba3d80220690b784c52c3375f4ba9e64cc5c0aeb6a1b9fc6aadda0062905c06ce3bbba57501",
|
|
258
|
-
"02fb255ed920db5c2f507289202eb60a160e5a067ee7e30199a4ed81b74c22e441"
|
|
259
|
-
],
|
|
260
|
-
"sequence": 4294967295
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
"txid": "28ad5054e029252d72da37f13fce66212d7f7763845b4a8c4aaf78e897b2bf9f",
|
|
264
|
-
"vout": 1,
|
|
265
|
-
"scriptSig": {
|
|
266
|
-
"asm": "0014c1ac0d63d0258ea1b6fe90ef72d0c35d8d773dd3",
|
|
267
|
-
"hex": "160014c1ac0d63d0258ea1b6fe90ef72d0c35d8d773dd3"
|
|
268
|
-
},
|
|
269
|
-
"txinwitness": [
|
|
270
|
-
"3044022049e7f3015a33ccdb015fe3891667564fd37111272df57e58447645c7bad8fed0022074d1e93ba946453896d0f0bc500df3a1e0d5bb5ad10cd9906736d5fbaebadd5801",
|
|
271
|
-
"02fb255ed920db5c2f507289202eb60a160e5a067ee7e30199a4ed81b74c22e441"
|
|
272
|
-
],
|
|
273
|
-
"sequence": 4294967295
|
|
274
|
-
}
|
|
275
|
-
],
|
|
276
|
-
"vout": [
|
|
277
|
-
{
|
|
278
|
-
"value": 0.01800000,
|
|
279
|
-
"n": 0,
|
|
280
|
-
"scriptPubKey": {
|
|
281
|
-
"asm": "OP_DUP OP_HASH160 f73384bcc3951ab6a75541ff79a9a51f82056ed8 OP_EQUALVERIFY OP_CHECKSIG",
|
|
282
|
-
"hex": "76a914f73384bcc3951ab6a75541ff79a9a51f82056ed888ac",
|
|
283
|
-
"address": "n442v1DrXQNim9gjjctKjyGVoe717hNdtG",
|
|
284
|
-
"type": "pubkeyhash"
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
],
|
|
288
|
-
"hex": "02000000000102249281f0b64b30e3920a819927b23f4c38ef2312e93d097f11264c8d78d512550100000017160014c1ac0d63d0258ea1b6fe90ef72d0c35d8d773dd3ffffffff9fbfb297e878af4a8c4a5b8463777f2d2166ce3ff137da722d2529e05450ad280100000017160014c1ac0d63d0258ea1b6fe90ef72d0c35d8d773dd3ffffffff0140771b00000000001976a914f73384bcc3951ab6a75541ff79a9a51f82056ed888ac024730440220543617c5f4504dc29d34d2d06d0d7733dac4ec418b77c67feefb29f3f82ba3d80220690b784c52c3375f4ba9e64cc5c0aeb6a1b9fc6aadda0062905c06ce3bbba575012102fb255ed920db5c2f507289202eb60a160e5a067ee7e30199a4ed81b74c22e44102473044022049e7f3015a33ccdb015fe3891667564fd37111272df57e58447645c7bad8fed0022074d1e93ba946453896d0f0bc500df3a1e0d5bb5ad10cd9906736d5fbaebadd58012102fb255ed920db5c2f507289202eb60a160e5a067ee7e30199a4ed81b74c22e44100000000",
|
|
289
|
-
"blockhash": "00000000025a711e6cd4bce9138dc852232a4494afbf36d8bb80499a786da2a4",
|
|
290
|
-
"confirmations": 1,
|
|
291
|
-
"time": 1633944124,
|
|
292
|
-
"blocktime": 1633944124
|
|
293
|
-
};
|
|
294
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
295
240
|
export const wrappedP2wpkhTwoInputs = {
|
|
296
241
|
"txid": "c03119b538c78f56c8ce2e6cc5fc6998d447eeef42e34c12692764a3f1a3da7c",
|
|
297
242
|
"hash": "6b3812304554a6964e43a6971ac533046f4be101e39609f72179856916e20268",
|