diff --git a/mining_pool.py b/mining_pool.py new file mode 100644 index 00000000..f008c597 --- /dev/null +++ b/mining_pool.py @@ -0,0 +1,506 @@ +// SPDX-License-Identifier: MIT +# SPDX-License-Identifier: MIT + +import sqlite3 +import hashlib +import time +import json +import logging +from flask import Flask, request, jsonify, render_template_string +from datetime import datetime, timedelta +import threading +from contextlib import contextmanager + +app = Flask(__name__) +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +DB_PATH = 'mining_pool.db' +POOL_FEE = 0.02 # 2% pool fee +MIN_PAYOUT = 1.0 # Minimum payout threshold +DIFFICULTY_TARGET = 0x00000fffff000000000000000000000000000000000000000000000000000000 + +@contextmanager +def get_db(): + conn = sqlite3.connect(DB_PATH) + try: + yield conn + finally: + conn.close() + +def init_db(): + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS miners ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + wallet_address TEXT UNIQUE NOT NULL, + nickname TEXT, + registered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + total_shares INTEGER DEFAULT 0, + total_rewards REAL DEFAULT 0.0, + pending_balance REAL DEFAULT 0.0, + status TEXT DEFAULT 'active' + ) + ''') + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS shares ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + miner_id INTEGER, + block_hash TEXT, + nonce TEXT, + difficulty REAL, + submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_valid BOOLEAN DEFAULT 0, + is_block BOOLEAN DEFAULT 0, + reward_amount REAL DEFAULT 0.0, + FOREIGN KEY (miner_id) REFERENCES miners (id) + ) + ''') + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS payouts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + miner_id INTEGER, + amount REAL, + txn_hash TEXT, + status TEXT DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + processed_at TIMESTAMP, + FOREIGN KEY (miner_id) REFERENCES miners (id) + ) + ''') + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS pool_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + total_hashrate REAL DEFAULT 0.0, + active_miners INTEGER DEFAULT 0, + blocks_found INTEGER DEFAULT 0, + total_rewards REAL DEFAULT 0.0, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + conn.commit() + +def validate_share(block_hash, nonce, difficulty): + """Validate submitted mining share""" + try: + hash_input = f"{block_hash}{nonce}".encode() + result_hash = hashlib.sha256(hash_input).hexdigest() + + # Convert to integer for comparison + hash_int = int(result_hash, 16) + target = int(difficulty) + + return hash_int < target + except Exception as e: + logger.error(f"Share validation error: {e}") + return False + +def calculate_reward_distribution(): + """Calculate and distribute rewards based on shares""" + with get_db() as conn: + cursor = conn.cursor() + + # Get recent shares (last 24 hours) + cursor.execute(''' + SELECT m.id, m.wallet_address, COUNT(s.id) as share_count + FROM miners m + JOIN shares s ON m.id = s.miner_id + WHERE s.submitted_at > datetime('now', '-24 hours') + AND s.is_valid = 1 + GROUP BY m.id, m.wallet_address + ''') + + share_data = cursor.fetchall() + if not share_data: + return + + total_shares = sum(row[2] for row in share_data) + + # Get pending block rewards + cursor.execute(''' + SELECT SUM(reward_amount) FROM shares + WHERE is_block = 1 AND reward_amount > 0 + AND submitted_at > datetime('now', '-24 hours') + ''') + + result = cursor.fetchone() + total_reward = result[0] if result[0] else 0.0 + + if total_reward > 0: + pool_fee_amount = total_reward * POOL_FEE + miner_reward = total_reward - pool_fee_amount + + # Distribute rewards proportionally + for miner_id, wallet, share_count in share_data: + miner_portion = (share_count / total_shares) * miner_reward + + cursor.execute(''' + UPDATE miners + SET pending_balance = pending_balance + ?, + total_rewards = total_rewards + ? + WHERE id = ? + ''', (miner_portion, miner_portion, miner_id)) + + conn.commit() + logger.info(f"Distributed {miner_reward} RTC among {len(share_data)} miners") + +@app.route('/') +def dashboard(): + template = ''' + + + + RustChain Mining Pool + + + +
+

⛏️ RustChain Mining Pool

+ +
+
+

