@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/payment.js CHANGED
@@ -1,22 +1,34 @@
1
1
  import { bech32, bech32m, 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 } from "./psbt.js";
4
- import { OpToNum, Script, VarBytes } from "./script.js";
6
+ import { MAX_SCRIPT_BYTE_LENGTH, OpToNum, Script, VarBytes } from "./script.js";
5
7
  import * as u from "./utils.js";
6
8
  import { NETWORK } from "./utils.js";
9
+ // Pay to Anchor (P2A)
10
+ // BIP433 Pay-to-Anchor witness program bytes; the scriptPubKey is `OP_1 <0x4e73>`.
11
+ const P2A_PROGRAM = /* @__PURE__ */ Uint8Array.from([0x4e, 0x73]);
7
12
  const OutP2A = {
8
13
  encode(from) {
9
- if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1]) || hex.encode(from[1]) !== '4e73')
14
+ // BIP433 defines P2A as the exact OP_1 <0x4e73> scriptPubKey.
15
+ if (from.length !== 2 ||
16
+ from[0] !== 1 ||
17
+ !u.isBytes(from[1]) ||
18
+ !u.equalBytes(from[1], P2A_PROGRAM))
10
19
  return;
11
20
  return { type: 'p2a', script: Script.encode(from) };
12
21
  },
13
22
  decode: (to) => {
14
23
  if (to.type !== 'p2a')
15
24
  return;
16
- return [1, hex.decode('4e73')];
25
+ // The decoded object keeps `script` for caller convenience, but the `p2a`
26
+ // tag always canonicalizes back to the fixed BIP433 script.
27
+ return [1, Uint8Array.from(P2A_PROGRAM)];
17
28
  },
18
29
  };
19
30
  function isValidPubkey(pub, type) {
31
+ // Payment coders use a boolean guard here and normalize validatePubkey failures to false.
20
32
  try {
21
33
  u.validatePubkey(pub, type);
22
34
  return true;
@@ -27,6 +39,8 @@ function isValidPubkey(pub, type) {
27
39
  }
28
40
  const OutPK = {
29
41
  encode(from) {
42
+ // BIP380/BIP381 `pk(KEY)` only admits SEC1 ECDSA pubkeys here; x-only
43
+ // 32-byte CHECKSIG scripts are left for the later tapscript coders.
30
44
  if (from.length !== 2 ||
31
45
  !u.isBytes(from[0]) ||
32
46
  !isValidPubkey(from[0], u.PubT.ecdsa) ||
@@ -34,44 +48,73 @@ const OutPK = {
34
48
  return;
35
49
  return { type: 'pk', pubkey: from[0] };
36
50
  },
37
- decode: (to) => (to.type === 'pk' ? [to.pubkey, 'CHECKSIG'] : undefined),
51
+ decode: (to) => {
52
+ if (to.type !== 'pk')
53
+ return;
54
+ // OutScript validates `pk.pubkey` before this branch emits the canonical
55
+ // `<pubkey> CHECKSIG` script.
56
+ return [to.pubkey, 'CHECKSIG'];
57
+ },
38
58
  };
39
59
  const OutPKH = {
40
60
  encode(from) {
41
61
  if (from.length !== 5 || from[0] !== 'DUP' || from[1] !== 'HASH160' || !u.isBytes(from[2]))
42
62
  return;
63
+ // Require the exact 20-byte HASH160 here so near-miss scripts fall through
64
+ // to OutUnknown instead of throwing in the OutScript validator on decode.
65
+ if (from[2].length !== 20)
66
+ return;
43
67
  if (from[3] !== 'EQUALVERIFY' || from[4] !== 'CHECKSIG')
44
68
  return;
45
69
  return { type: 'pkh', hash: from[2] };
46
70
  },
47
- decode: (to) => to.type === 'pkh' ? ['DUP', 'HASH160', to.hash, 'EQUALVERIFY', 'CHECKSIG'] : undefined,
71
+ // OutScript validates `pkh.hash` before this branch emits the canonical
72
+ // `DUP HASH160 <hash> EQUALVERIFY CHECKSIG` script.
73
+ decode: (to) => (to.type === 'pkh'
74
+ ? ['DUP', 'HASH160', to.hash, 'EQUALVERIFY', 'CHECKSIG']
75
+ : undefined),
48
76
  };
49
77
  const OutSH = {
50
78
  encode(from) {
51
79
  if (from.length !== 3 || from[0] !== 'HASH160' || !u.isBytes(from[1]) || from[2] !== 'EQUAL')
52
80
  return;
81
+ // Require the exact 20-byte HASH160 here so near-miss scripts fall through
82
+ // to OutUnknown instead of throwing in the OutScript validator on decode.
83
+ if (from[1].length !== 20)
84
+ return;
53
85
  return { type: 'sh', hash: from[1] };
54
86
  },
55
- decode: (to) => to.type === 'sh' ? ['HASH160', to.hash, 'EQUAL'] : undefined,
87
+ // OutScript validates `sh.hash` before this branch emits the canonical
88
+ // `HASH160 <hash> EQUAL` script.
89
+ decode: (to) => (to.type === 'sh' ? ['HASH160', to.hash, 'EQUAL'] : undefined),
56
90
  };
57
91
  const OutWSH = {
58
92
  encode(from) {
59
93
  if (from.length !== 2 || from[0] !== 0 || !u.isBytes(from[1]))
60
94
  return;
95
+ // BIP382 `wsh()` is specifically the version-0 32-byte witness program.
96
+ // Other witness versions stay with the later coders.
61
97
  if (from[1].length !== 32)
62
98
  return;
63
99
  return { type: 'wsh', hash: from[1] };
64
100
  },
101
+ // OutScript validates `wsh.hash` before this branch emits the canonical
102
+ // version-0 32-byte witness program.
65
103
  decode: (to) => (to.type === 'wsh' ? [0, to.hash] : undefined),
66
104
  };
67
105
  const OutWPKH = {
68
106
  encode(from) {
69
107
  if (from.length !== 2 || from[0] !== 0 || !u.isBytes(from[1]))
70
108
  return;
109
+ // BIP382 `wpkh()` is specifically the version-0 20-byte witness program.
110
+ // Compressed-key restrictions are enforced upstream, and other witness
111
+ // versions stay with the later coders.
71
112
  if (from[1].length !== 20)
72
113
  return;
73
114
  return { type: 'wpkh', hash: from[1] };
74
115
  },
116
+ // OutScript validates `wpkh.hash` before this branch emits the canonical
117
+ // version-0 20-byte witness program.
75
118
  decode: (to) => (to.type === 'wpkh' ? [0, to.hash] : undefined),
76
119
  };
77
120
  const OutMS = {
@@ -86,20 +129,43 @@ const OutMS = {
86
129
  const pubkeys = from.slice(1, -2);
87
130
  if (n !== pubkeys.length)
88
131
  return;
132
+ // Require valid ECDSA pubkeys and `0 < m <= n` here so near-miss
133
+ // CHECKMULTISIG scripts (garbage keys, degenerate 0-of-0) fall through to
134
+ // OutUnknown instead of throwing in the OutScript validator on decode.
135
+ // Script.decode only yields 0..16 for opcode numbers, so n <= 16 holds.
89
136
  for (const pub of pubkeys)
90
- if (!u.isBytes(pub))
137
+ if (!u.isBytes(pub) || !isValidPubkey(pub, u.PubT.ecdsa))
91
138
  return;
92
- return { type: 'ms', m, pubkeys: pubkeys }; // we don't need n, since it is the same as pubkeys
139
+ if (!Number.isSafeInteger(m) || m < 1 || m > n)
140
+ return;
141
+ // We don't need n here because it is the same as pubkeys.length.
142
+ return { type: 'ms', m, pubkeys: pubkeys };
93
143
  },
94
144
  // checkmultisig(n, ..pubkeys, m)
95
- decode: (to) => to.type === 'ms' ? [to.m, ...to.pubkeys, to.pubkeys.length, 'CHECKMULTISIG'] : undefined,
145
+ decode: (to) =>
146
+ // OutScript validates multisig pubkeys and `0 < m <= n <= 16`.
147
+ // This branch only emits the canonical `m <pubkeys...> n CHECKMULTISIG`
148
+ // script.
149
+ (to.type === 'ms'
150
+ ? [to.m, ...to.pubkeys, to.pubkeys.length, 'CHECKMULTISIG']
151
+ : undefined),
96
152
  };
97
153
  const OutTR = {
98
154
  encode(from) {
99
- if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1]))
155
+ // BIP141 witness programs are `OP_0..OP_16` followed by a direct 2..40-byte push.
156
+ // BIP341 assigns native taproot meaning only to version 1 with a 32-byte x-only program;
157
+ // other OP_1 program lengths remain reserved future witness programs and should fall through.
158
+ if (from.length !== 2 || from[0] !== 1 || !u.isBytes(from[1]) || from[1].length !== 32)
159
+ return;
160
+ // A 32-byte v1 program with an off-curve x coordinate is a fundable but
161
+ // taproot-unspendable output; classify it as unknown instead of throwing
162
+ // in the OutScript validator on decode.
163
+ if (!isValidPubkey(from[1], u.PubT.schnorr))
100
164
  return;
101
165
  return { type: 'tr', pubkey: from[1] };
102
166
  },
167
+ // OutScript validates `tr.pubkey` before this branch emits the canonical
168
+ // version-1 32-byte witness program.
103
169
  decode: (to) => (to.type === 'tr' ? [1, to.pubkey] : undefined),
104
170
  };
105
171
  const OutTRNS = {
@@ -116,10 +182,17 @@ const OutTRNS = {
116
182
  return;
117
183
  continue;
118
184
  }
119
- if (!u.isBytes(elm))
185
+ // Require actual Schnorr pubkeys here so near-miss `<bytes> CHECKSIG`
186
+ // scripts fall through to OutUnknown instead of failing later.
187
+ if (!u.isBytes(elm) || !isValidPubkey(elm, u.PubT.schnorr))
120
188
  return;
121
189
  pubkeys.push(elm);
122
190
  }
191
+ // BIP342 "Using a k-of-k script for every combination" documents the shape
192
+ // `<pubkey_1> CHECKSIGVERIFY ... <pubkey_n> CHECKSIG`; this matcher only
193
+ // classifies that embedded-pubkey form, so bare CHECKSIG stays unknown.
194
+ if (!pubkeys.length)
195
+ return;
123
196
  return { type: 'tr_ns', pubkeys };
124
197
  },
