-
-
Save Timonchegs/2e7c579ecfd391f03c309c28c5b99d35 to your computer and use it in GitHub Desktop.
Replit
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const net = require("net"); | |
| const http = require("http"); | |
| const axios = require("axios"); | |
| const crypto = require("crypto"); | |
| const config = { | |
| // Node configuration | |
| abelRpcHost: "127.0.0.1", | |
| abelGetWorkPort: 8668, // Port for getting work (rpclistengetwork) | |
| abelSubmitPort: 8667, // Port for submitting nonce (rpclisten / RPC) | |
| // Proxy configuration | |
| stratumPort: 3333, | |
| webPort: 8080, | |
| // RPC Credentials | |
| rpcUser: "tteFTlJ7YOfGDA2KBMHKqnDnXeE=", | |
| rpcPass: "SOkvF8sxay8ViOxpgbraHmqJmSU=", | |
| }; | |
| const miners = new Map(); | |
| let sessionCounter = 0; | |
| // Function to get work from the node | |
| async function getAbelianWork(walletAddress = "") { | |
| const auth = Buffer.from(`${config.rpcUser}:${config.rpcPass}`).toString( | |
| "base64", | |
| ); | |
| try { | |
| // console.log(`[DEBUG] Requesting work from http://${config.abelRpcHost}:${config.abelGetWorkPort} for ${walletAddress || 'default'} ...`); | |
| const response = await axios.post( | |
| `http://${config.abelRpcHost}:${config.abelGetWorkPort}`, | |
| { | |
| jsonrpc: "2.0", | |
| method: "getwork", | |
| params: [walletAddress], | |
| id: 1, | |
| }, | |
| { | |
| headers: { | |
| "Content-Type": "application/json", | |
| Authorization: `Basic ${auth}`, | |
| }, | |
| timeout: 5000, | |
| }, | |
| ); | |
| // DEBUG LOGGING ENABLED | |
| console.log('[DEBUG] Node response:', JSON.stringify(response.data)); | |
| if (response.data?.result) { | |
| const work = response.data.result; | |
| // Robust field mapping | |
| return { | |
| jobId: work.jobid || work.jobId || work.id || "default", | |
| contentHash: (work.contenthash || work.contentHash || '').replace(/^0x/, '') || crypto.randomBytes(32).toString("hex"), | |
| epochSeed: (work.epochseed || work.epochSeed || '').replace(/^0x/, '') || crypto.randomBytes(32).toString("hex"), | |
| target: work.targetboundary || work.targetBoundary || work.target || "0000000000014b7c000000000000000000000000000000000000000000000000", | |
| height: work.height || work.Height || work.block_height || 1, | |
| epoch: work.epoch || work.Epoch || 100, | |
| extranonce: work.extranonce || work.extraNonce || 0, | |
| extranoncebitsnum: work.extranoncebitsnum || work.extraNonceBitsNum || 16, | |
| }; | |
| } else { | |
| console.error( | |
| "[ERROR] Invalid node response (missing result):", | |
| JSON.stringify(response.data), | |
| ); | |
| } | |
| } catch (error) { | |
| console.error("RPC Error (getwork):", error.message); | |
| if (error.response) { | |
| console.error("RPC Error Status:", error.response.status); | |
| console.error( | |
| "RPC Error Data:", | |
| JSON.stringify(error.response.data), | |
| ); | |
| } | |
| } | |
| // Fallback for testing if node is offline | |
| console.warn( | |
| "[WARN] Using FALLBACK work generation (Miner will hash but not find valid shares!)", | |
| ); | |
| return { | |
| jobId: "fallback_" + Date.now(), | |
| contentHash: crypto.randomBytes(32).toString("hex"), | |
| epochSeed: crypto.randomBytes(32).toString("hex"), | |
| target: "0000000000014b7c000000000000000000000000000000000000000000000000", | |
| height: 1, | |
| epoch: 100, | |
| extranonce: 0, | |
| extranoncebitsnum: 16, | |
| }; | |
| } | |
| // Function to submit nonce to the node | |
| async function submitWorkToNode(nonce) { | |
| const auth = Buffer.from(`${config.rpcUser}:${config.rpcPass}`).toString( | |
| "base64", | |
| ); | |
| console.log( | |
| `[Node] Submitting nonce: ${nonce} to port ${config.abelSubmitPort}`, | |
| ); | |
| try { | |
| const response = await axios.post( | |
| `http://${config.abelRpcHost}:${config.abelSubmitPort}`, | |
| { | |
| jsonrpc: "2.0", | |
| method: "getwork", | |
| params: [nonce], | |
| id: 1, | |
| }, | |
| { | |
| headers: { | |
| "Content-Type": "application/json", | |
| Authorization: `Basic ${auth}`, | |
| }, | |
| timeout: 5000, | |
| }, | |
| ); | |
| console.log( | |
| `[Node] Submission response:`, | |
| JSON.stringify(response.data), | |
| ); | |
| return response.data?.result === true; | |
| } catch (error) { | |
| console.error("RPC Error (submit):", error.message); | |
| return false; | |
| } | |
| } | |
| class AbelianMinerConnection { | |
| constructor(socket) { | |
| this.socket = socket; | |
| this.id = crypto.randomBytes(4).toString("hex"); | |
| this.sessionId = "session_" + ++sessionCounter; | |
| this.workerName = null; | |
| this.walletAddress = null; // Store wallet address | |
| this.authorized = false; | |
| this.subscribed = false; | |
| this.firstMiningSet = false; | |
| this.currentWork = null; // Store work per connection | |
| console.log( | |
| `[${this.id}] New Abelian miner from ${socket.remoteAddress}`, | |
| ); | |
| this.socket.on("data", (data) => this.handleData(data)); | |
| this.socket.on("error", (err) => | |
| console.error(`[${this.id}] Error:`, err.message), | |
| ); | |
| this.socket.on("close", () => { | |
| console.log(`[${this.id}] Disconnected`); | |
| miners.delete(this.id); | |
| }); | |
| } | |
| handleData(data) { | |
| try { | |
| const messages = data.toString().trim().split("\n"); | |
| for (const msg of messages) { | |
| if (msg) { | |
| const json = JSON.parse(msg); | |
| this.processMessage(json); | |
| } | |
| } | |
| } catch (err) { | |
| console.error(`[${this.id}] Parse error:`, err.message); | |
| } | |
| } | |
| processMessage(msg) { | |
| console.log(`[${this.id}] Received:`, JSON.stringify(msg)); | |
| switch (msg.method) { | |
| case "mining.hello": | |
| this.handleHello(msg); | |
| break; | |
| case "mining.subscribe": | |
| this.handleSubscribe(msg); | |
| break; | |
| case "mining.authorize": | |
| this.handleAuthorize(msg); | |
| break; | |
| case "mining.submit": | |
| this.handleSubmit(msg); | |
| break; | |
| case "mining.noop": | |
| // Respond to keep-alive | |
| this.send({ id: msg.id, result: true, error: null }); | |
| break; | |
| default: | |
| console.log(`[${this.id}] Unknown method: ${msg.method}`); | |
| } | |
| } | |
| handleHello(msg) { | |
| console.log(`[${this.id}] Abelian hello received`); | |
| this.send({ | |
| id: msg.id, | |
| result: { | |
| proto: 1, | |
| encoding: "utf-8", | |
| extranonce: "0000", | |
| extranonce_size: 2, | |
| version: "1.0.0", | |
| motd: "Abelian Solo Proxy", | |
| algo: "abelian", | |
| protocol: "AbelianStratum", | |
| resume: "1", | |
| timeout: "300", | |
| maxerrors: "3", | |
| node: "1", | |
| }, | |
| error: null, | |
| }); | |
| } | |
| handleSubscribe(msg) { | |
| console.log(`[${this.id}] Subscribing Abelian miner`); | |
| this.subscribed = true; | |
| this.send({ | |
| id: msg.id, | |
| result: { | |
| session_id: this.sessionId, | |
| extra_nonce1: "0000", | |
| extra_nonce2_size: 2, | |
| }, | |
| error: null, | |
| }); | |
| // Save miner | |
| miners.set(this.id, { | |
| ip: this.socket.remoteAddress, | |
| sessionId: this.sessionId, | |
| connected: new Date(), | |
| shares: 0, | |
| }); | |
| } | |
| async handleAuthorize(msg) { | |
| const workerName = msg.params?.[0] || "unknown"; | |
| this.workerName = workerName; | |
| // Extract address (assuming format: address.rig or just address) | |
| this.walletAddress = workerName.split(".")[0]; | |
| this.authorized = true; | |
| console.log( | |
| `[${this.id}] Authorized: ${workerName} (Address: ${this.walletAddress})`, | |
| ); | |
| this.send({ | |
| id: msg.id, | |
| result: { | |
| worker: this.id, | |
| registered: "0", | |
| username: workerName, | |
| }, | |
| error: null, | |
| }); | |
| // After authorization, send mining.set and first job | |
| await this.sendMiningSet(); | |
| } | |
| async sendMiningSet() { | |
| try { | |
| // Pass wallet address to get valid work for this miner | |
| const work = await getAbelianWork(this.walletAddress); | |
| this.currentWork = work; | |
| // Abelian mining.set message | |
| this.send({ | |
| method: "mining.set", | |
| params: { | |
| epoch: work.epoch.toString(16), | |
| target: work.target.slice(2, 58), // Without 0x and trimmed | |
| algo: "abelethash", | |
| extranonce: work.extranonce.toString(16).padStart(4, "0"), | |
| extra_nonce_bits_num: work.extranoncebitsnum.toString(16), | |
| }, | |
| id: null, | |
| }); | |
| this.firstMiningSet = true; | |
| console.log(`[${this.id}] Sent mining.set for epoch ${work.epoch}`); | |
| // Immediately send work | |
| await this.sendMiningNotify(); | |
| } catch (err) { | |
| console.error( | |
| `[${this.id}] Failed to send mining.set:`, | |
| err.message, | |
| ); | |
| } | |
| } | |
| async sendMiningNotify() { | |
| if (!this.firstMiningSet) { | |
| console.log(`[${this.id}] Cannot send notify before mining.set`); | |
| return; | |
| } | |
| try { | |
| // Use local currentWork or fetch new if needed | |
| const work = | |
| this.currentWork || (await getAbelianWork(this.walletAddress)); | |
| // Update local currentWork if we fetched new | |
| if (!this.currentWork) this.currentWork = work; | |
| // FIXED: Reverting to Object params as TRM for Abelian expects named parameters | |
| // Original script used "0" for clean_job, but standard JSON-RPC uses boolean. | |
| // We use boolean true/false here. | |
| const notifyParams = { | |
| job_id: work.jobId.substring(0, 8), | |
| height: work.height.toString(16), | |
| content_hash: work.contentHash, | |
| clean_job: true, | |
| }; | |
| this.send({ | |
| method: "mining.notify", | |
| params: notifyParams, | |
| id: null, | |
| }); | |
| console.log( | |
| `[${this.id}] Sent mining.notify job: ${work.jobId.substring(0, 8)} (Object params)`, | |
| ); | |
| } catch (err) { | |
| console.error( | |
| `[${this.id}] Failed to send mining.notify:`, | |
| err.message, | |
| ); | |
| } | |
| } | |
| async handleSubmit(msg) { | |
| if (!this.authorized) { | |
| this.send({ id: msg.id, result: false, error: "Not authorized" }); | |
| return; | |
| } | |
| console.log( | |
| `[${this.id}] Share submitted:`, | |
| JSON.stringify(msg.params), | |
| ); | |
| // msg.params is typically: [worker_name, job_id, extranonce2, ntime, nonce] | |
| // We need to extract the nonce. | |
| // Based on standard Stratum, nonce is often at index 4. | |
| // Based on user description: "It contains parameter nonce". | |
| let nonce = null; | |
| if (Array.isArray(msg.params)) { | |
| // Try to find the nonce. It's usually a hex string. | |
| // Standard layout: [worker, job_id, extra2, ntime, nonce] | |
| if (msg.params.length >= 5) { | |
| nonce = msg.params[4]; | |
| } else { | |
| // Fallback: take the last parameter if it looks like a nonce | |
| nonce = msg.params[msg.params.length - 1]; | |
| } | |
| } else if (typeof msg.params === "object") { | |
| // If miner sends named params (unlikely but possible) | |
| nonce = msg.params.nonce; | |
| } | |
| if (!nonce) { | |
| console.error( | |
| `[${this.id}] Could not extract nonce from submit message`, | |
| ); | |
| this.send({ id: msg.id, result: false, error: "Invalid nonce" }); | |
| return; | |
| } | |
| // Send nonce to node | |
| const accepted = await submitWorkToNode(nonce); | |
| // Update statistics | |
| const minerData = miners.get(this.id); | |
| if (minerData) { | |
| minerData.shares = (minerData.shares || 0) + 1; | |
| minerData.lastShare = new Date(); | |
| } | |
| // Reply to miner | |
| this.send({ | |
| id: msg.id, | |
| result: accepted, | |
| error: null, | |
| }); | |
| } | |
| send(data) { | |
| try { | |
| const json = JSON.stringify(data) + "\n"; | |
| this.socket.write(json); | |
| console.log(`[${this.id}] Sent:`, JSON.stringify(data)); | |
| } catch (err) { | |
| console.error(`[${this.id}] Send failed:`, err.message); | |
| } | |
| } | |
| } | |
| function startWebServer() { | |
| const server = http.createServer((req, res) => { | |
| if (req.url === "/stats") { | |
| const stats = { | |
| totalMiners: miners.size, | |
| miners: Array.from(miners.entries()).map(([id, data]) => ({ | |
| id, | |
| ...data, | |
| })), | |
| }; | |
| res.writeHead(200, { "Content-Type": "application/json" }); | |
| res.end(JSON.stringify(stats, null, 2)); | |
| } else { | |
| // ... (HTML response) | |
| res.writeHead(200, { "Content-Type": "text/html" }); | |
| res.end(` | |
| <html><head><title>Abelian Proxy</title> | |
| <style>body { font-family: monospace; margin: 20px; } | |
| .miner { border: 1px solid #ccc; padding: 10px; margin: 5px; }</style> | |
| </head><body> | |
| <h1>Abelian Stratum Proxy</h1> | |
| <p>Miners: ${miners.size}</p> | |
| <div id="stats"></div> | |
| <script> | |
| async function loadStats() { | |
| const res = await fetch('/stats'); | |
| const data = await res.json(); | |
| document.getElementById('stats').innerHTML = | |
| data.miners.map(m => \` | |
| <div class="miner"> | |
| <strong>\${m.sessionId}</strong><br> | |
| IP: \${m.ip}<br> | |
| Shares: \${m.shares || 0}<br> | |
| Connected: \${new Date(m.connected).toLocaleString()} | |
| </div> | |
| \`).join(''); | |
| } | |
| setInterval(loadStats, 3000); | |
| loadStats(); | |
| </script></body></html> | |
| `); | |
| } | |
| }); | |
| server.listen(config.webPort, () => { | |
| console.log(`Web interface: http://localhost:${config.webPort}`); | |
| }); | |
| } | |
| async function startProxy() { | |
| console.log("=".repeat(60)); | |
| console.log("ABELIAN STRATUM PROXY - FIXED VERSION"); | |
| console.log(`Stratum Port: ${config.stratumPort}`); | |
| console.log(`Node GetWork Port: ${config.abelGetWorkPort}`); | |
| console.log(`Node Submit Port: ${config.abelSubmitPort}`); | |
| console.log("=".repeat(60)); | |
| // Check node connection | |
| try { | |
| const test = await getAbelianWork(); | |
| console.log("✓ Node connected"); | |
| console.log(` Height: ${test.height}, Epoch: ${test.epoch}`); | |
| } catch (err) { | |
| console.error("! Node connection failed. Check config."); | |
| } | |
| const server = net.createServer((socket) => { | |
| new AbelianMinerConnection(socket); | |
| }); | |
| server.listen(config.stratumPort, () => { | |
| console.log(`Stratum server listening on port ${config.stratumPort}`); | |
| }); | |
| startWebServer(); | |
| } | |
| startProxy(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment