@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.
Files changed (65) hide show
  1. package/README.md +52 -1
  2. package/esm/_type_test.js +1 -1
  3. package/esm/index.d.ts +12 -12
  4. package/esm/index.d.ts.map +1 -1
  5. package/esm/index.js +10 -10
  6. package/esm/index.js.map +1 -1
  7. package/esm/musig2.js +1 -1
  8. package/esm/p2p.d.ts +38 -0
  9. package/esm/p2p.d.ts.map +1 -0
  10. package/esm/p2p.js +156 -0
  11. package/esm/p2p.js.map +1 -0
  12. package/esm/payment.d.ts +77 -21
  13. package/esm/payment.d.ts.map +1 -1
  14. package/esm/payment.js +63 -60
  15. package/esm/payment.js.map +1 -1
  16. package/esm/psbt.d.ts +1 -1
  17. package/esm/psbt.d.ts.map +1 -1
  18. package/esm/psbt.js +2 -35
  19. package/esm/psbt.js.map +1 -1
  20. package/esm/script.js +1 -1
  21. package/esm/transaction.d.ts +56 -4
  22. package/esm/transaction.d.ts.map +1 -1
  23. package/esm/transaction.js +151 -8
  24. package/esm/transaction.js.map +1 -1
  25. package/esm/utxo.d.ts +3 -55
  26. package/esm/utxo.d.ts.map +1 -1
  27. package/esm/utxo.js +5 -114
  28. package/esm/utxo.js.map +1 -1
  29. package/index.d.ts +12 -12
  30. package/index.d.ts.map +1 -1
  31. package/index.js +58 -58
  32. package/index.js.map +1 -1
  33. package/musig2.d.ts +1 -1
  34. package/musig2.js +2 -2
  35. package/p2p.d.ts +38 -0
  36. package/p2p.d.ts.map +1 -0
  37. package/p2p.js +158 -0
  38. package/p2p.js.map +1 -0
  39. package/package.json +11 -11
  40. package/payment.d.ts +77 -21
  41. package/payment.d.ts.map +1 -1
  42. package/payment.js +81 -78
  43. package/payment.js.map +1 -1
  44. package/psbt.d.ts +1 -1
  45. package/psbt.d.ts.map +1 -1
  46. package/psbt.js +22 -55
  47. package/psbt.js.map +1 -1
  48. package/script.js +3 -3
  49. package/src/_type_test.ts +1 -1
  50. package/src/index.ts +12 -12
  51. package/src/musig2.ts +1 -1
  52. package/src/p2p.ts +158 -0
  53. package/src/payment.ts +160 -75
  54. package/src/psbt.ts +2 -36
  55. package/src/script.ts +1 -1
  56. package/src/transaction.ts +156 -12
  57. package/src/utxo.ts +9 -111
  58. package/transaction.d.ts +56 -4
  59. package/transaction.d.ts.map +1 -1
  60. package/transaction.js +222 -75
  61. package/transaction.js.map +1 -1
  62. package/utxo.d.ts +3 -55
  63. package/utxo.d.ts.map +1 -1
  64. package/utxo.js +43 -156
  65. 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.js';
4
- import { OpToNum, Script, type ScriptType, VarBytes } from './script.js';
5
- import * as u from './utils.js';
6
- import { type BTC_NETWORK, type Bytes, NETWORK } from './utils.js';
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
- // @ts-ignore
333
- export const p2pk = (pubkey: Bytes, network: BTC_NETWORK = NETWORK): P2Ret => {
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
- export const p2pkh = (publicKey: Bytes, network: BTC_NETWORK = NETWORK): P2Ret => {
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
- export const p2sh = (child: P2Ret, network: BTC_NETWORK = NETWORK): P2Ret => {
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
- const res: P2Ret = {
358
- type: 'sh',
359
- redeemScript: cs,
360
- script: OutScript.encode({ type: 'sh', hash }),
361
- address: Address(network).encode({ type: 'sh', hash }),
362
- };
363
- if (child.witnessScript) res.witnessScript = child.witnessScript;
364
- return res;
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
- export const p2wsh = (child: P2Ret, network: BTC_NETWORK = NETWORK): P2Ret => {
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
- export const p2wpkh = (publicKey: Bytes, network: BTC_NETWORK = NETWORK): P2Ret => {
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
- export const p2ms = (m: number, pubkeys: Bytes[], allowSamePubkeys = false): P2Ret => {
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 { type: 'ms', script: OutScript.encode({ type: 'ms', pubkeys, m }) };
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 P2TROut = P2Ret & {
495
+ export type P2TR = {
496
+ type: 'tr';
497
+ script: Bytes;
498
+ address: string;
442
499
  tweakedPubkey: Bytes;
443
500
  tapInternalKey: Bytes;
444
- tapMerkleRoot?: Bytes;
445
- tapLeafScript?: TransactionInput['tapLeafScript'];
446
- leaves?: TaprootLeaf[];
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<P2TROut>;
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
- ): P2TROut {
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
- let hashedTree = tree
580
- ? taprootAddPath(taprootHashTree(tree, pubKey, allowUnknownOutputs, customScripts))
581
- : undefined;
582
- const tapMerkleRoot = hashedTree ? hashedTree.hash : undefined;
583
- const [tweakedPubkey, parity] = u.taprootTweakPubkey(pubKey, tapMerkleRoot || P.EMPTY);
584
- let leaves;
585
- if (hashedTree) {
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 const p2tr_ns = (m: number, pubkeys: Bytes[], allowSamePubkeys = false): P2Ret[] => {
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((i) => ({
657
- type: 'tr_ns',
658
- script: OutScript.encode({ type: 'tr_ns', pubkeys: i }),
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 const p2tr_pk = (pubkey: Bytes): P2Ret => p2tr_ns(1, [pubkey], undefined)[0];
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[]): Uint8Array[] =>
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.js';
12
- import { Transaction } from './transaction.js'; // circular
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
@@ -1,5 +1,5 @@
1
1
  import * as P from 'micro-packed';
2
- import { isBytes } from './utils.js';
2
+ import { isBytes } from './utils.ts';
3
3
 
4
4
  export const MAX_SCRIPT_BYTE_LENGTH = 520;
5
5
 
@@ -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.js';
4
- import * as psbt from './psbt.js'; // circular
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.js';
14
- import * as u from './utils.js';
15
- import { type Bytes, NETWORK, concatBytes, equalBytes, isBytes } from './utils.js';
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
- finalScriptSig: P.EMPTY,
282
- ...parsed.global.unsignedTx?.inputs[j],
283
- ...i,
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) => psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, 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;