idle-node 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/idle-node.js +55 -0
- package/package.json +35 -0
- package/src/node.js +148 -0
- package/src/status.js +56 -0
package/bin/idle-node.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { startNode } from '../src/node.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('idle-node')
|
|
10
|
+
.description('IDLE Protocol node — earn from idle compute')
|
|
11
|
+
.version('0.1.0');
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.command('start')
|
|
15
|
+
.description('Start the IDLE node and begin processing jobs')
|
|
16
|
+
.requiredOption('--wallet <address>', 'Solana wallet address for payouts')
|
|
17
|
+
.option('--types <types>', 'Comma-separated job types to accept (default: fetch,validate)', 'fetch,validate')
|
|
18
|
+
.option('--poll-interval <ms>', 'Job poll interval in milliseconds', '5000')
|
|
19
|
+
.option('--max-concurrent <n>', 'Max concurrent jobs', '3')
|
|
20
|
+
.option('--supabase-url <url>', 'Supabase project URL', process.env.IDLE_SUPABASE_URL || 'https://vnhzyynewdtfpiynaaqd.supabase.co')
|
|
21
|
+
.option('--supabase-key <key>', 'Supabase anon key', process.env.IDLE_SUPABASE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZuaHp5eW5ld2R0ZnBpeW5hYXFkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY5NzgyMDksImV4cCI6MjA5MjU1NDIwOX0.6Sw8p9wnfx67NjhzJGmPWialMG4517Eubj9YyfHPaJc')
|
|
22
|
+
.action(async (opts) => {
|
|
23
|
+
console.log(chalk.green('IDLE Node v0.1.0'));
|
|
24
|
+
console.log(chalk.gray(`Wallet: ${opts.wallet}`));
|
|
25
|
+
console.log(chalk.gray(`Types: ${opts.types}`));
|
|
26
|
+
console.log(chalk.gray(`Poll interval: ${opts.pollInterval}ms`));
|
|
27
|
+
console.log('');
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await startNode({
|
|
31
|
+
wallet: opts.wallet,
|
|
32
|
+
types: opts.types.split(','),
|
|
33
|
+
pollInterval: parseInt(opts.pollInterval),
|
|
34
|
+
maxConcurrent: parseInt(opts.maxConcurrent),
|
|
35
|
+
supabaseUrl: opts.supabaseUrl,
|
|
36
|
+
supabaseKey: opts.supabaseKey,
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(chalk.red('Fatal:'), err.message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
program
|
|
45
|
+
.command('status')
|
|
46
|
+
.description('Check node status and earnings')
|
|
47
|
+
.requiredOption('--wallet <address>', 'Solana wallet address')
|
|
48
|
+
.option('--supabase-url <url>', 'Supabase project URL', process.env.IDLE_SUPABASE_URL || 'https://vnhzyynewdtfpiynaaqd.supabase.co')
|
|
49
|
+
.option('--supabase-key <key>', 'Supabase anon key', process.env.IDLE_SUPABASE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZuaHp5eW5ld2R0ZnBpeW5hYXFkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY5NzgyMDksImV4cCI6MjA5MjU1NDIwOX0.6Sw8p9wnfx67NjhzJGmPWialMG4517Eubj9YyfHPaJc')
|
|
50
|
+
.action(async (opts) => {
|
|
51
|
+
const { checkStatus } = await import('../src/status.js');
|
|
52
|
+
await checkStatus(opts);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "idle-node",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "IDLE Protocol node — turn idle compute into revenue",
|
|
5
|
+
"main": "./src/node.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"idle-node": "bin/idle-node.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"idle",
|
|
19
|
+
"solana",
|
|
20
|
+
"compute",
|
|
21
|
+
"earnings",
|
|
22
|
+
"decentralized",
|
|
23
|
+
"pay.sh"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/georgehspirit-ctrl/vera-edge-pro"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@supabase/supabase-js": "^2.45.0",
|
|
32
|
+
"commander": "^12.1.0",
|
|
33
|
+
"chalk": "^5.3.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/node.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
const EXECUTORS = {
|
|
5
|
+
fetch: async (payload) => {
|
|
6
|
+
const { url, method = 'GET' } = payload;
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
const res = await fetch(url, { method });
|
|
9
|
+
const body = await res.text();
|
|
10
|
+
return {
|
|
11
|
+
status: res.status,
|
|
12
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
13
|
+
body: body.slice(0, 10000),
|
|
14
|
+
latency_ms: Date.now() - start,
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
validate: async (payload) => {
|
|
19
|
+
const { data, schema } = payload;
|
|
20
|
+
// Basic JSON schema validation
|
|
21
|
+
const errors = [];
|
|
22
|
+
if (schema?.required) {
|
|
23
|
+
for (const field of schema.required) {
|
|
24
|
+
if (!(field in (data || {}))) errors.push(`missing required field: ${field}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { valid: errors.length === 0, errors };
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
query: async (payload) => {
|
|
31
|
+
// Natural language query placeholder — real implementation
|
|
32
|
+
// would connect to user's configured data source
|
|
33
|
+
return { error: 'query execution requires data source configuration' };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export async function startNode(config) {
|
|
38
|
+
if (!config.supabaseUrl || !config.supabaseKey) {
|
|
39
|
+
throw new Error('Supabase URL and key required. Set IDLE_SUPABASE_URL and IDLE_SUPABASE_KEY env vars, or pass --supabase-url and --supabase-key.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const supabase = createClient(config.supabaseUrl, config.supabaseKey);
|
|
43
|
+
let running = 0;
|
|
44
|
+
let totalProcessed = 0;
|
|
45
|
+
let totalEarned = 0;
|
|
46
|
+
|
|
47
|
+
console.log(chalk.green('Node started. Polling for jobs...'));
|
|
48
|
+
|
|
49
|
+
const poll = async () => {
|
|
50
|
+
if (running >= config.maxConcurrent) return;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Claim a pending job atomically
|
|
54
|
+
const { data: jobs, error } = await supabase
|
|
55
|
+
.rpc('idle_claim_node_job', {
|
|
56
|
+
p_node_wallet: config.wallet,
|
|
57
|
+
p_job_types: config.types,
|
|
58
|
+
p_limit: config.maxConcurrent - running,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (error) {
|
|
62
|
+
console.error(chalk.red('Poll error:'), error.message);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!jobs || jobs.length === 0) return;
|
|
67
|
+
|
|
68
|
+
for (const job of jobs) {
|
|
69
|
+
running++;
|
|
70
|
+
processJob(supabase, job, config.wallet).then((earned) => {
|
|
71
|
+
running--;
|
|
72
|
+
totalProcessed++;
|
|
73
|
+
totalEarned += earned;
|
|
74
|
+
if (totalProcessed % 10 === 0) {
|
|
75
|
+
console.log(chalk.cyan(`[stats] ${totalProcessed} jobs | $${totalEarned.toFixed(4)} earned`));
|
|
76
|
+
}
|
|
77
|
+
}).catch((err) => {
|
|
78
|
+
running--;
|
|
79
|
+
console.error(chalk.red(`Job ${job.id} failed:`), err.message);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(chalk.red('Poll error:'), err.message);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Poll loop
|
|
88
|
+
const interval = setInterval(poll, config.pollInterval);
|
|
89
|
+
|
|
90
|
+
// Graceful shutdown
|
|
91
|
+
const shutdown = () => {
|
|
92
|
+
console.log(chalk.yellow('\nShutting down...'));
|
|
93
|
+
clearInterval(interval);
|
|
94
|
+
console.log(chalk.green(`Processed ${totalProcessed} jobs, earned $${totalEarned.toFixed(4)}`));
|
|
95
|
+
process.exit(0);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
process.on('SIGINT', shutdown);
|
|
99
|
+
process.on('SIGTERM', shutdown);
|
|
100
|
+
|
|
101
|
+
// Initial poll
|
|
102
|
+
await poll();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function processJob(supabase, job, wallet) {
|
|
106
|
+
const executor = EXECUTORS[job.type];
|
|
107
|
+
if (!executor) {
|
|
108
|
+
await supabase
|
|
109
|
+
.from('idle_jobs')
|
|
110
|
+
.update({ status: 'failed', result: { error: `unsupported type: ${job.type}` } })
|
|
111
|
+
.eq('id', job.id);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const start = Date.now();
|
|
116
|
+
console.log(chalk.gray(`[${job.type}] Processing ${job.id.slice(0, 8)}...`));
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const result = await executor(job.payload);
|
|
120
|
+
const duration = Date.now() - start;
|
|
121
|
+
|
|
122
|
+
await supabase
|
|
123
|
+
.from('idle_jobs')
|
|
124
|
+
.update({
|
|
125
|
+
status: 'completed',
|
|
126
|
+
result,
|
|
127
|
+
completed_at: new Date().toISOString(),
|
|
128
|
+
node_wallet: wallet,
|
|
129
|
+
duration_ms: duration,
|
|
130
|
+
})
|
|
131
|
+
.eq('id', job.id);
|
|
132
|
+
|
|
133
|
+
const earned = job.node_payout_usd || 0;
|
|
134
|
+
console.log(chalk.green(`[${job.type}] ${job.id.slice(0, 8)} done (${duration}ms) +$${earned.toFixed(4)}`));
|
|
135
|
+
return earned;
|
|
136
|
+
} catch (err) {
|
|
137
|
+
await supabase
|
|
138
|
+
.from('idle_jobs')
|
|
139
|
+
.update({
|
|
140
|
+
status: 'failed',
|
|
141
|
+
result: { error: err.message },
|
|
142
|
+
completed_at: new Date().toISOString(),
|
|
143
|
+
node_wallet: wallet,
|
|
144
|
+
})
|
|
145
|
+
.eq('id', job.id);
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
}
|
package/src/status.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
export async function checkStatus(opts) {
|
|
5
|
+
if (!opts.supabaseUrl || !opts.supabaseKey) {
|
|
6
|
+
console.error(chalk.red('Supabase URL and key required.'));
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const supabase = createClient(opts.supabaseUrl, opts.supabaseKey);
|
|
11
|
+
|
|
12
|
+
// Get jobs for this wallet
|
|
13
|
+
const { data: jobs, error } = await supabase
|
|
14
|
+
.from('idle_jobs')
|
|
15
|
+
.select('status, node_payout_usd, type, completed_at')
|
|
16
|
+
.eq('node_wallet', opts.wallet)
|
|
17
|
+
.order('completed_at', { ascending: false })
|
|
18
|
+
.limit(100);
|
|
19
|
+
|
|
20
|
+
if (error) {
|
|
21
|
+
console.error(chalk.red('Error:'), error.message);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!jobs || jobs.length === 0) {
|
|
26
|
+
console.log(chalk.yellow('No jobs found for this wallet.'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const completed = jobs.filter(j => j.status === 'completed');
|
|
31
|
+
const paid = jobs.filter(j => j.status === 'paid');
|
|
32
|
+
const failed = jobs.filter(j => j.status === 'failed');
|
|
33
|
+
|
|
34
|
+
const totalEarned = [...completed, ...paid].reduce((sum, j) => sum + (j.node_payout_usd || 0), 0);
|
|
35
|
+
const totalPaid = paid.reduce((sum, j) => sum + (j.node_payout_usd || 0), 0);
|
|
36
|
+
const pending = totalEarned - totalPaid;
|
|
37
|
+
|
|
38
|
+
console.log(chalk.green('IDLE Node Status'));
|
|
39
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
40
|
+
console.log(`Wallet: ${opts.wallet}`);
|
|
41
|
+
console.log(`Completed: ${completed.length + paid.length} jobs`);
|
|
42
|
+
console.log(`Failed: ${failed.length} jobs`);
|
|
43
|
+
console.log(`Earned: $${totalEarned.toFixed(4)}`);
|
|
44
|
+
console.log(`Paid out: $${totalPaid.toFixed(4)}`);
|
|
45
|
+
console.log(`Pending: $${pending.toFixed(4)}`);
|
|
46
|
+
|
|
47
|
+
// Type breakdown
|
|
48
|
+
const byType = {};
|
|
49
|
+
for (const j of [...completed, ...paid]) {
|
|
50
|
+
byType[j.type] = (byType[j.type] || 0) + 1;
|
|
51
|
+
}
|
|
52
|
+
console.log(chalk.gray('\nBy type:'));
|
|
53
|
+
for (const [type, count] of Object.entries(byType)) {
|
|
54
|
+
console.log(` ${type}: ${count}`);
|
|
55
|
+
}
|
|
56
|
+
}
|