@scure/btc-signer 1.1.1 → 1.2.1
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 +146 -8
- package/index.ts +545 -120
- package/{index.js → lib/esm/index.js} +516 -129
- package/lib/esm/package.json +1 -0
- package/{index.d.ts → lib/index.d.ts} +199 -5
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +3048 -0
- package/package.json +20 -12
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -5,11 +5,11 @@ 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
|
-
-
|
|
12
|
-
- 🪶 ~2600 lines
|
|
12
|
+
- 🪶 3000 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
|
|
|
@@ -40,8 +40,6 @@ import * as btc from '@scure/btc-signer';
|
|
|
40
40
|
// import * as btc from "npm:@scure/btc-signer@1.0.0"; // Deno
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
### Table of Contents
|
|
44
|
-
|
|
45
43
|
- [Payments](#payments)
|
|
46
44
|
- [P2PK Pay To Public Key](#p2pk-pay-to-public-key)
|
|
47
45
|
- [P2PKH Public Key Hash](#p2pkh-public-key-hash)
|
|
@@ -53,17 +51,22 @@ import * as btc from '@scure/btc-signer';
|
|
|
53
51
|
- [P2TR Taproot](#p2tr-taproot)
|
|
54
52
|
- [P2TR-NS Taproot multisig](#p2tr-ns-taproot-multisig)
|
|
55
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)
|
|
56
55
|
- [Transaction](#transaction)
|
|
57
56
|
- [Encode/decode](#encodedecode)
|
|
58
57
|
- [Inputs](#inputs)
|
|
59
58
|
- [Outputs](#outputs)
|
|
60
59
|
- [Basic transaction sign](#basic-transaction-sign)
|
|
61
60
|
- [BIP174 PSBT multi-sig example](#bip174-psbt-multi-sig-example)
|
|
61
|
+
- [UTXO selection](#utxo-selection)
|
|
62
62
|
- [Utils](#utils)
|
|
63
63
|
- [getAddress](#getaddress)
|
|
64
64
|
- [WIF](#wif)
|
|
65
65
|
- [Script](#script)
|
|
66
66
|
- [OutScript](#outscript)
|
|
67
|
+
- [Security](#security)
|
|
68
|
+
- [Supply chain security](#supply-chain-security)
|
|
69
|
+
- [License](#license)
|
|
67
70
|
|
|
68
71
|
## Payments
|
|
69
72
|
|
|
@@ -624,8 +627,14 @@ const hdkey = bip32.HDKey.fromExtendedKey(epriv, testnet.bip32);
|
|
|
624
627
|
// const seed = 'cUkG8i1RFfWGWy5ziR11zJ5V4U4W3viSFCfyJmZnvQaUsd1xuF3T';
|
|
625
628
|
const tx = new btc.Transaction();
|
|
626
629
|
// A creator creating a PSBT for a transaction which creates the following outputs:
|
|
627
|
-
tx.addOutput({
|
|
628
|
-
|
|
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
|
+
});
|
|
629
638
|
// and spends the following inputs:
|
|
630
639
|
tx.addInput({
|
|
631
640
|
txid: '75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858',
|
|
@@ -696,8 +705,7 @@ tx2.updateOutput(1, {
|
|
|
696
705
|
const psbt2 = tx2.toPSBT();
|
|
697
706
|
// An updater which adds SIGHASH_ALL to the above PSBT must create this PSBT:
|
|
698
707
|
const tx3 = btc.Transaction.fromPSBT(psbt2);
|
|
699
|
-
for (let i = 0; i < tx3.inputs.length; i++)
|
|
700
|
-
tx3.updateInput(i, { sighashType: btc.SigHash.ALL });
|
|
708
|
+
for (let i = 0; i < tx3.inputs.length; i++) tx3.updateInput(i, { sighashType: btc.SigHash.ALL });
|
|
701
709
|
const psbt3 = tx3.toPSBT();
|
|
702
710
|
/*
|
|
703
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:
|
|
@@ -734,6 +742,134 @@ deepStrictEqual(
|
|
|
734
742
|
);
|
|
735
743
|
```
|
|
736
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
|
+
|
|
737
873
|
## Utils
|
|
738
874
|
|
|
739
875
|
### getAddress
|
|
@@ -844,6 +980,8 @@ The library has been independently audited:
|
|
|
844
980
|
- [Changes since audit](https://github.com/paulmillr/scure-btc-signer/compare/0.3.0..main).
|
|
845
981
|
- The audit has been funded by [Ryan Shea](https://shea.io)
|
|
846
982
|
|
|
983
|
+
UTXO selection functionality has not been audited yet.
|
|
984
|
+
|
|
847
985
|
### Supply chain security
|
|
848
986
|
|
|
849
987
|
1. **Commits** are signed with PGP keys, to prevent forgery. Make sure to verify commit signatures.
|