@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/src/transaction.ts
CHANGED
|
@@ -1,26 +1,83 @@
|
|
|
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, type CustomScript, OutScript, checkScript, tapLeafHash } from './payment.ts';
|
|
4
5
|
import * as psbt from './psbt.ts';
|
|
5
6
|
import {
|
|
6
7
|
CompactSizeLen,
|
|
8
|
+
OP,
|
|
7
9
|
RawOldTx,
|
|
10
|
+
RawInput,
|
|
8
11
|
RawOutput,
|
|
9
12
|
RawTx,
|
|
10
|
-
RawWitness,
|
|
11
13
|
Script,
|
|
14
|
+
scriptPushLen,
|
|
12
15
|
VarBytes,
|
|
13
16
|
} from './script.ts';
|
|
14
17
|
import * as u from './utils.ts';
|
|
15
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
type Bytes,
|
|
20
|
+
NETWORK,
|
|
21
|
+
abigint,
|
|
22
|
+
concatBytes,
|
|
23
|
+
equalBytes,
|
|
24
|
+
isBytes,
|
|
25
|
+
type TArg,
|
|
26
|
+
type TRet,
|
|
27
|
+
validateObject,
|
|
28
|
+
} from './utils.ts';
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
// Be friendly to bad ECMAScript parsers by not using bigint literals.
|
|
31
|
+
// prettier-ignore
|
|
32
|
+
const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
|
|
33
|
+
const U64_MAX = /* @__PURE__ */ BigInt('0xffffffffffffffff');
|
|
34
|
+
const EMPTY32: Uint8Array = /* @__PURE__ */ new Uint8Array(32);
|
|
18
35
|
const EMPTY_OUTPUT: P.UnwrapCoder<typeof RawOutput> = {
|
|
19
|
-
amount:
|
|
36
|
+
amount: U64_MAX,
|
|
20
37
|
script: P.EMPTY,
|
|
21
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Converts transaction weight units into virtual bytes.
|
|
41
|
+
* @param weight - transaction weight
|
|
42
|
+
* @returns Rounded-up virtual size.
|
|
43
|
+
* @example
|
|
44
|
+
* Convert transaction weight units into virtual bytes.
|
|
45
|
+
* ```ts
|
|
46
|
+
* toVsize(4);
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
22
49
|
export const toVsize = (weight: number): number => Math.ceil(weight / 4);
|
|
23
50
|
|
|
51
|
+
const stripCodeSeparator = (script: TArg<Bytes>): TRet<Bytes> => {
|
|
52
|
+
// Reuse Script's raw pushdata-length parser here. Legacy sighash must remove
|
|
53
|
+
// only actual OP_CODESEPARATOR opcodes while preserving every other original
|
|
54
|
+
// byte, because semantic decode/re-encode would change the signed digest.
|
|
55
|
+
let start = 0;
|
|
56
|
+
const out: Uint8Array[] = [];
|
|
57
|
+
for (let i = 0; i < script.length; ) {
|
|
58
|
+
const pos = i;
|
|
59
|
+
const op = script[i++];
|
|
60
|
+
if (op === OP.CODESEPARATOR) {
|
|
61
|
+
if (start < pos) out.push(script.subarray(start, pos));
|
|
62
|
+
start = i;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const len = scriptPushLen(op, (bytes) => {
|
|
66
|
+
if (i + bytes > script.length) throw new Error('Unexpected end of script');
|
|
67
|
+
let len = 0;
|
|
68
|
+
for (let j = 0; j < bytes; j++) len |= script[i + j] << (8 * j);
|
|
69
|
+
i += bytes;
|
|
70
|
+
return len;
|
|
71
|
+
});
|
|
72
|
+
if (len === undefined) continue;
|
|
73
|
+
i += len;
|
|
74
|
+
if (i > script.length) throw new Error('Unexpected end of script');
|
|
75
|
+
}
|
|
76
|
+
if (start === 0) return script as TRet<Bytes>;
|
|
77
|
+
if (start < script.length) out.push(script.subarray(start));
|
|
78
|
+
return (out.length ? concatBytes(...out) : P.EMPTY) as TRet<Bytes>;
|
|
79
|
+
};
|
|
80
|
+
|
|
24
81
|
// @scure/bip32 interface
|
|
25
82
|
interface HDKey {
|
|
26
83
|
publicKey: Bytes;
|
|
@@ -31,17 +88,61 @@ interface HDKey {
|
|
|
31
88
|
sign(hash: Bytes): Bytes;
|
|
32
89
|
}
|
|
33
90
|
|
|
91
|
+
/** Signing source accepted by transaction signing helpers. */
|
|
34
92
|
export type Signer = Bytes | HDKey;
|
|
35
93
|
|
|
94
|
+
/** Decimal precision used for BTC string formatting. */
|
|
36
95
|
export const PRECISION = 8;
|
|
96
|
+
/** Default transaction version used for newly created transactions. */
|
|
37
97
|
export const DEFAULT_VERSION = 2;
|
|
98
|
+
/** Default transaction locktime. */
|
|
38
99
|
export const DEFAULT_LOCKTIME = 0;
|
|
100
|
+
/** Default input sequence number.
|
|
101
|
+
* Final (`0xffffffff`): matches the PSBT omission default and disables nLockTime/CLTV semantics
|
|
102
|
+
* unless callers choose a lower sequence explicitly (for example `0xfffffffe` with lockTime).
|
|
103
|
+
*/
|
|
39
104
|
export const DEFAULT_SEQUENCE = 4294967295;
|
|
40
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Decimal coder for BTC-denominated strings.
|
|
107
|
+
* This is a fixed-precision BTC-string to satoshi-bigint helper, not a validator
|
|
108
|
+
* for transaction/PSBT output amounts. Signed values are intentional here, so
|
|
109
|
+
* callers can reuse the helper for display/history-style deltas as well as
|
|
110
|
+
* unsigned transfer amounts. It keeps the BTC scale at 8 fractional digits and
|
|
111
|
+
* rejects over-precise inputs instead of rounding.
|
|
112
|
+
* @example
|
|
113
|
+
* Convert between satoshi bigint values and BTC-denominated decimal strings.
|
|
114
|
+
* ```ts
|
|
115
|
+
* Decimal.encode(1n);
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export const Decimal: P.Coder<bigint, string> = /* @__PURE__ */ (() =>
|
|
119
|
+
Object.freeze(P.coders.decimal(PRECISION)))();
|
|
41
120
|
|
|
42
121
|
// Same as value || def, but doesn't overwrites zero ('0', 0, 0n, etc)
|
|
122
|
+
/**
|
|
123
|
+
* Returns a fallback only when the value is `undefined`.
|
|
124
|
+
* @param value - optional value
|
|
125
|
+
* @param def - fallback value
|
|
126
|
+
* @returns `value` when defined, otherwise `def`.
|
|
127
|
+
* @example
|
|
128
|
+
* Keep zero-like values but replace `undefined` with a fallback.
|
|
129
|
+
* ```ts
|
|
130
|
+
* def(undefined, 1);
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
43
133
|
export const def = <T>(value: T | undefined, def: T): T => (value === undefined ? def : value);
|
|
44
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Deep-clones plain transaction data structures.
|
|
137
|
+
* @param obj - value to clone
|
|
138
|
+
* @returns Deep copy of the input value.
|
|
139
|
+
* @throws If the value contains an unsupported runtime type. {@link Error}
|
|
140
|
+
* @example
|
|
141
|
+
* Clone plain transaction data structures before mutating them.
|
|
142
|
+
* ```ts
|
|
143
|
+
* cloneDeep({ a: [new Uint8Array([1])] });
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
45
146
|
export function cloneDeep<T>(obj: T): T {
|
|
46
147
|
if (Array.isArray(obj)) return obj.map((i) => cloneDeep(i)) as unknown as T;
|
|
47
148
|
// slice of nodejs Buffer doesn't copy
|
|
@@ -56,70 +157,110 @@ export function cloneDeep<T>(obj: T): T {
|
|
|
56
157
|
Object.entries(obj).map(([k, v]) => [k, cloneDeep(v)])
|
|
57
158
|
) as unknown as T;
|
|
58
159
|
}
|
|
59
|
-
|
|
160
|
+
// Don't interpolate unsupported values here: Symbol string coercion would
|
|
161
|
+
// throw before cloneDeep can surface its own stable helper error.
|
|
162
|
+
throw new Error(`cloneDeep: unknown type=${typeof obj}`);
|
|
60
163
|
}
|
|
61
164
|
|
|
62
165
|
// Mostly security features, hardened defaults;
|
|
63
166
|
// but you still can parse other people tx with unspendable outputs and stuff if you want
|
|
167
|
+
/** Transaction construction and parsing options. */
|
|
64
168
|
export interface TxOpts {
|
|
169
|
+
/** Transaction version to place into new transactions and imported PSBTs. */
|
|
65
170
|
version?: number;
|
|
171
|
+
/** Global locktime for the transaction. */
|
|
66
172
|
lockTime?: number;
|
|
173
|
+
/** PSBT version to emit when serializing. */
|
|
67
174
|
PSBTVersion?: number;
|
|
68
175
|
// Flags
|
|
69
176
|
// Allow non-standard transaction version
|
|
177
|
+
/** Allow transaction versions outside the standard small set. */
|
|
70
178
|
allowUnknownVersion?: boolean;
|
|
71
179
|
// Allow output scripts to be unknown scripts (probably unspendable)
|
|
72
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Deprecated alias for {@link allowUnknownOutputs}.
|
|
182
|
+
* @deprecated Use `allowUnknownOutputs`.
|
|
183
|
+
*/
|
|
73
184
|
allowUnknowOutput?: boolean;
|
|
185
|
+
/** Allow outputs with scripts this library does not recognize. */
|
|
74
186
|
allowUnknownOutputs?: boolean;
|
|
75
187
|
// Try to sign/finalize unknown input. All bets are off, but there is chance that it will work
|
|
76
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* Deprecated alias for {@link allowUnknownInputs}.
|
|
190
|
+
* @deprecated Use `allowUnknownInputs`.
|
|
191
|
+
*/
|
|
77
192
|
allowUnknowInput?: boolean;
|
|
193
|
+
/** Allow signing and finalizing inputs with unknown script shapes. */
|
|
78
194
|
allowUnknownInputs?: boolean;
|
|
79
195
|
// Check input/output scripts for sanity
|
|
196
|
+
/** Skip redeem-script and witness-script consistency checks. */
|
|
80
197
|
disableScriptCheck?: boolean;
|
|
81
198
|
// There is strange behaviour where tx without outputs encoded with empty output in the end,
|
|
82
199
|
// tx without outputs in BIP174 doesn't have itb
|
|
200
|
+
/** Match the odd empty-output encoding used by `bip174js`. */
|
|
83
201
|
bip174jsCompat?: boolean;
|
|
84
202
|
// If transaction data comes from untrusted source, then it can be modified in such way that will
|
|
85
203
|
// result paying higher mining fee
|
|
204
|
+
/** Permit legacy inputs that only provide witness UTXO data. */
|
|
86
205
|
allowLegacyWitnessUtxo?: boolean;
|
|
87
|
-
|
|
88
|
-
|
|
206
|
+
/** Grind ECDSA signatures until they use a low-R encoding. */
|
|
207
|
+
lowR?: boolean;
|
|
208
|
+
/** UNSAFE: additional custom payment-script codecs and finalizers. */
|
|
209
|
+
customScripts?: CustomScript[];
|
|
89
210
|
// Allow to add additional unknown keys/values to the "unknown" array member
|
|
211
|
+
/** Preserve unknown PSBT key/value pairs instead of stripping them. */
|
|
90
212
|
allowUnknown?: boolean;
|
|
91
213
|
}
|
|
92
214
|
|
|
93
215
|
/**
|
|
94
216
|
* Internal, exported only for backwards-compat. Use `SigHash` instead.
|
|
95
|
-
* @deprecated
|
|
217
|
+
* @deprecated Use {@link SigHash} instead.
|
|
218
|
+
* @example
|
|
219
|
+
* Combine the legacy bit flags when interoperating with older code.
|
|
220
|
+
* ```ts
|
|
221
|
+
* SignatureHash.ALL | SignatureHash.ANYONECANPAY;
|
|
222
|
+
* ```
|
|
96
223
|
*/
|
|
97
|
-
export const SignatureHash =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
224
|
+
export const SignatureHash = /* @__PURE__ */ (() =>
|
|
225
|
+
Object.freeze({
|
|
226
|
+
DEFAULT: 0,
|
|
227
|
+
ALL: 1,
|
|
228
|
+
NONE: 2,
|
|
229
|
+
SINGLE: 3,
|
|
230
|
+
ANYONECANPAY: 0x80,
|
|
231
|
+
} as const))();
|
|
104
232
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
233
|
+
/**
|
|
234
|
+
* Common signature hash flag combinations.
|
|
235
|
+
* @example
|
|
236
|
+
* Use the predefined signature-hash combinations exported by the library.
|
|
237
|
+
* ```ts
|
|
238
|
+
* SigHash.SINGLE_ANYONECANPAY;
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
export const SigHash = /* @__PURE__ */ (() =>
|
|
242
|
+
Object.freeze({
|
|
243
|
+
DEFAULT: SignatureHash.DEFAULT,
|
|
244
|
+
ALL: SignatureHash.ALL,
|
|
245
|
+
NONE: SignatureHash.NONE,
|
|
246
|
+
SINGLE: SignatureHash.SINGLE,
|
|
247
|
+
// BIP341 only permits 0x00, 0x01, 0x02, 0x03, 0x81, 0x82, and 0x83 for taproot, so
|
|
248
|
+
// the mechanical `DEFAULT | ANYONECANPAY` combination (0x80) is invalid and not exported.
|
|
249
|
+
// DEFAULT_ANYONECANPAY: SignatureHash.DEFAULT | SignatureHash.ANYONECANPAY,
|
|
250
|
+
ALL_ANYONECANPAY: SignatureHash.ALL | SignatureHash.ANYONECANPAY,
|
|
251
|
+
NONE_ANYONECANPAY: SignatureHash.NONE | SignatureHash.ANYONECANPAY,
|
|
252
|
+
SINGLE_ANYONECANPAY: SignatureHash.SINGLE | SignatureHash.ANYONECANPAY,
|
|
253
|
+
} as const))();
|
|
254
|
+
/** Reverse lookup table for signature hash flag names. */
|
|
255
|
+
export const SigHashNames = /* @__PURE__ */ (() => Object.freeze(u.reverseObject(SigHash)))();
|
|
256
|
+
/** Signature-hash flag number accepted by signing helpers. */
|
|
116
257
|
export type SigHash = u.ValueOf<typeof SigHash>;
|
|
117
258
|
|
|
118
259
|
function getTaprootKeys(
|
|
119
|
-
privKey: Bytes
|
|
120
|
-
pubKey: Bytes
|
|
121
|
-
internalKey: Bytes
|
|
122
|
-
merkleRoot: Bytes = P.EMPTY
|
|
260
|
+
privKey: TArg<Bytes>,
|
|
261
|
+
pubKey: TArg<Bytes>,
|
|
262
|
+
internalKey: TArg<Bytes>,
|
|
263
|
+
merkleRoot: TArg<Bytes> = P.EMPTY
|
|
123
264
|
) {
|
|
124
265
|
if (equalBytes(internalKey, pubKey)) {
|
|
125
266
|
privKey = u.taprootTweakPrivKey(privKey, merkleRoot);
|
|
@@ -129,40 +270,70 @@ function getTaprootKeys(
|
|
|
129
270
|
}
|
|
130
271
|
|
|
131
272
|
// User facing API with decoders
|
|
273
|
+
/** Minimal transaction input fields required to serialize and sign. */
|
|
132
274
|
export type TransactionInputRequired = {
|
|
275
|
+
/** Previous transaction id being spent. */
|
|
133
276
|
txid: Bytes;
|
|
277
|
+
/** Previous output index inside that transaction. */
|
|
134
278
|
index: number;
|
|
279
|
+
/** Final sequence number that will be serialized for the input. */
|
|
135
280
|
sequence: number;
|
|
281
|
+
/** Final scriptSig bytes that will be serialized for the input. */
|
|
136
282
|
finalScriptSig: Bytes;
|
|
137
283
|
};
|
|
138
284
|
|
|
139
285
|
// Force check amount/script
|
|
140
|
-
function outputBeforeSign(i: psbt.TransactionOutput): psbt.TransactionOutputRequired {
|
|
286
|
+
function outputBeforeSign(i: TArg<psbt.TransactionOutput>): TRet<psbt.TransactionOutputRequired> {
|
|
141
287
|
if (i.script === undefined || i.amount === undefined)
|
|
142
288
|
throw new Error('Transaction/output: script and amount required');
|
|
143
|
-
return { script: i.script, amount: i.amount }
|
|
289
|
+
return { script: i.script, amount: i.amount } as TRet<psbt.TransactionOutputRequired>;
|
|
144
290
|
}
|
|
145
291
|
|
|
146
292
|
// Force check index/txid/sequence
|
|
147
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Normalizes a PSBT input into the fields needed for signing.
|
|
295
|
+
* @param i - PSBT input to validate
|
|
296
|
+
* @returns Input fields required for signing.
|
|
297
|
+
* @throws If the input is missing `txid` or `index`. {@link Error}
|
|
298
|
+
* @example
|
|
299
|
+
* Fill in defaults for the fields the signer expects to see.
|
|
300
|
+
* ```ts
|
|
301
|
+
* import { hex } from '@scure/base';
|
|
302
|
+
* import { inputBeforeSign } from '@scure/btc-signer/transaction.js';
|
|
303
|
+
* inputBeforeSign({
|
|
304
|
+
* txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
|
|
305
|
+
* index: 0,
|
|
306
|
+
* });
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
export function inputBeforeSign(i: TArg<psbt.TransactionInput>): TRet<TransactionInputRequired> {
|
|
310
|
+
validateObject(i as Record<string, any>, {}, {}, 'i');
|
|
148
311
|
if (i.txid === undefined || i.index === undefined)
|
|
149
312
|
throw new Error('Transaction/input: txid and index required');
|
|
150
|
-
|
|
313
|
+
const res = {
|
|
151
314
|
txid: i.txid,
|
|
152
315
|
index: i.index,
|
|
153
316
|
sequence: def(i.sequence, DEFAULT_SEQUENCE),
|
|
154
317
|
finalScriptSig: def(i.finalScriptSig, P.EMPTY),
|
|
155
318
|
};
|
|
319
|
+
// This helper is the public "normalize for signing" boundary, so reuse RawInput's existing
|
|
320
|
+
// wire-shape checks here instead of letting malformed runtime field types fail much later.
|
|
321
|
+
RawInput.encode(res);
|
|
322
|
+
return res as TRet<TransactionInputRequired>;
|
|
156
323
|
}
|
|
157
|
-
function cleanFinalInput(i:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
324
|
+
function cleanFinalInput(i: TArg<PSBTInputs>) {
|
|
325
|
+
const _i = i as PSBTInputs;
|
|
326
|
+
// BIP174 finalizers clear non-final input metadata after constructing final scripts/witnesses.
|
|
327
|
+
// That intentionally drops sighashType here, so post-finalize mutation becomes conservative
|
|
328
|
+
// until callers explicitly reopen the input by removing finalScriptSig/finalScriptWitness.
|
|
329
|
+
for (const _k in _i) {
|
|
330
|
+
const k = _k as keyof PSBTInputs;
|
|
331
|
+
if (!psbt.PSBTInputFinalKeys.includes(k)) delete _i[k];
|
|
161
332
|
}
|
|
162
333
|
}
|
|
163
334
|
|
|
164
335
|
// (TxHash, Idx)
|
|
165
|
-
const TxHashIdx = P.struct({ txid: P.bytes(32, true), index: P.U32LE });
|
|
336
|
+
const TxHashIdx = /* @__PURE__ */ (() => P.struct({ txid: P.bytes(32, true), index: P.U32LE }))();
|
|
166
337
|
|
|
167
338
|
function validateSigHash(s: SigHash) {
|
|
168
339
|
if (typeof s !== 'number' || typeof SigHashNames[s] !== 'string')
|
|
@@ -179,9 +350,8 @@ function unpackSighash(hashType: number) {
|
|
|
179
350
|
};
|
|
180
351
|
}
|
|
181
352
|
|
|
182
|
-
function validateOpts(opts: TxOpts): Readonly<TxOpts
|
|
183
|
-
if (opts !== undefined
|
|
184
|
-
throw new Error(`Wrong object type for transaction options: ${opts}`);
|
|
353
|
+
function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
|
|
354
|
+
if (opts !== undefined) validateObject(opts as Record<string, any>, {}, {}, 'opts');
|
|
185
355
|
|
|
186
356
|
const _opts = {
|
|
187
357
|
...opts,
|
|
@@ -190,10 +360,12 @@ function validateOpts(opts: TxOpts): Readonly<TxOpts> {
|
|
|
190
360
|
lockTime: def(opts.lockTime, 0),
|
|
191
361
|
PSBTVersion: def(opts.PSBTVersion, 0),
|
|
192
362
|
};
|
|
363
|
+
// Normalize deprecated aliases on the owned copy so they still affect tx.opts without rewriting the
|
|
364
|
+
// caller-owned options object passed to the constructor.
|
|
193
365
|
if (typeof _opts.allowUnknowInput !== 'undefined')
|
|
194
|
-
|
|
366
|
+
_opts.allowUnknownInputs = _opts.allowUnknowInput;
|
|
195
367
|
if (typeof _opts.allowUnknowOutput !== 'undefined')
|
|
196
|
-
|
|
368
|
+
_opts.allowUnknownOutputs = _opts.allowUnknowOutput;
|
|
197
369
|
if (typeof _opts.lockTime !== 'number') throw new Error('Transaction lock time should be number');
|
|
198
370
|
P.U32LE.encode(_opts.lockTime); // Additional range checks that lockTime
|
|
199
371
|
// There is no PSBT v1, and any new version will probably have fields which we don't know how to parse, which
|
|
@@ -216,12 +388,15 @@ function validateOpts(opts: TxOpts): Readonly<TxOpts> {
|
|
|
216
388
|
throw new Error(`Transation options wrong type: ${k}=${v} (${typeof v})`);
|
|
217
389
|
}
|
|
218
390
|
// 0 and -1 happens in tests
|
|
391
|
+
// With allowUnknownVersion any numeric version is fine; the ternary was inverted
|
|
392
|
+
// before 2026-07 (audit), which made the option throw for every numeric version.
|
|
219
393
|
if (
|
|
220
394
|
_opts.allowUnknownVersion
|
|
221
|
-
? typeof _opts.version
|
|
395
|
+
? typeof _opts.version !== 'number'
|
|
222
396
|
: ![-1, 0, 1, 2, 3].includes(_opts.version)
|
|
223
397
|
)
|
|
224
398
|
throw new Error(`Unknown version: ${_opts.version}`);
|
|
399
|
+
P.I32LE.encode(_opts.version); // Validate the signed transaction-version wire domain.
|
|
225
400
|
if (_opts.customScripts !== undefined) {
|
|
226
401
|
const cs = _opts.customScripts;
|
|
227
402
|
if (!Array.isArray(cs)) {
|
|
@@ -236,23 +411,26 @@ function validateOpts(opts: TxOpts): Readonly<TxOpts> {
|
|
|
236
411
|
throw new Error(`wrong script=${s} (${typeof s})`);
|
|
237
412
|
}
|
|
238
413
|
}
|
|
239
|
-
return Object.freeze(_opts)
|
|
414
|
+
return Object.freeze(_opts) as TRet<Readonly<TxOpts>>;
|
|
240
415
|
}
|
|
241
416
|
|
|
242
417
|
// NOTE: we cannot do this inside PSBTInput coder, because there is no index/txid at this point!
|
|
243
|
-
function validateInput(i: psbt.TransactionInput):
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
418
|
+
function validateInput(i: TArg<psbt.TransactionInput>): TRet<PSBTInputs> {
|
|
419
|
+
validateObject(i as Record<string, any>, {}, {}, 'i');
|
|
420
|
+
const _i = i as PSBTInputs;
|
|
421
|
+
if (_i.nonWitnessUtxo && _i.index !== undefined) {
|
|
422
|
+
const last = _i.nonWitnessUtxo.outputs.length - 1;
|
|
423
|
+
if (_i.index > last) throw new Error(`validateInput: index(${_i.index}) not in nonWitnessUtxo`);
|
|
424
|
+
const prevOut = _i.nonWitnessUtxo.outputs[_i.index];
|
|
248
425
|
if (
|
|
249
|
-
|
|
250
|
-
(!equalBytes(
|
|
426
|
+
_i.witnessUtxo &&
|
|
427
|
+
(!equalBytes(_i.witnessUtxo.script, prevOut.script) ||
|
|
428
|
+
_i.witnessUtxo.amount !== prevOut.amount)
|
|
251
429
|
)
|
|
252
430
|
throw new Error('validateInput: witnessUtxo different from nonWitnessUtxo');
|
|
253
|
-
if (
|
|
254
|
-
const outputs =
|
|
255
|
-
if (outputs.length - 1 <
|
|
431
|
+
if (_i.txid) {
|
|
432
|
+
const outputs = _i.nonWitnessUtxo.outputs;
|
|
433
|
+
if (outputs.length - 1 < _i.index) throw new Error('nonWitnessUtxo: incorect output index');
|
|
256
434
|
// At this point, we are using previous tx output to create new input.
|
|
257
435
|
// Script safety checks are unnecessary:
|
|
258
436
|
// - User has no control over previous tx. If somebody send money in same tx
|
|
@@ -261,56 +439,126 @@ function validateInput(i: psbt.TransactionInput): psbt.TransactionInput {
|
|
|
261
439
|
// in case user wants to use wrong input by mistake
|
|
262
440
|
// - Worst case: tx will be rejected by nodes. Still better than disallowing user
|
|
263
441
|
// to spend real input, no matter how broken it looks
|
|
264
|
-
const tx = Transaction.fromRaw(RawTx.encode(
|
|
442
|
+
const tx = Transaction.fromRaw(RawTx.encode(_i.nonWitnessUtxo), {
|
|
265
443
|
allowUnknownOutputs: true,
|
|
266
444
|
disableScriptCheck: true,
|
|
267
445
|
allowUnknownInputs: true,
|
|
446
|
+
// Consensus does not restrict nVersion; a previous tx with a non-standard
|
|
447
|
+
// version is still spendable and its txid must still be verifiable.
|
|
448
|
+
allowUnknownVersion: true,
|
|
268
449
|
});
|
|
269
|
-
const txid = hex.encode(
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
450
|
+
const txid = hex.encode(_i.txid);
|
|
451
|
+
// BIP174 requires the provided nonWitnessUtxo to hash to the prevout txid even when the
|
|
452
|
+
// previous transaction is otherwise non-final; finality does not make its serialized txid optional.
|
|
453
|
+
// Keep the historical TransactionInput.txid convention here: internal txid bytes match
|
|
454
|
+
// `Transaction.id` (display-order hex), while raw-tx / PSBT boundary coders are responsible
|
|
455
|
+
// for any byte-order conversions required by their wire formats.
|
|
456
|
+
if (tx.id !== txid) throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
|
|
273
457
|
}
|
|
274
458
|
}
|
|
275
|
-
return
|
|
459
|
+
return _i as TRet<PSBTInputs>;
|
|
276
460
|
}
|
|
277
461
|
|
|
462
|
+
/** Canonical PSBT input shape used by the coder layer. */
|
|
278
463
|
export type PSBTInputs = psbt.PSBTKeyMapKeys<typeof psbt.PSBTInput>;
|
|
279
464
|
|
|
465
|
+
/** Canonical PSBT output shape used by the coder layer. */
|
|
466
|
+
export type PSBTOutputs = psbt.PSBTKeyMapKeys<typeof psbt.PSBTOutput>;
|
|
467
|
+
|
|
280
468
|
// Normalizes input
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
469
|
+
/**
|
|
470
|
+
* Extracts the previous output referenced by an input.
|
|
471
|
+
* @param input - PSBT input with previous output data
|
|
472
|
+
* @returns Previous output information.
|
|
473
|
+
* @throws If the input does not contain usable previous-output information. {@link Error}
|
|
474
|
+
* @example
|
|
475
|
+
* Read the previous output from either `witnessUtxo` or `nonWitnessUtxo`.
|
|
476
|
+
* ```ts
|
|
477
|
+
* getPrevOut({ witnessUtxo: { amount: 1n, script: new Uint8Array([0x51]) } });
|
|
478
|
+
* ```
|
|
479
|
+
*/
|
|
480
|
+
export function getPrevOut(input: TArg<psbt.TransactionInput>): P.UnwrapCoder<typeof RawOutput> {
|
|
481
|
+
validateObject(input as Record<string, any>, {}, {}, 'input');
|
|
482
|
+
const _input = input as PSBTInputs;
|
|
483
|
+
if (_input.nonWitnessUtxo) {
|
|
484
|
+
if (_input.index === undefined) throw new Error('Unknown input index');
|
|
485
|
+
// BIP174 `PSBT_IN_NON_WITNESS_UTXO` is the full spent transaction, so the
|
|
486
|
+
// input outpoint index must name an existing output instead of leaking a
|
|
487
|
+
// synthetic `undefined` prevout into later signing / estimation callers.
|
|
488
|
+
if (
|
|
489
|
+
!Number.isSafeInteger(_input.index) ||
|
|
490
|
+
_input.index < 0 ||
|
|
491
|
+
_input.index >= _input.nonWitnessUtxo.outputs.length
|
|
492
|
+
)
|
|
493
|
+
throw new Error(`Wrong input index=${_input.index}`);
|
|
494
|
+
return _input.nonWitnessUtxo.outputs[_input.index];
|
|
495
|
+
} else if ('witnessUtxo' in _input) {
|
|
496
|
+
// The presence check catches malformed provided values; narrow after the guard for TS.
|
|
497
|
+
const prev = _input.witnessUtxo as P.UnwrapCoder<typeof RawOutput>;
|
|
498
|
+
validateObject(prev as Record<string, any>, {}, {}, 'input.witnessUtxo');
|
|
499
|
+
abigint(prev.amount, 'input.witnessUtxo.amount');
|
|
500
|
+
if (!isBytes(prev.script))
|
|
501
|
+
throw new TypeError(
|
|
502
|
+
'"input.witnessUtxo.script" expected Uint8Array, got type=' + typeof prev.script
|
|
503
|
+
);
|
|
504
|
+
return prev;
|
|
505
|
+
} else throw new Error('Cannot find previous output info');
|
|
287
506
|
}
|
|
288
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Normalizes a transaction input update into canonical PSBT form.
|
|
510
|
+
* @param i - input update to normalize
|
|
511
|
+
* @param cur - existing input value to merge with
|
|
512
|
+
* @param allowedFields - fields that may still change on signed inputs
|
|
513
|
+
* @param disableScriptCheck - whether to skip redeem/witness script sanity checks
|
|
514
|
+
* @param allowUnknown - whether to keep unknown PSBT fields
|
|
515
|
+
* @returns Normalized PSBT input.
|
|
516
|
+
* @example
|
|
517
|
+
* Accept hex txids from callers in the same display-order form used by `Transaction.id`, then
|
|
518
|
+
* normalize them into the repo's internal `TransactionInput` shape.
|
|
519
|
+
* ```ts
|
|
520
|
+
* import { hex } from '@scure/base';
|
|
521
|
+
* import { normalizeInput } from '@scure/btc-signer/transaction.js';
|
|
522
|
+
* normalizeInput({
|
|
523
|
+
* txid: '0000000000000000000000000000000000000000000000000000000000000001',
|
|
524
|
+
* index: 0,
|
|
525
|
+
* witnessUtxo: { amount: 1n, script: new Uint8Array([0x51]) },
|
|
526
|
+
* });
|
|
527
|
+
* ```
|
|
528
|
+
*/
|
|
289
529
|
export function normalizeInput(
|
|
290
|
-
i: psbt.TransactionInputUpdate
|
|
291
|
-
cur?:
|
|
292
|
-
allowedFields?: (keyof
|
|
530
|
+
i: TArg<psbt.TransactionInputUpdate>,
|
|
531
|
+
cur?: TArg<PSBTInputs>,
|
|
532
|
+
allowedFields?: TArg<readonly (keyof PSBTInputs)[]>,
|
|
293
533
|
disableScriptCheck = false,
|
|
294
534
|
allowUnknown = false
|
|
295
|
-
):
|
|
296
|
-
|
|
535
|
+
): TRet<PSBTInputs> {
|
|
536
|
+
validateObject(i as Record<string, any>, {}, {}, 'i');
|
|
537
|
+
if (cur !== undefined) validateObject(cur as Record<string, any>, {}, {}, 'cur');
|
|
538
|
+
if (allowedFields !== undefined) u.aarray(allowedFields, 'allowedFields');
|
|
539
|
+
const _i = i as psbt.TransactionInputUpdate;
|
|
540
|
+
const _cur = cur as PSBTInputs | undefined;
|
|
541
|
+
const _allowedFields = allowedFields as readonly (keyof PSBTInputs)[] | undefined;
|
|
542
|
+
let { nonWitnessUtxo, txid } = _i;
|
|
297
543
|
// String support for common fields. We usually prefer Uint8Array to avoid errors
|
|
298
544
|
// like hex looking string accidentally passed, however, in case of nonWitnessUtxo
|
|
299
545
|
// it is better to expect string, since constructing this complex object will be
|
|
300
546
|
// difficult for user
|
|
301
547
|
if (typeof nonWitnessUtxo === 'string') nonWitnessUtxo = hex.decode(nonWitnessUtxo);
|
|
302
548
|
if (isBytes(nonWitnessUtxo)) nonWitnessUtxo = RawTx.decode(nonWitnessUtxo);
|
|
303
|
-
if (!('nonWitnessUtxo' in
|
|
304
|
-
nonWitnessUtxo =
|
|
549
|
+
if (!('nonWitnessUtxo' in _i) && nonWitnessUtxo === undefined)
|
|
550
|
+
nonWitnessUtxo = _cur?.nonWitnessUtxo;
|
|
305
551
|
if (typeof txid === 'string') txid = hex.decode(txid);
|
|
306
552
|
// TODO: if we have nonWitnessUtxo, we can extract txId from here
|
|
307
|
-
if (txid === undefined) txid =
|
|
308
|
-
let res: PSBTInputs = { ...
|
|
309
|
-
if (!('nonWitnessUtxo' in
|
|
553
|
+
if (txid === undefined) txid = _cur?.txid;
|
|
554
|
+
let res: PSBTInputs = { ..._cur, ..._i, nonWitnessUtxo, txid };
|
|
555
|
+
if (!('nonWitnessUtxo' in _i) && res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo;
|
|
310
556
|
if (res.sequence === undefined) res.sequence = DEFAULT_SEQUENCE;
|
|
311
557
|
if (res.tapMerkleRoot === null) delete res.tapMerkleRoot;
|
|
312
|
-
res = psbt.mergeKeyMap(psbt.PSBTInput, res,
|
|
313
|
-
|
|
558
|
+
res = psbt.mergeKeyMap(psbt.PSBTInput, res, _cur, _allowedFields, allowUnknown) as PSBTInputs;
|
|
559
|
+
// Public PSBT coder surface is wrapped with TArg/TRet for TS compatibility; normalizeInput keeps
|
|
560
|
+
// the repo's historical raw internal shape and casts only at the validation boundary here.
|
|
561
|
+
psbt.PSBTInputCoder.encode(res as Parameters<typeof psbt.PSBTInputCoder.encode>[0]); // Validates that everything is correct at this point
|
|
314
562
|
|
|
315
563
|
let prevOut;
|
|
316
564
|
if (res.nonWitnessUtxo && res.index !== undefined)
|
|
@@ -318,18 +566,45 @@ export function normalizeInput(
|
|
|
318
566
|
else if (res.witnessUtxo) prevOut = res.witnessUtxo;
|
|
319
567
|
if (prevOut && !disableScriptCheck)
|
|
320
568
|
checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
|
|
321
|
-
return res
|
|
569
|
+
return res as TRet<PSBTInputs>;
|
|
322
570
|
}
|
|
323
571
|
|
|
324
|
-
|
|
572
|
+
/**
|
|
573
|
+
* Determines how an input should be signed and finalized.
|
|
574
|
+
* Wrapper consistency is expected to be validated earlier by {@link normalizeInput}
|
|
575
|
+
* and {@link checkScript}; this helper classifies already-normalized inputs and is
|
|
576
|
+
* not a standalone redeemScript/witnessScript correctness gate for raw caller input.
|
|
577
|
+
* @param input - PSBT input to inspect
|
|
578
|
+
* @param allowLegacyWitnessUtxo - whether legacy inputs may rely on witness UTXO data only
|
|
579
|
+
* @returns Input classification including transaction type and sighash defaults.
|
|
580
|
+
* @throws If a documented runtime validation or state check fails. {@link Error}
|
|
581
|
+
* @example
|
|
582
|
+
* Detect how the signer should treat a SegWit input from its previous output script.
|
|
583
|
+
* ```ts
|
|
584
|
+
* import { hex } from '@scure/base';
|
|
585
|
+
* import { p2wpkh } from '@scure/btc-signer/payment.js';
|
|
586
|
+
* import { getInputType } from '@scure/btc-signer/transaction.js';
|
|
587
|
+
* import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
588
|
+
* getInputType({
|
|
589
|
+
* witnessUtxo: {
|
|
590
|
+
* amount: 1n,
|
|
591
|
+
* script: p2wpkh(pubECDSA(randomPrivateKeyBytes())).script,
|
|
592
|
+
* },
|
|
593
|
+
* });
|
|
594
|
+
* ```
|
|
595
|
+
*/
|
|
596
|
+
export function getInputType(input: TArg<psbt.TransactionInput>, allowLegacyWitnessUtxo = false) {
|
|
597
|
+
const _input = input as PSBTInputs;
|
|
325
598
|
let txType = 'legacy';
|
|
326
|
-
let defaultSighash = SignatureHash.ALL;
|
|
327
|
-
const prevOut = getPrevOut(
|
|
599
|
+
let defaultSighash: number = SignatureHash.ALL;
|
|
600
|
+
const prevOut = getPrevOut(_input as TArg<psbt.TransactionInput>);
|
|
328
601
|
const first = OutScript.decode(prevOut.script);
|
|
329
602
|
let type = first.type;
|
|
330
603
|
let cur = first;
|
|
331
604
|
const stack = [first];
|
|
332
605
|
if (first.type === 'tr') {
|
|
606
|
+
// Expected invariant: taproot inputs use PSBT_IN_TAP_* metadata only;
|
|
607
|
+
// legacy redeemScript/witnessScript fields belong to P2SH/P2WSH paths.
|
|
333
608
|
defaultSighash = SignatureHash.DEFAULT;
|
|
334
609
|
return {
|
|
335
610
|
txType: 'taproot',
|
|
@@ -337,13 +612,13 @@ export function getInputType(input: psbt.TransactionInput, allowLegacyWitnessUtx
|
|
|
337
612
|
last: first,
|
|
338
613
|
lastScript: prevOut.script,
|
|
339
614
|
defaultSighash,
|
|
340
|
-
sighash:
|
|
615
|
+
sighash: _input.sighashType || defaultSighash,
|
|
341
616
|
};
|
|
342
617
|
} else {
|
|
343
618
|
if (first.type === 'wpkh' || first.type === 'wsh') txType = 'segwit';
|
|
344
619
|
if (first.type === 'sh') {
|
|
345
|
-
if (!
|
|
346
|
-
let child = OutScript.decode(
|
|
620
|
+
if (!_input.redeemScript) throw new Error('inputType: sh without redeemScript');
|
|
621
|
+
let child = OutScript.decode(_input.redeemScript);
|
|
347
622
|
if (child.type === 'wpkh' || child.type === 'wsh') txType = 'segwit';
|
|
348
623
|
stack.push(child);
|
|
349
624
|
cur = child;
|
|
@@ -351,8 +626,8 @@ export function getInputType(input: psbt.TransactionInput, allowLegacyWitnessUtx
|
|
|
351
626
|
}
|
|
352
627
|
// wsh can be inside sh
|
|
353
628
|
if (cur.type === 'wsh') {
|
|
354
|
-
if (!
|
|
355
|
-
let child = OutScript.decode(
|
|
629
|
+
if (!_input.witnessScript) throw new Error('inputType: wsh without witnessScript');
|
|
630
|
+
let child = OutScript.decode(_input.witnessScript);
|
|
356
631
|
if (child.type === 'wsh') txType = 'segwit';
|
|
357
632
|
stack.push(child);
|
|
358
633
|
cur = child;
|
|
@@ -368,9 +643,9 @@ export function getInputType(input: psbt.TransactionInput, allowLegacyWitnessUtx
|
|
|
368
643
|
last,
|
|
369
644
|
lastScript,
|
|
370
645
|
defaultSighash,
|
|
371
|
-
sighash:
|
|
646
|
+
sighash: _input.sighashType || defaultSighash,
|
|
372
647
|
};
|
|
373
|
-
if (txType === 'legacy' && !allowLegacyWitnessUtxo && !
|
|
648
|
+
if (txType === 'legacy' && !allowLegacyWitnessUtxo && !_input.nonWitnessUtxo) {
|
|
374
649
|
throw new Error(
|
|
375
650
|
`Transaction/sign: legacy input without nonWitnessUtxo, can result in attack that forces paying higher fees. Pass allowLegacyWitnessUtxo=true, if you sure`
|
|
376
651
|
);
|
|
@@ -379,10 +654,31 @@ export function getInputType(input: psbt.TransactionInput, allowLegacyWitnessUtx
|
|
|
379
654
|
}
|
|
380
655
|
}
|
|
381
656
|
|
|
657
|
+
/**
|
|
658
|
+
* Mutable Bitcoin transaction and PSBT helper.
|
|
659
|
+
* @param opts - Transaction construction and PSBT serialization options. See {@link TxOpts}.
|
|
660
|
+
* @example
|
|
661
|
+
* Create a transaction, add one spend, and export it as PSBT.
|
|
662
|
+
* ```ts
|
|
663
|
+
* import { hex } from '@scure/base';
|
|
664
|
+
* import { p2wpkh } from '@scure/btc-signer/payment.js';
|
|
665
|
+
* import { Transaction } from '@scure/btc-signer/transaction.js';
|
|
666
|
+
* import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
|
|
667
|
+
* const spend = p2wpkh(pubECDSA(randomPrivateKeyBytes()));
|
|
668
|
+
* const tx = new Transaction();
|
|
669
|
+
* tx.addInput({
|
|
670
|
+
* txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
|
|
671
|
+
* index: 0,
|
|
672
|
+
* witnessUtxo: { amount: 2n, script: spend.script },
|
|
673
|
+
* });
|
|
674
|
+
* tx.addOutput({ script: spend.script, amount: 1n });
|
|
675
|
+
* tx.toPSBT();
|
|
676
|
+
* ```
|
|
677
|
+
*/
|
|
382
678
|
export class Transaction {
|
|
383
679
|
private global: psbt.PSBTKeyMapKeys<typeof psbt.PSBTGlobal> = {};
|
|
384
|
-
private inputs:
|
|
385
|
-
private outputs:
|
|
680
|
+
private inputs: PSBTInputs[] = []; // use getInput()
|
|
681
|
+
private outputs: PSBTOutputs[] = []; // use getOutput()
|
|
386
682
|
readonly opts: ReturnType<typeof validateOpts>;
|
|
387
683
|
constructor(opts: TxOpts = {}) {
|
|
388
684
|
const _opts = (this.opts = validateOpts(opts));
|
|
@@ -426,12 +722,13 @@ export class Transaction {
|
|
|
426
722
|
const tx = new Transaction({ ...opts, version, lockTime, PSBTVersion });
|
|
427
723
|
// We need slice here, because otherwise
|
|
428
724
|
const inputCount = PSBTVersion === 0 ? unsigned?.inputs.length : parsed.global.inputCount;
|
|
429
|
-
tx.inputs = parsed.inputs.slice(0, inputCount).map(
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
725
|
+
tx.inputs = parsed.inputs.slice(0, inputCount).map(
|
|
726
|
+
(i, j) =>
|
|
727
|
+
validateInput({
|
|
728
|
+
finalScriptSig: P.EMPTY,
|
|
729
|
+
...parsed.global.unsignedTx?.inputs[j],
|
|
730
|
+
...i,
|
|
731
|
+
}) as PSBTInputs
|
|
435
732
|
);
|
|
436
733
|
const outputCount = PSBTVersion === 0 ? unsigned?.outputs.length : parsed.global.outputCount;
|
|
437
734
|
tx.outputs = parsed.outputs.slice(0, outputCount).map((i, j) => ({
|
|
@@ -442,7 +739,12 @@ export class Transaction {
|
|
|
442
739
|
if (lockTime !== DEFAULT_LOCKTIME) tx.global.fallbackLocktime = lockTime;
|
|
443
740
|
return tx;
|
|
444
741
|
}
|
|
445
|
-
|
|
742
|
+
// Prefer `global.version` when present so cross-version combiners can serialize at the highest
|
|
743
|
+
// required PSBT version without mutating the frozen transaction options object.
|
|
744
|
+
toPSBT(
|
|
745
|
+
PSBTVersion: number | undefined = this.global.version || this.opts.PSBTVersion
|
|
746
|
+
): Uint8Array {
|
|
747
|
+
if (PSBTVersion !== undefined) anumber(PSBTVersion, 'PSBTVersion');
|
|
446
748
|
if (PSBTVersion !== 0 && PSBTVersion !== 2)
|
|
447
749
|
throw new Error(`Wrong PSBT version=${PSBTVersion}`);
|
|
448
750
|
// if (PSBTVersion === 0 && this.inputs.length === 0) {
|
|
@@ -451,7 +753,9 @@ export class Transaction {
|
|
|
451
753
|
// );
|
|
452
754
|
// }
|
|
453
755
|
const inputs = this.inputs.map((i) =>
|
|
454
|
-
|
|
756
|
+
// For PSBTv0 the prevout txid/index live in global.unsignedTx rather than the input map, so
|
|
757
|
+
// validate the full transaction input before version filtering drops those fields.
|
|
758
|
+
psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, validateInput(i) as TArg<PSBTInputs>)
|
|
455
759
|
);
|
|
456
760
|
for (const inp of inputs) {
|
|
457
761
|
// Don't serialize empty fields
|
|
@@ -472,16 +776,28 @@ export class Transaction {
|
|
|
472
776
|
RawOldTx.encode({
|
|
473
777
|
version: this.version,
|
|
474
778
|
lockTime: this.lockTime,
|
|
475
|
-
inputs: this.inputs
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
779
|
+
inputs: this.inputs
|
|
780
|
+
.map((i) => inputBeforeSign(i as TArg<psbt.TransactionInput>))
|
|
781
|
+
.map((i) => ({
|
|
782
|
+
...i,
|
|
783
|
+
finalScriptSig: P.EMPTY,
|
|
784
|
+
})),
|
|
785
|
+
outputs: this.outputs.map((o) => outputBeforeSign(o as TArg<psbt.TransactionOutput>)),
|
|
480
786
|
})
|
|
481
787
|
);
|
|
482
788
|
delete global.fallbackLocktime;
|
|
483
789
|
delete global.txVersion;
|
|
790
|
+
// PSBTv0 carries the unsigned transaction as one blob, so the PSBTv2 framing fields must be
|
|
791
|
+
// removed here. Keeping `global.version` would make validation treat this rebuilt v0 map as
|
|
792
|
+
// PSBTv2 and reject the required `unsignedTx` field.
|
|
793
|
+
delete global.inputCount;
|
|
794
|
+
delete global.outputCount;
|
|
795
|
+
delete global.version;
|
|
484
796
|
} else {
|
|
797
|
+
// Cross-version merges and v0->v2 re-exports can still carry the PSBTv0 unsignedTx blob in
|
|
798
|
+
// `this.global`, but PSBTv2 serializes the transaction through split global/input/output
|
|
799
|
+
// fields instead, so drop the stale v0-only field before PSBTv2 validation/encoding.
|
|
800
|
+
delete global.unsignedTx;
|
|
485
801
|
global.version = PSBTVersion;
|
|
486
802
|
global.txVersion = this.version;
|
|
487
803
|
global.inputCount = this.inputs.length;
|
|
@@ -493,11 +809,10 @@ export class Transaction {
|
|
|
493
809
|
if (!inputs.length) inputs.push({});
|
|
494
810
|
if (!outputs.length) outputs.push({});
|
|
495
811
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
});
|
|
812
|
+
const raw = { global, inputs, outputs };
|
|
813
|
+
return PSBTVersion === 0
|
|
814
|
+
? psbt.RawPSBTV0.encode(raw as Parameters<typeof psbt.RawPSBTV0.encode>[0])
|
|
815
|
+
: psbt.RawPSBTV2.encode(raw as Parameters<typeof psbt.RawPSBTV2.encode>[0]);
|
|
501
816
|
}
|
|
502
817
|
|
|
503
818
|
// BIP370 lockTime (https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#determining-lock-time)
|
|
@@ -590,26 +905,33 @@ export class Transaction {
|
|
|
590
905
|
|
|
591
906
|
// Info utils
|
|
592
907
|
get hasWitnesses(): boolean {
|
|
593
|
-
let out = false;
|
|
594
908
|
for (const i of this.inputs)
|
|
595
|
-
if (i.finalScriptWitness && i.finalScriptWitness.length)
|
|
596
|
-
return
|
|
909
|
+
if (i.finalScriptWitness && i.finalScriptWitness.length) return true;
|
|
910
|
+
return false;
|
|
597
911
|
}
|
|
598
912
|
// https://en.bitcoin.it/wiki/Weight_units
|
|
599
913
|
get weight(): number {
|
|
600
914
|
if (!this.isFinal) throw new Error('Transaction is not finalized');
|
|
915
|
+
// Serialized length of VarBytes(data) without allocating the encoded copy
|
|
916
|
+
const varLen = (dataLen: number) => CompactSizeLen.encode(dataLen).length + dataLen;
|
|
917
|
+
const hasWitnesses = this.hasWitnesses;
|
|
601
918
|
let out = 32;
|
|
602
919
|
// Outputs
|
|
603
920
|
const outputs = this.outputs.map(outputBeforeSign);
|
|
604
921
|
out += 4 * CompactSizeLen.encode(this.outputs.length).length;
|
|
605
|
-
for (const o of outputs) out += 32 + 4 *
|
|
922
|
+
for (const o of outputs) out += 32 + 4 * varLen(o.script.length);
|
|
606
923
|
// Inputs
|
|
607
|
-
if (
|
|
924
|
+
if (hasWitnesses) out += 2;
|
|
608
925
|
out += 4 * CompactSizeLen.encode(this.inputs.length).length;
|
|
609
926
|
for (const i of this.inputs) {
|
|
610
|
-
out += 160 + 4 *
|
|
611
|
-
|
|
612
|
-
|
|
927
|
+
out += 160 + 4 * varLen((i.finalScriptSig || P.EMPTY).length);
|
|
928
|
+
// Once segwit serialization is active, every input contributes one witness vector, including
|
|
929
|
+
// legacy inputs whose empty vector still encodes as a single zero-item-count byte.
|
|
930
|
+
if (hasWitnesses) {
|
|
931
|
+
const witness = i.finalScriptWitness || [];
|
|
932
|
+
out += CompactSizeLen.encode(witness.length).length;
|
|
933
|
+
for (const w of witness) out += varLen(w.length);
|
|
934
|
+
}
|
|
613
935
|
}
|
|
614
936
|
return out;
|
|
615
937
|
}
|
|
@@ -644,24 +966,35 @@ export class Transaction {
|
|
|
644
966
|
}
|
|
645
967
|
// Input stuff
|
|
646
968
|
private checkInputIdx(idx: number) {
|
|
647
|
-
|
|
648
|
-
|
|
969
|
+
anumber(idx, 'idx');
|
|
970
|
+
if (idx >= this.inputs.length) throw new Error(`Wrong input index=${idx}`);
|
|
649
971
|
}
|
|
650
972
|
getInput(idx: number): psbt.TransactionInput {
|
|
651
973
|
this.checkInputIdx(idx);
|
|
652
|
-
return cloneDeep(this.inputs[idx]);
|
|
974
|
+
return cloneDeep(this.inputs[idx]) as psbt.TransactionInput;
|
|
653
975
|
}
|
|
654
976
|
get inputsLength(): number {
|
|
655
977
|
return this.inputs.length;
|
|
656
978
|
}
|
|
657
979
|
// Modification
|
|
658
|
-
addInput(input: psbt.TransactionInputUpdate
|
|
980
|
+
addInput(input: TArg<psbt.TransactionInputUpdate>, _ignoreSignStatus = false): number {
|
|
981
|
+
validateObject(input as Record<string, any>, {}, {}, 'input');
|
|
659
982
|
if (!_ignoreSignStatus && !this.signStatus().addInput)
|
|
660
983
|
throw new Error('Tx has signed inputs, cannot add new one');
|
|
661
|
-
|
|
984
|
+
// normalizeInput preserves nested caller-owned byte arrays, so detach them here before the
|
|
985
|
+
// new input becomes transaction state and later caller mutation can rewrite it by aliasing.
|
|
986
|
+
this.inputs.push(
|
|
987
|
+
cloneDeep(
|
|
988
|
+
normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck)
|
|
989
|
+
) as PSBTInputs
|
|
990
|
+
);
|
|
662
991
|
return this.inputs.length - 1;
|
|
663
992
|
}
|
|
664
|
-
updateInput(
|
|
993
|
+
updateInput(
|
|
994
|
+
idx: number,
|
|
995
|
+
input: TArg<psbt.TransactionInputUpdate>,
|
|
996
|
+
_ignoreSignStatus = false
|
|
997
|
+
): void {
|
|
665
998
|
this.checkInputIdx(idx);
|
|
666
999
|
let allowedFields = undefined;
|
|
667
1000
|
if (!_ignoreSignStatus) {
|
|
@@ -669,49 +1002,53 @@ export class Transaction {
|
|
|
669
1002
|
if (!status.addInput || status.inputs.includes(idx))
|
|
670
1003
|
allowedFields = psbt.PSBTInputUnsignedKeys;
|
|
671
1004
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1005
|
+
// normalizeInput preserves nested caller-owned byte arrays, so detach the merged result here
|
|
1006
|
+
// before the updated input becomes transaction state and later caller mutation can rewrite it.
|
|
1007
|
+
this.inputs[idx] = cloneDeep(
|
|
1008
|
+
normalizeInput(
|
|
1009
|
+
input,
|
|
1010
|
+
this.inputs[idx],
|
|
1011
|
+
allowedFields,
|
|
1012
|
+
this.opts.disableScriptCheck,
|
|
1013
|
+
this.opts.allowUnknown
|
|
1014
|
+
)
|
|
1015
|
+
) as PSBTInputs;
|
|
679
1016
|
}
|
|
680
1017
|
// Output stuff
|
|
681
1018
|
private checkOutputIdx(idx: number) {
|
|
682
|
-
|
|
683
|
-
|
|
1019
|
+
anumber(idx, 'idx');
|
|
1020
|
+
if (idx >= this.outputs.length) throw new Error(`Wrong output index=${idx}`);
|
|
684
1021
|
}
|
|
685
1022
|
getOutput(idx: number): psbt.TransactionOutput {
|
|
686
1023
|
this.checkOutputIdx(idx);
|
|
687
|
-
return cloneDeep(this.outputs[idx]);
|
|
1024
|
+
return cloneDeep(this.outputs[idx]) as psbt.TransactionOutput;
|
|
688
1025
|
}
|
|
689
1026
|
getOutputAddress(idx: number, network: u.BTC_NETWORK = NETWORK): string | undefined {
|
|
690
1027
|
const out = this.getOutput(idx);
|
|
691
1028
|
if (!out.script) return;
|
|
692
|
-
return Address(network).encode(
|
|
1029
|
+
return Address(network).encode(
|
|
1030
|
+
OutScript.decode(out.script) as Parameters<ReturnType<typeof Address>['encode']>[0]
|
|
1031
|
+
);
|
|
693
1032
|
}
|
|
694
1033
|
|
|
695
1034
|
get outputsLength(): number {
|
|
696
1035
|
return this.outputs.length;
|
|
697
1036
|
}
|
|
698
1037
|
private normalizeOutput(
|
|
699
|
-
o: psbt.TransactionOutputUpdate
|
|
700
|
-
cur?:
|
|
701
|
-
allowedFields?: (keyof typeof psbt.PSBTOutput)[]
|
|
702
|
-
):
|
|
1038
|
+
o: TArg<psbt.TransactionOutputUpdate>,
|
|
1039
|
+
cur?: PSBTOutputs,
|
|
1040
|
+
allowedFields?: readonly (keyof typeof psbt.PSBTOutput)[]
|
|
1041
|
+
): PSBTOutputs {
|
|
1042
|
+
validateObject(o as Record<string, any>, {}, {}, 'o');
|
|
703
1043
|
let { amount, script } = o;
|
|
704
1044
|
if (amount === undefined) amount = cur?.amount;
|
|
705
|
-
|
|
706
|
-
throw new Error(
|
|
707
|
-
`Wrong amount type, should be of type bigint in sats, but got ${amount} of type ${typeof amount}`
|
|
708
|
-
);
|
|
1045
|
+
amount = abigint(amount, 'o.amount');
|
|
709
1046
|
if (typeof script === 'string') script = hex.decode(script);
|
|
710
1047
|
if (script === undefined) script = cur?.script;
|
|
711
|
-
let res:
|
|
1048
|
+
let res: PSBTOutputs = { ...cur, ...(o as PSBTOutputs & { script?: string }), amount, script };
|
|
712
1049
|
if (res.amount === undefined) delete res.amount;
|
|
713
1050
|
res = psbt.mergeKeyMap(psbt.PSBTOutput, res, cur, allowedFields, this.opts.allowUnknown);
|
|
714
|
-
psbt.PSBTOutputCoder.encode(res);
|
|
1051
|
+
psbt.PSBTOutputCoder.encode(res as Parameters<typeof psbt.PSBTOutputCoder.encode>[0]);
|
|
715
1052
|
if (
|
|
716
1053
|
res.script &&
|
|
717
1054
|
!this.opts.allowUnknownOutputs &&
|
|
@@ -724,13 +1061,19 @@ export class Transaction {
|
|
|
724
1061
|
if (!this.opts.disableScriptCheck) checkScript(res.script, res.redeemScript, res.witnessScript);
|
|
725
1062
|
return res;
|
|
726
1063
|
}
|
|
727
|
-
addOutput(o: psbt.TransactionOutputUpdate
|
|
1064
|
+
addOutput(o: TArg<psbt.TransactionOutputUpdate>, _ignoreSignStatus = false): number {
|
|
728
1065
|
if (!_ignoreSignStatus && !this.signStatus().addOutput)
|
|
729
1066
|
throw new Error('Tx has signed outputs, cannot add new one');
|
|
730
|
-
|
|
1067
|
+
// normalizeOutput preserves nested caller-owned script bytes, so detach them here before the
|
|
1068
|
+
// new output becomes transaction state and later caller mutation can rewrite it by aliasing.
|
|
1069
|
+
this.outputs.push(cloneDeep(this.normalizeOutput(o)));
|
|
731
1070
|
return this.outputs.length - 1;
|
|
732
1071
|
}
|
|
733
|
-
updateOutput(
|
|
1072
|
+
updateOutput(
|
|
1073
|
+
idx: number,
|
|
1074
|
+
output: TArg<psbt.TransactionOutputUpdate>,
|
|
1075
|
+
_ignoreSignStatus = false
|
|
1076
|
+
): void {
|
|
734
1077
|
this.checkOutputIdx(idx);
|
|
735
1078
|
let allowedFields = undefined;
|
|
736
1079
|
if (!_ignoreSignStatus) {
|
|
@@ -738,14 +1081,23 @@ export class Transaction {
|
|
|
738
1081
|
if (!status.addOutput || status.outputs.includes(idx))
|
|
739
1082
|
allowedFields = psbt.PSBTOutputUnsignedKeys;
|
|
740
1083
|
}
|
|
741
|
-
|
|
1084
|
+
// updateOutput replaces stored state with normalizeOutput(...) directly, so detach the result
|
|
1085
|
+
// before storing it or later caller mutation of `output.script` will rewrite transaction state.
|
|
1086
|
+
this.outputs[idx] = cloneDeep(this.normalizeOutput(output, this.outputs[idx], allowedFields));
|
|
742
1087
|
}
|
|
743
1088
|
addOutputAddress(address: string, amount: bigint, network: u.BTC_NETWORK = NETWORK): number {
|
|
744
|
-
return this.addOutput({
|
|
1089
|
+
return this.addOutput({
|
|
1090
|
+
// Address.decode() only returns recognized descriptors here, but its wrapped output type
|
|
1091
|
+
// still carries `undefined` for coder parity, so narrow before feeding OutScript.encode().
|
|
1092
|
+
script: OutScript.encode(
|
|
1093
|
+
Address(network).decode(address) as Parameters<typeof OutScript.encode>[0]
|
|
1094
|
+
),
|
|
1095
|
+
amount,
|
|
1096
|
+
});
|
|
745
1097
|
}
|
|
746
1098
|
// Utils
|
|
747
1099
|
get fee(): bigint {
|
|
748
|
-
let res =
|
|
1100
|
+
let res = _0n;
|
|
749
1101
|
for (const i of this.inputs) {
|
|
750
1102
|
const prevOut = getPrevOut(i);
|
|
751
1103
|
if (!prevOut) throw new Error('Empty input amount');
|
|
@@ -764,10 +1116,8 @@ export class Transaction {
|
|
|
764
1116
|
const { isAny, isNone, isSingle } = unpackSighash(hashType);
|
|
765
1117
|
if (idx < 0 || !Number.isSafeInteger(idx)) throw new Error(`Invalid input idx=${idx}`);
|
|
766
1118
|
if ((isSingle && idx >= this.outputs.length) || idx >= this.inputs.length)
|
|
767
|
-
return P.U256BE.encode(
|
|
768
|
-
prevOutScript =
|
|
769
|
-
Script.decode(prevOutScript).filter((i) => i !== 'CODESEPARATOR')
|
|
770
|
-
);
|
|
1119
|
+
return P.U256BE.encode(_1n);
|
|
1120
|
+
prevOutScript = stripCodeSeparator(prevOutScript);
|
|
771
1121
|
let inputs: TransactionInputRequired[] = this.inputs
|
|
772
1122
|
.map(inputBeforeSign)
|
|
773
1123
|
.map((input, inputIdx) => ({
|
|
@@ -784,7 +1134,10 @@ export class Transaction {
|
|
|
784
1134
|
let outputs = this.outputs.map(outputBeforeSign);
|
|
785
1135
|
if (isNone) outputs = [];
|
|
786
1136
|
else if (isSingle) {
|
|
787
|
-
outputs = outputs
|
|
1137
|
+
outputs = outputs
|
|
1138
|
+
.slice(0, idx)
|
|
1139
|
+
.fill(EMPTY_OUTPUT as (typeof outputs)[number])
|
|
1140
|
+
.concat([outputs[idx]]);
|
|
788
1141
|
}
|
|
789
1142
|
const tmpTx = RawTx.encode({
|
|
790
1143
|
lockTime: this.lockTime,
|
|
@@ -801,6 +1154,10 @@ export class Transaction {
|
|
|
801
1154
|
hashType: number,
|
|
802
1155
|
amount: bigint
|
|
803
1156
|
): Uint8Array {
|
|
1157
|
+
// BIP143 serializes txTo.vin[nIn].prevout and txTo.vin[nIn].nSequence, so reject an invalid
|
|
1158
|
+
// nIn explicitly instead of leaking a later undefined-input TypeError from inputs[idx].
|
|
1159
|
+
anumber(idx, 'idx');
|
|
1160
|
+
if (idx >= this.inputs.length) throw new Error(`Invalid input idx=${idx}`);
|
|
804
1161
|
const { isAny, isNone, isSingle } = unpackSighash(hashType);
|
|
805
1162
|
let inputHash = EMPTY32;
|
|
806
1163
|
let sequenceHash = EMPTY32;
|
|
@@ -839,9 +1196,15 @@ export class Transaction {
|
|
|
839
1196
|
leafVer = 0xc0,
|
|
840
1197
|
annex?: Bytes
|
|
841
1198
|
): Uint8Array {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1199
|
+
// BIP341 SigMsg commits either to input_index or to the selected input's outpoint/amount/script/
|
|
1200
|
+
// sequence under ANYONECANPAY, so reject an invalid index explicitly instead of hashing a
|
|
1201
|
+
// nonexistent input or leaking a later integer-encoding RangeError for negative idx.
|
|
1202
|
+
anumber(idx, 'idx');
|
|
1203
|
+
if (idx >= this.inputs.length) throw new Error(`Invalid input idx=${idx}`);
|
|
1204
|
+
u.aarray(amount, 'amount');
|
|
1205
|
+
u.aarray(prevOutScript, 'prevOutScript');
|
|
1206
|
+
if (this.inputs.length !== amount.length) throw new Error(`Invalid amounts array=${amount}`);
|
|
1207
|
+
if (this.inputs.length !== prevOutScript.length)
|
|
845
1208
|
throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
|
|
846
1209
|
const out: Bytes[] = [
|
|
847
1210
|
P.U8.encode(0),
|
|
@@ -886,27 +1249,100 @@ export class Transaction {
|
|
|
886
1249
|
}
|
|
887
1250
|
// Signer can be privateKey OR instance of bip32 HD stuff
|
|
888
1251
|
signIdx(privateKey: Signer, idx: number, allowedSighash?: SigHash[], _auxRand?: Bytes): boolean {
|
|
1252
|
+
if (!isBytes(privateKey)) {
|
|
1253
|
+
// HDKey is a structural external instance, so plain-object validation would
|
|
1254
|
+
// reject valid signers.
|
|
1255
|
+
if (
|
|
1256
|
+
!privateKey ||
|
|
1257
|
+
typeof privateKey !== 'object' ||
|
|
1258
|
+
typeof (privateKey as HDKey).deriveChild !== 'function'
|
|
1259
|
+
)
|
|
1260
|
+
throw new TypeError(
|
|
1261
|
+
'"privateKey" expected Uint8Array or HDKey, got type=' + typeof privateKey
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
889
1264
|
this.checkInputIdx(idx);
|
|
890
1265
|
const input = this.inputs[idx];
|
|
891
|
-
const inputType = getInputType(
|
|
892
|
-
|
|
1266
|
+
const inputType = getInputType(
|
|
1267
|
+
input as TArg<psbt.TransactionInput>,
|
|
1268
|
+
this.opts.allowLegacyWitnessUtxo
|
|
1269
|
+
);
|
|
1270
|
+
const canSign = (privateKey: Bytes): boolean => {
|
|
1271
|
+
if (inputType.txType === 'taproot') {
|
|
1272
|
+
const pubKey = u.pubSchnorr(privateKey);
|
|
1273
|
+
if (input.tapInternalKey && equalBytes(pubKey, input.tapInternalKey)) return true;
|
|
1274
|
+
if (!input.tapLeafScript) return false;
|
|
1275
|
+
for (const [_, leaf] of input.tapLeafScript) {
|
|
1276
|
+
for (const op of Script.decode(leaf.subarray(0, -1))) {
|
|
1277
|
+
if (isBytes(op) && equalBytes(op, pubKey)) return true;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return false;
|
|
1281
|
+
}
|
|
1282
|
+
const pubKey = u.pubECDSA(privateKey);
|
|
1283
|
+
const pubKeyHash = u.hash160(pubKey);
|
|
1284
|
+
for (const op of Script.decode(inputType.lastScript)) {
|
|
1285
|
+
if (isBytes(op) && (equalBytes(op, pubKey) || equalBytes(op, pubKeyHash))) return true;
|
|
1286
|
+
}
|
|
1287
|
+
return false;
|
|
1288
|
+
};
|
|
1289
|
+
// Expected invariant: HD signing should use bip32Derivation for legacy/segwit inputs,
|
|
1290
|
+
// tapBip32Derivation for taproot inputs, and preserve caller sighash/auxRand constraints.
|
|
893
1291
|
if (!isBytes(privateKey)) {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1292
|
+
const root = privateKey as HDKey;
|
|
1293
|
+
type DerRow = { pubKey: Bytes; fingerprint: number; path: readonly number[] };
|
|
1294
|
+
const deriveSigners = (
|
|
1295
|
+
label: string,
|
|
1296
|
+
rows: DerRow[] | undefined,
|
|
1297
|
+
pubKey: (signer: HDKey) => Bytes
|
|
1298
|
+
): HDKey[] => {
|
|
1299
|
+
if (!rows || !rows.length) throw new Error(`${label}: empty`);
|
|
1300
|
+
const signers = rows
|
|
1301
|
+
.filter((row) => row.fingerprint == root.fingerprint)
|
|
1302
|
+
.map((row) => {
|
|
1303
|
+
let s = root;
|
|
1304
|
+
for (const i of row.path) s = s.deriveChild(i);
|
|
1305
|
+
if (!equalBytes(pubKey(s), row.pubKey)) throw new Error(`${label}: wrong pubKey`);
|
|
1306
|
+
if (!s.privateKey) throw new Error(`${label}: no privateKey`);
|
|
1307
|
+
return s;
|
|
1308
|
+
});
|
|
1309
|
+
if (!signers.length)
|
|
1310
|
+
throw new Error(`${label}: no items with fingerprint=${root.fingerprint}`);
|
|
1311
|
+
return signers;
|
|
1312
|
+
};
|
|
1313
|
+
const signers =
|
|
1314
|
+
inputType.txType === 'taproot'
|
|
1315
|
+
? // BIP371 PSBT_IN_TAP_BIP32_DERIVATION stores x-only pubkeys plus `der`, so taproot HD
|
|
1316
|
+
// signing must derive against that map instead of legacy bip32Derivation.
|
|
1317
|
+
deriveSigners(
|
|
1318
|
+
'tapBip32Derivation',
|
|
1319
|
+
input.tapBip32Derivation?.map(([pubKey, { der }]) => ({
|
|
1320
|
+
pubKey,
|
|
1321
|
+
fingerprint: der.fingerprint,
|
|
1322
|
+
path: der.path,
|
|
1323
|
+
})),
|
|
1324
|
+
(s) => s.publicKey.slice(1)
|
|
1325
|
+
)
|
|
1326
|
+
: deriveSigners(
|
|
1327
|
+
'bip32Derivation',
|
|
1328
|
+
input.bip32Derivation?.map(([pubKey, der]) => ({
|
|
1329
|
+
pubKey,
|
|
1330
|
+
fingerprint: der.fingerprint,
|
|
1331
|
+
path: der.path,
|
|
1332
|
+
})),
|
|
1333
|
+
(s) => s.publicKey
|
|
1334
|
+
);
|
|
907
1335
|
let signed = false;
|
|
908
|
-
for (const s of signers)
|
|
909
|
-
|
|
1336
|
+
for (const s of signers) {
|
|
1337
|
+
// PSBT may legitimately carry multiple same-fingerprint derivation entries (multisig or
|
|
1338
|
+
// taproot internal/script-path keys). Skip unrelated derived children instead of aborting
|
|
1339
|
+
// the whole HD signing attempt on the first non-applicable candidate.
|
|
1340
|
+
if (!canSign(s.privateKey)) continue;
|
|
1341
|
+
if (this.signIdx(s.privateKey, idx, allowedSighash, _auxRand)) signed = true;
|
|
1342
|
+
}
|
|
1343
|
+
if (signed) return true;
|
|
1344
|
+
if (inputType.txType === 'taproot') throw new Error('No taproot scripts signed');
|
|
1345
|
+
throw new Error(`Input script doesn't have pubKey: ${inputType.lastScript}`);
|
|
910
1346
|
}
|
|
911
1347
|
// Sighash checks
|
|
912
1348
|
// Just for compat with bitcoinjs-lib, so users won't face unexpected behaviour.
|
|
@@ -1047,7 +1483,7 @@ export class Transaction {
|
|
|
1047
1483
|
|
|
1048
1484
|
finalizeIdx(idx: number): void {
|
|
1049
1485
|
this.checkInputIdx(idx);
|
|
1050
|
-
if (this.fee <
|
|
1486
|
+
if (this.fee < _0n) throw new Error('Outputs spends more than inputs amount');
|
|
1051
1487
|
const input = this.inputs[idx];
|
|
1052
1488
|
const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
|
|
1053
1489
|
// Taproot finalize
|
|
@@ -1116,8 +1552,8 @@ export class Transaction {
|
|
|
1116
1552
|
const finalized = c.finalizeTaproot(script, csEncoded, scriptSig);
|
|
1117
1553
|
if (!finalized) continue;
|
|
1118
1554
|
input.finalScriptWitness = finalized.concat(psbt.TaprootControlBlock.encode(cb));
|
|
1119
|
-
input.finalScriptSig
|
|
1120
|
-
cleanFinalInput(input);
|
|
1555
|
+
delete input.finalScriptSig;
|
|
1556
|
+
cleanFinalInput(input as TArg<PSBTInputs>);
|
|
1121
1557
|
return;
|
|
1122
1558
|
}
|
|
1123
1559
|
}
|
|
@@ -1131,8 +1567,9 @@ export class Transaction {
|
|
|
1131
1567
|
}
|
|
1132
1568
|
if (!input.finalScriptWitness) throw new Error('finalize/taproot: empty witness');
|
|
1133
1569
|
} else throw new Error('finalize/taproot: unknown input');
|
|
1134
|
-
input
|
|
1135
|
-
|
|
1570
|
+
// BIP174 Input Finalizer: if scriptSig is empty for an input, 0x07 remains unset.
|
|
1571
|
+
delete input.finalScriptSig;
|
|
1572
|
+
cleanFinalInput(input as TArg<PSBTInputs>);
|
|
1136
1573
|
return;
|
|
1137
1574
|
}
|
|
1138
1575
|
if (!input.partialSig || !input.partialSig.length) throw new Error('Not enough partial sign');
|
|
@@ -1192,7 +1629,7 @@ export class Transaction {
|
|
|
1192
1629
|
if (!finalScriptSig && !finalScriptWitness) throw new Error('Unknown error finalizing input');
|
|
1193
1630
|
if (finalScriptSig) input.finalScriptSig = finalScriptSig;
|
|
1194
1631
|
if (finalScriptWitness) input.finalScriptWitness = finalScriptWitness;
|
|
1195
|
-
cleanFinalInput(input);
|
|
1632
|
+
cleanFinalInput(input as TArg<PSBTInputs>);
|
|
1196
1633
|
}
|
|
1197
1634
|
finalize(): void {
|
|
1198
1635
|
for (let i = 0; i < this.inputs.length; i++) this.finalizeIdx(i);
|
|
@@ -1200,11 +1637,16 @@ export class Transaction {
|
|
|
1200
1637
|
extract(): Uint8Array {
|
|
1201
1638
|
if (!this.isFinal) throw new Error('Transaction has unfinalized inputs');
|
|
1202
1639
|
if (!this.outputs.length) throw new Error('Transaction has no outputs');
|
|
1203
|
-
if (this.fee <
|
|
1640
|
+
if (this.fee < _0n) throw new Error('Outputs spends more than inputs amount');
|
|
1204
1641
|
return this.toBytes(true, true);
|
|
1205
1642
|
}
|
|
1206
1643
|
combine(other: Transaction): this {
|
|
1207
|
-
|
|
1644
|
+
if (!(other instanceof Transaction))
|
|
1645
|
+
throw new TypeError('"other" expected Transaction, got type=' + typeof other);
|
|
1646
|
+
// BIP174 combiners merge same-transaction PSBTs across versions and emit the highest required
|
|
1647
|
+
// version, so PSBTVersion mismatches are normalized below instead of treated as conflicts.
|
|
1648
|
+
const PSBTVersion = Math.max(this.opts.PSBTVersion || 0, other.opts.PSBTVersion || 0);
|
|
1649
|
+
for (const k of ['version', 'lockTime'] as const) {
|
|
1208
1650
|
if (this.opts[k] !== other.opts[k]) {
|
|
1209
1651
|
throw new Error(
|
|
1210
1652
|
`Transaction/combine: different ${k} this=${this.opts[k]} other=${other.opts[k]}`
|
|
@@ -1218,11 +1660,9 @@ export class Transaction {
|
|
|
1218
1660
|
);
|
|
1219
1661
|
}
|
|
1220
1662
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
: P.EMPTY;
|
|
1225
|
-
if (!equalBytes(thisUnsigned, otherUnsigned))
|
|
1663
|
+
// Same-transaction checks must compare the normalized unsigned tx bytes here: PSBTv0 stores
|
|
1664
|
+
// `global.unsignedTx`, while PSBTv2 reconstructs the same transaction from split fields.
|
|
1665
|
+
if (!equalBytes(this.unsignedTx, other.unsignedTx))
|
|
1226
1666
|
throw new Error(`Transaction/combine: different unsigned tx`);
|
|
1227
1667
|
this.global = psbt.mergeKeyMap(
|
|
1228
1668
|
psbt.PSBTGlobal,
|
|
@@ -1231,31 +1671,62 @@ export class Transaction {
|
|
|
1231
1671
|
undefined,
|
|
1232
1672
|
this.opts.allowUnknown
|
|
1233
1673
|
);
|
|
1674
|
+
if (PSBTVersion) this.global.version = PSBTVersion;
|
|
1234
1675
|
for (let i = 0; i < this.inputs.length; i++) this.updateInput(i, other.inputs[i], true);
|
|
1235
1676
|
for (let i = 0; i < this.outputs.length; i++) this.updateOutput(i, other.outputs[i], true);
|
|
1236
1677
|
return this;
|
|
1237
1678
|
}
|
|
1238
1679
|
clone(): Transaction {
|
|
1239
1680
|
// deepClone probably faster, but this enforces that encoding is valid
|
|
1240
|
-
return Transaction.fromPSBT(this.toPSBT(
|
|
1681
|
+
return Transaction.fromPSBT(this.toPSBT(), this.opts);
|
|
1241
1682
|
}
|
|
1242
1683
|
}
|
|
1243
1684
|
|
|
1244
|
-
|
|
1685
|
+
/**
|
|
1686
|
+
* Merges multiple PSBT blobs into one.
|
|
1687
|
+
* @param psbts - PSBT byte arrays to combine
|
|
1688
|
+
* @returns Combined PSBT bytes.
|
|
1689
|
+
* @throws If the PSBT list is empty or the partial transactions cannot be combined. {@link Error}
|
|
1690
|
+
* @example
|
|
1691
|
+
* Merge separate partially signed PSBTs that share the same unsigned transaction.
|
|
1692
|
+
* ```ts
|
|
1693
|
+
* import { PSBTCombine, Transaction } from '@scure/btc-signer/transaction.js';
|
|
1694
|
+
* const psbt = new Transaction().toPSBT();
|
|
1695
|
+
* PSBTCombine([psbt, psbt]);
|
|
1696
|
+
* ```
|
|
1697
|
+
*/
|
|
1698
|
+
export function PSBTCombine(psbts: TArg<Bytes[]>): TRet<Bytes> {
|
|
1245
1699
|
if (!psbts || !Array.isArray(psbts) || !psbts.length)
|
|
1246
1700
|
throw new Error('PSBTCombine: wrong PSBT list');
|
|
1247
1701
|
const tx = Transaction.fromPSBT(psbts[0]);
|
|
1248
1702
|
for (let i = 1; i < psbts.length; i++) tx.combine(Transaction.fromPSBT(psbts[i]));
|
|
1249
|
-
return tx.toPSBT()
|
|
1703
|
+
return tx.toPSBT() as TRet<Bytes>;
|
|
1250
1704
|
}
|
|
1251
1705
|
|
|
1252
1706
|
// Copy-pasted from bip32 derive, maybe do something like 'bip32.parsePath'?
|
|
1253
1707
|
const HARDENED_OFFSET: number = 0x80000000;
|
|
1708
|
+
/**
|
|
1709
|
+
* Parses a BIP32 path string into child indices.
|
|
1710
|
+
* @param path - derivation path such as `m/0'/1`
|
|
1711
|
+
* @returns Array of encoded child indices.
|
|
1712
|
+
* @throws If the derivation path syntax or child indices are invalid. {@link Error}
|
|
1713
|
+
* @example
|
|
1714
|
+
* Parse a BIP32 derivation path into hardened and unhardened indices.
|
|
1715
|
+
* ```ts
|
|
1716
|
+
* bip32Path("m/0'/1");
|
|
1717
|
+
* ```
|
|
1718
|
+
*/
|
|
1254
1719
|
export function bip32Path(path: string): number[] {
|
|
1255
1720
|
const out: number[] = [];
|
|
1721
|
+
// PSBT key-origin records only carry raw child indices, so this convenience
|
|
1722
|
+
// parser normalizes textual BIP32 roots into the same integer path array and
|
|
1723
|
+
// uses apostrophe suffixes for hardening.
|
|
1256
1724
|
if (!/^[mM]'?/.test(path)) throw new Error('Path must start with "m" or "M"');
|
|
1257
1725
|
if (/^[mM]'?$/.test(path)) return out;
|
|
1258
1726
|
const parts = path.replace(/^[mM]'?\//, '').split('/');
|
|
1727
|
+
// BIP32 Serialization format `* 1 byte: depth`: extended keys cap depth at
|
|
1728
|
+
// 255, so deeper text paths cannot roundtrip.
|
|
1729
|
+
if (parts.length > 255) throw new Error('Path depth exceeds 255');
|
|
1259
1730
|
for (const c of parts) {
|
|
1260
1731
|
const m = /^(\d+)('?)$/.exec(c);
|
|
1261
1732
|
if (!m || m.length !== 3) throw new Error(`Invalid child index: ${c}`);
|