530 lines
20 KiB
Python
530 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
RinCoin Mining Pool Web Interface
|
|
Provides web dashboard for pool statistics and miner management
|
|
"""
|
|
|
|
import json
|
|
import sqlite3
|
|
import requests
|
|
from datetime import datetime, timedelta
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
import threading
|
|
import time
|
|
from requests.auth import HTTPBasicAuth
|
|
|
|
class PoolWebInterface:
|
|
def __init__(self, pool_db, host='0.0.0.0', port=8080, rpc_host='127.0.0.1', rpc_port=9556,
|
|
rpc_user='rinrpc', rpc_password='745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90'):
|
|
self.pool_db = pool_db
|
|
self.host = host
|
|
self.port = port
|
|
self.rpc_host = rpc_host
|
|
self.rpc_port = rpc_port
|
|
self.rpc_user = rpc_user
|
|
self.rpc_password = rpc_password
|
|
self.chart_time_window = 3600 # 1 hour default, adjustable
|
|
|
|
def set_chart_time_window(self, seconds):
|
|
"""Set the chart time window"""
|
|
self.chart_time_window = seconds
|
|
|
|
def format_hashrate(self, hashrate):
|
|
"""Format hashrate in human readable format"""
|
|
if hashrate >= 1e12:
|
|
return f"{hashrate/1e12:.2f} TH/s"
|
|
elif hashrate >= 1e9:
|
|
return f"{hashrate/1e9:.2f} GH/s"
|
|
elif hashrate >= 1e6:
|
|
return f"{hashrate/1e6:.2f} MH/s"
|
|
elif hashrate >= 1e3:
|
|
return f"{hashrate/1e3:.2f} KH/s"
|
|
elif hashrate >= 0.01:
|
|
return f"{hashrate:.2f} H/s"
|
|
elif hashrate > 0:
|
|
return f"{hashrate*1000:.2f} mH/s"
|
|
else:
|
|
return "0.00 H/s"
|
|
|
|
def get_pool_balance(self):
|
|
"""Get pool wallet balance via RPC"""
|
|
try:
|
|
url = f"http://{self.rpc_host}:{self.rpc_port}/"
|
|
headers = {'content-type': 'text/plain'}
|
|
auth = HTTPBasicAuth(self.rpc_user, self.rpc_password)
|
|
|
|
payload = {
|
|
"jsonrpc": "1.0",
|
|
"id": "pool_balance",
|
|
"method": "getbalance",
|
|
"params": []
|
|
}
|
|
|
|
response = requests.post(url, json=payload, headers=headers, auth=auth, timeout=10)
|
|
|
|
if response.status_code == 200:
|
|
result = response.json()
|
|
if 'error' in result and result['error'] is not None:
|
|
print(f"RPC Error getting balance: {result['error']}")
|
|
return 0.0
|
|
balance = result.get('result', 0)
|
|
return float(balance) / 100000000 # Convert from satoshis to RIN
|
|
else:
|
|
print(f"HTTP Error getting balance: {response.status_code}")
|
|
return 0.0
|
|
|
|
except Exception as e:
|
|
print(f"Error getting pool balance: {e}")
|
|
return 0.0
|
|
|
|
def get_pool_stats(self):
|
|
"""Get current pool statistics"""
|
|
try:
|
|
cursor = self.pool_db.cursor()
|
|
|
|
# Total miners (ever registered)
|
|
cursor.execute('SELECT COUNT(DISTINCT id) FROM miners')
|
|
total_miners = cursor.fetchone()[0]
|
|
|
|
# Active miners (last 5 minutes)
|
|
cursor.execute('''
|
|
SELECT COUNT(DISTINCT m.id) FROM miners m
|
|
JOIN shares s ON m.id = s.miner_id
|
|
WHERE s.submitted > datetime('now', '-5 minutes')
|
|
''')
|
|
active_miners = cursor.fetchone()[0]
|
|
|
|
# Total shares (last 24 hours)
|
|
cursor.execute('''
|
|
SELECT COUNT(*) FROM shares
|
|
WHERE submitted > datetime('now', '-24 hours')
|
|
''')
|
|
total_shares_24h = cursor.fetchone()[0]
|
|
|
|
# Pool hashrate: sum of miners.last_hashrate (instantaneous)
|
|
cursor.execute('SELECT COALESCE(SUM(last_hashrate), 0) FROM miners')
|
|
hashrate = cursor.fetchone()[0] or 0.0
|
|
|
|
# Debug stats
|
|
cursor.execute('''
|
|
SELECT SUM(difficulty), COUNT(*) FROM shares
|
|
WHERE submitted > datetime('now', '-5 minutes')
|
|
''')
|
|
rd = cursor.fetchone()
|
|
recent_difficulty = rd[0] if rd and rd[0] else 0
|
|
recent_share_count = rd[1] if rd and rd[1] else 0
|
|
|
|
# Get historical hashrate data for chart
|
|
cursor.execute('''
|
|
SELECT
|
|
strftime('%H:%M', submitted) as time,
|
|
COUNT(*) as shares,
|
|
SUM(difficulty) as total_difficulty
|
|
FROM shares
|
|
WHERE submitted > datetime('now', '-{} seconds')
|
|
GROUP BY strftime('%Y-%m-%d %H:%M', submitted)
|
|
ORDER BY submitted DESC
|
|
LIMIT 60
|
|
'''.format(self.chart_time_window))
|
|
historical_data = cursor.fetchall()
|
|
|
|
# Calculate individual miner hashrates
|
|
cursor.execute('''
|
|
SELECT
|
|
m.user, m.worker,
|
|
COUNT(s.id) as shares,
|
|
SUM(s.difficulty) as total_difficulty,
|
|
m.last_share
|
|
FROM miners m
|
|
LEFT JOIN shares s ON m.id = s.miner_id
|
|
AND s.submitted > datetime('now', '-5 minutes')
|
|
GROUP BY m.id, m.user, m.worker
|
|
ORDER BY shares DESC
|
|
''')
|
|
miner_stats = cursor.fetchall()
|
|
|
|
# Calculate individual hashrates (use miners.last_hashrate)
|
|
miner_hashrates = []
|
|
for user, worker, shares, difficulty, last_share in miner_stats:
|
|
cursor.execute('SELECT last_hashrate FROM miners WHERE user = ? AND worker = ? LIMIT 1', (user, worker))
|
|
row = cursor.fetchone()
|
|
miner_hashrate = row[0] if row and row[0] else 0.0
|
|
miner_hashrates.append((user, worker, shares, miner_hashrate, last_share))
|
|
|
|
# Total blocks found
|
|
cursor.execute('SELECT COUNT(*) FROM blocks')
|
|
total_blocks = cursor.fetchone()[0]
|
|
|
|
# Recent blocks
|
|
cursor.execute('''
|
|
SELECT block_hash, height, reward, found_at
|
|
FROM blocks
|
|
ORDER BY found_at DESC
|
|
LIMIT 10
|
|
''')
|
|
recent_blocks = cursor.fetchall()
|
|
|
|
# Top miners (last 24 hours) - show all miners, even without shares
|
|
cursor.execute('''
|
|
SELECT m.user, m.worker,
|
|
COALESCE(COUNT(s.id), 0) as shares,
|
|
m.last_share,
|
|
m.created
|
|
FROM miners m
|
|
LEFT JOIN shares s ON m.id = s.miner_id
|
|
AND s.submitted > datetime('now', '-24 hours')
|
|
GROUP BY m.id, m.user, m.worker
|
|
ORDER BY shares DESC, m.created DESC
|
|
LIMIT 20
|
|
''')
|
|
top_miners = cursor.fetchall()
|
|
|
|
# All active miners (for better visibility)
|
|
cursor.execute('''
|
|
SELECT user, worker, created, last_share
|
|
FROM miners
|
|
ORDER BY created DESC
|
|
LIMIT 10
|
|
''')
|
|
all_miners = cursor.fetchall()
|
|
|
|
# Get pool balance
|
|
pool_balance = self.get_pool_balance()
|
|
|
|
return {
|
|
'total_miners': total_miners,
|
|
'active_miners': active_miners,
|
|
'total_shares_24h': total_shares_24h,
|
|
'hashrate': hashrate,
|
|
'total_blocks': total_blocks,
|
|
'recent_blocks': recent_blocks,
|
|
'top_miners': top_miners,
|
|
'all_miners': all_miners,
|
|
'miner_hashrates': miner_hashrates,
|
|
'historical_data': historical_data,
|
|
'pool_balance': pool_balance,
|
|
'debug': {
|
|
'recent_difficulty': recent_difficulty,
|
|
'recent_share_count': recent_share_count,
|
|
'total_shares_24h': total_shares_24h
|
|
}
|
|
}
|
|
except Exception as e:
|
|
print(f"Error getting pool stats: {e}")
|
|
return {}
|
|
|
|
def generate_html(self, stats):
|
|
"""Generate HTML dashboard"""
|
|
html = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>RinCoin Mining Pool</title>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<style>
|
|
body {{ font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }}
|
|
.container {{ max-width: 1200px; margin: 0 auto; }}
|
|
.header {{ background: #2c3e50; color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }}
|
|
.stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; }}
|
|
.stat-card {{ background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
|
|
.stat-value {{ font-size: 2em; font-weight: bold; color: #3498db; }}
|
|
.stat-label {{ color: #7f8c8d; margin-top: 5px; }}
|
|
.section {{ background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }}
|
|
.section h2 {{ margin-top: 0; color: #2c3e50; }}
|
|
table {{ width: 100%; border-collapse: collapse; }}
|
|
th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }}
|
|
th {{ background: #f8f9fa; font-weight: bold; }}
|
|
.block-hash {{ font-family: monospace; font-size: 0.9em; }}
|
|
.refresh-btn {{ background: #3498db; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; }}
|
|
.refresh-btn:hover {{ background: #2980b9; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🏊♂️ RinCoin Mining Pool</h1>
|
|
<p>Distribute block rewards among multiple miners</p>
|
|
</div>
|
|
|
|
<button class="refresh-btn" onclick="location.reload()">🔄 Refresh</button>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-value">{stats.get('total_miners', 0)}</div>
|
|
<div class="stat-label">Total Miners</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{stats.get('active_miners', 0)}</div>
|
|
<div class="stat-label">Active Miners</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{self.format_hashrate(stats.get('hashrate', 0))}</div>
|
|
<div class="stat-label">Hashrate</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{stats.get('total_blocks', 0)}</div>
|
|
<div class="stat-label">Blocks Found</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{stats.get('pool_balance', 0):.2f}</div>
|
|
<div class="stat-label">Pool Balance (RIN)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>📊 Pool Statistics</h2>
|
|
<p><strong>24h Shares:</strong> {stats.get('total_shares_24h', 0):,}</p>
|
|
<p><strong>Pool Fee:</strong> 1%</p>
|
|
<p><strong>Pool Balance:</strong> {stats.get('pool_balance', 0):.8f} RIN</p>
|
|
<p><strong>Connection String:</strong> <code>stratum+tcp://YOUR_IP:3333</code></p>
|
|
|
|
<!-- Debug info -->
|
|
<details style="margin-top: 20px; padding: 10px; background: #f8f9fa; border-radius: 4px;">
|
|
<summary style="cursor: pointer; font-weight: bold;">🔍 Debug Info</summary>
|
|
<p><strong>Recent Difficulty (5min):</strong> {stats.get('debug', {}).get('recent_difficulty', 0):.6f}</p>
|
|
<p><strong>Recent Share Count (5min):</strong> {stats.get('debug', {}).get('recent_share_count', 0)}</p>
|
|
<p><strong>Total Shares (24h):</strong> {stats.get('debug', {}).get('total_shares_24h', 0):,}</p>
|
|
<p><strong>Active Miners:</strong> {stats.get('active_miners', 0)} (last 5 minutes)</p>
|
|
<p><strong>Total Miners:</strong> {stats.get('total_miners', 0)} (ever registered)</p>
|
|
</details>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>📈 Hashrate Chart</h2>
|
|
<div class="chart-controls">
|
|
<label>Time Window: </label>
|
|
<select onchange="changeTimeWindow(this.value)">
|
|
<option value="3600">1 Hour</option>
|
|
<option value="7200">2 Hours</option>
|
|
<option value="14400">4 Hours</option>
|
|
<option value="86400">24 Hours</option>
|
|
</select>
|
|
</div>
|
|
<div class="chart-container">
|
|
<canvas id="hashrateChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>👥 Connected Miners</h2>
|
|
<table>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Worker</th>
|
|
<th>Connected</th>
|
|
<th>Last Share</th>
|
|
</tr>
|
|
"""
|
|
|
|
for miner in stats.get('all_miners', []):
|
|
user, worker, created, last_share = miner
|
|
html += f"""
|
|
<tr>
|
|
<td>{user}</td>
|
|
<td>{worker}</td>
|
|
<td>{created}</td>
|
|
<td>{last_share or 'Never'}</td>
|
|
</tr>
|
|
"""
|
|
|
|
if not stats.get('all_miners', []):
|
|
html += """
|
|
<tr>
|
|
<td colspan="4" style="text-align: center; color: #7f8c8d;">No miners connected</td>
|
|
</tr>
|
|
"""
|
|
|
|
html += """
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>🏆 Top Miners (24h Shares)</h2>
|
|
<table>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Worker</th>
|
|
<th>Shares</th>
|
|
<th>Hashrate</th>
|
|
<th>Last Share</th>
|
|
</tr>
|
|
"""
|
|
|
|
for miner in stats.get('miner_hashrates', []):
|
|
user, worker, shares, hashrate, last_share = miner
|
|
html += f"""
|
|
<tr>
|
|
<td>{user}</td>
|
|
<td>{worker}</td>
|
|
<td>{shares:,}</td>
|
|
<td>{self.format_hashrate(hashrate)}</td>
|
|
<td>{last_share or 'Never'}</td>
|
|
</tr>
|
|
"""
|
|
|
|
if not stats.get('top_miners', []):
|
|
html += """
|
|
<tr>
|
|
<td colspan="4" style="text-align: center; color: #7f8c8d;">No shares submitted yet</td>
|
|
</tr>
|
|
"""
|
|
|
|
html += """
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>🏆 Recent Blocks</h2>
|
|
<table>
|
|
<tr>
|
|
<th>Height</th>
|
|
<th>Hash</th>
|
|
<th>Reward</th>
|
|
<th>Found At</th>
|
|
</tr>
|
|
"""
|
|
|
|
for block in stats.get('recent_blocks', []):
|
|
block_hash, height, reward, found_at = block
|
|
html += f"""
|
|
<tr>
|
|
<td>{height}</td>
|
|
<td class="block-hash">{block_hash[:16]}...</td>
|
|
<td>{reward:.8f} RIN</td>
|
|
<td>{found_at}</td>
|
|
</tr>
|
|
"""
|
|
|
|
html += """
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>🔗 Connect to Pool</h2>
|
|
<p>Use any RinHash-compatible miner:</p>
|
|
<pre><code>./cpuminer -a rinhash -o stratum+tcp://YOUR_IP:3333 -u username.workername -p x</code></pre>
|
|
<p><strong>Replace YOUR_IP with your server's public IP address</strong></p>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script>
|
|
// Historical data for chart
|
|
const historicalData = {json.dumps([{
|
|
'time': row[0],
|
|
'shares': row[1],
|
|
'difficulty': row[2] or 0
|
|
} for row in stats.get('historical_data', [])])};
|
|
|
|
// Create hashrate chart
|
|
const ctx = document.getElementById('hashrateChart').getContext('2d');
|
|
const chart = new Chart(ctx, {{
|
|
type: 'line',
|
|
data: {{
|
|
labels: historicalData.map(d => d.time).reverse(),
|
|
datasets: [{{
|
|
label: 'Hashrate (H/s)',
|
|
data: historicalData.map(d => (d.shares / 60.0) * 0.001).reverse(),
|
|
borderColor: '#3498db',
|
|
backgroundColor: 'rgba(52, 152, 219, 0.1)',
|
|
tension: 0.4
|
|
}}]
|
|
}},
|
|
options: {{
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {{
|
|
y: {{
|
|
beginAtZero: true,
|
|
title: {{
|
|
display: true,
|
|
text: 'Hashrate (H/s)'
|
|
}}
|
|
}},
|
|
x: {{
|
|
title: {{
|
|
display: true,
|
|
text: 'Time'
|
|
}}
|
|
}}
|
|
}}
|
|
}}
|
|
}});
|
|
|
|
function changeTimeWindow(seconds) {{
|
|
// Reload page with new time window
|
|
const url = new URL(window.location);
|
|
url.searchParams.set('window', seconds);
|
|
window.location.href = url.toString();
|
|
}}
|
|
</script>
|
|
|
|
<script>
|
|
// Auto-refresh every 30 seconds
|
|
setTimeout(() => location.reload(), 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
return html
|
|
|
|
class PoolWebHandler(BaseHTTPRequestHandler):
|
|
def __init__(self, *args, pool_interface=None, **kwargs):
|
|
self.pool_interface = pool_interface
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def do_GET(self):
|
|
if self.path == '/':
|
|
stats = self.pool_interface.get_pool_stats()
|
|
html = self.pool_interface.generate_html(stats)
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode('utf-8'))
|
|
elif self.path == '/api/stats':
|
|
stats = self.pool_interface.get_pool_stats()
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(stats).encode('utf-8'))
|
|
else:
|
|
self.send_response(404)
|
|
self.end_headers()
|
|
self.wfile.write(b'Not Found')
|
|
|
|
def log_message(self, format, *args):
|
|
# Suppress access logs
|
|
pass
|
|
|
|
def start_web_interface(pool_db, host='0.0.0.0', port=8083, rpc_host='127.0.0.1', rpc_port=9556,
|
|
rpc_user='rinrpc', rpc_password='745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90'):
|
|
"""Start the web interface server"""
|
|
interface = PoolWebInterface(pool_db, host, port, rpc_host, rpc_port, rpc_user, rpc_password)
|
|
|
|
class Handler(PoolWebHandler):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, pool_interface=interface, **kwargs)
|
|
|
|
try:
|
|
server = HTTPServer((host, port), Handler)
|
|
print(f"🌐 Web interface running on http://{host}:{port}")
|
|
print("Press Ctrl+C to stop")
|
|
|
|
server.serve_forever()
|
|
except OSError as e:
|
|
if "Address already in use" in str(e):
|
|
print(f"⚠️ Port {port} is already in use, web interface not started")
|
|
print(f"💡 Try a different port or kill the process using port {port}")
|
|
else:
|
|
print(f"❌ Failed to start web interface: {e}")
|
|
except KeyboardInterrupt:
|
|
print("\n🛑 Shutting down web interface...")
|
|
server.shutdown()
|
|
|
|
if __name__ == "__main__":
|
|
# This would be called from the main pool server
|
|
print("Web interface module loaded")
|