-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathBitGoPsbt.ts
More file actions
332 lines (311 loc) · 12.3 KB
/
BitGoPsbt.ts
File metadata and controls
332 lines (311 loc) · 12.3 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
import { BitGoPsbt as WasmBitGoPsbt } from "../wasm/wasm_utxo.js";
import { type WalletKeysArg, RootWalletKeys } from "./RootWalletKeys.js";
import { type ReplayProtectionArg, ReplayProtection } from "./ReplayProtection.js";
import { type BIP32Arg, BIP32 } from "../bip32.js";
import { type ECPairArg, ECPair } from "../ecpair.js";
import type { UtxolibName } from "../utxolibCompat.js";
import type { CoinName } from "../coinName.js";
export type NetworkName = UtxolibName | CoinName;
export type ScriptId = { chain: number; index: number };
export type InputScriptType =
| "p2shP2pk"
| "p2sh"
| "p2shP2wsh"
| "p2wsh"
| "p2trLegacy"
| "p2trMusig2ScriptPath"
| "p2trMusig2KeyPath";
export type OutPoint = {
txid: string;
vout: number;
};
export type ParsedInput = {
previousOutput: OutPoint;
address: string;
script: Uint8Array;
value: bigint;
scriptId: ScriptId | null;
scriptType: InputScriptType;
};
export type ParsedOutput = {
address: string | null;
script: Uint8Array;
value: bigint;
scriptId: ScriptId | null;
};
export type ParsedTransaction = {
inputs: ParsedInput[];
outputs: ParsedOutput[];
spendAmount: bigint;
minerFee: bigint;
virtualSize: number;
};
export class BitGoPsbt {
private constructor(private wasm: WasmBitGoPsbt) {}
/**
* 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);
}
/**
* Get the unsigned transaction ID
* @returns The unsigned transaction ID
*/
unsignedTxid(): string {
return this.wasm.unsigned_txid();
}
/**
* Parse transaction with wallet keys to identify wallet inputs/outputs
* @param walletKeys - The wallet keys to use for identification
* @param replayProtection - Scripts that are allowed as inputs without wallet validation
* @returns Parsed transaction information
*/
parseTransactionWithWalletKeys(
walletKeys: WalletKeysArg,
replayProtection: ReplayProtectionArg,
): ParsedTransaction {
const keys = RootWalletKeys.from(walletKeys);
const rp = ReplayProtection.from(replayProtection, this.wasm.network());
return this.wasm.parse_transaction_with_wallet_keys(keys.wasm, rp.wasm) 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
* @returns Array of parsed outputs
* @note This method does NOT validate wallet inputs. It only parses outputs.
*/
parseOutputsWithWalletKeys(walletKeys: WalletKeysArg): ParsedOutput[] {
const keys = RootWalletKeys.from(walletKeys);
return this.wasm.parse_outputs_with_wallet_keys(keys.wasm) as ParsedOutput[];
}
/**
* 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 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 - derives the key and signs
* - A raw privkey (ECPairArg: Buffer, ECPair instance, or WasmECPair) for replay protection inputs - signs directly
*
* This method automatically detects and handles different input types:
* - For regular inputs: uses standard PSBT signing
* - For MuSig2 inputs: uses the FirstRound state stored by generateMusig2Nonces()
* - For replay protection inputs: signs with legacy P2SH sighash
*
* @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
* // Parse transaction to identify input types
* const parsed = psbt.parseTransactionWithWalletKeys(walletKeys, replayProtection);
*
* // Sign regular wallet inputs with xpriv
* for (let i = 0; i < parsed.inputs.length; i++) {
* const input = parsed.inputs[i];
* if (input.scriptId !== null && input.scriptType !== "p2shP2pk") {
* psbt.sign(i, userXpriv);
* }
* }
*
* // Sign replay protection inputs with raw privkey
* const userPrivkey = bip32.fromBase58(userXpriv).privateKey!;
* for (let i = 0; i < parsed.inputs.length; i++) {
* const input = parsed.inputs[i];
* if (input.scriptType === "p2shP2pk") {
* psbt.sign(i, userPrivkey);
* }
* }
* ```
*/
sign(inputIndex: number, key: BIP32Arg | ECPairArg): void {
// Detect key type
// If string or has 'derive' method → BIP32Arg
// Otherwise → ECPairArg
if (
typeof key === "string" ||
(typeof key === "object" &&
key !== null &&
"derive" in key &&
typeof key.derive === "function")
) {
// It's a BIP32Arg
const wasmKey = BIP32.from(key as BIP32Arg);
this.wasm.sign_with_xpriv(inputIndex, wasmKey.wasm);
} else {
// It's an ECPairArg
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 serialized transaction bytes
* @throws Error if the PSBT is not fully finalized or extraction fails
*/
extractTransaction(): Uint8Array {
return this.wasm.extract_transaction();
}
}