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
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pool commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { NaraSDK } from "../../client";
|
|
7
|
+
import { DEFAULT_DBC_CONFIG_ADDRESS } from "../../constants";
|
|
8
|
+
import {
|
|
9
|
+
createPool,
|
|
10
|
+
createPoolWithFirstBuy,
|
|
11
|
+
getPoolInfo,
|
|
12
|
+
getPoolProgress,
|
|
13
|
+
} from "../../pool";
|
|
14
|
+
import { loadWallet, getRpcUrl } from "../utils/wallet";
|
|
15
|
+
import {
|
|
16
|
+
validatePublicKey,
|
|
17
|
+
validateRequired,
|
|
18
|
+
validatePositiveNumber,
|
|
19
|
+
validateNonNegativeNumber,
|
|
20
|
+
} from "../utils/validation";
|
|
21
|
+
import {
|
|
22
|
+
handleTransaction,
|
|
23
|
+
printTransactionResult,
|
|
24
|
+
} from "../utils/transaction";
|
|
25
|
+
import { formatOutput, printError, printInfo } from "../utils/output";
|
|
26
|
+
import type {
|
|
27
|
+
PoolCreateOptions,
|
|
28
|
+
PoolCreateWithBuyOptions,
|
|
29
|
+
PoolInfoOptions,
|
|
30
|
+
} from "../types";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register pool commands
|
|
34
|
+
* @param program Commander program
|
|
35
|
+
*/
|
|
36
|
+
export function registerPoolCommands(program: Command): void {
|
|
37
|
+
const pool = program.command("pool").description("Pool management commands");
|
|
38
|
+
|
|
39
|
+
// pool create
|
|
40
|
+
pool
|
|
41
|
+
.command("create")
|
|
42
|
+
.description("Create a new token pool")
|
|
43
|
+
.requiredOption("-n, --name <string>", "Token name")
|
|
44
|
+
.requiredOption("-s, --symbol <string>", "Token symbol")
|
|
45
|
+
.requiredOption("-u, --uri <string>", "Metadata URI")
|
|
46
|
+
.requiredOption(
|
|
47
|
+
"--dbc-config <address>",
|
|
48
|
+
"DBC config address (or set DBC_CONFIG_ADDRESS env)"
|
|
49
|
+
)
|
|
50
|
+
.option("--creator <address>", "Pool creator address")
|
|
51
|
+
.option("-e, --export-tx", "Export unsigned transaction", false)
|
|
52
|
+
.action(async (options: PoolCreateOptions) => {
|
|
53
|
+
try {
|
|
54
|
+
await handlePoolCreate(options);
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
printError(error.message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// pool create-with-buy
|
|
62
|
+
pool
|
|
63
|
+
.command("create-with-buy")
|
|
64
|
+
.description("Create a new token pool with initial buy")
|
|
65
|
+
.requiredOption("-n, --name <string>", "Token name")
|
|
66
|
+
.requiredOption("-s, --symbol <string>", "Token symbol")
|
|
67
|
+
.requiredOption("-u, --uri <string>", "Metadata URI")
|
|
68
|
+
.requiredOption(
|
|
69
|
+
"--dbc-config <address>",
|
|
70
|
+
"DBC config address (or set DBC_CONFIG_ADDRESS env)"
|
|
71
|
+
)
|
|
72
|
+
.requiredOption("--amount <number>", "Initial buy amount in NSO")
|
|
73
|
+
.option("--creator <address>", "Pool creator address")
|
|
74
|
+
.option("--buyer <address>", "Buyer address")
|
|
75
|
+
.option("--receiver <address>", "Token receiver address")
|
|
76
|
+
.option("--slippage <number>", "Slippage in basis points", "100")
|
|
77
|
+
.option("-e, --export-tx", "Export unsigned transaction", false)
|
|
78
|
+
.action(async (options: PoolCreateWithBuyOptions) => {
|
|
79
|
+
try {
|
|
80
|
+
await handlePoolCreateWithBuy(options);
|
|
81
|
+
} catch (error: any) {
|
|
82
|
+
printError(error.message);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// pool info
|
|
88
|
+
pool
|
|
89
|
+
.command("info <token-address>")
|
|
90
|
+
.description("Get pool information")
|
|
91
|
+
.action(async (tokenAddress: string, options: PoolInfoOptions) => {
|
|
92
|
+
try {
|
|
93
|
+
await handlePoolInfo(tokenAddress, options);
|
|
94
|
+
} catch (error: any) {
|
|
95
|
+
printError(error.message);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// pool progress
|
|
101
|
+
pool
|
|
102
|
+
.command("progress <token-address>")
|
|
103
|
+
.description("Get bonding curve progress")
|
|
104
|
+
.action(async (tokenAddress: string, options: PoolInfoOptions) => {
|
|
105
|
+
try {
|
|
106
|
+
await handlePoolProgress(tokenAddress, options);
|
|
107
|
+
} catch (error: any) {
|
|
108
|
+
printError(error.message);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Handle pool create command
|
|
116
|
+
* @param options Command options
|
|
117
|
+
*/
|
|
118
|
+
async function handlePoolCreate(options: PoolCreateOptions): Promise<void> {
|
|
119
|
+
// Load wallet
|
|
120
|
+
const wallet = await loadWallet(options.wallet);
|
|
121
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
122
|
+
|
|
123
|
+
printInfo(`Using RPC: ${rpcUrl}`);
|
|
124
|
+
printInfo(`Wallet: ${wallet.publicKey.toBase58()}`);
|
|
125
|
+
|
|
126
|
+
// Initialize SDK
|
|
127
|
+
const sdk = new NaraSDK({
|
|
128
|
+
rpcUrl,
|
|
129
|
+
commitment: "confirmed",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Get config address from option or env
|
|
133
|
+
const configAddress =
|
|
134
|
+
options.dbcConfig || DEFAULT_DBC_CONFIG_ADDRESS;
|
|
135
|
+
if (!configAddress) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
"DBC config address is required. Use --dbc-config flag or set DBC_CONFIG_ADDRESS environment variable."
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Parse addresses
|
|
142
|
+
const configPubkey = validatePublicKey(configAddress);
|
|
143
|
+
const creator = options.creator
|
|
144
|
+
? validatePublicKey(options.creator)
|
|
145
|
+
: wallet.publicKey;
|
|
146
|
+
|
|
147
|
+
printInfo("Creating token pool...");
|
|
148
|
+
|
|
149
|
+
// Create pool
|
|
150
|
+
const result = await createPool(sdk, {
|
|
151
|
+
name: options.name,
|
|
152
|
+
symbol: options.symbol,
|
|
153
|
+
uri: options.uri,
|
|
154
|
+
configAddress,
|
|
155
|
+
payer: wallet.publicKey,
|
|
156
|
+
poolCreator: creator,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
printInfo(`Pool address: ${result.poolAddress}`);
|
|
160
|
+
printInfo(`Token address: ${result.baseMint}`);
|
|
161
|
+
|
|
162
|
+
// Handle transaction
|
|
163
|
+
const txResult = await handleTransaction(
|
|
164
|
+
sdk,
|
|
165
|
+
result.transaction,
|
|
166
|
+
[wallet, result.baseMintKeypair], // Both wallet and baseMint keypair need to sign
|
|
167
|
+
options.exportTx || false
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Output result
|
|
171
|
+
if (options.json) {
|
|
172
|
+
const output = {
|
|
173
|
+
poolAddress: result.poolAddress,
|
|
174
|
+
tokenAddress: result.baseMint,
|
|
175
|
+
...(txResult.signature && { signature: txResult.signature }),
|
|
176
|
+
...(txResult.base64 && { transaction: txResult.base64 }),
|
|
177
|
+
};
|
|
178
|
+
console.log(JSON.stringify(output, null, 2));
|
|
179
|
+
} else {
|
|
180
|
+
console.log(`\nPool Address: ${result.poolAddress}`);
|
|
181
|
+
console.log(`Token Address: ${result.baseMint}`);
|
|
182
|
+
printTransactionResult(txResult, false);
|
|
183
|
+
|
|
184
|
+
if (txResult.signature) {
|
|
185
|
+
printInfo("\nSave this token address for buying/selling:");
|
|
186
|
+
console.log(`export TOKEN_ADDRESS="${result.baseMint}"`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Handle pool create-with-buy command
|
|
193
|
+
* @param options Command options
|
|
194
|
+
*/
|
|
195
|
+
async function handlePoolCreateWithBuy(
|
|
196
|
+
options: PoolCreateWithBuyOptions
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
// Load wallet
|
|
199
|
+
const wallet = await loadWallet(options.wallet);
|
|
200
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
201
|
+
|
|
202
|
+
printInfo(`Using RPC: ${rpcUrl}`);
|
|
203
|
+
printInfo(`Wallet: ${wallet.publicKey.toBase58()}`);
|
|
204
|
+
|
|
205
|
+
// Initialize SDK
|
|
206
|
+
const sdk = new NaraSDK({
|
|
207
|
+
rpcUrl,
|
|
208
|
+
commitment: "confirmed",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Get config address from option or env
|
|
212
|
+
const configAddress =
|
|
213
|
+
options.dbcConfig || DEFAULT_DBC_CONFIG_ADDRESS;
|
|
214
|
+
if (!configAddress) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
"DBC config address is required. Use --dbc-config flag or set DBC_CONFIG_ADDRESS environment variable."
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Parse addresses
|
|
221
|
+
const configPubkey = validatePublicKey(configAddress);
|
|
222
|
+
const creator = options.creator
|
|
223
|
+
? validatePublicKey(options.creator)
|
|
224
|
+
: wallet.publicKey;
|
|
225
|
+
const buyer = options.buyer
|
|
226
|
+
? validatePublicKey(options.buyer)
|
|
227
|
+
: wallet.publicKey;
|
|
228
|
+
const receiver = options.receiver
|
|
229
|
+
? validatePublicKey(options.receiver)
|
|
230
|
+
: buyer;
|
|
231
|
+
|
|
232
|
+
// Parse numbers
|
|
233
|
+
const amount = validatePositiveNumber(options.amount, "amount");
|
|
234
|
+
const slippage = validateNonNegativeNumber(
|
|
235
|
+
options.slippage || "100",
|
|
236
|
+
"slippage"
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
printInfo("Creating token pool with initial buy...");
|
|
240
|
+
printInfo(`Initial buy amount: ${amount} NSO`);
|
|
241
|
+
|
|
242
|
+
// Create pool with first buy
|
|
243
|
+
const result = await createPoolWithFirstBuy(sdk, {
|
|
244
|
+
name: options.name,
|
|
245
|
+
symbol: options.symbol,
|
|
246
|
+
uri: options.uri,
|
|
247
|
+
configAddress,
|
|
248
|
+
payer: wallet.publicKey,
|
|
249
|
+
poolCreator: creator,
|
|
250
|
+
initialBuyAmountSOL: amount,
|
|
251
|
+
buyer,
|
|
252
|
+
receiver,
|
|
253
|
+
slippageBps: slippage,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
printInfo(`Pool address: ${result.poolAddress}`);
|
|
257
|
+
printInfo(`Token address: ${result.baseMint}`);
|
|
258
|
+
|
|
259
|
+
// Handle transaction
|
|
260
|
+
const txResult = await handleTransaction(
|
|
261
|
+
sdk,
|
|
262
|
+
result.createPoolTx,
|
|
263
|
+
[wallet, result.baseMintKeypair], // Both wallet and baseMint keypair need to sign
|
|
264
|
+
options.exportTx || false
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Output result
|
|
268
|
+
if (options.json) {
|
|
269
|
+
const output = {
|
|
270
|
+
poolAddress: result.poolAddress,
|
|
271
|
+
tokenAddress: result.baseMint,
|
|
272
|
+
buyInfo: result.buyInfo,
|
|
273
|
+
...(txResult.signature && { signature: txResult.signature }),
|
|
274
|
+
...(txResult.base64 && { transaction: txResult.base64 }),
|
|
275
|
+
};
|
|
276
|
+
console.log(JSON.stringify(output, null, 2));
|
|
277
|
+
} else {
|
|
278
|
+
console.log(`\nPool Address: ${result.poolAddress}`);
|
|
279
|
+
console.log(`Token Address: ${result.baseMint}`);
|
|
280
|
+
console.log(`\nBuy Info:`);
|
|
281
|
+
console.log(` Amount In: ${(parseInt(result.buyInfo.amountIn) / 1e9).toFixed(4)} NSO`);
|
|
282
|
+
console.log(` Minimum Out: ${result.buyInfo.minimumAmountOut} tokens (smallest unit)`);
|
|
283
|
+
printTransactionResult(txResult, false);
|
|
284
|
+
|
|
285
|
+
if (txResult.signature) {
|
|
286
|
+
printInfo("\nSave this token address for buying/selling:");
|
|
287
|
+
console.log(`export TOKEN_ADDRESS="${result.baseMint}"`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Handle pool info command
|
|
294
|
+
* @param tokenAddress Token address
|
|
295
|
+
* @param options Command options
|
|
296
|
+
*/
|
|
297
|
+
async function handlePoolInfo(
|
|
298
|
+
tokenAddress: string,
|
|
299
|
+
options: PoolInfoOptions
|
|
300
|
+
): Promise<void> {
|
|
301
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
302
|
+
|
|
303
|
+
printInfo(`Using RPC: ${rpcUrl}`);
|
|
304
|
+
|
|
305
|
+
// Validate address
|
|
306
|
+
validatePublicKey(tokenAddress);
|
|
307
|
+
|
|
308
|
+
// Initialize SDK
|
|
309
|
+
const sdk = new NaraSDK({
|
|
310
|
+
rpcUrl,
|
|
311
|
+
commitment: "confirmed",
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
printInfo("Fetching pool information...");
|
|
315
|
+
|
|
316
|
+
// Get pool info
|
|
317
|
+
const poolInfo = await getPoolInfo(sdk, tokenAddress);
|
|
318
|
+
|
|
319
|
+
// Output result
|
|
320
|
+
formatOutput(poolInfo, options.json || false);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Handle pool progress command
|
|
325
|
+
* @param tokenAddress Token address
|
|
326
|
+
* @param options Command options
|
|
327
|
+
*/
|
|
328
|
+
async function handlePoolProgress(
|
|
329
|
+
tokenAddress: string,
|
|
330
|
+
options: PoolInfoOptions
|
|
331
|
+
): Promise<void> {
|
|
332
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
333
|
+
|
|
334
|
+
printInfo(`Using RPC: ${rpcUrl}`);
|
|
335
|
+
|
|
336
|
+
// Validate address
|
|
337
|
+
validatePublicKey(tokenAddress);
|
|
338
|
+
|
|
339
|
+
// Initialize SDK
|
|
340
|
+
const sdk = new NaraSDK({
|
|
341
|
+
rpcUrl,
|
|
342
|
+
commitment: "confirmed",
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
printInfo("Fetching bonding curve progress...");
|
|
346
|
+
|
|
347
|
+
// Get pool progress
|
|
348
|
+
const progress = await getPoolProgress(sdk, tokenAddress);
|
|
349
|
+
|
|
350
|
+
// Format progress as percentage for human-readable output
|
|
351
|
+
const output = {
|
|
352
|
+
progress: `${(progress.progress * 100).toFixed(2)}%`,
|
|
353
|
+
progressRaw: progress.progress,
|
|
354
|
+
quoteReserve: progress.quoteReserve,
|
|
355
|
+
isMigrated: progress.isMigrated,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// Output result
|
|
359
|
+
if (options.json) {
|
|
360
|
+
console.log(JSON.stringify(progress, null, 2));
|
|
361
|
+
} else {
|
|
362
|
+
formatOutput(output, false);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quest commands - interact with nara-quest on-chain quiz
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { Connection, Keypair } from "@solana/web3.js";
|
|
7
|
+
import { loadWallet, getRpcUrl } from "../utils/wallet";
|
|
8
|
+
import {
|
|
9
|
+
formatOutput,
|
|
10
|
+
printError,
|
|
11
|
+
printInfo,
|
|
12
|
+
printSuccess,
|
|
13
|
+
printWarning,
|
|
14
|
+
} from "../utils/output";
|
|
15
|
+
import type { GlobalOptions } from "../types";
|
|
16
|
+
import { DEFAULT_QUEST_RELAY_URL } from "../../constants";
|
|
17
|
+
import {
|
|
18
|
+
getQuestInfo,
|
|
19
|
+
hasAnswered,
|
|
20
|
+
generateProof,
|
|
21
|
+
submitAnswer,
|
|
22
|
+
submitAnswerViaRelay,
|
|
23
|
+
parseQuestReward,
|
|
24
|
+
} from "../../quest";
|
|
25
|
+
|
|
26
|
+
// ─── Anchor error parsing ────────────────────────────────────────
|
|
27
|
+
const QUEST_ERRORS: Record<number, string> = {
|
|
28
|
+
6000: "unauthorized",
|
|
29
|
+
6001: "poolNotActive",
|
|
30
|
+
6002: "deadlineExpired",
|
|
31
|
+
6003: "invalidProof",
|
|
32
|
+
6004: "invalidDeadline",
|
|
33
|
+
6005: "insufficientReward",
|
|
34
|
+
6006: "insufficientPoolBalance",
|
|
35
|
+
6007: "questionTooLong",
|
|
36
|
+
6008: "alreadyAnswered",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function anchorErrorCode(err: any): string {
|
|
40
|
+
const code = err?.error?.errorCode?.code;
|
|
41
|
+
if (code) return code;
|
|
42
|
+
const raw = err?.message ?? JSON.stringify(err) ?? "";
|
|
43
|
+
const m = raw.match(/"Custom":(\d+)/);
|
|
44
|
+
if (m) return QUEST_ERRORS[parseInt(m[1])] ?? "";
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
49
|
+
function formatTimeRemaining(seconds: number): string {
|
|
50
|
+
if (seconds <= 0) return "expired";
|
|
51
|
+
const m = Math.floor(seconds / 60);
|
|
52
|
+
const s = seconds % 60;
|
|
53
|
+
if (m > 0) return `${m}m ${s}s`;
|
|
54
|
+
return `${s}s`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Command: quest get ──────────────────────────────────────────
|
|
58
|
+
async function handleQuestGet(options: GlobalOptions) {
|
|
59
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
60
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
61
|
+
|
|
62
|
+
let wallet: Keypair;
|
|
63
|
+
try {
|
|
64
|
+
wallet = await loadWallet(options.wallet);
|
|
65
|
+
} catch {
|
|
66
|
+
wallet = Keypair.generate();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let quest;
|
|
70
|
+
try {
|
|
71
|
+
quest = await getQuestInfo(connection, wallet);
|
|
72
|
+
} catch (err: any) {
|
|
73
|
+
printError(`Failed to fetch quest info: ${err.message}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!quest.active) {
|
|
78
|
+
printWarning("No active quest at the moment");
|
|
79
|
+
if (options.json) {
|
|
80
|
+
formatOutput({ active: false }, true);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const data = {
|
|
86
|
+
round: quest.round,
|
|
87
|
+
questionId: quest.questionId,
|
|
88
|
+
question: quest.question,
|
|
89
|
+
rewardPerWinner: `${quest.rewardPerWinner} NSO`,
|
|
90
|
+
totalReward: `${quest.totalReward} NSO`,
|
|
91
|
+
rewardSlots: `${quest.winnerCount}/${quest.rewardCount}`,
|
|
92
|
+
remainingRewardSlots: quest.remainingSlots,
|
|
93
|
+
deadline: new Date(quest.deadline * 1000).toLocaleString(),
|
|
94
|
+
timeRemaining: formatTimeRemaining(quest.timeRemaining),
|
|
95
|
+
expired: quest.expired,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (options.json) {
|
|
99
|
+
formatOutput(data, true);
|
|
100
|
+
} else {
|
|
101
|
+
console.log("");
|
|
102
|
+
console.log(` Question: ${quest.question}`);
|
|
103
|
+
console.log(` Round: #${quest.round}`);
|
|
104
|
+
console.log(` Reward per winner: ${quest.rewardPerWinner} NSO`);
|
|
105
|
+
console.log(` Total reward: ${quest.totalReward} NSO`);
|
|
106
|
+
console.log(
|
|
107
|
+
` Reward slots: ${quest.winnerCount}/${quest.rewardCount} (${quest.remainingSlots} remaining)`
|
|
108
|
+
);
|
|
109
|
+
console.log(` Deadline: ${new Date(quest.deadline * 1000).toLocaleString()}`);
|
|
110
|
+
if (quest.timeRemaining > 0) {
|
|
111
|
+
console.log(` Time remaining: ${formatTimeRemaining(quest.timeRemaining)}`);
|
|
112
|
+
} else {
|
|
113
|
+
printWarning("Quest has expired");
|
|
114
|
+
}
|
|
115
|
+
console.log("");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Command: quest answer ───────────────────────────────────────
|
|
120
|
+
async function handleQuestAnswer(
|
|
121
|
+
answer: string,
|
|
122
|
+
options: GlobalOptions & { relay?: string }
|
|
123
|
+
) {
|
|
124
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
125
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
126
|
+
const wallet = await loadWallet(options.wallet);
|
|
127
|
+
|
|
128
|
+
// 1. Fetch quest info
|
|
129
|
+
let quest;
|
|
130
|
+
try {
|
|
131
|
+
quest = await getQuestInfo(connection, wallet);
|
|
132
|
+
} catch (err: any) {
|
|
133
|
+
printError(`Failed to fetch quest info: ${err.message}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!quest.active) {
|
|
138
|
+
printError("No active quest at the moment");
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (quest.expired) {
|
|
143
|
+
printError("Quest has expired");
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 2. Check if already answered this round
|
|
148
|
+
const alreadyAnswered = await hasAnswered(connection, wallet);
|
|
149
|
+
if (alreadyAnswered) {
|
|
150
|
+
printWarning("You have already answered this round");
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 3. Generate ZK proof
|
|
155
|
+
printInfo("Generating ZK proof...");
|
|
156
|
+
|
|
157
|
+
let proof;
|
|
158
|
+
try {
|
|
159
|
+
proof = await generateProof(answer, quest.answerHash, wallet.publicKey);
|
|
160
|
+
} catch (err: any) {
|
|
161
|
+
if (err.message?.includes("Assert Failed")) {
|
|
162
|
+
printError("Wrong answer");
|
|
163
|
+
} else {
|
|
164
|
+
printError(`ZK proof generation failed: ${err.message}`);
|
|
165
|
+
}
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 4. Check deadline again after proof generation
|
|
170
|
+
const nowAfterProof = Math.floor(Date.now() / 1000);
|
|
171
|
+
if (nowAfterProof >= quest.deadline) {
|
|
172
|
+
printError("Quest expired during proof generation");
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 5. Submit answer
|
|
177
|
+
if (options.relay) {
|
|
178
|
+
// Relay (gasless) submission
|
|
179
|
+
printInfo("Submitting answer via relay...");
|
|
180
|
+
try {
|
|
181
|
+
const relayResult = await submitAnswerViaRelay(
|
|
182
|
+
options.relay,
|
|
183
|
+
wallet.publicKey,
|
|
184
|
+
proof.hex
|
|
185
|
+
);
|
|
186
|
+
printSuccess("Answer submitted via relay!");
|
|
187
|
+
console.log(` Transaction: ${relayResult.txHash}`);
|
|
188
|
+
await handleReward(connection, relayResult.txHash, options);
|
|
189
|
+
} catch (err: any) {
|
|
190
|
+
printError(`Relay submission failed: ${err.message}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
// Direct on-chain submission
|
|
195
|
+
printInfo("Submitting answer...");
|
|
196
|
+
try {
|
|
197
|
+
const result = await submitAnswer(connection, wallet, proof.solana);
|
|
198
|
+
printSuccess("Answer submitted!");
|
|
199
|
+
console.log(` Transaction: ${result.signature}`);
|
|
200
|
+
await handleReward(connection, result.signature, options);
|
|
201
|
+
} catch (err: any) {
|
|
202
|
+
handleSubmitError(err);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─── Parse reward from transaction ───────────────────────────────
|
|
208
|
+
async function handleReward(
|
|
209
|
+
connection: Connection,
|
|
210
|
+
txSignature: string,
|
|
211
|
+
options: GlobalOptions
|
|
212
|
+
) {
|
|
213
|
+
printInfo("Fetching transaction details...");
|
|
214
|
+
|
|
215
|
+
let reward;
|
|
216
|
+
try {
|
|
217
|
+
reward = await parseQuestReward(connection, txSignature);
|
|
218
|
+
} catch {
|
|
219
|
+
printWarning("Failed to fetch transaction details. Please check manually later.");
|
|
220
|
+
console.log(
|
|
221
|
+
` https://solscan.io/tx/${txSignature}?cluster=devnet`
|
|
222
|
+
);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (reward.rewarded) {
|
|
227
|
+
printSuccess(`Congratulations! Reward received: ${reward.rewardNso} NSO (winner ${reward.winner})`);
|
|
228
|
+
if (options.json) {
|
|
229
|
+
formatOutput(
|
|
230
|
+
{
|
|
231
|
+
signature: txSignature,
|
|
232
|
+
rewarded: true,
|
|
233
|
+
rewardLamports: reward.rewardLamports,
|
|
234
|
+
rewardNso: reward.rewardNso,
|
|
235
|
+
winner: reward.winner,
|
|
236
|
+
},
|
|
237
|
+
true
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
printWarning("Correct answer, but no reward — all reward slots have been claimed");
|
|
242
|
+
if (options.json) {
|
|
243
|
+
formatOutput(
|
|
244
|
+
{ signature: txSignature, rewarded: false, rewardLamports: 0 },
|
|
245
|
+
true
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─── Error handling ──────────────────────────────────────────────
|
|
252
|
+
function handleSubmitError(err: any) {
|
|
253
|
+
const errCode = anchorErrorCode(err);
|
|
254
|
+
switch (errCode) {
|
|
255
|
+
case "alreadyAnswered":
|
|
256
|
+
printWarning("You have already answered this round");
|
|
257
|
+
break;
|
|
258
|
+
case "deadlineExpired":
|
|
259
|
+
printError("Quest has expired");
|
|
260
|
+
break;
|
|
261
|
+
case "invalidProof":
|
|
262
|
+
printError("Wrong answer (ZK proof verification failed)");
|
|
263
|
+
break;
|
|
264
|
+
case "poolNotActive":
|
|
265
|
+
printError("No active quest at the moment");
|
|
266
|
+
break;
|
|
267
|
+
default:
|
|
268
|
+
printError(`Failed to submit answer: ${err.message ?? String(err)}`);
|
|
269
|
+
if (err.logs) {
|
|
270
|
+
console.log(" Logs:");
|
|
271
|
+
err.logs.slice(-5).forEach((l: string) => console.log(` ${l}`));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── Register commands ───────────────────────────────────────────
|
|
278
|
+
export function registerQuestCommands(program: Command): void {
|
|
279
|
+
const quest = program
|
|
280
|
+
.command("quest")
|
|
281
|
+
.description("Quest commands");
|
|
282
|
+
|
|
283
|
+
// quest get
|
|
284
|
+
quest
|
|
285
|
+
.command("get")
|
|
286
|
+
.description("Get current quest info")
|
|
287
|
+
.action(async (_opts: any, cmd: Command) => {
|
|
288
|
+
try {
|
|
289
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
|
|
290
|
+
await handleQuestGet(globalOpts);
|
|
291
|
+
} catch (error: any) {
|
|
292
|
+
printError(error.message);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// quest answer
|
|
298
|
+
quest
|
|
299
|
+
.command("answer <answer>")
|
|
300
|
+
.description("Submit an answer")
|
|
301
|
+
.option("--relay [url]", `Submit via relay service, gasless (default: ${DEFAULT_QUEST_RELAY_URL})`)
|
|
302
|
+
.action(async (answer: string, opts: any, cmd: Command) => {
|
|
303
|
+
try {
|
|
304
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
|
|
305
|
+
const relayUrl = opts.relay === true ? DEFAULT_QUEST_RELAY_URL : opts.relay;
|
|
306
|
+
await handleQuestAnswer(answer, { ...globalOpts, relay: relayUrl });
|
|
307
|
+
} catch (error: any) {
|
|
308
|
+
printError(error.message);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|