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/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
- const addr = positionals[1] ?? die("usage: lightnode worker <address> [--net testnet]");
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
- const mod = (await import("node:crypto"));
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);