530 lines
21 KiB
Python
530 lines
21 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")
|