@btc-vision/transaction 1.1.1 → 1.1.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.
Files changed (30) hide show
  1. package/browser/_version.d.ts +1 -1
  2. package/browser/index.js +1 -1
  3. package/browser/transaction/browser/extensions/UnisatSigner.d.ts +4 -4
  4. package/browser/transaction/browser/types/Unisat.d.ts +1 -1
  5. package/browser/transaction/builders/DeploymentTransaction.d.ts +1 -0
  6. package/browser/transaction/builders/SharedInteractionTransaction.d.ts +2 -0
  7. package/browser/transaction/builders/TransactionBuilder.d.ts +2 -1
  8. package/browser/transaction/shared/TweakedTransaction.d.ts +14 -3
  9. package/build/_version.d.ts +1 -1
  10. package/build/_version.js +1 -1
  11. package/build/buffer/BinaryWriter.js +0 -1
  12. package/build/transaction/browser/extensions/UnisatSigner.d.ts +4 -4
  13. package/build/transaction/browser/extensions/UnisatSigner.js +103 -20
  14. package/build/transaction/browser/types/Unisat.d.ts +1 -1
  15. package/build/transaction/builders/DeploymentTransaction.d.ts +1 -0
  16. package/build/transaction/builders/DeploymentTransaction.js +17 -0
  17. package/build/transaction/builders/SharedInteractionTransaction.d.ts +2 -0
  18. package/build/transaction/builders/SharedInteractionTransaction.js +31 -10
  19. package/build/transaction/builders/TransactionBuilder.d.ts +2 -1
  20. package/build/transaction/shared/TweakedTransaction.d.ts +14 -3
  21. package/build/transaction/shared/TweakedTransaction.js +146 -23
  22. package/package.json +4 -1
  23. package/src/_version.ts +1 -1
  24. package/src/buffer/BinaryWriter.ts +0 -2
  25. package/src/transaction/browser/extensions/UnisatSigner.ts +139 -28
  26. package/src/transaction/browser/types/Unisat.ts +1 -1
  27. package/src/transaction/builders/DeploymentTransaction.ts +25 -0
  28. package/src/transaction/builders/SharedInteractionTransaction.ts +51 -21
  29. package/src/transaction/builders/TransactionBuilder.ts +2 -1
  30. package/src/transaction/shared/TweakedTransaction.ts +210 -113
@@ -14,6 +14,7 @@ import {
14
14
  Signer,
15
15
  Transaction,
16
16
  } from '@btc-vision/bitcoin';
17
+
17
18
  import { TweakedSigner, TweakSettings } from '../../signer/TweakedSigner.js';
18
19
  import { ECPairInterface } from 'ecpair';
19
20
  import { toXOnly } from '@btc-vision/bitcoin/src/psbt/bip371.js';
@@ -22,9 +23,11 @@ import { TapLeafScript } from '../interfaces/Tap.js';
22
23
  import { AddressTypes, AddressVerificator } from '../../keypair/AddressVerificator.js';
23
24
  import { ChainId } from '../../network/ChainId.js';
24
25
  import { varuint } from '@btc-vision/bitcoin/src/bufferutils.js';
26
+ import * as bscript from '@btc-vision/bitcoin/src/script.js';
27
+ import { UnisatSigner } from '../browser/extensions/UnisatSigner.js';
25
28
 
