567 lines
21 KiB
Python
567 lines
21 KiB
Python
#!/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_release(self) -> Optional[Tuple[str, list]]:
|
|
"""Fetch the latest XMRig version and available files 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())
|
|
version = data['tag_name'].lstrip('v')
|
|
|
|
# Extract list of asset filenames
|
|
assets = [asset['name'] for asset in data.get('assets', [])]
|
|
|
|
return version, assets
|
|
except Exception as e:
|
|
print(f"Error fetching latest release info: {e}")
|
|
return None
|
|
|
|
def find_matching_file(self, assets: list, os_type: str, arch: str) -> Optional[str]:
|
|
"""Find the matching file for the given OS and architecture"""
|
|
# Build search patterns based on OS and architecture
|
|
if os_type == 'macos':
|
|
if arch == 'arm64':
|
|
patterns = ['macos-arm64.tar.gz', 'macos-aarch64.tar.gz']
|
|
elif arch == 'x64':
|
|
patterns = ['macos-x64.tar.gz', 'macos-x86_64.tar.gz']
|
|
else:
|
|
return None
|
|
elif os_type == 'linux':
|
|
if arch == 'arm64':
|
|
patterns = ['linux-static-arm64.tar.gz', 'linux-arm64.tar.gz', 'linux-aarch64.tar.gz']
|
|
elif arch == 'x64':
|
|
patterns = ['linux-static-x64.tar.gz', 'linux-x64.tar.gz', 'linux-x86_64.tar.gz']
|
|
else:
|
|
return None
|
|
else:
|
|
return None
|
|
|
|
# Search for matching file
|
|
for asset in assets:
|
|
for pattern in patterns:
|
|
if pattern in asset.lower():
|
|
return asset
|
|
|
|
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 not in ['macos', 'linux']:
|
|
print(f"\nAutomatic installation is currently only supported for macOS and Linux.")
|
|
print(f"Please install XMRig manually from: https://github.com/xmrig/xmrig/releases")
|
|
return False
|
|
|
|
# Get latest release info
|
|
print("\nFetching latest XMRig release from GitHub...")
|
|
release_info = self.get_latest_xmrig_release()
|
|
|
|
if not release_info:
|
|
print("Could not fetch release information from GitHub.")
|
|
print("Please install XMRig manually from: https://github.com/xmrig/xmrig/releases")
|
|
return False
|
|
|
|
version, assets = release_info
|
|
print(f"Latest version: {version}")
|
|
print(f"Found {len(assets)} release files")
|
|
|
|
# Find matching file for this OS and architecture
|
|
filename = self.find_matching_file(assets, os_type, arch)
|
|
|
|
if not filename:
|
|
print(f"\nCould not find a compatible XMRig release for {os_type} {arch}")
|
|
print(f"Available files:")
|
|
for asset in assets:
|
|
if asset.endswith('.tar.gz'):
|
|
print(f" - {asset}")
|
|
print("\nPlease download manually from: https://github.com/xmrig/xmrig/releases")
|
|
return False
|
|
|
|
print(f"Found matching file: {filename}")
|
|
|
|
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 with retry logic
|
|
print(f"\nDownloading {filename}...")
|
|
local_filename = os.path.join(self.script_dir, filename)
|
|
|
|
max_retries = 3
|
|
for attempt in range(max_retries):
|
|
try:
|
|
req = urllib.request.Request(download_url)
|
|
req.add_header('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36')
|
|
req.add_header('Accept', '*/*')
|
|
|
|
with urllib.request.urlopen(req, timeout=30) as response:
|
|
total_size = int(response.headers.get('Content-Length', 0))
|
|
downloaded = 0
|
|
|
|
with open(local_filename, 'wb') as f:
|
|
while True:
|
|
chunk = response.read(8192)
|
|
if not chunk:
|
|
break
|
|
f.write(chunk)
|
|
downloaded += len(chunk)
|
|
|
|
if total_size > 0:
|
|
percent = (downloaded / total_size) * 100
|
|
print(f"\rProgress: {percent:.1f}% ({downloaded}/{total_size} bytes)", end='', flush=True)
|
|
|
|
print("\nDownload complete!")
|
|
break
|
|
|
|
except urllib.error.HTTPError as e:
|
|
print(f"\nHTTP Error {e.code}: {e.reason}")
|
|
if attempt < max_retries - 1:
|
|
print(f"Retrying... (Attempt {attempt + 2}/{max_retries})")
|
|
time.sleep(2)
|
|
else:
|
|
print(f"\nFailed to download after {max_retries} attempts.")
|
|
print(f"Please download manually from: {download_url}")
|
|
return False
|
|
except Exception as e:
|
|
print(f"\nError downloading file: {e}")
|
|
if attempt < max_retries - 1:
|
|
print(f"Retrying... (Attempt {attempt + 2}/{max_retries})")
|
|
time.sleep(2)
|
|
else:
|
|
print(f"\nFailed to download after {max_retries} attempts.")
|
|
print(f"Please download manually from: {download_url}")
|
|
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 - use ./xmrig for both macOS and Linux
|
|
if os_type in ['macos', 'linux']:
|
|
miner_executable = "./xmrig"
|
|
else:
|
|
miner_executable = "xmrig.exe" # Windows
|
|
|
|
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()
|