mirror of
https://github.com/curly60e/pyblock.git
synced 2026-08-13 12:33:15 +02:00
Replace all dynamic .split() patterns in subprocess calls with safe alternatives: shlex.split(), explicit list args, and _run_btc/_run_ln helpers in PyBlock.py. Covers PyBlock, block_visualizer, clockscript, lastblockdetail, mempoolclock, nodeconnection, and ai/context. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
184 lines
5.2 KiB
Python
184 lines
5.2 KiB
Python
"""Gather Bitcoin/Lightning node data for AI context injection."""
|
|
|
|
import codecs
|
|
import json
|
|
import shlex
|
|
import subprocess
|
|
|
|
import requests
|
|
|
|
|
|
def gather_node_context(path, lndconnectload=None):
|
|
"""Collect node data to send with AI queries.
|
|
|
|
path: dict with bitcoincli, ip_port, rpcuser, rpcpass
|
|
lndconnectload: dict with LND connection info (optional)
|
|
"""
|
|
ctx = {}
|
|
|
|
# Bitcoin Core data
|
|
if path.get("bitcoincli"):
|
|
ctx.update(_bitcoin_cli_context(path))
|
|
elif path.get("ip_port") and path.get("rpcuser"):
|
|
ctx.update(_bitcoin_rpc_context(path))
|
|
else:
|
|
ctx.update(_bitcoin_api_context())
|
|
|
|
# Lightning data
|
|
if lndconnectload and lndconnectload.get("ip_port"):
|
|
ctx.update(_lightning_context(lndconnectload))
|
|
|
|
return ctx
|
|
|
|
|
|
def _run_cli(cli_args, command):
|
|
"""Run a bitcoin-cli command safely. Returns stdout or empty string."""
|
|
# nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit
|
|
return subprocess.run(
|
|
cli_args + [command],
|
|
capture_output=True, text=True, timeout=10
|
|
).stdout
|
|
|
|
|
|
def _bitcoin_cli_context(path):
|
|
"""Gather context via bitcoin-cli."""
|
|
ctx = {}
|
|
cli = shlex.split(path["bitcoincli"])
|
|
try:
|
|
raw = _run_cli(cli, "getblockchaininfo")
|
|
info = json.loads(raw)
|
|
ctx["block_height"] = info.get("blocks", 0)
|
|
ctx["chain"] = info.get("chain", "")
|
|
ctx["verification_progress"] = round(
|
|
info.get("verificationprogress", 0), 4
|
|
)
|
|
ctx["size_on_disk_gb"] = round(
|
|
info.get("size_on_disk", 0) / 1e9, 2
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
raw = _run_cli(cli, "getmempoolinfo")
|
|
mempool = json.loads(raw)
|
|
ctx["mempool_size"] = mempool.get("size", 0)
|
|
ctx["mempool_bytes"] = mempool.get("bytes", 0)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
raw = _run_cli(cli, "getnetworkinfo")
|
|
net = json.loads(raw)
|
|
ctx["peer_count"] = net.get("connections", 0)
|
|
except Exception:
|
|
pass
|
|
|
|
# Fee rates from mempool.space (fast/medium/slow)
|
|
ctx.update(_fee_rates())
|
|
|
|
return ctx
|
|
|
|
|
|
def _bitcoin_rpc_context(path):
|
|
"""Gather context via JSON-RPC."""
|
|
ctx = {}
|
|
try:
|
|
def rpc(method, params=None):
|
|
payload = json.dumps({
|
|
"jsonrpc": "2.0", "id": "ai",
|
|
"method": method, "params": params or []
|
|
})
|
|
r = requests.post(
|
|
path["ip_port"],
|
|
auth=(path["rpcuser"], path["rpcpass"]),
|
|
data=payload, timeout=10
|
|
)
|
|
return r.json()["result"]
|
|
|
|
info = rpc("getblockchaininfo")
|
|
ctx["block_height"] = info.get("blocks", 0)
|
|
ctx["chain"] = info.get("chain", "")
|
|
|
|
mempool = rpc("getmempoolinfo")
|
|
ctx["mempool_size"] = mempool.get("size", 0)
|
|
|
|
net = rpc("getnetworkinfo")
|
|
ctx["peer_count"] = net.get("connections", 0)
|
|
except Exception:
|
|
pass
|
|
|
|
ctx.update(_fee_rates())
|
|
return ctx
|
|
|
|
|
|
def _bitcoin_api_context():
|
|
"""Gather context from mempool.space API (lite mode)."""
|
|
ctx = {}
|
|
try:
|
|
r = requests.get(
|
|
"https://mempool.space/api/blocks/tip/height", timeout=10
|
|
)
|
|
ctx["block_height"] = int(r.text.strip())
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
r = requests.get(
|
|
"https://mempool.space/api/mempool", timeout=10
|
|
)
|
|
data = r.json()
|
|
ctx["mempool_size"] = data.get("count", 0)
|
|
except Exception:
|
|
pass
|
|
|
|
ctx.update(_fee_rates())
|
|
return ctx
|
|
|
|
|
|
def _fee_rates():
|
|
"""Fetch recommended fee rates from mempool.space."""
|
|
try:
|
|
r = requests.get(
|
|
"https://mempool.space/api/v1/fees/recommended", timeout=10
|
|
)
|
|
fees = r.json()
|
|
return {
|
|
"fee_rates": {
|
|
"fast": fees.get("fastestFee", 0),
|
|
"medium": fees.get("halfHourFee", 0),
|
|
"slow": fees.get("hourFee", 0),
|
|
}
|
|
}
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def _lightning_context(lndconnectload):
|
|
"""Gather Lightning node context from LND."""
|
|
ctx = {}
|
|
try:
|
|
cert_path = lndconnectload.get("tls", "")
|
|
macaroon_path = lndconnectload.get("macaroon", "")
|
|
if not cert_path or not macaroon_path:
|
|
return ctx
|
|
|
|
with open(macaroon_path, "rb") as f:
|
|
macaroon = codecs.encode(f.read(), "hex")
|
|
headers = {"Grpc-Metadata-macaroon": macaroon}
|
|
url = f'https://{lndconnectload["ip_port"]}/v1/getinfo'
|
|
r = requests.get(url, headers=headers, verify=cert_path, timeout=10)
|
|
info = r.json()
|
|
ctx["ln_alias"] = info.get("alias", "")
|
|
ctx["ln_channels"] = info.get("num_active_channels", 0)
|
|
ctx["ln_peers"] = info.get("num_peers", 0)
|
|
|
|
# Channel balances
|
|
url_bal = f'https://{lndconnectload["ip_port"]}/v1/balance/channels'
|
|
r2 = requests.get(url_bal, headers=headers, verify=cert_path, timeout=10)
|
|
bal = r2.json()
|
|
ctx["local_balance_sats"] = int(bal.get("local_balance", {}).get("sat", 0))
|
|
ctx["remote_balance_sats"] = int(bal.get("remote_balance", {}).get("sat", 0))
|
|
except Exception:
|
|
pass
|
|
|
|
return ctx
|