@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.
- package/browser/_version.d.ts +1 -1
- package/browser/index.js +1 -1
- package/browser/transaction/browser/extensions/UnisatSigner.d.ts +4 -4
- package/browser/transaction/browser/types/Unisat.d.ts +1 -1
- package/browser/transaction/builders/DeploymentTransaction.d.ts +1 -0
- package/browser/transaction/builders/SharedInteractionTransaction.d.ts +2 -0
- package/browser/transaction/builders/TransactionBuilder.d.ts +2 -1
- package/browser/transaction/shared/TweakedTransaction.d.ts +14 -3
- package/build/_version.d.ts +1 -1
- package/build/_version.js +1 -1
- package/build/buffer/BinaryWriter.js +0 -1
- package/build/transaction/browser/extensions/UnisatSigner.d.ts +4 -4
- package/build/transaction/browser/extensions/UnisatSigner.js +103 -20
- package/build/transaction/browser/types/Unisat.d.ts +1 -1
- package/build/transaction/builders/DeploymentTransaction.d.ts +1 -0
- package/build/transaction/builders/DeploymentTransaction.js +17 -0
- package/build/transaction/builders/SharedInteractionTransaction.d.ts +2 -0
- package/build/transaction/builders/SharedInteractionTransaction.js +31 -10
- package/build/transaction/builders/TransactionBuilder.d.ts +2 -1
- package/build/transaction/shared/TweakedTransaction.d.ts +14 -3
- package/build/transaction/shared/TweakedTransaction.js +146 -23
- package/package.json +4 -1
- package/src/_version.ts +1 -1
- package/src/buffer/BinaryWriter.ts +0 -2
- package/src/transaction/browser/extensions/UnisatSigner.ts +139 -28
- package/src/transaction/browser/types/Unisat.ts +1 -1
- package/src/transaction/builders/DeploymentTransaction.ts +25 -0
- package/src/transaction/builders/SharedInteractionTransaction.ts +51 -21
- package/src/transaction/builders/TransactionBuilder.ts +2 -1
- 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}
|
|
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
|
-
|
|
377
|
+
input: PsbtInput,
|
|
369
378
|
i: number,
|
|
370
379
|
signer: Signer | ECPairInterface,
|
|
380
|
+
reverse: boolean = false,
|
|
371
381
|
): Promise<void> {
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
398
|
+
} else {
|
|
399
|
+
// Non-Taproot input
|
|
400
|
+
if (!reverse ? this.canSignNonTaprootInput(input, publicKey) : true) {
|
|
413
401
|
try {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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,
|
|
719
|
-
input: PsbtInput,
|
|
720
|
-
scriptA: Buffer,
|
|
721
|
-
isSegwit: boolean,
|
|
722
|
-
isP2SH: boolean,
|
|
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,
|
|
752
|
-
finalScriptWitness: undefined,
|
|
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
|
}
|