naracli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/pool.ts ADDED
@@ -0,0 +1,259 @@
1
+ import {
2
+ Keypair,
3
+ PublicKey,
4
+ Transaction,
5
+ VersionedTransaction,
6
+ } from "@solana/web3.js";
7
+ import { NATIVE_MINT } from "@solana/spl-token";
8
+ import BN from "bn.js";
9
+ import { deriveDbcPoolAddress } from "@meteora-ag/dynamic-bonding-curve-sdk";
10
+ import { NaraSDK } from "./client";
11
+
12
+ export interface CreatePoolParams {
13
+ name: string;
14
+ symbol: string;
15
+ uri: string;
16
+ configAddress: string;
17
+ payer: PublicKey;
18
+ poolCreator: PublicKey;
19
+ }
20
+
21
+ export interface CreatePoolResult {
22
+ /** Pool address */
23
+ poolAddress: string;
24
+ /** Token address (baseMint) */
25
+ baseMint: string;
26
+ /** Unsigned transaction for pool creation (returns VersionedTransaction if ALT is configured) */
27
+ transaction: Transaction | VersionedTransaction;
28
+ /** baseMint keypair (requires signature) */
29
+ baseMintKeypair: Keypair;
30
+ }
31
+
32
+ export interface CreatePoolWithFirstBuyParams extends CreatePoolParams {
33
+ /** Initial buy amount in SOL */
34
+ initialBuyAmountSOL: number;
35
+ /** Buyer (defaults to payer) */
36
+ buyer?: PublicKey;
37
+ /** Token receiver (defaults to buyer) */
38
+ receiver?: PublicKey;
39
+ /** Slippage in basis points (default 100 = 1%) */
40
+ slippageBps?: number;
41
+ }
42
+
43
+ export interface CreatePoolWithFirstBuyResult {
44
+ /** Pool address */
45
+ poolAddress: string;
46
+ /** Token address (baseMint) */
47
+ baseMint: string;
48
+ /** Pool creation transaction (returns VersionedTransaction if ALT is configured) */
49
+ createPoolTx: Transaction | VersionedTransaction;
50
+ /** First buy transaction (returns VersionedTransaction if ALT is configured) */
51
+ firstBuyTx: Transaction | VersionedTransaction;
52
+ /** baseMint keypair (requires signature) */
53
+ baseMintKeypair: Keypair;
54
+ /** Buy information */
55
+ buyInfo: {
56
+ amountIn: string;
57
+ minimumAmountOut: string;
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Create token pool transaction (returns unsigned transaction)
63
+ *
64
+ * Note: If you want to make an initial buy, wait for this transaction to confirm
65
+ * before using the buyToken() function
66
+ *
67
+ * @param sdk NaraSDK SDK instance
68
+ * @param params Pool parameters
69
+ * @returns Pool address, token address, unsigned transaction, and baseMint keypair
70
+ */
71
+ export async function createPool(
72
+ sdk: NaraSDK,
73
+ params: CreatePoolParams
74
+ ): Promise<CreatePoolResult> {
75
+ const connection = sdk.getConnection();
76
+ const client = sdk.getClient();
77
+
78
+ const baseMint = Keypair.generate();
79
+ const configPubkey = new PublicKey(params.configAddress);
80
+
81
+ // Create pool transaction
82
+ const createPoolTx = await client.pool.createPool({
83
+ baseMint: baseMint.publicKey,
84
+ config: configPubkey,
85
+ name: params.name,
86
+ symbol: params.symbol,
87
+ uri: params.uri,
88
+ payer: params.payer,
89
+ poolCreator: params.poolCreator,
90
+ });
91
+
92
+ // Get latest blockhash
93
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
94
+ createPoolTx.recentBlockhash = blockhash;
95
+ createPoolTx.feePayer = params.payer;
96
+
97
+ // Derive pool address
98
+ const poolPubkey = deriveDbcPoolAddress(
99
+ NATIVE_MINT,
100
+ baseMint.publicKey,
101
+ configPubkey
102
+ );
103
+
104
+ // Compile transaction with ALT if configured
105
+ const compiledTx = await sdk.compileTransactionWithALT(
106
+ createPoolTx,
107
+ params.payer
108
+ );
109
+
110
+ return {
111
+ poolAddress: poolPubkey.toBase58(),
112
+ baseMint: baseMint.publicKey.toBase58(),
113
+ transaction: compiledTx,
114
+ baseMintKeypair: baseMint,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Create token pool and perform first buy (one-step completion)
120
+ *
121
+ * Uses SDK's createPoolWithFirstBuy method to complete creation and buy in one transaction
122
+ *
123
+ * @param sdk NaraSDK SDK instance
124
+ * @param params Pool and buy parameters
125
+ * @returns Pool address, token address, unsigned transactions, and baseMint keypair
126
+ */
127
+ export async function createPoolWithFirstBuy(
128
+ sdk: NaraSDK,
129
+ params: CreatePoolWithFirstBuyParams
130
+ ): Promise<CreatePoolWithFirstBuyResult> {
131
+ const connection = sdk.getConnection();
132
+ const client = sdk.getClient();
133
+
134
+ const baseMint = Keypair.generate();
135
+ const configPubkey = new PublicKey(params.configAddress);
136
+ const buyer = params.buyer ?? params.payer;
137
+ const receiver = params.receiver ?? buyer;
138
+
139
+ // Calculate buy amount
140
+ const buyAmount = new BN(params.initialBuyAmountSOL * 1e9); // SOL to lamports
141
+ const slippageBps = params.slippageBps ?? 100;
142
+
143
+ // For first buy, use conservative minimumAmountOut
144
+ // Set to 0 since this is the first buy with optimal price
145
+ const minimumAmountOut = new BN(0);
146
+
147
+ // Use SDK's createPoolWithFirstBuy method
148
+ const result = await client.pool.createPoolWithFirstBuy({
149
+ createPoolParam: {
150
+ baseMint: baseMint.publicKey,
151
+ config: configPubkey,
152
+ name: params.name,
153
+ symbol: params.symbol,
154
+ uri: params.uri,
155
+ payer: params.payer,
156
+ poolCreator: params.poolCreator,
157
+ },
158
+ firstBuyParam: {
159
+ buyer,
160
+ receiver,
161
+ buyAmount,
162
+ minimumAmountOut,
163
+ referralTokenAccount: null,
164
+ },
165
+ });
166
+
167
+ // Get latest blockhash
168
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
169
+
170
+ // Combine two transactions into one
171
+ const combinedTx = new Transaction();
172
+
173
+ // Add pool creation instructions
174
+ combinedTx.add(...result.createPoolTx.instructions);
175
+
176
+ // If first buy transaction exists, add buy instructions
177
+ if (result.swapBuyTx) {
178
+ combinedTx.add(...result.swapBuyTx.instructions);
179
+ }
180
+
181
+ // Set transaction metadata
182
+ combinedTx.recentBlockhash = blockhash;
183
+ combinedTx.feePayer = params.payer;
184
+
185
+ // Derive pool address
186
+ const poolPubkey = deriveDbcPoolAddress(
187
+ NATIVE_MINT,
188
+ baseMint.publicKey,
189
+ configPubkey
190
+ );
191
+
192
+ // Compile transaction with ALT if configured
193
+ const compiledTx = await sdk.compileTransactionWithALT(
194
+ combinedTx,
195
+ params.payer
196
+ );
197
+
198
+ return {
199
+ poolAddress: poolPubkey.toBase58(),
200
+ baseMint: baseMint.publicKey.toBase58(),
201
+ createPoolTx: compiledTx, // Return combined transaction
202
+ firstBuyTx: compiledTx, // Same as createPoolTx since they are combined
203
+ baseMintKeypair: baseMint,
204
+ buyInfo: {
205
+ amountIn: buyAmount.toString(),
206
+ minimumAmountOut: minimumAmountOut.toString(),
207
+ },
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Get pool information
213
+ * @param sdk NaraSDK SDK instance
214
+ * @param tokenAddress Token address (baseMint)
215
+ * @returns Pool information
216
+ */
217
+ export async function getPoolInfo(sdk: NaraSDK, tokenAddress: string) {
218
+ const client = sdk.getClient();
219
+ const tokenPubkey = new PublicKey(tokenAddress);
220
+
221
+ // Get pool by token (baseMint) address
222
+ const poolAccount = await client.state.getPoolByBaseMint(tokenPubkey);
223
+ if (!poolAccount) {
224
+ throw new Error(`Pool not found for token: ${tokenAddress}`);
225
+ }
226
+
227
+ return {
228
+ ...poolAccount.account,
229
+ poolAddress: poolAccount.publicKey.toBase58(),
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Get pool curve progress
235
+ * @param sdk NaraSDK SDK instance
236
+ * @param tokenAddress Token address (baseMint)
237
+ * @returns Curve progress information
238
+ */
239
+ export async function getPoolProgress(sdk: NaraSDK, tokenAddress: string) {
240
+ const client = sdk.getClient();
241
+ const tokenPubkey = new PublicKey(tokenAddress);
242
+
243
+ // Get pool by token (baseMint) address
244
+ const poolAccount = await client.state.getPoolByBaseMint(tokenPubkey);
245
+ if (!poolAccount) {
246
+ throw new Error(`Pool not found for token: ${tokenAddress}`);
247
+ }
248
+
249
+ const progress = await client.state.getPoolCurveProgress(
250
+ poolAccount.publicKey
251
+ );
252
+ const pool = poolAccount.account;
253
+
254
+ return {
255
+ progress,
256
+ quoteReserve: pool.quoteReserve?.toString() ?? "0",
257
+ isMigrated: pool.isMigrated ?? false,
258
+ };
259
+ }
package/src/quest.ts ADDED
@@ -0,0 +1,379 @@
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
+
15
+ import { createRequire } from "module";
16
+ const _require = createRequire(import.meta.url);
17
+
18
+ // ─── ZK constants ────────────────────────────────────────────────
19
+ const BN254_FIELD =
20
+ 21888242871839275222246405745257275088696311157297823662689037894645226208583n;
21
+
22
+ import { fileURLToPath } from "url";
23
+ import { dirname, join, resolve } from "path";
24
+ import { existsSync } from "fs";
25
+ const __dirname = dirname(fileURLToPath(import.meta.url));
26
+
27
+ function findZkFile(name: string): string {
28
+ // 1. src/cli/zk/ (dev mode, running from src/)
29
+ const srcPath = join(__dirname, "cli/zk", name);
30
+ if (existsSync(srcPath)) return srcPath;
31
+ // 2. dist/zk/ (published, running from dist/)
32
+ const distPath = join(__dirname, "zk", name);
33
+ if (existsSync(distPath)) return distPath;
34
+ // 3. Fallback to src path (will error at runtime if missing)
35
+ return srcPath;
36
+ }
37
+
38
+ const DEFAULT_CIRCUIT_WASM = findZkFile("answer_proof.wasm");
39
+ const DEFAULT_ZKEY = findZkFile("answer_proof_final.zkey");
40
+
41
+ // ─── Types ───────────────────────────────────────────────────────
42
+
43
+ export interface QuestInfo {
44
+ active: boolean;
45
+ round: string;
46
+ questionId: string;
47
+ question: string;
48
+ answerHash: number[];
49
+ rewardPerWinner: number;
50
+ totalReward: number;
51
+ rewardCount: number;
52
+ winnerCount: number;
53
+ remainingSlots: number;
54
+ deadline: number;
55
+ timeRemaining: number;
56
+ expired: boolean;
57
+ }
58
+
59
+ export interface ZkProof {
60
+ proofA: number[];
61
+ proofB: number[];
62
+ proofC: number[];
63
+ }
64
+
65
+ export interface ZkProofHex {
66
+ proofA: string;
67
+ proofB: string;
68
+ proofC: string;
69
+ }
70
+
71
+ export interface SubmitAnswerResult {
72
+ signature: string;
73
+ }
74
+
75
+ export interface SubmitRelayResult {
76
+ txHash: string;
77
+ }
78
+
79
+ export interface QuestProveOptions {
80
+ circuitWasmPath?: string;
81
+ zkeyPath?: string;
82
+ }
83
+
84
+ // ─── ZK utilities ────────────────────────────────────────────────
85
+
86
+ function toBigEndian32(v: bigint): Buffer {
87
+ return Buffer.from(v.toString(16).padStart(64, "0"), "hex");
88
+ }
89
+
90
+ function answerToField(answer: string): bigint {
91
+ return (
92
+ BigInt("0x" + Buffer.from(answer, "utf-8").toString("hex")) % BN254_FIELD
93
+ );
94
+ }
95
+
96
+ function hashBytesToFieldStr(hashBytes: number[]): string {
97
+ return BigInt("0x" + Buffer.from(hashBytes).toString("hex")).toString();
98
+ }
99
+
100
+ function pubkeyToCircuitInputs(pubkey: PublicKey): {
101
+ lo: string;
102
+ hi: string;
103
+ } {
104
+ const bytes = pubkey.toBuffer();
105
+ return {
106
+ lo: BigInt("0x" + bytes.subarray(16, 32).toString("hex")).toString(),
107
+ hi: BigInt("0x" + bytes.subarray(0, 16).toString("hex")).toString(),
108
+ };
109
+ }
110
+
111
+ function proofToSolana(proof: any): ZkProof {
112
+ const negY = (y: string) => toBigEndian32(BN254_FIELD - BigInt(y));
113
+ const be = (s: string) => toBigEndian32(BigInt(s));
114
+ return {
115
+ proofA: Array.from(
116
+ Buffer.concat([be(proof.pi_a[0]), negY(proof.pi_a[1])])
117
+ ),
118
+ proofB: Array.from(
119
+ Buffer.concat([
120
+ be(proof.pi_b[0][1]),
121
+ be(proof.pi_b[0][0]),
122
+ be(proof.pi_b[1][1]),
123
+ be(proof.pi_b[1][0]),
124
+ ])
125
+ ),
126
+ proofC: Array.from(
127
+ Buffer.concat([be(proof.pi_c[0]), be(proof.pi_c[1])])
128
+ ),
129
+ };
130
+ }
131
+
132
+ function proofToHex(proof: any): ZkProofHex {
133
+ const negY = (y: string) => toBigEndian32(BN254_FIELD - BigInt(y));
134
+ const be = (s: string) => toBigEndian32(BigInt(s));
135
+ return {
136
+ proofA: Buffer.concat([be(proof.pi_a[0]), negY(proof.pi_a[1])]).toString("hex"),
137
+ proofB: Buffer.concat([
138
+ be(proof.pi_b[0][1]),
139
+ be(proof.pi_b[0][0]),
140
+ be(proof.pi_b[1][1]),
141
+ be(proof.pi_b[1][0]),
142
+ ]).toString("hex"),
143
+ proofC: Buffer.concat([be(proof.pi_c[0]), be(proof.pi_c[1])]).toString("hex"),
144
+ };
145
+ }
146
+
147
+ // Suppress console output from snarkjs WASM during proof generation.
148
+ async function silentProve(snarkjs: any, input: Record<string, string>, wasmPath: string, zkeyPath: string) {
149
+ const savedLog = console.log;
150
+ const savedError = console.error;
151
+ console.log = () => {};
152
+ console.error = () => {};
153
+ try {
154
+ return await snarkjs.groth16.fullProve(input, wasmPath, zkeyPath, null, null, { singleThread: true });
155
+ } finally {
156
+ console.log = savedLog;
157
+ console.error = savedError;
158
+ }
159
+ }
160
+
161
+ // ─── Anchor helpers ──────────────────────────────────────────────
162
+
163
+ function createProgram(
164
+ connection: Connection,
165
+ wallet: Keypair
166
+ ): Program<NaraQuest> {
167
+ const idlPath = existsSync(join(__dirname, "cli/quest/nara_quest.json"))
168
+ ? "./cli/quest/nara_quest.json"
169
+ : "./quest/nara_quest.json";
170
+ const idl = _require(idlPath);
171
+ const provider = new AnchorProvider(
172
+ connection,
173
+ new Wallet(wallet),
174
+ { commitment: "confirmed" }
175
+ );
176
+ anchor.setProvider(provider);
177
+ return new Program<NaraQuest>(idl as any, provider);
178
+ }
179
+
180
+ function getPoolPda(programId: PublicKey): PublicKey {
181
+ const [pda] = PublicKey.findProgramAddressSync(
182
+ [Buffer.from("pool")],
183
+ programId
184
+ );
185
+ return pda;
186
+ }
187
+
188
+ function getWinnerRecordPda(
189
+ programId: PublicKey,
190
+ user: PublicKey
191
+ ): PublicKey {
192
+ const [pda] = PublicKey.findProgramAddressSync(
193
+ [Buffer.from("winner"), user.toBuffer()],
194
+ programId
195
+ );
196
+ return pda;
197
+ }
198
+
199
+ // ─── SDK functions ───────────────────────────────────────────────
200
+
201
+ /**
202
+ * Get the current active quest info
203
+ */
204
+ export async function getQuestInfo(
205
+ connection: Connection,
206
+ wallet?: Keypair
207
+ ): Promise<QuestInfo> {
208
+ const kp = wallet ?? Keypair.generate();
209
+ const program = createProgram(connection, kp);
210
+ const poolPda = getPoolPda(program.programId);
211
+ const pool = await program.account.pool.fetch(poolPda);
212
+
213
+ const now = Math.floor(Date.now() / 1000);
214
+ const deadline = pool.deadline.toNumber();
215
+ const secsLeft = deadline - now;
216
+
217
+ return {
218
+ active: pool.isActive,
219
+ round: pool.round.toString(),
220
+ questionId: pool.questionId.toString(),
221
+ question: pool.question,
222
+ answerHash: Array.from(pool.answerHash),
223
+ rewardPerWinner: pool.rewardPerWinner.toNumber() / LAMPORTS_PER_SOL,
224
+ totalReward: pool.rewardAmount.toNumber() / LAMPORTS_PER_SOL,
225
+ rewardCount: pool.rewardCount,
226
+ winnerCount: pool.winnerCount,
227
+ remainingSlots: Math.max(0, pool.rewardCount - pool.winnerCount),
228
+ deadline,
229
+ timeRemaining: secsLeft,
230
+ expired: secsLeft <= 0,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Check if the user has already answered the current round
236
+ */
237
+ export async function hasAnswered(
238
+ connection: Connection,
239
+ wallet: Keypair
240
+ ): Promise<boolean> {
241
+ const program = createProgram(connection, wallet);
242
+ const quest = await getQuestInfo(connection, wallet);
243
+ const winnerPda = getWinnerRecordPda(program.programId, wallet.publicKey);
244
+ try {
245
+ const wr = await program.account.winnerRecord.fetch(winnerPda);
246
+ return wr.round.toString() === quest.round;
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Generate a ZK proof for a quest answer.
254
+ * Throws if the answer is wrong (circuit assertion fails).
255
+ */
256
+ export async function generateProof(
257
+ answer: string,
258
+ answerHash: number[],
259
+ userPubkey: PublicKey,
260
+ options?: QuestProveOptions
261
+ ): Promise<{ solana: ZkProof; hex: ZkProofHex }> {
262
+ const wasmPath = options?.circuitWasmPath ?? process.env.QUEST_CIRCUIT_WASM ?? DEFAULT_CIRCUIT_WASM;
263
+ const zkeyPath = options?.zkeyPath ?? process.env.QUEST_ZKEY ?? DEFAULT_ZKEY;
264
+
265
+ const snarkjs = await import("snarkjs");
266
+ const answerHashFieldStr = hashBytesToFieldStr(answerHash);
267
+ const { lo, hi } = pubkeyToCircuitInputs(userPubkey);
268
+
269
+ const result = await silentProve(
270
+ snarkjs,
271
+ {
272
+ answer: answerToField(answer).toString(),
273
+ answer_hash: answerHashFieldStr,
274
+ pubkey_lo: lo,
275
+ pubkey_hi: hi,
276
+ },
277
+ wasmPath,
278
+ zkeyPath
279
+ );
280
+
281
+ return {
282
+ solana: proofToSolana(result.proof),
283
+ hex: proofToHex(result.proof),
284
+ };
285
+ }
286
+
287
+ /**
288
+ * Submit a quest answer on-chain (direct submission, requires gas)
289
+ */
290
+ export async function submitAnswer(
291
+ connection: Connection,
292
+ wallet: Keypair,
293
+ proof: ZkProof
294
+ ): Promise<SubmitAnswerResult> {
295
+ const program = createProgram(connection, wallet);
296
+ const signature = await program.methods
297
+ .submitAnswer(proof.proofA as any, proof.proofB as any, proof.proofC as any)
298
+ .accounts({ user: wallet.publicKey, payer: wallet.publicKey })
299
+ .signers([wallet])
300
+ .rpc({ skipPreflight: true });
301
+
302
+ return { signature };
303
+ }
304
+
305
+ /**
306
+ * Submit a quest answer via relay (gasless submission)
307
+ */
308
+ export async function submitAnswerViaRelay(
309
+ relayUrl: string,
310
+ userPubkey: PublicKey,
311
+ proof: ZkProofHex
312
+ ): Promise<SubmitRelayResult> {
313
+ const base = relayUrl.replace(/\/+$/, "");
314
+ const res = await fetch(`${base}/submit-answer`, {
315
+ method: "POST",
316
+ headers: { "Content-Type": "application/json" },
317
+ body: JSON.stringify({
318
+ user: userPubkey.toBase58(),
319
+ proofA: proof.proofA,
320
+ proofB: proof.proofB,
321
+ proofC: proof.proofC,
322
+ }),
323
+ });
324
+
325
+ const data = (await res.json()) as any;
326
+ if (!res.ok) {
327
+ throw new Error(`Relay submission failed: ${data.error ?? `HTTP ${res.status}`}`);
328
+ }
329
+
330
+ return { txHash: data.txHash };
331
+ }
332
+
333
+ /**
334
+ * Parse reward info from a quest transaction's log messages
335
+ */
336
+ export async function parseQuestReward(
337
+ connection: Connection,
338
+ txSignature: string,
339
+ retries = 10
340
+ ): Promise<{ rewarded: boolean; rewardLamports: number; rewardNso: number; winner: string }> {
341
+ await new Promise((r) => setTimeout(r, 2000));
342
+
343
+ let txInfo: any;
344
+ for (let i = 0; i < retries; i++) {
345
+ try {
346
+ txInfo = await connection.getTransaction(txSignature, {
347
+ commitment: "confirmed",
348
+ maxSupportedTransactionVersion: 0,
349
+ });
350
+ if (txInfo) break;
351
+ } catch {
352
+ // retry
353
+ }
354
+ await new Promise((r) => setTimeout(r, 1000));
355
+ }
356
+
357
+ if (!txInfo) {
358
+ throw new Error("Failed to fetch transaction details");
359
+ }
360
+
361
+ let rewardLamports = 0;
362
+ let winner = "";
363
+ const logs: string[] = txInfo.meta?.logMessages ?? [];
364
+ for (const log of logs) {
365
+ const m = log.match(/reward (\d+) lamports \(winner (\d+\/\d+)\)/);
366
+ if (m) {
367
+ rewardLamports = parseInt(m[1]!);
368
+ winner = m[2]!;
369
+ break;
370
+ }
371
+ }
372
+
373
+ return {
374
+ rewarded: rewardLamports > 0,
375
+ rewardLamports,
376
+ rewardNso: rewardLamports / LAMPORTS_PER_SOL,
377
+ winner,
378
+ };
379
+ }