btcaaron 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {btcaaron-0.2.2 → btcaaron-0.2.3}/PKG-INFO +62 -29
- {btcaaron-0.2.2 → btcaaron-0.2.3}/README.md +54 -25
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/__init__.py +39 -3
- btcaaron-0.2.3/btcaaron/key.py +296 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/legacy.py +59 -0
- btcaaron-0.2.3/btcaaron/node_rpc.py +110 -0
- btcaaron-0.2.3/btcaaron/script/__init__.py +26 -0
- btcaaron-0.2.3/btcaaron/script/templates.py +239 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/spend/builder.py +15 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/tree/builder.py +50 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/tree/leaf.py +1 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/tree/program.py +14 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron.egg-info/PKG-INFO +62 -29
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron.egg-info/SOURCES.txt +4 -1
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron.egg-info/requires.txt +2 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/setup.py +17 -5
- {btcaaron-0.2.2 → btcaaron-0.2.3}/tests/test_btcaaron_v02.py +266 -2
- btcaaron-0.2.3/tests/test_version.py +18 -0
- btcaaron-0.2.2/btcaaron/key.py +0 -145
- btcaaron-0.2.2/btcaaron/script/__init__.py +0 -7
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/btcaaron.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/errors.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/explain/__init__.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/explain/program.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/explain/transaction.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/network/__init__.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/network/blockstream.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/network/mempool.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/network/provider.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/network/utxo.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/psbt.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/script/script.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/spend/__init__.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/spend/transaction.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/tree/__init__.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron/tree/tapmath.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron.egg-info/dependency_links.txt +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron.egg-info/entry_points.txt +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/btcaaron.egg-info/top_level.txt +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/setup.cfg +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/tests/test_btcaaron_v01.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/tests/test_btcaaron_v01_cpfp.py +0 -0
- {btcaaron-0.2.2 → btcaaron-0.2.3}/tests/test_rawscript_tapmath.py +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: btcaaron
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary:
|
|
5
|
-
Home-page: https://
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Pragmatic Bitcoin Taproot toolkit: Legacy/SegWit/Taproot, PSBT, optional Signet workflows, and Bitcoin Inquisition opcode templates (OP_CAT, CSFS, CTV).
|
|
5
|
+
Home-page: https://github.com/aaron-recompile/btcaaron
|
|
6
6
|
Author: Aaron Zhang
|
|
7
7
|
Author-email: aaron.recompile@gmail.com
|
|
8
|
-
|
|
8
|
+
Project-URL: X (author), https://x.com/aaron_recompile
|
|
9
|
+
Keywords: bitcoin,taproot,tapscript,bip341,bip342,schnorr,p2tr,taptree,script-path,miniscript,psbt,regtest,testnet,signet,bitcoin-inquisition,checksigfromstack,ctv
|
|
9
10
|
Classifier: Programming Language :: Python :: 3
|
|
10
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
11
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -20,6 +21,8 @@ Requires-Python: >=3.10,<3.13
|
|
|
20
21
|
Description-Content-Type: text/markdown
|
|
21
22
|
Requires-Dist: requests<3.0.0,>=2.25.0
|
|
22
23
|
Requires-Dist: bitcoin-utils<0.8.0,>=0.7.3
|
|
24
|
+
Requires-Dist: bip32>=4.0.0
|
|
25
|
+
Requires-Dist: base58>=2.1.1
|
|
23
26
|
Dynamic: author
|
|
24
27
|
Dynamic: author-email
|
|
25
28
|
Dynamic: classifier
|
|
@@ -27,6 +30,7 @@ Dynamic: description
|
|
|
27
30
|
Dynamic: description-content-type
|
|
28
31
|
Dynamic: home-page
|
|
29
32
|
Dynamic: keywords
|
|
33
|
+
Dynamic: project-url
|
|
30
34
|
Dynamic: requires-dist
|
|
31
35
|
Dynamic: requires-python
|
|
32
36
|
Dynamic: summary
|
|
@@ -35,15 +39,17 @@ Dynamic: summary
|
|
|
35
39
|
|
|
36
40
|
[](https://opensats.org)
|
|
37
41
|
|
|
38
|
-
A pragmatic Bitcoin toolkit
|
|
42
|
+
A pragmatic Bitcoin toolkit for Taproot engineering — Legacy, SegWit, and Taproot flows, PSBT, and optional **Signet** / **Bitcoin Inquisition** opcode templates (OP_CAT, CSFS, CTV).
|
|
39
43
|
|
|
40
|
-
Designed for reproducible testnet experiments, educational workflows, and script-path development.
|
|
44
|
+
Designed for reproducible testnet and signet experiments, educational workflows, and script-path development.
|
|
41
45
|
|
|
42
46
|
If you find btcaaron useful, a GitHub star is appreciated.
|
|
43
47
|
|
|
48
|
+
👉 Looking for Bitcoin Inquisition experimental opcode templates (OP_CAT / CSFS / CTV)? See **[INQUISITION.md](INQUISITION.md)**.
|
|
49
|
+
|
|
44
50
|
## Current Status
|
|
45
51
|
|
|
46
|
-
**v0.2.
|
|
52
|
+
**v0.2.3 (alpha preview)** — Core Taproot spend-path workflows are implemented and testnet/regtest-verified.
|
|
47
53
|
Current focus is release hardening: broader validation coverage, documentation alignment, and contributor testing feedback.
|
|
48
54
|
|
|
49
55
|
## Features
|
|
@@ -58,7 +64,7 @@ Production-tested on testnet with real transactions:
|
|
|
58
64
|
- Broadcast to Blockstream / Mempool endpoints
|
|
59
65
|
- Developer helpers (`WIFKey`, `quick_transfer`)
|
|
60
66
|
|
|
61
|
-
### Available Now (v0.2.
|
|
67
|
+
### Available Now (v0.2.3 - Alpha Preview)
|
|
62
68
|
|
|
63
69
|
Testnet-verified with real transactions (23 tests, all passing):
|
|
64
70
|
|
|
@@ -118,25 +124,7 @@ python -m pip install -e .
|
|
|
118
124
|
btcaaron-doctor
|
|
119
125
|
```
|
|
120
126
|
|
|
121
|
-
## Quick Start
|
|
122
|
-
|
|
123
|
-
```python
|
|
124
|
-
from btcaaron import WIFKey, quick_transfer
|
|
125
|
-
|
|
126
|
-
wif = "your_testnet_wif"
|
|
127
|
-
|
|
128
|
-
key = WIFKey(wif)
|
|
129
|
-
print("Taproot:", key.get_taproot().address)
|
|
130
|
-
|
|
131
|
-
balance = key.get_taproot().get_balance()
|
|
132
|
-
print("Balance:", balance, "sats")
|
|
133
|
-
|
|
134
|
-
if balance > 1000:
|
|
135
|
-
txid = quick_transfer(wif, "taproot", "tb1q...", amount=500, fee=300)
|
|
136
|
-
print("Broadcasted:", txid)
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## v0.2.2 API Example
|
|
127
|
+
## Quick Start (v0.2.3)
|
|
140
128
|
|
|
141
129
|
*Taproot-native API — core features available now.*
|
|
142
130
|
|
|
@@ -168,6 +156,51 @@ tx = (program.spend("hash")
|
|
|
168
156
|
tx.broadcast()
|
|
169
157
|
```
|
|
170
158
|
|
|
159
|
+
## Legacy Quick Start (v0.1.x)
|
|
160
|
+
|
|
161
|
+
For backward compatibility with the v0.1.x API:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from btcaaron import WIFKey, quick_transfer
|
|
165
|
+
|
|
166
|
+
wif = "your_testnet_wif"
|
|
167
|
+
|
|
168
|
+
key = WIFKey(wif)
|
|
169
|
+
print("Taproot:", key.get_taproot().address)
|
|
170
|
+
|
|
171
|
+
balance = key.get_taproot().get_balance()
|
|
172
|
+
print("Balance:", balance, "sats")
|
|
173
|
+
|
|
174
|
+
if balance > 1000:
|
|
175
|
+
txid = quick_transfer(wif, "taproot", "tb1q...", amount=500, fee=300)
|
|
176
|
+
print("Broadcasted:", txid)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Descriptor Wallet Quick Start (tprv/xpriv)
|
|
180
|
+
|
|
181
|
+
For descriptor-style HD wallet flows (RGB-friendly), derive directly from `tprv`:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from btcaaron import taproot_balance_from_tprv, quick_transfer_tprv, taproot_descriptor_from_tprv
|
|
185
|
+
|
|
186
|
+
tprv = "tprv8ZgxMBicQKs..."
|
|
187
|
+
print(taproot_descriptor_from_tprv(tprv, branch=0)) # tr(tprv.../86h/1h/0h/0/*)
|
|
188
|
+
|
|
189
|
+
balance = taproot_balance_from_tprv(tprv, branch=0, index=0)
|
|
190
|
+
print("Balance:", balance, "sats")
|
|
191
|
+
|
|
192
|
+
txid = quick_transfer_tprv(
|
|
193
|
+
tprv,
|
|
194
|
+
"tb1p...",
|
|
195
|
+
amount=10_000,
|
|
196
|
+
fee=800,
|
|
197
|
+
branch=0,
|
|
198
|
+
index=0,
|
|
199
|
+
debug=True,
|
|
200
|
+
)
|
|
201
|
+
print("Broadcasted:", txid)
|
|
202
|
+
```
|
|
203
|
+
|
|
171
204
|
Full specification in [DESIGN.md](./DESIGN.md).
|
|
172
205
|
|
|
173
206
|
## Testing
|
|
@@ -181,7 +214,7 @@ python -m pytest tests/
|
|
|
181
214
|
Run specific test suites:
|
|
182
215
|
|
|
183
216
|
```bash
|
|
184
|
-
# v0.2.
|
|
217
|
+
# v0.2.3 comprehensive tests (pytest)
|
|
185
218
|
python -m pytest tests/test_btcaaron_v02.py -v
|
|
186
219
|
|
|
187
220
|
# v0.1.x example-based tests
|
|
@@ -227,7 +260,7 @@ See [DESIGN.md](./DESIGN.md) for architecture details and development roadmap.
|
|
|
227
260
|
- default call blocks mainnet broadcast unless `allow_mainnet=True`
|
|
228
261
|
- `dry_run=True` is available for no-side-effect routing checks
|
|
229
262
|
- recommended smoke script: `python3 examples/core_test/scenarios/mainnet_readiness_smoke.py`
|
|
230
|
-
- **v0.2.
|
|
263
|
+
- **v0.2.3 Status**: Core Taproot spend-path flows are implemented and testnet-verified; ongoing work focuses on hardening, PSBT, and documentation.
|
|
231
264
|
|
|
232
265
|
## Acknowledgments
|
|
233
266
|
|
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://opensats.org)
|
|
4
4
|
|
|
5
|
-
A pragmatic Bitcoin toolkit
|
|
5
|
+
A pragmatic Bitcoin toolkit for Taproot engineering — Legacy, SegWit, and Taproot flows, PSBT, and optional **Signet** / **Bitcoin Inquisition** opcode templates (OP_CAT, CSFS, CTV).
|
|
6
6
|
|
|
7
|
-
Designed for reproducible testnet experiments, educational workflows, and script-path development.
|
|
7
|
+
Designed for reproducible testnet and signet experiments, educational workflows, and script-path development.
|
|
8
8
|
|
|
9
9
|
If you find btcaaron useful, a GitHub star is appreciated.
|
|
10
10
|
|
|
11
|
+
👉 Looking for Bitcoin Inquisition experimental opcode templates (OP_CAT / CSFS / CTV)? See **[INQUISITION.md](INQUISITION.md)**.
|
|
12
|
+
|
|
11
13
|
## Current Status
|
|
12
14
|
|
|
13
|
-
**v0.2.
|
|
15
|
+
**v0.2.3 (alpha preview)** — Core Taproot spend-path workflows are implemented and testnet/regtest-verified.
|
|
14
16
|
Current focus is release hardening: broader validation coverage, documentation alignment, and contributor testing feedback.
|
|
15
17
|
|
|
16
18
|
## Features
|
|
@@ -25,7 +27,7 @@ Production-tested on testnet with real transactions:
|
|
|
25
27
|
- Broadcast to Blockstream / Mempool endpoints
|
|
26
28
|
- Developer helpers (`WIFKey`, `quick_transfer`)
|
|
27
29
|
|
|
28
|
-
### Available Now (v0.2.
|
|
30
|
+
### Available Now (v0.2.3 - Alpha Preview)
|
|
29
31
|
|
|
30
32
|
Testnet-verified with real transactions (23 tests, all passing):
|
|
31
33
|
|
|
@@ -85,25 +87,7 @@ python -m pip install -e .
|
|
|
85
87
|
btcaaron-doctor
|
|
86
88
|
```
|
|
87
89
|
|
|
88
|
-
## Quick Start
|
|
89
|
-
|
|
90
|
-
```python
|
|
91
|
-
from btcaaron import WIFKey, quick_transfer
|
|
92
|
-
|
|
93
|
-
wif = "your_testnet_wif"
|
|
94
|
-
|
|
95
|
-
key = WIFKey(wif)
|
|
96
|
-
print("Taproot:", key.get_taproot().address)
|
|
97
|
-
|
|
98
|
-
balance = key.get_taproot().get_balance()
|
|
99
|
-
print("Balance:", balance, "sats")
|
|
100
|
-
|
|
101
|
-
if balance > 1000:
|
|
102
|
-
txid = quick_transfer(wif, "taproot", "tb1q...", amount=500, fee=300)
|
|
103
|
-
print("Broadcasted:", txid)
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## v0.2.2 API Example
|
|
90
|
+
## Quick Start (v0.2.3)
|
|
107
91
|
|
|
108
92
|
*Taproot-native API — core features available now.*
|
|
109
93
|
|
|
@@ -135,6 +119,51 @@ tx = (program.spend("hash")
|
|
|
135
119
|
tx.broadcast()
|
|
136
120
|
```
|
|
137
121
|
|
|
122
|
+
## Legacy Quick Start (v0.1.x)
|
|
123
|
+
|
|
124
|
+
For backward compatibility with the v0.1.x API:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from btcaaron import WIFKey, quick_transfer
|
|
128
|
+
|
|
129
|
+
wif = "your_testnet_wif"
|
|
130
|
+
|
|
131
|
+
key = WIFKey(wif)
|
|
132
|
+
print("Taproot:", key.get_taproot().address)
|
|
133
|
+
|
|
134
|
+
balance = key.get_taproot().get_balance()
|
|
135
|
+
print("Balance:", balance, "sats")
|
|
136
|
+
|
|
137
|
+
if balance > 1000:
|
|
138
|
+
txid = quick_transfer(wif, "taproot", "tb1q...", amount=500, fee=300)
|
|
139
|
+
print("Broadcasted:", txid)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Descriptor Wallet Quick Start (tprv/xpriv)
|
|
143
|
+
|
|
144
|
+
For descriptor-style HD wallet flows (RGB-friendly), derive directly from `tprv`:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from btcaaron import taproot_balance_from_tprv, quick_transfer_tprv, taproot_descriptor_from_tprv
|
|
148
|
+
|
|
149
|
+
tprv = "tprv8ZgxMBicQKs..."
|
|
150
|
+
print(taproot_descriptor_from_tprv(tprv, branch=0)) # tr(tprv.../86h/1h/0h/0/*)
|
|
151
|
+
|
|
152
|
+
balance = taproot_balance_from_tprv(tprv, branch=0, index=0)
|
|
153
|
+
print("Balance:", balance, "sats")
|
|
154
|
+
|
|
155
|
+
txid = quick_transfer_tprv(
|
|
156
|
+
tprv,
|
|
157
|
+
"tb1p...",
|
|
158
|
+
amount=10_000,
|
|
159
|
+
fee=800,
|
|
160
|
+
branch=0,
|
|
161
|
+
index=0,
|
|
162
|
+
debug=True,
|
|
163
|
+
)
|
|
164
|
+
print("Broadcasted:", txid)
|
|
165
|
+
```
|
|
166
|
+
|
|
138
167
|
Full specification in [DESIGN.md](./DESIGN.md).
|
|
139
168
|
|
|
140
169
|
## Testing
|
|
@@ -148,7 +177,7 @@ python -m pytest tests/
|
|
|
148
177
|
Run specific test suites:
|
|
149
178
|
|
|
150
179
|
```bash
|
|
151
|
-
# v0.2.
|
|
180
|
+
# v0.2.3 comprehensive tests (pytest)
|
|
152
181
|
python -m pytest tests/test_btcaaron_v02.py -v
|
|
153
182
|
|
|
154
183
|
# v0.1.x example-based tests
|
|
@@ -194,7 +223,7 @@ See [DESIGN.md](./DESIGN.md) for architecture details and development roadmap.
|
|
|
194
223
|
- default call blocks mainnet broadcast unless `allow_mainnet=True`
|
|
195
224
|
- `dry_run=True` is available for no-side-effect routing checks
|
|
196
225
|
- recommended smoke script: `python3 examples/core_test/scenarios/mainnet_readiness_smoke.py`
|
|
197
|
-
- **v0.2.
|
|
226
|
+
- **v0.2.3 Status**: Core Taproot spend-path flows are implemented and testnet-verified; ongoing work focuses on hardening, PSBT, and documentation.
|
|
198
227
|
|
|
199
228
|
## Acknowledgments
|
|
200
229
|
|
|
@@ -8,7 +8,7 @@ v0.2.x API (new):
|
|
|
8
8
|
from btcaaron import Key, TapTree
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
__version__ = "0.2.
|
|
11
|
+
__version__ = "0.2.3"
|
|
12
12
|
|
|
13
13
|
# ══════════════════════════════════════════════════════════════════
|
|
14
14
|
# Legacy API (v0.1.x) - backward compatible
|
|
@@ -19,16 +19,35 @@ from .legacy import (
|
|
|
19
19
|
BTCTransaction,
|
|
20
20
|
wif_to_addresses,
|
|
21
21
|
quick_transfer,
|
|
22
|
+
quick_transfer_tprv,
|
|
23
|
+
taproot_balance_from_tprv,
|
|
22
24
|
fund_program,
|
|
23
25
|
)
|
|
24
26
|
|
|
25
27
|
# ══════════════════════════════════════════════════════════════════
|
|
26
28
|
# New API (v0.2.x)
|
|
27
29
|
# ══════════════════════════════════════════════════════════════════
|
|
28
|
-
from .key import Key
|
|
30
|
+
from .key import Key, derive_wif_from_tprv, taproot_descriptor_from_tprv, wif_secret_bytes
|
|
29
31
|
from .tree import TapTree, TaprootProgram, LeafDescriptor
|
|
30
32
|
from .spend import SpendBuilder, Transaction
|
|
31
|
-
from .script import
|
|
33
|
+
from .script import (
|
|
34
|
+
Script,
|
|
35
|
+
RawScript,
|
|
36
|
+
ord_inscription_script,
|
|
37
|
+
brc20_mint_json,
|
|
38
|
+
inq_cat_hashlock_script,
|
|
39
|
+
inq_csfs_script,
|
|
40
|
+
inq_ctv_script,
|
|
41
|
+
inq_ctv_template_hash_for_output,
|
|
42
|
+
inq_ctv_program_for_output,
|
|
43
|
+
)
|
|
44
|
+
from .node_rpc import (
|
|
45
|
+
broadcast_tx_hex,
|
|
46
|
+
find_utxo_for_address,
|
|
47
|
+
sats_from_rpc_amount,
|
|
48
|
+
wallet_change_address,
|
|
49
|
+
wallet_send_sats,
|
|
50
|
+
)
|
|
32
51
|
from .psbt import Psbt, PsbtInput, PsbtV2, PsbtV2Input
|
|
33
52
|
from .errors import (
|
|
34
53
|
BtcAaronError,
|
|
@@ -47,10 +66,15 @@ __all__ = [
|
|
|
47
66
|
"BTCTransaction",
|
|
48
67
|
"wif_to_addresses",
|
|
49
68
|
"quick_transfer",
|
|
69
|
+
"quick_transfer_tprv",
|
|
70
|
+
"taproot_balance_from_tprv",
|
|
50
71
|
"fund_program",
|
|
51
72
|
|
|
52
73
|
# New API
|
|
53
74
|
"Key",
|
|
75
|
+
"derive_wif_from_tprv",
|
|
76
|
+
"taproot_descriptor_from_tprv",
|
|
77
|
+
"wif_secret_bytes",
|
|
54
78
|
"TapTree",
|
|
55
79
|
"TaprootProgram",
|
|
56
80
|
"LeafDescriptor",
|
|
@@ -58,6 +82,18 @@ __all__ = [
|
|
|
58
82
|
"Transaction",
|
|
59
83
|
"Script",
|
|
60
84
|
"RawScript",
|
|
85
|
+
"ord_inscription_script",
|
|
86
|
+
"brc20_mint_json",
|
|
87
|
+
"inq_cat_hashlock_script",
|
|
88
|
+
"inq_csfs_script",
|
|
89
|
+
"inq_ctv_script",
|
|
90
|
+
"inq_ctv_template_hash_for_output",
|
|
91
|
+
"inq_ctv_program_for_output",
|
|
92
|
+
"broadcast_tx_hex",
|
|
93
|
+
"find_utxo_for_address",
|
|
94
|
+
"sats_from_rpc_amount",
|
|
95
|
+
"wallet_change_address",
|
|
96
|
+
"wallet_send_sats",
|
|
61
97
|
"Psbt",
|
|
62
98
|
"PsbtInput",
|
|
63
99
|
"PsbtV2",
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
btcaaron.key - Key Management
|
|
3
|
+
|
|
4
|
+
The Key class provides a clean interface for Bitcoin key operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from bitcoinutils.setup import setup
|
|
10
|
+
from bitcoinutils.keys import PrivateKey
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _normalize_network(network: str) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Normalize app-level network labels to bitcoinutils setup labels.
|
|
16
|
+
|
|
17
|
+
bitcoinutils distinguishes mainnet/testnet/regtest. For signet-family
|
|
18
|
+
networks we use testnet address/version rules.
|
|
19
|
+
"""
|
|
20
|
+
n = (network or "testnet").lower()
|
|
21
|
+
if n == "mainnet":
|
|
22
|
+
return "mainnet"
|
|
23
|
+
if n == "regtest":
|
|
24
|
+
return "regtest"
|
|
25
|
+
return "testnet"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_network(network: str) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Set bitcoinutils global network context and return normalized value.
|
|
31
|
+
"""
|
|
32
|
+
normalized = _normalize_network(network)
|
|
33
|
+
setup(normalized)
|
|
34
|
+
return normalized
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _default_coin_type(network: str) -> int:
|
|
38
|
+
"""
|
|
39
|
+
Default BIP44/86 coin type by network.
|
|
40
|
+
|
|
41
|
+
mainnet -> 0, testnet/regtest/signet-family -> 1
|
|
42
|
+
"""
|
|
43
|
+
return 0 if _normalize_network(network) == "mainnet" else 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def wif_secret_bytes(wif: str) -> bytes:
|
|
47
|
+
"""
|
|
48
|
+
Decode WIF and return 32-byte raw private key bytes.
|
|
49
|
+
|
|
50
|
+
Supports both compressed and uncompressed WIF.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
import base58
|
|
54
|
+
except ImportError as exc:
|
|
55
|
+
raise ValueError("Missing dep: pip install base58") from exc
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
decoded = base58.b58decode(wif)
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
raise ValueError(f"Invalid WIF encoding: {exc}") from exc
|
|
61
|
+
|
|
62
|
+
if len(decoded) not in (37, 38):
|
|
63
|
+
raise ValueError("Invalid WIF length")
|
|
64
|
+
|
|
65
|
+
payload, checksum = decoded[:-4], decoded[-4:]
|
|
66
|
+
expected = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
|
|
67
|
+
if checksum != expected:
|
|
68
|
+
raise ValueError("Invalid WIF checksum")
|
|
69
|
+
|
|
70
|
+
prefix = payload[0]
|
|
71
|
+
if prefix not in (0x80, 0xEF):
|
|
72
|
+
raise ValueError("Invalid WIF network/version byte")
|
|
73
|
+
|
|
74
|
+
if len(payload) == 34:
|
|
75
|
+
# Compressed key marker must be present.
|
|
76
|
+
if payload[-1] != 0x01:
|
|
77
|
+
raise ValueError("Invalid compressed WIF marker")
|
|
78
|
+
secret = payload[1:-1]
|
|
79
|
+
else:
|
|
80
|
+
secret = payload[1:]
|
|
81
|
+
|
|
82
|
+
if len(secret) != 32:
|
|
83
|
+
raise ValueError("Invalid private key length in WIF")
|
|
84
|
+
return secret
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def derive_wif_from_tprv(
|
|
88
|
+
tprv: str,
|
|
89
|
+
*,
|
|
90
|
+
branch: int = 0,
|
|
91
|
+
index: int = 0,
|
|
92
|
+
purpose: int = 86,
|
|
93
|
+
coin_type: Optional[int] = None,
|
|
94
|
+
account: int = 0,
|
|
95
|
+
network: str = "testnet",
|
|
96
|
+
) -> str:
|
|
97
|
+
"""
|
|
98
|
+
Derive a compressed child WIF from an account-level xpriv/tprv.
|
|
99
|
+
|
|
100
|
+
Default path follows BIP86:
|
|
101
|
+
m / 86' / coin_type' / account' / branch / index
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
from bip32 import BIP32, HARDENED_INDEX
|
|
105
|
+
import base58
|
|
106
|
+
except ImportError as exc:
|
|
107
|
+
raise ValueError("Missing deps: pip install bip32 base58") from exc
|
|
108
|
+
|
|
109
|
+
if min(branch, index, purpose, account) < 0:
|
|
110
|
+
raise ValueError("branch/index/purpose/account must be non-negative")
|
|
111
|
+
|
|
112
|
+
ct = _default_coin_type(network) if coin_type is None else coin_type
|
|
113
|
+
if ct < 0:
|
|
114
|
+
raise ValueError("coin_type must be non-negative")
|
|
115
|
+
|
|
116
|
+
path = [
|
|
117
|
+
purpose | HARDENED_INDEX,
|
|
118
|
+
ct | HARDENED_INDEX,
|
|
119
|
+
account | HARDENED_INDEX,
|
|
120
|
+
branch,
|
|
121
|
+
index,
|
|
122
|
+
]
|
|
123
|
+
try:
|
|
124
|
+
bip32_obj = BIP32.from_xpriv(tprv)
|
|
125
|
+
private_key = bip32_obj.get_privkey_from_path(path)
|
|
126
|
+
except Exception as exc:
|
|
127
|
+
raise ValueError(f"Invalid tprv/xpriv or derivation path: {exc}") from exc
|
|
128
|
+
|
|
129
|
+
prefix = b"\x80" if _normalize_network(network) == "mainnet" else b"\xEF"
|
|
130
|
+
payload = prefix + private_key + b"\x01"
|
|
131
|
+
checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
|
|
132
|
+
return base58.b58encode(payload + checksum).decode()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def taproot_descriptor_from_tprv(
|
|
136
|
+
tprv: str,
|
|
137
|
+
*,
|
|
138
|
+
branch: int = 0,
|
|
139
|
+
purpose: int = 86,
|
|
140
|
+
coin_type: Optional[int] = None,
|
|
141
|
+
account: int = 0,
|
|
142
|
+
network: str = "testnet",
|
|
143
|
+
wildcard: bool = True,
|
|
144
|
+
index: int = 0,
|
|
145
|
+
) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Build a BIP86-style taproot descriptor string from xpriv/tprv.
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
tr(tprv.../86h/1h/0h/0/*)
|
|
151
|
+
"""
|
|
152
|
+
ct = _default_coin_type(network) if coin_type is None else coin_type
|
|
153
|
+
suffix = "*" if wildcard else str(index)
|
|
154
|
+
return f"tr({tprv}/{purpose}h/{ct}h/{account}h/{branch}/{suffix})"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# Default module context remains testnet-first.
|
|
158
|
+
set_network("testnet")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class Key:
|
|
162
|
+
"""
|
|
163
|
+
Bitcoin Key Manager (Immutable)
|
|
164
|
+
|
|
165
|
+
Handles private key operations, signing, and public key derivation.
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
alice = Key.from_wif("cRxebG1hY6vVgS9CSLNaEbEJaXkpZvc6nFeqqGT7v6gcW7MbzKNT")
|
|
169
|
+
print(alice.xonly) # x-only pubkey for Taproot
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(self, private_key: PrivateKey):
|
|
173
|
+
"""Internal constructor. Use from_wif() or generate() instead."""
|
|
174
|
+
self._private_key = private_key
|
|
175
|
+
self._public_key = private_key.get_public_key()
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def from_wif(cls, wif: str) -> "Key":
|
|
179
|
+
"""
|
|
180
|
+
Create Key from WIF (Wallet Import Format) string.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
wif: WIF-encoded private key
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Key instance
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
ValueError: If WIF format is invalid
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
private_key = PrivateKey(wif)
|
|
193
|
+
return cls(private_key)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
raise ValueError(f"Invalid WIF private key: {e}")
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def from_hex(cls, hex_privkey: str) -> "Key":
|
|
199
|
+
"""
|
|
200
|
+
Create Key from hex-encoded private key.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
hex_privkey: 32-byte hex string
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Key instance
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
private_key = PrivateKey(secret_exponent=int(hex_privkey, 16))
|
|
210
|
+
return cls(private_key)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
raise ValueError(f"Invalid hex private key: {e}")
|
|
213
|
+
|
|
214
|
+
@classmethod
|
|
215
|
+
def from_tprv(
|
|
216
|
+
cls,
|
|
217
|
+
tprv: str,
|
|
218
|
+
*,
|
|
219
|
+
branch: int = 0,
|
|
220
|
+
index: int = 0,
|
|
221
|
+
purpose: int = 86,
|
|
222
|
+
coin_type: Optional[int] = None,
|
|
223
|
+
account: int = 0,
|
|
224
|
+
network: str = "testnet",
|
|
225
|
+
) -> "Key":
|
|
226
|
+
"""
|
|
227
|
+
Create Key by deriving child private key from xpriv/tprv.
|
|
228
|
+
|
|
229
|
+
Default derivation path:
|
|
230
|
+
m/86'/coin_type'/account'/branch/index
|
|
231
|
+
"""
|
|
232
|
+
set_network(network)
|
|
233
|
+
wif = derive_wif_from_tprv(
|
|
234
|
+
tprv,
|
|
235
|
+
branch=branch,
|
|
236
|
+
index=index,
|
|
237
|
+
purpose=purpose,
|
|
238
|
+
coin_type=coin_type,
|
|
239
|
+
account=account,
|
|
240
|
+
network=network,
|
|
241
|
+
)
|
|
242
|
+
return cls.from_wif(wif)
|
|
243
|
+
|
|
244
|
+
@classmethod
|
|
245
|
+
def generate(cls, network: str = "testnet") -> "Key":
|
|
246
|
+
"""
|
|
247
|
+
Generate a new random key.
|
|
248
|
+
|
|
249
|
+
Uses the underlying bitcoin-utils PrivateKey() which generates
|
|
250
|
+
a cryptographically secure random private key via os.urandom().
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
network: "testnet" | "mainnet" (default: testnet)
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Key instance
|
|
257
|
+
"""
|
|
258
|
+
set_network(network)
|
|
259
|
+
private_key = PrivateKey()
|
|
260
|
+
return cls(private_key)
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def wif(self) -> str:
|
|
264
|
+
"""WIF-encoded private key"""
|
|
265
|
+
return self._private_key.to_wif()
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def pubkey(self) -> str:
|
|
269
|
+
"""33-byte compressed public key (hex)"""
|
|
270
|
+
return self._public_key.to_hex()
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def xonly(self) -> str:
|
|
274
|
+
"""32-byte x-only public key for Taproot (hex)"""
|
|
275
|
+
return self._public_key.to_x_only_hex()
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def _internal(self) -> PrivateKey:
|
|
279
|
+
"""Access to underlying bitcoinutils PrivateKey (internal use)"""
|
|
280
|
+
return self._private_key
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def _internal_pub(self):
|
|
284
|
+
"""Access to underlying bitcoinutils PublicKey (internal use)"""
|
|
285
|
+
return self._public_key
|
|
286
|
+
|
|
287
|
+
def __repr__(self) -> str:
|
|
288
|
+
return f"Key(xonly={self.xonly[:16]}...)"
|
|
289
|
+
|
|
290
|
+
def __eq__(self, other) -> bool:
|
|
291
|
+
if not isinstance(other, Key):
|
|
292
|
+
return False
|
|
293
|
+
return self.xonly == other.xonly
|
|
294
|
+
|
|
295
|
+
def __hash__(self) -> int:
|
|
296
|
+
return hash(self.xonly)
|