@scure/btc-signer 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,21 +5,22 @@ Audited & minimal library for creating, signing & decoding Bitcoin transactions.
5
5
  - 🔒 [**Audited**](#security) by an independent security firm
6
6
  - ✍️ Create transactions, inputs, outputs, sign them
7
7
  - 📡 No network code: simplified audits and offline usage
8
+ - 🔀 UTXO selection with different strategies
8
9
  - 🎻 Classic & SegWit: P2PK, P2PKH, P2WPKH, P2SH, P2WSH, P2MS
9
10
  - 🧪 Schnorr & Taproot BIP340/BIP341: P2TR, P2TR-NS, P2TR-MS
10
11
  - 📨 BIP174 PSBT
11
- - 👥 Multisig support
12
12
  - 🪶 ~2600 lines
13
13
 
14
14
  Initial development has been funded by [Ryan Shea](https://shea.io). Check out [the demo](https://signerdemo.micro-btc.dev/) & [its github](https://github.com/shea256/micro-btc-web-demo).
15
15
 
16
16
  ### This library belongs to _scure_
17
17
 
18
- > **scure** — secure, independently audited packages for every use case.
18
+ > **scure** — audited micro-libraries.
19
19
 
20
- - Minimal or zero dependencies
21
- - Releases are signed with PGP keys and built transparently with NPM provenance
22
- - Check out all libraries:
20
+ - Zero or minimal dependencies
21
+ - Highly readable TypeScript / JS code
22
+ - PGP-signed releases and transparent NPM builds
23
+ - Check out [homepage](https://paulmillr.com/noble/#scure) & all libraries:
23
24
  [base](https://github.com/paulmillr/scure-base),
24
25
  [bip32](https://github.com/paulmillr/scure-bip32),
25
26
  [bip39](https://github.com/paulmillr/scure-bip39),
@@ -39,8 +40,6 @@ import * as btc from '@scure/btc-signer';
39
40
  // import * as btc from "npm:@scure/btc-signer@1.0.0"; // Deno
40
41
  ```
41
42
 
42
- ### Table of Contents
43
-
44
43
  - [Payments](#payments)
45
44
  - [P2PK Pay To Public Key](#p2pk-pay-to-public-key)
46
45
  - [P2PKH Public Key Hash](#p2pkh-public-key-hash)
@@ -52,17 +51,22 @@ import * as btc from '@scure/btc-signer';
52
51
  - [P2TR Taproot](#p2tr-taproot)
53
52
  - [P2TR-NS Taproot multisig](#p2tr-ns-taproot-multisig)
54
53
  - [P2TR-MS Taproot M-of-N multisig](#p2tr-ms-taproot-m-of-n-multisig)
54
+ - [P2TR-PK Taproot single P2PK script](#p2tr-pk-taproot-single-p2pk-script)
55
55
  - [Transaction](#transaction)
56
56
  - [Encode/decode](#encodedecode)
57
57
  - [Inputs](#inputs)
58
58
  - [Outputs](#outputs)
59
59
  - [Basic transaction sign](#basic-transaction-sign)
60
60
  - [BIP174 PSBT multi-sig example](#bip174-psbt-multi-sig-example)
61
+ - [UTXO selection](#utxo-selection)
61
62
  - [Utils](#utils)
62
63
  - [getAddress](#getaddress)
63
64
  - [WIF](#wif)
64
65
  - [Script](#script)
65
66
  - [OutScript](#outscript)
67
+ - [Security](#security)
68
+ - [Supply chain security](#supply-chain-security)
69
+ - [License](#license)
66
70
 
67
71
  ## Payments
68
72
 
@@ -530,7 +534,7 @@ type TransactionOutput = {
530
534
 
531
535
  tx.addOutput(o: TransactionOutput): number;
532
536
  tx.updateOutput(idx: number, output: TransactionOutput);
533
- tx.addOutputAddress(address: string, amount: string | bigint, network = NETWORK): number;
537
+ tx.addOutputAddress(address: string, amount: bigint, network = NETWORK): number;
534
538
 
535
539
  const compressed = hex.decode(
536
540
  '030000000000000000000000000000000000000000000000000000000000000001'
@@ -623,8 +627,14 @@ const hdkey = bip32.HDKey.fromExtendedKey(epriv, testnet.bip32);
623
627
  // const seed = 'cUkG8i1RFfWGWy5ziR11zJ5V4U4W3viSFCfyJmZnvQaUsd1xuF3T';
624
628
  const tx = new btc.Transaction();
625
629
  // A creator creating a PSBT for a transaction which creates the following outputs:
626
- tx.addOutput({ script: '0014d85c2b71d0060b09c9886aeb815e50991dda124d', amount: btc.Decimal.decode('1.49990000') });
627
- tx.addOutput({ script: '001400aea9a2e5f0f876a588df5546e8742d1d87008f', amount: btc.Decimal.decode('1.00000000') });
630
+ tx.addOutput({
631
+ script: '0014d85c2b71d0060b09c9886aeb815e50991dda124d',
632
+ amount: btc.Decimal.decode('1.49990000'),
633
+ });
634
+ tx.addOutput({
635
+ script: '001400aea9a2e5f0f876a588df5546e8742d1d87008f',
636
+ amount: btc.Decimal.decode('1.00000000'),
637
+ });
628
638
  // and spends the following inputs:
629
639
  tx.addInput({
630
640
  txid: '75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858',
@@ -695,8 +705,7 @@ tx2.updateOutput(1, {
695
705
  const psbt2 = tx2.toPSBT();
696
706
  // An updater which adds SIGHASH_ALL to the above PSBT must create this PSBT:
697
707
  const tx3 = btc.Transaction.fromPSBT(psbt2);
698
- for (let i = 0; i < tx3.inputs.length; i++)
699
- tx3.updateInput(i, { sighashType: btc.SigHash.ALL });
708
+ for (let i = 0; i < tx3.inputs.length; i++) tx3.updateInput(i, { sighashType: btc.SigHash.ALL });
700
709
  const psbt3 = tx3.toPSBT();
701
710
  /*
702
711
  Given the above updated PSBT, a signer that supports SIGHASH_ALL for P2PKH and P2WPKH spends and uses RFC6979 for nonce generation and has the following keys:
@@ -733,6 +742,134 @@ deepStrictEqual(
733
742
  );
734
743
  ```
735
744
 
745
+ ### UTXO selection
746
+
747
+ UTXO selection is the process of choosing which UTXOs to use as inputs
748
+ when making an on-chain bitcoin payment. The library:
749
+
750
+ - can create tx, integrated with the signer
751
+ - ensures change address is always specified
752
+ - supports bip69
753
+ - supports segwit + taproot
754
+ - calculates weight with good precision
755
+ - implements multiple strategies
756
+
757
+ Taproot estimation is precise, but you have to pass sighash if you want to use non-default one,
758
+ because it changes signature size. For complex taproot trees you need to filter tapLeafScript
759
+ to include only leafs which you can sign we estimate size with smallest leaf (same as finalization),
760
+ but in specific case keys for this leaf can be unavailable (complex multisig)
761
+
762
+ `Oldest` / `Newest` expects UTXO provided in historical order (oldest first),
763
+ otherwise we have no way to detect age of tx.
764
+
765
+ #### Strategies
766
+
767
+ Strategy selection is complicated. Best should be: `exactBiggest/accumSmallest`.
768
+
769
+ `exactBiggest/accumBiggest` creates tx with smallest fees,
770
+ but it breaks big outputs to small ones, which in the end will create
771
+ a lot of outputs close to dust.
772
+
773
+ - `default`: good for privacy, same as `exactBiggest/accumBiggest`
774
+ - `all`: send all coins to change address (consolidation)
775
+ - `accum`: accumulates inputs until the target value (+fees) is reached, skipping detrimental inputs
776
+ - `exact`: accumulates inputs until the target value (+fees) is matched, does not accumulate inputs
777
+ that go over the target value (within a threshold)
778
+ - `accumNewest`
779
+ - `accumOldest`
780
+ - `accumSmallest`
781
+ - `accumBiggest`
782
+ - `exactNewest/accumNewest`
783
+ - `exactNewest/accumOldest`
784
+ - `exactNewest/accumSmallest`
785
+ - `exactNewest/accumBiggest`
786
+ - `exactOldest/accumNewest`
787
+ - `exactOldest/accumOldest`
788
+ - `exactOldest/accumSmallest`
789
+ - `exactOldest/accumBiggest`
790
+ - `exactSmallest/accumNewest`
791
+ - `exactSmallest/accumOldest`
792
+ - `exactSmallest/accumSmallest`
793
+ - `exactSmallest/accumBiggest`
794
+ - `exactBiggest/accumNewest`
795
+ - `exactBiggest/accumOldest`
796
+ - `exactBiggest/accumSmallest`
797
+ - `exactBiggest/accumBiggest`
798
+
799
+ #### Example
800
+
801
+ ```ts
802
+ const privKey = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
803
+ const pubKey = secp256k1.getPublicKey(privKey, true);
804
+ const spend = btc.p2wpkh(pubKey, regtest);
805
+ const utxo = [
806
+ {
807
+ ...spend, // add witness/redeem scripts from spend
808
+ // Get txid, index from explorer/network
809
+ txid: hex.decode('0af50a00a22f74ece24c12cd667c290d3a35d48124a69f4082700589172a3aa2'),
810
+ index: 0,
811
+ // utxo tx information
812
+ // script can be used from spend itself or from explorer
813
+ witnessUtxo: { script: spend.script, amount: 100_000n }, // value in satoshi
814
+ },
815
+ {
816
+ ...spend,
817
+ txid: hex.decode('0af50a00a22f74ece24c12cd667c290d3a35d48124a69f4082700589172a3aa2'),
818
+ index: 1,
819
+ witnessUtxo: { script: spend.script, amount: btc.Decimal.decode('1.5') }, // value in btc
820
+ },
821
+ // {
822
+ // ...spend,
823
+ // txid: hex.decode('75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858'),
824
+ // index: 0,
825
+ // // tx hex from blockchain (required for non-SegWit UTXO)
826
+ // nonWitnessUtxo: hex.decode(
827
+ // '0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000'
828
+ // ),
829
+ // },
830
+ ];
831
+ const outputs = [
832
+ { address: '2MvpbAgedBzJUBZWesDwdM7p3FEkBEwq3n3', amount: 50_000n }, // amount in satoshi
833
+ {
834
+ address: 'bcrt1pw53jtgez0wf69n06fchp0ctk48620zdscnrj8heh86wykp9mv20q7vd3gm',
835
+ amount: btc.Decimal.decode('0.5'), // amount in btc
836
+ },
837
+ ];
838
+ // Send all utxo to specific address (consolidation):
839
+ // const selected = btc.selectUTXO(utxo, [], 'all', {
840
+ // changeAddress: 'bcrt1pea3850rzre54e53eh7suwmrwc66un6nmu9npd7eqrhd6g4lh8uqsxcxln8', ...
841
+ const selected = btc.selectUTXO(utxo, outputs, 'default', {
842
+ changeAddress: 'bcrt1pea3850rzre54e53eh7suwmrwc66un6nmu9npd7eqrhd6g4lh8uqsxcxln8', // required, address to send change
843
+ feePerByte: 2n, // require, fee per vbyte in satoshi
844
+ bip69: true, // lexicographical Indexing of Transaction Inputs and Outputs
845
+ createTx: true, // create tx with selected inputs/outputs
846
+ network: regtest,
847
+ });
848
+ // NOTE: 'selected' will 'undefined' if there is not enough funds
849
+ deepStrictEqual(selected.fee, 394n); // estimated fee
850
+ deepStrictEqual(selected.change, true); // change address used
851
+ deepStrictEqual(selected.outputs, [
852
+ { address: '2MvpbAgedBzJUBZWesDwdM7p3FEkBEwq3n3', amount: 50000n },
853
+ {
854
+ address: 'bcrt1pw53jtgez0wf69n06fchp0ctk48620zdscnrj8heh86wykp9mv20q7vd3gm',
855
+ amount: 50_000_000n,
856
+ },
857
+ // Change address
858
+ // NOTE: with bip69 it is not neccesarily last item in outputs
859
+ {
860
+ address: 'bcrt1pea3850rzre54e53eh7suwmrwc66un6nmu9npd7eqrhd6g4lh8uqsxcxln8',
861
+ amount: 99_949_606n,
862
+ },
863
+ ]);
864
+ // No need to create tx manually!
865
+ const { tx } = selected;
866
+ tx.sign(privKey);
867
+ tx.finalize();
868
+ deepStrictEqual(tx.id, 'b702078d65edd65a84b2a97a669df5631b06f42a67b0d7090e540b02cc65aed5');
869
+ // real tx fee, can be bigger than estimated, since we expect signatures of maximal size
870
+ deepStrictEqual(tx.fee, 394n);
871
+ ```
872
+
736
873
  ## Utils
737
874
 
738
875
  ### getAddress
@@ -843,6 +980,8 @@ The library has been independently audited:
843
980
  - [Changes since audit](https://github.com/paulmillr/scure-btc-signer/compare/0.3.0..main).
844
981
  - The audit has been funded by [Ryan Shea](https://shea.io)
845
982
 
983
+ UTXO selection functionality has not been audited yet.
984
+
846
985
  ### Supply chain security
847
986
 
848
987
  1. **Commits** are signed with PGP keys, to prevent forgery. Make sure to verify commit signatures.