125
198
  decode: (to) => {
@@ -128,6 +201,8 @@ const OutTRNS = {
128
201
  const out = [];
129
202
  for (let i = 0; i < to.pubkeys.length - 1; i++)
130
203
  out.push(to.pubkeys[i], 'CHECKSIGVERIFY');
204
+ // This branch assumes at least one Schnorr pubkey; [] would otherwise emit
205
+ // `[undefined, CHECKSIG]` and only fail later in Script.encode.
131
206
  out.push(to.pubkeys[to.pubkeys.length - 1], 'CHECKSIG');
132
207
  return out;
133
208
  },
@@ -143,15 +218,22 @@ const OutTRMS = {
143
218
  return;
144
219
  for (let i = 0; i < last - 1; i++) {
145
220
  const elm = from[i];
221
+ // Structural mismatches should fall through to OutUnknown instead of
222
+ // throwing from the tr_ms matcher.
146
223
  if (i & 1) {
147
224
  if (elm !== (i === 1 ? 'CHECKSIG' : 'CHECKSIGADD'))
148
- throw new Error('OutScript.encode/tr_ms: wrong element');
225
+ return;
149
226
  continue;
150
227
  }
151
- if (!u.isBytes(elm))
152
- throw new Error('OutScript.encode/tr_ms: wrong key element');
228
+ // Require actual Schnorr pubkeys here (same as tr_ns) so near-miss
229
+ // CHECKSIGADD scripts fall through to OutUnknown instead of throwing
230
+ // in the OutScript validator on decode.
231
+ if (!u.isBytes(elm) || !isValidPubkey(elm, u.PubT.schnorr))
232
+ return;
153
233
  pubkeys.push(elm);
154
234
  }
235
+ if (!Number.isSafeInteger(m) || m < 1 || m > pubkeys.length || pubkeys.length > 999)
236
+ return;
155
237
  return { type: 'tr_ms', pubkeys, m };
156
238
  },
157
239
  decode: (to) => {
@@ -160,18 +242,29 @@ const OutTRMS = {
160
242
  const out = [to.pubkeys[0], 'CHECKSIG'];
161
243
  for (let i = 1; i < to.pubkeys.length; i++)
162
244
  out.push(to.pubkeys[i], 'CHECKSIGADD');
245
+ // This branch assumes `m` was already validated as an integer ScriptNum;
246
+ // fractional JS numbers would otherwise serialize as a different threshold.
163
247
  out.push(to.m, 'NUMEQUAL');
164
248
  return out;
165
249
  },
166
250
  };
167
251
  const OutUnknown = {
168
252
  encode(from) {
253
+ // This is the catch-all fallback for scripts no structured coder recognized,
254
+ // so earlier matchers must return `undefined` instead of throwing on mismatch.
255
+ // Because this reserializes the parsed Script AST, unknown scripts preserve
256
+ // semantics but not original non-minimal push spellings.
169
257
  return { type: 'unknown', script: Script.encode(from) };
170
258
  },
171
- decode: (to) => to.type === 'unknown' ? Script.decode(to.script) : undefined,
259
+ decode: (to) =>
260
+ // This reparses `unknown.script` through the semantic Script codec, so raw
261
+ // bytes must still be syntactically parseable and may canonicalize on re-encode.
262
+ (to.type === 'unknown' ? Script.decode(to.script) : undefined),
172
263
  };
173
264
  // /Payments
174
- const OutScripts = [
265
+ const OutScripts = /* @__PURE__ */ (() => [
266
+ // Order is semantic: specific structured coders run first and the catch-all
267
+ // unknown fallback must stay last.
175
268
  OutP2A,
176
269
  OutPK,
177
270
  OutPKH,
@@ -183,15 +276,28 @@ const OutScripts = [
183
276
  OutTRNS,
184
277
  OutTRMS,
185
278
  OutUnknown,
186
- ];
279
+ ])();
187
280
  // TODO: we can support user supplied output scripts now
188
281
  // - addOutScript
189
282
  // - removeOutScript
190
283
  // - We can do that as log we modify array in-place
191
284
  // - Actually is very hard, since there is sign/finalize logic
192
- const _OutScript = P.apply(Script, P.coders.match(OutScripts));
285
+ // Raw composition of semantic Script parsing with the ordered output-script
286
+ // matcher; OutScript adds the higher-level validation layer on top.
287
+ const _OutScript = /* @__PURE__ */ (() => P.apply(Script, P.coders.match(OutScripts)))();
193
288
  // We can validate this once, because of packed & coders
194
- export const OutScript = P.validate(_OutScript, (i) => {
289
+ /**
290
+ * Coder for recognized Bitcoin output scripts.
291
+ * @example
292
+ * Decode a serialized output script back into the tagged payment descriptor.
293
+ * ```ts
294
+ * import { OutScript, p2wpkh } from '@scure/btc-signer/payment.js';
295
+ * import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
296
+ * const pay = p2wpkh(pubECDSA(randomPrivateKeyBytes()));
297
+ * OutScript.decode(pay.script);
298
+ * ```
299
+ */
300
+ export const OutScript = /* @__PURE__ */ (() => Object.freeze(P.validate(_OutScript, (i) => {
195
301
  if (i.type === 'pk' && !isValidPubkey(i.pubkey, u.PubT.ecdsa))
196
302
  throw new Error('OutScript/pk: wrong key');
197
303
  if ((i.type === 'pkh' || i.type === 'sh' || i.type === 'wpkh') &&
@@ -209,6 +315,9 @@ export const OutScript = P.validate(_OutScript, (i) => {
209
315
  for (const p of i.pubkeys)
210
316
  if (!isValidPubkey(p, u.PubT.ecdsa))
211
317
  throw new Error('OutScript/multisig: wrong pubkey');
318
+ // Range checks are not enough here: non-integer JS numbers like 1.5 would
319
+ // otherwise slip through and serialize as a different ScriptNum threshold.
320
+ anumber(i.m, 'm');
212
321
  if (i.m <= 0 || n > 16 || i.m > n)
213
322
  throw new Error('OutScript/multisig: invalid params');
214
323
  }
@@ -219,61 +328,133 @@ export const OutScript = P.validate(_OutScript, (i) => {
219
328
  }
220
329
  if (i.type === 'tr_ms') {
221
330
  const n = i.pubkeys.length;
331
+ // BIP 342 keeps the 1000-element stack limit. This CHECKSIG/CHECKSIGADD form
332
+ // momentarily has n witness items plus one pushed pubkey on the stack, so n must stay <= 999.
333
+ anumber(i.m, 'm');
222
334
  if (i.m <= 0 || n > 999 || i.m > n)
223
335
  throw new Error('OutScript/tr_ms: invalid params');
224
336
  }
225
337
  return i;
226
- });
338
+ })))();
227
339
  // Basic sanity check for scripts
228
340
  function checkWSH(s, witnessScript) {
229
341
  if (!u.equalBytes(s.hash, u.sha256(witnessScript)))
230
342
  throw new Error('checkScript: wsh wrong witnessScript hash');
343
+ // BIP141 only requires the witnessScript hash match; the type-based rejects
344
+ // below are an extra descriptor sanity layer for BIP382 invalid-descriptor
345
+ // bullets `wpkh() nested in wsh()` and `wsh() nested in wsh()`.
231
346
  const w = OutScript.decode(witnessScript);
232
347
  if (w.type === 'tr' || w.type === 'tr_ns' || w.type === 'tr_ms')
233
348
  throw new Error(`checkScript: P2${w.type} cannot be wrapped in P2SH`);
234
- if (w.type === 'wpkh' || w.type === 'sh')
349
+ if (w.type === 'wpkh' || w.type === 'wsh' || w.type === 'sh')
235
350
  throw new Error(`checkScript: P2${w.type} cannot be wrapped in P2WSH`);
236
351
  }
352
+ /**
353
+ * Validates that nested redeem and witness scripts match their wrappers.
354
+ * @param script - top-level output script
355
+ * @param redeemScript - optional redeem script for P2SH wrappers
356
+ * @param witnessScript - optional witness script for P2WSH wrappers
357
+ * @throws If the script nesting is invalid or unsupported. {@link Error}
358
+ * @example
359
+ * Verify that wrapped scripts and hashes still match after custom edits.
360
+ * ```ts
361
+ * import { hex } from '@scure/base';
362
+ * import { checkScript, p2pkh, p2sh } from '@scure/btc-signer/payment.js';
363
+ * const wrapped = p2sh(
364
+ * p2pkh(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))
365
+ * );
366
+ * checkScript(wrapped.script, wrapped.redeemScript);
367
+ * ```
368
+ */
237
369
  export function checkScript(script, redeemScript, witnessScript) {
370
+ let hasWsh = false;
371
+ let r = undefined;
238
372
  if (script) {
239
373
  const s = OutScript.decode(script);
374
+ // BIP174 Data Signers Check For bullets: provided redeemScript must match
375
+ // the scriptPubKey, and provided witnessScript must match the scriptPubKey
376
+ // or redeemScript instead of being silently ignored as stray metadata.
240
377
  // ms||pk maybe work, but there will be no address, hard to spend
241
378
  if (s.type === 'tr_ns' || s.type === 'tr_ms' || s.type === 'ms' || s.type == 'pk')
242
379
  throw new Error(`checkScript: non-wrapped ${s.type}`);
243
- if (s.type === 'sh' && redeemScript) {
380
+ if (redeemScript) {
381
+ if (s.type !== 'sh')
382
+ throw new Error('checkScript: redeemScript without P2SH');
244
383
  if (!u.equalBytes(s.hash, u.hash160(redeemScript)))
245
384
  throw new Error('checkScript: sh wrong redeemScript hash');
246
- const r = OutScript.decode(redeemScript);
247
- if (r.type === 'tr' || r.type === 'tr_ns' || r.type === 'tr_ms')
385
+ r = OutScript.decode(redeemScript);
386
+ if (r?.type === 'tr' || r?.type === 'tr_ns' || r?.type === 'tr_ms')
248
387
  throw new Error(`checkScript: P2${r.type} cannot be wrapped in P2SH`);
249
388
  // Not sure if this unspendable, but we cannot represent this via PSBT
250
- if (r.type === 'sh')
389
+ if (r?.type === 'sh')
251
390
  throw new Error('checkScript: P2SH cannot be wrapped in P2SH');
252
391
  }
253
- if (s.type === 'wsh' && witnessScript)
254
- checkWSH(s, witnessScript);
392
+ if (s.type === 'wsh') {
393
+ hasWsh = true;
394
+ if (witnessScript)
395
+ checkWSH(s, witnessScript);
396
+ }
255
397
  }
256
398
  if (redeemScript) {
257
- const r = OutScript.decode(redeemScript);
258
- if (r.type === 'wsh' && witnessScript)
259
- checkWSH(r, witnessScript);
399
+ if (r === undefined)
400
+ r = OutScript.decode(redeemScript);
401
+ if (r?.type === 'wsh') {
402
+ hasWsh = true;
403
+ if (witnessScript)
404
+ checkWSH(r, witnessScript);
405
+ }
260
406
  }
407
+ if (witnessScript && !hasWsh)
408
+ throw new Error('checkScript: witnessScript without P2WSH');
261
409
  }
262
410
  function uniqPubkey(pubkeys) {
263
411
  const map = {};
264
412
  for (const pub of pubkeys) {
413
+ // Exact-byte duplicate filter only: BIP383 valid vectors still permit the
414
+ // same point to appear in compressed and uncompressed SEC1 form in multi().
265
415
  const key = hex.encode(pub);
266
416
  if (map[key])
267
417
  throw new Error(`Multisig: non-uniq pubkey: ${pubkeys.map(hex.encode)}`);
268
418
  map[key] = true;
269
419
  }
270
420
  }
421
+ /**
422
+ * Builds a pay-to-public-key script.
423
+ * @param pubkey - compressed or uncompressed ECDSA public key
424
+ * @param _network - unused network placeholder for API consistency
425
+ * @returns P2PK descriptor.
426
+ * @throws If the public key cannot be encoded as a P2PK output. {@link Error}
427
+ * @example
428
+ * Build a bare pay-to-public-key output.
429
+ * ```ts
430
+ * import { hex } from '@scure/base';
431
+ * import { p2pk } from '@scure/btc-signer/payment.js';
432
+ * p2pk(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'));
433
+ * ```
434
+ */
271
435
  export const p2pk = (pubkey, _network = NETWORK) => {
272
436
  // network is unused
273
437
  if (!isValidPubkey(pubkey, u.PubT.ecdsa))
274
438
  throw new Error('P2PK: invalid publicKey');
275
- return { type: 'pk', script: OutScript.encode({ type: 'pk', pubkey }) };
439
+ return {
440
+ type: 'pk',
441
+ script: OutScript.encode({ type: 'pk', pubkey }),
442
+ };
276
443
  };
444
+ /**
445
+ * Builds a P2PKH output from a public key.
446
+ * @param publicKey - compressed or uncompressed ECDSA public key bytes; HASH160 commits to the exact encoding
447
+ * @param network - address network parameters
448
+ * @returns P2PKH descriptor.
449
+ * @throws If the public key cannot be encoded as a P2PKH output. {@link Error}
450
+ * @example
451
+ * Build a classic pay-to-public-key-hash output.
452
+ * ```ts
453
+ * import { hex } from '@scure/base';
454
+ * import { p2pkh } from '@scure/btc-signer/payment.js';
455
+ * p2pkh(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'));
456
+ * ```
457
+ */
277
458
  export const p2pkh = (publicKey, network = NETWORK) => {
278
459
  if (!isValidPubkey(publicKey, u.PubT.ecdsa))
279
460
  throw new Error('P2PKH: invalid publicKey');
@@ -285,52 +466,108 @@ export const p2pkh = (publicKey, network = NETWORK) => {
285
466
  hash,
286
467
  };
287
468
  };
469
+ /**
470
+ * Wraps a child script inside P2SH.
471
+ * @param child - child payment descriptor to wrap
472
+ * @param network - address network parameters
473
+ * @returns P2SH descriptor preserving witness metadata when present.
474
+ * @throws If the wrapped script combination is invalid or unsupported. {@link Error}
475
+ * @example
476
+ * Wrap a child script in P2SH so it gets a base58 address form.
477
+ * ```ts
478
+ * import { hex } from '@scure/base';
479
+ * import { p2pk, p2sh, p2wsh } from '@scure/btc-signer/payment.js';
480
+ * p2sh(p2wsh(p2pk(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))));
481
+ * ```
482
+ */
288
483
  export const p2sh = (child, network = NETWORK) => {
484
+ u.validateObject(child, {}, {}, 'child');
289
485
  // It is already tested inside noble-hashes and checkScript
290
- const cs = child.script;
486
+ // BIP16 redeemScripts are pushed by scriptSig, so anything over the 520-byte pushed-element
487
+ // limit would be fundable by HASH160 but unspendable once wrapped in P2SH.
488
+ const c = child;
489
+ const cs = c.script;
291
490
  if (!u.isBytes(cs))
292
- throw new Error(`Wrong script: ${typeof child.script}, expected Uint8Array`);
491
+ throw new Error(`Wrong script: ${typeof c.script}, expected Uint8Array`);
492
+ if (cs.length > MAX_SCRIPT_BYTE_LENGTH)
493
+ throw new Error(`P2SH: redeemScript exceeds ${MAX_SCRIPT_BYTE_LENGTH}-byte push limit: len=${cs.length}`);
293
494
  const hash = u.hash160(cs);
294
- const script = OutScript.encode({ type: 'sh', hash });
295
- checkScript(script, cs, child.witnessScript);
296
- if (child.witnessScript) {
495
+ const out = { type: 'sh', hash };
496
+ const script = OutScript.encode(out);
497
+ const address = Address(network).encode(out);
498
+ checkScript(script, cs, c.witnessScript);
499
+ if (c.witnessScript) {
297
500
  return {
298
501
  type: 'sh',
299
502
  redeemScript: cs,
300
- script: OutScript.encode({ type: 'sh', hash }),
301
- address: Address(network).encode({ type: 'sh', hash }),
503
+ script,
504
+ address,
302
505
  hash,
303
- witnessScript: child.witnessScript,
506
+ witnessScript: c.witnessScript,
304
507
  };
305
508
  }
306
509
  else {
307
510
  return {
308
511
  type: 'sh',
309
512
  redeemScript: cs,
310
- script: OutScript.encode({ type: 'sh', hash }),
311
- address: Address(network).encode({ type: 'sh', hash }),
513
+ script,
514
+ address,
312
515
  hash,
313
516
  };
314
517
  }
315
518
  };
519
+ /**
520
+ * Wraps a child script inside native SegWit P2WSH.
521
+ * @param child - child payment descriptor to wrap
522
+ * @param network - address network parameters
523
+ * @returns P2WSH descriptor.
524
+ * @throws If the wrapped script combination is invalid or unsupported. {@link Error}
525
+ * @example
526
+ * Wrap a child script in native SegWit P2WSH.
527
+ * ```ts
528
+ * import { hex } from '@scure/base';
529
+ * import { p2pk, p2wsh } from '@scure/btc-signer/payment.js';
530
+ * p2wsh(p2pk(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')));
531
+ * ```
532
+ */
316
533
  export const p2wsh = (child, network = NETWORK) => {
534
+ u.validateObject(child, {}, {}, 'child');
317
535
  const cs = child.script;
318
536
  if (!u.isBytes(cs))
319
537
  throw new Error(`Wrong script: ${typeof cs}, expected Uint8Array`);
538
+ // BIP141 P2WSH says the witness "must consist of ... a serialized script (witnessScript)"
539
+ // and that witnessScript is limited to 10,000 bytes, so larger wrapped scripts must reject.
540
+ if (cs.length > 10000)
541
+ throw new Error('P2WSH: witnessScript exceeds 10,000 bytes');
320
542
  const hash = u.sha256(cs);
321
543
  const script = OutScript.encode({ type: 'wsh', hash });
322
544
  checkScript(script, undefined, cs);
323
545
  return {
324
546
  type: 'wsh',
325
547
  witnessScript: cs,
326
- script: OutScript.encode({ type: 'wsh', hash }),
548
+ script,
327
549
  address: Address(network).encode({ type: 'wsh', hash }),
328
550
  hash,
329
551
  };
330
552
  };
553
+ /**
554
+ * Builds a native SegWit P2WPKH output from a public key.
555
+ * @param publicKey - compressed ECDSA public key
556
+ * @param network - address network parameters
557
+ * @returns P2WPKH descriptor.
558
+ * @throws If the public key cannot be encoded as a P2WPKH output. {@link Error}
559
+ * @example
560
+ * Build a native SegWit pay-to-public-key-hash output.
561
+ * ```ts
562
+ * import { hex } from '@scure/base';
563
+ * import { p2wpkh } from '@scure/btc-signer/payment.js';
564
+ * p2wpkh(hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'));
565
+ * ```
566
+ */
331
567
  export const p2wpkh = (publicKey, network = NETWORK) => {
332
568
  if (!isValidPubkey(publicKey, u.PubT.ecdsa))
333
569
  throw new Error('P2WPKH: invalid publicKey');
570
+ // BIP 143 default policy: version-0 witness programs MUST use 33-byte compressed ECDSA keys.
334
571
  if (publicKey.length === 65)
335
572
  throw new Error('P2WPKH: uncompressed public key');
336
573
  const hash = u.hash160(publicKey);
@@ -341,7 +578,24 @@ export const p2wpkh = (publicKey, network = NETWORK) => {
341
578
  hash,
342
579
  };
343
580
  };
581
+ /**
582
+ * Builds a bare multisig script.
583
+ * @param m - number of required signatures
584
+ * @param pubkeys - participating public keys
585
+ * @param allowSamePubkeys - whether duplicate keys are allowed
586
+ * @returns P2MS descriptor.
587
+ * @throws If the multisig parameters are invalid. {@link Error}
588
+ * @example
589
+ * Build a bare multisig output script.
590
+ * ```ts
591
+ * import { hex } from '@scure/base';
592
+ * import { p2ms } from '@scure/btc-signer/payment.js';
593
+ * p2ms(1, [hex.decode('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')], true);
594
+ * ```
595
+ */
344
596
  export const p2ms = (m, pubkeys, allowSamePubkeys = false) => {
597
+ // BIP 11 only standardized bare multisig up to 3 keys; this helper still permits up to 16
598
+ // because the same script shape is commonly wrapped by p2sh()/p2wsh() instead of used bare.
345
599
  if (!allowSamePubkeys)
346
600
  uniqPubkey(pubkeys);
347
601
  return {
@@ -356,9 +610,17 @@ function checkTaprootScript(script, internalPubKey, allowUnknownOutputs = false,
356
610
  // disable custom. All custom scripts for taproot should have prefix 'tr_'
357
611
  if (customScripts) {
358
612
  const cs = P.apply(Script, P.coders.match(customScripts));
359
- const c = cs.decode(script);
613
+ let c;
614
+ // match() throws when no custom coder matches; treat that as "not a custom
615
+ // script" so the allowUnknownOutputs escape below stays reachable.
616
+ try {
617
+ c = cs.decode(script);
618
+ }
619
+ catch (e) {
620
+ c = undefined;
621
+ }
360
622
  if (c !== undefined) {
361
- if (typeof c.type !== 'string' || !c.type.startsWith('tr_'))
623
+ if (!u.astring(c.type, 'c.type').startsWith('tr_'))
362
624
  throw new Error(`P2TR: invalid custom type=${c.type}`);
363
625
  return;
364
626
  }
@@ -387,7 +649,37 @@ function checkTaprootScript(script, internalPubKey, allowUnknownOutputs = false,
387
649
  }
388
650
  }
389
651
  // Helper for generating binary tree from list, with weights
652
+ /**
653
+ * Converts a flat list of weighted leaves into a binary taproot tree.
654
+ * @param taprootList - weighted leaves to arrange
655
+ * @returns Binary taproot script tree.
656
+ * @throws If the list is empty and cannot describe any tree. {@link Error}
657
+ * @example
658
+ * Start from a flat weighted list, then let the helper build the binary tree shape.
659
+ * ```ts
660
+ * import { hex } from '@scure/base';
661
+ * import { p2tr_pk, taprootListToTree } from '@scure/btc-signer/payment.js';
662
+ * taprootListToTree([
663
+ * p2tr_pk(hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9')),
664
+ * p2tr_pk(hex.decode('dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659')),
665
+ * ]);
666
+ * ```
667
+ */
390
668
  export function taprootListToTree(taprootList) {
669
+ u.aarray(taprootList, 'taprootList', (leaf, title) => {
670
+ // p2tr reduces non-binary trees through this helper, so nested branch arrays are valid here.
671
+ if (Array.isArray(leaf))
672
+ return;
673
+ u.validateObject(leaf, {}, {}, title);
674
+ // This helper only arranges weighted tree nodes; p2tr validates leaf scripts while hashing.
675
+ if (leaf.weight !== undefined)
676
+ anumber(leaf.weight, title + '.weight');
677
+ });
678
+ // Empty flat lists cannot represent a taproot script tree; omit the tree entirely for
679
+ // key-path-only outputs instead of passing [] here, otherwise this helper would return
680
+ // undefined and downstream taproot tree walkers would fail much later on a non-tree value.
681
+ if (!taprootList.length)
682
+ throw new Error('taprootListToTree: empty tree');
391
683
  // Clone input in order to not corrupt it
392
684
  const lst = Array.from(taprootList);
393
685
  // We have at least 2 elements => can create branch
@@ -418,7 +710,8 @@ function taprootAddPath(tree, path = []) {
418
710
  return {
419
711
  ...tree,
420
712
  path,
421
- // Left element has right hash in path and otherwise
713
+ // BIP 341 control blocks serialize sibling hashes from leaf to root, so prepend the
714
+ // current sibling before descending into the child subtree.
422
715
  left: taprootAddPath(tree.left, [tree.right.hash, ...path]),
423
716
  right: taprootAddPath(tree.right, [tree.left.hash, ...path]),
424
717
  };
@@ -430,28 +723,33 @@ function taprootWalkTree(tree) {
430
723
  return [tree];
431
724
  if (tree.type !== 'branch')
432
725
  throw new Error(`taprootWalkTree: wrong type=${tree}`);
726
+ // Keep a stable left-to-right DFS leaf order when flattening the annotated tree.
433
727
  return [...taprootWalkTree(tree.left), ...taprootWalkTree(tree.right)];
434
728
  }
435
729
  function taprootHashTree(tree, internalPubKey, allowUnknownOutputs = false, customScripts) {
436
- if (!tree)
730
+ if (tree === undefined)
437
731
  throw new Error('taprootHashTree: empty tree');
732
+ if (!Array.isArray(tree) && !P.utils.isPlainObject(tree))
733
+ throw new TypeError('"tree" expected object or array, got type=' + typeof tree);
438
734
  if (Array.isArray(tree) && tree.length === 1)
439
735
  tree = tree[0];
440
736
  // Terminal node (leaf)
441
737
  if (!Array.isArray(tree)) {
442
- const { leafVersion: version, script: leafScript } = tree;
738
+ u.validateObject(tree, {}, {}, 'tree');
739
+ const version = tree.leafVersion;
740
+ const { script: leafScript } = tree;
443
741
  // Earliest tree walk where we can validate tapScripts
444
742
  if (tree.tapLeafScript || (tree.tapMerkleRoot && !u.equalBytes(tree.tapMerkleRoot, P.EMPTY)))
445
743
  throw new Error('P2TR: tapRoot leafScript cannot have tree');
446
- const script = typeof leafScript === 'string' ? hex.decode(leafScript) : leafScript;
447
- if (!u.isBytes(script))
448
- throw new Error(`checkScript: wrong script type=${script}`);
744
+ const script = typeof leafScript === 'string'
745
+ ? hex.decode(leafScript)
746
+ : abytes(leafScript, undefined, 'tree.script');
449
747
  checkTaprootScript(script, internalPubKey, allowUnknownOutputs, customScripts);
450
748
  return {
451
749
  type: 'leaf',
452
750
  version,
453
751
  script,
454
- hash: tapLeafHash(script, version),
752
+ hash: tapLeafHash(script, tapLeafVersion(version)),
455
753
  };
456
754
  }
457
755
  // If tree / branch is not binary tree, convert it
@@ -463,14 +761,44 @@ function taprootHashTree(tree, internalPubKey, allowUnknownOutputs = false, cust
463
761
  // Both nodes should exist
464
762
  const left = taprootHashTree(tree[0], internalPubKey, allowUnknownOutputs, customScripts);
465
763
  const right = taprootHashTree(tree[1], internalPubKey, allowUnknownOutputs, customScripts);
466
- // We cannot swap left/right here, since it will change structure of tree
764
+ // BIP 341 sorts TapBranch child hashes lexicographically for hashing, but the original
765
+ // left/right structure still determines the control-block sibling paths for each leaf.
467
766
  let [lH, rH] = [left.hash, right.hash];
468
767
  if (u.compareBytes(rH, lH) === -1)
469
768
  [lH, rH] = [rH, lH];
470
- return { type: 'branch', left, right, hash: u.tagSchnorr('TapBranch', lH, rH) };
769
+ return {
770
+ type: 'branch',
771
+ left,
772
+ right,
773
+ hash: u.tagSchnorr('TapBranch', lH, rH),
774
+ };
471
775
  }
776
+ /** Default tapleaf version used by taproot script-path outputs before adding the parity bit. */
472
777
  export const TAP_LEAF_VERSION = 0xc0;
473
- export const tapLeafHash = (script, version = TAP_LEAF_VERSION) => u.tagSchnorr('TapLeaf', new Uint8Array([version]), VarBytes.encode(script));
778
+ const tapLeafVersion = (version) => {
779
+ if (version === undefined)
780
+ return TAP_LEAF_VERSION;
781
+ anumber(version, 'leafVersion');
782
+ // BIP341 script-path validation defines the effective leaf version as `v = c[0] & 0xfe`
783
+ // and says it cannot be odd or `0x50`; tapleaf hashes also serialize this as one byte.
784
+ if (version > 0xfe || version === 0x50 || !!(version & 1))
785
+ throw new Error(`P2TR: invalid leafVersion=${version}`);
786
+ return version;
787
+ };
788
+ /**
789
+ * Computes the tagged hash of a tapleaf script.
790
+ * @param script - tapleaf script bytes
791
+ * @param version - base even tapleaf version byte (for tapscript, `0xc0`)
792
+ * @returns Tapleaf hash.
793
+ * @throws If the tapleaf version is not a valid even one-byte version.
794
+ * {@link Error}
795
+ * @example
796
+ * Hash a finalized tapscript leaf before placing it into a Merkle tree.
797
+ * ```ts
798
+ * tapLeafHash(new Uint8Array([0x51]));
799
+ * ```
800
+ */
801
+ export const tapLeafHash = (script, version = TAP_LEAF_VERSION) => u.tagSchnorr('TapLeaf', new Uint8Array([tapLeafVersion(version)]), VarBytes.encode(script));
474
802
  export function p2tr(internalPubKey, tree, network = NETWORK, allowUnknownOutputs = false, customScripts) {
475
803
  // Unspendable
476
804
  if (!internalPubKey && !tree)
@@ -484,14 +812,21 @@ export function p2tr(internalPubKey, tree, network = NETWORK, allowUnknownOutput
484
812
  let hashedTree = taprootAddPath(taprootHashTree(tree, pubKey, allowUnknownOutputs, customScripts));
485
813
  const tapMerkleRoot = hashedTree.hash;
486
814
  const [tweakedPubkey, parity] = u.taprootTweakPubkey(pubKey, tapMerkleRoot);
487
- const leaves = taprootWalkTree(hashedTree).map((l) => ({
488
- ...l,
489
- controlBlock: TaprootControlBlock.encode({
490
- version: (l.version || TAP_LEAF_VERSION) + parity,
815
+ const tapLeafScript = [];
816
+ const leaves = taprootWalkTree(hashedTree).map((l) => {
817
+ const version = tapLeafVersion(l.version);
818
+ // Leaf versions are stored as the base even byte; only the control block adds the
819
+ // output-key parity bit required by BIP 341 script-path spending.
820
+ const controlBlock = {
821
+ version: version + parity,
491
822
  internalKey: pubKey,
492
823
  merklePath: l.path,
493
- }),
494
- }));
824
+ };
825
+ // Skip an encode/decode copy for performance; callers must treat returned metadata as
826
+ // immutable.
827
+ tapLeafScript.push([controlBlock, u.concatBytes(l.script, new Uint8Array([version]))]);
828
+ return { ...l, controlBlock: TaprootControlBlock.encode(controlBlock) };
829
+ });
495
830
  return {
496
831
  type: 'tr',
497
832
  script: OutScript.encode({ type: 'tr', pubkey: tweakedPubkey }),
@@ -501,14 +836,13 @@ export function p2tr(internalPubKey, tree, network = NETWORK, allowUnknownOutput
501
836
  // PSBT stuff
502
837
  tapInternalKey: pubKey,
503
838
  leaves,
504
- tapLeafScript: leaves.map((l) => [
505
- TaprootControlBlock.decode(l.controlBlock),
506
- u.concatBytes(l.script, new Uint8Array([l.version || TAP_LEAF_VERSION])),
507
- ]),
839
+ tapLeafScript,
508
840
  tapMerkleRoot,
509
841
  };
510
842
  }
511
843
  else {
844
+ // BIP 341 / BIP 86: key-only Taproot still tweaks with the empty Merkle root so the
845
+ // output commits to an unspendable script path instead of leaving the key untweaked.
512
846
  const tweakedPubkey = u.taprootTweakPubkey(pubKey, P.EMPTY)[0];
513
847
  return {
514
848
  type: 'tr',
@@ -522,18 +856,33 @@ export function p2tr(internalPubKey, tree, network = NETWORK, allowUnknownOutput
522
856
  }
523
857
  }
524
858
  // Returns all combinations of size M from lst
859
+ /**
860
+ * Returns all size-`m` combinations from a list.
861
+ * @param m - size of each combination
862
+ * @param list - input items to combine
863
+ * @returns Array of combinations.
864
+ * @throws If the combination size or input list is invalid. {@link Error}
865
+ * @example
866
+ * Enumerate all size-two subsets of a short list.
867
+ * ```ts
868
+ * combinations(2, [1, 2, 3]);
869
+ * ```
870
+ */
525
871
  export function combinations(m, list) {
526
872
  const res = [];
527
873
  if (!Array.isArray(list))
528
874
  throw new Error('combinations: lst arg should be array');
529
875
  const n = list.length;
530
- if (m > n)
531
- throw new Error('combinations: m > lst.length, no combinations possible');
876
+ anumber(m, 'm');
877
+ if (m < 1 || m > n)
878
+ throw new Error('combinations: m must satisfy 1 <= m <= lst.length');
532
879
  /*
533
880
  Basically works as M nested loops like:
534
881
  for (;idx[0]<lst.length;idx[0]++) for (idx[1]=idx[0]+1;idx[1]<lst.length;idx[1]++)
535
882
  but since we cannot create nested loops dynamically, we unroll it to a single loop
536
883
  */
884
+ // This unrolled-loop implementation assumes an integer 1 <= m <= n; zero, negative, and
885
+ // fractional m values need an explicit guard before entering the loop.
537
886
  const idx = Array.from({ length: m }, (_, i) => i);
538
887
  const last = idx.length - 1;
539
888
  main: for (;;) {
@@ -555,6 +904,21 @@ export function combinations(m, list) {
555
904
  }
556
905
  return res;
557
906
  }
907
+ /**
908
+ * Builds the leaf set for an M-of-N `CHECKSIGVERIFY` taproot policy.
909
+ * @param m - number of required signatures
910
+ * @param pubkeys - participating Schnorr public keys
911
+ * @param allowSamePubkeys - whether duplicate keys are allowed
912
+ * @returns Array of taproot leaf descriptors.
913
+ * @throws If the taproot multisig parameters are invalid. {@link Error}
914
+ * @example
915
+ * Build the leaf set for an M-of-N taproot `CHECKSIGVERIFY` policy.
916
+ * ```ts
917
+ * import { hex } from '@scure/base';
918
+ * import { p2tr_ns } from '@scure/btc-signer/payment.js';
919
+ * p2tr_ns(1, [hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9')], true);
920
+ * ```
921
+ */
558
922
  export const p2tr_ns = (m, pubkeys, allowSamePubkeys = false) => {
559
923
  if (!allowSamePubkeys)
560
924
  uniqPubkey(pubkeys);
@@ -563,7 +927,37 @@ export const p2tr_ns = (m, pubkeys, allowSamePubkeys = false) => {
563
927
  script: OutScript.encode({ type: 'tr_ns', pubkeys: i }),
564
928
  }));
565
929
  };
930
+ /**
931
+ * Builds a single-key taproot leaf script.
932
+ * BIP 341 design guidance: if this is the most likely single-key spend path, prefer
933
+ * using that key as the `p2tr()` internal key instead of forcing it into a script leaf.
934
+ * @param pubkey - Schnorr public key
935
+ * @returns Taproot single-key leaf descriptor.
936
+ * @throws If the taproot single-key leaf cannot be encoded. {@link Error}
937
+ * @example
938
+ * Build a single-key tapscript leaf.
939
+ * ```ts
940
+ * import { hex } from '@scure/base';
941
+ * import { p2tr_pk } from '@scure/btc-signer/payment.js';
942
+ * p2tr_pk(hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9'));
943
+ * ```
944
+ */
566
945
  export const p2tr_pk = (pubkey) => p2tr_ns(1, [pubkey], undefined)[0];
946
+ /**
947
+ * Builds a `CHECKSIGADD` taproot multisig leaf.
948
+ * @param m - number of required signatures
949
+ * @param pubkeys - participating Schnorr public keys
950
+ * @param allowSamePubkeys - whether duplicate keys are allowed
951
+ * @returns Taproot multisig leaf descriptor.
952
+ * @throws If the taproot multisig parameters are invalid. {@link Error}
953
+ * @example
954
+ * Build a `CHECKSIGADD` taproot multisig leaf.
955
+ * ```ts
956
+ * import { hex } from '@scure/base';
957
+ * import { p2tr_ms } from '@scure/btc-signer/payment.js';
958
+ * p2tr_ms(1, [hex.decode('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9')], true);
959
+ * ```
960
+ */
567
961
  export function p2tr_ms(m, pubkeys, allowSamePubkeys = false) {
568
962
  if (!allowSamePubkeys)
569
963
  uniqPubkey(pubkeys);
@@ -573,10 +967,28 @@ export function p2tr_ms(m, pubkeys, allowSamePubkeys = false) {
573
967
  };
574
968
  }
575
969
  // Simple pubkey address, without complex scripts
970
+ /**
971
+ * Derives a simple address from a private key.
972
+ * @param type - address type to derive
973
+ * @param privKey - private key bytes
974
+ * @param network - address network parameters
975
+ * @returns Encoded Bitcoin address.
976
+ * @throws If the requested address type is unknown. {@link Error}
977
+ * @example
978
+ * Pick the output type first, then derive the matching address from the private key.
979
+ * ```ts
980
+ * import { getAddress } from '@scure/btc-signer/payment.js';
981
+ * import { randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
982
+ * getAddress('wpkh', randomPrivateKeyBytes());
983
+ * ```
984
+ */
576
985
  export function getAddress(type, privKey, network = NETWORK) {
986
+ u.astring(type, 'type');
577
987
  if (type === 'tr') {
578
988
  return p2tr(u.pubSchnorr(privKey), undefined, network).address;
579
989
  }
990
+ // This convenience wrapper always uses the compressed ECDSA public key; derive
991
+ // `pubECDSA(privKey, false)` and call `p2pkh(...)` directly for legacy uncompressed P2PKH.
580
992
  const pubKey = u.pubECDSA(privKey);
581
993
  if (type === 'pkh')
582
994
  return p2pkh(pubKey, network).address;
@@ -584,15 +996,65 @@ export function getAddress(type, privKey, network = NETWORK) {
584
996
  return p2wpkh(pubKey, network).address;
585
997
  throw new Error(`getAddress: unknown type=${type}`);
586
998
  }
999
+ // BIP67 defines canonical multisig ordering only for compressed pubkeys; this helper still sorts
1000
+ // raw bytes generically, and higher-level callers may accept uncompressed participants for compat.
587
1001
  export const _sortPubkeys = (pubkeys) => Array.from(pubkeys).sort(u.compareBytes);
1002
+ /**
1003
+ * Builds a classic M-of-N multisig output, wrapped in P2SH or P2WSH.
1004
+ * @param m - number of required signatures
1005
+ * @param pubkeys - participating public keys
1006
+ * @param sorted - whether to sort the public keys first
1007
+ * @param witness - whether to wrap the result as native SegWit
1008
+ * @param network - address network parameters
1009
+ * @returns Multisig payment descriptor.
1010
+ * @throws If the multisig parameters or wrapped script are invalid. {@link Error}
1011
+ * @example
1012
+ * Wrap a classic 2-of-2 script into an addressable multisig output.
1013
+ * ```ts
1014
+ * import { multisig } from '@scure/btc-signer/payment.js';
1015
+ * import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
1016
+ * multisig(
1017
+ * 2,
1018
+ * [pubECDSA(randomPrivateKeyBytes()), pubECDSA(randomPrivateKeyBytes())],
1019
+ * true,
1020
+ * true
1021
+ * );
1022
+ * ```
1023
+ */
588
1024
  export function multisig(m, pubkeys, sorted = false, witness = false, network = NETWORK) {
1025
+ // BIP 143 default policy: version-0 witness programs should use compressed ECDSA pubkeys only;
1026
+ // witness multisig callers must avoid uncompressed keys because p2ms accepts generic ECDSA encodings.
1027
+ // BIP 16 caps spendable compressed-key P2SH multisig at 15 pubkeys because larger redeem scripts
1028
+ // exceed the 520-byte push limit; use witness=true for larger classic multisig sets.
589
1029
  const ms = p2ms(m, sorted ? _sortPubkeys(pubkeys) : pubkeys);
590
- return witness ? p2wsh(ms, network) : p2sh(ms, network);
1030
+ return (witness ? p2wsh(ms, network) : p2sh(ms, network));
591
1031
  }
1032
+ /**
1033
+ * Builds a multisig output after lexicographically sorting the keys.
1034
+ * @param m - number of required signatures
1035
+ * @param pubkeys - participating public keys
1036
+ * @param witness - whether to wrap the result as native SegWit
1037
+ * @param network - address network parameters
1038
+ * @returns Sorted multisig payment descriptor.
1039
+ * @throws If the multisig parameters or wrapped script are invalid. {@link Error}
1040
+ * @example
1041
+ * Sort public keys deterministically before constructing the multisig address.
1042
+ * ```ts
1043
+ * import { sortedMultisig } from '@scure/btc-signer/payment.js';
1044
+ * import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
1045
+ * sortedMultisig(
1046
+ * 2,
1047
+ * [pubECDSA(randomPrivateKeyBytes()), pubECDSA(randomPrivateKeyBytes())],
1048
+ * true
1049
+ * );
1050
+ * ```
1051
+ */
592
1052
  export function sortedMultisig(m, pubkeys, witness = false, network = NETWORK) {
1053
+ // BIP67 canonical multisig is compressed-only, but this wrapper intentionally keeps the generic
1054
+ // sorted-multisig behavior and still allows uncompressed participant keys for compatibility.
593
1055
  return multisig(m, pubkeys, true, witness, network);
594
1056
  }
595
- const base58check = createBase58check(u.sha256);
1057
+ const base58check = /* @__PURE__ */ createBase58check(u.sha256);
596
1058
  function validateWitness(version, data) {
597
1059
  if (data.length < 2 || data.length > 40)
598
1060
  throw new Error('Witness: invalid length');
@@ -603,15 +1065,31 @@ function validateWitness(version, data) {
603
1065
  }
604
1066
  function programToWitness(version, data, network = NETWORK) {
605
1067
  validateWitness(version, data);
1068
+ // BIP 350 keeps segwit v0 on Bech32, while witness versions 1+ switch to Bech32m.
606
1069
  const coder = version === 0 ? bech32 : bech32m;
607
1070
  return coder.encode(network.bech32, [version].concat(coder.toWords(data)));
608
1071
  }
609
1072
  function formatKey(hashed, prefix) {
1073
+ // Legacy Base58Check paths all serialize [version-byte || payload] before checksumming.
610
1074
  return base58check.encode(u.concatBytes(Uint8Array.from(prefix), hashed));
611
1075
  }
1076
+ /**
1077
+ * Wallet-import-format coder for private keys.
1078
+ * @param network - address network parameters
1079
+ * @returns WIF coder.
1080
+ * @example
1081
+ * Encode or decode wallet-import-format private keys.
1082
+ * ```ts
1083
+ * const coder = WIF();
1084
+ * coder.encode(new Uint8Array(32).fill(1));
1085
+ * ```
1086
+ */
612
1087
  export function WIF(network = NETWORK) {
613
1088
  return {
614
1089
  encode(privKey) {
1090
+ // Compressed WIF is exactly 32 private-key bytes plus the 0x01 suffix; shorter or longer
1091
+ // inputs must be rejected instead of being silently padded or truncated by subarray().
1092
+ abytes(privKey, 32, 'privKey');
615
1093
  const compressed = u.concatBytes(privKey, new Uint8Array([0x01]));
616
1094
  return formatKey(compressed.subarray(0, 33), [network.wif]);
617
1095
  },
@@ -630,16 +1108,35 @@ export function WIF(network = NETWORK) {
630
1108
  };
631
1109
  }
632
1110
  // Returns OutType, which can be used to create outscript
1111
+ /**
1112
+ * Address encoder/decoder for a specific Bitcoin network.
1113
+ * @param network - address network parameters
1114
+ * @returns Address coder backed by the provided network.
1115
+ * @example
1116
+ * Create a network-specific address coder and encode a payment descriptor.
1117
+ * ```ts
1118
+ * import { Address, p2wpkh } from '@scure/btc-signer/payment.js';
1119
+ * import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
1120
+ * const coder = Address();
1121
+ * coder.encode(p2wpkh(pubECDSA(randomPrivateKeyBytes())));
1122
+ * ```
1123
+ */
633
1124
  export function Address(network = NETWORK) {
1125
+ u.validateObject(network, {}, {}, 'network');
634
1126
  return {
635
1127
  encode(from) {
1128
+ u.validateObject(from, {}, {}, 'from');
636
1129
  const { type } = from;
1130
+ u.astring(type, 'from.type');
637
1131
  if (type === 'wpkh')
638
1132
  return programToWitness(0, from.hash, network);
639
1133
  else if (type === 'wsh')
640
1134
  return programToWitness(0, from.hash, network);
641
1135
  else if (type === 'tr')
642
1136
  return programToWitness(1, from.pubkey, network);
1137
+ // BIP433 P2A is the fixed v1 witness program 0x4e73 ('bc1pfeessrawgf').
1138
+ else if (type === 'p2a')
1139
+ return programToWitness(1, P2A_PROGRAM, network);
643
1140
  else if (type === 'pkh')
644
1141
  return formatKey(from.hash, [network.pubKeyHash]);
645
1142
  else if (type === 'sh')
@@ -647,6 +1144,7 @@ export function Address(network = NETWORK) {
647
1144
  throw new Error(`Unknown address type=${type}`);
648
1145
  },
649
1146
  decode(address) {
1147
+ u.astring(address, 'address');
650
1148
  if (address.length < 14 || address.length > 74)
651
1149
  throw new Error('Invalid address length');
652
1150
  // Bech32
@@ -674,6 +1172,10 @@ export function Address(network = NETWORK) {
674
1172
  return { type: 'wpkh', hash: data };
675
1173
  else if (version === 1 && data.length === 32)
676
1174
  return { type: 'tr', pubkey: data };
1175
+ else if (version === 1 && u.equalBytes(data, P2A_PROGRAM))
1176
+ return { type: 'p2a', script: Script.encode([1, data]) };
1177
+ // Future witness versions can still be valid addresses, but this helper
1178
+ // only returns typed descriptors for recognized v0, taproot and P2A templates.
677
1179
  else
678
1180
  throw new Error('Unknown witness program');
679
1181
  }
@@ -694,4 +1196,3 @@ export function Address(network = NETWORK) {
694
1196
  },
695
1197
  };
696
1198
  }
697
- //# sourceMappingURL=payment.js.map