379 lines
12 KiB
Python
379 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Web Dashboard API for Palladium Node and ElectrumX Server Statistics
|
|
"""
|
|
|
|
from flask import Flask, jsonify, render_template, request
|
|
from flask_cors import CORS
|
|
import requests
|
|
import json
|
|
import os
|
|
import time
|
|
from datetime import datetime
|
|
import psutil
|
|
import socket
|
|
|
|
app = Flask(__name__)
|
|
CORS(app)
|
|
|
|
# Configuration
|
|
PALLADIUM_RPC_HOST = os.getenv('PALLADIUM_RPC_HOST', 'palladiumd')
|
|
PALLADIUM_RPC_PORT = int(os.getenv('PALLADIUM_RPC_PORT', '2332'))
|
|
ELECTRUMX_RPC_HOST = os.getenv('ELECTRUMX_RPC_HOST', 'electrumx')
|
|
ELECTRUMX_RPC_PORT = int(os.getenv('ELECTRUMX_RPC_PORT', '8000'))
|
|
|
|
# Read RPC credentials from palladium.conf
|
|
def get_rpc_credentials():
|
|
"""Read RPC credentials from palladium.conf"""
|
|
try:
|
|
conf_path = '/palladium-config/palladium.conf'
|
|
rpc_user = None
|
|
rpc_password = None
|
|
|
|
with open(conf_path, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line.startswith('rpcuser='):
|
|
rpc_user = line.split('=', 1)[1]
|
|
elif line.startswith('rpcpassword='):
|
|
rpc_password = line.split('=', 1)[1]
|
|
|
|
return rpc_user, rpc_password
|
|
except Exception as e:
|
|
print(f"Error reading RPC credentials: {e}")
|
|
return None, None
|
|
|
|
def palladium_rpc_call(method, params=None):
|
|
"""Make RPC call to Palladium node"""
|
|
if params is None:
|
|
params = []
|
|
|
|
rpc_user, rpc_password = get_rpc_credentials()
|
|
if not rpc_user or not rpc_password:
|
|
return None
|
|
|
|
url = f"http://{PALLADIUM_RPC_HOST}:{PALLADIUM_RPC_PORT}"
|
|
headers = {'content-type': 'application/json'}
|
|
payload = {
|
|
"jsonrpc": "2.0",
|
|
"id": "dashboard",
|
|
"method": method,
|
|
"params": params
|
|
}
|
|
|
|
try:
|
|
response = requests.post(
|
|
url,
|
|
auth=(rpc_user, rpc_password),
|
|
data=json.dumps(payload),
|
|
headers=headers,
|
|
timeout=10
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
result = response.json()
|
|
return result.get('result')
|
|
return None
|
|
except Exception as e:
|
|
print(f"RPC call error ({method}): {e}")
|
|
return None
|
|
|
|
def get_electrumx_stats():
|
|
"""Get ElectrumX statistics via Electrum protocol and system info"""
|
|
try:
|
|
import socket
|
|
import json
|
|
import subprocess
|
|
from datetime import datetime
|
|
|
|
stats = {
|
|
'server_version': 'Unknown',
|
|
'protocol_min': '',
|
|
'protocol_max': '',
|
|
'genesis_hash': '',
|
|
'hash_function': '',
|
|
'pruning': None,
|
|
'sessions': 0,
|
|
'requests': 0,
|
|
'subs': 0,
|
|
'uptime': 0,
|
|
'db_size': 0,
|
|
'tcp_port': 50001,
|
|
'ssl_port': 50002,
|
|
'server_ip': 'Unknown'
|
|
}
|
|
|
|
# Get server IP address
|
|
try:
|
|
# Try to get public IP from external service
|
|
response = requests.get('https://api.ipify.org?format=json', timeout=3)
|
|
if response.status_code == 200:
|
|
stats['server_ip'] = response.json().get('ip', 'Unknown')
|
|
else:
|
|
# Fallback to local IP
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
s.connect(("8.8.8.8", 80))
|
|
stats['server_ip'] = s.getsockname()[0]
|
|
s.close()
|
|
except Exception as e:
|
|
print(f"IP detection error: {e}")
|
|
# Last fallback: get hostname IP
|
|
try:
|
|
stats['server_ip'] = socket.gethostbyname(socket.gethostname())
|
|
except:
|
|
stats['server_ip'] = 'Unknown'
|
|
|
|
# Get server features via Electrum protocol
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(5)
|
|
sock.connect((ELECTRUMX_RPC_HOST, 50001))
|
|
|
|
request = {
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "server.features",
|
|
"params": []
|
|
}
|
|
|
|
sock.send((json.dumps(request) + '\n').encode())
|
|
response = sock.recv(4096).decode()
|
|
sock.close()
|
|
|
|
data = json.loads(response)
|
|
if 'result' in data:
|
|
result = data['result']
|
|
stats['server_version'] = result.get('server_version', 'Unknown')
|
|
stats['protocol_min'] = result.get('protocol_min', '')
|
|
stats['protocol_max'] = result.get('protocol_max', '')
|
|
stats['genesis_hash'] = result.get('genesis_hash', '')[:16] + '...'
|
|
stats['hash_function'] = result.get('hash_function', '')
|
|
stats['pruning'] = result.get('pruning')
|
|
except Exception as e:
|
|
print(f"ElectrumX protocol error: {e}")
|
|
|
|
# Try to get container stats via Docker
|
|
try:
|
|
# Get container uptime
|
|
result = subprocess.run(
|
|
['docker', 'inspect', 'electrumx-server', '--format', '{{.State.StartedAt}}'],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=2
|
|
)
|
|
if result.returncode == 0:
|
|
started_at = result.stdout.strip()
|
|
# Parse and calculate uptime
|
|
from dateutil import parser
|
|
start_time = parser.parse(started_at)
|
|
uptime_seconds = int((datetime.now(start_time.tzinfo) - start_time).total_seconds())
|
|
stats['uptime'] = uptime_seconds
|
|
except Exception as e:
|
|
print(f"Docker uptime error: {e}")
|
|
|
|
# Try to estimate DB size from data directory
|
|
try:
|
|
result = subprocess.run(
|
|
['docker', 'exec', 'electrumx-server', 'du', '-sb', '/data'],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5
|
|
)
|
|
if result.returncode == 0:
|
|
db_size = int(result.stdout.split()[0])
|
|
stats['db_size'] = db_size
|
|
except Exception as e:
|
|
print(f"DB size error: {e}")
|
|
|
|
# Count active connections (TCP sessions)
|
|
try:
|
|
result = subprocess.run(
|
|
['docker', 'exec', 'electrumx-server', 'sh', '-c',
|
|
'netstat -an 2>/dev/null | grep ":50001.*ESTABLISHED" | wc -l'],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=2
|
|
)
|
|
if result.returncode == 0:
|
|
sessions = int(result.stdout.strip())
|
|
stats['sessions'] = sessions
|
|
except Exception as e:
|
|
print(f"Sessions count error: {e}")
|
|
|
|
return stats
|
|
except Exception as e:
|
|
print(f"ElectrumX stats error: {e}")
|
|
return None
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""Serve main dashboard page"""
|
|
return render_template('index.html')
|
|
|
|
@app.route('/peers')
|
|
def peers():
|
|
"""Serve peers page"""
|
|
return render_template('peers.html')
|
|
|
|
@app.route('/api/palladium/info')
|
|
def palladium_info():
|
|
"""Get Palladium node blockchain info"""
|
|
try:
|
|
blockchain_info = palladium_rpc_call('getblockchaininfo')
|
|
network_info = palladium_rpc_call('getnetworkinfo')
|
|
mining_info = palladium_rpc_call('getmininginfo')
|
|
peer_info = palladium_rpc_call('getpeerinfo')
|
|
mempool_info = palladium_rpc_call('getmempoolinfo')
|
|
|
|
data = {
|
|
'blockchain': blockchain_info or {},
|
|
'network': network_info or {},
|
|
'mining': mining_info or {},
|
|
'peers': len(peer_info) if peer_info else 0,
|
|
'mempool': mempool_info or {},
|
|
'timestamp': datetime.now().isoformat()
|
|
}
|
|
|
|
return jsonify(data)
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/palladium/peers')
|
|
def palladium_peers():
|
|
"""Get detailed peer information"""
|
|
try:
|
|
peer_info = palladium_rpc_call('getpeerinfo')
|
|
if not peer_info:
|
|
return jsonify({'peers': []})
|
|
|
|
peers_data = []
|
|
for peer in peer_info:
|
|
peers_data.append({
|
|
'addr': peer.get('addr', 'Unknown'),
|
|
'inbound': peer.get('inbound', False),
|
|
'version': peer.get('subver', 'Unknown'),
|
|
'conntime': peer.get('conntime', 0),
|
|
'bytessent': peer.get('bytessent', 0),
|
|
'bytesrecv': peer.get('bytesrecv', 0)
|
|
})
|
|
|
|
return jsonify({
|
|
'peers': peers_data,
|
|
'total': len(peers_data),
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/palladium/blocks/recent')
|
|
def recent_blocks():
|
|
"""Get recent blocks information"""
|
|
try:
|
|
blockchain_info = palladium_rpc_call('getblockchaininfo')
|
|
if not blockchain_info:
|
|
return jsonify({'error': 'Cannot get blockchain info'}), 500
|
|
|
|
current_height = blockchain_info.get('blocks', 0)
|
|
blocks = []
|
|
|
|
# Get last 10 blocks
|
|
for i in range(min(10, current_height)):
|
|
height = current_height - i
|
|
block_hash = palladium_rpc_call('getblockhash', [height])
|
|
if block_hash:
|
|
block = palladium_rpc_call('getblock', [block_hash])
|
|
if block:
|
|
blocks.append({
|
|
'height': height,
|
|
'hash': block_hash,
|
|
'time': block.get('time'),
|
|
'size': block.get('size'),
|
|
'tx_count': len(block.get('tx', []))
|
|
})
|
|
|
|
return jsonify({'blocks': blocks})
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/electrumx/stats')
|
|
def electrumx_stats():
|
|
"""Get ElectrumX server statistics"""
|
|
try:
|
|
stats = get_electrumx_stats()
|
|
if stats:
|
|
# Get additional info from logs if available
|
|
try:
|
|
# Try to get container stats
|
|
import subprocess
|
|
result = subprocess.run(
|
|
['docker', 'exec', 'electrumx-server', 'sh', '-c',
|
|
'ps aux | grep electrumx_server | grep -v grep'],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=2
|
|
)
|
|
if result.returncode == 0:
|
|
# Process is running, add placeholder stats
|
|
stats['status'] = 'running'
|
|
stats['sessions'] = 0 # Will show 0 for now
|
|
stats['requests'] = 0
|
|
stats['subs'] = 0
|
|
except:
|
|
pass
|
|
|
|
return jsonify({
|
|
'stats': stats,
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
return jsonify({'error': 'Cannot connect to ElectrumX'}), 500
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/system/resources')
|
|
def system_resources():
|
|
"""Get system resource usage"""
|
|
try:
|
|
cpu_percent = psutil.cpu_percent(interval=1)
|
|
memory = psutil.virtual_memory()
|
|
disk = psutil.disk_usage('/')
|
|
|
|
data = {
|
|
'cpu': {
|
|
'percent': cpu_percent,
|
|
'count': psutil.cpu_count()
|
|
},
|
|
'memory': {
|
|
'total': memory.total,
|
|
'used': memory.used,
|
|
'percent': memory.percent
|
|
},
|
|
'disk': {
|
|
'total': disk.total,
|
|
'used': disk.used,
|
|
'percent': disk.percent
|
|
},
|
|
'timestamp': datetime.now().isoformat()
|
|
}
|
|
|
|
return jsonify(data)
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/health')
|
|
def health():
|
|
"""Health check endpoint"""
|
|
palladium_ok = palladium_rpc_call('getblockchaininfo') is not None
|
|
electrumx_ok = get_electrumx_stats() is not None
|
|
|
|
return jsonify({
|
|
'status': 'healthy' if (palladium_ok and electrumx_ok) else 'degraded',
|
|
'services': {
|
|
'palladium': 'up' if palladium_ok else 'down',
|
|
'electrumx': 'up' if electrumx_ok else 'down'
|
|
},
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
if __name__ == '__main__':
|
|
app.run(host='0.0.0.0', port=8080, debug=False)
|