#!/usr/bin/env python3 # Description: Monero (XMR) Mining Script - Optimized for CPU mining with support for solo and pool mining # Usage: python3 miner-monero.py # Author: Justin Oros # Source: https://github.com/JustinOros import json import subprocess import sys import os import time import signal import platform import urllib.request import tarfile import re from pathlib import Path from typing import Dict, Optional, Tuple class MoneroMiner: def __init__(self, config_file: str = "miner-monero.json"): self.config_file = config_file self.script_dir = os.path.dirname(os.path.abspath(__file__)) self.config = None self.process: Optional[subprocess.Popen] = None def detect_os(self) -> str: """Detect the operating system""" system = platform.system() if system == 'Darwin': return 'macos' elif system == 'Linux': return 'linux' elif system == 'Windows': return 'windows' else: return 'unknown' def detect_cpu_arch(self) -> str: """Detect CPU architecture""" machine = platform.machine().lower() if machine in ['arm64', 'aarch64']: return 'arm64' elif machine in ['x86_64', 'amd64', 'x64']: return 'x64' else: return machine def get_latest_xmrig_version(self) -> Optional[str]: """Fetch the latest XMRig version from GitHub""" try: url = "https://api.github.com/repos/xmrig/xmrig/releases/latest" req = urllib.request.Request(url) req.add_header('User-Agent', 'Mozilla/5.0') with urllib.request.urlopen(req, timeout=10) as response: data = json.loads(response.read().decode()) return data['tag_name'].lstrip('v') except Exception as e: print(f"Error fetching latest version: {e}") return None def check_xmrig_installed(self) -> bool: """Check if xmrig is installed""" # Check in script directory first local_paths = [ os.path.join(self.script_dir, 'xmrig'), os.path.join(self.script_dir, 'xmrig.exe'), ] for path in local_paths: if os.path.exists(path) and os.access(path, os.X_OK): return True # Check in system PATH try: result = subprocess.run(['which', 'xmrig'], capture_output=True, text=True, timeout=5) if result.returncode == 0: return True except: pass return False def download_and_install_xmrig(self) -> bool: """Download and install XMRig based on OS and architecture""" os_type = self.detect_os() arch = self.detect_cpu_arch() print(f"\nDetected OS: {os_type}") print(f"Detected Architecture: {arch}") if os_type != 'macos': print(f"\nAutomatic installation is currently only supported for macOS.") print(f"Please install XMRig manually from: https://github.com/xmrig/xmrig/releases") return False # Get latest version print("\nFetching latest XMRig version from GitHub...") version = self.get_latest_xmrig_version() if not version: print("Could not determine latest version. Using default version 6.24.0") version = "6.24.0" print(f"Latest version: {version}") # Determine filename based on architecture if arch == 'arm64': filename = f"xmrig-{version}-macos-arm64.tar.gz" elif arch == 'x64': filename = f"xmrig-{version}-macos-x64.tar.gz" else: print(f"Unsupported architecture: {arch}") return False download_url = f"https://github.com/xmrig/xmrig/releases/download/v{version}/{filename}" print(f"\nReady to download: {filename}") print(f"URL: {download_url}") # Prompt user while True: response = input("\nDownload and install XMRig? (Y/N): ").strip().upper() if response in ['Y', 'N']: break print("Please enter Y or N") if response == 'N': print("Installation cancelled.") return False # Download print(f"\nDownloading {filename}...") local_filename = os.path.join(self.script_dir, filename) try: urllib.request.urlretrieve(download_url, local_filename) print("Download complete!") except Exception as e: print(f"Error downloading file: {e}") return False # Extract print("Extracting archive...") try: with tarfile.open(local_filename, 'r:gz') as tar: # Find the xmrig binary in the archive for member in tar.getmembers(): if member.name.endswith('/xmrig') or member.name == 'xmrig': member.name = 'xmrig' tar.extract(member, self.script_dir) break xmrig_path = os.path.join(self.script_dir, 'xmrig') # Make executable os.chmod(xmrig_path, 0o755) print(f"XMRig installed successfully at: {xmrig_path}") # Clean up os.remove(local_filename) print("Cleaned up temporary files.") return True except Exception as e: print(f"Error extracting archive: {e}") return False def prompt_wallet_address(self) -> str: """Prompt user for wallet address""" default_wallet = "85NGrygcwq36kjLaNKZevT53H4EeHRQYeje5Y3gq3GYkKsEpwKpGjqd1tGxAVEjDfxEha6SYtyNYPbmPXduYqy47Q6JwbuW" print("\n" + "="*60) print("Monero Wallet Configuration") print("="*60) print(f"\nDefault wallet address:") print(f"{default_wallet}") print("\nPress ENTER to use the default wallet, or paste your own:") wallet = input("Wallet Address: ").strip() if not wallet: wallet = default_wallet print("Using default wallet address.") else: print("Using your wallet address.") return wallet def setup_initial_config(self): """Setup initial configuration with user input""" os_type = self.detect_os() arch = self.detect_cpu_arch() is_mac = os_type == 'macos' is_arm = arch == 'arm64' # Get wallet address from user wallet_address = self.prompt_wallet_address() # Determine xmrig path xmrig_path = os.path.join(self.script_dir, 'xmrig') if is_mac: miner_executable = "./xmrig" else: miner_executable = "xmrig" config = { "wallet_address": wallet_address, "mining_mode": "pool", "miner_executable": miner_executable, "pool_address": "pool.supportxmr.com:443", "pool_password": "x", "worker_name": f"m2-miner" if is_arm else "worker1", "node_address": "127.0.0.1:18081", "threads": 0, "cpu_max_usage": 75, "huge_pages": True if not is_mac else False, "randomx_mode": "auto", "tls_enabled": True, "nicehash": False, "extra_args": [], "log_file": "monero-miner.log", "api_port": 0, "donate_level": 1 } with open(self.config_file, 'w') as f: json.dump(config, f, indent=4) print(f"\nConfiguration saved to: {self.config_file}") return config def load_config(self) -> Dict: """Load configuration from JSON file""" if not os.path.exists(self.config_file): print(f"Configuration file '{self.config_file}' not found.") print("Setting up initial configuration...") return None try: with open(self.config_file, 'r') as f: config = json.load(f) self.validate_config(config) return config except json.JSONDecodeError as e: print(f"Error: Invalid JSON in configuration file: {e}") sys.exit(1) except Exception as e: print(f"Error loading configuration: {e}") sys.exit(1) def validate_config(self, config: Dict): """Validate required configuration fields""" required_fields = ['wallet_address', 'mining_mode'] for field in required_fields: if field not in config: raise ValueError(f"Missing required field: {field}") if config['mining_mode'] not in ['solo', 'pool']: raise ValueError("mining_mode must be 'solo' or 'pool'") if config['mining_mode'] == 'pool' and 'pool_address' not in config: raise ValueError("pool_address is required for pool mining") def initialize(self): """Initialize the miner - check for xmrig and setup config""" # Check if xmrig is installed xmrig_was_just_installed = False if not self.check_xmrig_installed(): print("\n" + "="*60) print("XMRig Not Detected") print("="*60) print("\nXMRig mining software is not installed.") # Try to install if not self.download_and_install_xmrig(): print("\nPlease install XMRig manually and try again.") sys.exit(1) xmrig_was_just_installed = True else: print("\nXMRig detected ✓") # Load or create config self.config = self.load_config() # If config doesn't exist OR xmrig was just installed, setup new config with wallet prompt if self.config is None or xmrig_was_just_installed: self.config = self.setup_initial_config() def build_command(self) -> list: """Build the XMRig mining command based on configuration""" cmd = [self.config.get('miner_executable', 'xmrig')] # Add wallet address wallet = self.config['wallet_address'] # Configure based on mining mode if self.config['mining_mode'] == 'solo': # Solo mining - connect to local node node_addr = self.config.get('node_address', '127.0.0.1:18081') cmd.extend(['--daemon', '--daemon-poll-interval=1000']) cmd.extend(['--url', node_addr]) cmd.extend(['--user', wallet]) print(f"Solo mining mode - connecting to node at {node_addr}") else: # Pool mining pool_addr = self.config['pool_address'] cmd.extend(['--url', pool_addr]) cmd.extend(['--user', wallet]) # Add worker name if specified worker = self.config.get('worker_name') if worker: cmd.extend(['--rig-id', worker]) # Pool password pool_pass = self.config.get('pool_password', 'x') cmd.extend(['--pass', pool_pass]) # TLS if self.config.get('tls_enabled', True): cmd.append('--tls') # NiceHash mode if self.config.get('nicehash', False): cmd.append('--nicehash') print(f"Pool mining mode - connecting to pool at {pool_addr}") # CPU Thread configuration threads = self.config.get('threads', 0) if threads > 0: cmd.extend(['--threads', str(threads)]) # CPU max usage cpu_max = self.config.get('cpu_max_usage') if cpu_max: cmd.extend(['--cpu-max-threads-hint', str(cpu_max)]) # Huge pages (not supported on macOS) if self.config.get('huge_pages', False) and platform.system() != 'Darwin': cmd.append('--hugepages') # RandomX mode rx_mode = self.config.get('randomx_mode', 'auto') if rx_mode in ['light', 'fast']: cmd.extend(['--randomx-mode', rx_mode]) # API port for monitoring api_port = self.config.get('api_port', 0) if api_port > 0: cmd.extend(['--http-port', str(api_port)]) cmd.append('--http-enabled') # Donate level donate = self.config.get('donate_level', 1) cmd.extend(['--donate-level', str(donate)]) # Add any extra arguments extra_args = self.config.get('extra_args', []) if extra_args: cmd.extend(extra_args) return cmd def start_mining(self): """Start the mining process""" cmd = self.build_command() print("\n" + "="*60) print("Monero Miner Starting") print("="*60) print(f"Wallet: {self.config['wallet_address'][:20]}...{self.config['wallet_address'][-10:]}") print(f"Mode: {self.config['mining_mode'].upper()}") print(f"Platform: {platform.system()} ({platform.machine()})") print(f"Command: {' '.join(cmd)}") print("="*60 + "\n") # Setup logging if specified log_file = self.config.get('log_file') log_handle = None try: if log_file: log_handle = open(log_file, 'a') log_handle.write(f"\n{'='*60}\n") log_handle.write(f"Mining started at {time.strftime('%Y-%m-%d %H:%M:%S')}\n") log_handle.write(f"Command: {' '.join(cmd)}\n") log_handle.write(f"{'='*60}\n\n") log_handle.flush() # Start the miner process self.process = subprocess.Popen( cmd, stdout=subprocess.PIPE if log_handle else None, stderr=subprocess.STDOUT if log_handle else None, universal_newlines=True, bufsize=1 ) # Monitor output if logging if log_handle and self.process.stdout: try: for line in self.process.stdout: print(line, end='') log_handle.write(line) log_handle.flush() except KeyboardInterrupt: print("\n\nReceived interrupt signal. Stopping miner...") self.stop_mining() else: # Wait for process to complete self.process.wait() except FileNotFoundError: print(f"\nError: Miner executable '{cmd[0]}' not found.") print("\nPlease ensure XMRig is properly installed.") sys.exit(1) except Exception as e: print(f"Error starting miner: {e}") sys.exit(1) finally: if log_handle: log_handle.close() def stop_mining(self): """Stop the mining process""" if self.process: print("Stopping miner...") self.process.terminate() try: self.process.wait(timeout=10) except subprocess.TimeoutExpired: print("Force killing miner process...") self.process.kill() print("Miner stopped.") def show_stats(self): """Display mining statistics if API is enabled""" api_port = self.config.get('api_port', 0) if api_port > 0: print(f"\nMining stats available at: http://127.0.0.1:{api_port}") print("You can view real-time statistics in your browser") def signal_handler(signum, frame): """Handle interrupt signals""" print("\n\nShutdown signal received...") sys.exit(0) def main(): # Register signal handlers signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) # Parse command line arguments config_file = "miner-monero.json" if len(sys.argv) > 1: config_file = sys.argv[1] # Create miner instance miner = MoneroMiner(config_file) try: # Initialize (check xmrig, setup config) miner.initialize() # Show stats info and start mining miner.show_stats() miner.start_mining() except KeyboardInterrupt: print("\n\nInterrupted by user") miner.stop_mining() except Exception as e: print(f"\nError: {e}") if miner.process: miner.stop_mining() sys.exit(1) if __name__ == "__main__": main()