@scure/btc-signer 1.7.0 → 1.8.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 +52 -1
- package/esm/_type_test.js +1 -1
- package/esm/index.d.ts +12 -12
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +10 -10
- package/esm/index.js.map +1 -1
- package/esm/musig2.js +1 -1
- package/esm/p2p.d.ts +38 -0
- package/esm/p2p.d.ts.map +1 -0
- package/esm/p2p.js +156 -0
- package/esm/p2p.js.map +1 -0
- package/esm/payment.d.ts +77 -21
- package/esm/payment.d.ts.map +1 -1
- package/esm/payment.js +63 -60
- package/esm/payment.js.map +1 -1
- package/esm/psbt.d.ts +1 -1
- package/esm/psbt.d.ts.map +1 -1
- package/esm/psbt.js +2 -35
- package/esm/psbt.js.map +1 -1
- package/esm/script.js +1 -1
- package/esm/transaction.d.ts +56 -4
- package/esm/transaction.d.ts.map +1 -1
- package/esm/transaction.js +151 -8
- package/esm/transaction.js.map +1 -1
- package/esm/utxo.d.ts +3 -55
- package/esm/utxo.d.ts.map +1 -1
- package/esm/utxo.js +5 -114
- package/esm/utxo.js.map +1 -1
- package/index.d.ts +12 -12
- package/index.d.ts.map +1 -1
- package/index.js +58 -58
- package/index.js.map +1 -1
- package/musig2.d.ts +1 -1
- package/musig2.js +2 -2
- package/p2p.d.ts +38 -0
- package/p2p.d.ts.map +1 -0
- package/p2p.js +158 -0
- package/p2p.js.map +1 -0
- package/package.json +11 -11
- package/payment.d.ts +77 -21
- package/payment.d.ts.map +1 -1
- package/payment.js +81 -78
- package/payment.js.map +1 -1
- package/psbt.d.ts +1 -1
- package/psbt.d.ts.map +1 -1
- package/psbt.js +22 -55
- package/psbt.js.map +1 -1
- package/script.js +3 -3
- package/src/_type_test.ts +1 -1
- package/src/index.ts +12 -12
- package/src/musig2.ts +1 -1
- package/src/p2p.ts +158 -0
- package/src/payment.ts +160 -75
- package/src/psbt.ts +2 -36
- package/src/script.ts +1 -1
- package/src/transaction.ts +156 -12
- package/src/utxo.ts +9 -111
- package/transaction.d.ts +56 -4
- package/transaction.d.ts.map +1 -1
- package/transaction.js +222 -75
- package/transaction.js.map +1 -1
- package/utxo.d.ts +3 -55
- package/utxo.d.ts.map +1 -1
- package/utxo.js +43 -156
- package/utxo.js.map +1 -1
package/src/payment.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { bech32, bech32m, type Coder, createBase58check, hex } from '@scure/base';
|
|
2
2
|
import * as P from 'micro-packed';
|
|
3
|
-
import { TaprootControlBlock, type TransactionInput } from './psbt.
|
|
4
|
-
import { OpToNum, Script, type ScriptType, VarBytes } from './script.
|
|
5
|
-
import * as u from './utils.
|
|
6
|
-
import { type BTC_NETWORK, type Bytes, NETWORK } from './utils.
|
|
3
|
+
import { TaprootControlBlock, type TransactionInput } from './psbt.ts';
|
|
4
|
+
import { OpToNum, Script, type ScriptType, VarBytes } from './script.ts';
|
|
5
|
+
import * as u from './utils.ts';
|
|
6
|
+
import { type BTC_NETWORK, type Bytes, NETWORK } from './utils.ts';
|
|
7
7
|
|
|
8
8
|
// We need following items:
|
|
9
9
|
// - encode/decode output script
|
|
@@ -16,6 +16,7 @@ export type P2Ret = {
|
|
|
16
16
|
address?: string;
|
|
17
17
|
redeemScript?: Bytes;
|
|
18
18
|
witnessScript?: Bytes;
|
|
19
|
+
hash?: Bytes;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
// Pay to Anchor (P2A)
|
|
@@ -328,42 +329,79 @@ function uniqPubkey(pubkeys: Bytes[]) {
|
|
|
328
329
|
map[key] = true;
|
|
329
330
|
}
|
|
330
331
|
}
|
|
332
|
+
// We want narrow types inside p2* methods, but always want to type-check if they compatible with P2Ret here!
|
|
333
|
+
// Also we use satisfies for additional check (ts 4.9+)
|
|
334
|
+
type Extends<T, U> = T extends U ? T : never;
|
|
331
335
|
|
|
332
|
-
|
|
333
|
-
export const p2pk = (pubkey: Bytes,
|
|
336
|
+
export type P2PK = { type: 'pk'; script: Bytes };
|
|
337
|
+
export const p2pk = (pubkey: Bytes, _network: BTC_NETWORK = NETWORK): Extends<P2PK, P2Ret> => {
|
|
334
338
|
// network is unused
|
|
335
339
|
if (!isValidPubkey(pubkey, u.PubT.ecdsa)) throw new Error('P2PK: invalid publicKey');
|
|
336
|
-
return {
|
|
337
|
-
type: 'pk',
|
|
338
|
-
script: OutScript.encode({ type: 'pk', pubkey }),
|
|
339
|
-
};
|
|
340
|
+
return { type: 'pk', script: OutScript.encode({ type: 'pk', pubkey }) } as const satisfies P2Ret;
|
|
340
341
|
};
|
|
341
|
-
|
|
342
|
+
|
|
343
|
+
export type P2PKH = { type: 'pkh'; script: Bytes; address: string; hash: Bytes };
|
|
344
|
+
export const p2pkh = (publicKey: Bytes, network: BTC_NETWORK = NETWORK): Extends<P2PKH, P2Ret> => {
|
|
342
345
|
if (!isValidPubkey(publicKey, u.PubT.ecdsa)) throw new Error('P2PKH: invalid publicKey');
|
|
343
346
|
const hash = u.hash160(publicKey);
|
|
344
347
|
return {
|
|
345
348
|
type: 'pkh',
|
|
346
349
|
script: OutScript.encode({ type: 'pkh', hash }),
|
|
347
350
|
address: Address(network).encode({ type: 'pkh', hash }),
|
|
348
|
-
|
|
351
|
+
hash,
|
|
352
|
+
} as const satisfies P2Ret;
|
|
349
353
|
};
|
|
350
|
-
|
|
354
|
+
|
|
355
|
+
export type P2SHBase = {
|
|
356
|
+
type: 'sh';
|
|
357
|
+
redeemScript: Bytes;
|
|
358
|
+
script: Bytes;
|
|
359
|
+
address: string;
|
|
360
|
+
hash: Bytes;
|
|
361
|
+
};
|
|
362
|
+
export type P2SHWithWitness = P2SHBase & { witnessScript: Bytes };
|
|
363
|
+
export type P2SHWithoutWitness = Omit<P2SHBase, 'witnessScript'>;
|
|
364
|
+
export type P2SHReturn<T extends P2Ret> = T extends { witnessScript: Bytes }
|
|
365
|
+
? P2SHWithWitness
|
|
366
|
+
: P2SHWithoutWitness;
|
|
367
|
+
export const p2sh = <T extends P2Ret>(
|
|
368
|
+
child: T,
|
|
369
|
+
network: BTC_NETWORK = NETWORK
|
|
370
|
+
): Extends<P2SHReturn<T>, P2Ret> => {
|
|
351
371
|
// It is already tested inside noble-hashes and checkScript
|
|
352
372
|
const cs = child.script;
|
|
353
373
|
if (!u.isBytes(cs)) throw new Error(`Wrong script: ${typeof child.script}, expected Uint8Array`);
|
|
354
374
|
const hash = u.hash160(cs);
|
|
355
375
|
const script = OutScript.encode({ type: 'sh', hash });
|
|
356
376
|
checkScript(script, cs, child.witnessScript);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
377
|
+
if (child.witnessScript) {
|
|
378
|
+
return {
|
|
379
|
+
type: 'sh',
|
|
380
|
+
redeemScript: cs,
|
|
381
|
+
script: OutScript.encode({ type: 'sh', hash }),
|
|
382
|
+
address: Address(network).encode({ type: 'sh', hash }),
|
|
383
|
+
hash,
|
|
384
|
+
witnessScript: child.witnessScript,
|
|
385
|
+
} as Extends<P2SHReturn<T>, P2Ret> satisfies P2Ret;
|
|
386
|
+
} else {
|
|
387
|
+
return {
|
|
388
|
+
type: 'sh',
|
|
389
|
+
redeemScript: cs,
|
|
390
|
+
script: OutScript.encode({ type: 'sh', hash }),
|
|
391
|
+
address: Address(network).encode({ type: 'sh', hash }),
|
|
392
|
+
hash,
|
|
393
|
+
} as Extends<P2SHReturn<T>, P2Ret> satisfies P2Ret;
|
|
394
|
+
}
|
|
365
395
|
};
|
|
366
|
-
|
|
396
|
+
|
|
397
|
+
export type P2WSH = {
|
|
398
|
+
type: 'wsh';
|
|
399
|
+
witnessScript: Bytes;
|
|
400
|
+
script: Bytes;
|
|
401
|
+
address: string;
|
|
402
|
+
hash: Bytes;
|
|
403
|
+
};
|
|
404
|
+
export const p2wsh = (child: P2Ret, network: BTC_NETWORK = NETWORK): Extends<P2WSH, P2Ret> => {
|
|
367
405
|
const cs = child.script;
|
|
368
406
|
if (!u.isBytes(cs)) throw new Error(`Wrong script: ${typeof cs}, expected Uint8Array`);
|
|
369
407
|
const hash = u.sha256(cs);
|
|
@@ -374,9 +412,15 @@ export const p2wsh = (child: P2Ret, network: BTC_NETWORK = NETWORK): P2Ret => {
|
|
|
374
412
|
witnessScript: cs,
|
|
375
413
|
script: OutScript.encode({ type: 'wsh', hash }),
|
|
376
414
|
address: Address(network).encode({ type: 'wsh', hash }),
|
|
377
|
-
|
|
415
|
+
hash,
|
|
416
|
+
} as const satisfies P2Ret;
|
|
378
417
|
};
|
|
379
|
-
|
|
418
|
+
|
|
419
|
+
export type P2WPKH = { type: 'wpkh'; script: Bytes; address: string; hash: Bytes };
|
|
420
|
+
export const p2wpkh = (
|
|
421
|
+
publicKey: Bytes,
|
|
422
|
+
network: BTC_NETWORK = NETWORK
|
|
423
|
+
): Extends<P2WPKH, P2Ret> => {
|
|
380
424
|
if (!isValidPubkey(publicKey, u.PubT.ecdsa)) throw new Error('P2WPKH: invalid publicKey');
|
|
381
425
|
if (publicKey.length === 65) throw new Error('P2WPKH: uncompressed public key');
|
|
382
426
|
const hash = u.hash160(publicKey);
|
|
@@ -384,11 +428,21 @@ export const p2wpkh = (publicKey: Bytes, network: BTC_NETWORK = NETWORK): P2Ret
|
|
|
384
428
|
type: 'wpkh',
|
|
385
429
|
script: OutScript.encode({ type: 'wpkh', hash }),
|
|
386
430
|
address: Address(network).encode({ type: 'wpkh', hash }),
|
|
387
|
-
|
|
431
|
+
hash,
|
|
432
|
+
} as const satisfies P2Ret;
|
|
388
433
|
};
|
|
389
|
-
|
|
434
|
+
|
|
435
|
+
export type P2MS = { type: 'ms'; script: Bytes };
|
|
436
|
+
export const p2ms = (
|
|
437
|
+
m: number,
|
|
438
|
+
pubkeys: Bytes[],
|
|
439
|
+
allowSamePubkeys = false
|
|
440
|
+
): Extends<P2MS, P2Ret> => {
|
|
390
441
|
if (!allowSamePubkeys) uniqPubkey(pubkeys);
|
|
391
|
-
return {
|
|
442
|
+
return {
|
|
443
|
+
type: 'ms',
|
|
444
|
+
script: OutScript.encode({ type: 'ms', pubkeys, m }),
|
|
445
|
+
} as const satisfies P2Ret;
|
|
392
446
|
};
|
|
393
447
|
|
|
394
448
|
export type HashedTree =
|
|
@@ -438,19 +492,24 @@ function checkTaprootScript(
|
|
|
438
492
|
}
|
|
439
493
|
}
|
|
440
494
|
|
|
441
|
-
export type
|
|
495
|
+
export type P2TR = {
|
|
496
|
+
type: 'tr';
|
|
497
|
+
script: Bytes;
|
|
498
|
+
address: string;
|
|
442
499
|
tweakedPubkey: Bytes;
|
|
443
500
|
tapInternalKey: Bytes;
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
501
|
+
};
|
|
502
|
+
export type P2TR_TREE = P2TR & {
|
|
503
|
+
tapMerkleRoot: Bytes;
|
|
504
|
+
tapLeafScript: TransactionInput['tapLeafScript'];
|
|
505
|
+
leaves: TaprootLeaf[];
|
|
447
506
|
};
|
|
448
507
|
|
|
449
508
|
export type TaprootNode = {
|
|
450
509
|
script: Bytes | string;
|
|
451
510
|
leafVersion?: number;
|
|
452
511
|
weight?: number;
|
|
453
|
-
} & Partial<
|
|
512
|
+
} & Partial<P2TR_TREE>;
|
|
454
513
|
export type TaprootScriptTree = TaprootNode | TaprootScriptTree[];
|
|
455
514
|
export type TaprootScriptList = TaprootNode[];
|
|
456
515
|
type _TaprootTreeInternal = {
|
|
@@ -562,13 +621,28 @@ export const tapLeafHash = (script: Bytes, version: number = TAP_LEAF_VERSION):
|
|
|
562
621
|
// Works as key OR tree.
|
|
563
622
|
// If we only have tree, need to add unspendable key, otherwise
|
|
564
623
|
// complex multisig wallet can be spent by owner of key only. See TAPROOT_UNSPENDABLE_KEY
|
|
624
|
+
export type P2TRRet<T> = T extends TaprootScriptTree ? P2TR_TREE : P2TR;
|
|
625
|
+
export function p2tr(
|
|
626
|
+
internalPubKey: Bytes | string,
|
|
627
|
+
tree?: undefined,
|
|
628
|
+
network?: BTC_NETWORK,
|
|
629
|
+
allowUnknownOutputs?: boolean,
|
|
630
|
+
customScripts?: CustomScript[]
|
|
631
|
+
): Extends<P2TR, P2Ret>;
|
|
632
|
+
export function p2tr(
|
|
633
|
+
internalPubKey: Bytes | string,
|
|
634
|
+
tree: TaprootScriptTree,
|
|
635
|
+
network?: BTC_NETWORK,
|
|
636
|
+
allowUnknownOutputs?: boolean,
|
|
637
|
+
customScripts?: CustomScript[]
|
|
638
|
+
): Extends<P2TR_TREE, P2Ret>;
|
|
565
639
|
export function p2tr(
|
|
566
640
|
internalPubKey?: Bytes | string,
|
|
567
641
|
tree?: TaprootScriptTree,
|
|
568
642
|
network: BTC_NETWORK = NETWORK,
|
|
569
643
|
allowUnknownOutputs = false,
|
|
570
644
|
customScripts?: CustomScript[]
|
|
571
|
-
):
|
|
645
|
+
): Extends<P2TR & Partial<P2TR_TREE>, P2Ret> {
|
|
572
646
|
// Unspendable
|
|
573
647
|
if (!internalPubKey && !tree) throw new Error('p2tr: should have pubKey or scriptTree (or both)');
|
|
574
648
|
const pubKey =
|
|
@@ -576,14 +650,13 @@ export function p2tr(
|
|
|
576
650
|
? hex.decode(internalPubKey)
|
|
577
651
|
: internalPubKey || u.TAPROOT_UNSPENDABLE_KEY;
|
|
578
652
|
if (!isValidPubkey(pubKey, u.PubT.schnorr)) throw new Error('p2tr: non-schnorr pubkey');
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
leaves = taprootWalkTree(hashedTree).map((l) => ({
|
|
653
|
+
if (tree) {
|
|
654
|
+
let hashedTree = taprootAddPath(
|
|
655
|
+
taprootHashTree(tree, pubKey, allowUnknownOutputs, customScripts)
|
|
656
|
+
);
|
|
657
|
+
const tapMerkleRoot = hashedTree.hash;
|
|
658
|
+
const [tweakedPubkey, parity] = u.taprootTweakPubkey(pubKey, tapMerkleRoot);
|
|
659
|
+
const leaves = taprootWalkTree(hashedTree).map((l) => ({
|
|
587
660
|
...l,
|
|
588
661
|
controlBlock: TaprootControlBlock.encode({
|
|
589
662
|
version: (l.version || TAP_LEAF_VERSION) + parity,
|
|
@@ -591,28 +664,33 @@ export function p2tr(
|
|
|
591
664
|
merklePath: l.path,
|
|
592
665
|
}),
|
|
593
666
|
}));
|
|
667
|
+
return {
|
|
668
|
+
type: 'tr',
|
|
669
|
+
script: OutScript.encode({ type: 'tr', pubkey: tweakedPubkey }),
|
|
670
|
+
address: Address(network).encode({ type: 'tr', pubkey: tweakedPubkey }),
|
|
671
|
+
// For tests
|
|
672
|
+
tweakedPubkey,
|
|
673
|
+
// PSBT stuff
|
|
674
|
+
tapInternalKey: pubKey,
|
|
675
|
+
leaves,
|
|
676
|
+
tapLeafScript: leaves.map((l) => [
|
|
677
|
+
TaprootControlBlock.decode(l.controlBlock),
|
|
678
|
+
u.concatBytes(l.script, new Uint8Array([l.version || TAP_LEAF_VERSION])),
|
|
679
|
+
]),
|
|
680
|
+
tapMerkleRoot,
|
|
681
|
+
} as const satisfies P2TR_TREE;
|
|
682
|
+
} else {
|
|
683
|
+
const tweakedPubkey = u.taprootTweakPubkey(pubKey, P.EMPTY)[0];
|
|
684
|
+
return {
|
|
685
|
+
type: 'tr',
|
|
686
|
+
script: OutScript.encode({ type: 'tr', pubkey: tweakedPubkey }),
|
|
687
|
+
address: Address(network).encode({ type: 'tr', pubkey: tweakedPubkey }),
|
|
688
|
+
// For tests
|
|
689
|
+
tweakedPubkey,
|
|
690
|
+
// PSBT stuff
|
|
691
|
+
tapInternalKey: pubKey,
|
|
692
|
+
} as const satisfies P2TR;
|
|
594
693
|
}
|
|
595
|
-
let tapLeafScript: TransactionInput['tapLeafScript'];
|
|
596
|
-
if (leaves) {
|
|
597
|
-
tapLeafScript = leaves.map((l) => [
|
|
598
|
-
TaprootControlBlock.decode(l.controlBlock),
|
|
599
|
-
u.concatBytes(l.script, new Uint8Array([l.version || TAP_LEAF_VERSION])),
|
|
600
|
-
]);
|
|
601
|
-
}
|
|
602
|
-
const res: P2TROut = {
|
|
603
|
-
type: 'tr',
|
|
604
|
-
script: OutScript.encode({ type: 'tr', pubkey: tweakedPubkey }),
|
|
605
|
-
address: Address(network).encode({ type: 'tr', pubkey: tweakedPubkey }),
|
|
606
|
-
// For tests
|
|
607
|
-
tweakedPubkey,
|
|
608
|
-
// PSBT stuff
|
|
609
|
-
tapInternalKey: pubKey,
|
|
610
|
-
};
|
|
611
|
-
// Just in case someone would want to select a specific script
|
|
612
|
-
if (leaves) res.leaves = leaves;
|
|
613
|
-
if (tapLeafScript) res.tapLeafScript = tapLeafScript;
|
|
614
|
-
if (tapMerkleRoot) res.tapMerkleRoot = tapMerkleRoot;
|
|
615
|
-
return res;
|
|
616
694
|
}
|
|
617
695
|
|
|
618
696
|
// Returns all combinations of size M from lst
|
|
@@ -651,29 +729,37 @@ export function combinations<T>(m: number, list: T[]): T[][] {
|
|
|
651
729
|
* Takes O(n^2) if m != n. 99-of-100 is ok, 5-of-100 is not.
|
|
652
730
|
* `2-of-[A,B,C] => [A,B] | [A,C] | [B,C]`
|
|
653
731
|
*/
|
|
654
|
-
export
|
|
732
|
+
export type P2TR_NS = { type: 'tr_ns'; script: Bytes };
|
|
733
|
+
export const p2tr_ns = (
|
|
734
|
+
m: number,
|
|
735
|
+
pubkeys: Bytes[],
|
|
736
|
+
allowSamePubkeys = false
|
|
737
|
+
): Extends<P2TR_NS, P2Ret>[] => {
|
|
655
738
|
if (!allowSamePubkeys) uniqPubkey(pubkeys);
|
|
656
|
-
return combinations(m, pubkeys).map(
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
739
|
+
return combinations(m, pubkeys).map(
|
|
740
|
+
(i) =>
|
|
741
|
+
({
|
|
742
|
+
type: 'tr_ns',
|
|
743
|
+
script: OutScript.encode({ type: 'tr_ns', pubkeys: i }),
|
|
744
|
+
}) as const
|
|
745
|
+
) satisfies P2Ret[];
|
|
660
746
|
};
|
|
661
747
|
// Taproot public key (case of p2tr_ns)
|
|
662
|
-
export
|
|
748
|
+
export type P2TR_PK = P2TR_NS;
|
|
749
|
+
export const p2tr_pk = (pubkey: Bytes): Extends<P2TR_PK, P2Ret> =>
|
|
750
|
+
p2tr_ns(1, [pubkey], undefined)[0] satisfies P2Ret;
|
|
663
751
|
|
|
752
|
+
export type P2TR_MS = { type: 'tr_ms'; script: Bytes };
|
|
664
753
|
export function p2tr_ms(
|
|
665
754
|
m: number,
|
|
666
755
|
pubkeys: Bytes[],
|
|
667
756
|
allowSamePubkeys = false
|
|
668
|
-
): {
|
|
669
|
-
type: string;
|
|
670
|
-
script: Uint8Array;
|
|
671
|
-
} {
|
|
757
|
+
): Extends<P2TR_MS, P2Ret> {
|
|
672
758
|
if (!allowSamePubkeys) uniqPubkey(pubkeys);
|
|
673
759
|
return {
|
|
674
760
|
type: 'tr_ms',
|
|
675
761
|
script: OutScript.encode({ type: 'tr_ms', pubkeys, m }),
|
|
676
|
-
};
|
|
762
|
+
} as const satisfies P2Ret;
|
|
677
763
|
}
|
|
678
764
|
|
|
679
765
|
// Simple pubkey address, without complex scripts
|
|
@@ -691,8 +777,7 @@ export function getAddress(
|
|
|
691
777
|
throw new Error(`getAddress: unknown type=${type}`);
|
|
692
778
|
}
|
|
693
779
|
|
|
694
|
-
export const _sortPubkeys = (pubkeys: Bytes[]):
|
|
695
|
-
Array.from(pubkeys).sort(u.compareBytes);
|
|
780
|
+
export const _sortPubkeys = (pubkeys: Bytes[]): Bytes[] => Array.from(pubkeys).sort(u.compareBytes);
|
|
696
781
|
|
|
697
782
|
export function multisig(
|
|
698
783
|
m: number,
|
package/src/psbt.ts
CHANGED
|
@@ -8,9 +8,8 @@ import {
|
|
|
8
8
|
RawTx,
|
|
9
9
|
RawWitness,
|
|
10
10
|
VarBytes,
|
|
11
|
-
} from './script.
|
|
12
|
-
import {
|
|
13
|
-
import { type Bytes, compareBytes, equalBytes, PubT, validatePubkey } from './utils.js';
|
|
11
|
+
} from './script.ts';
|
|
12
|
+
import { type Bytes, compareBytes, equalBytes, PubT, validatePubkey } from './utils.ts';
|
|
14
13
|
|
|
15
14
|
// PSBT BIP174, BIP370, BIP371
|
|
16
15
|
|
|
@@ -282,17 +281,6 @@ export const PSBTInputCoder = P.validate(PSBTKeyMap(PSBTInput), (i) => {
|
|
|
282
281
|
(i.requiredHeightLocktime <= 0 || i.requiredHeightLocktime >= 500000000)
|
|
283
282
|
)
|
|
284
283
|
throw new Error(`validateInput: wrong heighLocktime=${i.requiredHeightLocktime}`);
|
|
285
|
-
|
|
286
|
-
if (i.nonWitnessUtxo && i.index !== undefined) {
|
|
287
|
-
const last = i.nonWitnessUtxo.outputs.length - 1;
|
|
288
|
-
if (i.index > last) throw new Error(`validateInput: index(${i.index}) not in nonWitnessUtxo`);
|
|
289
|
-
const prevOut = i.nonWitnessUtxo.outputs[i.index];
|
|
290
|
-
if (
|
|
291
|
-
i.witnessUtxo &&
|
|
292
|
-
(!equalBytes(i.witnessUtxo.script, prevOut.script) || i.witnessUtxo.amount !== prevOut.amount)
|
|
293
|
-
)
|
|
294
|
-
throw new Error('validateInput: witnessUtxo different from nonWitnessUtxo');
|
|
295
|
-
}
|
|
296
284
|
if (i.tapLeafScript) {
|
|
297
285
|
// tap leaf version appears here twice: in control block and at the end of script
|
|
298
286
|
for (const [k, v] of i.tapLeafScript) {
|
|
@@ -302,28 +290,6 @@ export const PSBTInputCoder = P.validate(PSBTKeyMap(PSBTInput), (i) => {
|
|
|
302
290
|
throw new Error('validateInput: tapLeafScript version has parity bit!');
|
|
303
291
|
}
|
|
304
292
|
}
|
|
305
|
-
// Validate txid for nonWitnessUtxo is correct
|
|
306
|
-
if (i.nonWitnessUtxo && i.index !== undefined && i.txid) {
|
|
307
|
-
const outputs = i.nonWitnessUtxo.outputs;
|
|
308
|
-
if (outputs.length - 1 < i.index) throw new Error('nonWitnessUtxo: incorect output index');
|
|
309
|
-
// At this point, we are using previous tx output to create new input.
|
|
310
|
-
// Script safety checks are unnecessary:
|
|
311
|
-
// - User has no control over previous tx. If somebody send money in same tx
|
|
312
|
-
// as unspendable output, we still want user able to spend money
|
|
313
|
-
// - We still want some checks to notify user about possible errors early
|
|
314
|
-
// in case user wants to use wrong input by mistake
|
|
315
|
-
// - Worst case: tx will be rejected by nodes. Still better than disallowing user
|
|
316
|
-
// to spend real input, no matter how broken it looks
|
|
317
|
-
const tx = Transaction.fromRaw(RawTx.encode(i.nonWitnessUtxo), {
|
|
318
|
-
allowUnknownOutputs: true,
|
|
319
|
-
disableScriptCheck: true,
|
|
320
|
-
allowUnknownInputs: true,
|
|
321
|
-
});
|
|
322
|
-
const txid = hex.encode(i.txid);
|
|
323
|
-
// PSBTv2 vectors have non-final tx in inputs
|
|
324
|
-
if (tx.isFinal && tx.id !== txid)
|
|
325
|
-
throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
|
|
326
|
-
}
|
|
327
293
|
return i;
|
|
328
294
|
});
|
|
329
295
|
|
package/src/script.ts
CHANGED
package/src/transaction.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { hex } from '@scure/base';
|
|
2
2
|
import * as P from 'micro-packed';
|
|
3
|
-
import { Address, type CustomScript, OutScript, checkScript, tapLeafHash } from './payment.
|
|
4
|
-
import * as psbt from './psbt.
|
|
3
|
+
import { Address, type CustomScript, OutScript, checkScript, tapLeafHash } from './payment.ts';
|
|
4
|
+
import * as psbt from './psbt.ts';
|
|
5
5
|
import {
|
|
6
6
|
CompactSizeLen,
|
|
7
7
|
RawOldTx,
|
|
@@ -10,16 +10,16 @@ import {
|
|
|
10
10
|
RawWitness,
|
|
11
11
|
Script,
|
|
12
12
|
VarBytes,
|
|
13
|
-
} from './script.
|
|
14
|
-
import * as u from './utils.
|
|
15
|
-
import { type Bytes, NETWORK, concatBytes, equalBytes, isBytes } from './utils.
|
|
16
|
-
import { getInputType, getPrevOut, normalizeInput, toVsize } from './utxo.js'; // circular
|
|
13
|
+
} from './script.ts';
|
|
14
|
+
import * as u from './utils.ts';
|
|
15
|
+
import { type Bytes, NETWORK, concatBytes, equalBytes, isBytes } from './utils.ts';
|
|
17
16
|
|
|
18
17
|
const EMPTY32 = new Uint8Array(32);
|
|
19
18
|
const EMPTY_OUTPUT: P.UnwrapCoder<typeof RawOutput> = {
|
|
20
19
|
amount: 0xffffffffffffffffn,
|
|
21
20
|
script: P.EMPTY,
|
|
22
21
|
};
|
|
22
|
+
export const toVsize = (weight: number): number => Math.ceil(weight / 4);
|
|
23
23
|
|
|
24
24
|
// @scure/bip32 interface
|
|
25
25
|
interface HDKey {
|
|
@@ -230,6 +230,146 @@ function validateOpts(opts: TxOpts): Readonly<TxOpts> {
|
|
|
230
230
|
return Object.freeze(_opts);
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
// NOTE: we cannot do this inside PSBTInput coder, because there is no index/txid at this point!
|
|
234
|
+
function validateInput(i: psbt.TransactionInput): psbt.TransactionInput {
|
|
235
|
+
if (i.nonWitnessUtxo && i.index !== undefined) {
|
|
236
|
+
const last = i.nonWitnessUtxo.outputs.length - 1;
|
|
237
|
+
if (i.index > last) throw new Error(`validateInput: index(${i.index}) not in nonWitnessUtxo`);
|
|
238
|
+
const prevOut = i.nonWitnessUtxo.outputs[i.index];
|
|
239
|
+
if (
|
|
240
|
+
i.witnessUtxo &&
|
|
241
|
+
(!equalBytes(i.witnessUtxo.script, prevOut.script) || i.witnessUtxo.amount !== prevOut.amount)
|
|
242
|
+
)
|
|
243
|
+
throw new Error('validateInput: witnessUtxo different from nonWitnessUtxo');
|
|
244
|
+
if (i.txid) {
|
|
245
|
+
const outputs = i.nonWitnessUtxo.outputs;
|
|
246
|
+
if (outputs.length - 1 < i.index) throw new Error('nonWitnessUtxo: incorect output index');
|
|
247
|
+
// At this point, we are using previous tx output to create new input.
|
|
248
|
+
// Script safety checks are unnecessary:
|
|
249
|
+
// - User has no control over previous tx. If somebody send money in same tx
|
|
250
|
+
// as unspendable output, we still want user able to spend money
|
|
251
|
+
// - We still want some checks to notify user about possible errors early
|
|
252
|
+
// in case user wants to use wrong input by mistake
|
|
253
|
+
// - Worst case: tx will be rejected by nodes. Still better than disallowing user
|
|
254
|
+
// to spend real input, no matter how broken it looks
|
|
255
|
+
const tx = Transaction.fromRaw(RawTx.encode(i.nonWitnessUtxo), {
|
|
256
|
+
allowUnknownOutputs: true,
|
|
257
|
+
disableScriptCheck: true,
|
|
258
|
+
allowUnknownInputs: true,
|
|
259
|
+
});
|
|
260
|
+
const txid = hex.encode(i.txid);
|
|
261
|
+
// PSBTv2 vectors have non-final tx in inputs
|
|
262
|
+
if (tx.isFinal && tx.id !== txid)
|
|
263
|
+
throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return i;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export type PSBTInputs = psbt.PSBTKeyMapKeys<typeof psbt.PSBTInput>;
|
|
270
|
+
|
|
271
|
+
// Normalizes input
|
|
272
|
+
export function getPrevOut(input: psbt.TransactionInput): P.UnwrapCoder<typeof RawOutput> {
|
|
273
|
+
if (input.nonWitnessUtxo) {
|
|
274
|
+
if (input.index === undefined) throw new Error('Unknown input index');
|
|
275
|
+
return input.nonWitnessUtxo.outputs[input.index];
|
|
276
|
+
} else if (input.witnessUtxo) return input.witnessUtxo;
|
|
277
|
+
else throw new Error('Cannot find previous output info');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function normalizeInput(
|
|
281
|
+
i: psbt.TransactionInputUpdate,
|
|
282
|
+
cur?: psbt.TransactionInput,
|
|
283
|
+
allowedFields?: (keyof psbt.TransactionInput)[],
|
|
284
|
+
disableScriptCheck = false,
|
|
285
|
+
allowUnknown = false
|
|
286
|
+
): psbt.TransactionInput {
|
|
287
|
+
let { nonWitnessUtxo, txid } = i;
|
|
288
|
+
// String support for common fields. We usually prefer Uint8Array to avoid errors
|
|
289
|
+
// like hex looking string accidentally passed, however, in case of nonWitnessUtxo
|
|
290
|
+
// it is better to expect string, since constructing this complex object will be
|
|
291
|
+
// difficult for user
|
|
292
|
+
if (typeof nonWitnessUtxo === 'string') nonWitnessUtxo = hex.decode(nonWitnessUtxo);
|
|
293
|
+
if (isBytes(nonWitnessUtxo)) nonWitnessUtxo = RawTx.decode(nonWitnessUtxo);
|
|
294
|
+
if (!('nonWitnessUtxo' in i) && nonWitnessUtxo === undefined)
|
|
295
|
+
nonWitnessUtxo = cur?.nonWitnessUtxo;
|
|
296
|
+
if (typeof txid === 'string') txid = hex.decode(txid);
|
|
297
|
+
// TODO: if we have nonWitnessUtxo, we can extract txId from here
|
|
298
|
+
if (txid === undefined) txid = cur?.txid;
|
|
299
|
+
let res: PSBTInputs = { ...cur, ...i, nonWitnessUtxo, txid };
|
|
300
|
+
if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo;
|
|
301
|
+
if (res.sequence === undefined) res.sequence = DEFAULT_SEQUENCE;
|
|
302
|
+
if (res.tapMerkleRoot === null) delete res.tapMerkleRoot;
|
|
303
|
+
res = psbt.mergeKeyMap(psbt.PSBTInput, res, cur, allowedFields, allowUnknown);
|
|
304
|
+
psbt.PSBTInputCoder.encode(res); // Validates that everything is correct at this point
|
|
305
|
+
|
|
306
|
+
let prevOut;
|
|
307
|
+
if (res.nonWitnessUtxo && res.index !== undefined)
|
|
308
|
+
prevOut = res.nonWitnessUtxo.outputs[res.index];
|
|
309
|
+
else if (res.witnessUtxo) prevOut = res.witnessUtxo;
|
|
310
|
+
if (prevOut && !disableScriptCheck)
|
|
311
|
+
checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
|
|
312
|
+
return res;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function getInputType(input: psbt.TransactionInput, allowLegacyWitnessUtxo = false) {
|
|
316
|
+
let txType = 'legacy';
|
|
317
|
+
let defaultSighash = SignatureHash.ALL;
|
|
318
|
+
const prevOut = getPrevOut(input);
|
|
319
|
+
const first = OutScript.decode(prevOut.script);
|
|
320
|
+
let type = first.type;
|
|
321
|
+
let cur = first;
|
|
322
|
+
const stack = [first];
|
|
323
|
+
if (first.type === 'tr') {
|
|
324
|
+
defaultSighash = SignatureHash.DEFAULT;
|
|
325
|
+
return {
|
|
326
|
+
txType: 'taproot',
|
|
327
|
+
type: 'tr',
|
|
328
|
+
last: first,
|
|
329
|
+
lastScript: prevOut.script,
|
|
330
|
+
defaultSighash,
|
|
331
|
+
sighash: input.sighashType || defaultSighash,
|
|
332
|
+
};
|
|
333
|
+
} else {
|
|
334
|
+
if (first.type === 'wpkh' || first.type === 'wsh') txType = 'segwit';
|
|
335
|
+
if (first.type === 'sh') {
|
|
336
|
+
if (!input.redeemScript) throw new Error('inputType: sh without redeemScript');
|
|
337
|
+
let child = OutScript.decode(input.redeemScript);
|
|
338
|
+
if (child.type === 'wpkh' || child.type === 'wsh') txType = 'segwit';
|
|
339
|
+
stack.push(child);
|
|
340
|
+
cur = child;
|
|
341
|
+
type += `-${child.type}`;
|
|
342
|
+
}
|
|
343
|
+
// wsh can be inside sh
|
|
344
|
+
if (cur.type === 'wsh') {
|
|
345
|
+
if (!input.witnessScript) throw new Error('inputType: wsh without witnessScript');
|
|
346
|
+
let child = OutScript.decode(input.witnessScript);
|
|
347
|
+
if (child.type === 'wsh') txType = 'segwit';
|
|
348
|
+
stack.push(child);
|
|
349
|
+
cur = child;
|
|
350
|
+
type += `-${child.type}`;
|
|
351
|
+
}
|
|
352
|
+
const last = stack[stack.length - 1];
|
|
353
|
+
if (last.type === 'sh' || last.type === 'wsh')
|
|
354
|
+
throw new Error('inputType: sh/wsh cannot be terminal type');
|
|
355
|
+
const lastScript = OutScript.encode(last);
|
|
356
|
+
const res = {
|
|
357
|
+
type,
|
|
358
|
+
txType,
|
|
359
|
+
last,
|
|
360
|
+
lastScript,
|
|
361
|
+
defaultSighash,
|
|
362
|
+
sighash: input.sighashType || defaultSighash,
|
|
363
|
+
};
|
|
364
|
+
if (txType === 'legacy' && !allowLegacyWitnessUtxo && !input.nonWitnessUtxo) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Transaction/sign: legacy input without nonWitnessUtxo, can result in attack that forces paying higher fees. Pass allowLegacyWitnessUtxo=true, if you sure`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return res;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
233
373
|
export class Transaction {
|
|
234
374
|
private global: psbt.PSBTKeyMapKeys<typeof psbt.PSBTGlobal> = {};
|
|
235
375
|
private inputs: psbt.TransactionInput[] = []; // use getInput()
|
|
@@ -277,11 +417,13 @@ export class Transaction {
|
|
|
277
417
|
const tx = new Transaction({ ...opts, version, lockTime, PSBTVersion });
|
|
278
418
|
// We need slice here, because otherwise
|
|
279
419
|
const inputCount = PSBTVersion === 0 ? unsigned?.inputs.length : parsed.global.inputCount;
|
|
280
|
-
tx.inputs = parsed.inputs.slice(0, inputCount).map((i, j) =>
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
420
|
+
tx.inputs = parsed.inputs.slice(0, inputCount).map((i, j) =>
|
|
421
|
+
validateInput({
|
|
422
|
+
finalScriptSig: P.EMPTY,
|
|
423
|
+
...parsed.global.unsignedTx?.inputs[j],
|
|
424
|
+
...i,
|
|
425
|
+
})
|
|
426
|
+
);
|
|
285
427
|
const outputCount = PSBTVersion === 0 ? unsigned?.outputs.length : parsed.global.outputCount;
|
|
286
428
|
tx.outputs = parsed.outputs.slice(0, outputCount).map((i, j) => ({
|
|
287
429
|
...i,
|
|
@@ -299,7 +441,9 @@ export class Transaction {
|
|
|
299
441
|
// 'PSBT version=0 export for transaction without inputs disabled, please use version=2. Please check `toPSBT` method for explanation.'
|
|
300
442
|
// );
|
|
301
443
|
// }
|
|
302
|
-
const inputs = this.inputs.map((i) =>
|
|
444
|
+
const inputs = this.inputs.map((i) =>
|
|
445
|
+
validateInput(psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, i))
|
|
446
|
+
);
|
|
303
447
|
for (const inp of inputs) {
|
|
304
448
|
// Don't serialize empty fields
|
|
305
449
|
if (inp.partialSig && !inp.partialSig.length) delete inp.partialSig;
|