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