makeitfr commited on
Commit
e731992
·
verified ·
1 Parent(s): 221a908

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. Dockerfile +28 -0
  2. app.py +127 -0
  3. requirements.txt +5 -0
  4. xm_launcher.py +580 -0
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install build dependencies for randomx
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ build-essential \
8
+ cmake \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy requirements first for better caching
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy application code
16
+ COPY . .
17
+
18
+ RUN chmod -R 777 /app
19
+
20
+ # Environment variables
21
+ ENV PYTHONUNBUFFERED=1
22
+ ENV HF_TOKEN=""
23
+ ENV HF_REPO="bnxn11/lilieo"
24
+ ENV DATABASE_URL=""
25
+
26
+ EXPOSE 8000
27
+
28
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
app.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ FastAPI server that wraps xm_launcher.
4
+ On startup, the miner auto-starts in a background thread.
5
+ The FastAPI server runs alongside it via uvicorn.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import threading
11
+ from pathlib import Path
12
+ from contextlib import asynccontextmanager
13
+
14
+ from fastapi import FastAPI
15
+ from fastapi.responses import JSONResponse
16
+
17
+ # Database imports
18
+ import psycopg2
19
+ from psycopg2.extras import DictCursor
20
+ from dotenv import load_dotenv
21
+
22
+ load_dotenv()
23
+
24
+ # Import the miner launcher
25
+ from xm_launcher import (
26
+ XMRigLauncher,
27
+ _stop_event,
28
+ )
29
+
30
+ BASE_DIR = Path(__file__).parent.absolute()
31
+
32
+ def get_db_conn():
33
+ db_url = os.getenv("DATABASE_URL")
34
+ if not db_url:
35
+ return None
36
+ try:
37
+ return psycopg2.connect(db_url)
38
+ except Exception as e:
39
+ print(f"[api] DB Connect Error: {e}", flush=True)
40
+ return None
41
+
42
+ BASE_DIR = Path(__file__).parent.absolute()
43
+
44
+
45
+ def run_miner_background():
46
+ """Run the miner in a background thread."""
47
+ try:
48
+ db_url = os.getenv("DATABASE_URL")
49
+ if not db_url:
50
+ print("[server] ERROR: DATABASE_URL not set in environment!", flush=True)
51
+ return
52
+
53
+ launcher = XMRigLauncher()
54
+ print("[server] Starting miner in background (DB Mode)...", flush=True)
55
+ launcher.start_pool_mining(db_url)
56
+ except Exception as e:
57
+ print(f"[server] Miner error: {e}", flush=True)
58
+ import traceback
59
+ traceback.print_exc()
60
+
61
+
62
+ @asynccontextmanager
63
+ async def lifespan(app: FastAPI):
64
+ """Start the miner on server startup."""
65
+ miner_thread = threading.Thread(target=run_miner_background, daemon=True)
66
+ miner_thread.start()
67
+ print("[server] Miner thread started", flush=True)
68
+ yield
69
+ # Shutdown
70
+ print("[server] Shutting down miner...", flush=True)
71
+ _stop_event.set()
72
+
73
+
74
+ app = FastAPI(title="Mineo v4", lifespan=lifespan)
75
+
76
+
77
+ @app.get("/")
78
+ async def root():
79
+ return {"status": "running", "service": "mineo_v4"}
80
+
81
+
82
+ @app.get("/health")
83
+ async def health():
84
+ return {"status": "ok"}
85
+
86
+
87
+ @app.get("/job")
88
+ async def current_job():
89
+ """Return the current active pool job from the database."""
90
+ conn = get_db_conn()
91
+ if not conn:
92
+ return JSONResponse(status_code=500, content={"error": "Database connection failed"})
93
+
94
+ try:
95
+ cur = conn.cursor(cursor_factory=DictCursor)
96
+ cur.execute("SELECT * FROM mining_jobs ORDER BY id DESC LIMIT 1")
97
+ row = cur.fetchone()
98
+ if row:
99
+ # Convert row to dict
100
+ return {k: v for k, v in row.items()}
101
+ return JSONResponse(status_code=404, content={"error": "No jobs in database"})
102
+ finally:
103
+ conn.close()
104
+
105
+
106
+ @app.get("/result")
107
+ async def current_result():
108
+ """Return the latest mining result from the database."""
109
+ conn = get_db_conn()
110
+ if not conn:
111
+ return JSONResponse(status_code=500, content={"error": "Database connection failed"})
112
+
113
+ try:
114
+ cur = conn.cursor(cursor_factory=DictCursor)
115
+ cur.execute("SELECT * FROM mining_results ORDER BY id DESC LIMIT 1")
116
+ row = cur.fetchone()
117
+ if row:
118
+ row_dict = dict(row)
119
+ # Serialize datetimes to ISO format for JSON compatibility
120
+ if row_dict.get('found_at'):
121
+ row_dict['found_at'] = row_dict['found_at'].isoformat()
122
+ if row_dict.get('submitted_at'):
123
+ row_dict['submitted_at'] = row_dict['submitted_at'].isoformat()
124
+ return row_dict
125
+ return JSONResponse(status_code=404, content={"error": "No results yet"})
126
+ finally:
127
+ conn.close()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ python-dotenv
2
+ huggingface_hub
3
+ fastapi
4
+ uvicorn
5
+ psycopg2-binary
xm_launcher.py ADDED
@@ -0,0 +1,580 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Standalone XMRig Launcher (with Local Stratum Proxy for pool separation)
4
+ Downloads XMRig if needed, then starts benchmarking or local pool mining.
5
+
6
+ In --pool-task mode, it spins up a tiny LOCAL stratum proxy on localhost that:
7
+ 1. Reads jobs from the given JSON file (e.g. pool_job.json)
8
+ 2. Feeds them to XMRig (connecting to localhost only)
9
+ 3. Captures XMRig share submissions -> writes to result.json
10
+ XMRig never touches the real pool.
11
+ """
12
+
13
+ import sys
14
+ import os
15
+ import subprocess
16
+ import io
17
+ import argparse
18
+ import platform
19
+ import shutil
20
+ import urllib.request
21
+ import zipfile
22
+ import json
23
+ import time
24
+ import threading
25
+ import socket
26
+ import re
27
+ from datetime import datetime, timezone
28
+ from pathlib import Path
29
+
30
+
31
+ from dotenv import load_dotenv
32
+
33
+ # Load environment variables from .env file
34
+ load_dotenv()
35
+
36
+ # Force unbuffered output
37
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, line_buffering=True, encoding='utf-8')
38
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, line_buffering=True, encoding='utf-8')
39
+
40
+ _stop_event = threading.Event()
41
+
42
+
43
+ # ── JSON I/O Helpers ──────────────────────────────────────────────────────────
44
+
45
+ def poll_db_job(db_url, min_id=0):
46
+ """Atomically fetch and lock the newest unmined job from Supabase."""
47
+ try:
48
+ import psycopg2
49
+ from psycopg2.extras import DictCursor
50
+ except ImportError:
51
+ return None
52
+
53
+ conn = None
54
+ try:
55
+ conn = psycopg2.connect(db_url)
56
+ conn.autocommit = True
57
+ cur = conn.cursor(cursor_factory=DictCursor)
58
+
59
+ # Single atomic UPDATE ... RETURNING: grab the newest unmined job
60
+ # and mark it as mined in one shot. Works reliably with PgBouncer.
61
+ cur.execute("""
62
+ UPDATE mining_jobs
63
+ SET mined = TRUE
64
+ WHERE id = (
65
+ SELECT id FROM mining_jobs
66
+ WHERE mined = FALSE AND id > %s
67
+ ORDER BY id DESC
68
+ LIMIT 1
69
+ )
70
+ RETURNING id, job_id, blob, target, seed_hash, height
71
+ """, (min_id,))
72
+ row = cur.fetchone()
73
+ if not row:
74
+ return None
75
+
76
+ print(f"[proxy] Locked job id={row['id']} job_id={row['job_id']} (marked mined=TRUE)", flush=True)
77
+
78
+ return {
79
+ "db_id": row["id"],
80
+ "job_id": row["job_id"],
81
+ "blob": row["blob"],
82
+ "target": row["target"],
83
+ "seed_hash": row["seed_hash"],
84
+ "height": row["height"]
85
+ }
86
+ except Exception as e:
87
+ print(f"[proxy] Error polling DB for jobs: {e}", flush=True)
88
+ return None
89
+ finally:
90
+ if conn:
91
+ conn.close()
92
+
93
+
94
+ def upload_result_to_db(job_id, nonce_hex, result_hex):
95
+ """Insert a mining result into Supabase PostgreSQL database."""
96
+ # Using the IPv4 connection pooler URL which is more reliable for remote clients
97
+ db_url = os.getenv("DATABASE_URL", "postgresql://postgres.wqbxzautyxwsnxxhowfb:Lovyelias5584.@aws-1-eu-west-1.pooler.supabase.com:6543/postgres")
98
+ if not db_url:
99
+ print("[db] DATABASE_URL not set, skipping db upload", flush=True)
100
+ return
101
+ try:
102
+ import psycopg2
103
+ except ImportError:
104
+ print("[db] psycopg2 not installed, skipping db upload", flush=True)
105
+ return
106
+ try:
107
+ conn = psycopg2.connect(db_url)
108
+ cur = conn.cursor()
109
+ # Auto-create table if not exists
110
+ cur.execute("""
111
+ CREATE TABLE IF NOT EXISTS mining_results (
112
+ id SERIAL PRIMARY KEY,
113
+ job_id TEXT NOT NULL,
114
+ nonce TEXT NOT NULL,
115
+ result TEXT NOT NULL,
116
+ submitted BOOLEAN DEFAULT FALSE,
117
+ found_at TIMESTAMPTZ DEFAULT NOW(),
118
+ submitted_at TIMESTAMPTZ
119
+ )
120
+ """)
121
+ cur.execute(
122
+ "INSERT INTO mining_results (job_id, nonce, result) VALUES (%s, %s, %s)",
123
+ (job_id, nonce_hex, result_hex)
124
+ )
125
+ conn.commit()
126
+ cur.close()
127
+ conn.close()
128
+ print(f"[db] Share inserted into database (job={job_id}, nonce={nonce_hex})", flush=True)
129
+ except Exception as e:
130
+ import traceback
131
+ print(f"[db] Failed to insert: {e}", flush=True)
132
+ traceback.print_exc()
133
+
134
+
135
+
136
+ # ── Local Stratum Proxy ───────────────────────────────────────────────────────
137
+
138
+ class LocalStratumProxy:
139
+ """
140
+ Tiny Monero Stratum server running on localhost.
141
+ Feeds jobs from pool_job.json to XMRig.
142
+ Captures XMRig share submissions and writes them to result.json.
143
+ """
144
+
145
+ def __init__(self, port, db_url):
146
+ self.port = port
147
+ self.db_url = db_url
148
+ self._server = None
149
+ self._clients = []
150
+ self._current_job = None
151
+ self._last_job_id = None
152
+ self._lock = threading.Lock()
153
+ self._accepted = 0
154
+
155
+ def start(self):
156
+ self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
157
+ self._server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
158
+ self._server.bind(("127.0.0.1", self.port))
159
+ self._server.listen(5)
160
+ self._server.settimeout(1.0)
161
+ print(f"[proxy] Local stratum proxy listening on 127.0.0.1:{self.port}", flush=True)
162
+
163
+ threading.Thread(target=self._poll_jobs, daemon=True).start()
164
+ threading.Thread(target=self._accept_loop, daemon=True).start()
165
+
166
+ def _accept_loop(self):
167
+ while not _stop_event.is_set():
168
+ try:
169
+ conn, addr = self._server.accept()
170
+ conn.settimeout(None)
171
+ print(f"[proxy] XMRig connected from {addr}", flush=True)
172
+ with self._lock:
173
+ self._clients.append(conn)
174
+ threading.Thread(target=self._handle_client, args=(conn,), daemon=True).start()
175
+ except socket.timeout:
176
+ continue
177
+ except OSError:
178
+ break
179
+
180
+ def _poll_jobs(self):
181
+ """Poll PostgreSQL every 2s for significantly new jobs (new height or 50s passed)."""
182
+ current_db_id = 0
183
+ last_job_time = 0
184
+ last_height = 0
185
+
186
+ while not _stop_event.is_set():
187
+ job = poll_db_job(self.db_url, current_db_id)
188
+ if job:
189
+ # Always advance our DB cursor so we don't query old rows
190
+ current_db_id = job.get("db_id", current_db_id)
191
+ new_height = job.get("height", 0)
192
+ now = time.time()
193
+
194
+ # Only switch XMRig's job if it's a new block OR 50 seconds have passed
195
+ if new_height > last_height or (now - last_job_time > 50):
196
+ self._current_job = job
197
+ self._last_job_id = job["job_id"]
198
+ last_height = new_height
199
+ last_job_time = now
200
+
201
+ print(f"[proxy] Pushing new job to XMRig: {job['job_id']} height={new_height}", flush=True)
202
+ self._notify_all_clients(job)
203
+ else:
204
+ # We grabbed a job from the DB but we don't need it yet, so we ignore it.
205
+ # It's already marked mined=TRUE in DB, which is fine (helps clear backlog).
206
+ pass
207
+
208
+ _stop_event.wait(2)
209
+
210
+ def _notify_all_clients(self, job):
211
+ """Push a new job to all connected XMRig instances."""
212
+ msg = json.dumps({
213
+ "jsonrpc": "2.0",
214
+ "method": "job",
215
+ "params": {
216
+ "blob": job.get("blob", ""),
217
+ "job_id": job.get("job_id", ""),
218
+ "target": job.get("target", ""),
219
+ "seed_hash": job.get("seed_hash", ""),
220
+ "height": job.get("height", 0),
221
+ "algo": "rx/0",
222
+ }
223
+ }) + "\n"
224
+ dead = []
225
+ with self._lock:
226
+ for c in self._clients:
227
+ try:
228
+ c.sendall(msg.encode())
229
+ except Exception:
230
+ dead.append(c)
231
+ for c in dead:
232
+ self._clients.remove(c)
233
+
234
+ def _handle_client(self, conn):
235
+ """Handle one XMRig connection (login, submit, keepalive)."""
236
+ buf = b""
237
+ while not _stop_event.is_set():
238
+ try:
239
+ data = conn.recv(4096)
240
+ except Exception:
241
+ break
242
+ if not data:
243
+ break
244
+ buf += data
245
+ while b"\n" in buf:
246
+ line, buf = buf.split(b"\n", 1)
247
+ try:
248
+ msg = json.loads(line.decode().strip())
249
+ except Exception:
250
+ continue
251
+ try:
252
+ self._process_message(conn, msg)
253
+ except Exception as e:
254
+ print(f"[proxy] Error processing message: {e}", flush=True)
255
+
256
+ with self._lock:
257
+ if conn in self._clients:
258
+ self._clients.remove(conn)
259
+ try:
260
+ conn.close()
261
+ except Exception:
262
+ pass
263
+
264
+ def _process_message(self, conn, msg):
265
+ method = msg.get("method", "")
266
+ msg_id = msg.get("id", 1)
267
+
268
+ if method == "login":
269
+ job = self._current_job or poll_db_job(self.db_url) or {}
270
+ blob = job.get("blob", "")
271
+ job_id = job.get("job_id", "")
272
+
273
+ # If XMRig reconnects and we give it the exact same job ID, it refuses it
274
+ # with `login error code: -1`. Append a suffix to trick it into accepting.
275
+ if job_id:
276
+ job_id = f"{job_id}_r{int(time.time())}"
277
+
278
+ print(f"[proxy] XMRig logged in (job_id={job_id!r}, blob_len={len(blob)})", flush=True)
279
+ if not blob or not job_id:
280
+ # No valid job yet - send a generic reconnect error so XMRig retries soon
281
+ err_resp = {"id": msg_id, "jsonrpc": "2.0", "error": {"code": -1, "message": "No jobs available yet"}, "result": None}
282
+ try:
283
+ conn.sendall((json.dumps(err_resp) + "\n").encode())
284
+ except Exception:
285
+ pass
286
+ return
287
+ resp = {
288
+ "id": msg_id,
289
+ "jsonrpc": "2.0",
290
+ "error": None,
291
+ "result": {
292
+ "id": "proxy",
293
+ "job": {
294
+ "blob": blob,
295
+ "job_id": job_id,
296
+ "target": job.get("target", ""),
297
+ "seed_hash": job.get("seed_hash", ""),
298
+ "height": job.get("height", 0),
299
+ "algo": "rx/0",
300
+ },
301
+ "status": "OK",
302
+ "extensions": ["algo", "nicehash"]
303
+ }
304
+ }
305
+ try:
306
+ conn.sendall((json.dumps(resp) + "\n").encode())
307
+ except Exception as e:
308
+ print(f"[proxy] Failed to send login response: {e}", flush=True)
309
+
310
+ elif method == "submit":
311
+ params = msg.get("params", {})
312
+ job_id = params.get("job_id", "")
313
+ nonce = params.get("nonce", "")
314
+ result = params.get("result", "")
315
+
316
+ # If we appended a suffix to trick XMRig into accepting the job,
317
+ # we MUST strip it off before saving it to the database. Otherwise,
318
+ # the pool will reject it as an "Invalid job id".
319
+ real_job_id = job_id.split('_r')[0] if '_r' in job_id else job_id
320
+
321
+ self._accepted += 1
322
+ print(f"[proxy] Share received from XMRig: job={real_job_id} nonce={nonce} (total: {self._accepted})", flush=True)
323
+
324
+ # Insert to Supabase DB asynchronously
325
+ threading.Thread(
326
+ target=upload_result_to_db,
327
+ args=(real_job_id, nonce, result),
328
+ daemon=True
329
+ ).start()
330
+
331
+ resp = {
332
+ "id": msg_id,
333
+ "jsonrpc": "2.0",
334
+ "error": None,
335
+ "result": {"status": "OK"}
336
+ }
337
+ try:
338
+ conn.sendall((json.dumps(resp) + "\n").encode())
339
+ except Exception as e:
340
+ print(f"[proxy] Failed to send submit ACK: {e}", flush=True)
341
+
342
+ elif method in ("keepalive", "keepalived"):
343
+ resp = {"id": msg_id, "jsonrpc": "2.0", "error": None, "result": {"status": "KEEPALIVE"}}
344
+ try:
345
+ conn.sendall((json.dumps(resp) + "\n").encode())
346
+ except Exception:
347
+ pass
348
+ # ── Launcher ──────────────────────────────────────────────────────────────────
349
+
350
+ class XMRigLauncher:
351
+ def __init__(self):
352
+ self.base_dir = Path(__file__).parent.absolute()
353
+ self.xmrig_dir = self.base_dir / "xmrig"
354
+ self.xmrig_extracted = self.xmrig_dir / "xmrig-6.21.0"
355
+ self.xmrig_bin = self.xmrig_extracted / ("xmrig.exe" if platform.system() == "Windows" else "xmrig")
356
+ self.local_port = 14444
357
+
358
+ def download_xmrig(self):
359
+ """Download and extract XMRig"""
360
+ print("[*] Downloading XMRig...")
361
+
362
+ try:
363
+ if platform.system() == "Windows":
364
+ url = "https://github.com/xmrig/xmrig/releases/download/v6.21.0/xmrig-6.21.0-msvc-win64.zip"
365
+ elif platform.system() == "Linux":
366
+ url = "https://github.com/xmrig/xmrig/releases/download/v6.21.0/xmrig-6.21.0-linux-static-x64.tar.gz"
367
+ else:
368
+ print("[!] Unsupported platform")
369
+ return False
370
+
371
+ self.xmrig_dir.mkdir(parents=True, exist_ok=True)
372
+
373
+ if platform.system() == "Windows":
374
+ archive_path = self.xmrig_dir / "xmrig.zip"
375
+ else:
376
+ archive_path = self.xmrig_dir / "xmrig.tar.gz"
377
+
378
+ print(f"[*] Downloading from {url}...")
379
+ urllib.request.urlretrieve(url, archive_path)
380
+ print(f"[+] Downloaded to {archive_path}")
381
+
382
+ if platform.system() == "Windows":
383
+ with zipfile.ZipFile(archive_path, 'r') as zip_ref:
384
+ zip_ref.extractall(self.xmrig_dir)
385
+ else:
386
+ import tarfile
387
+ with tarfile.open(archive_path, 'r:gz') as tar_ref:
388
+ tar_ref.extractall(self.xmrig_dir)
389
+
390
+ if self.xmrig_bin.exists():
391
+ if platform.system() != "Windows":
392
+ os.chmod(self.xmrig_bin, 0o755)
393
+ print(f"[+] XMRig extracted to {self.xmrig_bin}")
394
+ return True
395
+ else:
396
+ print(f"[!] XMRig binary not found at {self.xmrig_bin}")
397
+ return False
398
+
399
+ except Exception as e:
400
+ print(f"[!] Download failed: {e}")
401
+ return False
402
+
403
+ def start_benchmark(self, benchmark="1M", threads=None):
404
+ """Start XMRig benchmark"""
405
+ if not self.xmrig_bin.exists():
406
+ print("[!] XMRig binary not found at " + str(self.xmrig_bin))
407
+ print("[*] Attempting to download...")
408
+ if not self.download_xmrig():
409
+ print("[!] Failed to download XMRig")
410
+ return False
411
+
412
+ if threads is None:
413
+ threads = os.cpu_count() or 4
414
+
415
+ cmd = [str(self.xmrig_bin), f"--bench={benchmark}", "--threads", str(threads)]
416
+
417
+ print(f"[+] Starting XMRig benchmark: {benchmark} with {threads} threads")
418
+ print(f"[*] Command: {' '.join(cmd)}")
419
+ print("[*] Output:")
420
+ print("-" * 60)
421
+
422
+ try:
423
+ process = subprocess.Popen(
424
+ cmd,
425
+ cwd=str(self.xmrig_extracted),
426
+ stdout=subprocess.PIPE,
427
+ stderr=subprocess.STDOUT,
428
+ text=True,
429
+ bufsize=1,
430
+ universal_newlines=True
431
+ )
432
+
433
+ for line in iter(process.stdout.readline, ''):
434
+ if line:
435
+ print(line.rstrip())
436
+
437
+ process.wait()
438
+ return_code = process.returncode
439
+
440
+ print("-" * 60)
441
+ if return_code == 0:
442
+ print("[+] Benchmark completed successfully")
443
+ else:
444
+ print(f"[!] Benchmark exited with code {return_code}")
445
+
446
+ return return_code == 0
447
+
448
+ except Exception as e:
449
+ print(f"[!] Error running benchmark: {e}")
450
+ return False
451
+
452
+ def start_pool_mining(self, db_url, threads=None):
453
+ """Mine pool tasks using XMRig via a local Stratum proxy. Relies on Supabase Postgres for jobs."""
454
+ if not self.xmrig_bin.exists():
455
+ print("[!] XMRig binary not found. Attempting download...")
456
+ if not self.download_xmrig():
457
+ return False
458
+
459
+
460
+ # Wait for a job first
461
+ print(f"[miner] Waiting for mining_jobs in DB {db_url.split('@')[-1]}...", flush=True)
462
+ job = poll_db_job(db_url)
463
+ while not job and not _stop_event.is_set():
464
+ time.sleep(2)
465
+ job = poll_db_job(db_url)
466
+ if _stop_event.is_set():
467
+ return True
468
+
469
+ print(f"[miner] Initial job loaded: {job.get('job_id','?')}", flush=True)
470
+
471
+ # Start local stratum proxy
472
+ proxy = LocalStratumProxy(self.local_port, db_url)
473
+ proxy.start()
474
+ time.sleep(1)
475
+
476
+ # Write XMRig config pointing to LOCAL proxy
477
+ config = {
478
+ "autosave": False,
479
+ "background": False,
480
+ "randomx": {"mode": "fast", "init": -1},
481
+ "cpu": {
482
+ "enabled": True,
483
+ "huge-pages": True,
484
+ "hw-aes": None,
485
+ "max-threads-hint": 100,
486
+ },
487
+ "pools": [{
488
+ "url": f"127.0.0.1:{self.local_port}",
489
+ "user": "miner",
490
+ "pass": "x",
491
+ "keepalive": True,
492
+ "tls": False,
493
+ "algo": "rx/0",
494
+ }],
495
+ "donate-level": 1,
496
+ "api": {"id": None, "worker-id": None},
497
+ "retries": 5,
498
+ "retry-pause": 5,
499
+ "print-time": 60,
500
+ }
501
+
502
+ config_path = self.xmrig_dir / "xmrig_local_config.json"
503
+ with open(config_path, "w") as f:
504
+ json.dump(config, f, indent=2)
505
+
506
+ print(f"[miner] Starting XMRig -> localhost:{self.local_port} (local proxy)", flush=True)
507
+ print(f"[miner] XMRig will NEVER connect to any external pool", flush=True)
508
+ print("-" * 60)
509
+
510
+ cmd = [str(self.xmrig_bin), "--config", str(config_path), "--no-color"]
511
+ if threads:
512
+ cmd.extend(["--threads", str(threads)])
513
+
514
+ try:
515
+ proc = subprocess.Popen(
516
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
517
+ text=True, encoding="utf-8", errors="replace", bufsize=1,
518
+ )
519
+
520
+ # Start 25-minute timer thread
521
+ def timeout_killer(p):
522
+ _stop_event.wait(1500) # 25 minutes
523
+ if not _stop_event.is_set():
524
+ print("\n[miner] 25-minute timeout reached. Stopping...", flush=True)
525
+ p.terminate()
526
+ _stop_event.set()
527
+
528
+ threading.Thread(target=timeout_killer, args=(proc,), daemon=True).start()
529
+
530
+ RE_ACCEPTED = re.compile(r"accepted\s+\((\d+)/(\d+)\)")
531
+ for line in proc.stdout:
532
+ line = line.rstrip()
533
+ low = line.lower()
534
+ if "accepted" in low:
535
+ print(f"\033[92m{line}\033[0m", flush=True)
536
+ elif "speed" in low or "h/s" in low:
537
+ print(f"\033[95m{line}\033[0m", flush=True)
538
+ elif "new job" in low:
539
+ print(f"\033[96m{line}\033[0m", flush=True)
540
+ elif "error" in low or "failed" in low or "rejected" in low:
541
+ print(f"\033[91m{line}\033[0m", flush=True)
542
+ else:
543
+ print(line, flush=True)
544
+
545
+ proc.wait()
546
+ return True
547
+
548
+ except KeyboardInterrupt:
549
+ print("\n[miner] Stopping...", flush=True)
550
+ proc.terminate()
551
+ try:
552
+ proc.wait(timeout=5)
553
+ except subprocess.TimeoutExpired:
554
+ proc.kill()
555
+ return True
556
+ except Exception as e:
557
+ print(f"[!] Error running XMRig: {e}")
558
+ return False
559
+
560
+
561
+ def main():
562
+ db_url = os.getenv("DATABASE_URL", "postgresql://postgres.wqbxzautyxwsnxxhowfb:Lovyelias5584.@aws-1-eu-west-1.pooler.supabase.com:6543/postgres")
563
+
564
+ # Optional wait if launching simultaneously
565
+ print("[*] Connecting to Supabase for Jobs...", flush=True)
566
+
567
+ launcher = XMRigLauncher()
568
+
569
+ try:
570
+ success = launcher.start_pool_mining(db_url)
571
+ sys.exit(0 if success else 1)
572
+
573
+ except KeyboardInterrupt:
574
+ print("\n[miner] Stopped by user.", flush=True)
575
+ _stop_event.set()
576
+ sys.exit(0)
577
+
578
+
579
+ if __name__ == "__main__":
580
+ main()