Pool Stats

+

Active Miners: {{ stats.active_miners }}

+

Total Hashrate: {{ "%.2f"|format(stats.total_hashrate) }} H/s

+

Blocks Found: {{ stats.blocks_found }}

+

Total Rewards: {{ "%.4f"|format(stats.total_rewards) }} RTC

+
+
+ +

Top Miners (24h)

+ + + + + + + + + + + + {% for miner in miners %} + + + + + + + + {% endfor %} + +
WalletNicknameSharesPending BalanceLast Active
{{ miner[1][:12] }}...{{ miner[2] or 'Anonymous' }}{{ miner[6] }}{{ "%.4f"|format(miner[7]) }} RTC{{ miner[4] }}
+ +

API Endpoints

+ +
+ + + ''' + + with get_db() as conn: + cursor = conn.cursor() + + # Get pool stats + cursor.execute(''' + SELECT active_miners, total_hashrate, blocks_found, total_rewards + FROM pool_stats ORDER BY updated_at DESC LIMIT 1 + ''') + stats_row = cursor.fetchone() + stats = { + 'active_miners': stats_row[0] if stats_row else 0, + 'total_hashrate': stats_row[1] if stats_row else 0.0, + 'blocks_found': stats_row[2] if stats_row else 0, + 'total_rewards': stats_row[3] if stats_row else 0.0 + } + + # Get top miners + cursor.execute(''' + SELECT m.*, COUNT(s.id) as recent_shares, m.pending_balance + FROM miners m + LEFT JOIN shares s ON m.id = s.miner_id + AND s.submitted_at > datetime('now', '-24 hours') + GROUP BY m.id + ORDER BY recent_shares DESC, m.total_shares DESC + LIMIT 10 + ''') + miners = cursor.fetchall() + + return render_template_string(template, stats=stats, miners=miners) + +@app.route('/register', methods=['POST']) +def register_miner(): + """Register new miner in pool""" + data = request.get_json() + if not data or 'wallet_address' not in data: + return jsonify({'error': 'wallet_address required'}), 400 + + wallet_address = data['wallet_address'] + nickname = data.get('nickname', '') + + with get_db() as conn: + cursor = conn.cursor() + + try: + cursor.execute(''' + INSERT INTO miners (wallet_address, nickname) + VALUES (?, ?) + ''', (wallet_address, nickname)) + + miner_id = cursor.lastrowid + conn.commit() + + logger.info(f"New miner registered: {wallet_address[:12]}...") + return jsonify({ + 'success': True, + 'miner_id': miner_id, + 'message': 'Miner registered successfully' + }) + + except sqlite3.IntegrityError: + return jsonify({'error': 'Wallet already registered'}), 409 + +@app.route('/submit', methods=['POST']) +def submit_share(): + """Submit mining share for validation""" + data = request.get_json() + required_fields = ['wallet_address', 'block_hash', 'nonce', 'difficulty'] + + if not data or not all(field in data for field in required_fields): + return jsonify({'error': 'Missing required fields'}), 400 + + wallet_address = data['wallet_address'] + block_hash = data['block_hash'] + nonce = data['nonce'] + difficulty = float(data['difficulty']) + + with get_db() as conn: + cursor = conn.cursor() + + # Get miner ID + cursor.execute('SELECT id FROM miners WHERE wallet_address = ?', (wallet_address,)) + miner_row = cursor.fetchone() + + if not miner_row: + return jsonify({'error': 'Miner not registered'}), 404 + + miner_id = miner_row[0] + + # Validate share + is_valid = validate_share(block_hash, nonce, difficulty) + is_block = difficulty >= DIFFICULTY_TARGET if is_valid else False + reward_amount = 50.0 if is_block else 0.0 # Block reward + + # Store share + cursor.execute(''' + INSERT INTO shares (miner_id, block_hash, nonce, difficulty, is_valid, is_block, reward_amount) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', (miner_id, block_hash, nonce, difficulty, is_valid, is_block, reward_amount)) + + if is_valid: + # Update miner stats + cursor.execute(''' + UPDATE miners + SET total_shares = total_shares + 1, last_active = CURRENT_TIMESTAMP + WHERE id = ? + ''', (miner_id,)) + + conn.commit() + + response = { + 'valid': is_valid, + 'block_found': is_block, + 'reward': reward_amount + } + + if is_block: + logger.info(f"BLOCK FOUND by {wallet_address[:12]}... - Reward: {reward_amount} RTC") + + return jsonify(response) + +@app.route('/stats/') +def get_miner_stats(wallet_address): + """Get miner statistics""" + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute(''' + SELECT m.*, COUNT(s.id) as total_shares_db, + COUNT(CASE WHEN s.is_valid = 1 THEN 1 END) as valid_shares, + COUNT(CASE WHEN s.is_block = 1 THEN 1 END) as blocks_found + FROM miners m + LEFT JOIN shares s ON m.id = s.miner_id + WHERE m.wallet_address = ? + GROUP BY m.id + ''', (wallet_address,)) + + result = cursor.fetchone() + if not result: + return jsonify({'error': 'Miner not found'}), 404 + + # Get recent shares (24h) + cursor.execute(''' + SELECT COUNT(*) FROM shares s + JOIN miners m ON s.miner_id = m.id + WHERE m.wallet_address = ? + AND s.submitted_at > datetime('now', '-24 hours') + AND s.is_valid = 1 + ''', (wallet_address,)) + + recent_shares = cursor.fetchone()[0] + + stats = { + 'wallet_address': result[1], + 'nickname': result[2], + 'registered_at': result[3], + 'total_shares': result[5], + 'valid_shares': result[7], + 'blocks_found': result[8], + 'total_rewards': result[6], + 'pending_balance': result[7], + 'recent_shares_24h': recent_shares, + 'status': result[8] + } + + return jsonify(stats) + +@app.route('/payout', methods=['POST']) +def request_payout(): + """Process payout request""" + data = request.get_json() + if not data or 'wallet_address' not in data: + return jsonify({'error': 'wallet_address required'}), 400 + + wallet_address = data['wallet_address'] + + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute(''' + SELECT id, pending_balance FROM miners WHERE wallet_address = ? + ''', (wallet_address,)) + + result = cursor.fetchone() + if not result: + return jsonify({'error': 'Miner not found'}), 404 + + miner_id, pending_balance = result + + if pending_balance < MIN_PAYOUT: + return jsonify({ + 'error': f'Minimum payout is {MIN_PAYOUT} RTC', + 'pending_balance': pending_balance + }), 400 + + # Create payout record + payout_hash = hashlib.sha256(f"{wallet_address}{time.time()}".encode()).hexdigest() + + cursor.execute(''' + INSERT INTO payouts (miner_id, amount, txn_hash, status) + VALUES (?, ?, ?, 'processing') + ''', (miner_id, pending_balance, payout_hash)) + + # Reset pending balance + cursor.execute(''' + UPDATE miners SET pending_balance = 0.0 WHERE id = ? + ''', (miner_id,)) + + conn.commit() + + logger.info(f"Payout requested: {pending_balance} RTC to {wallet_address[:12]}...") + + return jsonify({ + 'success': True, + 'amount': pending_balance, + 'txn_hash': payout_hash, + 'status': 'processing' + }) + +def update_pool_stats(): + """Background task to update pool statistics""" + while True: + try: + with get_db() as conn: + cursor = conn.cursor() + + # Count active miners (active in last hour) + cursor.execute(''' + SELECT COUNT(*) FROM miners + WHERE last_active > datetime('now', '-1 hour') + ''') + active_miners = cursor.fetchone()[0] + + # Calculate total blocks found + cursor.execute('SELECT COUNT(*) FROM shares WHERE is_block = 1') + blocks_found = cursor.fetchone()[0] + + # Calculate total rewards distributed + cursor.execute('SELECT SUM(total_rewards) FROM miners') + result = cursor.fetchone() + total_rewards = result[0] if result[0] else 0.0 + + # Estimate hashrate based on recent shares + cursor.execute(''' + SELECT COUNT(*) FROM shares + WHERE submitted_at > datetime('now', '-10 minutes') + AND is_valid = 1 + ''') + recent_shares = cursor.fetchone()[0] + estimated_hashrate = recent_shares * 6 # Rough estimate + + # Update or insert stats + cursor.execute(''' + INSERT OR REPLACE INTO pool_stats + (id, active_miners, total_hashrate, blocks_found, total_rewards, updated_at) + VALUES (1, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ''', (active_miners, estimated_hashrate, blocks_found, total_rewards)) + + conn.commit() + + except Exception as e: + logger.error(f"Stats update error: {e}") + + time.sleep(60) # Update every minute + +if __name__ == '__main__': + init_db() + + # Start background stats updater + stats_thread = threading.Thread(target=update_pool_stats, daemon=True) + stats_thread.start() + + # Start reward distribution scheduler + def reward_scheduler(): + while True: + try: + calculate_reward_distribution() + time.sleep(3600) # Run every hour + except Exception as e: + logger.error(f"Reward distribution error: {e}") + time.sleep(300) # Retry in 5 minutes + + reward_thread = threading.Thread(target=reward_scheduler, daemon=True) + reward_thread.start() + + logger.info("Mining pool server starting...") + app.run(host='0.0.0.0', port=8080, debug=False) diff --git a/pool_cli.py b/pool_cli.py new file mode 100644 index 00000000..4f3adf65 --- /dev/null +++ b/pool_cli.py @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: MIT +# SPDX-License-Identifier: MIT + +import sqlite3 +import argparse +import sys +import json +from datetime import datetime, timedelta +from typing import Dict, List, Optional + +DB_PATH = 'rustchain.db' + +class PoolCLI: + def __init__(self): + self.ensure_pool_tables() + + def ensure_pool_tables(self): + """Initialize pool-related database tables if they don't exist""" + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + + # Pool configuration table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS pool_config ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Pool statistics table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS pool_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + total_miners INTEGER DEFAULT 0, + active_miners INTEGER DEFAULT 0, + total_hashrate REAL DEFAULT 0.0, + blocks_found INTEGER DEFAULT 0, + total_shares INTEGER DEFAULT 0, + pool_fee REAL DEFAULT 0.0 + ) + ''') + + # Miner shares tracking + cursor.execute(''' + CREATE TABLE IF NOT EXISTS miner_shares ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + miner_address TEXT NOT NULL, + shares INTEGER DEFAULT 0, + difficulty REAL DEFAULT 0.0, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + block_height INTEGER DEFAULT 0 + ) + ''') + + # Payout history + cursor.execute(''' + CREATE TABLE IF NOT EXISTS payout_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + miner_address TEXT NOT NULL, + amount REAL NOT NULL, + transaction_hash TEXT, + status TEXT DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + processed_at TIMESTAMP + ) + ''') + + conn.commit() + + def set_config(self, key: str, value: str): + """Set pool configuration value""" + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT OR REPLACE INTO pool_config (key, value, updated_at) + VALUES (?, ?, ?) + ''', (key, value, datetime.now().isoformat())) + conn.commit() + print(f"Set {key} = {value}") + + def get_config(self, key: str) -> Optional[str]: + """Get pool configuration value""" + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute('SELECT value FROM pool_config WHERE key = ?', (key,)) + result = cursor.fetchone() + return result[0] if result else None + + def show_config(self): + """Display all pool configuration""" + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute('SELECT key, value, updated_at FROM pool_config ORDER BY key') + configs = cursor.fetchall() + + if not configs: + print("No configuration found") + return + + print("Pool Configuration:") + print("-" * 50) + for key, value, updated in configs: + print(f"{key:<20}: {value} (updated: {updated})") + + def show_stats(self, hours: int = 24): + """Show pool statistics for the last N hours""" + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + + # Get latest stats + cursor.execute(''' + SELECT * FROM pool_stats + ORDER BY timestamp DESC LIMIT 1 + ''') + latest = cursor.fetchone() + + if not latest: + print("No pool statistics available") + return + + # Get stats from N hours ago + cutoff = datetime.now() - timedelta(hours=hours) + cursor.execute(''' + SELECT AVG(total_hashrate), SUM(blocks_found), COUNT(*) as entries + FROM pool_stats + WHERE timestamp >= ? + ''', (cutoff.isoformat(),)) + period_stats = cursor.fetchone() + + print(f"Pool Statistics (last {hours} hours):") + print("-" * 40) + print(f"Active Miners : {latest[3]}") + print(f"Total Miners : {latest[2]}") + print(f"Current Hashrate : {latest[4]:.2f} H/s") + print(f"Average Hashrate : {period_stats[0]:.2f} H/s" if period_stats[0] else "0.00 H/s") + print(f"Blocks Found : {period_stats[1] or 0}") + print(f"Pool Fee : {latest[6]:.2f}%") + print(f"Total Shares : {latest[5]}") + + def show_miners(self, limit: int = 20): + """Show top miners by shares""" + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + + cursor.execute(''' + SELECT miner_address, SUM(shares) as total_shares, + COUNT(*) as submissions, MAX(timestamp) as last_seen + FROM miner_shares + GROUP BY miner_address + ORDER BY total_shares DESC + LIMIT ? + ''', (limit,)) + + miners = cursor.fetchall() + + if not miners: + print("No miners found") + return + + print(f"Top {limit} Miners:") + print("-" * 80) + print(f"{'Address':<42} {'Shares':<10} {'Submissions':<12} {'Last Seen':<15}") + print("-" * 80) + + for address, shares, subs, last_seen in miners: + short_addr = f"{address[:8]}...{address[-8:]}" if len(address) > 20 else address + print(f"{short_addr:<42} {shares:<10} {subs:<12} {last_seen[:10]:<15}") + + def process_payouts(self, min_amount: float = 1.0, dry_run: bool = False): + """Process pending payouts for miners""" + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + + # Calculate earnings per miner based on shares + cursor.execute(''' + SELECT miner_address, SUM(shares) as total_shares + FROM miner_shares + WHERE timestamp >= datetime('now', '-24 hours') + GROUP BY miner_address + HAVING total_shares > 0 + ''') + + miner_shares = cursor.fetchall() + + if not miner_shares: + print("No shares found for payout calculation") + return + + # Get pool fee + pool_fee = float(self.get_config('pool_fee') or '2.5') + + # Mock reward calculation (in a real pool, this would come from found blocks) + total_reward = 10.0 # Example: 10 RTC found in last 24h + total_shares = sum(shares for _, shares in miner_shares) + + payouts_processed = 0 + total_paid = 0.0 + + print("Processing Payouts:") + print("-" * 60) + + for miner_addr, shares in miner_shares: + share_percent = shares / total_shares + gross_amount = total_reward * share_percent + net_amount = gross_amount * (1 - pool_fee / 100) + + if net_amount >= min_amount: + if not dry_run: + # Record payout in database + cursor.execute(''' + INSERT INTO payout_history + (miner_address, amount, status, created_at) + VALUES (?, ?, 'pending', ?) + ''', (miner_addr, net_amount, datetime.now().isoformat())) + + short_addr = f"{miner_addr[:8]}...{miner_addr[-8:]}" + print(f"{short_addr} - {net_amount:.6f} RTC ({'DRY RUN' if dry_run else 'QUEUED'})") + payouts_processed += 1 + total_paid += net_amount + + if not dry_run: + conn.commit() + + print("-" * 60) + print(f"Payouts processed: {payouts_processed}") + print(f"Total amount: {total_paid:.6f} RTC") + print(f"Pool fee collected: {total_reward * (pool_fee / 100):.6f} RTC") + + def show_payouts(self, limit: int = 20): + """Show recent payout history""" + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + + cursor.execute(''' + SELECT miner_address, amount, status, created_at, processed_at + FROM payout_history + ORDER BY created_at DESC + LIMIT ? + ''', (limit,)) + + payouts = cursor.fetchall() + + if not payouts: + print("No payout history found") + return + + print(f"Recent Payouts (last {limit}):") + print("-" * 80) + print(f"{'Address':<42} {'Amount':<12} {'Status':<10} {'Created':<15}") + print("-" * 80) + + for addr, amount, status, created, processed in payouts: + short_addr = f"{addr[:8]}...{addr[-8:]}" if len(addr) > 20 else addr + print(f"{short_addr:<42} {amount:<12.6f} {status:<10} {created[:10]:<15}") + +def main(): + parser = argparse.ArgumentParser(description='RustChain Mining Pool CLI') + subparsers = parser.add_subparsers(dest='command', help='Available commands') + + # Config commands + config_parser = subparsers.add_parser('config', help='Manage pool configuration') + config_subparsers = config_parser.add_subparsers(dest='config_action') + + set_parser = config_subparsers.add_parser('set', help='Set configuration value') + set_parser.add_argument('key', help='Configuration key') + set_parser.add_argument('value', help='Configuration value') + + config_subparsers.add_parser('show', help='Show all configuration') + + # Stats command + stats_parser = subparsers.add_parser('stats', help='Show pool statistics') + stats_parser.add_argument('--hours', type=int, default=24, help='Hours to look back (default: 24)') + + # Miners command + miners_parser = subparsers.add_parser('miners', help='Show miner information') + miners_parser.add_argument('--limit', type=int, default=20, help='Number of miners to show (default: 20)') + + # Payout commands + payout_parser = subparsers.add_parser('payout', help='Manage payouts') + payout_subparsers = payout_parser.add_subparsers(dest='payout_action') + + process_parser = payout_subparsers.add_parser('process', help='Process pending payouts') + process_parser.add_argument('--min-amount', type=float, default=1.0, help='Minimum payout amount (default: 1.0)') + process_parser.add_argument('--dry-run', action='store_true', help='Show what would be paid without processing') + + history_parser = payout_subparsers.add_parser('history', help='Show payout history') + history_parser.add_argument('--limit', type=int, default=20, help='Number of payouts to show (default: 20)') + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return + + cli = PoolCLI() + + try: + if args.command == 'config': + if args.config_action == 'set': + cli.set_config(args.key, args.value) + elif args.config_action == 'show': + cli.show_config() + else: + config_parser.print_help() + + elif args.command == 'stats': + cli.show_stats(args.hours) + + elif args.command == 'miners': + cli.show_miners(args.limit) + + elif args.command == 'payout': + if args.payout_action == 'process': + cli.process_payouts(args.min_amount, args.dry_run) + elif args.payout_action == 'history': + cli.show_payouts(args.limit) + else: + payout_parser.print_help() + + else: + parser.print_help() + + except Exception as e: + print(f"Error: {e}") + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/pool_web_interface.py b/pool_web_interface.py new file mode 100644 index 00000000..5850835c --- /dev/null +++ b/pool_web_interface.py @@ -0,0 +1,559 @@ +// SPDX-License-Identifier: MIT +# SPDX-License-Identifier: MIT + +from flask import Flask, request, jsonify, render_template_string +import sqlite3 +import json +import time +from datetime import datetime +import hashlib +import os + +app = Flask(__name__) + +DB_PATH = 'pool.db' + +def init_pool_db(): + """Initialize the mining pool database""" + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS miners ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + wallet_address TEXT UNIQUE NOT NULL, + alias TEXT, + registration_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_seen TIMESTAMP, + total_shares INTEGER DEFAULT 0, + total_earnings REAL DEFAULT 0.0, + status TEXT DEFAULT 'active' + ) + ''') + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS shares ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + miner_id INTEGER, + block_hash TEXT NOT NULL, + difficulty INTEGER, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + valid BOOLEAN DEFAULT 1, + FOREIGN KEY (miner_id) REFERENCES miners (id) + ) + ''') + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS payouts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + miner_id INTEGER, + amount REAL NOT NULL, + transaction_hash TEXT, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status TEXT DEFAULT 'pending', + FOREIGN KEY (miner_id) REFERENCES miners (id) + ) + ''') + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS pool_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + total_hashrate REAL DEFAULT 0.0, + active_miners INTEGER DEFAULT 0, + blocks_found INTEGER DEFAULT 0, + last_block_time TIMESTAMP, + pool_fee REAL DEFAULT 2.0, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + conn.commit() + +# HTML Templates +MAIN_TEMPLATE = ''' + + + + + + RustChain Mining Pool + + + +
+
+

