@btc-vision/transaction 1.6.1 → 1.6.4
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/epoch/ChallengeSolution.d.ts +3 -3
- package/browser/epoch/validator/EpochValidator.d.ts +5 -6
- package/browser/index.js +1 -1
- package/browser/keypair/AddressVerificator.d.ts +2 -1
- package/browser/transaction/builders/TransactionBuilder.d.ts +3 -0
- package/browser/transaction/interfaces/ITransactionParameters.d.ts +1 -0
- package/browser/transaction/shared/TweakedTransaction.d.ts +18 -0
- package/build/_version.d.ts +1 -1
- package/build/_version.js +1 -1
- package/build/epoch/ChallengeSolution.d.ts +3 -3
- package/build/epoch/ChallengeSolution.js +3 -3
- package/build/epoch/validator/EpochValidator.d.ts +5 -6
- package/build/epoch/validator/EpochValidator.js +11 -12
- package/build/keypair/AddressVerificator.d.ts +2 -1
- package/build/keypair/AddressVerificator.js +4 -0
- package/build/transaction/builders/TransactionBuilder.d.ts +3 -0
- package/build/transaction/builders/TransactionBuilder.js +18 -3
- package/build/transaction/interfaces/ITransactionParameters.d.ts +1 -0
- package/build/transaction/shared/TweakedTransaction.d.ts +18 -0
- package/build/transaction/shared/TweakedTransaction.js +135 -18
- package/package.json +2 -2
- package/src/_version.ts +1 -1
- package/src/epoch/ChallengeSolution.ts +4 -4
- package/src/epoch/validator/EpochValidator.ts +12 -16
- package/src/keypair/AddressVerificator.ts +7 -1
- package/src/transaction/builders/TransactionBuilder.ts +30 -3
- package/src/transaction/interfaces/ITransactionParameters.ts +1 -0
- package/src/transaction/shared/TweakedTransaction.ts +210 -23
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
address as bitAddress,
|
|
4
4
|
crypto as bitCrypto,
|
|
5
5
|
getFinalScripts,
|
|
6
|
+
isP2A,
|
|
6
7
|
isP2MS,
|
|
7
8
|
isP2PK,
|
|
8
9
|
isP2PKH,
|
|
@@ -14,7 +15,6 @@ import {
|
|
|
14
15
|
Network,
|
|
15
16
|
opcodes,
|
|
16
17
|
P2TRPayment,
|
|
17
|
-
Payment,
|
|
18
18
|
payments,
|
|
19
19
|
PaymentType,
|
|
20
20
|
Psbt,
|
|
@@ -38,6 +38,9 @@ import {
|
|
|
38
38
|
isTaprootInput,
|
|
39
39
|
pubkeyInScript,
|
|
40
40
|
} from '../../signer/SignerUtils.js';
|
|
41
|
+
import { TransactionBuilder } from '../builders/TransactionBuilder.js';
|
|
42
|
+
|
|
43
|
+
export type SupportedTransactionVersion = 1 | 2 | 3;
|
|
41
44
|
|
|
42
45
|
export interface ITweakedTransactionData {
|
|
43
46
|
readonly signer: Signer | ECPairInterface | UnisatSigner;
|
|
@@ -46,6 +49,7 @@ export interface ITweakedTransactionData {
|
|
|
46
49
|
readonly nonWitnessUtxo?: Buffer;
|
|
47
50
|
readonly noSignatures?: boolean;
|
|
48
51
|
readonly unlockScript?: Buffer[];
|
|
52
|
+
readonly txVersion?: SupportedTransactionVersion;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
/**
|
|
@@ -56,6 +60,13 @@ export enum TransactionSequence {
|
|
|
56
60
|
FINAL = 0xffffffff,
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
export enum CSVModes {
|
|
64
|
+
BLOCKS = 0,
|
|
65
|
+
TIMESTAMPS = 1,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const CSV_ENABLED_BLOCKS_MASK = 0x3fffffff;
|
|
69
|
+
|
|
59
70
|
/**
|
|
60
71
|
* @description PSBT Transaction processor.
|
|
61
72
|
* */
|
|
@@ -88,33 +99,40 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
88
99
|
* @protected
|
|
89
100
|
*/
|
|
90
101
|
protected abstract readonly transaction: Psbt;
|
|
102
|
+
|
|
91
103
|
/**
|
|
92
104
|
* @description The sighash types of the transaction
|
|
93
105
|
* @protected
|
|
94
106
|
*/
|
|
95
107
|
protected sighashTypes: number[] | undefined;
|
|
108
|
+
|
|
96
109
|
/**
|
|
97
110
|
* @description The script data of the transaction
|
|
98
111
|
*/
|
|
99
112
|
protected scriptData: P2TRPayment | null = null;
|
|
113
|
+
|
|
100
114
|
/**
|
|
101
115
|
* @description The tap data of the transaction
|
|
102
116
|
*/
|
|
103
117
|
protected tapData: P2TRPayment | null = null;
|
|
118
|
+
|
|
104
119
|
/**
|
|
105
120
|
* @description The inputs of the transaction
|
|
106
121
|
*/
|
|
107
122
|
protected readonly inputs: PsbtInputExtended[] = [];
|
|
123
|
+
|
|
108
124
|
/**
|
|
109
125
|
* @description The sequence of the transaction
|
|
110
126
|
* @protected
|
|
111
127
|
*/
|
|
112
128
|
protected sequence: number = TransactionSequence.REPLACE_BY_FEE;
|
|
129
|
+
|
|
113
130
|
/**
|
|
114
131
|
* The tap leaf script
|
|
115
132
|
* @protected
|
|
116
133
|
*/
|
|
117
134
|
protected tapLeafScript: TapLeafScript | null = null;
|
|
135
|
+
|
|
118
136
|
/**
|
|
119
137
|
* Add a non-witness utxo to the transaction
|
|
120
138
|
* @protected
|
|
@@ -127,11 +145,20 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
127
145
|
*/
|
|
128
146
|
protected readonly isBrowser: boolean = false;
|
|
129
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Track which inputs contain CSV scripts
|
|
150
|
+
* @protected
|
|
151
|
+
*/
|
|
152
|
+
protected csvInputIndices: Set<number> = new Set();
|
|
153
|
+
protected anchorInputIndices: Set<number> = new Set();
|
|
154
|
+
|
|
130
155
|
protected regenerated: boolean = false;
|
|
131
156
|
protected ignoreSignatureErrors: boolean = false;
|
|
132
157
|
protected noSignatures: boolean = false;
|
|
133
158
|
protected unlockScript: Buffer[] | undefined;
|
|
134
159
|
|
|
160
|
+
protected txVersion: SupportedTransactionVersion = 2;
|
|
161
|
+
|
|
135
162
|
protected constructor(data: ITweakedTransactionData) {
|
|
136
163
|
super();
|
|
137
164
|
|
|
@@ -143,6 +170,10 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
143
170
|
this.unlockScript = data.unlockScript;
|
|
144
171
|
|
|
145
172
|
this.isBrowser = typeof window !== 'undefined';
|
|
173
|
+
|
|
174
|
+
if (data.txVersion) {
|
|
175
|
+
this.txVersion = data.txVersion;
|
|
176
|
+
}
|
|
146
177
|
}
|
|
147
178
|
|
|
148
179
|
/**
|
|
@@ -303,6 +334,11 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
303
334
|
this.sequence = TransactionSequence.FINAL;
|
|
304
335
|
|
|
305
336
|
for (const input of this.inputs) {
|
|
337
|
+
// This would disable CSV! You need to check if the input has CSV
|
|
338
|
+
if (this.csvInputIndices.has(this.inputs.indexOf(input))) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
306
342
|
input.sequence = TransactionSequence.FINAL;
|
|
307
343
|
}
|
|
308
344
|
}
|
|
@@ -402,6 +438,8 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
402
438
|
reverse: boolean = false,
|
|
403
439
|
errored: boolean = false,
|
|
404
440
|
): Promise<void> {
|
|
441
|
+
if (this.anchorInputIndices.has(i)) return;
|
|
442
|
+
|
|
405
443
|
const publicKey = signer.publicKey;
|
|
406
444
|
|
|
407
445
|
let isTaproot = isTaprootInput(input);
|
|
@@ -672,7 +710,7 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
672
710
|
i: number,
|
|
673
711
|
_extra: boolean = false,
|
|
674
712
|
): PsbtInputExtended {
|
|
675
|
-
const
|
|
713
|
+
const scriptPub = Buffer.from(utxo.scriptPubKey.hex, 'hex');
|
|
676
714
|
|
|
677
715
|
const input: PsbtInputExtended = {
|
|
678
716
|
hash: utxo.transactionId,
|
|
@@ -680,12 +718,12 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
680
718
|
sequence: this.sequence,
|
|
681
719
|
witnessUtxo: {
|
|
682
720
|
value: Number(utxo.value),
|
|
683
|
-
script,
|
|
721
|
+
script: scriptPub,
|
|
684
722
|
},
|
|
685
723
|
};
|
|
686
724
|
|
|
687
725
|
// Handle P2PKH (Legacy)
|
|
688
|
-
if (isP2PKH(
|
|
726
|
+
if (isP2PKH(scriptPub)) {
|
|
689
727
|
// Legacy input requires nonWitnessUtxo
|
|
690
728
|
if (utxo.nonWitnessUtxo) {
|
|
691
729
|
//delete input.witnessUtxo;
|
|
@@ -698,28 +736,18 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
698
736
|
}
|
|
699
737
|
|
|
700
738
|
// Handle P2WPKH (SegWit)
|
|
701
|
-
else if (isP2WPKH(
|
|
739
|
+
else if (isP2WPKH(scriptPub) || isUnknownSegwitVersion(scriptPub)) {
|
|
702
740
|
// No redeemScript required for pure P2WPKH
|
|
703
741
|
// witnessUtxo is enough, no nonWitnessUtxo needed.
|
|
704
742
|
}
|
|
705
743
|
|
|
706
744
|
// Handle P2WSH (SegWit)
|
|
707
|
-
else if (isP2WSHScript(
|
|
708
|
-
|
|
709
|
-
if (!utxo.witnessScript) {
|
|
710
|
-
// Can't just invent a witnessScript out of thin air. If not provided, it's an error.
|
|
711
|
-
throw new Error('Missing witnessScript for P2WSH UTXO');
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
input.witnessScript = Buffer.isBuffer(utxo.witnessScript)
|
|
715
|
-
? utxo.witnessScript
|
|
716
|
-
: Buffer.from(utxo.witnessScript, 'hex');
|
|
717
|
-
|
|
718
|
-
// No nonWitnessUtxo needed for segwit
|
|
745
|
+
else if (isP2WSHScript(scriptPub)) {
|
|
746
|
+
this.processP2WSHInput(utxo, input, i);
|
|
719
747
|
}
|
|
720
748
|
|
|
721
749
|
// Handle P2SH (Can be legacy or wrapping segwit)
|
|
722
|
-
else if (isP2SHScript(
|
|
750
|
+
else if (isP2SHScript(scriptPub)) {
|
|
723
751
|
// Redeem script is required for P2SH
|
|
724
752
|
let redeemScriptBuf: Buffer | undefined;
|
|
725
753
|
|
|
@@ -771,9 +799,8 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
771
799
|
// P2SH-P2WSH
|
|
772
800
|
// Use witnessUtxo + redeemScript + witnessScript
|
|
773
801
|
delete input.nonWitnessUtxo; // ensure we do NOT have nonWitnessUtxo
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
802
|
+
|
|
803
|
+
this.processP2WSHInput(utxo, input, i);
|
|
777
804
|
} else {
|
|
778
805
|
// Legacy P2SH
|
|
779
806
|
// Use nonWitnessUtxo
|
|
@@ -782,7 +809,7 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
782
809
|
}
|
|
783
810
|
|
|
784
811
|
// Handle P2TR (Taproot)
|
|
785
|
-
else if (isP2TR(
|
|
812
|
+
else if (isP2TR(scriptPub)) {
|
|
786
813
|
// Taproot inputs do not require nonWitnessUtxo, witnessUtxo is sufficient.
|
|
787
814
|
|
|
788
815
|
// If there's a configured sighash type
|
|
@@ -796,8 +823,15 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
796
823
|
input.tapInternalKey = this.internalPubKeyToXOnly();
|
|
797
824
|
}
|
|
798
825
|
|
|
826
|
+
// Handle P2A (Any SegWit version, future versions)
|
|
827
|
+
else if (isP2A(scriptPub)) {
|
|
828
|
+
this.anchorInputIndices.add(i);
|
|
829
|
+
|
|
830
|
+
input.isPayToAnchor = true;
|
|
831
|
+
}
|
|
832
|
+
|
|
799
833
|
// Handle P2PK (legacy) or P2MS (bare multisig)
|
|
800
|
-
else if (isP2PK(
|
|
834
|
+
else if (isP2PK(scriptPub) || isP2MS(scriptPub)) {
|
|
801
835
|
// These are legacy scripts, need nonWitnessUtxo
|
|
802
836
|
if (utxo.nonWitnessUtxo) {
|
|
803
837
|
input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
|
|
@@ -836,6 +870,56 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
836
870
|
return input;
|
|
837
871
|
}
|
|
838
872
|
|
|
873
|
+
protected processP2WSHInput(utxo: UTXO, input: PsbtInputExtended, i: number): void {
|
|
874
|
+
// P2WSH requires a witnessScript
|
|
875
|
+
if (!utxo.witnessScript) {
|
|
876
|
+
// Can't just invent a witnessScript out of thin air. If not provided, it's an error.
|
|
877
|
+
throw new Error('Missing witnessScript for P2WSH UTXO');
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
input.witnessScript = Buffer.isBuffer(utxo.witnessScript)
|
|
881
|
+
? utxo.witnessScript
|
|
882
|
+
: Buffer.from(utxo.witnessScript, 'hex');
|
|
883
|
+
|
|
884
|
+
// No nonWitnessUtxo needed for segwit
|
|
885
|
+
|
|
886
|
+
const decompiled = script.decompile(input.witnessScript);
|
|
887
|
+
if (decompiled && this.isCSVScript(decompiled)) {
|
|
888
|
+
const decompiled = script.decompile(input.witnessScript);
|
|
889
|
+
if (decompiled && this.isCSVScript(decompiled)) {
|
|
890
|
+
this.csvInputIndices.add(i);
|
|
891
|
+
|
|
892
|
+
// Extract CSV value from witness script
|
|
893
|
+
const csvBlocks = this.extractCSVBlocks(decompiled);
|
|
894
|
+
|
|
895
|
+
console.log('csvBlocks', csvBlocks);
|
|
896
|
+
|
|
897
|
+
// Use the setCSVSequence method to properly set the sequence
|
|
898
|
+
input.sequence = this.setCSVSequence(csvBlocks, this.sequence);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
protected secondsToCSVTimeUnits(seconds: number): number {
|
|
904
|
+
return Math.floor(seconds / 512);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
protected createTimeBasedCSV(seconds: number): number {
|
|
908
|
+
const timeUnits = this.secondsToCSVTimeUnits(seconds);
|
|
909
|
+
if (timeUnits > 0xffff) {
|
|
910
|
+
throw new Error(`Time units ${timeUnits} exceeds maximum of 65,535`);
|
|
911
|
+
}
|
|
912
|
+
return timeUnits | (1 << 22);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
protected isCSVEnabled(sequence: number): boolean {
|
|
916
|
+
return (sequence & (1 << 31)) === 0;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
protected extractCSVValue(sequence: number): number {
|
|
920
|
+
return sequence & 0x0000ffff;
|
|
921
|
+
}
|
|
922
|
+
|
|
839
923
|
protected customFinalizerP2SH = (
|
|
840
924
|
inputIndex: number,
|
|
841
925
|
input: PsbtInput,
|
|
@@ -858,6 +942,33 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
858
942
|
};
|
|
859
943
|
}
|
|
860
944
|
|
|
945
|
+
if (this.anchorInputIndices.has(inputIndex)) {
|
|
946
|
+
return {
|
|
947
|
+
finalScriptSig: undefined,
|
|
948
|
+
finalScriptWitness: Buffer.from([0]),
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (isP2WSH && isSegwit && input.witnessScript) {
|
|
953
|
+
if (!input.partialSig || input.partialSig.length === 0) {
|
|
954
|
+
throw new Error(`No signatures for P2WSH input #${inputIndex}`);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Check if this is a CSV input
|
|
958
|
+
const isCSVInput = this.csvInputIndices.has(inputIndex);
|
|
959
|
+
if (isCSVInput) {
|
|
960
|
+
// For CSV P2WSH, the witness stack should be: [signature, witnessScript]
|
|
961
|
+
const witnessStack = [input.partialSig[0].signature, input.witnessScript];
|
|
962
|
+
return {
|
|
963
|
+
finalScriptSig: undefined,
|
|
964
|
+
finalScriptWitness:
|
|
965
|
+
TransactionBuilder.witnessStackToScriptWitness(witnessStack),
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// For non-CSV P2WSH, use default finalization
|
|
970
|
+
}
|
|
971
|
+
|
|
861
972
|
return getFinalScripts(
|
|
862
973
|
inputIndex,
|
|
863
974
|
input,
|
|
@@ -884,6 +995,82 @@ export abstract class TweakedTransaction extends Logger {
|
|
|
884
995
|
this.finalized = true;
|
|
885
996
|
}
|
|
886
997
|
|
|
998
|
+
protected isCSVScript(decompiled: (number | Buffer)[]): boolean {
|
|
999
|
+
return decompiled.some((op) => op === opcodes.OP_CHECKSEQUENCEVERIFY);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
protected setCSVSequence(csvBlocks: number, currentSequence: number): number {
|
|
1003
|
+
if (this.txVersion < 2) {
|
|
1004
|
+
throw new Error('CSV requires transaction version 2 or higher');
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (csvBlocks > 0xffff) {
|
|
1008
|
+
throw new Error(`CSV blocks ${csvBlocks} exceeds maximum of 65,535`);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Layout of nSequence field (32 bits) when CSV is active (bit 31 = 0):
|
|
1012
|
+
// Bit 31: Must be 0 (CSV enable flag)
|
|
1013
|
+
// Bits 23-30: Unused by BIP68 (available for custom use)
|
|
1014
|
+
// Bit 22: Time flag (0 = blocks, 1 = time)
|
|
1015
|
+
// Bits 16-21: Unused by BIP68 (available for custom use)
|
|
1016
|
+
// Bits 0-15: CSV lock-time value
|
|
1017
|
+
|
|
1018
|
+
// Extract the time flag if it's set in csvBlocks
|
|
1019
|
+
const isTimeBased = (csvBlocks & (1 << 22)) !== 0;
|
|
1020
|
+
|
|
1021
|
+
// Start with the CSV value
|
|
1022
|
+
let sequence = csvBlocks & 0x0000ffff;
|
|
1023
|
+
|
|
1024
|
+
// Preserve the time flag if set
|
|
1025
|
+
if (isTimeBased) {
|
|
1026
|
+
sequence |= 1 << 22;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (currentSequence === (TransactionSequence.REPLACE_BY_FEE as number)) {
|
|
1030
|
+
// Set bit 25 as our explicit RBF flag
|
|
1031
|
+
// This is in the unused range (bits 23-30) when CSV is active
|
|
1032
|
+
sequence |= 1 << 25;
|
|
1033
|
+
|
|
1034
|
+
// We could use other unused bits for version/features
|
|
1035
|
+
// sequence |= (1 << 26); // Could indicate tx flags if we wanted
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Final safety check: ensure bit 31 is 0 (CSV enabled)
|
|
1039
|
+
sequence = sequence & 0x7fffffff;
|
|
1040
|
+
|
|
1041
|
+
return sequence;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
protected getCSVType(csvValue: number): CSVModes {
|
|
1045
|
+
// Bit 22 determines if it's time-based (1) or block-based (0)
|
|
1046
|
+
return csvValue & (1 << 22) ? CSVModes.TIMESTAMPS : CSVModes.BLOCKS;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
private extractCSVBlocks(decompiled: (number | Buffer)[]): number {
|
|
1050
|
+
for (let i = 0; i < decompiled.length; i++) {
|
|
1051
|
+
if (decompiled[i] === opcodes.OP_CHECKSEQUENCEVERIFY && i > 0) {
|
|
1052
|
+
const csvValue = decompiled[i - 1];
|
|
1053
|
+
if (Buffer.isBuffer(csvValue)) {
|
|
1054
|
+
return script.number.decode(csvValue);
|
|
1055
|
+
} else if (typeof csvValue === 'number') {
|
|
1056
|
+
// Handle OP_N directly
|
|
1057
|
+
if (csvValue === opcodes.OP_0 || csvValue === opcodes.OP_FALSE) {
|
|
1058
|
+
return 0;
|
|
1059
|
+
} else if (csvValue === opcodes.OP_1NEGATE) {
|
|
1060
|
+
return -1;
|
|
1061
|
+
} else if (csvValue >= opcodes.OP_1 && csvValue <= opcodes.OP_16) {
|
|
1062
|
+
return csvValue - opcodes.OP_1 + 1;
|
|
1063
|
+
} else {
|
|
1064
|
+
// For other numbers, they should have been Buffers
|
|
1065
|
+
// This shouldn't happen in properly decompiled scripts
|
|
1066
|
+
throw new Error(`Unexpected raw number in script: ${csvValue}`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return 0;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
887
1074
|
private async attemptSignTaproot(
|
|
888
1075
|
transaction: Psbt,
|
|
889
1076
|
input: PsbtInput,
|