@btc-vision/transaction 1.1.15 → 1.1.17

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 (38) hide show
  1. package/browser/_version.d.ts +1 -1
  2. package/browser/index.js +1 -1
  3. package/browser/keypair/Address.d.ts +2 -0
  4. package/browser/opnet.d.ts +1 -0
  5. package/browser/signer/SignerUtils.d.ts +6 -0
  6. package/browser/transaction/shared/TweakedTransaction.d.ts +5 -6
  7. package/browser/utxo/interfaces/IUTXO.d.ts +1 -0
  8. package/build/_version.d.ts +1 -1
  9. package/build/_version.js +1 -1
  10. package/build/keypair/Address.d.ts +2 -0
  11. package/build/keypair/Address.js +9 -0
  12. package/build/opnet.d.ts +1 -0
  13. package/build/opnet.js +1 -0
  14. package/build/signer/SignerUtils.d.ts +6 -0
  15. package/build/signer/SignerUtils.js +56 -0
  16. package/build/transaction/browser/extensions/UnisatSigner.js +5 -32
  17. package/build/transaction/browser/extensions/XverseSigner.js +5 -48
  18. package/build/transaction/builders/FundingTransaction.js +6 -1
  19. package/build/transaction/builders/TransactionBuilder.js +3 -1
  20. package/build/transaction/shared/TweakedTransaction.d.ts +5 -6
  21. package/build/transaction/shared/TweakedTransaction.js +121 -91
  22. package/build/utils/BitcoinUtils.js +4 -4
  23. package/build/utxo/OPNetLimitedProvider.js +1 -0
  24. package/build/utxo/interfaces/IUTXO.d.ts +1 -0
  25. package/package.json +2 -5
  26. package/src/_version.ts +1 -1
  27. package/src/keypair/Address.ts +15 -0
  28. package/src/opnet.ts +2 -0
  29. package/src/signer/SignerUtils.ts +78 -0
  30. package/src/transaction/TransactionFactory.ts +0 -253
  31. package/src/transaction/browser/extensions/UnisatSigner.ts +4 -40
  32. package/src/transaction/browser/extensions/XverseSigner.ts +9 -68
  33. package/src/transaction/builders/FundingTransaction.ts +7 -2
  34. package/src/transaction/builders/TransactionBuilder.ts +3 -1
  35. package/src/transaction/shared/TweakedTransaction.ts +224 -77
  36. package/src/utils/BitcoinUtils.ts +4 -4
  37. package/src/utxo/OPNetLimitedProvider.ts +1 -0
  38. package/src/utxo/interfaces/IUTXO.ts +2 -0
@@ -20,11 +20,23 @@ import { ECPairInterface } from 'ecpair';
20
20
  import { toXOnly } from '@btc-vision/bitcoin/src/psbt/bip371.js';
21
21
  import { UTXO } from '../../utxo/interfaces/IUTXO.js';
22
22
  import { TapLeafScript } from '../interfaces/Tap.js';
23
- import { AddressTypes, AddressVerificator } from '../../keypair/AddressVerificator.js';
24
23
  import { ChainId } from '../../network/ChainId.js';
25
24
  import { varuint } from '@btc-vision/bitcoin/src/bufferutils.js';
26
- import * as bscript from '@btc-vision/bitcoin/src/script.js';
27
25
  import { UnisatSigner } from '../browser/extensions/UnisatSigner.js';
26
+ import {
27
+ canSignNonTaprootInput,
28
+ isTaprootInput,
29
+ pubkeyInScript,
30
+ } from '../../signer/SignerUtils.js';
31
+ import {
32
+ isP2MS,
33
+ isP2PK,
34
+ isP2PKH,
35
+ isP2SHScript,
36
+ isP2TR,
37
+ isP2WPKH,
38
+ isP2WSHScript,
39
+ } from '@btc-vision/bitcoin/src/psbt/psbtutils.js';
28
40
 