🦀 RustChain Mining Pool

+

Decentralized mining for the RustChain network

+
+ + + + {{ content }} +
+ + + + +''' + +DASHBOARD_CONTENT = ''' +
+
+
{{ stats.total_hashrate or 0 }}
+
Total Hashrate (GH/s)
+
+
+
{{ stats.active_miners or 0 }}
+
Active Miners
+
+
+
{{ stats.blocks_found or 0 }}
+
Blocks Found
+
+
+
{{ stats.pool_fee or 2.0 }}%
+
Pool Fee
+
+
+ +
+

Recent Activity

+ + + + + + + + + + + {% for activity in recent_activity %} + + + + + + + {% endfor %} + +
TimeMinerEventDetails
{{ activity.timestamp }}{{ activity.miner_alias or activity.wallet_address[:12] + '...' }}{{ activity.event_type }}{{ activity.details }}
+
+''' + +MINERS_CONTENT = ''' +
+

Registered Miners

+ + + + + + + + + + + + + {% for miner in miners %} + + + + + + + + + {% endfor %} + +
AliasWallet AddressTotal SharesTotal EarningsLast SeenStatus
{{ miner.alias or 'Anonymous' }}{{ miner.wallet_address[:12] + '...' if miner.wallet_address|length > 12 else miner.wallet_address }}{{ miner.total_shares }}{{ miner.total_earnings }} RTC{{ miner.last_seen or 'Never' }}{{ miner.status }}
+
+''' + +REGISTER_CONTENT = ''' +
+

