@scure/btc-signer 2.0.0 → 2.2.0

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/src/payment.ts CHANGED
@@ -1,43 +1,57 @@
1
1
  import { bech32, bech32m, type Coder, createBase58check, hex } from '@scure/base';
2
+ import { anumber } from '@noble/hashes/utils.js';
3
+ import { abytes } from '@noble/curves/utils.js';
2
4
  import * as P from 'micro-packed';
3
5
  import { TaprootControlBlock, type TransactionInput } from './psbt.ts';
4
- import { OpToNum, Script, type ScriptType, VarBytes } from './script.ts';
6
+ import { MAX_SCRIPT_BYTE_LENGTH, OpToNum, Script, type ScriptType, VarBytes } from './script.ts';
5
7
  import * as u from './utils.ts';
6
- import { type BTC_NETWORK, type Bytes, NETWORK } from './utils.ts';
8
+ import { type BTC_NETWORK, type Bytes, NETWORK, type TArg, type TRet } from './utils.ts';
7
9
 
8
10
  // We need following items:
9
11
  // - encode/decode output script
10
12
  // - generate input script
11
13
  // - generate address/output/redeem from user input
12
14
  // P2ret represents generic interface for all p2* methods
15
+ /** Common shape returned by payment helper constructors. */
13
16
  export type P2Ret = {
17
+ /** Payment-script tag such as `pkh`, `wpkh`, or `tr`. */
14
18
  type: string;
19
+ /** Serialized output script for the payment descriptor. */
15
20
  script: Bytes;
21
+ /** Encoded address when the script has a standard address form. */
16
22
  address?: string;
23
+ /** Redeem script for wrapped script-hash descriptors. */
17
24
  redeemScript?: Bytes;
25
+ /** Witness script for SegWit script-hash descriptors. */
18
26
  witnessScript?: Bytes;
27
+ /** Hash committed by the output script when applicable. */
19
28
  hash?: Bytes;
20
29
  };
21
30
 
22
31
  // Pay to Anchor (P2A)
23
32
  type OutP2AType = { type: 'p2a'; script: Bytes };
24
33
  const OutP2A: Coder<OptScript, OutP2AType | undefined> = {
25
- encode(from: ScriptType): OutP2AType | undefined {
34
+ encode(from: TArg<ScriptType>): TRet<OutP2AType | undefined> {
35
+ // BIP433 defines P2A as the exact OP_1 <0x4e73> scriptPubKey.
26
36
  if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1]) || hex.encode(from[1]) !== '4e73')
27
37
  return;
28
- return { type: 'p2a', script: Script.encode(from) };
38
+ return { type: 'p2a', script: Script.encode(from) } as TRet<OutP2AType | undefined>;
29
39
  },
30
- decode: (to: OutP2AType): OptScript => {
40
+ decode: (to: TArg<OutP2AType>): TRet<OptScript> => {
31
41
  if (to.type !== 'p2a') return;
32
- return [1, hex.decode('4e73')];
42
+ // The decoded object keeps `script` for caller convenience, but the `p2a`
43
+ // tag always canonicalizes back to the fixed BIP433 script.
44
+ return [1, hex.decode('4e73')] as TRet<OptScript>;
33
45
  },
34
46
  };
35
47
 
36
48
  // Public Key (P2PK)
37
49
  type OutPKType = { type: 'pk'; pubkey: Bytes };
50
+ /** Optional parsed script result used by output-script coders. */
38
51
  export type OptScript = ScriptType | undefined;
39
52
 
