lightnode-sdk 0.7.13 → 0.7.15
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/add.d.ts +28 -0
- package/dist/add.js +444 -238
- package/dist/cli.js +38 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/add.d.ts
CHANGED
|
@@ -48,6 +48,34 @@ export declare function addChat(opts?: AddOpts): {
|
|
|
48
48
|
template: Template;
|
|
49
49
|
network: Network;
|
|
50
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* `lightnode add chat-web3` - the user-pays counterpart to addChat.
|
|
53
|
+
*
|
|
54
|
+
* Architecture:
|
|
55
|
+
* - Each visitor's own wallet signs SIWE + createSession per turn.
|
|
56
|
+
* - The dev's app holds zero funds; cost is borne by each user (0.02 LCAI
|
|
57
|
+
* per llama3-8b turn on mainnet).
|
|
58
|
+
* - No backend, no PRIVATE_KEY, no server-side hot wallet.
|
|
59
|
+
*
|
|
60
|
+
* Fit:
|
|
61
|
+
* - Web3 dApps where users already have a wallet (NFT, meme coin, on-chain
|
|
62
|
+
* games, LightChallenge-style challenge platforms).
|
|
63
|
+
* - For SaaS chatbots where users do NOT have a wallet, use `add chat`
|
|
64
|
+
* instead (dev pays, server-side route).
|
|
65
|
+
*/
|
|
66
|
+
export declare function addChatWeb3(opts?: AddOpts): {
|
|
67
|
+
written: WrittenFile[];
|
|
68
|
+
install: string;
|
|
69
|
+
template: Template;
|
|
70
|
+
network: Network;
|
|
71
|
+
needsWagmi: boolean;
|
|
72
|
+
};
|
|
73
|
+
export declare function addJudge(opts?: AddOpts): {
|
|
74
|
+
written: WrittenFile[];
|
|
75
|
+
install: string;
|
|
76
|
+
template: Template;
|
|
77
|
+
network: Network;
|
|
78
|
+
};
|
|
51
79
|
export declare function addNftMint(opts?: AddOpts): {
|
|
52
80
|
written: WrittenFile[];
|
|
53
81
|
install: string;
|
package/dist/add.js
CHANGED
|
@@ -39,109 +39,48 @@ MODEL=llama3-8b
|
|
|
39
39
|
`;
|
|
40
40
|
const NEXTJS_ROUTE = `// app/api/inference/route.ts
|
|
41
41
|
// Generated by 'lightnode add inference'. See https://lightnode.app/build
|
|
42
|
+
// Uses runInferenceWithKey: one call, full encrypted-inference flow.
|
|
42
43
|
import { NextResponse } from "next/server";
|
|
43
|
-
import
|
|
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";
|
|
44
|
+
import { runInferenceWithKey } from "lightnode-sdk";
|
|
51
45
|
|
|
52
46
|
export const runtime = "nodejs";
|
|
53
47
|
export const dynamic = "force-dynamic";
|
|
48
|
+
// Mainnet 8b takes 60-90s under load; give the function room to finish.
|
|
54
49
|
export const maxDuration = 120;
|
|
55
50
|
|
|
56
|
-
const NETWORK = (process.env.NETWORK ?? "testnet") as
|
|
51
|
+
const NETWORK = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
|
|
57
52
|
const MODEL = process.env.MODEL ?? "llama3-8b";
|
|
58
53
|
|
|
59
54
|
export async function POST(req: Request) {
|
|
60
55
|
if (!process.env.PRIVATE_KEY?.startsWith("0x")) {
|
|
61
56
|
return NextResponse.json({ error: "PRIVATE_KEY not configured" }, { status: 500 });
|
|
62
57
|
}
|
|
63
|
-
const body = (await req.json().catch(() => ({}))) as { prompt?: string };
|
|
58
|
+
const body = (await req.json().catch(() => ({}))) as { prompt?: string; system?: string };
|
|
64
59
|
const prompt = body.prompt?.trim();
|
|
65
60
|
if (!prompt) return NextResponse.json({ error: "prompt is required" }, { status: 400 });
|
|
66
61
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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;
|
|
62
|
+
try {
|
|
63
|
+
const { answer, worker, txs, jobId } = await runInferenceWithKey({
|
|
64
|
+
network: NETWORK,
|
|
65
|
+
privateKey: process.env.PRIVATE_KEY as \`0x\${string}\`,
|
|
66
|
+
model: MODEL,
|
|
67
|
+
// Optional system prompt - leave undefined for raw user prompts.
|
|
68
|
+
system: body.system?.trim() || undefined,
|
|
69
|
+
prompt,
|
|
70
|
+
});
|
|
71
|
+
return NextResponse.json({
|
|
72
|
+
answer,
|
|
73
|
+
worker,
|
|
74
|
+
jobId: jobId.toString(),
|
|
75
|
+
txs: {
|
|
76
|
+
createSession: txs.createSession,
|
|
77
|
+
submitJob: txs.submitJob,
|
|
78
|
+
jobCompleted: txs.jobCompleted,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
133
83
|
}
|
|
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
84
|
}
|
|
146
85
|
`;
|
|
147
86
|
const HONO_HANDLER = `// lightchain-inference.ts
|
|
@@ -153,179 +92,65 @@ const HONO_HANDLER = `// lightchain-inference.ts
|
|
|
153
92
|
// const app = new Hono();
|
|
154
93
|
// app.post("/inference", inferenceHandler);
|
|
155
94
|
//
|
|
156
|
-
import WS from "ws";
|
|
157
|
-
import { createPublicClient, createWalletClient, http, parseAbi, parseAbiItem, parseEther, type Log } from "viem";
|
|
158
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
159
95
|
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";
|
|
96
|
+
import { runInferenceWithKey } from "lightnode-sdk";
|
|
165
97
|
|
|
166
|
-
const NETWORK = (process.env.NETWORK ?? "testnet") as
|
|
98
|
+
const NETWORK = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
|
|
167
99
|
const MODEL = process.env.MODEL ?? "llama3-8b";
|
|
168
100
|
|
|
169
101
|
export async function inferenceHandler(c: Context) {
|
|
170
102
|
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 }
|
|
103
|
+
const body = (await c.req.json().catch(() => ({}))) as { prompt?: string; system?: string };
|
|
172
104
|
const prompt = body.prompt?.trim();
|
|
173
105
|
if (!prompt) return c.json({ error: "prompt is required" }, 400);
|
|
174
106
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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;
|
|
107
|
+
try {
|
|
108
|
+
const { answer, worker, txs, jobId } = await runInferenceWithKey({
|
|
109
|
+
network: NETWORK,
|
|
110
|
+
privateKey: process.env.PRIVATE_KEY as \`0x\${string}\`,
|
|
111
|
+
model: MODEL,
|
|
112
|
+
system: body.system?.trim() || undefined,
|
|
113
|
+
prompt,
|
|
114
|
+
});
|
|
115
|
+
return c.json({
|
|
116
|
+
answer,
|
|
117
|
+
worker,
|
|
118
|
+
jobId: jobId.toString(),
|
|
119
|
+
txs: {
|
|
120
|
+
createSession: txs.createSession,
|
|
121
|
+
submitJob: txs.submitJob,
|
|
122
|
+
jobCompleted: txs.jobCompleted,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return c.json({ error: (e as Error).message }, 500);
|
|
235
127
|
}
|
|
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
128
|
}
|
|
241
129
|
`;
|
|
242
130
|
const NODE_SCRIPT = `// lightchain-inference.ts
|
|
243
131
|
// Generated by 'lightnode add inference'. Run with: tsx lightchain-inference.ts "your prompt"
|
|
244
|
-
import
|
|
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";
|
|
132
|
+
import { runInferenceWithKey, LightNode } from "lightnode-sdk";
|
|
252
133
|
|
|
253
|
-
const NETWORK = (process.env.NETWORK ?? "testnet") as
|
|
134
|
+
const NETWORK = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
|
|
254
135
|
const MODEL = process.env.MODEL ?? "llama3-8b";
|
|
255
136
|
const PROMPT = process.argv.slice(2).join(" ").trim() || "Reply with a one-sentence fun fact.";
|
|
256
137
|
const PRIVATE_KEY = process.env.PRIVATE_KEY as \`0x\${string}\` | undefined;
|
|
257
|
-
if (!PRIVATE_KEY
|
|
138
|
+
if (!PRIVATE_KEY) { console.error("PRIVATE_KEY not set. Put one in .env (testnet faucet: https://lightfaucet.ai)"); process.exit(1); }
|
|
139
|
+
const KEY = PRIVATE_KEY as \`0x\${string}\`;
|
|
258
140
|
|
|
259
141
|
const ln = new LightNode(NETWORK);
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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,
|
|
142
|
+
const { answer, worker, txs, jobId } = await runInferenceWithKey({
|
|
143
|
+
network: NETWORK,
|
|
144
|
+
privateKey: KEY,
|
|
145
|
+
model: MODEL,
|
|
146
|
+
prompt: PROMPT,
|
|
280
147
|
});
|
|
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
148
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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);
|
|
149
|
+
console.log("\\nanswer :", answer);
|
|
150
|
+
console.log("job id :", jobId.toString());
|
|
151
|
+
console.log("worker :", worker);
|
|
152
|
+
console.log("submitJob tx :", ln.explorerTxUrl(txs.submitJob));
|
|
153
|
+
if (txs.jobCompleted) console.log("completed tx :", ln.explorerTxUrl(txs.jobCompleted));
|
|
329
154
|
`;
|
|
330
155
|
function writeFile(abs, contents, force) {
|
|
331
156
|
const rel = path.relative(process.cwd(), abs) || abs;
|
|
@@ -846,6 +671,207 @@ export default function Chat() {
|
|
|
846
671
|
);
|
|
847
672
|
}
|
|
848
673
|
`;
|
|
674
|
+
const NEXTJS_CHAT_WEB3_PAGE = `// app/chat-web3/page.tsx
|
|
675
|
+
// Generated by 'lightnode add chat-web3'. User-pays chat: each visitor's own
|
|
676
|
+
// wallet signs SIWE + createSession per turn. Your app holds zero funds.
|
|
677
|
+
//
|
|
678
|
+
// Prereqs:
|
|
679
|
+
// - wagmi configured in your app (https://wagmi.sh/react/getting-started)
|
|
680
|
+
// - the connected wallet has LCAI on the LightChain network it is on
|
|
681
|
+
// (mainnet 9200 or testnet 8200). Mainnet llama3-8b costs 0.02 LCAI per
|
|
682
|
+
// turn plus a small gas amount.
|
|
683
|
+
"use client";
|
|
684
|
+
|
|
685
|
+
import { useEffect, useState } from "react";
|
|
686
|
+
import { useAccount, useWalletClient, usePublicClient } from "wagmi";
|
|
687
|
+
import { siweSignIn, GatewayClient, runInference, estimateJobFee, NETWORKS } from "lightnode-sdk";
|
|
688
|
+
|
|
689
|
+
type Turn = {
|
|
690
|
+
role: "user" | "assistant";
|
|
691
|
+
text: string;
|
|
692
|
+
worker?: string | null;
|
|
693
|
+
jobId?: string | null;
|
|
694
|
+
submitTx?: \`0x\${string}\` | null;
|
|
695
|
+
jobCompletedTx?: \`0x\${string}\` | null;
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const MODEL = "llama3-8b";
|
|
699
|
+
|
|
700
|
+
export default function ChatWeb3() {
|
|
701
|
+
const { address, chain } = useAccount();
|
|
702
|
+
const network: "mainnet" | "testnet" | null =
|
|
703
|
+
chain?.id === 9200 ? "mainnet" : chain?.id === 8200 ? "testnet" : null;
|
|
704
|
+
const { data: walletClient } = useWalletClient({ chainId: chain?.id });
|
|
705
|
+
const publicClient = usePublicClient({ chainId: chain?.id });
|
|
706
|
+
|
|
707
|
+
const [turns, setTurns] = useState<Turn[]>([]);
|
|
708
|
+
const [input, setInput] = useState("");
|
|
709
|
+
const [busy, setBusy] = useState(false);
|
|
710
|
+
const [busyStage, setBusyStage] = useState("");
|
|
711
|
+
const [err, setErr] = useState<string | null>(null);
|
|
712
|
+
const [feeLcai, setFeeLcai] = useState<number | null>(null);
|
|
713
|
+
|
|
714
|
+
// Read the on-chain fee for the connected network so we can show the
|
|
715
|
+
// visitor the real cost per turn before they click Send.
|
|
716
|
+
useEffect(() => {
|
|
717
|
+
if (!network) { setFeeLcai(null); return; }
|
|
718
|
+
let cancelled = false;
|
|
719
|
+
estimateJobFee(NETWORKS[network], MODEL).then(
|
|
720
|
+
(fee) => { if (!cancelled) setFeeLcai(fee); },
|
|
721
|
+
() => { if (!cancelled) setFeeLcai(null); },
|
|
722
|
+
);
|
|
723
|
+
return () => { cancelled = true; };
|
|
724
|
+
}, [network]);
|
|
725
|
+
|
|
726
|
+
/** Build a single prompt from history + new user input. */
|
|
727
|
+
function composePrompt(history: Turn[], next: string, system: string): string {
|
|
728
|
+
const lines: string[] = [];
|
|
729
|
+
for (const t of history) {
|
|
730
|
+
lines.push(\`\${t.role === "user" ? "User" : "Assistant"}: \${t.text}\`);
|
|
731
|
+
}
|
|
732
|
+
lines.push(\`User: \${next}\`);
|
|
733
|
+
lines.push("Assistant:");
|
|
734
|
+
return system ? \`\${system}\\n\\n\${lines.join("\\n\\n")}\` : lines.join("\\n\\n");
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async function send() {
|
|
738
|
+
if (!walletClient || !publicClient || !address || !network) {
|
|
739
|
+
setErr("Connect a wallet on LightChain mainnet (9200) or testnet (8200) first.");
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const next = input.trim();
|
|
743
|
+
if (!next) return;
|
|
744
|
+
setBusy(true);
|
|
745
|
+
setErr(null);
|
|
746
|
+
const history = [...turns];
|
|
747
|
+
// Optimistic user bubble so it appears immediately.
|
|
748
|
+
setTurns([...history, { role: "user", text: next }]);
|
|
749
|
+
setInput("");
|
|
750
|
+
try {
|
|
751
|
+
const system = "You are a concise assistant. Reply in one or two short sentences.";
|
|
752
|
+
const prompt = composePrompt(history, next, system);
|
|
753
|
+
|
|
754
|
+
setBusyStage("Sign in with your wallet (SIWE)...");
|
|
755
|
+
const session = await siweSignIn(walletClient as unknown as Parameters<typeof siweSignIn>[0], network);
|
|
756
|
+
|
|
757
|
+
setBusyStage("Approve the createSession transaction in your wallet...");
|
|
758
|
+
const gateway = new GatewayClient({ network, bearer: session.bearer });
|
|
759
|
+
const result = await runInference({
|
|
760
|
+
prompt,
|
|
761
|
+
gateway,
|
|
762
|
+
wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
|
|
763
|
+
publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
|
|
764
|
+
network: NETWORKS[network],
|
|
765
|
+
model: MODEL,
|
|
766
|
+
jobCompletedTimeoutMs: 120_000,
|
|
767
|
+
maxRetries: 1,
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
setTurns([...history, { role: "user", text: next }, {
|
|
771
|
+
role: "assistant",
|
|
772
|
+
text: result.answer,
|
|
773
|
+
worker: result.worker,
|
|
774
|
+
jobId: result.jobId?.toString() ?? null,
|
|
775
|
+
submitTx: result.txs?.submitJob ?? null,
|
|
776
|
+
jobCompletedTx: result.txs?.jobCompleted ?? null,
|
|
777
|
+
}]);
|
|
778
|
+
} catch (e) {
|
|
779
|
+
// Roll back the optimistic user bubble so the visitor can retry.
|
|
780
|
+
setTurns(history);
|
|
781
|
+
setInput(next);
|
|
782
|
+
const msg = (e as Error).message ?? "chat failed";
|
|
783
|
+
setErr(
|
|
784
|
+
/user rejected|user denied|reject/i.test(msg)
|
|
785
|
+
? "You rejected the wallet popup. Try again."
|
|
786
|
+
: /insufficient funds|insufficient balance/i.test(msg)
|
|
787
|
+
? \`Your wallet has no \${network === "mainnet" ? "LCAI" : "testnet LCAI"}. Top it up before sending.\`
|
|
788
|
+
: msg.split("\\n")[0],
|
|
789
|
+
);
|
|
790
|
+
} finally {
|
|
791
|
+
setBusy(false);
|
|
792
|
+
setBusyStage("");
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return (
|
|
797
|
+
<main style={{ maxWidth: 720, margin: "32px auto", padding: 16, fontFamily: "system-ui" }}>
|
|
798
|
+
<h1>Chat (user-pays)</h1>
|
|
799
|
+
<p style={{ color: "#666" }}>
|
|
800
|
+
Each turn signs one createSession transaction from your wallet on{" "}
|
|
801
|
+
<code>{network ?? "(connect a wallet)"}</code>. Fee:{" "}
|
|
802
|
+
<code>{feeLcai != null ? \`\${feeLcai} LCAI\` : "(fetching)"}</code> per turn plus a small gas amount.
|
|
803
|
+
</p>
|
|
804
|
+
{!address && (
|
|
805
|
+
<div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, margin: "12px 0" }}>
|
|
806
|
+
Connect a wallet to start chatting. (Use whichever connector your app exposes - e.g. RainbowKit,
|
|
807
|
+
ConnectKit, Reown AppKit, or wagmi's useConnect directly.)
|
|
808
|
+
</div>
|
|
809
|
+
)}
|
|
810
|
+
|
|
811
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8, margin: "16px 0" }}>
|
|
812
|
+
{turns.map((t, i) => (
|
|
813
|
+
<div
|
|
814
|
+
key={i}
|
|
815
|
+
style={{
|
|
816
|
+
alignSelf: t.role === "user" ? "flex-end" : "flex-start",
|
|
817
|
+
maxWidth: "85%",
|
|
818
|
+
borderRadius: 12,
|
|
819
|
+
padding: "8px 12px",
|
|
820
|
+
background: t.role === "user" ? "#e9e7ff" : "#f5f5f7",
|
|
821
|
+
}}
|
|
822
|
+
>
|
|
823
|
+
<div style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>{t.text}</div>
|
|
824
|
+
{t.role === "assistant" && t.submitTx ? (
|
|
825
|
+
<div style={{ marginTop: 6, fontSize: 11, color: "#666", display: "flex", gap: 8, flexWrap: "wrap" }}>
|
|
826
|
+
{t.worker && (
|
|
827
|
+
<a href={\`https://\${network}.lightscan.app/address/\${t.worker}\`} target="_blank" rel="noopener noreferrer">
|
|
828
|
+
worker
|
|
829
|
+
</a>
|
|
830
|
+
)}
|
|
831
|
+
{t.jobId && <span>job #{t.jobId}</span>}
|
|
832
|
+
{t.submitTx && (
|
|
833
|
+
<a href={\`https://\${network}.lightscan.app/tx/\${t.submitTx}\`} target="_blank" rel="noopener noreferrer">
|
|
834
|
+
submitJob
|
|
835
|
+
</a>
|
|
836
|
+
)}
|
|
837
|
+
{t.jobCompletedTx && (
|
|
838
|
+
<a href={\`https://\${network}.lightscan.app/tx/\${t.jobCompletedTx}\`} target="_blank" rel="noopener noreferrer">
|
|
839
|
+
completed
|
|
840
|
+
</a>
|
|
841
|
+
)}
|
|
842
|
+
</div>
|
|
843
|
+
) : null}
|
|
844
|
+
</div>
|
|
845
|
+
))}
|
|
846
|
+
</div>
|
|
847
|
+
|
|
848
|
+
<textarea
|
|
849
|
+
value={input}
|
|
850
|
+
onChange={(e) => setInput(e.target.value)}
|
|
851
|
+
onKeyDown={(e) => {
|
|
852
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (!busy && input.trim()) send(); }
|
|
853
|
+
}}
|
|
854
|
+
rows={2}
|
|
855
|
+
placeholder={turns.length === 0 ? "Say hello (cmd+enter to send)" : "Reply..."}
|
|
856
|
+
style={{ width: "100%", padding: 8, fontFamily: "inherit" }}
|
|
857
|
+
/>
|
|
858
|
+
<button
|
|
859
|
+
type="button"
|
|
860
|
+
onClick={() => send()}
|
|
861
|
+
disabled={busy || !input.trim() || !address || !network}
|
|
862
|
+
style={{ marginTop: 8, padding: "8px 16px" }}
|
|
863
|
+
>
|
|
864
|
+
{busy ? (busyStage || "Sending...") : (turns.length === 0 ? "Send first message" : "Send")}
|
|
865
|
+
</button>
|
|
866
|
+
{err && (
|
|
867
|
+
<p style={{ marginTop: 8, padding: "8px 12px", border: "1px solid #f5c2c7", background: "#f8d7da", color: "#842029", borderRadius: 6 }}>
|
|
868
|
+
{err}
|
|
869
|
+
</p>
|
|
870
|
+
)}
|
|
871
|
+
</main>
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
`;
|
|
849
875
|
const NODE_CHAT_REPL = `// chat-repl.ts
|
|
850
876
|
// Generated by 'lightnode add chat'. Interactive chat REPL in your terminal.
|
|
851
877
|
// npm install lightnode-sdk viem ws
|
|
@@ -1083,6 +1109,186 @@ export function addChat(opts = {}) {
|
|
|
1083
1109
|
written.push(writeFile(path.join(cwd, ".env.example"), ENV_EXAMPLE(network), force));
|
|
1084
1110
|
return { written, install: `npm install ${depsNeeded(template).join(" ")}`, template, network };
|
|
1085
1111
|
}
|
|
1112
|
+
/**
|
|
1113
|
+
* `lightnode add chat-web3` - the user-pays counterpart to addChat.
|
|
1114
|
+
*
|
|
1115
|
+
* Architecture:
|
|
1116
|
+
* - Each visitor's own wallet signs SIWE + createSession per turn.
|
|
1117
|
+
* - The dev's app holds zero funds; cost is borne by each user (0.02 LCAI
|
|
1118
|
+
* per llama3-8b turn on mainnet).
|
|
1119
|
+
* - No backend, no PRIVATE_KEY, no server-side hot wallet.
|
|
1120
|
+
*
|
|
1121
|
+
* Fit:
|
|
1122
|
+
* - Web3 dApps where users already have a wallet (NFT, meme coin, on-chain
|
|
1123
|
+
* games, LightChallenge-style challenge platforms).
|
|
1124
|
+
* - For SaaS chatbots where users do NOT have a wallet, use `add chat`
|
|
1125
|
+
* instead (dev pays, server-side route).
|
|
1126
|
+
*/
|
|
1127
|
+
export function addChatWeb3(opts = {}) {
|
|
1128
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1129
|
+
const network = opts.network ?? "mainnet";
|
|
1130
|
+
// chat-web3 is browser-only. If the project is not Next.js (or another
|
|
1131
|
+
// React framework we'd detect), fall back to nextjs-api anyway and warn
|
|
1132
|
+
// the user in the CLI's next-steps that the file expects a Next.js
|
|
1133
|
+
// setup with wagmi.
|
|
1134
|
+
const detected = detectTemplate(cwd);
|
|
1135
|
+
const template = opts.template && opts.template !== "auto" ? opts.template : detected;
|
|
1136
|
+
const force = !!opts.force;
|
|
1137
|
+
const written = [];
|
|
1138
|
+
// Detect whether the project already has wagmi; if not, the next-steps
|
|
1139
|
+
// output prints the install line.
|
|
1140
|
+
const pkg = readPackageJson(cwd);
|
|
1141
|
+
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
1142
|
+
const hasWagmi = Boolean(deps["wagmi"]);
|
|
1143
|
+
written.push(writeFile(path.join(cwd, "app/chat-web3/page.tsx"), NEXTJS_CHAT_WEB3_PAGE, force));
|
|
1144
|
+
return {
|
|
1145
|
+
written,
|
|
1146
|
+
install: `npm install lightnode-sdk viem` + (hasWagmi ? "" : " wagmi @tanstack/react-query"),
|
|
1147
|
+
template,
|
|
1148
|
+
network,
|
|
1149
|
+
needsWagmi: !hasWagmi,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
const NEXTJS_JUDGE_ROUTE = `// app/api/judge/route.ts
|
|
1153
|
+
// Generated by 'lightnode add judge'. See https://lightnode.app/build
|
|
1154
|
+
// The LightChallenge-style evaluator: post evidence + criteria, get a
|
|
1155
|
+
// pass/fail verdict + confidence + reason, plus an on-chain receipt
|
|
1156
|
+
// (submitJob + jobCompleted tx) anyone can verify.
|
|
1157
|
+
import { NextResponse } from "next/server";
|
|
1158
|
+
import { runInferenceWithKey } from "lightnode-sdk";
|
|
1159
|
+
|
|
1160
|
+
export const runtime = "nodejs";
|
|
1161
|
+
export const dynamic = "force-dynamic";
|
|
1162
|
+
export const maxDuration = 120;
|
|
1163
|
+
|
|
1164
|
+
const NETWORK = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
|
|
1165
|
+
const MODEL = process.env.MODEL ?? "llama3-8b";
|
|
1166
|
+
|
|
1167
|
+
interface JudgeRequest {
|
|
1168
|
+
criteria: string; // what the user has to satisfy
|
|
1169
|
+
evidence: unknown; // anything serialisable (the proof to grade)
|
|
1170
|
+
// Optional: extend the rubric the model uses. Defaults to passed/confidence/reason.
|
|
1171
|
+
schema?: string;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
interface Verdict {
|
|
1175
|
+
passed: boolean;
|
|
1176
|
+
confidence: number;
|
|
1177
|
+
reason: string;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const DEFAULT_SCHEMA = '{ "passed": boolean, "confidence": 0-1, "reason": string }';
|
|
1181
|
+
|
|
1182
|
+
export async function POST(req: Request) {
|
|
1183
|
+
if (!process.env.PRIVATE_KEY?.startsWith("0x")) {
|
|
1184
|
+
return NextResponse.json({ error: "PRIVATE_KEY not configured" }, { status: 500 });
|
|
1185
|
+
}
|
|
1186
|
+
const body = (await req.json().catch(() => ({}))) as Partial<JudgeRequest>;
|
|
1187
|
+
if (!body.criteria?.trim()) return NextResponse.json({ error: "criteria is required" }, { status: 400 });
|
|
1188
|
+
if (body.evidence === undefined) return NextResponse.json({ error: "evidence is required" }, { status: 400 });
|
|
1189
|
+
|
|
1190
|
+
const schema = body.schema?.trim() || DEFAULT_SCHEMA;
|
|
1191
|
+
const prompt = \`Criteria: \${body.criteria.trim()}
|
|
1192
|
+
|
|
1193
|
+
Evidence: \${JSON.stringify(body.evidence)}
|
|
1194
|
+
|
|
1195
|
+
Reply with STRICT JSON only, matching: \${schema}\`;
|
|
1196
|
+
|
|
1197
|
+
try {
|
|
1198
|
+
const { answer, worker, txs, jobId } = await runInferenceWithKey({
|
|
1199
|
+
network: NETWORK,
|
|
1200
|
+
privateKey: process.env.PRIVATE_KEY as \`0x\${string}\`,
|
|
1201
|
+
model: MODEL,
|
|
1202
|
+
system: "You are a careful judge. Reply with STRICT JSON only, no prose.",
|
|
1203
|
+
prompt,
|
|
1204
|
+
});
|
|
1205
|
+
// Parse the verdict defensively. If the model adds prose, extract the
|
|
1206
|
+
// first {...} block. Surface the raw answer either way so callers can
|
|
1207
|
+
// audit when the parser falls back.
|
|
1208
|
+
let verdict: Verdict | null = null;
|
|
1209
|
+
try {
|
|
1210
|
+
verdict = JSON.parse(answer) as Verdict;
|
|
1211
|
+
} catch {
|
|
1212
|
+
const m = answer.match(/\{[\s\\S]*\}/);
|
|
1213
|
+
if (m) {
|
|
1214
|
+
try { verdict = JSON.parse(m[0]) as Verdict; } catch { /* keep null */ }
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return NextResponse.json({
|
|
1218
|
+
verdict,
|
|
1219
|
+
raw: answer,
|
|
1220
|
+
worker,
|
|
1221
|
+
jobId: jobId.toString(),
|
|
1222
|
+
txs: {
|
|
1223
|
+
createSession: txs.createSession,
|
|
1224
|
+
submitJob: txs.submitJob,
|
|
1225
|
+
jobCompleted: txs.jobCompleted,
|
|
1226
|
+
},
|
|
1227
|
+
});
|
|
1228
|
+
} catch (e) {
|
|
1229
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
`;
|
|
1233
|
+
const NODE_JUDGE_SCRIPT = `// judge.ts
|
|
1234
|
+
// Generated by 'lightnode add judge'. Run with: tsx judge.ts '<criteria>' '<evidence>'
|
|
1235
|
+
// Example:
|
|
1236
|
+
// tsx judge.ts 'Run a mile under 8 minutes' '{"distance_km":1.61,"time_minutes":7.4}'
|
|
1237
|
+
import { runInferenceWithKey, LightNode } from "lightnode-sdk";
|
|
1238
|
+
|
|
1239
|
+
const NETWORK = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
|
|
1240
|
+
const MODEL = process.env.MODEL ?? "llama3-8b";
|
|
1241
|
+
const PRIVATE_KEY = process.env.PRIVATE_KEY as \`0x\${string}\` | undefined;
|
|
1242
|
+
if (!PRIVATE_KEY) { console.error("PRIVATE_KEY not set. Put one in .env (testnet faucet: https://lightfaucet.ai)"); process.exit(1); }
|
|
1243
|
+
const KEY = PRIVATE_KEY as \`0x\${string}\`;
|
|
1244
|
+
|
|
1245
|
+
const [criteria, evidenceJson] = process.argv.slice(2);
|
|
1246
|
+
if (!criteria || !evidenceJson) {
|
|
1247
|
+
console.error("usage: tsx judge.ts '<criteria>' '<evidence-json>'");
|
|
1248
|
+
process.exit(2);
|
|
1249
|
+
}
|
|
1250
|
+
const evidence = JSON.parse(evidenceJson);
|
|
1251
|
+
|
|
1252
|
+
const ln = new LightNode(NETWORK);
|
|
1253
|
+
const { answer, worker, txs, jobId } = await runInferenceWithKey({
|
|
1254
|
+
network: NETWORK,
|
|
1255
|
+
privateKey: KEY,
|
|
1256
|
+
model: MODEL,
|
|
1257
|
+
system: "You are a careful judge. Reply with STRICT JSON only, no prose.",
|
|
1258
|
+
prompt: \`Criteria: \${criteria}
|
|
1259
|
+
|
|
1260
|
+
Evidence: \${JSON.stringify(evidence)}
|
|
1261
|
+
|
|
1262
|
+
Reply with STRICT JSON only: { "passed": boolean, "confidence": 0-1, "reason": string }\`,
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
console.log("\\nraw answer :", answer);
|
|
1266
|
+
let verdict: { passed: boolean; confidence: number; reason: string } | null = null;
|
|
1267
|
+
try { verdict = JSON.parse(answer); } catch {
|
|
1268
|
+
const m = answer.match(/\\\{[\\\s\\S]*\\\}/);
|
|
1269
|
+
if (m) { try { verdict = JSON.parse(m[0]); } catch { /* keep null */ } }
|
|
1270
|
+
}
|
|
1271
|
+
console.log("verdict :", verdict);
|
|
1272
|
+
console.log("job id :", jobId.toString());
|
|
1273
|
+
console.log("worker :", worker);
|
|
1274
|
+
console.log("submitJob tx:", ln.explorerTxUrl(txs.submitJob));
|
|
1275
|
+
if (txs.jobCompleted) console.log("completed tx:", ln.explorerTxUrl(txs.jobCompleted));
|
|
1276
|
+
`;
|
|
1277
|
+
export function addJudge(opts = {}) {
|
|
1278
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1279
|
+
const network = opts.network ?? "testnet";
|
|
1280
|
+
const template = opts.template && opts.template !== "auto" ? opts.template : detectTemplate(cwd);
|
|
1281
|
+
const force = !!opts.force;
|
|
1282
|
+
const written = [];
|
|
1283
|
+
if (template === "nextjs-api") {
|
|
1284
|
+
written.push(writeFile(path.join(cwd, "app/api/judge/route.ts"), NEXTJS_JUDGE_ROUTE, force));
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
written.push(writeFile(path.join(cwd, "judge.ts"), NODE_JUDGE_SCRIPT, force));
|
|
1288
|
+
}
|
|
1289
|
+
written.push(writeFile(path.join(cwd, ".env.example"), ENV_EXAMPLE(network), force));
|
|
1290
|
+
return { written, install: `npm install ${depsNeeded(template).join(" ")}`, template, network };
|
|
1291
|
+
}
|
|
1086
1292
|
export function addNftMint(opts = {}) {
|
|
1087
1293
|
const cwd = opts.cwd ?? process.cwd();
|
|
1088
1294
|
const network = opts.network ?? "testnet";
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { LightNode, modelStatsCsv, workerStatsCsv, workerJobsCsv, runInferenceWithKey, runInferenceBatch, Agent, isStalledWorker, workerPreflight, workerWatch, WorkerOperator, isWorkerOpError, BRIDGE_ROUTE, DAO, DAO_ADDRESSES } from "./index.js";
|
|
3
|
-
import { addInference, addAnalyticsDashboard, addNftMint, addChat, addAgent } from "./add.js";
|
|
3
|
+
import { addInference, addAnalyticsDashboard, addNftMint, addChat, addChatWeb3, addAgent, addJudge } from "./add.js";
|
|
4
4
|
import { createPublicClient, createWalletClient, http, parseEther } from "viem";
|
|
5
5
|
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
|
|
6
6
|
function flag(name) {
|
|
@@ -462,7 +462,7 @@ async function main() {
|
|
|
462
462
|
const template = flag("--template") ?? "auto";
|
|
463
463
|
const force = process.argv.includes("--force");
|
|
464
464
|
const network = (net === "mainnet" ? "mainnet" : "testnet");
|
|
465
|
-
const known = ["inference", "chat", "agent", "analytics-dashboard", "nft-mint-with-inference"];
|
|
465
|
+
const known = ["inference", "chat", "chat-web3", "agent", "judge", "analytics-dashboard", "nft-mint-with-inference"];
|
|
466
466
|
if (!known.includes(sub ?? "")) {
|
|
467
467
|
die(`usage: lightnode add <${known.join("|")}> [--template auto|nextjs-api|hono|node] [--net testnet|mainnet] [--force]`);
|
|
468
468
|
}
|
|
@@ -470,11 +470,15 @@ async function main() {
|
|
|
470
470
|
? addAnalyticsDashboard({ template, network, force })
|
|
471
471
|
: sub === "nft-mint-with-inference"
|
|
472
472
|
? addNftMint({ template, network, force })
|
|
473
|
-
: sub === "chat"
|
|
474
|
-
?
|
|
475
|
-
: sub === "
|
|
476
|
-
?
|
|
477
|
-
:
|
|
473
|
+
: sub === "chat-web3"
|
|
474
|
+
? addChatWeb3({ template, network, force })
|
|
475
|
+
: sub === "chat"
|
|
476
|
+
? addChat({ template, network, force })
|
|
477
|
+
: sub === "agent"
|
|
478
|
+
? addAgent({ template, network, force })
|
|
479
|
+
: sub === "judge"
|
|
480
|
+
? addJudge({ template, network, force })
|
|
481
|
+
: addInference({ template, network, force });
|
|
478
482
|
console.log(`▶ add ${sub} (${result.template} template, default network ${result.network})`);
|
|
479
483
|
for (const f of result.written) {
|
|
480
484
|
if (f.skipped)
|
|
@@ -489,7 +493,25 @@ async function main() {
|
|
|
489
493
|
else {
|
|
490
494
|
console.log(`\nNext steps (these files were added to your CURRENT folder, not a new project):`);
|
|
491
495
|
console.log(` 1. ${result.install}`);
|
|
492
|
-
if (sub === "
|
|
496
|
+
if (sub === "chat-web3") {
|
|
497
|
+
// chat-web3 has no PRIVATE_KEY (each visitor pays their own way).
|
|
498
|
+
const needsWagmi = result.needsWagmi;
|
|
499
|
+
if (needsWagmi) {
|
|
500
|
+
console.log(` 2. Set up wagmi in your app if you have not already.`);
|
|
501
|
+
console.log(` See https://wagmi.sh/react/getting-started - wrap your root layout with`);
|
|
502
|
+
console.log(` <WagmiProvider config={wagmiConfig}> and add a Connect button using`);
|
|
503
|
+
console.log(` useConnect / RainbowKit / Reown AppKit / ConnectKit, whatever you prefer.`);
|
|
504
|
+
console.log(` 3. npm run dev, open /chat-web3`);
|
|
505
|
+
console.log(` 4. Connect a wallet on LightChain ${result.network === "mainnet" ? "mainnet (chainId 9200)" : "testnet (chainId 8200)"}.`);
|
|
506
|
+
console.log(` Mainnet llama3-8b costs 0.02 LCAI per turn; testnet is free from https://lightfaucet.ai`);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
console.log(` 2. npm run dev, open /chat-web3`);
|
|
510
|
+
console.log(` 3. Connect a wallet on LightChain ${result.network === "mainnet" ? "mainnet (chainId 9200)" : "testnet (chainId 8200)"}.`);
|
|
511
|
+
console.log(` Mainnet llama3-8b costs 0.02 LCAI per turn; testnet is free from https://lightfaucet.ai`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
else if (sub === "nft-mint-with-inference" || sub === "inference" || sub === "chat" || sub === "agent" || sub === "judge") {
|
|
493
515
|
console.log(` 2. cp .env.example .env (and put a funded ${result.network} PRIVATE_KEY in it)`);
|
|
494
516
|
if (sub === "agent" && result.template === "nextjs-api") {
|
|
495
517
|
console.log(` 3. Set CRON_SECRET in your Vercel env vars + edit AGENT_TASK in .env`);
|
|
@@ -505,6 +527,14 @@ async function main() {
|
|
|
505
527
|
else if (sub === "chat") {
|
|
506
528
|
console.log(` 3. npx tsx chat-repl.ts (interactive terminal chat)`);
|
|
507
529
|
}
|
|
530
|
+
else if (sub === "judge" && result.template === "nextjs-api") {
|
|
531
|
+
console.log(` 3. npm run dev`);
|
|
532
|
+
console.log(` 4. curl -X POST localhost:3000/api/judge -H 'content-type: application/json' \\\\`);
|
|
533
|
+
console.log(` -d '{"criteria":"Run a mile under 8 minutes","evidence":{"time_minutes":7.4,"distance_km":1.61}}'`);
|
|
534
|
+
}
|
|
535
|
+
else if (sub === "judge") {
|
|
536
|
+
console.log(` 3. npx tsx judge.ts 'Run a mile under 8 minutes' '{"time_minutes":7.4,"distance_km":1.61}'`);
|
|
537
|
+
}
|
|
508
538
|
else if (sub === "nft-mint-with-inference" && result.template === "nextjs-api") {
|
|
509
539
|
console.log(` 3. Make sure /api/inference is mounted too (run: npx lightnode add inference)`);
|
|
510
540
|
console.log(` 4. npm run dev, open /nft-mint`);
|
package/dist/index.d.ts
CHANGED
|
@@ -134,7 +134,7 @@ export declare class LightNode {
|
|
|
134
134
|
* (especially in registry-proxy environments like StackBlitz where lockfiles
|
|
135
135
|
* may pin an older minor than the local install command suggests).
|
|
136
136
|
*/
|
|
137
|
-
export declare const SDK_VERSION = "0.7.
|
|
137
|
+
export declare const SDK_VERSION = "0.7.15";
|
|
138
138
|
export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, resolveJobTransactions, siweSignIn, siweChallenge, siweVerify, fetchWorkerModels, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, runInferenceStream, Conversation, chat, runInferenceBatch, Agent, parseAgentOutput, workerPreflight, workerWatch, Bridge, BRIDGE_ROUTE, HYPERLANE_ROUTER_ABI, ERC20_ABI, addressToBytes32, quoteBridgeFee, bridgeableBalance, bridgeAllowance, approveBridge, bridgeTransfer, DAO, DAO_ADDRESSES, ProposalState, PROPOSAL_STATE_LABEL, VoteSupport, GOVERNOR_ABI, VOTES_ABI, OnchainModelRegistry, AIVM_MODEL_REGISTRY_ABI, BENCHMARK_REGISTRY_ABI, ModelStatus, MODEL_STATUS_LABEL, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, WorkerOperator, WORKER_REGISTRY_ABI, JOB_REGISTRY_OPERATOR_ABI, AI_CONFIG_ABI, JOB_STATE, decodeWorkerError, WorkerOpError, isWorkerOpError, };
|
|
139
139
|
export type { BearerSource, GatewayClientOptions, SelectSessionResult, PrepareSessionResult, UploadBlobResult, SessionTokenResult } from "./gateway.js";
|
|
140
140
|
export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs, RunInferenceStreamResult } from "./inference.js";
|
package/dist/index.js
CHANGED
|
@@ -213,7 +213,7 @@ export class LightNode {
|
|
|
213
213
|
* (especially in registry-proxy environments like StackBlitz where lockfiles
|
|
214
214
|
* may pin an older minor than the local install command suggests).
|
|
215
215
|
*/
|
|
216
|
-
export const SDK_VERSION = "0.7.
|
|
216
|
+
export const SDK_VERSION = "0.7.15";
|
|
217
217
|
export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei,
|
|
218
218
|
// v0.7.3 per-job transaction-hash resolver (lifts the upstream
|
|
219
219
|
// subgraph's "block-only" Job entity to a deep-linkable Job + tx pair).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lightnode-sdk",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.15",
|
|
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",
|