Register New Miner

+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ + +''' + +PAYOUTS_CONTENT = ''' +
+

Payout History

+ + + + + + + + + + + + {% for payout in payouts %} + + + + + + + + {% endfor %} + +
DateMinerAmountTransaction HashStatus
{{ payout.timestamp }}{{ payout.miner_alias or payout.wallet_address[:12] + '...' }}{{ payout.amount }} RTC{{ payout.transaction_hash[:16] + '...' if payout.transaction_hash else 'Pending' }}{{ payout.status.title() }}
+
+''' + +@app.route('/') +def dashboard(): + stats = get_pool_stats() + recent_activity = get_recent_activity() + content = render_template_string(DASHBOARD_CONTENT, stats=stats, recent_activity=recent_activity) + return render_template_string(MAIN_TEMPLATE, content=content) + +@app.route('/miners') +def miners_page(): + miners = get_all_miners() + content = render_template_string(MINERS_CONTENT, miners=miners) + return render_template_string(MAIN_TEMPLATE, content=content) + +@app.route('/register') +def register_page(): + content = render_template_string(REGISTER_CONTENT) + return render_template_string(MAIN_TEMPLATE, content=content) + +@app.route('/payouts') +def payouts_page(): + payouts = get_payout_history() + content = render_template_string(PAYOUTS_CONTENT, payouts=payouts) + return render_template_string(MAIN_TEMPLATE, content=content) + +@app.route('/api/register', methods=['POST']) +def register_miner(): + try: + data = request.get_json() + wallet_address = data.get('wallet_address') + alias = data.get('alias', '') + + if not wallet_address: + return jsonify({'success': False, 'error': 'Wallet address required'}) + + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO miners (wallet_address, alias, registration_time) + VALUES (?, ?, ?) + ''', (wallet_address, alias, datetime.now())) + conn.commit() + + return jsonify({'success': True, 'message': 'Miner registered successfully'}) + + except sqlite3.IntegrityError: + return jsonify({'success': False, 'error': 'Wallet address already registered'}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + +@app.route('/api/submit_share', methods=['POST']) +def submit_share(): + try: + data = request.get_json() + wallet_address = data.get('wallet_address') + block_hash = data.get('block_hash') + difficulty = data.get('difficulty', 1) + + if not wallet_address or not block_hash: + return jsonify({'success': False, 'error': 'Missing required fields'}) + + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + + # Get miner ID + cursor.execute('SELECT id FROM miners WHERE wallet_address = ?', (wallet_address,)) + miner = cursor.fetchone() + + if not miner: + return jsonify({'success': False, 'error': 'Miner not registered'}) + + miner_id = miner[0] + + # Insert share + cursor.execute(''' + INSERT INTO shares (miner_id, block_hash, difficulty, timestamp) + VALUES (?, ?, ?, ?) + ''', (miner_id, block_hash, difficulty, datetime.now())) + + # Update miner stats + cursor.execute(''' + UPDATE miners SET total_shares = total_shares + 1, last_seen = ? + WHERE id = ? + ''', (datetime.now(), miner_id)) + + conn.commit() + + return jsonify({'success': True, 'message': 'Share submitted successfully'}) + + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + +@app.route('/api/stats') +def get_stats_api(): + stats = get_pool_stats() + return jsonify({'success': True, 'stats': stats}) + +@app.route('/api/miner/') +def get_miner_stats(wallet_address): + try: + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute(''' + SELECT m.*, + COUNT(s.id) as recent_shares, + SUM(p.amount) as pending_balance + FROM miners m + LEFT JOIN shares s ON m.id = s.miner_id AND s.timestamp > datetime('now', '-24 hours') + LEFT JOIN payouts p ON m.id = p.miner_id AND p.status = 'pending' + WHERE m.wallet_address = ? + GROUP BY m.id + ''', (wallet_address,)) + + result = cursor.fetchone() + if not result: + return jsonify({'success': False, 'error': 'Miner not found'}) + + miner_data = { + 'wallet_address': result[1], + 'alias': result[2], + 'total_shares': result[5], + 'total_earnings': result[6], + 'recent_shares': result[8] or 0, + 'pending_balance': result[9] or 0.0, + 'status': result[7] + } + + return jsonify({'success': True, 'miner': miner_data}) + + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + +def get_pool_stats(): + try: + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + + # Get active miners count + cursor.execute(''' + SELECT COUNT(*) FROM miners + WHERE last_seen > datetime('now', '-1 hour') + ''') + active_miners = cursor.fetchone()[0] + + # Get total blocks found + cursor.execute('SELECT COUNT(DISTINCT block_hash) FROM shares WHERE valid = 1') + blocks_found = cursor.fetchone()[0] + + # Calculate approximate hashrate (simplified) + cursor.execute(''' + SELECT COUNT(*) * 10.0 as hashrate + FROM shares + WHERE timestamp > datetime('now', '-1 hour') + ''') + hashrate_result = cursor.fetchone() + total_hashrate = hashrate_result[0] if hashrate_result else 0.0 + + return { + 'total_hashrate': round(total_hashrate, 2), + 'active_miners': active_miners, + 'blocks_found': blocks_found, + 'pool_fee': 2.0 + } + + except Exception: + return { + 'total_hashrate': 0.0, + 'active_miners': 0, + 'blocks_found': 0, + 'pool_fee': 2.0 + } + +def get_all_miners(): + try: + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute(''' + SELECT wallet_address, alias, total_shares, total_earnings, + last_seen, status + FROM miners + ORDER BY total_shares DESC + ''') + + miners = [] + for row in cursor.fetchall(): + miners.append({ + 'wallet_address': row[0], + 'alias': row[1], + 'total_shares': row[2], + 'total_earnings': row[3], + 'last_seen': row[4], + 'status': row[5] + }) + + return miners + + except Exception: + return [] + +def get_recent_activity(): + try: + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute(''' + SELECT s.timestamp, m.wallet_address, m.alias, 'Share Submitted' as event_type, + 'Difficulty: ' || s.difficulty as details + FROM shares s + JOIN miners m ON s.miner_id = m.id + ORDER BY s.timestamp DESC + LIMIT 10 + ''') + + activity = [] + for row in cursor.fetchall(): + activity.append({ + 'timestamp': row[0], + 'wallet_address': row[1], + 'miner_alias': row[2], + 'event_type': row[3], + 'details': row[4] + }) + + return activity + + except Exception: + return [] + +def get_payout_history(): + try: + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute(''' + SELECT p.timestamp, p.amount, p.transaction_hash, p.status, + m.wallet_address, m.alias + FROM payouts p + JOIN miners m ON p.miner_id = m.id + ORDER BY p.timestamp DESC + LIMIT 50 + ''') + + payouts = [] + for row in cursor.fetchall(): + payouts.append({ + 'timestamp': row[0], + 'amount': row[1], + 'transaction_hash': row[2], + 'status': row[3], + 'wallet_address': row[4], + 'miner_alias': row[5] + }) + + return payouts + + except Exception: + return [] + +if __name__ == '__main__': + if not os.path.exists(DB_PATH): + init_pool_db() + + app.run(debug=True, host='0.0.0.0', port=5000)