Download app.py from SAUSERLA/M1: direct link, hf CLI and curl.
- Browser
- Download file 37.6 kB
-
https://huggingface.co/spaces/SAUSERLA/M1/resolve/main/app.py
- Command line
-
hf download hf://spaces/SAUSERLA/M1/app.py
-
curl -L -o app.py https://huggingface.co/spaces/SAUSERLA/M1/resolve/main/app.py
37.6 kB
| """ | |
| Hugging Face Space β Solana Key Generator & Miner | |
| Simple, reliable implementation optimized for cloud deployment | |
| """ | |
| import os | |
| import queue | |
| import threading | |
| import time | |
| import base58 | |
| import json | |
| import requests | |
| import collections | |
| import hashlib | |
| import secrets | |
| from typing import List, Dict, Optional, Tuple | |
| from datetime import datetime | |
| import gradio as gr | |
| from solana.rpc.api import Client | |
| from solana.rpc.core import RPCException | |
| from solders.keypair import Keypair | |
| from solders.pubkey import Pubkey | |
| # ---------- Safe keypair generator ---------- | |
| def _safe_keypair() -> Keypair: | |
| """Generate a valid keypair using the standard method.""" | |
| # Use the standard Solana keypair generation | |
| return Keypair() | |
| # ---------- Enhanced Console Logger ---------- | |
| import logging | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") | |
| log = logging.getLogger("solana_miner") | |
| # ---------- Enhanced Configuration with Multi-Miner Support ---------- | |
| class Config: | |
| DISCORD_WEBHOOK = os.getenv("DISCORD_WEBHOOK", "") | |
| MINER_ID = os.getenv("MINER_ID", f"Miner-{secrets.token_hex(4)}") | |
| MINER_NAME = os.getenv("MINER_NAME", f"SolMiner-{secrets.token_hex(3)}") | |
| LOCATION = os.getenv("MINER_LOCATION", "Unknown") | |
| RPC_URLS = [ | |
| "https://api.mainnet-beta.solana.com", | |
| "https://solana-api.projectserum.com", | |
| "https://rpc.ankr.com/solana", | |
| "https://solana.public-rpc.com" | |
| ] | |
| current_rpc_index = 0 | |
| MIN_SOL = float(os.getenv("MIN_SOL", "0.00001")) | |
| RPC_BATCH_SIZE = int(os.getenv("RPC_BATCH_SIZE", "100")) | |
| MINER_SLEEP = float(os.getenv("MINER_SLEEP", "1.0")) | |
| MAX_RETRIES = 2 | |
| FEED_MAX_ITEMS = 50 | |
| # Discord isolation settings | |
| DISCORD_RETRY_LIMIT = 3 | |
| DISCORD_TIMEOUT = 5 | |
| DISCORD_COOLDOWN = 300 # 5 minutes between reconnect attempts | |
| def get_current_rpc(cls): | |
| return cls.RPC_URLS[cls.current_rpc_index] | |
| def rotate_rpc(cls): | |
| cls.current_rpc_index = (cls.current_rpc_index + 1) % len(cls.RPC_URLS) | |
| log.info(f"Switched to RPC: {cls.get_current_rpc()}") | |
| config = Config() | |
| # ---------- Advanced Discord Management ---------- | |
| STATUS_MESSAGE_ID = None | |
| STATUS_EDIT_URL = None | |
| DISCORD_DISABLED = False | |
| DISCORD_RETRY_COUNT = 0 | |
| DISCORD_LAST_ERROR = None | |
| DISCORD_LAST_SUCCESS = time.time() | |
| MINER_START_TIME = datetime.now() | |
| DISCORD_QUEUE = queue.Queue(maxsize=1000) # Isolated queue for Discord operations | |
| # ---------- Enhanced Multi-Miner State Management ---------- | |
| class MinerState: | |
| def __init__(self): | |
| self.total_mined = 0 | |
| self.funded_found = 0 | |
| self.errors_count = 0 | |
| self.last_funded_time = None | |
| self.run_flag = threading.Event() | |
| self.upload_queue = queue.Queue() | |
| self.lock = threading.Lock() | |
| # Enhanced tracking | |
| self.start_time = datetime.now() | |
| self.last_batch_time = None | |
| self.current_rpc = None | |
| self.rpc_switches = 0 | |
| self.total_batches = 0 | |
| self.average_batch_time = 0.0 | |
| self.best_streak = 0 | |
| self.current_streak = 0 | |
| self.wallets_per_second = 0.0 | |
| self.connection_status = "Initializing" | |
| self.last_activity = time.time() | |
| # Performance metrics | |
| self.batch_times = collections.deque(maxlen=100) # Last 100 batch times | |
| self.rpc_performance = {} # Track RPC response times | |
| # 30-minute cycle tracking | |
| self.cycle_start_time = time.time() | |
| self.cycle_active = True | |
| self.cycle_break_start = None | |
| self.total_cycles = 0 | |
| self.in_break_mode = False | |
| def increment_mined(self): | |
| with self.lock: | |
| self.total_mined += 1 | |
| def increment_errors(self): | |
| with self.lock: | |
| self.errors_count += 1 | |
| def update_batch_stats(self, batch_size: int, batch_time: float, rpc_url: str): | |
| with self.lock: | |
| self.last_batch_time = datetime.now() | |
| self.total_batches += 1 | |
| self.batch_times.append(batch_time) | |
| self.current_rpc = rpc_url | |
| self.last_activity = time.time() | |
| # Calculate averages | |
| if self.batch_times: | |
| self.average_batch_time = sum(self.batch_times) / len(self.batch_times) | |
| total_time = (datetime.now() - self.start_time).total_seconds() | |
| if total_time > 0: | |
| self.wallets_per_second = self.total_mined / total_time | |
| # Track RPC performance | |
| if rpc_url not in self.rpc_performance: | |
| self.rpc_performance[rpc_url] = [] | |
| self.rpc_performance[rpc_url].append(batch_time) | |
| if len(self.rpc_performance[rpc_url]) > 50: | |
| self.rpc_performance[rpc_url].pop(0) | |
| def increment_funded(self): | |
| with self.lock: | |
| self.funded_found += 1 | |
| self.last_funded_time = datetime.now() | |
| self.current_streak += 1 | |
| if self.current_streak > self.best_streak: | |
| self.best_streak = self.current_streak | |
| def reset_streak(self): | |
| with self.lock: | |
| self.current_streak = 0 | |
| def get_comprehensive_stats(self) -> Dict: | |
| with self.lock: | |
| uptime = datetime.now() - self.start_time | |
| return { | |
| # Basic stats | |
| 'total_mined': self.total_mined, | |
| 'funded_found': self.funded_found, | |
| 'errors_count': self.errors_count, | |
| 'last_funded': self.last_funded_time, | |
| 'queue_size': self.upload_queue.qsize(), | |
| 'is_running': self.run_flag.is_set(), | |
| # Enhanced stats | |
| 'miner_id': config.MINER_ID, | |
| 'miner_name': config.MINER_NAME, | |
| 'location': config.LOCATION, | |
| 'start_time': self.start_time, | |
| 'uptime_seconds': uptime.total_seconds(), | |
| 'uptime_str': str(uptime).split('.')[0], | |
| 'current_rpc': self.current_rpc or "None", | |
| 'rpc_switches': self.rpc_switches, | |
| 'total_batches': self.total_batches, | |
| 'average_batch_time': round(self.average_batch_time, 2), | |
| 'wallets_per_second': round(self.wallets_per_second, 2), | |
| 'best_streak': self.best_streak, | |
| 'current_streak': self.current_streak, | |
| 'connection_status': self.connection_status, | |
| 'last_activity': self.last_activity, | |
| # 30-minute cycle info | |
| 'cycle_active': self.cycle_active, | |
| 'in_break_mode': self.in_break_mode, | |
| 'total_cycles': self.total_cycles, | |
| 'cycle_time_remaining': self.get_cycle_time_remaining(), | |
| # Performance metrics | |
| 'batch_times_count': len(self.batch_times), | |
| 'rpc_performance': {rpc: round(sum(times)/len(times), 2) | |
| for rpc, times in self.rpc_performance.items() if times} | |
| } | |
| def get_cycle_time_remaining(self) -> Dict: | |
| """Calculate remaining time in current cycle (30 min active / 30 min break)""" | |
| current_time = time.time() | |
| cycle_duration = 30 * 60 # 30 minutes in seconds | |
| if self.in_break_mode: | |
| # In break mode | |
| if self.cycle_break_start: | |
| elapsed_break = current_time - self.cycle_break_start | |
| remaining_break = max(0, cycle_duration - elapsed_break) | |
| return { | |
| 'mode': 'break', | |
| 'remaining_seconds': int(remaining_break), | |
| 'remaining_minutes': int(remaining_break / 60), | |
| 'progress_percent': min(100, (elapsed_break / cycle_duration) * 100) | |
| } | |
| else: | |
| # In mining mode | |
| elapsed_mining = current_time - self.cycle_start_time | |
| remaining_mining = max(0, cycle_duration - elapsed_mining) | |
| return { | |
| 'mode': 'mining', | |
| 'remaining_seconds': int(remaining_mining), | |
| 'remaining_minutes': int(remaining_mining / 60), | |
| 'progress_percent': min(100, (elapsed_mining / cycle_duration) * 100) | |
| } | |
| return {'mode': 'unknown', 'remaining_seconds': 0, 'remaining_minutes': 0, 'progress_percent': 0} | |
| def check_cycle_status(self) -> bool: | |
| """Check if we should switch between mining and break mode. Returns True if should continue mining.""" | |
| current_time = time.time() | |
| cycle_duration = 30 * 60 # 30 minutes | |
| if self.in_break_mode: | |
| # Currently in break, check if break time is over | |
| if self.cycle_break_start and (current_time - self.cycle_break_start) >= cycle_duration: | |
| # Break is over, switch to mining | |
| self.in_break_mode = False | |
| self.cycle_active = True | |
| self.cycle_start_time = current_time | |
| self.cycle_break_start = None | |
| log.info("π’ 30-minute break completed, resuming mining...") | |
| return True | |
| return False # Still in break | |
| else: | |
| # Currently mining, check if mining time is over | |
| if (current_time - self.cycle_start_time) >= cycle_duration: | |
| # Mining period is over, switch to break | |
| self.in_break_mode = True | |
| self.cycle_active = False | |
| self.cycle_break_start = current_time | |
| self.total_cycles += 1 | |
| log.info(f"π΄ 30-minute mining cycle #{self.total_cycles} completed, starting 30-minute break...") | |
| return False | |
| return True # Continue mining | |
| def get_stats(self) -> Dict: | |
| # Maintain backward compatibility | |
| return self.get_comprehensive_stats() | |
| miner_state = MinerState() | |
| # ---------- Live Feed ---------- | |
| class LiveFeed: | |
| def __init__(self, max_items: int = 100): | |
| self.feed = collections.deque(maxlen=max_items) | |
| self.lock = threading.Lock() | |
| def add_entry(self, secret: str, sol: float, address: str = ""): | |
| with self.lock: | |
| timestamp = datetime.now().strftime('%H:%M:%S') | |
| if sol >= config.MIN_SOL: | |
| entry = f"π’ **FUNDED** {sol:.6f} SOL | `{secret[:20]}...` | {address[:20]}... | {timestamp}" | |
| log.info(f"π° FUNDED WALLET: {sol:.6f} SOL") | |
| else: | |
| entry = f"π΄ Empty | `{secret[:20]}...` | {timestamp}" | |
| self.feed.append(entry) | |
| def get_feed(self) -> str: | |
| with self.lock: | |
| return "\n".join(self.feed) | |
| def clear_feed(self): | |
| with self.lock: | |
| self.feed.clear() | |
| live_feed = LiveFeed(config.FEED_MAX_ITEMS) | |
| # ---------- Key Generation ---------- | |
| BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" | |
| def generate_secure_keypair() -> Tuple[str, str, Keypair]: | |
| """Generate a secure keypair with Base58 encoding""" | |
| try: | |
| keypair = _safe_keypair() | |
| seed = bytes(keypair)[:32] | |
| private_key_b58 = base58.b58encode(seed).decode('utf-8') | |
| public_key = str(keypair.pubkey()) | |
| return private_key_b58, public_key, keypair | |
| except Exception as e: | |
| log.error(f"Keypair generation failed: {e}") | |
| raise Exception(f"Failed to generate keypair: {e}") | |
| def get_key_info(private_key: str) -> Dict: | |
| try: | |
| decoded = base58.b58decode(private_key) | |
| keypair = _safe_keypair() | |
| return { | |
| "private_key": private_key, | |
| "public_key": str(keypair.pubkey()), | |
| "key_length": len(private_key), | |
| "is_valid": True, | |
| "private_key_hex": decoded.hex(), | |
| "public_key_hex": bytes(keypair.pubkey()).hex(), | |
| } | |
| except Exception as e: | |
| return {"private_key": private_key, "error": str(e), "is_valid": False} | |
| # ---------- Simple Solana Client ---------- | |
| def create_solana_client() -> Client: | |
| """Create client with simple RPC rotation""" | |
| for attempt in range(len(config.RPC_URLS)): | |
| rpc_url = config.get_current_rpc() | |
| try: | |
| client = Client(rpc_url.strip()) | |
| client.get_slot() # Test connection | |
| log.info(f"β Connected to Solana RPC: {rpc_url}") | |
| return client | |
| except Exception as e: | |
| log.warning(f"β RPC {rpc_url} failed: {e}") | |
| config.rotate_rpc() | |
| time.sleep(2) | |
| raise Exception("All RPC endpoints failed") | |
| def get_balances_with_retry(client: Client, addresses: List[str]) -> List[float]: | |
| """Simple balance checking with retry logic""" | |
| chunk_size = min(50, len(addresses)) | |
| for attempt in range(config.MAX_RETRIES): | |
| try: | |
| all_balances = [] | |
| for i in range(0, len(addresses), chunk_size): | |
| chunk = addresses[i:i + chunk_size] | |
| pubkeys = [Pubkey.from_string(addr) for addr in chunk] | |
| time.sleep(0.5) # Rate limiting | |
| resp = client.get_multiple_accounts(pubkeys) | |
| chunk_balances = [] | |
| for acc in resp.value: | |
| chunk_balances.append((acc.lamports / 1e9) if acc else 0.0) | |
| all_balances.extend(chunk_balances) | |
| # Small delay between chunks | |
| if i + chunk_size < len(addresses): | |
| time.sleep(0.2) | |
| return all_balances | |
| except Exception as e: | |
| log.warning(f"Balance check attempt {attempt + 1} failed: {e}") | |
| miner_state.increment_errors() | |
| config.rotate_rpc() | |
| time.sleep(2) | |
| return [0.0] * len(addresses) | |
| # ---------- Isolated Discord Operations ---------- | |
| def queue_discord_message(message_type: str, data: dict): | |
| """Queue Discord message for isolated processing""" | |
| if not config.DISCORD_WEBHOOK: | |
| return | |
| try: | |
| DISCORD_QUEUE.put_nowait({ | |
| 'type': message_type, | |
| 'data': data, | |
| 'timestamp': time.time() | |
| }) | |
| except queue.Full: | |
| # If queue is full, remove oldest message and add new one | |
| try: | |
| DISCORD_QUEUE.get_nowait() | |
| DISCORD_QUEUE.put_nowait({ | |
| 'type': message_type, | |
| 'data': data, | |
| 'timestamp': time.time() | |
| }) | |
| except: | |
| pass # Silently fail to avoid impacting mining | |
| def send_funded_ping(private_key: str, public_key: str, balance: float): | |
| """Queue funded wallet notification - never blocks mining""" | |
| stats = miner_state.get_comprehensive_stats() | |
| queue_discord_message('funded_wallet', { | |
| 'private_key': private_key, | |
| 'public_key': public_key, | |
| 'balance': balance, | |
| 'miner_stats': stats, | |
| 'discovery_time': datetime.now().isoformat() | |
| }) | |
| def process_funded_wallet_message(data: dict): | |
| """Process funded wallet Discord message""" | |
| stats = data['miner_stats'] | |
| payload = { | |
| "content": f"@everyone π¨ **{stats['miner_name']}** found FUNDED wallet!", | |
| "embeds": [{ | |
| "title": f"π° FUNDED WALLET DISCOVERED - {stats['miner_name']}", | |
| "color": 0x00ff00, | |
| "thumbnail": {"url": "https://cryptologos.cc/logos/solana-sol-logo.png"}, | |
| "fields": [ | |
| {"name": "πΌ Wallet Details", "value": | |
| f"**Balance:** {data['balance']:.8f} SOL\n" | |
| f"**Public Key:** `{data['public_key']}`\n" | |
| f"**Private Key:** ||{data['private_key']}||", "inline": False}, | |
| {"name": "π Miner Performance", "value": | |
| f"**Total Mined:** {stats['total_mined']:,} wallets\n" | |
| f"**Total Funded:** {stats['funded_found']} wallets\n" | |
| f"**Success Rate:** {(stats['funded_found']/max(stats['total_mined'],1)*100):.6f}%", "inline": True}, | |
| {"name": "β‘ Current Stats", "value": | |
| f"**Speed:** {stats['wallets_per_second']:.1f} wallets/sec\n" | |
| f"**Uptime:** {stats['uptime_str']}\n" | |
| f"**Current Streak:** {stats['current_streak']}", "inline": True}, | |
| {"name": "π Miner Info", "value": | |
| f"**ID:** {stats['miner_id']}\n" | |
| f"**Location:** {stats['location']}\n" | |
| f"**RPC:** {stats['current_rpc'].split('//')[-1][:25]}", "inline": True}, | |
| {"name": "π Network Stats", "value": | |
| f"**Batches Processed:** {stats['total_batches']}\n" | |
| f"**Avg Batch Time:** {stats['average_batch_time']}s\n" | |
| f"**RPC Switches:** {stats['rpc_switches']}", "inline": True}, | |
| {"name": "π Discovery Time", "value": f"<t:{int(time.time())}:F>", "inline": False} | |
| ], | |
| "footer": {"text": f"Miner: {stats['miner_name']} | Best Streak: {stats['best_streak']}"} | |
| }] | |
| } | |
| return requests.post(config.DISCORD_WEBHOOK, data=json.dumps(payload), | |
| headers={"Content-Type": "application/json"}, | |
| timeout=config.DISCORD_TIMEOUT) | |
| def queue_status_update(): | |
| """Queue status update - never blocks mining""" | |
| stats = miner_state.get_comprehensive_stats() | |
| queue_discord_message('status_update', { | |
| 'stats': stats, | |
| 'timestamp': time.time() | |
| }) | |
| def process_status_update(data: dict): | |
| """Process comprehensive status update""" | |
| global STATUS_MESSAGE_ID, STATUS_EDIT_URL | |
| stats = data['stats'] | |
| # Enhanced status embed with comprehensive information | |
| embed = { | |
| "title": f"πͺ {stats['miner_name']} - Live Mining Status", | |
| "color": 0x0099ff if stats['is_running'] else 0xff4444, | |
| "thumbnail": {"url": "https://cryptologos.cc/logos/solana-sol-logo.png"}, | |
| "fields": [ | |
| {"name": "π Core Statistics", "value": | |
| f"**Status:** {'π’ MINING' if stats['is_running'] else 'π΄ STOPPED'}\n" | |
| f"**Total Mined:** {stats['total_mined']:,} wallets\n" | |
| f"**Funded Found:** {stats['funded_found']} wallets\n" | |
| f"**Success Rate:** {(stats['funded_found']/max(stats['total_mined'],1)*100):.6f}%", "inline": True}, | |
| {"name": "β‘ Performance Metrics", "value": | |
| f"**Speed:** {stats['wallets_per_second']:.2f} wallets/sec\n" | |
| f"**Uptime:** {stats['uptime_str']}\n" | |
| f"**Batches:** {stats['total_batches']}\n" | |
| f"**Avg Batch Time:** {stats['average_batch_time']}s", "inline": True}, | |
| {"name": "π Streak & Records", "value": | |
| f"**Current Streak:** {stats['current_streak']}\n" | |
| f"**Best Streak:** {stats['best_streak']}\n" | |
| f"**Errors:** {stats['errors_count']}\n" | |
| f"**Queue Size:** {stats['queue_size']}", "inline": True}, | |
| {"name": "π Network & Infrastructure", "value": | |
| f"**Current RPC:** {stats['current_rpc'].split('//')[-1][:30] if stats['current_rpc'] else 'None'}\n" | |
| f"**RPC Switches:** {stats['rpc_switches']}\n" | |
| f"**Connection:** {stats['connection_status']}\n" | |
| f"**Location:** {stats['location']}", "inline": False}, | |
| {"name": "π°οΈ Timing Information", "value": | |
| f"**Started:** <t:{int(stats['start_time'].timestamp())}:R>\n" | |
| f"**Last Activity:** <t:{int(stats['last_activity'])}:R>\n" | |
| f"**Last Funded:** {'<t:' + str(int(stats['last_funded'].timestamp())) + ':R>' if stats['last_funded'] else 'Never'}\n" | |
| f"**Update:** <t:{int(time.time())}:f>", "inline": False} | |
| ], | |
| "footer": { | |
| "text": f"Miner ID: {stats['miner_id']} | Batch #{stats['total_batches']}", | |
| "icon_url": "https://cryptologos.cc/logos/solana-sol-logo.png" | |
| }, | |
| "timestamp": datetime.utcnow().isoformat() | |
| } | |
| # Add RPC performance if available | |
| if stats['rpc_performance']: | |
| rpc_perf = "\n".join([f"**{rpc.split('//')[-1][:20]}:** {avg_time}s" | |
| for rpc, avg_time in stats['rpc_performance'].items()]) | |
| embed["fields"].append({ | |
| "name": "π RPC Performance", | |
| "value": rpc_perf[:1000], | |
| "inline": False | |
| }) | |
| # First message or edit existing | |
| if STATUS_MESSAGE_ID is None: | |
| payload = {"embeds": [embed]} | |
| r = requests.post(config.DISCORD_WEBHOOK + "?wait=true", | |
| data=json.dumps(payload), | |
| headers={"Content-Type": "application/json"}, | |
| timeout=config.DISCORD_TIMEOUT) | |
| if r.status_code == 200: | |
| data = r.json() | |
| STATUS_MESSAGE_ID = data['id'] | |
| STATUS_EDIT_URL = f"{config.DISCORD_WEBHOOK}/messages/{STATUS_MESSAGE_ID}" | |
| return r | |
| if STATUS_EDIT_URL: | |
| return requests.patch(STATUS_EDIT_URL, data=json.dumps({"embeds": [embed]}), | |
| headers={"Content-Type": "application/json"}, | |
| timeout=config.DISCORD_TIMEOUT) | |
| return None | |
| def isolated_discord_worker(): | |
| """Completely isolated Discord worker - never affects mining""" | |
| global DISCORD_DISABLED, DISCORD_RETRY_COUNT, DISCORD_LAST_ERROR, DISCORD_LAST_SUCCESS | |
| log.info(f"π Discord worker started for miner: {config.MINER_NAME}") | |
| while True: | |
| try: | |
| # Process Discord queue without blocking | |
| try: | |
| message = DISCORD_QUEUE.get(timeout=1) | |
| # Skip old messages (older than 5 minutes) | |
| if time.time() - message['timestamp'] > 300: | |
| continue | |
| # Process message based on type | |
| response = None | |
| if message['type'] == 'funded_wallet': | |
| response = process_funded_wallet_message(message['data']) | |
| elif message['type'] == 'status_update': | |
| response = process_status_update(message['data']) | |
| elif message['type'] == 'miner_startup': | |
| response = process_miner_startup(message['data']) | |
| # Handle response | |
| if response and response.status_code in [200, 204]: | |
| DISCORD_RETRY_COUNT = 0 | |
| DISCORD_LAST_SUCCESS = time.time() | |
| DISCORD_DISABLED = False | |
| log.info(f"π¬ Discord message sent: {message['type']}") | |
| elif response: | |
| raise Exception(f"Discord API returned {response.status_code}: {response.text[:100]}") | |
| except queue.Empty: | |
| # No messages to process, continue | |
| pass | |
| except requests.exceptions.RequestException as e: | |
| # Network-related errors | |
| DISCORD_RETRY_COUNT += 1 | |
| DISCORD_LAST_ERROR = f"Network error: {str(e)[:100]}" | |
| if DISCORD_RETRY_COUNT >= config.DISCORD_RETRY_LIMIT: | |
| if not DISCORD_DISABLED: | |
| log.info(f"π Discord temporarily disabled after {DISCORD_RETRY_COUNT} failures. Will retry in {config.DISCORD_COOLDOWN}s") | |
| DISCORD_DISABLED = True | |
| time.sleep(config.DISCORD_COOLDOWN) # Long cooldown | |
| DISCORD_RETRY_COUNT = 0 # Reset after cooldown | |
| else: | |
| time.sleep(min(10, DISCORD_RETRY_COUNT * 2)) # Exponential backoff | |
| except Exception as e: | |
| # Other errors | |
| DISCORD_LAST_ERROR = f"Processing error: {str(e)[:100]}" | |
| log.warning(f"β οΈ Discord processing error: {e}") | |
| time.sleep(5) | |
| # Auto-queue status updates every 2 minutes when not disabled | |
| if not DISCORD_DISABLED and time.time() - DISCORD_LAST_SUCCESS > 120: | |
| queue_status_update() | |
| except Exception as e: | |
| # Critical error handler - never crash the Discord worker | |
| log.error(f"π¨ Critical Discord worker error: {e}") | |
| time.sleep(30) # Long sleep after critical errors | |
| time.sleep(0.1) # Small sleep to prevent tight loop | |
| def process_miner_startup(data: dict): | |
| """Process miner startup notification""" | |
| stats = data['stats'] | |
| payload = { | |
| "content": f"π **{stats['miner_name']}** has started mining!", | |
| "embeds": [{ | |
| "title": f"π New Miner Online - {stats['miner_name']}", | |
| "color": 0x00ff88, | |
| "fields": [ | |
| {"name": "π Miner Information", "value": | |
| f"**Name:** {stats['miner_name']}\n" | |
| f"**ID:** {stats['miner_id']}\n" | |
| f"**Location:** {stats['location']}\n" | |
| f"**Started:** <t:{int(time.time())}:F>", "inline": True}, | |
| {"name": "βοΈ Configuration", "value": | |
| f"**Min SOL:** {config.MIN_SOL}\n" | |
| f"**Batch Size:** {config.RPC_BATCH_SIZE}\n" | |
| f"**Sleep Time:** {config.MINER_SLEEP}s\n" | |
| f"**RPC Endpoints:** {len(config.RPC_URLS)}", "inline": True} | |
| ], | |
| "footer": {"text": f"Miner ready to start discovering funded wallets!"} | |
| }] | |
| } | |
| return requests.post(config.DISCORD_WEBHOOK, data=json.dumps(payload), | |
| headers={"Content-Type": "application/json"}, | |
| timeout=config.DISCORD_TIMEOUT) | |
| # Start isolated Discord worker | |
| threading.Thread(target=isolated_discord_worker, daemon=True).start() | |
| # Queue startup notification | |
| if config.DISCORD_WEBHOOK: | |
| queue_discord_message('miner_startup', { | |
| 'stats': { | |
| 'miner_name': config.MINER_NAME, | |
| 'miner_id': config.MINER_ID, | |
| 'location': config.LOCATION | |
| } | |
| }) | |
| # Auto-start mining code will be placed after mining_loop function is defined | |
| def upload_daemon(): | |
| """Background uploader for funded wallets - integrated with new Discord system""" | |
| while True: | |
| try: | |
| private_key, public_key, balance = miner_state.upload_queue.get(timeout=1) | |
| # Use the new isolated Discord system | |
| send_funded_ping(private_key, public_key, balance) | |
| miner_state.upload_queue.task_done() | |
| except queue.Empty: | |
| continue | |
| except Exception as e: | |
| log.error(f"Upload daemon error: {e}") | |
| threading.Thread(target=upload_daemon, daemon=True).start() | |
| # ---------- Simple Mining Loop ---------- | |
| def mining_loop(): | |
| """Main mining loop with 30-minute cycle management""" | |
| log.info("π Mining loop started with 30-minute cycles") | |
| log.info(f"π Run flag status: {miner_state.run_flag.is_set()}") | |
| client = None | |
| consecutive_errors = 0 | |
| batch_private_keys, batch_addresses = [], [] | |
| while miner_state.run_flag.is_set(): | |
| try: | |
| # Check 30-minute cycle status | |
| should_mine = miner_state.check_cycle_status() | |
| if not should_mine: | |
| # In break mode, sleep and continue | |
| miner_state.connection_status = "On Break (30 min cycle)" | |
| log.info(f"π€ In break mode, sleeping for 60 seconds...") | |
| time.sleep(60) # Check every minute during break | |
| continue | |
| miner_state.connection_status = "Mining Active" | |
| log.info(f"βοΈ Starting mining batch of {config.RPC_BATCH_SIZE} wallets...") | |
| # Ensure client connection | |
| if client is None: | |
| try: | |
| client = create_solana_client() | |
| consecutive_errors = 0 | |
| except Exception as e: | |
| log.error(f"Cannot connect to RPC: {e}") | |
| time.sleep(10) | |
| continue | |
| # Generate keypair batch | |
| log.info(f"π Generating batch of {config.RPC_BATCH_SIZE} keypairs...") | |
| while len(batch_addresses) < config.RPC_BATCH_SIZE and miner_state.run_flag.is_set(): | |
| try: | |
| private_key, public_key, _ = generate_secure_keypair() | |
| batch_private_keys.append(private_key) | |
| batch_addresses.append(public_key) | |
| miner_state.increment_mined() | |
| if len(batch_addresses) % 20 == 0: | |
| log.info(f"Generated {len(batch_addresses)} keypairs so far...") | |
| time.sleep(0.01) # Prevent CPU hogging | |
| except Exception as e: | |
| log.error(f"Keypair generation error: {e}") | |
| continue | |
| log.info(f"β Generated {len(batch_addresses)} keypairs, checking balances...") | |
| # Check balances | |
| if batch_addresses: | |
| batch_start_time = time.time() | |
| log.info(f"π Checking {len(batch_addresses)} addresses...") | |
| balances = get_balances_with_retry(client, batch_addresses) | |
| if not balances or len(balances) != len(batch_addresses): | |
| log.warning("RPC response mismatch, reconnecting...") | |
| client = None | |
| continue | |
| funded_in_batch = 0 | |
| for i, balance in enumerate(balances): | |
| if balance >= config.MIN_SOL: | |
| priv, addr = batch_private_keys[i], batch_addresses[i] | |
| funded_in_batch += 1 | |
| miner_state.increment_funded() | |
| miner_state.upload_queue.put((priv, addr, balance)) | |
| log.info(f"π° FUNDED: {balance:.8f} SOL | {addr}") | |
| live_feed.add_entry(priv, balance, addr) | |
| elif i % 50 == 0: # Sample empty wallets | |
| live_feed.add_entry(batch_private_keys[i], balance, batch_addresses[i]) | |
| log.info(f"β Batch complete: {funded_in_batch} funded, {len(batch_addresses)} total") | |
| # Update comprehensive stats | |
| batch_time = time.time() - batch_start_time | |
| miner_state.update_batch_stats(len(batch_addresses), batch_time, config.get_current_rpc()) | |
| miner_state.connection_status = "Active Mining" | |
| # Reset batch | |
| batch_private_keys, batch_addresses = [], [] | |
| consecutive_errors = 0 | |
| # Queue status update every 50 batches or when funded wallet found | |
| if funded_in_batch > 0 or miner_state.total_batches % 50 == 0: | |
| queue_status_update() | |
| # Sleep between batches | |
| time.sleep(config.MINER_SLEEP) | |
| except Exception as e: | |
| consecutive_errors += 1 | |
| log.error(f"Mining loop error: {e}") | |
| miner_state.increment_errors() | |
| if consecutive_errors >= 3: | |
| client = None | |
| miner_state.connection_status = "Reconnecting" | |
| miner_state.rpc_switches += 1 | |
| log.info("Resetting RPC connection due to errors") | |
| time.sleep(5) | |
| log.info("π Mining loop stopped") | |
| # Auto-start mining when app loads | |
| log.info("π Auto-starting mining on app launch...") | |
| miner_state.run_flag.set() | |
| threading.Thread(target=mining_loop, daemon=True).start() | |
| # ---------- Gradio Controls ---------- | |
| def start_mining() -> str: | |
| if miner_state.run_flag.is_set(): | |
| return "β οΈ Already running" | |
| log.info("π Starting mining manually via button...") | |
| miner_state.run_flag.set() | |
| threading.Thread(target=mining_loop, daemon=True).start() | |
| return "π’ Mining started (background)" | |
| def stop_mining() -> str: | |
| if not miner_state.run_flag.is_set(): | |
| return "β οΈ Not running" | |
| miner_state.run_flag.clear() | |
| return "π΄ Mining stopped!" | |
| def get_mining_stats() -> str: | |
| stats = miner_state.get_comprehensive_stats() | |
| running = "π’ Running" if stats['is_running'] else "π΄ Stopped" | |
| last = "" | |
| if stats['last_funded']: | |
| ago = (datetime.now() - stats['last_funded']).seconds | |
| last = f" | Last funded: {ago//3600}h {(ago//60)%60}m ago" | |
| # Add cycle information | |
| cycle_info = stats['cycle_time_remaining'] | |
| if cycle_info['mode'] == 'break': | |
| cycle_status = f"π΄ Break Mode ({cycle_info['remaining_minutes']}m left)" | |
| elif cycle_info['mode'] == 'mining': | |
| cycle_status = f"π’ Mining ({cycle_info['remaining_minutes']}m left)" | |
| else: | |
| cycle_status = "π Cycle Starting" | |
| return (f"{running} | {cycle_status} | Cycles: {stats['total_cycles']} | " | |
| f"Mined: {stats['total_mined']:,} | Funded: {stats['funded_found']} | " | |
| f"Speed: {stats['wallets_per_second']:.1f}/s | Errors: {stats['errors_count']} | " | |
| f"Uptime: {stats['uptime_str']}{last}") | |
| def clear_feed() -> str: | |
| live_feed.clear_feed() | |
| return "" | |
| def generate_single_key() -> tuple: | |
| try: | |
| private_key, public_key, _ = generate_secure_keypair() | |
| info = get_key_info(private_key) | |
| return private_key, public_key, info.get('private_key_hex', ''), str(info.get('key_length', 0)) | |
| except Exception as e: | |
| log.error(f"Key generation error: {e}") | |
| err = f"Error: {e}" | |
| return err, err, err, err | |
| def generate_bulk_keys(count: int) -> str: | |
| if count <= 0 or count > 1000: | |
| return "β Enter 1-1000" | |
| keys = [] | |
| errors = 0 | |
| for i in range(count): | |
| try: | |
| private_key, _, _ = generate_secure_keypair() | |
| keys.append(private_key) | |
| if count > 100 and (i + 1) % 50 == 0: | |
| log.info(f"Generated {i + 1}/{count} keys...") | |
| except Exception as e: | |
| errors += 1 | |
| log.error(f"Failed to generate key {i + 1}: {e}") | |
| if errors > 10: | |
| return f"β Too many errors. Generated {len(keys)} keys successfully." | |
| result = "\n".join(keys) | |
| if errors > 0: | |
| result += f"\n\nβ οΈ Note: {errors} keys failed to generate" | |
| return result | |
| # ---------- Gradio Interface ---------- | |
| css = """ | |
| #live_feed { | |
| font-family: monospace; | |
| font-size: 12px; | |
| max-height: 500px; | |
| overflow-y: auto; | |
| } | |
| """ | |
| with gr.Blocks(title="Solana Key Generator & Miner", css=css, theme=gr.themes.Soft()) as demo: | |
| gr.Markdown(f"# π Enhanced Solana Key Generator & Miner") | |
| gr.Markdown(f"**{config.MINER_NAME}** ({config.MINER_ID}) - Multi-miner Discord integration enabled!") | |
| gr.Markdown("Optimized for Hugging Face Spaces - runs forever even if you close the browser.") | |
| with gr.Tab("π² Single Key Generator"): | |
| with gr.Row(): | |
| gen_btn = gr.Button("π― Generate New Keypair", variant="primary") | |
| with gr.Row(): | |
| private_key_out = gr.Textbox(label="π Private Key (Base58)", show_copy_button=True) | |
| public_key_out = gr.Textbox(label="π Public Address", show_copy_button=True) | |
| with gr.Row(): | |
| private_hex_out = gr.Textbox(label="π’ Private Key (Hex)", show_copy_button=True) | |
| key_info_out = gr.Textbox(label="βΉοΈ Key Information", interactive=False) | |
| gen_btn.click(generate_single_key, outputs=[private_key_out, public_key_out, private_hex_out, key_info_out]) | |
| with gr.Tab("π¦ Bulk Key Generator"): | |
| count = gr.Slider(1, 1000, value=10, step=1, label="Number of Keys") | |
| btn = gr.Button("π Generate Bulk Keys", variant="primary") | |
| keys = gr.Textbox(label="Generated Private Keys", lines=20, show_copy_button=True) | |
| btn.click(generate_bulk_keys, inputs=count, outputs=keys) | |
| with gr.Tab("πͺ Wallet Miner"): | |
| with gr.Row(): | |
| start_btn = gr.Button("π’ Start Mining", variant="primary", size="lg") | |
| stop_btn = gr.Button("π΄ Stop Mining", variant="stop", size="lg") | |
| clear_btn = gr.Button("π§Ή Clear Feed", variant="secondary") | |
| status = gr.Textbox(label="π― Mining Status", value="Ready to start mining...", interactive=False) | |
| stats_display = gr.Textbox(label="π Real-time Statistics", interactive=False) | |
| live_feed_display = gr.Textbox(label="π‘ Live Mining Feed", lines=25, max_lines=30, interactive=False, elem_id="live_feed") | |
| start_btn.click(start_mining, outputs=status) | |
| stop_btn.click(stop_mining, outputs=status) | |
| clear_btn.click(clear_feed, outputs=live_feed_display) | |
| # Auto-refresh for real-time updates | |
| def _refresh(): | |
| return get_mining_stats(), live_feed.get_feed() | |
| refresh_btn = gr.Button("π Refresh", visible=False) | |
| refresh_btn.click(_refresh, outputs=[stats_display, live_feed_display]) | |
| # Set up auto-refresh after all components are defined | |
| demo.load(_refresh, outputs=[stats_display, live_feed_display]) | |
| if __name__ == "__main__": | |
| # Multi-platform compatibility: 7860 for Hugging Face, 5000 for Replit | |
| port = int(os.environ.get('PORT', 7860)) | |
| # Detect if running on Replit | |
| if os.environ.get('REPLIT_DB_URL') or os.environ.get('REPL_ID'): | |
| port = 5000 | |
| demo.queue().launch( | |
| server_name="0.0.0.0", | |
| server_port=port, | |
| share=False, | |
| show_error=True | |
| ) |