@ledgerhq/hw-app-btc 10.16.0-nightly.20260116024452 → 10.16.0-nightly.20260116124336

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.
Files changed (120) hide show
  1. package/CHANGELOG.md +5 -3
  2. package/README.md +106 -56
  3. package/lib/Btc.d.ts +37 -0
  4. package/lib/Btc.d.ts.map +1 -1
  5. package/lib/Btc.js +30 -2
  6. package/lib/Btc.js.map +1 -1
  7. package/lib/BtcNew.d.ts +84 -0
  8. package/lib/BtcNew.d.ts.map +1 -1
  9. package/lib/BtcNew.js +326 -9
  10. package/lib/BtcNew.js.map +1 -1
  11. package/lib/createTransaction.d.ts.map +1 -1
  12. package/lib/createTransaction.js +3 -2
  13. package/lib/createTransaction.js.map +1 -1
  14. package/lib/getTrustedInputBIP143.d.ts +1 -2
  15. package/lib/getTrustedInputBIP143.d.ts.map +1 -1
  16. package/lib/getTrustedInputBIP143.js +1 -1
  17. package/lib/getTrustedInputBIP143.js.map +1 -1
  18. package/lib/newops/accounttype.d.ts +3 -3
  19. package/lib/newops/accounttype.d.ts.map +1 -1
  20. package/lib/newops/accounttype.js +15 -14
  21. package/lib/newops/accounttype.js.map +1 -1
  22. package/lib/newops/appClient.d.ts +1 -1
  23. package/lib/newops/appClient.d.ts.map +1 -1
  24. package/lib/newops/clientCommands.js +2 -2
  25. package/lib/newops/clientCommands.js.map +1 -1
  26. package/lib/newops/merkelizedPsbt.d.ts +1 -1
  27. package/lib/newops/merkelizedPsbt.d.ts.map +1 -1
  28. package/lib/newops/merkelizedPsbt.js +1 -1
  29. package/lib/newops/merkelizedPsbt.js.map +1 -1
  30. package/lib/newops/policy.js +2 -2
  31. package/lib/newops/policy.js.map +1 -1
  32. package/lib/newops/psbtExtractor.d.ts +1 -1
  33. package/lib/newops/psbtExtractor.d.ts.map +1 -1
  34. package/lib/newops/psbtExtractor.js +3 -3
  35. package/lib/newops/psbtExtractor.js.map +1 -1
  36. package/lib/newops/psbtFinalizer.d.ts +1 -1
  37. package/lib/newops/psbtFinalizer.d.ts.map +1 -1
  38. package/lib/newops/psbtFinalizer.js +5 -6
  39. package/lib/newops/psbtFinalizer.js.map +1 -1
  40. package/lib/signP2SHTransaction.d.ts.map +1 -1
  41. package/lib/signP2SHTransaction.js +3 -2
  42. package/lib/signP2SHTransaction.js.map +1 -1
  43. package/lib-es/Btc.d.ts +37 -0
  44. package/lib-es/Btc.d.ts.map +1 -1
  45. package/lib-es/Btc.js +30 -2
  46. package/lib-es/Btc.js.map +1 -1
  47. package/lib-es/BtcNew.d.ts +84 -0
  48. package/lib-es/BtcNew.d.ts.map +1 -1
  49. package/lib-es/BtcNew.js +325 -8
  50. package/lib-es/BtcNew.js.map +1 -1
  51. package/lib-es/createTransaction.d.ts.map +1 -1
  52. package/lib-es/createTransaction.js +3 -2
  53. package/lib-es/createTransaction.js.map +1 -1
  54. package/lib-es/getTrustedInputBIP143.d.ts +1 -2
  55. package/lib-es/getTrustedInputBIP143.d.ts.map +1 -1
  56. package/lib-es/getTrustedInputBIP143.js +1 -1
  57. package/lib-es/getTrustedInputBIP143.js.map +1 -1
  58. package/lib-es/newops/accounttype.d.ts +3 -3
  59. package/lib-es/newops/accounttype.d.ts.map +1 -1
  60. package/lib-es/newops/accounttype.js +11 -10
  61. package/lib-es/newops/accounttype.js.map +1 -1
  62. package/lib-es/newops/appClient.d.ts +1 -1
  63. package/lib-es/newops/appClient.d.ts.map +1 -1
  64. package/lib-es/newops/clientCommands.js +1 -1
  65. package/lib-es/newops/clientCommands.js.map +1 -1
  66. package/lib-es/newops/merkelizedPsbt.d.ts +1 -1
  67. package/lib-es/newops/merkelizedPsbt.d.ts.map +1 -1
  68. package/lib-es/newops/merkelizedPsbt.js +1 -1
  69. package/lib-es/newops/merkelizedPsbt.js.map +1 -1
  70. package/lib-es/newops/policy.js +1 -1
  71. package/lib-es/newops/policy.js.map +1 -1
  72. package/lib-es/newops/psbtExtractor.d.ts +1 -1
  73. package/lib-es/newops/psbtExtractor.d.ts.map +1 -1
  74. package/lib-es/newops/psbtExtractor.js +1 -1
  75. package/lib-es/newops/psbtExtractor.js.map +1 -1
  76. package/lib-es/newops/psbtFinalizer.d.ts +1 -1
  77. package/lib-es/newops/psbtFinalizer.d.ts.map +1 -1
  78. package/lib-es/newops/psbtFinalizer.js +1 -2
  79. package/lib-es/newops/psbtFinalizer.js.map +1 -1
  80. package/lib-es/signP2SHTransaction.d.ts.map +1 -1
  81. package/lib-es/signP2SHTransaction.js +3 -2
  82. package/lib-es/signP2SHTransaction.js.map +1 -1
  83. package/package.json +6 -6
  84. package/src/Btc.ts +41 -2
  85. package/src/BtcNew.ts +483 -9
  86. package/src/createTransaction.ts +4 -3
  87. package/src/getTrustedInputBIP143.ts +0 -2
  88. package/src/newops/accounttype.ts +11 -12
  89. package/src/newops/appClient.ts +1 -1
  90. package/src/newops/clientCommands.ts +1 -1
  91. package/src/newops/merkelizedPsbt.ts +1 -1
  92. package/src/newops/policy.ts +1 -1
  93. package/src/newops/psbtExtractor.ts +1 -2
  94. package/src/newops/psbtFinalizer.ts +1 -2
  95. package/src/signP2SHTransaction.ts +3 -2
  96. package/tests/Btc.test.ts +848 -20
  97. package/tests/newops/BtcNew.signMessage.test.ts +35 -0
  98. package/tests/newops/BtcNew.signPsbtBuffer.test.ts +391 -0
  99. package/tests/newops/BtcNew.test.ts +13 -1
  100. package/tests/newops/integrationtools.ts +1 -1
  101. package/lib/buffertools.d.ts +0 -31
  102. package/lib/buffertools.d.ts.map +0 -1
  103. package/lib/buffertools.js +0 -129
  104. package/lib/buffertools.js.map +0 -1
  105. package/lib/newops/psbtv2.d.ts +0 -150
  106. package/lib/newops/psbtv2.d.ts.map +0 -1
  107. package/lib/newops/psbtv2.js +0 -469
  108. package/lib/newops/psbtv2.js.map +0 -1
  109. package/lib-es/buffertools.d.ts +0 -31
  110. package/lib-es/buffertools.d.ts.map +0 -1
  111. package/lib-es/buffertools.js +0 -119
  112. package/lib-es/buffertools.js.map +0 -1
  113. package/lib-es/newops/psbtv2.d.ts +0 -150
  114. package/lib-es/newops/psbtv2.d.ts.map +0 -1
  115. package/lib-es/newops/psbtv2.js +0 -464
  116. package/lib-es/newops/psbtv2.js.map +0 -1
  117. package/src/buffertools.ts +0 -137
  118. package/src/newops/psbtv2.ts +0 -525
  119. package/tests/buffertools.test.ts +0 -25
  120. package/tests/newops/psbtv2.test.ts +0 -15
package/src/BtcNew.ts CHANGED
@@ -1,11 +1,5 @@
1
1
  import { crypto } from "bitcoinjs-lib";
2
2
  import { secp256k1 } from "@noble/curves/secp256k1";
3
-
4
- // Replacement for pointCompress from tiny-secp256k1
5
- function pointCompress(point: Uint8Array, compressed = true): Uint8Array {
6
- const p = secp256k1.ProjectivePoint.fromHex(point);
7
- return p.toRawBytes(compressed);
8
- }
9
3
  import {
10
4
  getXpubComponents,
11
5
  hardenedPathOf,
@@ -13,7 +7,7 @@ import {
13
7
  pathStringToArray,
14
8
  pubkeyFromXpub,
15
9
  } from "./bip32";
16
- import { BufferReader } from "./buffertools";
10
+ import { BufferReader, psbtIn, PsbtV2 } from "@ledgerhq/psbtv2";
17
11
  import type { CreateTransactionArg } from "./createTransaction";
18
12
  import type { AddressFormat } from "./getWalletPublicKey";
19
13
  import {
@@ -28,10 +22,15 @@ import { AppClient as Client } from "./newops/appClient";
28
22
  import { createKey, DefaultDescriptorTemplate, WalletPolicy } from "./newops/policy";
29
23
  import { extract } from "./newops/psbtExtractor";
30
24
  import { finalize } from "./newops/psbtFinalizer";
31
- import { psbtIn, PsbtV2 } from "./newops/psbtv2";
32
25
  import { serializeTransaction } from "./serializeTransaction";
33
26
  import type { Transaction } from "./types";
34
27
 
28
+ // Replacement for pointCompress from tiny-secp256k1
29
+ function pointCompress(point: Uint8Array, compressed = true): Uint8Array {
30
+ const p = secp256k1.ProjectivePoint.fromHex(point);
31
+ return p.toRawBytes(compressed);
32
+ }
33
+
35
34
  /**
36
35
  * @class BtcNew
37
36
  * @description This class implements the same interface as BtcOld (formerly
@@ -282,7 +281,7 @@ export default class BtcNew {
282
281
  const progressCallback = () => {
283
282
  if (!firstSigned) {
284
283
  firstSigned = true;
285
- arg.onDeviceSignatureGranted && arg.onDeviceSignatureGranted();
284
+ if (arg.onDeviceSignatureGranted) arg.onDeviceSignatureGranted();
286
285
  }
287
286
  progress();
288
287
  };
@@ -293,6 +292,481 @@ export default class BtcNew {
293
292
  return serializedTx.toString("hex");
294
293
  }
295
294
 
295
+ /**
296
+ * Signs a PSBT buffer using the Bitcoin app (new protocol).
297
+ *
298
+ * - If the PSBT is v2, it is deserialized directly.
299
+ * - If the PSBT is v0, it is converted to v2 internally.
300
+ * - The account type (legacy, wrapped segwit, native segwit, taproot) is
301
+ * inferred from PSBT data when possible, or from the provided options.
302
+ *
303
+ * Note: All internal inputs (inputs that can be signed by the device) must
304
+ * belong to the same account and use the same account type. Mixed input types
305
+ * or inputs from different accounts are not supported and will throw an error.
306
+ *
307
+ * @param psbtBuffer - Raw PSBT buffer (v0 or v2) to be signed.
308
+ * @param options - Optional signing configuration.
309
+ * @param options.finalizePsbt - Whether to finalize the PSBT after signing
310
+ * (default: true). If true, the returned `tx` is a fully signed
311
+ * transaction ready for broadcast.
312
+ * @param options.accountPath - BIP32 account path (for example,
313
+ * "m/84'/0'/0'") used when BIP32 derivation information is missing from
314
+ * the PSBT. Required if the PSBT does not contain BIP32 derivation data.
315
+ * @param options.addressFormat - Explicit address format to use when the
316
+ * account type cannot be inferred from the PSBT ("legacy", "p2sh",
317
+ * "bech32", or "bech32m").
318
+ * @param options.onDeviceSignatureRequested - Callback when signature is about to be requested from device.
319
+ * @param options.onDeviceSignatureGranted - Callback when the first signature is granted by device.
320
+ * @param options.onDeviceStreaming - Callback to track signing progress with index and total.
321
+ *
322
+ * @returns An object containing:
323
+ * - `psbt`: a non-finalized PSBT buffer including signatures.
324
+ * - `tx`: the fully signed transaction hex string (if `finalizePsbt` is
325
+ * true), or the hex of the transaction that would be extracted after
326
+ * finalization.
327
+ */
328
+ async signPsbtBuffer(
329
+ psbtBuffer: Buffer,
330
+ options?: {
331
+ finalizePsbt?: boolean;
332
+ accountPath?: string;
333
+ addressFormat?: AddressFormat;
334
+ onDeviceSignatureRequested?: () => void;
335
+ onDeviceSignatureGranted?: () => void;
336
+ onDeviceStreaming?: (arg: { progress: number; total: number; index: number }) => void;
337
+ },
338
+ ) {
339
+ const psbt = this.deserializePsbt(psbtBuffer);
340
+ const inputCount = psbt.getGlobalInputCount();
341
+
342
+ if (inputCount === 0) {
343
+ throw new Error("No inputs in PSBT");
344
+ }
345
+
346
+ const masterFp = await this.client.getMasterFingerprint();
347
+ const { accountPath, detectedScriptType, internalInputIndices } = this.analyzeAllInputs(
348
+ psbt,
349
+ inputCount,
350
+ masterFp,
351
+ options?.accountPath,
352
+ );
353
+
354
+ const accountXpub = await this.client.getExtendedPubkey(false, accountPath);
355
+ const referenceInputIndex = internalInputIndices.length > 0 ? internalInputIndices[0] : 0;
356
+
357
+ const accountType = this.determineAccountType(
358
+ psbt,
359
+ referenceInputIndex,
360
+ masterFp,
361
+ detectedScriptType,
362
+ accountPath,
363
+ options?.addressFormat,
364
+ );
365
+
366
+ const walletPolicy = this.createWalletPolicy(masterFp, accountPath, accountXpub, accountType);
367
+ const progressCallback = this.createProgressCallback(inputCount, options);
368
+
369
+ await this.signPsbt(psbt, walletPolicy, progressCallback);
370
+
371
+ return this.finalizePsbtAndExtract(psbt, options?.finalizePsbt);
372
+ }
373
+
374
+ private deserializePsbt(psbtBuffer: Buffer): PsbtV2 {
375
+ const psbtVersion = PsbtV2.getPsbtVersionNumber(psbtBuffer);
376
+ const psbt = psbtVersion === 2 ? new PsbtV2() : PsbtV2.fromV0(psbtBuffer, true);
377
+
378
+ if (psbtVersion === 2) {
379
+ psbt.deserialize(psbtBuffer);
380
+ }
381
+
382
+ return psbt;
383
+ }
384
+
385
+ private analyzeAllInputs(
386
+ psbt: PsbtV2,
387
+ inputCount: number,
388
+ masterFp: Buffer,
389
+ accountPathOption?: string,
390
+ ): {
391
+ accountPath: number[];
392
+ detectedScriptType: string | undefined;
393
+ internalInputIndices: number[];
394
+ } {
395
+ const internalInputIndices: number[] = [];
396
+ let accountPath: number[] = [];
397
+ let detectedScriptType: string | undefined;
398
+
399
+ for (let i = 0; i < inputCount; i++) {
400
+ const inputInfo = this.analyzeInput(psbt, i, masterFp);
401
+
402
+ if (!inputInfo.isInternal) {
403
+ continue;
404
+ }
405
+
406
+ internalInputIndices.push(i);
407
+ this.validateAccountPathConsistency(accountPath, inputInfo.accountPath, i);
408
+
409
+ if (accountPath.length === 0) {
410
+ accountPath = inputInfo.accountPath;
411
+ }
412
+
413
+ this.validateScriptTypeConsistency(detectedScriptType, inputInfo.scriptType, i);
414
+
415
+ if (!detectedScriptType) {
416
+ detectedScriptType = inputInfo.scriptType;
417
+ }
418
+ }
419
+
420
+ if (internalInputIndices.length === 0) {
421
+ accountPath = this.resolveAccountPathFromOptions(accountPathOption);
422
+ }
423
+
424
+ return { accountPath, detectedScriptType, internalInputIndices };
425
+ }
426
+
427
+ private validateAccountPathConsistency(
428
+ accountPath: number[],
429
+ newAccountPath: number[],
430
+ inputIndex: number,
431
+ ): void {
432
+ if (accountPath.length > 0 && !this.arePathsEqual(accountPath, newAccountPath)) {
433
+ throw new Error(
434
+ `Mixed accounts detected in PSBT. Input ${inputIndex} uses account path ` +
435
+ `${pathArrayToString(newAccountPath)} but expected ` +
436
+ `${pathArrayToString(accountPath)}. All internal inputs must belong to the same account.`,
437
+ );
438
+ }
439
+ }
440
+
441
+ private validateScriptTypeConsistency(
442
+ detectedScriptType: string | undefined,
443
+ newScriptType: string | undefined,
444
+ inputIndex: number,
445
+ ): void {
446
+ if (detectedScriptType && newScriptType && detectedScriptType !== newScriptType) {
447
+ throw new Error(
448
+ `Mixed input types detected in PSBT. Input ${inputIndex} uses ${newScriptType} ` +
449
+ `but expected ${detectedScriptType}. All internal inputs must use the same script type.`,
450
+ );
451
+ }
452
+ }
453
+
454
+ private resolveAccountPathFromOptions(accountPathOption?: string): number[] {
455
+ if (!accountPathOption) {
456
+ throw new Error(
457
+ "No internal inputs found in PSBT (no BIP32 derivation matching device fingerprint) " +
458
+ "and no account path provided in options. Please provide accountPath in options " +
459
+ "(e.g., \"m/84'/0'/0'\" for native segwit)",
460
+ );
461
+ }
462
+ return pathStringToArray(accountPathOption);
463
+ }
464
+
465
+ private createWalletPolicy(
466
+ masterFp: Buffer,
467
+ accountPath: number[],
468
+ accountXpub: string,
469
+ accountType: AccountType,
470
+ ): WalletPolicy {
471
+ const key = createKey(masterFp, accountPath, accountXpub);
472
+ return new WalletPolicy(accountType.getDescriptorTemplate(), key);
473
+ }
474
+
475
+ private createProgressCallback(
476
+ inputCount: number,
477
+ options?: {
478
+ onDeviceSignatureRequested?: () => void;
479
+ onDeviceSignatureGranted?: () => void;
480
+ onDeviceStreaming?: (arg: { progress: number; total: number; index: number }) => void;
481
+ },
482
+ ): () => void {
483
+ let notifyCount = 0;
484
+ let firstSigned = false;
485
+
486
+ const progress = () => {
487
+ if (!options?.onDeviceStreaming) return;
488
+ options.onDeviceStreaming({
489
+ total: 2 * inputCount,
490
+ index: notifyCount,
491
+ progress: ++notifyCount / (2 * inputCount),
492
+ });
493
+ };
494
+
495
+ if (options?.onDeviceSignatureRequested) options.onDeviceSignatureRequested();
496
+
497
+ return () => {
498
+ if (!firstSigned) {
499
+ firstSigned = true;
500
+ if (options?.onDeviceSignatureGranted) options.onDeviceSignatureGranted();
501
+ }
502
+ progress();
503
+ };
504
+ }
505
+
506
+ private finalizePsbtAndExtract(
507
+ psbt: PsbtV2,
508
+ shouldFinalize?: boolean,
509
+ ): { psbt: Buffer; tx: string } {
510
+ if (shouldFinalize ?? true) {
511
+ finalize(psbt);
512
+ }
513
+ const serializedTx = extract(psbt);
514
+
515
+ return {
516
+ psbt: psbt.serialize(),
517
+ tx: serializedTx.toString("hex"),
518
+ };
519
+ }
520
+
521
+ /**
522
+ * Analyzes a single input to determine if it's internal (can be signed by the device)
523
+ * and extracts its account path and script type.
524
+ */
525
+ private analyzeInput(
526
+ psbt: PsbtV2,
527
+ inputIndex: number,
528
+ masterFp: Buffer,
529
+ ): {
530
+ isInternal: boolean;
531
+ accountPath: number[];
532
+ scriptType: string | undefined;
533
+ } {
534
+ const derivationResult = this.checkBip32Derivation(psbt, inputIndex, masterFp);
535
+ const scriptType = this.determineInputScriptType(psbt, inputIndex);
536
+
537
+ return {
538
+ isInternal: derivationResult.isInternal,
539
+ accountPath: derivationResult.accountPath,
540
+ scriptType,
541
+ };
542
+ }
543
+
544
+ private checkBip32Derivation(
545
+ psbt: PsbtV2,
546
+ inputIndex: number,
547
+ masterFp: Buffer,
548
+ ): { isInternal: boolean; accountPath: number[] } {
549
+ // Check standard BIP32 derivation
550
+ const standardResult = this.checkStandardBip32Derivation(psbt, inputIndex, masterFp);
551
+ if (standardResult.isInternal) {
552
+ return standardResult;
553
+ }
554
+
555
+ // Check TAP_BIP32_DERIVATION for taproot inputs
556
+ return this.checkTaprootBip32Derivation(psbt, inputIndex, masterFp);
557
+ }
558
+
559
+ private checkStandardBip32Derivation(
560
+ psbt: PsbtV2,
561
+ inputIndex: number,
562
+ masterFp: Buffer,
563
+ ): { isInternal: boolean; accountPath: number[] } {
564
+ const keyDatas = psbt.getInputKeyDatas(inputIndex, psbtIn.BIP32_DERIVATION);
565
+
566
+ for (const pubkey of keyDatas) {
567
+ const derivationInfo = psbt.getInputBip32Derivation(inputIndex, pubkey);
568
+ if (derivationInfo?.masterFingerprint.equals(masterFp)) {
569
+ return this.extractAccountPath(derivationInfo.path);
570
+ }
571
+ }
572
+
573
+ return { isInternal: false, accountPath: [] };
574
+ }
575
+
576
+ private checkTaprootBip32Derivation(
577
+ psbt: PsbtV2,
578
+ inputIndex: number,
579
+ masterFp: Buffer,
580
+ ): { isInternal: boolean; accountPath: number[] } {
581
+ const tapKeyDatas = psbt.getInputKeyDatas(inputIndex, psbtIn.TAP_BIP32_DERIVATION);
582
+
583
+ for (const pubkey of tapKeyDatas) {
584
+ const derivationInfo = psbt.getInputTapBip32Derivation(inputIndex, pubkey);
585
+ if (derivationInfo?.masterFingerprint.equals(masterFp)) {
586
+ return this.extractAccountPath(derivationInfo.path);
587
+ }
588
+ }
589
+
590
+ return { isInternal: false, accountPath: [] };
591
+ }
592
+
593
+ private extractAccountPath(fullPath: number[]): { isInternal: true; accountPath: number[] } {
594
+ // Extract account path (full path minus last 2 elements for change/index)
595
+ const accountPath = fullPath.length >= 2 ? fullPath.slice(0, -2) : [];
596
+ return { isInternal: true, accountPath };
597
+ }
598
+
599
+ private determineInputScriptType(psbt: PsbtV2, inputIndex: number): string | undefined {
600
+ const witnessUtxo = psbt.getInputWitnessUtxo(inputIndex);
601
+ if (witnessUtxo) {
602
+ return this.detectScriptType(witnessUtxo.scriptPubKey);
603
+ }
604
+
605
+ const redeemScript = psbt.getInputRedeemScript(inputIndex);
606
+ if (redeemScript) {
607
+ return "p2sh-p2wpkh";
608
+ }
609
+
610
+ return undefined;
611
+ }
612
+
613
+ /**
614
+ * Detects the script type from a scriptPubKey.
615
+ */
616
+ private detectScriptType(scriptPubKey: Buffer): string | undefined {
617
+ if (scriptPubKey.length === 22 && scriptPubKey[0] === 0x00 && scriptPubKey[1] === 0x14) {
618
+ return "p2wpkh";
619
+ }
620
+ if (scriptPubKey.length === 34 && scriptPubKey[0] === 0x51 && scriptPubKey[1] === 0x20) {
621
+ return "p2tr";
622
+ }
623
+ if (scriptPubKey.length === 23 && scriptPubKey[0] === 0xa9 && scriptPubKey[22] === 0x87) {
624
+ return "p2sh";
625
+ }
626
+ if (scriptPubKey.length === 25 && scriptPubKey[0] === 0x76 && scriptPubKey[1] === 0xa9) {
627
+ return "p2pkh";
628
+ }
629
+ return undefined;
630
+ }
631
+
632
+ /**
633
+ * Compares two derivation paths for equality.
634
+ */
635
+ private arePathsEqual(path1: number[], path2: number[]): boolean {
636
+ if (path1.length !== path2.length) return false;
637
+ return path1.every((elem, idx) => elem === path2[idx]);
638
+ }
639
+
640
+ /**
641
+ * Determines the account type based on detected script type, account path, or options.
642
+ */
643
+ private determineAccountType(
644
+ psbt: PsbtV2,
645
+ inputIndex: number,
646
+ masterFp: Buffer,
647
+ detectedScriptType: string | undefined,
648
+ accountPath: number[],
649
+ addressFormat?: AddressFormat,
650
+ ): AccountType {
651
+ // Use detected script type if available
652
+ if (detectedScriptType) {
653
+ return this.createAccountTypeFromScriptType(detectedScriptType, psbt, masterFp);
654
+ }
655
+
656
+ // Fall back to witness UTXO analysis
657
+ const witnessUtxoType = this.determineAccountTypeFromWitnessUtxo(psbt, inputIndex, masterFp);
658
+ if (witnessUtxoType) {
659
+ return witnessUtxoType;
660
+ }
661
+
662
+ // Try redeem script
663
+ if (psbt.getInputRedeemScript(inputIndex)) {
664
+ return new p2wpkhWrapped(psbt, masterFp);
665
+ }
666
+
667
+ // Use address format option
668
+ if (addressFormat) {
669
+ return this.createAccountTypeFromAddressFormat(addressFormat, psbt, masterFp);
670
+ }
671
+
672
+ // Infer from account path purpose
673
+ const purposeBasedType = this.determineAccountTypeFromPurpose(accountPath, psbt, masterFp);
674
+ if (purposeBasedType) {
675
+ return purposeBasedType;
676
+ }
677
+
678
+ // Default to native segwit
679
+ return new p2wpkh(psbt, masterFp);
680
+ }
681
+
682
+ private createAccountTypeFromScriptType(
683
+ scriptType: string,
684
+ psbt: PsbtV2,
685
+ masterFp: Buffer,
686
+ ): AccountType {
687
+ switch (scriptType) {
688
+ case "p2wpkh":
689
+ return new p2wpkh(psbt, masterFp);
690
+ case "p2tr":
691
+ return new p2tr(psbt, masterFp);
692
+ case "p2sh":
693
+ case "p2sh-p2wpkh":
694
+ return new p2wpkhWrapped(psbt, masterFp);
695
+ case "p2pkh":
696
+ return new p2pkh(psbt, masterFp);
697
+ default:
698
+ return new p2wpkh(psbt, masterFp);
699
+ }
700
+ }
701
+
702
+ private determineAccountTypeFromWitnessUtxo(
703
+ psbt: PsbtV2,
704
+ inputIndex: number,
705
+ masterFp: Buffer,
706
+ ): AccountType | null {
707
+ const witnessUtxo = psbt.getInputWitnessUtxo(inputIndex);
708
+ if (!witnessUtxo) {
709
+ return null;
710
+ }
711
+
712
+ const scriptPubKey = witnessUtxo.scriptPubKey;
713
+
714
+ if (scriptPubKey.length === 22 && scriptPubKey[0] === 0x00 && scriptPubKey[1] === 0x14) {
715
+ return new p2wpkh(psbt, masterFp);
716
+ }
717
+ if (scriptPubKey.length === 34 && scriptPubKey[0] === 0x51 && scriptPubKey[1] === 0x20) {
718
+ return new p2tr(psbt, masterFp);
719
+ }
720
+ if (scriptPubKey.length === 23 && scriptPubKey[0] === 0xa9 && scriptPubKey[22] === 0x87) {
721
+ return new p2wpkhWrapped(psbt, masterFp);
722
+ }
723
+ if (scriptPubKey.length === 25 && scriptPubKey[0] === 0x76 && scriptPubKey[1] === 0xa9) {
724
+ return new p2pkh(psbt, masterFp);
725
+ }
726
+
727
+ throw new Error(`Unsupported script type: ${scriptPubKey.toString("hex")}`);
728
+ }
729
+
730
+ private createAccountTypeFromAddressFormat(
731
+ addressFormat: AddressFormat,
732
+ psbt: PsbtV2,
733
+ masterFp: Buffer,
734
+ ): AccountType {
735
+ const descrTemplate = descrTemplFrom(addressFormat);
736
+
737
+ if (descrTemplate === "pkh(@0)") return new p2pkh(psbt, masterFp);
738
+ if (descrTemplate === "wpkh(@0)") return new p2wpkh(psbt, masterFp);
739
+ if (descrTemplate === "sh(wpkh(@0))") return new p2wpkhWrapped(psbt, masterFp);
740
+ if (descrTemplate === "tr(@0)") return new p2tr(psbt, masterFp);
741
+
742
+ throw new Error(`Unsupported descriptor template: ${descrTemplate}`);
743
+ }
744
+
745
+ private determineAccountTypeFromPurpose(
746
+ accountPath: number[],
747
+ psbt: PsbtV2,
748
+ masterFp: Buffer,
749
+ ): AccountType | null {
750
+ if (accountPath.length < 1) {
751
+ return null;
752
+ }
753
+
754
+ const purpose = accountPath[0] - 0x80000000;
755
+
756
+ switch (purpose) {
757
+ case 44:
758
+ return new p2pkh(psbt, masterFp);
759
+ case 49:
760
+ return new p2wpkhWrapped(psbt, masterFp);
761
+ case 84:
762
+ return new p2wpkh(psbt, masterFp);
763
+ case 86:
764
+ return new p2tr(psbt, masterFp);
765
+ default:
766
+ return null;
767
+ }
768
+ }
769
+
296
770
  /**
297
771
  * Signs an arbitrary hex-formatted message with the private key at
298
772
  * the provided derivation path according to the Bitcoin Signature format
@@ -202,8 +202,6 @@ export async function createTransaction(
202
202
  version: defaultVersion,
203
203
  timestamp: Buffer.alloc(0),
204
204
  };
205
- const getTrustedInputCall =
206
- useBip143 && !useTrustedInputForSegwit ? getTrustedInputBIP143 : getTrustedInput;
207
205
  const outputScript = Buffer.from(outputScriptHex, "hex");
208
206
  notify(0, 0);
209
207
  // first pass on inputs to get trusted inputs
@@ -212,7 +210,10 @@ export async function createTransaction(
212
210
  if (isZcash) {
213
211
  input[0].consensusBranchId = getZcashBranchId(input[4]);
214
212
  }
215
- const trustedInput = await getTrustedInputCall(transport, input[1], input[0], additionals);
213
+ const trustedInput =
214
+ useBip143 && !useTrustedInputForSegwit
215
+ ? getTrustedInputBIP143(input[1], input[0], additionals)
216
+ : await getTrustedInput(transport, input[1], input[0], additionals);
216
217
  log("hw", "got trustedInput=" + trustedInput);
217
218
  const sequence = Buffer.alloc(4);
218
219
  sequence.writeUInt32LE(
@@ -1,9 +1,7 @@
1
- import Transport from "@ledgerhq/hw-transport";
2
1
  import { sha256 } from "@noble/hashes/sha256";
3
2
  import type { Transaction } from "./types";
4
3
  import { serializeTransaction } from "./serializeTransaction";
5
4
  export function getTrustedInputBIP143(
6
- transport: Transport,
7
5
  indexLookup: number,
8
6
  transaction: Transaction,
9
7
  additionals: Array<string> = [],
@@ -1,5 +1,9 @@
1
1
  import { crypto } from "bitcoinjs-lib";
2
2
  import { secp256k1 } from "@noble/curves/secp256k1";
3
+ import { BufferWriter, PsbtV2 } from "@ledgerhq/psbtv2";
4
+ import { HASH_SIZE, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160 } from "../constants";
5
+ import { hashPublicKey } from "../hashPublicKey";
6
+ import { DefaultDescriptorTemplate } from "./policy";
3
7
 
4
8
  // Helper function to convert bytes to bigint for scalar operations
5
9
  function bytesToBigInt(bytes: Uint8Array): bigint {
@@ -20,11 +24,6 @@ function pointAddScalar(point: Uint8Array, scalar: Uint8Array): Uint8Array | nul
20
24
  return null;
21
25
  }
22
26
  }
23
- import { BufferWriter } from "../buffertools";
24
- import { HASH_SIZE, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160 } from "../constants";
25
- import { hashPublicKey } from "../hashPublicKey";
26
- import { DefaultDescriptorTemplate } from "./policy";
27
- import { PsbtV2 } from "./psbtv2";
28
27
 
29
28
  export type SpendingCondition = {
30
29
  scriptPubKey: Buffer;
@@ -175,7 +174,7 @@ export class p2pkh extends SingleKeyAccount {
175
174
  this.psbt.setInputBip32Derivation(i, pubkey, this.masterFp, path);
176
175
  }
177
176
 
178
- setSingleKeyOutput(i: number, cond: SpendingCondition, pubkey: Buffer, path: number[]) {
177
+ setSingleKeyOutput(i: number, _cond: SpendingCondition, pubkey: Buffer, path: number[]) {
179
178
  this.psbt.setOutputBip32Derivation(i, pubkey, this.masterFp, path);
180
179
  }
181
180
 
@@ -186,7 +185,7 @@ export class p2pkh extends SingleKeyAccount {
186
185
 
187
186
  export class p2tr extends SingleKeyAccount {
188
187
  singleKeyCondition(pubkey: Buffer): SpendingCondition {
189
- const xonlyPubkey = pubkey.slice(1); // x-only pubkey
188
+ const xonlyPubkey = pubkey.subarray(1); // x-only pubkey
190
189
  const buf = new BufferWriter();
191
190
  const outputKey = this.getTaprootOutputKey(xonlyPubkey);
192
191
  buf.writeSlice(Buffer.from([0x51, 32])); // push1, pubkeylen
@@ -201,13 +200,13 @@ export class p2tr extends SingleKeyAccount {
201
200
  pubkey: Buffer,
202
201
  path: number[],
203
202
  ) {
204
- const xonly = pubkey.slice(1);
203
+ const xonly = pubkey.subarray(1);
205
204
  this.psbt.setInputTapBip32Derivation(i, xonly, [], this.masterFp, path);
206
205
  this.psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.cond.scriptPubKey);
207
206
  }
208
207
 
209
- setSingleKeyOutput(i: number, cond: SpendingCondition, pubkey: Buffer, path: number[]) {
210
- const xonly = pubkey.slice(1);
208
+ setSingleKeyOutput(i: number, _cond: SpendingCondition, pubkey: Buffer, path: number[]) {
209
+ const xonly = pubkey.subarray(1);
211
210
  this.psbt.setOutputTapBip32Derivation(i, xonly, [], this.masterFp, path);
212
211
  }
213
212
 
@@ -251,7 +250,7 @@ export class p2tr extends SingleKeyAccount {
251
250
  if (!tweakedKey) throw new Error("Point addition failed");
252
251
  const outputEcdsaKey = Buffer.from(tweakedKey);
253
252
  // Convert to schnorr.
254
- const outputSchnorrKey = outputEcdsaKey.slice(1);
253
+ const outputSchnorrKey = outputEcdsaKey.subarray(1);
255
254
  // Create address
256
255
  return outputSchnorrKey;
257
256
  }
@@ -295,7 +294,7 @@ export class p2wpkhWrapped extends SingleKeyAccount {
295
294
  }
296
295
 
297
296
  setSingleKeyOutput(i: number, cond: SpendingCondition, pubkey: Buffer, path: number[]) {
298
- this.psbt.setOutputRedeemScript(i, cond.redeemScript!);
297
+ if (cond.redeemScript) this.psbt.setOutputRedeemScript(i, cond.redeemScript);
299
298
  this.psbt.setOutputBip32Derivation(i, pubkey, this.masterFp, path);
300
299
  }
301
300
 
@@ -1,6 +1,6 @@
1
1
  import Transport from "@ledgerhq/hw-transport";
2
2
  import { pathElementsToBuffer } from "../bip32";
3
- import { PsbtV2 } from "./psbtv2";
3
+ import { PsbtV2 } from "@ledgerhq/psbtv2";
4
4
  import { MerkelizedPsbt } from "./merkelizedPsbt";
5
5
  import { ClientCommandInterpreter } from "./clientCommands";
6
6
  import { WalletPolicy } from "./policy";
@@ -1,5 +1,5 @@
1
1
  import { crypto } from "bitcoinjs-lib";
2
- import { BufferReader } from "../buffertools";
2
+ import { BufferReader } from "@ledgerhq/psbtv2";
3
3
  import { createVarint } from "../varint";
4
4
  import { hashLeaf, Merkle } from "./merkle";
5
5
  import { MerkleMap } from "./merkleMap";
@@ -1,5 +1,5 @@
1
1
  import { MerkleMap } from "./merkleMap";
2
- import { PsbtV2 } from "./psbtv2";
2
+ import { PsbtV2 } from "@ledgerhq/psbtv2";
3
3
 
4
4
  /**
5
5
  * This class merkelizes a PSBTv2, by merkelizing the different
@@ -1,6 +1,6 @@
1
1
  import { crypto } from "bitcoinjs-lib";
2
2
  import { pathArrayToString } from "../bip32";
3
- import { BufferWriter } from "../buffertools";
3
+ import { BufferWriter } from "@ledgerhq/psbtv2";
4
4
  import { hashLeaf, Merkle } from "./merkle";
5
5
 
6
6
  export type DefaultDescriptorTemplate = "pkh(@0)" | "sh(wpkh(@0))" | "wpkh(@0)" | "tr(@0)";
@@ -1,5 +1,4 @@
1
- import { BufferWriter } from "../buffertools";
2
- import { PsbtV2 } from "./psbtv2";
1
+ import { BufferWriter, PsbtV2 } from "@ledgerhq/psbtv2";
3
2
 
4
3
  /**
5
4
  * This implements the "Transaction Extractor" role of BIP370 (PSBTv2
@@ -1,5 +1,4 @@
1
- import { BufferWriter } from "../buffertools";
2
- import { psbtIn, PsbtV2 } from "./psbtv2";
1
+ import { BufferWriter, psbtIn, PsbtV2 } from "@ledgerhq/psbtv2";
3
2
 
4
3
  /**
5
4
  * This roughly implements the "input finalizer" role of BIP370 (PSBTv2