diff --git a/.gitignore b/.gitignore index df7bb3f..c3f9d5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ .env venv/ -/venv/ \ No newline at end of file +/venv/ diff --git a/docker-compose.yml b/docker-compose.yml index e9e318c..b1f3f9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,3 +13,8 @@ services: volumes: - .:/app restart: always + + +networks: + bitcoin_bitcoin-net: + external: true # ← angepasst diff --git a/main.py b/main.py index d6ca4ef..23c3091 100644 --- a/main.py +++ b/main.py @@ -1,29 +1,78 @@ import os +import socket +import json +from hashlib import sha256 as hashlib_sha256 +import base58 from flask import Flask, jsonify, request from bitcoinlib.services.bitcoind import BitcoindClient from dotenv import load_dotenv -# Lade die Variablen aus der .env-Datei in die Umgebung load_dotenv() 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_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' +IP_ADRESSE = '192.168.178.225' # ← Docker-Servicename statt IP +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: 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(): 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 # ========================================== @@ -33,8 +82,8 @@ def get_blockhoehe(): node = get_node() info = node.getblockchaininfo() return jsonify({ - 'status': 'success', - 'blockhoehe': info['blocks'], + 'status': 'success', + 'blockhoehe': info['blocks'], 'sync_fortschritt_prozent': round(info.get('verificationprogress', 0) * 100, 2) }) except Exception as e: @@ -48,7 +97,7 @@ def get_block(hoehe): try: node = get_node() block_hash = node.getblockhash(hoehe) - block_daten = node.getblock(block_hash, 2) + block_daten = node.getblock(block_hash, 2) return jsonify({'status': 'success', 'block': block_daten}) except Exception as e: return jsonify({'status': 'error', 'message': str(e)}), 500 @@ -66,46 +115,62 @@ def get_tx(txid): return jsonify({'status': 'error', 'message': str(e)}), 500 # ========================================== -# ENDPUNKT 4: Guthaben einer beliebigen Adresse +# ENDPUNKT 4: Guthaben & UTXOs via Electrs # ========================================== @app.route('/api/guthaben/', methods=['GET']) def get_guthaben(adresse): try: - node = get_node() - scan_result = node.scantxoutset("start", [f"addr({adresse})"]) + script_hash = address_to_electrum_hash(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({ - 'status': 'success', - 'adresse': adresse, - 'guthaben_btc': scan_result.get('total_amount', 0), - 'anzahl_utxos': len(scan_result.get('unspents', [])), - # NEU: Wir schicken die komplette Liste der Münzen mit! - 'utxos': scan_result.get('unspents', []) + 'status': 'success', + 'adresse': adresse, + 'guthaben_btc': confirmed / 1e8, + 'unconfirmed_btc': unconfirmed / 1e8, + 'guthaben_sat': confirmed, + '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: 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']) def send_tx(): try: - # Wir erwarten ein JSON-Body mit dem Inhalt: {"hex": "01000000..."} daten = request.get_json() - 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'] node = get_node() - - # Sende die Transaktion an den Node, der sie ins Netzwerk broadcastet txid = node.sendrawtransaction(raw_tx_hex) - + return jsonify({ - 'status': 'success', + 'status': 'success', 'message': 'Transaktion erfolgreich an das Netzwerk uebermittelt!', - 'txid': txid + 'txid': txid }) except Exception as e: return jsonify({'status': 'error', 'message': str(e)}), 500 @@ -115,5 +180,4 @@ def send_tx(): # ========================================== if __name__ == '__main__': 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) diff --git a/requirements.txt b/requirements.txt index 06bc7e1..1241826 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,5 @@ Flask==3.0.2 python-dotenv==1.0.1 bitcoinlib==0.6.15 requests==2.31.0 +base58 +bech32 \ No newline at end of file