naracli 1.0.86 → 1.0.88

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