naracli 1.0.17 → 1.0.22

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.
Files changed (41) hide show
  1. package/README.md +101 -114
  2. package/bin/nara-cli.ts +0 -20
  3. package/dist/nara-cli.mjs +49930 -2222
  4. package/index.ts +10 -58
  5. package/package.json +7 -6
  6. package/src/cli/commands/quest.ts +8 -7
  7. package/src/cli/commands/skills.ts +491 -0
  8. package/src/cli/commands/skillsInstall.ts +793 -0
  9. package/src/cli/commands/wallet.ts +13 -114
  10. package/src/cli/commands/zkid.ts +410 -0
  11. package/src/cli/index.ts +215 -9
  12. package/src/cli/prompts/searchMultiselect.ts +297 -0
  13. package/src/cli/types.ts +0 -138
  14. package/src/cli/utils/transaction.ts +1 -1
  15. package/src/cli/utils/validation.ts +0 -40
  16. package/src/cli/utils/wallet.ts +3 -1
  17. package/src/tests/helpers.ts +78 -0
  18. package/src/tests/skills.e2e.test.ts +126 -0
  19. package/src/tests/skills.test.ts +192 -0
  20. package/src/tests/test_skill.md +18 -0
  21. package/src/tests/zkid.e2e.test.ts +128 -0
  22. package/src/tests/zkid.test.ts +153 -0
  23. package/src/types/snarkjs.d.ts +4 -1
  24. package/dist/quest/nara_quest.json +0 -534
  25. package/dist/zk/answer_proof.wasm +0 -0
  26. package/dist/zk/answer_proof_final.zkey +0 -0
  27. package/src/cli/commands/config.ts +0 -125
  28. package/src/cli/commands/migrate.ts +0 -270
  29. package/src/cli/commands/pool.ts +0 -364
  30. package/src/cli/commands/swap.ts +0 -349
  31. package/src/cli/quest/nara_quest.json +0 -534
  32. package/src/cli/quest/nara_quest_types.ts +0 -540
  33. package/src/cli/zk/answer_proof.wasm +0 -0
  34. package/src/cli/zk/answer_proof_final.zkey +0 -0
  35. package/src/client.ts +0 -96
  36. package/src/config.ts +0 -132
  37. package/src/constants.ts +0 -35
  38. package/src/migrate.ts +0 -222
  39. package/src/pool.ts +0 -259
  40. package/src/quest.ts +0 -387
  41. package/src/swap.ts +0 -608
