Skip to content

Commit 1bd7df7

Browse files
Test related files (may delete some after PR review).
1 parent 326464a commit 1bd7df7

File tree

6 files changed

+237
-15
lines changed

6 files changed

+237
-15
lines changed

bsv/fee_models/live_policy.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
from dataclasses import dataclass
88
from typing import Optional, Tuple
99

10-
import requests
11-
1210
from ..constants import HTTP_REQUEST_TIMEOUT
11+
from ..http_client import default_http_client
1312
from .satoshis_per_kilobyte import SatoshisPerKilobyte
1413

1514

@@ -89,19 +88,19 @@ def get_instance(
8988
)
9089
return cls._instance
9190

92-
def compute_fee(self, tx) -> int: # type: ignore[override]
91+
async def compute_fee(self, tx) -> int: # type: ignore[override]
9392
"""Compute a fee for ``tx`` using the latest ARC rate."""
94-
rate = self.current_rate_sat_per_kb()
93+
rate = await self.current_rate_sat_per_kb()
9594
self.value = rate
9695
return super().compute_fee(tx)
9796

98-
def current_rate_sat_per_kb(self) -> int:
97+
async def current_rate_sat_per_kb(self) -> int:
9998
"""Return the cached sat/kB rate or fetch a new value from ARC."""
10099
cache = self._get_cache(allow_stale=True)
101100
if cache and self._cache_valid(cache):
102101
return cache.value
103102

104-
rate, error = self._fetch_sat_per_kb()
103+
rate, error = await self._fetch_sat_per_kb()
105104
if rate is not None:
106105
self._set_cache(rate)
107106
return rate
@@ -143,20 +142,24 @@ def _set_cache(self, value: int) -> None:
143142
with self._cache_lock:
144143
self._cache = _CachedRate(value=value, fetched_at_ms=time.time() * 1000)
145144

146-
def _fetch_sat_per_kb(self) -> Tuple[Optional[int], Optional[Exception]]:
145+
async def _fetch_sat_per_kb(self) -> Tuple[Optional[int], Optional[Exception]]:
147146
"""Fetch the latest fee policy from ARC and coerce it to sat/kB."""
148147
try:
149148
headers = {"Accept": "application/json"}
150149
if self.api_key:
151150
headers["Authorization"] = self.api_key
152151

153-
response = requests.get(
152+
http_client = default_http_client()
153+
response = await http_client.get(
154154
self.arc_policy_url,
155155
headers=headers,
156156
timeout=self.request_timeout,
157157
)
158-
response.raise_for_status()
159-
payload = response.json()
158+
payload = response.json_data
159+
if isinstance(payload, dict) and "data" in payload:
160+
data_section = payload.get("data")
161+
if isinstance(data_section, dict):
162+
payload = data_section
160163
except Exception as exc:
161164
return None, exc
162165

bsv/http_client.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,27 @@ def __init__(self, ok: bool, status_code: int, json_data: dict):
1919
def json(self):
2020
return self._json_data
2121

22+
@property
23+
def json_data(self):
24+
return self._json_data
25+
2226

2327
class DefaultHttpClient(HttpClient):
2428
async def fetch(self, url: str, options: dict) -> HttpResponse:
29+
timeout_value = options.get("timeout")
30+
aiohttp_timeout = (
31+
aiohttp.ClientTimeout(total=timeout_value)
32+
if timeout_value is not None
33+
else None
34+
)
35+
2536
async with aiohttp.ClientSession() as session:
2637
async with session.request(
2738
method=options["method"],
2839
url=url,
2940
headers=options.get("headers", {}),
3041
json=options.get("data", None),
42+
timeout=aiohttp_timeout,
3143
) as response:
3244
try:
3345
json_data = await response.json()
@@ -45,6 +57,34 @@ async def fetch(self, url: str, options: dict) -> HttpResponse:
4557
json_data={},
4658
)
4759