40
- function isValidPubkey(pub: Bytes, type: u.PubT): boolean {
53
+ function isValidPubkey(pub: TArg<Bytes>, type: u.PubT): boolean {
54
+ // Payment coders use a boolean guard here and normalize validatePubkey failures to false.
41
55
  try {
42
56
  u.validatePubkey(pub, type);
43
57
  return true;
@@ -47,7 +61,9 @@ function isValidPubkey(pub: Bytes, type: u.PubT): boolean {
47
61
  }
48
62
 
49
63
  const OutPK: Coder<OptScript, OutPKType | undefined> = {
50
- encode(from: ScriptType): OutPKType | undefined {
64
+ encode(from: TArg<ScriptType>): TRet<OutPKType | undefined> {
65
+ // BIP380/BIP381 `pk(KEY)` only admits SEC1 ECDSA pubkeys here; x-only
66
+ // 32-byte CHECKSIG scripts are left for the later tapscript coders.
51
67
  if (
52
68
  from.length !== 2 ||
53
69
  !u.isBytes(from[0]) ||
@@ -55,61 +71,87 @@ const OutPK: Coder<OptScript, OutPKType | undefined> = {
55
71
  from[1] !== 'CHECKSIG'
56
72
  )
57
73
  return;
58
- return { type: 'pk', pubkey: from[0] };
74
+ return { type: 'pk', pubkey: from[0] } as TRet<OutPKType | undefined>;
75
+ },
76
+ decode: (to: TArg<OutPKType>): TRet<OptScript> => {
77
+ if (to.type !== 'pk') return;
78
+ // OutScript validates `pk.pubkey` before this branch emits the canonical
79
+ // `<pubkey> CHECKSIG` script.
80
+ return [to.pubkey, 'CHECKSIG'] as TRet<OptScript>;
59
81
  },
60
- decode: (to: OutPKType): OptScript => (to.type === 'pk' ? [to.pubkey, 'CHECKSIG'] : undefined),
61
82
  };
62
83
 
63
84
  // Public Key Hash (P2PKH)
64
85
  type OutPKHType = { type: 'pkh'; hash: Bytes };
65
86
  const OutPKH: Coder<OptScript, OutPKHType | undefined> = {
66
- encode(from: ScriptType): OutPKHType | undefined {
87
+ encode(from: TArg<ScriptType>): TRet<OutPKHType | undefined> {
67
88
  if (from.length !== 5 || from[0] !== 'DUP' || from[1] !== 'HASH160' || !u.isBytes(from[2]))
68
89
  return;
90
+ // OutScript validates that the pushed HASH160 is exactly 20 bytes.
91
+ // This child matcher only recognizes the canonical P2PKH opcode skeleton.
69
92
  if (from[3] !== 'EQUALVERIFY' || from[4] !== 'CHECKSIG') return;
70
- return { type: 'pkh', hash: from[2] };
93
+ return { type: 'pkh', hash: from[2] } as TRet<OutPKHType | undefined>;
71
94
  },
72
- decode: (to: OutPKHType): OptScript =>
73
- to.type === 'pkh' ? ['DUP', 'HASH160', to.hash, 'EQUALVERIFY', 'CHECKSIG'] : undefined,
95
+ // OutScript validates `pkh.hash` before this branch emits the canonical
96
+ // `DUP HASH160 <hash> EQUALVERIFY CHECKSIG` script.
97
+ decode: (to: TArg<OutPKHType>): TRet<OptScript> =>
98
+ (to.type === 'pkh'
99
+ ? ['DUP', 'HASH160', to.hash, 'EQUALVERIFY', 'CHECKSIG']
100
+ : undefined) as TRet<OptScript>,
74
101
  };
75
102
  // Script Hash (P2SH)
76
103
  type OutSHType = { type: 'sh'; hash: Bytes };
77
104
  const OutSH: Coder<OptScript, OutSHType | undefined> = {
78
- encode(from: ScriptType): OutSHType | undefined {
105
+ encode(from: TArg<ScriptType>): TRet<OutSHType | undefined> {
79
106
  if (from.length !== 3 || from[0] !== 'HASH160' || !u.isBytes(from[1]) || from[2] !== 'EQUAL')
80
107
  return;
81
- return { type: 'sh', hash: from[1] };
108
+ // OutScript validates that the pushed HASH160 is exactly 20 bytes.
109
+ // This child matcher only recognizes the canonical P2SH opcode skeleton.
110
+ return { type: 'sh', hash: from[1] } as TRet<OutSHType | undefined>;
82
111
  },
83
- decode: (to: OutSHType): OptScript =>
84
- to.type === 'sh' ? ['HASH160', to.hash, 'EQUAL'] : undefined,
112
+ // OutScript validates `sh.hash` before this branch emits the canonical
113
+ // `HASH160 <hash> EQUAL` script.
114
+ decode: (to: TArg<OutSHType>): TRet<OptScript> =>
115
+ (to.type === 'sh' ? ['HASH160', to.hash, 'EQUAL'] : undefined) as TRet<OptScript>,
85
116
  };
86
117
 
87
118
  // Witness Script Hash (P2WSH)
88
119
  type OutWSHType = { type: 'wsh'; hash: Bytes };
89
120
  const OutWSH: Coder<OptScript, OutWSHType | undefined> = {
90
- encode(from: ScriptType): OutWSHType | undefined {
121
+ encode(from: TArg<ScriptType>): TRet<OutWSHType | undefined> {
91
122
  if (from.length !== 2 || from[0] !== 0 || !u.isBytes(from[1])) return;
123
+ // BIP382 `wsh()` is specifically the version-0 32-byte witness program.
124
+ // Other witness versions stay with the later coders.
92
125
  if (from[1].length !== 32) return;
93
- return { type: 'wsh', hash: from[1] };
126
+ return { type: 'wsh', hash: from[1] } as TRet<OutWSHType | undefined>;
94
127
  },
95
- decode: (to: OutWSHType): OptScript => (to.type === 'wsh' ? [0, to.hash] : undefined),
128
+ // OutScript validates `wsh.hash` before this branch emits the canonical
129
+ // version-0 32-byte witness program.
130
+ decode: (to: TArg<OutWSHType>): TRet<OptScript> =>
131
+ (to.type === 'wsh' ? [0, to.hash] : undefined) as TRet<OptScript>,
96
132
  };
97
133
 
98
134
  // Witness Public Key Hash (P2WPKH)
99
135
  type OutWPKHType = { type: 'wpkh'; hash: Bytes };
100
136
  const OutWPKH: Coder<OptScript, OutWPKHType | undefined> = {
101
- encode(from: ScriptType): OutWPKHType | undefined {
137
+ encode(from: TArg<ScriptType>): TRet<OutWPKHType | undefined> {
102
138
  if (from.length !== 2 || from[0] !== 0 || !u.isBytes(from[1])) return;
139
+ // BIP382 `wpkh()` is specifically the version-0 20-byte witness program.
140
+ // Compressed-key restrictions are enforced upstream, and other witness
141
+ // versions stay with the later coders.
103
142
  if (from[1].length !== 20) return;
104
- return { type: 'wpkh', hash: from[1] };
143
+ return { type: 'wpkh', hash: from[1] } as TRet<OutWPKHType | undefined>;
105
144
  },
106
- decode: (to: OutWPKHType): OptScript => (to.type === 'wpkh' ? [0, to.hash] : undefined),
145
+ // OutScript validates `wpkh.hash` before this branch emits the canonical
146
+ // version-0 20-byte witness program.
147
+ decode: (to: TArg<OutWPKHType>): TRet<OptScript> =>
148
+ (to.type === 'wpkh' ? [0, to.hash] : undefined) as TRet<OptScript>,
107
149
  };
108
150
 
109
151
  // Multisig (P2MS)
110
152
  type OutMSType = { type: 'ms'; pubkeys: Bytes[]; m: number };
111
153
  const OutMS: Coder<OptScript, OutMSType | undefined> = {
112
- encode(from: ScriptType): OutMSType | undefined {
154
+ encode(from: TArg<ScriptType>): TRet<OutMSType | undefined> {
113
155
  const last = from.length - 1;
114
156
  if (from[last] !== 'CHECKMULTISIG') return;
115
157
  const m = from[0];
@@ -118,26 +160,40 @@ const OutMS: Coder<OptScript, OutMSType | undefined> = {
118
160
  const pubkeys = from.slice(1, -2);
119
161
  if (n !== pubkeys.length) return;
120
162
  for (const pub of pubkeys) if (!u.isBytes(pub)) return;
121
- return { type: 'ms', m, pubkeys: pubkeys as Bytes[] }; // we don't need n, since it is the same as pubkeys
163
+ // OutScript validates pubkey encodings and `0 < m <= n <= 16`.
164
+ // This child matcher only recognizes the canonical CHECKMULTISIG skeleton.
165
+ // We don't need n here because it is the same as pubkeys.length.
166
+ return { type: 'ms', m, pubkeys: pubkeys as Bytes[] } as TRet<OutMSType | undefined>;
122
167
  },
123
168
  // checkmultisig(n, ..pubkeys, m)
124
- decode: (to: OutMSType): OptScript =>
125
- to.type === 'ms' ? [to.m, ...to.pubkeys, to.pubkeys.length, 'CHECKMULTISIG'] : undefined,
169
+ decode: (to: TArg<OutMSType>): TRet<OptScript> =>
170
+ // OutScript validates multisig pubkeys and `0 < m <= n <= 16`.
171
+ // This branch only emits the canonical `m <pubkeys...> n CHECKMULTISIG`
172
+ // script.
173
+ (to.type === 'ms'
174
+ ? [to.m, ...to.pubkeys, to.pubkeys.length, 'CHECKMULTISIG']
175
+ : undefined) as TRet<OptScript>,
126
176
  };
127
177
  // Taproot (P2TR)
128
178
  type OutTRType = { type: 'tr'; pubkey: Bytes };
129
179
  const OutTR: Coder<OptScript, OutTRType | undefined> = {
130
- encode(from: ScriptType): OutTRType | undefined {
131
- if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1])) return;
132
- return { type: 'tr', pubkey: from[1] };
180
+ encode(from: TArg<ScriptType>): TRet<OutTRType | undefined> {
181
+ // BIP141 witness programs are `OP_0..OP_16` followed by a direct 2..40-byte push.
182
+ // BIP341 assigns native taproot meaning only to version 1 with a 32-byte x-only program;
183
+ // other OP_1 program lengths remain reserved future witness programs and should fall through.
184
+ if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1]) || from[1].length !== 32) return;
185
+ return { type: 'tr', pubkey: from[1] } as TRet<OutTRType | undefined>;
133
186
  },
134
- decode: (to: OutTRType): OptScript => (to.type === 'tr' ? [1, to.pubkey] : undefined),
187
+ // OutScript validates `tr.pubkey` before this branch emits the canonical
188
+ // version-1 32-byte witness program.
189
+ decode: (to: TArg<OutTRType>): TRet<OptScript> =>
190
+ (to.type === 'tr' ? [1, to.pubkey] : undefined) as TRet<OptScript>,
135
191
  };
136
192
 
137
193
  // Taproot N-of-N multisig (P2TR_NS)
138
194
  type OutTRNSType = { type: 'tr_ns'; pubkeys: Bytes[] };
139
195
  const OutTRNS: Coder<OptScript, OutTRNSType | undefined> = {
140
- encode(from: ScriptType): OutTRNSType | undefined {
196
+ encode(from: TArg<ScriptType>): TRet<OutTRNSType | undefined> {
141
197
  const last = from.length - 1;
142
198
  if (from[last] !== 'CHECKSIG') return;
143
199
  const pubkeys = [];
@@ -148,24 +204,32 @@ const OutTRNS: Coder<OptScript, OutTRNSType | undefined> = {
148
204
  if (elm !== 'CHECKSIGVERIFY' || i === last - 1) return;
149
205
  continue;
150
206
  }
151
- if (!u.isBytes(elm)) return;
207
+ // Require actual Schnorr pubkeys here so near-miss `<bytes> CHECKSIG`
208
+ // scripts fall through to OutUnknown instead of failing later.
209
+ if (!u.isBytes(elm) || !isValidPubkey(elm, u.PubT.schnorr)) return;
152
210
  pubkeys.push(elm);
153
211
  }
154
- return { type: 'tr_ns', pubkeys };
212
+ // BIP342 "Using a k-of-k script for every combination" documents the shape
213
+ // `<pubkey_1> CHECKSIGVERIFY ... <pubkey_n> CHECKSIG`; this matcher only
214
+ // classifies that embedded-pubkey form, so bare CHECKSIG stays unknown.
215
+ if (!pubkeys.length) return;
216
+ return { type: 'tr_ns', pubkeys } as TRet<OutTRNSType | undefined>;
155
217
  },
156
- decode: (to: OutTRNSType): OptScript => {
218
+ decode: (to: TArg<OutTRNSType>): TRet<OptScript> => {
157
219
  if (to.type !== 'tr_ns') return;
158
220
  const out: ScriptType = [];
159
221
  for (let i = 0; i < to.pubkeys.length - 1; i++) out.push(to.pubkeys[i], 'CHECKSIGVERIFY');
222
+ // This branch assumes at least one Schnorr pubkey; [] would otherwise emit
223
+ // `[undefined, CHECKSIG]` and only fail later in Script.encode.
160
224
  out.push(to.pubkeys[to.pubkeys.length - 1], 'CHECKSIG');
161
- return out;
225
+ return out as TRet<OptScript>;
162
226
  },
163
227
  };
164
228
 
165
229
  // Taproot M-of-N Multisig (P2TR_MS)
166
230
  type OutTRMSType = { type: 'tr_ms'; pubkeys: Bytes[]; m: number };
167
231
  const OutTRMS: Coder<OptScript, OutTRMSType | undefined> = {
168
- encode(from: ScriptType): OutTRMSType | undefined {
232
+ encode(from: TArg<ScriptType>): TRet<OutTRMSType | undefined> {
169
233
  const last = from.length - 1;
170
234
  if (from[last] !== 'NUMEQUAL' || from[1] !== 'CHECKSIG') return;
171
235
  const pubkeys = [];
@@ -173,37 +237,48 @@ const OutTRMS: Coder<OptScript, OutTRMSType | undefined> = {
173
237
  if (typeof m !== 'number') return;
174
238
  for (let i = 0; i < last - 1; i++) {
175
239
  const elm = from[i];
240
+ // Structural mismatches should fall through to OutUnknown instead of
241
+ // throwing from the tr_ms matcher.
176
242
  if (i & 1) {
177
- if (elm !== (i === 1 ? 'CHECKSIG' : 'CHECKSIGADD'))
178
- throw new Error('OutScript.encode/tr_ms: wrong element');
243
+ if (elm !== (i === 1 ? 'CHECKSIG' : 'CHECKSIGADD')) return;
179
244
  continue;
180
245
  }
181
- if (!u.isBytes(elm)) throw new Error('OutScript.encode/tr_ms: wrong key element');
246
+ if (!u.isBytes(elm)) return;
182
247
  pubkeys.push(elm);
183
248
  }
184
- return { type: 'tr_ms', pubkeys, m };
249
+ return { type: 'tr_ms', pubkeys, m } as TRet<OutTRMSType | undefined>;
185
250
  },
186
- decode: (to: OutTRMSType): OptScript => {
251
+ decode: (to: TArg<OutTRMSType>): TRet<OptScript> => {
187
252
  if (to.type !== 'tr_ms') return;
188
253
  const out: ScriptType = [to.pubkeys[0], 'CHECKSIG'];
189
254
  for (let i = 1; i < to.pubkeys.length; i++) out.push(to.pubkeys[i], 'CHECKSIGADD');
255
+ // This branch assumes `m` was already validated as an integer ScriptNum;
256
+ // fractional JS numbers would otherwise serialize as a different threshold.
190
257
  out.push(to.m, 'NUMEQUAL');
191
- return out;
258
+ return out as TRet<OptScript>;
192
259
  },
193
260
  };
194
261
 
195
262
  // Unknown output type
196
263
  type OutUnknownType = { type: 'unknown'; script: Bytes };
197
264
  const OutUnknown: Coder<OptScript, OutUnknownType | undefined> = {
198
- encode(from: ScriptType): OutUnknownType | undefined {
199
- return { type: 'unknown', script: Script.encode(from) };
265
+ encode(from: TArg<ScriptType>): TRet<OutUnknownType | undefined> {
266
+ // This is the catch-all fallback for scripts no structured coder recognized,
267
+ // so earlier matchers must return `undefined` instead of throwing on mismatch.
268
+ // Because this reserializes the parsed Script AST, unknown scripts preserve
269
+ // semantics but not original non-minimal push spellings.
270
+ return { type: 'unknown', script: Script.encode(from) } as TRet<OutUnknownType | undefined>;
200
271
  },
201
- decode: (to: OutUnknownType): OptScript =>
202
- to.type === 'unknown' ? Script.decode(to.script) : undefined,
272
+ decode: (to: TArg<OutUnknownType>): TRet<OptScript> =>
273
+ // This reparses `unknown.script` through the semantic Script codec, so raw
274
+ // bytes must still be syntactically parseable and may canonicalize on re-encode.
275
+ (to.type === 'unknown' ? Script.decode(to.script) : undefined) as TRet<OptScript>,
203
276
  };
204
277
  // /Payments
205
278
 
206
- const OutScripts = [
279
+ const OutScripts = /* @__PURE__ */ (() => [
280
+ // Order is semantic: specific structured coders run first and the catch-all
281
+ // unknown fallback must stay last.
207
282
  OutP2A,
208
283
  OutPK,
209
284
  OutPKH,
@@ -215,22 +290,26 @@ const OutScripts = [
215
290
  OutTRNS,
216
291
  OutTRMS,
217
292
  OutUnknown,
218
- ];
293
+ ])();
219
294
  // TODO: we can support user supplied output scripts now
220
295
  // - addOutScript
221
296
  // - removeOutScript
222
297
  // - We can do that as log we modify array in-place
223
298
  // - Actually is very hard, since there is sign/finalize logic
224
- const _OutScript = P.apply(Script, P.coders.match(OutScripts));
299
+ // Raw composition of semantic Script parsing with the ordered output-script
300
+ // matcher; OutScript adds the higher-level validation layer on top.
301
+ const _OutScript = /* @__PURE__ */ (() => P.apply(Script, P.coders.match(OutScripts)))();
225
302
 
226
303
  /*
227
304
  * UNSAFE: Custom scripts: mostly ordinals, be very careful when crafting new scripts
228
305
  * Only taproot supported for now.
229
- * NOTE: we can use same to move finalization logic from Transaction, but it will significantly change audited code.
306
+ * NOTE: we can use same to move finalization logic from Transaction,
307
+ * but it will significantly change audited code.
230
308
  */
231
309
 
232
310
  type FinalizeSignature = [{ pubKey: Bytes; leafHash: Bytes }, Bytes];
233
311
  type CustomScriptOut = { type: string } & Record<string, any>;
312
+ /** Custom taproot script coder/finalizer hook. */
234
313
  export type CustomScript = Coder<OptScript, CustomScriptOut | undefined> & {
235
314
  finalizeTaproot?: (
236
315
  script: Bytes,
@@ -240,90 +319,173 @@ export type CustomScript = Coder<OptScript, CustomScriptOut | undefined> & {
240
319
  };
241
320
 
242
321
  // We can validate this once, because of packed & coders
243
- export const OutScript: P.CoderType<
244
- NonNullable<
245
- | OutP2AType
246
- | OutPKType
247
- | OutPKHType
248
- | OutSHType
249
- | OutWSHType
250
- | OutWPKHType
251
- | OutMSType
252
- | OutTRType
253
- | OutTRNSType
254
- | OutTRMSType
255
- | OutUnknownType
256
- | undefined
322
+ /**
323
+ * Coder for recognized Bitcoin output scripts.
324
+ * @example
325
+ * Decode a serialized output script back into the tagged payment descriptor.
326
+ * ```ts
327
+ * import { OutScript, p2wpkh } from '@scure/btc-signer/payment.js';
328
+ * import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
329
+ * const pay = p2wpkh(pubECDSA(randomPrivateKeyBytes()));
330
+ * OutScript.decode(pay.script);
331
+ * ```
332
+ */
333
+ export const OutScript: TRet<
334
+ P.CoderType<
335
+ NonNullable<
336
+ | OutP2AType
337
+ | OutPKType
338
+ | OutPKHType
339
+ | OutSHType
340
+ | OutWSHType
341
+ | OutWPKHType
342
+ | OutMSType
343
+ | OutTRType
344
+ | OutTRNSType
345
+ | OutTRMSType
346
+ | OutUnknownType
347
+ | undefined
348
+ >
257
349
  >
258
- > = P.validate(_OutScript, (i) => {
259
- if (i.type === 'pk' && !isValidPubkey(i.pubkey, u.PubT.ecdsa))
260
- throw new Error('OutScript/pk: wrong key');
261
- if (
262
- (i.type === 'pkh' || i.type === 'sh' || i.type === 'wpkh') &&
263
- (!u.isBytes(i.hash) || i.hash.length !== 20)
264
- )
265
- throw new Error(`OutScript/${i.type}: wrong hash`);
266
- if (i.type === 'wsh' && (!u.isBytes(i.hash) || i.hash.length !== 32))
267
- throw new Error(`OutScript/wsh: wrong hash`);
268
- if (i.type === 'tr' && (!u.isBytes(i.pubkey) || !isValidPubkey(i.pubkey, u.PubT.schnorr)))
269
- throw new Error('OutScript/tr: wrong taproot public key');
270
- if (i.type === 'ms' || i.type === 'tr_ns' || i.type === 'tr_ms')
271
- if (!Array.isArray(i.pubkeys)) throw new Error('OutScript/multisig: wrong pubkeys array');
272
- if (i.type === 'ms') {
273
- const n = i.pubkeys.length;
274
- for (const p of i.pubkeys)
275
- if (!isValidPubkey(p, u.PubT.ecdsa)) throw new Error('OutScript/multisig: wrong pubkey');
276
- if (i.m <= 0 || n > 16 || i.m > n) throw new Error('OutScript/multisig: invalid params');
277
- }
278
- if (i.type === 'tr_ns' || i.type === 'tr_ms') {
279
- for (const p of i.pubkeys)
280
- if (!isValidPubkey(p, u.PubT.schnorr)) throw new Error(`OutScript/${i.type}: wrong pubkey`);
281
- }
282
- if (i.type === 'tr_ms') {
283
- const n = i.pubkeys.length;
284
- if (i.m <= 0 || n > 999 || i.m > n) throw new Error('OutScript/tr_ms: invalid params');
285
- }
286
- return i;
287
- });
350
+ > = /* @__PURE__ */ (() =>
351
+ Object.freeze(
352
+ P.validate(_OutScript, (i) => {
353
+ if (i.type === 'pk' && !isValidPubkey(i.pubkey, u.PubT.ecdsa))
354
+ throw new Error('OutScript/pk: wrong key');
355
+ if (
356
+ (i.type === 'pkh' || i.type === 'sh' || i.type === 'wpkh') &&
357
+ (!u.isBytes(i.hash) || i.hash.length !== 20)
358
+ )
359
+ throw new Error(`OutScript/${i.type}: wrong hash`);
360
+ if (i.type === 'wsh' && (!u.isBytes(i.hash) || i.hash.length !== 32))
361
+ throw new Error(`OutScript/wsh: wrong hash`);
362
+ if (i.type === 'tr' && (!u.isBytes(i.pubkey) || !isValidPubkey(i.pubkey, u.PubT.schnorr)))
363
+ throw new Error('OutScript/tr: wrong taproot public key');
364
+ if (i.type === 'ms' || i.type === 'tr_ns' || i.type === 'tr_ms')
365
+ if (!Array.isArray(i.pubkeys)) throw new Error('OutScript/multisig: wrong pubkeys array');
366
+ if (i.type === 'ms') {
367
+ const n = i.pubkeys.length;
368
+ for (const p of i.pubkeys)
369
+ if (!isValidPubkey(p, u.PubT.ecdsa)) throw new Error('OutScript/multisig: wrong pubkey');
370
+ // Range checks are not enough here: non-integer JS numbers like 1.5 would
371
+ // otherwise slip through and serialize as a different ScriptNum threshold.
372
+ anumber(i.m, 'm');
373
+ if (i.m <= 0 || n > 16 || i.m > n) throw new Error('OutScript/multisig: invalid params');
374
+ }
375
+ if (i.type === 'tr_ns' || i.type === 'tr_ms') {
376
+ for (const p of i.pubkeys)
377
+ if (!isValidPubkey(p, u.PubT.schnorr))
378
+ throw new Error(`OutScript/${i.type}: wrong pubkey`);
379
+ }
380
+ if (i.type === 'tr_ms') {
381
+ const n = i.pubkeys.length;
382
+ // BIP 342 keeps the 1000-element stack limit. This CHECKSIG/CHECKSIGADD form
383
+ // momentarily has n witness items plus one pushed pubkey on the stack, so n must stay <= 999.
384
+ anumber(i.m, 'm');
385
+ if (i.m <= 0 || n > 999 || i.m > n) throw new Error('OutScript/tr_ms: invalid params');
386
+ }
387
+ return i;
388
+ })
389
+ ))() as TRet<
390
+ P.CoderType<
391
+ NonNullable<
392
+ | OutP2AType
393
+ | OutPKType
394
+ | OutPKHType
395
+ | OutSHType
396
+ | OutWSHType
397
+ | OutWPKHType
398
+ | OutMSType
399
+ | OutTRType
400
+ | OutTRNSType
401
+ | OutTRMSType
402
+ | OutUnknownType
403
+ | undefined
404
+ >
405
+ >
406
+ >;
407
+ /** Type of the output-script coder. */
288
408
  export type OutScriptType = typeof OutScript;
409
+ // TRet-wrapping OutScript changes decode() to the normalized descriptor surface, but the local
410
+ // checkScript/Address caches still need an explicit alias that can carry the decode-side `undefined`.
411
+ type OutScriptValue = ReturnType<OutScriptType['decode']> | undefined;
289
412
 
290
413
  // Basic sanity check for scripts
291
- function checkWSH(s: OutWSHType, witnessScript: Bytes) {
414
+ function checkWSH(s: TArg<OutWSHType>, witnessScript: TArg<Bytes>) {
292
415
  if (!u.equalBytes(s.hash, u.sha256(witnessScript)))
293
416
  throw new Error('checkScript: wsh wrong witnessScript hash');
417
+ // BIP141 only requires the witnessScript hash match; the type-based rejects
418
+ // below are an extra descriptor sanity layer for BIP382 invalid-descriptor
419
+ // bullets `wpkh() nested in wsh()` and `wsh() nested in wsh()`.
294
420
  const w = OutScript.decode(witnessScript);
295
421
  if (w.type === 'tr' || w.type === 'tr_ns' || w.type === 'tr_ms')
296
422
  throw new Error(`checkScript: P2${w.type} cannot be wrapped in P2SH`);
297
- if (w.type === 'wpkh' || w.type === 'sh')
423
+ if (w.type === 'wpkh' || w.type === 'wsh' || w.type === 'sh')
298
424
  throw new Error(`checkScript: P2${w.type} cannot be wrapped in P2WSH`);
299
425
  }
300
426
 
301
- export function checkScript(script?: Bytes, redeemScript?: Bytes, witnessScript?: Bytes): void {
427
+ /**
428
+ * Validates that nested redeem and witness scripts match their wrappers.
429
+ * @param script - top-level output script
430
+ * @param redeemScript - optional redeem script for P2SH wrappers
431
+ * @param witnessScript - optional witness script for P2WSH wrappers
432
+ * @throws If the script nesting is invalid or unsupported. {@link Error}
433
+ * @example
434
+ * Verify that wrapped scripts and hashes still match after custom edits.
435
+ * ```ts
436
+ * import { hex } from '@scure/base';
437
+ * import { checkScript, p2pkh, p2sh } from '@scure/btc-signer/payment.js';
438
+ * const wrapped = p2sh(
439
+ * p2pkh(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))
440
+ * );
441
+ * checkScript(wrapped.script, wrapped.redeemScript);
442
+ * ```
443
+ */
444
+ export function checkScript(
445
+ script?: TArg<Bytes>,
446
+ redeemScript?: TArg<Bytes>,
447
+ witnessScript?: TArg<Bytes>
448
+ ): void {
449
+ let hasWsh = false;
450
+ let r: OutScriptValue = undefined;
302
451
  if (script) {
303
452
  const s = OutScript.decode(script);
453
+ // BIP174 Data Signers Check For bullets: provided redeemScript must match
454
+ // the scriptPubKey, and provided witnessScript must match the scriptPubKey
455
+ // or redeemScript instead of being silently ignored as stray metadata.
304
456
  // ms||pk maybe work, but there will be no address, hard to spend
305
457
  if (s.type === 'tr_ns' || s.type === 'tr_ms' || s.type === 'ms' || s.type == 'pk')
306
458
  throw new Error(`checkScript: non-wrapped ${s.type}`);
307
- if (s.type === 'sh' && redeemScript) {
459
+ if (redeemScript) {
460
+ if (s.type !== 'sh') throw new Error('checkScript: redeemScript without P2SH');
308
461
  if (!u.equalBytes(s.hash, u.hash160(redeemScript)))
309
462
  throw new Error('checkScript: sh wrong redeemScript hash');
310
- const r = OutScript.decode(redeemScript);
311
- if (r.type === 'tr' || r.type === 'tr_ns' || r.type === 'tr_ms')
463
+ r = OutScript.decode(redeemScript) as OutScriptValue;
464
+ if (r?.type === 'tr' || r?.type === 'tr_ns' || r?.type === 'tr_ms')
312
465
  throw new Error(`checkScript: P2${r.type} cannot be wrapped in P2SH`);
313
466
  // Not sure if this unspendable, but we cannot represent this via PSBT
314
- if (r.type === 'sh') throw new Error('checkScript: P2SH cannot be wrapped in P2SH');
467
+ if (r?.type === 'sh') throw new Error('checkScript: P2SH cannot be wrapped in P2SH');
468
+ }
469
+ if (s.type === 'wsh') {
470
+ hasWsh = true;
471
+ if (witnessScript) checkWSH(s, witnessScript);
315
472
  }
316
- if (s.type === 'wsh' && witnessScript) checkWSH(s, witnessScript);
317
473
  }
318
474
  if (redeemScript) {
319
- const r = OutScript.decode(redeemScript);
320
- if (r.type === 'wsh' && witnessScript) checkWSH(r, witnessScript);
475
+ if (r === undefined) r = OutScript.decode(redeemScript) as OutScriptValue;
476
+ if (r?.type === 'wsh') {
477
+ hasWsh = true;
478
+ if (witnessScript) checkWSH(r as TArg<OutWSHType>, witnessScript);
479
+ }
321
480
  }
481
+ if (witnessScript && !hasWsh) throw new Error('checkScript: witnessScript without P2WSH');
322
482
  }
323
483
 
324
- function uniqPubkey(pubkeys: Bytes[]) {
484
+ function uniqPubkey(pubkeys: TArg<Bytes[]>) {
325
485
  const map: Record<string, boolean> = {};
326
486
  for (const pub of pubkeys) {
487
+ // Exact-byte duplicate filter only: BIP383 valid vectors still permit the
488
+ // same point to appear in compressed and uncompressed SEC1 form in multi().
327
489
  const key = hex.encode(pub);
328
490
  if (map[key]) throw new Error(`Multisig: non-uniq pubkey: ${pubkeys.map(hex.encode)}`);
329
491
  map[key] = true;
@@ -333,15 +495,68 @@ function uniqPubkey(pubkeys: Bytes[]) {
333
495
  // Also we use satisfies for additional check (ts 4.9+)
334
496
  type Extends<T, U> = T extends U ? T : never;
335
497
 
336
- export type P2PK = { type: 'pk'; script: Bytes };
337
- export const p2pk = (pubkey: Bytes, _network: BTC_NETWORK = NETWORK): Extends<P2PK, P2Ret> => {
498
+ /** Pay-to-public-key output descriptor. */
499
+ export type P2PK = {
500
+ /** Payment-script tag for pay-to-public-key outputs. */
501
+ type: 'pk';
502
+ /** Serialized `pubkey CHECKSIG` script. */
503
+ script: TRet<Bytes>;
504
+ };
505
+ /**
506
+ * Builds a pay-to-public-key script.
507
+ * @param pubkey - compressed or uncompressed ECDSA public key
508
+ * @param _network - unused network placeholder for API consistency
509
+ * @returns P2PK descriptor.
510
+ * @throws If the public key cannot be encoded as a P2PK output. {@link Error}
511
+ * @example
512
+ * Build a bare pay-to-public-key output.
513
+ * ```ts
514
+ * import { hex } from '@scure/base';
515
+ * import { p2pk } from '@scure/btc-signer/payment.js';
516
+ * p2pk(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'));
517
+ * ```
518
+ */
519
+ export const p2pk = (
520
+ pubkey: TArg<Bytes>,
521
+ _network: BTC_NETWORK = NETWORK
522
+ ): TRet<Extends<P2PK, P2Ret>> => {
338
523
  // network is unused
339
524
  if (!isValidPubkey(pubkey, u.PubT.ecdsa)) throw new Error('P2PK: invalid publicKey');
340
- return { type: 'pk', script: OutScript.encode({ type: 'pk', pubkey }) } as const satisfies P2Ret;
525
+ return {
526
+ type: 'pk',
527
+ script: OutScript.encode({ type: 'pk', pubkey }),
528
+ } as const as TRet<Extends<P2PK, P2Ret>>;
341
529
  };
342
530
 
343
- export type P2PKH = { type: 'pkh'; script: Bytes; address: string; hash: Bytes };
344
- export const p2pkh = (publicKey: Bytes, network: BTC_NETWORK = NETWORK): Extends<P2PKH, P2Ret> => {
531
+ /** Pay-to-public-key-hash output descriptor. */
532
+ export type P2PKH = {
533
+ /** Payment-script tag for pay-to-public-key-hash outputs. */
534
+ type: 'pkh';
535
+ /** Serialized P2PKH script. */
536
+ script: TRet<Bytes>;
537
+ /** Base58Check address for the descriptor. */
538
+ address: string;
539
+ /** HASH160 committed by the script. */
540
+ hash: TRet<Bytes>;
541
+ };
542
+ /**
543
+ * Builds a P2PKH output from a public key.
544
+ * @param publicKey - compressed or uncompressed ECDSA public key bytes; HASH160 commits to the exact encoding
545
+ * @param network - address network parameters
546
+ * @returns P2PKH descriptor.
547
+ * @throws If the public key cannot be encoded as a P2PKH output. {@link Error}
548
+ * @example
549
+ * Build a classic pay-to-public-key-hash output.
550
+ * ```ts
551
+ * import { hex } from '@scure/base';
552
+ * import { p2pkh } from '@scure/btc-signer/payment.js';
553
+ * p2pkh(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'));
554
+ * ```
555
+ */
556
+ export const p2pkh = (
557
+ publicKey: TArg<Bytes>,
558
+ network: BTC_NETWORK = NETWORK
559
+ ): TRet<Extends<P2PKH, P2Ret>> => {
345
560
  if (!isValidPubkey(publicKey, u.PubT.ecdsa)) throw new Error('P2PKH: invalid publicKey');
346
561
  const hash = u.hash160(publicKey);
347
562
  return {
@@ -349,79 +564,162 @@ export const p2pkh = (publicKey: Bytes, network: BTC_NETWORK = NETWORK): Extends
349
564
  script: OutScript.encode({ type: 'pkh', hash }),
350
565
  address: Address(network).encode({ type: 'pkh', hash }),
351
566
  hash,
352
- } as const satisfies P2Ret;
567
+ } as const as TRet<Extends<P2PKH, P2Ret>>;
353
568
  };
354
569
 
570
+ /** Shared fields for pay-to-script-hash outputs. */
355
571
  export type P2SHBase = {
572
+ /** Payment-script tag for pay-to-script-hash outputs. */
356
573
  type: 'sh';
357
- redeemScript: Bytes;
358
- script: Bytes;
574
+ /** Child script wrapped by the P2SH output. */
575
+ redeemScript: TRet<Bytes>;
576
+ /** Serialized P2SH script. */
577
+ script: TRet<Bytes>;
578
+ /** Base58Check address for the descriptor. */
359
579
  address: string;
360
- hash: Bytes;
580
+ /** HASH160 committed by the script. */
581
+ hash: TRet<Bytes>;
361
582
  };
362
- export type P2SHWithWitness = P2SHBase & { witnessScript: Bytes };
583
+ /** P2SH descriptor with an embedded witness script. */
584
+ export type P2SHWithWitness = P2SHBase & { witnessScript: TRet<Bytes> };
585
+ /** P2SH descriptor without an embedded witness script. */
363
586
  export type P2SHWithoutWitness = Omit<P2SHBase, 'witnessScript'>;
587
+ /** Conditional P2SH return type for wrapped scripts. */
364
588
  export type P2SHReturn<T extends P2Ret> = T extends { witnessScript: Bytes }
365
589
  ? P2SHWithWitness
366
590
  : P2SHWithoutWitness;
591
+ /**
592
+ * Wraps a child script inside P2SH.
593
+ * @param child - child payment descriptor to wrap
594
+ * @param network - address network parameters
595
+ * @returns P2SH descriptor preserving witness metadata when present.
596
+ * @throws If the wrapped script combination is invalid or unsupported. {@link Error}
597
+ * @example
598
+ * Wrap a child script in P2SH so it gets a base58 address form.
599
+ * ```ts
600
+ * import { hex } from '@scure/base';
601
+ * import { p2pk, p2sh, p2wsh } from '@scure/btc-signer/payment.js';
602
+ * p2sh(p2wsh(p2pk(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))));
603
+ * ```
604
+ */
367
605
  export const p2sh = <T extends P2Ret>(
368
- child: T,
606
+ child: TArg<T>,
369
607
  network: BTC_NETWORK = NETWORK
370
- ): Extends<P2SHReturn<T>, P2Ret> => {
608
+ ): TRet<Extends<P2SHReturn<T>, P2Ret>> => {
371
609
  // It is already tested inside noble-hashes and checkScript
372
- const cs = child.script;
373
- if (!u.isBytes(cs)) throw new Error(`Wrong script: ${typeof child.script}, expected Uint8Array`);
610
+ // BIP16 redeemScripts are pushed by scriptSig, so anything over the 520-byte pushed-element
611
+ // limit would be fundable by HASH160 but unspendable once wrapped in P2SH.
612
+ const c = child as T;
613
+ const cs = c.script;
614
+ if (!u.isBytes(cs)) throw new Error(`Wrong script: ${typeof c.script}, expected Uint8Array`);
615
+ if (cs.length > MAX_SCRIPT_BYTE_LENGTH)
616
+ throw new Error(
617
+ `P2SH: redeemScript exceeds ${MAX_SCRIPT_BYTE_LENGTH}-byte push limit: len=${cs.length}`
618
+ );
374
619
  const hash = u.hash160(cs);
375
- const script = OutScript.encode({ type: 'sh', hash });
376
- checkScript(script, cs, child.witnessScript);
377
- if (child.witnessScript) {
620
+ const out = { type: 'sh', hash } as const;
621
+ const script = OutScript.encode(out);
622
+ const address = Address(network).encode(out);
623
+ checkScript(script, cs, c.witnessScript);
624
+ if (c.witnessScript) {
378
625
  return {
379
626
  type: 'sh',
380
627
  redeemScript: cs,
381
- script: OutScript.encode({ type: 'sh', hash }),
382
- address: Address(network).encode({ type: 'sh', hash }),
628
+ script,
629
+ address,
383
630
  hash,
384
- witnessScript: child.witnessScript,
385
- } as Extends<P2SHReturn<T>, P2Ret> satisfies P2Ret;
631
+ witnessScript: c.witnessScript,
632
+ } as unknown as TRet<Extends<P2SHReturn<T>, P2Ret>>;
386
633
  } else {
387
634
  return {
388
635
  type: 'sh',
389
636
  redeemScript: cs,
390
- script: OutScript.encode({ type: 'sh', hash }),
391
- address: Address(network).encode({ type: 'sh', hash }),
637
+ script,
638
+ address,
392
639
  hash,
393
- } as Extends<P2SHReturn<T>, P2Ret> satisfies P2Ret;
640
+ } as unknown as TRet<Extends<P2SHReturn<T>, P2Ret>>;
394
641
  }
395
642
  };
396
643
 
644
+ /** Pay-to-witness-script-hash descriptor. */
397
645
  export type P2WSH = {
646
+ /** Payment-script tag for pay-to-witness-script-hash outputs. */
398
647
  type: 'wsh';
399
- witnessScript: Bytes;
400
- script: Bytes;
648
+ /** Child script committed by the witness program. */
649
+ witnessScript: TRet<Bytes>;
650
+ /** Serialized P2WSH script. */
651
+ script: TRet<Bytes>;
652
+ /** Bech32 address for the descriptor. */
401
653
  address: string;
402
- hash: Bytes;
654
+ /** SHA256 committed by the witness program. */
655
+ hash: TRet<Bytes>;
403
656
  };
404
- export const p2wsh = (child: P2Ret, network: BTC_NETWORK = NETWORK): Extends<P2WSH, P2Ret> => {
657
+ /**
658
+ * Wraps a child script inside native SegWit P2WSH.
659
+ * @param child - child payment descriptor to wrap
660
+ * @param network - address network parameters
661
+ * @returns P2WSH descriptor.
662
+ * @throws If the wrapped script combination is invalid or unsupported. {@link Error}
663
+ * @example
664
+ * Wrap a child script in native SegWit P2WSH.
665
+ * ```ts
666
+ * import { hex } from '@scure/base';
667
+ * import { p2pk, p2wsh } from '@scure/btc-signer/payment.js';
668
+ * p2wsh(p2pk(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')));
669
+ * ```
670
+ */
671
+ export const p2wsh = (
672
+ child: TArg<P2Ret>,
673
+ network: BTC_NETWORK = NETWORK
674
+ ): TRet<Extends<P2WSH, P2Ret>> => {
405
675
  const cs = child.script;
406
676
  if (!u.isBytes(cs)) throw new Error(`Wrong script: ${typeof cs}, expected Uint8Array`);
677
+ // BIP141 P2WSH says the witness "must consist of ... a serialized script (witnessScript)"
678
+ // and that witnessScript is limited to 10,000 bytes, so larger wrapped scripts must reject.
679
+ if (cs.length > 10000) throw new Error('P2WSH: witnessScript exceeds 10,000 bytes');
407
680
  const hash = u.sha256(cs);
408
681
  const script = OutScript.encode({ type: 'wsh', hash });
409
682
  checkScript(script, undefined, cs);
410
683
  return {
411
684
  type: 'wsh',
412
685
  witnessScript: cs,
413
- script: OutScript.encode({ type: 'wsh', hash }),
686
+ script,
414
687
  address: Address(network).encode({ type: 'wsh', hash }),
415
688
  hash,
416
- } as const satisfies P2Ret;
689
+ } as const as TRet<Extends<P2WSH, P2Ret>>;
417
690
  };
418
691
 
419
- export type P2WPKH = { type: 'wpkh'; script: Bytes; address: string; hash: Bytes };
692
+ /** Pay-to-witness-public-key-hash descriptor. */
693
+ export type P2WPKH = {
694
+ /** Payment-script tag for pay-to-witness-public-key-hash outputs. */
695
+ type: 'wpkh';
696
+ /** Serialized P2WPKH script. */
697
+ script: TRet<Bytes>;
698
+ /** Bech32 address for the descriptor. */
699
+ address: string;
700
+ /** HASH160 committed by the witness program. */
701
+ hash: TRet<Bytes>;
702
+ };
703
+ /**
704
+ * Builds a native SegWit P2WPKH output from a public key.
705
+ * @param publicKey - compressed ECDSA public key
706
+ * @param network - address network parameters
707
+ * @returns P2WPKH descriptor.
708
+ * @throws If the public key cannot be encoded as a P2WPKH output. {@link Error}
709
+ * @example
710
+ * Build a native SegWit pay-to-public-key-hash output.
711
+ * ```ts
712
+ * import { hex } from '@scure/base';
713
+ * import { p2wpkh } from '@scure/btc-signer/payment.js';
714
+ * p2wpkh(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'));
715
+ * ```
716
+ */
420
717
  export const p2wpkh = (
421
- publicKey: Bytes,
718
+ publicKey: TArg<Bytes>,
422
719
  network: BTC_NETWORK = NETWORK
423
- ): Extends<P2WPKH, P2Ret> => {
720
+ ): TRet<Extends<P2WPKH, P2Ret>> => {
424
721
  if (!isValidPubkey(publicKey, u.PubT.ecdsa)) throw new Error('P2WPKH: invalid publicKey');
722
+ // BIP 143 default policy: version-0 witness programs MUST use 33-byte compressed ECDSA keys.
425
723
  if (publicKey.length === 65) throw new Error('P2WPKH: uncompressed public key');
426
724
  const hash = u.hash160(publicKey);
427
725
  return {
@@ -429,30 +727,54 @@ export const p2wpkh = (
429
727
  script: OutScript.encode({ type: 'wpkh', hash }),
430
728
  address: Address(network).encode({ type: 'wpkh', hash }),
431
729
  hash,
432
- } as const satisfies P2Ret;
730
+ } as const as TRet<Extends<P2WPKH, P2Ret>>;
433
731
  };
434
732
 
435
- export type P2MS = { type: 'ms'; script: Bytes };
733
+ /** Bare multisig output descriptor. */
734
+ export type P2MS = {
735
+ /** Payment-script tag for bare multisig outputs. */
736
+ type: 'ms';
737
+ /** Serialized bare multisig script. */
738
+ script: TRet<Bytes>;
739
+ };
740
+ /**
741
+ * Builds a bare multisig script.
742
+ * @param m - number of required signatures
743
+ * @param pubkeys - participating public keys
744
+ * @param allowSamePubkeys - whether duplicate keys are allowed
745
+ * @returns P2MS descriptor.
746
+ * @throws If the multisig parameters are invalid. {@link Error}
747
+ * @example
748
+ * Build a bare multisig output script.
749
+ * ```ts
750
+ * import { hex } from '@scure/base';
751
+ * import { p2ms } from '@scure/btc-signer/payment.js';
752
+ * p2ms(1, [hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')], true);
753
+ * ```
754
+ */
436
755
  export const p2ms = (
437
756
  m: number,
438
- pubkeys: Bytes[],
757
+ pubkeys: TArg<Bytes[]>,
439
758
  allowSamePubkeys = false
440
- ): Extends<P2MS, P2Ret> => {
759
+ ): TRet<Extends<P2MS, P2Ret>> => {
760
+ // BIP 11 only standardized bare multisig up to 3 keys; this helper still permits up to 16
761
+ // because the same script shape is commonly wrapped by p2sh()/p2wsh() instead of used bare.
441
762
  if (!allowSamePubkeys) uniqPubkey(pubkeys);
442
763
  return {
443
764
  type: 'ms',
444
765
  script: OutScript.encode({ type: 'ms', pubkeys, m }),
445
- } as const satisfies P2Ret;
766
+ } as const as TRet<Extends<P2MS, P2Ret>>;
446
767
  };
447
768
 
769
+ /** Internal taproot hash tree without merkle paths. */
448
770
  export type HashedTree =
449
771
  | { type: 'leaf'; version?: number; script: Bytes; hash: Bytes }
450
772
  | { type: 'branch'; left: HashedTree; right: HashedTree; hash: Bytes };
451
773
  function checkTaprootScript(
452
- script: Bytes,
453
- internalPubKey: Bytes,
774
+ script: TArg<Bytes>,
775
+ internalPubKey: TArg<Bytes>,
454
776
  allowUnknownOutputs = false,
455
- customScripts?: CustomScript[]
777
+ customScripts?: TArg<CustomScript[]>
456
778
  ) {
457
779
  const out = OutScript.decode(script);
458
780
  if (out.type === 'unknown') {
@@ -492,25 +814,35 @@ function checkTaprootScript(
492
814
  }
493
815
  }
494
816
 
817
+ /** Taproot key-path descriptor. */
495
818
  export type P2TR = {
819
+ /** Payment-script tag for taproot outputs. */
496
820
  type: 'tr';
821
+ /** Serialized v1 witness-program script. */
497
822
  script: Bytes;
823
+ /** Bech32m address for the descriptor. */
498
824
  address: string;
825
+ /** Tweaked x-only output key committed by the address and script. */
499
826
  tweakedPubkey: Bytes;
827
+ /** Internal x-only taproot key before tweaking. */
500
828
  tapInternalKey: Bytes;
501
829
  };
830
+ /** Taproot descriptor with a script tree attached. */
502
831
  export type P2TR_TREE = P2TR & {
503
832
  tapMerkleRoot: Bytes;
504
833
  tapLeafScript: TransactionInput['tapLeafScript'];
505
834
  leaves: TaprootLeaf[];
506
835
  };
507
836
 
837
+ /** Node accepted when constructing a taproot script tree. */
508
838
  export type TaprootNode = {
509
839
  script: Bytes | string;
510
840
  leafVersion?: number;
511
841
  weight?: number;
512
842
  } & Partial<P2TR_TREE>;
843
+ /** Recursive taproot tree input. */
513
844
  export type TaprootScriptTree = TaprootNode | TaprootScriptTree[];
845
+ /** Flat list of weighted taproot leaves. */
514
846
  export type TaprootScriptList = TaprootNode[];
515
847
  type _TaprootTreeInternal = {
516
848
  weight?: number;
@@ -518,7 +850,27 @@ type _TaprootTreeInternal = {
518
850
  };
519
851
 
520
852
  // Helper for generating binary tree from list, with weights
521
- export function taprootListToTree(taprootList: TaprootScriptList): TaprootScriptTree {
853
+ /**
854
+ * Converts a flat list of weighted leaves into a binary taproot tree.
855
+ * @param taprootList - weighted leaves to arrange
856
+ * @returns Binary taproot script tree.
857
+ * @throws If the list is empty and cannot describe any tree. {@link Error}
858
+ * @example
859
+ * Start from a flat weighted list, then let the helper build the binary tree shape.
860
+ * ```ts
861
+ * import { hex } from '@scure/base';
862
+ * import { p2tr_pk, taprootListToTree } from '@scure/btc-signer/payment.js';
863
+ * taprootListToTree([
864
+ * p2tr_pk(hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9')),
865
+ * p2tr_pk(hex.decode('dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659')),
866
+ * ]);
867
+ * ```
868
+ */
869
+ export function taprootListToTree(taprootList: TArg<TaprootScriptList>): TRet<TaprootScriptTree> {
870
+ // Empty flat lists cannot represent a taproot script tree; omit the tree entirely for
871
+ // key-path-only outputs instead of passing [] here, otherwise this helper would return
872
+ // undefined and downstream taproot tree walkers would fail much later on a non-tree value.
873
+ if (!taprootList.length) throw new Error('taprootListToTree: empty tree');
522
874
  // Clone input in order to not corrupt it
523
875
  const lst = Array.from(taprootList) as _TaprootTreeInternal[];
524
876
  // We have at least 2 elements => can create branch
@@ -537,17 +889,24 @@ export function taprootListToTree(taprootList: TaprootScriptList): TaprootScript
537
889
  }
538
890
  // At this point there is always 1 element in lst
539
891
  const last = lst[0];
540
- return (last?.childs || last) as TaprootScriptTree;
892
+ return (last?.childs || last) as TRet<TaprootScriptTree>;
541
893
  }
542
894
 
895
+ /** Taproot leaf with its merkle path. */
543
896
  export type TaprootLeaf = {
897
+ /** Leaf marker inside the annotated taproot tree. */
544
898
  type: 'leaf';
899
+ /** Tapleaf version committed by the merkle tree. */
545
900
  version?: number;
901
+ /** Serialized leaf script. */
546
902
  script: Bytes;
903
+ /** Tagged tapleaf hash for the script and version. */
547
904
  hash: Bytes;
905
+ /** Merkle path hashes required for script-path spending. */
548
906
  path: Bytes[];
549
907
  };
550
908
 
909
+ /** Internal taproot tree annotated with merkle paths. */
551
910
  export type HashedTreeWithPath =
552
911
  | TaprootLeaf
553
912
  | {
@@ -558,36 +917,42 @@ export type HashedTreeWithPath =
558
917
  path: Bytes[];
559
918
  };
560
919
 
561
- function taprootAddPath(tree: HashedTree, path: Bytes[] = []): HashedTreeWithPath {
920
+ function taprootAddPath(
921
+ tree: TArg<HashedTree>,
922
+ path: TArg<Bytes[]> = []
923
+ ): TRet<HashedTreeWithPath> {
562
924
  if (!tree) throw new Error(`taprootAddPath: empty tree`);
563
- if (tree.type === 'leaf') return { ...tree, path };
925
+ if (tree.type === 'leaf') return { ...tree, path } as TRet<HashedTreeWithPath>;
564
926
  if (tree.type !== 'branch') throw new Error(`taprootAddPath: wrong type=${tree}`);
565
927
  return {
566
928
  ...tree,
567
929
  path,
568
- // Left element has right hash in path and otherwise
930
+ // BIP 341 control blocks serialize sibling hashes from leaf to root, so prepend the
931
+ // current sibling before descending into the child subtree.
569
932
  left: taprootAddPath(tree.left, [tree.right.hash, ...path]),
570
933
  right: taprootAddPath(tree.right, [tree.left.hash, ...path]),
571
- };
934
+ } as TRet<HashedTreeWithPath>;
572
935
  }
573
- function taprootWalkTree(tree: HashedTreeWithPath): TaprootLeaf[] {
936
+ function taprootWalkTree(tree: TArg<HashedTreeWithPath>): TRet<TaprootLeaf[]> {
574
937
  if (!tree) throw new Error(`taprootAddPath: empty tree`);
575
- if (tree.type === 'leaf') return [tree];
938
+ if (tree.type === 'leaf') return [tree] as TRet<TaprootLeaf[]>;
576
939
  if (tree.type !== 'branch') throw new Error(`taprootWalkTree: wrong type=${tree}`);
577
- return [...taprootWalkTree(tree.left), ...taprootWalkTree(tree.right)];
940
+ // Keep a stable left-to-right DFS leaf order when flattening the annotated tree.
941
+ return [...taprootWalkTree(tree.left), ...taprootWalkTree(tree.right)] as TRet<TaprootLeaf[]>;
578
942
  }
579
943
 
580
944
  function taprootHashTree(
581
- tree: TaprootScriptTree,
582
- internalPubKey: Bytes,
945
+ tree: TArg<TaprootScriptTree>,
946
+ internalPubKey: TArg<Bytes>,
583
947
  allowUnknownOutputs = false,
584
- customScripts?: CustomScript[]
585
- ): HashedTree {
948
+ customScripts?: TArg<CustomScript[]>
949
+ ): TRet<HashedTree> {
586
950
  if (!tree) throw new Error('taprootHashTree: empty tree');
587
951
  if (Array.isArray(tree) && tree.length === 1) tree = tree[0];
588
952
  // Terminal node (leaf)
589
953
  if (!Array.isArray(tree)) {
590
- const { leafVersion: version, script: leafScript } = tree;
954
+ const version = tree.leafVersion;
955
+ const { script: leafScript } = tree;
591
956
  // Earliest tree walk where we can validate tapScripts
592
957
  if (tree.tapLeafScript || (tree.tapMerkleRoot && !u.equalBytes(tree.tapMerkleRoot, P.EMPTY)))
593
958
  throw new Error('P2TR: tapRoot leafScript cannot have tree');
@@ -598,8 +963,8 @@ function taprootHashTree(
598
963
  type: 'leaf',
599
964
  version,
600
965
  script,
601
- hash: tapLeafHash(script, version),
602
- };
966
+ hash: tapLeafHash(script, tapLeafVersion(version)),
967
+ } as TRet<HashedTree>;
603
968
  }
604
969
  // If tree / branch is not binary tree, convert it
605
970
  if (tree.length !== 2) tree = taprootListToTree(tree as TaprootNode[]) as TaprootNode[];
@@ -608,41 +973,91 @@ function taprootHashTree(
608
973
  // Both nodes should exist
609
974
  const left = taprootHashTree(tree[0], internalPubKey, allowUnknownOutputs, customScripts);
610
975
  const right = taprootHashTree(tree[1], internalPubKey, allowUnknownOutputs, customScripts);
611
- // We cannot swap left/right here, since it will change structure of tree
976
+ // BIP 341 sorts TapBranch child hashes lexicographically for hashing, but the original
977
+ // left/right structure still determines the control-block sibling paths for each leaf.
612
978
  let [lH, rH] = [left.hash, right.hash];
613
979
  if (u.compareBytes(rH, lH) === -1) [lH, rH] = [rH, lH];
614
- return { type: 'branch', left, right, hash: u.tagSchnorr('TapBranch', lH, rH) };
980
+ return {
981
+ type: 'branch',
982
+ left,
983
+ right,
984
+ hash: u.tagSchnorr('TapBranch', lH, rH),
985
+ } as TRet<HashedTree>;
615
986
  }
616
987
 
988
+ /** Default tapleaf version used by taproot script-path outputs before adding the parity bit. */
617
989
  export const TAP_LEAF_VERSION = 0xc0;
618
- export const tapLeafHash = (script: Bytes, version: number = TAP_LEAF_VERSION): Bytes =>
619
- u.tagSchnorr('TapLeaf', new Uint8Array([version]), VarBytes.encode(script));
990
+ const tapLeafVersion = (version: number | undefined): number => {
991
+ if (version === undefined) return TAP_LEAF_VERSION;
992
+ anumber(version, 'leafVersion');
993
+ // BIP341 script-path validation defines the effective leaf version as `v = c[0] & 0xfe`
994
+ // and says it cannot be odd or `0x50`; tapleaf hashes also serialize this as one byte.
995
+ if (version > 0xfe || version === 0x50 || !!(version & 1))
996
+ throw new Error(`P2TR: invalid leafVersion=${version}`);
997
+ return version;
998
+ };
999
+ /**
1000
+ * Computes the tagged hash of a tapleaf script.
1001
+ * @param script - tapleaf script bytes
1002
+ * @param version - base even tapleaf version byte (for tapscript, `0xc0`)
1003
+ * @returns Tapleaf hash.
1004
+ * @throws If the tapleaf version is not a valid even one-byte version.
1005
+ * {@link Error}
1006
+ * @example
1007
+ * Hash a finalized tapscript leaf before placing it into a Merkle tree.
1008
+ * ```ts
1009
+ * tapLeafHash(new Uint8Array([0x51]));
1010
+ * ```
1011
+ */
1012
+ export const tapLeafHash = (script: TArg<Bytes>, version: number = TAP_LEAF_VERSION): TRet<Bytes> =>
1013
+ u.tagSchnorr('TapLeaf', new Uint8Array([tapLeafVersion(version)]), VarBytes.encode(script));
620
1014
 
621
1015
  // Works as key OR tree.
622
1016
  // If we only have tree, need to add unspendable key, otherwise
623
1017
  // complex multisig wallet can be spent by owner of key only. See TAPROOT_UNSPENDABLE_KEY
1018
+ /** Conditional taproot return type for key-only or tree-backed outputs. */
624
1019
  export type P2TRRet<T> = T extends TaprootScriptTree ? P2TR_TREE : P2TR;
1020
+ /**
1021
+ * Builds a taproot output from an internal key and optional script tree.
1022
+ * @param internalPubKey - x-only internal public key, hex string, or `undefined` for script-only outputs
1023
+ * @param tree - optional taproot script tree
1024
+ * @param network - address network parameters
1025
+ * @param allowUnknownOutputs - whether unknown leaf scripts are allowed
1026
+ * @param customScripts - optional custom script codecs for taproot leaves
1027
+ * @returns Taproot descriptor with optional script-path metadata.
1028
+ * @throws If the internal key or taproot script tree is invalid. {@link Error}
1029
+ * @example
1030
+ * Combine script leaves into a final taproot output descriptor and address.
1031
+ * ```ts
1032
+ * import { hex } from '@scure/base';
1033
+ * import { p2tr, p2tr_pk } from '@scure/btc-signer/payment.js';
1034
+ * p2tr(
1035
+ * undefined,
1036
+ * [p2tr_pk(hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'))]
1037
+ * );
1038
+ * ```
1039
+ */
625
1040
  export function p2tr(
626
- internalPubKey: Bytes | string,
1041
+ internalPubKey: TArg<Bytes | string>,
627
1042
  tree?: undefined,
628
1043
  network?: BTC_NETWORK,
629
1044
  allowUnknownOutputs?: boolean,
630
- customScripts?: CustomScript[]
631
- ): Extends<P2TR, P2Ret>;
1045
+ customScripts?: TArg<CustomScript[]>
1046
+ ): TRet<Extends<P2TR, P2Ret>>;
632
1047
  export function p2tr(
633
- internalPubKey: Bytes | string,
634
- tree: TaprootScriptTree,
1048
+ internalPubKey: TArg<Bytes | string | undefined>,
1049
+ tree: TArg<TaprootScriptTree>,
635
1050
  network?: BTC_NETWORK,
636
1051
  allowUnknownOutputs?: boolean,
637
- customScripts?: CustomScript[]
638
- ): Extends<P2TR_TREE, P2Ret>;
1052
+ customScripts?: TArg<CustomScript[]>
1053
+ ): TRet<Extends<P2TR_TREE, P2Ret>>;
639
1054
  export function p2tr(
640
- internalPubKey?: Bytes | string,
641
- tree?: TaprootScriptTree,
1055
+ internalPubKey?: TArg<Bytes | string>,
1056
+ tree?: TArg<TaprootScriptTree>,
642
1057
  network: BTC_NETWORK = NETWORK,
643
1058
  allowUnknownOutputs = false,
644
- customScripts?: CustomScript[]
645
- ): Extends<P2TR & Partial<P2TR_TREE>, P2Ret> {
1059
+ customScripts?: TArg<CustomScript[]>
1060
+ ): TRet<Extends<P2TR & Partial<P2TR_TREE>, P2Ret>> {
646
1061
  // Unspendable
647
1062
  if (!internalPubKey && !tree) throw new Error('p2tr: should have pubKey or scriptTree (or both)');
648
1063
  const pubKey =
@@ -656,14 +1071,19 @@ export function p2tr(
656
1071
  );
657
1072
  const tapMerkleRoot = hashedTree.hash;
658
1073
  const [tweakedPubkey, parity] = u.taprootTweakPubkey(pubKey, tapMerkleRoot);
659
- const leaves = taprootWalkTree(hashedTree).map((l) => ({
660
- ...l,
661
- controlBlock: TaprootControlBlock.encode({
662
- version: (l.version || TAP_LEAF_VERSION) + parity,
663
- internalKey: pubKey,
664
- merklePath: l.path,
665
- }),
666
- }));
1074
+ const leaves = taprootWalkTree(hashedTree).map((l) => {
1075
+ const version = tapLeafVersion(l.version);
1076
+ return {
1077
+ ...l,
1078
+ // Leaf versions are stored as the base even byte; only the control block adds the
1079
+ // output-key parity bit required by BIP 341 script-path spending.
1080
+ controlBlock: TaprootControlBlock.encode({
1081
+ version: version + parity,
1082
+ internalKey: pubKey,
1083
+ merklePath: l.path,
1084
+ }),
1085
+ };
1086
+ });
667
1087
  return {
668
1088
  type: 'tr',
669
1089
  script: OutScript.encode({ type: 'tr', pubkey: tweakedPubkey }),
@@ -675,11 +1095,13 @@ export function p2tr(
675
1095
  leaves,
676
1096
  tapLeafScript: leaves.map((l) => [
677
1097
  TaprootControlBlock.decode(l.controlBlock),
678
- u.concatBytes(l.script, new Uint8Array([l.version || TAP_LEAF_VERSION])),
1098
+ u.concatBytes(l.script, new Uint8Array([tapLeafVersion(l.version)])),
679
1099
  ]),
680
1100
  tapMerkleRoot,
681
- } as const satisfies P2TR_TREE;
1101
+ } as const as TRet<Extends<P2TR_TREE, P2Ret>>;
682
1102
  } else {
1103
+ // BIP 341 / BIP 86: key-only Taproot still tweaks with the empty Merkle root so the
1104
+ // output commits to an unspendable script path instead of leaving the key untweaked.
683
1105
  const tweakedPubkey = u.taprootTweakPubkey(pubKey, P.EMPTY)[0];
684
1106
  return {
685
1107
  type: 'tr',
@@ -689,21 +1111,36 @@ export function p2tr(
689
1111
  tweakedPubkey,
690
1112
  // PSBT stuff
691
1113
  tapInternalKey: pubKey,
692
- } as const satisfies P2TR;
1114
+ } as const as TRet<Extends<P2TR, P2Ret>>;
693
1115
  }
694
1116
  }
695
1117
 
696
1118
  // Returns all combinations of size M from lst
1119
+ /**
1120
+ * Returns all size-`m` combinations from a list.
1121
+ * @param m - size of each combination
1122
+ * @param list - input items to combine
1123
+ * @returns Array of combinations.
1124
+ * @throws If the combination size or input list is invalid. {@link Error}
1125
+ * @example
1126
+ * Enumerate all size-two subsets of a short list.
1127
+ * ```ts
1128
+ * combinations(2, [1, 2, 3]);
1129
+ * ```
1130
+ */
697
1131
  export function combinations<T>(m: number, list: T[]): T[][] {
698
1132
  const res: T[][] = [];
699
1133
  if (!Array.isArray(list)) throw new Error('combinations: lst arg should be array');
700
1134
  const n = list.length;
701
- if (m > n) throw new Error('combinations: m > lst.length, no combinations possible');
1135
+ anumber(m, 'm');
1136
+ if (m < 1 || m > n) throw new Error('combinations: m must satisfy 1 <= m <= lst.length');
702
1137
  /*
703
1138
  Basically works as M nested loops like:
704
1139
  for (;idx[0]<lst.length;idx[0]++) for (idx[1]=idx[0]+1;idx[1]<lst.length;idx[1]++)
705
1140
  but since we cannot create nested loops dynamically, we unroll it to a single loop
706
1141
  */
1142
+ // This unrolled-loop implementation assumes an integer 1 <= m <= n; zero, negative, and
1143
+ // fractional m values need an explicit guard before entering the loop.
707
1144
  const idx = Array.from({ length: m }, (_, i) => i);
708
1145
  const last = idx.length - 1;
709
1146
  main: for (;;) {
@@ -727,119 +1164,274 @@ export function combinations<T>(m: number, list: T[]): T[][] {
727
1164
  /**
728
1165
  * M-of-N multi-leaf wallet via p2tr_ns. If m == n, single script is emitted.
729
1166
  * Takes O(n^2) if m != n. 99-of-100 is ok, 5-of-100 is not.
1167
+ * It materializes C(n, m) leaves, so middle-of-the-range thresholds blow up combinatorially.
730
1168
  * `2-of-[A,B,C] => [A,B] | [A,C] | [B,C]`
731
1169
  */
732
- export type P2TR_NS = { type: 'tr_ns'; script: Bytes };
1170
+ export type P2TR_NS = {
1171
+ /** Payment-script tag for taproot `CHECKSIGVERIFY` leaf scripts. */
1172
+ type: 'tr_ns';
1173
+ /** Serialized tapscript leaf. */
1174
+ script: TRet<Bytes>;
1175
+ };
1176
+ /**
1177
+ * Builds the leaf set for an M-of-N `CHECKSIGVERIFY` taproot policy.
1178
+ * @param m - number of required signatures
1179
+ * @param pubkeys - participating Schnorr public keys
1180
+ * @param allowSamePubkeys - whether duplicate keys are allowed
1181
+ * @returns Array of taproot leaf descriptors.
1182
+ * @throws If the taproot multisig parameters are invalid. {@link Error}
1183
+ * @example
1184
+ * Build the leaf set for an M-of-N taproot `CHECKSIGVERIFY` policy.
1185
+ * ```ts
1186
+ * import { hex } from '@scure/base';
1187
+ * import { p2tr_ns } from '@scure/btc-signer/payment.js';
1188
+ * p2tr_ns(1, [hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9')], true);
1189
+ * ```
1190
+ */
733
1191
  export const p2tr_ns = (
734
1192
  m: number,
735
- pubkeys: Bytes[],
1193
+ pubkeys: TArg<Bytes[]>,
736
1194
  allowSamePubkeys = false
737
- ): Extends<P2TR_NS, P2Ret>[] => {
1195
+ ): TRet<Extends<P2TR_NS, P2Ret>[]> => {
738
1196
  if (!allowSamePubkeys) uniqPubkey(pubkeys);
739
1197
  return combinations(m, pubkeys).map(
740
1198
  (i) =>
741
1199
  ({
742
1200
  type: 'tr_ns',
743
1201
  script: OutScript.encode({ type: 'tr_ns', pubkeys: i }),
744
- }) as const
745
- ) satisfies P2Ret[];
1202
+ }) as const as TRet<Extends<P2TR_NS, P2Ret>>
1203
+ ) as TRet<Extends<P2TR_NS, P2Ret>[]>;
746
1204
  };
747
1205
  // Taproot public key (case of p2tr_ns)
1206
+ /** Single-key taproot leaf descriptor. */
748
1207
  export type P2TR_PK = P2TR_NS;
749
- export const p2tr_pk = (pubkey: Bytes): Extends<P2TR_PK, P2Ret> =>
750
- p2tr_ns(1, [pubkey], undefined)[0] satisfies P2Ret;
1208
+ /**
1209
+ * Builds a single-key taproot leaf script.
1210
+ * BIP 341 design guidance: if this is the most likely single-key spend path, prefer
1211
+ * using that key as the `p2tr()` internal key instead of forcing it into a script leaf.
1212
+ * @param pubkey - Schnorr public key
1213
+ * @returns Taproot single-key leaf descriptor.
1214
+ * @throws If the taproot single-key leaf cannot be encoded. {@link Error}
1215
+ * @example
1216
+ * Build a single-key tapscript leaf.
1217
+ * ```ts
1218
+ * import { hex } from '@scure/base';
1219
+ * import { p2tr_pk } from '@scure/btc-signer/payment.js';
1220
+ * p2tr_pk(hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'));
1221
+ * ```
1222
+ */
1223
+ export const p2tr_pk = (pubkey: TArg<Bytes>): TRet<Extends<P2TR_PK, P2Ret>> =>
1224
+ p2tr_ns(1, [pubkey], undefined)[0];
751
1225
 
752
- export type P2TR_MS = { type: 'tr_ms'; script: Bytes };
1226
+ /** Taproot `CHECKSIGADD` multisig leaf descriptor. */
1227
+ export type P2TR_MS = {
1228
+ /** Payment-script tag for taproot `CHECKSIGADD` leaf scripts. */
1229
+ type: 'tr_ms';
1230
+ /** Serialized tapscript leaf. */
1231
+ script: TRet<Bytes>;
1232
+ };
1233
+ /**
1234
+ * Builds a `CHECKSIGADD` taproot multisig leaf.
1235
+ * @param m - number of required signatures
1236
+ * @param pubkeys - participating Schnorr public keys
1237
+ * @param allowSamePubkeys - whether duplicate keys are allowed
1238
+ * @returns Taproot multisig leaf descriptor.
1239
+ * @throws If the taproot multisig parameters are invalid. {@link Error}
1240
+ * @example
1241
+ * Build a `CHECKSIGADD` taproot multisig leaf.
1242
+ * ```ts
1243
+ * import { hex } from '@scure/base';
1244
+ * import { p2tr_ms } from '@scure/btc-signer/payment.js';
1245
+ * p2tr_ms(1, [hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9')], true);
1246
+ * ```
1247
+ */
753
1248
  export function p2tr_ms(
754
1249
  m: number,
755
- pubkeys: Bytes[],
1250
+ pubkeys: TArg<Bytes[]>,
756
1251
  allowSamePubkeys = false
757
- ): Extends<P2TR_MS, P2Ret> {
1252
+ ): TRet<Extends<P2TR_MS, P2Ret>> {
758
1253
  if (!allowSamePubkeys) uniqPubkey(pubkeys);
759
1254
  return {
760
1255
  type: 'tr_ms',
761
1256
  script: OutScript.encode({ type: 'tr_ms', pubkeys, m }),
762
- } as const satisfies P2Ret;
1257
+ } as const as TRet<Extends<P2TR_MS, P2Ret>>;
763
1258
  }
764
1259
 
765
1260
  // Simple pubkey address, without complex scripts
1261
+ /**
1262
+ * Derives a simple address from a private key.
1263
+ * @param type - address type to derive
1264
+ * @param privKey - private key bytes
1265
+ * @param network - address network parameters
1266
+ * @returns Encoded Bitcoin address.
1267
+ * @throws If the requested address type is unknown. {@link Error}
1268
+ * @example
1269
+ * Pick the output type first, then derive the matching address from the private key.
1270
+ * ```ts
1271
+ * import { getAddress } from '@scure/btc-signer/payment.js';
1272
+ * import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
1273
+ * getAddress('wpkh', randomPrivateKeyBytes());
1274
+ * ```
1275
+ */
766
1276
  export function getAddress(
767
1277
  type: 'pkh' | 'wpkh' | 'tr',
768
- privKey: Bytes,
1278
+ privKey: TArg<Bytes>,
769
1279
  network: BTC_NETWORK = NETWORK
770
- ): string | undefined {
1280
+ ): string {
771
1281
  if (type === 'tr') {
772
1282
  return p2tr(u.pubSchnorr(privKey), undefined, network).address;
773
1283
  }
1284
+ // This convenience wrapper always uses the compressed ECDSA public key; derive
1285
+ // `pubECDSA(privKey, false)` and call `p2pkh(...)` directly for legacy uncompressed P2PKH.
774
1286
  const pubKey = u.pubECDSA(privKey);
775
1287
  if (type === 'pkh') return p2pkh(pubKey, network).address;
776
1288
  if (type === 'wpkh') return p2wpkh(pubKey, network).address;
777
1289
  throw new Error(`getAddress: unknown type=${type}`);
778
1290
  }
779
1291
 
780
- export const _sortPubkeys = (pubkeys: Bytes[]): Bytes[] => Array.from(pubkeys).sort(u.compareBytes);
1292
+ // BIP67 defines canonical multisig ordering only for compressed pubkeys; this helper still sorts
1293
+ // raw bytes generically, and higher-level callers may accept uncompressed participants for compat.
1294
+ export const _sortPubkeys = (pubkeys: TArg<Bytes[]>): TRet<Bytes[]> =>
1295
+ Array.from(pubkeys).sort(u.compareBytes) as TRet<Bytes[]>;
781
1296
 
1297
+ /**
1298
+ * Builds a classic M-of-N multisig output, wrapped in P2SH or P2WSH.
1299
+ * @param m - number of required signatures
1300
+ * @param pubkeys - participating public keys
1301
+ * @param sorted - whether to sort the public keys first
1302
+ * @param witness - whether to wrap the result as native SegWit
1303
+ * @param network - address network parameters
1304
+ * @returns Multisig payment descriptor.
1305
+ * @throws If the multisig parameters or wrapped script are invalid. {@link Error}
1306
+ * @example
1307
+ * Wrap a classic 2-of-2 script into an addressable multisig output.
1308
+ * ```ts
1309
+ * import { multisig } from '@scure/btc-signer/payment.js';
1310
+ * import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
1311
+ * multisig(
1312
+ * 2,
1313
+ * [pubECDSA(randomPrivateKeyBytes()), pubECDSA(randomPrivateKeyBytes())],
1314
+ * true,
1315
+ * true
1316
+ * );
1317
+ * ```
1318
+ */
782
1319
  export function multisig(
783
1320
  m: number,
784
- pubkeys: Bytes[],
1321
+ pubkeys: TArg<Bytes[]>,
785
1322
  sorted = false,
786
1323
  witness = false,
787
1324
  network: BTC_NETWORK = NETWORK
788
- ): P2Ret {
1325
+ ): TRet<P2Ret> {
1326
+ // BIP 143 default policy: version-0 witness programs should use compressed ECDSA pubkeys only;
1327
+ // witness multisig callers must avoid uncompressed keys because p2ms accepts generic ECDSA encodings.
1328
+ // BIP 16 caps spendable compressed-key P2SH multisig at 15 pubkeys because larger redeem scripts
1329
+ // exceed the 520-byte push limit; use witness=true for larger classic multisig sets.
789
1330
  const ms = p2ms(m, sorted ? _sortPubkeys(pubkeys) : pubkeys);
790
- return witness ? p2wsh(ms, network) : p2sh(ms, network);
1331
+ return (witness ? p2wsh(ms, network) : p2sh(ms, network)) as TRet<P2Ret>;
791
1332
  }
792
1333
 
1334
+ /**
1335
+ * Builds a multisig output after lexicographically sorting the keys.
1336
+ * @param m - number of required signatures
1337
+ * @param pubkeys - participating public keys
1338
+ * @param witness - whether to wrap the result as native SegWit
1339
+ * @param network - address network parameters
1340
+ * @returns Sorted multisig payment descriptor.
1341
+ * @throws If the multisig parameters or wrapped script are invalid. {@link Error}
1342
+ * @example
1343
+ * Sort public keys deterministically before constructing the multisig address.
1344
+ * ```ts
1345
+ * import { sortedMultisig } from '@scure/btc-signer/payment.js';
1346
+ * import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
1347
+ * sortedMultisig(
1348
+ * 2,
1349
+ * [pubECDSA(randomPrivateKeyBytes()), pubECDSA(randomPrivateKeyBytes())],
1350
+ * true
1351
+ * );
1352
+ * ```
1353
+ */
793
1354
  export function sortedMultisig(
794
1355
  m: number,
795
- pubkeys: Bytes[],
1356
+ pubkeys: TArg<Bytes[]>,
796
1357
  witness = false,
797
1358
  network: BTC_NETWORK = NETWORK
798
- ): P2Ret {
799
- return multisig(m, pubkeys, true, witness, network);
1359
+ ): TRet<P2Ret> {
1360
+ // BIP67 canonical multisig is compressed-only, but this wrapper intentionally keeps the generic
1361
+ // sorted-multisig behavior and still allows uncompressed participant keys for compatibility.
1362
+ return multisig(m, pubkeys, true, witness, network) as TRet<P2Ret>;
800
1363
  }
801
1364
 
802
- const base58check = createBase58check(u.sha256);
1365
+ const base58check = /* @__PURE__ */ createBase58check(u.sha256);
803
1366
 
804
- function validateWitness(version: number, data: Bytes) {
1367
+ function validateWitness(version: number, data: TArg<Bytes>) {
805
1368
  if (data.length < 2 || data.length > 40) throw new Error('Witness: invalid length');
806
1369
  if (version > 16) throw new Error('Witness: invalid version');
807
1370
  if (version === 0 && !(data.length === 20 || data.length === 32))
808
1371
  throw new Error('Witness: invalid length for version');
809
1372
  }
810
1373
 
811
- function programToWitness(version: number, data: Bytes, network = NETWORK) {
1374
+ function programToWitness(version: number, data: TArg<Bytes>, network = NETWORK) {
812
1375
  validateWitness(version, data);
1376
+ // BIP 350 keeps segwit v0 on Bech32, while witness versions 1+ switch to Bech32m.
813
1377
  const coder = version === 0 ? bech32 : bech32m;
814
1378
  return coder.encode(network.bech32, [version].concat(coder.toWords(data)));
815
1379
  }
816
1380
 
817
- function formatKey(hashed: Bytes, prefix: number[]): string {
1381
+ function formatKey(hashed: TArg<Bytes>, prefix: number[]): string {
1382
+ // Legacy Base58Check paths all serialize [version-byte || payload] before checksumming.
818
1383
  return base58check.encode(u.concatBytes(Uint8Array.from(prefix), hashed));
819
1384
  }
820
1385
 
821
- export function WIF(network: BTC_NETWORK = NETWORK): Coder<Bytes, string> {
1386
+ /**
1387
+ * Wallet-import-format coder for private keys.
1388
+ * @param network - address network parameters
1389
+ * @returns WIF coder.
1390
+ * @example
1391
+ * Encode or decode wallet-import-format private keys.
1392
+ * ```ts
1393
+ * const coder = WIF();
1394
+ * coder.encode(new Uint8Array(32).fill(1));
1395
+ * ```
1396
+ */
1397
+ export function WIF(network: BTC_NETWORK = NETWORK): TRet<Coder<Bytes, string>> {
822
1398
  return {
823
- encode(privKey: Bytes) {
1399
+ encode(privKey: TArg<Bytes>) {
1400
+ // Compressed WIF is exactly 32 private-key bytes plus the 0x01 suffix; shorter or longer
1401
+ // inputs must be rejected instead of being silently padded or truncated by subarray().
1402
+ abytes(privKey, 32, 'privKey');
824
1403
  const compressed = u.concatBytes(privKey, new Uint8Array([0x01]));
825
1404
  return formatKey(compressed.subarray(0, 33), [network.wif]);
826
1405
  },
827
- decode(wif: string) {
1406
+ decode(wif: string): TRet<Bytes> {
828
1407
  let parsed = base58check.decode(wif);
829
1408
  if (parsed[0] !== network.wif) throw new Error('Wrong WIF prefix');
830
1409
  parsed = parsed.subarray(1);
831
1410
  // Check what it is. Compressed flag?
832
1411
  if (parsed.length !== 33) throw new Error('Wrong WIF length');
833
1412
  if (parsed[32] !== 0x01) throw new Error('Wrong WIF postfix');
834
- return parsed.subarray(0, -1);
1413
+ return parsed.subarray(0, -1) as TRet<Bytes>;
835
1414
  },
836
1415
  };
837
1416
  }
838
1417
 
839
1418
  // Returns OutType, which can be used to create outscript
1419
+ /**
1420
+ * Address encoder/decoder for a specific Bitcoin network.
1421
+ * @param network - address network parameters
1422
+ * @returns Address coder backed by the provided network.
1423
+ * @example
1424
+ * Create a network-specific address coder and encode a payment descriptor.
1425
+ * ```ts
1426
+ * import { Address, p2wpkh } from '@scure/btc-signer/payment.js';
1427
+ * import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
1428
+ * const coder = Address();
1429
+ * coder.encode(p2wpkh(pubECDSA(randomPrivateKeyBytes())));
1430
+ * ```
1431
+ */
840
1432
  export function Address(network: BTC_NETWORK = NETWORK) {
841
1433
  return {
842
- encode(from: P.UnwrapCoder<OutScriptType>): string {
1434
+ encode(from: Exclude<OutScriptValue, undefined>): string {
843
1435
  const { type } = from;
844
1436
  if (type === 'wpkh') return programToWitness(0, from.hash, network);
845
1437
  else if (type === 'wsh') return programToWitness(0, from.hash, network);
@@ -848,7 +1440,7 @@ export function Address(network: BTC_NETWORK = NETWORK) {
848
1440
  else if (type === 'sh') return formatKey(from.hash, [network.scriptHash]);
849
1441
  throw new Error(`Unknown address type=${type}`);
850
1442
  },
851
- decode(address: string): P.UnwrapCoder<OutScriptType> {
1443
+ decode(address: string): OutScriptValue {
852
1444
  if (address.length < 14 || address.length > 74) throw new Error('Invalid address length');
853
1445
  // Bech32
854
1446
  if (network.bech32 && address.toLowerCase().startsWith(`${network.bech32}1`)) {
@@ -865,21 +1457,26 @@ export function Address(network: BTC_NETWORK = NETWORK) {
865
1457
  const [version, ...program] = res.words;
866
1458
  const data = bech32.fromWords(program);
867
1459
  validateWitness(version, data);
868
- if (version === 0 && data.length === 32) return { type: 'wsh', hash: data };
869
- else if (version === 0 && data.length === 20) return { type: 'wpkh', hash: data };
870
- else if (version === 1 && data.length === 32) return { type: 'tr', pubkey: data };
1460
+ if (version === 0 && data.length === 32)
1461
+ return { type: 'wsh', hash: data } as OutScriptValue;
1462
+ else if (version === 0 && data.length === 20)
1463
+ return { type: 'wpkh', hash: data } as OutScriptValue;
1464
+ else if (version === 1 && data.length === 32)
1465
+ return { type: 'tr', pubkey: data } as OutScriptValue;
1466
+ // Future witness versions can still be valid addresses, but this helper
1467
+ // only returns typed descriptors for recognized v0 and taproot templates.
871
1468
  else throw new Error('Unknown witness program');
872
1469
  }
873
1470
  const data = base58check.decode(address);
874
1471
  if (data.length !== 21) throw new Error('Invalid base58 address');
875
1472
  // Pay To Public Key Hash
876
1473
  if (data[0] === network.pubKeyHash) {
877
- return { type: 'pkh', hash: data.slice(1) };
1474
+ return { type: 'pkh', hash: data.slice(1) } as OutScriptValue;
878
1475
  } else if (data[0] === network.scriptHash) {
879
1476
  return {
880
1477
  type: 'sh',
881
1478
  hash: data.slice(1),
882
- };
1479
+ } as OutScriptValue;
883
1480
  }
884
1481
  throw new Error(`Invalid address prefix=${data[0]}`);
885
1482
  },