@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/script.ts CHANGED
@@ -1,10 +1,39 @@
1
1
  import * as P from 'micro-packed';
2
- import { isBytes, reverseObject, type ValueOf, type Bytes } from './utils.ts';
2
+ import {
3
+ aarray,
4
+ abytes,
5
+ isBytes,
6
+ reverseObject,
7
+ type Bytes,
8
+ type TArg,
9
+ type TRet,
10
+ type ValueOf,
11
+ } from './utils.ts';
3
12
 
13
+ /**
14
+ * Maximum byte size allowed for a single pushed script element.
15
+ * BIP 342 keeps this 520-byte stack-element limit even though tapscript removes
16
+ * the old 10,000-byte overall script-size cap.
17
+ */
4
18
  export const MAX_SCRIPT_BYTE_LENGTH = 520;
5
19
 
20
+ // Be friendly to bad ECMAScript parsers by not using bigint literals.
6
21
  // prettier-ignore
7
- export const OP = {
22
+ const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1), _2n = /* @__PURE__ */ BigInt(2), _8n = /* @__PURE__ */ BigInt(8);
23
+ const U8_MAX = /* @__PURE__ */ BigInt(0xff);
24
+ const COMPACT_DIRECT_MAX = /* @__PURE__ */ BigInt(0xfc);
25
+
26
+ // prettier-ignore
27
+ /**
28
+ * Bitcoin Script opcode table.
29
+ * @example
30
+ * Use opcode numbers when you need the raw byte form instead of Script mnemonics.
31
+ * ```ts
32
+ * import { OP } from '@scure/btc-signer/script.js';
33
+ * new Uint8Array([OP.OP_1, OP.OP_2, OP.CHECKMULTISIG]);
34
+ * ```
35
+ */
36
+ export const OP = /* @__PURE__ */ Object.freeze({
8
37
  OP_0: 0, PUSHDATA1: 76, PUSHDATA2: 77, PUSHDATA4: 78, '1NEGATE': 79,
9
38
  RESERVED: 80,
10
39
  OP_1: 81, OP_2: 82, OP_3: 83, OP_4: 84, OP_5: 85, OP_6: 86, OP_7: 87, OP_8: 88, OP_9: 89,
@@ -33,24 +62,51 @@ export const OP = {
33
62
  CHECKSIGADD: 186,
34
63
  // Invalid
35
64
  INVALID: 255,
36
- };
65
+ });
37
66
 
38
- export const OPNames = reverseObject(OP);
67
+ /**
68
+ * Reverse lookup map from opcode numbers back to names.
69
+ * @example
70
+ * Turn parsed opcode numbers back into their mnemonic names.
71
+ * ```ts
72
+ * import { OP, OPNames } from '@scure/btc-signer/script.js';
73
+ * OPNames[OP.CHECKSIG];
74
+ * ```
75
+ */
76
+ export const OPNames: Record<number, keyof typeof OP> = /* @__PURE__ */ (() =>
77
+ Object.freeze(reverseObject(OP) as Record<number, keyof typeof OP>))();
78
+ /** Numeric opcode value from {@link OP}. */
39
79
  export type OP = ValueOf<typeof OP>;
40
80
 
81
+ /** Single script element accepted by the script encoder. */
41
82
  export type ScriptOP = keyof typeof OP | Uint8Array | number;
83
+ /** Parsed Bitcoin script as a list of script elements. */
42
84
  export type ScriptType = ScriptOP[];
43
85
 
44
86
  // We can encode almost any number as ScriptNum, however, parsing will be a problem
45
87
  // since we can't know if buffer is a number or something else.
