@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.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/commands/clmm/index.js +2118 -0
- package/dist/commands/config/index.js +113 -0
- package/dist/commands/cpmm/index.js +480 -0
- package/dist/commands/launchpad/index.js +1677 -0
- package/dist/commands/pools/index.js +81 -0
- package/dist/commands/swap/index.js +490 -0
- package/dist/commands/tokens/index.js +93 -0
- package/dist/commands/wallet/index.js +267 -0
- package/dist/index.js +43 -0
- package/dist/lib/clmm-utils.js +296 -0
- package/dist/lib/codex-sdk.js +17 -0
- package/dist/lib/config-manager.js +67 -0
- package/dist/lib/connection.js +9 -0
- package/dist/lib/ipfs.js +117 -0
- package/dist/lib/output.js +59 -0
- package/dist/lib/paths.js +11 -0
- package/dist/lib/prompt.js +58 -0
- package/dist/lib/raydium-client.js +23 -0
- package/dist/lib/token-price.js +45 -0
- package/dist/lib/wallet-manager.js +173 -0
- package/dist/types/config.js +11 -0
- package/package.json +56 -0
|
@@ -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
|
+
}
|