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.
- package/README.md +101 -114
- package/bin/nara-cli.ts +0 -20
- package/dist/nara-cli.mjs +49930 -2222
- package/index.ts +10 -58
- package/package.json +7 -6
- package/src/cli/commands/quest.ts +8 -7
- package/src/cli/commands/skills.ts +491 -0
- package/src/cli/commands/skillsInstall.ts +793 -0
- package/src/cli/commands/wallet.ts +13 -114
- package/src/cli/commands/zkid.ts +410 -0
- package/src/cli/index.ts +215 -9
- package/src/cli/prompts/searchMultiselect.ts +297 -0
- package/src/cli/types.ts +0 -138
- package/src/cli/utils/transaction.ts +1 -1
- package/src/cli/utils/validation.ts +0 -40
- package/src/cli/utils/wallet.ts +3 -1
- package/src/tests/helpers.ts +78 -0
- package/src/tests/skills.e2e.test.ts +126 -0
- package/src/tests/skills.test.ts +192 -0
- package/src/tests/test_skill.md +18 -0
- package/src/tests/zkid.e2e.test.ts +128 -0
- package/src/tests/zkid.test.ts +153 -0
- package/src/types/snarkjs.d.ts +4 -1
- package/dist/quest/nara_quest.json +0 -534
- package/dist/zk/answer_proof.wasm +0 -0
- package/dist/zk/answer_proof_final.zkey +0 -0
- package/src/cli/commands/config.ts +0 -125
- package/src/cli/commands/migrate.ts +0 -270
- package/src/cli/commands/pool.ts +0 -364
- package/src/cli/commands/swap.ts +0 -349
- package/src/cli/quest/nara_quest.json +0 -534
- package/src/cli/quest/nara_quest_types.ts +0 -540
- package/src/cli/zk/answer_proof.wasm +0 -0
- package/src/cli/zk/answer_proof_final.zkey +0 -0
- package/src/client.ts +0 -96
- package/src/config.ts +0 -132
- package/src/constants.ts +0 -35
- package/src/migrate.ts +0 -222
- package/src/pool.ts +0 -259
- package/src/quest.ts +0 -387
- 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
|
-
}
|