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