-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathBitGoPsbt.ts
More file actions
1070 lines (999 loc) · 36.1 KB
/
BitGoPsbt.ts
File metadata and controls
1070 lines (999 loc) · 36.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import {
BitGoPsbt as WasmBitGoPsbt,
FixedScriptWalletNamespace,
WasmBIP32,
type PsbtInputData,
type PsbtOutputData,
type PsbtOutputDataWithAddress,
} from "../wasm/wasm_utxo.js";
import type { IPsbtWithAddress } from "../psbt.js";
import { type WalletKeysArg, RootWalletKeys } from "./RootWalletKeys.js";
import { type ReplayProtectionArg, ReplayProtection } from "./ReplayProtection.js";
import { type BIP32Arg, BIP32, isBIP32Arg } from "../bip32.js";
import { type ECPairArg, ECPair } from "../ecpair.js";
import type { UtxolibName } from "../utxolibCompat.js";
import type { CoinName } from "../coinName.js";
import type { InputScriptType } from "./scriptType.js";
import type { PsbtKvKey } from "./BitGoKeySubtype.js";
import {
Transaction,
DashTransaction,
ZcashTransaction,
type ITransaction,
} from "../transaction.js";
export type { InputScriptType };
export type NetworkName = UtxolibName | CoinName;
export type ScriptId = { chain: number; index: number };
export type OutPoint = {
txid: string;
vout: number;
};
export type ParsedInput = {
previousOutput: OutPoint;
address: string;
script: Uint8Array;
value: bigint;
scriptId: ScriptId | null;
scriptType: InputScriptType;
sequence: number;
};
export type ParsedOutput = {
address: string | null;
script: Uint8Array;
value: bigint;
scriptId: ScriptId | null;
paygo: boolean;
};
export type ParsedTransaction = {
inputs: ParsedInput[];
outputs: ParsedOutput[];
spendAmount: bigint;
minerFee: bigint;
virtualSize: number;
};
export type CreateEmptyOptions = {
/** Transaction version (default: 2) */
version?: number;
/** Lock time (default: 0) */
lockTime?: number;
};
export type AddInputOptions = {
/** Previous transaction ID (hex string) */
txid: string;
/** Output index being spent */
vout: number;
/** Value in satoshis (for witness_utxo) */
value: bigint;
/** Sequence number (default: 0xFFFFFFFE for RBF) */
sequence?: number;
/** Full previous transaction (for non-segwit strict compliance) */
prevTx?: Uint8Array;
};
export type AddOutputOptions =
| {
script: Uint8Array;
/** Value in satoshis */
value: bigint;
}
| {
address: string;
/** Value in satoshis */
value: bigint;
};
/** Key identifier for signing ("user", "backup", or "bitgo") */
export type SignerKey = "user" | "backup" | "bitgo";
/** Specifies signer and cosigner for Taproot inputs */
export type SignPath = {
/** Key that will sign */
signer: SignerKey;
/** Key that will co-sign */
cosigner: SignerKey;
};
export type AddWalletInputOptions = {
/** Script location in wallet (chain + index) */
scriptId: ScriptId;
/** Sign path - required for p2tr/p2trMusig2 (chains 30-41) */
signPath?: SignPath;
};
export type AddWalletOutputOptions = {
/** Chain code (0/1=p2sh, 10/11=p2shP2wsh, 20/21=p2wsh, 30/31=p2tr, 40/41=p2trMusig2) */
chain: number;
/** Derivation index */
index: number;
/** Value in satoshis */
value: bigint;
};
export type ParseTransactionOptions = {
replayProtection: ReplayProtectionArg;
payGoPubkeys?: ECPairArg[];
};
export type ParseOutputsOptions = {
payGoPubkeys?: ECPairArg[];
};
export type HydrationUnspent = {
chain: number;
index: number;
value: bigint;
};
export class BitGoPsbt implements IPsbtWithAddress {
protected constructor(protected _wasm: WasmBitGoPsbt) {}
/**
* Get the underlying WASM instance
* @internal - for use by other wasm-utxo modules
*/
get wasm(): WasmBitGoPsbt {
return this._wasm;
}
/**
* Create an empty PSBT for the given network with wallet keys
*
* The wallet keys are used to set global xpubs in the PSBT, which identifies
* the keys that will be used for signing.
*
* For Zcash networks, use ZcashBitGoPsbt.createEmpty() instead.
*
* @param network - Network name (utxolib name like "bitcoin" or coin name like "btc")
* @param walletKeys - The wallet's root keys (sets global xpubs in the PSBT)
* @param options - Optional transaction parameters (version, lockTime)
* @returns A new empty BitGoPsbt instance
*
* @example
* ```typescript
* // Create empty PSBT with wallet keys
* const psbt = BitGoPsbt.createEmpty("bitcoin", walletKeys);
*
* // Create with custom version and lockTime
* const psbt = BitGoPsbt.createEmpty("bitcoin", walletKeys, { version: 1, lockTime: 500000 });
* ```
*/
static createEmpty(
network: NetworkName,
walletKeys: WalletKeysArg,
options?: CreateEmptyOptions,
): BitGoPsbt {
const keys = RootWalletKeys.from(walletKeys);
const wasmPsbt = WasmBitGoPsbt.create_empty(
network,
keys.wasm,
options?.version,
options?.lockTime,
);
return new BitGoPsbt(wasmPsbt);
}
/**
* Deserialize a PSBT from bytes
* @param bytes - The PSBT bytes
* @param network - The network to use for deserialization (either utxolib name like "bitcoin" or coin name like "btc")
* @returns A BitGoPsbt instance
*/
static fromBytes(bytes: Uint8Array, network: NetworkName): BitGoPsbt {
const wasm = WasmBitGoPsbt.from_bytes(bytes, network);
return new BitGoPsbt(wasm);
}
/**
* Convert a half-signed legacy transaction to a psbt-lite.
*
* Extracts partial signatures from scriptSig/witness and creates a PSBT
* with proper wallet metadata (bip32Derivation, scripts, witnessUtxo).
* Only supports p2sh, p2shP2wsh, and p2wsh inputs (not taproot).
*
* @param txBytes - The serialized half-signed legacy transaction
* @param network - Network name
* @param walletKeys - The wallet's root keys
* @param unspents - Chain, index, and value for each input
*/
static fromHalfSignedLegacyTransaction(
txBytes: Uint8Array,
network: NetworkName,
walletKeys: WalletKeysArg,
unspents: HydrationUnspent[],
): BitGoPsbt {
const keys = RootWalletKeys.from(walletKeys);
const wasm = WasmBitGoPsbt.from_half_signed_legacy_transaction(
txBytes,
network,
keys.wasm,
unspents,
);
return new BitGoPsbt(wasm);
}
/**
* Add an input to the PSBT
*
* This adds a transaction input and corresponding PSBT input metadata.
* The witness_utxo is automatically populated for modern signing compatibility.
*
* @param options - Input options (txid, vout, value, sequence)
* @param script - Output script of the UTXO being spent
* @returns The index of the newly added input
*
* @example
* ```typescript
* const inputIndex = psbt.addInput({
* txid: "abc123...",
* vout: 0,
* value: 100000n,
* }, outputScript);
* ```
*/
addInputAtIndex(
index: number,
txid: string,
vout: number,
value: bigint,
script: Uint8Array,
sequence?: number,
): number;
addInputAtIndex(index: number, options: AddInputOptions, script: Uint8Array): number;
addInputAtIndex(
index: number,
txidOrOptions: string | AddInputOptions,
voutOrScript: number | Uint8Array,
value?: bigint,
script?: Uint8Array,
sequence?: number,
): number {
if (typeof txidOrOptions === "string") {
return this._wasm.add_input_at_index(
index,
txidOrOptions,
voutOrScript as number,
value,
script,
sequence,
);
}
const options = txidOrOptions;
return this._wasm.add_input_at_index(
index,
options.txid,
options.vout,
options.value,
voutOrScript as Uint8Array,
options.sequence,
options.prevTx,
);
}
addInput(options: AddInputOptions, script: Uint8Array): number {
return this._wasm.add_input(
options.txid,
options.vout,
options.value,
script,
options.sequence,
options.prevTx,
);
}
/**
* Add an output to the PSBT
*
* @param script - The output script (scriptPubKey)
* @param value - Value in satoshis
* @returns The index of the newly added output
*
* @example
* ```typescript
* const outputIndex = psbt.addOutput(outputScript, 50000n);
* ```
*/
addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number;
addOutputAtIndex(index: number, address: string, value: bigint): number;
addOutputAtIndex(index: number, options: AddOutputOptions): number;
addOutputAtIndex(
index: number,
scriptOrOptions: Uint8Array | string | AddOutputOptions,
value?: bigint,
): number {
if (scriptOrOptions instanceof Uint8Array || typeof scriptOrOptions === "string") {
if (value === undefined) {
throw new Error("Value is required when passing a script or address");
}
if (scriptOrOptions instanceof Uint8Array) {
return this._wasm.add_output_at_index(index, scriptOrOptions, value);
}
return this._wasm.add_output_with_address_at_index(index, scriptOrOptions, value);
}
const options = scriptOrOptions;
if ("script" in options) {
return this._wasm.add_output_at_index(index, options.script, options.value);
}
if ("address" in options) {
return this._wasm.add_output_with_address_at_index(index, options.address, options.value);
}
throw new Error("Invalid output options");
}
addOutput(script: Uint8Array, value: bigint): number;
addOutput(address: string, value: bigint): number;
addOutput(options: AddOutputOptions): number;
addOutput(scriptOrOptions: Uint8Array | string | AddOutputOptions, value?: bigint): number {
if (scriptOrOptions instanceof Uint8Array || typeof scriptOrOptions === "string") {
if (value === undefined) {
throw new Error("Value is required when passing a script or address");
}
if (scriptOrOptions instanceof Uint8Array) {
return this._wasm.add_output(scriptOrOptions, value);
}
return this._wasm.add_output_with_address(scriptOrOptions, value);
}
const options = scriptOrOptions;
if ("script" in options) {
return this._wasm.add_output(options.script, options.value);
}
if ("address" in options) {
return this._wasm.add_output_with_address(options.address, options.value);
}
throw new Error("Invalid output options");
}
/**
* Add a wallet input with full PSBT metadata
*
* This is a higher-level method that adds an input and populates all required
* PSBT fields (scripts, derivation info, etc.) based on the wallet's chain type.
*
* For p2sh/p2shP2wsh/p2wsh: Sets bip32Derivation, witnessScript, redeemScript (signPath not needed)
* For p2tr/p2trMusig2 script path: Sets tapLeafScript, tapBip32Derivation (signPath required)
* For p2trMusig2 key path: Sets tapInternalKey, tapMerkleRoot, tapBip32Derivation, musig2 participants (signPath required)
*
* @param inputOptions - Common input options (txid, vout, value, sequence)
* @param walletKeys - The wallet's root keys
* @param walletOptions - Wallet-specific options (scriptId, signPath, prevTx)
* @returns The index of the newly added input
*
* @example
* ```typescript
* // Add a p2shP2wsh input (signPath not needed)
* const inputIndex = psbt.addWalletInput(
* { txid: "abc123...", vout: 0, value: 100000n },
* walletKeys,
* { scriptId: { chain: 10, index: 0 } }, // p2shP2wsh external
* );
*
* // Add a p2trMusig2 key path input (signPath required)
* const inputIndex = psbt.addWalletInput(
* { txid: "def456...", vout: 1, value: 50000n },
* walletKeys,
* { scriptId: { chain: 40, index: 5 }, signPath: { signer: "user", cosigner: "bitgo" } },
* );
*
* // Add p2trMusig2 with backup key (script path spend)
* const inputIndex = psbt.addWalletInput(
* { txid: "ghi789...", vout: 0, value: 75000n },
* walletKeys,
* { scriptId: { chain: 40, index: 3 }, signPath: { signer: "user", cosigner: "backup" } },
* );
* ```
*/
addWalletInputAtIndex(
index: number,
inputOptions: AddInputOptions,
walletKeys: WalletKeysArg,
walletOptions: AddWalletInputOptions,
): number {
const keys = RootWalletKeys.from(walletKeys);
return this._wasm.add_wallet_input_at_index(
index,
inputOptions.txid,
inputOptions.vout,
inputOptions.value,
keys.wasm,
walletOptions.scriptId.chain,
walletOptions.scriptId.index,
walletOptions.signPath?.signer,
walletOptions.signPath?.cosigner,
inputOptions.sequence,
inputOptions.prevTx,
);
}
addWalletInput(
inputOptions: AddInputOptions,
walletKeys: WalletKeysArg,
walletOptions: AddWalletInputOptions,
): number {
const keys = RootWalletKeys.from(walletKeys);
return this._wasm.add_wallet_input(
inputOptions.txid,
inputOptions.vout,
inputOptions.value,
keys.wasm,
walletOptions.scriptId.chain,
walletOptions.scriptId.index,
walletOptions.signPath?.signer,
walletOptions.signPath?.cosigner,
inputOptions.sequence,
inputOptions.prevTx,
);
}
/**
* Add a wallet output with full PSBT metadata
*
* This creates a verifiable wallet output (typically for change) with all required
* PSBT fields (scripts, derivation info) based on the wallet's chain type.
*
* For p2sh/p2shP2wsh/p2wsh: Sets bip32Derivation, witnessScript, redeemScript
* For p2tr/p2trMusig2: Sets tapInternalKey, tapBip32Derivation
*
* @param walletKeys - The wallet's root keys
* @param options - Output options including chain, index, and value
* @returns The index of the newly added output
*
* @example
* ```typescript
* // Add a p2shP2wsh change output
* const outputIndex = psbt.addWalletOutput(walletKeys, {
* chain: 11, // p2shP2wsh internal (change)
* index: 0,
* value: 50000n,
* });
*
* // Add a p2trMusig2 change output
* const outputIndex = psbt.addWalletOutput(walletKeys, {
* chain: 41, // p2trMusig2 internal (change)
* index: 5,
* value: 25000n,
* });
* ```
*/
addWalletOutputAtIndex(
index: number,
walletKeys: WalletKeysArg,
options: AddWalletOutputOptions,
): number {
const keys = RootWalletKeys.from(walletKeys);
return this._wasm.add_wallet_output_at_index(
index,
options.chain,
options.index,
options.value,
keys.wasm,
);
}
addWalletOutput(walletKeys: WalletKeysArg, options: AddWalletOutputOptions): number {
const keys = RootWalletKeys.from(walletKeys);
return this._wasm.add_wallet_output(options.chain, options.index, options.value, keys.wasm);
}
/**
* Add a replay protection input to the PSBT
*
* Replay protection inputs are P2SH-P2PK inputs used on forked networks to prevent
* transaction replay attacks. They use a simple pubkey script without wallet derivation.
*
* @param inputOptions - Common input options (txid, vout, value, sequence)
* @param key - ECPair containing the public key for the replay protection input
* @returns The index of the newly added input
*
* @example
* ```typescript
* // Add a replay protection input using ECPair
* const inputIndex = psbt.addReplayProtectionInput(
* { txid: "abc123...", vout: 0, value: 1000n },
* replayProtectionKey,
* );
* ```
*/
addReplayProtectionInputAtIndex(
index: number,
inputOptions: AddInputOptions,
key: ECPairArg,
): number {
const ecpair = ECPair.from(key);
return this._wasm.add_replay_protection_input_at_index(
index,
ecpair.wasm,
inputOptions.txid,
inputOptions.vout,
inputOptions.value,
inputOptions.sequence,
inputOptions.prevTx,
);
}
addReplayProtectionInput(inputOptions: AddInputOptions, key: ECPairArg): number {
const ecpair = ECPair.from(key);
return this._wasm.add_replay_protection_input(
ecpair.wasm,
inputOptions.txid,
inputOptions.vout,
inputOptions.value,
inputOptions.sequence,
inputOptions.prevTx,
);
}
removeInput(index: number): void {
this._wasm.remove_input(index);
}
removeOutput(index: number): void {
this._wasm.remove_output(index);
}
/**
* Get the unsigned transaction ID
* @returns The unsigned transaction ID
*/
unsignedTxId(): string {
return this._wasm.unsigned_txid();
}
/**
* Get the transaction version
* @returns The transaction version number
*/
version(): number {
return this._wasm.version();
}
lockTime(): number {
return this._wasm.lock_time();
}
/** Set an arbitrary KV pair on the PSBT global map. */
setKV(key: PsbtKvKey, value: Uint8Array): void {
this._wasm.set_kv(key, value);
}
/** Get a KV value from the PSBT global map. Returns `undefined` if not present. */
getKV(key: PsbtKvKey): Uint8Array | undefined {
return this._wasm.get_kv(key) ?? undefined;
}
/** Set an arbitrary KV pair on a specific PSBT input. */
setInputKV(index: number, key: PsbtKvKey, value: Uint8Array): void {
this._wasm.set_input_kv(index, key, value);
}
/** Get a KV value from a specific PSBT input. Returns `undefined` if not present. */
getInputKV(index: number, key: PsbtKvKey): Uint8Array | undefined {
return this._wasm.get_input_kv(index, key) ?? undefined;
}
/** Set an arbitrary KV pair on a specific PSBT output. */
setOutputKV(index: number, key: PsbtKvKey, value: Uint8Array): void {
this._wasm.set_output_kv(index, key, value);
}
/** Get a KV value from a specific PSBT output. Returns `undefined` if not present. */
getOutputKV(index: number, key: PsbtKvKey): Uint8Array | undefined {
return this._wasm.get_output_kv(index, key) ?? undefined;
}
/**
* Parse transaction with wallet keys to identify wallet inputs/outputs
* @param walletKeys - The wallet keys to use for identification
* @param options - Options for parsing
* @param options.replayProtection - Scripts that are allowed as inputs without wallet validation
* @param options.payGoPubkeys - Optional public keys for PayGo attestation verification
* @returns Parsed transaction information
*/
parseTransactionWithWalletKeys(
walletKeys: WalletKeysArg,
options: ParseTransactionOptions,
): ParsedTransaction {
const keys = RootWalletKeys.from(walletKeys);
const rp = ReplayProtection.from(options.replayProtection, this._wasm.network());
const pubkeys = options.payGoPubkeys?.map((arg) => ECPair.from(arg).wasm);
return this._wasm.parse_transaction_with_wallet_keys(
keys.wasm,
rp.wasm,
pubkeys,
) as ParsedTransaction;
}
/**
* Parse outputs with wallet keys to identify which outputs belong to a wallet
* with the given wallet keys.
*
* This is useful in cases where we want to identify outputs that belong to a different
* wallet than the inputs.
*
* @param walletKeys - The wallet keys to use for identification
* @param options - Optional options for parsing
* @param options.payGoPubkeys - Optional public keys for PayGo attestation verification
* @returns Array of parsed outputs
* @note This method does NOT validate wallet inputs. It only parses outputs.
*/
parseOutputsWithWalletKeys(
walletKeys: WalletKeysArg,
options?: ParseOutputsOptions,
): ParsedOutput[] {
const keys = RootWalletKeys.from(walletKeys);
const pubkeys = options?.payGoPubkeys?.map((arg) => ECPair.from(arg).wasm);
return this._wasm.parse_outputs_with_wallet_keys(keys.wasm, pubkeys) as ParsedOutput[];
}
/**
* Add a PayGo attestation to a PSBT output
*
* This adds a cryptographic proof that the output address was authorized by a signing authority.
* The attestation is stored in PSBT proprietary key-values and can be verified later.
*
* @param outputIndex - The index of the output to add the attestation to
* @param entropy - 64 bytes of entropy (must be exactly 64 bytes)
* @param signature - ECDSA signature bytes (typically 65 bytes in recoverable format)
* @throws Error if output index is out of bounds or entropy is not 64 bytes
*/
addPayGoAttestation(outputIndex: number, entropy: Uint8Array, signature: Uint8Array): void {
this._wasm.add_paygo_attestation(outputIndex, entropy, signature);
}
/**
* Verify if a valid signature exists for a given key at the specified input index.
*
* This method can verify signatures using either:
* - Extended public key (xpub): Derives the public key using the derivation path from PSBT
* - ECPair (private key): Extracts the public key and verifies directly
*
* When using xpub, it supports:
* - ECDSA signatures (for legacy/SegWit inputs)
* - Schnorr signatures (for Taproot script path inputs)
* - MuSig2 partial signatures (for Taproot keypath MuSig2 inputs)
*
* When using ECPair, it supports:
* - ECDSA signatures (for legacy/SegWit inputs)
* - Schnorr signatures (for Taproot script path inputs)
* Note: MuSig2 inputs require xpubs for derivation
*
* @param inputIndex - The index of the input to check (0-based)
* @param key - Either an extended public key (base58 string, BIP32 instance, or WasmBIP32) or an ECPair (private key Buffer, ECPair instance, or WasmECPair)
* @returns true if a valid signature exists, false if no signature exists
* @throws Error if input index is out of bounds, key is invalid, or verification fails
*
* @example
* ```typescript
* // Verify wallet input signature with xpub
* const hasUserSig = psbt.verifySignature(0, userXpub);
*
* // Verify signature with ECPair (private key)
* const ecpair = ECPair.fromPrivateKey(privateKeyBuffer);
* const hasReplaySig = psbt.verifySignature(1, ecpair);
*
* // Or pass private key directly
* const hasReplaySig2 = psbt.verifySignature(1, privateKeyBuffer);
* ```
*/
verifySignature(inputIndex: number, key: BIP32Arg | ECPairArg): boolean {
// Try to parse as BIP32Arg first (string or BIP32 instance)
if (typeof key === "string" || ("derive" in key && typeof key.derive === "function")) {
const wasmKey = BIP32.from(key as BIP32Arg).wasm;
return this._wasm.verify_signature_with_xpub(inputIndex, wasmKey);
}
// Otherwise it's an ECPairArg (Uint8Array, ECPair, or WasmECPair)
const wasmECPair = ECPair.from(key as ECPairArg).wasm;
return this._wasm.verify_signature_with_pub(inputIndex, wasmECPair);
}
/**
* Sign all matching inputs with a private key.
*
* This method signs all inputs that match the provided key in a single efficient pass.
* It accepts either:
* - An xpriv (BIP32Arg: base58 string, BIP32 instance, or WasmBIP32) for wallet inputs
* - A raw privkey (ECPairArg: Buffer, ECPair instance, or WasmECPair) for replay protection inputs
*
* **Note:** MuSig2 inputs are skipped by this method when using xpriv because they require
* FirstRound state. After calling this method, sign MuSig2 inputs individually using
* `signInput()` after calling `generateMusig2Nonces()`.
*
* @param key - Either an xpriv (BIP32Arg) or a raw privkey (ECPairArg)
* @returns Array of input indices that were signed
* @throws Error if signing fails
*
* @example
* ```typescript
* // Sign all wallet inputs with user's xpriv
* const signedIndices = psbt.sign(userXpriv);
* console.log(`Signed inputs: ${signedIndices.join(", ")}`);
*
* // Sign all replay protection inputs with raw privkey
* const rpSignedIndices = psbt.sign(replayProtectionPrivkey);
* ```
*/
sign(key: BIP32Arg | ECPairArg): number[];
/**
* Sign a single input with a private key.
*
* @deprecated Use `sign(key)` to sign all matching inputs (more efficient), or use
* `signInput(inputIndex, key)` for explicit single-input signing.
*
* **Note:** This method is NOT more efficient than `sign(key)` for non-MuSig2 inputs.
* The underlying miniscript library signs all inputs regardless. This overload exists
* for backward compatibility only.
*
* @param inputIndex - The index of the input to sign (0-based)
* @param key - Either an xpriv (BIP32Arg) or a raw privkey (ECPairArg)
* @throws Error if signing fails, or if generateMusig2Nonces() was not called first for MuSig2 inputs
*/
sign(inputIndex: number, key: BIP32Arg | ECPairArg): void;
sign(
inputIndexOrKey: number | BIP32Arg | ECPairArg,
key?: BIP32Arg | ECPairArg,
): number[] | void {
// Detect which overload was called
if (typeof inputIndexOrKey === "number") {
// Called as sign(inputIndex, key) - deprecated single-input signing
if (key === undefined) {
throw new Error("Key is required when signing a single input");
}
this.signInput(inputIndexOrKey, key);
return;
}
// Called as sign(key) - sign all matching inputs
const keyArg = inputIndexOrKey;
if (isBIP32Arg(keyArg)) {
// It's a BIP32Arg - sign all wallet inputs (ECDSA + MuSig2)
const wasmKey = BIP32.from(keyArg);
// Sign all non-MuSig2 wallet inputs
const walletSigned = this._wasm.sign_all_wallet_inputs(wasmKey.wasm) as number[];
// Sign all MuSig2 keypath inputs (more efficient - reuses SighashCache)
const musig2Signed = this._wasm.sign_all_musig2_inputs(wasmKey.wasm) as number[];
return [...walletSigned, ...musig2Signed];
} else {
// It's an ECPairArg - sign all replay protection inputs
const wasmKey = ECPair.from(keyArg as ECPairArg);
return this._wasm.sign_replay_protection_inputs(wasmKey.wasm) as number[];
}
}
/**
* Sign a single input with a private key.
*
* This method signs a specific input using the provided key. It accepts either:
* - An xpriv (BIP32Arg: base58 string, BIP32 instance, or WasmBIP32) for wallet inputs
* - A raw privkey (ECPairArg: Buffer, ECPair instance, or WasmECPair) for replay protection inputs
*
* **Important:** This method is NOT faster than `sign(key)` for non-MuSig2 inputs.
* The underlying miniscript library signs all inputs regardless. This method uses a
* save/restore pattern to ensure only the target input receives the signature.
*
* Use this method only when you need precise control over which inputs are signed,
* for example:
* - Signing MuSig2 inputs (after calling generateMusig2Nonces())
* - Mixed transactions where different inputs need different keys
* - Testing or debugging signing behavior
*
* @param inputIndex - The index of the input to sign (0-based)
* @param key - Either an xpriv (BIP32Arg) or a raw privkey (ECPairArg)
* @throws Error if signing fails, or if generateMusig2Nonces() was not called first for MuSig2 inputs
*
* @example
* ```typescript
* // Sign a specific MuSig2 input after nonce generation
* psbt.generateMusig2Nonces(userXpriv);
* psbt.signInput(musig2InputIndex, userXpriv);
*
* // Sign a specific replay protection input
* psbt.signInput(rpInputIndex, replayProtectionPrivkey);
* ```
*/
signInput(inputIndex: number, key: BIP32Arg | ECPairArg): void {
if (isBIP32Arg(key)) {
// It's a BIP32Arg
const wasmKey = BIP32.from(key);
// Route to the appropriate method based on input type
if (this._wasm.is_musig2_input(inputIndex)) {
// MuSig2 keypath: true single-input signing (efficient)
this._wasm.sign_musig2_input(inputIndex, wasmKey.wasm);
} else {
// ECDSA/Schnorr script path: save/restore pattern (not faster than bulk)
this._wasm.sign_wallet_input(inputIndex, wasmKey.wasm);
}
} else {
// It's an ECPairArg - for replay protection inputs
const wasmKey = ECPair.from(key as ECPairArg);
this._wasm.sign_with_privkey(inputIndex, wasmKey.wasm);
}
}
/**
* @deprecated - use verifySignature with the replay protection key instead
*
* Verify if a replay protection input has a valid signature.
*
* This method checks if a given input is a replay protection input (like P2shP2pk) and verifies
* the signature. Replay protection inputs don't use standard derivation paths, so this method
* verifies signatures without deriving from xpub.
*
* For P2PK replay protection inputs, this:
* - Extracts the signature from final_script_sig
* - Extracts the public key from redeem_script
* - Computes the legacy P2SH sighash
* - Verifies the ECDSA signature cryptographically
*
* @param inputIndex - The index of the input to check (0-based)
* @param replayProtection - Scripts that identify replay protection inputs (same format as parseTransactionWithWalletKeys)
* @returns true if the input is a replay protection input and has a valid signature, false if no valid signature
* @throws Error if the input is not a replay protection input, index is out of bounds, or scripts are invalid
*/
verifyReplayProtectionSignature(
inputIndex: number,
replayProtection: ReplayProtectionArg,
): boolean {
const rp = ReplayProtection.from(replayProtection, this._wasm.network());
return this._wasm.verify_replay_protection_signature(inputIndex, rp.wasm);
}
/**
* Serialize the PSBT to bytes
*
* @returns The serialized PSBT as a byte array
*/
serialize(): Uint8Array {
return this._wasm.serialize();
}
/**
* Generate and store MuSig2 nonces for all MuSig2 inputs
*
* This method generates nonces using the State-Machine API and stores them in the PSBT.
* The nonces are stored as proprietary fields in the PSBT and will be included when serialized.
* After ALL participants have generated their nonces, you can sign MuSig2 inputs using
* sign().
*
* @param key - The extended private key (xpriv) for signing. Can be a base58 string, BIP32 instance, or WasmBIP32
* @param sessionId - Optional 32-byte session ID for nonce generation. **Only allowed on testnets**.
* On mainnets, a secure random session ID is always generated automatically.
* Must be unique per signing session.
* @throws Error if nonce generation fails, sessionId length is invalid, or custom sessionId is
* provided on a mainnet (security restriction)
*
* @security The sessionId MUST be cryptographically random and unique for each signing session.
* Never reuse a sessionId with the same key! On mainnets, sessionId is always randomly
* generated for security. Custom sessionId is only allowed on testnets for testing purposes.
*
* @example
* ```typescript
* // Phase 1: Both parties generate nonces (with auto-generated session ID)
* psbt.generateMusig2Nonces(userXpriv);
* // Nonces are stored in the PSBT
* // Send PSBT to counterparty
*
* // Phase 2: After receiving counterparty PSBT with their nonces
* const counterpartyPsbt = BitGoPsbt.fromBytes(counterpartyPsbtBytes, network);
* psbt.combineMusig2Nonces(counterpartyPsbt);
* // Sign MuSig2 key path inputs
* const parsed = psbt.parseTransactionWithWalletKeys(walletKeys, { replayProtection });
* for (let i = 0; i < parsed.inputs.length; i++) {
* if (parsed.inputs[i].scriptType === "p2trMusig2KeyPath") {
* psbt.sign(i, userXpriv);
* }
* }
* ```
*/
generateMusig2Nonces(key: BIP32Arg, sessionId?: Uint8Array): void {
const wasmKey = BIP32.from(key);
this._wasm.generate_musig2_nonces(wasmKey.wasm, sessionId);
}
/**
* Combine/merge data from another PSBT into this one
*
* This method copies MuSig2 nonces and signatures (proprietary key-value pairs) from the
* source PSBT to this PSBT. This is useful for merging PSBTs during the nonce exchange
* and signature collection phases.
*
* @param sourcePsbt - The source PSBT containing data to merge
* @throws Error if networks don't match
*
* @example
* ```typescript
* // After receiving counterparty's PSBT with their nonces
* const counterpartyPsbt = BitGoPsbt.fromBytes(counterpartyPsbtBytes, network);
* psbt.combineMusig2Nonces(counterpartyPsbt);
* // Now can sign with all nonces present
* psbt.sign(0, userXpriv);
* ```
*/
combineMusig2Nonces(sourcePsbt: BitGoPsbt): void {
this._wasm.combine_musig2_nonces(sourcePsbt.wasm);
}
/**
* Finalize all inputs in the PSBT
*
* @throws Error if any input failed to finalize
*/
finalizeAllInputs(): void {
this._wasm.finalize_all_inputs();
}
/**
* Extract the final transaction from a finalized PSBT
*
* @returns The extracted transaction instance
* @throws Error if the PSBT is not fully finalized or extraction fails
*/
extractTransaction(): ITransaction {
const networkType = this._wasm.get_network_type();
const wasm: unknown = this._wasm.extract_transaction();
switch (networkType) {
case "dash":
return DashTransaction.fromWasm(wasm as Parameters<typeof DashTransaction.fromWasm>[0]);
case "zcash":
return ZcashTransaction.fromWasm(wasm as Parameters<typeof ZcashTransaction.fromWasm>[0]);
default:
return Transaction.fromWasm(wasm as Parameters<typeof Transaction.fromWasm>[0]);
}
}
/**
* Extract a half-signed transaction in legacy format for p2ms-based script types.
*
* This method extracts a transaction where each input has exactly one signature,
* formatted in the legacy style used by utxo-lib and bitcoinjs-lib. The legacy
* format places signatures in the correct position (0, 1, or 2) based on which
* key signed, with empty placeholders for unsigned positions.
*
* Requirements:
* - All inputs must be p2ms-based (p2sh, p2shP2wsh, or p2wsh)
* - Each input must have exactly 1 partial signature
*
* @returns The serialized half-signed transaction bytes
* @throws Error if any input is not a p2ms type (Taproot, replay protection, etc.)
* @throws Error if any input has 0 or more than 1 partial signature
*
* @example
* ```typescript
* // Sign with user key only
* psbt.sign(userXpriv);
*
* // Extract half-signed transaction in legacy format
* const halfSignedTx = psbt.getHalfSignedLegacyFormat();
* ```
*/
getHalfSignedLegacyFormat(): Uint8Array {
return this._wasm.extract_half_signed_legacy_tx();
}
/**
* Get the number of inputs in the PSBT
* @returns The number of inputs
*/
inputCount(): number {
return this._wasm.input_count();
}
outputCount(): number {
return this._wasm.output_count();
}
/**
* Get all PSBT inputs as an array
*