lightnode-sdk 0.3.1 → 0.3.2

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/README.md CHANGED
@@ -80,6 +80,12 @@ npx lightnode registered 0x6781…6e0f # true | false | null
80
80
  npx lightnode fee llama3-8b # on-chain job fee
81
81
  npx lightnode analytics --csv # per-model performance (CSV)
82
82
  npx lightnode reliability --csv # per-worker reliability (CSV)
83
+
84
+ # Scaffold inference into an existing project (auto-detects Next.js, Hono, or Node):
85
+ npx lightnode add inference [--template auto|nextjs-api|hono|node] [--net testnet|mainnet] [--force]
86
+
87
+ # Or scaffold a brand-new project:
88
+ npm create lightnode-app my-app
83
89
  ```
84
90
 
85
91
  ## Submitting inference
package/dist/add.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `lightnode add inference` - patch an EXISTING project to do encrypted
3
+ * LightChain AI inference. Detects the framework from package.json (Next.js,
4
+ * Hono, or plain Node) and writes the appropriate route/script + a .env.example
5
+ * if one isn't already there. Idempotent: existing files are not overwritten
6
+ * unless --force is passed.
7
+ *
8
+ * No runtime dependencies; templates are inlined as string literals.
9
+ */
10
+ type Template = "nextjs-api" | "hono" | "node";
11
+ type Network = "testnet" | "mainnet";
12
+ interface AddOpts {
13
+ template?: Template | "auto";
14
+ network?: Network;
15
+ force?: boolean;
16
+ cwd?: string;
17
+ }
18
+ interface WrittenFile {
19
+ path: string;
20
+ skipped?: boolean;
21
+ reason?: string;
22
+ }
23
+ /**
24
+ * Implementation called by `lightnode add inference [...]`.
25
+ * Returns the list of files written + the install command the user should run.
26
+ */
27
+ export declare function addInference(opts?: AddOpts): {
28
+ written: WrittenFile[];
29
+ install: string;
30
+ template: Template;
31
+ network: Network;
32
+ };
33
+ export {};
package/dist/add.js ADDED
@@ -0,0 +1,372 @@
1
+ /**
2
+ * `lightnode add inference` - patch an EXISTING project to do encrypted
3
+ * LightChain AI inference. Detects the framework from package.json (Next.js,
4
+ * Hono, or plain Node) and writes the appropriate route/script + a .env.example
5
+ * if one isn't already there. Idempotent: existing files are not overwritten
6
+ * unless --force is passed.
7
+ *
8
+ * No runtime dependencies; templates are inlined as string literals.
9
+ */
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ function readPackageJson(cwd) {
13
+ const p = path.join(cwd, "package.json");
14
+ if (!fs.existsSync(p))
15
+ return null;
16
+ try {
17
+ return JSON.parse(fs.readFileSync(p, "utf8"));
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function detectTemplate(cwd) {
24
+ const pkg = readPackageJson(cwd);
25
+ if (!pkg)
26
+ return "node";
27
+ const all = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
28
+ if (all["next"])
29
+ return "nextjs-api";
30
+ if (all["hono"])
31
+ return "hono";
32
+ return "node";
33
+ }
34
+ const ENV_EXAMPLE = (net) => `# Funded private key. Testnet works free (faucet at https://lightfaucet.ai).
35
+ PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
36
+
37
+ NETWORK=${net}
38
+ MODEL=llama3-8b
39
+ `;
40
+ const NEXTJS_ROUTE = `// app/api/inference/route.ts
41
+ // Generated by 'lightnode add inference'. See https://lightnode.app/build
42
+ import { NextResponse } from "next/server";
43
+ import WS from "ws";
44
+ import { createPublicClient, createWalletClient, http, parseAbi, parseAbiItem, parseEther, type Log } from "viem";
45
+ import { privateKeyToAccount } from "viem/accounts";
46
+ import {
47
+ LightNode, prepareSession, submitPrompt, decryptResponse,
48
+ estimateJobFee, consumerGatewayUrl, JOB_REGISTRY_CONSUMER_ABI,
49
+ GatewayClient, type NetworkId,
50
+ } from "lightnode-sdk";
51
+
52
+ export const runtime = "nodejs";
53
+ export const dynamic = "force-dynamic";
54
+ export const maxDuration = 120;
55
+
56
+ const NETWORK = (process.env.NETWORK ?? "testnet") as NetworkId;
57
+ const MODEL = process.env.MODEL ?? "llama3-8b";
58
+
59
+ export async function POST(req: Request) {
60
+ if (!process.env.PRIVATE_KEY?.startsWith("0x")) {
61
+ return NextResponse.json({ error: "PRIVATE_KEY not configured" }, { status: 500 });
62
+ }
63
+ const body = (await req.json().catch(() => ({}))) as { prompt?: string };
64
+ const prompt = body.prompt?.trim();
65
+ if (!prompt) return NextResponse.json({ error: "prompt is required" }, { status: 400 });
66
+
67
+ const ln = new LightNode(NETWORK);
68
+ const cfg = ln.network;
69
+ const acct = privateKeyToAccount(process.env.PRIVATE_KEY as \`0x\${string}\`);
70
+ const chain = { id: cfg.chainId, name: cfg.label, nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 }, rpcUrls: { default: { http: [cfg.rpc] } } };
71
+ const pub = createPublicClient({ transport: http(cfg.rpc), chain });
72
+ const wal = createWalletClient({ account: acct, transport: http(cfg.rpc), chain });
73
+ const abi = parseAbi(JOB_REGISTRY_CONSUMER_ABI);
74
+
75
+ const ch = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/challenge?address=\${acct.address}\`)).json() as { message?: string };
76
+ if (!ch.message) return NextResponse.json({ error: "auth challenge failed" }, { status: 502 });
77
+ const sig = await wal.signMessage({ message: ch.message });
78
+ const verify = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/verify\`, {
79
+ method: "POST", headers: { "Content-Type": "application/json" },
80
+ body: JSON.stringify({ message: ch.message, signature: sig }),
81
+ })).json() as { token?: string };
82
+ if (!verify.token) return NextResponse.json({ error: "auth verify failed" }, { status: 502 });
83
+
84
+ const gateway = new GatewayClient({ network: NETWORK, bearer: verify.token });
85
+ const { sessionKey, createSessionArgs } = await prepareSession(gateway, MODEL);
86
+ const fee = await estimateJobFee(cfg, MODEL);
87
+ const createTx = await wal.writeContract({
88
+ address: cfg.jobRegistry as \`0x\${string}\`, abi, functionName: "createSession",
89
+ args: [createSessionArgs.paramsHash, createSessionArgs.worker, createSessionArgs.encWorkerKey, createSessionArgs.ephemeralPubKey, createSessionArgs.initState, createSessionArgs.expiry],
90
+ gas: 1_000_000n,
91
+ });
92
+ const createReceipt = await pub.waitForTransactionReceipt({ hash: createTx });
93
+ const sessionCreated = parseAbiItem("event SessionCreated(uint256 indexed sessionId, address indexed user, bytes32 indexed paramsHash, address worker, bytes encWorkerKey, bytes ephemeralPubKey)");
94
+ const sessionLog = (await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: sessionCreated, blockHash: createReceipt.blockHash })).find((l) => l.transactionHash === createTx);
95
+ const sessionId = sessionLog?.args.sessionId;
96
+ if (!sessionId) return NextResponse.json({ error: "SessionCreated missing" }, { status: 500 });
97
+
98
+ let relayToken: string | undefined;
99
+ for (let i = 0; i < 30 && !relayToken; i++) {
100
+ const r = await gateway.getSessionToken(Number(sessionId));
101
+ if ("token" in r && r.token) relayToken = r.token; else await new Promise((res) => setTimeout(res, 1000));
102
+ }
103
+ if (!relayToken) return NextResponse.json({ error: "relay token never became ready" }, { status: 504 });
104
+ const ws = new WS(\`wss://relay.\${NETWORK}.lightchain.ai/ws?token=\${encodeURIComponent(relayToken)}\`);
105
+ const chunks: string[] = [];
106
+ await new Promise<void>((res, rej) => { ws.once("open", () => res()); ws.once("error", rej); });
107
+ ws.on("message", async (data: Buffer) => {
108
+ let f: { type?: string; payload?: string };
109
+ try { f = JSON.parse(data.toString("utf8")); } catch { return; }
110
+ if (!f.payload) return;
111
+ if (f.type === "chunk") { try { chunks.push(await decryptResponse(sessionKey, f.payload)); } catch {} }
112
+ else if (f.type === "complete" && chunks.length === 0) { try { chunks.push(await decryptResponse(sessionKey, f.payload)); } catch {} }
113
+ });
114
+
115
+ const promptHash = await submitPrompt(gateway, sessionKey, prompt);
116
+ const submitTx = await wal.writeContract({
117
+ address: cfg.jobRegistry as \`0x\${string}\`, abi, functionName: "submitJob",
118
+ args: [sessionId, promptHash], value: parseEther(String(fee)), gas: 500_000n,
119
+ });
120
+ const submitReceipt = await pub.waitForTransactionReceipt({ hash: submitTx });
121
+ const jobSubmitted = parseAbiItem("event JobSubmitted(uint256 indexed jobId, uint256 indexed sessionId, address worker)");
122
+ const jobLog = (await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: jobSubmitted, blockHash: submitReceipt.blockHash })).find((l) => l.transactionHash === submitTx);
123
+ const jobId = jobLog?.args.jobId;
124
+ if (!jobId) return NextResponse.json({ error: "JobSubmitted missing" }, { status: 500 });
125
+
126
+ const jobCompleted = parseAbiItem("event JobCompleted(uint256 indexed jobId, address indexed worker, bytes32 responseHash, bytes32 ciphertextHash)");
127
+ const deadline = Date.now() + 90_000;
128
+ let completed: Log | null = null;
129
+ while (!completed && Date.now() < deadline) {
130
+ await new Promise((res) => setTimeout(res, 3000));
131
+ const logs = await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: jobCompleted, args: { jobId }, fromBlock: submitReceipt.blockNumber });
132
+ if (logs.length) completed = logs[0] as Log;
133
+ }
134
+ await new Promise((res) => setTimeout(res, 4000));
135
+ ws.close();
136
+ if (!completed) return NextResponse.json({ error: "worker stalled", txs: { createSession: createTx, submitJob: submitTx } }, { status: 504 });
137
+
138
+ return NextResponse.json({
139
+ answer: chunks.join(""),
140
+ txs: { createSession: createTx, submitJob: submitTx, jobCompleted: completed.transactionHash },
141
+ sessionId: sessionId.toString(),
142
+ jobId: jobId.toString(),
143
+ worker: createSessionArgs.worker,
144
+ });
145
+ }
146
+ `;
147
+ const HONO_HANDLER = `// lightchain-inference.ts
148
+ // Generated by 'lightnode add inference'. See https://lightnode.app/build
149
+ // Plug the handler below into your Hono router:
150
+ //
151
+ // import { Hono } from "hono";
152
+ // import { inferenceHandler } from "./lightchain-inference.js";
153
+ // const app = new Hono();
154
+ // app.post("/inference", inferenceHandler);
155
+ //
156
+ import WS from "ws";
157
+ import { createPublicClient, createWalletClient, http, parseAbi, parseAbiItem, parseEther, type Log } from "viem";
158
+ import { privateKeyToAccount } from "viem/accounts";
159
+ import type { Context } from "hono";
160
+ import {
161
+ LightNode, prepareSession, submitPrompt, decryptResponse,
162
+ estimateJobFee, consumerGatewayUrl, JOB_REGISTRY_CONSUMER_ABI,
163
+ GatewayClient, type NetworkId,
164
+ } from "lightnode-sdk";
165
+
166
+ const NETWORK = (process.env.NETWORK ?? "testnet") as NetworkId;
167
+ const MODEL = process.env.MODEL ?? "llama3-8b";
168
+
169
+ export async function inferenceHandler(c: Context) {
170
+ if (!process.env.PRIVATE_KEY?.startsWith("0x")) return c.json({ error: "PRIVATE_KEY not set" }, 500);
171
+ const body = await c.req.json().catch(() => ({} as { prompt?: string }));
172
+ const prompt = body.prompt?.trim();
173
+ if (!prompt) return c.json({ error: "prompt is required" }, 400);
174
+
175
+ const ln = new LightNode(NETWORK);
176
+ const cfg = ln.network;
177
+ const acct = privateKeyToAccount(process.env.PRIVATE_KEY as \`0x\${string}\`);
178
+ const chain = { id: cfg.chainId, name: cfg.label, nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 }, rpcUrls: { default: { http: [cfg.rpc] } } };
179
+ const pub = createPublicClient({ transport: http(cfg.rpc), chain });
180
+ const wal = createWalletClient({ account: acct, transport: http(cfg.rpc), chain });
181
+ const abi = parseAbi(JOB_REGISTRY_CONSUMER_ABI);
182
+
183
+ const ch = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/challenge?address=\${acct.address}\`)).json() as { message?: string };
184
+ if (!ch.message) return c.json({ error: "auth challenge failed" }, 502);
185
+ const sig = await wal.signMessage({ message: ch.message });
186
+ const verify = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/verify\`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: ch.message, signature: sig }) })).json() as { token?: string };
187
+ if (!verify.token) return c.json({ error: "auth verify failed" }, 502);
188
+ const gateway = new GatewayClient({ network: NETWORK, bearer: verify.token });
189
+
190
+ const { sessionKey, createSessionArgs } = await prepareSession(gateway, MODEL);
191
+ const fee = await estimateJobFee(cfg, MODEL);
192
+ const createTx = await wal.writeContract({
193
+ address: cfg.jobRegistry as \`0x\${string}\`, abi, functionName: "createSession",
194
+ args: [createSessionArgs.paramsHash, createSessionArgs.worker, createSessionArgs.encWorkerKey, createSessionArgs.ephemeralPubKey, createSessionArgs.initState, createSessionArgs.expiry],
195
+ gas: 1_000_000n,
196
+ });
197
+ const createReceipt = await pub.waitForTransactionReceipt({ hash: createTx });
198
+ const sessionCreated = parseAbiItem("event SessionCreated(uint256 indexed sessionId, address indexed user, bytes32 indexed paramsHash, address worker, bytes encWorkerKey, bytes ephemeralPubKey)");
199
+ const sessionLog = (await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: sessionCreated, blockHash: createReceipt.blockHash })).find((l) => l.transactionHash === createTx);
200
+ const sessionId = sessionLog?.args.sessionId;
201
+ if (!sessionId) return c.json({ error: "SessionCreated missing" }, 500);
202
+
203
+ let relayToken: string | undefined;
204
+ for (let i = 0; i < 30 && !relayToken; i++) { const r = await gateway.getSessionToken(Number(sessionId)); if ("token" in r && r.token) relayToken = r.token; else await new Promise((res) => setTimeout(res, 1000)); }
205
+ if (!relayToken) return c.json({ error: "relay token never became ready" }, 504);
206
+ const ws = new WS(\`wss://relay.\${NETWORK}.lightchain.ai/ws?token=\${encodeURIComponent(relayToken)}\`);
207
+ const chunks: string[] = [];
208
+ await new Promise<void>((res, rej) => { ws.once("open", () => res()); ws.once("error", rej); });
209
+ ws.on("message", async (data: Buffer) => {
210
+ let f: { type?: string; payload?: string };
211
+ try { f = JSON.parse(data.toString("utf8")); } catch { return; }
212
+ if (!f.payload) return;
213
+ if (f.type === "chunk") { try { chunks.push(await decryptResponse(sessionKey, f.payload)); } catch {} }
214
+ else if (f.type === "complete" && chunks.length === 0) { try { chunks.push(await decryptResponse(sessionKey, f.payload)); } catch {} }
215
+ });
216
+
217
+ const promptHash = await submitPrompt(gateway, sessionKey, prompt);
218
+ const submitTx = await wal.writeContract({
219
+ address: cfg.jobRegistry as \`0x\${string}\`, abi, functionName: "submitJob",
220
+ args: [sessionId, promptHash], value: parseEther(String(fee)), gas: 500_000n,
221
+ });
222
+ const submitReceipt = await pub.waitForTransactionReceipt({ hash: submitTx });
223
+ const jobSubmitted = parseAbiItem("event JobSubmitted(uint256 indexed jobId, uint256 indexed sessionId, address worker)");
224
+ const jobLog = (await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: jobSubmitted, blockHash: submitReceipt.blockHash })).find((l) => l.transactionHash === submitTx);
225
+ const jobId = jobLog?.args.jobId;
226
+ if (!jobId) return c.json({ error: "JobSubmitted missing" }, 500);
227
+
228
+ const jobCompleted = parseAbiItem("event JobCompleted(uint256 indexed jobId, address indexed worker, bytes32 responseHash, bytes32 ciphertextHash)");
229
+ const deadline = Date.now() + 90_000;
230
+ let completed: Log | null = null;
231
+ while (!completed && Date.now() < deadline) {
232
+ await new Promise((res) => setTimeout(res, 3000));
233
+ const logs = await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: jobCompleted, args: { jobId }, fromBlock: submitReceipt.blockNumber });
234
+ if (logs.length) completed = logs[0] as Log;
235
+ }
236
+ await new Promise((res) => setTimeout(res, 4000));
237
+ ws.close();
238
+ if (!completed) return c.json({ error: "worker stalled", txs: { createSession: createTx, submitJob: submitTx } }, 504);
239
+ return c.json({ answer: chunks.join(""), txs: { createSession: createTx, submitJob: submitTx, jobCompleted: completed.transactionHash }, sessionId: sessionId.toString(), jobId: jobId.toString(), worker: createSessionArgs.worker });
240
+ }
241
+ `;
242
+ const NODE_SCRIPT = `// lightchain-inference.ts
243
+ // Generated by 'lightnode add inference'. Run with: tsx lightchain-inference.ts "your prompt"
244
+ import WS from "ws";
245
+ import { createPublicClient, createWalletClient, http, parseAbi, parseAbiItem, parseEther, type Log } from "viem";
246
+ import { privateKeyToAccount } from "viem/accounts";
247
+ import {
248
+ LightNode, prepareSession, submitPrompt, decryptResponse,
249
+ estimateJobFee, consumerGatewayUrl, JOB_REGISTRY_CONSUMER_ABI,
250
+ GatewayClient, type NetworkId,
251
+ } from "lightnode-sdk";
252
+
253
+ const NETWORK = (process.env.NETWORK ?? "testnet") as NetworkId;
254
+ const MODEL = process.env.MODEL ?? "llama3-8b";
255
+ const PROMPT = process.argv.slice(2).join(" ").trim() || "Reply with a one-sentence fun fact.";
256
+ const PRIVATE_KEY = process.env.PRIVATE_KEY as \`0x\${string}\` | undefined;
257
+ if (!PRIVATE_KEY?.startsWith("0x") || PRIVATE_KEY.length !== 66) { console.error("set PRIVATE_KEY in .env"); process.exit(1); }
258
+
259
+ const ln = new LightNode(NETWORK);
260
+ const cfg = ln.network;
261
+ const acct = privateKeyToAccount(PRIVATE_KEY);
262
+ const chain = { id: cfg.chainId, name: cfg.label, nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 }, rpcUrls: { default: { http: [cfg.rpc] } } };
263
+ const pub = createPublicClient({ transport: http(cfg.rpc), chain });
264
+ const wal = createWalletClient({ account: acct, transport: http(cfg.rpc), chain });
265
+ const abi = parseAbi(JOB_REGISTRY_CONSUMER_ABI);
266
+
267
+ const ch = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/challenge?address=\${acct.address}\`)).json() as { message?: string };
268
+ if (!ch.message) throw new Error("auth challenge failed");
269
+ const sig = await wal.signMessage({ message: ch.message });
270
+ const verify = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/verify\`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: ch.message, signature: sig }) })).json() as { token?: string };
271
+ if (!verify.token) throw new Error("auth verify failed");
272
+ const gateway = new GatewayClient({ network: NETWORK, bearer: verify.token });
273
+
274
+ const { sessionKey, createSessionArgs } = await prepareSession(gateway, MODEL);
275
+ const fee = await estimateJobFee(cfg, MODEL);
276
+ const createTx = await wal.writeContract({
277
+ address: cfg.jobRegistry as \`0x\${string}\`, abi, functionName: "createSession",
278
+ args: [createSessionArgs.paramsHash, createSessionArgs.worker, createSessionArgs.encWorkerKey, createSessionArgs.ephemeralPubKey, createSessionArgs.initState, createSessionArgs.expiry],
279
+ gas: 1_000_000n,
280
+ });
281
+ const createReceipt = await pub.waitForTransactionReceipt({ hash: createTx });
282
+ const sessionCreated = parseAbiItem("event SessionCreated(uint256 indexed sessionId, address indexed user, bytes32 indexed paramsHash, address worker, bytes encWorkerKey, bytes ephemeralPubKey)");
283
+ const sessionLog = (await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: sessionCreated, blockHash: createReceipt.blockHash })).find((l) => l.transactionHash === createTx);
284
+ if (!sessionLog?.args.sessionId) throw new Error("SessionCreated missing");
285
+ const sessionId = sessionLog.args.sessionId;
286
+
287
+ let relayToken: string | undefined;
288
+ for (let i = 0; i < 30 && !relayToken; i++) {
289
+ const r = await gateway.getSessionToken(Number(sessionId));
290
+ if ("token" in r && r.token) relayToken = r.token; else await new Promise((res) => setTimeout(res, 1000));
291
+ }
292
+ if (!relayToken) throw new Error("relay token never became ready");
293
+ const ws = new WS(\`wss://relay.\${NETWORK}.lightchain.ai/ws?token=\${encodeURIComponent(relayToken)}\`);
294
+ const chunks: string[] = [];
295
+ await new Promise<void>((res, rej) => { ws.once("open", () => res()); ws.once("error", rej); });
296
+ ws.on("message", async (data: Buffer) => {
297
+ let f: { type?: string; payload?: string };
298
+ try { f = JSON.parse(data.toString("utf8")); } catch { return; }
299
+ if (!f.payload) return;
300
+ if (f.type === "chunk") { try { chunks.push(await decryptResponse(sessionKey, f.payload)); } catch {} }
301
+ else if (f.type === "complete" && chunks.length === 0) { try { chunks.push(await decryptResponse(sessionKey, f.payload)); } catch {} }
302
+ });
303
+
304
+ const promptHash = await submitPrompt(gateway, sessionKey, PROMPT);
305
+ const submitTx = await wal.writeContract({
306
+ address: cfg.jobRegistry as \`0x\${string}\`, abi, functionName: "submitJob",
307
+ args: [sessionId, promptHash], value: parseEther(String(fee)), gas: 500_000n,
308
+ });
309
+ const submitReceipt = await pub.waitForTransactionReceipt({ hash: submitTx });
310
+ const jobSubmitted = parseAbiItem("event JobSubmitted(uint256 indexed jobId, uint256 indexed sessionId, address worker)");
311
+ const jobLog = (await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: jobSubmitted, blockHash: submitReceipt.blockHash })).find((l) => l.transactionHash === submitTx);
312
+ const jobId = jobLog?.args.jobId;
313
+ if (!jobId) throw new Error("JobSubmitted missing");
314
+
315
+ const jobCompleted = parseAbiItem("event JobCompleted(uint256 indexed jobId, address indexed worker, bytes32 responseHash, bytes32 ciphertextHash)");
316
+ const deadline = Date.now() + 90_000;
317
+ let completed: Log | null = null;
318
+ while (!completed && Date.now() < deadline) {
319
+ await new Promise((res) => setTimeout(res, 3000));
320
+ const logs = await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: jobCompleted, args: { jobId }, fromBlock: submitReceipt.blockNumber });
321
+ if (logs.length) completed = logs[0] as Log;
322
+ }
323
+ if (!completed) { console.error("worker stalled - re-run for a different worker"); process.exit(1); }
324
+ await new Promise((res) => setTimeout(res, 4000));
325
+ ws.close();
326
+ console.log("\\n=== ANSWER ===\\n" + chunks.join("") + "\\n");
327
+ console.log("createSession:", createTx, "\\nsubmitJob: ", submitTx, "\\njobCompleted: ", completed.transactionHash);
328
+ process.exit(0);
329
+ `;
330
+ function writeFile(abs, contents, force) {
331
+ const rel = path.relative(process.cwd(), abs) || abs;
332
+ if (fs.existsSync(abs) && !force) {
333
+ return { path: rel, skipped: true, reason: "already exists (use --force to overwrite)" };
334
+ }
335
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
336
+ fs.writeFileSync(abs, contents);
337
+ return { path: rel };
338
+ }
339
+ function depsNeeded(template) {
340
+ if (template === "nextjs-api")
341
+ return ["lightnode-sdk", "viem", "ws"];
342
+ if (template === "hono")
343
+ return ["lightnode-sdk", "viem", "ws"];
344
+ return ["lightnode-sdk", "viem", "ws"];
345
+ }
346
+ /**
347
+ * Implementation called by `lightnode add inference [...]`.
348
+ * Returns the list of files written + the install command the user should run.
349
+ */
350
+ export function addInference(opts = {}) {
351
+ const cwd = opts.cwd ?? process.cwd();
352
+ const network = opts.network ?? "testnet";
353
+ const template = opts.template && opts.template !== "auto" ? opts.template : detectTemplate(cwd);
354
+ const force = !!opts.force;
355
+ const written = [];
356
+ if (template === "nextjs-api") {
357
+ written.push(writeFile(path.join(cwd, "app/api/inference/route.ts"), NEXTJS_ROUTE, force));
358
+ }
359
+ else if (template === "hono") {
360
+ written.push(writeFile(path.join(cwd, "lightchain-inference.ts"), HONO_HANDLER, force));
361
+ }
362
+ else {
363
+ written.push(writeFile(path.join(cwd, "lightchain-inference.ts"), NODE_SCRIPT, force));
364
+ }
365
+ written.push(writeFile(path.join(cwd, ".env.example"), ENV_EXAMPLE(network), force));
366
+ return {
367
+ written,
368
+ install: `npm install ${depsNeeded(template).join(" ")}`,
369
+ template,
370
+ network,
371
+ };
372
+ }
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { LightNode, modelStatsCsv, workerStatsCsv, workerJobsCsv } from "./index.js";
3
+ import { addInference } from "./add.js";
3
4
  function flag(name) {
4
5
  const i = process.argv.indexOf(name);
5
6
  return i >= 0 ? process.argv[i + 1] : undefined;
@@ -16,14 +17,19 @@ const lcai = (wei) => (wei ? Number(BigInt(wei)) / 1e18 : 0);
16
17
  const rate = (r) => (r == null ? "-" : `${Math.round(r * 100)}%`);
17
18
  const HELP = `lightnode <command> [--net mainnet|testnet]
18
19
 
19
- network network summary (workers, jobs, models, earnings)
20
- models registered models + per-job fee
21
- worker <addr> a worker: on-chain registration + recent jobs
22
- jobs <addr> [--csv] one worker's job history (table or CSV)
23
- registered <addr> true | false | null (read from chain events)
24
- fee [model] on-chain inference fee (default llama3-8b)
25
- analytics [--csv] per-model performance (completion, p50/p95, incomplete)
26
- reliability [--csv] per-worker reliability, busiest first`;
20
+ network network summary (workers, jobs, models, earnings)
21
+ models registered models + per-job fee
22
+ worker <addr> a worker: on-chain registration + recent jobs
23
+ jobs <addr> [--csv] one worker's job history (table or CSV)
24
+ registered <addr> true | false | null (read from chain events)
25
+ fee [model] on-chain inference fee (default llama3-8b)
26
+ analytics [--csv] per-model performance (completion, p50/p95, incomplete)
27
+ reliability [--csv] per-worker reliability, busiest first
28
+
29
+ add inference patch the current project for end-to-end inference
30
+ [--template auto|nextjs-api|hono|node] [--force]
31
+
32
+ To scaffold a new project instead, run: npm create lightnode-app my-app`;
27
33
  async function main() {
28
34
  const ln = new LightNode(net);
29
35
  switch (cmd) {
@@ -89,6 +95,39 @@ async function main() {
89
95
  }
90
96
  break;
91
97
  }
98
+ case "add": {
99
+ const sub = positionals[1];
100
+ if (sub !== "inference")
101
+ die("usage: lightnode add inference [--template auto|nextjs-api|hono|node] [--net testnet|mainnet] [--force]");
102
+ const template = flag("--template") ?? "auto";
103
+ const force = process.argv.includes("--force");
104
+ const result = addInference({ template, network: net === "mainnet" ? "mainnet" : "testnet", force });
105
+ console.log(`▶ add inference (${result.template} template, default network ${result.network})`);
106
+ for (const f of result.written) {
107
+ if (f.skipped)
108
+ console.log(` ⤴ ${f.path} (skipped - ${f.reason})`);
109
+ else
110
+ console.log(` ✓ ${f.path}`);
111
+ }
112
+ const anyWritten = result.written.some((f) => !f.skipped);
113
+ if (!anyWritten) {
114
+ console.log("\nNothing to do - all target files already exist. Pass --force to overwrite.");
115
+ }
116
+ else {
117
+ console.log(`\nNext steps:`);
118
+ console.log(` 1. ${result.install}`);
119
+ console.log(` 2. cp .env.example .env (and put a funded ${result.network} PRIVATE_KEY in it)`);
120
+ if (result.template === "nextjs-api")
121
+ console.log(` 3. npm run dev (then POST /api/inference)`);
122
+ else if (result.template === "hono")
123
+ console.log(` 3. wire inferenceHandler into your Hono app, then start it`);
124
+ else
125
+ console.log(` 3. tsx lightchain-inference.ts "your prompt"`);
126
+ console.log(`\nFree testnet LCAI: https://lightfaucet.ai`);
127
+ console.log(`Builder docs: https://lightnode.app/build`);
128
+ }
129
+ break;
130
+ }
92
131
  default:
93
132
  console.log(HELP);
94
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Read-only TypeScript client for LightChain AI: workers, jobs, models, on-chain registration, and per-model network analytics. Independent, community-built (not an official LightChain package).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",