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/README.md +155 -0
- package/bin/nara-cli.ts +32 -0
- package/dist/nara-cli.mjs +2631 -0
- package/dist/quest/nara_quest.json +534 -0
- package/dist/zk/answer_proof.wasm +0 -0
- package/dist/zk/answer_proof_final.zkey +0 -0
- package/index.ts +76 -0
- package/package.json +54 -0
- package/src/cli/commands/config.ts +125 -0
- package/src/cli/commands/migrate.ts +270 -0
- package/src/cli/commands/pool.ts +364 -0
- package/src/cli/commands/quest.ts +312 -0
- package/src/cli/commands/swap.ts +349 -0
- package/src/cli/commands/wallet.ts +719 -0
- package/src/cli/index.ts +25 -0
- package/src/cli/quest/nara_quest.json +534 -0
- package/src/cli/quest/nara_quest_types.ts +540 -0
- package/src/cli/types.ts +207 -0
- package/src/cli/utils/output.ts +110 -0
- package/src/cli/utils/transaction.ts +146 -0
- package/src/cli/utils/validation.ts +120 -0
- package/src/cli/utils/wallet.ts +72 -0
- package/src/cli/zk/answer_proof.wasm +0 -0
- package/src/cli/zk/answer_proof_final.zkey +0 -0
- package/src/client.ts +96 -0
- package/src/config.ts +132 -0
- package/src/constants.ts +29 -0
- package/src/migrate.ts +222 -0
- package/src/pool.ts +259 -0
- package/src/quest.ts +379 -0
- package/src/swap.ts +608 -0
- package/src/types/snarkjs.d.ts +9 -0
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
|
+
}
|