btcaaron 0.1.1__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.1.1/PKG-INFO +107 -0
- btcaaron-0.1.1/README.md +79 -0
- btcaaron-0.1.1/btcaaron/__init__.py +9 -0
- btcaaron-0.1.1/btcaaron/btcaaron.py +484 -0
- btcaaron-0.1.1/btcaaron.egg-info/PKG-INFO +107 -0
- btcaaron-0.1.1/btcaaron.egg-info/SOURCES.txt +10 -0
- btcaaron-0.1.1/btcaaron.egg-info/dependency_links.txt +1 -0
- btcaaron-0.1.1/btcaaron.egg-info/requires.txt +2 -0
- btcaaron-0.1.1/btcaaron.egg-info/top_level.txt +1 -0
- btcaaron-0.1.1/setup.cfg +4 -0
- btcaaron-0.1.1/setup.py +27 -0
- btcaaron-0.1.1/tests/test_btcaaron.py +163 -0
btcaaron-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: btcaaron
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A Bitcoin Testnet transaction toolkit supporting Legacy, SegWit, and Taproot
|
|
5
|
+
Home-page: https://x.com/aaron_recompile
|
|
6
|
+
Author: Aaron Zhang
|
|
7
|
+
Author-email: aaron.recompile@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
15
|
+
Requires-Python: >=3.7
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: requests>=2.25.0
|
|
18
|
+
Requires-Dist: bitcoin-utils>=0.7.1
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: requires-dist
|
|
26
|
+
Dynamic: requires-python
|
|
27
|
+
Dynamic: summary
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# btcaaron
|
|
32
|
+
|
|
33
|
+
A simple Bitcoin Testnet toolkit for developers.
|
|
34
|
+
Easily generate addresses, scan UTXOs, build and broadcast transactions β with full support for Legacy, SegWit, and Taproot.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## π§ Features
|
|
39
|
+
|
|
40
|
+
- β
Generate Legacy / SegWit / Taproot addresses from WIF
|
|
41
|
+
- π Scan UTXOs and check balance via public APIs
|
|
42
|
+
- π§ Build & sign transactions (manual or quick mode)
|
|
43
|
+
- π Broadcast to Blockstream or Mempool endpoints
|
|
44
|
+
- π§ͺ Simple test suite for local debugging
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## π¦ Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install btcaaron
|
|
52
|
+
|
|
53
|
+
Or install from source:
|
|
54
|
+
|
|
55
|
+
git clone https://github.com/aaron-recompile/btcaaron.git
|
|
56
|
+
cd btcaaron
|
|
57
|
+
pip install .
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
βΈ»
|
|
61
|
+
|
|
62
|
+
π Quick Start
|
|
63
|
+
|
|
64
|
+
from btcaaron import WIFKey, quick_transfer
|
|
65
|
+
|
|
66
|
+
# Your testnet WIF private key
|
|
67
|
+
wif = ""
|
|
68
|
+
|
|
69
|
+
# Generate addresses
|
|
70
|
+
key = WIFKey(wif)
|
|
71
|
+
print("Taproot:", key.get_taproot().address)
|
|
72
|
+
|
|
73
|
+
# Check balance
|
|
74
|
+
balance = key.get_taproot().get_balance()
|
|
75
|
+
print("Balance:", balance, "sats")
|
|
76
|
+
|
|
77
|
+
# Quick transfer
|
|
78
|
+
if balance > 1000:
|
|
79
|
+
txid = quick_transfer(wif, "taproot", "tb1q...", amount=500, fee=300)
|
|
80
|
+
print("Broadcasted:", txid)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
βΈ»
|
|
84
|
+
|
|
85
|
+
π Project Structure
|
|
86
|
+
|
|
87
|
+
btcaaron/
|
|
88
|
+
βββ btcaaron.py # Main library
|
|
89
|
+
βββ test.py # Example-based test runner
|
|
90
|
+
βββ README.md # This file
|
|
91
|
+
βββ setup.py # Install and packaging
|
|
92
|
+
βββ LICENSE # MIT License
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
βΈ»
|
|
96
|
+
|
|
97
|
+
π¨βπ» Author
|
|
98
|
+
|
|
99
|
+
Aaron Zhang
|
|
100
|
+
https://x.com/aaron_recompile
|
|
101
|
+
|
|
102
|
+
βΈ»
|
|
103
|
+
|
|
104
|
+
π License
|
|
105
|
+
|
|
106
|
+
MIT License - Free for commercial and personal use.
|
|
107
|
+
|
btcaaron-0.1.1/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# btcaaron
|
|
4
|
+
|
|
5
|
+
A simple Bitcoin Testnet toolkit for developers.
|
|
6
|
+
Easily generate addresses, scan UTXOs, build and broadcast transactions β with full support for Legacy, SegWit, and Taproot.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## π§ Features
|
|
11
|
+
|
|
12
|
+
- β
Generate Legacy / SegWit / Taproot addresses from WIF
|
|
13
|
+
- π Scan UTXOs and check balance via public APIs
|
|
14
|
+
- π§ Build & sign transactions (manual or quick mode)
|
|
15
|
+
- π Broadcast to Blockstream or Mempool endpoints
|
|
16
|
+
- π§ͺ Simple test suite for local debugging
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## π¦ Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install btcaaron
|
|
24
|
+
|
|
25
|
+
Or install from source:
|
|
26
|
+
|
|
27
|
+
git clone https://github.com/aaron-recompile/btcaaron.git
|
|
28
|
+
cd btcaaron
|
|
29
|
+
pip install .
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
βΈ»
|
|
33
|
+
|
|
34
|
+
π Quick Start
|
|
35
|
+
|
|
36
|
+
from btcaaron import WIFKey, quick_transfer
|
|
37
|
+
|
|
38
|
+
# Your testnet WIF private key
|
|
39
|
+
wif = ""
|
|
40
|
+
|
|
41
|
+
# Generate addresses
|
|
42
|
+
key = WIFKey(wif)
|
|
43
|
+
print("Taproot:", key.get_taproot().address)
|
|
44
|
+
|
|
45
|
+
# Check balance
|
|
46
|
+
balance = key.get_taproot().get_balance()
|
|
47
|
+
print("Balance:", balance, "sats")
|
|
48
|
+
|
|
49
|
+
# Quick transfer
|
|
50
|
+
if balance > 1000:
|
|
51
|
+
txid = quick_transfer(wif, "taproot", "tb1q...", amount=500, fee=300)
|
|
52
|
+
print("Broadcasted:", txid)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
βΈ»
|
|
56
|
+
|
|
57
|
+
π Project Structure
|
|
58
|
+
|
|
59
|
+
btcaaron/
|
|
60
|
+
βββ btcaaron.py # Main library
|
|
61
|
+
βββ test.py # Example-based test runner
|
|
62
|
+
βββ README.md # This file
|
|
63
|
+
βββ setup.py # Install and packaging
|
|
64
|
+
βββ LICENSE # MIT License
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
βΈ»
|
|
68
|
+
|
|
69
|
+
π¨βπ» Author
|
|
70
|
+
|
|
71
|
+
Aaron Zhang
|
|
72
|
+
https://x.com/aaron_recompile
|
|
73
|
+
|
|
74
|
+
βΈ»
|
|
75
|
+
|
|
76
|
+
π License
|
|
77
|
+
|
|
78
|
+
MIT License - Free for commercial and personal use.
|
|
79
|
+
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""
|
|
2
|
+
btcaaron - Lightweight Bitcoin Testnet Toolkit
|
|
3
|
+
|
|
4
|
+
A simple, clean toolkit for Bitcoin testnet operations including address generation,
|
|
5
|
+
UTXO querying, transaction creation, signing, and broadcasting.
|
|
6
|
+
|
|
7
|
+
Author: Aaron Zhang (https://x.com/aaron_recompile)
|
|
8
|
+
Version: 0.1.1
|
|
9
|
+
License: MIT
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import List, Dict, Optional
|
|
13
|
+
import requests
|
|
14
|
+
import concurrent.futures
|
|
15
|
+
from bitcoinutils.setup import setup
|
|
16
|
+
from bitcoinutils.keys import PrivateKey, P2pkhAddress, P2wpkhAddress, P2trAddress
|
|
17
|
+
from bitcoinutils.transactions import Transaction, TxInput, TxOutput, TxWitnessInput
|
|
18
|
+
from bitcoinutils.script import Script
|
|
19
|
+
|
|
20
|
+
# Initialize testnet environment
|
|
21
|
+
setup('testnet')
|
|
22
|
+
|
|
23
|
+
# Network configuration
|
|
24
|
+
TIMEOUT = 5
|
|
25
|
+
MAX_WORKERS = 2
|
|
26
|
+
DEFAULT_FEE = 300
|
|
27
|
+
DUST_LIMIT = 546
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WIFKey:
|
|
31
|
+
"""
|
|
32
|
+
WIF Private Key Manager
|
|
33
|
+
|
|
34
|
+
Generate different types of Bitcoin addresses from a WIF private key
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, wif_str: str):
|
|
38
|
+
"""
|
|
39
|
+
Initialize WIF private key
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
wif_str: WIF format private key string
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: When WIF format is invalid
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
self.wif = wif_str
|
|
49
|
+
self.private_key = PrivateKey(wif_str)
|
|
50
|
+
self.public_key = self.private_key.get_public_key()
|
|
51
|
+
except Exception as e:
|
|
52
|
+
raise ValueError(f"Invalid WIF private key: {str(e)}")
|
|
53
|
+
|
|
54
|
+
def get_legacy(self) -> 'BTCAddress':
|
|
55
|
+
"""Get Legacy address object (P2PKH)"""
|
|
56
|
+
address = self.public_key.get_address().to_string()
|
|
57
|
+
return BTCAddress(self.wif, address, "legacy", self.private_key, self.public_key)
|
|
58
|
+
|
|
59
|
+
def get_segwit(self) -> 'BTCAddress':
|
|
60
|
+
"""Get SegWit address object (P2WPKH)"""
|
|
61
|
+
address = self.public_key.get_segwit_address().to_string()
|
|
62
|
+
return BTCAddress(self.wif, address, "segwit", self.private_key, self.public_key)
|
|
63
|
+
|
|
64
|
+
def get_taproot(self) -> 'BTCAddress':
|
|
65
|
+
"""Get Taproot address object (P2TR)"""
|
|
66
|
+
address = self.public_key.get_taproot_address().to_string()
|
|
67
|
+
return BTCAddress(self.wif, address, "taproot", self.private_key, self.public_key)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BTCAddress:
|
|
71
|
+
"""
|
|
72
|
+
Bitcoin Address Handler
|
|
73
|
+
|
|
74
|
+
Handles specific address type operations (UTXO queries, transfers, etc.)
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, wif: str, address: str, addr_type: str, private_key, public_key):
|
|
78
|
+
self.wif = wif
|
|
79
|
+
self.address = address
|
|
80
|
+
self.type = addr_type
|
|
81
|
+
self.private_key = private_key
|
|
82
|
+
self.public_key = public_key
|
|
83
|
+
|
|
84
|
+
# Create address object
|
|
85
|
+
if addr_type == "legacy":
|
|
86
|
+
self.addr_obj = P2pkhAddress(address)
|
|
87
|
+
elif addr_type == "segwit":
|
|
88
|
+
self.addr_obj = P2wpkhAddress(address)
|
|
89
|
+
elif addr_type == "taproot":
|
|
90
|
+
self.addr_obj = P2trAddress(address)
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError(f"Unsupported address type: {addr_type}")
|
|
93
|
+
|
|
94
|
+
def __str__(self) -> str:
|
|
95
|
+
return f"{self.type.upper()} Address: {self.address}"
|
|
96
|
+
|
|
97
|
+
def scan_utxos(self, debug: bool = False) -> List[Dict]:
|
|
98
|
+
"""
|
|
99
|
+
Scan all available UTXOs for this address
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
debug: Whether to output debug information
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List[Dict]: List of UTXOs
|
|
106
|
+
"""
|
|
107
|
+
if debug:
|
|
108
|
+
print(f"Scanning UTXOs for {self.type} address: {self.address}")
|
|
109
|
+
|
|
110
|
+
# API endpoints
|
|
111
|
+
apis = [
|
|
112
|
+
("Blockstream testnet", f"https://blockstream.info/testnet/api/address/{self.address}/utxo"),
|
|
113
|
+
("Mempool testnet", f"https://mempool.space/testnet/api/address/{self.address}/utxo"),
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
def fetch_utxos(api_info) -> Optional[List[Dict]]:
|
|
117
|
+
"""Fetch UTXOs from a single API"""
|
|
118
|
+
api_name, api_url = api_info
|
|
119
|
+
try:
|
|
120
|
+
response = requests.get(api_url, timeout=TIMEOUT)
|
|
121
|
+
if response.status_code == 200:
|
|
122
|
+
raw_utxos = response.json()
|
|
123
|
+
if debug:
|
|
124
|
+
print(f" {api_name}: Found {len(raw_utxos)} UTXOs")
|
|
125
|
+
|
|
126
|
+
utxos = []
|
|
127
|
+
for utxo in raw_utxos:
|
|
128
|
+
processed_utxo = {
|
|
129
|
+
'txid': utxo['txid'],
|
|
130
|
+
'vout': utxo['vout'],
|
|
131
|
+
'amount': utxo['value'],
|
|
132
|
+
'scriptPubKey': self.addr_obj.to_script_pub_key().to_hex(),
|
|
133
|
+
'type': self.type.upper()
|
|
134
|
+
}
|
|
135
|
+
utxos.append(processed_utxo)
|
|
136
|
+
|
|
137
|
+
if debug:
|
|
138
|
+
print(f" - {utxo['txid'][:16]}...:{utxo['vout']} = {utxo['value']} sats")
|
|
139
|
+
|
|
140
|
+
return utxos
|
|
141
|
+
else:
|
|
142
|
+
if debug:
|
|
143
|
+
print(f" {api_name}: HTTP {response.status_code}")
|
|
144
|
+
except Exception as e:
|
|
145
|
+
if debug:
|
|
146
|
+
print(f" {api_name}: Error - {str(e)}")
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
# Try APIs sequentially (simpler than concurrent)
|
|
150
|
+
for api_name, api_url in apis:
|
|
151
|
+
result = fetch_utxos((api_name, api_url))
|
|
152
|
+
if result is not None:
|
|
153
|
+
if debug:
|
|
154
|
+
total = sum(utxo['amount'] for utxo in result)
|
|
155
|
+
print(f" Success: {len(result)} UTXOs, Total: {total} sats")
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
if debug:
|
|
159
|
+
print(" All APIs failed")
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
def get_balance(self, debug: bool = False) -> int:
|
|
163
|
+
"""
|
|
164
|
+
Get address balance
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
debug: Whether to output debug information
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
int: Address balance in satoshis
|
|
171
|
+
"""
|
|
172
|
+
if debug:
|
|
173
|
+
print(f"Checking balance for {self.type} address: {self.address}")
|
|
174
|
+
|
|
175
|
+
utxos = self.scan_utxos(debug=debug)
|
|
176
|
+
balance = sum(utxo['amount'] for utxo in utxos)
|
|
177
|
+
|
|
178
|
+
if debug:
|
|
179
|
+
print(f" Balance: {balance:,} sats")
|
|
180
|
+
|
|
181
|
+
return balance
|
|
182
|
+
|
|
183
|
+
def send(self, to_addr: str, amount: int, fee: int = DEFAULT_FEE, debug: bool = False) -> 'BTCTransaction':
|
|
184
|
+
"""
|
|
185
|
+
Create and sign a transfer transaction
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
to_addr: Recipient address
|
|
189
|
+
amount: Transfer amount in satoshis
|
|
190
|
+
fee: Transaction fee in satoshis
|
|
191
|
+
debug: Whether to output debug information
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
BTCTransaction: Signed transaction object
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
ValueError: When insufficient balance or invalid parameters
|
|
198
|
+
"""
|
|
199
|
+
if debug:
|
|
200
|
+
print(f"Creating transaction from {self.type} address:")
|
|
201
|
+
print(f" From: {self.address}")
|
|
202
|
+
print(f" To: {to_addr}")
|
|
203
|
+
print(f" Amount: {amount:,} sats")
|
|
204
|
+
print(f" Fee: {fee:,} sats")
|
|
205
|
+
|
|
206
|
+
# Get UTXOs
|
|
207
|
+
utxos = self.scan_utxos(debug=debug)
|
|
208
|
+
if not utxos:
|
|
209
|
+
raise ValueError(f"No UTXOs available for address {self.address}")
|
|
210
|
+
|
|
211
|
+
# Check balance
|
|
212
|
+
total_needed = amount + fee
|
|
213
|
+
total_available = sum(utxo['amount'] for utxo in utxos)
|
|
214
|
+
|
|
215
|
+
if total_available < total_needed:
|
|
216
|
+
raise ValueError(f"Insufficient balance. Need: {total_needed:,}, Available: {total_available:,}")
|
|
217
|
+
|
|
218
|
+
# Select UTXOs (largest first)
|
|
219
|
+
utxos.sort(key=lambda x: x['amount'], reverse=True)
|
|
220
|
+
selected_utxos = []
|
|
221
|
+
total_input = 0
|
|
222
|
+
|
|
223
|
+
for utxo in utxos:
|
|
224
|
+
selected_utxos.append(utxo)
|
|
225
|
+
total_input += utxo['amount']
|
|
226
|
+
if total_input >= total_needed:
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
if debug:
|
|
230
|
+
print(f" Selected UTXOs:")
|
|
231
|
+
for i, utxo in enumerate(selected_utxos, 1):
|
|
232
|
+
print(f" {i}. {utxo['txid'][:16]}...:{utxo['vout']} = {utxo['amount']:,} sats")
|
|
233
|
+
print(f" Total input: {total_input:,} sats")
|
|
234
|
+
|
|
235
|
+
# Create transaction inputs
|
|
236
|
+
tx_inputs = []
|
|
237
|
+
for utxo in selected_utxos:
|
|
238
|
+
tx_input = TxInput(utxo['txid'], utxo['vout'])
|
|
239
|
+
tx_inputs.append(tx_input)
|
|
240
|
+
|
|
241
|
+
# Create transaction outputs
|
|
242
|
+
tx_outputs = []
|
|
243
|
+
|
|
244
|
+
# 1. Output to recipient
|
|
245
|
+
to_addr_obj = self._create_address_object(to_addr)
|
|
246
|
+
tx_out = TxOutput(amount, to_addr_obj.to_script_pub_key())
|
|
247
|
+
tx_outputs.append(tx_out)
|
|
248
|
+
|
|
249
|
+
if debug:
|
|
250
|
+
print(f" Output 1: {amount:,} sats -> {to_addr}")
|
|
251
|
+
|
|
252
|
+
# 2. Change output (check dust limit)
|
|
253
|
+
change_amount = total_input - amount - fee
|
|
254
|
+
if change_amount > 0:
|
|
255
|
+
if change_amount < DUST_LIMIT:
|
|
256
|
+
# Change too small, add to fee
|
|
257
|
+
if debug:
|
|
258
|
+
print(f" Warning: Change amount {change_amount} sats < {DUST_LIMIT} sats (dust limit)")
|
|
259
|
+
print(f" Adding change to fee: {fee} + {change_amount} = {fee + change_amount} sats")
|
|
260
|
+
else:
|
|
261
|
+
change_out = TxOutput(change_amount, self.addr_obj.to_script_pub_key())
|
|
262
|
+
tx_outputs.append(change_out)
|
|
263
|
+
if debug:
|
|
264
|
+
print(f" Change: {change_amount:,} sats -> {self.address}")
|
|
265
|
+
elif debug:
|
|
266
|
+
print(f" No change (exact amount match)")
|
|
267
|
+
|
|
268
|
+
# Create transaction
|
|
269
|
+
has_segwit = self.type in ['segwit', 'taproot']
|
|
270
|
+
tx = Transaction(tx_inputs, tx_outputs, has_segwit=has_segwit)
|
|
271
|
+
|
|
272
|
+
if debug:
|
|
273
|
+
print(f" Created {'SegWit' if has_segwit else 'Legacy'} transaction")
|
|
274
|
+
|
|
275
|
+
# Sign transaction
|
|
276
|
+
self._sign_transaction(tx, selected_utxos, debug=debug)
|
|
277
|
+
|
|
278
|
+
signed_hex = tx.serialize()
|
|
279
|
+
|
|
280
|
+
if debug:
|
|
281
|
+
print(f" Signing complete!")
|
|
282
|
+
print(f" Transaction size: {len(signed_hex)//2} bytes")
|
|
283
|
+
|
|
284
|
+
return BTCTransaction(signed_hex, debug=debug)
|
|
285
|
+
|
|
286
|
+
def _create_address_object(self, address: str):
|
|
287
|
+
"""Create address object from address string"""
|
|
288
|
+
if address.startswith(('1', 'm', 'n')): # Legacy
|
|
289
|
+
return P2pkhAddress(address)
|
|
290
|
+
elif address.startswith(('bc1q', 'tb1q')): # SegWit v0
|
|
291
|
+
return P2wpkhAddress(address)
|
|
292
|
+
elif address.startswith(('bc1p', 'tb1p')): # Taproot
|
|
293
|
+
return P2trAddress(address)
|
|
294
|
+
else:
|
|
295
|
+
raise ValueError(f"Unsupported address format: {address}")
|
|
296
|
+
|
|
297
|
+
def _sign_transaction(self, tx: Transaction, selected_utxos: List[Dict], debug: bool = False):
|
|
298
|
+
"""Sign transaction based on address type"""
|
|
299
|
+
if debug:
|
|
300
|
+
print(f" Signing with {self.type} method...")
|
|
301
|
+
|
|
302
|
+
if self.type == 'legacy':
|
|
303
|
+
# P2PKH signing
|
|
304
|
+
for i, tx_input in enumerate(tx.inputs):
|
|
305
|
+
previous_locking_script = self.addr_obj.to_script_pub_key()
|
|
306
|
+
sig = self.private_key.sign_input(tx, i, previous_locking_script)
|
|
307
|
+
pk = self.private_key.get_public_key().to_hex()
|
|
308
|
+
unlocking_script = Script([sig, pk])
|
|
309
|
+
tx_input.script_sig = unlocking_script
|
|
310
|
+
|
|
311
|
+
elif self.type == 'segwit':
|
|
312
|
+
# P2WPKH signing
|
|
313
|
+
for i, tx_input in enumerate(tx.inputs):
|
|
314
|
+
script_code = self.public_key.get_address().to_script_pub_key()
|
|
315
|
+
input_amount = selected_utxos[i]['amount']
|
|
316
|
+
sig = self.private_key.sign_segwit_input(tx, i, script_code, input_amount)
|
|
317
|
+
public_key_hex = self.private_key.get_public_key().to_hex()
|
|
318
|
+
tx_input.script_sig = Script([])
|
|
319
|
+
tx.witnesses.append(TxWitnessInput([sig, public_key_hex]))
|
|
320
|
+
|
|
321
|
+
elif self.type == 'taproot':
|
|
322
|
+
# P2TR signing
|
|
323
|
+
input_amounts = [utxo['amount'] for utxo in selected_utxos]
|
|
324
|
+
input_scripts = [self.addr_obj.to_script_pub_key() for _ in selected_utxos]
|
|
325
|
+
|
|
326
|
+
for i, tx_input in enumerate(tx.inputs):
|
|
327
|
+
sig = self.private_key.sign_taproot_input(tx, i, input_scripts, input_amounts)
|
|
328
|
+
tx_input.script_sig = Script([])
|
|
329
|
+
tx.witnesses.append(TxWitnessInput([sig]))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class BTCTransaction:
|
|
333
|
+
"""
|
|
334
|
+
Bitcoin Transaction Handler
|
|
335
|
+
|
|
336
|
+
Handles signed transaction broadcasting
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
def __init__(self, tx_hex: str, debug: bool = False):
|
|
340
|
+
self.tx_hex = tx_hex
|
|
341
|
+
self.debug = debug
|
|
342
|
+
self.txid = None
|
|
343
|
+
|
|
344
|
+
def __str__(self) -> str:
|
|
345
|
+
status = f"(TxID: {self.txid})" if self.txid else "(Not broadcasted)"
|
|
346
|
+
return f"BTCTransaction {status}"
|
|
347
|
+
|
|
348
|
+
def broadcast(self) -> Optional[str]:
|
|
349
|
+
"""
|
|
350
|
+
Broadcast transaction to network
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Optional[str]: Transaction ID if successful, None if failed
|
|
354
|
+
"""
|
|
355
|
+
if self.debug:
|
|
356
|
+
print(f"Broadcasting transaction:")
|
|
357
|
+
print(f" Transaction size: {len(self.tx_hex)//2} bytes")
|
|
358
|
+
|
|
359
|
+
# Broadcast endpoints
|
|
360
|
+
broadcast_apis = [
|
|
361
|
+
("Blockstream testnet", "https://blockstream.info/testnet/api/tx"),
|
|
362
|
+
("Mempool testnet", "https://mempool.space/testnet/api/tx"),
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
# Try each API sequentially
|
|
366
|
+
for api_name, api_url in broadcast_apis:
|
|
367
|
+
try:
|
|
368
|
+
if self.debug:
|
|
369
|
+
print(f" Trying {api_name}...")
|
|
370
|
+
|
|
371
|
+
response = requests.post(
|
|
372
|
+
api_url,
|
|
373
|
+
data=self.tx_hex,
|
|
374
|
+
timeout=TIMEOUT,
|
|
375
|
+
headers={'Content-Type': 'text/plain'}
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
if response.status_code == 200:
|
|
379
|
+
response_text = response.text.strip()
|
|
380
|
+
# Validate response is a valid transaction ID
|
|
381
|
+
if len(response_text) == 64 and all(c in '0123456789abcdef' for c in response_text.lower()):
|
|
382
|
+
self.txid = response_text
|
|
383
|
+
if self.debug:
|
|
384
|
+
print(f" Success: {response_text}")
|
|
385
|
+
return response_text
|
|
386
|
+
else:
|
|
387
|
+
if self.debug:
|
|
388
|
+
print(f" Invalid response format: {response_text}")
|
|
389
|
+
else:
|
|
390
|
+
if self.debug:
|
|
391
|
+
print(f" HTTP {response.status_code}: {response.text[:100]}...")
|
|
392
|
+
|
|
393
|
+
except Exception as e:
|
|
394
|
+
if self.debug:
|
|
395
|
+
print(f" Error: {e}")
|
|
396
|
+
|
|
397
|
+
if self.debug:
|
|
398
|
+
print(" All broadcast attempts failed")
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# Convenience functions
|
|
403
|
+
def wif_to_addresses(wif: str) -> Dict[str, str]:
|
|
404
|
+
"""
|
|
405
|
+
Generate all address types from WIF
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
wif: WIF private key string
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Dict[str, str]: Dictionary containing all address types
|
|
412
|
+
"""
|
|
413
|
+
wif_key = WIFKey(wif)
|
|
414
|
+
return {
|
|
415
|
+
'legacy': wif_key.get_legacy().address,
|
|
416
|
+
'segwit': wif_key.get_segwit().address,
|
|
417
|
+
'taproot': wif_key.get_taproot().address
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def quick_transfer(wif: str, from_type: str, to_addr: str, amount: int,
|
|
422
|
+
fee: int = DEFAULT_FEE, debug: bool = False) -> Optional[str]:
|
|
423
|
+
"""
|
|
424
|
+
Quick transfer using specified address type
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
wif: WIF private key
|
|
428
|
+
from_type: Sender address type ('legacy', 'segwit', 'taproot')
|
|
429
|
+
to_addr: Recipient address
|
|
430
|
+
amount: Transfer amount in satoshis
|
|
431
|
+
fee: Transaction fee in satoshis
|
|
432
|
+
debug: Whether to output debug information
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Optional[str]: Transaction ID if successful, None if failed
|
|
436
|
+
"""
|
|
437
|
+
try:
|
|
438
|
+
if debug:
|
|
439
|
+
print(f"Quick transfer ({from_type}):")
|
|
440
|
+
|
|
441
|
+
# Create WIF key object
|
|
442
|
+
wif_key = WIFKey(wif)
|
|
443
|
+
|
|
444
|
+
# Get address object by type
|
|
445
|
+
if from_type.lower() == 'legacy':
|
|
446
|
+
from_addr = wif_key.get_legacy()
|
|
447
|
+
elif from_type.lower() == 'segwit':
|
|
448
|
+
from_addr = wif_key.get_segwit()
|
|
449
|
+
elif from_type.lower() == 'taproot':
|
|
450
|
+
from_addr = wif_key.get_taproot()
|
|
451
|
+
else:
|
|
452
|
+
raise ValueError(f"Unsupported address type: {from_type}")
|
|
453
|
+
|
|
454
|
+
# Create and sign transaction
|
|
455
|
+
tx = from_addr.send(to_addr, amount, fee, debug=debug)
|
|
456
|
+
|
|
457
|
+
# Broadcast transaction
|
|
458
|
+
txid = tx.broadcast()
|
|
459
|
+
|
|
460
|
+
if txid:
|
|
461
|
+
if debug:
|
|
462
|
+
print(f" Transfer successful! TxID: {txid}")
|
|
463
|
+
return txid
|
|
464
|
+
else:
|
|
465
|
+
if debug:
|
|
466
|
+
print(f" Transfer failed")
|
|
467
|
+
return None
|
|
468
|
+
|
|
469
|
+
except Exception as e:
|
|
470
|
+
if debug:
|
|
471
|
+
print(f"Quick transfer failed: {str(e)}")
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# Module information
|
|
476
|
+
__version__ = "0.1.1"
|
|
477
|
+
__author__ = "Aaron Zhang"
|
|
478
|
+
__all__ = [
|
|
479
|
+
'WIFKey',
|
|
480
|
+
'BTCAddress',
|
|
481
|
+
'BTCTransaction',
|
|
482
|
+
'wif_to_addresses',
|
|
483
|
+
'quick_transfer'
|
|
484
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: btcaaron
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A Bitcoin Testnet transaction toolkit supporting Legacy, SegWit, and Taproot
|
|
5
|
+
Home-page: https://x.com/aaron_recompile
|
|
6
|
+
Author: Aaron Zhang
|
|
7
|
+
Author-email: aaron.recompile@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
15
|
+
Requires-Python: >=3.7
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: requests>=2.25.0
|
|
18
|
+
Requires-Dist: bitcoin-utils>=0.7.1
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: requires-dist
|
|
26
|
+
Dynamic: requires-python
|
|
27
|
+
Dynamic: summary
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# btcaaron
|
|
32
|
+
|
|
33
|
+
A simple Bitcoin Testnet toolkit for developers.
|
|
34
|
+
Easily generate addresses, scan UTXOs, build and broadcast transactions β with full support for Legacy, SegWit, and Taproot.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## π§ Features
|
|
39
|
+
|
|
40
|
+
- β
Generate Legacy / SegWit / Taproot addresses from WIF
|
|
41
|
+
- π Scan UTXOs and check balance via public APIs
|
|
42
|
+
- π§ Build & sign transactions (manual or quick mode)
|
|
43
|
+
- π Broadcast to Blockstream or Mempool endpoints
|
|
44
|
+
- π§ͺ Simple test suite for local debugging
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## π¦ Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install btcaaron
|
|
52
|
+
|
|
53
|
+
Or install from source:
|
|
54
|
+
|
|
55
|
+
git clone https://github.com/aaron-recompile/btcaaron.git
|
|
56
|
+
cd btcaaron
|
|
57
|
+
pip install .
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
βΈ»
|
|
61
|
+
|
|
62
|
+
π Quick Start
|
|
63
|
+
|
|
64
|
+
from btcaaron import WIFKey, quick_transfer
|
|
65
|
+
|
|
66
|
+
# Your testnet WIF private key
|
|
67
|
+
wif = ""
|
|
68
|
+
|
|
69
|
+
# Generate addresses
|
|
70
|
+
key = WIFKey(wif)
|
|
71
|
+
print("Taproot:", key.get_taproot().address)
|
|
72
|
+
|
|
73
|
+
# Check balance
|
|
74
|
+
balance = key.get_taproot().get_balance()
|
|
75
|
+
print("Balance:", balance, "sats")
|
|
76
|
+
|
|
77
|
+
# Quick transfer
|
|
78
|
+
if balance > 1000:
|
|
79
|
+
txid = quick_transfer(wif, "taproot", "tb1q...", amount=500, fee=300)
|
|
80
|
+
print("Broadcasted:", txid)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
βΈ»
|
|
84
|
+
|
|
85
|
+
π Project Structure
|
|
86
|
+
|
|
87
|
+
btcaaron/
|
|
88
|
+
βββ btcaaron.py # Main library
|
|
89
|
+
βββ test.py # Example-based test runner
|
|
90
|
+
βββ README.md # This file
|
|
91
|
+
βββ setup.py # Install and packaging
|
|
92
|
+
βββ LICENSE # MIT License
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
βΈ»
|
|
96
|
+
|
|
97
|
+
π¨βπ» Author
|
|
98
|
+
|
|
99
|
+
Aaron Zhang
|
|
100
|
+
https://x.com/aaron_recompile
|
|
101
|
+
|
|
102
|
+
βΈ»
|
|
103
|
+
|
|
104
|
+
π License
|
|
105
|
+
|
|
106
|
+
MIT License - Free for commercial and personal use.
|
|
107
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
btcaaron/__init__.py
|
|
4
|
+
btcaaron/btcaaron.py
|
|
5
|
+
btcaaron.egg-info/PKG-INFO
|
|
6
|
+
btcaaron.egg-info/SOURCES.txt
|
|
7
|
+
btcaaron.egg-info/dependency_links.txt
|
|
8
|
+
btcaaron.egg-info/requires.txt
|
|
9
|
+
btcaaron.egg-info/top_level.txt
|
|
10
|
+
tests/test_btcaaron.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
btcaaron
|
btcaaron-0.1.1/setup.cfg
ADDED
btcaaron-0.1.1/setup.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="btcaaron",
|
|
5
|
+
version="0.1.1",
|
|
6
|
+
description="A Bitcoin Testnet transaction toolkit supporting Legacy, SegWit, and Taproot",
|
|
7
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
author="Aaron Zhang",
|
|
10
|
+
author_email="aaron.recompile@gmail.com",
|
|
11
|
+
url="https://x.com/aaron_recompile",
|
|
12
|
+
packages=["btcaaron"],
|
|
13
|
+
install_requires=[
|
|
14
|
+
"requests>=2.25.0",
|
|
15
|
+
"bitcoin-utils>=0.7.1"
|
|
16
|
+
],
|
|
17
|
+
classifiers=[
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
25
|
+
],
|
|
26
|
+
python_requires=">=3.7",
|
|
27
|
+
)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
btcaaron - Simple Test Examples (Refactored)
|
|
4
|
+
"""
|
|
5
|
+
from btcaaron import WIFKey, wif_to_addresses, quick_transfer
|
|
6
|
+
|
|
7
|
+
WIF = "cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT"
|
|
8
|
+
RECIPIENT = "tb1q2w85fm5g8kfhk9f63njplzu3yzcnluz9dgztjz"
|
|
9
|
+
FEE = 300
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_addresses(wif):
|
|
13
|
+
key = WIFKey(wif)
|
|
14
|
+
return {
|
|
15
|
+
"Legacy": key.get_legacy(),
|
|
16
|
+
"SegWit": key.get_segwit(),
|
|
17
|
+
"Taproot": key.get_taproot(),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def test_address_generation():
|
|
21
|
+
print("=" * 50)
|
|
22
|
+
print("TEST 1: Address Generation")
|
|
23
|
+
print("=" * 50)
|
|
24
|
+
try:
|
|
25
|
+
addresses = wif_to_addresses(WIF)
|
|
26
|
+
for typ, addr in addresses.items():
|
|
27
|
+
print(f" {typ}: {addr}")
|
|
28
|
+
|
|
29
|
+
print("\nUsing WIFKey object:")
|
|
30
|
+
for typ, addr_obj in get_addresses(WIF).items():
|
|
31
|
+
print(f" {typ:8}: {addr_obj.address}")
|
|
32
|
+
|
|
33
|
+
print("β
Address generation test passed!")
|
|
34
|
+
return True
|
|
35
|
+
except Exception as e:
|
|
36
|
+
print(f"β Address generation test failed: {e}")
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
def test_utxo_scanning():
|
|
40
|
+
print("\n" + "=" * 50)
|
|
41
|
+
print("TEST 2: UTXO Scanning")
|
|
42
|
+
print("=" * 50)
|
|
43
|
+
try:
|
|
44
|
+
for typ, addr in get_addresses(WIF).items():
|
|
45
|
+
print(f"\n{typ} Address: {addr.address}")
|
|
46
|
+
utxos = addr.scan_utxos(debug=True)
|
|
47
|
+
balance = addr.get_balance()
|
|
48
|
+
print(f" UTXOs: {len(utxos)} | Balance: {balance:,} sats")
|
|
49
|
+
for i, utxo in enumerate(utxos[:3], 1):
|
|
50
|
+
print(f" {i}. {utxo['txid'][:16]}...:{utxo['vout']} = {utxo['amount']} sats")
|
|
51
|
+
if len(utxos) > 3:
|
|
52
|
+
print(f" ... and {len(utxos) - 3} more")
|
|
53
|
+
|
|
54
|
+
print("\nβ
UTXO scanning test completed!")
|
|
55
|
+
return True
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"β UTXO scanning test failed: {e}")
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def test_balance_check():
|
|
61
|
+
print("\n" + "=" * 50)
|
|
62
|
+
print("TEST 3: Balance Check")
|
|
63
|
+
print("=" * 50)
|
|
64
|
+
try:
|
|
65
|
+
balances = [(typ, addr.get_balance(), addr.address) for typ, addr in get_addresses(WIF).items()]
|
|
66
|
+
total = sum(b for _, b, _ in balances)
|
|
67
|
+
|
|
68
|
+
for typ, bal, _ in balances:
|
|
69
|
+
print(f" {typ:8}: {bal:,} sats")
|
|
70
|
+
print(f" Total : {total:,} sats")
|
|
71
|
+
|
|
72
|
+
if total:
|
|
73
|
+
balances.sort(key=lambda x: x[1], reverse=True)
|
|
74
|
+
typ, bal, addr = balances[0]
|
|
75
|
+
print(f"\nHighest balance: {typ} | {bal:,} sats | {addr}")
|
|
76
|
+
|
|
77
|
+
print("\nβ
Balance check test completed!")
|
|
78
|
+
return True
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print(f"β Balance check test failed: {e}")
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
def test_transfer():
|
|
84
|
+
print("\n" + "=" * 50)
|
|
85
|
+
print("TEST 4: Transfer Transaction")
|
|
86
|
+
print("=" * 50)
|
|
87
|
+
AMOUNT = 500
|
|
88
|
+
try:
|
|
89
|
+
candidates = [(t, a, a.get_balance()) for t, a in get_addresses(WIF).items() if a.get_balance() >= AMOUNT + FEE]
|
|
90
|
+
if not candidates:
|
|
91
|
+
print("β No address has sufficient balance.")
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
from_type, from_addr, bal = sorted(candidates, key=lambda x: x[2], reverse=True)[0]
|
|
95
|
+
print(f"From: {from_addr.address} ({from_type}, {bal:,} sats)")
|
|
96
|
+
print(f"To: {RECIPIENT} | Amount: {AMOUNT} | Fee: {FEE}")
|
|
97
|
+
if input("Proceed? (y/N): ").lower() != 'y':
|
|
98
|
+
print("Cancelled")
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
txid = from_addr.send(RECIPIENT, AMOUNT, fee=FEE, debug=True).broadcast()
|
|
102
|
+
print("β
Broadcast TXID:", txid)
|
|
103
|
+
return True
|
|
104
|
+
except Exception as e:
|
|
105
|
+
print(f"β Transfer test failed: {e}")
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
def test_quick_transfer():
|
|
109
|
+
print("\n" + "=" * 50)
|
|
110
|
+
print("TEST 5: Quick Transfer")
|
|
111
|
+
print("=" * 50)
|
|
112
|
+
AMOUNT = 400
|
|
113
|
+
FROM = "Taproot"
|
|
114
|
+
try:
|
|
115
|
+
addr = get_addresses(WIF)[FROM]
|
|
116
|
+
bal = addr.get_balance()
|
|
117
|
+
print(f"Using {FROM}: {addr.address} | {bal:,} sats")
|
|
118
|
+
if bal < AMOUNT + FEE:
|
|
119
|
+
print("β Insufficient balance.")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
if input("Proceed? (y/N): ").lower() != 'y':
|
|
123
|
+
print("Cancelled")
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
txid = quick_transfer(WIF, FROM.lower(), RECIPIENT, AMOUNT, fee=FEE, debug=True)
|
|
127
|
+
print("β
Quick TXID:", txid)
|
|
128
|
+
return True
|
|
129
|
+
except Exception as e:
|
|
130
|
+
print(f"β Quick transfer test failed: {e}")
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
def main():
|
|
134
|
+
tests = [
|
|
135
|
+
("Address Generation", test_address_generation),
|
|
136
|
+
("UTXO Scanning", test_utxo_scanning),
|
|
137
|
+
("Balance Check", test_balance_check),
|
|
138
|
+
("Transfer Transaction", test_transfer),
|
|
139
|
+
("Quick Transfer", test_quick_transfer)
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
print("\nπ¦ btcaaron Test Suite")
|
|
143
|
+
for i, (name, _) in enumerate(tests, 1):
|
|
144
|
+
print(f"{i}. {name}")
|
|
145
|
+
print("0. Run All")
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
sel = input("Select (0-N): ").strip()
|
|
149
|
+
if sel == "0":
|
|
150
|
+
for name, fn in tests:
|
|
151
|
+
print(f"\nβΆ {name}")
|
|
152
|
+
fn()
|
|
153
|
+
elif sel.isdigit() and 0 < int(sel) <= len(tests):
|
|
154
|
+
name, fn = tests[int(sel) - 1]
|
|
155
|
+
print(f"\nβΆ {name}")
|
|
156
|
+
fn()
|
|
157
|
+
else:
|
|
158
|
+
print("β Invalid selection")
|
|
159
|
+
except Exception as e:
|
|
160
|
+
print(f"β Error: {e}")
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
main()
|