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.
@@ -0,0 +1,2631 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/nara-cli.ts
4
+ import { Command as Command8 } from "commander";
5
+
6
+ // src/cli/index.ts
7
+ import "commander";
8
+
9
+ // src/cli/commands/config.ts
10
+ import "commander";
11
+
12
+ // src/client.ts
13
+ import {
14
+ Connection,
15
+ PublicKey,
16
+ MessageV0,
17
+ VersionedTransaction
18
+ } from "@solana/web3.js";
19
+ import { DynamicBondingCurveClient } from "@meteora-ag/dynamic-bonding-curve-sdk";
20
+ var NaraSDK = class {
21
+ connection;
22
+ client;
23
+ addressLookupTableAddresses;
24
+ constructor(config) {
25
+ this.connection = new Connection(
26
+ config.rpcUrl,
27
+ config.commitment || "confirmed"
28
+ );
29
+ this.client = new DynamicBondingCurveClient(
30
+ this.connection,
31
+ config.commitment || "confirmed"
32
+ );
33
+ this.addressLookupTableAddresses = (config.addressLookupTableAddresses || []).map((addr) => new PublicKey(addr));
34
+ }
35
+ getConnection() {
36
+ return this.connection;
37
+ }
38
+ getClient() {
39
+ return this.client;
40
+ }
41
+ getAddressLookupTableAddresses() {
42
+ return this.addressLookupTableAddresses;
43
+ }
44
+ /**
45
+ * Compile transaction with Address Lookup Tables
46
+ * Converts Transaction to VersionedTransaction if ALT is configured
47
+ * @param transaction Original transaction
48
+ * @param feePayer Fee payer public key
49
+ * @returns VersionedTransaction or original Transaction
50
+ */
51
+ async compileTransactionWithALT(transaction, feePayer) {
52
+ if (this.addressLookupTableAddresses.length === 0) {
53
+ return transaction;
54
+ }
55
+ const lookupTableAccounts = [];
56
+ for (const address of this.addressLookupTableAddresses) {
57
+ const accountInfo = await this.connection.getAddressLookupTable(address);
58
+ if (accountInfo.value) {
59
+ lookupTableAccounts.push(accountInfo.value);
60
+ }
61
+ }
62
+ if (lookupTableAccounts.length === 0) {
63
+ return transaction;
64
+ }
65
+ const { blockhash } = await this.connection.getLatestBlockhash(
66
+ this.connection.commitment
67
+ );
68
+ const messageV0 = MessageV0.compile({
69
+ payerKey: feePayer,
70
+ instructions: transaction.instructions,
71
+ recentBlockhash: blockhash,
72
+ addressLookupTableAccounts: lookupTableAccounts
73
+ });
74
+ return new VersionedTransaction(messageV0);
75
+ }
76
+ };
77
+
78
+ // src/config.ts
79
+ import { Keypair } from "@solana/web3.js";
80
+ import {
81
+ buildCurveWithMarketCap,
82
+ ActivationType,
83
+ CollectFeeMode,
84
+ BaseFeeMode,
85
+ MigrationFeeOption,
86
+ MigrationOption,
87
+ TokenDecimal,
88
+ TokenType,
89
+ TokenUpdateAuthorityOption
90
+ } from "@meteora-ag/dynamic-bonding-curve-sdk";
91
+ import { NATIVE_MINT } from "@solana/spl-token";
92
+ async function createConfig(sdk, options) {
93
+ const connection = sdk.getConnection();
94
+ const client = sdk.getClient();
95
+ const config = Keypair.generate();
96
+ const curveConfig = buildCurveWithMarketCap({
97
+ totalTokenSupply: options.totalTokenSupply ?? 1e9,
98
+ // Total token supply
99
+ initialMarketCap: options.initialMarketCap ?? 30,
100
+ // Initial market cap
101
+ migrationMarketCap: options.migrationMarketCap ?? 540,
102
+ // Migration market cap threshold
103
+ migrationOption: MigrationOption.MET_DAMM_V2,
104
+ // Migration option: use Meteora DAMM V2
105
+ tokenBaseDecimal: TokenDecimal.SIX,
106
+ // Base token decimals: 6
107
+ tokenQuoteDecimal: TokenDecimal.NINE,
108
+ // Quote token decimals: 9 (SOL)
109
+ // Locked vesting parameters
110
+ lockedVestingParams: {
111
+ totalLockedVestingAmount: 0,
112
+ // Total locked vesting amount
113
+ numberOfVestingPeriod: 0,
114
+ // Number of vesting periods
115
+ cliffUnlockAmount: 0,
116
+ // Cliff unlock amount
117
+ totalVestingDuration: 0,
118
+ // Total vesting duration
119
+ cliffDurationFromMigrationTime: 0
120
+ // Cliff duration from migration time
121
+ },
122
+ // Base fee parameters
123
+ baseFeeParams: {
124
+ baseFeeMode: BaseFeeMode.FeeSchedulerLinear,
125
+ // Fee mode: linear scheduler
126
+ feeSchedulerParam: {
127
+ startingFeeBps: 100,
128
+ // Starting fee (basis points): 1%
129
+ endingFeeBps: 100,
130
+ // Ending fee (basis points): 1%
131
+ numberOfPeriod: 0,
132
+ // Number of periods
133
+ totalDuration: 0
134
+ // Total duration
135
+ }
136
+ },
137
+ dynamicFeeEnabled: true,
138
+ // Enable dynamic fees
139
+ activationType: ActivationType.Slot,
140
+ // Activation type: slot-based
141
+ collectFeeMode: CollectFeeMode.QuoteToken,
142
+ // Fee collection mode: quote token
143
+ migrationFeeOption: MigrationFeeOption.FixedBps25,
144
+ // Migration fee option: fixed 1%
145
+ tokenType: TokenType.SPL,
146
+ // Token type: SPL token
147
+ partnerLiquidityPercentage: 0,
148
+ // Partner liquidity percentage
149
+ creatorLiquidityPercentage: 0,
150
+ // Creator liquidity percentage
151
+ partnerPermanentLockedLiquidityPercentage: 100,
152
+ // Partner permanent locked liquidity: 100%
153
+ creatorPermanentLockedLiquidityPercentage: 0,
154
+ // Creator permanent locked liquidity: 0%
155
+ creatorTradingFeePercentage: 0,
156
+ // Creator trading fee percentage: 0%
157
+ leftover: 0,
158
+ // Leftover token amount
159
+ tokenUpdateAuthority: TokenUpdateAuthorityOption.Immutable,
160
+ // Token update authority: immutable
161
+ // Migration fee
162
+ migrationFee: {
163
+ feePercentage: 0,
164
+ // Fee percentage
165
+ creatorFeePercentage: 0
166
+ // Creator fee percentage
167
+ },
168
+ poolCreationFee: 0.1,
169
+ // Pool creation fee
170
+ enableFirstSwapWithMinFee: true
171
+ // Enable first swap with minimum fee
172
+ });
173
+ const transaction = await client.partner.createConfig({
174
+ config: config.publicKey,
175
+ // Config public key
176
+ feeClaimer: options.feeClaimer,
177
+ // Fee claimer
178
+ leftoverReceiver: options.leftoverReceiver,
179
+ // Leftover receiver
180
+ payer: options.payer,
181
+ // Payer
182
+ quoteMint: NATIVE_MINT,
183
+ // Quote mint: native SOL
184
+ ...curveConfig
185
+ // Curve config parameters
186
+ });
187
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
188
+ transaction.recentBlockhash = blockhash;
189
+ transaction.feePayer = options.payer;
190
+ transaction.partialSign(config);
191
+ return {
192
+ configAddress: config.publicKey.toBase58(),
193
+ // Config address
194
+ transaction,
195
+ // Unsigned transaction (already has config's partial signature)
196
+ configKeypair: config
197
+ // Config keypair
198
+ };
199
+ }
200
+
201
+ // src/cli/utils/wallet.ts
202
+ import { Keypair as Keypair2 } from "@solana/web3.js";
203
+ import { join } from "node:path";
204
+ import { homedir } from "node:os";
205
+
206
+ // src/constants.ts
207
+ var DEFAULT_RPC_URL = process.env.RPC_URL || "https://mainnet-api.nara.build/";
208
+ var DEFAULT_DBC_CONFIG_ADDRESS = process.env.DBC_CONFIG_ADDRESS || "";
209
+ var DEFAULT_WALLET_PATH = process.env.WALLET_PATH || "~/.config/nara/id.json";
210
+ var DEFAULT_QUEST_RELAY_URL = process.env.QUEST_RELAY_URL || "https://quest-api.nara.build/";
211
+
212
+ // src/cli/utils/wallet.ts
213
+ var DEFAULT_WALLET_PATH2 = DEFAULT_WALLET_PATH.startsWith("~") ? join(homedir(), DEFAULT_WALLET_PATH.slice(1)) : DEFAULT_WALLET_PATH;
214
+ async function loadWallet(walletPath) {
215
+ const path = walletPath || DEFAULT_WALLET_PATH2;
216
+ try {
217
+ const fs = await import("node:fs/promises");
218
+ const file = await fs.readFile(path, "utf-8");
219
+ const data = JSON.parse(file);
220
+ if (Array.isArray(data)) {
221
+ return Keypair2.fromSecretKey(new Uint8Array(data));
222
+ } else if (data.secretKey) {
223
+ return Keypair2.fromSecretKey(new Uint8Array(data.secretKey));
224
+ } else {
225
+ throw new Error(
226
+ "Invalid wallet file format. Expected array or object with secretKey field."
227
+ );
228
+ }
229
+ } catch (error) {
230
+ if (!walletPath) {
231
+ throw new Error(
232
+ `Wallet not found. Please create a wallet at ${DEFAULT_WALLET_PATH2} or use --wallet flag to specify a different path.`
233
+ );
234
+ } else {
235
+ throw new Error(`Failed to load wallet from ${path}: ${error.message}`);
236
+ }
237
+ }
238
+ }
239
+ function getRpcUrl(rpcUrl) {
240
+ return rpcUrl || DEFAULT_RPC_URL;
241
+ }
242
+
243
+ // src/cli/utils/validation.ts
244
+ import { PublicKey as PublicKey3 } from "@solana/web3.js";
245
+ function validatePublicKey(address) {
246
+ try {
247
+ return new PublicKey3(address);
248
+ } catch (error) {
249
+ throw new Error(`Invalid Solana address: ${address}`);
250
+ }
251
+ }
252
+ function validatePositiveNumber(value, name) {
253
+ const num = typeof value === "string" ? parseFloat(value) : value;
254
+ if (isNaN(num)) {
255
+ throw new Error(`${name} must be a valid number`);
256
+ }
257
+ if (num <= 0) {
258
+ throw new Error(`${name} must be greater than 0`);
259
+ }
260
+ return num;
261
+ }
262
+ function validateNonNegativeNumber(value, name) {
263
+ const num = typeof value === "string" ? parseFloat(value) : value;
264
+ if (isNaN(num)) {
265
+ throw new Error(`${name} must be a valid number`);
266
+ }
267
+ if (num < 0) {
268
+ throw new Error(`${name} must be non-negative`);
269
+ }
270
+ return num;
271
+ }
272
+ function validateSwapMode(mode) {
273
+ const validModes = ["exact-in", "partial-fill", "exact-out"];
274
+ const normalized = mode.toLowerCase();
275
+ if (!validModes.includes(normalized)) {
276
+ throw new Error(
277
+ `Invalid swap mode: ${mode}. Must be one of: ${validModes.join(", ")}`
278
+ );
279
+ }
280
+ return normalized;
281
+ }
282
+ function validateDirection(direction) {
283
+ const validDirections = ["buy", "sell"];
284
+ const normalized = direction.toLowerCase();
285
+ if (!validDirections.includes(normalized)) {
286
+ throw new Error(
287
+ `Invalid direction: ${direction}. Must be one of: ${validDirections.join(
288
+ ", "
289
+ )}`
290
+ );
291
+ }
292
+ return normalized;
293
+ }
294
+
295
+ // src/cli/utils/transaction.ts
296
+ import {
297
+ sendAndConfirmTransaction,
298
+ VersionedTransaction as VersionedTransaction2
299
+ } from "@solana/web3.js";
300
+
301
+ // src/cli/utils/output.ts
302
+ function formatOutput(data, jsonMode = false) {
303
+ if (jsonMode) {
304
+ console.log(JSON.stringify(data, null, 2));
305
+ } else {
306
+ formatHumanReadable(data);
307
+ }
308
+ }
309
+ function formatHumanReadable(data) {
310
+ if (typeof data === "object" && data !== null) {
311
+ for (const [key, value] of Object.entries(data)) {
312
+ const label = formatLabel(key);
313
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
314
+ console.log(`
315
+ ${label}:`);
316
+ formatHumanReadable(value);
317
+ } else {
318
+ console.log(`${label}: ${formatValue(value)}`);
319
+ }
320
+ }
321
+ } else {
322
+ console.log(data);
323
+ }
324
+ }
325
+ function formatLabel(key) {
326
+ const words = key.replace(/([A-Z])/g, " $1").trim();
327
+ return words.charAt(0).toUpperCase() + words.slice(1);
328
+ }
329
+ function formatValue(value) {
330
+ if (value === null || value === void 0) {
331
+ return "N/A";
332
+ }
333
+ if (typeof value === "boolean") {
334
+ return value ? "Yes" : "No";
335
+ }
336
+ if (typeof value === "number") {
337
+ if (value >= 1e3) {
338
+ return value.toLocaleString();
339
+ }
340
+ return value.toString();
341
+ }
342
+ if (Array.isArray(value)) {
343
+ return value.join(", ");
344
+ }
345
+ return value.toString();
346
+ }
347
+ function printSuccess(message) {
348
+ console.log(`\u2705 ${message}`);
349
+ }
350
+ function printError(message) {
351
+ console.error(`\u274C Error: ${message}`);
352
+ }
353
+ function printInfo(message) {
354
+ console.log(`\u2139\uFE0F ${message}`);
355
+ }
356
+ function printWarning(message) {
357
+ console.log(`\u26A0\uFE0F ${message}`);
358
+ }
359
+
360
+ // src/cli/utils/transaction.ts
361
+ async function handleTransaction(sdk, transaction, signers, exportMode = false) {
362
+ if (exportMode) {
363
+ return exportTransaction(transaction);
364
+ }
365
+ return await signAndSendTransaction(sdk, transaction, signers);
366
+ }
367
+ function exportTransaction(transaction) {
368
+ try {
369
+ const serialized = transaction.serialize({
370
+ requireAllSignatures: false,
371
+ verifySignatures: false
372
+ });
373
+ const base64 = Buffer.from(serialized).toString("base64");
374
+ return { base64 };
375
+ } catch (error) {
376
+ throw new Error(`Failed to serialize transaction: ${error.message}`);
377
+ }
378
+ }
379
+ async function signAndSendTransaction(sdk, transaction, signers) {
380
+ const connection = sdk.getConnection();
381
+ try {
382
+ printInfo("Signing transaction...");
383
+ let signature;
384
+ if (transaction instanceof VersionedTransaction2) {
385
+ transaction.sign(signers);
386
+ printInfo("Sending transaction...");
387
+ signature = await connection.sendTransaction(transaction, {
388
+ maxRetries: 3
389
+ });
390
+ printInfo("Confirming transaction...");
391
+ await connection.confirmTransaction(signature, "confirmed");
392
+ } else {
393
+ printInfo("Sending transaction...");
394
+ signature = await sendAndConfirmTransaction(
395
+ connection,
396
+ transaction,
397
+ signers,
398
+ {
399
+ commitment: "confirmed",
400
+ skipPreflight: true
401
+ }
402
+ );
403
+ }
404
+ return { signature };
405
+ } catch (error) {
406
+ throw new Error(`Transaction failed: ${error.message}`);
407
+ }
408
+ }
409
+ function printTransactionResult(result, jsonMode = false) {
410
+ if (result.signature) {
411
+ if (jsonMode) {
412
+ console.log(JSON.stringify({ signature: result.signature }, null, 2));
413
+ } else {
414
+ printSuccess("Transaction successful!");
415
+ console.log(`Signature: ${result.signature}`);
416
+ console.log(
417
+ `View on Solscan: https://solscan.io/tx/${result.signature}?cluster=devnet`
418
+ );
419
+ }
420
+ } else if (result.base64) {
421
+ if (jsonMode) {
422
+ console.log(JSON.stringify({ transaction: result.base64 }, null, 2));
423
+ } else {
424
+ printSuccess("Transaction exported!");
425
+ console.log(`
426
+ Base64 transaction:
427
+ ${result.base64}`);
428
+ }
429
+ }
430
+ }
431
+
432
+ // src/cli/commands/config.ts
433
+ function registerConfigCommands(program2) {
434
+ const config = program2.command("config").description("Configuration management commands");
435
+ config.command("create").description("Create a bonding curve configuration").option("--fee-claimer <address>", "Fee claimer wallet address").option(
436
+ "--leftover-receiver <address>",
437
+ "Leftover token receiver wallet address"
438
+ ).option(
439
+ "--total-supply <number>",
440
+ "Total token supply",
441
+ "1000000000"
442
+ ).option("--initial-mcap <number>", "Initial market cap", "30").option("--migration-mcap <number>", "Migration market cap", "540").option("-e, --export-tx", "Export unsigned transaction", false).action(async (options) => {
443
+ try {
444
+ await handleConfigCreate(options);
445
+ } catch (error) {
446
+ printError(error.message);
447
+ process.exit(1);
448
+ }
449
+ });
450
+ }
451
+ async function handleConfigCreate(options) {
452
+ const wallet = await loadWallet(options.wallet);
453
+ const rpcUrl = getRpcUrl(options.rpcUrl);
454
+ printInfo(`Using RPC: ${rpcUrl}`);
455
+ printInfo(`Wallet: ${wallet.publicKey.toBase58()}`);
456
+ const sdk = new NaraSDK({
457
+ rpcUrl,
458
+ commitment: "confirmed"
459
+ });
460
+ const feeClaimer = options.feeClaimer ? validatePublicKey(options.feeClaimer) : wallet.publicKey;
461
+ const leftoverReceiver = options.leftoverReceiver ? validatePublicKey(options.leftoverReceiver) : wallet.publicKey;
462
+ const totalTokenSupply = parseInt(String(options.totalSupply || "1000000000"));
463
+ const initialMarketCap = parseFloat(String(options.initialMcap || "30"));
464
+ const migrationMarketCap = parseFloat(String(options.migrationMcap || "540"));
465
+ printInfo("Creating bonding curve configuration...");
466
+ const result = await createConfig(sdk, {
467
+ feeClaimer,
468
+ leftoverReceiver,
469
+ payer: wallet.publicKey,
470
+ totalTokenSupply,
471
+ initialMarketCap,
472
+ migrationMarketCap
473
+ });
474
+ printInfo(`Config address: ${result.configAddress}`);
475
+ const txResult = await handleTransaction(
476
+ sdk,
477
+ result.transaction,
478
+ [wallet, result.configKeypair],
479
+ // Both wallet and config keypair need to sign
480
+ options.exportTx || false
481
+ );
482
+ if (options.json) {
483
+ const output = {
484
+ configAddress: result.configAddress,
485
+ ...txResult.signature && { signature: txResult.signature },
486
+ ...txResult.base64 && { transaction: txResult.base64 }
487
+ };
488
+ console.log(JSON.stringify(output, null, 2));
489
+ } else {
490
+ console.log(`
491
+ Config Address: ${result.configAddress}`);
492
+ printTransactionResult(txResult, false);
493
+ if (txResult.signature) {
494
+ printInfo("\nSave this DBC config address for creating pools:");
495
+ console.log(`export DBC_CONFIG_ADDRESS="${result.configAddress}"`);
496
+ }
497
+ }
498
+ }
499
+
500
+ // src/cli/commands/pool.ts
501
+ import "commander";
502
+
503
+ // src/pool.ts
504
+ import {
505
+ Keypair as Keypair4,
506
+ PublicKey as PublicKey4,
507
+ Transaction as Transaction4
508
+ } from "@solana/web3.js";
509
+ import { NATIVE_MINT as NATIVE_MINT2 } from "@solana/spl-token";
510
+ import BN from "bn.js";
511
+ import { deriveDbcPoolAddress } from "@meteora-ag/dynamic-bonding-curve-sdk";
512
+ async function createPool(sdk, params) {
513
+ const connection = sdk.getConnection();
514
+ const client = sdk.getClient();
515
+ const baseMint = Keypair4.generate();
516
+ const configPubkey = new PublicKey4(params.configAddress);
517
+ const createPoolTx = await client.pool.createPool({
518
+ baseMint: baseMint.publicKey,
519
+ config: configPubkey,
520
+ name: params.name,
521
+ symbol: params.symbol,
522
+ uri: params.uri,
523
+ payer: params.payer,
524
+ poolCreator: params.poolCreator
525
+ });
526
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
527
+ createPoolTx.recentBlockhash = blockhash;
528
+ createPoolTx.feePayer = params.payer;
529
+ const poolPubkey = deriveDbcPoolAddress(
530
+ NATIVE_MINT2,
531
+ baseMint.publicKey,
532
+ configPubkey
533
+ );
534
+ const compiledTx = await sdk.compileTransactionWithALT(
535
+ createPoolTx,
536
+ params.payer
537
+ );
538
+ return {
539
+ poolAddress: poolPubkey.toBase58(),
540
+ baseMint: baseMint.publicKey.toBase58(),
541
+ transaction: compiledTx,
542
+ baseMintKeypair: baseMint
543
+ };
544
+ }
545
+ async function createPoolWithFirstBuy(sdk, params) {
546
+ const connection = sdk.getConnection();
547
+ const client = sdk.getClient();
548
+ const baseMint = Keypair4.generate();
549
+ const configPubkey = new PublicKey4(params.configAddress);
550
+ const buyer = params.buyer ?? params.payer;
551
+ const receiver = params.receiver ?? buyer;
552
+ const buyAmount = new BN(params.initialBuyAmountSOL * 1e9);
553
+ const slippageBps = params.slippageBps ?? 100;
554
+ const minimumAmountOut = new BN(0);
555
+ const result = await client.pool.createPoolWithFirstBuy({
556
+ createPoolParam: {
557
+ baseMint: baseMint.publicKey,
558
+ config: configPubkey,
559
+ name: params.name,
560
+ symbol: params.symbol,
561
+ uri: params.uri,
562
+ payer: params.payer,
563
+ poolCreator: params.poolCreator
564
+ },
565
+ firstBuyParam: {
566
+ buyer,
567
+ receiver,
568
+ buyAmount,
569
+ minimumAmountOut,
570
+ referralTokenAccount: null
571
+ }
572
+ });
573
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
574
+ const combinedTx = new Transaction4();
575
+ combinedTx.add(...result.createPoolTx.instructions);
576
+ if (result.swapBuyTx) {
577
+ combinedTx.add(...result.swapBuyTx.instructions);
578
+ }
579
+ combinedTx.recentBlockhash = blockhash;
580
+ combinedTx.feePayer = params.payer;
581
+ const poolPubkey = deriveDbcPoolAddress(
582
+ NATIVE_MINT2,
583
+ baseMint.publicKey,
584
+ configPubkey
585
+ );
586
+ const compiledTx = await sdk.compileTransactionWithALT(
587
+ combinedTx,
588
+ params.payer
589
+ );
590
+ return {
591
+ poolAddress: poolPubkey.toBase58(),
592
+ baseMint: baseMint.publicKey.toBase58(),
593
+ createPoolTx: compiledTx,
594
+ // Return combined transaction
595
+ firstBuyTx: compiledTx,
596
+ // Same as createPoolTx since they are combined
597
+ baseMintKeypair: baseMint,
598
+ buyInfo: {
599
+ amountIn: buyAmount.toString(),
600
+ minimumAmountOut: minimumAmountOut.toString()
601
+ }
602
+ };
603
+ }
604
+ async function getPoolInfo(sdk, tokenAddress) {
605
+ const client = sdk.getClient();
606
+ const tokenPubkey = new PublicKey4(tokenAddress);
607
+ const poolAccount = await client.state.getPoolByBaseMint(tokenPubkey);
608
+ if (!poolAccount) {
609
+ throw new Error(`Pool not found for token: ${tokenAddress}`);
610
+ }
611
+ return {
612
+ ...poolAccount.account,
613
+ poolAddress: poolAccount.publicKey.toBase58()
614
+ };
615
+ }
616
+ async function getPoolProgress(sdk, tokenAddress) {
617
+ const client = sdk.getClient();
618
+ const tokenPubkey = new PublicKey4(tokenAddress);
619
+ const poolAccount = await client.state.getPoolByBaseMint(tokenPubkey);
620
+ if (!poolAccount) {
621
+ throw new Error(`Pool not found for token: ${tokenAddress}`);
622
+ }
623
+ const progress = await client.state.getPoolCurveProgress(
624
+ poolAccount.publicKey
625
+ );
626
+ const pool = poolAccount.account;
627
+ return {
628
+ progress,
629
+ quoteReserve: pool.quoteReserve?.toString() ?? "0",
630
+ isMigrated: pool.isMigrated ?? false
631
+ };
632
+ }
633
+
634
+ // src/cli/commands/pool.ts
635
+ function registerPoolCommands(program2) {
636
+ const pool = program2.command("pool").description("Pool management commands");
637
+ pool.command("create").description("Create a new token pool").requiredOption("-n, --name <string>", "Token name").requiredOption("-s, --symbol <string>", "Token symbol").requiredOption("-u, --uri <string>", "Metadata URI").requiredOption(
638
+ "--dbc-config <address>",
639
+ "DBC config address (or set DBC_CONFIG_ADDRESS env)"
640
+ ).option("--creator <address>", "Pool creator address").option("-e, --export-tx", "Export unsigned transaction", false).action(async (options) => {
641
+ try {
642
+ await handlePoolCreate(options);
643
+ } catch (error) {
644
+ printError(error.message);
645
+ process.exit(1);
646
+ }
647
+ });
648
+ pool.command("create-with-buy").description("Create a new token pool with initial buy").requiredOption("-n, --name <string>", "Token name").requiredOption("-s, --symbol <string>", "Token symbol").requiredOption("-u, --uri <string>", "Metadata URI").requiredOption(
649
+ "--dbc-config <address>",
650
+ "DBC config address (or set DBC_CONFIG_ADDRESS env)"
651
+ ).requiredOption("--amount <number>", "Initial buy amount in NSO").option("--creator <address>", "Pool creator address").option("--buyer <address>", "Buyer address").option("--receiver <address>", "Token receiver address").option("--slippage <number>", "Slippage in basis points", "100").option("-e, --export-tx", "Export unsigned transaction", false).action(async (options) => {
652
+ try {
653
+ await handlePoolCreateWithBuy(options);
654
+ } catch (error) {
655
+ printError(error.message);
656
+ process.exit(1);
657
+ }
658
+ });
659
+ pool.command("info <token-address>").description("Get pool information").action(async (tokenAddress, options) => {
660
+ try {
661
+ await handlePoolInfo(tokenAddress, options);
662
+ } catch (error) {
663
+ printError(error.message);
664
+ process.exit(1);
665
+ }
666
+ });
667
+ pool.command("progress <token-address>").description("Get bonding curve progress").action(async (tokenAddress, options) => {
668
+ try {
669
+ await handlePoolProgress(tokenAddress, options);
670
+ } catch (error) {
671
+ printError(error.message);
672
+ process.exit(1);
673
+ }
674
+ });
675
+ }
676
+ async function handlePoolCreate(options) {
677
+ const wallet = await loadWallet(options.wallet);
678
+ const rpcUrl = getRpcUrl(options.rpcUrl);
679
+ printInfo(`Using RPC: ${rpcUrl}`);
680
+ printInfo(`Wallet: ${wallet.publicKey.toBase58()}`);
681
+ const sdk = new NaraSDK({
682
+ rpcUrl,
683
+ commitment: "confirmed"
684
+ });
685
+ const configAddress = options.dbcConfig || DEFAULT_DBC_CONFIG_ADDRESS;
686
+ if (!configAddress) {
687
+ throw new Error(
688
+ "DBC config address is required. Use --dbc-config flag or set DBC_CONFIG_ADDRESS environment variable."
689
+ );
690
+ }
691
+ const configPubkey = validatePublicKey(configAddress);
692
+ const creator = options.creator ? validatePublicKey(options.creator) : wallet.publicKey;
693
+ printInfo("Creating token pool...");
694
+ const result = await createPool(sdk, {
695
+ name: options.name,
696
+ symbol: options.symbol,
697
+ uri: options.uri,
698
+ configAddress,
699
+ payer: wallet.publicKey,
700
+ poolCreator: creator
701
+ });
702
+ printInfo(`Pool address: ${result.poolAddress}`);
703
+ printInfo(`Token address: ${result.baseMint}`);
704
+ const txResult = await handleTransaction(
705
+ sdk,
706
+ result.transaction,
707
+ [wallet, result.baseMintKeypair],
708
+ // Both wallet and baseMint keypair need to sign
709
+ options.exportTx || false
710
+ );
711
+ if (options.json) {
712
+ const output = {
713
+ poolAddress: result.poolAddress,
714
+ tokenAddress: result.baseMint,
715
+ ...txResult.signature && { signature: txResult.signature },
716
+ ...txResult.base64 && { transaction: txResult.base64 }
717
+ };
718
+ console.log(JSON.stringify(output, null, 2));
719
+ } else {
720
+ console.log(`
721
+ Pool Address: ${result.poolAddress}`);
722
+ console.log(`Token Address: ${result.baseMint}`);
723
+ printTransactionResult(txResult, false);
724
+ if (txResult.signature) {
725
+ printInfo("\nSave this token address for buying/selling:");
726
+ console.log(`export TOKEN_ADDRESS="${result.baseMint}"`);
727
+ }
728
+ }
729
+ }
730
+ async function handlePoolCreateWithBuy(options) {
731
+ const wallet = await loadWallet(options.wallet);
732
+ const rpcUrl = getRpcUrl(options.rpcUrl);
733
+ printInfo(`Using RPC: ${rpcUrl}`);
734
+ printInfo(`Wallet: ${wallet.publicKey.toBase58()}`);
735
+ const sdk = new NaraSDK({
736
+ rpcUrl,
737
+ commitment: "confirmed"
738
+ });
739
+ const configAddress = options.dbcConfig || DEFAULT_DBC_CONFIG_ADDRESS;
740
+ if (!configAddress) {
741
+ throw new Error(
742
+ "DBC config address is required. Use --dbc-config flag or set DBC_CONFIG_ADDRESS environment variable."
743
+ );
744
+ }
745
+ const configPubkey = validatePublicKey(configAddress);
746
+ const creator = options.creator ? validatePublicKey(options.creator) : wallet.publicKey;
747
+ const buyer = options.buyer ? validatePublicKey(options.buyer) : wallet.publicKey;
748
+ const receiver = options.receiver ? validatePublicKey(options.receiver) : buyer;
749
+ const amount = validatePositiveNumber(options.amount, "amount");
750
+ const slippage = validateNonNegativeNumber(
751
+ options.slippage || "100",
752
+ "slippage"
753
+ );
754
+ printInfo("Creating token pool with initial buy...");
755
+ printInfo(`Initial buy amount: ${amount} NSO`);
756
+ const result = await createPoolWithFirstBuy(sdk, {
757
+ name: options.name,
758
+ symbol: options.symbol,
759
+ uri: options.uri,
760
+ configAddress,
761
+ payer: wallet.publicKey,
762
+ poolCreator: creator,
763
+ initialBuyAmountSOL: amount,
764
+ buyer,
765
+ receiver,
766
+ slippageBps: slippage
767
+ });
768
+ printInfo(`Pool address: ${result.poolAddress}`);
769
+ printInfo(`Token address: ${result.baseMint}`);
770
+ const txResult = await handleTransaction(
771
+ sdk,
772
+ result.createPoolTx,
773
+ [wallet, result.baseMintKeypair],
774
+ // Both wallet and baseMint keypair need to sign
775
+ options.exportTx || false
776
+ );
777
+ if (options.json) {
778
+ const output = {
779
+ poolAddress: result.poolAddress,
780
+ tokenAddress: result.baseMint,
781
+ buyInfo: result.buyInfo,
782
+ ...txResult.signature && { signature: txResult.signature },
783
+ ...txResult.base64 && { transaction: txResult.base64 }
784
+ };
785
+ console.log(JSON.stringify(output, null, 2));
786
+ } else {
787
+ console.log(`
788
+ Pool Address: ${result.poolAddress}`);
789
+ console.log(`Token Address: ${result.baseMint}`);
790
+ console.log(`
791
+ Buy Info:`);
792
+ console.log(` Amount In: ${(parseInt(result.buyInfo.amountIn) / 1e9).toFixed(4)} NSO`);
793
+ console.log(` Minimum Out: ${result.buyInfo.minimumAmountOut} tokens (smallest unit)`);
794
+ printTransactionResult(txResult, false);
795
+ if (txResult.signature) {
796
+ printInfo("\nSave this token address for buying/selling:");
797
+ console.log(`export TOKEN_ADDRESS="${result.baseMint}"`);
798
+ }
799
+ }
800
+ }
801
+ async function handlePoolInfo(tokenAddress, options) {
802
+ const rpcUrl = getRpcUrl(options.rpcUrl);
803
+ printInfo(`Using RPC: ${rpcUrl}`);
804
+ validatePublicKey(tokenAddress);
805
+ const sdk = new NaraSDK({
806
+ rpcUrl,
807
+ commitment: "confirmed"
808
+ });
809
+ printInfo("Fetching pool information...");
810
+ const poolInfo = await getPoolInfo(sdk, tokenAddress);
811
+ formatOutput(poolInfo, options.json || false);
812
+ }
813
+ async function handlePoolProgress(tokenAddress, options) {
814
+ const rpcUrl = getRpcUrl(options.rpcUrl);
815
+ printInfo(`Using RPC: ${rpcUrl}`);
816
+ validatePublicKey(tokenAddress);
817
+ const sdk = new NaraSDK({
818
+ rpcUrl,
819
+ commitment: "confirmed"
820
+ });
821
+ printInfo("Fetching bonding curve progress...");
822
+ const progress = await getPoolProgress(sdk, tokenAddress);
823
+ const output = {
824
+ progress: `${(progress.progress * 100).toFixed(2)}%`,
825
+ progressRaw: progress.progress,
826
+ quoteReserve: progress.quoteReserve,
827
+ isMigrated: progress.isMigrated
828
+ };
829
+ if (options.json) {
830
+ console.log(JSON.stringify(progress, null, 2));
831
+ } else {
832
+ formatOutput(output, false);
833
+ }
834
+ }
835
+
836
+ // src/cli/commands/swap.ts
837
+ import "commander";
838
+ import BN3 from "bn.js";
839
+
840
+ // src/swap.ts
841
+ import { PublicKey as PublicKey5 } from "@solana/web3.js";
842
+ import BN2 from "bn.js";
843
+ import {
844
+ deriveDammV2PoolAddress,
845
+ DAMM_V2_MIGRATION_FEE_ADDRESS
846
+ } from "@meteora-ag/dynamic-bonding-curve-sdk";
847
+ import { NATIVE_MINT as NATIVE_MINT3, TOKEN_PROGRAM_ID } from "@solana/spl-token";
848
+ import {
849
+ CpAmm,
850
+ SwapMode as CpAmmSwapMode,
851
+ getCurrentPoint
852
+ } from "@meteora-ag/cp-amm-sdk";
853
+ async function checkPoolMigration(sdk, tokenAddress) {
854
+ const client = sdk.getClient();
855
+ const tokenPubkey = new PublicKey5(tokenAddress);
856
+ const poolAccount = await client.state.getPoolByBaseMint(tokenPubkey);
857
+ if (!poolAccount) {
858
+ throw new Error(`Pool not found for token: ${tokenAddress}`);
859
+ }
860
+ if (poolAccount.account.isMigrated) {
861
+ const virtualPool = poolAccount.account;
862
+ const poolConfig = await client.state.getPoolConfig(virtualPool.config);
863
+ const migrationFeeOption = poolConfig.migrationFeeOption || 0;
864
+ const dammConfig = DAMM_V2_MIGRATION_FEE_ADDRESS[migrationFeeOption];
865
+ if (!dammConfig) {
866
+ throw new Error(
867
+ `Invalid migration fee option: ${migrationFeeOption}. Cannot determine DAMM V2 config address.`
868
+ );
869
+ }
870
+ const dammV2Pool = deriveDammV2PoolAddress(
871
+ dammConfig,
872
+ NATIVE_MINT3,
873
+ // tokenA = SOL
874
+ tokenPubkey
875
+ // tokenB = token
876
+ );
877
+ const connection = sdk.getConnection();
878
+ const poolAccountInfo = await connection.getAccountInfo(dammV2Pool);
879
+ if (!poolAccountInfo) {
880
+ return { isMigrated: false };
881
+ }
882
+ return {
883
+ isMigrated: true,
884
+ dammV2Pool,
885
+ dammConfig
886
+ };
887
+ }
888
+ return { isMigrated: false };
889
+ }
890
+ async function getSwapQuote(sdk, tokenAddress, amountIn, swapBaseForQuote, slippageBps = 100) {
891
+ const client = sdk.getClient();
892
+ const tokenPubkey = new PublicKey5(tokenAddress);
893
+ const poolAccount = await client.state.getPoolByBaseMint(tokenPubkey);
894
+ if (!poolAccount) {
895
+ throw new Error(`Pool not found for token: ${tokenAddress}`);
896
+ }
897
+ const virtualPool = poolAccount.account;
898
+ const poolConfig = await client.state.getPoolConfig(virtualPool.config);
899
+ const quote = client.pool.swapQuote({
900
+ virtualPool,
901
+ config: poolConfig,
902
+ swapBaseForQuote,
903
+ amountIn,
904
+ slippageBps,
905
+ hasReferral: false,
906
+ eligibleForFirstSwapWithMinFee: false,
907
+ currentPoint: new BN2(0)
908
+ });
909
+ const quoteResult = quote;
910
+ return {
911
+ amountIn: amountIn.toString(),
912
+ outputAmount: quoteResult.outputAmount.toString(),
913
+ minimumAmountOut: quote.minimumAmountOut.toString(),
914
+ nextSqrtPrice: quoteResult.nextSqrtPrice.toString(),
915
+ tradingFee: quoteResult.tradingFee.toString(),
916
+ protocolFee: quoteResult.protocolFee.toString(),
917
+ referralFee: quoteResult.referralFee.toString()
918
+ };
919
+ }
920
+ async function buyToken(sdk, params) {
921
+ const client = sdk.getClient();
922
+ const connection = sdk.getConnection();
923
+ const tokenPubkey = new PublicKey5(params.tokenAddress);
924
+ const amountIn = new BN2(params.amountInSOL * 1e9);
925
+ const slippageBps = params.slippageBps ?? 100;
926
+ const swapMode = params.swapMode ?? 1 /* PartialFill */;
927
+ const migrationInfo = await checkPoolMigration(sdk, params.tokenAddress);
928
+ if (migrationInfo.isMigrated && migrationInfo.dammV2Pool) {
929
+ console.log("\u{1F680} Pool launched to DAMM V2, using CP-AMM for swap");
930
+ const cpAmm = new CpAmm(connection);
931
+ const poolState = await cpAmm.fetchPoolState(migrationInfo.dammV2Pool);
932
+ const currentPoint = await getCurrentPoint(
933
+ connection,
934
+ poolState.activationType
935
+ );
936
+ const isAToB = poolState.tokenAMint.equals(NATIVE_MINT3);
937
+ const inputTokenMint = isAToB ? poolState.tokenAMint : poolState.tokenBMint;
938
+ const cpAmmSwapMode = swapMode === 1 /* PartialFill */ ? CpAmmSwapMode.PartialFill : swapMode === 2 /* ExactOut */ ? CpAmmSwapMode.ExactOut : CpAmmSwapMode.ExactIn;
939
+ const quoteBaseParams = {
940
+ inputTokenMint,
941
+ slippage: slippageBps / 1e4,
942
+ currentPoint,
943
+ poolState,
944
+ tokenADecimal: 9,
945
+ // SOL decimals
946
+ tokenBDecimal: 6,
947
+ // Token decimals (assumed)
948
+ hasReferral: false
949
+ };
950
+ let quote2;
951
+ if (cpAmmSwapMode === CpAmmSwapMode.ExactOut) {
952
+ quote2 = cpAmm.getQuote2({
953
+ ...quoteBaseParams,
954
+ swapMode: cpAmmSwapMode,
955
+ amountOut: amountIn
956
+ // ExactOut: desired output amount
957
+ });
958
+ } else {
959
+ quote2 = cpAmm.getQuote2({
960
+ ...quoteBaseParams,
961
+ swapMode: cpAmmSwapMode,
962
+ amountIn
963
+ });
964
+ }
965
+ const swapBaseParams = {
966
+ payer: params.owner,
967
+ pool: migrationInfo.dammV2Pool,
968
+ inputTokenMint,
969
+ outputTokenMint: isAToB ? poolState.tokenBMint : poolState.tokenAMint,
970
+ tokenAMint: poolState.tokenAMint,
971
+ tokenBMint: poolState.tokenBMint,
972
+ tokenAVault: poolState.tokenAVault,
973
+ tokenBVault: poolState.tokenBVault,
974
+ tokenAProgram: TOKEN_PROGRAM_ID,
975
+ tokenBProgram: TOKEN_PROGRAM_ID,
976
+ referralTokenAccount: null,
977
+ poolState
978
+ };
979
+ let transaction2;
980
+ if (cpAmmSwapMode === CpAmmSwapMode.ExactOut) {
981
+ transaction2 = await cpAmm.swap2({
982
+ ...swapBaseParams,
983
+ swapMode: cpAmmSwapMode,
984
+ amountOut: amountIn,
985
+ maximumAmountIn: quote2.maxSwapInAmount || amountIn.muln(2)
986
+ // 2x as max
987
+ });
988
+ } else {
989
+ transaction2 = await cpAmm.swap2({
990
+ ...swapBaseParams,
991
+ swapMode: cpAmmSwapMode,
992
+ amountIn,
993
+ minimumAmountOut: quote2.minSwapOutAmount || new BN2(0)
994
+ });
995
+ }
996
+ const compiledTx2 = await sdk.compileTransactionWithALT(
997
+ transaction2,
998
+ params.owner
999
+ );
1000
+ return {
1001
+ transaction: compiledTx2,
1002
+ amountIn: quote2.swapInAmount?.toString() || amountIn.toString(),
1003
+ expectedAmountOut: quote2.swapOutAmount?.toString() || "0",
1004
+ minimumAmountOut: quote2.minSwapOutAmount?.toString() || "0"
1005
+ };
1006
+ }
1007
+ const poolAccount = await client.state.getPoolByBaseMint(tokenPubkey);
1008
+ if (!poolAccount) {
1009
+ throw new Error(`Pool not found for token: ${params.tokenAddress}`);
1010
+ }
1011
+ let quote;
1012
+ try {
1013
+ quote = await getSwapQuote(
1014
+ sdk,
1015
+ params.tokenAddress,
1016
+ amountIn,
1017
+ false,
1018
+ slippageBps
1019
+ );
1020
+ } catch (err) {
1021
+ if (swapMode === 1 /* PartialFill */ && err.message?.includes("Insufficient Liquidity")) {
1022
+ console.warn("\u26A0\uFE0F Insufficient liquidity, using PartialFill mode to accept any available amount");
1023
+ quote = {
1024
+ amountIn: amountIn.toString(),
1025
+ outputAmount: "0",
1026
+ minimumAmountOut: "0",
1027
+ nextSqrtPrice: "0",
1028
+ tradingFee: "0",
1029
+ protocolFee: "0",
1030
+ referralFee: "0"
1031
+ };
1032
+ } else {
1033
+ throw err;
1034
+ }
1035
+ }
1036
+ let transaction;
1037
+ if (swapMode === 1 /* PartialFill */) {
1038
+ transaction = await client.pool.swap2({
1039
+ owner: params.owner,
1040
+ pool: poolAccount.publicKey,
1041
+ swapBaseForQuote: false,
1042
+ referralTokenAccount: null,
1043
+ swapMode: 1 /* PartialFill */,
1044
+ amountIn,
1045
+ minimumAmountOut: new BN2(quote.minimumAmountOut)
1046
+ });
1047
+ } else if (swapMode === 2 /* ExactOut */) {
1048
+ transaction = await client.pool.swap2({
1049
+ owner: params.owner,
1050
+ pool: poolAccount.publicKey,
1051
+ swapBaseForQuote: false,
1052
+ referralTokenAccount: null,
1053
+ swapMode: 2 /* ExactOut */,
1054
+ amountOut: new BN2(quote.outputAmount),
1055
+ maximumAmountIn: amountIn
1056
+ });
1057
+ } else {
1058
+ transaction = await client.pool.swap2({
1059
+ owner: params.owner,
1060
+ pool: poolAccount.publicKey,
1061
+ swapBaseForQuote: false,
1062
+ referralTokenAccount: null,
1063
+ swapMode: 0 /* ExactIn */,
1064
+ amountIn,
1065
+ minimumAmountOut: new BN2(quote.minimumAmountOut)
1066
+ });
1067
+ }
1068
+ const compiledTx = await sdk.compileTransactionWithALT(
1069
+ transaction,
1070
+ params.owner
1071
+ );
1072
+ return {
1073
+ transaction: compiledTx,
1074
+ amountIn: amountIn.toString(),
1075
+ expectedAmountOut: quote.outputAmount,
1076
+ minimumAmountOut: quote.minimumAmountOut
1077
+ };
1078
+ }
1079
+ async function sellToken(sdk, params) {
1080
+ const client = sdk.getClient();
1081
+ const connection = sdk.getConnection();
1082
+ const tokenPubkey = new PublicKey5(params.tokenAddress);
1083
+ const tokenDecimals = params.tokenDecimals ?? 6;
1084
+ const amountIn = new BN2(params.amountInToken * 10 ** tokenDecimals);
1085
+ const slippageBps = params.slippageBps ?? 100;
1086
+ const swapMode = params.swapMode ?? 1 /* PartialFill */;
1087
+ const migrationInfo = await checkPoolMigration(sdk, params.tokenAddress);
1088
+ if (migrationInfo.isMigrated && migrationInfo.dammV2Pool) {
1089
+ console.log("\u{1F680} Pool launched to DAMM V2, using CP-AMM for swap");
1090
+ const cpAmm = new CpAmm(connection);
1091
+ const poolState = await cpAmm.fetchPoolState(migrationInfo.dammV2Pool);
1092
+ const currentPoint = await getCurrentPoint(
1093
+ connection,
1094
+ poolState.activationType
1095
+ );
1096
+ const isAToB = poolState.tokenAMint.equals(tokenPubkey);
1097
+ const inputTokenMint = isAToB ? poolState.tokenAMint : poolState.tokenBMint;
1098
+ const cpAmmSwapMode = swapMode === 1 /* PartialFill */ ? CpAmmSwapMode.PartialFill : swapMode === 2 /* ExactOut */ ? CpAmmSwapMode.ExactOut : CpAmmSwapMode.ExactIn;
1099
+ const quoteBaseParams = {
1100
+ inputTokenMint,
1101
+ slippage: slippageBps / 1e4,
1102
+ currentPoint,
1103
+ poolState,
1104
+ tokenADecimal: 9,
1105
+ // SOL decimals
1106
+ tokenBDecimal: tokenDecimals,
1107
+ hasReferral: false
1108
+ };
1109
+ let quote2;
1110
+ if (cpAmmSwapMode === CpAmmSwapMode.ExactOut) {
1111
+ quote2 = cpAmm.getQuote2({
1112
+ ...quoteBaseParams,
1113
+ swapMode: cpAmmSwapMode,
1114
+ amountOut: amountIn
1115
+ // ExactOut: desired output amount
1116
+ });
1117
+ } else {
1118
+ quote2 = cpAmm.getQuote2({
1119
+ ...quoteBaseParams,
1120
+ swapMode: cpAmmSwapMode,
1121
+ amountIn
1122
+ });
1123
+ }
1124
+ const swapBaseParams = {
1125
+ payer: params.owner,
1126
+ pool: migrationInfo.dammV2Pool,
1127
+ inputTokenMint,
1128
+ outputTokenMint: isAToB ? poolState.tokenBMint : poolState.tokenAMint,
1129
+ tokenAMint: poolState.tokenAMint,
1130
+ tokenBMint: poolState.tokenBMint,
1131
+ tokenAVault: poolState.tokenAVault,
1132
+ tokenBVault: poolState.tokenBVault,
1133
+ tokenAProgram: TOKEN_PROGRAM_ID,
1134
+ tokenBProgram: TOKEN_PROGRAM_ID,
1135
+ referralTokenAccount: null,
1136
+ poolState
1137
+ };
1138
+ let transaction2;
1139
+ if (cpAmmSwapMode === CpAmmSwapMode.ExactOut) {
1140
+ transaction2 = await cpAmm.swap2({
1141
+ ...swapBaseParams,
1142
+ swapMode: cpAmmSwapMode,
1143
+ amountOut: amountIn,
1144
+ maximumAmountIn: quote2.maxSwapInAmount || amountIn.muln(2)
1145
+ });
1146
+ } else {
1147
+ transaction2 = await cpAmm.swap2({
1148
+ ...swapBaseParams,
1149
+ swapMode: cpAmmSwapMode,
1150
+ amountIn,
1151
+ minimumAmountOut: quote2.minSwapOutAmount || new BN2(0)
1152
+ });
1153
+ }
1154
+ const compiledTx2 = await sdk.compileTransactionWithALT(
1155
+ transaction2,
1156
+ params.owner
1157
+ );
1158
+ return {
1159
+ transaction: compiledTx2,
1160
+ amountIn: quote2.swapInAmount?.toString() || amountIn.toString(),
1161
+ expectedAmountOut: quote2.swapOutAmount?.toString() || "0",
1162
+ minimumAmountOut: quote2.minSwapOutAmount?.toString() || "0"
1163
+ };
1164
+ }
1165
+ const poolAccount = await client.state.getPoolByBaseMint(tokenPubkey);
1166
+ if (!poolAccount) {
1167
+ throw new Error(`Pool not found for token: ${params.tokenAddress}`);
1168
+ }
1169
+ let quote;
1170
+ try {
1171
+ quote = await getSwapQuote(
1172
+ sdk,
1173
+ params.tokenAddress,
1174
+ amountIn,
1175
+ true,
1176
+ slippageBps
1177
+ );
1178
+ } catch (err) {
1179
+ if (swapMode === 1 /* PartialFill */ && err.message?.includes("Insufficient Liquidity")) {
1180
+ console.warn("\u26A0\uFE0F Insufficient liquidity, using PartialFill mode to accept any available amount");
1181
+ quote = {
1182
+ amountIn: amountIn.toString(),
1183
+ outputAmount: "0",
1184
+ minimumAmountOut: "0",
1185
+ nextSqrtPrice: "0",
1186
+ tradingFee: "0",
1187
+ protocolFee: "0",
1188
+ referralFee: "0"
1189
+ };
1190
+ } else {
1191
+ throw err;
1192
+ }
1193
+ }
1194
+ let transaction;
1195
+ if (swapMode === 1 /* PartialFill */) {
1196
+ transaction = await client.pool.swap2({
1197
+ owner: params.owner,
1198
+ pool: poolAccount.publicKey,
1199
+ swapBaseForQuote: true,
1200
+ // Token -> SOL (sell)
1201
+ referralTokenAccount: null,
1202
+ swapMode: 1 /* PartialFill */,
1203
+ amountIn,
1204
+ minimumAmountOut: new BN2(quote.minimumAmountOut)
1205
+ });
1206
+ } else if (swapMode === 2 /* ExactOut */) {
1207
+ transaction = await client.pool.swap2({
1208
+ owner: params.owner,
1209
+ pool: poolAccount.publicKey,
1210
+ swapBaseForQuote: true,
1211
+ referralTokenAccount: null,
1212
+ swapMode: 2 /* ExactOut */,
1213
+ amountOut: new BN2(quote.outputAmount),
1214
+ maximumAmountIn: amountIn
1215
+ });
1216
+ } else {
1217
+ transaction = await client.pool.swap2({
1218
+ owner: params.owner,
1219
+ pool: poolAccount.publicKey,
1220
+ swapBaseForQuote: true,
1221
+ referralTokenAccount: null,
1222
+ swapMode: 0 /* ExactIn */,
1223
+ amountIn,
1224
+ minimumAmountOut: new BN2(quote.minimumAmountOut)
1225
+ });
1226
+ }
1227
+ const compiledTx = await sdk.compileTransactionWithALT(
1228
+ transaction,
1229
+ params.owner
1230
+ );
1231
+ return {
1232
+ transaction: compiledTx,
1233
+ amountIn: amountIn.toString(),
1234
+ expectedAmountOut: quote.outputAmount,
1235
+ minimumAmountOut: quote.minimumAmountOut
1236
+ };
1237
+ }
1238
+
1239
+ // src/cli/commands/swap.ts
1240
+ function registerSwapCommands(program2) {
1241
+ const swap = program2.command("swap").description("Token swap commands");
1242
+ swap.command("buy <token-address> <amount>").description("Buy tokens with NSO").option("--slippage <number>", "Slippage in basis points", "100").option(
1243
+ "--mode <mode>",
1244
+ "Swap mode: exact-in|partial-fill|exact-out",
1245
+ "partial-fill"
1246
+ ).option("-e, --export-tx", "Export unsigned transaction", false).action(
1247
+ async (tokenAddress, amount, options) => {
1248
+ try {
1249
+ await handleSwapBuy(tokenAddress, amount, options);
1250
+ } catch (error) {
1251
+ printError(error.message);
1252
+ process.exit(1);
1253
+ }
1254
+ }
1255
+ );
1256
+ swap.command("sell <token-address> <amount>").description("Sell tokens for NSO").option("--decimals <number>", "Token decimals", "6").option("--slippage <number>", "Slippage in basis points", "100").option(
1257
+ "--mode <mode>",
1258
+ "Swap mode: exact-in|partial-fill|exact-out",
1259
+ "partial-fill"
1260
+ ).option("-e, --export-tx", "Export unsigned transaction", false).action(
1261
+ async (tokenAddress, amount, options) => {
1262
+ try {
1263
+ await handleSwapSell(tokenAddress, amount, options);
1264
+ } catch (error) {
1265
+ printError(error.message);
1266
+ process.exit(1);
1267
+ }
1268
+ }
1269
+ );
1270
+ swap.command("quote <token-address> <amount> <direction>").description("Get swap quote (direction: buy|sell)").option("--decimals <number>", "Token decimals (for sell only)", "6").option("--slippage <number>", "Slippage in basis points", "100").action(
1271
+ async (tokenAddress, amount, direction, options) => {
1272
+ try {
1273
+ await handleSwapQuote(tokenAddress, amount, direction, options);
1274
+ } catch (error) {
1275
+ printError(error.message);
1276
+ process.exit(1);
1277
+ }
1278
+ }
1279
+ );
1280
+ }
1281
+ function parseSwapMode(mode) {
1282
+ const normalized = validateSwapMode(mode);
1283
+ switch (normalized) {
1284
+ case "exact-in":
1285
+ return 0 /* ExactIn */;
1286
+ case "partial-fill":
1287
+ return 1 /* PartialFill */;
1288
+ case "exact-out":
1289
+ return 2 /* ExactOut */;
1290
+ default:
1291
+ return 1 /* PartialFill */;
1292
+ }
1293
+ }
1294
+ async function handleSwapBuy(tokenAddress, amount, options) {
1295
+ const wallet = await loadWallet(options.wallet);
1296
+ const rpcUrl = getRpcUrl(options.rpcUrl);
1297
+ printInfo(`Using RPC: ${rpcUrl}`);
1298
+ printInfo(`Wallet: ${wallet.publicKey.toBase58()}`);
1299
+ validatePublicKey(tokenAddress);
1300
+ const amountInSOL = validatePositiveNumber(amount, "amount");
1301
+ const slippage = validateNonNegativeNumber(
1302
+ options.slippage || "100",
1303
+ "slippage"
1304
+ );
1305
+ const swapMode = parseSwapMode(options.mode || "partial-fill");
1306
+ const sdk = new NaraSDK({
1307
+ rpcUrl,
1308
+ commitment: "confirmed"
1309
+ });
1310
+ printInfo(`Buying tokens with ${amountInSOL} NSO...`);
1311
+ const result = await buyToken(sdk, {
1312
+ tokenAddress,
1313
+ amountInSOL,
1314
+ owner: wallet.publicKey,
1315
+ slippageBps: slippage,
1316
+ swapMode
1317
+ });
1318
+ printInfo(`Expected output: ${result.expectedAmountOut} tokens (smallest unit)`);
1319
+ printInfo(`Minimum output: ${result.minimumAmountOut} tokens (smallest unit)`);
1320
+ const txResult = await handleTransaction(
1321
+ sdk,
1322
+ result.transaction,
1323
+ [wallet],
1324
+ options.exportTx || false
1325
+ );
1326
+ if (options.json) {
1327
+ const output = {
1328
+ amountIn: result.amountIn,
1329
+ expectedAmountOut: result.expectedAmountOut,
1330
+ minimumAmountOut: result.minimumAmountOut,
1331
+ ...txResult.signature && { signature: txResult.signature },
1332
+ ...txResult.base64 && { transaction: txResult.base64 }
1333
+ };
1334
+ console.log(JSON.stringify(output, null, 2));
1335
+ } else {
1336
+ console.log(`
1337
+ Swap Details:`);
1338
+ console.log(` Input: ${(parseInt(result.amountIn) / 1e9).toFixed(4)} NSO`);
1339
+ console.log(` Expected Output: ${result.expectedAmountOut} tokens`);
1340
+ console.log(` Minimum Output: ${result.minimumAmountOut} tokens`);
1341
+ printTransactionResult(txResult, false);
1342
+ }
1343
+ }
1344
+ async function handleSwapSell(tokenAddress, amount, options) {
1345
+ const wallet = await loadWallet(options.wallet);
1346
+ const rpcUrl = getRpcUrl(options.rpcUrl);
1347
+ printInfo(`Using RPC: ${rpcUrl}`);
1348
+ printInfo(`Wallet: ${wallet.publicKey.toBase58()}`);
1349
+ validatePublicKey(tokenAddress);
1350
+ const amountInToken = validatePositiveNumber(amount, "amount");
1351
+ const decimals = parseInt(String(options.decimals || "6"));
1352
+ const slippage = validateNonNegativeNumber(
1353
+ options.slippage || "100",
1354
+ "slippage"
1355
+ );
1356
+ const swapMode = parseSwapMode(options.mode || "partial-fill");
1357
+ const sdk = new NaraSDK({
1358
+ rpcUrl,
1359
+ commitment: "confirmed"
1360
+ });
1361
+ printInfo(`Selling ${amountInToken} tokens...`);
1362
+ const result = await sellToken(sdk, {
1363
+ tokenAddress,
1364
+ amountInToken,
1365
+ owner: wallet.publicKey,
1366
+ tokenDecimals: decimals,
1367
+ slippageBps: slippage,
1368
+ swapMode
1369
+ });
1370
+ printInfo(`Expected output: ${(parseInt(result.expectedAmountOut) / 1e9).toFixed(4)} NSO`);
1371
+ printInfo(`Minimum output: ${(parseInt(result.minimumAmountOut) / 1e9).toFixed(4)} NSO`);
1372
+ const txResult = await handleTransaction(
1373
+ sdk,
1374
+ result.transaction,
1375
+ [wallet],
1376
+ options.exportTx || false
1377
+ );
1378
+ if (options.json) {
1379
+ const output = {
1380
+ amountIn: result.amountIn,
1381
+ expectedAmountOut: result.expectedAmountOut,
1382
+ minimumAmountOut: result.minimumAmountOut,
1383
+ ...txResult.signature && { signature: txResult.signature },
1384
+ ...txResult.base64 && { transaction: txResult.base64 }
1385
+ };
1386
+ console.log(JSON.stringify(output, null, 2));
1387
+ } else {
1388
+ console.log(`
1389
+ Swap Details:`);
1390
+ console.log(` Input: ${result.amountIn} tokens`);
1391
+ console.log(` Expected Output: ${(parseInt(result.expectedAmountOut) / 1e9).toFixed(4)} NSO`);
1392
+ console.log(` Minimum Output: ${(parseInt(result.minimumAmountOut) / 1e9).toFixed(4)} NSO`);
1393
+ printTransactionResult(txResult, false);
1394
+ }
1395
+ }
1396
+ async function handleSwapQuote(tokenAddress, amount, direction, options) {
1397
+ const rpcUrl = getRpcUrl(options.rpcUrl);
1398
+ printInfo(`Using RPC: ${rpcUrl}`);
1399
+ validatePublicKey(tokenAddress);
1400
+ const amountNum = validatePositiveNumber(amount, "amount");
1401
+ const dir = validateDirection(direction);
1402
+ const decimals = parseInt(String(options.decimals || "6"));
1403
+ const slippage = validateNonNegativeNumber(
1404
+ options.slippage || "100",
1405
+ "slippage"
1406
+ );
1407
+ const sdk = new NaraSDK({
1408
+ rpcUrl,
1409
+ commitment: "confirmed"
1410
+ });
1411
+ const swapBaseForQuote = dir === "sell";
1412
+ const amountIn = swapBaseForQuote ? new BN3(amountNum * 10 ** decimals) : new BN3(amountNum * 1e9);
1413
+ printInfo(`Getting ${dir} quote for ${amountNum} ${swapBaseForQuote ? "tokens" : "NSO"}...`);
1414
+ const quote = await getSwapQuote(
1415
+ sdk,
1416
+ tokenAddress,
1417
+ amountIn,
1418
+ swapBaseForQuote,
1419
+ slippage
1420
+ );
1421
+ if (options.json) {
1422
+ console.log(JSON.stringify(quote, null, 2));
1423
+ } else {
1424
+ console.log(`
1425
+ Quote:`);
1426
+ if (swapBaseForQuote) {
1427
+ console.log(` Input: ${(parseInt(quote.amountIn) / 10 ** decimals).toFixed(4)} tokens`);
1428
+ console.log(` Expected Output: ${(parseInt(quote.outputAmount) / 1e9).toFixed(4)} NSO`);
1429
+ console.log(` Minimum Output: ${(parseInt(quote.minimumAmountOut) / 1e9).toFixed(4)} NSO`);
1430
+ } else {
1431
+ console.log(` Input: ${(parseInt(quote.amountIn) / 1e9).toFixed(4)} NSO`);
1432
+ console.log(` Expected Output: ${quote.outputAmount} tokens (smallest unit)`);
1433
+ console.log(` Minimum Output: ${quote.minimumAmountOut} tokens (smallest unit)`);
1434
+ }
1435
+ console.log(` Trading Fee: ${quote.tradingFee}`);
1436
+ console.log(` Protocol Fee: ${quote.protocolFee}`);
1437
+ }
1438
+ }
1439
+
1440
+ // src/cli/commands/migrate.ts
1441
+ import "commander";
1442
+
1443
+ // src/migrate.ts
1444
+ import {
1445
+ PublicKey as PublicKey6
1446
+ } from "@solana/web3.js";
1447
+ import "bn.js";
1448
+ import { DAMM_V2_MIGRATION_FEE_ADDRESS as DAMM_V2_MIGRATION_FEE_ADDRESS2 } from "@meteora-ag/dynamic-bonding-curve-sdk";
1449
+ async function migrateToDAMMV2(sdk, params) {
1450
+ const connection = sdk.getConnection();
1451
+ const client = sdk.getClient();
1452
+ const tokenPubkey = new PublicKey6(params.tokenAddress);
1453
+ const poolAccount = await client.state.getPoolByBaseMint(tokenPubkey);
1454
+ if (!poolAccount) {
1455
+ throw new Error(`Pool not found for token: ${params.tokenAddress}`);
1456
+ }
1457
+ if (poolAccount.account.isMigrated) {
1458
+ throw new Error("Pool has already been migrated");
1459
+ }
1460
+ const virtualPool = poolAccount.account;
1461
+ const poolConfig = await client.state.getPoolConfig(virtualPool.config);
1462
+ const migrationFeeOption = poolConfig.migrationFeeOption || 0;
1463
+ const dammConfig = DAMM_V2_MIGRATION_FEE_ADDRESS2[migrationFeeOption];
1464
+ if (!dammConfig) {
1465
+ throw new Error(
1466
+ `Invalid migration fee option: ${migrationFeeOption}. Cannot determine DAMM config address.`
1467
+ );
1468
+ }
1469
+ const result = await client.migration.migrateToDammV2({
1470
+ payer: params.payer,
1471
+ virtualPool: poolAccount.publicKey,
1472
+ dammConfig
1473
+ });
1474
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
1475
+ result.transaction.recentBlockhash = blockhash;
1476
+ result.transaction.feePayer = params.payer;
1477
+ const compiledTx = await sdk.compileTransactionWithALT(
1478
+ result.transaction,
1479
+ params.payer
1480
+ );
1481
+ return {
1482
+ transaction: compiledTx,
1483
+ firstPositionNftKeypair: result.firstPositionNftKeypair,
1484
+ secondPositionNftKeypair: result.secondPositionNftKeypair,
1485
+ poolAddress: poolAccount.publicKey.toBase58()
1486
+ };
1487
+ }
1488
+ async function createLocker(sdk, params) {
1489
+ const connection = sdk.getConnection();
1490
+ const client = sdk.getClient();
1491
+ const tokenPubkey = new PublicKey6(params.tokenAddress);
1492
+ const poolAccount = await client.state.getPoolByBaseMint(tokenPubkey);
1493
+ if (!poolAccount) {
1494
+ throw new Error(`Pool not found for token: ${params.tokenAddress}`);
1495
+ }
1496
+ const transaction = await client.migration.createLocker({
1497
+ payer: params.payer,
1498
+ virtualPool: poolAccount.publicKey
1499
+ });
1500
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
1501
+ transaction.recentBlockhash = blockhash;
1502
+ transaction.feePayer = params.payer;
1503
+ const compiledTx = await sdk.compileTransactionWithALT(
1504
+ transaction,
1505
+ params.payer
1506
+ );
1507
+ return {
1508
+ transaction: compiledTx,
1509
+ poolAddress: poolAccount.publicKey.toBase58()
1510
+ };
1511
+ }
1512
+ async function canMigrate(sdk, tokenAddress) {
1513
+ const client = sdk.getClient();
1514
+ const tokenPubkey = new PublicKey6(tokenAddress);
1515
+ const poolAccount = await client.state.getPoolByBaseMint(tokenPubkey);
1516
+ if (!poolAccount) {
1517
+ return {
1518
+ canMigrate: false,
1519
+ reason: "Pool not found",
1520
+ progress: 0
1521
+ };
1522
+ }
1523
+ if (poolAccount.account.isMigrated) {
1524
+ return {
1525
+ canMigrate: false,
1526
+ reason: "Pool has already been migrated",
1527
+ progress: 1
1528
+ };
1529
+ }
1530
+ const progress = await client.state.getPoolCurveProgress(
1531
+ poolAccount.publicKey
1532
+ );
1533
+ if (progress >= 1) {
1534
+ return {
1535
+ canMigrate: true,
1536
+ progress
1537
+ };
1538
+ }
1539
+ return {
1540
+ canMigrate: false,
1541
+ reason: `Curve not complete. Current progress: ${(progress * 100).toFixed(2)}%`,
1542
+ progress
1543
+ };
1544
+ }
1545
+
1546
+ // src/cli/commands/migrate.ts
1547
+ function registerMigrateCommands(program2) {
1548
+ const migrate = program2.command("migrate").description("Pool migration commands");
1549
+ migrate.command("launch <token-address>").description("Migrate pool to DAMM V2 (graduation)").option("-e, --export-tx", "Export unsigned transaction", false).action(
1550
+ async (tokenAddress, options) => {
1551
+ try {
1552
+ await handleMigrateLaunch(tokenAddress, options);
1553
+ } catch (error) {
1554
+ printError(error.message);
1555
+ process.exit(1);
1556
+ }
1557
+ }
1558
+ );
1559
+ migrate.command("create-locker <token-address>").description("Create locker for pools with vesting").option("-e, --export-tx", "Export unsigned transaction", false).action(
1560
+ async (tokenAddress, options) => {
1561
+ try {
1562
+ await handleMigrateCreateLocker(tokenAddress, options);
1563
+ } catch (error) {
1564
+ printError(error.message);
1565
+ process.exit(1);
1566
+ }
1567
+ }
1568
+ );
1569
+ migrate.command("check <token-address>").description("Check if pool can be migrated").action(
1570
+ async (tokenAddress, options) => {
1571
+ try {
1572
+ await handleMigrateCheck(tokenAddress, options);
1573
+ } catch (error) {
1574
+ printError(error.message);
1575
+ process.exit(1);
1576
+ }
1577
+ }
1578
+ );
1579
+ }
1580
+ async function handleMigrateLaunch(tokenAddress, options) {
1581
+ const wallet = await loadWallet(options.wallet);
1582
+ const rpcUrl = getRpcUrl(options.rpcUrl);
1583
+ printInfo(`Using RPC: ${rpcUrl}`);
1584
+ printInfo(`Wallet: ${wallet.publicKey.toBase58()}`);
1585
+ validatePublicKey(tokenAddress);
1586
+ const sdk = new NaraSDK({
1587
+ rpcUrl,
1588
+ commitment: "confirmed"
1589
+ });
1590
+ printInfo("Checking if pool can be migrated...");
1591
+ const checkResult = await canMigrate(sdk, tokenAddress);
1592
+ if (!checkResult.canMigrate) {
1593
+ printError(`Cannot migrate: ${checkResult.reason}`);
1594
+ process.exit(1);
1595
+ }
1596
+ printSuccess("Pool is ready for migration!");
1597
+ printInfo("Migrating pool to DAMM V2...");
1598
+ const result = await migrateToDAMMV2(sdk, {
1599
+ tokenAddress,
1600
+ payer: wallet.publicKey
1601
+ });
1602
+ printInfo(`Pool address: ${result.poolAddress}`);
1603
+ const txResult = await handleTransaction(
1604
+ sdk,
1605
+ result.transaction,
1606
+ [wallet, result.firstPositionNftKeypair, result.secondPositionNftKeypair],
1607
+ options.exportTx || false
1608
+ );
1609
+ if (options.json) {
1610
+ const output = {
1611
+ poolAddress: result.poolAddress,
1612
+ ...txResult.signature && { signature: txResult.signature },
1613
+ ...txResult.base64 && { transaction: txResult.base64 }
1614
+ };
1615
+ console.log(JSON.stringify(output, null, 2));
1616
+ } else {
1617
+ console.log(`
1618
+ Pool Address: ${result.poolAddress}`);
1619
+ printTransactionResult(txResult, false);
1620
+ if (txResult.signature) {
1621
+ printSuccess("\nPool successfully migrated to DAMM V2!");
1622
+ printInfo("Pool is now a constant product AMM with full liquidity.");
1623
+ }
1624
+ }
1625
+ }
1626
+ async function handleMigrateCreateLocker(tokenAddress, options) {
1627
+ const wallet = await loadWallet(options.wallet);
1628
+ const rpcUrl = getRpcUrl(options.rpcUrl);
1629
+ printInfo(`Using RPC: ${rpcUrl}`);
1630
+ printInfo(`Wallet: ${wallet.publicKey.toBase58()}`);
1631
+ validatePublicKey(tokenAddress);
1632
+ const sdk = new NaraSDK({
1633
+ rpcUrl,
1634
+ commitment: "confirmed"
1635
+ });
1636
+ printInfo("Creating locker for pool with vesting parameters...");
1637
+ const result = await createLocker(sdk, {
1638
+ tokenAddress,
1639
+ payer: wallet.publicKey
1640
+ });
1641
+ printInfo(`Pool address: ${result.poolAddress}`);
1642
+ const txResult = await handleTransaction(
1643
+ sdk,
1644
+ result.transaction,
1645
+ [wallet],
1646
+ options.exportTx || false
1647
+ );
1648
+ if (options.json) {
1649
+ const output = {
1650
+ poolAddress: result.poolAddress,
1651
+ ...txResult.signature && { signature: txResult.signature },
1652
+ ...txResult.base64 && { transaction: txResult.base64 }
1653
+ };
1654
+ console.log(JSON.stringify(output, null, 2));
1655
+ } else {
1656
+ console.log(`
1657
+ Pool Address: ${result.poolAddress}`);
1658
+ printTransactionResult(txResult, false);
1659
+ if (txResult.signature) {
1660
+ printSuccess("\nLocker created successfully!");
1661
+ printInfo("You can now proceed with migrating the pool.");
1662
+ }
1663
+ }
1664
+ }
1665
+ async function handleMigrateCheck(tokenAddress, options) {
1666
+ const rpcUrl = getRpcUrl(options.rpcUrl);
1667
+ printInfo(`Using RPC: ${rpcUrl}`);
1668
+ validatePublicKey(tokenAddress);
1669
+ const sdk = new NaraSDK({
1670
+ rpcUrl,
1671
+ commitment: "confirmed"
1672
+ });
1673
+ printInfo("Checking migration status...");
1674
+ const result = await canMigrate(sdk, tokenAddress);
1675
+ if (options.json) {
1676
+ console.log(JSON.stringify(result, null, 2));
1677
+ } else {
1678
+ console.log(`
1679
+ Migration Status:`);
1680
+ console.log(` Can Migrate: ${result.canMigrate ? "Yes" : "No"}`);
1681
+ console.log(` Progress: ${(result.progress * 100).toFixed(2)}%`);
1682
+ if (result.reason) {
1683
+ console.log(` Reason: ${result.reason}`);
1684
+ }
1685
+ if (result.canMigrate) {
1686
+ printSuccess("\nPool is ready for migration!");
1687
+ printInfo("Run: nara-cli migrate launch <token-address>");
1688
+ } else {
1689
+ printWarning("\nPool is not ready for migration yet.");
1690
+ if (result.progress < 1) {
1691
+ printInfo(`Curve completion required: 100% (current: ${(result.progress * 100).toFixed(2)}%)`);
1692
+ }
1693
+ }
1694
+ }
1695
+ }
1696
+
1697
+ // src/cli/commands/wallet.ts
1698
+ import "commander";
1699
+ import {
1700
+ Keypair as Keypair6,
1701
+ LAMPORTS_PER_SOL,
1702
+ SystemProgram,
1703
+ Transaction as Transaction7
1704
+ } from "@solana/web3.js";
1705
+ import {
1706
+ getAssociatedTokenAddress,
1707
+ createTransferInstruction,
1708
+ TOKEN_PROGRAM_ID as TOKEN_PROGRAM_ID2
1709
+ } from "@solana/spl-token";
1710
+ import * as bip39 from "bip39";
1711
+ import { derivePath } from "ed25519-hd-key";
1712
+ import { join as join2 } from "node:path";
1713
+ import { homedir as homedir2 } from "node:os";
1714
+ import { mkdir } from "node:fs/promises";
1715
+ import bs58 from "bs58";
1716
+ var DEFAULT_WALLET_PATH3 = DEFAULT_WALLET_PATH.startsWith("~") ? join2(homedir2(), DEFAULT_WALLET_PATH.slice(1)) : DEFAULT_WALLET_PATH;
1717
+ function registerWalletCommands(program2) {
1718
+ const wallet = program2.command("wallet").description("Wallet management commands");
1719
+ wallet.command("create").description("Create a new wallet").option("-o, --output <path>", "Output path for wallet file (default: ~/.config/nara/id.json)").action(async (options) => {
1720
+ try {
1721
+ await handleWalletCreate(options);
1722
+ } catch (error) {
1723
+ printError(error.message);
1724
+ process.exit(1);
1725
+ }
1726
+ });
1727
+ wallet.command("import").description("Import a wallet from mnemonic or private key").option("-m, --mnemonic <phrase>", "Mnemonic phrase (12 or 24 words)").option("-k, --private-key <key>", "Private key (base58 or JSON array)").option("-o, --output <path>", "Output path for wallet file (default: ~/.config/nara/id.json)").action(async (options) => {
1728
+ try {
1729
+ await handleWalletImport(options);
1730
+ } catch (error) {
1731
+ printError(error.message);
1732
+ process.exit(1);
1733
+ }
1734
+ });
1735
+ wallet.command("address").description("Show wallet address").action(async (options) => {
1736
+ try {
1737
+ await handleWalletAddress(options);
1738
+ } catch (error) {
1739
+ printError(error.message);
1740
+ process.exit(1);
1741
+ }
1742
+ });
1743
+ wallet.command("balance").description("Check NSO balance").argument("[address]", "Wallet address (optional, defaults to current wallet)").action(async (address, options) => {
1744
+ try {
1745
+ await handleWalletBalance(address, options);
1746
+ } catch (error) {
1747
+ printError(error.message);
1748
+ process.exit(1);
1749
+ }
1750
+ });
1751
+ wallet.command("token-balance <token-address>").description("Check token balance").option("--owner <address>", "Owner address (optional, defaults to current wallet)").action(
1752
+ async (tokenAddress, options) => {
1753
+ try {
1754
+ await handleTokenBalance(tokenAddress, options);
1755
+ } catch (error) {
1756
+ printError(error.message);
1757
+ process.exit(1);
1758
+ }
1759
+ }
1760
+ );
1761
+ wallet.command("tx-status <signature>").description("Check transaction status").action(
1762
+ async (signature, options) => {
1763
+ try {
1764
+ await handleTxStatus(signature, options);
1765
+ } catch (error) {
1766
+ printError(error.message);
1767
+ process.exit(1);
1768
+ }
1769
+ }
1770
+ );
1771
+ wallet.command("transfer <to> <amount>").description("Transfer NSO to another wallet").option("-e, --export-tx", "Export unsigned transaction", false).action(
1772
+ async (to, amount, options) => {
1773
+ try {
1774
+ await handleTransferSol(to, amount, options);
1775
+ } catch (error) {
1776
+ printError(error.message);
1777
+ process.exit(1);
1778
+ }
1779
+ }
1780
+ );
1781
+ wallet.command("transfer-token <token-address> <to> <amount>").description("Transfer tokens to another wallet").option("--decimals <number>", "Token decimals", "6").option("-e, --export-tx", "Export unsigned transaction", false).action(
1782
+ async (tokenAddress, to, amount, options) => {
1783
+ try {
1784
+ await handleTransferToken(tokenAddress, to, amount, options);
1785
+ } catch (error) {
1786
+ printError(error.message);
1787
+ process.exit(1);
1788
+ }
1789
+ }
1790
+ );
1791
+ }
1792
+ async function handleWalletBalance(address, options) {
1793
+ const rpcUrl = getRpcUrl(options.rpcUrl);
1794
+ printInfo(`Using RPC: ${rpcUrl}`);
1795
+ const sdk = new NaraSDK({
1796
+ rpcUrl,
1797
+ commitment: "confirmed"
1798
+ });
1799
+ const connection = sdk.getConnection();
1800
+ let pubkey;
1801
+ if (address) {
1802
+ pubkey = validatePublicKey(address);
1803
+ } else {
1804
+ const wallet = await loadWallet(options.wallet);
1805
+ pubkey = wallet.publicKey;
1806
+ }
1807
+ printInfo(`Checking balance for: ${pubkey.toBase58()}`);
1808
+ const balance = await connection.getBalance(pubkey);
1809
+ const balanceSOL = balance / LAMPORTS_PER_SOL;
1810
+ if (options.json) {
1811
+ const output = {
1812
+ address: pubkey.toBase58(),
1813
+ balance: balanceSOL,
1814
+ lamports: balance
1815
+ };
1816
+ console.log(JSON.stringify(output, null, 2));
1817
+ } else {
1818
+ console.log(`
1819
+ Wallet: ${pubkey.toBase58()}`);
1820
+ console.log(`Balance: ${balanceSOL.toFixed(4)} NSO (${balance.toLocaleString()} lamports)`);
1821
+ }
1822
+ }
1823
+ async function handleTokenBalance(tokenAddress, options) {
1824
+ const rpcUrl = getRpcUrl(options.rpcUrl);
1825
+ printInfo(`Using RPC: ${rpcUrl}`);
1826
+ const tokenMint = validatePublicKey(tokenAddress);
1827
+ const sdk = new NaraSDK({
1828
+ rpcUrl,
1829
+ commitment: "confirmed"
1830
+ });
1831
+ const connection = sdk.getConnection();
1832
+ let owner;
1833
+ if (options.owner) {
1834
+ owner = validatePublicKey(options.owner);
1835
+ } else {
1836
+ const wallet = await loadWallet(options.wallet);
1837
+ owner = wallet.publicKey;
1838
+ }
1839
+ printInfo(`Owner: ${owner.toBase58()}`);
1840
+ printInfo(`Token: ${tokenAddress}`);
1841
+ const tokenAccount = await getAssociatedTokenAddress(tokenMint, owner);
1842
+ try {
1843
+ const accountInfo = await connection.getTokenAccountBalance(tokenAccount);
1844
+ const balance = accountInfo.value;
1845
+ if (options.json) {
1846
+ const output = {
1847
+ owner: owner.toBase58(),
1848
+ tokenAddress,
1849
+ tokenAccount: tokenAccount.toBase58(),
1850
+ balance: balance.uiAmountString,
1851
+ amount: balance.amount,
1852
+ decimals: balance.decimals
1853
+ };
1854
+ console.log(JSON.stringify(output, null, 2));
1855
+ } else {
1856
+ console.log(`
1857
+ Token Account: ${tokenAccount.toBase58()}`);
1858
+ console.log(`Balance: ${balance.uiAmountString || "0"} tokens`);
1859
+ console.log(`Amount: ${balance.amount} (smallest unit)`);
1860
+ console.log(`Decimals: ${balance.decimals}`);
1861
+ }
1862
+ } catch (error) {
1863
+ if (error.message?.includes("could not find account")) {
1864
+ if (options.json) {
1865
+ const output = {
1866
+ owner: owner.toBase58(),
1867
+ tokenAddress,
1868
+ tokenAccount: tokenAccount.toBase58(),
1869
+ balance: "0",
1870
+ amount: "0"
1871
+ };
1872
+ console.log(JSON.stringify(output, null, 2));
1873
+ } else {
1874
+ printInfo("\nToken account does not exist yet.");
1875
+ console.log(`Balance: 0 tokens`);
1876
+ console.log(`Token Account (will be created on first transfer): ${tokenAccount.toBase58()}`);
1877
+ }
1878
+ } else {
1879
+ throw error;
1880
+ }
1881
+ }
1882
+ }
1883
+ async function handleTxStatus(signature, options) {
1884
+ const rpcUrl = getRpcUrl(options.rpcUrl);
1885
+ printInfo(`Using RPC: ${rpcUrl}`);
1886
+ printInfo(`Checking transaction: ${signature}`);
1887
+ const sdk = new NaraSDK({
1888
+ rpcUrl,
1889
+ commitment: "confirmed"
1890
+ });
1891
+ const connection = sdk.getConnection();
1892
+ const status = await connection.getSignatureStatus(signature);
1893
+ if (!status || !status.value) {
1894
+ if (options.json) {
1895
+ console.log(JSON.stringify({ signature, status: "not_found" }, null, 2));
1896
+ } else {
1897
+ printError("Transaction not found");
1898
+ }
1899
+ return;
1900
+ }
1901
+ const transaction = await connection.getTransaction(signature, {
1902
+ maxSupportedTransactionVersion: 0
1903
+ });
1904
+ const output = {
1905
+ signature,
1906
+ status: status.value.confirmationStatus || "unknown",
1907
+ slot: status.value.slot,
1908
+ confirmations: status.value.confirmations
1909
+ };
1910
+ if (status.value.err) {
1911
+ output.error = status.value.err;
1912
+ output.success = false;
1913
+ } else {
1914
+ output.success = true;
1915
+ }
1916
+ if (transaction) {
1917
+ output.blockTime = transaction.blockTime;
1918
+ output.fee = transaction.meta?.fee;
1919
+ }
1920
+ if (options.json) {
1921
+ console.log(JSON.stringify(output, null, 2));
1922
+ } else {
1923
+ console.log(`
1924
+ Transaction: ${signature}`);
1925
+ console.log(`Status: ${output.status}`);
1926
+ console.log(`Success: ${output.success ? "Yes" : "No"}`);
1927
+ console.log(`Slot: ${output.slot}`);
1928
+ if (output.confirmations !== null) {
1929
+ console.log(`Confirmations: ${output.confirmations}`);
1930
+ }
1931
+ if (output.blockTime) {
1932
+ const date = new Date(output.blockTime * 1e3);
1933
+ console.log(`Time: ${date.toISOString()}`);
1934
+ }
1935
+ if (output.fee) {
1936
+ console.log(`Fee: ${output.fee / LAMPORTS_PER_SOL} NSO`);
1937
+ }
1938
+ if (output.error) {
1939
+ console.log(`Error: ${JSON.stringify(output.error)}`);
1940
+ }
1941
+ console.log(
1942
+ `
1943
+ View on Solscan: https://solscan.io/tx/${signature}?cluster=devnet`
1944
+ );
1945
+ }
1946
+ }
1947
+ async function handleTransferSol(to, amount, options) {
1948
+ const wallet = await loadWallet(options.wallet);
1949
+ const rpcUrl = getRpcUrl(options.rpcUrl);
1950
+ printInfo(`Using RPC: ${rpcUrl}`);
1951
+ printInfo(`From: ${wallet.publicKey.toBase58()}`);
1952
+ const recipient = validatePublicKey(to);
1953
+ const amountSOL = validatePositiveNumber(amount, "amount");
1954
+ const lamports = Math.floor(amountSOL * LAMPORTS_PER_SOL);
1955
+ printInfo(`To: ${recipient.toBase58()}`);
1956
+ printInfo(`Amount: ${amountSOL} NSO`);
1957
+ const sdk = new NaraSDK({
1958
+ rpcUrl,
1959
+ commitment: "confirmed"
1960
+ });
1961
+ const connection = sdk.getConnection();
1962
+ const transferInstruction = SystemProgram.transfer({
1963
+ fromPubkey: wallet.publicKey,
1964
+ toPubkey: recipient,
1965
+ lamports
1966
+ });
1967
+ const transaction = new Transaction7().add(transferInstruction);
1968
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
1969
+ transaction.recentBlockhash = blockhash;
1970
+ transaction.feePayer = wallet.publicKey;
1971
+ const txResult = await handleTransaction(
1972
+ sdk,
1973
+ transaction,
1974
+ [wallet],
1975
+ options.exportTx || false
1976
+ );
1977
+ if (options.json) {
1978
+ const output = {
1979
+ from: wallet.publicKey.toBase58(),
1980
+ to: recipient.toBase58(),
1981
+ amount: amountSOL,
1982
+ lamports,
1983
+ ...txResult.signature && { signature: txResult.signature },
1984
+ ...txResult.base64 && { transaction: txResult.base64 }
1985
+ };
1986
+ console.log(JSON.stringify(output, null, 2));
1987
+ } else {
1988
+ console.log(`
1989
+ Transfer Details:`);
1990
+ console.log(` From: ${wallet.publicKey.toBase58()}`);
1991
+ console.log(` To: ${recipient.toBase58()}`);
1992
+ console.log(` Amount: ${amountSOL} NSO`);
1993
+ printTransactionResult(txResult, false);
1994
+ }
1995
+ }
1996
+ async function handleTransferToken(tokenAddress, to, amount, options) {
1997
+ const wallet = await loadWallet(options.wallet);
1998
+ const rpcUrl = getRpcUrl(options.rpcUrl);
1999
+ printInfo(`Using RPC: ${rpcUrl}`);
2000
+ printInfo(`From: ${wallet.publicKey.toBase58()}`);
2001
+ const tokenMint = validatePublicKey(tokenAddress);
2002
+ const recipient = validatePublicKey(to);
2003
+ const amountInToken = validatePositiveNumber(amount, "amount");
2004
+ const decimals = parseInt(String(options.decimals || "6"));
2005
+ const amountInSmallestUnit = Math.floor(amountInToken * 10 ** decimals);
2006
+ printInfo(`To: ${recipient.toBase58()}`);
2007
+ printInfo(`Token: ${tokenAddress}`);
2008
+ printInfo(`Amount: ${amountInToken} tokens`);
2009
+ const sdk = new NaraSDK({
2010
+ rpcUrl,
2011
+ commitment: "confirmed"
2012
+ });
2013
+ const connection = sdk.getConnection();
2014
+ const sourceAccount = await getAssociatedTokenAddress(
2015
+ tokenMint,
2016
+ wallet.publicKey
2017
+ );
2018
+ const destinationAccount = await getAssociatedTokenAddress(
2019
+ tokenMint,
2020
+ recipient
2021
+ );
2022
+ const transferInstruction = createTransferInstruction(
2023
+ sourceAccount,
2024
+ destinationAccount,
2025
+ wallet.publicKey,
2026
+ amountInSmallestUnit,
2027
+ [],
2028
+ TOKEN_PROGRAM_ID2
2029
+ );
2030
+ const transaction = new Transaction7().add(transferInstruction);
2031
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
2032
+ transaction.recentBlockhash = blockhash;
2033
+ transaction.feePayer = wallet.publicKey;
2034
+ const txResult = await handleTransaction(
2035
+ sdk,
2036
+ transaction,
2037
+ [wallet],
2038
+ options.exportTx || false
2039
+ );
2040
+ if (options.json) {
2041
+ const output = {
2042
+ from: wallet.publicKey.toBase58(),
2043
+ to: recipient.toBase58(),
2044
+ tokenAddress,
2045
+ amount: amountInToken,
2046
+ amountSmallestUnit: amountInSmallestUnit.toString(),
2047
+ decimals,
2048
+ ...txResult.signature && { signature: txResult.signature },
2049
+ ...txResult.base64 && { transaction: txResult.base64 }
2050
+ };
2051
+ console.log(JSON.stringify(output, null, 2));
2052
+ } else {
2053
+ console.log(`
2054
+ Token Transfer Details:`);
2055
+ console.log(` From: ${wallet.publicKey.toBase58()}`);
2056
+ console.log(` To: ${recipient.toBase58()}`);
2057
+ console.log(` Token: ${tokenAddress}`);
2058
+ console.log(` Amount: ${amountInToken} tokens`);
2059
+ printTransactionResult(txResult, false);
2060
+ }
2061
+ }
2062
+ async function handleWalletCreate(options) {
2063
+ const outputPath = options.output || DEFAULT_WALLET_PATH3;
2064
+ if (await Bun.file(outputPath).exists()) {
2065
+ throw new Error(
2066
+ `Wallet file already exists at ${outputPath}. Please use a different path or remove the existing file first.`
2067
+ );
2068
+ }
2069
+ const mnemonic = bip39.generateMnemonic(128);
2070
+ const seed = await bip39.mnemonicToSeed(mnemonic);
2071
+ const derivedSeed = derivePath("m/44'/501'/0'/0'", seed.toString("hex")).key;
2072
+ const keypair = Keypair6.fromSeed(derivedSeed);
2073
+ const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
2074
+ await mkdir(dir, { recursive: true });
2075
+ const walletData = Array.from(keypair.secretKey);
2076
+ await Bun.write(outputPath, JSON.stringify(walletData, null, 2));
2077
+ console.log("\n\u2705 Wallet created successfully!");
2078
+ console.log(`
2079
+ \u{1F4C1} Wallet saved to: ${outputPath}`);
2080
+ console.log(`\u{1F511} Public Key: ${keypair.publicKey.toBase58()}`);
2081
+ printWarning("\n\u26A0\uFE0F IMPORTANT: Save your mnemonic phrase securely!");
2082
+ printWarning("\u26A0\uFE0F You will need it to recover your wallet.");
2083
+ console.log("\n\u{1F4DD} Mnemonic phrase (12 words):");
2084
+ console.log(`
2085
+ ${mnemonic}
2086
+ `);
2087
+ printWarning("\u26A0\uFE0F Never share your mnemonic phrase with anyone!");
2088
+ printWarning("\u26A0\uFE0F Anyone with your mnemonic can access your funds.\n");
2089
+ }
2090
+ async function handleWalletImport(options) {
2091
+ const outputPath = options.output || DEFAULT_WALLET_PATH3;
2092
+ if (await Bun.file(outputPath).exists()) {
2093
+ throw new Error(
2094
+ `Wallet file already exists at ${outputPath}. Please use a different path or remove the existing file first.`
2095
+ );
2096
+ }
2097
+ let keypair;
2098
+ if (options.mnemonic) {
2099
+ const mnemonic = options.mnemonic.trim();
2100
+ if (!bip39.validateMnemonic(mnemonic)) {
2101
+ throw new Error("Invalid mnemonic phrase. Please check your words and try again.");
2102
+ }
2103
+ const seed = await bip39.mnemonicToSeed(mnemonic);
2104
+ const derivedSeed = derivePath("m/44'/501'/0'/0'", seed.toString("hex")).key;
2105
+ keypair = Keypair6.fromSeed(derivedSeed);
2106
+ printInfo("Importing wallet from mnemonic...");
2107
+ } else if (options.privateKey) {
2108
+ const privateKey = options.privateKey.trim();
2109
+ try {
2110
+ if (privateKey.startsWith("[")) {
2111
+ const data = JSON.parse(privateKey);
2112
+ keypair = Keypair6.fromSecretKey(new Uint8Array(data));
2113
+ } else {
2114
+ keypair = Keypair6.fromSecretKey(bs58.decode(privateKey));
2115
+ }
2116
+ printInfo("Importing wallet from private key...");
2117
+ } catch (error) {
2118
+ throw new Error(`Invalid private key format: ${error.message}`);
2119
+ }
2120
+ } else {
2121
+ throw new Error("Please provide either --mnemonic or --private-key option.");
2122
+ }
2123
+ const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
2124
+ await mkdir(dir, { recursive: true });
2125
+ const walletData = Array.from(keypair.secretKey);
2126
+ await Bun.write(outputPath, JSON.stringify(walletData, null, 2));
2127
+ console.log("\n\u2705 Wallet imported successfully!");
2128
+ console.log(`
2129
+ \u{1F4C1} Wallet saved to: ${outputPath}`);
2130
+ console.log(`\u{1F511} Public Key: ${keypair.publicKey.toBase58()}
2131
+ `);
2132
+ }
2133
+ async function handleWalletAddress(options) {
2134
+ const wallet = await loadWallet(options.wallet);
2135
+ if (options.json) {
2136
+ const output = {
2137
+ address: wallet.publicKey.toBase58()
2138
+ };
2139
+ console.log(JSON.stringify(output, null, 2));
2140
+ } else {
2141
+ console.log(`
2142
+ \u{1F511} Wallet Address: ${wallet.publicKey.toBase58()}
2143
+ `);
2144
+ }
2145
+ }
2146
+
2147
+ // src/cli/commands/quest.ts
2148
+ import "commander";
2149
+ import { Connection as Connection3, Keypair as Keypair8 } from "@solana/web3.js";
2150
+
2151
+ // src/quest.ts
2152
+ import {
2153
+ Keypair as Keypair7,
2154
+ LAMPORTS_PER_SOL as LAMPORTS_PER_SOL2,
2155
+ PublicKey as PublicKey8
2156
+ } from "@solana/web3.js";
2157
+ import * as anchor from "@coral-xyz/anchor";
2158
+ import { Program, AnchorProvider, Wallet } from "@coral-xyz/anchor";
2159
+ import { createRequire } from "module";
2160
+ import { fileURLToPath } from "url";
2161
+ import { dirname, join as join3 } from "path";
2162
+ import { existsSync } from "fs";
2163
+ var _require = createRequire(import.meta.url);
2164
+ var BN254_FIELD = 21888242871839275222246405745257275088696311157297823662689037894645226208583n;
2165
+ var __dirname = dirname(fileURLToPath(import.meta.url));
2166
+ function findZkFile(name) {
2167
+ const srcPath = join3(__dirname, "cli/zk", name);
2168
+ if (existsSync(srcPath)) return srcPath;
2169
+ const distPath = join3(__dirname, "zk", name);
2170
+ if (existsSync(distPath)) return distPath;
2171
+ return srcPath;
2172
+ }
2173
+ var DEFAULT_CIRCUIT_WASM = findZkFile("answer_proof.wasm");
2174
+ var DEFAULT_ZKEY = findZkFile("answer_proof_final.zkey");
2175
+ function toBigEndian32(v) {
2176
+ return Buffer.from(v.toString(16).padStart(64, "0"), "hex");
2177
+ }
2178
+ function answerToField(answer) {
2179
+ return BigInt("0x" + Buffer.from(answer, "utf-8").toString("hex")) % BN254_FIELD;
2180
+ }
2181
+ function hashBytesToFieldStr(hashBytes) {
2182
+ return BigInt("0x" + Buffer.from(hashBytes).toString("hex")).toString();
2183
+ }
2184
+ function pubkeyToCircuitInputs(pubkey) {
2185
+ const bytes = pubkey.toBuffer();
2186
+ return {
2187
+ lo: BigInt("0x" + bytes.subarray(16, 32).toString("hex")).toString(),
2188
+ hi: BigInt("0x" + bytes.subarray(0, 16).toString("hex")).toString()
2189
+ };
2190
+ }
2191
+ function proofToSolana(proof) {
2192
+ const negY = (y) => toBigEndian32(BN254_FIELD - BigInt(y));
2193
+ const be = (s) => toBigEndian32(BigInt(s));
2194
+ return {
2195
+ proofA: Array.from(
2196
+ Buffer.concat([be(proof.pi_a[0]), negY(proof.pi_a[1])])
2197
+ ),
2198
+ proofB: Array.from(
2199
+ Buffer.concat([
2200
+ be(proof.pi_b[0][1]),
2201
+ be(proof.pi_b[0][0]),
2202
+ be(proof.pi_b[1][1]),
2203
+ be(proof.pi_b[1][0])
2204
+ ])
2205
+ ),
2206
+ proofC: Array.from(
2207
+ Buffer.concat([be(proof.pi_c[0]), be(proof.pi_c[1])])
2208
+ )
2209
+ };
2210
+ }
2211
+ function proofToHex(proof) {
2212
+ const negY = (y) => toBigEndian32(BN254_FIELD - BigInt(y));
2213
+ const be = (s) => toBigEndian32(BigInt(s));
2214
+ return {
2215
+ proofA: Buffer.concat([be(proof.pi_a[0]), negY(proof.pi_a[1])]).toString("hex"),
2216
+ proofB: Buffer.concat([
2217
+ be(proof.pi_b[0][1]),
2218
+ be(proof.pi_b[0][0]),
2219
+ be(proof.pi_b[1][1]),
2220
+ be(proof.pi_b[1][0])
2221
+ ]).toString("hex"),
2222
+ proofC: Buffer.concat([be(proof.pi_c[0]), be(proof.pi_c[1])]).toString("hex")
2223
+ };
2224
+ }
2225
+ async function silentProve(snarkjs, input, wasmPath, zkeyPath) {
2226
+ const savedLog = console.log;
2227
+ const savedError = console.error;
2228
+ console.log = () => {
2229
+ };
2230
+ console.error = () => {
2231
+ };
2232
+ try {
2233
+ return await snarkjs.groth16.fullProve(input, wasmPath, zkeyPath, null, null, { singleThread: true });
2234
+ } finally {
2235
+ console.log = savedLog;
2236
+ console.error = savedError;
2237
+ }
2238
+ }
2239
+ function createProgram(connection, wallet) {
2240
+ const idlPath = existsSync(join3(__dirname, "cli/quest/nara_quest.json")) ? "./cli/quest/nara_quest.json" : "./quest/nara_quest.json";
2241
+ const idl = _require(idlPath);
2242
+ const provider = new AnchorProvider(
2243
+ connection,
2244
+ new Wallet(wallet),
2245
+ { commitment: "confirmed" }
2246
+ );
2247
+ anchor.setProvider(provider);
2248
+ return new Program(idl, provider);
2249
+ }
2250
+ function getPoolPda(programId) {
2251
+ const [pda] = PublicKey8.findProgramAddressSync(
2252
+ [Buffer.from("pool")],
2253
+ programId
2254
+ );
2255
+ return pda;
2256
+ }
2257
+ function getWinnerRecordPda(programId, user) {
2258
+ const [pda] = PublicKey8.findProgramAddressSync(
2259
+ [Buffer.from("winner"), user.toBuffer()],
2260
+ programId
2261
+ );
2262
+ return pda;
2263
+ }
2264
+ async function getQuestInfo(connection, wallet) {
2265
+ const kp = wallet ?? Keypair7.generate();
2266
+ const program2 = createProgram(connection, kp);
2267
+ const poolPda = getPoolPda(program2.programId);
2268
+ const pool = await program2.account.pool.fetch(poolPda);
2269
+ const now = Math.floor(Date.now() / 1e3);
2270
+ const deadline = pool.deadline.toNumber();
2271
+ const secsLeft = deadline - now;
2272
+ return {
2273
+ active: pool.isActive,
2274
+ round: pool.round.toString(),
2275
+ questionId: pool.questionId.toString(),
2276
+ question: pool.question,
2277
+ answerHash: Array.from(pool.answerHash),
2278
+ rewardPerWinner: pool.rewardPerWinner.toNumber() / LAMPORTS_PER_SOL2,
2279
+ totalReward: pool.rewardAmount.toNumber() / LAMPORTS_PER_SOL2,
2280
+ rewardCount: pool.rewardCount,
2281
+ winnerCount: pool.winnerCount,
2282
+ remainingSlots: Math.max(0, pool.rewardCount - pool.winnerCount),
2283
+ deadline,
2284
+ timeRemaining: secsLeft,
2285
+ expired: secsLeft <= 0
2286
+ };
2287
+ }
2288
+ async function hasAnswered(connection, wallet) {
2289
+ const program2 = createProgram(connection, wallet);
2290
+ const quest = await getQuestInfo(connection, wallet);
2291
+ const winnerPda = getWinnerRecordPda(program2.programId, wallet.publicKey);
2292
+ try {
2293
+ const wr = await program2.account.winnerRecord.fetch(winnerPda);
2294
+ return wr.round.toString() === quest.round;
2295
+ } catch {
2296
+ return false;
2297
+ }
2298
+ }
2299
+ async function generateProof(answer, answerHash, userPubkey, options) {
2300
+ const wasmPath = options?.circuitWasmPath ?? process.env.QUEST_CIRCUIT_WASM ?? DEFAULT_CIRCUIT_WASM;
2301
+ const zkeyPath = options?.zkeyPath ?? process.env.QUEST_ZKEY ?? DEFAULT_ZKEY;
2302
+ const snarkjs = await import("snarkjs");
2303
+ const answerHashFieldStr = hashBytesToFieldStr(answerHash);
2304
+ const { lo, hi } = pubkeyToCircuitInputs(userPubkey);
2305
+ const result = await silentProve(
2306
+ snarkjs,
2307
+ {
2308
+ answer: answerToField(answer).toString(),
2309
+ answer_hash: answerHashFieldStr,
2310
+ pubkey_lo: lo,
2311
+ pubkey_hi: hi
2312
+ },
2313
+ wasmPath,
2314
+ zkeyPath
2315
+ );
2316
+ return {
2317
+ solana: proofToSolana(result.proof),
2318
+ hex: proofToHex(result.proof)
2319
+ };
2320
+ }
2321
+ async function submitAnswer(connection, wallet, proof) {
2322
+ const program2 = createProgram(connection, wallet);
2323
+ const signature = await program2.methods.submitAnswer(proof.proofA, proof.proofB, proof.proofC).accounts({ user: wallet.publicKey, payer: wallet.publicKey }).signers([wallet]).rpc({ skipPreflight: true });
2324
+ return { signature };
2325
+ }
2326
+ async function submitAnswerViaRelay(relayUrl, userPubkey, proof) {
2327
+ const base = relayUrl.replace(/\/+$/, "");
2328
+ const res = await fetch(`${base}/submit-answer`, {
2329
+ method: "POST",
2330
+ headers: { "Content-Type": "application/json" },
2331
+ body: JSON.stringify({
2332
+ user: userPubkey.toBase58(),
2333
+ proofA: proof.proofA,
2334
+ proofB: proof.proofB,
2335
+ proofC: proof.proofC
2336
+ })
2337
+ });
2338
+ const data = await res.json();
2339
+ if (!res.ok) {
2340
+ throw new Error(`Relay submission failed: ${data.error ?? `HTTP ${res.status}`}`);
2341
+ }
2342
+ return { txHash: data.txHash };
2343
+ }
2344
+ async function parseQuestReward(connection, txSignature, retries = 10) {
2345
+ await new Promise((r) => setTimeout(r, 2e3));
2346
+ let txInfo;
2347
+ for (let i = 0; i < retries; i++) {
2348
+ try {
2349
+ txInfo = await connection.getTransaction(txSignature, {
2350
+ commitment: "confirmed",
2351
+ maxSupportedTransactionVersion: 0
2352
+ });
2353
+ if (txInfo) break;
2354
+ } catch {
2355
+ }
2356
+ await new Promise((r) => setTimeout(r, 1e3));
2357
+ }
2358
+ if (!txInfo) {
2359
+ throw new Error("Failed to fetch transaction details");
2360
+ }
2361
+ let rewardLamports = 0;
2362
+ let winner = "";
2363
+ const logs = txInfo.meta?.logMessages ?? [];
2364
+ for (const log of logs) {
2365
+ const m = log.match(/reward (\d+) lamports \(winner (\d+\/\d+)\)/);
2366
+ if (m) {
2367
+ rewardLamports = parseInt(m[1]);
2368
+ winner = m[2];
2369
+ break;
2370
+ }
2371
+ }
2372
+ return {
2373
+ rewarded: rewardLamports > 0,
2374
+ rewardLamports,
2375
+ rewardNso: rewardLamports / LAMPORTS_PER_SOL2,
2376
+ winner
2377
+ };
2378
+ }
2379
+
2380
+ // src/cli/commands/quest.ts
2381
+ var QUEST_ERRORS = {
2382
+ 6e3: "unauthorized",
2383
+ 6001: "poolNotActive",
2384
+ 6002: "deadlineExpired",
2385
+ 6003: "invalidProof",
2386
+ 6004: "invalidDeadline",
2387
+ 6005: "insufficientReward",
2388
+ 6006: "insufficientPoolBalance",
2389
+ 6007: "questionTooLong",
2390
+ 6008: "alreadyAnswered"
2391
+ };
2392
+ function anchorErrorCode(err) {
2393
+ const code = err?.error?.errorCode?.code;
2394
+ if (code) return code;
2395
+ const raw = err?.message ?? JSON.stringify(err) ?? "";
2396
+ const m = raw.match(/"Custom":(\d+)/);
2397
+ if (m) return QUEST_ERRORS[parseInt(m[1])] ?? "";
2398
+ return "";
2399
+ }
2400
+ function formatTimeRemaining(seconds) {
2401
+ if (seconds <= 0) return "expired";
2402
+ const m = Math.floor(seconds / 60);
2403
+ const s = seconds % 60;
2404
+ if (m > 0) return `${m}m ${s}s`;
2405
+ return `${s}s`;
2406
+ }
2407
+ async function handleQuestGet(options) {
2408
+ const rpcUrl = getRpcUrl(options.rpcUrl);
2409
+ const connection = new Connection3(rpcUrl, "confirmed");
2410
+ let wallet;
2411
+ try {
2412
+ wallet = await loadWallet(options.wallet);
2413
+ } catch {
2414
+ wallet = Keypair8.generate();
2415
+ }
2416
+ let quest;
2417
+ try {
2418
+ quest = await getQuestInfo(connection, wallet);
2419
+ } catch (err) {
2420
+ printError(`Failed to fetch quest info: ${err.message}`);
2421
+ process.exit(1);
2422
+ }
2423
+ if (!quest.active) {
2424
+ printWarning("No active quest at the moment");
2425
+ if (options.json) {
2426
+ formatOutput({ active: false }, true);
2427
+ }
2428
+ return;
2429
+ }
2430
+ const data = {
2431
+ round: quest.round,
2432
+ questionId: quest.questionId,
2433
+ question: quest.question,
2434
+ rewardPerWinner: `${quest.rewardPerWinner} NSO`,
2435
+ totalReward: `${quest.totalReward} NSO`,
2436
+ rewardSlots: `${quest.winnerCount}/${quest.rewardCount}`,
2437
+ remainingRewardSlots: quest.remainingSlots,
2438
+ deadline: new Date(quest.deadline * 1e3).toLocaleString(),
2439
+ timeRemaining: formatTimeRemaining(quest.timeRemaining),
2440
+ expired: quest.expired
2441
+ };
2442
+ if (options.json) {
2443
+ formatOutput(data, true);
2444
+ } else {
2445
+ console.log("");
2446
+ console.log(` Question: ${quest.question}`);
2447
+ console.log(` Round: #${quest.round}`);
2448
+ console.log(` Reward per winner: ${quest.rewardPerWinner} NSO`);
2449
+ console.log(` Total reward: ${quest.totalReward} NSO`);
2450
+ console.log(
2451
+ ` Reward slots: ${quest.winnerCount}/${quest.rewardCount} (${quest.remainingSlots} remaining)`
2452
+ );
2453
+ console.log(` Deadline: ${new Date(quest.deadline * 1e3).toLocaleString()}`);
2454
+ if (quest.timeRemaining > 0) {
2455
+ console.log(` Time remaining: ${formatTimeRemaining(quest.timeRemaining)}`);
2456
+ } else {
2457
+ printWarning("Quest has expired");
2458
+ }
2459
+ console.log("");
2460
+ }
2461
+ }
2462
+ async function handleQuestAnswer(answer, options) {
2463
+ const rpcUrl = getRpcUrl(options.rpcUrl);
2464
+ const connection = new Connection3(rpcUrl, "confirmed");
2465
+ const wallet = await loadWallet(options.wallet);
2466
+ let quest;
2467
+ try {
2468
+ quest = await getQuestInfo(connection, wallet);
2469
+ } catch (err) {
2470
+ printError(`Failed to fetch quest info: ${err.message}`);
2471
+ process.exit(1);
2472
+ }
2473
+ if (!quest.active) {
2474
+ printError("No active quest at the moment");
2475
+ process.exit(1);
2476
+ }
2477
+ if (quest.expired) {
2478
+ printError("Quest has expired");
2479
+ process.exit(1);
2480
+ }
2481
+ const alreadyAnswered = await hasAnswered(connection, wallet);
2482
+ if (alreadyAnswered) {
2483
+ printWarning("You have already answered this round");
2484
+ process.exit(0);
2485
+ }
2486
+ printInfo("Generating ZK proof...");
2487
+ let proof;
2488
+ try {
2489
+ proof = await generateProof(answer, quest.answerHash, wallet.publicKey);
2490
+ } catch (err) {
2491
+ if (err.message?.includes("Assert Failed")) {
2492
+ printError("Wrong answer");
2493
+ } else {
2494
+ printError(`ZK proof generation failed: ${err.message}`);
2495
+ }
2496
+ process.exit(1);
2497
+ }
2498
+ const nowAfterProof = Math.floor(Date.now() / 1e3);
2499
+ if (nowAfterProof >= quest.deadline) {
2500
+ printError("Quest expired during proof generation");
2501
+ process.exit(1);
2502
+ }
2503
+ if (options.relay) {
2504
+ printInfo("Submitting answer via relay...");
2505
+ try {
2506
+ const relayResult = await submitAnswerViaRelay(
2507
+ options.relay,
2508
+ wallet.publicKey,
2509
+ proof.hex
2510
+ );
2511
+ printSuccess("Answer submitted via relay!");
2512
+ console.log(` Transaction: ${relayResult.txHash}`);
2513
+ await handleReward(connection, relayResult.txHash, options);
2514
+ } catch (err) {
2515
+ printError(`Relay submission failed: ${err.message}`);
2516
+ process.exit(1);
2517
+ }
2518
+ } else {
2519
+ printInfo("Submitting answer...");
2520
+ try {
2521
+ const result = await submitAnswer(connection, wallet, proof.solana);
2522
+ printSuccess("Answer submitted!");
2523
+ console.log(` Transaction: ${result.signature}`);
2524
+ await handleReward(connection, result.signature, options);
2525
+ } catch (err) {
2526
+ handleSubmitError(err);
2527
+ }
2528
+ }
2529
+ }
2530
+ async function handleReward(connection, txSignature, options) {
2531
+ printInfo("Fetching transaction details...");
2532
+ let reward;
2533
+ try {
2534
+ reward = await parseQuestReward(connection, txSignature);
2535
+ } catch {
2536
+ printWarning("Failed to fetch transaction details. Please check manually later.");
2537
+ console.log(
2538
+ ` https://solscan.io/tx/${txSignature}?cluster=devnet`
2539
+ );
2540
+ return;
2541
+ }
2542
+ if (reward.rewarded) {
2543
+ printSuccess(`Congratulations! Reward received: ${reward.rewardNso} NSO (winner ${reward.winner})`);
2544
+ if (options.json) {
2545
+ formatOutput(
2546
+ {
2547
+ signature: txSignature,
2548
+ rewarded: true,
2549
+ rewardLamports: reward.rewardLamports,
2550
+ rewardNso: reward.rewardNso,
2551
+ winner: reward.winner
2552
+ },
2553
+ true
2554
+ );
2555
+ }
2556
+ } else {
2557
+ printWarning("Correct answer, but no reward \u2014 all reward slots have been claimed");
2558
+ if (options.json) {
2559
+ formatOutput(
2560
+ { signature: txSignature, rewarded: false, rewardLamports: 0 },
2561
+ true
2562
+ );
2563
+ }
2564
+ }
2565
+ }
2566
+ function handleSubmitError(err) {
2567
+ const errCode = anchorErrorCode(err);
2568
+ switch (errCode) {
2569
+ case "alreadyAnswered":
2570
+ printWarning("You have already answered this round");
2571
+ break;
2572
+ case "deadlineExpired":
2573
+ printError("Quest has expired");
2574
+ break;
2575
+ case "invalidProof":
2576
+ printError("Wrong answer (ZK proof verification failed)");
2577
+ break;
2578
+ case "poolNotActive":
2579
+ printError("No active quest at the moment");
2580
+ break;
2581
+ default:
2582
+ printError(`Failed to submit answer: ${err.message ?? String(err)}`);
2583
+ if (err.logs) {
2584
+ console.log(" Logs:");
2585
+ err.logs.slice(-5).forEach((l) => console.log(` ${l}`));
2586
+ }
2587
+ }
2588
+ process.exit(1);
2589
+ }
2590
+ function registerQuestCommands(program2) {
2591
+ const quest = program2.command("quest").description("Quest commands");
2592
+ quest.command("get").description("Get current quest info").action(async (_opts, cmd) => {
2593
+ try {
2594
+ const globalOpts = cmd.optsWithGlobals();
2595
+ await handleQuestGet(globalOpts);
2596
+ } catch (error) {
2597
+ printError(error.message);
2598
+ process.exit(1);
2599
+ }
2600
+ });
2601
+ quest.command("answer <answer>").description("Submit an answer").option("--relay [url]", `Submit via relay service, gasless (default: ${DEFAULT_QUEST_RELAY_URL})`).action(async (answer, opts, cmd) => {
2602
+ try {
2603
+ const globalOpts = cmd.optsWithGlobals();
2604
+ const relayUrl = opts.relay === true ? DEFAULT_QUEST_RELAY_URL : opts.relay;
2605
+ await handleQuestAnswer(answer, { ...globalOpts, relay: relayUrl });
2606
+ } catch (error) {
2607
+ printError(error.message);
2608
+ process.exit(1);
2609
+ }
2610
+ });
2611
+ }
2612
+
2613
+ // src/cli/index.ts
2614
+ function registerCommands(program2) {
2615
+ registerConfigCommands(program2);
2616
+ registerPoolCommands(program2);
2617
+ registerSwapCommands(program2);
2618
+ registerMigrateCommands(program2);
2619
+ registerWalletCommands(program2);
2620
+ registerQuestCommands(program2);
2621
+ }
2622
+
2623
+ // bin/nara-cli.ts
2624
+ var program = new Command8();
2625
+ program.name("nara-cli").description("CLI for the Nara chain (Solana-compatible)").version("0.1.0");
2626
+ program.option("-r, --rpc-url <url>", "RPC endpoint URL").option("-w, --wallet <path>", "Path to wallet keypair JSON file").option("-j, --json", "Output in JSON format");
2627
+ registerCommands(program);
2628
+ program.parse(process.argv);
2629
+ if (!process.argv.slice(2).length) {
2630
+ program.outputHelp();
2631
+ }