@scure/btc-signer 2.2.0 → 2.4.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 +196 -37
- package/index.d.ts +4 -4
- package/index.js +3 -4
- package/musig2.d.ts +28 -9
- package/musig2.js +46 -18
- package/net.d.ts +355 -0
- package/net.js +880 -0
- package/p2p.d.ts +0 -1
- package/p2p.js +23 -7
- package/package.json +18 -21
- package/payment.d.ts +18 -11
- package/payment.js +203 -58
- package/psbt.d.ts +680 -10
- package/psbt.js +204 -43
- package/script.d.ts +1 -2
- package/script.js +71 -59
- package/src/_type_test.ts +83 -0
- package/src/index.ts +4 -2
- package/src/musig2.ts +71 -19
- package/src/net.ts +1111 -0
- package/src/p2p.ts +22 -6
- package/src/payment.ts +233 -70
- package/src/psbt.ts +228 -39
- package/src/script.ts +57 -35
- package/src/transaction.ts +869 -168
- package/src/utils.ts +92 -4
- package/src/utxo.ts +223 -113
- package/transaction.d.ts +40 -7
- package/transaction.js +745 -151
- package/utils.d.ts +45 -2
- package/utils.js +80 -5
- package/utxo.d.ts +192 -2
- package/utxo.js +200 -99
- 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,6 +1,14 @@
|
|
|
1
1
|
import { hex } from '@scure/base';
|
|
2
|
+
import { anumber } from '@noble/hashes/utils.js';
|
|
2
3
|
import * as P from 'micro-packed';
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
Address,
|
|
6
|
+
type CustomScript,
|
|
7
|
+
OutScript,
|
|
8
|
+
_WitnessOutScript,
|
|
9
|
+
checkScript,
|
|
10
|
+
tapLeafHash,
|
|
11
|
+
} from './payment.ts';
|
|
4
12
|
import * as psbt from './psbt.ts';
|
|
5
13
|
import {
|
|
6
14
|
CompactSizeLen,
|
|
@@ -18,16 +26,22 @@ import * as u from './utils.ts';
|
|
|
18
26
|
import {
|
|
19
27
|
type Bytes,
|
|
20
28
|
NETWORK,
|
|
29
|
+
abigint,
|
|
21
30
|
concatBytes,
|
|
22
31
|
equalBytes,
|
|
23
32
|
isBytes,
|
|
24
33
|
type TArg,
|
|
25
34
|
type TRet,
|
|
35
|
+
validateObject,
|
|
26
36
|
} from './utils.ts';
|
|
27
37
|
|
|
38
|
+
// Be friendly to bad ECMAScript parsers by not using bigint literals.
|
|
39
|
+
// prettier-ignore
|
|
40
|
+
const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
|
|
41
|
+
const U64_MAX = /* @__PURE__ */ BigInt('0xffffffffffffffff');
|
|
28
42
|
const EMPTY32: Uint8Array = /* @__PURE__ */ new Uint8Array(32);
|
|
29
43
|
const EMPTY_OUTPUT: P.UnwrapCoder<typeof RawOutput> = {
|
|
30
|
-
amount:
|
|
44
|
+
amount: U64_MAX,
|
|
31
45
|
script: P.EMPTY,
|
|
32
46
|
};
|
|
33
47
|
/**
|
|
@@ -48,7 +62,7 @@ const stripCodeSeparator = (script: TArg<Bytes>): TRet<Bytes> => {
|
|
|
48
62
|
// byte, because semantic decode/re-encode would change the signed digest.
|
|
49
63
|
let start = 0;
|
|
50
64
|
const out: Uint8Array[] = [];
|
|
51
|
-
for (let i = 0; i < script.length;
|
|
65
|
+
for (let i = 0; i < script.length;) {
|
|
52
66
|
const pos = i;
|
|
53
67
|
const op = script[i++];
|
|
54
68
|
if (op === OP.CODESEPARATOR) {
|
|
@@ -158,6 +172,8 @@ export function cloneDeep<T>(obj: T): T {
|
|
|
158
172
|
|
|
159
173
|
// Mostly security features, hardened defaults;
|
|
160
174
|
// but you still can parse other people tx with unspendable outputs and stuff if you want
|
|
175
|
+
/** PSBT unknown/proprietary-field handling policy. */
|
|
176
|
+
export type Unknowns = psbt.Unknowns;
|
|
161
177
|
/** Transaction construction and parsing options. */
|
|
162
178
|
export interface TxOpts {
|
|
163
179
|
/** Transaction version to place into new transactions and imported PSBTs. */
|
|
@@ -187,7 +203,7 @@ export interface TxOpts {
|
|
|
187
203
|
/** Allow signing and finalizing inputs with unknown script shapes. */
|
|
188
204
|
allowUnknownInputs?: boolean;
|
|
189
205
|
// Check input/output scripts for sanity
|
|
190
|
-
/** Skip redeem-script and
|
|
206
|
+
/** Skip redeem/witness-script and Taproot commitment consistency checks. */
|
|
191
207
|
disableScriptCheck?: boolean;
|
|
192
208
|
// There is strange behaviour where tx without outputs encoded with empty output in the end,
|
|
193
209
|
// tx without outputs in BIP174 doesn't have itb
|
|
@@ -197,12 +213,30 @@ export interface TxOpts {
|
|
|
197
213
|
// result paying higher mining fee
|
|
198
214
|
/** Permit legacy inputs that only provide witness UTXO data. */
|
|
199
215
|
allowLegacyWitnessUtxo?: boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Before signing, require every input to provide a full previous transaction whose txid and
|
|
218
|
+
* selected output match the input. Use this for untrusted or multi-party PSBTs to prevent forged
|
|
219
|
+
* witness-UTXO amounts from hiding an excessive transaction fee.
|
|
220
|
+
*/
|
|
221
|
+
strictPrevoutValidation?: boolean;
|
|
200
222
|
/** Grind ECDSA signatures until they use a low-R encoding. */
|
|
201
223
|
lowR?: boolean;
|
|
202
224
|
/** UNSAFE: additional custom payment-script codecs and finalizers. */
|
|
203
225
|
customScripts?: CustomScript[];
|
|
204
|
-
|
|
205
|
-
|
|
226
|
+
/** Unknown PSBT field policy. Defaults to `strip`. */
|
|
227
|
+
unknown?: Unknowns;
|
|
228
|
+
/** Proprietary PSBT field policy. Defaults to the resolved {@link unknown} policy. */
|
|
229
|
+
proprietary?: Unknowns;
|
|
230
|
+
/**
|
|
231
|
+
* Treat an absent PSBTv2 transaction-modifiable field as allowing input/output changes. Older
|
|
232
|
+
* scure versions emitted PSBTv2 without this field, so this opts out of strict BIP370 behavior
|
|
233
|
+
* when upgrading and editing their PSBTs.
|
|
234
|
+
*/
|
|
235
|
+
allowMissingTxModifiable?: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Deprecated alias for {@link unknown}: true selects `ignore`, false selects `strip`.
|
|
238
|
+
* @deprecated Use `unknown`.
|
|
239
|
+
*/
|
|
206
240
|
allowUnknown?: boolean;
|
|
207
241
|
}
|
|
208
242
|
|
|
@@ -301,6 +335,7 @@ function outputBeforeSign(i: TArg<psbt.TransactionOutput>): TRet<psbt.Transactio
|
|
|
301
335
|
* ```
|
|
302
336
|
*/
|
|
303
337
|
export function inputBeforeSign(i: TArg<psbt.TransactionInput>): TRet<TransactionInputRequired> {
|
|
338
|
+
validateObject(i as Record<string, any>, {}, {}, 'i');
|
|
304
339
|
if (i.txid === undefined || i.index === undefined)
|
|
305
340
|
throw new Error('Transaction/input: txid and index required');
|
|
306
341
|
const res = {
|
|
@@ -314,13 +349,65 @@ export function inputBeforeSign(i: TArg<psbt.TransactionInput>): TRet<Transactio
|
|
|
314
349
|
RawInput.encode(res);
|
|
315
350
|
return res as TRet<TransactionInputRequired>;
|
|
316
351
|
}
|
|
317
|
-
|
|
352
|
+
type ExtensionMap = { unknown?: unknown[]; proprietary?: unknown[] };
|
|
353
|
+
const cleanExtensions = <T extends ExtensionMap>(
|
|
354
|
+
map: T,
|
|
355
|
+
unknownMode: Unknowns,
|
|
356
|
+
proprietaryMode: Unknowns,
|
|
357
|
+
rejectStrip = false
|
|
358
|
+
): T => {
|
|
359
|
+
const out = { ...map } as T & Record<string, unknown>;
|
|
360
|
+
for (const [name, mode] of [
|
|
361
|
+
['unknown', unknownMode],
|
|
362
|
+
['proprietary', proprietaryMode],
|
|
363
|
+
] as const) {
|
|
364
|
+
const value = out[name];
|
|
365
|
+
// Policy cleanup must not reinterpret malformed caller metadata as an empty keyed map.
|
|
366
|
+
if (value !== undefined) u.aarray(value, `${name} PSBT field`);
|
|
367
|
+
const rows = value as unknown[] | undefined;
|
|
368
|
+
if (!rows?.length) {
|
|
369
|
+
delete out[name];
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (mode === 'strict')
|
|
373
|
+
throw new Error(`PSBT: ${name} PSBT field is not allowed in strict mode`);
|
|
374
|
+
if (mode === 'strip') {
|
|
375
|
+
// Silent stripping is appropriate at relay/cleanup boundaries. On direct mutation it would
|
|
376
|
+
// hide a caller bug by accepting metadata that can never become transaction state.
|
|
377
|
+
if (rejectStrip)
|
|
378
|
+
throw new Error(`PSBT: ${name} PSBT field cannot be supplied when policy is strip`);
|
|
379
|
+
delete out[name];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return out;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const cleanTxModifiable = (value: number | undefined, mode: Unknowns): number | undefined => {
|
|
386
|
+
if (value === undefined || !(value & ~0b111)) return value;
|
|
387
|
+
if (mode === 'strict') throw new Error('PSBT: unknown txModifiable bits in strict mode');
|
|
388
|
+
return mode === 'strip' ? value & 0b111 : value;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
function cleanFinalInput(
|
|
392
|
+
i: TArg<PSBTInputs>,
|
|
393
|
+
unknownMode: Unknowns = 'strip',
|
|
394
|
+
proprietaryMode: Unknowns = 'strip'
|
|
395
|
+
) {
|
|
318
396
|
const _i = i as PSBTInputs;
|
|
397
|
+
const extensions = cleanExtensions(_i as ExtensionMap, unknownMode, proprietaryMode);
|
|
398
|
+
if (extensions.unknown) _i.unknown = extensions.unknown as PSBTInputs['unknown'];
|
|
399
|
+
else delete _i.unknown;
|
|
400
|
+
if (extensions.proprietary) _i.proprietary = extensions.proprietary as PSBTInputs['proprietary'];
|
|
401
|
+
else delete _i.proprietary;
|
|
319
402
|
// BIP174 finalizers clear non-final input metadata after constructing final scripts/witnesses.
|
|
320
403
|
// That intentionally drops sighashType here, so post-finalize mutation becomes conservative
|
|
321
|
-
// until callers explicitly
|
|
404
|
+
// until callers explicitly clear satisfaction by removing finalScriptSig/finalScriptWitness.
|
|
322
405
|
for (const _k in _i) {
|
|
323
406
|
const k = _k as keyof PSBTInputs;
|
|
407
|
+
// Proprietary records are cleanup metadata too, but callers may need their opaque protocol
|
|
408
|
+
// state after finalization for PSBT coordination outside transaction extraction. An empty
|
|
409
|
+
// keyed list encodes no records, so canonicalize it to absence like its serialized clone.
|
|
410
|
+
if (proprietaryMode === 'ignore' && k === 'proprietary' && _i.proprietary?.length) continue;
|
|
324
411
|
if (!psbt.PSBTInputFinalKeys.includes(k)) delete _i[k];
|
|
325
412
|
}
|
|
326
413
|
}
|
|
@@ -343,9 +430,27 @@ function unpackSighash(hashType: number) {
|
|
|
343
430
|
};
|
|
344
431
|
}
|
|
345
432
|
|
|
433
|
+
const sighashScope = (sighash: number) => ({
|
|
434
|
+
sigInputs: sighash & SignatureHash.ANYONECANPAY,
|
|
435
|
+
sigOutputs: sighash === SignatureHash.DEFAULT ? SignatureHash.ALL : sighash & 0b11,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const normalizeUnknowns = (
|
|
439
|
+
name: 'unknown' | 'proprietary',
|
|
440
|
+
mode: Unknowns | undefined,
|
|
441
|
+
legacy: boolean | undefined,
|
|
442
|
+
fallback: Unknowns = 'strip'
|
|
443
|
+
): Unknowns => {
|
|
444
|
+
const alias = legacy === undefined ? undefined : legacy ? 'ignore' : 'strip';
|
|
445
|
+
if (mode !== undefined && mode !== 'ignore' && mode !== 'strip' && mode !== 'strict')
|
|
446
|
+
throw new Error(`Transaction options wrong value: ${name}=${mode}`);
|
|
447
|
+
if (mode !== undefined && alias !== undefined && mode !== alias)
|
|
448
|
+
throw new Error(`Transaction options: conflicting ${name} options`);
|
|
449
|
+
return mode || alias || fallback;
|
|
450
|
+
};
|
|
451
|
+
|
|
346
452
|
function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
|
|
347
|
-
if (opts !== undefined
|
|
348
|
-
throw new Error(`Wrong object type for transaction options: ${opts}`);
|
|
453
|
+
if (opts !== undefined) validateObject(opts as Record<string, any>, {}, {}, 'opts');
|
|
349
454
|
|
|
350
455
|
const _opts = {
|
|
351
456
|
...opts,
|
|
@@ -360,6 +465,7 @@ function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
|
|
|
360
465
|
_opts.allowUnknownInputs = _opts.allowUnknowInput;
|
|
361
466
|
if (typeof _opts.allowUnknowOutput !== 'undefined')
|
|
362
467
|
_opts.allowUnknownOutputs = _opts.allowUnknowOutput;
|
|
468
|
+
if (_opts.allowMissingTxModifiable === undefined) _opts.allowMissingTxModifiable = true;
|
|
363
469
|
if (typeof _opts.lockTime !== 'number') throw new Error('Transaction lock time should be number');
|
|
364
470
|
P.U32LE.encode(_opts.lockTime); // Additional range checks that lockTime
|
|
365
471
|
// There is no PSBT v1, and any new version will probably have fields which we don't know how to parse, which
|
|
@@ -374,20 +480,28 @@ function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
|
|
|
374
480
|
'disableScriptCheck',
|
|
375
481
|
'bip174jsCompat',
|
|
376
482
|
'allowLegacyWitnessUtxo',
|
|
483
|
+
'strictPrevoutValidation',
|
|
377
484
|
'lowR',
|
|
485
|
+
'allowUnknown',
|
|
486
|
+
'allowMissingTxModifiable',
|
|
378
487
|
] as const) {
|
|
379
488
|
const v = _opts[k];
|
|
380
489
|
if (v === undefined) continue; // optional
|
|
381
490
|
if (typeof v !== 'boolean')
|
|
382
491
|
throw new Error(`Transation options wrong type: ${k}=${v} (${typeof v})`);
|
|
383
492
|
}
|
|
493
|
+
_opts.unknown = normalizeUnknowns('unknown', _opts.unknown, _opts.allowUnknown);
|
|
494
|
+
_opts.proprietary = normalizeUnknowns('proprietary', _opts.proprietary, undefined, _opts.unknown);
|
|
384
495
|
// 0 and -1 happens in tests
|
|
496
|
+
// With allowUnknownVersion any numeric version is fine; the ternary was inverted
|
|
497
|
+
// before 2026-07 (audit), which made the option throw for every numeric version.
|
|
385
498
|
if (
|
|
386
499
|
_opts.allowUnknownVersion
|
|
387
|
-
? typeof _opts.version
|
|
500
|
+
? typeof _opts.version !== 'number'
|
|
388
501
|
: ![-1, 0, 1, 2, 3].includes(_opts.version)
|
|
389
502
|
)
|
|
390
503
|
throw new Error(`Unknown version: ${_opts.version}`);
|
|
504
|
+
P.I32LE.encode(_opts.version); // Validate the signed transaction-version wire domain.
|
|
391
505
|
if (_opts.customScripts !== undefined) {
|
|
392
506
|
const cs = _opts.customScripts;
|
|
393
507
|
if (!Array.isArray(cs)) {
|
|
@@ -405,13 +519,117 @@ function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
|
|
|
405
519
|
return Object.freeze(_opts) as TRet<Readonly<TxOpts>>;
|
|
406
520
|
}
|
|
407
521
|
|
|
522
|
+
function checkTaprootInputCommitments(input: TArg<PSBTInputs>, prevScript: TArg<Bytes>): void {
|
|
523
|
+
const output = _WitnessOutScript.decode(prevScript);
|
|
524
|
+
const hasTaprootCommitments =
|
|
525
|
+
input.tapInternalKey !== undefined ||
|
|
526
|
+
input.tapMerkleRoot !== undefined ||
|
|
527
|
+
// Repeated keyed fields only exist on the PSBT wire when at least one entry is encoded.
|
|
528
|
+
!!input.tapLeafScript?.length;
|
|
529
|
+
if (output.type !== 'tr') {
|
|
530
|
+
if (hasTaprootCommitments)
|
|
531
|
+
throw new Error('validateInput: Taproot metadata without P2TR previous output');
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const checkOutputKey = (
|
|
536
|
+
internalKey: TArg<Bytes>,
|
|
537
|
+
merkleRoot: TArg<Bytes>,
|
|
538
|
+
parity?: number
|
|
539
|
+
): void => {
|
|
540
|
+
const [outputKey, outputParity] = u.taprootTweakPubkey(internalKey, merkleRoot);
|
|
541
|
+
if (!equalBytes(outputKey, output.pubkey))
|
|
542
|
+
throw new Error('validateInput: Taproot commitment does not match previous output');
|
|
543
|
+
if (parity !== undefined && outputParity !== parity)
|
|
544
|
+
throw new Error('validateInput: Taproot control-block parity does not match previous output');
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
if (input.tapLeafScript) {
|
|
548
|
+
for (const [controlBlock, scriptWithVersion] of input.tapLeafScript) {
|
|
549
|
+
const leafVersion = scriptWithVersion[scriptWithVersion.length - 1];
|
|
550
|
+
const script = scriptWithVersion.subarray(0, -1);
|
|
551
|
+
let merkleRoot = tapLeafHash(script, leafVersion);
|
|
552
|
+
for (const sibling of controlBlock.merklePath) {
|
|
553
|
+
merkleRoot =
|
|
554
|
+
u.compareBytes(sibling, merkleRoot) === -1
|
|
555
|
+
? u.tagSchnorr('TapBranch', sibling, merkleRoot)
|
|
556
|
+
: u.tagSchnorr('TapBranch', merkleRoot, sibling);
|
|
557
|
+
}
|
|
558
|
+
checkOutputKey(controlBlock.internalKey, merkleRoot, controlBlock.version & 1);
|
|
559
|
+
if (input.tapInternalKey && !equalBytes(input.tapInternalKey, controlBlock.internalKey))
|
|
560
|
+
throw new Error('validateInput: tapInternalKey does not match Taproot control block');
|
|
561
|
+
if (input.tapMerkleRoot && !equalBytes(input.tapMerkleRoot, merkleRoot))
|
|
562
|
+
throw new Error('validateInput: tapMerkleRoot does not match Taproot control block');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// A tree-bearing input can omit the aggregate root while still providing independently
|
|
567
|
+
// verifiable control blocks. With no leaves, an internal key without a root describes the
|
|
568
|
+
// standard key-only (empty-root) commitment.
|
|
569
|
+
if (input.tapInternalKey && (input.tapMerkleRoot || !input.tapLeafScript?.length))
|
|
570
|
+
checkOutputKey(input.tapInternalKey, input.tapMerkleRoot || P.EMPTY);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const LOCKTIME_THRESHOLD = 500_000_000;
|
|
574
|
+
function validateRequiredLocktimes(input: TArg<PSBTInputs>): void {
|
|
575
|
+
const height = input.requiredHeightLocktime;
|
|
576
|
+
if (height !== undefined) {
|
|
577
|
+
anumber(height, 'requiredHeightLocktime');
|
|
578
|
+
if (height === 0 || height >= LOCKTIME_THRESHOLD)
|
|
579
|
+
throw new RangeError(
|
|
580
|
+
`requiredHeightLocktime must be between 1 and ${LOCKTIME_THRESHOLD - 1}, got ${height}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
const time = input.requiredTimeLocktime;
|
|
584
|
+
if (time !== undefined) {
|
|
585
|
+
anumber(time, 'requiredTimeLocktime');
|
|
586
|
+
if (time < LOCKTIME_THRESHOLD || time > 0xffffffff)
|
|
587
|
+
throw new RangeError(
|
|
588
|
+
`requiredTimeLocktime must be between ${LOCKTIME_THRESHOLD} and 4294967295, got ${time}`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function resolvePSBTLocktime(
|
|
594
|
+
inputs: TArg<readonly PSBTInputs[]>,
|
|
595
|
+
fallback = DEFAULT_LOCKTIME
|
|
596
|
+
): number {
|
|
597
|
+
let height = DEFAULT_LOCKTIME;
|
|
598
|
+
let time = DEFAULT_LOCKTIME;
|
|
599
|
+
let hasRequirements = false;
|
|
600
|
+
let heightSupported = true;
|
|
601
|
+
let timeSupported = true;
|
|
602
|
+
for (const input of inputs) {
|
|
603
|
+
validateRequiredLocktimes(input);
|
|
604
|
+
const hasHeight = input.requiredHeightLocktime !== undefined;
|
|
605
|
+
const hasTime = input.requiredTimeLocktime !== undefined;
|
|
606
|
+
if (!hasHeight && !hasTime) continue;
|
|
607
|
+
hasRequirements = true;
|
|
608
|
+
if (hasHeight) height = Math.max(height, input.requiredHeightLocktime!);
|
|
609
|
+
else heightSupported = false;
|
|
610
|
+
if (hasTime) time = Math.max(time, input.requiredTimeLocktime!);
|
|
611
|
+
else timeSupported = false;
|
|
612
|
+
}
|
|
613
|
+
if (!hasRequirements) return fallback;
|
|
614
|
+
// BIP370 requires height when every relevant input supports both domains.
|
|
615
|
+
if (heightSupported) return height;
|
|
616
|
+
if (timeSupported) return time;
|
|
617
|
+
throw new Error('PSBTv2: incompatible height-based and time-based locktime requirements');
|
|
618
|
+
}
|
|
619
|
+
|
|
408
620
|
// NOTE: we cannot do this inside PSBTInput coder, because there is no index/txid at this point!
|
|
409
|
-
function validateInput(
|
|
621
|
+
function validateInput(
|
|
622
|
+
i: TArg<psbt.TransactionInput>,
|
|
623
|
+
disableScriptCheck = false
|
|
624
|
+
): TRet<PSBTInputs> {
|
|
625
|
+
validateObject(i as Record<string, any>, {}, {}, 'i');
|
|
410
626
|
const _i = i as PSBTInputs;
|
|
627
|
+
validateRequiredLocktimes(_i);
|
|
628
|
+
let prevOut: P.UnwrapCoder<typeof RawOutput> | undefined;
|
|
411
629
|
if (_i.nonWitnessUtxo && _i.index !== undefined) {
|
|
412
630
|
const last = _i.nonWitnessUtxo.outputs.length - 1;
|
|
413
631
|
if (_i.index > last) throw new Error(`validateInput: index(${_i.index}) not in nonWitnessUtxo`);
|
|
414
|
-
|
|
632
|
+
prevOut = _i.nonWitnessUtxo.outputs[_i.index];
|
|
415
633
|
if (
|
|
416
634
|
_i.witnessUtxo &&
|
|
417
635
|
(!equalBytes(_i.witnessUtxo.script, prevOut.script) ||
|
|
@@ -433,6 +651,9 @@ function validateInput(i: TArg<psbt.TransactionInput>): TRet<PSBTInputs> {
|
|
|
433
651
|
allowUnknownOutputs: true,
|
|
434
652
|
disableScriptCheck: true,
|
|
435
653
|
allowUnknownInputs: true,
|
|
654
|
+
// Consensus does not restrict nVersion; a previous tx with a non-standard
|
|
655
|
+
// version is still spendable and its txid must still be verifiable.
|
|
656
|
+
allowUnknownVersion: true,
|
|
436
657
|
});
|
|
437
658
|
const txid = hex.encode(_i.txid);
|
|
438
659
|
// BIP174 requires the provided nonWitnessUtxo to hash to the prevout txid even when the
|
|
@@ -442,6 +663,10 @@ function validateInput(i: TArg<psbt.TransactionInput>): TRet<PSBTInputs> {
|
|
|
442
663
|
// for any byte-order conversions required by their wire formats.
|
|
443
664
|
if (tx.id !== txid) throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
|
|
444
665
|
}
|
|
666
|
+
} else if (_i.witnessUtxo) prevOut = _i.witnessUtxo;
|
|
667
|
+
if (prevOut && !disableScriptCheck) {
|
|
668
|
+
checkScript(prevOut.script, _i.redeemScript, _i.witnessScript);
|
|
669
|
+
checkTaprootInputCommitments(_i, prevOut.script);
|
|
445
670
|
}
|
|
446
671
|
return _i as TRet<PSBTInputs>;
|
|
447
672
|
}
|
|
@@ -452,6 +677,35 @@ export type PSBTInputs = psbt.PSBTKeyMapKeys<typeof psbt.PSBTInput>;
|
|
|
452
677
|
/** Canonical PSBT output shape used by the coder layer. */
|
|
453
678
|
export type PSBTOutputs = psbt.PSBTKeyMapKeys<typeof psbt.PSBTOutput>;
|
|
454
679
|
|
|
680
|
+
type InputSignature = { sighash: number; taproot: boolean; scriptPath: boolean };
|
|
681
|
+
|
|
682
|
+
const inputSignatures = (input: TArg<PSBTInputs>): InputSignature[] => {
|
|
683
|
+
const _input = input as PSBTInputs;
|
|
684
|
+
const res: InputSignature[] = [];
|
|
685
|
+
const add = (signature: TArg<Bytes>, taproot: boolean, scriptPath = false) => {
|
|
686
|
+
const sig = signature as Bytes;
|
|
687
|
+
if (!sig.length) return;
|
|
688
|
+
// Taproot's 64-byte encoding omits the SIGHASH_DEFAULT byte; every other PSBT signature
|
|
689
|
+
// carries its sighash in the final byte, including signatures imported from another signer.
|
|
690
|
+
const sighash = taproot && sig.length === 64 ? SignatureHash.DEFAULT : sig[sig.length - 1];
|
|
691
|
+
res.push({ sighash, taproot, scriptPath });
|
|
692
|
+
};
|
|
693
|
+
for (const [, signature] of _input.partialSig || []) add(signature, false);
|
|
694
|
+
if (_input.tapKeySig) add(_input.tapKeySig, true);
|
|
695
|
+
for (const [, signature] of _input.tapScriptSig || []) add(signature, true, true);
|
|
696
|
+
return res;
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const inputSignedKeys = {
|
|
700
|
+
// sighashType is signer policy rather than a digest byte, but changing it after one signature
|
|
701
|
+
// exists would make later signers interpret the same input under a different policy.
|
|
702
|
+
self: ['txid', 'index', 'sequence', 'nonWitnessUtxo', 'witnessUtxo', 'sighashType'],
|
|
703
|
+
ecdsa: ['redeemScript', 'witnessScript'],
|
|
704
|
+
tapscript: ['tapLeafScript'],
|
|
705
|
+
cross: ['txid', 'index'],
|
|
706
|
+
prevout: ['nonWitnessUtxo', 'witnessUtxo'],
|
|
707
|
+
} as const satisfies Record<string, readonly (keyof PSBTInputs)[]>;
|
|
708
|
+
|
|
455
709
|
// Normalizes input
|
|
456
710
|
/**
|
|
457
711
|
* Extracts the previous output referenced by an input.
|
|
@@ -465,6 +719,7 @@ export type PSBTOutputs = psbt.PSBTKeyMapKeys<typeof psbt.PSBTOutput>;
|
|
|
465
719
|
* ```
|
|
466
720
|
*/
|
|
467
721
|
export function getPrevOut(input: TArg<psbt.TransactionInput>): P.UnwrapCoder<typeof RawOutput> {
|
|
722
|
+
validateObject(input as Record<string, any>, {}, {}, 'input');
|
|
468
723
|
const _input = input as PSBTInputs;
|
|
469
724
|
if (_input.nonWitnessUtxo) {
|
|
470
725
|
if (_input.index === undefined) throw new Error('Unknown input index');
|
|
@@ -478,8 +733,17 @@ export function getPrevOut(input: TArg<psbt.TransactionInput>): P.UnwrapCoder<ty
|
|
|
478
733
|
)
|
|
479
734
|
throw new Error(`Wrong input index=${_input.index}`);
|
|
480
735
|
return _input.nonWitnessUtxo.outputs[_input.index];
|
|
481
|
-
} else if (
|
|
482
|
-
|
|
736
|
+
} else if ('witnessUtxo' in _input) {
|
|
737
|
+
// The presence check catches malformed provided values; narrow after the guard for TS.
|
|
738
|
+
const prev = _input.witnessUtxo as P.UnwrapCoder<typeof RawOutput>;
|
|
739
|
+
validateObject(prev as Record<string, any>, {}, {}, 'input.witnessUtxo');
|
|
740
|
+
abigint(prev.amount, 'input.witnessUtxo.amount');
|
|
741
|
+
if (!isBytes(prev.script))
|
|
742
|
+
throw new TypeError(
|
|
743
|
+
'"input.witnessUtxo.script" expected Uint8Array, got type=' + typeof prev.script
|
|
744
|
+
);
|
|
745
|
+
return prev;
|
|
746
|
+
} else throw new Error('Cannot find previous output info');
|
|
483
747
|
}
|
|
484
748
|
|
|
485
749
|
/**
|
|
@@ -487,9 +751,12 @@ export function getPrevOut(input: TArg<psbt.TransactionInput>): P.UnwrapCoder<ty
|
|
|
487
751
|
* @param i - input update to normalize
|
|
488
752
|
* @param cur - existing input value to merge with
|
|
489
753
|
* @param allowedFields - fields that may still change on signed inputs
|
|
490
|
-
* @param disableScriptCheck - whether to skip
|
|
491
|
-
* @param
|
|
754
|
+
* @param disableScriptCheck - whether to skip wrapper and Taproot commitment sanity checks
|
|
755
|
+
* @param unknown - handling policy for unknown PSBT fields
|
|
756
|
+
* @param proprietary - handling policy for proprietary PSBT fields
|
|
492
757
|
* @returns Normalized PSBT input.
|
|
758
|
+
* @throws If the update conflicts with the existing input or its signatures. {@link Error}
|
|
759
|
+
* @throws If a numeric input field is outside its wire or protocol range. {@link RangeError}
|
|
493
760
|
* @example
|
|
494
761
|
* Accept hex txids from callers in the same display-order form used by `Transaction.id`, then
|
|
495
762
|
* normalize them into the repo's internal `TransactionInput` shape.
|
|
@@ -508,8 +775,12 @@ export function normalizeInput(
|
|
|
508
775
|
cur?: TArg<PSBTInputs>,
|
|
509
776
|
allowedFields?: TArg<readonly (keyof PSBTInputs)[]>,
|
|
510
777
|
disableScriptCheck = false,
|
|
511
|
-
|
|
778
|
+
unknown: Unknowns | boolean = 'strip',
|
|
779
|
+
proprietary: Unknowns | boolean = 'strip'
|
|
512
780
|
): TRet<PSBTInputs> {
|
|
781
|
+
validateObject(i as Record<string, any>, {}, {}, 'i');
|
|
782
|
+
if (cur !== undefined) validateObject(cur as Record<string, any>, {}, {}, 'cur');
|
|
783
|
+
if (allowedFields !== undefined) u.aarray(allowedFields, 'allowedFields');
|
|
513
784
|
const _i = i as psbt.TransactionInputUpdate;
|
|
514
785
|
const _cur = cur as PSBTInputs | undefined;
|
|
515
786
|
const _allowedFields = allowedFields as readonly (keyof PSBTInputs)[] | undefined;
|
|
@@ -529,18 +800,26 @@ export function normalizeInput(
|
|
|
529
800
|
if (!('nonWitnessUtxo' in _i) && res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo;
|
|
530
801
|
if (res.sequence === undefined) res.sequence = DEFAULT_SEQUENCE;
|
|
531
802
|
if (res.tapMerkleRoot === null) delete res.tapMerkleRoot;
|
|
532
|
-
res = psbt.mergeKeyMap(
|
|
803
|
+
res = psbt.mergeKeyMap(
|
|
804
|
+
psbt.PSBTInput,
|
|
805
|
+
res,
|
|
806
|
+
_cur,
|
|
807
|
+
_allowedFields,
|
|
808
|
+
unknown,
|
|
809
|
+
proprietary
|
|
810
|
+
) as PSBTInputs;
|
|
811
|
+
// An actual empty repeated field emits no PSBT keypairs. Canonicalize only arrays so malformed
|
|
812
|
+
// falsy values still reach the PSBT coder's validation instead of becoming valid absence.
|
|
813
|
+
if (Array.isArray(res.tapLeafScript) && !res.tapLeafScript.length) delete res.tapLeafScript;
|
|
814
|
+
validateRequiredLocktimes(res);
|
|
533
815
|
// Public PSBT coder surface is wrapped with TArg/TRet for TS compatibility; normalizeInput keeps
|
|
534
816
|
// the repo's historical raw internal shape and casts only at the validation boundary here.
|
|
535
817
|
psbt.PSBTInputCoder.encode(res as Parameters<typeof psbt.PSBTInputCoder.encode>[0]); // Validates that everything is correct at this point
|
|
536
818
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if (prevOut && !disableScriptCheck)
|
|
542
|
-
checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
|
|
543
|
-
return res as TRet<PSBTInputs>;
|
|
819
|
+
// Direct construction and UTXO selection consume nonWitnessUtxo amounts without crossing a
|
|
820
|
+
// PSBT serialization boundary. Enforce the same outpoint binding here so a mismatched previous
|
|
821
|
+
// transaction cannot understate a legacy input amount and turn the difference into mining fees.
|
|
822
|
+
return validateInput(res as TArg<psbt.TransactionInput>, disableScriptCheck);
|
|
544
823
|
}
|
|
545
824
|
|
|
546
825
|
/**
|
|
@@ -572,10 +851,13 @@ export function getInputType(input: TArg<psbt.TransactionInput>, allowLegacyWitn
|
|
|
572
851
|
let txType = 'legacy';
|
|
573
852
|
let defaultSighash: number = SignatureHash.ALL;
|
|
574
853
|
const prevOut = getPrevOut(_input as TArg<psbt.TransactionInput>);
|
|
575
|
-
const first =
|
|
854
|
+
const first = _WitnessOutScript.decode(prevOut.script);
|
|
576
855
|
let type = first.type;
|
|
577
856
|
let cur = first;
|
|
578
857
|
const stack = [first];
|
|
858
|
+
// Classification is semantic, but legacy/BIP143 scriptCode and finalization must retain the
|
|
859
|
+
// exact committed spelling (including consensus-valid non-minimal pushes).
|
|
860
|
+
let lastScript = prevOut.script;
|
|
579
861
|
if (first.type === 'tr') {
|
|
580
862
|
// Expected invariant: taproot inputs use PSBT_IN_TAP_* metadata only;
|
|
581
863
|
// legacy redeemScript/witnessScript fields belong to P2SH/P2WSH paths.
|
|
@@ -592,10 +874,11 @@ export function getInputType(input: TArg<psbt.TransactionInput>, allowLegacyWitn
|
|
|
592
874
|
if (first.type === 'wpkh' || first.type === 'wsh') txType = 'segwit';
|
|
593
875
|
if (first.type === 'sh') {
|
|
594
876
|
if (!_input.redeemScript) throw new Error('inputType: sh without redeemScript');
|
|
595
|
-
let child =
|
|
877
|
+
let child = _WitnessOutScript.decode(_input.redeemScript);
|
|
596
878
|
if (child.type === 'wpkh' || child.type === 'wsh') txType = 'segwit';
|
|
597
879
|
stack.push(child);
|
|
598
880
|
cur = child;
|
|
881
|
+
lastScript = _input.redeemScript;
|
|
599
882
|
type += `-${child.type}`;
|
|
600
883
|
}
|
|
601
884
|
// wsh can be inside sh
|
|
@@ -605,12 +888,12 @@ export function getInputType(input: TArg<psbt.TransactionInput>, allowLegacyWitn
|
|
|
605
888
|
if (child.type === 'wsh') txType = 'segwit';
|
|
606
889
|
stack.push(child);
|
|
607
890
|
cur = child;
|
|
891
|
+
lastScript = _input.witnessScript;
|
|
608
892
|
type += `-${child.type}`;
|
|
609
893
|
}
|
|
610
894
|
const last = stack[stack.length - 1];
|
|
611
895
|
if (last.type === 'sh' || last.type === 'wsh')
|
|
612
896
|
throw new Error('inputType: sh/wsh cannot be terminal type');
|
|
613
|
-
const lastScript = OutScript.encode(last);
|
|
614
897
|
const res = {
|
|
615
898
|
type,
|
|
616
899
|
txType,
|
|
@@ -657,8 +940,62 @@ export class Transaction {
|
|
|
657
940
|
constructor(opts: TxOpts = {}) {
|
|
658
941
|
const _opts = (this.opts = validateOpts(opts));
|
|
659
942
|
// Merge with global structure of PSBTv2
|
|
660
|
-
|
|
943
|
+
// Bitcoin Core sets fallback even when it is zero. Matching its common encoding reduces the
|
|
944
|
+
// fingerprint of locally created PSBTv2s; imported PSBTs replace this map and retain omission.
|
|
945
|
+
this.global.fallbackLocktime = _opts.lockTime;
|
|
661
946
|
this.global.txVersion = _opts.version;
|
|
947
|
+
// A locally-created PSBTv2 is still under construction. Imported PSBTs replace this global
|
|
948
|
+
// map below, so an omitted field there retains BIP370's immutable meaning.
|
|
949
|
+
if (_opts.PSBTVersion === 2) this.global.txModifiable = 0b011;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
private isPSBTv2(): boolean {
|
|
953
|
+
return (this.global.version ?? this.opts.PSBTVersion) === 2;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private requireTxModifiable(bit: number, kind: 'inputs' | 'outputs'): void {
|
|
957
|
+
if (!this.isPSBTv2()) return;
|
|
958
|
+
if (!(this.txModifiablePolicy() & bit)) throw new Error(`PSBTv2: ${kind} are not modifiable`);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
private txModifiablePolicy(
|
|
962
|
+
allowMissing = this.opts.allowMissingTxModifiable,
|
|
963
|
+
unknownMode = this.opts.unknown!
|
|
964
|
+
): number {
|
|
965
|
+
if (!this.isPSBTv2()) return 0b011;
|
|
966
|
+
if (this.global.txModifiable !== undefined)
|
|
967
|
+
return cleanTxModifiable(this.global.txModifiable, unknownMode)!;
|
|
968
|
+
return allowMissing ? 0b011 : 0;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private modifiable(
|
|
972
|
+
allowMissing = this.opts.allowMissingTxModifiable,
|
|
973
|
+
unknownMode = this.opts.unknown!
|
|
974
|
+
): number {
|
|
975
|
+
let flags = this.txModifiablePolicy(allowMissing, unknownMode);
|
|
976
|
+
let hasOpaqueFinal = false;
|
|
977
|
+
let hasSingle = false;
|
|
978
|
+
for (let idx = 0; idx < this.inputs.length; idx++) {
|
|
979
|
+
const signatures = inputSignatures(this.inputs[idx]);
|
|
980
|
+
if (!signatures.length && this.inputStatus(idx) === 'finalized') hasOpaqueFinal = true;
|
|
981
|
+
for (const { sighash } of signatures) {
|
|
982
|
+
const { isAny, isNone, isSingle } = unpackSighash(sighash);
|
|
983
|
+
if (!isAny) flags &= ~0b001;
|
|
984
|
+
if (!isNone) flags &= ~0b010;
|
|
985
|
+
if (isSingle) hasSingle = true;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
// Bit 2 summarizes signatures rather than granting policy. Preserve an imported summary for
|
|
989
|
+
// opaque or externally managed state, and union in every signature visible to this object.
|
|
990
|
+
if (hasSingle) flags |= 0b100;
|
|
991
|
+
// PSBTv0 and legacy field-less PSBTv2 cannot describe an opaque finalized sighash. Promotion
|
|
992
|
+
// must therefore deny both mutations instead of manufacturing permissions from absence.
|
|
993
|
+
if (hasOpaqueFinal && this.global.txModifiable === undefined) flags &= ~0b011;
|
|
994
|
+
return flags;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
private get txModifiable(): number {
|
|
998
|
+
return this.modifiable();
|
|
662
999
|
}
|
|
663
1000
|
|
|
664
1001
|
// Import
|
|
@@ -696,21 +1033,54 @@ export class Transaction {
|
|
|
696
1033
|
const tx = new Transaction({ ...opts, version, lockTime, PSBTVersion });
|
|
697
1034
|
// We need slice here, because otherwise
|
|
698
1035
|
const inputCount = PSBTVersion === 0 ? unsigned?.inputs.length : parsed.global.inputCount;
|
|
699
|
-
tx.inputs = parsed.inputs.slice(0, inputCount).map(
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1036
|
+
tx.inputs = parsed.inputs.slice(0, inputCount).map((i, j) => {
|
|
1037
|
+
const input = {
|
|
1038
|
+
...parsed.global.unsignedTx?.inputs[j],
|
|
1039
|
+
...i,
|
|
1040
|
+
};
|
|
1041
|
+
// The unsigned transaction's empty scriptSig is framing, not a PSBT_IN_FINAL_SCRIPTSIG
|
|
1042
|
+
// record. Keeping it makes combination conflict with an otherwise identical finalized PSBT.
|
|
1043
|
+
if (!i.finalScriptSig?.length) delete input.finalScriptSig;
|
|
1044
|
+
return validateInput(input, tx.opts.disableScriptCheck) as PSBTInputs;
|
|
1045
|
+
});
|
|
707
1046
|
const outputCount = PSBTVersion === 0 ? unsigned?.outputs.length : parsed.global.outputCount;
|
|
708
|
-
|
|
1047
|
+
// bip174js writes a phantom empty input map when a PSBTv0 transaction has zero inputs. Raw v0
|
|
1048
|
+
// framing necessarily reads it as the first output map, so skip it before pairing real maps
|
|
1049
|
+
// with the unsigned transaction's declared outputs.
|
|
1050
|
+
const hasBip174InputMap =
|
|
1051
|
+
PSBTVersion === 0 &&
|
|
1052
|
+
inputCount === 0 &&
|
|
1053
|
+
Object.keys(parsed.outputs[0] || {}).length === 0 &&
|
|
1054
|
+
((outputCount! > 0 && parsed.outputs.length === outputCount! + 1) ||
|
|
1055
|
+
(outputCount === 0 &&
|
|
1056
|
+
parsed.outputs.length === 2 &&
|
|
1057
|
+
Object.keys(parsed.outputs[1]).length === 0));
|
|
1058
|
+
const outputStart = hasBip174InputMap ? 1 : 0;
|
|
1059
|
+
tx.outputs = parsed.outputs.slice(outputStart, outputStart + outputCount!).map((i, j) => ({
|
|
709
1060
|
...i,
|
|
710
1061
|
...parsed.global.unsignedTx?.outputs[j],
|
|
711
1062
|
}));
|
|
712
|
-
|
|
713
|
-
|
|
1063
|
+
const unknownMode = tx.opts.unknown!;
|
|
1064
|
+
const proprietaryMode = tx.opts.proprietary!;
|
|
1065
|
+
// Unknown PSBT rows can carry opaque metadata between participants. The documented default is
|
|
1066
|
+
// to strip them; callers that need forward compatibility must opt in explicitly. Proprietary
|
|
1067
|
+
// (0xfc) rows follow the same explicit policy, which defaults to the resolved unknown mode.
|
|
1068
|
+
tx.global = cleanExtensions(
|
|
1069
|
+
{ ...parsed.global, txVersion: version },
|
|
1070
|
+
unknownMode,
|
|
1071
|
+
proprietaryMode
|
|
1072
|
+
);
|
|
1073
|
+
tx.inputs = tx.inputs.map((input) => cleanExtensions(input, unknownMode, proprietaryMode));
|
|
1074
|
+
tx.outputs = tx.outputs.map((output) => cleanExtensions(output, unknownMode, proprietaryMode));
|
|
1075
|
+
if (tx.global.txModifiable !== undefined)
|
|
1076
|
+
tx.global.txModifiable = cleanTxModifiable(tx.global.txModifiable, unknownMode);
|
|
1077
|
+
// A high-level Transaction must have a determinable nLockTime. Raw PSBT coders can still be
|
|
1078
|
+
// used by callers that need to inspect or relay a structurally valid but incompatible PSBT.
|
|
1079
|
+
resolvePSBTLocktime(tx.inputs, tx.global.fallbackLocktime ?? DEFAULT_LOCKTIME);
|
|
1080
|
+
// PSBTv0 always provides nLockTime in its unsigned transaction. Retain zero internally too so
|
|
1081
|
+
// promotion to v2 matches fresh construction and Bitcoin Core rather than gaining a
|
|
1082
|
+
// fingerprint.
|
|
1083
|
+
if (PSBTVersion === 0) tx.global.fallbackLocktime = def(lockTime, DEFAULT_LOCKTIME);
|
|
714
1084
|
return tx;
|
|
715
1085
|
}
|
|
716
1086
|
// Prefer `global.version` when present so cross-version combiners can serialize at the highest
|
|
@@ -718,6 +1088,7 @@ export class Transaction {
|
|
|
718
1088
|
toPSBT(
|
|
719
1089
|
PSBTVersion: number | undefined = this.global.version || this.opts.PSBTVersion
|
|
720
1090
|
): Uint8Array {
|
|
1091
|
+
if (PSBTVersion !== undefined) anumber(PSBTVersion, 'PSBTVersion');
|
|
721
1092
|
if (PSBTVersion !== 0 && PSBTVersion !== 2)
|
|
722
1093
|
throw new Error(`Wrong PSBT version=${PSBTVersion}`);
|
|
723
1094
|
// if (PSBTVersion === 0 && this.inputs.length === 0) {
|
|
@@ -726,9 +1097,17 @@ export class Transaction {
|
|
|
726
1097
|
// );
|
|
727
1098
|
// }
|
|
728
1099
|
const inputs = this.inputs.map((i) =>
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1100
|
+
cleanExtensions(
|
|
1101
|
+
// For PSBTv0 the prevout txid/index live in global.unsignedTx rather than the input map, so
|
|
1102
|
+
// validate the full transaction input before version filtering drops those fields.
|
|
1103
|
+
psbt.cleanPSBTFields(
|
|
1104
|
+
PSBTVersion,
|
|
1105
|
+
psbt.PSBTInput,
|
|
1106
|
+
validateInput(i, this.opts.disableScriptCheck) as TArg<PSBTInputs>
|
|
1107
|
+
),
|
|
1108
|
+
this.opts.unknown!,
|
|
1109
|
+
this.opts.proprietary!
|
|
1110
|
+
)
|
|
732
1111
|
);
|
|
733
1112
|
for (const inp of inputs) {
|
|
734
1113
|
// Don't serialize empty fields
|
|
@@ -736,8 +1115,16 @@ export class Transaction {
|
|
|
736
1115
|
if (inp.finalScriptSig && !inp.finalScriptSig.length) delete inp.finalScriptSig;
|
|
737
1116
|
if (inp.finalScriptWitness && !inp.finalScriptWitness.length) delete inp.finalScriptWitness;
|
|
738
1117
|
}
|
|
739
|
-
const outputs = this.outputs.map((i) =>
|
|
740
|
-
|
|
1118
|
+
const outputs = this.outputs.map((i) =>
|
|
1119
|
+
cleanExtensions(
|
|
1120
|
+
psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTOutput, i),
|
|
1121
|
+
this.opts.unknown!,
|
|
1122
|
+
this.opts.proprietary!
|
|
1123
|
+
)
|
|
1124
|
+
);
|
|
1125
|
+
const global = cleanExtensions({ ...this.global }, this.opts.unknown!, this.opts.proprietary!);
|
|
1126
|
+
if (global.txModifiable !== undefined)
|
|
1127
|
+
global.txModifiable = cleanTxModifiable(global.txModifiable, this.opts.unknown!);
|
|
741
1128
|
if (PSBTVersion === 0) {
|
|
742
1129
|
/*
|
|
743
1130
|
- Bitcoin raw transaction expects to have at least 1 input because it uses case with zero inputs as marker for SegWit
|
|
@@ -762,10 +1149,12 @@ export class Transaction {
|
|
|
762
1149
|
delete global.txVersion;
|
|
763
1150
|
// PSBTv0 carries the unsigned transaction as one blob, so the PSBTv2 framing fields must be
|
|
764
1151
|
// removed here. Keeping `global.version` would make validation treat this rebuilt v0 map as
|
|
765
|
-
// PSBTv2 and reject the required `unsignedTx` field.
|
|
1152
|
+
// PSBTv2 and reject the required `unsignedTx` field. Transaction-modifiable is also v2-only;
|
|
1153
|
+
// its restrictions remain represented by the signatures when explicitly converting to v0.
|
|
766
1154
|
delete global.inputCount;
|
|
767
1155
|
delete global.outputCount;
|
|
768
1156
|
delete global.version;
|
|
1157
|
+
delete global.txModifiable;
|
|
769
1158
|
} else {
|
|
770
1159
|
// Cross-version merges and v0->v2 re-exports can still carry the PSBTv0 unsignedTx blob in
|
|
771
1160
|
// `this.global`, but PSBTv2 serializes the transaction through split global/input/output
|
|
@@ -775,13 +1164,19 @@ export class Transaction {
|
|
|
775
1164
|
global.txVersion = this.version;
|
|
776
1165
|
global.inputCount = this.inputs.length;
|
|
777
1166
|
global.outputCount = this.outputs.length;
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1167
|
+
// Core serializes this optional field exactly as stored. Preserve no-op v2 round-trips;
|
|
1168
|
+
// only v0 promotion and the explicit legacy-omission compatibility mode materialize policy.
|
|
1169
|
+
if (
|
|
1170
|
+
!this.isPSBTv2() ||
|
|
1171
|
+
(global.txModifiable === undefined && this.opts.allowMissingTxModifiable)
|
|
1172
|
+
)
|
|
1173
|
+
global.txModifiable = this.txModifiable;
|
|
784
1174
|
}
|
|
1175
|
+
// bip174js historically emits one empty output map for a PSBTv0 transaction with no outputs.
|
|
1176
|
+
// Input maps are count-framed by the unsigned transaction, so a phantom input map cannot be
|
|
1177
|
+
// represented: with zero inputs it would be decoded as an output map instead. PSBTv2 has
|
|
1178
|
+
// explicit counts for both map arrays and does not use this compatibility encoding.
|
|
1179
|
+
if (this.opts.bip174jsCompat && PSBTVersion === 0 && !outputs.length) outputs.push({});
|
|
785
1180
|
const raw = { global, inputs, outputs };
|
|
786
1181
|
return PSBTVersion === 0
|
|
787
1182
|
? psbt.RawPSBTV0.encode(raw as Parameters<typeof psbt.RawPSBTV0.encode>[0])
|
|
@@ -790,23 +1185,7 @@ export class Transaction {
|
|
|
790
1185
|
|
|
791
1186
|
// BIP370 lockTime (https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#determining-lock-time)
|
|
792
1187
|
get lockTime(): number {
|
|
793
|
-
|
|
794
|
-
let heightCnt = 0;
|
|
795
|
-
let time = DEFAULT_LOCKTIME;
|
|
796
|
-
let timeCnt = 0;
|
|
797
|
-
for (const i of this.inputs) {
|
|
798
|
-
if (i.requiredHeightLocktime) {
|
|
799
|
-
height = Math.max(height, i.requiredHeightLocktime);
|
|
800
|
-
heightCnt++;
|
|
801
|
-
}
|
|
802
|
-
if (i.requiredTimeLocktime) {
|
|
803
|
-
time = Math.max(time, i.requiredTimeLocktime);
|
|
804
|
-
timeCnt++;
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
if (heightCnt && heightCnt >= timeCnt) return height;
|
|
808
|
-
if (time !== DEFAULT_LOCKTIME) return time;
|
|
809
|
-
return this.global.fallbackLocktime || DEFAULT_LOCKTIME;
|
|
1188
|
+
return resolvePSBTLocktime(this.inputs, this.global.fallbackLocktime ?? DEFAULT_LOCKTIME);
|
|
810
1189
|
}
|
|
811
1190
|
|
|
812
1191
|
get version(): number {
|
|
@@ -828,6 +1207,13 @@ export class Transaction {
|
|
|
828
1207
|
if (input.partialSig && input.partialSig.length) return 'signed';
|
|
829
1208
|
return 'unsigned';
|
|
830
1209
|
}
|
|
1210
|
+
private cleanFinalInput(input: PSBTInputs): void {
|
|
1211
|
+
// Core preserves producer policy during finalization. Once cleanup makes signatures opaque,
|
|
1212
|
+
// signStatus conservatively locks transaction mutation until the input is explicitly reopened.
|
|
1213
|
+
cleanFinalInput(input as TArg<PSBTInputs>, this.opts.unknown!, this.opts.proprietary!);
|
|
1214
|
+
if (this.global.txModifiable !== undefined)
|
|
1215
|
+
this.global.txModifiable = cleanTxModifiable(this.global.txModifiable, this.opts.unknown!);
|
|
1216
|
+
}
|
|
831
1217
|
// Cannot replace unpackSighash, tests rely on very generic implemenetation with signing inputs outside of range
|
|
832
1218
|
// We will lose some vectors -> smaller test coverage of preimages (very important!)
|
|
833
1219
|
private inputSighash(idx: number) {
|
|
@@ -840,30 +1226,70 @@ export class Transaction {
|
|
|
840
1226
|
// ALL + ANYONE -- specific input + all outputs
|
|
841
1227
|
// NONE + ANYONE -- specific input + no outputs
|
|
842
1228
|
// SINGLE -- specific inputs + output with same index
|
|
843
|
-
|
|
844
|
-
const sigInputs = sighash & SignatureHash.ANYONECANPAY;
|
|
845
|
-
return { sigInputs, sigOutputs };
|
|
1229
|
+
return sighashScope(sighash);
|
|
846
1230
|
}
|
|
847
1231
|
// Very nice for debug purposes, but slow. If there is too much inputs/outputs to add, will be quadratic.
|
|
848
1232
|
// Some cache will be nice, but there chance to have bugs with cache invalidation
|
|
849
|
-
private
|
|
1233
|
+
private signatures() {
|
|
1234
|
+
const res: (InputSignature & { idx: number })[] = [];
|
|
1235
|
+
for (let idx = 0; idx < this.inputs.length; idx++) {
|
|
1236
|
+
const actual = inputSignatures(this.inputs[idx]);
|
|
1237
|
+
for (const signature of actual) res.push({ idx, ...signature });
|
|
1238
|
+
if (actual.length || this.inputStatus(idx) !== 'finalized') continue;
|
|
1239
|
+
let taproot = true;
|
|
1240
|
+
try {
|
|
1241
|
+
taproot = _WitnessOutScript.decode(getPrevOut(this.inputs[idx]).script).type === 'tr';
|
|
1242
|
+
} catch {
|
|
1243
|
+
// Finalization may remove the data needed to classify an imported opaque signature.
|
|
1244
|
+
// Treating it as Taproot conservatively protects the larger all-prevout commitment.
|
|
1245
|
+
}
|
|
1246
|
+
const declared = this.inputs[idx].sighashType;
|
|
1247
|
+
const sighash = declared === undefined ? SignatureHash.DEFAULT : declared;
|
|
1248
|
+
res.push({ idx, sighash, taproot, scriptPath: taproot });
|
|
1249
|
+
}
|
|
1250
|
+
return res;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
private signedInputKeys(idx: number, signatures = this.signatures()): (keyof PSBTInputs)[] {
|
|
1254
|
+
const res = new Set<keyof PSBTInputs>();
|
|
1255
|
+
const add = (keys: readonly (keyof PSBTInputs)[]) => {
|
|
1256
|
+
for (const key of keys) res.add(key);
|
|
1257
|
+
};
|
|
1258
|
+
for (const signature of signatures) {
|
|
1259
|
+
const { isAny, isNone, isSingle } = unpackSighash(signature.sighash);
|
|
1260
|
+
if (signature.idx === idx) {
|
|
1261
|
+
add(inputSignedKeys.self);
|
|
1262
|
+
if (signature.taproot) {
|
|
1263
|
+
if (signature.scriptPath) add(inputSignedKeys.tapscript);
|
|
1264
|
+
} else add(inputSignedKeys.ecdsa);
|
|
1265
|
+
} else if (!isAny) {
|
|
1266
|
+
add(inputSignedKeys.cross);
|
|
1267
|
+
// Legacy and BIP143 omit other sequences for NONE/SINGLE. BIP341 commits every sequence
|
|
1268
|
+
// whenever ANYONECANPAY is absent, independently of the output sighash mode.
|
|
1269
|
+
if (signature.taproot || (!isNone && !isSingle)) res.add('sequence');
|
|
1270
|
+
if (signature.taproot) add(inputSignedKeys.prevout);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return [...res];
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private signStatus(signatures = this.signatures()) {
|
|
850
1277
|
// if addInput or addOutput is not possible, then all inputs or outputs are signed
|
|
851
1278
|
let addInput = true,
|
|
852
1279
|
addOutput = true;
|
|
853
|
-
let inputs = [],
|
|
854
|
-
outputs = [];
|
|
855
|
-
for (
|
|
856
|
-
const
|
|
857
|
-
// Unsigned input doesn't affect anything
|
|
858
|
-
if (status === 'unsigned') continue;
|
|
859
|
-
const { sigInputs, sigOutputs } = this.inputSighash(idx);
|
|
1280
|
+
let inputs: number[] = [],
|
|
1281
|
+
outputs: number[] = [];
|
|
1282
|
+
for (const { idx, sighash } of signatures) {
|
|
1283
|
+
const { sigInputs, sigOutputs } = sighashScope(sighash);
|
|
860
1284
|
// Input type
|
|
861
|
-
if (sigInputs === SignatureHash.ANYONECANPAY)
|
|
862
|
-
|
|
1285
|
+
if (sigInputs === SignatureHash.ANYONECANPAY) {
|
|
1286
|
+
if (!inputs.includes(idx)) inputs.push(idx);
|
|
1287
|
+
} else addInput = false;
|
|
863
1288
|
// Output type
|
|
864
1289
|
if (sigOutputs === SignatureHash.ALL) addOutput = false;
|
|
865
|
-
else if (sigOutputs === SignatureHash.SINGLE)
|
|
866
|
-
|
|
1290
|
+
else if (sigOutputs === SignatureHash.SINGLE) {
|
|
1291
|
+
if (!outputs.includes(idx)) outputs.push(idx);
|
|
1292
|
+
} else if (sigOutputs === SignatureHash.NONE) {
|
|
867
1293
|
// Doesn't affect any outputs at all
|
|
868
1294
|
} else throw new Error(`Wrong signature hash output type: ${sigOutputs}`);
|
|
869
1295
|
}
|
|
@@ -878,27 +1304,33 @@ export class Transaction {
|
|
|
878
1304
|
|
|
879
1305
|
// Info utils
|
|
880
1306
|
get hasWitnesses(): boolean {
|
|
881
|
-
let out = false;
|
|
882
1307
|
for (const i of this.inputs)
|
|
883
|
-
if (i.finalScriptWitness && i.finalScriptWitness.length)
|
|
884
|
-
return
|
|
1308
|
+
if (i.finalScriptWitness && i.finalScriptWitness.length) return true;
|
|
1309
|
+
return false;
|
|
885
1310
|
}
|
|
886
1311
|
// https://en.bitcoin.it/wiki/Weight_units
|
|
887
1312
|
get weight(): number {
|
|
888
1313
|
if (!this.isFinal) throw new Error('Transaction is not finalized');
|
|
1314
|
+
// Serialized length of VarBytes(data) without allocating the encoded copy
|
|
1315
|
+
const varLen = (dataLen: number) => CompactSizeLen.encode(dataLen).length + dataLen;
|
|
1316
|
+
const hasWitnesses = this.hasWitnesses;
|
|
889
1317
|
let out = 32;
|
|
890
1318
|
// Outputs
|
|
891
1319
|
const outputs = this.outputs.map(outputBeforeSign);
|
|
892
1320
|
out += 4 * CompactSizeLen.encode(this.outputs.length).length;
|
|
893
|
-
for (const o of outputs) out += 32 + 4 *
|
|
1321
|
+
for (const o of outputs) out += 32 + 4 * varLen(o.script.length);
|
|
894
1322
|
// Inputs
|
|
895
|
-
if (
|
|
1323
|
+
if (hasWitnesses) out += 2;
|
|
896
1324
|
out += 4 * CompactSizeLen.encode(this.inputs.length).length;
|
|
897
1325
|
for (const i of this.inputs) {
|
|
898
|
-
out += 160 + 4 *
|
|
1326
|
+
out += 160 + 4 * varLen((i.finalScriptSig || P.EMPTY).length);
|
|
899
1327
|
// Once segwit serialization is active, every input contributes one witness vector, including
|
|
900
1328
|
// legacy inputs whose empty vector still encodes as a single zero-item-count byte.
|
|
901
|
-
if (
|
|
1329
|
+
if (hasWitnesses) {
|
|
1330
|
+
const witness = i.finalScriptWitness || [];
|
|
1331
|
+
out += CompactSizeLen.encode(witness.length).length;
|
|
1332
|
+
for (const w of witness) out += varLen(w.length);
|
|
1333
|
+
}
|
|
902
1334
|
}
|
|
903
1335
|
return out;
|
|
904
1336
|
}
|
|
@@ -933,8 +1365,28 @@ export class Transaction {
|
|
|
933
1365
|
}
|
|
934
1366
|
// Input stuff
|
|
935
1367
|
private checkInputIdx(idx: number) {
|
|
936
|
-
|
|
937
|
-
|
|
1368
|
+
anumber(idx, 'idx');
|
|
1369
|
+
if (idx >= this.inputs.length) throw new Error(`Wrong input index=${idx}`);
|
|
1370
|
+
}
|
|
1371
|
+
private validatePrevoutsForSigning(): void {
|
|
1372
|
+
if (!this.opts.strictPrevoutValidation) return;
|
|
1373
|
+
for (let i = 0; i < this.inputs.length; i++) {
|
|
1374
|
+
const input = this.inputs[i];
|
|
1375
|
+
if (!input.nonWitnessUtxo) {
|
|
1376
|
+
throw new Error(
|
|
1377
|
+
`Transaction/sign: strictPrevoutValidation requires nonWitnessUtxo for input=${i}`
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
if (input.txid === undefined || input.index === undefined) {
|
|
1381
|
+
throw new Error(
|
|
1382
|
+
`Transaction/sign: strictPrevoutValidation requires an outpoint for input=${i}`
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
// A full previous transaction is only a trusted amount/script commitment after its txid and
|
|
1386
|
+
// selected output have been checked against the unsigned transaction. Also cross-check a
|
|
1387
|
+
// redundant witnessUtxo when one is present.
|
|
1388
|
+
validateInput(input as TArg<psbt.TransactionInput>, this.opts.disableScriptCheck);
|
|
1389
|
+
}
|
|
938
1390
|
}
|
|
939
1391
|
getInput(idx: number): psbt.TransactionInput {
|
|
940
1392
|
this.checkInputIdx(idx);
|
|
@@ -945,15 +1397,32 @@ export class Transaction {
|
|
|
945
1397
|
}
|
|
946
1398
|
// Modification
|
|
947
1399
|
addInput(input: TArg<psbt.TransactionInputUpdate>, _ignoreSignStatus = false): number {
|
|
948
|
-
|
|
949
|
-
|
|
1400
|
+
validateObject(input as Record<string, any>, {}, {}, 'input');
|
|
1401
|
+
cleanExtensions(input as ExtensionMap, this.opts.unknown!, this.opts.proprietary!, true);
|
|
1402
|
+
this.requireTxModifiable(0b001, 'inputs');
|
|
1403
|
+
const signatures = _ignoreSignStatus ? undefined : this.signatures();
|
|
1404
|
+
const status = signatures && this.signStatus(signatures);
|
|
1405
|
+
if (status && !status.addInput) throw new Error('Tx has signed inputs, cannot add new one');
|
|
950
1406
|
// normalizeInput preserves nested caller-owned byte arrays, so detach them here before the
|
|
951
1407
|
// new input becomes transaction state and later caller mutation can rewrite it by aliasing.
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1408
|
+
const normalized = cloneDeep(
|
|
1409
|
+
normalizeInput(
|
|
1410
|
+
input,
|
|
1411
|
+
undefined,
|
|
1412
|
+
undefined,
|
|
1413
|
+
this.opts.disableScriptCheck,
|
|
1414
|
+
this.opts.unknown!,
|
|
1415
|
+
this.opts.proprietary!
|
|
1416
|
+
)
|
|
1417
|
+
) as PSBTInputs;
|
|
1418
|
+
const nextLockTime = resolvePSBTLocktime(
|
|
1419
|
+
[...this.inputs, normalized],
|
|
1420
|
+
this.global.fallbackLocktime ?? DEFAULT_LOCKTIME
|
|
956
1421
|
);
|
|
1422
|
+
// ANYONECANPAY permits adding an outpoint, but every signature still commits to nLockTime.
|
|
1423
|
+
if (signatures?.length && nextLockTime !== this.lockTime)
|
|
1424
|
+
throw new Error('Tx has signed inputs, cannot change lockTime');
|
|
1425
|
+
this.inputs.push(normalized);
|
|
957
1426
|
return this.inputs.length - 1;
|
|
958
1427
|
}
|
|
959
1428
|
updateInput(
|
|
@@ -962,28 +1431,59 @@ export class Transaction {
|
|
|
962
1431
|
_ignoreSignStatus = false
|
|
963
1432
|
): void {
|
|
964
1433
|
this.checkInputIdx(idx);
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1434
|
+
cleanExtensions(input as ExtensionMap, this.opts.unknown!, this.opts.proprietary!, true);
|
|
1435
|
+
let allowedFields: (keyof PSBTInputs)[] | undefined;
|
|
1436
|
+
const signatures = _ignoreSignStatus ? undefined : this.signatures();
|
|
1437
|
+
if (signatures?.length) {
|
|
1438
|
+
if (this.inputStatus(idx) === 'finalized') {
|
|
1439
|
+
// Once finalized, only already-present signature/final fields may be repeated or removed.
|
|
1440
|
+
// In particular, do not let a native-SegWit final witness gain a stray finalScriptSig.
|
|
1441
|
+
allowedFields = psbt.PSBTInputSignatureKeys.filter(
|
|
1442
|
+
(key) => this.inputs[idx][key] !== undefined
|
|
1443
|
+
);
|
|
1444
|
+
} else {
|
|
1445
|
+
const signed = new Set(this.signedInputKeys(idx, signatures));
|
|
1446
|
+
if (signed.size)
|
|
1447
|
+
allowedFields = (Object.keys(psbt.PSBTInput) as (keyof PSBTInputs)[]).filter(
|
|
1448
|
+
(key) => !signed.has(key)
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
970
1451
|
}
|
|
971
1452
|
// normalizeInput preserves nested caller-owned byte arrays, so detach the merged result here
|
|
972
1453
|
// before the updated input becomes transaction state and later caller mutation can rewrite it.
|
|
973
|
-
|
|
1454
|
+
const normalized = cloneDeep(
|
|
974
1455
|
normalizeInput(
|
|
975
1456
|
input,
|
|
976
1457
|
this.inputs[idx],
|
|
977
1458
|
allowedFields,
|
|
978
1459
|
this.opts.disableScriptCheck,
|
|
979
|
-
this.opts.
|
|
1460
|
+
this.opts.unknown!,
|
|
1461
|
+
this.opts.proprietary!
|
|
980
1462
|
)
|
|
981
1463
|
) as PSBTInputs;
|
|
1464
|
+
const inputs = this.inputs.slice();
|
|
1465
|
+
inputs[idx] = normalized;
|
|
1466
|
+
const nextLockTime = resolvePSBTLocktime(
|
|
1467
|
+
inputs,
|
|
1468
|
+
this.global.fallbackLocktime ?? DEFAULT_LOCKTIME
|
|
1469
|
+
);
|
|
1470
|
+
if (signatures?.length && nextLockTime !== this.lockTime)
|
|
1471
|
+
throw new Error('Tx has signed inputs, cannot change lockTime');
|
|
1472
|
+
const current = this.inputs[idx];
|
|
1473
|
+
const transactionChanged =
|
|
1474
|
+
current.index !== normalized.index ||
|
|
1475
|
+
def(current.sequence, DEFAULT_SEQUENCE) !== def(normalized.sequence, DEFAULT_SEQUENCE) ||
|
|
1476
|
+
(current.txid === undefined
|
|
1477
|
+
? normalized.txid !== undefined
|
|
1478
|
+
: normalized.txid === undefined || !equalBytes(current.txid, normalized.txid)) ||
|
|
1479
|
+
nextLockTime !== this.lockTime;
|
|
1480
|
+
if (transactionChanged) this.requireTxModifiable(0b001, 'inputs');
|
|
1481
|
+
this.inputs[idx] = normalized;
|
|
982
1482
|
}
|
|
983
1483
|
// Output stuff
|
|
984
1484
|
private checkOutputIdx(idx: number) {
|
|
985
|
-
|
|
986
|
-
|
|
1485
|
+
anumber(idx, 'idx');
|
|
1486
|
+
if (idx >= this.outputs.length) throw new Error(`Wrong output index=${idx}`);
|
|
987
1487
|
}
|
|
988
1488
|
getOutput(idx: number): psbt.TransactionOutput {
|
|
989
1489
|
this.checkOutputIdx(idx);
|
|
@@ -993,7 +1493,7 @@ export class Transaction {
|
|
|
993
1493
|
const out = this.getOutput(idx);
|
|
994
1494
|
if (!out.script) return;
|
|
995
1495
|
return Address(network).encode(
|
|
996
|
-
|
|
1496
|
+
_WitnessOutScript.decode(out.script) as Parameters<ReturnType<typeof Address>['encode']>[0]
|
|
997
1497
|
);
|
|
998
1498
|
}
|
|
999
1499
|
|
|
@@ -1005,22 +1505,27 @@ export class Transaction {
|
|
|
1005
1505
|
cur?: PSBTOutputs,
|
|
1006
1506
|
allowedFields?: readonly (keyof typeof psbt.PSBTOutput)[]
|
|
1007
1507
|
): PSBTOutputs {
|
|
1508
|
+
validateObject(o as Record<string, any>, {}, {}, 'o');
|
|
1008
1509
|
let { amount, script } = o;
|
|
1009
1510
|
if (amount === undefined) amount = cur?.amount;
|
|
1010
|
-
|
|
1011
|
-
throw new Error(
|
|
1012
|
-
`Wrong amount type, should be of type bigint in sats, but got ${amount} of type ${typeof amount}`
|
|
1013
|
-
);
|
|
1511
|
+
amount = abigint(amount, 'o.amount');
|
|
1014
1512
|
if (typeof script === 'string') script = hex.decode(script);
|
|
1015
1513
|
if (script === undefined) script = cur?.script;
|
|
1016
1514
|
let res: PSBTOutputs = { ...cur, ...(o as PSBTOutputs & { script?: string }), amount, script };
|
|
1017
1515
|
if (res.amount === undefined) delete res.amount;
|
|
1018
|
-
res = psbt.mergeKeyMap(
|
|
1516
|
+
res = psbt.mergeKeyMap(
|
|
1517
|
+
psbt.PSBTOutput,
|
|
1518
|
+
res,
|
|
1519
|
+
cur,
|
|
1520
|
+
allowedFields,
|
|
1521
|
+
this.opts.unknown!,
|
|
1522
|
+
this.opts.proprietary!
|
|
1523
|
+
);
|
|
1019
1524
|
psbt.PSBTOutputCoder.encode(res as Parameters<typeof psbt.PSBTOutputCoder.encode>[0]);
|
|
1020
1525
|
if (
|
|
1021
1526
|
res.script &&
|
|
1022
1527
|
!this.opts.allowUnknownOutputs &&
|
|
1023
|
-
|
|
1528
|
+
_WitnessOutScript.decode(res.script).type === 'unknown'
|
|
1024
1529
|
) {
|
|
1025
1530
|
throw new Error(
|
|
1026
1531
|
'Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
|
|
@@ -1030,7 +1535,11 @@ export class Transaction {
|
|
|
1030
1535
|
return res;
|
|
1031
1536
|
}
|
|
1032
1537
|
addOutput(o: TArg<psbt.TransactionOutputUpdate>, _ignoreSignStatus = false): number {
|
|
1033
|
-
|
|
1538
|
+
cleanExtensions(o as ExtensionMap, this.opts.unknown!, this.opts.proprietary!, true);
|
|
1539
|
+
this.requireTxModifiable(0b010, 'outputs');
|
|
1540
|
+
const status = _ignoreSignStatus ? undefined : this.signStatus();
|
|
1541
|
+
// Appending the previously missing same-index output changes a SIGHASH_SINGLE digest.
|
|
1542
|
+
if (status && (!status.addOutput || status.outputs.includes(this.outputs.length)))
|
|
1034
1543
|
throw new Error('Tx has signed outputs, cannot add new one');
|
|
1035
1544
|
// normalizeOutput preserves nested caller-owned script bytes, so detach them here before the
|
|
1036
1545
|
// new output becomes transaction state and later caller mutation can rewrite it by aliasing.
|
|
@@ -1043,6 +1552,7 @@ export class Transaction {
|
|
|
1043
1552
|
_ignoreSignStatus = false
|
|
1044
1553
|
): void {
|
|
1045
1554
|
this.checkOutputIdx(idx);
|
|
1555
|
+
cleanExtensions(output as ExtensionMap, this.opts.unknown!, this.opts.proprietary!, true);
|
|
1046
1556
|
let allowedFields = undefined;
|
|
1047
1557
|
if (!_ignoreSignStatus) {
|
|
1048
1558
|
const status = this.signStatus();
|
|
@@ -1051,7 +1561,15 @@ export class Transaction {
|
|
|
1051
1561
|
}
|
|
1052
1562
|
// updateOutput replaces stored state with normalizeOutput(...) directly, so detach the result
|
|
1053
1563
|
// before storing it or later caller mutation of `output.script` will rewrite transaction state.
|
|
1054
|
-
|
|
1564
|
+
const current = this.outputs[idx];
|
|
1565
|
+
const normalized = cloneDeep(this.normalizeOutput(output, current, allowedFields));
|
|
1566
|
+
const transactionChanged =
|
|
1567
|
+
current.amount !== normalized.amount ||
|
|
1568
|
+
(current.script === undefined
|
|
1569
|
+
? normalized.script !== undefined
|
|
1570
|
+
: normalized.script === undefined || !equalBytes(current.script, normalized.script));
|
|
1571
|
+
if (transactionChanged) this.requireTxModifiable(0b010, 'outputs');
|
|
1572
|
+
this.outputs[idx] = normalized;
|
|
1055
1573
|
}
|
|
1056
1574
|
addOutputAddress(address: string, amount: bigint, network: u.BTC_NETWORK = NETWORK): number {
|
|
1057
1575
|
return this.addOutput({
|
|
@@ -1065,7 +1583,7 @@ export class Transaction {
|
|
|
1065
1583
|
}
|
|
1066
1584
|
// Utils
|
|
1067
1585
|
get fee(): bigint {
|
|
1068
|
-
let res =
|
|
1586
|
+
let res = _0n;
|
|
1069
1587
|
for (const i of this.inputs) {
|
|
1070
1588
|
const prevOut = getPrevOut(i);
|
|
1071
1589
|
if (!prevOut) throw new Error('Empty input amount');
|
|
@@ -1084,7 +1602,8 @@ export class Transaction {
|
|
|
1084
1602
|
const { isAny, isNone, isSingle } = unpackSighash(hashType);
|
|
1085
1603
|
if (idx < 0 || !Number.isSafeInteger(idx)) throw new Error(`Invalid input idx=${idx}`);
|
|
1086
1604
|
if ((isSingle && idx >= this.outputs.length) || idx >= this.inputs.length)
|
|
1087
|
-
|
|
1605
|
+
// Bitcoin Core passes uint256::ONE's internal little-endian bytes directly to ECDSA.
|
|
1606
|
+
return P.U256LE.encode(_1n);
|
|
1088
1607
|
prevOutScript = stripCodeSeparator(prevOutScript);
|
|
1089
1608
|
let inputs: TransactionInputRequired[] = this.inputs
|
|
1090
1609
|
.map(inputBeforeSign)
|
|
@@ -1124,8 +1643,8 @@ export class Transaction {
|
|
|
1124
1643
|
): Uint8Array {
|
|
1125
1644
|
// BIP143 serializes txTo.vin[nIn].prevout and txTo.vin[nIn].nSequence, so reject an invalid
|
|
1126
1645
|
// nIn explicitly instead of leaking a later undefined-input TypeError from inputs[idx].
|
|
1127
|
-
|
|
1128
|
-
|
|
1646
|
+
anumber(idx, 'idx');
|
|
1647
|
+
if (idx >= this.inputs.length) throw new Error(`Invalid input idx=${idx}`);
|
|
1129
1648
|
const { isAny, isNone, isSingle } = unpackSighash(hashType);
|
|
1130
1649
|
let inputHash = EMPTY32;
|
|
1131
1650
|
let sequenceHash = EMPTY32;
|
|
@@ -1164,15 +1683,16 @@ export class Transaction {
|
|
|
1164
1683
|
leafVer = 0xc0,
|
|
1165
1684
|
annex?: Bytes
|
|
1166
1685
|
): Uint8Array {
|
|
1167
|
-
if (!Array.isArray(amount) || this.inputs.length !== amount.length)
|
|
1168
|
-
throw new Error(`Invalid amounts array=${amount}`);
|
|
1169
|
-
if (!Array.isArray(prevOutScript) || this.inputs.length !== prevOutScript.length)
|
|
1170
|
-
throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
|
|
1171
1686
|
// BIP341 SigMsg commits either to input_index or to the selected input's outpoint/amount/script/
|
|
1172
1687
|
// sequence under ANYONECANPAY, so reject an invalid index explicitly instead of hashing a
|
|
1173
1688
|
// nonexistent input or leaking a later integer-encoding RangeError for negative idx.
|
|
1174
|
-
|
|
1175
|
-
|
|
1689
|
+
anumber(idx, 'idx');
|
|
1690
|
+
if (idx >= this.inputs.length) throw new Error(`Invalid input idx=${idx}`);
|
|
1691
|
+
u.aarray(amount, 'amount');
|
|
1692
|
+
u.aarray(prevOutScript, 'prevOutScript');
|
|
1693
|
+
if (this.inputs.length !== amount.length) throw new Error(`Invalid amounts array=${amount}`);
|
|
1694
|
+
if (this.inputs.length !== prevOutScript.length)
|
|
1695
|
+
throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
|
|
1176
1696
|
const out: Bytes[] = [
|
|
1177
1697
|
P.U8.encode(0),
|
|
1178
1698
|
P.U8.encode(hashType), // U8 sigHash
|
|
@@ -1183,6 +1703,14 @@ export class Transaction {
|
|
|
1183
1703
|
const inType = hashType & SignatureHash.ANYONECANPAY;
|
|
1184
1704
|
const inputs = this.inputs.map(inputBeforeSign);
|
|
1185
1705
|
const outputs = this.outputs.map(outputBeforeSign);
|
|
1706
|
+
// Unlike legacy and segwit v0, BIP341 defines no digest for SINGLE when the
|
|
1707
|
+
// corresponding output does not exist. Returning a digest here would produce
|
|
1708
|
+
// signatures that consensus can never accept.
|
|
1709
|
+
if (outType === SignatureHash.SINGLE && idx >= outputs.length) {
|
|
1710
|
+
throw new Error(
|
|
1711
|
+
`Input with sighash SINGLE, but there is no output with corresponding index=${idx}`
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1186
1714
|
if (inType !== SignatureHash.ANYONECANPAY) {
|
|
1187
1715
|
out.push(
|
|
1188
1716
|
...[
|
|
@@ -1208,16 +1736,31 @@ export class Transaction {
|
|
|
1208
1736
|
);
|
|
1209
1737
|
} else out.push(P.U32LE.encode(idx));
|
|
1210
1738
|
if (spendType & 1) out.push(u.sha256(VarBytes.encode(annex || P.EMPTY)));
|
|
1211
|
-
if (outType === SignatureHash.SINGLE)
|
|
1212
|
-
out.push(idx < outputs.length ? u.sha256(RawOutput.encode(outputs[idx])) : EMPTY32);
|
|
1739
|
+
if (outType === SignatureHash.SINGLE) out.push(u.sha256(RawOutput.encode(outputs[idx])));
|
|
1213
1740
|
if (leafScript)
|
|
1214
1741
|
out.push(tapLeafHash(leafScript, leafVer), P.U8.encode(0), P.I32LE.encode(codeSeparator));
|
|
1215
1742
|
return u.tagSchnorr('TapSighash', ...out);
|
|
1216
1743
|
}
|
|
1217
1744
|
// Signer can be privateKey OR instance of bip32 HD stuff
|
|
1218
1745
|
signIdx(privateKey: Signer, idx: number, allowedSighash?: SigHash[], _auxRand?: Bytes): boolean {
|
|
1746
|
+
if (!isBytes(privateKey)) {
|
|
1747
|
+
// HDKey is a structural external instance, so plain-object validation would
|
|
1748
|
+
// reject valid signers.
|
|
1749
|
+
if (
|
|
1750
|
+
!privateKey ||
|
|
1751
|
+
typeof privateKey !== 'object' ||
|
|
1752
|
+
typeof (privateKey as HDKey).deriveChild !== 'function'
|
|
1753
|
+
)
|
|
1754
|
+
throw new TypeError(
|
|
1755
|
+
'"privateKey" expected Uint8Array or HDKey, got type=' + typeof privateKey
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1219
1758
|
this.checkInputIdx(idx);
|
|
1220
|
-
|
|
1759
|
+
this.validatePrevoutsForSigning();
|
|
1760
|
+
const input = validateInput(
|
|
1761
|
+
this.inputs[idx] as TArg<psbt.TransactionInput>,
|
|
1762
|
+
this.opts.disableScriptCheck
|
|
1763
|
+
);
|
|
1221
1764
|
const inputType = getInputType(
|
|
1222
1765
|
input as TArg<psbt.TransactionInput>,
|
|
1223
1766
|
this.opts.allowLegacyWitnessUtxo
|
|
@@ -1426,6 +1969,9 @@ export class Transaction {
|
|
|
1426
1969
|
// Even worse: another user can add bip32 derivation, and spend money from different address.
|
|
1427
1970
|
// Better api: signIdx
|
|
1428
1971
|
sign(privateKey: Signer, allowedSighash?: number[], _auxRand?: Bytes): number {
|
|
1972
|
+
// Check transaction-wide strict requirements outside the per-input catch below so callers get
|
|
1973
|
+
// the actionable validation error instead of the generic "No inputs signed" result.
|
|
1974
|
+
this.validatePrevoutsForSigning();
|
|
1429
1975
|
let num = 0;
|
|
1430
1976
|
for (let i = 0; i < this.inputs.length; i++) {
|
|
1431
1977
|
try {
|
|
@@ -1438,19 +1984,27 @@ export class Transaction {
|
|
|
1438
1984
|
|
|
1439
1985
|
finalizeIdx(idx: number): void {
|
|
1440
1986
|
this.checkInputIdx(idx);
|
|
1441
|
-
if (this.fee <
|
|
1987
|
+
if (this.fee < _0n) throw new Error('Outputs spends more than inputs amount');
|
|
1442
1988
|
const input = this.inputs[idx];
|
|
1989
|
+
// Validate strict extension policy before constructing satisfaction so a rejection is atomic.
|
|
1990
|
+
cleanExtensions(input, this.opts.unknown!, this.opts.proprietary!);
|
|
1991
|
+
cleanTxModifiable(this.global.txModifiable, this.opts.unknown!);
|
|
1443
1992
|
const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
|
|
1444
1993
|
// Taproot finalize
|
|
1445
1994
|
if (inputType.txType === 'taproot') {
|
|
1446
1995
|
if (input.tapKeySig) input.finalScriptWitness = [input.tapKeySig];
|
|
1447
1996
|
else if (input.tapLeafScript && input.tapScriptSig) {
|
|
1448
|
-
//
|
|
1449
|
-
const leafs = input.tapLeafScript
|
|
1450
|
-
(
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1997
|
+
// Preserve the old shallowest-path tie-break without mutating caller-visible leaf order.
|
|
1998
|
+
const leafs = input.tapLeafScript
|
|
1999
|
+
.slice()
|
|
2000
|
+
.sort(
|
|
2001
|
+
(a, b) =>
|
|
2002
|
+
psbt.TaprootControlBlock.encode(a[0]).length -
|
|
2003
|
+
psbt.TaprootControlBlock.encode(b[0]).length
|
|
2004
|
+
);
|
|
2005
|
+
let smallest: Bytes[] | undefined;
|
|
2006
|
+
let smallestSize = Number.POSITIVE_INFINITY;
|
|
2007
|
+
let unsupported = false;
|
|
1454
2008
|
for (const [cb, _script] of leafs) {
|
|
1455
2009
|
// Last byte is version
|
|
1456
2010
|
const script = _script.slice(0, -1);
|
|
@@ -1459,6 +2013,7 @@ export class Transaction {
|
|
|
1459
2013
|
const hash = tapLeafHash(script, ver);
|
|
1460
2014
|
const scriptSig = input.tapScriptSig.filter((i) => equalBytes(i[0].leafHash, hash));
|
|
1461
2015
|
let signatures: Bytes[] = [];
|
|
2016
|
+
let witness: Bytes[] | undefined;
|
|
1462
2017
|
if (outScript.type === 'tr_ms') {
|
|
1463
2018
|
const m = outScript.m;
|
|
1464
2019
|
const pubkeys = outScript.pubkeys;
|
|
@@ -1498,33 +2053,43 @@ export class Transaction {
|
|
|
1498
2053
|
if (!signatures.length) continue;
|
|
1499
2054
|
} else {
|
|
1500
2055
|
const custom = this.opts.customScripts;
|
|
2056
|
+
let recognized = false;
|
|
1501
2057
|
if (custom) {
|
|
1502
2058
|
for (const c of custom) {
|
|
1503
2059
|
if (!c.finalizeTaproot) continue;
|
|
1504
2060
|
const scriptDecoded = Script.decode(script);
|
|
1505
2061
|
const csEncoded = c.encode(scriptDecoded);
|
|
1506
2062
|
if (csEncoded === undefined) continue;
|
|
2063
|
+
recognized = true;
|
|
2064
|
+
// Do not catch hook errors here. `undefined` means no satisfaction, while a throw
|
|
2065
|
+
// from a matching custom finalizer reports broken leaf/signature data and must
|
|
2066
|
+
// abort even when another leaf has already produced a valid witness candidate.
|
|
1507
2067
|
const finalized = c.finalizeTaproot(script, csEncoded, scriptSig);
|
|
1508
2068
|
if (!finalized) continue;
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
cleanFinalInput(input as TArg<PSBTInputs>);
|
|
1512
|
-
return;
|
|
2069
|
+
witness = finalized.concat(psbt.TaprootControlBlock.encode(cb));
|
|
2070
|
+
break;
|
|
1513
2071
|
}
|
|
1514
2072
|
}
|
|
1515
|
-
|
|
2073
|
+
// Minimum search inspects every leaf, so an unsupported path cannot block an already
|
|
2074
|
+
// complete known path merely because it appears later. Retain the old error when no
|
|
2075
|
+
// supported satisfaction exists at all.
|
|
2076
|
+
if (!witness && !recognized && scriptSig.length) unsupported = true;
|
|
2077
|
+
if (!witness) continue;
|
|
1516
2078
|
}
|
|
1517
2079
|
// Witness is stack, so last element will be used first
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
2080
|
+
witness ||= signatures.reverse().concat([script, psbt.TaprootControlBlock.encode(cb)]);
|
|
2081
|
+
const size = RawWitness.encode(witness).length;
|
|
2082
|
+
if (size >= smallestSize) continue;
|
|
2083
|
+
smallest = witness;
|
|
2084
|
+
smallestSize = size;
|
|
1522
2085
|
}
|
|
1523
|
-
if (!
|
|
2086
|
+
if (!smallest && unsupported) throw new Error('Finalize: Unknown tapLeafScript');
|
|
2087
|
+
if (!smallest) throw new Error('finalize/taproot: empty witness');
|
|
2088
|
+
input.finalScriptWitness = smallest;
|
|
1524
2089
|
} else throw new Error('finalize/taproot: unknown input');
|
|
1525
2090
|
// BIP174 Input Finalizer: if scriptSig is empty for an input, 0x07 remains unset.
|
|
1526
2091
|
delete input.finalScriptSig;
|
|
1527
|
-
cleanFinalInput(input
|
|
2092
|
+
this.cleanFinalInput(input);
|
|
1528
2093
|
return;
|
|
1529
2094
|
}
|
|
1530
2095
|
if (!input.partialSig || !input.partialSig.length) throw new Error('Not enough partial sign');
|
|
@@ -1584,7 +2149,7 @@ export class Transaction {
|
|
|
1584
2149
|
if (!finalScriptSig && !finalScriptWitness) throw new Error('Unknown error finalizing input');
|
|
1585
2150
|
if (finalScriptSig) input.finalScriptSig = finalScriptSig;
|
|
1586
2151
|
if (finalScriptWitness) input.finalScriptWitness = finalScriptWitness;
|
|
1587
|
-
cleanFinalInput(input
|
|
2152
|
+
this.cleanFinalInput(input);
|
|
1588
2153
|
}
|
|
1589
2154
|
finalize(): void {
|
|
1590
2155
|
for (let i = 0; i < this.inputs.length; i++) this.finalizeIdx(i);
|
|
@@ -1592,19 +2157,31 @@ export class Transaction {
|
|
|
1592
2157
|
extract(): Uint8Array {
|
|
1593
2158
|
if (!this.isFinal) throw new Error('Transaction has unfinalized inputs');
|
|
1594
2159
|
if (!this.outputs.length) throw new Error('Transaction has no outputs');
|
|
1595
|
-
if (this.fee <
|
|
2160
|
+
if (this.fee < _0n) throw new Error('Outputs spends more than inputs amount');
|
|
1596
2161
|
return this.toBytes(true, true);
|
|
1597
2162
|
}
|
|
1598
2163
|
combine(other: Transaction): this {
|
|
2164
|
+
if (!(other instanceof Transaction))
|
|
2165
|
+
throw new TypeError('"other" expected Transaction, got type=' + typeof other);
|
|
2166
|
+
// Match main's accumulator model: operation policy belongs to the receiver that is mutated.
|
|
2167
|
+
const opts = this.opts;
|
|
1599
2168
|
// BIP174 combiners merge same-transaction PSBTs across versions and emit the highest required
|
|
1600
2169
|
// version, so PSBTVersion mismatches are normalized below instead of treated as conflicts.
|
|
1601
2170
|
const PSBTVersion = Math.max(this.opts.PSBTVersion || 0, other.opts.PSBTVersion || 0);
|
|
1602
|
-
|
|
1603
|
-
|
|
2171
|
+
if (this.opts.version !== other.opts.version)
|
|
2172
|
+
throw new Error(
|
|
2173
|
+
`Transaction/combine: different version this=${this.opts.version} ` +
|
|
2174
|
+
`other=${other.opts.version}`
|
|
2175
|
+
);
|
|
2176
|
+
const thisV2 = this.isPSBTv2();
|
|
2177
|
+
const otherV2 = other.isPSBTv2();
|
|
2178
|
+
if (!thisV2 || !otherV2) {
|
|
2179
|
+
const thisLockTime = this.lockTime;
|
|
2180
|
+
const otherLockTime = other.lockTime;
|
|
2181
|
+
if (thisLockTime !== otherLockTime)
|
|
1604
2182
|
throw new Error(
|
|
1605
|
-
`Transaction/combine: different
|
|
2183
|
+
`Transaction/combine: different lockTime this=${thisLockTime} other=${otherLockTime}`
|
|
1606
2184
|
);
|
|
1607
|
-
}
|
|
1608
2185
|
}
|
|
1609
2186
|
for (const k of ['inputs', 'outputs'] as const) {
|
|
1610
2187
|
if (this[k].length !== other[k].length) {
|
|
@@ -1615,18 +2192,139 @@ export class Transaction {
|
|
|
1615
2192
|
}
|
|
1616
2193
|
// Same-transaction checks must compare the normalized unsigned tx bytes here: PSBTv0 stores
|
|
1617
2194
|
// `global.unsignedTx`, while PSBTv2 reconstructs the same transaction from split fields.
|
|
1618
|
-
|
|
2195
|
+
const unsignedTx = this.unsignedTx;
|
|
2196
|
+
if (!equalBytes(unsignedTx, other.unsignedTx))
|
|
1619
2197
|
throw new Error(`Transaction/combine: different unsigned tx`);
|
|
1620
|
-
|
|
2198
|
+
let txModifiable: number | undefined;
|
|
2199
|
+
if (thisV2 && otherV2) {
|
|
2200
|
+
// Core combines the stored optional bytes without deriving replacements from signatures.
|
|
2201
|
+
// Only explicit legacy-omission compatibility gives an absent field an effective value.
|
|
2202
|
+
const policy = (tx: Transaction) => {
|
|
2203
|
+
if (tx.global.txModifiable !== undefined)
|
|
2204
|
+
return cleanTxModifiable(tx.global.txModifiable, opts.unknown!)!;
|
|
2205
|
+
return opts.allowMissingTxModifiable ? tx.modifiable(true, opts.unknown!) : 0;
|
|
2206
|
+
};
|
|
2207
|
+
const a = policy(this);
|
|
2208
|
+
const b = policy(other);
|
|
2209
|
+
// Known mutability permissions use intersection and SIGHASH_SINGLE presence uses union.
|
|
2210
|
+
// Future flag bits must agree because this implementation does not know how to merge them.
|
|
2211
|
+
if ((a & ~0b111) !== (b & ~0b111))
|
|
2212
|
+
throw new Error('Transaction/combine: conflicting unknown txModifiable flags');
|
|
2213
|
+
txModifiable = (a & ~0b111) | (a & b & 0b011) | ((a | b) & 0b100);
|
|
2214
|
+
// Preserve Core's optional-field semantics when neither participant supplied policy.
|
|
2215
|
+
if (
|
|
2216
|
+
txModifiable === 0 &&
|
|
2217
|
+
this.global.txModifiable === undefined &&
|
|
2218
|
+
other.global.txModifiable === undefined &&
|
|
2219
|
+
!opts.allowMissingTxModifiable
|
|
2220
|
+
)
|
|
2221
|
+
txModifiable = undefined;
|
|
2222
|
+
} else if (thisV2) txModifiable = this.modifiable(opts.allowMissingTxModifiable, opts.unknown!);
|
|
2223
|
+
else if (otherV2) txModifiable = other.modifiable(opts.allowMissingTxModifiable, opts.unknown!);
|
|
2224
|
+
const thisGlobal = { ...this.global };
|
|
2225
|
+
const otherGlobal = { ...other.global };
|
|
2226
|
+
// PSBTv0 has no fallback-locktime field: fromPSBT caches unsignedTx.nLockTime there only for
|
|
2227
|
+
// effective-locktime resolution and v2 promotion. Do not merge that cache as a v2 wire value.
|
|
2228
|
+
if (thisV2 !== otherV2) {
|
|
2229
|
+
if (!thisV2) delete thisGlobal.fallbackLocktime;
|
|
2230
|
+
if (!otherV2) delete otherGlobal.fallbackLocktime;
|
|
2231
|
+
// BIP174 permits v0 to encode version zero explicitly or omit it. Remove only that v0
|
|
2232
|
+
// spelling before scalar conflicts; retaining the v2 field anchors repeated promotion when
|
|
2233
|
+
// the accumulator's original options still target v0.
|
|
2234
|
+
if (!thisV2) delete thisGlobal.version;
|
|
2235
|
+
if (!otherV2) delete otherGlobal.version;
|
|
2236
|
+
}
|
|
2237
|
+
// Transaction-modifiable has dedicated bitwise merge rules above. Fallback locktime is only
|
|
2238
|
+
// one input to the effective locktime resolved from the combined input maps, so retain receiver
|
|
2239
|
+
// precedence and validate the resulting unsigned transaction after those maps merge below.
|
|
2240
|
+
const fallbackLocktime =
|
|
2241
|
+
thisGlobal.fallbackLocktime !== undefined
|
|
2242
|
+
? thisGlobal.fallbackLocktime
|
|
2243
|
+
: otherGlobal.fallbackLocktime;
|
|
2244
|
+
delete thisGlobal.txModifiable;
|
|
2245
|
+
delete otherGlobal.txModifiable;
|
|
2246
|
+
delete thisGlobal.fallbackLocktime;
|
|
2247
|
+
delete otherGlobal.fallbackLocktime;
|
|
2248
|
+
// Every ordinary global scalar must agree when both participants provide it; silently choosing
|
|
2249
|
+
// either value can detach extension metadata such as a BIP322 message from its signatures.
|
|
2250
|
+
const global = psbt.combineKeyMap(
|
|
1621
2251
|
psbt.PSBTGlobal,
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
2252
|
+
thisGlobal,
|
|
2253
|
+
otherGlobal,
|
|
2254
|
+
opts.unknown!,
|
|
2255
|
+
opts.proprietary!
|
|
1626
2256
|
);
|
|
1627
|
-
if (
|
|
1628
|
-
|
|
1629
|
-
|
|
2257
|
+
if (fallbackLocktime !== undefined) global.fallbackLocktime = fallbackLocktime;
|
|
2258
|
+
if (PSBTVersion) global.version = PSBTVersion;
|
|
2259
|
+
if (txModifiable === undefined) delete global.txModifiable;
|
|
2260
|
+
else global.txModifiable = txModifiable;
|
|
2261
|
+
let hasOpaqueFinalizedV0 = false;
|
|
2262
|
+
const inputs = this.inputs.map((current, i) => {
|
|
2263
|
+
const currentFinal = this.inputStatus(i) === 'finalized';
|
|
2264
|
+
const otherFinal = other.inputStatus(i) === 'finalized';
|
|
2265
|
+
// Finalized v0 maps no longer contain the partial signatures needed to derive v2 flags.
|
|
2266
|
+
if ((!thisV2 && currentFinal) || (!otherV2 && otherFinal)) hasOpaqueFinalizedV0 = true;
|
|
2267
|
+
if (currentFinal && otherFinal) {
|
|
2268
|
+
// Two finalized PSBTs must describe the same complete satisfaction. Requiring matching
|
|
2269
|
+
// presence as well as matching values prevents combining witness-only and scriptSig-only
|
|
2270
|
+
// final states into a third, unreviewed satisfaction.
|
|
2271
|
+
for (const k of ['finalScriptSig', 'finalScriptWitness'] as const) {
|
|
2272
|
+
const currentHas = !!this.inputs[i][k]?.length;
|
|
2273
|
+
const otherHas = !!other.inputs[i][k]?.length;
|
|
2274
|
+
if (currentHas !== otherHas)
|
|
2275
|
+
throw new Error(`Transaction/combine: different finalized field=${k} input=${i}`);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
const combined = psbt.combineKeyMap(
|
|
2279
|
+
psbt.PSBTInput,
|
|
2280
|
+
current,
|
|
2281
|
+
other.inputs[i],
|
|
2282
|
+
opts.unknown!,
|
|
2283
|
+
opts.proprietary!
|
|
2284
|
+
) as PSBTInputs;
|
|
2285
|
+
// A final satisfaction supersedes partial signatures and transient signing metadata. This
|
|
2286
|
+
// also avoids manufacturing a contradictory final+partial input from two valid PSBTs.
|
|
2287
|
+
if (currentFinal || otherFinal)
|
|
2288
|
+
cleanFinalInput(combined as TArg<PSBTInputs>, opts.unknown!, opts.proprietary!);
|
|
2289
|
+
return cloneDeep(
|
|
2290
|
+
normalizeInput(
|
|
2291
|
+
combined,
|
|
2292
|
+
undefined,
|
|
2293
|
+
undefined,
|
|
2294
|
+
opts.disableScriptCheck,
|
|
2295
|
+
opts.unknown!,
|
|
2296
|
+
opts.proprietary!
|
|
2297
|
+
)
|
|
2298
|
+
) as PSBTInputs;
|
|
2299
|
+
});
|
|
2300
|
+
// A promoted opaque satisfaction may commit to both transaction halves. Clear only known
|
|
2301
|
+
// permissions; retain a v2 participant's SIGHASH_SINGLE indicator and any future flag bits.
|
|
2302
|
+
if (hasOpaqueFinalizedV0 && global.txModifiable !== undefined) global.txModifiable &= ~0b011;
|
|
2303
|
+
const candidate = new Transaction({ ...opts, PSBTVersion });
|
|
2304
|
+
const outputs = this.outputs.map((current, i) => {
|
|
2305
|
+
const combined = psbt.combineKeyMap(
|
|
2306
|
+
psbt.PSBTOutput,
|
|
2307
|
+
current,
|
|
2308
|
+
other.outputs[i],
|
|
2309
|
+
opts.unknown!,
|
|
2310
|
+
opts.proprietary!
|
|
2311
|
+
);
|
|
2312
|
+
return cloneDeep(candidate.normalizeOutput(combined));
|
|
2313
|
+
});
|
|
2314
|
+
// Build and validate a detached candidate before touching the receiver. Combining
|
|
2315
|
+
// complementary v2 locktime fields can otherwise create a different unsigned transaction.
|
|
2316
|
+
candidate.global = global;
|
|
2317
|
+
candidate.inputs = inputs;
|
|
2318
|
+
candidate.outputs = outputs;
|
|
2319
|
+
// A v0 input map can contribute signatures while the combined transaction is promoted to v2.
|
|
2320
|
+
// All-v2 restrictions were already intersected above, preserving mutual field omission.
|
|
2321
|
+
if ((!thisV2 || !otherV2) && candidate.isPSBTv2())
|
|
2322
|
+
candidate.global.txModifiable = candidate.txModifiable;
|
|
2323
|
+
if (!equalBytes(candidate.unsignedTx, unsignedTx))
|
|
2324
|
+
throw new Error('Transaction/combine: combined unsigned tx differs');
|
|
2325
|
+
this.global = candidate.global;
|
|
2326
|
+
this.inputs = candidate.inputs;
|
|
2327
|
+
this.outputs = candidate.outputs;
|
|
1630
2328
|
return this;
|
|
1631
2329
|
}
|
|
1632
2330
|
clone(): Transaction {
|
|
@@ -1638,6 +2336,7 @@ export class Transaction {
|
|
|
1638
2336
|
/**
|
|
1639
2337
|
* Merges multiple PSBT blobs into one.
|
|
1640
2338
|
* @param psbts - PSBT byte arrays to combine
|
|
2339
|
+
* @param opts - Transaction parsing, combination, and serialization options. See {@link TxOpts}.
|
|
1641
2340
|
* @returns Combined PSBT bytes.
|
|
1642
2341
|
* @throws If the PSBT list is empty or the partial transactions cannot be combined. {@link Error}
|
|
1643
2342
|
* @example
|
|
@@ -1648,12 +2347,14 @@ export class Transaction {
|
|
|
1648
2347
|
* PSBTCombine([psbt, psbt]);
|
|
1649
2348
|
* ```
|
|
1650
2349
|
*/
|
|
1651
|
-
export function PSBTCombine(psbts: TArg<Bytes[]>): TRet<Bytes> {
|
|
2350
|
+
export function PSBTCombine(psbts: TArg<Bytes[]>, opts: TArg<TxOpts> = {}): TRet<Bytes> {
|
|
1652
2351
|
if (!psbts || !Array.isArray(psbts) || !psbts.length)
|
|
1653
2352
|
throw new Error('PSBTCombine: wrong PSBT list');
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
2353
|
+
// Options affect both map cleanup during combination and the encoding of the returned PSBT.
|
|
2354
|
+
const combineOpts = opts as TxOpts;
|
|
2355
|
+
const tx = Transaction.fromPSBT(psbts[0], combineOpts);
|
|
2356
|
+
for (let i = 1; i < psbts.length; i++) tx.combine(Transaction.fromPSBT(psbts[i], tx.opts));
|
|
2357
|
+
return tx.toPSBT(combineOpts.PSBTVersion) as TRet<Bytes>;
|
|
1657
2358
|
}
|
|
1658
2359
|
|
|
1659
2360
|
// Copy-pasted from bip32 derive, maybe do something like 'bip32.parsePath'?
|