package/src/quest.ts DELETED
@@ -1,387 +0,0 @@
1
- /**
2
- * Quest SDK - interact with nara-quest on-chain quiz
3
- */
4
-
5
- import {
6
- Connection,
7
- Keypair,
8
- LAMPORTS_PER_SOL,
9
- PublicKey,
10
- } from "@solana/web3.js";
11
- import * as anchor from "@coral-xyz/anchor";
12
- import { Program, AnchorProvider, Wallet } from "@coral-xyz/anchor";
13
- import type { NaraQuest } from "./cli/quest/nara_quest_types";
14
- import { DEFAULT_QUEST_PROGRAM_ID } from "./constants";
15
-
16
- import { createRequire } from "module";
17
- const _require = createRequire(import.meta.url);
18
-
19
- // ─── ZK constants ────────────────────────────────────────────────
20
- const BN254_FIELD =
21
- 21888242871839275222246405745257275088696311157297823662689037894645226208583n;
22
-
23
- import { fileURLToPath } from "url";
24
- import { dirname, join, resolve } from "path";
25
- import { existsSync } from "fs";
26
- const __dirname = dirname(fileURLToPath(import.meta.url));
27
-
28
- function findZkFile(name: string): string {
29
- // 1. src/cli/zk/ (dev mode, running from src/)
30
- const srcPath = join(__dirname, "cli/zk", name);
31
- if (existsSync(srcPath)) return srcPath;
32
- // 2. dist/zk/ (published, running from dist/)
33
- const distPath = join(__dirname, "zk", name);
34
- if (existsSync(distPath)) return distPath;
35
- // 3. Fallback to src path (will error at runtime if missing)
36
- return srcPath;
37
- }
38
-
39
- const DEFAULT_CIRCUIT_WASM = findZkFile("answer_proof.wasm");
40
- const DEFAULT_ZKEY = findZkFile("answer_proof_final.zkey");
41
-
42
- // ─── Types ───────────────────────────────────────────────────────
43
-
44
- export interface QuestInfo {
45
- active: boolean;
46
- round: string;
47
- questionId: string;
48
- question: string;
49
- answerHash: number[];
50
- rewardPerWinner: number;
51
- totalReward: number;
52
- rewardCount: number;
53
- winnerCount: number;
54
- remainingSlots: number;
55
- deadline: number;
56
- timeRemaining: number;
57
- expired: boolean;
58
- }
59
-
60
- export interface ZkProof {
61
- proofA: number[];
62
- proofB: number[];
63
- proofC: number[];
64
- }
65
-
66
- export interface ZkProofHex {
67
- proofA: string;
68
- proofB: string;
69
- proofC: string;
70
- }
71
-
72
- export interface SubmitAnswerResult {
73
- signature: string;
74
- }
75
-
76
- export interface SubmitRelayResult {
77
- txHash: string;
78
- }
79
-
80
- export interface QuestOptions {
81
- programId?: string;
82
- circuitWasmPath?: string;
83
- zkeyPath?: string;
84
- }
85
-
86
- // ─── ZK utilities ────────────────────────────────────────────────
87
-
88
- function toBigEndian32(v: bigint): Buffer {
89
- return Buffer.from(v.toString(16).padStart(64, "0"), "hex");
90
- }
91
-
92
- function answerToField(answer: string): bigint {
93
- return (
94
- BigInt("0x" + Buffer.from(answer, "utf-8").toString("hex")) % BN254_FIELD
95
- );
96
- }
97
-
98
- function hashBytesToFieldStr(hashBytes: number[]): string {
99
- return BigInt("0x" + Buffer.from(hashBytes).toString("hex")).toString();
100
- }
101
-
102
- function pubkeyToCircuitInputs(pubkey: PublicKey): {
103
- lo: string;
104
- hi: string;
105
- } {
106
- const bytes = pubkey.toBuffer();
107
- return {
108
- lo: BigInt("0x" + bytes.subarray(16, 32).toString("hex")).toString(),
109
- hi: BigInt("0x" + bytes.subarray(0, 16).toString("hex")).toString(),
110
- };
111
- }
112
-
113
- function proofToSolana(proof: any): ZkProof {
114
- const negY = (y: string) => toBigEndian32(BN254_FIELD - BigInt(y));
115
- const be = (s: string) => toBigEndian32(BigInt(s));
116
- return {
117
- proofA: Array.from(
118
- Buffer.concat([be(proof.pi_a[0]), negY(proof.pi_a[1])])
119
- ),
120
- proofB: Array.from(
121
- Buffer.concat([
122
- be(proof.pi_b[0][1]),
123
- be(proof.pi_b[0][0]),
124
- be(proof.pi_b[1][1]),
125
- be(proof.pi_b[1][0]),
126
- ])
127
- ),
128
- proofC: Array.from(
129
- Buffer.concat([be(proof.pi_c[0]), be(proof.pi_c[1])])
130
- ),
131
- };
132
- }
133
-
134
- function proofToHex(proof: any): ZkProofHex {
135
- const negY = (y: string) => toBigEndian32(BN254_FIELD - BigInt(y));
136
- const be = (s: string) => toBigEndian32(BigInt(s));
137
- return {
138
- proofA: Buffer.concat([be(proof.pi_a[0]), negY(proof.pi_a[1])]).toString("hex"),
139
- proofB: Buffer.concat([
140
- be(proof.pi_b[0][1]),
141
- be(proof.pi_b[0][0]),
142
- be(proof.pi_b[1][1]),
143
- be(proof.pi_b[1][0]),
144
- ]).toString("hex"),
145
- proofC: Buffer.concat([be(proof.pi_c[0]), be(proof.pi_c[1])]).toString("hex"),
146
- };
147
- }
148
-
149
- // Suppress console output from snarkjs WASM during proof generation.
150
- async function silentProve(snarkjs: any, input: Record<string, string>, wasmPath: string, zkeyPath: string) {
151
- const savedLog = console.log;
152
- const savedError = console.error;
153
- console.log = () => {};
154
- console.error = () => {};
155
- try {
156
- return await snarkjs.groth16.fullProve(input, wasmPath, zkeyPath, null, null, { singleThread: true });
157
- } finally {
158
- console.log = savedLog;
159
- console.error = savedError;
160
- }
161
- }
162
-
163
- // ─── Anchor helpers ──────────────────────────────────────────────
164
-
165
- function createProgram(
166
- connection: Connection,
167
- wallet: Keypair,
168
- programId?: string
169
- ): Program<NaraQuest> {
170
- const idlPath = existsSync(join(__dirname, "cli/quest/nara_quest.json"))
171
- ? "./cli/quest/nara_quest.json"
172
- : "./quest/nara_quest.json";
173
- const idl = _require(idlPath);
174
- const pid = programId ?? DEFAULT_QUEST_PROGRAM_ID;
175
- const idlWithPid = { ...idl, address: pid };
176
- const provider = new AnchorProvider(
177
- connection,
178
- new Wallet(wallet),
179
- { commitment: "confirmed" }
180
- );
181
- anchor.setProvider(provider);
182
- return new Program<NaraQuest>(idlWithPid as any, provider);
183
- }
184
-
185
- function getPoolPda(programId: PublicKey): PublicKey {
186
- const [pda] = PublicKey.findProgramAddressSync(
187
- [Buffer.from("pool")],
188
- programId
189
- );
190
- return pda;
191
- }
192
-
193
- function getWinnerRecordPda(
194
- programId: PublicKey,
195
- user: PublicKey
196
- ): PublicKey {
197
- const [pda] = PublicKey.findProgramAddressSync(
198
- [Buffer.from("winner"), user.toBuffer()],
199
- programId
200
- );
201
- return pda;
202
- }
203
-
204
- // ─── SDK functions ───────────────────────────────────────────────
205
-
206
- /**
207
- * Get the current active quest info
208
- */
209
- export async function getQuestInfo(
210
- connection: Connection,
211
- wallet?: Keypair,
212
- options?: QuestOptions
213
- ): Promise<QuestInfo> {
214
- const kp = wallet ?? Keypair.generate();
215
- const program = createProgram(connection, kp, options?.programId);
216
- const poolPda = getPoolPda(program.programId);
217
- const pool = await program.account.pool.fetch(poolPda);
218
-
219
- const now = Math.floor(Date.now() / 1000);
220
- const deadline = pool.deadline.toNumber();
221
- const secsLeft = deadline - now;
222
-
223
- return {
224
- active: pool.isActive,
225
- round: pool.round.toString(),
226
- questionId: pool.questionId.toString(),
227
- question: pool.question,
228
- answerHash: Array.from(pool.answerHash),
229
- rewardPerWinner: pool.rewardPerWinner.toNumber() / LAMPORTS_PER_SOL,
230
- totalReward: pool.rewardAmount.toNumber() / LAMPORTS_PER_SOL,
231
- rewardCount: pool.rewardCount,
232
- winnerCount: pool.winnerCount,
233
- remainingSlots: Math.max(0, pool.rewardCount - pool.winnerCount),
234
- deadline,
235
- timeRemaining: secsLeft,
236
- expired: secsLeft <= 0,
237
- };
238
- }
239
-
240
- /**
241
- * Check if the user has already answered the current round
242
- */
243
- export async function hasAnswered(
244
- connection: Connection,
245
- wallet: Keypair,
246
- options?: QuestOptions
247
- ): Promise<boolean> {
248
- const program = createProgram(connection, wallet, options?.programId);
249
- const quest = await getQuestInfo(connection, wallet, options);
250
- const winnerPda = getWinnerRecordPda(program.programId, wallet.publicKey);
251
- try {
252
- const wr = await program.account.winnerRecord.fetch(winnerPda);
253
- return wr.round.toString() === quest.round;
254
- } catch {
255
- return false;
256
- }
257
- }
258
-
259
- /**
260
- * Generate a ZK proof for a quest answer.
261
- * Throws if the answer is wrong (circuit assertion fails).
262
- */
263
- export async function generateProof(
264
- answer: string,
265
- answerHash: number[],
266
- userPubkey: PublicKey,
267
- options?: QuestOptions
268
- ): Promise<{ solana: ZkProof; hex: ZkProofHex }> {
269
- const wasmPath = options?.circuitWasmPath ?? process.env.QUEST_CIRCUIT_WASM ?? DEFAULT_CIRCUIT_WASM;
270
- const zkeyPath = options?.zkeyPath ?? process.env.QUEST_ZKEY ?? DEFAULT_ZKEY;
271
-
272
- const snarkjs = await import("snarkjs");
273
- const answerHashFieldStr = hashBytesToFieldStr(answerHash);
274
- const { lo, hi } = pubkeyToCircuitInputs(userPubkey);
275
-
276
- const result = await silentProve(
277
- snarkjs,
278
- {
279
- answer: answerToField(answer).toString(),
280
- answer_hash: answerHashFieldStr,
281
- pubkey_lo: lo,
282
- pubkey_hi: hi,
283
- },
284
- wasmPath,
285
- zkeyPath
286
- );
287
-
288
- return {
289
- solana: proofToSolana(result.proof),
290
- hex: proofToHex(result.proof),
291
- };
292
- }
293
-
294
- /**
295
- * Submit a quest answer on-chain (direct submission, requires gas)
296
- */
297
- export async function submitAnswer(
298
- connection: Connection,
299
- wallet: Keypair,
300
- proof: ZkProof,
301
- options?: QuestOptions
302
- ): Promise<SubmitAnswerResult> {
303
- const program = createProgram(connection, wallet, options?.programId);
304
- const signature = await program.methods
305
- .submitAnswer(proof.proofA as any, proof.proofB as any, proof.proofC as any)
306
- .accounts({ user: wallet.publicKey, payer: wallet.publicKey })
307
- .signers([wallet])
308
- .rpc({ skipPreflight: true });
309
-
310
- return { signature };
311
- }
312
-
313
- /**
314
- * Submit a quest answer via relay (gasless submission)
315
- */
316
- export async function submitAnswerViaRelay(
317
- relayUrl: string,
318
- userPubkey: PublicKey,
319
- proof: ZkProofHex
320
- ): Promise<SubmitRelayResult> {
321
- const base = relayUrl.replace(/\/+$/, "");
322
- const res = await fetch(`${base}/submit-answer`, {
323
- method: "POST",
324
- headers: { "Content-Type": "application/json" },
325
- body: JSON.stringify({
326
- user: userPubkey.toBase58(),
327
- proofA: proof.proofA,
328
- proofB: proof.proofB,
329
- proofC: proof.proofC,
330
- }),
331
- });
332
-
333
- const data = (await res.json()) as any;
334
- if (!res.ok) {
335
- throw new Error(`Relay submission failed: ${data.error ?? `HTTP ${res.status}`}`);
336
- }
337
-
338
- return { txHash: data.txHash };
339
- }
340
-
341
- /**
342
- * Parse reward info from a quest transaction's log messages
343
- */
344
- export async function parseQuestReward(
345
- connection: Connection,
346
- txSignature: string,
347
- retries = 10
348
- ): Promise<{ rewarded: boolean; rewardLamports: number; rewardNso: number; winner: string }> {
349
- await new Promise((r) => setTimeout(r, 2000));
350
-
351
- let txInfo: any;
352
- for (let i = 0; i < retries; i++) {
353
- try {
354
- txInfo = await connection.getTransaction(txSignature, {
355
- commitment: "confirmed",
356
- maxSupportedTransactionVersion: 0,
357
- });
358
- if (txInfo) break;
359
- } catch {
360
- // retry
361
- }
362
- await new Promise((r) => setTimeout(r, 1000));
363
- }
364
-
365
- if (!txInfo) {
366
- throw new Error("Failed to fetch transaction details");
367
- }
368
-
369
- let rewardLamports = 0;
370
- let winner = "";
371
- const logs: string[] = txInfo.meta?.logMessages ?? [];
372
- for (const log of logs) {
373
- const m = log.match(/reward (\d+) lamports \(winner (\d+\/\d+)\)/);
374
- if (m) {
375
- rewardLamports = parseInt(m[1]!);
376
- winner = m[2]!;
377
- break;
378
- }
379
- }
380
-
381
- return {
382
- rewarded: rewardLamports > 0,
383
- rewardLamports,
384
- rewardNso: rewardLamports / LAMPORTS_PER_SOL,
385
- winner,
386
- };
387
- }