@scure/btc-signer 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +189 -39
- package/index.d.ts +15 -5
- package/index.d.ts.map +1 -1
- package/index.js +16 -6
- package/index.js.map +1 -1
- package/musig2.d.ts +202 -64
- package/musig2.d.ts.map +1 -1
- package/musig2.js +324 -87
- package/musig2.js.map +1 -1
- package/p2p.d.ts +17 -7
- package/p2p.d.ts.map +1 -1
- package/p2p.js +40 -4
- package/p2p.js.map +1 -1
- package/package.json +14 -10
- package/payment.d.ts +407 -38
- package/payment.d.ts.map +1 -1
- package/payment.js +504 -57
- package/payment.js.map +1 -1
- package/psbt.d.ts +2958 -559
- package/psbt.d.ts.map +1 -1
- package/psbt.js +462 -118
- package/psbt.js.map +1 -1
- package/script.d.ts +311 -132
- package/script.d.ts.map +1 -1
- package/script.js +246 -35
- package/script.js.map +1 -1
- package/src/index.ts +34 -11
- package/src/musig2.ts +387 -145
- package/src/p2p.ts +54 -18
- package/src/payment.ts +823 -226
- package/src/psbt.ts +633 -228
- package/src/script.ts +353 -117
- package/src/transaction.ts +593 -169
- package/src/utils.ts +322 -43
- package/src/utxo.ts +154 -51
- package/transaction.d.ts +242 -31
- package/transaction.d.ts.map +1 -1
- package/transaction.js +460 -100
- package/transaction.js.map +1 -1
- package/utils.d.ts +266 -24
- package/utils.d.ts.map +1 -1
- package/utils.js +278 -27
- package/utils.js.map +1 -1
- package/utxo.d.ts +438 -75
- package/utxo.d.ts.map +1 -1
- package/utxo.js +123 -36
- package/utxo.js.map +1 -1
package/transaction.js
CHANGED
|
@@ -2,22 +2,110 @@ import { hex } from '@scure/base';
|
|
|
2
2
|
import * as P from 'micro-packed';
|
|
3
3
|
import { Address, OutScript, checkScript, tapLeafHash } from "./payment.js";
|
|
4
4
|
import * as psbt from "./psbt.js";
|
|
5
|
-
import { CompactSizeLen, RawOldTx, RawOutput, RawTx, RawWitness, Script, VarBytes, } from "./script.js";
|
|
5
|
+
import { CompactSizeLen, OP, RawOldTx, RawInput, RawOutput, RawTx, RawWitness, Script, scriptPushLen, VarBytes, } from "./script.js";
|
|
6
6
|
import * as u from "./utils.js";
|
|
7
|
-
import { NETWORK, concatBytes, equalBytes, isBytes } from "./utils.js";
|
|
8
|
-
const EMPTY32 = new Uint8Array(32);
|
|
7
|
+
import { NETWORK, concatBytes, equalBytes, isBytes, } from "./utils.js";
|
|
8
|
+
const EMPTY32 = /* @__PURE__ */ new Uint8Array(32);
|
|
9
9
|
const EMPTY_OUTPUT = {
|
|
10
10
|
amount: 0xffffffffffffffffn,
|
|
11
11
|
script: P.EMPTY,
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Converts transaction weight units into virtual bytes.
|
|
15
|
+
* @param weight - transaction weight
|
|
16
|
+
* @returns Rounded-up virtual size.
|
|
17
|
+
* @example
|
|
18
|
+
* Convert transaction weight units into virtual bytes.
|
|
19
|
+
* ```ts
|
|
20
|
+
* toVsize(4);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
13
23
|
export const toVsize = (weight) => Math.ceil(weight / 4);
|
|
24
|
+
const stripCodeSeparator = (script) => {
|
|
25
|
+
// Reuse Script's raw pushdata-length parser here. Legacy sighash must remove
|
|
26
|
+
// only actual OP_CODESEPARATOR opcodes while preserving every other original
|
|
27
|
+
// byte, because semantic decode/re-encode would change the signed digest.
|
|
28
|
+
let start = 0;
|
|
29
|
+
const out = [];
|
|
30
|
+
for (let i = 0; i < script.length;) {
|
|
31
|
+
const pos = i;
|
|
32
|
+
const op = script[i++];
|
|
33
|
+
if (op === OP.CODESEPARATOR) {
|
|
34
|
+
if (start < pos)
|
|
35
|
+
out.push(script.subarray(start, pos));
|
|
36
|
+
start = i;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const len = scriptPushLen(op, (bytes) => {
|
|
40
|
+
if (i + bytes > script.length)
|
|
41
|
+
throw new Error('Unexpected end of script');
|
|
42
|
+
let len = 0;
|
|
43
|
+
for (let j = 0; j < bytes; j++)
|
|
44
|
+
len |= script[i + j] << (8 * j);
|
|
45
|
+
i += bytes;
|
|
46
|
+
return len;
|
|
47
|
+
});
|
|
48
|
+
if (len === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
i += len;
|
|
51
|
+
if (i > script.length)
|
|
52
|
+
throw new Error('Unexpected end of script');
|
|
53
|
+
}
|
|
54
|
+
if (start === 0)
|
|
55
|
+
return script;
|
|
56
|
+
if (start < script.length)
|
|
57
|
+
out.push(script.subarray(start));
|
|
58
|
+
return (out.length ? concatBytes(...out) : P.EMPTY);
|
|
59
|
+
};
|
|
60
|
+
/** Decimal precision used for BTC string formatting. */
|
|
14
61
|
export const PRECISION = 8;
|
|
62
|
+
/** Default transaction version used for newly created transactions. */
|
|
15
63
|
export const DEFAULT_VERSION = 2;
|
|
64
|
+
/** Default transaction locktime. */
|
|
16
65
|
export const DEFAULT_LOCKTIME = 0;
|
|
66
|
+
/** Default input sequence number.
|
|
67
|
+
* Final (`0xffffffff`): matches the PSBT omission default and disables nLockTime/CLTV semantics
|
|
68
|
+
* unless callers choose a lower sequence explicitly (for example `0xfffffffe` with lockTime).
|
|
69
|
+
*/
|
|
17
70
|
export const DEFAULT_SEQUENCE = 4294967295;
|
|
18
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Decimal coder for BTC-denominated strings.
|
|
73
|
+
* This is a fixed-precision BTC-string to satoshi-bigint helper, not a validator
|
|
74
|
+
* for transaction/PSBT output amounts. Signed values are intentional here, so
|
|
75
|
+
* callers can reuse the helper for display/history-style deltas as well as
|
|
76
|
+
* unsigned transfer amounts. It keeps the BTC scale at 8 fractional digits and
|
|
77
|
+
* rejects over-precise inputs instead of rounding.
|
|
78
|
+
* @example
|
|
79
|
+
* Convert between satoshi bigint values and BTC-denominated decimal strings.
|
|
80
|
+
* ```ts
|
|
81
|
+
* Decimal.encode(1n);
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export const Decimal = /* @__PURE__ */ (() => Object.freeze(P.coders.decimal(PRECISION)))();
|
|
19
85
|
// Same as value || def, but doesn't overwrites zero ('0', 0, 0n, etc)
|
|
86
|
+
/**
|
|
87
|
+
* Returns a fallback only when the value is `undefined`.
|
|
88
|
+
* @param value - optional value
|
|
89
|
+
* @param def - fallback value
|
|
90
|
+
* @returns `value` when defined, otherwise `def`.
|
|
91
|
+
* @example
|
|
92
|
+
* Keep zero-like values but replace `undefined` with a fallback.
|
|
93
|
+
* ```ts
|
|
94
|
+
* def(undefined, 1);
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
20
97
|
export const def = (value, def) => (value === undefined ? def : value);
|
|
98
|
+
/**
|
|
99
|
+
* Deep-clones plain transaction data structures.
|
|
100
|
+
* @param obj - value to clone
|
|
101
|
+
* @returns Deep copy of the input value.
|
|
102
|
+
* @throws If the value contains an unsupported runtime type. {@link Error}
|
|
103
|
+
* @example
|
|
104
|
+
* Clone plain transaction data structures before mutating them.
|
|
105
|
+
* ```ts
|
|
106
|
+
* cloneDeep({ a: [new Uint8Array([1])] });
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
21
109
|
export function cloneDeep(obj) {
|
|
22
110
|
if (Array.isArray(obj))
|
|
23
111
|
return obj.map((i) => cloneDeep(i));
|
|
@@ -34,30 +122,48 @@ export function cloneDeep(obj) {
|
|
|
34
122
|
else if (typeof obj === 'object') {
|
|
35
123
|
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, cloneDeep(v)]));
|
|
36
124
|
}
|
|
37
|
-
|
|
125
|
+
// Don't interpolate unsupported values here: Symbol string coercion would
|
|
126
|
+
// throw before cloneDeep can surface its own stable helper error.
|
|
127
|
+
throw new Error(`cloneDeep: unknown type=${typeof obj}`);
|
|
38
128
|
}
|
|
39
129
|
/**
|
|
40
130
|
* Internal, exported only for backwards-compat. Use `SigHash` instead.
|
|
41
|
-
* @deprecated
|
|
131
|
+
* @deprecated Use {@link SigHash} instead.
|
|
132
|
+
* @example
|
|
133
|
+
* Combine the legacy bit flags when interoperating with older code.
|
|
134
|
+
* ```ts
|
|
135
|
+
* SignatureHash.ALL | SignatureHash.ANYONECANPAY;
|
|
136
|
+
* ```
|
|
42
137
|
*/
|
|
43
|
-
export const SignatureHash = {
|
|
138
|
+
export const SignatureHash = /* @__PURE__ */ (() => Object.freeze({
|
|
44
139
|
DEFAULT: 0,
|
|
45
140
|
ALL: 1,
|
|
46
141
|
NONE: 2,
|
|
47
142
|
SINGLE: 3,
|
|
48
143
|
ANYONECANPAY: 0x80,
|
|
49
|
-
};
|
|
50
|
-
|
|
144
|
+
}))();
|
|
145
|
+
/**
|
|
146
|
+
* Common signature hash flag combinations.
|
|
147
|
+
* @example
|
|
148
|
+
* Use the predefined signature-hash combinations exported by the library.
|
|
149
|
+
* ```ts
|
|
150
|
+
* SigHash.SINGLE_ANYONECANPAY;
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export const SigHash = /* @__PURE__ */ (() => Object.freeze({
|
|
51
154
|
DEFAULT: SignatureHash.DEFAULT,
|
|
52
155
|
ALL: SignatureHash.ALL,
|
|
53
156
|
NONE: SignatureHash.NONE,
|
|
54
157
|
SINGLE: SignatureHash.SINGLE,
|
|
55
|
-
|
|
158
|
+
// BIP341 only permits 0x00, 0x01, 0x02, 0x03, 0x81, 0x82, and 0x83 for taproot, so
|
|
159
|
+
// the mechanical `DEFAULT | ANYONECANPAY` combination (0x80) is invalid and not exported.
|
|
160
|
+
// DEFAULT_ANYONECANPAY: SignatureHash.DEFAULT | SignatureHash.ANYONECANPAY,
|
|
56
161
|
ALL_ANYONECANPAY: SignatureHash.ALL | SignatureHash.ANYONECANPAY,
|
|
57
162
|
NONE_ANYONECANPAY: SignatureHash.NONE | SignatureHash.ANYONECANPAY,
|
|
58
163
|
SINGLE_ANYONECANPAY: SignatureHash.SINGLE | SignatureHash.ANYONECANPAY,
|
|
59
|
-
};
|
|
60
|
-
|
|
164
|
+
}))();
|
|
165
|
+
/** Reverse lookup table for signature hash flag names. */
|
|
166
|
+
export const SigHashNames = /* @__PURE__ */ (() => Object.freeze(u.reverseObject(SigHash)))();
|
|
61
167
|
function getTaprootKeys(privKey, pubKey, internalKey, merkleRoot = P.EMPTY) {
|
|
62
168
|
if (equalBytes(internalKey, pubKey)) {
|
|
63
169
|
privKey = u.taprootTweakPrivKey(privKey, merkleRoot);
|
|
@@ -72,25 +178,49 @@ function outputBeforeSign(i) {
|
|
|
72
178
|
return { script: i.script, amount: i.amount };
|
|
73
179
|
}
|
|
74
180
|
// Force check index/txid/sequence
|
|
181
|
+
/**
|
|
182
|
+
* Normalizes a PSBT input into the fields needed for signing.
|
|
183
|
+
* @param i - PSBT input to validate
|
|
184
|
+
* @returns Input fields required for signing.
|
|
185
|
+
* @throws If the input is missing `txid` or `index`. {@link Error}
|
|
186
|
+
* @example
|
|
187
|
+
* Fill in defaults for the fields the signer expects to see.
|
|
188
|
+
* ```ts
|
|
189
|
+
* import { hex } from '@scure/base';
|
|
190
|
+
* import { inputBeforeSign } from '@scure/btc-signer/transaction.js';
|
|
191
|
+
* inputBeforeSign({
|
|
192
|
+
* txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
|
|
193
|
+
* index: 0,
|
|
194
|
+
* });
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
75
197
|
export function inputBeforeSign(i) {
|
|
76
198
|
if (i.txid === undefined || i.index === undefined)
|
|
77
199
|
throw new Error('Transaction/input: txid and index required');
|
|
78
|
-
|
|
200
|
+
const res = {
|
|
79
201
|
txid: i.txid,
|
|
80
202
|
index: i.index,
|
|
81
203
|
sequence: def(i.sequence, DEFAULT_SEQUENCE),
|
|
82
204
|
finalScriptSig: def(i.finalScriptSig, P.EMPTY),
|
|
83
205
|
};
|
|
206
|
+
// This helper is the public "normalize for signing" boundary, so reuse RawInput's existing
|
|
207
|
+
// wire-shape checks here instead of letting malformed runtime field types fail much later.
|
|
208
|
+
RawInput.encode(res);
|
|
209
|
+
return res;
|
|
84
210
|
}
|
|
85
211
|
function cleanFinalInput(i) {
|
|
86
|
-
|
|
212
|
+
const _i = i;
|
|
213
|
+
// BIP174 finalizers clear non-final input metadata after constructing final scripts/witnesses.
|
|
214
|
+
// That intentionally drops sighashType here, so post-finalize mutation becomes conservative
|
|
215
|
+
// until callers explicitly reopen the input by removing finalScriptSig/finalScriptWitness.
|
|
216
|
+
for (const _k in _i) {
|
|
87
217
|
const k = _k;
|
|
88
218
|
if (!psbt.PSBTInputFinalKeys.includes(k))
|
|
89
|
-
delete
|
|
219
|
+
delete _i[k];
|
|
90
220
|
}
|
|
91
221
|
}
|
|
92
222
|
// (TxHash, Idx)
|
|
93
|
-
const TxHashIdx = P.struct({ txid: P.bytes(32, true), index: P.U32LE });
|
|
223
|
+
const TxHashIdx = /* @__PURE__ */ (() => P.struct({ txid: P.bytes(32, true), index: P.U32LE }))();
|
|
94
224
|
function validateSigHash(s) {
|
|
95
225
|
if (typeof s !== 'number' || typeof SigHashNames[s] !== 'string')
|
|
96
226
|
throw new Error(`Invalid SigHash=${s}`);
|
|
@@ -114,10 +244,12 @@ function validateOpts(opts) {
|
|
|
114
244
|
lockTime: def(opts.lockTime, 0),
|
|
115
245
|
PSBTVersion: def(opts.PSBTVersion, 0),
|
|
116
246
|
};
|
|
247
|
+
// Normalize deprecated aliases on the owned copy so they still affect tx.opts without rewriting the
|
|
248
|
+
// caller-owned options object passed to the constructor.
|
|
117
249
|
if (typeof _opts.allowUnknowInput !== 'undefined')
|
|
118
|
-
|
|
250
|
+
_opts.allowUnknownInputs = _opts.allowUnknowInput;
|
|
119
251
|
if (typeof _opts.allowUnknowOutput !== 'undefined')
|
|
120
|
-
|
|
252
|
+
_opts.allowUnknownOutputs = _opts.allowUnknowOutput;
|
|
121
253
|
if (typeof _opts.lockTime !== 'number')
|
|
122
254
|
throw new Error('Transaction lock time should be number');
|
|
123
255
|
P.U32LE.encode(_opts.lockTime); // Additional range checks that lockTime
|
|
@@ -162,17 +294,19 @@ function validateOpts(opts) {
|
|
|
162
294
|
}
|
|
163
295
|
// NOTE: we cannot do this inside PSBTInput coder, because there is no index/txid at this point!
|
|
164
296
|
function validateInput(i) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
297
|
+
const _i = i;
|
|
298
|
+
if (_i.nonWitnessUtxo && _i.index !== undefined) {
|
|
299
|
+
const last = _i.nonWitnessUtxo.outputs.length - 1;
|
|
300
|
+
if (_i.index > last)
|
|
301
|
+
throw new Error(`validateInput: index(${_i.index}) not in nonWitnessUtxo`);
|
|
302
|
+
const prevOut = _i.nonWitnessUtxo.outputs[_i.index];
|
|
303
|
+
if (_i.witnessUtxo &&
|
|
304
|
+
(!equalBytes(_i.witnessUtxo.script, prevOut.script) ||
|
|
305
|
+
_i.witnessUtxo.amount !== prevOut.amount))
|
|
172
306
|
throw new Error('validateInput: witnessUtxo different from nonWitnessUtxo');
|
|
173
|
-
if (
|
|
174
|
-
const outputs =
|
|
175
|
-
if (outputs.length - 1 <
|
|
307
|
+
if (_i.txid) {
|
|
308
|
+
const outputs = _i.nonWitnessUtxo.outputs;
|
|
309
|
+
if (outputs.length - 1 < _i.index)
|
|
176
310
|
throw new Error('nonWitnessUtxo: incorect output index');
|
|
177
311
|
// At this point, we are using previous tx output to create new input.
|
|
178
312
|
// Script safety checks are unnecessary:
|
|
@@ -182,33 +316,80 @@ function validateInput(i) {
|
|
|
182
316
|
// in case user wants to use wrong input by mistake
|
|
183
317
|
// - Worst case: tx will be rejected by nodes. Still better than disallowing user
|
|
184
318
|
// to spend real input, no matter how broken it looks
|
|
185
|
-
const tx = Transaction.fromRaw(RawTx.encode(
|
|
319
|
+
const tx = Transaction.fromRaw(RawTx.encode(_i.nonWitnessUtxo), {
|
|
186
320
|
allowUnknownOutputs: true,
|
|
187
321
|
disableScriptCheck: true,
|
|
188
322
|
allowUnknownInputs: true,
|
|
189
323
|
});
|
|
190
|
-
const txid = hex.encode(
|
|
191
|
-
//
|
|
192
|
-
|
|
324
|
+
const txid = hex.encode(_i.txid);
|
|
325
|
+
// BIP174 requires the provided nonWitnessUtxo to hash to the prevout txid even when the
|
|
326
|
+
// previous transaction is otherwise non-final; finality does not make its serialized txid optional.
|
|
327
|
+
// Keep the historical TransactionInput.txid convention here: internal txid bytes match
|
|
328
|
+
// `Transaction.id` (display-order hex), while raw-tx / PSBT boundary coders are responsible
|
|
329
|
+
// for any byte-order conversions required by their wire formats.
|
|
330
|
+
if (tx.id !== txid)
|
|
193
331
|
throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
|
|
194
332
|
}
|
|
195
333
|
}
|
|
196
|
-
return
|
|
334
|
+
return _i;
|
|
197
335
|
}
|
|
198
336
|
// Normalizes input
|
|
337
|
+
/**
|
|
338
|
+
* Extracts the previous output referenced by an input.
|
|
339
|
+
* @param input - PSBT input with previous output data
|
|
340
|
+
* @returns Previous output information.
|
|
341
|
+
* @throws If the input does not contain usable previous-output information. {@link Error}
|
|
342
|
+
* @example
|
|
343
|
+
* Read the previous output from either `witnessUtxo` or `nonWitnessUtxo`.
|
|
344
|
+
* ```ts
|
|
345
|
+
* getPrevOut({ witnessUtxo: { amount: 1n, script: new Uint8Array([0x51]) } });
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
199
348
|
export function getPrevOut(input) {
|
|
200
|
-
|
|
201
|
-
|
|
349
|
+
const _input = input;
|
|
350
|
+
if (_input.nonWitnessUtxo) {
|
|
351
|
+
if (_input.index === undefined)
|
|
202
352
|
throw new Error('Unknown input index');
|
|
203
|
-
|
|
353
|
+
// BIP174 `PSBT_IN_NON_WITNESS_UTXO` is the full spent transaction, so the
|
|
354
|
+
// input outpoint index must name an existing output instead of leaking a
|
|
355
|
+
// synthetic `undefined` prevout into later signing / estimation callers.
|
|
356
|
+
if (!Number.isSafeInteger(_input.index) ||
|
|
357
|
+
_input.index < 0 ||
|
|
358
|
+
_input.index >= _input.nonWitnessUtxo.outputs.length)
|
|
359
|
+
throw new Error(`Wrong input index=${_input.index}`);
|
|
360
|
+
return _input.nonWitnessUtxo.outputs[_input.index];
|
|
204
361
|
}
|
|
205
|
-
else if (
|
|
206
|
-
return
|
|
362
|
+
else if (_input.witnessUtxo)
|
|
363
|
+
return _input.witnessUtxo;
|
|
207
364
|
else
|
|
208
365
|
throw new Error('Cannot find previous output info');
|
|
209
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Normalizes a transaction input update into canonical PSBT form.
|
|
369
|
+
* @param i - input update to normalize
|
|
370
|
+
* @param cur - existing input value to merge with
|
|
371
|
+
* @param allowedFields - fields that may still change on signed inputs
|
|
372
|
+
* @param disableScriptCheck - whether to skip redeem/witness script sanity checks
|
|
373
|
+
* @param allowUnknown - whether to keep unknown PSBT fields
|
|
374
|
+
* @returns Normalized PSBT input.
|
|
375
|
+
* @example
|
|
376
|
+
* Accept hex txids from callers in the same display-order form used by `Transaction.id`, then
|
|
377
|
+
* normalize them into the repo's internal `TransactionInput` shape.
|
|
378
|
+
* ```ts
|
|
379
|
+
* import { hex } from '@scure/base';
|
|
380
|
+
* import { normalizeInput } from '@scure/btc-signer/transaction.js';
|
|
381
|
+
* normalizeInput({
|
|
382
|
+
* txid: '0000000000000000000000000000000000000000000000000000000000000001',
|
|
383
|
+
* index: 0,
|
|
384
|
+
* witnessUtxo: { amount: 1n, script: new Uint8Array([0x51]) },
|
|
385
|
+
* });
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
210
388
|
export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false, allowUnknown = false) {
|
|
211
|
-
|
|
389
|
+
const _i = i;
|
|
390
|
+
const _cur = cur;
|
|
391
|
+
const _allowedFields = allowedFields;
|
|
392
|
+
let { nonWitnessUtxo, txid } = _i;
|
|
212
393
|
// String support for common fields. We usually prefer Uint8Array to avoid errors
|
|
213
394
|
// like hex looking string accidentally passed, however, in case of nonWitnessUtxo
|
|
214
395
|
// it is better to expect string, since constructing this complex object will be
|
|
@@ -217,21 +398,23 @@ export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false
|
|
|
217
398
|
nonWitnessUtxo = hex.decode(nonWitnessUtxo);
|
|
218
399
|
if (isBytes(nonWitnessUtxo))
|
|
219
400
|
nonWitnessUtxo = RawTx.decode(nonWitnessUtxo);
|
|
220
|
-
if (!('nonWitnessUtxo' in
|
|
221
|
-
nonWitnessUtxo =
|
|
401
|
+
if (!('nonWitnessUtxo' in _i) && nonWitnessUtxo === undefined)
|
|
402
|
+
nonWitnessUtxo = _cur?.nonWitnessUtxo;
|
|
222
403
|
if (typeof txid === 'string')
|
|
223
404
|
txid = hex.decode(txid);
|
|
224
405
|
// TODO: if we have nonWitnessUtxo, we can extract txId from here
|
|
225
406
|
if (txid === undefined)
|
|
226
|
-
txid =
|
|
227
|
-
let res = { ...
|
|
228
|
-
if (!('nonWitnessUtxo' in
|
|
407
|
+
txid = _cur?.txid;
|
|
408
|
+
let res = { ..._cur, ..._i, nonWitnessUtxo, txid };
|
|
409
|
+
if (!('nonWitnessUtxo' in _i) && res.nonWitnessUtxo === undefined)
|
|
229
410
|
delete res.nonWitnessUtxo;
|
|
230
411
|
if (res.sequence === undefined)
|
|
231
412
|
res.sequence = DEFAULT_SEQUENCE;
|
|
232
413
|
if (res.tapMerkleRoot === null)
|
|
233
414
|
delete res.tapMerkleRoot;
|
|
234
|
-
res = psbt.mergeKeyMap(psbt.PSBTInput, res,
|
|
415
|
+
res = psbt.mergeKeyMap(psbt.PSBTInput, res, _cur, _allowedFields, allowUnknown);
|
|
416
|
+
// Public PSBT coder surface is wrapped with TArg/TRet for TS compatibility; normalizeInput keeps
|
|
417
|
+
// the repo's historical raw internal shape and casts only at the validation boundary here.
|
|
235
418
|
psbt.PSBTInputCoder.encode(res); // Validates that everything is correct at this point
|
|
236
419
|
let prevOut;
|
|
237
420
|
if (res.nonWitnessUtxo && res.index !== undefined)
|
|
@@ -242,15 +425,42 @@ export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false
|
|
|
242
425
|
checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
|
|
243
426
|
return res;
|
|
244
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Determines how an input should be signed and finalized.
|
|
430
|
+
* Wrapper consistency is expected to be validated earlier by {@link normalizeInput}
|
|
431
|
+
* and {@link checkScript}; this helper classifies already-normalized inputs and is
|
|
432
|
+
* not a standalone redeemScript/witnessScript correctness gate for raw caller input.
|
|
433
|
+
* @param input - PSBT input to inspect
|
|
434
|
+
* @param allowLegacyWitnessUtxo - whether legacy inputs may rely on witness UTXO data only
|
|
435
|
+
* @returns Input classification including transaction type and sighash defaults.
|
|
436
|
+
* @throws If a documented runtime validation or state check fails. {@link Error}
|
|
437
|
+
* @example
|
|
438
|
+
* Detect how the signer should treat a SegWit input from its previous output script.
|
|
439
|
+
* ```ts
|
|
440
|
+
* import { hex } from '@scure/base';
|
|
441
|
+
* import { p2wpkh } from '@scure/btc-signer/payment.js';
|
|
442
|
+
* import { getInputType } from '@scure/btc-signer/transaction.js';
|
|
443
|
+
* import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
444
|
+
* getInputType({
|
|
445
|
+
* witnessUtxo: {
|
|
446
|
+
* amount: 1n,
|
|
447
|
+
* script: p2wpkh(pubECDSA(randomPrivateKeyBytes())).script,
|
|
448
|
+
* },
|
|
449
|
+
* });
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
245
452
|
export function getInputType(input, allowLegacyWitnessUtxo = false) {
|
|
453
|
+
const _input = input;
|
|
246
454
|
let txType = 'legacy';
|
|
247
455
|
let defaultSighash = SignatureHash.ALL;
|
|
248
|
-
const prevOut = getPrevOut(
|
|
456
|
+
const prevOut = getPrevOut(_input);
|
|
249
457
|
const first = OutScript.decode(prevOut.script);
|
|
250
458
|
let type = first.type;
|
|
251
459
|
let cur = first;
|
|
252
460
|
const stack = [first];
|
|
253
461
|
if (first.type === 'tr') {
|
|
462
|
+
// Expected invariant: taproot inputs use PSBT_IN_TAP_* metadata only;
|
|
463
|
+
// legacy redeemScript/witnessScript fields belong to P2SH/P2WSH paths.
|
|
254
464
|
defaultSighash = SignatureHash.DEFAULT;
|
|
255
465
|
return {
|
|
256
466
|
txType: 'taproot',
|
|
@@ -258,16 +468,16 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
|
|
|
258
468
|
last: first,
|
|
259
469
|
lastScript: prevOut.script,
|
|
260
470
|
defaultSighash,
|
|
261
|
-
sighash:
|
|
471
|
+
sighash: _input.sighashType || defaultSighash,
|
|
262
472
|
};
|
|
263
473
|
}
|
|
264
474
|
else {
|
|
265
475
|
if (first.type === 'wpkh' || first.type === 'wsh')
|
|
266
476
|
txType = 'segwit';
|
|
267
477
|
if (first.type === 'sh') {
|
|
268
|
-
if (!
|
|
478
|
+
if (!_input.redeemScript)
|
|
269
479
|
throw new Error('inputType: sh without redeemScript');
|
|
270
|
-
let child = OutScript.decode(
|
|
480
|
+
let child = OutScript.decode(_input.redeemScript);
|
|
271
481
|
if (child.type === 'wpkh' || child.type === 'wsh')
|
|
272
482
|
txType = 'segwit';
|
|
273
483
|
stack.push(child);
|
|
@@ -276,9 +486,9 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
|
|
|
276
486
|
}
|
|
277
487
|
// wsh can be inside sh
|
|
278
488
|
if (cur.type === 'wsh') {
|
|
279
|
-
if (!
|
|
489
|
+
if (!_input.witnessScript)
|
|
280
490
|
throw new Error('inputType: wsh without witnessScript');
|
|
281
|
-
let child = OutScript.decode(
|
|
491
|
+
let child = OutScript.decode(_input.witnessScript);
|
|
282
492
|
if (child.type === 'wsh')
|
|
283
493
|
txType = 'segwit';
|
|
284
494
|
stack.push(child);
|
|
@@ -295,14 +505,35 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
|
|
|
295
505
|
last,
|
|
296
506
|
lastScript,
|
|
297
507
|
defaultSighash,
|
|
298
|
-
sighash:
|
|
508
|
+
sighash: _input.sighashType || defaultSighash,
|
|
299
509
|
};
|
|
300
|
-
if (txType === 'legacy' && !allowLegacyWitnessUtxo && !
|
|
510
|
+
if (txType === 'legacy' && !allowLegacyWitnessUtxo && !_input.nonWitnessUtxo) {
|
|
301
511
|
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
512
|
}
|
|
303
513
|
return res;
|
|
304
514
|
}
|
|
305
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Mutable Bitcoin transaction and PSBT helper.
|
|
518
|
+
* @param opts - Transaction construction and PSBT serialization options. See {@link TxOpts}.
|
|
519
|
+
* @example
|
|
520
|
+
* Create a transaction, add one spend, and export it as PSBT.
|
|
521
|
+
* ```ts
|
|
522
|
+
* import { hex } from '@scure/base';
|
|
523
|
+
* import { p2wpkh } from '@scure/btc-signer/payment.js';
|
|
524
|
+
* import { Transaction } from '@scure/btc-signer/transaction.js';
|
|
525
|
+
* import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
526
|
+
* const spend = p2wpkh(pubECDSA(randomPrivateKeyBytes()));
|
|
527
|
+
* const tx = new Transaction();
|
|
528
|
+
* tx.addInput({
|
|
529
|
+
* txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
|
|
530
|
+
* index: 0,
|
|
531
|
+
* witnessUtxo: { amount: 2n, script: spend.script },
|
|
532
|
+
* });
|
|
533
|
+
* tx.addOutput({ script: spend.script, amount: 1n });
|
|
534
|
+
* tx.toPSBT();
|
|
535
|
+
* ```
|
|
536
|
+
*/
|
|
306
537
|
export class Transaction {
|
|
307
538
|
global = {};
|
|
308
539
|
inputs = []; // use getInput()
|
|
@@ -368,7 +599,9 @@ export class Transaction {
|
|
|
368
599
|
tx.global.fallbackLocktime = lockTime;
|
|
369
600
|
return tx;
|
|
370
601
|
}
|
|
371
|
-
|
|
602
|
+
// Prefer `global.version` when present so cross-version combiners can serialize at the highest
|
|
603
|
+
// required PSBT version without mutating the frozen transaction options object.
|
|
604
|
+
toPSBT(PSBTVersion = this.global.version || this.opts.PSBTVersion) {
|
|
372
605
|
if (PSBTVersion !== 0 && PSBTVersion !== 2)
|
|
373
606
|
throw new Error(`Wrong PSBT version=${PSBTVersion}`);
|
|
374
607
|
// if (PSBTVersion === 0 && this.inputs.length === 0) {
|
|
@@ -376,7 +609,10 @@ export class Transaction {
|
|
|
376
609
|
// 'PSBT version=0 export for transaction without inputs disabled, please use version=2. Please check `toPSBT` method for explanation.'
|
|
377
610
|
// );
|
|
378
611
|
// }
|
|
379
|
-
const inputs = this.inputs.map((i) =>
|
|
612
|
+
const inputs = this.inputs.map((i) =>
|
|
613
|
+
// For PSBTv0 the prevout txid/index live in global.unsignedTx rather than the input map, so
|
|
614
|
+
// validate the full transaction input before version filtering drops those fields.
|
|
615
|
+
psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, validateInput(i)));
|
|
380
616
|
for (const inp of inputs) {
|
|
381
617
|
// Don't serialize empty fields
|
|
382
618
|
if (inp.partialSig && !inp.partialSig.length)
|
|
@@ -398,16 +634,28 @@ export class Transaction {
|
|
|
398
634
|
global.unsignedTx = RawOldTx.decode(RawOldTx.encode({
|
|
399
635
|
version: this.version,
|
|
400
636
|
lockTime: this.lockTime,
|
|
401
|
-
inputs: this.inputs
|
|
637
|
+
inputs: this.inputs
|
|
638
|
+
.map((i) => inputBeforeSign(i))
|
|
639
|
+
.map((i) => ({
|
|
402
640
|
...i,
|
|
403
641
|
finalScriptSig: P.EMPTY,
|
|
404
642
|
})),
|
|
405
|
-
outputs: this.outputs.map(outputBeforeSign),
|
|
643
|
+
outputs: this.outputs.map((o) => outputBeforeSign(o)),
|
|
406
644
|
}));
|
|
407
645
|
delete global.fallbackLocktime;
|
|
408
646
|
delete global.txVersion;
|
|
647
|
+
// PSBTv0 carries the unsigned transaction as one blob, so the PSBTv2 framing fields must be
|
|
648
|
+
// removed here. Keeping `global.version` would make validation treat this rebuilt v0 map as
|
|
649
|
+
// PSBTv2 and reject the required `unsignedTx` field.
|
|
650
|
+
delete global.inputCount;
|
|
651
|
+
delete global.outputCount;
|
|
652
|
+
delete global.version;
|
|
409
653
|
}
|
|
410
654
|
else {
|
|
655
|
+
// Cross-version merges and v0->v2 re-exports can still carry the PSBTv0 unsignedTx blob in
|
|
656
|
+
// `this.global`, but PSBTv2 serializes the transaction through split global/input/output
|
|
657
|
+
// fields instead, so drop the stale v0-only field before PSBTv2 validation/encoding.
|
|
658
|
+
delete global.unsignedTx;
|
|
411
659
|
global.version = PSBTVersion;
|
|
412
660
|
global.txVersion = this.version;
|
|
413
661
|
global.inputCount = this.inputs.length;
|
|
@@ -421,11 +669,10 @@ export class Transaction {
|
|
|
421
669
|
if (!outputs.length)
|
|
422
670
|
outputs.push({});
|
|
423
671
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
});
|
|
672
|
+
const raw = { global, inputs, outputs };
|
|
673
|
+
return PSBTVersion === 0
|
|
674
|
+
? psbt.RawPSBTV0.encode(raw)
|
|
675
|
+
: psbt.RawPSBTV2.encode(raw);
|
|
429
676
|
}
|
|
430
677
|
// BIP370 lockTime (https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#determining-lock-time)
|
|
431
678
|
get lockTime() {
|
|
@@ -549,8 +796,10 @@ export class Transaction {
|
|
|
549
796
|
out += 4 * CompactSizeLen.encode(this.inputs.length).length;
|
|
550
797
|
for (const i of this.inputs) {
|
|
551
798
|
out += 160 + 4 * VarBytes.encode(i.finalScriptSig || P.EMPTY).length;
|
|
552
|
-
|
|
553
|
-
|
|
799
|
+
// Once segwit serialization is active, every input contributes one witness vector, including
|
|
800
|
+
// legacy inputs whose empty vector still encodes as a single zero-item-count byte.
|
|
801
|
+
if (this.hasWitnesses)
|
|
802
|
+
out += RawWitness.encode(i.finalScriptWitness || []).length;
|
|
554
803
|
}
|
|
555
804
|
return out;
|
|
556
805
|
}
|
|
@@ -598,7 +847,9 @@ export class Transaction {
|
|
|
598
847
|
addInput(input, _ignoreSignStatus = false) {
|
|
599
848
|
if (!_ignoreSignStatus && !this.signStatus().addInput)
|
|
600
849
|
throw new Error('Tx has signed inputs, cannot add new one');
|
|
601
|
-
|
|
850
|
+
// normalizeInput preserves nested caller-owned byte arrays, so detach them here before the
|
|
851
|
+
// new input becomes transaction state and later caller mutation can rewrite it by aliasing.
|
|
852
|
+
this.inputs.push(cloneDeep(normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck)));
|
|
602
853
|
return this.inputs.length - 1;
|
|
603
854
|
}
|
|
604
855
|
updateInput(idx, input, _ignoreSignStatus = false) {
|
|
@@ -609,7 +860,9 @@ export class Transaction {
|
|
|
609
860
|
if (!status.addInput || status.inputs.includes(idx))
|
|
610
861
|
allowedFields = psbt.PSBTInputUnsignedKeys;
|
|
611
862
|
}
|
|
612
|
-
|
|
863
|
+
// normalizeInput preserves nested caller-owned byte arrays, so detach the merged result here
|
|
864
|
+
// before the updated input becomes transaction state and later caller mutation can rewrite it.
|
|
865
|
+
this.inputs[idx] = cloneDeep(normalizeInput(input, this.inputs[idx], allowedFields, this.opts.disableScriptCheck, this.opts.allowUnknown));
|
|
613
866
|
}
|
|
614
867
|
// Output stuff
|
|
615
868
|
checkOutputIdx(idx) {
|
|
@@ -656,7 +909,9 @@ export class Transaction {
|
|
|
656
909
|
addOutput(o, _ignoreSignStatus = false) {
|
|
657
910
|
if (!_ignoreSignStatus && !this.signStatus().addOutput)
|
|
658
911
|
throw new Error('Tx has signed outputs, cannot add new one');
|
|
659
|
-
|
|
912
|
+
// normalizeOutput preserves nested caller-owned script bytes, so detach them here before the
|
|
913
|
+
// new output becomes transaction state and later caller mutation can rewrite it by aliasing.
|
|
914
|
+
this.outputs.push(cloneDeep(this.normalizeOutput(o)));
|
|
660
915
|
return this.outputs.length - 1;
|
|
661
916
|
}
|
|
662
917
|
updateOutput(idx, output, _ignoreSignStatus = false) {
|
|
@@ -667,10 +922,17 @@ export class Transaction {
|
|
|
667
922
|
if (!status.addOutput || status.outputs.includes(idx))
|
|
668
923
|
allowedFields = psbt.PSBTOutputUnsignedKeys;
|
|
669
924
|
}
|
|
670
|
-
|
|
925
|
+
// updateOutput replaces stored state with normalizeOutput(...) directly, so detach the result
|
|
926
|
+
// before storing it or later caller mutation of `output.script` will rewrite transaction state.
|
|
927
|
+
this.outputs[idx] = cloneDeep(this.normalizeOutput(output, this.outputs[idx], allowedFields));
|
|
671
928
|
}
|
|
672
929
|
addOutputAddress(address, amount, network = NETWORK) {
|
|
673
|
-
return this.addOutput({
|
|
930
|
+
return this.addOutput({
|
|
931
|
+
// Address.decode() only returns recognized descriptors here, but its wrapped output type
|
|
932
|
+
// still carries `undefined` for coder parity, so narrow before feeding OutScript.encode().
|
|
933
|
+
script: OutScript.encode(Address(network).decode(address)),
|
|
934
|
+
amount,
|
|
935
|
+
});
|
|
674
936
|
}
|
|
675
937
|
// Utils
|
|
676
938
|
get fee() {
|
|
@@ -696,7 +958,7 @@ export class Transaction {
|
|
|
696
958
|
throw new Error(`Invalid input idx=${idx}`);
|
|
697
959
|
if ((isSingle && idx >= this.outputs.length) || idx >= this.inputs.length)
|
|
698
960
|
return P.U256BE.encode(1n);
|
|
699
|
-
prevOutScript =
|
|
961
|
+
prevOutScript = stripCodeSeparator(prevOutScript);
|
|
700
962
|
let inputs = this.inputs
|
|
701
963
|
.map(inputBeforeSign)
|
|
702
964
|
.map((input, inputIdx) => ({
|
|
@@ -715,7 +977,10 @@ export class Transaction {
|
|
|
715
977
|
if (isNone)
|
|
716
978
|
outputs = [];
|
|
717
979
|
else if (isSingle) {
|
|
718
|
-
outputs = outputs
|
|
980
|
+
outputs = outputs
|
|
981
|
+
.slice(0, idx)
|
|
982
|
+
.fill(EMPTY_OUTPUT)
|
|
983
|
+
.concat([outputs[idx]]);
|
|
719
984
|
}
|
|
720
985
|
const tmpTx = RawTx.encode({
|
|
721
986
|
lockTime: this.lockTime,
|
|
@@ -727,6 +992,10 @@ export class Transaction {
|
|
|
727
992
|
return u.sha256x2(tmpTx, P.I32LE.encode(hashType));
|
|
728
993
|
}
|
|
729
994
|
preimageWitnessV0(idx, prevOutScript, hashType, amount) {
|
|
995
|
+
// BIP143 serializes txTo.vin[nIn].prevout and txTo.vin[nIn].nSequence, so reject an invalid
|
|
996
|
+
// nIn explicitly instead of leaking a later undefined-input TypeError from inputs[idx].
|
|
997
|
+
if (idx < 0 || !Number.isSafeInteger(idx) || idx >= this.inputs.length)
|
|
998
|
+
throw new Error(`Invalid input idx=${idx}`);
|
|
730
999
|
const { isAny, isNone, isSingle } = unpackSighash(hashType);
|
|
731
1000
|
let inputHash = EMPTY32;
|
|
732
1001
|
let sequenceHash = EMPTY32;
|
|
@@ -750,6 +1019,11 @@ export class Transaction {
|
|
|
750
1019
|
throw new Error(`Invalid amounts array=${amount}`);
|
|
751
1020
|
if (!Array.isArray(prevOutScript) || this.inputs.length !== prevOutScript.length)
|
|
752
1021
|
throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
|
|
1022
|
+
// BIP341 SigMsg commits either to input_index or to the selected input's outpoint/amount/script/
|
|
1023
|
+
// sequence under ANYONECANPAY, so reject an invalid index explicitly instead of hashing a
|
|
1024
|
+
// nonexistent input or leaking a later integer-encoding RangeError for negative idx.
|
|
1025
|
+
if (idx < 0 || !Number.isSafeInteger(idx) || idx >= this.inputs.length)
|
|
1026
|
+
throw new Error(`Invalid input idx=${idx}`);
|
|
753
1027
|
const out = [
|
|
754
1028
|
P.U8.encode(0),
|
|
755
1029
|
P.U8.encode(hashType), // U8 sigHash
|
|
@@ -792,29 +1066,80 @@ export class Transaction {
|
|
|
792
1066
|
this.checkInputIdx(idx);
|
|
793
1067
|
const input = this.inputs[idx];
|
|
794
1068
|
const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
|
|
795
|
-
|
|
1069
|
+
const canSign = (privateKey) => {
|
|
1070
|
+
if (inputType.txType === 'taproot') {
|
|
1071
|
+
const pubKey = u.pubSchnorr(privateKey);
|
|
1072
|
+
if (input.tapInternalKey && equalBytes(pubKey, input.tapInternalKey))
|
|
1073
|
+
return true;
|
|
1074
|
+
if (!input.tapLeafScript)
|
|
1075
|
+
return false;
|
|
1076
|
+
for (const [_, leaf] of input.tapLeafScript) {
|
|
1077
|
+
for (const op of Script.decode(leaf.subarray(0, -1))) {
|
|
1078
|
+
if (isBytes(op) && equalBytes(op, pubKey))
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
const pubKey = u.pubECDSA(privateKey);
|
|
1085
|
+
const pubKeyHash = u.hash160(pubKey);
|
|
1086
|
+
for (const op of Script.decode(inputType.lastScript)) {
|
|
1087
|
+
if (isBytes(op) && (equalBytes(op, pubKey) || equalBytes(op, pubKeyHash)))
|
|
1088
|
+
return true;
|
|
1089
|
+
}
|
|
1090
|
+
return false;
|
|
1091
|
+
};
|
|
1092
|
+
// Expected invariant: HD signing should use bip32Derivation for legacy/segwit inputs,
|
|
1093
|
+
// tapBip32Derivation for taproot inputs, and preserve caller sighash/auxRand constraints.
|
|
796
1094
|
if (!isBytes(privateKey)) {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
s =
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1095
|
+
const root = privateKey;
|
|
1096
|
+
const deriveSigners = (label, rows, pubKey) => {
|
|
1097
|
+
if (!rows || !rows.length)
|
|
1098
|
+
throw new Error(`${label}: empty`);
|
|
1099
|
+
const signers = rows
|
|
1100
|
+
.filter((row) => row.fingerprint == root.fingerprint)
|
|
1101
|
+
.map((row) => {
|
|
1102
|
+
let s = root;
|
|
1103
|
+
for (const i of row.path)
|
|
1104
|
+
s = s.deriveChild(i);
|
|
1105
|
+
if (!equalBytes(pubKey(s), row.pubKey))
|
|
1106
|
+
throw new Error(`${label}: wrong pubKey`);
|
|
1107
|
+
if (!s.privateKey)
|
|
1108
|
+
throw new Error(`${label}: no privateKey`);
|
|
1109
|
+
return s;
|
|
1110
|
+
});
|
|
1111
|
+
if (!signers.length)
|
|
1112
|
+
throw new Error(`${label}: no items with fingerprint=${root.fingerprint}`);
|
|
1113
|
+
return signers;
|
|
1114
|
+
};
|
|
1115
|
+
const signers = inputType.txType === 'taproot'
|
|
1116
|
+
? // BIP371 PSBT_IN_TAP_BIP32_DERIVATION stores x-only pubkeys plus `der`, so taproot HD
|
|
1117
|
+
// signing must derive against that map instead of legacy bip32Derivation.
|
|
1118
|
+
deriveSigners('tapBip32Derivation', input.tapBip32Derivation?.map(([pubKey, { der }]) => ({
|
|
1119
|
+
pubKey,
|
|
1120
|
+
fingerprint: der.fingerprint,
|
|
1121
|
+
path: der.path,
|
|
1122
|
+
})), (s) => s.publicKey.slice(1))
|
|
1123
|
+
: deriveSigners('bip32Derivation', input.bip32Derivation?.map(([pubKey, der]) => ({
|
|
1124
|
+
pubKey,
|
|
1125
|
+
fingerprint: der.fingerprint,
|
|
1126
|
+
path: der.path,
|
|
1127
|
+
})), (s) => s.publicKey);
|
|
813
1128
|
let signed = false;
|
|
814
|
-
for (const s of signers)
|
|
815
|
-
|
|
1129
|
+
for (const s of signers) {
|
|
1130
|
+
// PSBT may legitimately carry multiple same-fingerprint derivation entries (multisig or
|
|
1131
|
+
// taproot internal/script-path keys). Skip unrelated derived children instead of aborting
|
|
1132
|
+
// the whole HD signing attempt on the first non-applicable candidate.
|
|
1133
|
+
if (!canSign(s.privateKey))
|
|
1134
|
+
continue;
|
|
1135
|
+
if (this.signIdx(s.privateKey, idx, allowedSighash, _auxRand))
|
|
816
1136
|
signed = true;
|
|
817
|
-
|
|
1137
|
+
}
|
|
1138
|
+
if (signed)
|
|
1139
|
+
return true;
|
|
1140
|
+
if (inputType.txType === 'taproot')
|
|
1141
|
+
throw new Error('No taproot scripts signed');
|
|
1142
|
+
throw new Error(`Input script doesn't have pubKey: ${inputType.lastScript}`);
|
|
818
1143
|
}
|
|
819
1144
|
// Sighash checks
|
|
820
1145
|
// Just for compat with bitcoinjs-lib, so users won't face unexpected behaviour.
|
|
@@ -1013,7 +1338,7 @@ export class Transaction {
|
|
|
1013
1338
|
if (!finalized)
|
|
1014
1339
|
continue;
|
|
1015
1340
|
input.finalScriptWitness = finalized.concat(psbt.TaprootControlBlock.encode(cb));
|
|
1016
|
-
input.finalScriptSig
|
|
1341
|
+
delete input.finalScriptSig;
|
|
1017
1342
|
cleanFinalInput(input);
|
|
1018
1343
|
return;
|
|
1019
1344
|
}
|
|
@@ -1031,7 +1356,8 @@ export class Transaction {
|
|
|
1031
1356
|
}
|
|
1032
1357
|
else
|
|
1033
1358
|
throw new Error('finalize/taproot: unknown input');
|
|
1034
|
-
input
|
|
1359
|
+
// BIP174 Input Finalizer: if scriptSig is empty for an input, 0x07 remains unset.
|
|
1360
|
+
delete input.finalScriptSig;
|
|
1035
1361
|
cleanFinalInput(input);
|
|
1036
1362
|
return;
|
|
1037
1363
|
}
|
|
@@ -1119,7 +1445,10 @@ export class Transaction {
|
|
|
1119
1445
|
return this.toBytes(true, true);
|
|
1120
1446
|
}
|
|
1121
1447
|
combine(other) {
|
|
1122
|
-
|
|
1448
|
+
// BIP174 combiners merge same-transaction PSBTs across versions and emit the highest required
|
|
1449
|
+
// version, so PSBTVersion mismatches are normalized below instead of treated as conflicts.
|
|
1450
|
+
const PSBTVersion = Math.max(this.opts.PSBTVersion || 0, other.opts.PSBTVersion || 0);
|
|
1451
|
+
for (const k of ['version', 'lockTime']) {
|
|
1123
1452
|
if (this.opts[k] !== other.opts[k]) {
|
|
1124
1453
|
throw new Error(`Transaction/combine: different ${k} this=${this.opts[k]} other=${other.opts[k]}`);
|
|
1125
1454
|
}
|
|
@@ -1129,13 +1458,13 @@ export class Transaction {
|
|
|
1129
1458
|
throw new Error(`Transaction/combine: different ${k} length this=${this[k].length} other=${other[k].length}`);
|
|
1130
1459
|
}
|
|
1131
1460
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
: P.EMPTY;
|
|
1136
|
-
if (!equalBytes(thisUnsigned, otherUnsigned))
|
|
1461
|
+
// Same-transaction checks must compare the normalized unsigned tx bytes here: PSBTv0 stores
|
|
1462
|
+
// `global.unsignedTx`, while PSBTv2 reconstructs the same transaction from split fields.
|
|
1463
|
+
if (!equalBytes(this.unsignedTx, other.unsignedTx))
|
|
1137
1464
|
throw new Error(`Transaction/combine: different unsigned tx`);
|
|
1138
1465
|
this.global = psbt.mergeKeyMap(psbt.PSBTGlobal, this.global, other.global, undefined, this.opts.allowUnknown);
|
|
1466
|
+
if (PSBTVersion)
|
|
1467
|
+
this.global.version = PSBTVersion;
|
|
1139
1468
|
for (let i = 0; i < this.inputs.length; i++)
|
|
1140
1469
|
this.updateInput(i, other.inputs[i], true);
|
|
1141
1470
|
for (let i = 0; i < this.outputs.length; i++)
|
|
@@ -1144,9 +1473,22 @@ export class Transaction {
|
|
|
1144
1473
|
}
|
|
1145
1474
|
clone() {
|
|
1146
1475
|
// deepClone probably faster, but this enforces that encoding is valid
|
|
1147
|
-
return Transaction.fromPSBT(this.toPSBT(
|
|
1476
|
+
return Transaction.fromPSBT(this.toPSBT(), this.opts);
|
|
1148
1477
|
}
|
|
1149
1478
|
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Merges multiple PSBT blobs into one.
|
|
1481
|
+
* @param psbts - PSBT byte arrays to combine
|
|
1482
|
+
* @returns Combined PSBT bytes.
|
|
1483
|
+
* @throws If the PSBT list is empty or the partial transactions cannot be combined. {@link Error}
|
|
1484
|
+
* @example
|
|
1485
|
+
* Merge separate partially signed PSBTs that share the same unsigned transaction.
|
|
1486
|
+
* ```ts
|
|
1487
|
+
* import { PSBTCombine, Transaction } from '@scure/btc-signer/transaction.js';
|
|
1488
|
+
* const psbt = new Transaction().toPSBT();
|
|
1489
|
+
* PSBTCombine([psbt, psbt]);
|
|
1490
|
+
* ```
|
|
1491
|
+
*/
|
|
1150
1492
|
export function PSBTCombine(psbts) {
|
|
1151
1493
|
if (!psbts || !Array.isArray(psbts) || !psbts.length)
|
|
1152
1494
|
throw new Error('PSBTCombine: wrong PSBT list');
|
|
@@ -1157,13 +1499,31 @@ export function PSBTCombine(psbts) {
|
|
|
1157
1499
|
}
|
|
1158
1500
|
// Copy-pasted from bip32 derive, maybe do something like 'bip32.parsePath'?
|
|
1159
1501
|
const HARDENED_OFFSET = 0x80000000;
|
|
1502
|
+
/**
|
|
1503
|
+
* Parses a BIP32 path string into child indices.
|
|
1504
|
+
* @param path - derivation path such as `m/0'/1`
|
|
1505
|
+
* @returns Array of encoded child indices.
|
|
1506
|
+
* @throws If the derivation path syntax or child indices are invalid. {@link Error}
|
|
1507
|
+
* @example
|
|
1508
|
+
* Parse a BIP32 derivation path into hardened and unhardened indices.
|
|
1509
|
+
* ```ts
|
|
1510
|
+
* bip32Path("m/0'/1");
|
|
1511
|
+
* ```
|
|
1512
|
+
*/
|
|
1160
1513
|
export function bip32Path(path) {
|
|
1161
1514
|
const out = [];
|
|
1515
|
+
// PSBT key-origin records only carry raw child indices, so this convenience
|
|
1516
|
+
// parser normalizes textual BIP32 roots into the same integer path array and
|
|
1517
|
+
// uses apostrophe suffixes for hardening.
|
|
1162
1518
|
if (!/^[mM]'?/.test(path))
|
|
1163
1519
|
throw new Error('Path must start with "m" or "M"');
|
|
1164
1520
|
if (/^[mM]'?$/.test(path))
|
|
1165
1521
|
return out;
|
|
1166
1522
|
const parts = path.replace(/^[mM]'?\//, '').split('/');
|
|
1523
|
+
// BIP32 Serialization format `* 1 byte: depth`: extended keys cap depth at
|
|
1524
|
+
// 255, so deeper text paths cannot roundtrip.
|
|
1525
|
+
if (parts.length > 255)
|
|
1526
|
+
throw new Error('Path depth exceeds 255');
|
|
1167
1527
|
for (const c of parts) {
|
|
1168
1528
|
const m = /^(\d+)('?)$/.exec(c);
|
|
1169
1529
|
if (!m || m.length !== 3)
|