740 lines
21 KiB
JavaScript
740 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* @ruvector/edge-net Contribution Daemon
|
|
*
|
|
* Real CPU contribution daemon that runs in the background and earns QDAG credits.
|
|
* Connects to the relay server and sends contribution_credit messages periodically.
|
|
*
|
|
* Usage:
|
|
* npx @ruvector/edge-net contribute # Start daemon (foreground)
|
|
* npx @ruvector/edge-net contribute --daemon # Start daemon (background)
|
|
* npx @ruvector/edge-net contribute --stop # Stop daemon
|
|
* npx @ruvector/edge-net contribute --status # Show daemon status
|
|
* npx @ruvector/edge-net contribute --cpu 50 # Set CPU limit (default: 50%)
|
|
* npx @ruvector/edge-net contribute --key <pubkey> # Use specific public key
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'fs';
|
|
import { fileURLToPath } from 'url';
|
|
import { dirname, join } from 'path';
|
|
import { webcrypto } from 'crypto';
|
|
import { performance } from 'perf_hooks';
|
|
import { homedir, cpus } from 'os';
|
|
import { spawn } from 'child_process';
|
|
import WebSocket from 'ws';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
// Relay server URL
|
|
const RELAY_URL = process.env.RELAY_URL || 'wss://edge-net-relay-875130704813.us-central1.run.app';
|
|
|
|
// Contribution settings
|
|
const DEFAULT_CPU_LIMIT = 50; // Default 50% CPU
|
|
const CONTRIBUTION_INTERVAL = 30000; // Report every 30 seconds
|
|
const RECONNECT_DELAY = 5000; // Reconnect after 5 seconds on disconnect
|
|
const HEARTBEAT_INTERVAL = 15000; // Heartbeat every 15 seconds
|
|
|
|
// ANSI colors
|
|
const c = {
|
|
reset: '\x1b[0m',
|
|
bold: '\x1b[1m',
|
|
dim: '\x1b[2m',
|
|
cyan: '\x1b[36m',
|
|
green: '\x1b[32m',
|
|
yellow: '\x1b[33m',
|
|
red: '\x1b[31m',
|
|
magenta: '\x1b[35m',
|
|
};
|
|
|
|
// Config directory
|
|
function getConfigDir() {
|
|
const dir = join(homedir(), '.ruvector');
|
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
function getPidFile() {
|
|
return join(getConfigDir(), 'contribute-daemon.pid');
|
|
}
|
|
|
|
function getLogFile() {
|
|
return join(getConfigDir(), 'contribute-daemon.log');
|
|
}
|
|
|
|
function getStateFile() {
|
|
return join(getConfigDir(), 'contribute-state.json');
|
|
}
|
|
|
|
// Parse command line arguments
|
|
function parseArgs(args) {
|
|
const opts = {
|
|
daemon: false,
|
|
stop: false,
|
|
status: false,
|
|
cpu: DEFAULT_CPU_LIMIT,
|
|
key: null,
|
|
site: 'cli-contributor',
|
|
help: false,
|
|
};
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
switch (arg) {
|
|
case '--daemon':
|
|
case '-d':
|
|
opts.daemon = true;
|
|
break;
|
|
case '--stop':
|
|
opts.stop = true;
|
|
break;
|
|
case '--status':
|
|
opts.status = true;
|
|
break;
|
|
case '--cpu':
|
|
opts.cpu = parseInt(args[++i]) || DEFAULT_CPU_LIMIT;
|
|
break;
|
|
case '--key':
|
|
opts.key = args[++i];
|
|
break;
|
|
case '--site':
|
|
opts.site = args[++i];
|
|
break;
|
|
case '--help':
|
|
case '-h':
|
|
opts.help = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return opts;
|
|
}
|
|
|
|
// Load or generate identity
|
|
async function loadIdentity(opts) {
|
|
const identitiesDir = join(getConfigDir(), 'identities');
|
|
if (!existsSync(identitiesDir)) mkdirSync(identitiesDir, { recursive: true });
|
|
|
|
const metaPath = join(identitiesDir, `${opts.site}.meta.json`);
|
|
|
|
// If --key is provided, use it directly
|
|
if (opts.key) {
|
|
return {
|
|
publicKey: opts.key,
|
|
shortId: `pi:${opts.key.slice(0, 16)}`,
|
|
siteId: opts.site,
|
|
};
|
|
}
|
|
|
|
// Try to load existing identity
|
|
if (existsSync(metaPath)) {
|
|
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
return {
|
|
publicKey: meta.publicKey,
|
|
shortId: meta.shortId,
|
|
siteId: meta.siteId,
|
|
};
|
|
}
|
|
|
|
// Generate new identity using WASM
|
|
const { createRequire } = await import('module');
|
|
const require = createRequire(import.meta.url);
|
|
|
|
// Setup polyfills
|
|
if (typeof globalThis.crypto === 'undefined') {
|
|
globalThis.crypto = webcrypto;
|
|
}
|
|
|
|
console.log(`${c.dim}Generating new identity...${c.reset}`);
|
|
const wasm = require('./node/ruvector_edge_net.cjs');
|
|
const piKey = new wasm.PiKey();
|
|
|
|
const publicKey = Array.from(piKey.getPublicKey())
|
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
|
|
const meta = {
|
|
version: 1,
|
|
siteId: opts.site,
|
|
shortId: piKey.getShortId(),
|
|
publicKey,
|
|
createdAt: new Date().toISOString(),
|
|
lastUsed: new Date().toISOString(),
|
|
totalSessions: 1,
|
|
totalContributions: 0,
|
|
};
|
|
|
|
writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
piKey.free();
|
|
|
|
return {
|
|
publicKey: meta.publicKey,
|
|
shortId: meta.shortId,
|
|
siteId: meta.siteId,
|
|
};
|
|
}
|
|
|
|
// Load daemon state
|
|
function loadState() {
|
|
const stateFile = getStateFile();
|
|
if (existsSync(stateFile)) {
|
|
return JSON.parse(readFileSync(stateFile, 'utf-8'));
|
|
}
|
|
return {
|
|
totalCredits: 0,
|
|
totalContributions: 0,
|
|
totalSeconds: 0,
|
|
startTime: null,
|
|
lastSync: null,
|
|
};
|
|
}
|
|
|
|
// Save daemon state
|
|
function saveState(state) {
|
|
writeFileSync(getStateFile(), JSON.stringify(state, null, 2));
|
|
}
|
|
|
|
// Measure real CPU usage
|
|
function measureCpuUsage(durationMs = 1000) {
|
|
return new Promise((resolve) => {
|
|
const startCpus = cpus();
|
|
const startTotal = startCpus.reduce((acc, cpu) => {
|
|
const times = cpu.times;
|
|
return acc + times.user + times.nice + times.sys + times.idle + times.irq;
|
|
}, 0);
|
|
const startIdle = startCpus.reduce((acc, cpu) => acc + cpu.times.idle, 0);
|
|
|
|
setTimeout(() => {
|
|
const endCpus = cpus();
|
|
const endTotal = endCpus.reduce((acc, cpu) => {
|
|
const times = cpu.times;
|
|
return acc + times.user + times.nice + times.sys + times.idle + times.irq;
|
|
}, 0);
|
|
const endIdle = endCpus.reduce((acc, cpu) => acc + cpu.times.idle, 0);
|
|
|
|
const totalDiff = endTotal - startTotal;
|
|
const idleDiff = endIdle - startIdle;
|
|
|
|
const cpuUsage = totalDiff > 0 ? ((totalDiff - idleDiff) / totalDiff) * 100 : 0;
|
|
resolve(Math.min(100, Math.max(0, cpuUsage)));
|
|
}, durationMs);
|
|
});
|
|
}
|
|
|
|
// Real CPU work (compute hashes)
|
|
function doRealWork(cpuLimit, durationMs) {
|
|
return new Promise((resolve) => {
|
|
const startTime = Date.now();
|
|
const workInterval = 10; // Work in 10ms chunks
|
|
const workRatio = cpuLimit / 100;
|
|
|
|
let computeUnits = 0;
|
|
|
|
const doWork = () => {
|
|
const now = Date.now();
|
|
if (now - startTime >= durationMs) {
|
|
resolve(computeUnits);
|
|
return;
|
|
}
|
|
|
|
// Do actual CPU work (hash computation)
|
|
const workStart = Date.now();
|
|
const workTime = workInterval * workRatio;
|
|
|
|
while (Date.now() - workStart < workTime) {
|
|
// Real cryptographic work
|
|
let data = new Uint8Array(64);
|
|
for (let i = 0; i < 64; i++) {
|
|
data[i] = (i * 7 + computeUnits) & 0xff;
|
|
}
|
|
// Simple hash-like operation (real CPU work)
|
|
for (let j = 0; j < 100; j++) {
|
|
let hash = 0;
|
|
for (let i = 0; i < data.length; i++) {
|
|
hash = ((hash << 5) - hash + data[i]) | 0;
|
|
}
|
|
data[0] = hash & 0xff;
|
|
}
|
|
computeUnits++;
|
|
}
|
|
|
|
// Rest period to respect CPU limit
|
|
const restTime = workInterval * (1 - workRatio);
|
|
setTimeout(doWork, restTime);
|
|
};
|
|
|
|
doWork();
|
|
});
|
|
}
|
|
|
|
// Contribution daemon class
|
|
class ContributionDaemon {
|
|
constructor(identity, cpuLimit) {
|
|
this.identity = identity;
|
|
this.cpuLimit = cpuLimit;
|
|
this.ws = null;
|
|
this.state = loadState();
|
|
this.isRunning = false;
|
|
this.nodeId = `node-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
this.contributionTimer = null;
|
|
this.heartbeatTimer = null;
|
|
this.reconnectTimer = null;
|
|
this.sessionStart = Date.now();
|
|
this.sessionCredits = 0;
|
|
this.sessionContributions = 0;
|
|
}
|
|
|
|
async start() {
|
|
this.isRunning = true;
|
|
this.state.startTime = Date.now();
|
|
saveState(this.state);
|
|
|
|
console.log(`\n${c.cyan}${c.bold}Edge-Net Contribution Daemon${c.reset}`);
|
|
console.log(`${c.dim}Real CPU contribution to earn QDAG credits${c.reset}\n`);
|
|
|
|
console.log(`${c.bold}Configuration:${c.reset}`);
|
|
console.log(` ${c.cyan}Identity:${c.reset} ${this.identity.shortId}`);
|
|
console.log(` ${c.cyan}Public Key:${c.reset} ${this.identity.publicKey.slice(0, 16)}...`);
|
|
console.log(` ${c.cyan}CPU Limit:${c.reset} ${this.cpuLimit}%`);
|
|
console.log(` ${c.cyan}Relay:${c.reset} ${RELAY_URL}`);
|
|
console.log(` ${c.cyan}Interval:${c.reset} ${CONTRIBUTION_INTERVAL / 1000}s\n`);
|
|
|
|
await this.connect();
|
|
|
|
// Handle shutdown
|
|
process.on('SIGINT', () => this.stop('SIGINT'));
|
|
process.on('SIGTERM', () => this.stop('SIGTERM'));
|
|
}
|
|
|
|
async connect() {
|
|
if (!this.isRunning) return;
|
|
|
|
console.log(`${c.dim}Connecting to relay...${c.reset}`);
|
|
|
|
try {
|
|
this.ws = new WebSocket(RELAY_URL);
|
|
|
|
this.ws.on('open', () => {
|
|
console.log(`${c.green}Connected to relay${c.reset}`);
|
|
|
|
// Register with relay
|
|
this.send({
|
|
type: 'register',
|
|
nodeId: this.nodeId,
|
|
publicKey: this.identity.publicKey,
|
|
capabilities: ['compute', 'cli-daemon'],
|
|
version: '1.0.0',
|
|
});
|
|
|
|
// Start heartbeat
|
|
this.startHeartbeat();
|
|
|
|
// Request initial balance from QDAG
|
|
setTimeout(() => {
|
|
this.send({
|
|
type: 'ledger_sync',
|
|
nodeId: this.nodeId,
|
|
publicKey: this.identity.publicKey,
|
|
});
|
|
}, 500);
|
|
});
|
|
|
|
this.ws.on('message', (data) => this.handleMessage(data.toString()));
|
|
|
|
this.ws.on('close', () => {
|
|
console.log(`${c.yellow}Disconnected from relay${c.reset}`);
|
|
this.stopHeartbeat();
|
|
this.stopContributing();
|
|
this.scheduleReconnect();
|
|
});
|
|
|
|
this.ws.on('error', (err) => {
|
|
console.log(`${c.red}WebSocket error: ${err.message}${c.reset}`);
|
|
});
|
|
|
|
} catch (err) {
|
|
console.log(`${c.red}Connection failed: ${err.message}${c.reset}`);
|
|
this.scheduleReconnect();
|
|
}
|
|
}
|
|
|
|
handleMessage(data) {
|
|
try {
|
|
const msg = JSON.parse(data);
|
|
|
|
switch (msg.type) {
|
|
case 'welcome':
|
|
console.log(`${c.green}Registered as ${msg.nodeId}${c.reset}`);
|
|
console.log(`${c.dim}Network: ${msg.networkState?.activeNodes || 0} active nodes${c.reset}`);
|
|
this.startContributing();
|
|
break;
|
|
|
|
case 'ledger_sync_response':
|
|
const earned = Number(msg.ledger?.earned || 0) / 1e9;
|
|
const spent = Number(msg.ledger?.spent || 0) / 1e9;
|
|
const available = earned - spent;
|
|
console.log(`${c.cyan}QDAG Balance: ${available.toFixed(4)} rUv${c.reset} (earned: ${earned.toFixed(4)}, spent: ${spent.toFixed(4)})`);
|
|
this.state.totalCredits = earned;
|
|
saveState(this.state);
|
|
break;
|
|
|
|
case 'contribution_credit_success':
|
|
const credited = msg.credited || 0;
|
|
this.sessionCredits += credited;
|
|
this.sessionContributions++;
|
|
this.state.totalCredits = Number(msg.balance?.earned || 0) / 1e9;
|
|
this.state.totalContributions++;
|
|
this.state.lastSync = Date.now();
|
|
saveState(this.state);
|
|
|
|
const balance = Number(msg.balance?.available || 0) / 1e9;
|
|
console.log(`${c.green}+${credited.toFixed(4)} rUv${c.reset} | Balance: ${balance.toFixed(4)} rUv | Total: ${this.state.totalContributions} contributions`);
|
|
break;
|
|
|
|
case 'contribution_credit_error':
|
|
console.log(`${c.yellow}Contribution rejected: ${msg.error}${c.reset}`);
|
|
break;
|
|
|
|
case 'time_crystal_sync':
|
|
// Silently handle time crystal sync
|
|
break;
|
|
|
|
case 'heartbeat_ack':
|
|
// Heartbeat acknowledged
|
|
break;
|
|
|
|
case 'error':
|
|
console.log(`${c.red}Relay error: ${msg.message}${c.reset}`);
|
|
break;
|
|
|
|
default:
|
|
// Ignore unknown messages
|
|
}
|
|
} catch (err) {
|
|
console.log(`${c.red}Error parsing message: ${err.message}${c.reset}`);
|
|
}
|
|
}
|
|
|
|
send(msg) {
|
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
this.ws.send(JSON.stringify(msg));
|
|
}
|
|
}
|
|
|
|
startHeartbeat() {
|
|
this.stopHeartbeat();
|
|
this.heartbeatTimer = setInterval(() => {
|
|
this.send({ type: 'heartbeat' });
|
|
}, HEARTBEAT_INTERVAL);
|
|
}
|
|
|
|
stopHeartbeat() {
|
|
if (this.heartbeatTimer) {
|
|
clearInterval(this.heartbeatTimer);
|
|
this.heartbeatTimer = null;
|
|
}
|
|
}
|
|
|
|
startContributing() {
|
|
this.stopContributing();
|
|
console.log(`${c.cyan}Starting contribution loop (CPU: ${this.cpuLimit}%)${c.reset}\n`);
|
|
|
|
// Immediate first contribution
|
|
this.contribute();
|
|
|
|
// Then continue every interval
|
|
this.contributionTimer = setInterval(() => {
|
|
this.contribute();
|
|
}, CONTRIBUTION_INTERVAL);
|
|
}
|
|
|
|
stopContributing() {
|
|
if (this.contributionTimer) {
|
|
clearInterval(this.contributionTimer);
|
|
this.contributionTimer = null;
|
|
}
|
|
}
|
|
|
|
async contribute() {
|
|
const startTime = Date.now();
|
|
|
|
// Do real CPU work for 5 seconds
|
|
console.log(`${c.dim}[${new Date().toLocaleTimeString()}] Working...${c.reset}`);
|
|
await doRealWork(this.cpuLimit, 5000);
|
|
|
|
// Measure actual CPU usage
|
|
const cpuUsage = await measureCpuUsage(1000);
|
|
|
|
const contributionSeconds = 30; // Claiming 30 seconds since last report
|
|
const effectiveCpu = Math.min(this.cpuLimit, cpuUsage);
|
|
|
|
// Send contribution credit request
|
|
this.send({
|
|
type: 'contribution_credit',
|
|
nodeId: this.nodeId,
|
|
publicKey: this.identity.publicKey,
|
|
contributionSeconds,
|
|
cpuUsage: Math.round(effectiveCpu),
|
|
timestamp: Date.now(),
|
|
});
|
|
|
|
this.state.totalSeconds += contributionSeconds;
|
|
saveState(this.state);
|
|
}
|
|
|
|
scheduleReconnect() {
|
|
if (!this.isRunning) return;
|
|
if (this.reconnectTimer) return;
|
|
|
|
console.log(`${c.dim}Reconnecting in ${RECONNECT_DELAY / 1000}s...${c.reset}`);
|
|
this.reconnectTimer = setTimeout(() => {
|
|
this.reconnectTimer = null;
|
|
this.connect();
|
|
}, RECONNECT_DELAY);
|
|
}
|
|
|
|
stop(signal = 'unknown') {
|
|
console.log(`\n${c.yellow}Stopping daemon (${signal})...${c.reset}`);
|
|
|
|
this.isRunning = false;
|
|
this.stopContributing();
|
|
this.stopHeartbeat();
|
|
|
|
if (this.reconnectTimer) {
|
|
clearTimeout(this.reconnectTimer);
|
|
this.reconnectTimer = null;
|
|
}
|
|
|
|
if (this.ws) {
|
|
this.ws.close();
|
|
this.ws = null;
|
|
}
|
|
|
|
// Save final state
|
|
saveState(this.state);
|
|
|
|
// Print session summary
|
|
const sessionDuration = (Date.now() - this.sessionStart) / 1000;
|
|
console.log(`\n${c.bold}Session Summary:${c.reset}`);
|
|
console.log(` ${c.cyan}Duration:${c.reset} ${Math.round(sessionDuration)}s`);
|
|
console.log(` ${c.cyan}Contributions:${c.reset} ${this.sessionContributions}`);
|
|
console.log(` ${c.cyan}Credits:${c.reset} ${this.sessionCredits.toFixed(4)} rUv`);
|
|
console.log(` ${c.cyan}Total Earned:${c.reset} ${this.state.totalCredits.toFixed(4)} rUv\n`);
|
|
|
|
// Remove PID file
|
|
const pidFile = getPidFile();
|
|
if (existsSync(pidFile)) {
|
|
unlinkSync(pidFile);
|
|
}
|
|
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
// Show daemon status
|
|
async function showStatus(opts) {
|
|
const pidFile = getPidFile();
|
|
const state = loadState();
|
|
const identity = await loadIdentity(opts);
|
|
|
|
console.log(`\n${c.cyan}${c.bold}Edge-Net Contribution Daemon Status${c.reset}\n`);
|
|
|
|
// Check if daemon is running
|
|
if (existsSync(pidFile)) {
|
|
const pid = parseInt(readFileSync(pidFile, 'utf-8').trim());
|
|
try {
|
|
process.kill(pid, 0); // Check if process exists
|
|
console.log(`${c.green}Status: Running${c.reset} (PID: ${pid})`);
|
|
} catch {
|
|
console.log(`${c.yellow}Status: Stale PID file${c.reset} (process not found)`);
|
|
unlinkSync(pidFile);
|
|
}
|
|
} else {
|
|
console.log(`${c.dim}Status: Not running${c.reset}`);
|
|
}
|
|
|
|
console.log(`\n${c.bold}Identity:${c.reset}`);
|
|
console.log(` ${c.cyan}Short ID:${c.reset} ${identity.shortId}`);
|
|
console.log(` ${c.cyan}Public Key:${c.reset} ${identity.publicKey.slice(0, 32)}...`);
|
|
|
|
console.log(`\n${c.bold}Statistics:${c.reset}`);
|
|
console.log(` ${c.cyan}Total Credits:${c.reset} ${state.totalCredits?.toFixed(4) || 0} rUv`);
|
|
console.log(` ${c.cyan}Total Contributions:${c.reset} ${state.totalContributions || 0}`);
|
|
console.log(` ${c.cyan}Total Time:${c.reset} ${state.totalSeconds || 0}s`);
|
|
|
|
if (state.lastSync) {
|
|
const lastSync = new Date(state.lastSync);
|
|
console.log(` ${c.cyan}Last Sync:${c.reset} ${lastSync.toLocaleString()}`);
|
|
}
|
|
|
|
console.log(`\n${c.bold}Files:${c.reset}`);
|
|
console.log(` ${c.dim}State:${c.reset} ${getStateFile()}`);
|
|
console.log(` ${c.dim}Log:${c.reset} ${getLogFile()}`);
|
|
console.log(` ${c.dim}PID:${c.reset} ${pidFile}\n`);
|
|
}
|
|
|
|
// Stop daemon
|
|
function stopDaemon() {
|
|
const pidFile = getPidFile();
|
|
|
|
if (!existsSync(pidFile)) {
|
|
console.log(`${c.yellow}Daemon is not running${c.reset}`);
|
|
return;
|
|
}
|
|
|
|
const pid = parseInt(readFileSync(pidFile, 'utf-8').trim());
|
|
|
|
try {
|
|
process.kill(pid, 'SIGTERM');
|
|
console.log(`${c.green}Sent SIGTERM to daemon (PID: ${pid})${c.reset}`);
|
|
|
|
// Wait a bit and check if it stopped
|
|
setTimeout(() => {
|
|
try {
|
|
process.kill(pid, 0);
|
|
console.log(`${c.yellow}Daemon still running, sending SIGKILL...${c.reset}`);
|
|
process.kill(pid, 'SIGKILL');
|
|
} catch {
|
|
console.log(`${c.green}Daemon stopped${c.reset}`);
|
|
}
|
|
if (existsSync(pidFile)) {
|
|
unlinkSync(pidFile);
|
|
}
|
|
}, 2000);
|
|
} catch {
|
|
console.log(`${c.yellow}Process not found, cleaning up...${c.reset}`);
|
|
unlinkSync(pidFile);
|
|
}
|
|
}
|
|
|
|
// Start daemon in background
|
|
function startDaemonBackground(args) {
|
|
const pidFile = getPidFile();
|
|
const logFile = getLogFile();
|
|
|
|
if (existsSync(pidFile)) {
|
|
const pid = parseInt(readFileSync(pidFile, 'utf-8').trim());
|
|
try {
|
|
process.kill(pid, 0);
|
|
console.log(`${c.yellow}Daemon already running (PID: ${pid})${c.reset}`);
|
|
console.log(`${c.dim}Use --stop to stop it first${c.reset}`);
|
|
return;
|
|
} catch {
|
|
unlinkSync(pidFile);
|
|
}
|
|
}
|
|
|
|
console.log(`${c.cyan}Starting daemon in background...${c.reset}`);
|
|
|
|
const out = require('fs').openSync(logFile, 'a');
|
|
const err = require('fs').openSync(logFile, 'a');
|
|
|
|
// Remove --daemon flag and respawn
|
|
const filteredArgs = args.filter(a => a !== '--daemon' && a !== '-d');
|
|
|
|
const child = spawn(process.execPath, [__filename, ...filteredArgs], {
|
|
detached: true,
|
|
stdio: ['ignore', out, err],
|
|
});
|
|
|
|
writeFileSync(pidFile, String(child.pid));
|
|
child.unref();
|
|
|
|
console.log(`${c.green}Daemon started (PID: ${child.pid})${c.reset}`);
|
|
console.log(`${c.dim}Log file: ${logFile}${c.reset}`);
|
|
console.log(`${c.dim}Use --status to check status${c.reset}`);
|
|
console.log(`${c.dim}Use --stop to stop daemon${c.reset}`);
|
|
}
|
|
|
|
// Print help
|
|
function printHelp() {
|
|
console.log(`
|
|
${c.cyan}${c.bold}Edge-Net Contribution Daemon${c.reset}
|
|
${c.dim}Contribute CPU to earn QDAG credits${c.reset}
|
|
|
|
${c.bold}USAGE:${c.reset}
|
|
npx @ruvector/edge-net contribute [options]
|
|
|
|
${c.bold}OPTIONS:${c.reset}
|
|
${c.yellow}--daemon, -d${c.reset} Run in background (detached)
|
|
${c.yellow}--stop${c.reset} Stop running daemon
|
|
${c.yellow}--status${c.reset} Show daemon status
|
|
${c.yellow}--cpu <percent>${c.reset} CPU usage limit (default: 50)
|
|
${c.yellow}--key <pubkey>${c.reset} Use specific public key
|
|
${c.yellow}--site <id>${c.reset} Site identifier (default: cli-contributor)
|
|
${c.yellow}--help, -h${c.reset} Show this help
|
|
|
|
${c.bold}EXAMPLES:${c.reset}
|
|
${c.dim}# Start contributing in foreground${c.reset}
|
|
$ npx @ruvector/edge-net contribute
|
|
|
|
${c.dim}# Start as background daemon with 30% CPU${c.reset}
|
|
$ npx @ruvector/edge-net contribute --daemon --cpu 30
|
|
|
|
${c.dim}# Use specific public key${c.reset}
|
|
$ npx @ruvector/edge-net contribute --key 38a3bcd1732fe04c...
|
|
|
|
${c.dim}# Check status${c.reset}
|
|
$ npx @ruvector/edge-net contribute --status
|
|
|
|
${c.dim}# Stop daemon${c.reset}
|
|
$ npx @ruvector/edge-net contribute --stop
|
|
|
|
${c.bold}HOW IT WORKS:${c.reset}
|
|
1. Daemon connects to Edge-Net relay server
|
|
2. Every 30 seconds, does real CPU work
|
|
3. Reports contribution to relay
|
|
4. Relay credits QDAG (Firestore) with earned rUv
|
|
5. Credits persist and sync across all devices
|
|
|
|
${c.bold}CREDIT RATE:${c.reset}
|
|
Base rate: ~0.047 rUv/second of contribution
|
|
Max rate: ~0.05 rUv/second (180 rUv/hour max)
|
|
Formula: contributionSeconds * 0.047 * (cpuUsage / 100)
|
|
`);
|
|
}
|
|
|
|
// Main entry point
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
|
|
// Filter out 'contribute' if passed
|
|
const filteredArgs = args.filter(a => a !== 'contribute');
|
|
const opts = parseArgs(filteredArgs);
|
|
|
|
if (opts.help) {
|
|
printHelp();
|
|
return;
|
|
}
|
|
|
|
if (opts.status) {
|
|
await showStatus(opts);
|
|
return;
|
|
}
|
|
|
|
if (opts.stop) {
|
|
stopDaemon();
|
|
return;
|
|
}
|
|
|
|
if (opts.daemon) {
|
|
startDaemonBackground(filteredArgs);
|
|
return;
|
|
}
|
|
|
|
// Start daemon in foreground
|
|
try {
|
|
const identity = await loadIdentity(opts);
|
|
const daemon = new ContributionDaemon(identity, opts.cpu);
|
|
await daemon.start();
|
|
} catch (err) {
|
|
console.error(`${c.red}Error: ${err.message}${c.reset}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error(`${c.red}Fatal error: ${err.message}${c.reset}`);
|
|
process.exit(1);
|
|
});
|