29
41
  export interface ITweakedTransactionData {
30
42
  readonly signer: Signer | ECPairInterface | UnisatSigner;
@@ -370,6 +382,7 @@ export abstract class TweakedTransaction extends Logger {
370
382
  * @param {number} i - The index of the input
371
383
  * @param {Signer} signer - The signer to use
372
384
  * @param {boolean} [reverse=false] - Should the input be signed in reverse
385
+ * @param {boolean} [errored=false] - Was there an error
373
386
  * @protected
374
387
  */
375
388
  protected async signInput(
@@ -378,38 +391,48 @@ export abstract class TweakedTransaction extends Logger {
378
391
  i: number,
379
392
  signer: Signer | ECPairInterface,
380
393
  reverse: boolean = false,
394
+ errored: boolean = false,
381
395
  ): Promise<void> {
382
396
  const publicKey = signer.publicKey;
383
- let isTaproot = this.isTaprootInput(input);
384
397
 
398
+ let isTaproot = isTaprootInput(input);
385
399
  if (reverse) {
386
400
  isTaproot = !isTaproot;
387
401
  }
388
402
 
389
403
  let signed: boolean = false;
390
-
404
+ let didError: boolean = false;
391
405
  if (isTaproot) {
392
406
  try {
393
407
  await this.attemptSignTaproot(transaction, input, i, signer, publicKey);
394
408
  signed = true;
395
409
  } catch (e) {
396
- this.error(`Failed to sign Taproot script path input ${i}: ${e}`);
410
+ this.error(
411
+ `Failed to sign Taproot script path input ${i} (reverse: ${reverse}): ${(e as Error).message}`,
412
+ );
413
+
414
+ didError = true;
397
415
  }
398
416
  } else {
399
417
  // Non-Taproot input
400
- if (!reverse ? this.canSignNonTaprootInput(input, publicKey) : true) {
418
+ if (!reverse ? canSignNonTaprootInput(input, publicKey) : true) {
401
419
  try {
402
420
  await this.signNonTaprootInput(signer, transaction, i);
403
421
  signed = true;
404
422
  } catch (e) {
405
- this.error(`Failed to sign non-Taproot input ${i}: ${e}`);
423
+ this.error(`Failed to sign non-Taproot input ${i}: ${(e as Error).stack}`);
424
+ didError = true;
406
425
  }
407
426
  }
408
427
  }
409
428
 
410
429
  if (!signed) {
430
+ if (didError && errored) {
431
+ throw new Error(`Failed to sign input ${i} with the provided signer.`);
432
+ }
433
+
411
434
  try {
412
- await this.signInput(transaction, input, i, signer, true);
435
+ await this.signInput(transaction, input, i, signer, true, didError);
413
436
  } catch {
414
437
  throw new Error(`Cannot sign input ${i} with the provided signer.`);
415
438
  }
@@ -585,14 +608,202 @@ export abstract class TweakedTransaction extends Logger {
585
608
  return;
586
609
  }
587
610
 
611
+ protected generateP2SHP2PKHRedeemScript(inputAddr: string):
612
+ | {
613
+ redeemScript: Buffer;
614
+ outputScript: Buffer;
615
+ }
616
+ | undefined {
617
+ const pubkey = Buffer.isBuffer(this.signer.publicKey)
618
+ ? this.signer.publicKey
619
+ : Buffer.from(this.signer.publicKey, 'hex');
620
+
621
+ const w = payments.p2wpkh({
622
+ pubkey: pubkey,
623
+ network: this.network,
624
+ });
625
+
626
+ const p = payments.p2sh({
627
+ redeem: w,
628
+ network: this.network,
629
+ });
630
+
631
+ const address = p.address;
632
+ const redeemScript = p.redeem?.output;
633
+ if (!redeemScript) {
634
+ throw new Error('Failed to generate P2SH-P2WPKH redeem script');
635
+ }
636
+
637
+ if (address === inputAddr && p.redeem && p.redeem.output && p.output) {
638
+ return {
639
+ redeemScript: p.redeem.output,
640
+ outputScript: p.output,
641
+ };
642
+ }
643
+
644
+ return;
645
+ }
646
+
588
647
  /**
589
- * Generate the PSBT input extended
648
+ * Generate the PSBT input extended, supporting various script types
590
649
  * @param {UTXO} utxo The UTXO
591
650
  * @param {number} i The index of the input
592
651
  * @protected
593
652
  * @returns {PsbtInputExtended} The PSBT input extended
594
653
  */
595
654
  protected generatePsbtInputExtended(utxo: UTXO, i: number): PsbtInputExtended {
655
+ const script = Buffer.from(utxo.scriptPubKey.hex, 'hex');
656
+
657
+ const input: PsbtInputExtended = {
658
+ hash: utxo.transactionId,
659
+ index: utxo.outputIndex,
660
+ sequence: this.sequence,
661
+ witnessUtxo: {
662
+ value: Number(utxo.value),
663
+ script,
664
+ },
665
+ };
666
+
667
+ // Handle P2PKH (Legacy)
668
+ if (isP2PKH(script)) {
669
+ // Legacy input requires nonWitnessUtxo
670
+ if (utxo.nonWitnessUtxo) {
671
+ //delete input.witnessUtxo;
672
+ input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
673
+ ? utxo.nonWitnessUtxo
674
+ : Buffer.from(utxo.nonWitnessUtxo, 'hex');
675
+ } else {
676
+ throw new Error('Missing nonWitnessUtxo for P2PKH UTXO');
677
+ }
678
+ }
679
+
680
+ // Handle P2WPKH (SegWit)
681
+ else if (isP2WPKH(script)) {
682
+ // No redeemScript required for pure P2WPKH
683
+ // witnessUtxo is enough, no nonWitnessUtxo needed.
684
+ }
685
+
686
+ // Handle P2WSH (SegWit)
687
+ else if (isP2WSHScript(script)) {
688
+ // P2WSH requires a witnessScript
689
+ if (!utxo.witnessScript) {
690
+ // Can't just invent a witnessScript out of thin air. If not provided, it's an error.
691
+ throw new Error('Missing witnessScript for P2WSH UTXO');
692
+ }
693
+
694
+ input.witnessScript = Buffer.isBuffer(utxo.witnessScript)
695
+ ? utxo.witnessScript
696
+ : Buffer.from(utxo.witnessScript, 'hex');
697
+
698
+ // No nonWitnessUtxo needed for segwit
699
+ }
700
+
701
+ // Handle P2SH (Can be legacy or wrapping segwit)
702
+ else if (isP2SHScript(script)) {
703
+ // Redeem script is required for P2SH
704
+ let redeemScriptBuf: Buffer | undefined;
705
+
706
+ if (utxo.redeemScript) {
707
+ redeemScriptBuf = Buffer.isBuffer(utxo.redeemScript)
708
+ ? utxo.redeemScript
709
+ : Buffer.from(utxo.redeemScript, 'hex');
710
+ } else {
711
+ // Attempt to generate a redeem script if missing
712
+ if (!utxo.scriptPubKey.address) {
713
+ throw new Error(
714
+ 'Missing redeemScript and no address to regenerate it for P2SH UTXO',
715
+ );
716
+ }
717
+
718
+ const legacyScripts = this.generateP2SHP2PKHRedeemScript(utxo.scriptPubKey.address);
719
+ if (!legacyScripts) {
720
+ throw new Error('Missing redeemScript for P2SH UTXO and unable to regenerate');
721
+ }
722
+
723
+ redeemScriptBuf = legacyScripts.redeemScript;
724
+ }
725
+
726
+ input.redeemScript = redeemScriptBuf;
727
+
728
+ // Check if redeemScript is wrapping segwit (like P2SH-P2WPKH or P2SH-P2WSH)
729
+ const payment = payments.p2sh({ redeem: { output: input.redeemScript } });
730
+ if (!payment.redeem) {
731
+ throw new Error('Failed to extract redeem script from P2SH UTXO');
732
+ }
733
+
734
+ const redeemOutput = payment.redeem.output;
735
+ if (!redeemOutput) {
736
+ throw new Error('Failed to extract redeem output from P2SH UTXO');
737
+ }
738
+
739
+ if (utxo.nonWitnessUtxo) {
740
+ input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
741
+ ? utxo.nonWitnessUtxo
742
+ : Buffer.from(utxo.nonWitnessUtxo, 'hex');
743
+ }
744
+
745
+ if (isP2WPKH(redeemOutput)) {
746
+ // P2SH-P2WPKH
747
+ // Use witnessUtxo + redeemScript
748
+ delete input.nonWitnessUtxo; // ensure we do NOT have nonWitnessUtxo
749
+ // witnessScript is not needed
750
+ } else if (isP2WSHScript(redeemOutput)) {
751
+ // P2SH-P2WSH
752
+ // Use witnessUtxo + redeemScript + witnessScript
753
+ delete input.nonWitnessUtxo; // ensure we do NOT have nonWitnessUtxo
754
+ if (!input.witnessScript) {
755
+ throw new Error('Missing witnessScript for P2SH-P2WSH UTXO');
756
+ }
757
+ } else {
758
+ // Legacy P2SH
759
+ // Use nonWitnessUtxo
760
+ delete input.witnessUtxo; // ensure we do NOT have witnessUtxo
761
+ }
762
+ }
763
+
764
+ // Handle P2TR (Taproot)
765
+ else if (isP2TR(script)) {
766
+ // Taproot inputs do not require nonWitnessUtxo, witnessUtxo is sufficient.
767
+
768
+ // If there's a configured sighash type
769
+ if (this.sighashTypes) {
770
+ const inputSign = TweakedTransaction.calculateSignHash(this.sighashTypes);
771
+ if (inputSign) input.sighashType = inputSign;
772
+ }
773
+
774
+ // Taproot internal key
775
+ this.tweakSigner();
776
+ input.tapInternalKey = this.internalPubKeyToXOnly();
777
+ }
778
+
779
+ // Handle P2PK (legacy) or P2MS (bare multisig)
780
+ else if (isP2PK(script) || isP2MS(script)) {
781
+ // These are legacy scripts, need nonWitnessUtxo
782
+ if (utxo.nonWitnessUtxo) {
783
+ input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
784
+ ? utxo.nonWitnessUtxo
785
+ : Buffer.from(utxo.nonWitnessUtxo, 'hex');
786
+ } else {
787
+ throw new Error('Missing nonWitnessUtxo for P2PK or P2MS UTXO');
788
+ }
789
+ } else {
790
+ this.error(`Unknown or unsupported script type for output: ${utxo.scriptPubKey.hex}`);
791
+ }
792
+
793
+ // TapLeafScript if available
794
+ if (this.tapLeafScript) {
795
+ input.tapLeafScript = [this.tapLeafScript];
796
+ }
797
+
798
+ // If the first input and we have a global nonWitnessUtxo not yet set
799
+ if (i === 0 && this.nonWitnessUtxo) {
800
+ input.nonWitnessUtxo = this.nonWitnessUtxo;
801
+ }
802
+
803
+ return input;
804
+ }
805
+
806
+ /*protected generatePsbtInputExtended(utxo: UTXO, i: number): PsbtInputExtended {
596
807
  const input: PsbtInputExtended = {
597
808
  hash: utxo.transactionId,
598
809
  index: utxo.outputIndex,
@@ -655,10 +866,6 @@ export abstract class TweakedTransaction extends Logger {
655
866
  if (inputSign) input.sighashType = inputSign;
656
867
  }
657
868
 
658
- if (this.tapLeafScript) {
659
- input.tapLeafScript = [this.tapLeafScript];
660
- }
661
-
662
869
  if (i === 0 && this.nonWitnessUtxo) {
663
870
  input.nonWitnessUtxo = this.nonWitnessUtxo;
664
871
  }
@@ -673,8 +880,10 @@ export abstract class TweakedTransaction extends Logger {
673
880
  input.tapInternalKey = this.internalPubKeyToXOnly();
674
881
  }
675
882
 
883
+ console.log(input);
884
+
676
885
  return input;
677
- }
886
+ }*/
678
887
 
679
888
  protected customFinalizerP2SH = (
680
889
  inputIndex: number,
@@ -747,7 +956,7 @@ export abstract class TweakedTransaction extends Logger {
747
956
  if (input.tapLeafScript && input.tapLeafScript.length > 0) {
748
957
  // Check if the signer's public key is involved in any tapLeafScript
749
958
  for (const tapLeafScript of input.tapLeafScript) {
750
- if (this.pubkeyInScript(publicKey, tapLeafScript.script)) {
959
+ if (pubkeyInScript(publicKey, tapLeafScript.script)) {
751
960
  // The public key is in the script; it's a script spend
752
961
  return true;
753
962
  }
@@ -756,68 +965,6 @@ export abstract class TweakedTransaction extends Logger {
756
965
  return false;
757
966
  }
758
967
 
759
- // Helper method to determine if an input is Taproot
760
- private isTaprootInput(input: PsbtInput): boolean {
761
- if (input.tapInternalKey || input.tapKeySig || input.tapScriptSig || input.tapLeafScript) {
762
- return true;
763
- }
764
-
765
- if (input.witnessUtxo) {
766
- const script = input.witnessUtxo.script;
767
- // Check if the script is a P2TR output (OP_1 [32-byte key])
768
- return script.length === 34 && script[0] === opcodes.OP_1 && script[1] === 0x20;
769
- }
770
-
771
- return false;
772
- }
773
-
774
- // Check if the signer can sign the non-Taproot input
775
- private canSignNonTaprootInput(input: PsbtInput, publicKey: Buffer): boolean {
776
- const script = this.getInputRelevantScript(input);
777
- if (script) {
778
- return this.pubkeyInScript(publicKey, script);
779
- }
780
- return false;
781
- }
782
-
783
- // Helper method to extract the relevant script from the input
784
- private getInputRelevantScript(input: PsbtInput): Buffer | null {
785
- if (input.redeemScript) {
786
- return input.redeemScript;
787
- }
788
- if (input.witnessScript) {
789
- return input.witnessScript;
790
- }
791
- if (input.witnessUtxo) {
792
- return input.witnessUtxo.script;
793
- }
794
- if (input.nonWitnessUtxo) {
795
- // Additional logic can be added to extract script from nonWitnessUtxo
796
- return null;
797
- }
798
- return null;
799
- }
800
-
801
- // Helper method to check if a public key is in a script
802
- private pubkeyInScript(pubkey: Buffer, script: Buffer): boolean {
803
- return this.pubkeyPositionInScript(pubkey, script) !== -1;
804
- }
805
-
806
- private pubkeyPositionInScript(pubkey: Buffer, script: Buffer): number {
807
- const pubkeyHash = bitCrypto.hash160(pubkey);
808
- const pubkeyXOnly = toXOnly(pubkey);
809
-
810
- const decompiled = bscript.decompile(script);
811
- if (decompiled === null) throw new Error('Unknown script error');
812
-
813
- return decompiled.findIndex((element) => {
814
- if (typeof element === 'number') return false;
815
- return (
816
- element.equals(pubkey) || element.equals(pubkeyHash) || element.equals(pubkeyXOnly)
817
- );
818
- });
819
- }
820
-
821
968
  private async signTaprootInput(
822
969
  signer: Signer | ECPairInterface,
823
970
  transaction: Psbt,
@@ -1,4 +1,4 @@
1
- import crypto, { createHash } from 'crypto';
1
+ import { createHash } from 'crypto';
2
2
 
3
3
  /**
4
4
  * Utility class for Bitcoin related functions
@@ -34,16 +34,16 @@ export class BitcoinUtils {
34
34
  window.crypto.getRandomValues(array);
35
35
 
36
36
  return Buffer.from(array);
37
- } else if (crypto && typeof crypto.getRandomValues === 'function') {
37
+ } else if (globalThis.crypto && typeof globalThis.crypto.getRandomValues === 'function') {
38
38
  const array = new Uint8Array(length);
39
- crypto.getRandomValues(array);
39
+ globalThis.crypto.getRandomValues(array);
40
40
 
41
41
  return Buffer.from(array);
42
42
  } else {
43
43
  console.log(
44
44
  `No secure random number generator available. Please upgrade your environment.`,
45
45
  globalThis.window.crypto,
46
- crypto,
46
+ globalThis.crypto,
47
47
  );
48
48
  throw new Error(
49
49
  'No secure random number generator available. Please upgrade your environment.',
@@ -102,6 +102,7 @@ export class OPNetLimitedProvider {
102
102
  outputIndex: utxo.outputIndex,
103
103
  value: utxoValue,
104
104
  scriptPubKey: utxo.scriptPubKey,
105
+ nonWitnessUtxo: Buffer.from(utxo.raw, 'base64'),
105
106
  });
106
107
 
107
108
  if (currentAmount > amountRequested) {
@@ -5,6 +5,7 @@ export interface UTXO {
5
5
  readonly outputIndex: number;
6
6
  readonly value: bigint;
7
7
  readonly scriptPubKey: ScriptPubKey;
8
+
8
9
  redeemScript?: string | Buffer;
9
10
  witnessScript?: string | Buffer;
10
11
  nonWitnessUtxo?: string | Buffer;
@@ -31,4 +32,5 @@ export interface RawUTXOResponse {
31
32
  readonly outputIndex: number;
32
33
  readonly value: string;
33
34
  readonly scriptPubKey: ScriptPubKey;
35
+ readonly raw: string;
34
36
  }