@zoidz123/raydium-cli 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,2118 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerClmmCommands = registerClmmCommands;
7
+ const web3_js_1 = require("@solana/web3.js");
8
+ const raydium_sdk_v2_1 = require("@raydium-io/raydium-sdk-v2");
9
+ const decimal_js_1 = __importDefault(require("decimal.js"));
10
+ const bn_js_1 = __importDefault(require("bn.js"));
11
+ const config_manager_1 = require("../../lib/config-manager");
12
+ const wallet_manager_1 = require("../../lib/wallet-manager");
13
+ const prompt_1 = require("../../lib/prompt");
14
+ const output_1 = require("../../lib/output");
15
+ const raydium_client_1 = require("../../lib/raydium-client");
16
+ const clmm_utils_1 = require("../../lib/clmm-utils");
17
+ const token_price_1 = require("../../lib/token-price");
18
+ const VALID_CLMM_PROGRAM_IDS = new Set([
19
+ raydium_sdk_v2_1.CLMM_PROGRAM_ID.toBase58(),
20
+ raydium_sdk_v2_1.DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID.toBase58()
21
+ ]);
22
+ const WRAPPED_SOL_MINT = "So11111111111111111111111111111111111111112";
23
+ const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
24
+ const TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
25
+ /**
26
+ * Populate rewardDefaultInfos from on-chain rewardInfos
27
+ * This is needed because getPoolInfoFromRpc returns empty rewardDefaultInfos
28
+ * but the program expects reward accounts for active rewards
29
+ */
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ function populateRewardDefaultInfos(poolInfo) {
32
+ if (poolInfo.rewardDefaultInfos && poolInfo.rewardDefaultInfos.length > 0) {
33
+ return; // Already populated
34
+ }
35
+ const rewardInfos = poolInfo.rewardInfos || [];
36
+ const activeRewards = [];
37
+ for (const reward of rewardInfos) {
38
+ const rewardState = typeof reward.rewardState === "number"
39
+ ? reward.rewardState
40
+ : parseInt(reward.rewardState, 10);
41
+ // Get mint address as string
42
+ const mintAddress = typeof reward.tokenMint === "string"
43
+ ? reward.tokenMint
44
+ : reward.tokenMint?.toBase58?.() || reward.tokenMint?.toString() || "";
45
+ // Skip inactive rewards or system program placeholder
46
+ if (rewardState === 0 || mintAddress === SYSTEM_PROGRAM_ID || mintAddress === "") {
47
+ continue;
48
+ }
49
+ activeRewards.push({
50
+ mint: {
51
+ address: mintAddress,
52
+ programId: TOKEN_PROGRAM_ID
53
+ }
54
+ });
55
+ }
56
+ poolInfo.rewardDefaultInfos = activeRewards;
57
+ }
58
+ /**
59
+ * Get token balance for a wallet
60
+ */
61
+ async function getTokenBalance(connection, owner, mintAddress) {
62
+ if (mintAddress === WRAPPED_SOL_MINT) {
63
+ const balance = await connection.getBalance(owner);
64
+ return new bn_js_1.default(balance);
65
+ }
66
+ try {
67
+ const tokenAccounts = await connection.getTokenAccountsByOwner(owner, {
68
+ mint: new web3_js_1.PublicKey(mintAddress)
69
+ });
70
+ if (tokenAccounts.value.length > 0) {
71
+ const accountData = tokenAccounts.value[0].account.data;
72
+ // Token account: first 32 bytes mint, next 32 bytes owner, next 8 bytes amount
73
+ const amountBytes = accountData.slice(64, 72);
74
+ return new bn_js_1.default(amountBytes, "le");
75
+ }
76
+ }
77
+ catch {
78
+ // Return 0 on error
79
+ }
80
+ return new bn_js_1.default(0);
81
+ }
82
+ function registerClmmCommands(program) {
83
+ const clmm = program.command("clmm").description("CLMM (concentrated liquidity) commands");
84
+ // clmm pool <pool-id>
85
+ clmm
86
+ .command("pool")
87
+ .description("Show CLMM pool state")
88
+ .argument("<pool-id>", "Pool address")
89
+ .action(handlePoolCommand);
90
+ // clmm ticks <pool-id>
91
+ clmm
92
+ .command("ticks")
93
+ .description("List initialized ticks with liquidity")
94
+ .argument("<pool-id>", "Pool address")
95
+ .option("--min-tick <tick>", "Minimum tick index")
96
+ .option("--max-tick <tick>", "Maximum tick index")
97
+ .option("--limit <number>", "Maximum ticks to display", "50")
98
+ .action(handleTicksCommand);
99
+ // clmm positions
100
+ clmm
101
+ .command("positions")
102
+ .description("List all positions for the active wallet")
103
+ .option("--wallet <name>", "Wallet name to use (defaults to active wallet)")
104
+ .action(handlePositionsCommand);
105
+ // clmm position <nft-mint>
106
+ clmm
107
+ .command("position")
108
+ .description("Show detailed position state")
109
+ .argument("<nft-mint>", "Position NFT mint address")
110
+ .action(handlePositionCommand);
111
+ // clmm collect-fees
112
+ clmm
113
+ .command("collect-fees")
114
+ .description("Collect accumulated fees from position(s)")
115
+ .option("--nft-mint <address>", "Position NFT mint address")
116
+ .option("--all", "Collect fees from all positions with unclaimed fees")
117
+ .option("--priority-fee <sol>", "Priority fee in SOL")
118
+ .action(handleCollectFeesCommand);
119
+ // clmm close-position
120
+ clmm
121
+ .command("close-position")
122
+ .description("Close a CLMM position")
123
+ .requiredOption("--nft-mint <address>", "Position NFT mint address")
124
+ .option("--force", "Remove all liquidity first, then close")
125
+ .option("--slippage <percent>", "Slippage tolerance for force mode")
126
+ .option("--priority-fee <sol>", "Priority fee in SOL")
127
+ .action(handleClosePositionCommand);
128
+ // clmm decrease-liquidity
129
+ clmm
130
+ .command("decrease-liquidity")
131
+ .description("Remove liquidity from a position")
132
+ .requiredOption("--nft-mint <address>", "Position NFT mint address")
133
+ .requiredOption("--percent <number>", "Percentage of liquidity to remove (1-100)")
134
+ .option("--slippage <percent>", "Slippage tolerance")
135
+ .option("--priority-fee <sol>", "Priority fee in SOL")
136
+ .option("--swap-to-sol", "Swap both withdrawn tokens to SOL after removing liquidity")
137
+ .action(handleDecreaseLiquidityCommand);
138
+ // clmm increase-liquidity
139
+ clmm
140
+ .command("increase-liquidity")
141
+ .description("Add liquidity to an existing position")
142
+ .requiredOption("--nft-mint <address>", "Position NFT mint address")
143
+ .requiredOption("--amount <number>", "Amount to add")
144
+ .option("--token <A|B>", "Which token the amount refers to", "A")
145
+ .option("--slippage <percent>", "Slippage tolerance")
146
+ .option("--priority-fee <sol>", "Priority fee in SOL")
147
+ .option("--auto-swap", "Automatically swap tokens if you don't have enough of the other token")
148
+ .action(handleIncreaseLiquidityCommand);
149
+ // clmm open-position
150
+ clmm
151
+ .command("open-position")
152
+ .description("Open a new liquidity position")
153
+ .requiredOption("--pool-id <address>", "Pool address")
154
+ .requiredOption("--price-lower <number>", "Lower price bound")
155
+ .requiredOption("--price-upper <number>", "Upper price bound")
156
+ .requiredOption("--amount <number>", "Deposit amount")
157
+ .option("--token <A|B>", "Which token the amount refers to", "A")
158
+ .option("--slippage <percent>", "Slippage tolerance")
159
+ .option("--priority-fee <sol>", "Priority fee in SOL")
160
+ .option("--auto-swap", "Automatically swap to get required tokens if balance is insufficient")
161
+ .action(handleOpenPositionCommand);
162
+ // clmm create-pool
163
+ clmm
164
+ .command("create-pool")
165
+ .description("Create a new CLMM pool")
166
+ .requiredOption("--mint-a <address>", "Token A mint address")
167
+ .requiredOption("--mint-b <address>", "Token B mint address")
168
+ .requiredOption("--fee-tier <bps>", "Fee tier in basis points (e.g., 500, 3000, 10000)")
169
+ .requiredOption("--initial-price <number>", "Initial price of token A in terms of token B")
170
+ .option("--priority-fee <sol>", "Priority fee in SOL")
171
+ .action(handleCreatePoolCommand);
172
+ }
173
+ async function handlePoolCommand(poolIdStr) {
174
+ let poolId;
175
+ try {
176
+ poolId = new web3_js_1.PublicKey(poolIdStr);
177
+ }
178
+ catch {
179
+ (0, output_1.logError)("Invalid pool ID");
180
+ process.exitCode = 1;
181
+ return;
182
+ }
183
+ const raydium = await (0, output_1.withSpinner)("Loading SDK", () => (0, raydium_client_1.loadRaydium)({ disableLoadToken: true }));
184
+ let poolInfo;
185
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
186
+ let computePoolInfo;
187
+ try {
188
+ const data = await (0, output_1.withSpinner)("Fetching pool info", async () => {
189
+ const result = await raydium.clmm.getPoolInfoFromRpc(poolId.toBase58());
190
+ if (!result.poolInfo) {
191
+ throw new Error("Pool not found");
192
+ }
193
+ if (!VALID_CLMM_PROGRAM_IDS.has(result.poolInfo.programId)) {
194
+ throw new Error("Not a CLMM pool");
195
+ }
196
+ return result;
197
+ });
198
+ poolInfo = data.poolInfo;
199
+ computePoolInfo = data.computePoolInfo;
200
+ }
201
+ catch (error) {
202
+ const msg = error instanceof Error ? error.message : String(error);
203
+ (0, output_1.logError)("Failed to fetch pool info", msg);
204
+ process.exitCode = 1;
205
+ return;
206
+ }
207
+ const mintA = poolInfo.mintA;
208
+ const mintB = poolInfo.mintB;
209
+ const decimalsA = mintA.decimals;
210
+ const decimalsB = mintB.decimals;
211
+ // Use computePoolInfo for BN values
212
+ const sqrtPriceX64 = computePoolInfo.sqrtPriceX64.toString();
213
+ const liquidity = computePoolInfo.liquidity.toString();
214
+ const tickCurrent = computePoolInfo.tickCurrent;
215
+ const tickSpacing = computePoolInfo.tickSpacing;
216
+ const currentPrice = (0, clmm_utils_1.sqrtPriceX64ToPrice)(sqrtPriceX64, decimalsA, decimalsB);
217
+ const symbolA = mintA.symbol || mintA.address.slice(0, 6);
218
+ const symbolB = mintB.symbol || mintB.address.slice(0, 6);
219
+ // Fee rate is in the poolInfo from API
220
+ const feeRate = poolInfo.feeRate;
221
+ const protocolFeeRate = computePoolInfo.ammConfig?.protocolFeeRate ?? 0;
222
+ const fundFeeRate = computePoolInfo.ammConfig?.fundFeeRate ?? 0;
223
+ // Pool reserves (raw amounts from API, need to adjust for decimals)
224
+ const mintAmountARaw = poolInfo.mintAmountA;
225
+ const mintAmountBRaw = poolInfo.mintAmountB;
226
+ const amountA = new decimal_js_1.default(mintAmountARaw).div(new decimal_js_1.default(10).pow(decimalsA));
227
+ const amountB = new decimal_js_1.default(mintAmountBRaw).div(new decimal_js_1.default(10).pow(decimalsB));
228
+ const tvl = poolInfo.tvl;
229
+ // Fetch optional USD prices
230
+ const prices = await (0, output_1.withSpinner)("Fetching token prices", () => (0, token_price_1.getTokenPrices)([mintA.address, mintB.address]));
231
+ const priceA = prices.get(mintA.address) ?? null;
232
+ const priceB = prices.get(mintB.address) ?? null;
233
+ const calculatedTvl = (0, clmm_utils_1.calculateUsdValue)(amountA, amountB, priceA, priceB);
234
+ if ((0, output_1.isJsonOutput)()) {
235
+ (0, output_1.logJson)({
236
+ poolId: poolId.toBase58(),
237
+ programId: poolInfo.programId,
238
+ mintA: {
239
+ address: mintA.address,
240
+ symbol: mintA.symbol,
241
+ decimals: decimalsA,
242
+ amount: amountA.toString(),
243
+ ...(priceA !== null && { priceUsd: priceA })
244
+ },
245
+ mintB: {
246
+ address: mintB.address,
247
+ symbol: mintB.symbol,
248
+ decimals: decimalsB,
249
+ amount: amountB.toString(),
250
+ ...(priceB !== null && { priceUsd: priceB })
251
+ },
252
+ currentTick: tickCurrent,
253
+ sqrtPriceX64: sqrtPriceX64,
254
+ price: currentPrice.toString(),
255
+ liquidity: liquidity,
256
+ tvl: tvl,
257
+ ...(calculatedTvl !== null && { tvlCalculated: calculatedTvl.toNumber() }),
258
+ feeRate: feeRate,
259
+ tickSpacing: tickSpacing,
260
+ protocolFeeRate: protocolFeeRate,
261
+ fundFeeRate: fundFeeRate
262
+ });
263
+ return;
264
+ }
265
+ (0, output_1.logInfo)(`Pool: ${poolId.toBase58()}`);
266
+ (0, output_1.logInfo)(`Program: ${poolInfo.programId}`);
267
+ (0, output_1.logInfo)("");
268
+ (0, output_1.logInfo)("Tokens:");
269
+ (0, output_1.logInfo)(` ${symbolA}: ${mintA.address} (${decimalsA} decimals)`);
270
+ if (priceA !== null)
271
+ (0, output_1.logInfo)(` Price: ${(0, clmm_utils_1.formatUsd)(priceA)}`);
272
+ (0, output_1.logInfo)(` ${symbolB}: ${mintB.address} (${decimalsB} decimals)`);
273
+ if (priceB !== null)
274
+ (0, output_1.logInfo)(` Price: ${(0, clmm_utils_1.formatUsd)(priceB)}`);
275
+ (0, output_1.logInfo)("");
276
+ (0, output_1.logInfo)("Price:");
277
+ (0, output_1.logInfo)(` Current tick: ${tickCurrent}`);
278
+ (0, output_1.logInfo)(` sqrtPriceX64: ${sqrtPriceX64}`);
279
+ (0, output_1.logInfo)(` Price: ${(0, clmm_utils_1.formatPrice)(currentPrice)} ${symbolB}/${symbolA}`);
280
+ (0, output_1.logInfo)("");
281
+ (0, output_1.logInfo)("Liquidity:");
282
+ (0, output_1.logInfo)(` In-range liquidity: ${liquidity}`);
283
+ (0, output_1.logInfo)(` ${symbolA} in pool: ${(0, clmm_utils_1.formatTokenAmount)(amountA)}`);
284
+ (0, output_1.logInfo)(` ${symbolB} in pool: ${(0, clmm_utils_1.formatTokenAmount)(amountB)}`);
285
+ if (tvl > 0)
286
+ (0, output_1.logInfo)(` TVL: $${tvl.toLocaleString()}`);
287
+ if (calculatedTvl !== null && tvl <= 0)
288
+ (0, output_1.logInfo)(` TVL: ${(0, clmm_utils_1.formatUsd)(calculatedTvl)}`);
289
+ (0, output_1.logInfo)("");
290
+ (0, output_1.logInfo)("Fees:");
291
+ (0, output_1.logInfo)(` Fee rate: ${(0, clmm_utils_1.formatFeeRate)(feeRate)}`);
292
+ (0, output_1.logInfo)(` Tick spacing: ${tickSpacing}`);
293
+ (0, output_1.logInfo)(` Protocol fee rate: ${protocolFeeRate / 10000}%`);
294
+ (0, output_1.logInfo)(` Fund fee rate: ${fundFeeRate / 10000}%`);
295
+ }
296
+ async function handleTicksCommand(poolIdStr, options) {
297
+ let poolId;
298
+ try {
299
+ poolId = new web3_js_1.PublicKey(poolIdStr);
300
+ }
301
+ catch {
302
+ (0, output_1.logError)("Invalid pool ID");
303
+ process.exitCode = 1;
304
+ return;
305
+ }
306
+ const limit = options.limit ? Number(options.limit) : 50;
307
+ if (!Number.isFinite(limit) || limit < 1) {
308
+ (0, output_1.logError)("Invalid limit");
309
+ process.exitCode = 1;
310
+ return;
311
+ }
312
+ const minTick = options.minTick ? Number(options.minTick) : undefined;
313
+ const maxTick = options.maxTick ? Number(options.maxTick) : undefined;
314
+ const raydium = await (0, output_1.withSpinner)("Loading SDK", () => (0, raydium_client_1.loadRaydium)({ disableLoadToken: true }));
315
+ let poolInfo;
316
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
317
+ let computePoolInfo;
318
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
319
+ let tickData;
320
+ try {
321
+ const data = await (0, output_1.withSpinner)("Fetching pool and tick data", async () => {
322
+ const result = await raydium.clmm.getPoolInfoFromRpc(poolId.toBase58());
323
+ if (!result.poolInfo) {
324
+ throw new Error("Pool not found");
325
+ }
326
+ if (!VALID_CLMM_PROGRAM_IDS.has(result.poolInfo.programId)) {
327
+ throw new Error("Not a CLMM pool");
328
+ }
329
+ return result;
330
+ });
331
+ poolInfo = data.poolInfo;
332
+ computePoolInfo = data.computePoolInfo;
333
+ tickData = data.tickData;
334
+ }
335
+ catch (error) {
336
+ const msg = error instanceof Error ? error.message : String(error);
337
+ (0, output_1.logError)("Failed to fetch pool info", msg);
338
+ process.exitCode = 1;
339
+ return;
340
+ }
341
+ const mintA = poolInfo.mintA;
342
+ const mintB = poolInfo.mintB;
343
+ const decimalsA = mintA.decimals;
344
+ const decimalsB = mintB.decimals;
345
+ const currentTick = computePoolInfo.tickCurrent;
346
+ const tickSpacing = computePoolInfo.tickSpacing;
347
+ const sqrtPriceX64 = computePoolInfo.sqrtPriceX64.toString();
348
+ // Get initialized ticks from tickData
349
+ // tickData structure: tickData[poolId][startTickIndex] = { ticks: [...] }
350
+ const initializedTicks = [];
351
+ if (tickData && typeof tickData === "object") {
352
+ // Get the pool's tick data
353
+ const poolTickData = tickData[poolId.toBase58()];
354
+ if (poolTickData && typeof poolTickData === "object") {
355
+ // Iterate over tick arrays (keyed by start index)
356
+ for (const tickArray of Object.values(poolTickData)) {
357
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
358
+ const ticks = tickArray?.ticks;
359
+ if (ticks && Array.isArray(ticks)) {
360
+ for (const tick of ticks) {
361
+ if (tick && tick.liquidityGross && !tick.liquidityGross.isZero()) {
362
+ const tickIndex = tick.tick;
363
+ if (minTick !== undefined && tickIndex < minTick)
364
+ continue;
365
+ if (maxTick !== undefined && tickIndex > maxTick)
366
+ continue;
367
+ initializedTicks.push({
368
+ tick: tickIndex,
369
+ liquidityNet: tick.liquidityNet.toString(),
370
+ liquidityGross: tick.liquidityGross.toString()
371
+ });
372
+ }
373
+ }
374
+ }
375
+ }
376
+ }
377
+ }
378
+ // Sort by tick index
379
+ initializedTicks.sort((a, b) => a.tick - b.tick);
380
+ // Apply limit
381
+ const displayTicks = initializedTicks.slice(0, limit);
382
+ const symbolA = mintA.symbol || mintA.address.slice(0, 6);
383
+ const symbolB = mintB.symbol || mintB.address.slice(0, 6);
384
+ // Fetch optional USD prices
385
+ const prices = await (0, output_1.withSpinner)("Fetching token prices", () => (0, token_price_1.getTokenPrices)([mintA.address, mintB.address]));
386
+ const priceA = prices.get(mintA.address) ?? null;
387
+ const priceB = prices.get(mintB.address) ?? null;
388
+ // Calculate amounts for each tick
389
+ const ticksWithAmounts = displayTicks.map((t) => {
390
+ const price = (0, clmm_utils_1.tickToPrice)(t.tick, decimalsA, decimalsB);
391
+ const amounts = (0, clmm_utils_1.getAmountsForTickRange)(t.liquidityNet, t.tick, tickSpacing, currentTick, sqrtPriceX64, decimalsA, decimalsB);
392
+ const usdValue = (0, clmm_utils_1.calculateUsdValue)(amounts.amount0, amounts.amount1, priceA, priceB);
393
+ return {
394
+ tick: t.tick,
395
+ price: price.toString(),
396
+ liquidityNet: t.liquidityNet,
397
+ liquidityGross: t.liquidityGross,
398
+ amount0: amounts.amount0.toString(),
399
+ amount1: amounts.amount1.toString(),
400
+ ...(usdValue !== null && { usdValue: usdValue.toNumber() })
401
+ };
402
+ });
403
+ if ((0, output_1.isJsonOutput)()) {
404
+ (0, output_1.logJson)({
405
+ poolId: poolId.toBase58(),
406
+ currentTick,
407
+ tickSpacing,
408
+ totalInitialized: initializedTicks.length,
409
+ displayed: displayTicks.length,
410
+ ticks: ticksWithAmounts
411
+ });
412
+ return;
413
+ }
414
+ (0, output_1.logInfo)(`Pool: ${poolId.toBase58()}`);
415
+ (0, output_1.logInfo)(`Current tick: ${currentTick}`);
416
+ (0, output_1.logInfo)(`Tick spacing: ${tickSpacing}`);
417
+ (0, output_1.logInfo)(`Initialized ticks: ${initializedTicks.length} (showing ${displayTicks.length})`);
418
+ (0, output_1.logInfo)("");
419
+ if (displayTicks.length === 0) {
420
+ (0, output_1.logInfo)("No initialized ticks found in range");
421
+ return;
422
+ }
423
+ for (const t of ticksWithAmounts) {
424
+ const isNearCurrent = Math.abs(t.tick - currentTick) <= tickSpacing;
425
+ const marker = isNearCurrent ? " <-- current" : "";
426
+ const price = new decimal_js_1.default(t.price);
427
+ (0, output_1.logInfo)(`Tick ${t.tick}${marker}`);
428
+ (0, output_1.logInfo)(` Price: ${(0, clmm_utils_1.formatPrice)(price)} ${symbolB}/${symbolA}`);
429
+ (0, output_1.logInfo)(` Liquidity net: ${t.liquidityNet}`);
430
+ (0, output_1.logInfo)(` Liquidity gross: ${t.liquidityGross}`);
431
+ (0, output_1.logInfo)(` ${symbolA}: ${(0, clmm_utils_1.formatTokenAmount)(new decimal_js_1.default(t.amount0))}`);
432
+ (0, output_1.logInfo)(` ${symbolB}: ${(0, clmm_utils_1.formatTokenAmount)(new decimal_js_1.default(t.amount1))}`);
433
+ if (t.usdValue !== undefined)
434
+ (0, output_1.logInfo)(` Value: ${(0, clmm_utils_1.formatUsd)(t.usdValue)}`);
435
+ }
436
+ }
437
+ async function handlePositionsCommand(options) {
438
+ const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
439
+ const walletName = options.wallet ?? config.activeWallet;
440
+ if (!walletName) {
441
+ (0, output_1.logError)("No wallet specified and no active wallet set");
442
+ (0, output_1.logInfo)("Use --wallet <name> or set an active wallet with: raydium wallet set <name>");
443
+ process.exitCode = 1;
444
+ return;
445
+ }
446
+ const password = await (0, prompt_1.promptPassword)("Enter wallet password");
447
+ let owner;
448
+ try {
449
+ owner = await (0, wallet_manager_1.decryptWallet)(walletName, password);
450
+ }
451
+ catch (error) {
452
+ (0, output_1.logError)("Failed to decrypt wallet", error.message);
453
+ process.exitCode = 1;
454
+ return;
455
+ }
456
+ const raydium = await (0, output_1.withSpinner)("Loading SDK", () => (0, raydium_client_1.loadRaydium)({ owner, disableLoadToken: true }));
457
+ const isDevnet = raydium.cluster !== "mainnet";
458
+ const clmmProgramId = isDevnet ? raydium_sdk_v2_1.DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID : raydium_sdk_v2_1.CLMM_PROGRAM_ID;
459
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
460
+ let positions;
461
+ try {
462
+ positions = await (0, output_1.withSpinner)("Fetching positions", () => raydium.clmm.getOwnerPositionInfo({ programId: clmmProgramId }));
463
+ }
464
+ catch (error) {
465
+ const msg = error instanceof Error ? error.message : String(error);
466
+ (0, output_1.logError)("Failed to fetch positions", msg);
467
+ process.exitCode = 1;
468
+ return;
469
+ }
470
+ if (!positions || positions.length === 0) {
471
+ if ((0, output_1.isJsonOutput)()) {
472
+ (0, output_1.logJson)({ positions: [], count: 0 });
473
+ }
474
+ else {
475
+ (0, output_1.logInfo)(`No CLMM positions found for wallet: ${owner.publicKey.toBase58()}`);
476
+ }
477
+ return;
478
+ }
479
+ // Collect unique pool IDs
480
+ const poolIds = new Set();
481
+ for (const pos of positions) {
482
+ if (pos.poolId)
483
+ poolIds.add(pos.poolId.toBase58());
484
+ }
485
+ // Fetch fresh pool data from RPC for accurate current tick
486
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
487
+ const poolDataMap = new Map();
488
+ if (poolIds.size > 0) {
489
+ try {
490
+ const poolDataResults = await (0, output_1.withSpinner)("Fetching pool data", async () => {
491
+ const results = await Promise.all(Array.from(poolIds).map(async (poolId) => {
492
+ try {
493
+ const data = await raydium.clmm.getPoolInfoFromRpc(poolId);
494
+ return { poolId, data };
495
+ }
496
+ catch {
497
+ return { poolId, data: null };
498
+ }
499
+ }));
500
+ return results;
501
+ });
502
+ for (const { poolId, data } of poolDataResults) {
503
+ if (data?.poolInfo) {
504
+ poolDataMap.set(poolId, data);
505
+ }
506
+ }
507
+ }
508
+ catch {
509
+ // Continue without fresh pool data
510
+ }
511
+ }
512
+ // Collect unique mint addresses for price fetching
513
+ const uniqueMints = new Set();
514
+ for (const pos of positions) {
515
+ const freshPoolData = poolDataMap.get(pos.poolId?.toBase58() ?? "");
516
+ const poolInfo = freshPoolData?.poolInfo ?? pos.poolInfo;
517
+ if (poolInfo?.mintA?.address)
518
+ uniqueMints.add(poolInfo.mintA.address);
519
+ if (poolInfo?.mintB?.address)
520
+ uniqueMints.add(poolInfo.mintB.address);
521
+ }
522
+ // Fetch optional USD prices
523
+ const tokenPrices = await (0, output_1.withSpinner)("Fetching token prices", () => (0, token_price_1.getTokenPrices)(Array.from(uniqueMints)));
524
+ const positionsData = positions.map((pos) => {
525
+ const poolIdStr = pos.poolId?.toBase58() ?? "";
526
+ const freshPoolData = poolDataMap.get(poolIdStr);
527
+ const poolInfo = freshPoolData?.poolInfo ?? pos.poolInfo;
528
+ const computePoolInfo = freshPoolData?.computePoolInfo;
529
+ const mintA = poolInfo?.mintA;
530
+ const mintB = poolInfo?.mintB;
531
+ const decimalsA = mintA?.decimals ?? 9;
532
+ const decimalsB = mintB?.decimals ?? 9;
533
+ // Use fresh tick data from RPC if available
534
+ const currentTick = computePoolInfo?.tickCurrent ?? poolInfo?.tickCurrent ?? 0;
535
+ const sqrtPriceX64 = computePoolInfo?.sqrtPriceX64?.toString() ?? poolInfo?.sqrtPriceX64?.toString() ?? "0";
536
+ const inRange = (0, clmm_utils_1.isPositionInRange)(pos.tickLower, pos.tickUpper, currentTick);
537
+ const amounts = (0, clmm_utils_1.getAmountsFromLiquidity)(pos.liquidity?.toString() ?? "0", sqrtPriceX64, pos.tickLower, pos.tickUpper, decimalsA, decimalsB);
538
+ const priceA = mintA?.address ? (tokenPrices.get(mintA.address) ?? null) : null;
539
+ const priceB = mintB?.address ? (tokenPrices.get(mintB.address) ?? null) : null;
540
+ const usdValue = (0, clmm_utils_1.calculateUsdValue)(amounts.amount0, amounts.amount1, priceA, priceB);
541
+ return {
542
+ nftMint: pos.nftMint?.toBase58() ?? "unknown",
543
+ poolId: poolIdStr || "unknown",
544
+ tickLower: pos.tickLower,
545
+ tickUpper: pos.tickUpper,
546
+ currentTick,
547
+ inRange,
548
+ liquidity: pos.liquidity?.toString() ?? "0",
549
+ amount0: amounts.amount0.toString(),
550
+ amount1: amounts.amount1.toString(),
551
+ symbol0: mintA?.symbol || mintA?.address?.slice(0, 6) || "token0",
552
+ symbol1: mintB?.symbol || mintB?.address?.slice(0, 6) || "token1",
553
+ feesOwed0: pos.tokenFeesOwedA?.toString() ?? "0",
554
+ feesOwed1: pos.tokenFeesOwedB?.toString() ?? "0",
555
+ ...(usdValue !== null && { usdValue: usdValue.toNumber() })
556
+ };
557
+ });
558
+ if ((0, output_1.isJsonOutput)()) {
559
+ (0, output_1.logJson)({
560
+ wallet: owner.publicKey.toBase58(),
561
+ positions: positionsData,
562
+ count: positionsData.length
563
+ });
564
+ return;
565
+ }
566
+ (0, output_1.logInfo)(`Wallet: ${owner.publicKey.toBase58()}`);
567
+ (0, output_1.logInfo)(`Positions: ${positionsData.length}`);
568
+ (0, output_1.logInfo)("");
569
+ for (const pos of positionsData) {
570
+ const rangeStatus = pos.inRange ? "IN RANGE" : "OUT OF RANGE";
571
+ (0, output_1.logInfo)(`Position: ${pos.nftMint}`);
572
+ (0, output_1.logInfo)(` Pool: ${pos.poolId}`);
573
+ (0, output_1.logInfo)(` Range: [${pos.tickLower}, ${pos.tickUpper}] (current: ${pos.currentTick}) - ${rangeStatus}`);
574
+ (0, output_1.logInfo)(` Liquidity: ${pos.liquidity}`);
575
+ (0, output_1.logInfo)(` ${pos.symbol0}: ${(0, clmm_utils_1.formatTokenAmount)(new decimal_js_1.default(pos.amount0))}`);
576
+ (0, output_1.logInfo)(` ${pos.symbol1}: ${(0, clmm_utils_1.formatTokenAmount)(new decimal_js_1.default(pos.amount1))}`);
577
+ if (pos.usdValue !== undefined)
578
+ (0, output_1.logInfo)(` Value: ${(0, clmm_utils_1.formatUsd)(pos.usdValue)}`);
579
+ if (pos.feesOwed0 !== "0" || pos.feesOwed1 !== "0") {
580
+ (0, output_1.logInfo)(` Fees owed: ${pos.feesOwed0} ${pos.symbol0}, ${pos.feesOwed1} ${pos.symbol1}`);
581
+ }
582
+ (0, output_1.logInfo)("");
583
+ }
584
+ }
585
+ async function handlePositionCommand(nftMintStr) {
586
+ let nftMint;
587
+ try {
588
+ nftMint = new web3_js_1.PublicKey(nftMintStr);
589
+ }
590
+ catch {
591
+ (0, output_1.logError)("Invalid NFT mint address");
592
+ process.exitCode = 1;
593
+ return;
594
+ }
595
+ const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
596
+ const walletName = config.activeWallet;
597
+ if (!walletName) {
598
+ (0, output_1.logError)("No active wallet set. Required to fetch position info.");
599
+ (0, output_1.logInfo)("Set an active wallet with: raydium wallet set <name>");
600
+ process.exitCode = 1;
601
+ return;
602
+ }
603
+ const password = await (0, prompt_1.promptPassword)("Enter wallet password");
604
+ let owner;
605
+ try {
606
+ owner = await (0, wallet_manager_1.decryptWallet)(walletName, password);
607
+ }
608
+ catch (error) {
609
+ (0, output_1.logError)("Failed to decrypt wallet", error.message);
610
+ process.exitCode = 1;
611
+ return;
612
+ }
613
+ const raydium = await (0, output_1.withSpinner)("Loading SDK", () => (0, raydium_client_1.loadRaydium)({ owner, disableLoadToken: true }));
614
+ const isDevnet = raydium.cluster !== "mainnet";
615
+ const clmmProgramId = isDevnet ? raydium_sdk_v2_1.DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID : raydium_sdk_v2_1.CLMM_PROGRAM_ID;
616
+ // Fetch all positions and find the one with matching NFT mint
617
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
618
+ let positions;
619
+ try {
620
+ positions = await (0, output_1.withSpinner)("Fetching positions", () => raydium.clmm.getOwnerPositionInfo({ programId: clmmProgramId }));
621
+ }
622
+ catch (error) {
623
+ const msg = error instanceof Error ? error.message : String(error);
624
+ (0, output_1.logError)("Failed to fetch positions", msg);
625
+ process.exitCode = 1;
626
+ return;
627
+ }
628
+ const position = positions.find((p) => p.nftMint?.toBase58() === nftMint.toBase58());
629
+ if (!position) {
630
+ (0, output_1.logError)(`Position not found for NFT mint: ${nftMint.toBase58()}`);
631
+ (0, output_1.logInfo)("Make sure the NFT is owned by the active wallet");
632
+ process.exitCode = 1;
633
+ return;
634
+ }
635
+ // Fetch fresh pool data from RPC for accurate current tick
636
+ const poolIdStr = position.poolId?.toBase58();
637
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
638
+ let freshPoolData = null;
639
+ if (poolIdStr) {
640
+ try {
641
+ freshPoolData = await (0, output_1.withSpinner)("Fetching pool data", () => raydium.clmm.getPoolInfoFromRpc(poolIdStr));
642
+ }
643
+ catch {
644
+ // Continue with position's poolInfo
645
+ }
646
+ }
647
+ const poolInfo = freshPoolData?.poolInfo ?? position.poolInfo;
648
+ const computePoolInfo = freshPoolData?.computePoolInfo;
649
+ const mintA = poolInfo?.mintA;
650
+ const mintB = poolInfo?.mintB;
651
+ const decimalsA = mintA?.decimals ?? 9;
652
+ const decimalsB = mintB?.decimals ?? 9;
653
+ // Use fresh tick data from RPC if available
654
+ const currentTick = computePoolInfo?.tickCurrent ?? poolInfo?.tickCurrent ?? 0;
655
+ const sqrtPriceX64 = computePoolInfo?.sqrtPriceX64?.toString() ?? poolInfo?.sqrtPriceX64?.toString() ?? "0";
656
+ const inRange = (0, clmm_utils_1.isPositionInRange)(position.tickLower, position.tickUpper, currentTick);
657
+ const amounts = (0, clmm_utils_1.getAmountsFromLiquidity)(position.liquidity?.toString() ?? "0", sqrtPriceX64, position.tickLower, position.tickUpper, decimalsA, decimalsB);
658
+ const priceLower = (0, clmm_utils_1.tickToPrice)(position.tickLower, decimalsA, decimalsB);
659
+ const priceUpper = (0, clmm_utils_1.tickToPrice)(position.tickUpper, decimalsA, decimalsB);
660
+ const currentPrice = (0, clmm_utils_1.sqrtPriceX64ToPrice)(sqrtPriceX64, decimalsA, decimalsB);
661
+ const symbolA = mintA?.symbol || mintA?.address?.slice(0, 6) || "token0";
662
+ const symbolB = mintB?.symbol || mintB?.address?.slice(0, 6) || "token1";
663
+ // Fetch optional USD prices
664
+ const mintAddresses = [];
665
+ if (mintA?.address)
666
+ mintAddresses.push(mintA.address);
667
+ if (mintB?.address)
668
+ mintAddresses.push(mintB.address);
669
+ const tokenPrices = await (0, output_1.withSpinner)("Fetching token prices", () => (0, token_price_1.getTokenPrices)(mintAddresses));
670
+ const priceAUsd = mintA?.address ? (tokenPrices.get(mintA.address) ?? null) : null;
671
+ const priceBUsd = mintB?.address ? (tokenPrices.get(mintB.address) ?? null) : null;
672
+ const usdValue = (0, clmm_utils_1.calculateUsdValue)(amounts.amount0, amounts.amount1, priceAUsd, priceBUsd);
673
+ if ((0, output_1.isJsonOutput)()) {
674
+ (0, output_1.logJson)({
675
+ nftMint: nftMint.toBase58(),
676
+ poolId: position.poolId?.toBase58() ?? "unknown",
677
+ mintA: {
678
+ address: mintA?.address,
679
+ symbol: mintA?.symbol,
680
+ decimals: decimalsA,
681
+ ...(priceAUsd !== null && { priceUsd: priceAUsd })
682
+ },
683
+ mintB: {
684
+ address: mintB?.address,
685
+ symbol: mintB?.symbol,
686
+ decimals: decimalsB,
687
+ ...(priceBUsd !== null && { priceUsd: priceBUsd })
688
+ },
689
+ tickLower: position.tickLower,
690
+ tickUpper: position.tickUpper,
691
+ priceLower: priceLower.toString(),
692
+ priceUpper: priceUpper.toString(),
693
+ currentTick,
694
+ currentPrice: currentPrice.toString(),
695
+ inRange,
696
+ liquidity: position.liquidity?.toString() ?? "0",
697
+ amount0: amounts.amount0.toString(),
698
+ amount1: amounts.amount1.toString(),
699
+ ...(usdValue !== null && { usdValue: usdValue.toNumber() }),
700
+ feesOwed0: position.tokenFeesOwedA?.toString() ?? "0",
701
+ feesOwed1: position.tokenFeesOwedB?.toString() ?? "0",
702
+ rewardInfos: position.rewardInfos?.map((r) => ({
703
+ rewardAmountOwed: r.rewardAmountOwed?.toString() ?? "0"
704
+ })) ?? []
705
+ });
706
+ return;
707
+ }
708
+ (0, output_1.logInfo)(`Position: ${nftMint.toBase58()}`);
709
+ (0, output_1.logInfo)(`Pool: ${position.poolId?.toBase58() ?? "unknown"}`);
710
+ (0, output_1.logInfo)("");
711
+ (0, output_1.logInfo)("Tokens:");
712
+ (0, output_1.logInfo)(` ${symbolA}: ${mintA?.address} (${decimalsA} decimals)`);
713
+ if (priceAUsd !== null)
714
+ (0, output_1.logInfo)(` Price: ${(0, clmm_utils_1.formatUsd)(priceAUsd)}`);
715
+ (0, output_1.logInfo)(` ${symbolB}: ${mintB?.address} (${decimalsB} decimals)`);
716
+ if (priceBUsd !== null)
717
+ (0, output_1.logInfo)(` Price: ${(0, clmm_utils_1.formatUsd)(priceBUsd)}`);
718
+ (0, output_1.logInfo)("");
719
+ (0, output_1.logInfo)("Range:");
720
+ (0, output_1.logInfo)(` Tick range: [${position.tickLower}, ${position.tickUpper}]`);
721
+ (0, output_1.logInfo)(` Price range: ${(0, clmm_utils_1.formatPrice)(priceLower)} - ${(0, clmm_utils_1.formatPrice)(priceUpper)} ${symbolB}/${symbolA}`);
722
+ (0, output_1.logInfo)(` Current tick: ${currentTick}`);
723
+ (0, output_1.logInfo)(` Current price: ${(0, clmm_utils_1.formatPrice)(currentPrice)} ${symbolB}/${symbolA}`);
724
+ (0, output_1.logInfo)(` Status: ${inRange ? "IN RANGE" : "OUT OF RANGE"}`);
725
+ (0, output_1.logInfo)("");
726
+ (0, output_1.logInfo)("Liquidity:");
727
+ (0, output_1.logInfo)(` Total: ${position.liquidity?.toString() ?? "0"}`);
728
+ (0, output_1.logInfo)(` ${symbolA}: ${(0, clmm_utils_1.formatTokenAmount)(amounts.amount0)}`);
729
+ (0, output_1.logInfo)(` ${symbolB}: ${(0, clmm_utils_1.formatTokenAmount)(amounts.amount1)}`);
730
+ if (usdValue !== null)
731
+ (0, output_1.logInfo)(` Value: ${(0, clmm_utils_1.formatUsd)(usdValue)}`);
732
+ (0, output_1.logInfo)("");
733
+ (0, output_1.logInfo)("Fees Owed:");
734
+ (0, output_1.logInfo)(` ${symbolA}: ${position.tokenFeesOwedA?.toString() ?? "0"}`);
735
+ (0, output_1.logInfo)(` ${symbolB}: ${position.tokenFeesOwedB?.toString() ?? "0"}`);
736
+ if (position.rewardInfos?.length) {
737
+ (0, output_1.logInfo)("");
738
+ (0, output_1.logInfo)("Rewards:");
739
+ for (let i = 0; i < position.rewardInfos.length; i++) {
740
+ const reward = position.rewardInfos[i];
741
+ if (reward.rewardAmountOwed && !reward.rewardAmountOwed.isZero?.()) {
742
+ (0, output_1.logInfo)(` Reward ${i + 1}: ${reward.rewardAmountOwed?.toString() ?? "0"}`);
743
+ }
744
+ }
745
+ }
746
+ }
747
+ // Helper function to get priority fee config
748
+ function getPriorityFeeConfig(priorityFeeSol) {
749
+ if (priorityFeeSol <= 0)
750
+ return undefined;
751
+ const DEFAULT_COMPUTE_UNITS = 600000;
752
+ const priorityFeeLamports = priorityFeeSol * 1e9;
753
+ const priorityFeeMicroLamports = Math.round((priorityFeeLamports * 1e6) / DEFAULT_COMPUTE_UNITS);
754
+ return { units: DEFAULT_COMPUTE_UNITS, microLamports: priorityFeeMicroLamports };
755
+ }
756
+ // Helper to find position by NFT mint
757
+ function findPositionByNftMint(positions, nftMint) {
758
+ const nftMintStr = nftMint.toBase58();
759
+ return positions.find((p) => p.nftMint?.toBase58() === nftMintStr);
760
+ }
761
+ // Helper to check if position has unclaimed fees
762
+ function hasUnclaimedFees(position) {
763
+ const feesA = position.tokenFeesOwedA;
764
+ const feesB = position.tokenFeesOwedB;
765
+ const hasFeesA = feesA && !feesA.isZero();
766
+ const hasFeesB = feesB && !feesB.isZero();
767
+ return hasFeesA || hasFeesB;
768
+ }
769
+ async function handleCollectFeesCommand(options) {
770
+ if (!options.nftMint && !options.all) {
771
+ (0, output_1.logError)("Must specify --nft-mint <address> or --all");
772
+ process.exitCode = 1;
773
+ return;
774
+ }
775
+ const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
776
+ const walletName = config.activeWallet;
777
+ if (!walletName) {
778
+ (0, output_1.logError)("No active wallet set");
779
+ (0, output_1.logInfo)("Set an active wallet with: raydium wallet set <name>");
780
+ process.exitCode = 1;
781
+ return;
782
+ }
783
+ const priorityFeeSol = options.priorityFee ? Number(options.priorityFee) : config["priority-fee"];
784
+ if (!Number.isFinite(priorityFeeSol) || priorityFeeSol < 0) {
785
+ (0, output_1.logError)("Invalid priority fee");
786
+ process.exitCode = 1;
787
+ return;
788
+ }
789
+ let nftMint;
790
+ if (options.nftMint) {
791
+ try {
792
+ nftMint = new web3_js_1.PublicKey(options.nftMint);
793
+ }
794
+ catch {
795
+ (0, output_1.logError)("Invalid NFT mint address");
796
+ process.exitCode = 1;
797
+ return;
798
+ }
799
+ }
800
+ const password = await (0, prompt_1.promptPassword)("Enter wallet password");
801
+ let owner;
802
+ try {
803
+ owner = await (0, wallet_manager_1.decryptWallet)(walletName, password);
804
+ }
805
+ catch (error) {
806
+ (0, output_1.logError)("Failed to decrypt wallet", error.message);
807
+ process.exitCode = 1;
808
+ return;
809
+ }
810
+ const raydium = await (0, output_1.withSpinner)("Loading SDK", () => (0, raydium_client_1.loadRaydium)({ owner, disableLoadToken: true }));
811
+ const isDevnet = raydium.cluster !== "mainnet";
812
+ const clmmProgramId = isDevnet ? raydium_sdk_v2_1.DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID : raydium_sdk_v2_1.CLMM_PROGRAM_ID;
813
+ // Fetch all positions
814
+ let positions;
815
+ try {
816
+ positions = await (0, output_1.withSpinner)("Fetching positions", () => raydium.clmm.getOwnerPositionInfo({ programId: clmmProgramId }));
817
+ }
818
+ catch (error) {
819
+ const msg = error instanceof Error ? error.message : String(error);
820
+ (0, output_1.logError)("Failed to fetch positions", msg);
821
+ process.exitCode = 1;
822
+ return;
823
+ }
824
+ if (!positions || positions.length === 0) {
825
+ (0, output_1.logError)("No CLMM positions found");
826
+ process.exitCode = 1;
827
+ return;
828
+ }
829
+ // Filter positions
830
+ let targetPositions;
831
+ if (options.all) {
832
+ targetPositions = positions.filter(hasUnclaimedFees);
833
+ if (targetPositions.length === 0) {
834
+ (0, output_1.logInfo)("No positions with unclaimed fees found");
835
+ return;
836
+ }
837
+ }
838
+ else {
839
+ const position = findPositionByNftMint(positions, nftMint);
840
+ if (!position) {
841
+ (0, output_1.logError)(`Position not found for NFT mint: ${nftMint.toBase58()}`);
842
+ process.exitCode = 1;
843
+ return;
844
+ }
845
+ if (!hasUnclaimedFees(position)) {
846
+ (0, output_1.logInfo)("No fees to collect for this position");
847
+ return;
848
+ }
849
+ targetPositions = [position];
850
+ }
851
+ // Show preview
852
+ (0, output_1.logInfo)(`Positions to collect fees from: ${targetPositions.length}`);
853
+ (0, output_1.logInfo)("");
854
+ for (const pos of targetPositions) {
855
+ const mintA = pos.poolInfo?.mintA;
856
+ const mintB = pos.poolInfo?.mintB;
857
+ const symbolA = mintA?.symbol || mintA?.address?.slice(0, 6) || "token0";
858
+ const symbolB = mintB?.symbol || mintB?.address?.slice(0, 6) || "token1";
859
+ (0, output_1.logInfo)(`Position: ${pos.nftMint.toBase58()}`);
860
+ (0, output_1.logInfo)(` Fees owed: ${pos.tokenFeesOwedA?.toString() ?? "0"} ${symbolA}, ${pos.tokenFeesOwedB?.toString() ?? "0"} ${symbolB}`);
861
+ }
862
+ (0, output_1.logInfo)("");
863
+ const ok = await (0, prompt_1.promptConfirm)("Proceed with collecting fees?", false);
864
+ if (!ok) {
865
+ (0, output_1.logInfo)("Cancelled");
866
+ return;
867
+ }
868
+ const computeBudgetConfig = getPriorityFeeConfig(priorityFeeSol);
869
+ const results = [];
870
+ for (const pos of targetPositions) {
871
+ try {
872
+ // Collect fees by calling decreaseLiquidity with 0 liquidity
873
+ // This collects fees without removing any liquidity
874
+ const poolData = await raydium.clmm.getPoolInfoFromRpc(pos.poolId.toBase58());
875
+ if (!poolData.poolInfo) {
876
+ throw new Error("Pool not found");
877
+ }
878
+ // Populate rewardDefaultInfos from on-chain data (needed for pools with rewards)
879
+ populateRewardDefaultInfos(poolData.poolInfo);
880
+ const txData = await (0, output_1.withSpinner)(`Building transaction for ${pos.nftMint.toBase58().slice(0, 8)}...`, () => raydium.clmm.decreaseLiquidity({
881
+ poolInfo: poolData.poolInfo,
882
+ ownerPosition: pos,
883
+ ownerInfo: {
884
+ useSOLBalance: true,
885
+ closePosition: false
886
+ },
887
+ liquidity: new bn_js_1.default(0), // 0 liquidity = collect fees only
888
+ amountMinA: new bn_js_1.default(0),
889
+ amountMinB: new bn_js_1.default(0),
890
+ txVersion: raydium_sdk_v2_1.TxVersion.V0,
891
+ computeBudgetConfig
892
+ }));
893
+ const result = await (0, output_1.withSpinner)(`Sending transaction for ${pos.nftMint.toBase58().slice(0, 8)}...`, () => txData.execute({ sendAndConfirm: true }));
894
+ results.push({ nftMint: pos.nftMint.toBase58(), txId: result.txId });
895
+ }
896
+ catch (error) {
897
+ const msg = error instanceof Error ? error.message : String(error);
898
+ results.push({ nftMint: pos.nftMint.toBase58(), error: msg });
899
+ }
900
+ }
901
+ // Output results
902
+ if ((0, output_1.isJsonOutput)()) {
903
+ (0, output_1.logJson)({ results });
904
+ }
905
+ else {
906
+ (0, output_1.logInfo)("");
907
+ for (const r of results) {
908
+ if (r.txId) {
909
+ (0, output_1.logSuccess)(`${r.nftMint}: ${r.txId}`);
910
+ }
911
+ else {
912
+ (0, output_1.logError)(`${r.nftMint}: ${r.error}`);
913
+ }
914
+ }
915
+ }
916
+ }
917
+ async function handleClosePositionCommand(options) {
918
+ const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
919
+ const walletName = config.activeWallet;
920
+ if (!walletName) {
921
+ (0, output_1.logError)("No active wallet set");
922
+ (0, output_1.logInfo)("Set an active wallet with: raydium wallet set <name>");
923
+ process.exitCode = 1;
924
+ return;
925
+ }
926
+ let nftMint;
927
+ try {
928
+ nftMint = new web3_js_1.PublicKey(options.nftMint);
929
+ }
930
+ catch {
931
+ (0, output_1.logError)("Invalid NFT mint address");
932
+ process.exitCode = 1;
933
+ return;
934
+ }
935
+ const slippagePercent = options.slippage ? Number(options.slippage) : config["default-slippage"];
936
+ if (!Number.isFinite(slippagePercent) || slippagePercent < 0) {
937
+ (0, output_1.logError)("Invalid slippage percent");
938
+ process.exitCode = 1;
939
+ return;
940
+ }
941
+ const priorityFeeSol = options.priorityFee ? Number(options.priorityFee) : config["priority-fee"];
942
+ if (!Number.isFinite(priorityFeeSol) || priorityFeeSol < 0) {
943
+ (0, output_1.logError)("Invalid priority fee");
944
+ process.exitCode = 1;
945
+ return;
946
+ }
947
+ const password = await (0, prompt_1.promptPassword)("Enter wallet password");
948
+ let owner;
949
+ try {
950
+ owner = await (0, wallet_manager_1.decryptWallet)(walletName, password);
951
+ }
952
+ catch (error) {
953
+ (0, output_1.logError)("Failed to decrypt wallet", error.message);
954
+ process.exitCode = 1;
955
+ return;
956
+ }
957
+ const raydium = await (0, output_1.withSpinner)("Loading SDK", () => (0, raydium_client_1.loadRaydium)({ owner, disableLoadToken: true }));
958
+ const isDevnet = raydium.cluster !== "mainnet";
959
+ const clmmProgramId = isDevnet ? raydium_sdk_v2_1.DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID : raydium_sdk_v2_1.CLMM_PROGRAM_ID;
960
+ // Fetch positions
961
+ let positions;
962
+ try {
963
+ positions = await (0, output_1.withSpinner)("Fetching positions", () => raydium.clmm.getOwnerPositionInfo({ programId: clmmProgramId }));
964
+ }
965
+ catch (error) {
966
+ const msg = error instanceof Error ? error.message : String(error);
967
+ (0, output_1.logError)("Failed to fetch positions", msg);
968
+ process.exitCode = 1;
969
+ return;
970
+ }
971
+ const position = findPositionByNftMint(positions, nftMint);
972
+ if (!position) {
973
+ (0, output_1.logError)(`Position not found for NFT mint: ${nftMint.toBase58()}`);
974
+ process.exitCode = 1;
975
+ return;
976
+ }
977
+ const hasLiquidity = position.liquidity && !position.liquidity.isZero();
978
+ if (hasLiquidity && !options.force) {
979
+ (0, output_1.logError)("Position still has liquidity. Use --force to remove liquidity and close.");
980
+ (0, output_1.logInfo)(`Current liquidity: ${position.liquidity.toString()}`);
981
+ process.exitCode = 1;
982
+ return;
983
+ }
984
+ // Fetch pool info
985
+ const poolData = await (0, output_1.withSpinner)("Fetching pool info", () => raydium.clmm.getPoolInfoFromRpc(position.poolId.toBase58()));
986
+ if (!poolData.poolInfo) {
987
+ (0, output_1.logError)("Pool not found");
988
+ process.exitCode = 1;
989
+ return;
990
+ }
991
+ const poolInfo = poolData.poolInfo;
992
+ const computePoolInfo = poolData.computePoolInfo;
993
+ // Populate rewardDefaultInfos from on-chain data (needed for pools with rewards)
994
+ populateRewardDefaultInfos(poolInfo);
995
+ const mintA = poolInfo.mintA;
996
+ const mintB = poolInfo.mintB;
997
+ const symbolA = mintA?.symbol || mintA?.address?.slice(0, 6) || "token0";
998
+ const symbolB = mintB?.symbol || mintB?.address?.slice(0, 6) || "token1";
999
+ // Show preview
1000
+ (0, output_1.logInfo)(`Position: ${nftMint.toBase58()}`);
1001
+ (0, output_1.logInfo)(`Pool: ${position.poolId.toBase58()}`);
1002
+ if (hasLiquidity) {
1003
+ const decimalsA = mintA.decimals;
1004
+ const decimalsB = mintB.decimals;
1005
+ const amounts = (0, clmm_utils_1.getAmountsFromLiquidity)(position.liquidity.toString(), computePoolInfo.sqrtPriceX64.toString(), position.tickLower, position.tickUpper, decimalsA, decimalsB);
1006
+ (0, output_1.logInfo)(`Liquidity to remove: ${position.liquidity.toString()}`);
1007
+ (0, output_1.logInfo)(`Expected ${symbolA}: ${(0, clmm_utils_1.formatTokenAmount)(amounts.amount0)}`);
1008
+ (0, output_1.logInfo)(`Expected ${symbolB}: ${(0, clmm_utils_1.formatTokenAmount)(amounts.amount1)}`);
1009
+ (0, output_1.logInfo)(`Slippage: ${slippagePercent}%`);
1010
+ }
1011
+ if (hasUnclaimedFees(position)) {
1012
+ (0, output_1.logInfo)(`Fees to collect: ${position.tokenFeesOwedA?.toString() ?? "0"} ${symbolA}, ${position.tokenFeesOwedB?.toString() ?? "0"} ${symbolB}`);
1013
+ }
1014
+ (0, output_1.logInfo)("");
1015
+ const ok = await (0, prompt_1.promptConfirm)("Proceed with closing position?", false);
1016
+ if (!ok) {
1017
+ (0, output_1.logInfo)("Cancelled");
1018
+ return;
1019
+ }
1020
+ const computeBudgetConfig = getPriorityFeeConfig(priorityFeeSol);
1021
+ try {
1022
+ let result;
1023
+ if (hasLiquidity) {
1024
+ // Calculate minimum amounts with slippage
1025
+ const decimalsA = mintA.decimals;
1026
+ const decimalsB = mintB.decimals;
1027
+ const amounts = (0, clmm_utils_1.getAmountsFromLiquidity)(position.liquidity.toString(), computePoolInfo.sqrtPriceX64.toString(), position.tickLower, position.tickUpper, decimalsA, decimalsB);
1028
+ const amountARaw = amounts.amount0.mul(new decimal_js_1.default(10).pow(decimalsA));
1029
+ const amountBRaw = amounts.amount1.mul(new decimal_js_1.default(10).pow(decimalsB));
1030
+ const amountMinA = (0, clmm_utils_1.applySlippage)(new bn_js_1.default(amountARaw.floor().toString()), slippagePercent, true);
1031
+ const amountMinB = (0, clmm_utils_1.applySlippage)(new bn_js_1.default(amountBRaw.floor().toString()), slippagePercent, true);
1032
+ // Decrease liquidity with closePosition flag
1033
+ const txData = await (0, output_1.withSpinner)("Building transaction", () => raydium.clmm.decreaseLiquidity({
1034
+ poolInfo,
1035
+ ownerPosition: position,
1036
+ ownerInfo: {
1037
+ useSOLBalance: true,
1038
+ closePosition: true
1039
+ },
1040
+ liquidity: position.liquidity,
1041
+ amountMinA,
1042
+ amountMinB,
1043
+ txVersion: raydium_sdk_v2_1.TxVersion.V0,
1044
+ computeBudgetConfig
1045
+ }));
1046
+ result = await (0, output_1.withSpinner)("Sending transaction", () => txData.execute({ sendAndConfirm: true }));
1047
+ }
1048
+ else {
1049
+ // Just close the empty position
1050
+ const txData = await (0, output_1.withSpinner)("Building transaction", () => raydium.clmm.closePosition({
1051
+ poolInfo,
1052
+ ownerPosition: position,
1053
+ txVersion: raydium_sdk_v2_1.TxVersion.V0,
1054
+ computeBudgetConfig
1055
+ }));
1056
+ result = await (0, output_1.withSpinner)("Sending transaction", () => txData.execute({ sendAndConfirm: true }));
1057
+ }
1058
+ if ((0, output_1.isJsonOutput)()) {
1059
+ (0, output_1.logJson)({ txId: result.txId });
1060
+ }
1061
+ else {
1062
+ (0, output_1.logSuccess)(`Position closed: ${result.txId}`);
1063
+ }
1064
+ }
1065
+ catch (error) {
1066
+ const msg = error instanceof Error ? error.message : String(error);
1067
+ (0, output_1.logError)("Failed to close position", msg);
1068
+ process.exitCode = 1;
1069
+ }
1070
+ }
1071
+ async function handleDecreaseLiquidityCommand(options) {
1072
+ const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
1073
+ const walletName = config.activeWallet;
1074
+ if (!walletName) {
1075
+ (0, output_1.logError)("No active wallet set");
1076
+ (0, output_1.logInfo)("Set an active wallet with: raydium wallet set <name>");
1077
+ process.exitCode = 1;
1078
+ return;
1079
+ }
1080
+ let nftMint;
1081
+ try {
1082
+ nftMint = new web3_js_1.PublicKey(options.nftMint);
1083
+ }
1084
+ catch {
1085
+ (0, output_1.logError)("Invalid NFT mint address");
1086
+ process.exitCode = 1;
1087
+ return;
1088
+ }
1089
+ const percent = Number(options.percent);
1090
+ if (!Number.isFinite(percent) || percent < 1 || percent > 100) {
1091
+ (0, output_1.logError)("Percent must be between 1 and 100");
1092
+ process.exitCode = 1;
1093
+ return;
1094
+ }
1095
+ const slippagePercent = options.slippage ? Number(options.slippage) : config["default-slippage"];
1096
+ if (!Number.isFinite(slippagePercent) || slippagePercent < 0) {
1097
+ (0, output_1.logError)("Invalid slippage percent");
1098
+ process.exitCode = 1;
1099
+ return;
1100
+ }
1101
+ const priorityFeeSol = options.priorityFee ? Number(options.priorityFee) : config["priority-fee"];
1102
+ if (!Number.isFinite(priorityFeeSol) || priorityFeeSol < 0) {
1103
+ (0, output_1.logError)("Invalid priority fee");
1104
+ process.exitCode = 1;
1105
+ return;
1106
+ }
1107
+ const password = await (0, prompt_1.promptPassword)("Enter wallet password");
1108
+ let owner;
1109
+ try {
1110
+ owner = await (0, wallet_manager_1.decryptWallet)(walletName, password);
1111
+ }
1112
+ catch (error) {
1113
+ (0, output_1.logError)("Failed to decrypt wallet", error.message);
1114
+ process.exitCode = 1;
1115
+ return;
1116
+ }
1117
+ const raydium = await (0, output_1.withSpinner)("Loading SDK", () => (0, raydium_client_1.loadRaydium)({ owner, disableLoadToken: true }));
1118
+ const isDevnet = raydium.cluster !== "mainnet";
1119
+ const clmmProgramId = isDevnet ? raydium_sdk_v2_1.DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID : raydium_sdk_v2_1.CLMM_PROGRAM_ID;
1120
+ // Fetch positions
1121
+ let positions;
1122
+ try {
1123
+ positions = await (0, output_1.withSpinner)("Fetching positions", () => raydium.clmm.getOwnerPositionInfo({ programId: clmmProgramId }));
1124
+ }
1125
+ catch (error) {
1126
+ const msg = error instanceof Error ? error.message : String(error);
1127
+ (0, output_1.logError)("Failed to fetch positions", msg);
1128
+ process.exitCode = 1;
1129
+ return;
1130
+ }
1131
+ const position = findPositionByNftMint(positions, nftMint);
1132
+ if (!position) {
1133
+ (0, output_1.logError)(`Position not found for NFT mint: ${nftMint.toBase58()}`);
1134
+ process.exitCode = 1;
1135
+ return;
1136
+ }
1137
+ if (!position.liquidity || position.liquidity.isZero()) {
1138
+ (0, output_1.logError)("Position has no liquidity to remove");
1139
+ process.exitCode = 1;
1140
+ return;
1141
+ }
1142
+ // Fetch pool info
1143
+ const poolData = await (0, output_1.withSpinner)("Fetching pool info", () => raydium.clmm.getPoolInfoFromRpc(position.poolId.toBase58()));
1144
+ if (!poolData.poolInfo) {
1145
+ (0, output_1.logError)("Pool not found");
1146
+ process.exitCode = 1;
1147
+ return;
1148
+ }
1149
+ const poolInfo = poolData.poolInfo;
1150
+ const computePoolInfo = poolData.computePoolInfo;
1151
+ // Populate rewardDefaultInfos from on-chain data (needed for pools with rewards)
1152
+ populateRewardDefaultInfos(poolInfo);
1153
+ const mintA = poolInfo.mintA;
1154
+ const mintB = poolInfo.mintB;
1155
+ const decimalsA = mintA.decimals;
1156
+ const decimalsB = mintB.decimals;
1157
+ const symbolA = mintA?.symbol || mintA?.address?.slice(0, 6) || "token0";
1158
+ const symbolB = mintB?.symbol || mintB?.address?.slice(0, 6) || "token1";
1159
+ // Calculate liquidity to remove
1160
+ const liquidityToRemove = position.liquidity.mul(new bn_js_1.default(percent)).div(new bn_js_1.default(100));
1161
+ // Calculate expected amounts
1162
+ const amounts = (0, clmm_utils_1.getAmountsFromLiquidity)(liquidityToRemove.toString(), computePoolInfo.sqrtPriceX64.toString(), position.tickLower, position.tickUpper, decimalsA, decimalsB);
1163
+ // Show preview
1164
+ (0, output_1.logInfo)(`Position: ${nftMint.toBase58()}`);
1165
+ (0, output_1.logInfo)(`Pool: ${position.poolId.toBase58()}`);
1166
+ (0, output_1.logInfo)(`Removing: ${percent}% of liquidity`);
1167
+ (0, output_1.logInfo)(`Current liquidity: ${position.liquidity.toString()}`);
1168
+ (0, output_1.logInfo)(`Liquidity to remove: ${liquidityToRemove.toString()}`);
1169
+ (0, output_1.logInfo)(`Expected ${symbolA}: ${(0, clmm_utils_1.formatTokenAmount)(amounts.amount0)}`);
1170
+ (0, output_1.logInfo)(`Expected ${symbolB}: ${(0, clmm_utils_1.formatTokenAmount)(amounts.amount1)}`);
1171
+ (0, output_1.logInfo)(`Slippage: ${slippagePercent}%`);
1172
+ (0, output_1.logInfo)("");
1173
+ const ok = await (0, prompt_1.promptConfirm)("Proceed with removing liquidity?", false);
1174
+ if (!ok) {
1175
+ (0, output_1.logInfo)("Cancelled");
1176
+ return;
1177
+ }
1178
+ const computeBudgetConfig = getPriorityFeeConfig(priorityFeeSol);
1179
+ try {
1180
+ // Calculate minimum amounts with slippage
1181
+ const amountARaw = amounts.amount0.mul(new decimal_js_1.default(10).pow(decimalsA));
1182
+ const amountBRaw = amounts.amount1.mul(new decimal_js_1.default(10).pow(decimalsB));
1183
+ const amountMinA = (0, clmm_utils_1.applySlippage)(new bn_js_1.default(amountARaw.floor().toString()), slippagePercent, true);
1184
+ const amountMinB = (0, clmm_utils_1.applySlippage)(new bn_js_1.default(amountBRaw.floor().toString()), slippagePercent, true);
1185
+ const txData = await (0, output_1.withSpinner)("Building transaction", () => raydium.clmm.decreaseLiquidity({
1186
+ poolInfo,
1187
+ ownerPosition: position,
1188
+ ownerInfo: {
1189
+ useSOLBalance: true,
1190
+ closePosition: false
1191
+ },
1192
+ liquidity: liquidityToRemove,
1193
+ amountMinA,
1194
+ amountMinB,
1195
+ txVersion: raydium_sdk_v2_1.TxVersion.V0,
1196
+ computeBudgetConfig
1197
+ }));
1198
+ const result = await (0, output_1.withSpinner)("Sending transaction", () => txData.execute({ sendAndConfirm: true }));
1199
+ if ((0, output_1.isJsonOutput)()) {
1200
+ (0, output_1.logJson)({ txId: result.txId, liquidityRemoved: liquidityToRemove.toString() });
1201
+ }
1202
+ else {
1203
+ (0, output_1.logSuccess)(`Liquidity removed: ${result.txId}`);
1204
+ }
1205
+ // Swap to SOL if requested
1206
+ if (options.swapToSol) {
1207
+ (0, output_1.logInfo)("");
1208
+ (0, output_1.logInfo)("Swapping withdrawn tokens to SOL...");
1209
+ // Brief pause to let the withdrawal settle
1210
+ await new Promise((r) => setTimeout(r, 2000));
1211
+ const swapSlippage = slippagePercent / 100;
1212
+ const tokensToSwap = [];
1213
+ // Check if we have tokenA to swap (skip if it's SOL)
1214
+ if (mintA.address !== WRAPPED_SOL_MINT && amounts.amount0.gt(0)) {
1215
+ tokensToSwap.push({ mint: mintA, amount: amounts.amount0, symbol: symbolA, decimals: decimalsA });
1216
+ }
1217
+ // Check if we have tokenB to swap (skip if it's SOL)
1218
+ if (mintB.address !== WRAPPED_SOL_MINT && amounts.amount1.gt(0)) {
1219
+ tokensToSwap.push({ mint: mintB, amount: amounts.amount1, symbol: symbolB, decimals: decimalsB });
1220
+ }
1221
+ for (const tokenToSwap of tokensToSwap) {
1222
+ try {
1223
+ // Find swap pool for this token to SOL
1224
+ const swapPoolData = await (0, output_1.withSpinner)(`Finding ${tokenToSwap.symbol}/SOL swap pool`, async () => {
1225
+ const poolsResult = await raydium.api.fetchPoolByMints({
1226
+ mint1: tokenToSwap.mint.address,
1227
+ mint2: WRAPPED_SOL_MINT
1228
+ });
1229
+ const pools = poolsResult.data || [];
1230
+ const ammPool = pools.find((p) => p.programId === "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
1231
+ if (ammPool) {
1232
+ return raydium.liquidity.getPoolInfoFromRpc({ poolId: ammPool.id });
1233
+ }
1234
+ return null;
1235
+ });
1236
+ if (!swapPoolData || !swapPoolData.poolInfo) {
1237
+ (0, output_1.logError)(`No swap pool found for ${tokenToSwap.symbol}/SOL, skipping swap`);
1238
+ continue;
1239
+ }
1240
+ const swapPoolInfo = swapPoolData.poolInfo;
1241
+ const swapPoolKeys = swapPoolData.poolKeys;
1242
+ const swapRpcData = swapPoolData.poolRpcData;
1243
+ // Get actual balance (might differ from expected due to fees/slippage)
1244
+ const actualBalance = await getTokenBalance(raydium.connection, owner.publicKey, tokenToSwap.mint.address);
1245
+ const actualBalanceDecimal = new decimal_js_1.default(actualBalance.toString()).div(new decimal_js_1.default(10).pow(tokenToSwap.decimals));
1246
+ if (actualBalanceDecimal.lte(0)) {
1247
+ (0, output_1.logInfo)(`No ${tokenToSwap.symbol} balance to swap`);
1248
+ continue;
1249
+ }
1250
+ // Calculate expected SOL output
1251
+ const poolMintA = swapPoolInfo.mintA;
1252
+ const poolMintB = swapPoolInfo.mintB;
1253
+ const poolDecimalsA = poolMintA.decimals;
1254
+ const poolDecimalsB = poolMintB.decimals;
1255
+ const reserveA = new decimal_js_1.default(swapRpcData.baseReserve.toString()).div(new decimal_js_1.default(10).pow(poolDecimalsA));
1256
+ const reserveB = new decimal_js_1.default(swapRpcData.quoteReserve.toString()).div(new decimal_js_1.default(10).pow(poolDecimalsB));
1257
+ const tokenIsPoolMintA = poolMintA.address === tokenToSwap.mint.address;
1258
+ const priceOfTokenInSol = tokenIsPoolMintA
1259
+ ? reserveB.div(reserveA)
1260
+ : reserveA.div(reserveB);
1261
+ const estimatedSolOut = actualBalanceDecimal.mul(priceOfTokenInSol);
1262
+ (0, output_1.logInfo)(`Swapping ~${(0, clmm_utils_1.formatTokenAmount)(actualBalanceDecimal)} ${tokenToSwap.symbol} for ~${(0, clmm_utils_1.formatTokenAmount)(estimatedSolOut)} SOL`);
1263
+ const swapAmountRaw = actualBalance;
1264
+ const computeOut = raydium.liquidity.computeAmountOut({
1265
+ poolInfo: {
1266
+ ...swapPoolInfo,
1267
+ baseReserve: swapRpcData.baseReserve,
1268
+ quoteReserve: swapRpcData.quoteReserve,
1269
+ status: swapRpcData.status.toNumber(),
1270
+ version: 4
1271
+ },
1272
+ amountIn: swapAmountRaw,
1273
+ mintIn: tokenToSwap.mint.address,
1274
+ mintOut: WRAPPED_SOL_MINT,
1275
+ slippage: swapSlippage
1276
+ });
1277
+ const swapTxData = await (0, output_1.withSpinner)(`Building ${tokenToSwap.symbol} swap`, () => raydium.liquidity.swap({
1278
+ txVersion: raydium_sdk_v2_1.TxVersion.V0,
1279
+ poolInfo: swapPoolInfo,
1280
+ poolKeys: swapPoolKeys,
1281
+ amountIn: swapAmountRaw,
1282
+ amountOut: computeOut.minAmountOut,
1283
+ inputMint: tokenToSwap.mint.address,
1284
+ fixedSide: "in",
1285
+ config: {
1286
+ associatedOnly: true,
1287
+ inputUseSolBalance: false,
1288
+ outputUseSolBalance: true
1289
+ },
1290
+ computeBudgetConfig
1291
+ }));
1292
+ const swapResult = await (0, output_1.withSpinner)(`Executing ${tokenToSwap.symbol} swap`, () => swapTxData.execute({ sendAndConfirm: true }));
1293
+ (0, output_1.logSuccess)(`${tokenToSwap.symbol} swapped to SOL: ${swapResult.txId}`);
1294
+ // Brief pause between swaps
1295
+ await new Promise((r) => setTimeout(r, 1000));
1296
+ }
1297
+ catch (error) {
1298
+ const msg = error instanceof Error ? error.message : String(error);
1299
+ (0, output_1.logError)(`Failed to swap ${tokenToSwap.symbol} to SOL`, msg);
1300
+ // Continue with other swaps
1301
+ }
1302
+ }
1303
+ }
1304
+ }
1305
+ catch (error) {
1306
+ const msg = error instanceof Error ? error.message : String(error);
1307
+ (0, output_1.logError)("Failed to remove liquidity", msg || "(no message)");
1308
+ console.error("Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error || {}), 2));
1309
+ if (error instanceof Error && error.stack) {
1310
+ console.error("Stack:", error.stack);
1311
+ }
1312
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1313
+ const anyError = error;
1314
+ if (anyError?.logs) {
1315
+ console.error("Transaction logs:", anyError.logs);
1316
+ }
1317
+ if (anyError?.error) {
1318
+ console.error("Inner error:", anyError.error);
1319
+ }
1320
+ process.exitCode = 1;
1321
+ }
1322
+ }
1323
+ async function handleIncreaseLiquidityCommand(options) {
1324
+ const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
1325
+ const walletName = config.activeWallet;
1326
+ if (!walletName) {
1327
+ (0, output_1.logError)("No active wallet set");
1328
+ (0, output_1.logInfo)("Set an active wallet with: raydium wallet set <name>");
1329
+ process.exitCode = 1;
1330
+ return;
1331
+ }
1332
+ let nftMint;
1333
+ try {
1334
+ nftMint = new web3_js_1.PublicKey(options.nftMint);
1335
+ }
1336
+ catch {
1337
+ (0, output_1.logError)("Invalid NFT mint address");
1338
+ process.exitCode = 1;
1339
+ return;
1340
+ }
1341
+ const amount = Number(options.amount);
1342
+ if (!Number.isFinite(amount) || amount <= 0) {
1343
+ (0, output_1.logError)("Amount must be a positive number");
1344
+ process.exitCode = 1;
1345
+ return;
1346
+ }
1347
+ const baseToken = (options.token?.toUpperCase() ?? "A");
1348
+ if (baseToken !== "A" && baseToken !== "B") {
1349
+ (0, output_1.logError)("Token must be A or B");
1350
+ process.exitCode = 1;
1351
+ return;
1352
+ }
1353
+ const slippagePercent = options.slippage ? Number(options.slippage) : config["default-slippage"];
1354
+ if (!Number.isFinite(slippagePercent) || slippagePercent < 0) {
1355
+ (0, output_1.logError)("Invalid slippage percent");
1356
+ process.exitCode = 1;
1357
+ return;
1358
+ }
1359
+ const priorityFeeSol = options.priorityFee ? Number(options.priorityFee) : config["priority-fee"];
1360
+ if (!Number.isFinite(priorityFeeSol) || priorityFeeSol < 0) {
1361
+ (0, output_1.logError)("Invalid priority fee");
1362
+ process.exitCode = 1;
1363
+ return;
1364
+ }
1365
+ const password = await (0, prompt_1.promptPassword)("Enter wallet password");
1366
+ let owner;
1367
+ try {
1368
+ owner = await (0, wallet_manager_1.decryptWallet)(walletName, password);
1369
+ }
1370
+ catch (error) {
1371
+ (0, output_1.logError)("Failed to decrypt wallet", error.message);
1372
+ process.exitCode = 1;
1373
+ return;
1374
+ }
1375
+ const raydium = await (0, output_1.withSpinner)("Loading SDK", () => (0, raydium_client_1.loadRaydium)({ owner, disableLoadToken: true }));
1376
+ const isDevnet = raydium.cluster !== "mainnet";
1377
+ const clmmProgramId = isDevnet ? raydium_sdk_v2_1.DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID : raydium_sdk_v2_1.CLMM_PROGRAM_ID;
1378
+ // Fetch positions
1379
+ let positions;
1380
+ try {
1381
+ positions = await (0, output_1.withSpinner)("Fetching positions", () => raydium.clmm.getOwnerPositionInfo({ programId: clmmProgramId }));
1382
+ }
1383
+ catch (error) {
1384
+ const msg = error instanceof Error ? error.message : String(error);
1385
+ (0, output_1.logError)("Failed to fetch positions", msg);
1386
+ process.exitCode = 1;
1387
+ return;
1388
+ }
1389
+ const position = findPositionByNftMint(positions, nftMint);
1390
+ if (!position) {
1391
+ (0, output_1.logError)(`Position not found for NFT mint: ${nftMint.toBase58()}`);
1392
+ process.exitCode = 1;
1393
+ return;
1394
+ }
1395
+ // Fetch pool info
1396
+ const poolData = await (0, output_1.withSpinner)("Fetching pool info", () => raydium.clmm.getPoolInfoFromRpc(position.poolId.toBase58()));
1397
+ if (!poolData.poolInfo) {
1398
+ (0, output_1.logError)("Pool not found");
1399
+ process.exitCode = 1;
1400
+ return;
1401
+ }
1402
+ const poolInfo = poolData.poolInfo;
1403
+ const computePoolInfo = poolData.computePoolInfo;
1404
+ const mintA = poolInfo.mintA;
1405
+ const mintB = poolInfo.mintB;
1406
+ const decimalsA = mintA.decimals;
1407
+ const decimalsB = mintB.decimals;
1408
+ const symbolA = mintA?.symbol || mintA?.address?.slice(0, 6) || "token0";
1409
+ const symbolB = mintB?.symbol || mintB?.address?.slice(0, 6) || "token1";
1410
+ const inRange = (0, clmm_utils_1.isPositionInRange)(position.tickLower, position.tickUpper, computePoolInfo.tickCurrent);
1411
+ // Calculate base amount in raw units
1412
+ const baseDecimals = baseToken === "A" ? decimalsA : decimalsB;
1413
+ const baseAmount = new bn_js_1.default(new decimal_js_1.default(amount).mul(new decimal_js_1.default(10).pow(baseDecimals)).floor().toString());
1414
+ // Use SDK to calculate liquidity and other amount
1415
+ const epochInfo = await raydium.connection.getEpochInfo();
1416
+ const liquidityInfo = await (0, output_1.withSpinner)("Calculating liquidity", () => raydium_sdk_v2_1.PoolUtils.getLiquidityAmountOutFromAmountIn({
1417
+ poolInfo,
1418
+ inputA: baseToken === "A",
1419
+ tickLower: position.tickLower,
1420
+ tickUpper: position.tickUpper,
1421
+ amount: baseAmount,
1422
+ slippage: slippagePercent / 100,
1423
+ add: true,
1424
+ epochInfo,
1425
+ amountHasFee: false
1426
+ }));
1427
+ const otherAmount = baseToken === "A" ? liquidityInfo.amountB : liquidityInfo.amountA;
1428
+ const otherSlippageAmount = baseToken === "A" ? liquidityInfo.amountSlippageB : liquidityInfo.amountSlippageA;
1429
+ const otherDecimals = baseToken === "A" ? decimalsB : decimalsA;
1430
+ const otherSymbol = baseToken === "A" ? symbolB : symbolA;
1431
+ const baseSymbol = baseToken === "A" ? symbolA : symbolB;
1432
+ const baseMint = baseToken === "A" ? mintA : mintB;
1433
+ const otherMint = baseToken === "A" ? mintB : mintA;
1434
+ const computeBudgetConfig = getPriorityFeeConfig(priorityFeeSol);
1435
+ // Auto-swap logic
1436
+ if (options.autoSwap) {
1437
+ const otherAmountRequired = new decimal_js_1.default(otherSlippageAmount.amount.toString()).div(new decimal_js_1.default(10).pow(otherDecimals));
1438
+ // Check balances
1439
+ const balances = await (0, output_1.withSpinner)("Checking token balances", async () => {
1440
+ const baseBalance = await getTokenBalance(raydium.connection, owner.publicKey, baseMint.address);
1441
+ const otherBalance = await getTokenBalance(raydium.connection, owner.publicKey, otherMint.address);
1442
+ return {
1443
+ baseBalance: new decimal_js_1.default(baseBalance.toString()).div(new decimal_js_1.default(10).pow(baseDecimals)),
1444
+ otherBalance: new decimal_js_1.default(otherBalance.toString()).div(new decimal_js_1.default(10).pow(otherDecimals))
1445
+ };
1446
+ });
1447
+ (0, output_1.logInfo)(`Your balances:`);
1448
+ (0, output_1.logInfo)(` ${baseSymbol}: ${(0, clmm_utils_1.formatTokenAmount)(balances.baseBalance)}`);
1449
+ (0, output_1.logInfo)(` ${otherSymbol}: ${(0, clmm_utils_1.formatTokenAmount)(balances.otherBalance)}`);
1450
+ (0, output_1.logInfo)("");
1451
+ const otherShortfall = otherAmountRequired.sub(balances.otherBalance);
1452
+ if (otherShortfall.gt(0)) {
1453
+ (0, output_1.logInfo)(`Shortfall: ${(0, clmm_utils_1.formatTokenAmount)(otherShortfall)} ${otherSymbol}`);
1454
+ // Find a swap pool
1455
+ const swapPoolData = await (0, output_1.withSpinner)("Finding swap pool", async () => {
1456
+ const poolsResult = await raydium.api.fetchPoolByMints({
1457
+ mint1: baseMint.address,
1458
+ mint2: otherMint.address
1459
+ });
1460
+ const pools = poolsResult.data || [];
1461
+ const ammPool = pools.find((p) => p.programId === "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
1462
+ if (ammPool) {
1463
+ return raydium.liquidity.getPoolInfoFromRpc({ poolId: ammPool.id });
1464
+ }
1465
+ return null;
1466
+ });
1467
+ if (!swapPoolData || !swapPoolData.poolInfo) {
1468
+ (0, output_1.logError)(`No swap pool found for ${baseSymbol}/${otherSymbol}`);
1469
+ (0, output_1.logInfo)("You can manually swap tokens first, then retry without --auto-swap");
1470
+ process.exitCode = 1;
1471
+ return;
1472
+ }
1473
+ const swapPoolInfo = swapPoolData.poolInfo;
1474
+ const swapPoolKeys = swapPoolData.poolKeys;
1475
+ const swapRpcData = swapPoolData.poolRpcData;
1476
+ (0, output_1.logInfo)(`Using swap pool: ${swapPoolInfo.id}`);
1477
+ const swapSlippage = slippagePercent / 100;
1478
+ const swapAmountNeeded = otherShortfall.mul(1 + swapSlippage);
1479
+ const poolMintA = swapPoolInfo.mintA;
1480
+ const poolMintB = swapPoolInfo.mintB;
1481
+ const poolDecimalsA = poolMintA.decimals;
1482
+ const poolDecimalsB = poolMintB.decimals;
1483
+ const reserveA = new decimal_js_1.default(swapRpcData.baseReserve.toString()).div(new decimal_js_1.default(10).pow(poolDecimalsA));
1484
+ const reserveB = new decimal_js_1.default(swapRpcData.quoteReserve.toString()).div(new decimal_js_1.default(10).pow(poolDecimalsB));
1485
+ const baseMintIsPoolMintA = poolMintA.address === baseMint.address;
1486
+ const priceOfBaseInOther = baseMintIsPoolMintA
1487
+ ? reserveB.div(reserveA)
1488
+ : reserveA.div(reserveB);
1489
+ const estimatedSwapIn = swapAmountNeeded.div(priceOfBaseInOther).mul(1.05);
1490
+ (0, output_1.logInfo)(`Swapping ~${(0, clmm_utils_1.formatTokenAmount)(estimatedSwapIn)} ${baseSymbol} for ~${(0, clmm_utils_1.formatTokenAmount)(swapAmountNeeded)} ${otherSymbol}`);
1491
+ (0, output_1.logInfo)("");
1492
+ const swapOk = await (0, prompt_1.promptConfirm)("Proceed with swap first?", false);
1493
+ if (!swapOk) {
1494
+ (0, output_1.logInfo)("Cancelled");
1495
+ return;
1496
+ }
1497
+ try {
1498
+ const swapAmountRaw = new bn_js_1.default(estimatedSwapIn.mul(new decimal_js_1.default(10).pow(baseDecimals)).floor().toString());
1499
+ const computeOut = raydium.liquidity.computeAmountOut({
1500
+ poolInfo: {
1501
+ ...swapPoolInfo,
1502
+ baseReserve: swapRpcData.baseReserve,
1503
+ quoteReserve: swapRpcData.quoteReserve,
1504
+ status: swapRpcData.status.toNumber(),
1505
+ version: 4
1506
+ },
1507
+ amountIn: swapAmountRaw,
1508
+ mintIn: baseMint.address,
1509
+ mintOut: otherMint.address,
1510
+ slippage: swapSlippage
1511
+ });
1512
+ const inputIsSol = baseMint.address === WRAPPED_SOL_MINT;
1513
+ const outputIsSol = otherMint.address === WRAPPED_SOL_MINT;
1514
+ const swapTxData = await (0, output_1.withSpinner)("Building swap transaction", () => raydium.liquidity.swap({
1515
+ txVersion: raydium_sdk_v2_1.TxVersion.V0,
1516
+ poolInfo: swapPoolInfo,
1517
+ poolKeys: swapPoolKeys,
1518
+ amountIn: swapAmountRaw,
1519
+ amountOut: computeOut.minAmountOut,
1520
+ inputMint: baseMint.address,
1521
+ fixedSide: "in",
1522
+ config: {
1523
+ associatedOnly: true,
1524
+ inputUseSolBalance: inputIsSol,
1525
+ outputUseSolBalance: outputIsSol
1526
+ },
1527
+ computeBudgetConfig
1528
+ }));
1529
+ const swapResult = await (0, output_1.withSpinner)("Executing swap", () => swapTxData.execute({ sendAndConfirm: true }));
1530
+ (0, output_1.logSuccess)(`Swap completed: ${swapResult.txId}`);
1531
+ (0, output_1.logInfo)("");
1532
+ await new Promise((r) => setTimeout(r, 2000));
1533
+ }
1534
+ catch (error) {
1535
+ const msg = error instanceof Error ? error.message : String(error);
1536
+ (0, output_1.logError)("Swap failed", msg);
1537
+ process.exitCode = 1;
1538
+ return;
1539
+ }
1540
+ }
1541
+ }
1542
+ // Show preview
1543
+ (0, output_1.logInfo)(`Position: ${nftMint.toBase58()}`);
1544
+ (0, output_1.logInfo)(`Pool: ${position.poolId.toBase58()}`);
1545
+ (0, output_1.logInfo)(`Status: ${inRange ? "IN RANGE" : "OUT OF RANGE"}`);
1546
+ (0, output_1.logInfo)(`Current liquidity: ${position.liquidity.toString()}`);
1547
+ (0, output_1.logInfo)("");
1548
+ (0, output_1.logInfo)(`Adding:`);
1549
+ (0, output_1.logInfo)(` ${baseSymbol}: ${amount}`);
1550
+ (0, output_1.logInfo)(` ${otherSymbol}: ${(0, clmm_utils_1.formatTokenAmount)(new decimal_js_1.default(otherAmount.amount.toString()).div(new decimal_js_1.default(10).pow(otherDecimals)))}`);
1551
+ (0, output_1.logInfo)(` Max ${otherSymbol} (with slippage): ${(0, clmm_utils_1.formatTokenAmount)(new decimal_js_1.default(otherSlippageAmount.amount.toString()).div(new decimal_js_1.default(10).pow(otherDecimals)))}`);
1552
+ (0, output_1.logInfo)(`Slippage: ${slippagePercent}%`);
1553
+ (0, output_1.logInfo)("");
1554
+ const ok = await (0, prompt_1.promptConfirm)("Proceed with adding liquidity?", false);
1555
+ if (!ok) {
1556
+ (0, output_1.logInfo)("Cancelled");
1557
+ return;
1558
+ }
1559
+ try {
1560
+ // The SDK types expect ClmmPoolPersonalPosition but only uses tickLower, tickUpper, nftMint
1561
+ // which all exist on ClmmPositionLayout, so we can safely cast
1562
+ const txData = await (0, output_1.withSpinner)("Building transaction", () => raydium.clmm.increasePositionFromBase({
1563
+ poolInfo,
1564
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1565
+ ownerPosition: position,
1566
+ ownerInfo: {
1567
+ useSOLBalance: true
1568
+ },
1569
+ base: baseToken === "A" ? "MintA" : "MintB",
1570
+ baseAmount,
1571
+ otherAmountMax: otherSlippageAmount.amount,
1572
+ txVersion: raydium_sdk_v2_1.TxVersion.V0,
1573
+ computeBudgetConfig
1574
+ }));
1575
+ const result = await (0, output_1.withSpinner)("Sending transaction", () => txData.execute({ sendAndConfirm: true }));
1576
+ if ((0, output_1.isJsonOutput)()) {
1577
+ (0, output_1.logJson)({ txId: result.txId });
1578
+ }
1579
+ else {
1580
+ (0, output_1.logSuccess)(`Liquidity added: ${result.txId}`);
1581
+ }
1582
+ }
1583
+ catch (error) {
1584
+ const msg = error instanceof Error ? error.message : String(error);
1585
+ (0, output_1.logError)("Failed to add liquidity", msg);
1586
+ process.exitCode = 1;
1587
+ }
1588
+ }
1589
+ async function handleOpenPositionCommand(options) {
1590
+ const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
1591
+ const walletName = config.activeWallet;
1592
+ if (!walletName) {
1593
+ (0, output_1.logError)("No active wallet set");
1594
+ (0, output_1.logInfo)("Set an active wallet with: raydium wallet set <name>");
1595
+ process.exitCode = 1;
1596
+ return;
1597
+ }
1598
+ let poolId;
1599
+ try {
1600
+ poolId = new web3_js_1.PublicKey(options.poolId);
1601
+ }
1602
+ catch {
1603
+ (0, output_1.logError)("Invalid pool ID");
1604
+ process.exitCode = 1;
1605
+ return;
1606
+ }
1607
+ const priceLower = Number(options.priceLower);
1608
+ const priceUpper = Number(options.priceUpper);
1609
+ if (!Number.isFinite(priceLower) || !Number.isFinite(priceUpper) || priceLower <= 0 || priceUpper <= 0) {
1610
+ (0, output_1.logError)("Price bounds must be positive numbers");
1611
+ process.exitCode = 1;
1612
+ return;
1613
+ }
1614
+ if (priceLower >= priceUpper) {
1615
+ (0, output_1.logError)("Lower price must be less than upper price");
1616
+ process.exitCode = 1;
1617
+ return;
1618
+ }
1619
+ const amount = Number(options.amount);
1620
+ if (!Number.isFinite(amount) || amount <= 0) {
1621
+ (0, output_1.logError)("Amount must be a positive number");
1622
+ process.exitCode = 1;
1623
+ return;
1624
+ }
1625
+ const baseToken = (options.token?.toUpperCase() ?? "A");
1626
+ if (baseToken !== "A" && baseToken !== "B") {
1627
+ (0, output_1.logError)("Token must be A or B");
1628
+ process.exitCode = 1;
1629
+ return;
1630
+ }
1631
+ const slippagePercent = options.slippage ? Number(options.slippage) : config["default-slippage"];
1632
+ if (!Number.isFinite(slippagePercent) || slippagePercent < 0) {
1633
+ (0, output_1.logError)("Invalid slippage percent");
1634
+ process.exitCode = 1;
1635
+ return;
1636
+ }
1637
+ const priorityFeeSol = options.priorityFee ? Number(options.priorityFee) : config["priority-fee"];
1638
+ if (!Number.isFinite(priorityFeeSol) || priorityFeeSol < 0) {
1639
+ (0, output_1.logError)("Invalid priority fee");
1640
+ process.exitCode = 1;
1641
+ return;
1642
+ }
1643
+ const computeBudgetConfig = getPriorityFeeConfig(priorityFeeSol);
1644
+ const password = await (0, prompt_1.promptPassword)("Enter wallet password");
1645
+ let owner;
1646
+ try {
1647
+ owner = await (0, wallet_manager_1.decryptWallet)(walletName, password);
1648
+ }
1649
+ catch (error) {
1650
+ (0, output_1.logError)("Failed to decrypt wallet", error.message);
1651
+ process.exitCode = 1;
1652
+ return;
1653
+ }
1654
+ const raydium = await (0, output_1.withSpinner)("Loading SDK", () => (0, raydium_client_1.loadRaydium)({ owner, disableLoadToken: true }));
1655
+ // Fetch pool info
1656
+ const poolData = await (0, output_1.withSpinner)("Fetching pool info", () => raydium.clmm.getPoolInfoFromRpc(poolId.toBase58()));
1657
+ if (!poolData.poolInfo) {
1658
+ (0, output_1.logError)("Pool not found");
1659
+ process.exitCode = 1;
1660
+ return;
1661
+ }
1662
+ if (!VALID_CLMM_PROGRAM_IDS.has(poolData.poolInfo.programId)) {
1663
+ (0, output_1.logError)("Not a CLMM pool");
1664
+ process.exitCode = 1;
1665
+ return;
1666
+ }
1667
+ const poolInfo = poolData.poolInfo;
1668
+ const computePoolInfo = poolData.computePoolInfo;
1669
+ const mintA = poolInfo.mintA;
1670
+ const mintB = poolInfo.mintB;
1671
+ const decimalsA = mintA.decimals;
1672
+ const decimalsB = mintB.decimals;
1673
+ const symbolA = mintA?.symbol || mintA?.address?.slice(0, 6) || "token0";
1674
+ const symbolB = mintB?.symbol || mintB?.address?.slice(0, 6) || "token1";
1675
+ const tickSpacing = computePoolInfo.tickSpacing;
1676
+ // Convert prices to ticks aligned with tick spacing
1677
+ const tickLower = (0, clmm_utils_1.priceToAlignedTick)(new decimal_js_1.default(priceLower), tickSpacing, decimalsA, decimalsB);
1678
+ const tickUpper = (0, clmm_utils_1.priceToAlignedTick)(new decimal_js_1.default(priceUpper), tickSpacing, decimalsA, decimalsB);
1679
+ // Calculate actual prices from aligned ticks
1680
+ const actualPriceLower = (0, clmm_utils_1.tickToPrice)(tickLower, decimalsA, decimalsB);
1681
+ const actualPriceUpper = (0, clmm_utils_1.tickToPrice)(tickUpper, decimalsA, decimalsB);
1682
+ const currentPrice = (0, clmm_utils_1.sqrtPriceX64ToPrice)(computePoolInfo.sqrtPriceX64.toString(), decimalsA, decimalsB);
1683
+ const inRange = computePoolInfo.tickCurrent >= tickLower && computePoolInfo.tickCurrent < tickUpper;
1684
+ // Calculate base amount in raw units
1685
+ const baseDecimals = baseToken === "A" ? decimalsA : decimalsB;
1686
+ const baseAmount = new bn_js_1.default(new decimal_js_1.default(amount).mul(new decimal_js_1.default(10).pow(baseDecimals)).floor().toString());
1687
+ // Use SDK to calculate liquidity and other amount
1688
+ const epochInfo = await raydium.connection.getEpochInfo();
1689
+ const liquidityInfo = await (0, output_1.withSpinner)("Calculating liquidity", () => raydium_sdk_v2_1.PoolUtils.getLiquidityAmountOutFromAmountIn({
1690
+ poolInfo,
1691
+ inputA: baseToken === "A",
1692
+ tickLower,
1693
+ tickUpper,
1694
+ amount: baseAmount,
1695
+ slippage: slippagePercent / 100,
1696
+ add: true,
1697
+ epochInfo,
1698
+ amountHasFee: false
1699
+ }));
1700
+ const otherAmount = baseToken === "A" ? liquidityInfo.amountB : liquidityInfo.amountA;
1701
+ const otherSlippageAmount = baseToken === "A" ? liquidityInfo.amountSlippageB : liquidityInfo.amountSlippageA;
1702
+ const otherDecimals = baseToken === "A" ? decimalsB : decimalsA;
1703
+ const otherSymbol = baseToken === "A" ? symbolB : symbolA;
1704
+ const baseSymbol = baseToken === "A" ? symbolA : symbolB;
1705
+ const otherMint = baseToken === "A" ? mintB : mintA;
1706
+ const baseMint = baseToken === "A" ? mintA : mintB;
1707
+ // Calculate required amounts in human-readable format
1708
+ const otherAmountRequired = new decimal_js_1.default(otherSlippageAmount.amount.toString()).div(new decimal_js_1.default(10).pow(otherDecimals));
1709
+ const baseAmountRequired = new decimal_js_1.default(amount);
1710
+ // Check balances if auto-swap is enabled
1711
+ if (options.autoSwap) {
1712
+ const WRAPPED_SOL_MINT = "So11111111111111111111111111111111111111112";
1713
+ // Get user's token balances
1714
+ const balances = await (0, output_1.withSpinner)("Checking token balances", async () => {
1715
+ const solBalance = await raydium.connection.getBalance(owner.publicKey);
1716
+ const solBalanceDecimal = new decimal_js_1.default(solBalance).div(1e9);
1717
+ // Check for the "other" token balance
1718
+ let otherBalance = new decimal_js_1.default(0);
1719
+ if (otherMint.address === WRAPPED_SOL_MINT) {
1720
+ otherBalance = solBalanceDecimal;
1721
+ }
1722
+ else {
1723
+ try {
1724
+ const tokenAccounts = await raydium.connection.getTokenAccountsByOwner(owner.publicKey, {
1725
+ mint: new web3_js_1.PublicKey(otherMint.address)
1726
+ });
1727
+ if (tokenAccounts.value.length > 0) {
1728
+ // Parse token account data to get balance
1729
+ const accountData = tokenAccounts.value[0].account.data;
1730
+ // Token account: first 32 bytes mint, next 32 bytes owner, next 8 bytes amount
1731
+ const amountBytes = accountData.slice(64, 72);
1732
+ const rawAmount = new bn_js_1.default(amountBytes, "le");
1733
+ otherBalance = new decimal_js_1.default(rawAmount.toString()).div(new decimal_js_1.default(10).pow(otherDecimals));
1734
+ }
1735
+ }
1736
+ catch {
1737
+ // No token account found
1738
+ }
1739
+ }
1740
+ // Check base token balance
1741
+ let baseBalance = new decimal_js_1.default(0);
1742
+ if (baseMint.address === WRAPPED_SOL_MINT) {
1743
+ baseBalance = solBalanceDecimal;
1744
+ }
1745
+ else {
1746
+ try {
1747
+ const tokenAccounts = await raydium.connection.getTokenAccountsByOwner(owner.publicKey, {
1748
+ mint: new web3_js_1.PublicKey(baseMint.address)
1749
+ });
1750
+ if (tokenAccounts.value.length > 0) {
1751
+ const accountData = tokenAccounts.value[0].account.data;
1752
+ const amountBytes = accountData.slice(64, 72);
1753
+ const rawAmount = new bn_js_1.default(amountBytes, "le");
1754
+ baseBalance = new decimal_js_1.default(rawAmount.toString()).div(new decimal_js_1.default(10).pow(baseDecimals));
1755
+ }
1756
+ }
1757
+ catch {
1758
+ // No token account found
1759
+ }
1760
+ }
1761
+ return { solBalance: solBalanceDecimal, otherBalance, baseBalance };
1762
+ });
1763
+ (0, output_1.logInfo)(`Your balances:`);
1764
+ (0, output_1.logInfo)(` ${baseSymbol}: ${(0, clmm_utils_1.formatTokenAmount)(balances.baseBalance)}`);
1765
+ (0, output_1.logInfo)(` ${otherSymbol}: ${(0, clmm_utils_1.formatTokenAmount)(balances.otherBalance)}`);
1766
+ (0, output_1.logInfo)("");
1767
+ // Check if we need to swap
1768
+ const otherShortfall = otherAmountRequired.sub(balances.otherBalance);
1769
+ if (otherShortfall.gt(0)) {
1770
+ (0, output_1.logInfo)(`Shortfall: ${(0, clmm_utils_1.formatTokenAmount)(otherShortfall)} ${otherSymbol}`);
1771
+ // Find a swap pool and execute swap
1772
+ // Use Raydium API to find pools containing both tokens
1773
+ const swapPoolData = await (0, output_1.withSpinner)("Finding swap pool", async () => {
1774
+ // Search for pools with both tokens
1775
+ const poolsResult = await raydium.api.fetchPoolByMints({
1776
+ mint1: baseMint.address,
1777
+ mint2: otherMint.address
1778
+ });
1779
+ const pools = poolsResult.data || [];
1780
+ // Find an AMM V4 pool (standard swap pool)
1781
+ const ammPool = pools.find((p) => p.programId === "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
1782
+ if (ammPool) {
1783
+ // Get RPC data for the pool
1784
+ return raydium.liquidity.getPoolInfoFromRpc({ poolId: ammPool.id });
1785
+ }
1786
+ return null;
1787
+ });
1788
+ if (!swapPoolData || !swapPoolData.poolInfo) {
1789
+ (0, output_1.logError)(`No swap pool found for ${baseSymbol}/${otherSymbol}`);
1790
+ (0, output_1.logInfo)("You can manually swap tokens first, then retry without --auto-swap");
1791
+ process.exitCode = 1;
1792
+ return;
1793
+ }
1794
+ const swapPoolInfo = swapPoolData.poolInfo;
1795
+ const swapPoolKeys = swapPoolData.poolKeys;
1796
+ const swapRpcData = swapPoolData.poolRpcData;
1797
+ (0, output_1.logInfo)(`Using swap pool: ${swapPoolInfo.id}`);
1798
+ // Calculate how much of base token to swap (add some buffer for slippage)
1799
+ const swapSlippage = slippagePercent / 100;
1800
+ const swapAmountNeeded = otherShortfall.mul(1 + swapSlippage); // Add buffer
1801
+ // Estimate how much base token we need to swap to get the required other token
1802
+ // Pool reserves are in raw units, need to account for decimals
1803
+ const poolMintA = swapPoolInfo.mintA;
1804
+ const poolMintB = swapPoolInfo.mintB;
1805
+ const poolDecimalsA = poolMintA.decimals;
1806
+ const poolDecimalsB = poolMintB.decimals;
1807
+ // Convert reserves to human-readable amounts
1808
+ const reserveA = new decimal_js_1.default(swapRpcData.baseReserve.toString()).div(new decimal_js_1.default(10).pow(poolDecimalsA));
1809
+ const reserveB = new decimal_js_1.default(swapRpcData.quoteReserve.toString()).div(new decimal_js_1.default(10).pow(poolDecimalsB));
1810
+ // Determine which pool token is our base token (the one we're swapping FROM)
1811
+ const baseMintIsPoolMintA = poolMintA.address === baseMint.address;
1812
+ // Price of baseMint in terms of otherMint
1813
+ // If baseMint is mintA: price = reserveB / reserveA (how much B per A)
1814
+ // If baseMint is mintB: price = reserveA / reserveB (how much A per B)
1815
+ const priceOfBaseInOther = baseMintIsPoolMintA
1816
+ ? reserveB.div(reserveA)
1817
+ : reserveA.div(reserveB);
1818
+ // To get swapAmountNeeded of otherMint, we need: swapAmountNeeded / priceOfBaseInOther of baseMint
1819
+ const estimatedSwapIn = swapAmountNeeded.div(priceOfBaseInOther).mul(1.05); // 5% buffer
1820
+ (0, output_1.logInfo)(`Swapping ~${(0, clmm_utils_1.formatTokenAmount)(estimatedSwapIn)} ${baseSymbol} for ~${(0, clmm_utils_1.formatTokenAmount)(swapAmountNeeded)} ${otherSymbol}`);
1821
+ (0, output_1.logInfo)("");
1822
+ const swapOk = await (0, prompt_1.promptConfirm)("Proceed with swap first?", false);
1823
+ if (!swapOk) {
1824
+ (0, output_1.logInfo)("Cancelled");
1825
+ return;
1826
+ }
1827
+ // Execute the swap
1828
+ try {
1829
+ const swapAmountRaw = new bn_js_1.default(estimatedSwapIn.mul(new decimal_js_1.default(10).pow(baseDecimals)).floor().toString());
1830
+ const computeOut = raydium.liquidity.computeAmountOut({
1831
+ poolInfo: {
1832
+ ...swapPoolInfo,
1833
+ baseReserve: swapRpcData.baseReserve,
1834
+ quoteReserve: swapRpcData.quoteReserve,
1835
+ status: swapRpcData.status.toNumber(),
1836
+ version: 4
1837
+ },
1838
+ amountIn: swapAmountRaw,
1839
+ mintIn: baseMint.address,
1840
+ mintOut: otherMint.address,
1841
+ slippage: swapSlippage
1842
+ });
1843
+ const inputIsSol = baseMint.address === WRAPPED_SOL_MINT;
1844
+ const outputIsSol = otherMint.address === WRAPPED_SOL_MINT;
1845
+ const swapTxData = await (0, output_1.withSpinner)("Building swap transaction", () => raydium.liquidity.swap({
1846
+ txVersion: raydium_sdk_v2_1.TxVersion.V0,
1847
+ poolInfo: swapPoolInfo,
1848
+ poolKeys: swapPoolKeys,
1849
+ amountIn: swapAmountRaw,
1850
+ amountOut: computeOut.minAmountOut,
1851
+ inputMint: baseMint.address,
1852
+ fixedSide: "in",
1853
+ config: {
1854
+ associatedOnly: true,
1855
+ inputUseSolBalance: inputIsSol,
1856
+ outputUseSolBalance: outputIsSol
1857
+ },
1858
+ computeBudgetConfig
1859
+ }));
1860
+ const swapResult = await (0, output_1.withSpinner)("Executing swap", () => swapTxData.execute({ sendAndConfirm: true }));
1861
+ (0, output_1.logSuccess)(`Swap completed: ${swapResult.txId}`);
1862
+ (0, output_1.logInfo)("");
1863
+ // Brief pause to let the transaction settle
1864
+ await new Promise((r) => setTimeout(r, 2000));
1865
+ }
1866
+ catch (error) {
1867
+ const msg = error instanceof Error ? error.message : String(error);
1868
+ (0, output_1.logError)("Swap failed", msg);
1869
+ if (error instanceof Error && error.stack) {
1870
+ console.error(error.stack);
1871
+ }
1872
+ console.error("Full error:", error);
1873
+ process.exitCode = 1;
1874
+ return;
1875
+ }
1876
+ }
1877
+ }
1878
+ // Show preview
1879
+ (0, output_1.logInfo)(`Pool: ${poolId.toBase58()}`);
1880
+ (0, output_1.logInfo)(`Pair: ${symbolA}/${symbolB}`);
1881
+ (0, output_1.logInfo)(`Tick spacing: ${tickSpacing}`);
1882
+ (0, output_1.logInfo)("");
1883
+ (0, output_1.logInfo)("Price Range:");
1884
+ (0, output_1.logInfo)(` Lower: ${(0, clmm_utils_1.formatPrice)(actualPriceLower)} ${symbolB}/${symbolA} (tick ${tickLower})`);
1885
+ (0, output_1.logInfo)(` Upper: ${(0, clmm_utils_1.formatPrice)(actualPriceUpper)} ${symbolB}/${symbolA} (tick ${tickUpper})`);
1886
+ (0, output_1.logInfo)(` Current: ${(0, clmm_utils_1.formatPrice)(currentPrice)} ${symbolB}/${symbolA} (tick ${computePoolInfo.tickCurrent})`);
1887
+ (0, output_1.logInfo)(` Status: ${inRange ? "IN RANGE" : "OUT OF RANGE"}`);
1888
+ (0, output_1.logInfo)("");
1889
+ (0, output_1.logInfo)("Deposit:");
1890
+ (0, output_1.logInfo)(` ${baseSymbol}: ${amount}`);
1891
+ (0, output_1.logInfo)(` ${otherSymbol}: ${(0, clmm_utils_1.formatTokenAmount)(new decimal_js_1.default(otherAmount.amount.toString()).div(new decimal_js_1.default(10).pow(otherDecimals)))}`);
1892
+ (0, output_1.logInfo)(` Max ${otherSymbol} (with slippage): ${(0, clmm_utils_1.formatTokenAmount)(new decimal_js_1.default(otherSlippageAmount.amount.toString()).div(new decimal_js_1.default(10).pow(otherDecimals)))}`);
1893
+ (0, output_1.logInfo)(`Slippage: ${slippagePercent}%`);
1894
+ (0, output_1.logInfo)("");
1895
+ const ok = await (0, prompt_1.promptConfirm)("Proceed with opening position?", false);
1896
+ if (!ok) {
1897
+ (0, output_1.logInfo)("Cancelled");
1898
+ return;
1899
+ }
1900
+ try {
1901
+ const txData = await (0, output_1.withSpinner)("Building transaction", () => raydium.clmm.openPositionFromBase({
1902
+ poolInfo,
1903
+ ownerInfo: {
1904
+ useSOLBalance: true
1905
+ },
1906
+ tickLower,
1907
+ tickUpper,
1908
+ base: baseToken === "A" ? "MintA" : "MintB",
1909
+ baseAmount,
1910
+ otherAmountMax: otherSlippageAmount.amount,
1911
+ txVersion: raydium_sdk_v2_1.TxVersion.V0,
1912
+ computeBudgetConfig
1913
+ }));
1914
+ const result = await (0, output_1.withSpinner)("Sending transaction", () => txData.execute({ sendAndConfirm: true }));
1915
+ if ((0, output_1.isJsonOutput)()) {
1916
+ (0, output_1.logJson)({
1917
+ txId: result.txId,
1918
+ nftMint: txData.extInfo.nftMint.toBase58()
1919
+ });
1920
+ }
1921
+ else {
1922
+ (0, output_1.logSuccess)(`Position opened: ${result.txId}`);
1923
+ (0, output_1.logInfo)(`NFT Mint: ${txData.extInfo.nftMint.toBase58()}`);
1924
+ }
1925
+ }
1926
+ catch (error) {
1927
+ const msg = error instanceof Error ? error.message : String(error);
1928
+ (0, output_1.logError)("Failed to open position", msg);
1929
+ process.exitCode = 1;
1930
+ }
1931
+ }
1932
+ async function handleCreatePoolCommand(options) {
1933
+ const config = await (0, config_manager_1.loadConfig)({ createIfMissing: true });
1934
+ const walletName = config.activeWallet;
1935
+ if (!walletName) {
1936
+ (0, output_1.logError)("No active wallet set");
1937
+ (0, output_1.logInfo)("Set an active wallet with: raydium wallet set <name>");
1938
+ process.exitCode = 1;
1939
+ return;
1940
+ }
1941
+ let mintAPubkey;
1942
+ let mintBPubkey;
1943
+ try {
1944
+ mintAPubkey = new web3_js_1.PublicKey(options.mintA);
1945
+ mintBPubkey = new web3_js_1.PublicKey(options.mintB);
1946
+ }
1947
+ catch {
1948
+ (0, output_1.logError)("Invalid mint address");
1949
+ process.exitCode = 1;
1950
+ return;
1951
+ }
1952
+ const feeTierBps = Number(options.feeTier);
1953
+ if (!Number.isFinite(feeTierBps) || feeTierBps <= 0) {
1954
+ (0, output_1.logError)("Fee tier must be a positive number (in basis points)");
1955
+ process.exitCode = 1;
1956
+ return;
1957
+ }
1958
+ const initialPrice = Number(options.initialPrice);
1959
+ if (!Number.isFinite(initialPrice) || initialPrice <= 0) {
1960
+ (0, output_1.logError)("Initial price must be a positive number");
1961
+ process.exitCode = 1;
1962
+ return;
1963
+ }
1964
+ const priorityFeeSol = options.priorityFee ? Number(options.priorityFee) : config["priority-fee"];
1965
+ if (!Number.isFinite(priorityFeeSol) || priorityFeeSol < 0) {
1966
+ (0, output_1.logError)("Invalid priority fee");
1967
+ process.exitCode = 1;
1968
+ return;
1969
+ }
1970
+ const password = await (0, prompt_1.promptPassword)("Enter wallet password");
1971
+ let owner;
1972
+ try {
1973
+ owner = await (0, wallet_manager_1.decryptWallet)(walletName, password);
1974
+ }
1975
+ catch (error) {
1976
+ (0, output_1.logError)("Failed to decrypt wallet", error.message);
1977
+ process.exitCode = 1;
1978
+ return;
1979
+ }
1980
+ const raydium = await (0, output_1.withSpinner)("Loading SDK", () => (0, raydium_client_1.loadRaydium)({ owner, disableLoadToken: true }));
1981
+ // Fetch token info for both mints
1982
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1983
+ let mintAInfo;
1984
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1985
+ let mintBInfo;
1986
+ try {
1987
+ const tokenInfos = await (0, output_1.withSpinner)("Fetching token info", async () => {
1988
+ const [infoA, infoB] = await Promise.all([
1989
+ raydium.token.getTokenInfo(mintAPubkey),
1990
+ raydium.token.getTokenInfo(mintBPubkey)
1991
+ ]);
1992
+ return { infoA, infoB };
1993
+ });
1994
+ mintAInfo = tokenInfos.infoA;
1995
+ mintBInfo = tokenInfos.infoB;
1996
+ }
1997
+ catch (error) {
1998
+ const msg = error instanceof Error ? error.message : String(error);
1999
+ (0, output_1.logError)("Failed to fetch token info", msg);
2000
+ process.exitCode = 1;
2001
+ return;
2002
+ }
2003
+ if (!mintAInfo || !mintBInfo) {
2004
+ (0, output_1.logError)("Could not find token info for one or both mints");
2005
+ process.exitCode = 1;
2006
+ return;
2007
+ }
2008
+ const symbolA = mintAInfo.symbol || mintAPubkey.toBase58().slice(0, 6);
2009
+ const symbolB = mintBInfo.symbol || mintBPubkey.toBase58().slice(0, 6);
2010
+ const decimalsA = mintAInfo.decimals;
2011
+ const decimalsB = mintBInfo.decimals;
2012
+ // Fetch CLMM configs and find matching fee tier
2013
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2014
+ let clmmConfigs;
2015
+ try {
2016
+ clmmConfigs = await (0, output_1.withSpinner)("Fetching CLMM configs", () => raydium.api.getClmmConfigs());
2017
+ }
2018
+ catch (error) {
2019
+ const msg = error instanceof Error ? error.message : String(error);
2020
+ (0, output_1.logError)("Failed to fetch CLMM configs", msg);
2021
+ process.exitCode = 1;
2022
+ return;
2023
+ }
2024
+ // Find config matching fee tier (tradeFeeRate is in 1e6 format)
2025
+ // e.g., 500 bps = 0.05% = 5000 in 1e6 format
2026
+ const targetFeeRate = feeTierBps * 10; // Convert bps to 1e6 format
2027
+ const matchingConfig = clmmConfigs.find((c) => c.tradeFeeRate === targetFeeRate);
2028
+ if (!matchingConfig) {
2029
+ (0, output_1.logError)(`No CLMM config found for fee tier ${feeTierBps} bps`);
2030
+ (0, output_1.logInfo)("Available fee tiers:");
2031
+ for (const c of clmmConfigs) {
2032
+ const bps = c.tradeFeeRate / 10;
2033
+ (0, output_1.logInfo)(` ${bps} bps (tick spacing: ${c.tickSpacing})`);
2034
+ }
2035
+ process.exitCode = 1;
2036
+ return;
2037
+ }
2038
+ const tickSpacing = matchingConfig.tickSpacing;
2039
+ // Show preview
2040
+ (0, output_1.logInfo)("Create CLMM Pool:");
2041
+ (0, output_1.logInfo)(` Token A: ${symbolA} (${mintAPubkey.toBase58()})`);
2042
+ (0, output_1.logInfo)(` Token B: ${symbolB} (${mintBPubkey.toBase58()})`);
2043
+ (0, output_1.logInfo)(` Fee tier: ${feeTierBps / 100}% (${feeTierBps} bps)`);
2044
+ (0, output_1.logInfo)(` Tick spacing: ${tickSpacing}`);
2045
+ (0, output_1.logInfo)(` Initial price: ${initialPrice} ${symbolB}/${symbolA}`);
2046
+ (0, output_1.logInfo)("");
2047
+ const ok = await (0, prompt_1.promptConfirm)("Proceed with creating pool?", false);
2048
+ if (!ok) {
2049
+ (0, output_1.logInfo)("Cancelled");
2050
+ return;
2051
+ }
2052
+ const computeBudgetConfig = getPriorityFeeConfig(priorityFeeSol);
2053
+ try {
2054
+ const isDevnet = raydium.cluster !== "mainnet";
2055
+ const clmmProgramId = isDevnet ? raydium_sdk_v2_1.DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID : raydium_sdk_v2_1.CLMM_PROGRAM_ID;
2056
+ const txData = await (0, output_1.withSpinner)("Building transaction", () => raydium.clmm.createPool({
2057
+ programId: clmmProgramId,
2058
+ mint1: {
2059
+ address: mintAPubkey.toBase58(),
2060
+ decimals: decimalsA,
2061
+ symbol: symbolA,
2062
+ name: mintAInfo.name || symbolA,
2063
+ programId: mintAInfo.programId || "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
2064
+ chainId: 101,
2065
+ logoURI: "",
2066
+ tags: [],
2067
+ extensions: {}
2068
+ },
2069
+ mint2: {
2070
+ address: mintBPubkey.toBase58(),
2071
+ decimals: decimalsB,
2072
+ symbol: symbolB,
2073
+ name: mintBInfo.name || symbolB,
2074
+ programId: mintBInfo.programId || "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
2075
+ chainId: 101,
2076
+ logoURI: "",
2077
+ tags: [],
2078
+ extensions: {}
2079
+ },
2080
+ ammConfig: {
2081
+ id: new web3_js_1.PublicKey(matchingConfig.id),
2082
+ index: matchingConfig.index,
2083
+ protocolFeeRate: matchingConfig.protocolFeeRate,
2084
+ tradeFeeRate: matchingConfig.tradeFeeRate,
2085
+ tickSpacing: matchingConfig.tickSpacing,
2086
+ fundFeeRate: matchingConfig.fundFeeRate,
2087
+ fundOwner: "",
2088
+ description: ""
2089
+ },
2090
+ initialPrice: new decimal_js_1.default(initialPrice),
2091
+ txVersion: raydium_sdk_v2_1.TxVersion.V0,
2092
+ computeBudgetConfig
2093
+ }));
2094
+ const result = await (0, output_1.withSpinner)("Sending transaction", () => txData.execute({ sendAndConfirm: true }));
2095
+ const poolIdStr = txData.extInfo.address.id;
2096
+ const vaultA = txData.extInfo.address.vault.A;
2097
+ const vaultB = txData.extInfo.address.vault.B;
2098
+ if ((0, output_1.isJsonOutput)()) {
2099
+ (0, output_1.logJson)({
2100
+ txId: result.txId,
2101
+ poolId: poolIdStr,
2102
+ vaultA,
2103
+ vaultB
2104
+ });
2105
+ }
2106
+ else {
2107
+ (0, output_1.logSuccess)(`Pool created: ${result.txId}`);
2108
+ (0, output_1.logInfo)(`Pool ID: ${poolIdStr}`);
2109
+ (0, output_1.logInfo)(`Vault A: ${vaultA}`);
2110
+ (0, output_1.logInfo)(`Vault B: ${vaultB}`);
2111
+ }
2112
+ }
2113
+ catch (error) {
2114
+ const msg = error instanceof Error ? error.message : String(error);
2115
+ (0, output_1.logError)("Failed to create pool", msg);
2116
+ process.exitCode = 1;
2117
+ }
2118
+ }