electrs mit ip adressiert
This commit is contained in:
parent
92d13e2973
commit
a3978376af
4 changed files with 102 additions and 31 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
.env
|
.env
|
||||||
venv/
|
venv/
|
||||||
/venv/
|
/venv/
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,8 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
bitcoin_bitcoin-net:
|
||||||
|
external: true # ← angepasst
|
||||||
|
|
|
||||||
124
main.py
124
main.py
|
|
@ -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
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
@ -33,8 +82,8 @@ def get_blockhoehe():
|
||||||
node = get_node()
|
node = get_node()
|
||||||
info = node.getblockchaininfo()
|
info = node.getblockchaininfo()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'blockhoehe': info['blocks'],
|
'blockhoehe': info['blocks'],
|
||||||
'sync_fortschritt_prozent': round(info.get('verificationprogress', 0) * 100, 2)
|
'sync_fortschritt_prozent': round(info.get('verificationprogress', 0) * 100, 2)
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -48,7 +97,7 @@ def get_block(hoehe):
|
||||||
try:
|
try:
|
||||||
node = get_node()
|
node = get_node()
|
||||||
block_hash = node.getblockhash(hoehe)
|
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})
|
return jsonify({'status': 'success', 'block': block_daten})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in a new issue