Skip to content

Instantly share code, notes, and snippets.

@Timonchegs
Last active December 16, 2025 23:47
Show Gist options
  • Select an option

  • Save Timonchegs/2e7c579ecfd391f03c309c28c5b99d35 to your computer and use it in GitHub Desktop.

Select an option

Save Timonchegs/2e7c579ecfd391f03c309c28c5b99d35 to your computer and use it in GitHub Desktop.
Replit
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