60+
async def get(
61+
self,
62+
url: str,
63+
headers: Optional[Dict[str, str]] = None,
64+
timeout: Optional[int] = None,
65+
) -> HttpResponse:
66+
options = {
67+
"method": "GET",
68+
"headers": headers or {},
69+
"timeout": timeout,
70+
}
71+
return await self.fetch(url, options)
72+
73+
async def post(
74+
self,
75+
url: str,
76+
data: Optional[dict] = None,
77+
headers: Optional[Dict[str, str]] = None,
78+
timeout: Optional[int] = None,
79+
) -> HttpResponse:
80+
options = {
81+
"method": "POST",
82+
"headers": headers or {},
83+
"data": data,
84+
"timeout": timeout,
85+
}
86+
return await self.fetch(url, options)
87+
4888
class SyncHttpClient(HttpClient):
4989
"""Synchronous HTTP client compatible with DefaultHttpClient"""
5090

@@ -137,4 +177,4 @@ def default_sync_http_client() -> SyncHttpClient:
137177

138178

139179
def default_http_client() -> HttpClient:
140-
return DefaultHttpClient()
180+
return DefaultHttpClient()

bsv/transaction.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import asyncio
2+
import inspect
13
import math
24
from contextlib import suppress
35
from typing import List, Optional, Union, Dict, Any
@@ -184,16 +186,31 @@ def fee(self, model_or_fee=None, change_distribution='equal'):
184186
)
185187

186188
if isinstance(model_or_fee, int):
187-
fee = model_or_fee
188-
else:
189-
fee = model_or_fee.compute_fee(self)
189+
return self._apply_fee_amount(model_or_fee, change_distribution)
190+
191+
fee_estimate = model_or_fee.compute_fee(self)
192+
193+
if inspect.isawaitable(fee_estimate):
194+
async def _resolve_and_apply():
195+
resolved_fee = await fee_estimate
196+
return self._apply_fee_amount(resolved_fee, change_distribution)
197+
198+
try:
199+
asyncio.get_running_loop()
200+
except RuntimeError:
201+
return asyncio.run(_resolve_and_apply())
202+
else:
203+
return _resolve_and_apply()
190204

205+
return self._apply_fee_amount(fee_estimate, change_distribution)
206+
207+
def _apply_fee_amount(self, fee: int, change_distribution: str):
191208
change = 0
192209
for tx_in in self.inputs:
193210
if not tx_in.source_transaction:
194211
raise ValueError('Source transactions are required for all inputs during fee computation')
195212
change += tx_in.source_transaction.outputs[tx_in.source_output_index].satoshis
196-
213+
197214
change -= fee
198215

199216
change_count = 0
@@ -218,6 +235,7 @@ def fee(self, model_or_fee=None, change_distribution='equal'):
218235
for out in self.outputs:
219236
if out.change:
220237
out.satoshis = per_output
238+
return None
221239

222240
async def broadcast(
223241
self,

create_wallet.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from bsv import PrivateKey
2+
3+
4+
def main():
5+
"""
6+
Generates a new BSV sender and receiver wallet and saves the info to wallet_info.txt.
7+
"""
8+
# Generate sender address (Address A)
9+
priv_key_a = PrivateKey()
10+
wif_a = priv_key_a.wif() # Wallet Import Format
11+
address_a = priv_key_a.address()
12+
13+
# Generate receiver address (Address B)
14+
priv_key_b = PrivateKey()
15+
wif_b = priv_key_b.wif()
16+
address_b = priv_key_b.address()
17+
18+
# Print out the keys and addresses
19+
print("\n===== SENDER INFORMATION =====")
20+
print(f"Private Key: {wif_a}")
21+
print(f"Address: {address_a}")
22+
23+
print("\n===== RECEIVER INFORMATION =====")
24+
print(f"Private Key: {wif_b}")
25+
print(f"Address: {address_b}")
26+
27+
# Save data to file for easy reference
28+
with open("wallet_info.txt", "w") as f:
29+
f.write(f"Sender Private Key: {wif_a}\n")
30+
f.write(f"Sender Address: {address_a}\n\n")
31+
f.write(f"Receiver Private Key: {wif_b}\n")
32+
f.write(f"Receiver Address: {address_b}\n")
33+
print("\nThis information has been saved to wallet_info.txt")
34+
35+
if __name__ == "__main__":
36+
main()

inspect_live_policy.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import asyncio
2+
import json
3+
import logging
4+
5+
from bsv.fee_models.live_policy import LivePolicy
6+
from bsv.http_client import default_http_client
7+
8+
9+
async def main() -> None:
10+
logging.basicConfig(level=logging.INFO)
11+
logging.getLogger("bsv.fee_models.live_policy").setLevel(logging.DEBUG)
12+
13+
policy = LivePolicy(cache_ttl_ms=0)
14+
live_rate = await policy.current_rate_sat_per_kb()
15+
print(f"Live fee rate: {live_rate} sat/kB")
16+
17+
http_client = default_http_client()
18+
response = await http_client.get(
19+
policy.arc_policy_url,
20+
headers={"Accept": "application/json"},
21+
timeout=policy.request_timeout,
22+
)
23+
print(f"HTTP status: {response.status_code}")
24+
payload = response.json_data
25+
print("Policy payload:")
26+
print(json.dumps(payload, indent=2, sort_keys=True))
27+
28+
29+
if __name__ == "__main__":
30+
asyncio.run(main())

live_test.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import asyncio
2+
import logging
3+
from bsv import (
4+
PrivateKey, P2PKH, Transaction, TransactionInput, TransactionOutput
5+
)
6+
from bsv.fee_models.live_policy import LivePolicy
7+
from bsv.keys import PublicKey
8+
9+
logging.basicConfig(level=logging.INFO)
10+
logging.getLogger("bsv.fee_models.live_policy").setLevel(logging.DEBUG)
11+
12+
async def main():
13+
"""
14+
A live test script to send BSV.
15+
16+
Instructions:
17+
1. Fund the SENDER_WIF with some BSV. You can get a WIF from a new wallet or use one you have.
18+
2. Go to a block explorer like https://whatsonchain.com and find a transaction
19+
where you received funds to the sender's address.
20+
3. Copy the 'raw transaction hex' of that transaction and paste it into SOURCE_TX_HEX.
21+
4. Update SOURCE_OUTPUT_INDEX to the correct index (usually 0 or 1) that corresponds
22+
to the UTXO you want to spend.
23+
5. Run the script: `python live_test.py`
24+
"""
25+
26+
# --- CONFIGURATION - PLEASE EDIT THESE VALUES ---
27+
28+
# 1. The private key of the wallet that has funds.
29+
# You can generate one using create_wallet.py, or use an existing one.
30+
SENDER_WIF = "Kwr1hjXs7E9uCKknaKLXDHoKMLZ37EbnNU7b4bHx6qLh2tPiwkNf"
31+
32+
# 2. The address you want to send BSV to.
33+
RECIPIENT_ADDRESS = "1CaS8TVYPWdGhHukE3Q1nxqN1NMPQYUUnJ" # The address from your wallet_info.txt
34+
35+
# 3. The raw hex of a transaction where you received BSV to the SENDER_WIF address.
36+
SOURCE_TX_HEX = '010000000309c18e11424ab71674d4bc9e390cc928ed27c001316ea607e6abd8e5fd996849010000006a473044022061b06684612b3d72e824430d93ccf09b04cd6872f5f116ea3214565938ecb0d802203178f8ecca4146852adee9ff5d8078aeb4fb9412c24cfe2694bcd9e3edd18de6412102a773b3a312dc7e0488d978b4fb2089ef466780cbdb639c49af97ffe06fca671cffffffff3bc3ce7935248145779ebdff453a22f9b39819155ba93bde35aba7944a8a64d4030000006a473044022033b1a478bd834349abb768e788dbbebd44f71bbe3bc618f689cd9e7c2defb35f022032582013de69e4fb62ad90b2a0299f21e76956f01032399ef3bc1445cf15331e41210387c8dc9c38330bec6118692b864da8f2a18e6cc732ba106c0d803dfbc74932ccffffffff3bc3ce7935248145779ebdff453a22f9b39819155ba93bde35aba7944a8a64d4040000006a473044022061d20e11129c9c4beb5eeee26652de855614011e05b2aa28b5f2b00a571c4fe902205c6795d9461a1d409c54b1586237d7f9f8ca5b847dbb6200ebb7e7a5dc16d9d141210387c8dc9c38330bec6118692b864da8f2a18e6cc732ba106c0d803dfbc74932ccffffffff02f59e0000000000001976a9146d2d67bed1863b2e39794df441532b5ed02f136588ac5b240000000000001976a914e2d61c65f720d6f8020b5211c3945f65ad7da3f988ac00000000' # From https://whatsonchain.com/tx/831e5b10660ff612ec3a0f0ae15cc74573366c7423ee7efbe94a457b30a7f323
37+
38+
# 4. The output index from the source transaction that you want to spend.
39+
SOURCE_OUTPUT_INDEX = 0 # This is the output that sent funds to 1AxH3ishqURaeNUvuQoqNQXELdDyBti52v
40+
41+
# 5. Amount to send in satoshis (1 BSV = 100,000,000 satoshis)
42+
SATOSHIS_TO_SEND = 500 # A small amount for a test
43+
44+
# --- END OF CONFIGURATION ---
45+
46+
if "L1xx" in SENDER_WIF or "0100000001xx" in SOURCE_TX_HEX:
47+
print("ERROR: Please update the SENDER_WIF and SOURCE_TX_HEX variables in the script.")
48+
return
49+
50+
sender_priv_key = PrivateKey(SENDER_WIF)
51+
sender_address = sender_priv_key.address()
52+
print(f"\nSender Address: {sender_address}")
53+
54+
# Create a transaction object from the source hex
55+
source_tx = Transaction.from_hex(SOURCE_TX_HEX)
56+
57+
# Create the transaction input from the UTXO we want to spend
58+
tx_input = TransactionInput(
59+
source_transaction=source_tx,
60+
source_txid=source_tx.txid(),
61+
source_output_index=SOURCE_OUTPUT_INDEX,
62+
unlocking_script_template=P2PKH().unlock(sender_priv_key),
63+
)
64+
65+
# Create the output to the recipient
66+
tx_output_recipient = TransactionOutput(
67+
locking_script=P2PKH().lock(RECIPIENT_ADDRESS),
68+
satoshis=SATOSHIS_TO_SEND
69+
)
70+
71+
# Create the change output back to the sender
72+
tx_output_change = TransactionOutput(
73+
locking_script=P2PKH().lock(sender_address),
74+
change=True
75+
)
76+
77+
# Build, sign, and broadcast the transaction
78+
print("\nFetching live fee policy...")
79+
live_policy = LivePolicy.get_instance() # Use a safer fallback rate
80+
fee_rate = await live_policy.current_rate_sat_per_kb()
81+
print(f"Using fee rate: {fee_rate} sat/kB")
82+
83+
tx = Transaction([tx_input], [tx_output_recipient, tx_output_change])
84+
await tx.fee(live_policy) # Automatically calculate fee and adjust change
85+
86+
tx.sign()
87+
88+
print(f"\nBroadcasting transaction... Raw Hex: {tx.hex()}")
89+
response = await tx.broadcast()
90+
print(f"Broadcast Response: {response}")
91+
print(f"Transaction ID: {tx.txid()}")
92+
print(f"\nCheck on WhatsOnChain: https://whatsonchain.com/tx/{tx.txid()}")
93+
94+
if __name__ == "__main__":
95+
asyncio.run(main())

0 commit comments

Comments
 (0)