@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/README.md +334 -64
- package/index.d.ts +15 -6
- package/index.js +16 -7
- package/musig2.d.ts +212 -69
- package/musig2.js +352 -99
- package/net.d.ts +355 -0
- package/net.js +875 -0
- package/p2p.d.ts +17 -8
- package/p2p.js +63 -11
- package/package.json +17 -17
- package/payment.d.ts +406 -41
- package/payment.js +570 -69
- package/psbt.d.ts +2958 -560
- package/psbt.js +475 -119
- package/script.d.ts +311 -133
- package/script.js +313 -90
- package/src/_type_test.ts +69 -0
- package/src/index.ts +34 -11
- package/src/musig2.ts +424 -155
- package/src/net.ts +1106 -0
- package/src/p2p.ts +76 -24
- package/src/payment.ts +882 -235
- package/src/psbt.ts +648 -229
- package/src/script.ts +397 -139
- package/src/transaction.ts +667 -196
- package/src/utils.ts +392 -47
- package/src/utxo.ts +182 -83
- package/transaction.d.ts +242 -32
- package/transaction.js +531 -121
- package/utils.d.ts +296 -25
- package/utils.js +337 -30
- package/utxo.d.ts +438 -76
- package/utxo.js +150 -59
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/musig2.d.ts.map +0 -1
- package/musig2.js.map +0 -1
- package/p2p.d.ts.map +0 -1
- package/p2p.js.map +0 -1
- package/payment.d.ts.map +0 -1
- package/payment.js.map +0 -1
- package/psbt.d.ts.map +0 -1
- package/psbt.js.map +0 -1
- package/script.d.ts.map +0 -1
- package/script.js.map +0 -1
- package/transaction.d.ts.map +0 -1
- package/transaction.js.map +0 -1
- package/utils.d.ts.map +0 -1
- package/utils.js.map +0 -1
- package/utxo.d.ts.map +0 -1
- package/utxo.js.map +0 -1
package/transaction.js
CHANGED
|
@@ -1,23 +1,116 @@
|
|
|
1
1
|
import { hex } from '@scure/base';
|
|
2
|
+
import { anumber } from '@noble/hashes/utils.js';
|
|
2
3
|
import * as P from 'micro-packed';
|
|
3
4
|
import { Address, OutScript, checkScript, tapLeafHash } from "./payment.js";
|
|
4
5
|
import * as psbt from "./psbt.js";
|
|
5
|
-
import { CompactSizeLen, RawOldTx, RawOutput, RawTx,
|
|
6
|
+
import { CompactSizeLen, OP, RawOldTx, RawInput, RawOutput, RawTx, Script, scriptPushLen, VarBytes, } from "./script.js";
|
|
6
7
|
import * as u from "./utils.js";
|
|
7
|
-
import { NETWORK, concatBytes, equalBytes, isBytes } from "./utils.js";
|
|
8
|
-
|
|
8
|
+
import { NETWORK, abigint, concatBytes, equalBytes, isBytes, validateObject, } from "./utils.js";
|
|
9
|
+
// Be friendly to bad ECMAScript parsers by not using bigint literals.
|
|
10
|
+
// prettier-ignore
|
|
11
|
+
const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
|
|
12
|
+
const U64_MAX = /* @__PURE__ */ BigInt('0xffffffffffffffff');
|
|
13
|
+
const EMPTY32 = /* @__PURE__ */ new Uint8Array(32);
|
|
9
14
|
const EMPTY_OUTPUT = {
|
|
10
|
-
amount:
|
|
15
|
+
amount: U64_MAX,
|
|
11
16
|
script: P.EMPTY,
|
|
12
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Converts transaction weight units into virtual bytes.
|
|
20
|
+
* @param weight - transaction weight
|
|
21
|
+
* @returns Rounded-up virtual size.
|
|
22
|
+
* @example
|
|
23
|
+
* Convert transaction weight units into virtual bytes.
|
|
24
|
+
* ```ts
|
|
25
|
+
* toVsize(4);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
13
28
|
export const toVsize = (weight) => Math.ceil(weight / 4);
|
|
29
|
+
const stripCodeSeparator = (script) => {
|
|
30
|
+
// Reuse Script's raw pushdata-length parser here. Legacy sighash must remove
|
|
31
|
+
// only actual OP_CODESEPARATOR opcodes while preserving every other original
|
|
32
|
+
// byte, because semantic decode/re-encode would change the signed digest.
|
|
33
|
+
let start = 0;
|
|
34
|
+
const out = [];
|
|
35
|
+
for (let i = 0; i < script.length;) {
|
|
36
|
+
const pos = i;
|
|
37
|
+
const op = script[i++];
|
|
38
|
+
if (op === OP.CODESEPARATOR) {
|
|
39
|
+
if (start < pos)
|
|
40
|
+
out.push(script.subarray(start, pos));
|
|
41
|
+
start = i;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const len = scriptPushLen(op, (bytes) => {
|
|
45
|
+
if (i + bytes > script.length)
|
|
46
|
+
throw new Error('Unexpected end of script');
|
|
47
|
+
let len = 0;
|
|
48
|
+
for (let j = 0; j < bytes; j++)
|
|
49
|
+
len |= script[i + j] << (8 * j);
|
|
50
|
+
i += bytes;
|
|
51
|
+
return len;
|
|
52
|
+
});
|
|
53
|
+
if (len === undefined)
|
|
54
|
+
continue;
|
|
55
|
+
i += len;
|
|
56
|
+
if (i > script.length)
|
|
57
|
+
throw new Error('Unexpected end of script');
|
|
58
|
+
}
|
|
59
|
+
if (start === 0)
|
|
60
|
+
return script;
|
|
61
|
+
if (start < script.length)
|
|
62
|
+
out.push(script.subarray(start));
|
|
63
|
+
return (out.length ? concatBytes(...out) : P.EMPTY);
|
|
64
|
+
};
|
|
65
|
+
/** Decimal precision used for BTC string formatting. */
|
|
14
66
|
export const PRECISION = 8;
|
|
67
|
+
/** Default transaction version used for newly created transactions. */
|
|
15
68
|
export const DEFAULT_VERSION = 2;
|
|
69
|
+
/** Default transaction locktime. */
|
|
16
70
|
export const DEFAULT_LOCKTIME = 0;
|
|
71
|
+
/** Default input sequence number.
|
|
72
|
+
* Final (`0xffffffff`): matches the PSBT omission default and disables nLockTime/CLTV semantics
|
|
73
|
+
* unless callers choose a lower sequence explicitly (for example `0xfffffffe` with lockTime).
|
|
74
|
+
*/
|
|
17
75
|
export const DEFAULT_SEQUENCE = 4294967295;
|
|
18
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Decimal coder for BTC-denominated strings.
|
|
78
|
+
* This is a fixed-precision BTC-string to satoshi-bigint helper, not a validator
|
|
79
|
+
* for transaction/PSBT output amounts. Signed values are intentional here, so
|
|
80
|
+
* callers can reuse the helper for display/history-style deltas as well as
|
|
81
|
+
* unsigned transfer amounts. It keeps the BTC scale at 8 fractional digits and
|
|
82
|
+
* rejects over-precise inputs instead of rounding.
|
|
83
|
+
* @example
|
|
84
|
+
* Convert between satoshi bigint values and BTC-denominated decimal strings.
|
|
85
|
+
* ```ts
|
|
86
|
+
* Decimal.encode(1n);
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export const Decimal = /* @__PURE__ */ (() => Object.freeze(P.coders.decimal(PRECISION)))();
|
|
19
90
|
// Same as value || def, but doesn't overwrites zero ('0', 0, 0n, etc)
|
|
91
|
+
/**
|
|
92
|
+
* Returns a fallback only when the value is `undefined`.
|
|
93
|
+
* @param value - optional value
|
|
94
|
+
* @param def - fallback value
|
|
95
|
+
* @returns `value` when defined, otherwise `def`.
|
|
96
|
+
* @example
|
|
97
|
+
* Keep zero-like values but replace `undefined` with a fallback.
|
|
98
|
+
* ```ts
|
|
99
|
+
* def(undefined, 1);
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
20
102
|
export const def = (value, def) => (value === undefined ? def : value);
|
|
103
|
+
/**
|
|
104
|
+
* Deep-clones plain transaction data structures.
|
|
105
|
+
* @param obj - value to clone
|
|
106
|
+
* @returns Deep copy of the input value.
|
|
107
|
+
* @throws If the value contains an unsupported runtime type. {@link Error}
|
|
108
|
+
* @example
|
|
109
|
+
* Clone plain transaction data structures before mutating them.
|
|
110
|
+
* ```ts
|
|
111
|
+
* cloneDeep({ a: [new Uint8Array([1])] });
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
21
114
|
export function cloneDeep(obj) {
|
|
22
115
|
if (Array.isArray(obj))
|
|
23
116
|
return obj.map((i) => cloneDeep(i));
|
|
@@ -34,30 +127,48 @@ export function cloneDeep(obj) {
|
|
|
34
127
|
else if (typeof obj === 'object') {
|
|
35
128
|
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, cloneDeep(v)]));
|
|
36
129
|
}
|
|
37
|
-
|
|
130
|
+
// Don't interpolate unsupported values here: Symbol string coercion would
|
|
131
|
+
// throw before cloneDeep can surface its own stable helper error.
|
|
132
|
+
throw new Error(`cloneDeep: unknown type=${typeof obj}`);
|
|
38
133
|
}
|
|
39
134
|
/**
|
|
40
135
|
* Internal, exported only for backwards-compat. Use `SigHash` instead.
|
|
41
|
-
* @deprecated
|
|
136
|
+
* @deprecated Use {@link SigHash} instead.
|
|
137
|
+
* @example
|
|
138
|
+
* Combine the legacy bit flags when interoperating with older code.
|
|
139
|
+
* ```ts
|
|
140
|
+
* SignatureHash.ALL | SignatureHash.ANYONECANPAY;
|
|
141
|
+
* ```
|
|
42
142
|
*/
|
|
43
|
-
export const SignatureHash = {
|
|
143
|
+
export const SignatureHash = /* @__PURE__ */ (() => Object.freeze({
|
|
44
144
|
DEFAULT: 0,
|
|
45
145
|
ALL: 1,
|
|
46
146
|
NONE: 2,
|
|
47
147
|
SINGLE: 3,
|
|
48
148
|
ANYONECANPAY: 0x80,
|
|
49
|
-
};
|
|
50
|
-
|
|
149
|
+
}))();
|
|
150
|
+
/**
|
|
151
|
+
* Common signature hash flag combinations.
|
|
152
|
+
* @example
|
|
153
|
+
* Use the predefined signature-hash combinations exported by the library.
|
|
154
|
+
* ```ts
|
|
155
|
+
* SigHash.SINGLE_ANYONECANPAY;
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export const SigHash = /* @__PURE__ */ (() => Object.freeze({
|
|
51
159
|
DEFAULT: SignatureHash.DEFAULT,
|
|
52
160
|
ALL: SignatureHash.ALL,
|
|
53
161
|
NONE: SignatureHash.NONE,
|
|
54
162
|
SINGLE: SignatureHash.SINGLE,
|
|
55
|
-
|
|
163
|
+
// BIP341 only permits 0x00, 0x01, 0x02, 0x03, 0x81, 0x82, and 0x83 for taproot, so
|
|
164
|
+
// the mechanical `DEFAULT | ANYONECANPAY` combination (0x80) is invalid and not exported.
|
|
165
|
+
// DEFAULT_ANYONECANPAY: SignatureHash.DEFAULT | SignatureHash.ANYONECANPAY,
|
|
56
166
|
ALL_ANYONECANPAY: SignatureHash.ALL | SignatureHash.ANYONECANPAY,
|
|
57
167
|
NONE_ANYONECANPAY: SignatureHash.NONE | SignatureHash.ANYONECANPAY,
|
|
58
168
|
SINGLE_ANYONECANPAY: SignatureHash.SINGLE | SignatureHash.ANYONECANPAY,
|
|
59
|
-
};
|
|
60
|
-
|
|
169
|
+
}))();
|
|
170
|
+
/** Reverse lookup table for signature hash flag names. */
|
|
171
|
+
export const SigHashNames = /* @__PURE__ */ (() => Object.freeze(u.reverseObject(SigHash)))();
|
|
61
172
|
function getTaprootKeys(privKey, pubKey, internalKey, merkleRoot = P.EMPTY) {
|
|
62
173
|
if (equalBytes(internalKey, pubKey)) {
|
|
63
174
|
privKey = u.taprootTweakPrivKey(privKey, merkleRoot);
|
|
@@ -72,25 +183,50 @@ function outputBeforeSign(i) {
|
|
|
72
183
|
return { script: i.script, amount: i.amount };
|
|
73
184
|
}
|
|
74
185
|
// Force check index/txid/sequence
|
|
186
|
+
/**
|
|
187
|
+
* Normalizes a PSBT input into the fields needed for signing.
|
|
188
|
+
* @param i - PSBT input to validate
|
|
189
|
+
* @returns Input fields required for signing.
|
|
190
|
+
* @throws If the input is missing `txid` or `index`. {@link Error}
|
|
191
|
+
* @example
|
|
192
|
+
* Fill in defaults for the fields the signer expects to see.
|
|
193
|
+
* ```ts
|
|
194
|
+
* import { hex } from '@scure/base';
|
|
195
|
+
* import { inputBeforeSign } from '@scure/btc-signer/transaction.js';
|
|
196
|
+
* inputBeforeSign({
|
|
197
|
+
* txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
|
|
198
|
+
* index: 0,
|
|
199
|
+
* });
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
75
202
|
export function inputBeforeSign(i) {
|
|
203
|
+
validateObject(i, {}, {}, 'i');
|
|
76
204
|
if (i.txid === undefined || i.index === undefined)
|
|
77
205
|
throw new Error('Transaction/input: txid and index required');
|
|
78
|
-
|
|
206
|
+
const res = {
|
|
79
207
|
txid: i.txid,
|
|
80
208
|
index: i.index,
|
|
81
209
|
sequence: def(i.sequence, DEFAULT_SEQUENCE),
|
|
82
210
|
finalScriptSig: def(i.finalScriptSig, P.EMPTY),
|
|
83
211
|
};
|
|
212
|
+
// This helper is the public "normalize for signing" boundary, so reuse RawInput's existing
|
|
213
|
+
// wire-shape checks here instead of letting malformed runtime field types fail much later.
|
|
214
|
+
RawInput.encode(res);
|
|
215
|
+
return res;
|
|
84
216
|
}
|
|
85
217
|
function cleanFinalInput(i) {
|
|
86
|
-
|
|
218
|
+
const _i = i;
|
|
219
|
+
// BIP174 finalizers clear non-final input metadata after constructing final scripts/witnesses.
|
|
220
|
+
// That intentionally drops sighashType here, so post-finalize mutation becomes conservative
|
|
221
|
+
// until callers explicitly reopen the input by removing finalScriptSig/finalScriptWitness.
|
|
222
|
+
for (const _k in _i) {
|
|
87
223
|
const k = _k;
|
|
88
224
|
if (!psbt.PSBTInputFinalKeys.includes(k))
|
|
89
|
-
delete
|
|
225
|
+
delete _i[k];
|
|
90
226
|
}
|
|
91
227
|
}
|
|
92
228
|
// (TxHash, Idx)
|
|
93
|
-
const TxHashIdx = P.struct({ txid: P.bytes(32, true), index: P.U32LE });
|
|
229
|
+
const TxHashIdx = /* @__PURE__ */ (() => P.struct({ txid: P.bytes(32, true), index: P.U32LE }))();
|
|
94
230
|
function validateSigHash(s) {
|
|
95
231
|
if (typeof s !== 'number' || typeof SigHashNames[s] !== 'string')
|
|
96
232
|
throw new Error(`Invalid SigHash=${s}`);
|
|
@@ -105,8 +241,8 @@ function unpackSighash(hashType) {
|
|
|
105
241
|
};
|
|
106
242
|
}
|
|
107
243
|
function validateOpts(opts) {
|
|
108
|
-
if (opts !== undefined
|
|
109
|
-
|
|
244
|
+
if (opts !== undefined)
|
|
245
|
+
validateObject(opts, {}, {}, 'opts');
|
|
110
246
|
const _opts = {
|
|
111
247
|
...opts,
|
|
112
248
|
// Defaults
|
|
@@ -114,10 +250,12 @@ function validateOpts(opts) {
|
|
|
114
250
|
lockTime: def(opts.lockTime, 0),
|
|
115
251
|
PSBTVersion: def(opts.PSBTVersion, 0),
|
|
116
252
|
};
|
|
253
|
+
// Normalize deprecated aliases on the owned copy so they still affect tx.opts without rewriting the
|
|
254
|
+
// caller-owned options object passed to the constructor.
|
|
117
255
|
if (typeof _opts.allowUnknowInput !== 'undefined')
|
|
118
|
-
|
|
256
|
+
_opts.allowUnknownInputs = _opts.allowUnknowInput;
|
|
119
257
|
if (typeof _opts.allowUnknowOutput !== 'undefined')
|
|
120
|
-
|
|
258
|
+
_opts.allowUnknownOutputs = _opts.allowUnknowOutput;
|
|
121
259
|
if (typeof _opts.lockTime !== 'number')
|
|
122
260
|
throw new Error('Transaction lock time should be number');
|
|
123
261
|
P.U32LE.encode(_opts.lockTime); // Additional range checks that lockTime
|
|
@@ -142,10 +280,13 @@ function validateOpts(opts) {
|
|
|
142
280
|
throw new Error(`Transation options wrong type: ${k}=${v} (${typeof v})`);
|
|
143
281
|
}
|
|
144
282
|
// 0 and -1 happens in tests
|
|
283
|
+
// With allowUnknownVersion any numeric version is fine; the ternary was inverted
|
|
284
|
+
// before 2026-07 (audit), which made the option throw for every numeric version.
|
|
145
285
|
if (_opts.allowUnknownVersion
|
|
146
|
-
? typeof _opts.version
|
|
286
|
+
? typeof _opts.version !== 'number'
|
|
147
287
|
: ![-1, 0, 1, 2, 3].includes(_opts.version))
|
|
148
288
|
throw new Error(`Unknown version: ${_opts.version}`);
|
|
289
|
+
P.I32LE.encode(_opts.version); // Validate the signed transaction-version wire domain.
|
|
149
290
|
if (_opts.customScripts !== undefined) {
|
|
150
291
|
const cs = _opts.customScripts;
|
|
151
292
|
if (!Array.isArray(cs)) {
|
|
@@ -162,17 +303,20 @@ function validateOpts(opts) {
|
|
|
162
303
|
}
|
|
163
304
|
// NOTE: we cannot do this inside PSBTInput coder, because there is no index/txid at this point!
|
|
164
305
|
function validateInput(i) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
306
|
+
validateObject(i, {}, {}, 'i');
|
|
307
|
+
const _i = i;
|
|
308
|
+
if (_i.nonWitnessUtxo && _i.index !== undefined) {
|
|
309
|
+
const last = _i.nonWitnessUtxo.outputs.length - 1;
|
|
310
|
+
if (_i.index > last)
|
|
311
|
+
throw new Error(`validateInput: index(${_i.index}) not in nonWitnessUtxo`);
|
|
312
|
+
const prevOut = _i.nonWitnessUtxo.outputs[_i.index];
|
|
313
|
+
if (_i.witnessUtxo &&
|
|
314
|
+
(!equalBytes(_i.witnessUtxo.script, prevOut.script) ||
|
|
315
|
+
_i.witnessUtxo.amount !== prevOut.amount))
|
|
172
316
|
throw new Error('validateInput: witnessUtxo different from nonWitnessUtxo');
|
|
173
|
-
if (
|
|
174
|
-
const outputs =
|
|
175
|
-
if (outputs.length - 1 <
|
|
317
|
+
if (_i.txid) {
|
|
318
|
+
const outputs = _i.nonWitnessUtxo.outputs;
|
|
319
|
+
if (outputs.length - 1 < _i.index)
|
|
176
320
|
throw new Error('nonWitnessUtxo: incorect output index');
|
|
177
321
|
// At this point, we are using previous tx output to create new input.
|
|
178
322
|
// Script safety checks are unnecessary:
|
|
@@ -182,33 +326,96 @@ function validateInput(i) {
|
|
|
182
326
|
// in case user wants to use wrong input by mistake
|
|
183
327
|
// - Worst case: tx will be rejected by nodes. Still better than disallowing user
|
|
184
328
|
// to spend real input, no matter how broken it looks
|
|
185
|
-
const tx = Transaction.fromRaw(RawTx.encode(
|
|
329
|
+
const tx = Transaction.fromRaw(RawTx.encode(_i.nonWitnessUtxo), {
|
|
186
330
|
allowUnknownOutputs: true,
|
|
187
331
|
disableScriptCheck: true,
|
|
188
332
|
allowUnknownInputs: true,
|
|
333
|
+
// Consensus does not restrict nVersion; a previous tx with a non-standard
|
|
334
|
+
// version is still spendable and its txid must still be verifiable.
|
|
335
|
+
allowUnknownVersion: true,
|
|
189
336
|
});
|
|
190
|
-
const txid = hex.encode(
|
|
191
|
-
//
|
|
192
|
-
|
|
337
|
+
const txid = hex.encode(_i.txid);
|
|
338
|
+
// BIP174 requires the provided nonWitnessUtxo to hash to the prevout txid even when the
|
|
339
|
+
// previous transaction is otherwise non-final; finality does not make its serialized txid optional.
|
|
340
|
+
// Keep the historical TransactionInput.txid convention here: internal txid bytes match
|
|
341
|
+
// `Transaction.id` (display-order hex), while raw-tx / PSBT boundary coders are responsible
|
|
342
|
+
// for any byte-order conversions required by their wire formats.
|
|
343
|
+
if (tx.id !== txid)
|
|
193
344
|
throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
|
|
194
345
|
}
|
|
195
346
|
}
|
|
196
|
-
return
|
|
347
|
+
return _i;
|
|
197
348
|
}
|
|
198
349
|
// Normalizes input
|
|
350
|
+
/**
|
|
351
|
+
* Extracts the previous output referenced by an input.
|
|
352
|
+
* @param input - PSBT input with previous output data
|
|
353
|
+
* @returns Previous output information.
|
|
354
|
+
* @throws If the input does not contain usable previous-output information. {@link Error}
|
|
355
|
+
* @example
|
|
356
|
+
* Read the previous output from either `witnessUtxo` or `nonWitnessUtxo`.
|
|
357
|
+
* ```ts
|
|
358
|
+
* getPrevOut({ witnessUtxo: { amount: 1n, script: new Uint8Array([0x51]) } });
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
199
361
|
export function getPrevOut(input) {
|
|
200
|
-
|
|
201
|
-
|
|
362
|
+
validateObject(input, {}, {}, 'input');
|
|
363
|
+
const _input = input;
|
|
364
|
+
if (_input.nonWitnessUtxo) {
|
|
365
|
+
if (_input.index === undefined)
|
|
202
366
|
throw new Error('Unknown input index');
|
|
203
|
-
|
|
367
|
+
// BIP174 `PSBT_IN_NON_WITNESS_UTXO` is the full spent transaction, so the
|
|
368
|
+
// input outpoint index must name an existing output instead of leaking a
|
|
369
|
+
// synthetic `undefined` prevout into later signing / estimation callers.
|
|
370
|
+
if (!Number.isSafeInteger(_input.index) ||
|
|
371
|
+
_input.index < 0 ||
|
|
372
|
+
_input.index >= _input.nonWitnessUtxo.outputs.length)
|
|
373
|
+
throw new Error(`Wrong input index=${_input.index}`);
|
|
374
|
+
return _input.nonWitnessUtxo.outputs[_input.index];
|
|
375
|
+
}
|
|
376
|
+
else if ('witnessUtxo' in _input) {
|
|
377
|
+
// The presence check catches malformed provided values; narrow after the guard for TS.
|
|
378
|
+
const prev = _input.witnessUtxo;
|
|
379
|
+
validateObject(prev, {}, {}, 'input.witnessUtxo');
|
|
380
|
+
abigint(prev.amount, 'input.witnessUtxo.amount');
|
|
381
|
+
if (!isBytes(prev.script))
|
|
382
|
+
throw new TypeError('"input.witnessUtxo.script" expected Uint8Array, got type=' + typeof prev.script);
|
|
383
|
+
return prev;
|
|
204
384
|
}
|
|
205
|
-
else if (input.witnessUtxo)
|
|
206
|
-
return input.witnessUtxo;
|
|
207
385
|
else
|
|
208
386
|
throw new Error('Cannot find previous output info');
|
|
209
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* Normalizes a transaction input update into canonical PSBT form.
|
|
390
|
+
* @param i - input update to normalize
|
|
391
|
+
* @param cur - existing input value to merge with
|
|
392
|
+
* @param allowedFields - fields that may still change on signed inputs
|
|
393
|
+
* @param disableScriptCheck - whether to skip redeem/witness script sanity checks
|
|
394
|
+
* @param allowUnknown - whether to keep unknown PSBT fields
|
|
395
|
+
* @returns Normalized PSBT input.
|
|
396
|
+
* @example
|
|
397
|
+
* Accept hex txids from callers in the same display-order form used by `Transaction.id`, then
|
|
398
|
+
* normalize them into the repo's internal `TransactionInput` shape.
|
|
399
|
+
* ```ts
|
|
400
|
+
* import { hex } from '@scure/base';
|
|
401
|
+
* import { normalizeInput } from '@scure/btc-signer/transaction.js';
|
|
402
|
+
* normalizeInput({
|
|
403
|
+
* txid: '0000000000000000000000000000000000000000000000000000000000000001',
|
|
404
|
+
* index: 0,
|
|
405
|
+
* witnessUtxo: { amount: 1n, script: new Uint8Array([0x51]) },
|
|
406
|
+
* });
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
210
409
|
export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false, allowUnknown = false) {
|
|
211
|
-
|
|
410
|
+
validateObject(i, {}, {}, 'i');
|
|
411
|
+
if (cur !== undefined)
|
|
412
|
+
validateObject(cur, {}, {}, 'cur');
|
|
413
|
+
if (allowedFields !== undefined)
|
|
414
|
+
u.aarray(allowedFields, 'allowedFields');
|
|
415
|
+
const _i = i;
|
|
416
|
+
const _cur = cur;
|
|
417
|
+
const _allowedFields = allowedFields;
|
|
418
|
+
let { nonWitnessUtxo, txid } = _i;
|
|
212
419
|
// String support for common fields. We usually prefer Uint8Array to avoid errors
|
|
213
420
|
// like hex looking string accidentally passed, however, in case of nonWitnessUtxo
|
|
214
421
|
// it is better to expect string, since constructing this complex object will be
|
|
@@ -217,21 +424,23 @@ export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false
|
|
|
217
424
|
nonWitnessUtxo = hex.decode(nonWitnessUtxo);
|
|
218
425
|
if (isBytes(nonWitnessUtxo))
|
|
219
426
|
nonWitnessUtxo = RawTx.decode(nonWitnessUtxo);
|
|
220
|
-
if (!('nonWitnessUtxo' in
|
|
221
|
-
nonWitnessUtxo =
|
|
427
|
+
if (!('nonWitnessUtxo' in _i) && nonWitnessUtxo === undefined)
|
|
428
|
+
nonWitnessUtxo = _cur?.nonWitnessUtxo;
|
|
222
429
|
if (typeof txid === 'string')
|
|
223
430
|
txid = hex.decode(txid);
|
|
224
431
|
// TODO: if we have nonWitnessUtxo, we can extract txId from here
|
|
225
432
|
if (txid === undefined)
|
|
226
|
-
txid =
|
|
227
|
-
let res = { ...
|
|
228
|
-
if (!('nonWitnessUtxo' in
|
|
433
|
+
txid = _cur?.txid;
|
|
434
|
+
let res = { ..._cur, ..._i, nonWitnessUtxo, txid };
|
|
435
|
+
if (!('nonWitnessUtxo' in _i) && res.nonWitnessUtxo === undefined)
|
|
229
436
|
delete res.nonWitnessUtxo;
|
|
230
437
|
if (res.sequence === undefined)
|
|
231
438
|
res.sequence = DEFAULT_SEQUENCE;
|
|
232
439
|
if (res.tapMerkleRoot === null)
|
|
233
440
|
delete res.tapMerkleRoot;
|
|
234
|
-
res = psbt.mergeKeyMap(psbt.PSBTInput, res,
|
|
441
|
+
res = psbt.mergeKeyMap(psbt.PSBTInput, res, _cur, _allowedFields, allowUnknown);
|
|
442
|
+
// Public PSBT coder surface is wrapped with TArg/TRet for TS compatibility; normalizeInput keeps
|
|
443
|
+
// the repo's historical raw internal shape and casts only at the validation boundary here.
|
|
235
444
|
psbt.PSBTInputCoder.encode(res); // Validates that everything is correct at this point
|
|
236
445
|
let prevOut;
|
|
237
446
|
if (res.nonWitnessUtxo && res.index !== undefined)
|
|
@@ -242,15 +451,42 @@ export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false
|
|
|
242
451
|
checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
|
|
243
452
|
return res;
|
|
244
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Determines how an input should be signed and finalized.
|
|
456
|
+
* Wrapper consistency is expected to be validated earlier by {@link normalizeInput}
|
|
457
|
+
* and {@link checkScript}; this helper classifies already-normalized inputs and is
|
|
458
|
+
* not a standalone redeemScript/witnessScript correctness gate for raw caller input.
|
|
459
|
+
* @param input - PSBT input to inspect
|
|
460
|
+
* @param allowLegacyWitnessUtxo - whether legacy inputs may rely on witness UTXO data only
|
|
461
|
+
* @returns Input classification including transaction type and sighash defaults.
|
|
462
|
+
* @throws If a documented runtime validation or state check fails. {@link Error}
|
|
463
|
+
* @example
|
|
464
|
+
* Detect how the signer should treat a SegWit input from its previous output script.
|
|
465
|
+
* ```ts
|
|
466
|
+
* import { hex } from '@scure/base';
|
|
467
|
+
* import { p2wpkh } from '@scure/btc-signer/payment.js';
|
|
468
|
+
* import { getInputType } from '@scure/btc-signer/transaction.js';
|
|
469
|
+
* import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
470
|
+
* getInputType({
|
|
471
|
+
* witnessUtxo: {
|
|
472
|
+
* amount: 1n,
|
|
473
|
+
* script: p2wpkh(pubECDSA(randomPrivateKeyBytes())).script,
|
|
474
|
+
* },
|
|
475
|
+
* });
|
|
476
|
+
* ```
|
|
477
|
+
*/
|
|
245
478
|
export function getInputType(input, allowLegacyWitnessUtxo = false) {
|
|
479
|
+
const _input = input;
|
|
246
480
|
let txType = 'legacy';
|
|
247
481
|
let defaultSighash = SignatureHash.ALL;
|
|
248
|
-
const prevOut = getPrevOut(
|
|
482
|
+
const prevOut = getPrevOut(_input);
|
|
249
483
|
const first = OutScript.decode(prevOut.script);
|
|
250
484
|
let type = first.type;
|
|
251
485
|
let cur = first;
|
|
252
486
|
const stack = [first];
|
|
253
487
|
if (first.type === 'tr') {
|
|
488
|
+
// Expected invariant: taproot inputs use PSBT_IN_TAP_* metadata only;
|
|
489
|
+
// legacy redeemScript/witnessScript fields belong to P2SH/P2WSH paths.
|
|
254
490
|
defaultSighash = SignatureHash.DEFAULT;
|
|
255
491
|
return {
|
|
256
492
|
txType: 'taproot',
|
|
@@ -258,16 +494,16 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
|
|
|
258
494
|
last: first,
|
|
259
495
|
lastScript: prevOut.script,
|
|
260
496
|
defaultSighash,
|
|
261
|
-
sighash:
|
|
497
|
+
sighash: _input.sighashType || defaultSighash,
|
|
262
498
|
};
|
|
263
499
|
}
|
|
264
500
|
else {
|
|
265
501
|
if (first.type === 'wpkh' || first.type === 'wsh')
|
|
266
502
|
txType = 'segwit';
|
|
267
503
|
if (first.type === 'sh') {
|
|
268
|
-
if (!
|
|
504
|
+
if (!_input.redeemScript)
|
|
269
505
|
throw new Error('inputType: sh without redeemScript');
|
|
270
|
-
let child = OutScript.decode(
|
|
506
|
+
let child = OutScript.decode(_input.redeemScript);
|
|
271
507
|
if (child.type === 'wpkh' || child.type === 'wsh')
|
|
272
508
|
txType = 'segwit';
|
|
273
509
|
stack.push(child);
|
|
@@ -276,9 +512,9 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
|
|
|
276
512
|
}
|
|
277
513
|
// wsh can be inside sh
|
|
278
514
|
if (cur.type === 'wsh') {
|
|
279
|
-
if (!
|
|
515
|
+
if (!_input.witnessScript)
|
|
280
516
|
throw new Error('inputType: wsh without witnessScript');
|
|
281
|
-
let child = OutScript.decode(
|
|
517
|
+
let child = OutScript.decode(_input.witnessScript);
|
|
282
518
|
if (child.type === 'wsh')
|
|
283
519
|
txType = 'segwit';
|
|
284
520
|
stack.push(child);
|
|
@@ -295,14 +531,35 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
|
|
|
295
531
|
last,
|
|
296
532
|
lastScript,
|
|
297
533
|
defaultSighash,
|
|
298
|
-
sighash:
|
|
534
|
+
sighash: _input.sighashType || defaultSighash,
|
|
299
535
|
};
|
|
300
|
-
if (txType === 'legacy' && !allowLegacyWitnessUtxo && !
|
|
536
|
+
if (txType === 'legacy' && !allowLegacyWitnessUtxo && !_input.nonWitnessUtxo) {
|
|
301
537
|
throw new Error(`Transaction/sign: legacy input without nonWitnessUtxo, can result in attack that forces paying higher fees. Pass allowLegacyWitnessUtxo=true, if you sure`);
|
|
302
538
|
}
|
|
303
539
|
return res;
|
|
304
540
|
}
|
|
305
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* Mutable Bitcoin transaction and PSBT helper.
|
|
544
|
+
* @param opts - Transaction construction and PSBT serialization options. See {@link TxOpts}.
|
|
545
|
+
* @example
|
|
546
|
+
* Create a transaction, add one spend, and export it as PSBT.
|
|
547
|
+
* ```ts
|
|
548
|
+
* import { hex } from '@scure/base';
|
|
549
|
+
* import { p2wpkh } from '@scure/btc-signer/payment.js';
|
|
550
|
+
* import { Transaction } from '@scure/btc-signer/transaction.js';
|
|
551
|
+
* import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
552
|
+
* const spend = p2wpkh(pubECDSA(randomPrivateKeyBytes()));
|
|
553
|
+
* const tx = new Transaction();
|
|
554
|
+
* tx.addInput({
|
|
555
|
+
* txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
|
|
556
|
+
* index: 0,
|
|
557
|
+
* witnessUtxo: { amount: 2n, script: spend.script },
|
|
558
|
+
* });
|
|
559
|
+
* tx.addOutput({ script: spend.script, amount: 1n });
|
|
560
|
+
* tx.toPSBT();
|
|
561
|
+
* ```
|
|
562
|
+
*/
|
|
306
563
|
export class Transaction {
|
|
307
564
|
global = {};
|
|
308
565
|
inputs = []; // use getInput()
|
|
@@ -368,7 +625,11 @@ export class Transaction {
|
|
|
368
625
|
tx.global.fallbackLocktime = lockTime;
|
|
369
626
|
return tx;
|
|
370
627
|
}
|
|
371
|
-
|
|
628
|
+
// Prefer `global.version` when present so cross-version combiners can serialize at the highest
|
|
629
|
+
// required PSBT version without mutating the frozen transaction options object.
|
|
630
|
+
toPSBT(PSBTVersion = this.global.version || this.opts.PSBTVersion) {
|
|
631
|
+
if (PSBTVersion !== undefined)
|
|
632
|
+
anumber(PSBTVersion, 'PSBTVersion');
|
|
372
633
|
if (PSBTVersion !== 0 && PSBTVersion !== 2)
|
|
373
634
|
throw new Error(`Wrong PSBT version=${PSBTVersion}`);
|
|
374
635
|
// if (PSBTVersion === 0 && this.inputs.length === 0) {
|
|
@@ -376,7 +637,10 @@ export class Transaction {
|
|
|
376
637
|
// 'PSBT version=0 export for transaction without inputs disabled, please use version=2. Please check `toPSBT` method for explanation.'
|
|
377
638
|
// );
|
|
378
639
|
// }
|
|
379
|
-
const inputs = this.inputs.map((i) =>
|
|
640
|
+
const inputs = this.inputs.map((i) =>
|
|
641
|
+
// For PSBTv0 the prevout txid/index live in global.unsignedTx rather than the input map, so
|
|
642
|
+
// validate the full transaction input before version filtering drops those fields.
|
|
643
|
+
psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, validateInput(i)));
|
|
380
644
|
for (const inp of inputs) {
|
|
381
645
|
// Don't serialize empty fields
|
|
382
646
|
if (inp.partialSig && !inp.partialSig.length)
|
|
@@ -398,16 +662,28 @@ export class Transaction {
|
|
|
398
662
|
global.unsignedTx = RawOldTx.decode(RawOldTx.encode({
|
|
399
663
|
version: this.version,
|
|
400
664
|
lockTime: this.lockTime,
|
|
401
|
-
inputs: this.inputs
|
|
665
|
+
inputs: this.inputs
|
|
666
|
+
.map((i) => inputBeforeSign(i))
|
|
667
|
+
.map((i) => ({
|
|
402
668
|
...i,
|
|
403
669
|
finalScriptSig: P.EMPTY,
|
|
404
670
|
})),
|
|
405
|
-
outputs: this.outputs.map(outputBeforeSign),
|
|
671
|
+
outputs: this.outputs.map((o) => outputBeforeSign(o)),
|
|
406
672
|
}));
|
|
407
673
|
delete global.fallbackLocktime;
|
|
408
674
|
delete global.txVersion;
|
|
675
|
+
// PSBTv0 carries the unsigned transaction as one blob, so the PSBTv2 framing fields must be
|
|
676
|
+
// removed here. Keeping `global.version` would make validation treat this rebuilt v0 map as
|
|
677
|
+
// PSBTv2 and reject the required `unsignedTx` field.
|
|
678
|
+
delete global.inputCount;
|
|
679
|
+
delete global.outputCount;
|
|
680
|
+
delete global.version;
|
|
409
681
|
}
|
|
410
682
|
else {
|
|
683
|
+
// Cross-version merges and v0->v2 re-exports can still carry the PSBTv0 unsignedTx blob in
|
|
684
|
+
// `this.global`, but PSBTv2 serializes the transaction through split global/input/output
|
|
685
|
+
// fields instead, so drop the stale v0-only field before PSBTv2 validation/encoding.
|
|
686
|
+
delete global.unsignedTx;
|
|
411
687
|
global.version = PSBTVersion;
|
|
412
688
|
global.txVersion = this.version;
|
|
413
689
|
global.inputCount = this.inputs.length;
|
|
@@ -421,11 +697,10 @@ export class Transaction {
|
|
|
421
697
|
if (!outputs.length)
|
|
422
698
|
outputs.push({});
|
|
423
699
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
});
|
|
700
|
+
const raw = { global, inputs, outputs };
|
|
701
|
+
return PSBTVersion === 0
|
|
702
|
+
? psbt.RawPSBTV0.encode(raw)
|
|
703
|
+
: psbt.RawPSBTV2.encode(raw);
|
|
429
704
|
}
|
|
430
705
|
// BIP370 lockTime (https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#determining-lock-time)
|
|
431
706
|
get lockTime() {
|
|
@@ -527,30 +802,38 @@ export class Transaction {
|
|
|
527
802
|
}
|
|
528
803
|
// Info utils
|
|
529
804
|
get hasWitnesses() {
|
|
530
|
-
let out = false;
|
|
531
805
|
for (const i of this.inputs)
|
|
532
806
|
if (i.finalScriptWitness && i.finalScriptWitness.length)
|
|
533
|
-
|
|
534
|
-
return
|
|
807
|
+
return true;
|
|
808
|
+
return false;
|
|
535
809
|
}
|
|
536
810
|
// https://en.bitcoin.it/wiki/Weight_units
|
|
537
811
|
get weight() {
|
|
538
812
|
if (!this.isFinal)
|
|
539
813
|
throw new Error('Transaction is not finalized');
|
|
814
|
+
// Serialized length of VarBytes(data) without allocating the encoded copy
|
|
815
|
+
const varLen = (dataLen) => CompactSizeLen.encode(dataLen).length + dataLen;
|
|
816
|
+
const hasWitnesses = this.hasWitnesses;
|
|
540
817
|
let out = 32;
|
|
541
818
|
// Outputs
|
|
542
819
|
const outputs = this.outputs.map(outputBeforeSign);
|
|
543
820
|
out += 4 * CompactSizeLen.encode(this.outputs.length).length;
|
|
544
821
|
for (const o of outputs)
|
|
545
|
-
out += 32 + 4 *
|
|
822
|
+
out += 32 + 4 * varLen(o.script.length);
|
|
546
823
|
// Inputs
|
|
547
|
-
if (
|
|
824
|
+
if (hasWitnesses)
|
|
548
825
|
out += 2;
|
|
549
826
|
out += 4 * CompactSizeLen.encode(this.inputs.length).length;
|
|
550
827
|
for (const i of this.inputs) {
|
|
551
|
-
out += 160 + 4 *
|
|
552
|
-
|
|
553
|
-
|
|
828
|
+
out += 160 + 4 * varLen((i.finalScriptSig || P.EMPTY).length);
|
|
829
|
+
// Once segwit serialization is active, every input contributes one witness vector, including
|
|
830
|
+
// legacy inputs whose empty vector still encodes as a single zero-item-count byte.
|
|
831
|
+
if (hasWitnesses) {
|
|
832
|
+
const witness = i.finalScriptWitness || [];
|
|
833
|
+
out += CompactSizeLen.encode(witness.length).length;
|
|
834
|
+
for (const w of witness)
|
|
835
|
+
out += varLen(w.length);
|
|
836
|
+
}
|
|
554
837
|
}
|
|
555
838
|
return out;
|
|
556
839
|
}
|
|
@@ -584,7 +867,8 @@ export class Transaction {
|
|
|
584
867
|
}
|
|
585
868
|
// Input stuff
|
|
586
869
|
checkInputIdx(idx) {
|
|
587
|
-
|
|
870
|
+
anumber(idx, 'idx');
|
|
871
|
+
if (idx >= this.inputs.length)
|
|
588
872
|
throw new Error(`Wrong input index=${idx}`);
|
|
589
873
|
}
|
|
590
874
|
getInput(idx) {
|
|
@@ -596,9 +880,12 @@ export class Transaction {
|
|
|
596
880
|
}
|
|
597
881
|
// Modification
|
|
598
882
|
addInput(input, _ignoreSignStatus = false) {
|
|
883
|
+
validateObject(input, {}, {}, 'input');
|
|
599
884
|
if (!_ignoreSignStatus && !this.signStatus().addInput)
|
|
600
885
|
throw new Error('Tx has signed inputs, cannot add new one');
|
|
601
|
-
|
|
886
|
+
// normalizeInput preserves nested caller-owned byte arrays, so detach them here before the
|
|
887
|
+
// new input becomes transaction state and later caller mutation can rewrite it by aliasing.
|
|
888
|
+
this.inputs.push(cloneDeep(normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck)));
|
|
602
889
|
return this.inputs.length - 1;
|
|
603
890
|
}
|
|
604
891
|
updateInput(idx, input, _ignoreSignStatus = false) {
|
|
@@ -609,11 +896,14 @@ export class Transaction {
|
|
|
609
896
|
if (!status.addInput || status.inputs.includes(idx))
|
|
610
897
|
allowedFields = psbt.PSBTInputUnsignedKeys;
|
|
611
898
|
}
|
|
612
|
-
|
|
899
|
+
// normalizeInput preserves nested caller-owned byte arrays, so detach the merged result here
|
|
900
|
+
// before the updated input becomes transaction state and later caller mutation can rewrite it.
|
|
901
|
+
this.inputs[idx] = cloneDeep(normalizeInput(input, this.inputs[idx], allowedFields, this.opts.disableScriptCheck, this.opts.allowUnknown));
|
|
613
902
|
}
|
|
614
903
|
// Output stuff
|
|
615
904
|
checkOutputIdx(idx) {
|
|
616
|
-
|
|
905
|
+
anumber(idx, 'idx');
|
|
906
|
+
if (idx >= this.outputs.length)
|
|
617
907
|
throw new Error(`Wrong output index=${idx}`);
|
|
618
908
|
}
|
|
619
909
|
getOutput(idx) {
|
|
@@ -630,11 +920,11 @@ export class Transaction {
|
|
|
630
920
|
return this.outputs.length;
|
|
631
921
|
}
|
|
632
922
|
normalizeOutput(o, cur, allowedFields) {
|
|
923
|
+
validateObject(o, {}, {}, 'o');
|
|
633
924
|
let { amount, script } = o;
|
|
634
925
|
if (amount === undefined)
|
|
635
926
|
amount = cur?.amount;
|
|
636
|
-
|
|
637
|
-
throw new Error(`Wrong amount type, should be of type bigint in sats, but got ${amount} of type ${typeof amount}`);
|
|
927
|
+
amount = abigint(amount, 'o.amount');
|
|
638
928
|
if (typeof script === 'string')
|
|
639
929
|
script = hex.decode(script);
|
|
640
930
|
if (script === undefined)
|
|
@@ -656,7 +946,9 @@ export class Transaction {
|
|
|
656
946
|
addOutput(o, _ignoreSignStatus = false) {
|
|
657
947
|
if (!_ignoreSignStatus && !this.signStatus().addOutput)
|
|
658
948
|
throw new Error('Tx has signed outputs, cannot add new one');
|
|
659
|
-
|
|
949
|
+
// normalizeOutput preserves nested caller-owned script bytes, so detach them here before the
|
|
950
|
+
// new output becomes transaction state and later caller mutation can rewrite it by aliasing.
|
|
951
|
+
this.outputs.push(cloneDeep(this.normalizeOutput(o)));
|
|
660
952
|
return this.outputs.length - 1;
|
|
661
953
|
}
|
|
662
954
|
updateOutput(idx, output, _ignoreSignStatus = false) {
|
|
@@ -667,14 +959,21 @@ export class Transaction {
|
|
|
667
959
|
if (!status.addOutput || status.outputs.includes(idx))
|
|
668
960
|
allowedFields = psbt.PSBTOutputUnsignedKeys;
|
|
669
961
|
}
|
|
670
|
-
|
|
962
|
+
// updateOutput replaces stored state with normalizeOutput(...) directly, so detach the result
|
|
963
|
+
// before storing it or later caller mutation of `output.script` will rewrite transaction state.
|
|
964
|
+
this.outputs[idx] = cloneDeep(this.normalizeOutput(output, this.outputs[idx], allowedFields));
|
|
671
965
|
}
|
|
672
966
|
addOutputAddress(address, amount, network = NETWORK) {
|
|
673
|
-
return this.addOutput({
|
|
967
|
+
return this.addOutput({
|
|
968
|
+
// Address.decode() only returns recognized descriptors here, but its wrapped output type
|
|
969
|
+
// still carries `undefined` for coder parity, so narrow before feeding OutScript.encode().
|
|
970
|
+
script: OutScript.encode(Address(network).decode(address)),
|
|
971
|
+
amount,
|
|
972
|
+
});
|
|
674
973
|
}
|
|
675
974
|
// Utils
|
|
676
975
|
get fee() {
|
|
677
|
-
let res =
|
|
976
|
+
let res = _0n;
|
|
678
977
|
for (const i of this.inputs) {
|
|
679
978
|
const prevOut = getPrevOut(i);
|
|
680
979
|
if (!prevOut)
|
|
@@ -695,8 +994,8 @@ export class Transaction {
|
|
|
695
994
|
if (idx < 0 || !Number.isSafeInteger(idx))
|
|
696
995
|
throw new Error(`Invalid input idx=${idx}`);
|
|
697
996
|
if ((isSingle && idx >= this.outputs.length) || idx >= this.inputs.length)
|
|
698
|
-
return P.U256BE.encode(
|
|
699
|
-
prevOutScript =
|
|
997
|
+
return P.U256BE.encode(_1n);
|
|
998
|
+
prevOutScript = stripCodeSeparator(prevOutScript);
|
|
700
999
|
let inputs = this.inputs
|
|
701
1000
|
.map(inputBeforeSign)
|
|
702
1001
|
.map((input, inputIdx) => ({
|
|
@@ -715,7 +1014,10 @@ export class Transaction {
|
|
|
715
1014
|
if (isNone)
|
|
716
1015
|
outputs = [];
|
|
717
1016
|
else if (isSingle) {
|
|
718
|
-
outputs = outputs
|
|
1017
|
+
outputs = outputs
|
|
1018
|
+
.slice(0, idx)
|
|
1019
|
+
.fill(EMPTY_OUTPUT)
|
|
1020
|
+
.concat([outputs[idx]]);
|
|
719
1021
|
}
|
|
720
1022
|
const tmpTx = RawTx.encode({
|
|
721
1023
|
lockTime: this.lockTime,
|
|
@@ -727,6 +1029,11 @@ export class Transaction {
|
|
|
727
1029
|
return u.sha256x2(tmpTx, P.I32LE.encode(hashType));
|
|
728
1030
|
}
|
|
729
1031
|
preimageWitnessV0(idx, prevOutScript, hashType, amount) {
|
|
1032
|
+
// BIP143 serializes txTo.vin[nIn].prevout and txTo.vin[nIn].nSequence, so reject an invalid
|
|
1033
|
+
// nIn explicitly instead of leaking a later undefined-input TypeError from inputs[idx].
|
|
1034
|
+
anumber(idx, 'idx');
|
|
1035
|
+
if (idx >= this.inputs.length)
|
|
1036
|
+
throw new Error(`Invalid input idx=${idx}`);
|
|
730
1037
|
const { isAny, isNone, isSingle } = unpackSighash(hashType);
|
|
731
1038
|
let inputHash = EMPTY32;
|
|
732
1039
|
let sequenceHash = EMPTY32;
|
|
@@ -746,9 +1053,17 @@ export class Transaction {
|
|
|
746
1053
|
return u.sha256x2(P.I32LE.encode(this.version), inputHash, sequenceHash, P.bytes(32, true).encode(input.txid), P.U32LE.encode(input.index), VarBytes.encode(prevOutScript), P.U64LE.encode(amount), P.U32LE.encode(input.sequence), outputHash, P.U32LE.encode(this.lockTime), P.U32LE.encode(hashType));
|
|
747
1054
|
}
|
|
748
1055
|
preimageWitnessV1(idx, prevOutScript, hashType, amount, codeSeparator = -1, leafScript, leafVer = 0xc0, annex) {
|
|
749
|
-
|
|
1056
|
+
// BIP341 SigMsg commits either to input_index or to the selected input's outpoint/amount/script/
|
|
1057
|
+
// sequence under ANYONECANPAY, so reject an invalid index explicitly instead of hashing a
|
|
1058
|
+
// nonexistent input or leaking a later integer-encoding RangeError for negative idx.
|
|
1059
|
+
anumber(idx, 'idx');
|
|
1060
|
+
if (idx >= this.inputs.length)
|
|
1061
|
+
throw new Error(`Invalid input idx=${idx}`);
|
|
1062
|
+
u.aarray(amount, 'amount');
|
|
1063
|
+
u.aarray(prevOutScript, 'prevOutScript');
|
|
1064
|
+
if (this.inputs.length !== amount.length)
|
|
750
1065
|
throw new Error(`Invalid amounts array=${amount}`);
|
|
751
|
-
if (
|
|
1066
|
+
if (this.inputs.length !== prevOutScript.length)
|
|
752
1067
|
throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
|
|
753
1068
|
const out = [
|
|
754
1069
|
P.U8.encode(0),
|
|
@@ -789,32 +1104,91 @@ export class Transaction {
|
|
|
789
1104
|
}
|
|
790
1105
|
// Signer can be privateKey OR instance of bip32 HD stuff
|
|
791
1106
|
signIdx(privateKey, idx, allowedSighash, _auxRand) {
|
|
1107
|
+
if (!isBytes(privateKey)) {
|
|
1108
|
+
// HDKey is a structural external instance, so plain-object validation would
|
|
1109
|
+
// reject valid signers.
|
|
1110
|
+
if (!privateKey ||
|
|
1111
|
+
typeof privateKey !== 'object' ||
|
|
1112
|
+
typeof privateKey.deriveChild !== 'function')
|
|
1113
|
+
throw new TypeError('"privateKey" expected Uint8Array or HDKey, got type=' + typeof privateKey);
|
|
1114
|
+
}
|
|
792
1115
|
this.checkInputIdx(idx);
|
|
793
1116
|
const input = this.inputs[idx];
|
|
794
1117
|
const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
|
|
795
|
-
|
|
1118
|
+
const canSign = (privateKey) => {
|
|
1119
|
+
if (inputType.txType === 'taproot') {
|
|
1120
|
+
const pubKey = u.pubSchnorr(privateKey);
|
|
1121
|
+
if (input.tapInternalKey && equalBytes(pubKey, input.tapInternalKey))
|
|
1122
|
+
return true;
|
|
1123
|
+
if (!input.tapLeafScript)
|
|
1124
|
+
return false;
|
|
1125
|
+
for (const [_, leaf] of input.tapLeafScript) {
|
|
1126
|
+
for (const op of Script.decode(leaf.subarray(0, -1))) {
|
|
1127
|
+
if (isBytes(op) && equalBytes(op, pubKey))
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
const pubKey = u.pubECDSA(privateKey);
|
|
1134
|
+
const pubKeyHash = u.hash160(pubKey);
|
|
1135
|
+
for (const op of Script.decode(inputType.lastScript)) {
|
|
1136
|
+
if (isBytes(op) && (equalBytes(op, pubKey) || equalBytes(op, pubKeyHash)))
|
|
1137
|
+
return true;
|
|
1138
|
+
}
|
|
1139
|
+
return false;
|
|
1140
|
+
};
|
|
1141
|
+
// Expected invariant: HD signing should use bip32Derivation for legacy/segwit inputs,
|
|
1142
|
+
// tapBip32Derivation for taproot inputs, and preserve caller sighash/auxRand constraints.
|
|
796
1143
|
if (!isBytes(privateKey)) {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
s =
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1144
|
+
const root = privateKey;
|
|
1145
|
+
const deriveSigners = (label, rows, pubKey) => {
|
|
1146
|
+
if (!rows || !rows.length)
|
|
1147
|
+
throw new Error(`${label}: empty`);
|
|
1148
|
+
const signers = rows
|
|
1149
|
+
.filter((row) => row.fingerprint == root.fingerprint)
|
|
1150
|
+
.map((row) => {
|
|
1151
|
+
let s = root;
|
|
1152
|
+
for (const i of row.path)
|
|
1153
|
+
s = s.deriveChild(i);
|
|
1154
|
+
if (!equalBytes(pubKey(s), row.pubKey))
|
|
1155
|
+
throw new Error(`${label}: wrong pubKey`);
|
|
1156
|
+
if (!s.privateKey)
|
|
1157
|
+
throw new Error(`${label}: no privateKey`);
|
|
1158
|
+
return s;
|
|
1159
|
+
});
|
|
1160
|
+
if (!signers.length)
|
|
1161
|
+
throw new Error(`${label}: no items with fingerprint=${root.fingerprint}`);
|
|
1162
|
+
return signers;
|
|
1163
|
+
};
|
|
1164
|
+
const signers = inputType.txType === 'taproot'
|
|
1165
|
+
? // BIP371 PSBT_IN_TAP_BIP32_DERIVATION stores x-only pubkeys plus `der`, so taproot HD
|
|
1166
|
+
// signing must derive against that map instead of legacy bip32Derivation.
|
|
1167
|
+
deriveSigners('tapBip32Derivation', input.tapBip32Derivation?.map(([pubKey, { der }]) => ({
|
|
1168
|
+
pubKey,
|
|
1169
|
+
fingerprint: der.fingerprint,
|
|
1170
|
+
path: der.path,
|
|
1171
|
+
})), (s) => s.publicKey.slice(1))
|
|
1172
|
+
: deriveSigners('bip32Derivation', input.bip32Derivation?.map(([pubKey, der]) => ({
|
|
1173
|
+
pubKey,
|
|
1174
|
+
fingerprint: der.fingerprint,
|
|
1175
|
+
path: der.path,
|
|
1176
|
+
})), (s) => s.publicKey);
|
|
813
1177
|
let signed = false;
|
|
814
|
-
for (const s of signers)
|
|
815
|
-
|
|
1178
|
+
for (const s of signers) {
|
|
1179
|
+
// PSBT may legitimately carry multiple same-fingerprint derivation entries (multisig or
|
|
1180
|
+
// taproot internal/script-path keys). Skip unrelated derived children instead of aborting
|
|
1181
|
+
// the whole HD signing attempt on the first non-applicable candidate.
|
|
1182
|
+
if (!canSign(s.privateKey))
|
|
1183
|
+
continue;
|
|
1184
|
+
if (this.signIdx(s.privateKey, idx, allowedSighash, _auxRand))
|
|
816
1185
|
signed = true;
|
|
817
|
-
|
|
1186
|
+
}
|
|
1187
|
+
if (signed)
|
|
1188
|
+
return true;
|
|
1189
|
+
if (inputType.txType === 'taproot')
|
|
1190
|
+
throw new Error('No taproot scripts signed');
|
|
1191
|
+
throw new Error(`Input script doesn't have pubKey: ${inputType.lastScript}`);
|
|
818
1192
|
}
|
|
819
1193
|
// Sighash checks
|
|
820
1194
|
// Just for compat with bitcoinjs-lib, so users won't face unexpected behaviour.
|
|
@@ -935,7 +1309,7 @@ export class Transaction {
|
|
|
935
1309
|
}
|
|
936
1310
|
finalizeIdx(idx) {
|
|
937
1311
|
this.checkInputIdx(idx);
|
|
938
|
-
if (this.fee <
|
|
1312
|
+
if (this.fee < _0n)
|
|
939
1313
|
throw new Error('Outputs spends more than inputs amount');
|
|
940
1314
|
const input = this.inputs[idx];
|
|
941
1315
|
const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
|
|
@@ -1013,7 +1387,7 @@ export class Transaction {
|
|
|
1013
1387
|
if (!finalized)
|
|
1014
1388
|
continue;
|
|
1015
1389
|
input.finalScriptWitness = finalized.concat(psbt.TaprootControlBlock.encode(cb));
|
|
1016
|
-
input.finalScriptSig
|
|
1390
|
+
delete input.finalScriptSig;
|
|
1017
1391
|
cleanFinalInput(input);
|
|
1018
1392
|
return;
|
|
1019
1393
|
}
|
|
@@ -1031,7 +1405,8 @@ export class Transaction {
|
|
|
1031
1405
|
}
|
|
1032
1406
|
else
|
|
1033
1407
|
throw new Error('finalize/taproot: unknown input');
|
|
1034
|
-
input
|
|
1408
|
+
// BIP174 Input Finalizer: if scriptSig is empty for an input, 0x07 remains unset.
|
|
1409
|
+
delete input.finalScriptSig;
|
|
1035
1410
|
cleanFinalInput(input);
|
|
1036
1411
|
return;
|
|
1037
1412
|
}
|
|
@@ -1114,12 +1489,17 @@ export class Transaction {
|
|
|
1114
1489
|
throw new Error('Transaction has unfinalized inputs');
|
|
1115
1490
|
if (!this.outputs.length)
|
|
1116
1491
|
throw new Error('Transaction has no outputs');
|
|
1117
|
-
if (this.fee <
|
|
1492
|
+
if (this.fee < _0n)
|
|
1118
1493
|
throw new Error('Outputs spends more than inputs amount');
|
|
1119
1494
|
return this.toBytes(true, true);
|
|
1120
1495
|
}
|
|
1121
1496
|
combine(other) {
|
|
1122
|
-
|
|
1497
|
+
if (!(other instanceof Transaction))
|
|
1498
|
+
throw new TypeError('"other" expected Transaction, got type=' + typeof other);
|
|
1499
|
+
// BIP174 combiners merge same-transaction PSBTs across versions and emit the highest required
|
|
1500
|
+
// version, so PSBTVersion mismatches are normalized below instead of treated as conflicts.
|
|
1501
|
+
const PSBTVersion = Math.max(this.opts.PSBTVersion || 0, other.opts.PSBTVersion || 0);
|
|
1502
|
+
for (const k of ['version', 'lockTime']) {
|
|
1123
1503
|
if (this.opts[k] !== other.opts[k]) {
|
|
1124
1504
|
throw new Error(`Transaction/combine: different ${k} this=${this.opts[k]} other=${other.opts[k]}`);
|
|
1125
1505
|
}
|
|
@@ -1129,13 +1509,13 @@ export class Transaction {
|
|
|
1129
1509
|
throw new Error(`Transaction/combine: different ${k} length this=${this[k].length} other=${other[k].length}`);
|
|
1130
1510
|
}
|
|
1131
1511
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
: P.EMPTY;
|
|
1136
|
-
if (!equalBytes(thisUnsigned, otherUnsigned))
|
|
1512
|
+
// Same-transaction checks must compare the normalized unsigned tx bytes here: PSBTv0 stores
|
|
1513
|
+
// `global.unsignedTx`, while PSBTv2 reconstructs the same transaction from split fields.
|
|
1514
|
+
if (!equalBytes(this.unsignedTx, other.unsignedTx))
|
|
1137
1515
|
throw new Error(`Transaction/combine: different unsigned tx`);
|
|
1138
1516
|
this.global = psbt.mergeKeyMap(psbt.PSBTGlobal, this.global, other.global, undefined, this.opts.allowUnknown);
|
|
1517
|
+
if (PSBTVersion)
|
|
1518
|
+
this.global.version = PSBTVersion;
|
|
1139
1519
|
for (let i = 0; i < this.inputs.length; i++)
|
|
1140
1520
|
this.updateInput(i, other.inputs[i], true);
|
|
1141
1521
|
for (let i = 0; i < this.outputs.length; i++)
|
|
@@ -1144,9 +1524,22 @@ export class Transaction {
|
|
|
1144
1524
|
}
|
|
1145
1525
|
clone() {
|
|
1146
1526
|
// deepClone probably faster, but this enforces that encoding is valid
|
|
1147
|
-
return Transaction.fromPSBT(this.toPSBT(
|
|
1527
|
+
return Transaction.fromPSBT(this.toPSBT(), this.opts);
|
|
1148
1528
|
}
|
|
1149
1529
|
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Merges multiple PSBT blobs into one.
|
|
1532
|
+
* @param psbts - PSBT byte arrays to combine
|
|
1533
|
+
* @returns Combined PSBT bytes.
|
|
1534
|
+
* @throws If the PSBT list is empty or the partial transactions cannot be combined. {@link Error}
|
|
1535
|
+
* @example
|
|
1536
|
+
* Merge separate partially signed PSBTs that share the same unsigned transaction.
|
|
1537
|
+
* ```ts
|
|
1538
|
+
* import { PSBTCombine, Transaction } from '@scure/btc-signer/transaction.js';
|
|
1539
|
+
* const psbt = new Transaction().toPSBT();
|
|
1540
|
+
* PSBTCombine([psbt, psbt]);
|
|
1541
|
+
* ```
|
|
1542
|
+
*/
|
|
1150
1543
|
export function PSBTCombine(psbts) {
|
|
1151
1544
|
if (!psbts || !Array.isArray(psbts) || !psbts.length)
|
|
1152
1545
|
throw new Error('PSBTCombine: wrong PSBT list');
|
|
@@ -1157,13 +1550,31 @@ export function PSBTCombine(psbts) {
|
|
|
1157
1550
|
}
|
|
1158
1551
|
// Copy-pasted from bip32 derive, maybe do something like 'bip32.parsePath'?
|
|
1159
1552
|
const HARDENED_OFFSET = 0x80000000;
|
|
1553
|
+
/**
|
|
1554
|
+
* Parses a BIP32 path string into child indices.
|
|
1555
|
+
* @param path - derivation path such as `m/0'/1`
|
|
1556
|
+
* @returns Array of encoded child indices.
|
|
1557
|
+
* @throws If the derivation path syntax or child indices are invalid. {@link Error}
|
|
1558
|
+
* @example
|
|
1559
|
+
* Parse a BIP32 derivation path into hardened and unhardened indices.
|
|
1560
|
+
* ```ts
|
|
1561
|
+
* bip32Path("m/0'/1");
|
|
1562
|
+
* ```
|
|
1563
|
+
*/
|
|
1160
1564
|
export function bip32Path(path) {
|
|
1161
1565
|
const out = [];
|
|
1566
|
+
// PSBT key-origin records only carry raw child indices, so this convenience
|
|
1567
|
+
// parser normalizes textual BIP32 roots into the same integer path array and
|
|
1568
|
+
// uses apostrophe suffixes for hardening.
|
|
1162
1569
|
if (!/^[mM]'?/.test(path))
|
|
1163
1570
|
throw new Error('Path must start with "m" or "M"');
|
|
1164
1571
|
if (/^[mM]'?$/.test(path))
|
|
1165
1572
|
return out;
|
|
1166
1573
|
const parts = path.replace(/^[mM]'?\//, '').split('/');
|
|
1574
|
+
// BIP32 Serialization format `* 1 byte: depth`: extended keys cap depth at
|
|
1575
|
+
// 255, so deeper text paths cannot roundtrip.
|
|
1576
|
+
if (parts.length > 255)
|
|
1577
|
+
throw new Error('Path depth exceeds 255');
|
|
1167
1578
|
for (const c of parts) {
|
|
1168
1579
|
const m = /^(\d+)('?)$/.exec(c);
|
|
1169
1580
|
if (!m || m.length !== 3)
|
|
@@ -1178,4 +1589,3 @@ export function bip32Path(path) {
|
|
|
1178
1589
|
}
|
|
1179
1590
|
return out;
|
|
1180
1591
|
}
|
|
1181
|
-
//# sourceMappingURL=transaction.js.map
|