26
29
  export interface ITweakedTransactionData {
27
- readonly signer: Signer | ECPairInterface;
30
+ readonly signer: Signer | ECPairInterface | UnisatSigner;
28
31
  readonly network: Network;
29
32
  readonly chainId?: ChainId;
30
33
  readonly nonWitnessUtxo?: Buffer;
@@ -44,22 +47,27 @@ export enum TransactionSequence {
44
47
  export abstract class TweakedTransaction extends Logger {
45
48
  public readonly logColor: string = '#00ffe1';
46
49
  public finalized: boolean = false;
50
+
47
51
  /**
48
52
  * @description Was the transaction signed?
49
53
  */
50
- protected signer: Signer | ECPairInterface;
54
+ protected signer: Signer | ECPairInterface | UnisatSigner;
55
+
51
56
  /**
52
57
  * @description Tweaked signer
53
58
  */
54
59
  protected tweakedSigner?: ECPairInterface;
60
+
55
61
  /**
56
62
  * @description The network of the transaction
57
63
  */
58
64
  protected network: Network;
65
+
59
66
  /**
60
67
  * @description Was the transaction signed?
61
68
  */
62
69
  protected signed: boolean = false;
70
+
63
71
  /**
64
72
  * @description The transaction
65
73
  * @protected
@@ -358,100 +366,54 @@ export abstract class TweakedTransaction extends Logger {
358
366
  /**
359
367
  * Signs an input of the transaction.
360
368
  * @param {Psbt} transaction - The transaction to sign
361
- * @param {PsbtInput} _input - The input to sign
369
+ * @param {PsbtInput} input - The input to sign
362
370
  * @param {number} i - The index of the input
363
371
  * @param {Signer} signer - The signer to use
372
+ * @param {boolean} [reverse=false] - Should the input be signed in reverse
364
373
  * @protected
365
374
  */
366
375
  protected async signInput(
367
376
  transaction: Psbt,
368
- _input: PsbtInput,
377
+ input: PsbtInput,
369
378
  i: number,
370
379
  signer: Signer | ECPairInterface,
380
+ reverse: boolean = false,
371
381
  ): Promise<void> {
372
- try {
373
- if ('signInput' in signer) {
374
- // @ts-expect-error - we know it's a signer
375
- return await (signer.signInput(transaction, i) as Promise<void>);
376
- }
382
+ const publicKey = signer.publicKey;
383
+ let isTaproot = this.isTaprootInput(input);
377
384
 
378
- transaction.signInput(i, signer);
379
- } catch {
380
- try {
381
- if ('signTaprootInput' in signer) {
382
- // @ts-expect-error - we know it's a taproot signer
383
- return await (signer.signTaprootInput(transaction, i) as Promise<void>);
384
- }
385
-
386
- transaction.signTaprootInput(i, signer);
387
- } catch {
388
- throw new Error('Failed to sign input');
389
- }
385
+ if (reverse) {
386
+ isTaproot = !isTaproot;
390
387
  }
391
388
 
392
- /*const signHash =
393
- this.sighashTypes && this.sighashTypes.length
394
- ? [TweakedTransaction.calculateSignHash(this.sighashTypes)]
395
- : undefined;
396
-
397
- signer = signer || this.getSignerKey();
389
+ let signed: boolean = false;
398
390
 
399
- let testedTap: boolean = false;
400
- if (input.tapInternalKey) {
401
- if (!this.tweakedSigner) this.tweakSigner();
402
-
403
- let tweakedSigner: ECPairInterface | undefined;
404
- if (signer !== this.signer) {
405
- tweakedSigner = this.getTweakedSigner(true, signer);
406
- } else {
407
- tweakedSigner = this.tweakedSigner;
391
+ if (isTaproot) {
392
+ try {
393
+ await this.attemptSignTaproot(transaction, input, i, signer, publicKey);
394
+ signed = true;
395
+ } catch (e) {
396
+ this.error(`Failed to sign Taproot script path input ${i}: ${e}`);
408
397
  }
409
-
410
- if (tweakedSigner) {
411
- testedTap = true;
412
-
398
+ } else {
399
+ // Non-Taproot input
400
+ if (!reverse ? this.canSignNonTaprootInput(input, publicKey) : true) {
413
401
  try {
414
- if ('signTaprootInput' in signer) {
415
- // @ts-expect-error - we know it's a taproot signer
416
- return await (signer.signTaprootInput(
417
- transaction,
418
- i,
419
- signHash,
420
- ) as Promise<void>);
421
- } else {
422
- transaction.signTaprootInput(i, tweakedSigner, undefined, signHash);
423
- }
424
-
425
- return;
426
- } catch {}
402
+ await this.signNonTaprootInput(signer, transaction, i);
403
+ signed = true;
404
+ } catch (e) {
405
+ this.error(`Failed to sign non-Taproot input ${i}: ${e}`);
406
+ }
427
407
  }
428
408
  }
429
409
 
430
- try {
431
- if ('signInput' in signer) {
432
- // @ts-expect-error - we know it's a signer
433
- return await (signer.signInput(transaction, i, signHash) as Promise<void>);
434
- } else {
435
- transaction.signInput(i, signer, signHash);
436
- }
437
- } catch (e) {
438
- if (!testedTap) {
439
- // and we try again taproot...
440
-
441
- if ('signTaprootInput' in signer) {
442
- // @ts-expect-error - we know it's a taproot signer
443
- return await (signer.signTaprootInput(
444
- transaction,
445
- i,
446
- signHash,
447
- ) as Promise<void>);
448
- } else if (this.tweakedSigner) {
449
- transaction.signTaprootInput(i, this.tweakedSigner, undefined, signHash);
450
- } else {
451
- throw e;
452
- }
410
+ if (!signed) {
411
+ try {
412
+ await this.signInput(transaction, input, i, signer, true);
413
+ } catch {
414
+ throw new Error(`Cannot sign input ${i} with the provided signer.`);
453
415
  }
454
- }*/
416
+ }
455
417
  }
456
418
 
457
419
  protected splitArray<T>(arr: T[], chunkSize: number): T[][] {
@@ -460,7 +422,6 @@ export abstract class TweakedTransaction extends Logger {
460
422
  }
461
423
 
462
424
  const result: T[][] = [];
463
-
464
425
  for (let i = 0; i < arr.length; i += chunkSize) {
465
426
  result.push(arr.slice(i, i + chunkSize));
466
427
  }
@@ -475,6 +436,12 @@ export abstract class TweakedTransaction extends Logger {
475
436
  * @returns {Promise<void>}
476
437
  */
477
438
  protected async signInputs(transaction: Psbt): Promise<void> {
439
+ if ('multiSignPsbt' in this.signer) {
440
+ await this.signInputsWalletBased(transaction);
441
+ return;
442
+ }
443
+
444
+ // non web based signing.
478
445
  const txs: PsbtInput[] = transaction.data.inputs;
479
446
 
480
447
  const batchSize: number = 20;
@@ -499,15 +466,11 @@ export abstract class TweakedTransaction extends Logger {
499
466
  await Promise.all(promises);
500
467
  }
501
468
 
502
- transaction.finalizeInput(0, this.customFinalizerP2SH);
503
-
504
- try {
505
- transaction.finalizeAllInputs();
506
-
507
- this.finalized = true;
508
- } catch (e) {
509
- this.finalized = false;
469
+ for (let i = 0; i < transaction.data.inputs.length; i++) {
470
+ transaction.finalizeInput(i, this.customFinalizerP2SH);
510
471
  }
472
+
473
+ this.finalized = true;
511
474
  }
512
475
 
513
476
  /**
@@ -698,10 +661,9 @@ export abstract class TweakedTransaction extends Logger {
698
661
 
699
662
  if (i === 0 && this.nonWitnessUtxo) {
700
663
  input.nonWitnessUtxo = this.nonWitnessUtxo;
701
- this.log(`Using non-witness utxo for input ${i}`);
702
664
  }
703
665
 
704
- // automatically detect p2tr inputs.
666
+ // Automatically detect P2TR inputs.
705
667
  if (
706
668
  utxo.scriptPubKey.address &&
707
669
  AddressVerificator.isValidP2TRAddress(utxo.scriptPubKey.address, this.network)
@@ -715,11 +677,11 @@ export abstract class TweakedTransaction extends Logger {
715
677
  }
716
678
 
717
679
  protected customFinalizerP2SH = (
718
- inputIndex: number, // Which input is it?
719
- input: PsbtInput, // The PSBT input contents
720
- scriptA: Buffer, // The "meaningful" locking script Buffer (redeemScript for P2SH etc.)
721
- isSegwit: boolean, // Is it segwit?
722
- isP2SH: boolean, // Is it P2SH?
680
+ inputIndex: number,
681
+ input: PsbtInput,
682
+ scriptA: Buffer,
683
+ isSegwit: boolean,
684
+ isP2SH: boolean,
723
685
  isP2WSH: boolean,
724
686
  ): {
725
687
  finalScriptSig: Buffer | undefined;
@@ -728,31 +690,166 @@ export abstract class TweakedTransaction extends Logger {
728
690
  const inputDecoded = this.inputs[inputIndex];
729
691
  if (isP2SH && input.partialSig && inputDecoded && inputDecoded.redeemScript) {
730
692
  const signatures = input.partialSig.map((sig) => sig.signature);
731
-
732
- /*const fakeSignature = Buffer.from([
733
- 0x30,
734
- 0x45, // DER prefix: 0x30 (Compound), 0x45 (length = 69 bytes)
735
- 0x02,
736
- 0x20, // Integer marker: 0x02 (integer), 0x20 (length = 32 bytes)
737
- ...Buffer.alloc(32, 0x00), // 32-byte fake 'r' value (all zeros)
738
- 0x02,
739
- 0x21, // Integer marker: 0x02 (integer), 0x21 (length = 33 bytes)
740
- ...Buffer.alloc(33, 0x00), // 33-byte fake 's' value (all zeros)
741
- 0x01, // SIGHASH_ALL flag (0x01)
742
- ]);*/
743
-
744
- const scriptSig = script.compile([
745
- ...signatures,
746
- //fakeSignature,
747
- inputDecoded.redeemScript,
748
- ]);
693
+ const scriptSig = script.compile([...signatures, inputDecoded.redeemScript]);
749
694
 
750
695
  return {
751
- finalScriptSig: scriptSig, // Manually set the final scriptSig
752
- finalScriptWitness: undefined, // Manually set the final scriptWitness
696
+ finalScriptSig: scriptSig,
697
+ finalScriptWitness: undefined,
753
698
  };
754
699
  }
755
700
 
756
701
  return getFinalScripts(inputIndex, input, scriptA, isSegwit, isP2SH, isP2WSH);
757
702
  };
703
+
704
+ protected async signInputsWalletBased(transaction: Psbt): Promise<void> {
705
+ const signer: UnisatSigner = this.signer as UnisatSigner;
706
+
707
+ // then, we sign all the remaining inputs with the wallet signer.
708
+ await signer.multiSignPsbt([transaction]);
709
+
710
+ // Then, we finalize every input.
711
+ for (let i = 0; i < transaction.data.inputs.length; i++) {
712
+ transaction.finalizeInput(i, this.customFinalizerP2SH);
713
+ }
714
+
715
+ this.finalized = true;
716
+ }
717
+
718
+ private async attemptSignTaproot(
719
+ transaction: Psbt,
720
+ input: PsbtInput,
721
+ i: number,
722
+ signer: Signer | ECPairInterface,
723
+ publicKey: Buffer,
724
+ ): Promise<void> {
725
+ const isScriptSpend = this.isTaprootScriptSpend(input, publicKey);
726
+
727
+ if (isScriptSpend) {
728
+ await this.signTaprootInput(signer, transaction, i);
729
+ } else {
730
+ let tweakedSigner: ECPairInterface | undefined;
731
+ if (signer !== this.signer) {
732
+ tweakedSigner = this.getTweakedSigner(true, signer);
733
+ } else {
734
+ if (!this.tweakedSigner) this.tweakSigner();
735
+ tweakedSigner = this.tweakedSigner;
736
+ }
737
+
738
+ if (tweakedSigner) {
739
+ await this.signTaprootInput(tweakedSigner, transaction, i);
740
+ } else {
741
+ this.error(`Failed to obtain tweaked signer for input ${i}.`);
742
+ }
743
+ }
744
+ }
745
+
746
+ private isTaprootScriptSpend(input: PsbtInput, publicKey: Buffer): boolean {
747
+ if (input.tapLeafScript && input.tapLeafScript.length > 0) {
748
+ // Check if the signer's public key is involved in any tapLeafScript
749
+ for (const tapLeafScript of input.tapLeafScript) {
750
+ if (this.pubkeyInScript(publicKey, tapLeafScript.script)) {
751
+ // The public key is in the script; it's a script spend
752
+ return true;
753
+ }
754
+ }
755
+ }
756
+ return false;
757
+ }
758
+
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
+ private async signTaprootInput(
822
+ signer: Signer | ECPairInterface,
823
+ transaction: Psbt,
824
+ i: number,
825
+ tapLeafHash?: Buffer,
826
+ ): Promise<void> {
827
+ if ('signTaprootInput' in signer) {
828
+ try {
829
+ await (
830
+ signer.signTaprootInput as (
831
+ tx: Psbt,
832
+ i: number,
833
+ tapLeafHash?: Buffer,
834
+ ) => Promise<void>
835
+ )(transaction, i, tapLeafHash);
836
+ } catch {
837
+ throw new Error('Failed to sign Taproot input with provided signer.');
838
+ }
839
+ } else {
840
+ transaction.signTaprootInput(i, signer); //tapLeafHash
841
+ }
842
+ }
843
+
844
+ private async signNonTaprootInput(
845
+ signer: Signer | ECPairInterface,
846
+ transaction: Psbt,
847
+ i: number,
848
+ ): Promise<void> {
849
+ if ('signInput' in signer) {
850
+ await (signer.signInput as (tx: Psbt, i: number) => Promise<void>)(transaction, i);
851
+ } else {
852
+ transaction.signInput(i, signer);
853
+ }
854
+ }
758
855
  }