heraldiis/nara-mining-template
0
1#!/usr/bin/env node2/**3 * NARA PoMI Mining Loop - HF Space Template4 * - Fetch quest every 3s5 * - When new round detected with slots available → AI answer → submit immediately6 */7 8const { execSync, spawn } = require('child_process');9const http = require('http');10const https = require('https');11const fs = require('fs');12const path = require('path');13 14const WALLET = process.env.WALLET_PATH || '/home/user/app/wallets/W1.json';15const LOG_DIR = process.env.LOG_DIR || '/home/user/app/logs';16 17const AI_BASE_URL = process.env.AI_BASE_URL || '';18const AI_API_KEY = process.env.AI_API_KEY || '';19const AI_MODEL = process.env.AI_MODEL || 'gpt-4';20const AGENT_MODEL = process.env.NARA_AGENT_MODEL || AI_MODEL || 'gpt-4';21const POLL_INTERVAL = 3000;22const WALLET_NAME = process.env.WALLET_NAME || 'W1';23 24let lastRound = null;25let submittedRounds = new Set();26 27// Check if wallet exists, if not wait for it28function checkWallet() {29 if (!fs.existsSync(WALLET)) {30 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] Wallet not found: ${WALLET}`);31 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] Waiting for wallet file...`);32 return false;33 }34 return true;35}36 37function logToFile(msg) {38 const timestamp = new Date().toISOString();39 const entry = `[${timestamp}] [${WALLET_NAME}] ${msg}`;40 console.log(entry);41 42 const logFile = path.join(LOG_DIR, `${WALLET_NAME}.log`);43 try {44 if (!fs.existsSync(LOG_DIR)) {45 fs.mkdirSync(LOG_DIR, { recursive: true });46 }47 fs.appendFileSync(logFile, entry + '\n');48 } catch (e) {49 console.error(`[${WALLET_NAME}] Log write error:`, e.message);50 }51}52 53async function askAI(question) {54 return new Promise((resolve, reject) => {55 if (!AI_BASE_URL) {56 return reject(new Error('AI_BASE_URL not set'));57 }58 59 const isMultiChoice = /\nA\.|\nA\)/.test(question);60 let systemPrompt, userContent;61 62 if (isMultiChoice) {63 systemPrompt = 'You answer multiple choice questions with ONLY the letter (A, B, C, or D). No explanation.';64 userContent = question;65 } else {66 systemPrompt = 'You answer with ONLY the shortest possible exact answer.';67 userContent = question;68 }69 70 const body = JSON.stringify({71 model: AI_MODEL,72 messages: [73 { role: 'system', content: systemPrompt },74 { role: 'user', content: userContent }75 ],76 temperature: 0,77 max_tokens: 1078 });79 80 const base = new URL(AI_BASE_URL);81 const isHttps = base.protocol === 'https:';82 const client = isHttps ? https : http;83 const pathBase = base.pathname.endsWith('/') ? base.pathname.slice(0, -1) : base.pathname;84 const reqPath = `${pathBase}/chat/completions`;85 86 const req = client.request({87 protocol: base.protocol,88 hostname: base.hostname,89 port: base.port || (isHttps ? 443 : 80),90 path: reqPath,91 method: 'POST',92 headers: {93 'Content-Type': 'application/json',94 'Authorization': `Bearer ${AI_API_KEY}`,95 'Content-Length': Buffer.byteLength(body)96 }97 }, (res) => {98 let data = '';99 res.on('data', d => data += d);100 res.on('end', () => {101 try {102 const parsed = JSON.parse(data || '{}');103 const answer = parsed.choices?.[0]?.message?.content?.trim();104 if (!answer) {105 return reject(new Error('AI empty response'));106 }107 resolve(answer);108 } catch (e) {109 reject(new Error('AI parse error'));110 }111 });112 });113 req.on('error', reject);114 req.write(body);115 req.end();116 });117}118 119function getQuest() {120 try {121 const out = execSync(`npx naracli quest get --json --wallet "${WALLET}"`, { timeout: 15000 }).toString();122 return JSON.parse(out);123 } catch (e) {124 return null;125 }126}127 128function submitAnswer(answer) {129 return new Promise((resolve) => {130 const proc = spawn('npx', [131 'naracli', 'quest', 'answer', answer,132 '--relay',133 '--agent', 'openclaw',134 '--model', AGENT_MODEL,135 '--wallet', WALLET136 ], { timeout: 60000 });137 138 let out = '';139 proc.stdout.on('data', d => out += d);140 proc.stderr.on('data', d => out += d);141 proc.on('close', (code) => resolve({ code, out }));142 });143}144 145async function loop() {146 try {147 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] NARA mining loop started`);148 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] Wallet: ${WALLET}`);149 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] AI_BASE_URL: ${AI_BASE_URL || 'NOT SET'}`);150 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] AI_MODEL: ${AI_MODEL || 'NOT SET'}`);151 152 // Check if AI_BASE_URL is set153 if (!AI_BASE_URL) {154 console.error(`[${new Date().toISOString()}] [${WALLET_NAME}] ERROR: AI_BASE_URL not set!`);155 console.error(`[${new Date().toISOString()}] [${WALLET_NAME}] Please set AI_BASE_URL in Settings → Secrets`);156 while (true) {157 await new Promise(r => setTimeout(r, 30000)); // Wait forever158 }159 }160 161 // Wait for wallet file to exist162 while (!checkWallet()) {163 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] Waiting for wallet...`);164 await new Promise(r => setTimeout(r, 10000));165 }166 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] Wallet found! Starting mining...`);167 168 while (true) {169 try {170 const quest = getQuest();171 if (!quest) {172 await new Promise(r => setTimeout(r, POLL_INTERVAL));173 continue;174 }175 176 const round = quest.round;177 const slots = quest.remainingRewardSlots;178 const question = quest.question;179 180 if (round !== lastRound) {181 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] New round: ${round} | slots: ${slots}`);182 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] Q: ${question.slice(0, 100)}`);183 lastRound = round;184 }185 186 if (slots > 0 && !submittedRounds.has(round)) {187 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] Slots available (${slots})! Getting AI answer...`);188 submittedRounds.add(round);189 190 let answer;191 try {192 answer = await askAI(question);193 if (!answer || answer.trim() === '') {194 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] AI returned empty, skipping`);195 submittedRounds.delete(round);196 await new Promise(r => setTimeout(r, POLL_INTERVAL));197 continue;198 }199 answer = answer.trim().replace(/^["']|["']$/g, '');200 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] AI answer: "${answer}"`);201 } catch (e) {202 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] AI error: ${e.message}`);203 submittedRounds.delete(round);204 await new Promise(r => setTimeout(r, POLL_INTERVAL));205 continue;206 }207 208 const { code, out } = await submitAnswer(answer);209 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] Submit result (${code}): ${out.trim().substring(0, 200)}`);210 211 if (out.includes('Congratulations! Reward')) {212 const rewardMatch = out.match(/received: ([\d.]+) NARA/);213 const reward = rewardMatch ? rewardMatch[1] : '?';214 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] WIN - ${reward} NARA`);215 } else if (out.includes('Correct but no slot')) {216 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] No Slot`);217 } else if (out.includes('Wrong answer')) {218 console.log(`[${new Date().toISOString()}] [${WALLET_NAME}] Wrong`);219 }220 }221 } catch (e) {222 console.error(`[${new Date().toISOString()}] [${WALLET_NAME}] Loop error: ${e.message}`);223 }224 225 await new Promise(r => setTimeout(r, POLL_INTERVAL));226 }227 } catch (err) {228 console.error(`[${new Date().toISOString()}] [${WALLET_NAME}] FATAL LOOP ERROR: ${err.message}`);229 console.error(err.stack);230 throw err;231 }232}233 234// Start with error handling235loop().catch(err => {236 console.error(`[${WALLET_NAME}] Unhandled error: ${err.message}`);237 process.exit(1);238});239 