88
+ /**
89
+ * Coder for Bitcoin Script numbers.
90
+ * bytesLimit only constrains decode. encode still serializes any bigint in
91
+ * Script's signed-magnitude byte form so higher-level consumers can enforce
92
+ * opcode-specific 4-byte or 5-byte bounds separately.
93
+ * @param bytesLimit - maximum decoded length in bytes
94
+ * @param forceMinimal - whether to reject non-minimal encodings
95
+ * @returns Script number coder.
96
+ * @example
97
+ * Encode a small integer using Script number rules.
98
+ * ```ts
99
+ * ScriptNum().encode(1n);
100
+ * ```
101
+ */
46
102
  export function ScriptNum(bytesLimit = 6, forceMinimal = false): P.CoderType<bigint> {
47
103
  return P.wrap({
48
104
  encodeStream: (w: P.Writer, value: bigint) => {
49
- if (value === 0n) return;
105
+ if (value === _0n) return;
50
106
  const neg = value < 0;
51
107
  const val = BigInt(value);
52
108
  const nums = [];
53
- for (let abs = neg ? -val : val; abs; abs >>= 8n) nums.push(Number(abs & 0xffn));
109
+ for (let abs = neg ? -val : val; abs; abs >>= _8n) nums.push(Number(abs & U8_MAX));
54
110
  if (nums[nums.length - 1] >= 0x80) nums.push(neg ? 0x80 : 0);
55
111
  else if (neg) nums[nums.length - 1] |= 0x80;
56
112
  w.bytes(new Uint8Array(nums));
@@ -59,24 +115,22 @@ export function ScriptNum(bytesLimit = 6, forceMinimal = false): P.CoderType<big
59
115
  const len = r.leftBytes;
60
116
  if (len > bytesLimit)
61
117
  throw new Error(`ScriptNum: number (${len}) bigger than limit=${bytesLimit}`);
62
- if (len === 0) return 0n;
118
+ if (len === 0) return _0n;
119
+ // Read the payload once instead of peeking for the minimality check and
120
+ // then re-reading it byte-by-byte through the Reader.
121
+ const data = r.bytes(len);
63
122
  if (forceMinimal) {
64
- const data = r.bytes(len, true);
65
123
  // MSB is zero (without sign bit) -> not minimally encoded
66
- if ((data[data.length - 1] & 0x7f) === 0) {
124
+ if ((data[len - 1] & 0x7f) === 0) {
67
125
  // exception
68
- if (len <= 1 || (data[data.length - 2] & 0x80) === 0)
126
+ if (len <= 1 || (data[len - 2] & 0x80) === 0)
69
127
  throw new Error('Non-minimally encoded ScriptNum');
70
128
  }
71
129
  }
72
- let last = 0;
73
- let res = 0n;
74
- for (let i = 0; i < len; ++i) {
75
- last = r.byte();
76
- res |= BigInt(last) << (8n * BigInt(i));
77
- }
78
- if (last >= 0x80) {
79
- res &= (2n ** BigInt(len * 8) - 1n) >> 1n;
130
+ let res = _0n;
131
+ for (let i = 0; i < len; ++i) res |= BigInt(data[i]) << (_8n * BigInt(i));
132
+ if (data[len - 1] >= 0x80) {
133
+ res &= (_2n ** BigInt(len * 8) - _1n) >> _1n;
80
134
  res = -res;
81
135
  }
82
136
  return res;
@@ -84,12 +138,32 @@ export function ScriptNum(bytesLimit = 6, forceMinimal = false): P.CoderType<big
84
138
  });
85
139
  }
86
140
 
87
- export function OpToNum(op: ScriptOP, bytesLimit = 4, forceMinimal = true): number | undefined {
141
+ /**
142
+ * Attempts to decode a numeric script element into a JavaScript number.
143
+ * Accepts decoded small integers already represented as JS numbers and pushed
144
+ * `ScriptNum` byte payloads, but does not interpret opcode mnemonics like `1NEGATE`.
145
+ * @param op - script element to decode
146
+ * @param bytesLimit - maximum encoded length in bytes
147
+ * @param forceMinimal - whether to enforce minimal `ScriptNum` encoding
148
+ * @returns Decoded number, or `undefined` when the element is not a JS number or valid `ScriptNum` bytes.
149
+ * @example
150
+ * Decode a script element back into a JavaScript number when possible.
151
+ * ```ts
152
+ * OpToNum(1);
153
+ * ```
154
+ */
155
+ export function OpToNum(
156
+ op: TArg<ScriptOP>,
157
+ bytesLimit = 4,
158
+ forceMinimal = true
159
+ ): number | undefined {
88
160
  if (typeof op === 'number') return op;
89
161
  if (isBytes(op)) {
90
162
  try {
91
163
  const val = ScriptNum(bytesLimit, forceMinimal).decode(op);
92
- if (val > Number.MAX_SAFE_INTEGER) return;
164
+ // Symmetric safe-integer bound: large negative values would otherwise
165
+ // coerce through Number() with silent precision loss.
166
+ if (val > Number.MAX_SAFE_INTEGER || val < -Number.MAX_SAFE_INTEGER) return;
93
167
  return Number(val);
94
168
  } catch (e) {
95
169
  return;
@@ -98,6 +172,28 @@ export function OpToNum(op: ScriptOP, bytesLimit = 4, forceMinimal = true): numb
98
172
  return;
99
173
  }
100
174
 
175
+ // Shared raw pushdata-length parser. Keep this aligned with Script.decodeStream
176
+ // so byte-preserving walkers can skip pushed data without semantic decode/re-encode.
177
+ /**
178
+ * Returns the pushed-data length for a push opcode.
179
+ * @param op - opcode byte already read from the script stream
180
+ * @param read - callback that reads the following 1/2/4-byte little-endian length
181
+ * @returns Push length for data-carrying opcodes, or `undefined` for non-push opcodes.
182
+ * @throws If the opcode falls through the recognized push-opcode set unexpectedly.
183
+ * {@link Error}
184
+ */
185
+ export const scriptPushLen = (
186
+ op: number,
187
+ read: (bytes: 1 | 2 | 4) => number
188
+ ): number | undefined => {
189
+ if (!(OP.OP_0 < op && op <= OP.PUSHDATA4)) return;
190
+ if (op < OP.PUSHDATA1) return op;
191
+ if (op === OP.PUSHDATA1) return read(1);
192
+ if (op === OP.PUSHDATA2) return read(2);
193
+ if (op === OP.PUSHDATA4) return read(4);
194
+ throw new Error('Should be not possible');
195
+ };
196
+
101
197
  // Converts script bytes to parsed script
102
198
  // 5221030000000000000000000000000000000000000000000000000000000000000001210300000000000000000000000000000000000000000000000000000000000000022103000000000000000000000000000000000000000000000000000000000000000353ae
103
199
  // =>
@@ -107,142 +203,304 @@ export function OpToNum(op: ScriptOP, bytesLimit = 4, forceMinimal = true): numb
107
203
  // 030000000000000000000000000000000000000000000000000000000000000003
108
204
  // OP_3
109
205
  // CHECKMULTISIG
110
- export const Script: P.CoderType<ScriptType> = P.wrap({
111
- encodeStream: (w: P.Writer, value: ScriptType) => {
112
- for (let o of value) {
113
- if (typeof o === 'string') {
114
- if (OP[o] === undefined) throw new Error(`Unknown opcode=${o}`);
115
- w.byte(OP[o]);
116
- continue;
117
- } else if (typeof o === 'number') {
118
- if (o === 0x00) {
119
- w.byte(0x00);
120
- continue;
121
- } else if (1 <= o && o <= 16) {
122
- w.byte(OP.OP_1 - 1 + o);
123
- continue;
206
+ // This is a semantic AST codec, not a byte-preserving parser: encode
207
+ // canonicalizes small integers to OP_n and chooses the shortest push opcode,
208
+ // so decode(...)+encode(...) rewrites non-minimal push spellings.
209
+ /**
210
+ * Bitcoin script coder.
211
+ * @example
212
+ * Encode a short script from opcode mnemonics and small integers.
213
+ * ```ts
214
+ * Script.encode(['OP_1', 'OP_2']);
215
+ * ```
216
+ */
217
+ export const Script: TRet<P.CoderType<ScriptType>> = /* @__PURE__ */ (() =>
218
+ Object.freeze(
219
+ P.wrap({
220
+ encodeStream: (w: P.Writer, value: TArg<ScriptType>) => {
221
+ aarray(value, 'value');
222
+ for (let o of value) {
223
+ if (typeof o === 'string') {
224
+ const op = OP[o];
225
+ // OP is a plain object, so inherited Object.prototype keys ('toString',
226
+ // 'constructor', ...) are not opcodes and must be rejected here too.
227
+ if (typeof op !== 'number') throw new Error(`Unknown opcode=${o}`);
228
+ w.byte(op);
229
+ continue;
230
+ } else if (typeof o === 'number') {
231
+ if (o === 0x00) {
232
+ w.byte(0x00);
233
+ continue;
234
+ } else if (o === -1) {
235
+ // BIP62 minimal push / number rules spell numeric -1 as OP_1NEGATE
236
+ // instead of a pushed ScriptNum payload 0x81.
237
+ w.byte(OP['1NEGATE']);
238
+ continue;
239
+ } else if (1 <= o && o <= 16) {
240
+ w.byte(OP.OP_1 - 1 + o);
241
+ continue;
242
+ }
243
+ }
244
+ // Encode big numbers
245
+ if (typeof o === 'number') o = ScriptNum().encode(BigInt(o));
246
+ abytes(o, undefined, 'value');
247
+ // Bytes
248
+ const len = o.length;
249
+ if (len < OP.PUSHDATA1) w.byte(len);
250
+ else if (len <= 0xff) {
251
+ w.byte(OP.PUSHDATA1);
252
+ w.byte(len);
253
+ } else if (len <= 0xffff) {
254
+ w.byte(OP.PUSHDATA2);
255
+ w.bytes(P.U16LE.encode(len));
256
+ } else {
257
+ w.byte(OP.PUSHDATA4);
258
+ w.bytes(P.U32LE.encode(len));
259
+ }
260
+ w.bytes(o);
124
261
  }
125
- }
126
- // Encode big numbers
127
- if (typeof o === 'number') o = ScriptNum().encode(BigInt(o));
128
- if (!isBytes(o)) throw new Error(`Wrong Script OP=${o} (${typeof o})`);
129
- // Bytes
130
- const len = o.length;
131
- if (len < OP.PUSHDATA1) w.byte(len);
132
- else if (len <= 0xff) {
133
- w.byte(OP.PUSHDATA1);
134
- w.byte(len);
135
- } else if (len <= 0xffff) {
136
- w.byte(OP.PUSHDATA2);
137
- w.bytes(P.U16LE.encode(len));
138
- } else {
139
- w.byte(OP.PUSHDATA4);
140
- w.bytes(P.U32LE.encode(len));
141
- }
142
- w.bytes(o);
143
- }
144
- },
145
- decodeStream: (r: P.Reader): ScriptType => {
146
- const out: ScriptType = [];
147
- while (!r.isEnd()) {
148
- const cur = r.byte();
149
- // if 0 < cur < 78
150
- if (OP.OP_0 < cur && cur <= OP.PUSHDATA4) {
151
- let len;
152
- if (cur < OP.PUSHDATA1) len = cur;
153
- else if (cur === OP.PUSHDATA1) len = P.U8.decodeStream(r);
154
- else if (cur === OP.PUSHDATA2) len = P.U16LE.decodeStream(r);
155
- else if (cur === OP.PUSHDATA4) len = P.U32LE.decodeStream(r);
156
- else throw new Error('Should be not possible');
157
- out.push(r.bytes(len));
158
- } else if (cur === 0x00) {
159
- out.push(0);
160
- } else if (OP.OP_1 <= cur && cur <= OP.OP_16) {
161
- out.push(cur - (OP.OP_1 - 1));
162
- } else {
163
- const op = OPNames[cur] as keyof typeof OP;
164
- if (op === undefined) throw new Error(`Unknown opcode=${cur.toString(16)}`);
165
- out.push(op);
166
- }
167
- }
168
- return out;
169
- },
170
- });
262
+ },
263
+ decodeStream: (r: P.Reader): TRet<ScriptType> => {
264
+ const out: ScriptType = [];
265
+ while (!r.isEnd()) {
266
+ const cur = r.byte();
267
+ const len = scriptPushLen(cur, (bytes) => {
268
+ if (bytes === 1) return P.U8.decodeStream(r);
269
+ if (bytes === 2) return P.U16LE.decodeStream(r);
270
+ return P.U32LE.decodeStream(r);
271
+ });
272
+ if (len !== undefined) {
273
+ out.push(r.bytes(len));
274
+ } else if (cur === 0x00) {
275
+ out.push(0);
276
+ } else if (OP.OP_1 <= cur && cur <= OP.OP_16) {
277
+ out.push(cur - (OP.OP_1 - 1));
278
+ } else {
279
+ const op = OPNames[cur] as keyof typeof OP;
280
+ if (op === undefined) throw new Error(`Unknown opcode=${cur.toString(16)}`);
281
+ out.push(op);
282
+ }
283
+ }
284
+ return out as TRet<ScriptType>;
285
+ },
286
+ })
287
+ ))() as TRet<P.CoderType<ScriptType>>;
171
288
 
172
- // BTC specific variable length integer encoding
173
- // https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
174
- const CSLimits: Record<number, [number, number, bigint, bigint]> = {
175
- 0xfd: [0xfd, 2, 253n, 65535n],
176
- 0xfe: [0xfe, 4, 65536n, 4294967295n],
177
- 0xff: [0xff, 8, 4294967296n, 18446744073709551615n],
178
- };
179
- export const CompactSize: P.CoderType<bigint> = P.wrap({
180
- encodeStream: (w: P.Writer, value: bigint) => {
181
- if (typeof value === 'number') value = BigInt(value);
182
- if (0n <= value && value <= 252n) return w.byte(Number(value));
183
- for (const [flag, bytes, start, stop] of Object.values(CSLimits)) {
184
- if (start > value || value > stop) continue;
185
- w.byte(flag);
186
- for (let i = 0; i < bytes; i++) w.byte(Number((value >> (8n * BigInt(i))) & 0xffn));
187
- return;
188
- }
189
- throw w.err(`VarInt too big: ${value}`);
190
- },
191
- decodeStream: (r: P.Reader): bigint => {
192
- const b0 = r.byte();
193
- if (b0 <= 0xfc) return BigInt(b0);
194
- const [_, bytes, start] = CSLimits[b0];
195
- let num = 0n;
196
- for (let i = 0; i < bytes; i++) num |= BigInt(r.byte()) << (8n * BigInt(i));
197
- if (num < start) throw r.err(`Wrong CompactSize(${8 * bytes})`);
198
- return num;
199
- },
200
- });
289
+ /**
290
+ * Bitcoin CompactSize integer coder.
291
+ * @example
292
+ * Encode a CompactSize integer for wire serialization.
293
+ * ```ts
294
+ * CompactSize.encode(1n);
295
+ * ```
296
+ */
297
+ export const CompactSize: P.CoderType<bigint> = /* @__PURE__ */ (() => {
298
+ // BTC specific variable length integer encoding
299
+ // https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
300
+ const limits: Record<number, [number, number, bigint, bigint]> = {
301
+ 0xfd: [0xfd, 2, BigInt(0xfd), BigInt(0xffff)],
302
+ 0xfe: [0xfe, 4, BigInt(0x10000), BigInt(0xffffffff)],
303
+ 0xff: [0xff, 8, BigInt(0x100000000), BigInt('0xffffffffffffffff')],
304
+ };
305
+ // Hoisted: Object.values() would otherwise allocate a fresh array on every encode.
306
+ const limitsList = Object.values(limits);
307
+ return Object.freeze(
308
+ P.wrap({
309
+ encodeStream: (w: P.Writer, value: bigint) => {
310
+ if (typeof value === 'number') value = BigInt(value);
311
+ if (_0n <= value && value <= COMPACT_DIRECT_MAX) return w.byte(Number(value));
312
+ for (const [flag, bytes, start, stop] of limitsList) {
313
+ if (start > value || value > stop) continue;
314
+ w.byte(flag);
315
+ for (let i = 0; i < bytes; i++) w.byte(Number((value >> (_8n * BigInt(i))) & U8_MAX));
316
+ return;
317
+ }
318
+ throw w.err(`VarInt too big: ${value}`);
319
+ },
320
+ decodeStream: (r: P.Reader): bigint => {
321
+ const b0 = r.byte();
322
+ if (b0 <= 0xfc) return BigInt(b0);
323
+ const [_, bytes, start] = limits[b0];
324
+ let num = _0n;
325
+ for (let i = 0; i < bytes; i++) num |= BigInt(r.byte()) << (_8n * BigInt(i));
326
+ // BIP 152 / BIP 174: CompactSize fields must use the shortest encoding,
327
+ // so wider prefixes for smaller values are rejected here.
328
+ if (num < start) throw r.err(`Wrong CompactSize(${8 * bytes})`);
329
+ return num;
330
+ },
331
+ })
332
+ );
333
+ })();
201
334
 
202
335
  // Same thing, but in number instead of bigint. Checks for safe integer inside
203
- export const CompactSizeLen: P.CoderType<number> = P.apply(CompactSize, P.coders.numberBigint);
336
+ /**
337
+ * CompactSize coder that decodes into JavaScript numbers.
338
+ * @example
339
+ * Use the number-based CompactSize helper when the value fits a JS number.
340
+ * ```ts
341
+ * CompactSizeLen.encode(1);
342
+ * ```
343
+ */
344
+ export const CompactSizeLen: P.CoderType<number> = /* @__PURE__ */ (() =>
345
+ Object.freeze(P.apply(CompactSize, P.coders.numberBigint)))();
204
346
 
205
347
  // ui8a of size <CompactSize>
206
- export const VarBytes: P.CoderType<Bytes> = P.bytes(CompactSize);
348
+ // Keep an unwrapped local coder for raw transaction structs. The exported VarBytes surface uses
349
+ // TRet for declaration stability, but RawInput/RawOutput still need the old internal field shape.
350
+ const _VarBytes = /* @__PURE__ */ (() => Object.freeze(P.bytes(CompactSize)))();
351
+ /**
352
+ * Length-prefixed byte array coder.
353
+ * @example
354
+ * Prefix a byte array with its CompactSize length.
355
+ * ```ts
356
+ * VarBytes.encode(new Uint8Array([1, 2, 3]));
357
+ * ```
358
+ */
359
+ export const VarBytes: TRet<P.CoderType<Bytes>> = _VarBytes as TRet<P.CoderType<Bytes>>;
207
360
 
208
361
  // SegWit v0 stack of witness buffers
209
- export const RawWitness: P.CoderType<Bytes[]> = P.array(CompactSizeLen, VarBytes);
362
+ // Raw witness serialization is reused across witness versions and PSBT
363
+ // finalScriptWitness values; version-specific stack and element limits are
364
+ // enforced by the spending rules, not by this byte codec.
365
+ // Same split for raw witness stacks: export the wrapped surface, but keep the raw tx struct coder
366
+ // on the unwrapped local alias so witness arrays inside RawTx keep their previous internal shape.
367
+ const _RawWitness = /* @__PURE__ */ (() => Object.freeze(P.array(CompactSizeLen, _VarBytes)))();
368
+ /**
369
+ * SegWit witness stack coder.
370
+ * @example
371
+ * Encode one witness stack for a SegWit input.
372
+ * ```ts
373
+ * RawWitness.encode([new Uint8Array([1])]);
374
+ * ```
375
+ */
376
+ export const RawWitness: TRet<P.CoderType<Bytes[]>> = _RawWitness as TRet<P.CoderType<Bytes[]>>;
210
377
 
211
378
  // Array of size <CompactSize>
379
+ /**
380
+ * Coder for CompactSize-prefixed arrays.
381
+ * @param t - element coder
382
+ * @returns Array coder.
383
+ * @example
384
+ * CompactSize-prefix a small list of fixed-width integers.
385
+ * ```ts
386
+ * import * as P from 'micro-packed';
387
+ * import { BTCArray } from '@scure/btc-signer/script.js';
388
+ * BTCArray(P.U8).encode([1, 2, 3]);
389
+ * ```
390
+ */
212
391
  export const BTCArray = <T>(t: P.CoderType<T>): P.CoderType<T[]> => P.array(CompactSize, t);
213
392
 
214
- export const RawInput = P.struct({
215
- txid: P.bytes(32, true), // hash(prev_tx),
216
- index: P.U32LE, // output number of previous tx
217
- finalScriptSig: VarBytes, // btc merges input and output script, executes it. If ok = tx passes
218
- sequence: P.U32LE, // ?
219
- });
393
+ /**
394
+ * Raw Bitcoin transaction input coder.
395
+ * @example
396
+ * Encode one transaction input exactly as it appears on the wire.
397
+ * ```ts
398
+ * import { hex } from '@scure/base';
399
+ * import { RawInput } from '@scure/btc-signer/script.js';
400
+ * RawInput.encode({
401
+ * txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
402
+ * index: 0,
403
+ * finalScriptSig: new Uint8Array([0x51]),
404
+ * sequence: 0xffffffff,
405
+ * });
406
+ * ```
407
+ */
408
+ export const RawInput = /* @__PURE__ */ (() =>
409
+ Object.freeze(
410
+ P.struct({
411
+ txid: P.bytes(32, true), // hash(prev_tx),
412
+ index: P.U32LE, // output number of previous tx
413
+ finalScriptSig: _VarBytes, // btc merges input and output script, executes it. If ok = tx passes
414
+ sequence: P.U32LE, // ?
415
+ })
416
+ ))();
220
417
 
221
- export const RawOutput = P.struct({ amount: P.U64LE, script: VarBytes });
418
+ /**
419
+ * Raw Bitcoin transaction output coder.
420
+ * @example
421
+ * Encode one transaction output with amount and scriptPubKey.
422
+ * ```ts
423
+ * import { RawOutput } from '@scure/btc-signer/script.js';
424
+ * RawOutput.encode({ amount: 1n, script: new Uint8Array([0x51]) });
425
+ * ```
426
+ */
427
+ export const RawOutput = /* @__PURE__ */ (() =>
428
+ Object.freeze(P.struct({ amount: P.U64LE, script: _VarBytes })))();
222
429
 
223
430
  // https://en.bitcoin.it/wiki/Protocol_documentation#tx
224
- const _RawTx = P.struct({
225
- version: P.I32LE,
226
- segwitFlag: P.flag(new Uint8Array([0x00, 0x01])),
227
- inputs: BTCArray(RawInput),
228
- outputs: BTCArray(RawOutput),
229
- witnesses: P.flagged('segwitFlag', P.array('inputs/length', RawWitness)),
230
- // < 500000000 Block number at which this transaction is unlocked
231
- // >= 500000000 UNIX timestamp at which this transaction is unlocked
232
- // Handled as part of PSBTv2
233
- lockTime: P.U32LE,
234
- });
431
+ const _RawTx = /* @__PURE__ */ (() =>
432
+ P.struct({
433
+ version: P.I32LE,
434
+ segwitFlag: P.flag(new Uint8Array([0x00, 0x01])),
435
+ inputs: BTCArray(RawInput),
436
+ outputs: BTCArray(RawOutput),
437
+ // BIP144 does not encode a witness-count field; one RawWitness entry is
438
+ // implied for each txin and follows the same order as inputs.
439
+ witnesses: P.flagged('segwitFlag', P.array('inputs/length', _RawWitness)),
440
+ // < 500000000 Block number at which this transaction is unlocked
441
+ // >= 500000000 UNIX timestamp at which this transaction is unlocked
442
+ // Handled as part of PSBTv2
443
+ lockTime: P.U32LE,
444
+ }))();
235
445
 
236
446
  function validateRawTx(tx: P.UnwrapCoder<typeof _RawTx>) {
237
- if (tx.segwitFlag && tx.witnesses && !tx.witnesses.length)
238
- throw new Error('Segwit flag with empty witnesses array');
447
+ // BIP 144: if every per-input witness field is empty, callers must use the old
448
+ // serialization format instead of the marker/flag witness form.
449
+ if (tx.segwitFlag && tx.witnesses && tx.witnesses.every((w) => !w.length))
450
+ throw new Error('Segwit flag with only empty witness fields');
239
451
  return tx;
240
452
  }
241
- export const RawTx: typeof _RawTx = P.validate(_RawTx, validateRawTx);
453
+ /**
454
+ * Raw Bitcoin transaction coder.
455
+ * @example
456
+ * Encode a SegWit transaction with one input, one output, and one witness stack.
457
+ * ```ts
458
+ * import { hex } from '@scure/base';
459
+ * import { RawTx } from '@scure/btc-signer/script.js';
460
+ * RawTx.encode({
461
+ * version: 2,
462
+ * segwitFlag: true,
463
+ * inputs: [{
464
+ * txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
465
+ * index: 0,
466
+ * finalScriptSig: new Uint8Array(),
467
+ * sequence: 0xffffffff,
468
+ * }],
469
+ * outputs: [{ amount: 1n, script: new Uint8Array([0x51]) }],
470
+ * witnesses: [[new Uint8Array([1])]],
471
+ * lockTime: 0,
472
+ * });
473
+ * ```
474
+ */
475
+ export const RawTx: typeof _RawTx = /* @__PURE__ */ (() =>
476
+ Object.freeze(P.validate(_RawTx, validateRawTx)))();
242
477
  // Pre-SegWit serialization format (for PSBTv0)
243
- export const RawOldTx = P.struct({
244
- version: P.I32LE,
245
- inputs: BTCArray(RawInput),
246
- outputs: BTCArray(RawOutput),
247
- lockTime: P.U32LE,
248
- });
478
+ /**
479
+ * Pre-SegWit transaction coder used by PSBTv0.
480
+ * @example
481
+ * Encode the legacy unsigned transaction format used inside PSBTv0 globals.
482
+ * ```ts
483
+ * import { hex } from '@scure/base';
484
+ * import { RawOldTx } from '@scure/btc-signer/script.js';
485
+ * RawOldTx.encode({
486
+ * version: 2,
487
+ * inputs: [{
488
+ * txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
489
+ * index: 0,
490
+ * finalScriptSig: new Uint8Array(),
491
+ * sequence: 0xffffffff,
492
+ * }],
493
+ * outputs: [{ amount: 1n, script: new Uint8Array([0x51]) }],
494
+ * lockTime: 0,
495
+ * });
496
+ * ```
497
+ */
498
+ export const RawOldTx = /* @__PURE__ */ (() =>
499
+ Object.freeze(
500
+ P.struct({
501
+ version: P.I32LE,
502
+ inputs: BTCArray(RawInput),
503
+ outputs: BTCArray(RawOutput),
504
+ lockTime: P.U32LE,
505
+ })
506
+ ))();