lightnode-sdk 0.4.8 → 0.5.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/dist/bridge.d.ts +233 -0
- package/dist/bridge.js +201 -0
- package/dist/chat.d.ts +70 -0
- package/dist/chat.js +88 -0
- package/dist/cli.js +202 -2
- package/dist/crypto.js +6 -1
- package/dist/dao.d.ts +439 -0
- package/dist/dao.js +234 -0
- package/dist/index.d.ts +31 -4
- package/dist/index.js +63 -4
- package/dist/inference.d.ts +34 -0
- package/dist/inference.js +107 -6
- package/dist/onchain-models.d.ts +380 -0
- package/dist/onchain-models.js +187 -0
- package/dist/subgraph.d.ts +2 -0
- package/dist/subgraph.js +6 -0
- package/dist/worker.d.ts +104 -0
- package/dist/worker.js +186 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { LightNode, modelStatsCsv, workerStatsCsv, workerJobsCsv } from "./index.js";
|
|
2
|
+
import { LightNode, modelStatsCsv, workerStatsCsv, workerJobsCsv, runInferenceWithKey, isStalledWorker, workerPreflight, workerWatch, BRIDGE_ROUTE, DAO, DAO_ADDRESSES } from "./index.js";
|
|
3
3
|
import { addInference, addAnalyticsDashboard, addNftMint, addChat, addAgent } from "./add.js";
|
|
4
|
+
import { createPublicClient, http, parseEther } from "viem";
|
|
5
|
+
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
|
|
4
6
|
function flag(name) {
|
|
5
7
|
const i = process.argv.indexOf(name);
|
|
6
8
|
return i >= 0 ? process.argv[i + 1] : undefined;
|
|
@@ -17,15 +19,38 @@ const lcai = (wei) => (wei ? Number(BigInt(wei)) / 1e18 : 0);
|
|
|
17
19
|
const rate = (r) => (r == null ? "-" : `${Math.round(r * 100)}%`);
|
|
18
20
|
const HELP = `lightnode <command> [--net mainnet|testnet]
|
|
19
21
|
|
|
22
|
+
Run one inference (needs PRIVATE_KEY in env):
|
|
23
|
+
chat <prompt> stream one encrypted inference answer to stdout
|
|
24
|
+
([--model llama3-8b] [--key 0x...])
|
|
25
|
+
|
|
26
|
+
Wallet helpers:
|
|
27
|
+
wallet new generate a fresh testnet key, print it
|
|
28
|
+
wallet address print the address of PRIVATE_KEY
|
|
29
|
+
wallet balance [--net] print LCAI balance for PRIVATE_KEY's address
|
|
30
|
+
|
|
31
|
+
Read-only network commands (no key):
|
|
20
32
|
network network summary (workers, jobs, models, earnings)
|
|
21
33
|
models registered models + per-job fee
|
|
22
34
|
worker <addr> a worker: on-chain registration + recent jobs
|
|
35
|
+
worker watch <addr> poll worker status, print event on change
|
|
36
|
+
([--interval 30] [--stale 90])
|
|
23
37
|
jobs <addr> [--csv] one worker's job history (table or CSV)
|
|
38
|
+
job <jobId> one job's status (category, refundable, worker, timings)
|
|
24
39
|
registered <addr> true | false | null (read from chain events)
|
|
25
40
|
fee [model] on-chain inference fee (default llama3-8b)
|
|
26
41
|
analytics [--csv] per-model performance (completion, p50/p95, incomplete)
|
|
27
42
|
reliability [--csv] per-worker reliability, busiest first
|
|
28
43
|
|
|
44
|
+
Preflight (needs PRIVATE_KEY in env):
|
|
45
|
+
worker preflight run one real test inference, print verdict + timings
|
|
46
|
+
([--key 0x...] [--model llama3-8b] [--deadline 60])
|
|
47
|
+
|
|
48
|
+
Ecosystem (read-only):
|
|
49
|
+
bridge addresses print bridge route (Ethereum <-> LightChain) addresses
|
|
50
|
+
dao addresses print LCAI Governor + Timelock + Treasury addresses
|
|
51
|
+
dao config print voting delay / period / threshold (live read)
|
|
52
|
+
|
|
53
|
+
Scaffold templates into the current project:
|
|
29
54
|
add inference end-to-end encrypted inference route/script
|
|
30
55
|
add chat chat-style UI with conversation history
|
|
31
56
|
add agent scheduled/loop inference (cron-style)
|
|
@@ -34,9 +59,93 @@ const HELP = `lightnode <command> [--net mainnet|testnet]
|
|
|
34
59
|
(all add commands: [--template auto|nextjs-api|hono|node] [--force])
|
|
35
60
|
|
|
36
61
|
To scaffold a new project instead, run: npm create lightnode-app my-app`;
|
|
62
|
+
function pickKey() {
|
|
63
|
+
const k = flag("--key") ?? process.env.PRIVATE_KEY;
|
|
64
|
+
if (!k || !k.startsWith("0x") || k.length !== 66) {
|
|
65
|
+
die("set PRIVATE_KEY=0x... in your env, or pass --key 0x... (need a funded EVM key)");
|
|
66
|
+
}
|
|
67
|
+
return k;
|
|
68
|
+
}
|
|
37
69
|
async function main() {
|
|
38
70
|
const ln = new LightNode(net);
|
|
39
71
|
switch (cmd) {
|
|
72
|
+
case "chat": {
|
|
73
|
+
// One-shot encrypted inference straight from the CLI. Pipe the prompt as
|
|
74
|
+
// positional args (or read from stdin if there are none) so this composes
|
|
75
|
+
// with shell scripts: `cat doc.md | lightnode chat` works.
|
|
76
|
+
const inlinePrompt = positionals.slice(1).join(" ").trim();
|
|
77
|
+
const prompt = inlinePrompt ||
|
|
78
|
+
(await new Promise((resolve) => {
|
|
79
|
+
let buf = "";
|
|
80
|
+
process.stdin.setEncoding("utf8");
|
|
81
|
+
process.stdin.on("data", (d) => (buf += d));
|
|
82
|
+
process.stdin.on("end", () => resolve(buf.trim()));
|
|
83
|
+
}));
|
|
84
|
+
if (!prompt)
|
|
85
|
+
die("usage: lightnode chat <prompt> (or pipe the prompt to stdin)");
|
|
86
|
+
const model = flag("--model") ?? "llama3-8b";
|
|
87
|
+
const privateKey = pickKey();
|
|
88
|
+
try {
|
|
89
|
+
const { answer, txs, worker, jobId } = await runInferenceWithKey({
|
|
90
|
+
network: net,
|
|
91
|
+
privateKey,
|
|
92
|
+
prompt,
|
|
93
|
+
model,
|
|
94
|
+
onChunk: (chunk) => process.stdout.write(chunk),
|
|
95
|
+
});
|
|
96
|
+
process.stdout.write("\n");
|
|
97
|
+
// Tiny one-liner trailer so the receipt is reachable without burying
|
|
98
|
+
// the answer. JSON is grep-friendly for shell pipelines.
|
|
99
|
+
const explorer = ln.network.explorer;
|
|
100
|
+
process.stderr.write(JSON.stringify({
|
|
101
|
+
chars: answer.length,
|
|
102
|
+
worker,
|
|
103
|
+
jobId: jobId.toString(),
|
|
104
|
+
createSession: `${explorer}/tx/${txs.createSession}`,
|
|
105
|
+
submitJob: `${explorer}/tx/${txs.submitJob}`,
|
|
106
|
+
jobCompleted: txs.jobCompleted ? `${explorer}/tx/${txs.jobCompleted}` : null,
|
|
107
|
+
}) + "\n");
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
if (isStalledWorker(e))
|
|
111
|
+
die("3 workers stalled in a row. Protocol refunds the fees; try again later.");
|
|
112
|
+
die("inference failed: " + e.message);
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "wallet": {
|
|
117
|
+
const sub = positionals[1];
|
|
118
|
+
if (sub === "new") {
|
|
119
|
+
// Fresh testnet-shaped key. Plain stdout output so it's copy-pasteable
|
|
120
|
+
// out of a script: `lightnode wallet new --quiet | head -1` works.
|
|
121
|
+
const pk = generatePrivateKey();
|
|
122
|
+
const addr = privateKeyToAccount(pk).address;
|
|
123
|
+
console.log(`PRIVATE_KEY=${pk}`);
|
|
124
|
+
console.error(`# address: ${addr}`);
|
|
125
|
+
console.error(`# fund at https://lightfaucet.ai before running paid commands`);
|
|
126
|
+
}
|
|
127
|
+
else if (sub === "address") {
|
|
128
|
+
const pk = pickKey();
|
|
129
|
+
console.log(privateKeyToAccount(pk).address);
|
|
130
|
+
}
|
|
131
|
+
else if (sub === "balance") {
|
|
132
|
+
const pk = pickKey();
|
|
133
|
+
const addr = privateKeyToAccount(pk).address;
|
|
134
|
+
const pub = createPublicClient({ transport: http(ln.network.rpc) });
|
|
135
|
+
const bal = await pub.getBalance({ address: addr });
|
|
136
|
+
const lcaiVal = Number(bal) / 1e18;
|
|
137
|
+
console.log(`${lcaiVal} LCAI`);
|
|
138
|
+
if (bal < parseEther("0.05")) {
|
|
139
|
+
console.error(`# under 0.05 LCAI - too low to run one inference`);
|
|
140
|
+
if (net === "testnet")
|
|
141
|
+
console.error(`# get free testnet LCAI: https://lightfaucet.ai`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
die("usage: lightnode wallet <new|address|balance> [--net testnet|mainnet]");
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
40
149
|
case "network": {
|
|
41
150
|
console.log(JSON.stringify(await ln.getNetworkAnalytics(), null, 2));
|
|
42
151
|
break;
|
|
@@ -48,11 +157,63 @@ async function main() {
|
|
|
48
157
|
break;
|
|
49
158
|
}
|
|
50
159
|
case "worker": {
|
|
51
|
-
|
|
160
|
+
// Two sub-shapes: `lightnode worker <addr>` (one-shot status) and
|
|
161
|
+
// `lightnode worker watch <addr>` (long-running event stream) and
|
|
162
|
+
// `lightnode worker preflight` (submit a test inference).
|
|
163
|
+
const sub = positionals[1];
|
|
164
|
+
if (sub === "watch") {
|
|
165
|
+
const addr = positionals[2] ?? die("usage: lightnode worker watch <address> [--interval 30] [--stale 90]");
|
|
166
|
+
const intervalSec = Number(flag("--interval") ?? "30");
|
|
167
|
+
const staleSecs = Number(flag("--stale") ?? "90");
|
|
168
|
+
const handle = workerWatch(ln, addr, { intervalMs: intervalSec * 1000, staleSecs });
|
|
169
|
+
process.on("SIGINT", () => {
|
|
170
|
+
handle.stop();
|
|
171
|
+
process.exit(0);
|
|
172
|
+
});
|
|
173
|
+
for await (const event of handle.events) {
|
|
174
|
+
console.log(JSON.stringify(event));
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
if (sub === "preflight") {
|
|
179
|
+
const privateKey = pickKey();
|
|
180
|
+
const model = flag("--model") ?? "llama3-8b";
|
|
181
|
+
const deadlineMs = Number(flag("--deadline") ?? "60") * 1000;
|
|
182
|
+
console.error(`> preflight against ${net} (model=${model}, deadline=${deadlineMs / 1000}s)...`);
|
|
183
|
+
const r = await workerPreflight({ network: net, privateKey, model, deadlineMs });
|
|
184
|
+
const explorer = ln.network.explorer;
|
|
185
|
+
console.log(JSON.stringify({
|
|
186
|
+
verdict: r.verdict,
|
|
187
|
+
elapsedSec: Math.round(r.elapsedMs / 100) / 10,
|
|
188
|
+
worker: r.worker,
|
|
189
|
+
summary: r.summary,
|
|
190
|
+
txs: {
|
|
191
|
+
createSession: r.txs.createSession ? `${explorer}/tx/${r.txs.createSession}` : null,
|
|
192
|
+
submitJob: r.txs.submitJob ? `${explorer}/tx/${r.txs.submitJob}` : null,
|
|
193
|
+
jobCompleted: r.txs.jobCompleted ? `${explorer}/tx/${r.txs.jobCompleted}` : null,
|
|
194
|
+
},
|
|
195
|
+
error: r.error,
|
|
196
|
+
}, null, 2));
|
|
197
|
+
if (r.verdict === "failed" || r.verdict === "stalled")
|
|
198
|
+
process.exit(1);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
// Default: one-shot worker summary by address.
|
|
202
|
+
const addr = sub ?? die("usage: lightnode worker <address|watch|preflight> [...]");
|
|
52
203
|
const [w, registered, jobs] = await Promise.all([ln.getWorker(addr), ln.isRegistered(addr), ln.getWorkerJobs(addr, 5)]);
|
|
53
204
|
console.log(JSON.stringify({ onchainRegistered: registered, worker: w, recentJobs: jobs.map((j) => ({ id: j.id, state: j.state })) }, null, 2));
|
|
54
205
|
break;
|
|
55
206
|
}
|
|
207
|
+
case "job": {
|
|
208
|
+
const id = positionals[1] ?? die("usage: lightnode job <jobId> [--net testnet]");
|
|
209
|
+
const status = await ln.getJobStatus(id);
|
|
210
|
+
if (!status) {
|
|
211
|
+
console.log(JSON.stringify({ jobId: id, status: "not-indexed" }, null, 2));
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
console.log(JSON.stringify(status, null, 2));
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
56
217
|
case "jobs": {
|
|
57
218
|
const addr = positionals[1] ?? die("usage: lightnode jobs <address> [--csv] [--net testnet]");
|
|
58
219
|
const jobs = await ln.getWorkerJobs(addr, 100);
|
|
@@ -99,6 +260,45 @@ async function main() {
|
|
|
99
260
|
}
|
|
100
261
|
break;
|
|
101
262
|
}
|
|
263
|
+
case "bridge": {
|
|
264
|
+
const sub = positionals[1];
|
|
265
|
+
if (sub === "addresses") {
|
|
266
|
+
console.log(JSON.stringify(BRIDGE_ROUTE, null, 2));
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
die("usage: lightnode bridge <addresses>");
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case "dao": {
|
|
273
|
+
const sub = positionals[1];
|
|
274
|
+
if (sub === "addresses") {
|
|
275
|
+
console.log(JSON.stringify(DAO_ADDRESSES.ethereum, null, 2));
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
if (sub === "config") {
|
|
279
|
+
// Live read against Ethereum mainnet. We use viem's HTTP transport
|
|
280
|
+
// via a minimal inline client (no ethers dep). This is the only
|
|
281
|
+
// ecosystem read that needs a live RPC, so we wire it lazily.
|
|
282
|
+
const { createPublicClient, http } = await import("viem");
|
|
283
|
+
const ethRpc = flag("--rpc") ?? "https://eth.llamarpc.com";
|
|
284
|
+
const pub = createPublicClient({ transport: http(ethRpc) });
|
|
285
|
+
// The DAO ctor accepts a structurally-typed MinimalPublicClient; viem's
|
|
286
|
+
// PublicClient satisfies it. The unknown cast is the standard SDK pattern
|
|
287
|
+
// for keeping the public API free of viem generic noise.
|
|
288
|
+
const dao = new DAO(pub, "ethereum");
|
|
289
|
+
const cfg = await dao.config();
|
|
290
|
+
console.log(JSON.stringify({
|
|
291
|
+
votingDelayBlocks: cfg.votingDelayBlocks.toString(),
|
|
292
|
+
votingPeriodBlocks: cfg.votingPeriodBlocks.toString(),
|
|
293
|
+
votingPeriodSecs: cfg.votingPeriodSecs,
|
|
294
|
+
proposalThresholdLcai: Number(cfg.proposalThresholdWei) / 1e18,
|
|
295
|
+
addresses: dao.addresses,
|
|
296
|
+
}, null, 2));
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
die("usage: lightnode dao <addresses|config> [--rpc <ethereum-rpc>]");
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
102
302
|
case "add": {
|
|
103
303
|
const sub = positionals[1];
|
|
104
304
|
const template = flag("--template") ?? "auto";
|
package/dist/crypto.js
CHANGED
|
@@ -44,7 +44,12 @@ async function getRng() {
|
|
|
44
44
|
return bound;
|
|
45
45
|
}
|
|
46
46
|
try {
|
|
47
|
-
|
|
47
|
+
// The /* webpackIgnore: true */ magic comment stops Next.js / webpack
|
|
48
|
+
// from trying to bundle node:crypto for the browser. In a real browser
|
|
49
|
+
// we never reach this line (globalThis.crypto is available), so the
|
|
50
|
+
// import is dead code there - but webpack analyzes it statically and
|
|
51
|
+
// errors on the `node:` URI scheme without the hint.
|
|
52
|
+
const mod = (await import(/* webpackIgnore: true */ "node:crypto"));
|
|
48
53
|
const wc = mod.webcrypto;
|
|
49
54
|
if (wc && typeof wc.getRandomValues === "function") {
|
|
50
55
|
const bound = wc.getRandomValues.bind(wc);
|