electrs mit ip adressiert

This commit is contained in:
Johannes Trömml 2026-04-29 20:32:37 +00:00
parent 92d13e2973
commit a3978376af
4 changed files with 102 additions and 31 deletions

View file

@ -13,3 +13,8 @@ services:
volumes: volumes:
- .:/app - .:/app
restart: always restart: always
networks:
bitcoin_bitcoin-net:
external: true # ← angepasst

114
main.py
View file

@ -1,29 +1,78 @@
import os import os
import socket
import json
from hashlib import sha256 as hashlib_sha256
import base58
from flask import Flask, jsonify, request from flask import Flask, jsonify, request
from bitcoinlib.services.bitcoind import BitcoindClient from bitcoinlib.services.bitcoind import BitcoindClient
from dotenv import load_dotenv from dotenv import load_dotenv
# Lade die Variablen aus der .env-Datei in die Umgebung
load_dotenv() load_dotenv()
app = Flask(__name__) app = Flask(__name__)
# --- ZUGANGSDATEN AUS UMGEBUNGSVARIABLEN LADEN ---
# os.getenv sucht nach dem Namen und liest den Wert aus
RPC_USER = os.getenv('RPC_USER') RPC_USER = os.getenv('RPC_USER')
RPC_PASS = os.getenv('RPC_PASS') RPC_PASS = os.getenv('RPC_PASS')
# Da das Skript IN der Windows-VM läuft, ist der Node lokal erreichbar: IP_ADRESSE = '192.168.178.225' # ← Docker-Servicename statt IP
IP_ADRESSE = '192.168.178.225' ELECTRS_HOST = '192.168.178.225' # ← Docker-Servicename statt IP
ELECTRS_PORT = 50001
# Sicherheits-Check: Falls die .env fehlt oder leer ist, bricht das Skript ab
if not RPC_USER or not RPC_PASS: if not RPC_USER or not RPC_PASS:
raise ValueError("Fehler: RPC_USER oder RPC_PASS wurden nicht in der Umgebung gefunden!") raise ValueError("Fehler: RPC_USER oder RPC_PASS wurden nicht in der Umgebung gefunden!")
rpc_url = f'http://{RPC_USER}:{RPC_PASS}@{IP_ADRESSE}:8332' rpc_url = f'http://{RPC_USER}:{RPC_PASS}@192.168.178.225:8332'
def get_node(): def get_node():
return BitcoindClient(network='bitcoin', base_url=rpc_url).proxy return BitcoindClient(network='bitcoin', base_url=rpc_url).proxy
# ==========================================
# ELECTRS HILFSFUNKTIONEN
# ==========================================
def address_to_script(address: str) -> str:
"""Konvertiert Bitcoin-Adresse → Output-Script (hex)"""
if address.startswith("bc1"):
# P2WPKH / P2WSH (bech32)
# Manuell dekodieren ohne externe bech32-Library
import bech32
witver, witprog = bech32.decode("bc", address)
if witprog is None:
raise ValueError(f"Ungültige bech32-Adresse: {address}")
if len(witprog) == 20:
return "0014" + bytes(witprog).hex() # P2WPKH
return "0020" + bytes(witprog).hex() # P2WSH
decoded = base58.b58decode_check(address)
prefix = decoded[0]
payload = decoded[1:].hex()
if prefix == 0x00: # 1... → P2PKH
return f"76a914{payload}88ac"
elif prefix == 0x05: # 3... → P2SH
return f"a914{payload}87"
raise ValueError(f"Unbekanntes Adressformat: {address}")
def address_to_electrum_hash(address: str) -> str:
"""Berechnet den Electrum script_hash für eine Adresse"""
script = bytes.fromhex(address_to_script(address))
return hashlib_sha256(script).digest()[::-1].hex()
def electrs_query(method: str, params: list):
"""Sendet eine Anfrage an den lokalen Electrs-Server"""
req = json.dumps({"id": 1, "method": method, "params": params}) + "\n"
with socket.create_connection((ELECTRS_HOST, ELECTRS_PORT), timeout=10) as s:
s.sendall(req.encode())
data = b""
while True:
chunk = s.recv(4096)
data += chunk
if b"\n" in chunk:
break
result = json.loads(data.decode())
if "error" in result and result["error"]:
raise Exception(f"Electrs Fehler: {result['error']}")
return result["result"]
# ========================================== # ==========================================
# ENDPUNKT 1: Aktuelle Blockhöhe abfragen # ENDPUNKT 1: Aktuelle Blockhöhe abfragen
# ========================================== # ==========================================
@ -66,46 +115,62 @@ def get_tx(txid):
return jsonify({'status': 'error', 'message': str(e)}), 500 return jsonify({'status': 'error', 'message': str(e)}), 500
# ========================================== # ==========================================
# ENDPUNKT 4: Guthaben einer beliebigen Adresse # ENDPUNKT 4: Guthaben & UTXOs via Electrs
# ========================================== # ==========================================
@app.route('/api/guthaben/<adresse>', methods=['GET']) @app.route('/api/guthaben/<adresse>', methods=['GET'])
def get_guthaben(adresse): def get_guthaben(adresse):
try: try:
node = get_node() script_hash = address_to_electrum_hash(adresse)
scan_result = node.scantxoutset("start", [f"addr({adresse})"])
# Balance (confirmed + unconfirmed)
balance = electrs_query("blockchain.scripthash.get_balance", [script_hash])
# UTXOs (nur unspent outputs)
utxos = electrs_query("blockchain.scripthash.listunspent", [script_hash])
confirmed = balance["confirmed"]
unconfirmed = balance["unconfirmed"]
return jsonify({ return jsonify({
'status': 'success', 'status': 'success',
'adresse': adresse, 'adresse': adresse,
'guthaben_btc': scan_result.get('total_amount', 0), 'guthaben_btc': confirmed / 1e8,
'anzahl_utxos': len(scan_result.get('unspents', [])), 'unconfirmed_btc': unconfirmed / 1e8,
# NEU: Wir schicken die komplette Liste der Münzen mit! 'guthaben_sat': confirmed,
'utxos': scan_result.get('unspents', []) 'unconfirmed_sat': unconfirmed,
'anzahl_utxos': len(utxos),
'utxos': [
{
'tx_hash': u['tx_hash'],
'tx_pos': u['tx_pos'],
'value': u['value'], # Satoshi
'height': u['height']
}
for u in utxos
]
}) })
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 return jsonify({'status': 'error', 'message': str(e)}), 500
# ========================================== # ==========================================
# NEU: ENDPUNKT 5: Transaktion senden (POST) # ENDPUNKT 5: Transaktion senden (POST)
# ========================================== # ==========================================
@app.route('/api/sendtx', methods=['POST']) @app.route('/api/sendtx', methods=['POST'])
def send_tx(): def send_tx():
try: try:
# Wir erwarten ein JSON-Body mit dem Inhalt: {"hex": "01000000..."}
daten = request.get_json() daten = request.get_json()
if not daten or 'hex' not in daten: if not daten or 'hex' not in daten:
return jsonify({'status': 'error', 'message': 'Kein gueltiger Hex-String uebergeben. Sende ein JSON mit dem Key "hex".'}), 400 return jsonify({'status': 'error', 'message': 'Kein gueltiger Hex-String uebergeben.'}), 400
raw_tx_hex = daten['hex'] raw_tx_hex = daten['hex']
node = get_node() node = get_node()
# Sende die Transaktion an den Node, der sie ins Netzwerk broadcastet
txid = node.sendrawtransaction(raw_tx_hex) txid = node.sendrawtransaction(raw_tx_hex)
return jsonify({ return jsonify({
'status': 'success', 'status': 'success',
'message': 'Transaktion erfolgreich an das Netzwerk uebermittelt!', 'message': 'Transaktion erfolgreich an das Netzwerk uebermittelt!',
'txid': txid 'txid': txid
}) })
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 return jsonify({'status': 'error', 'message': str(e)}), 500
@ -115,5 +180,4 @@ def send_tx():
# ========================================== # ==========================================
if __name__ == '__main__': if __name__ == '__main__':
print("🚀 API Server startet...") print("🚀 API Server startet...")
# host='0.0.0.0' bedeutet, dass die API von anderen Geräten im Heimnetzwerk erreichbar ist!
app.run(host='0.0.0.0', port=5001) app.run(host='0.0.0.0', port=5001)

View file

@ -2,3 +2,5 @@ Flask==3.0.2
python-dotenv==1.0.1 python-dotenv==1.0.1
bitcoinlib==0.6.15 bitcoinlib==0.6.15
requests==2.31.0 requests==2.31.0
base58
bech32