naracli 1.0.86 → 1.0.87

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.
@@ -5,6 +5,7 @@
5
5
  import { Command } from "commander";
6
6
  import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
7
7
  import BN from "bn.js";
8
+ import DLMM from "@meteora-ag/dlmm";
8
9
  import { loadWallet, getRpcUrl } from "../utils/wallet";
9
10
  import {
10
11
  printError,
@@ -28,6 +29,37 @@ function identifyPoolType(owner: string): PoolType | null {
28
29
  return null;
29
30
  }
30
31
 
32
+ /** Get current point (slot or timestamp) without relying on RPC getBlockTime which can fail on recent slots. */
33
+ async function getCurrentPointSafe(connection: Connection, activationType: number): Promise<BN> {
34
+ // activationType 0 = slot, 1 = timestamp
35
+ if (activationType === 1) {
36
+ return new BN(Math.floor(Date.now() / 1000));
37
+ }
38
+ const slot = await connection.getSlot();
39
+ return new BN(slot);
40
+ }
41
+
42
+ const KNOWN_TOKENS: Record<string, string> = {
43
+ "So11111111111111111111111111111111111111112": "NARA",
44
+ "8P7UGWjq86N3WUmwEgKeGHJZLcoMJqr5jnRUmeBN7YwR": "USDC",
45
+ "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB": "USDT",
46
+ "7fKh7DqPZmsYPHdGvt9Qw2rZkSEGp9F5dBa3XuuuhavU": "SOL",
47
+ "AqJX47z8UT6k6gFpJjzvcAAP4NJkfykW8U8za1evry7J": "POINT",
48
+ };
49
+
50
+ function tokenSymbol(mint: string): string {
51
+ return KNOWN_TOKENS[mint] ?? mint.slice(0, 4) + "..";
52
+ }
53
+
54
+ /** Resolve token symbol shortcut (e.g. "NARA") to mint address, or pass through if already a pubkey. */
55
+ function resolveTokenMint(input: string): string {
56
+ const upper = input.toUpperCase();
57
+ for (const [mint, symbol] of Object.entries(KNOWN_TOKENS)) {
58
+ if (symbol === upper) return mint;
59
+ }
60
+ return input;
61
+ }
62
+
31
63
  async function getMintDecimals(connection: Connection, mint: PublicKey): Promise<number> {
32
64
  try {
33
65
  const info = await connection.getParsedAccountInfo(mint);
@@ -37,6 +69,480 @@ async function getMintDecimals(connection: Connection, mint: PublicKey): Promise
37
69
  return 9;
38
70
  }
39
71
 
72
+ // ═══════════════════════════════════════════════════════════════════
73
+ // POOLS (by token)
74
+ // ═══════════════════════════════════════════════════════════════════
75
+
76
+ // Account offsets for mint fields (8-byte discriminator included)
77
+ const CPAMM_TOKENA_OFFSET = 168;
78
+ const CPAMM_TOKENB_OFFSET = 200;
79
+ const DLMM_TOKENX_OFFSET = 88;
80
+ const DLMM_TOKENY_OFFSET = 120;
81
+ const DLMM_LBPAIR_SIZE = 904;
82
+ const DBC_BASEMINT_OFFSET = 136;
83
+
84
+ async function findProgramAccountsByMint(
85
+ connection: Connection, programId: string, mint: PublicKey, offsets: number[]
86
+ ): Promise<PublicKey[]> {
87
+ const pubkeys = new Set<string>();
88
+ for (const offset of offsets) {
89
+ try {
90
+ const accounts = await connection.getProgramAccounts(new PublicKey(programId), {
91
+ filters: [{ memcmp: { offset, bytes: mint.toBase58() } }],
92
+ dataSlice: { offset: 0, length: 0 },
93
+ });
94
+ for (const a of accounts) pubkeys.add(a.pubkey.toBase58());
95
+ } catch {}
96
+ }
97
+ return Array.from(pubkeys).map(s => new PublicKey(s));
98
+ }
99
+
100
+ async function findDlmmPoolsByMint(connection: Connection, mint: PublicKey): Promise<PublicKey[]> {
101
+ const pubkeys = new Set<string>();
102
+ for (const offset of [DLMM_TOKENX_OFFSET, DLMM_TOKENY_OFFSET]) {
103
+ try {
104
+ const accounts = await connection.getProgramAccounts(new PublicKey(DLMM_PROGRAM_ID), {
105
+ filters: [
106
+ { dataSize: DLMM_LBPAIR_SIZE },
107
+ { memcmp: { offset, bytes: mint.toBase58() } },
108
+ ],
109
+ dataSlice: { offset: 0, length: 0 },
110
+ });
111
+ for (const a of accounts) pubkeys.add(a.pubkey.toBase58());
112
+ } catch {}
113
+ }
114
+ return Array.from(pubkeys).map(s => new PublicKey(s));
115
+ }
116
+
117
+ async function handlePools(token: string, options: GlobalOptions) {
118
+ const rpcUrl = getRpcUrl(options.rpcUrl);
119
+ const connection = new Connection(rpcUrl, "confirmed");
120
+ const tokenMint = new PublicKey(token);
121
+ const tokenDecimals = await getMintDecimals(connection, tokenMint);
122
+
123
+ if (!options.json) printInfo(`Searching pools containing ${token}...`);
124
+
125
+ // Find pool addresses for each pool type via memcmp filters
126
+ const [cpammAddrs, dlmmAddrs, dbcAddrs] = await Promise.all([
127
+ findProgramAccountsByMint(connection, CPAMM_PROGRAM_ID, tokenMint, [CPAMM_TOKENA_OFFSET, CPAMM_TOKENB_OFFSET]),
128
+ findDlmmPoolsByMint(connection, tokenMint),
129
+ findProgramAccountsByMint(connection, DBC_PROGRAM_ID, tokenMint, [DBC_BASEMINT_OFFSET]),
130
+ ]);
131
+
132
+ const results: any[] = [];
133
+
134
+ // Decode CPAMM pools
135
+ if (cpammAddrs.length > 0) {
136
+ try {
137
+ const { CpAmm, getReservesAmountForConcentratedLiquidity } = await import("@meteora-ag/cp-amm-sdk");
138
+ const cpAmm = new CpAmm(connection);
139
+ for (const addr of cpammAddrs) {
140
+ try {
141
+ const state = await cpAmm.fetchPoolState(addr);
142
+ const decA = await getMintDecimals(connection, state.tokenAMint);
143
+ const decB = await getMintDecimals(connection, state.tokenBMint);
144
+ const [resA, resB] = getReservesAmountForConcentratedLiquidity(
145
+ state.sqrtPrice, state.sqrtMinPrice, state.sqrtMaxPrice, state.liquidity
146
+ );
147
+ const amountA = Number(resA.toString()) / 10 ** decA;
148
+ const amountB = Number(resB.toString()) / 10 ** decB;
149
+ // price = (sqrtPrice / 2^64)^2 * 10^(decA - decB) = B per A
150
+ const sqrtNum = Number(state.sqrtPrice.toString()) / 2 ** 64;
151
+ const priceBA = sqrtNum * sqrtNum * 10 ** (decA - decB);
152
+
153
+ results.push({
154
+ type: "DAMM v2",
155
+ pool: addr.toBase58(),
156
+ tokenA: state.tokenAMint.toBase58(),
157
+ tokenB: state.tokenBMint.toBase58(),
158
+ amountA, amountB,
159
+ price: priceBA,
160
+ });
161
+ } catch {}
162
+ }
163
+ } catch {}
164
+ }
165
+
166
+ // Decode DLMM pools
167
+ if (dlmmAddrs.length > 0) {
168
+ try {
169
+ const { default: DLMM } = await import("@meteora-ag/dlmm");
170
+ const dlmms = await DLMM.createMultiple(connection, dlmmAddrs);
171
+ for (const dlmm of dlmms) {
172
+ try {
173
+ const activeBin = await dlmm.getActiveBin();
174
+ const reserves = await Promise.all([
175
+ connection.getTokenAccountBalance(dlmm.tokenX.reserve).catch(() => null),
176
+ connection.getTokenAccountBalance(dlmm.tokenY.reserve).catch(() => null),
177
+ ]);
178
+ const amountX = reserves[0] ? Number(reserves[0].value.uiAmount ?? 0) : 0;
179
+ const amountY = reserves[1] ? Number(reserves[1].value.uiAmount ?? 0) : 0;
180
+ results.push({
181
+ type: "DLMM",
182
+ pool: dlmm.pubkey.toBase58(),
183
+ tokenA: dlmm.tokenX.publicKey.toBase58(),
184
+ tokenB: dlmm.tokenY.publicKey.toBase58(),
185
+ amountA: amountX,
186
+ amountB: amountY,
187
+ price: Number(activeBin.price),
188
+ });
189
+ } catch {}
190
+ }
191
+ } catch {}
192
+ }
193
+
194
+ // Decode DBC pools
195
+ if (dbcAddrs.length > 0) {
196
+ try {
197
+ const { DynamicBondingCurveClient } = await import("@meteora-ag/dynamic-bonding-curve-sdk");
198
+ const client = DynamicBondingCurveClient.create(connection);
199
+ for (const addr of dbcAddrs) {
200
+ try {
201
+ const pool = await client.pool.getPool(addr);
202
+ const config = await client.pool.getPoolConfig(pool.config);
203
+ const decBase = await getMintDecimals(connection, pool.baseMint);
204
+ const decQuote = await getMintDecimals(connection, pool.quoteMint);
205
+
206
+ const baseBal = await connection.getTokenAccountBalance(pool.baseVault).catch(() => null);
207
+ const quoteBal = await connection.getTokenAccountBalance(pool.quoteVault).catch(() => null);
208
+ const amountBase = baseBal ? Number(baseBal.value.uiAmount ?? 0) : 0;
209
+ const amountQuote = quoteBal ? Number(quoteBal.value.uiAmount ?? 0) : 0;
210
+
211
+ // sqrtPrice Q64 → price = (sqrtPrice / 2^64)^2 * 10^(decBase - decQuote)
212
+ const sqrtNum = Number(pool.sqrtPrice?.toString() ?? "0") / 2 ** 64;
213
+ const price = sqrtNum * sqrtNum * 10 ** (decBase - decQuote);
214
+
215
+ results.push({
216
+ type: "DBC",
217
+ pool: addr.toBase58(),
218
+ tokenA: pool.baseMint.toBase58(),
219
+ tokenB: pool.quoteMint.toBase58(),
220
+ amountA: amountBase,
221
+ amountB: amountQuote,
222
+ price,
223
+ });
224
+ } catch {}
225
+ }
226
+ } catch {}
227
+ }
228
+
229
+ if (options.json) {
230
+ formatOutput(results, true);
231
+ } else {
232
+ if (results.length === 0) {
233
+ printInfo("No pools found for this token.");
234
+ return;
235
+ }
236
+ console.log("");
237
+ for (const r of results) {
238
+ const symA = tokenSymbol(r.tokenA);
239
+ const symB = tokenSymbol(r.tokenB);
240
+ console.log(` [${r.type}] ${symA}/${symB} ${r.pool}`);
241
+ console.log(` ${symA}: ${r.tokenA}`);
242
+ console.log(` ${symB}: ${r.tokenB}`);
243
+ console.log(` Reserves: ${r.amountA.toFixed(4)} ${symA} / ${r.amountB.toFixed(4)} ${symB}`);
244
+ console.log(` Price: 1 ${symA} = ${r.price.toFixed(6)} ${symB}`);
245
+ console.log("");
246
+ }
247
+ console.log(` Total: ${results.length} pool(s)`);
248
+ console.log("");
249
+ }
250
+ }
251
+
252
+ // ═══════════════════════════════════════════════════════════════════
253
+ // SMART ROUTER (via https://smart-router.nara.build/)
254
+ // ═══════════════════════════════════════════════════════════════════
255
+
256
+ const SMART_ROUTER_URL = process.env.SMART_ROUTER_URL || "https://smart-router.nara.build";
257
+
258
+ async function handleSmartQuote(
259
+ inputToken: string, outputToken: string, amount: string,
260
+ options: GlobalOptions
261
+ ) {
262
+ const rpcUrl = getRpcUrl(options.rpcUrl);
263
+ const connection = new Connection(rpcUrl, "confirmed");
264
+ const inputMint = new PublicKey(resolveTokenMint(inputToken));
265
+ const outputMint = new PublicKey(resolveTokenMint(outputToken));
266
+ const inputDecimals = await getMintDecimals(connection, inputMint);
267
+ const outputDecimals = await getMintDecimals(connection, outputMint);
268
+ const rawAmount = BigInt(Math.floor(parseFloat(amount) * 10 ** inputDecimals));
269
+
270
+ if (!options.json) printInfo("Fetching quote from smart router...");
271
+ const url = `${SMART_ROUTER_URL}/quote?input_mint=${inputMint.toBase58()}&output_mint=${outputMint.toBase58()}&amount_in=${rawAmount}`;
272
+ const res = await fetch(url);
273
+ const data = await res.json() as any;
274
+ if (!res.ok || data.error) {
275
+ printError(`Smart router: ${data.error ?? res.status}`);
276
+ process.exit(1);
277
+ }
278
+
279
+ const amountInNum = Number(data.amount_in) / 10 ** inputDecimals;
280
+ const amountOutNum = Number(data.amount_out) / 10 ** outputDecimals;
281
+ const minOutNum = Number(data.min_amount_out) / 10 ** outputDecimals;
282
+ const price = amountInNum > 0 ? amountOutNum / amountInNum : 0;
283
+
284
+ if (options.json) {
285
+ formatOutput(data, true);
286
+ } else {
287
+ const symIn = tokenSymbol(inputMint.toBase58());
288
+ const symOut = tokenSymbol(outputMint.toBase58());
289
+ console.log("");
290
+ console.log(` Input: ${amountInNum} ${symIn}`);
291
+ console.log(` Output: ${amountOutNum.toFixed(outputDecimals)} ${symOut}`);
292
+ console.log(` Min output: ${minOutNum.toFixed(outputDecimals)} ${symOut}`);
293
+ console.log(` Price: 1 ${symIn} = ${price.toFixed(6)} ${symOut}`);
294
+ if (Array.isArray(data.route_legs) && data.route_legs.length > 0) {
295
+ console.log(` Route:`);
296
+ for (const leg of data.route_legs) {
297
+ for (const hop of leg.path ?? []) {
298
+ const symHopIn = tokenSymbol(hop.token_in);
299
+ const symHopOut = tokenSymbol(hop.token_out);
300
+ console.log(` ${symHopIn} → ${symHopOut} [${hop.pool_type}] ${hop.pool_id}`);
301
+ }
302
+ }
303
+ }
304
+ console.log("");
305
+ }
306
+ }
307
+
308
+ async function handleSmartSwap(
309
+ inputToken: string, outputToken: string, amount: string,
310
+ options: GlobalOptions & { slippage?: string }
311
+ ) {
312
+ const rpcUrl = getRpcUrl(options.rpcUrl);
313
+ const connection = new Connection(rpcUrl, "confirmed");
314
+ const wallet = await loadWallet(options.wallet);
315
+ const inputMint = new PublicKey(resolveTokenMint(inputToken));
316
+ const outputMint = new PublicKey(resolveTokenMint(outputToken));
317
+ const inputDecimals = await getMintDecimals(connection, inputMint);
318
+ const outputDecimals = await getMintDecimals(connection, outputMint);
319
+ const rawAmount = BigInt(Math.floor(parseFloat(amount) * 10 ** inputDecimals));
320
+ const slippageBps = options.slippage ? Math.round(parseFloat(options.slippage) * 100) : 100;
321
+
322
+ const symIn = tokenSymbol(inputMint.toBase58());
323
+ const symOut = tokenSymbol(outputMint.toBase58());
324
+
325
+ if (!options.json) printInfo(`Creating order: ${amount} ${symIn} → ${symOut} (slippage ${slippageBps / 100}%)...`);
326
+ const orderRes = await fetch(`${SMART_ROUTER_URL}/order`, {
327
+ method: "POST",
328
+ headers: { "Content-Type": "application/json" },
329
+ body: JSON.stringify({
330
+ input_mint: inputMint.toBase58(),
331
+ output_mint: outputMint.toBase58(),
332
+ amount_in: Number(rawAmount),
333
+ slippage_bps: slippageBps,
334
+ user_pubkey: wallet.publicKey.toBase58(),
335
+ }),
336
+ });
337
+ const order = await orderRes.json() as any;
338
+ if (!orderRes.ok || order.error) {
339
+ printError(`Order failed: ${order.error ?? orderRes.status}`);
340
+ process.exit(1);
341
+ }
342
+
343
+ if (!options.json) {
344
+ const amountOutNum = Number(order.amount_out) / 10 ** outputDecimals;
345
+ const minOutNum = Number(order.min_amount_out) / 10 ** outputDecimals;
346
+ console.log(` Expected out: ${amountOutNum.toFixed(outputDecimals)} ${symOut}`);
347
+ console.log(` Min out: ${minOutNum.toFixed(outputDecimals)} ${symOut}`);
348
+ console.log(` Order ID: ${order.order_id}`);
349
+ printInfo("Signing and submitting...");
350
+ }
351
+
352
+ // Sign tx (supports both versioned and legacy)
353
+ const { VersionedTransaction, Transaction } = await import("@solana/web3.js");
354
+ const txBuf = Buffer.from(order.unsigned_tx_base64, "base64");
355
+ let signedB64: string;
356
+ try {
357
+ const vtx = VersionedTransaction.deserialize(new Uint8Array(txBuf));
358
+ vtx.sign([wallet]);
359
+ signedB64 = Buffer.from(vtx.serialize()).toString("base64");
360
+ } catch {
361
+ const ltx = Transaction.from(txBuf);
362
+ ltx.sign(wallet);
363
+ signedB64 = ltx.serialize().toString("base64");
364
+ }
365
+
366
+ const execRes = await fetch(`${SMART_ROUTER_URL}/execute`, {
367
+ method: "POST",
368
+ headers: { "Content-Type": "application/json" },
369
+ body: JSON.stringify({ order_id: order.order_id, signed_tx_base64: signedB64 }),
370
+ });
371
+ const exec = await execRes.json() as any;
372
+ if (!execRes.ok || exec.error) {
373
+ printError(`Execute failed: ${exec.error ?? execRes.status}`);
374
+ process.exit(1);
375
+ }
376
+
377
+ if (options.json) {
378
+ formatOutput({ order, exec }, true);
379
+ } else {
380
+ printSuccess(`Swap ${exec.confirmed ? "confirmed" : "submitted"}!`);
381
+ console.log(` Transaction: ${exec.signature}`);
382
+ }
383
+ }
384
+
385
+ // ═══════════════════════════════════════════════════════════════════
386
+ // QUOTE
387
+ // ═══════════════════════════════════════════════════════════════════
388
+
389
+ async function handleQuote(
390
+ pool: string, inputToken: string, amount: string,
391
+ options: GlobalOptions & { slippage?: string }
392
+ ) {
393
+ const rpcUrl = getRpcUrl(options.rpcUrl);
394
+ const connection = new Connection(rpcUrl, "confirmed");
395
+ const poolAddress = new PublicKey(pool);
396
+ const inputMint = new PublicKey(resolveTokenMint(inputToken));
397
+ const slippageBps = options.slippage ? Math.round(parseFloat(options.slippage) * 100) : 100;
398
+
399
+ // Detect pool type
400
+ if (!options.json) printInfo("Detecting pool type...");
401
+ const accountInfo = await connection.getAccountInfo(poolAddress);
402
+ if (!accountInfo) { printError("Pool account not found"); process.exit(1); }
403
+
404
+ const poolType = identifyPoolType(accountInfo.owner.toBase58());
405
+ if (!poolType) {
406
+ printError(`Unrecognized pool. Owner: ${accountInfo.owner.toBase58()}. Supported: DAMM v2, DLMM, DBC`);
407
+ process.exit(1);
408
+ }
409
+ if (!options.json) printInfo(`Pool type: ${poolType.toUpperCase()}`);
410
+
411
+ const inputDecimals = await getMintDecimals(connection, inputMint);
412
+ const rawAmount = new BN(Math.floor(parseFloat(amount) * 10 ** inputDecimals).toString());
413
+
414
+ let outputMint: PublicKey;
415
+ let amountOut: BN;
416
+ let minOut: BN;
417
+ let fee: BN;
418
+ let outputDecimals = 9;
419
+
420
+ let feeBps: number | null = null;
421
+
422
+ if (poolType === "cpamm") {
423
+ const {
424
+ CpAmm, swapQuoteExactInput,
425
+ getBaseFeeHandlerFromPodAlignedData, feeNumeratorToBps,
426
+ } = await import("@meteora-ag/cp-amm-sdk");
427
+ const cpAmm = new CpAmm(connection);
428
+ const poolState = await cpAmm.fetchPoolState(poolAddress);
429
+
430
+ // Decode pool fee from poolFees.baseFee
431
+ try {
432
+ const rawData = (poolState.poolFees as any)?.baseFee?.baseFeeInfo?.data;
433
+ if (rawData) {
434
+ let bytes: number[];
435
+ if (Array.isArray(rawData)) bytes = rawData;
436
+ else if (rawData instanceof Uint8Array) bytes = Array.from(rawData);
437
+ else if ((rawData as any)?.data && Array.isArray((rawData as any).data)) bytes = (rawData as any).data;
438
+ else bytes = Array.from(Buffer.from(rawData));
439
+ const handler = getBaseFeeHandlerFromPodAlignedData(bytes);
440
+ feeBps = Number(feeNumeratorToBps(handler.getMinFeeNumerator()));
441
+ }
442
+ } catch {}
443
+ const aToB = inputMint.equals(poolState.tokenAMint);
444
+ if (!aToB && !inputMint.equals(poolState.tokenBMint)) {
445
+ printError(`Input token not in pool`); process.exit(1);
446
+ }
447
+ outputMint = aToB ? poolState.tokenBMint : poolState.tokenAMint;
448
+ outputDecimals = await getMintDecimals(connection, outputMint);
449
+
450
+ const currentPoint = await getCurrentPointSafe(connection, poolState.activationType);
451
+ const quote = swapQuoteExactInput(
452
+ poolState, currentPoint, rawAmount,
453
+ slippageBps, aToB, false,
454
+ inputDecimals, outputDecimals,
455
+ );
456
+ const q = quote as any;
457
+ amountOut = new BN(q.outputAmount?.toString() ?? "0");
458
+ minOut = q.minimumAmountOut ?? new BN(0);
459
+ // Sum LP/protocol/referral fees (all in input token lamports)
460
+ fee = new BN((q.claimingFee ?? "0").toString())
461
+ .add(new BN((q.protocolFee ?? "0").toString()))
462
+ .add(new BN((q.compoundingFee ?? "0").toString()))
463
+ .add(new BN((q.referralFee ?? "0").toString()));
464
+ } else if (poolType === "dlmm") {
465
+ const { default: DLMM } = await import("@meteora-ag/dlmm");
466
+ const dlmm = await DLMM.create(connection, poolAddress);
467
+ const swapForY = inputMint.equals(dlmm.tokenX.publicKey);
468
+ if (!swapForY && !inputMint.equals(dlmm.tokenY.publicKey)) {
469
+ printError(`Input token not in pool`); process.exit(1);
470
+ }
471
+ outputMint = swapForY ? dlmm.tokenY.publicKey : dlmm.tokenX.publicKey;
472
+ outputDecimals = await getMintDecimals(connection, outputMint);
473
+
474
+ const binArrays = await dlmm.getBinArrayForSwap(swapForY);
475
+ const quote = dlmm.swapQuote(rawAmount, swapForY, new BN(slippageBps), binArrays);
476
+ amountOut = quote.outAmount;
477
+ minOut = quote.minOutAmount;
478
+ fee = quote.fee;
479
+ // Compute DLMM fee bps from baseFactor + binStep + baseFeePowerFactor
480
+ try {
481
+ const params = (dlmm.lbPair as any).parameters;
482
+ const baseFactor = Number(params?.baseFactor ?? 0);
483
+ const binStep = Number((dlmm.lbPair as any).binStep ?? 0);
484
+ const pf = Number(params?.baseFeePowerFactor ?? 0);
485
+ if (baseFactor > 0 && binStep > 0) {
486
+ const fi: any = (DLMM as any).calculateFeeInfo(baseFactor, binStep, pf);
487
+ feeBps = Number(fi.baseFeeRatePercentage) * 100; // percent → bps
488
+ }
489
+ } catch {}
490
+ } else {
491
+ // DBC
492
+ const { DynamicBondingCurveClient } = await import("@meteora-ag/dynamic-bonding-curve-sdk");
493
+ const client = DynamicBondingCurveClient.create(connection);
494
+ const p = await client.pool.getPool(poolAddress);
495
+ const swapBaseForQuote = inputMint.equals(p.baseMint);
496
+ if (!swapBaseForQuote && !inputMint.equals(p.quoteMint)) {
497
+ printError(`Input token not in pool`); process.exit(1);
498
+ }
499
+ outputMint = swapBaseForQuote ? p.quoteMint : p.baseMint;
500
+ outputDecimals = await getMintDecimals(connection, outputMint);
501
+
502
+ const config = await client.pool.getPoolConfig(p.config);
503
+ const quote = client.pool.swapQuote({
504
+ virtualPool: p, config, swapBaseForQuote,
505
+ amountIn: rawAmount, slippageBps, hasReferral: false,
506
+ currentPoint: p.currentPoint,
507
+ });
508
+ amountOut = new BN((quote.outputAmount ?? quote.amountOut ?? "0").toString());
509
+ minOut = quote.minimumAmountOut ?? new BN(0);
510
+ fee = new BN((quote.fee ?? quote.totalFee ?? "0").toString());
511
+ }
512
+
513
+ const amountInNum = Number(rawAmount.toString()) / 10 ** inputDecimals;
514
+ const amountOutNum = Number(amountOut.toString()) / 10 ** outputDecimals;
515
+ const minOutNum = Number(minOut.toString()) / 10 ** outputDecimals;
516
+ // If feeBps known, compute fee from input; otherwise fallback to SDK-reported fee
517
+ const feeNum = feeBps !== null ? amountInNum * (feeBps / 10000) : Number(fee.toString()) / 10 ** inputDecimals;
518
+ const price = amountInNum > 0 ? amountOutNum / amountInNum : 0;
519
+
520
+ if (options.json) {
521
+ formatOutput({
522
+ poolType, inputMint: inputMint.toBase58(), outputMint: outputMint.toBase58(),
523
+ amountIn: amountInNum, amountOut: amountOutNum,
524
+ minOut: minOutNum, fee: feeNum, feeBps, price,
525
+ slippageBps,
526
+ }, true);
527
+ } else {
528
+ console.log("");
529
+ const symIn = tokenSymbol(inputMint.toBase58());
530
+ const symOut = tokenSymbol(outputMint.toBase58());
531
+ console.log(` Pool type: ${poolType.toUpperCase()}`);
532
+ console.log(` Input: ${amountInNum} ${symIn} (${inputMint.toBase58()})`);
533
+ console.log(` Output: ${amountOutNum.toFixed(outputDecimals)} ${symOut} (${outputMint.toBase58()})`);
534
+ console.log(` Min output: ${minOutNum.toFixed(outputDecimals)} ${symOut} (@ ${slippageBps / 100}% slippage)`);
535
+ if (feeBps !== null) {
536
+ console.log(` Fee: ${(feeBps / 100).toFixed(2)}%`);
537
+ } else {
538
+ const feeStr = feeNum.toFixed(inputDecimals).replace(/\.?0+$/, "");
539
+ console.log(` Fee: ${feeStr} ${symIn}`);
540
+ }
541
+ console.log(` Price: 1 ${symIn} = ${price.toFixed(6)} ${symOut}`);
542
+ console.log("");
543
+ }
544
+ }
545
+
40
546
  // ═══════════════════════════════════════════════════════════════════
41
547
  // SWAP
42
548
  // ═══════════════════════════════════════════════════════════════════
@@ -49,7 +555,7 @@ async function swapCpAmm(
49
555
  amountIn: BN,
50
556
  slippageBps: number,
51
557
  ) {
52
- const { CpAmm, SwapMode } = await import("@meteora-ag/cp-amm-sdk");
558
+ const { CpAmm, SwapMode, swapQuoteExactInput } = await import("@meteora-ag/cp-amm-sdk");
53
559
  const cpAmm = new CpAmm(connection);
54
560
  const poolState = await cpAmm.fetchPoolState(poolAddress);
55
561
 
@@ -60,26 +566,33 @@ async function swapCpAmm(
60
566
  throw new Error(`Input token ${inputMint.toBase58()} not in pool (A: ${tokenAMint.toBase58()}, B: ${tokenBMint.toBase58()})`);
61
567
  }
62
568
 
63
- const quote = cpAmm.swapQuoteExactInput(
64
- poolState, poolState.currentPoint, amountIn,
65
- slippageBps / 10000, aToB, false,
66
- poolState.tokenADecimals ?? 9, poolState.tokenBDecimals ?? 9,
569
+ const decA = await getMintDecimals(connection, tokenAMint);
570
+ const decB = await getMintDecimals(connection, tokenBMint);
571
+ const currentPoint = await getCurrentPointSafe(connection, poolState.activationType);
572
+ const quote = swapQuoteExactInput(
573
+ poolState, currentPoint, amountIn,
574
+ slippageBps / 10000, aToB, false, decA, decB,
67
575
  );
68
576
 
69
577
  const outputMint = aToB ? tokenBMint : tokenAMint;
70
- const minOut = quote.minimumAmountOut ?? new BN(0);
578
+ const minOut = (quote as any).minimumAmountOut ?? new BN(0);
579
+
580
+ // Get token program for each mint (SPL Token or Token-2022)
581
+ const [mintAInfo, mintBInfo] = await connection.getMultipleAccountsInfo([tokenAMint, tokenBMint]);
582
+ const tokenAProgram = mintAInfo!.owner;
583
+ const tokenBProgram = mintBInfo!.owner;
71
584
 
72
585
  const txBuilder = cpAmm.swap2({
73
586
  payer: wallet.publicKey, pool: poolAddress,
74
587
  inputTokenMint: inputMint, outputTokenMint: outputMint,
75
588
  tokenAMint, tokenBMint,
76
589
  tokenAVault: poolState.tokenAVault, tokenBVault: poolState.tokenBVault,
77
- tokenAProgram: poolState.tokenAProgram, tokenBProgram: poolState.tokenBProgram,
590
+ tokenAProgram, tokenBProgram,
78
591
  referralTokenAccount: null, poolState,
79
592
  swapMode: SwapMode.ExactIn, amountIn, minimumAmountOut: minOut,
80
593
  });
81
594
 
82
- const tx = await txBuilder.transaction();
595
+ const tx = await txBuilder;
83
596
  tx.feePayer = wallet.publicKey;
84
597
  tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
85
598
  tx.sign(wallet);
@@ -173,7 +686,7 @@ async function handleSwap(
173
686
  const connection = new Connection(rpcUrl, "confirmed");
174
687
  const wallet = await loadWallet(options.wallet);
175
688
  const poolAddress = new PublicKey(pool);
176
- const inputMint = new PublicKey(inputToken);
689
+ const inputMint = new PublicKey(resolveTokenMint(inputToken));
177
690
  const slippageBps = options.slippage ? Math.round(parseFloat(options.slippage) * 100) : 100;
178
691
 
179
692
  // Detect pool type
@@ -191,7 +704,8 @@ async function handleSwap(
191
704
  const decimals = await getMintDecimals(connection, inputMint);
192
705
  const rawAmount = new BN(Math.floor(parseFloat(amount) * 10 ** decimals).toString());
193
706
 
194
- if (!options.json) printInfo(`Swapping ${amount} tokens (slippage: ${slippageBps / 100}%)...`);
707
+ const symIn = tokenSymbol(inputMint.toBase58());
708
+ if (!options.json) printInfo(`Swapping ${amount} ${symIn} (slippage: ${slippageBps / 100}%)...`);
195
709
 
196
710
  let result: { signature: string; outputMint: PublicKey; minOut: BN };
197
711
  switch (poolType) {
@@ -203,10 +717,13 @@ async function handleSwap(
203
717
  if (options.json) {
204
718
  formatOutput({ signature: result.signature, poolType, inputMint: inputMint.toBase58(), outputMint: result.outputMint.toBase58(), amountIn: amount, minAmountOut: result.minOut.toString() }, true);
205
719
  } else {
720
+ const symOut = tokenSymbol(result.outputMint.toBase58());
721
+ const outDec = await getMintDecimals(connection, result.outputMint);
722
+ const minOutStr = (Number(result.minOut.toString()) / 10 ** outDec).toFixed(outDec).replace(/\.?0+$/, "");
206
723
  printSuccess("Swap submitted!");
207
724
  console.log(` Transaction: ${result.signature}`);
208
- console.log(` Output mint: ${result.outputMint.toBase58()}`);
209
- console.log(` Min output: ${result.minOut.toString()}`);
725
+ console.log(` Output: ${symOut} (${result.outputMint.toBase58()})`);
726
+ console.log(` Min output: ${minOutStr} ${symOut}`);
210
727
  }
211
728
  }
212
729
 
@@ -222,7 +739,7 @@ async function handleAddLiquidity(
222
739
  const connection = new Connection(rpcUrl, "confirmed");
223
740
  const wallet = await loadWallet(options.wallet);
224
741
  const poolAddress = new PublicKey(pool);
225
- const inputMint = new PublicKey(inputToken);
742
+ const inputMint = new PublicKey(resolveTokenMint(inputToken));
226
743
  const slippageBps = options.slippage ? Math.round(parseFloat(options.slippage) * 100) : 100;
227
744
 
228
745
  // Detect pool type
@@ -332,7 +849,7 @@ async function handleAddLiquidity(
332
849
  tokenAProgram: poolState.tokenAProgram, tokenBProgram: poolState.tokenBProgram,
333
850
  });
334
851
 
335
- const tx = await txBuilder.transaction();
852
+ const tx = await txBuilder;
336
853
  tx.feePayer = wallet.publicKey;
337
854
  tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
338
855
  tx.sign(wallet);
@@ -352,7 +869,7 @@ async function handleAddLiquidity(
352
869
  poolState,
353
870
  });
354
871
 
355
- const tx = await txBuilder.transaction();
872
+ const tx = await txBuilder;
356
873
  tx.feePayer = wallet.publicKey;
357
874
  tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
358
875
  tx.sign(wallet, positionNft);
@@ -556,7 +1073,7 @@ async function handleCreateCpAmm(
556
1073
  tokenAProgram, tokenBProgram,
557
1074
  });
558
1075
 
559
- const tx = await txBuilder.transaction();
1076
+ const tx = await txBuilder;
560
1077
  tx.feePayer = wallet.publicKey;
561
1078
  tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
562
1079
  tx.sign(wallet, positionNft);
@@ -844,7 +1361,7 @@ async function handleRemoveLiquidity(
844
1361
  txBuilder = cpAmm.removeLiquidity({ ...commonParams, liquidityDelta });
845
1362
  }
846
1363
 
847
- const tx = await txBuilder.transaction();
1364
+ const tx = await txBuilder;
848
1365
  tx.feePayer = wallet.publicKey;
849
1366
  tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
850
1367
  tx.sign(wallet);
@@ -936,7 +1453,7 @@ async function handleClaimFee(
936
1453
  receiver: wallet.publicKey,
937
1454
  });
938
1455
 
939
- const tx = await txBuilder.transaction();
1456
+ const tx = await txBuilder;
940
1457
  tx.feePayer = wallet.publicKey;
941
1458
  tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
942
1459
  tx.sign(wallet);
@@ -977,8 +1494,84 @@ async function handleClaimFee(
977
1494
 
978
1495
  export function registerDexCommands(program: Command): void {
979
1496
  const dex = program
980
- .command("dex", { hidden: true })
981
- .description("DEX operations — swap tokens and create pools on Meteora (DAMM v2 / DLMM / DBC)");
1497
+ .command("dex")
1498
+ .description("DEX — swap tokens via smart router or specific Meteora pool (DAMM v2 / DLMM / DBC)")
1499
+ .addHelpText("after", `
1500
+ Token symbols (NARA, USDC, USDT, SOL) can be used instead of mint addresses.
1501
+
1502
+ Examples:
1503
+ # Discover pools
1504
+ npx naracli dex pools # List NARA pools (default)
1505
+ npx naracli dex pools USDC # List USDC pools
1506
+
1507
+ # Smart routing (best price across DAMM v2 / DLMM / DBC)
1508
+ npx naracli dex smart-quote NARA USDC 1 # Quote: sell 1 NARA for USDC
1509
+ npx naracli dex smart-quote USDC NARA 10 # Quote: buy NARA with 10 USDC
1510
+ npx naracli dex smart-swap NARA USDC 1 --slippage 0.5 # Execute: sell 1 NARA → USDC
1511
+ npx naracli dex smart-swap USDC NARA 10 # Execute: buy NARA with 10 USDC
1512
+
1513
+ # Single-pool quote / swap
1514
+ npx naracli dex quote <pool-address> NARA 1 # Quote on a specific pool
1515
+ npx naracli dex swap <pool-address> NARA 1 --slippage 0.5`);
1516
+
1517
+ // dex pools
1518
+ dex
1519
+ .command("pools [token-mint]")
1520
+ .description("Find Meteora pools containing a given token (default: NARA), show reserves and price")
1521
+ .action(async (token: string | undefined, _opts: any, cmd: Command) => {
1522
+ try {
1523
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
1524
+ const mint = token || "So11111111111111111111111111111111111111112";
1525
+ await handlePools(mint, globalOpts);
1526
+ } catch (error: any) {
1527
+ printError(error.message);
1528
+ process.exit(1);
1529
+ }
1530
+ });
1531
+
1532
+ // dex smart-quote
1533
+ dex
1534
+ .command("smart-quote <input-mint> <output-mint> <amount>")
1535
+ .description("Get a best-route swap quote via nara smart router (aggregates DAMM v2 / DLMM / DBC)")
1536
+ .action(async (inputToken: string, outputToken: string, amount: string, _opts: any, cmd: Command) => {
1537
+ try {
1538
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
1539
+ await handleSmartQuote(inputToken, outputToken, amount, globalOpts);
1540
+ } catch (error: any) {
1541
+ printError(error.message);
1542
+ process.exit(1);
1543
+ }
1544
+ });
1545
+
1546
+ // dex smart-swap
1547
+ dex
1548
+ .command("smart-swap <input-mint> <output-mint> <amount>")
1549
+ .description("Execute a best-route swap via nara smart router")
1550
+ .option("--slippage <percent>", "Slippage tolerance in percent (default: 1)")
1551
+ .action(async (inputToken: string, outputToken: string, amount: string, opts: any, cmd: Command) => {
1552
+ try {
1553
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
1554
+ await handleSmartSwap(inputToken, outputToken, amount, { ...globalOpts, slippage: opts.slippage });
1555
+ } catch (error: any) {
1556
+ printError(error.message);
1557
+ process.exit(1);
1558
+ }
1559
+ });
1560
+
1561
+ // dex quote
1562
+ dex
1563
+ .command("quote <pool> <input-token-mint> <amount>")
1564
+ .description("Get a swap quote without executing (shows expected output, min output, fee, price)")
1565
+ .option("--slippage <percent>", "Slippage tolerance in percent (default: 1)")
1566
+ .action(async (pool: string, inputToken: string, amount: string, opts: any, cmd: Command) => {
1567
+ try {
1568
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
1569
+ await handleQuote(pool, inputToken, amount, { ...globalOpts, slippage: opts.slippage });
1570
+ } catch (error: any) {
1571
+ printError(error.message);
1572
+ process.exit(1);
1573
+ }
1574
+ });
982
1575
 
983
1576
  // dex swap
984
1577
  dex
@@ -1001,7 +1594,7 @@ Examples:
1001
1594
 
1002
1595
  // dex add-liquidity
1003
1596
  dex
1004
- .command("add-liquidity <pool> <token-mint> <amount>")
1597
+ .command("liquidity-add <pool> <token-mint> <amount>", { hidden: true })
1005
1598
  .description("Add liquidity to a Meteora pool (DAMM v2 / DLMM). Calculates the paired token amount from pool price.")
1006
1599
  .option("--amount-b <number>", "Explicitly set paired token amount (skip price calculation)")
1007
1600
  .option("--position <address>", "Existing position address (creates new if omitted)")
@@ -1022,7 +1615,7 @@ Examples:
1022
1615
 
1023
1616
  // dex liquidity-positions
1024
1617
  dex
1025
- .command("liquidity-positions [owner-address]")
1618
+ .command("liquidity-positions [owner-address]", { hidden: true })
1026
1619
  .description("List all liquidity positions across DAMM v2 and DLMM pools. Defaults to your wallet.")
1027
1620
  .action(async (ownerAddress: string | undefined, _opts: any, cmd: Command) => {
1028
1621
  try {
@@ -1036,7 +1629,7 @@ Examples:
1036
1629
 
1037
1630
  // dex remove-liquidity
1038
1631
  dex
1039
- .command("remove-liquidity <pool> <position>")
1632
+ .command("liquidity-remove <pool> <position>", { hidden: true })
1040
1633
  .description("Remove liquidity from a Meteora pool position (DAMM v2 / DLMM)")
1041
1634
  .option("--bps <number>", "Basis points to remove (10000 = 100%, default: 10000)")
1042
1635
  .option("--all", "Remove all liquidity and close position")
@@ -1052,7 +1645,7 @@ Examples:
1052
1645
 
1053
1646
  // dex claim-fee
1054
1647
  dex
1055
- .command("claim-fee <pool> <position>")
1648
+ .command("claim-fee <pool> <position>", { hidden: true })
1056
1649
  .description("Claim accumulated trading fees from a position (DAMM v2 / DLMM)")
1057
1650
  .action(async (pool: string, position: string, _opts: any, cmd: Command) => {
1058
1651
  try {
@@ -1066,7 +1659,7 @@ Examples:
1066
1659
 
1067
1660
  // dex create-pool
1068
1661
  const createPool = dex
1069
- .command("create-pool")
1662
+ .command("create-pool", { hidden: true })
1070
1663
  .description("Create a new liquidity pool on Meteora");
1071
1664
 
1072
1665
  // dex create-pool cpamm