naracli 1.0.83 → 1.0.85
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/README.md +9 -3
- package/dist/nara-cli-bundle.cjs +107561 -5421
- package/package.json +5 -2
- package/src/cli/commands/bridge.ts +405 -0
- package/src/cli/commands/dex.ts +1133 -0
- package/src/cli/commands/quest.ts +3 -3
- package/src/cli/index.ts +67 -4
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DEX commands - swap and pool creation on Meteora (DAMM v2, DLMM, DBC)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
|
|
7
|
+
import BN from "bn.js";
|
|
8
|
+
import { loadWallet, getRpcUrl } from "../utils/wallet";
|
|
9
|
+
import {
|
|
10
|
+
printError,
|
|
11
|
+
printInfo,
|
|
12
|
+
printSuccess,
|
|
13
|
+
formatOutput,
|
|
14
|
+
} from "../utils/output";
|
|
15
|
+
import type { GlobalOptions } from "../types";
|
|
16
|
+
|
|
17
|
+
// Program IDs for pool type detection
|
|
18
|
+
const CPAMM_PROGRAM_ID = "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG";
|
|
19
|
+
const DLMM_PROGRAM_ID = "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo";
|
|
20
|
+
const DBC_PROGRAM_ID = "dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN";
|
|
21
|
+
|
|
22
|
+
type PoolType = "cpamm" | "dlmm" | "dbc";
|
|
23
|
+
|
|
24
|
+
function identifyPoolType(owner: string): PoolType | null {
|
|
25
|
+
if (owner === CPAMM_PROGRAM_ID) return "cpamm";
|
|
26
|
+
if (owner === DLMM_PROGRAM_ID) return "dlmm";
|
|
27
|
+
if (owner === DBC_PROGRAM_ID) return "dbc";
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function getMintDecimals(connection: Connection, mint: PublicKey): Promise<number> {
|
|
32
|
+
try {
|
|
33
|
+
const info = await connection.getParsedAccountInfo(mint);
|
|
34
|
+
const parsed = (info.value?.data as any)?.parsed;
|
|
35
|
+
if (parsed?.info?.decimals !== undefined) return parsed.info.decimals;
|
|
36
|
+
} catch {}
|
|
37
|
+
return 9;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
41
|
+
// SWAP
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
43
|
+
|
|
44
|
+
async function swapCpAmm(
|
|
45
|
+
connection: Connection,
|
|
46
|
+
wallet: import("@solana/web3.js").Keypair,
|
|
47
|
+
poolAddress: PublicKey,
|
|
48
|
+
inputMint: PublicKey,
|
|
49
|
+
amountIn: BN,
|
|
50
|
+
slippageBps: number,
|
|
51
|
+
) {
|
|
52
|
+
const { CpAmm, SwapMode } = await import("@meteora-ag/cp-amm-sdk");
|
|
53
|
+
const cpAmm = new CpAmm(connection);
|
|
54
|
+
const poolState = await cpAmm.fetchPoolState(poolAddress);
|
|
55
|
+
|
|
56
|
+
const tokenAMint = poolState.tokenAMint;
|
|
57
|
+
const tokenBMint = poolState.tokenBMint;
|
|
58
|
+
const aToB = inputMint.equals(tokenAMint);
|
|
59
|
+
if (!aToB && !inputMint.equals(tokenBMint)) {
|
|
60
|
+
throw new Error(`Input token ${inputMint.toBase58()} not in pool (A: ${tokenAMint.toBase58()}, B: ${tokenBMint.toBase58()})`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const quote = cpAmm.swapQuoteExactInput(
|
|
64
|
+
poolState, poolState.currentPoint, amountIn,
|
|
65
|
+
slippageBps / 10000, aToB, false,
|
|
66
|
+
poolState.tokenADecimals ?? 9, poolState.tokenBDecimals ?? 9,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const outputMint = aToB ? tokenBMint : tokenAMint;
|
|
70
|
+
const minOut = quote.minimumAmountOut ?? new BN(0);
|
|
71
|
+
|
|
72
|
+
const txBuilder = cpAmm.swap2({
|
|
73
|
+
payer: wallet.publicKey, pool: poolAddress,
|
|
74
|
+
inputTokenMint: inputMint, outputTokenMint: outputMint,
|
|
75
|
+
tokenAMint, tokenBMint,
|
|
76
|
+
tokenAVault: poolState.tokenAVault, tokenBVault: poolState.tokenBVault,
|
|
77
|
+
tokenAProgram: poolState.tokenAProgram, tokenBProgram: poolState.tokenBProgram,
|
|
78
|
+
referralTokenAccount: null, poolState,
|
|
79
|
+
swapMode: SwapMode.ExactIn, amountIn, minimumAmountOut: minOut,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const tx = await txBuilder.transaction();
|
|
83
|
+
tx.feePayer = wallet.publicKey;
|
|
84
|
+
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
85
|
+
tx.sign(wallet);
|
|
86
|
+
const sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true });
|
|
87
|
+
return { signature: sig, outputMint, minOut };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function swapDlmm(
|
|
91
|
+
connection: Connection,
|
|
92
|
+
wallet: import("@solana/web3.js").Keypair,
|
|
93
|
+
poolAddress: PublicKey,
|
|
94
|
+
inputMint: PublicKey,
|
|
95
|
+
amountIn: BN,
|
|
96
|
+
slippageBps: number,
|
|
97
|
+
) {
|
|
98
|
+
const { default: DLMM } = await import("@meteora-ag/dlmm");
|
|
99
|
+
const dlmm = await DLMM.create(connection, poolAddress);
|
|
100
|
+
|
|
101
|
+
const tokenXMint = dlmm.tokenX.publicKey;
|
|
102
|
+
const tokenYMint = dlmm.tokenY.publicKey;
|
|
103
|
+
const swapForY = inputMint.equals(tokenXMint);
|
|
104
|
+
if (!swapForY && !inputMint.equals(tokenYMint)) {
|
|
105
|
+
throw new Error(`Input token ${inputMint.toBase58()} not in pool (X: ${tokenXMint.toBase58()}, Y: ${tokenYMint.toBase58()})`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const binArrays = await dlmm.getBinArrayForSwap(swapForY);
|
|
109
|
+
const quote = dlmm.swapQuote(amountIn, swapForY, new BN(slippageBps), binArrays);
|
|
110
|
+
const outputMint = swapForY ? tokenYMint : tokenXMint;
|
|
111
|
+
|
|
112
|
+
const swapTx = await dlmm.swap({
|
|
113
|
+
inToken: inputMint, outToken: outputMint,
|
|
114
|
+
inAmount: amountIn, minOutAmount: quote.minOutAmount,
|
|
115
|
+
lbPair: poolAddress, user: wallet.publicKey,
|
|
116
|
+
binArraysPubkey: quote.binArraysPubkey,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
swapTx.feePayer = wallet.publicKey;
|
|
120
|
+
swapTx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
121
|
+
swapTx.sign(wallet);
|
|
122
|
+
const sig = await connection.sendRawTransaction(swapTx.serialize(), { skipPreflight: true });
|
|
123
|
+
return { signature: sig, outputMint, minOut: quote.minOutAmount };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function swapDbc(
|
|
127
|
+
connection: Connection,
|
|
128
|
+
wallet: import("@solana/web3.js").Keypair,
|
|
129
|
+
poolAddress: PublicKey,
|
|
130
|
+
inputMint: PublicKey,
|
|
131
|
+
amountIn: BN,
|
|
132
|
+
slippageBps: number,
|
|
133
|
+
) {
|
|
134
|
+
const { DynamicBondingCurveClient } = await import("@meteora-ag/dynamic-bonding-curve-sdk");
|
|
135
|
+
const client = DynamicBondingCurveClient.create(connection);
|
|
136
|
+
const pool = await client.pool.getPool(poolAddress);
|
|
137
|
+
|
|
138
|
+
const baseMint = pool.baseMint;
|
|
139
|
+
const quoteMint = pool.quoteMint;
|
|
140
|
+
const swapBaseForQuote = inputMint.equals(baseMint);
|
|
141
|
+
if (!swapBaseForQuote && !inputMint.equals(quoteMint)) {
|
|
142
|
+
throw new Error(`Input token ${inputMint.toBase58()} not in pool (base: ${baseMint.toBase58()}, quote: ${quoteMint.toBase58()})`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const config = await client.pool.getPoolConfig(pool.config);
|
|
146
|
+
const quote = client.pool.swapQuote({
|
|
147
|
+
virtualPool: pool, config, swapBaseForQuote,
|
|
148
|
+
amountIn, slippageBps, hasReferral: false,
|
|
149
|
+
currentPoint: pool.currentPoint,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const minOut = quote.minimumAmountOut ?? new BN(0);
|
|
153
|
+
const outputMint = swapBaseForQuote ? quoteMint : baseMint;
|
|
154
|
+
|
|
155
|
+
const swapTx = await client.pool.swap({
|
|
156
|
+
owner: wallet.publicKey, pool: poolAddress,
|
|
157
|
+
amountIn, minimumAmountOut: minOut,
|
|
158
|
+
swapBaseForQuote, referralTokenAccount: null,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
swapTx.feePayer = wallet.publicKey;
|
|
162
|
+
swapTx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
163
|
+
swapTx.sign(wallet);
|
|
164
|
+
const sig = await connection.sendRawTransaction(swapTx.serialize(), { skipPreflight: true });
|
|
165
|
+
return { signature: sig, outputMint, minOut };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function handleSwap(
|
|
169
|
+
pool: string, inputToken: string, amount: string,
|
|
170
|
+
options: GlobalOptions & { slippage?: string }
|
|
171
|
+
) {
|
|
172
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
173
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
174
|
+
const wallet = await loadWallet(options.wallet);
|
|
175
|
+
const poolAddress = new PublicKey(pool);
|
|
176
|
+
const inputMint = new PublicKey(inputToken);
|
|
177
|
+
const slippageBps = options.slippage ? Math.round(parseFloat(options.slippage) * 100) : 100;
|
|
178
|
+
|
|
179
|
+
// Detect pool type
|
|
180
|
+
if (!options.json) printInfo("Detecting pool type...");
|
|
181
|
+
const accountInfo = await connection.getAccountInfo(poolAddress);
|
|
182
|
+
if (!accountInfo) { printError("Pool account not found"); process.exit(1); }
|
|
183
|
+
|
|
184
|
+
const poolType = identifyPoolType(accountInfo.owner.toBase58());
|
|
185
|
+
if (!poolType) {
|
|
186
|
+
printError(`Unrecognized pool. Owner: ${accountInfo.owner.toBase58()}. Supported: DAMM v2, DLMM, DBC`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
if (!options.json) printInfo(`Pool type: ${poolType.toUpperCase()}`);
|
|
190
|
+
|
|
191
|
+
const decimals = await getMintDecimals(connection, inputMint);
|
|
192
|
+
const rawAmount = new BN(Math.floor(parseFloat(amount) * 10 ** decimals).toString());
|
|
193
|
+
|
|
194
|
+
if (!options.json) printInfo(`Swapping ${amount} tokens (slippage: ${slippageBps / 100}%)...`);
|
|
195
|
+
|
|
196
|
+
let result: { signature: string; outputMint: PublicKey; minOut: BN };
|
|
197
|
+
switch (poolType) {
|
|
198
|
+
case "cpamm": result = await swapCpAmm(connection, wallet, poolAddress, inputMint, rawAmount, slippageBps); break;
|
|
199
|
+
case "dlmm": result = await swapDlmm(connection, wallet, poolAddress, inputMint, rawAmount, slippageBps); break;
|
|
200
|
+
case "dbc": result = await swapDbc(connection, wallet, poolAddress, inputMint, rawAmount, slippageBps); break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (options.json) {
|
|
204
|
+
formatOutput({ signature: result.signature, poolType, inputMint: inputMint.toBase58(), outputMint: result.outputMint.toBase58(), amountIn: amount, minAmountOut: result.minOut.toString() }, true);
|
|
205
|
+
} else {
|
|
206
|
+
printSuccess("Swap submitted!");
|
|
207
|
+
console.log(` Transaction: ${result.signature}`);
|
|
208
|
+
console.log(` Output mint: ${result.outputMint.toBase58()}`);
|
|
209
|
+
console.log(` Min output: ${result.minOut.toString()}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
214
|
+
// ADD LIQUIDITY
|
|
215
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
216
|
+
|
|
217
|
+
async function handleAddLiquidity(
|
|
218
|
+
pool: string, inputToken: string, amount: string,
|
|
219
|
+
options: GlobalOptions & { slippage?: string; position?: string; yes?: boolean; amountB?: string }
|
|
220
|
+
) {
|
|
221
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
222
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
223
|
+
const wallet = await loadWallet(options.wallet);
|
|
224
|
+
const poolAddress = new PublicKey(pool);
|
|
225
|
+
const inputMint = new PublicKey(inputToken);
|
|
226
|
+
const slippageBps = options.slippage ? Math.round(parseFloat(options.slippage) * 100) : 100;
|
|
227
|
+
|
|
228
|
+
// Detect pool type
|
|
229
|
+
if (!options.json) printInfo("Detecting pool type...");
|
|
230
|
+
const accountInfo = await connection.getAccountInfo(poolAddress);
|
|
231
|
+
if (!accountInfo) { printError("Pool account not found"); process.exit(1); }
|
|
232
|
+
|
|
233
|
+
const poolType = identifyPoolType(accountInfo.owner.toBase58());
|
|
234
|
+
if (!poolType) {
|
|
235
|
+
printError(`Unrecognized pool. Owner: ${accountInfo.owner.toBase58()}`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
if (!options.json) printInfo(`Pool type: ${poolType.toUpperCase()}`);
|
|
239
|
+
|
|
240
|
+
if (poolType === "dbc") {
|
|
241
|
+
printError("DBC pools do not support adding liquidity via CLI.");
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let sig: string;
|
|
246
|
+
|
|
247
|
+
if (poolType === "cpamm") {
|
|
248
|
+
const { CpAmm } = await import("@meteora-ag/cp-amm-sdk");
|
|
249
|
+
const { Keypair: SolKeypair, LAMPORTS_PER_SOL: LSOL } = await import("@solana/web3.js");
|
|
250
|
+
const cpAmm = new CpAmm(connection);
|
|
251
|
+
const poolState = await cpAmm.fetchPoolState(poolAddress);
|
|
252
|
+
|
|
253
|
+
const tokenAMint = poolState.tokenAMint;
|
|
254
|
+
const tokenBMint = poolState.tokenBMint;
|
|
255
|
+
const isA = inputMint.equals(tokenAMint);
|
|
256
|
+
if (!isA && !inputMint.equals(tokenBMint)) {
|
|
257
|
+
printError(`Token ${inputMint.toBase58()} not in pool (A: ${tokenAMint.toBase58()}, B: ${tokenBMint.toBase58()})`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const decA = await getMintDecimals(connection, tokenAMint);
|
|
262
|
+
const decB = await getMintDecimals(connection, tokenBMint);
|
|
263
|
+
const inputDec = isA ? decA : decB;
|
|
264
|
+
const inputRaw = new BN(Math.floor(parseFloat(amount) * 10 ** inputDec).toString());
|
|
265
|
+
|
|
266
|
+
// Calculate other token amount from pool price
|
|
267
|
+
// sqrtPrice is in Q64 format: actualSqrtPrice = sqrtPrice / 2^64
|
|
268
|
+
// price = (sqrtPrice / 2^64)^2 * 10^(decA - decB) = priceB/A
|
|
269
|
+
const sqrtPriceNum = Number(poolState.currentSqrtPrice.toString()) / 2 ** 64;
|
|
270
|
+
const priceBA = sqrtPriceNum * sqrtPriceNum * 10 ** (decA - decB); // token B per token A
|
|
271
|
+
|
|
272
|
+
let tokenAAmount: BN, tokenBAmount: BN;
|
|
273
|
+
let otherAmount: number;
|
|
274
|
+
let otherSymbol: string;
|
|
275
|
+
|
|
276
|
+
if (options.amountB) {
|
|
277
|
+
// User explicitly provided both amounts
|
|
278
|
+
tokenAAmount = isA ? inputRaw : new BN(Math.floor(parseFloat(options.amountB) * 10 ** decA).toString());
|
|
279
|
+
tokenBAmount = isA ? new BN(Math.floor(parseFloat(options.amountB) * 10 ** decB).toString()) : inputRaw;
|
|
280
|
+
} else if (isA) {
|
|
281
|
+
tokenAAmount = inputRaw;
|
|
282
|
+
otherAmount = parseFloat(amount) * priceBA;
|
|
283
|
+
tokenBAmount = new BN(Math.floor(otherAmount * 10 ** decB).toString());
|
|
284
|
+
otherSymbol = tokenBMint.toBase58().slice(0, 8) + "...";
|
|
285
|
+
} else {
|
|
286
|
+
tokenBAmount = inputRaw;
|
|
287
|
+
otherAmount = parseFloat(amount) / priceBA;
|
|
288
|
+
tokenAAmount = new BN(Math.floor(otherAmount * 10 ** decA).toString());
|
|
289
|
+
otherSymbol = tokenAMint.toBase58().slice(0, 8) + "...";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Confirm with user unless --yes
|
|
293
|
+
if (!options.amountB && !options.yes && !options.json) {
|
|
294
|
+
console.log("");
|
|
295
|
+
console.log(` Token A: ${tokenAMint.toBase58()}`);
|
|
296
|
+
console.log(` Token B: ${tokenBMint.toBase58()}`);
|
|
297
|
+
console.log(` Amount A: ${Number(tokenAAmount.toString()) / 10 ** decA}`);
|
|
298
|
+
console.log(` Amount B: ${Number(tokenBAmount.toString()) / 10 ** decB}`);
|
|
299
|
+
console.log(` Pool price: ${priceBA.toFixed(6)} B/A`);
|
|
300
|
+
console.log("");
|
|
301
|
+
|
|
302
|
+
const readline = await import("node:readline");
|
|
303
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
304
|
+
const answer = await new Promise<string>(resolve => rl.question(" Confirm? (y/N) ", resolve));
|
|
305
|
+
rl.close();
|
|
306
|
+
if (answer.toLowerCase() !== "y") {
|
|
307
|
+
printInfo("Cancelled.");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (options.position) {
|
|
313
|
+
const positionKey = new PublicKey(options.position);
|
|
314
|
+
if (!options.json) printInfo("Adding liquidity to existing position...");
|
|
315
|
+
|
|
316
|
+
const prepared = cpAmm.preparePoolCreationParams({
|
|
317
|
+
tokenAAmount, tokenBAmount,
|
|
318
|
+
minSqrtPrice: poolState.sqrtMinPrice ?? new BN(0),
|
|
319
|
+
maxSqrtPrice: poolState.sqrtMaxPrice ?? new BN("340282366920938463463374607431768211455"),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const { getAssociatedTokenAddress } = await import("@solana/spl-token");
|
|
323
|
+
const positionNftAccount = await getAssociatedTokenAddress(positionKey, wallet.publicKey);
|
|
324
|
+
|
|
325
|
+
const txBuilder = cpAmm.addLiquidity({
|
|
326
|
+
owner: wallet.publicKey, position: positionKey, pool: poolAddress,
|
|
327
|
+
positionNftAccount, liquidityDelta: prepared.liquidityDelta,
|
|
328
|
+
maxAmountTokenA: tokenAAmount, maxAmountTokenB: tokenBAmount,
|
|
329
|
+
tokenAAmountThreshold: new BN(0), tokenBAmountThreshold: new BN(0),
|
|
330
|
+
tokenAMint, tokenBMint,
|
|
331
|
+
tokenAVault: poolState.tokenAVault, tokenBVault: poolState.tokenBVault,
|
|
332
|
+
tokenAProgram: poolState.tokenAProgram, tokenBProgram: poolState.tokenBProgram,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const tx = await txBuilder.transaction();
|
|
336
|
+
tx.feePayer = wallet.publicKey;
|
|
337
|
+
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
338
|
+
tx.sign(wallet);
|
|
339
|
+
sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true });
|
|
340
|
+
} else {
|
|
341
|
+
if (!options.json) printInfo("Creating position and adding liquidity...");
|
|
342
|
+
const positionNft = SolKeypair.generate();
|
|
343
|
+
|
|
344
|
+
const txBuilder = cpAmm.createPositionAndAddLiquidity({
|
|
345
|
+
owner: wallet.publicKey, payer: wallet.publicKey,
|
|
346
|
+
pool: poolAddress, positionNft: positionNft.publicKey,
|
|
347
|
+
tokenAAmount, tokenBAmount,
|
|
348
|
+
maxAmountTokenA: tokenAAmount, maxAmountTokenB: tokenBAmount,
|
|
349
|
+
tokenAMint, tokenBMint,
|
|
350
|
+
tokenAVault: poolState.tokenAVault, tokenBVault: poolState.tokenBVault,
|
|
351
|
+
tokenAProgram: poolState.tokenAProgram, tokenBProgram: poolState.tokenBProgram,
|
|
352
|
+
poolState,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const tx = await txBuilder.transaction();
|
|
356
|
+
tx.feePayer = wallet.publicKey;
|
|
357
|
+
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
358
|
+
tx.sign(wallet, positionNft);
|
|
359
|
+
sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true });
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
// DLMM
|
|
363
|
+
const { default: DLMM } = await import("@meteora-ag/dlmm");
|
|
364
|
+
const dlmm = await DLMM.create(connection, poolAddress);
|
|
365
|
+
|
|
366
|
+
const tokenXMint = dlmm.tokenX.publicKey;
|
|
367
|
+
const tokenYMint = dlmm.tokenY.publicKey;
|
|
368
|
+
const isX = inputMint.equals(tokenXMint);
|
|
369
|
+
if (!isX && !inputMint.equals(tokenYMint)) {
|
|
370
|
+
printError(`Token ${inputMint.toBase58()} not in pool (X: ${tokenXMint.toBase58()}, Y: ${tokenYMint.toBase58()})`);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const decX = await getMintDecimals(connection, tokenXMint);
|
|
375
|
+
const decY = await getMintDecimals(connection, tokenYMint);
|
|
376
|
+
const inputDec = isX ? decX : decY;
|
|
377
|
+
const inputRaw = new BN(Math.floor(parseFloat(amount) * 10 ** inputDec).toString());
|
|
378
|
+
|
|
379
|
+
// Get active bin price to calculate other token amount
|
|
380
|
+
const activeBin = await dlmm.getActiveBin();
|
|
381
|
+
const binPrice = Number(activeBin.price); // Y per X
|
|
382
|
+
|
|
383
|
+
let totalXAmount: BN, totalYAmount: BN;
|
|
384
|
+
|
|
385
|
+
if (options.amountB) {
|
|
386
|
+
totalXAmount = isX ? inputRaw : new BN(Math.floor(parseFloat(options.amountB) * 10 ** decX).toString());
|
|
387
|
+
totalYAmount = isX ? new BN(Math.floor(parseFloat(options.amountB) * 10 ** decY).toString()) : inputRaw;
|
|
388
|
+
} else if (isX) {
|
|
389
|
+
totalXAmount = inputRaw;
|
|
390
|
+
const otherAmount = parseFloat(amount) * binPrice;
|
|
391
|
+
totalYAmount = new BN(Math.floor(otherAmount * 10 ** decY).toString());
|
|
392
|
+
} else {
|
|
393
|
+
totalYAmount = inputRaw;
|
|
394
|
+
const otherAmount = parseFloat(amount) / binPrice;
|
|
395
|
+
totalXAmount = new BN(Math.floor(otherAmount * 10 ** decX).toString());
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Confirm with user unless --yes
|
|
399
|
+
if (!options.amountB && !options.yes && !options.json) {
|
|
400
|
+
console.log("");
|
|
401
|
+
console.log(` Token X: ${tokenXMint.toBase58()}`);
|
|
402
|
+
console.log(` Token Y: ${tokenYMint.toBase58()}`);
|
|
403
|
+
console.log(` Amount X: ${Number(totalXAmount.toString()) / 10 ** decX}`);
|
|
404
|
+
console.log(` Amount Y: ${Number(totalYAmount.toString()) / 10 ** decY}`);
|
|
405
|
+
console.log(` Bin price: ${binPrice.toFixed(6)} Y/X`);
|
|
406
|
+
console.log("");
|
|
407
|
+
|
|
408
|
+
const readline = await import("node:readline");
|
|
409
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
410
|
+
const answer = await new Promise<string>(resolve => rl.question(" Confirm? (y/N) ", resolve));
|
|
411
|
+
rl.close();
|
|
412
|
+
if (answer.toLowerCase() !== "y") {
|
|
413
|
+
printInfo("Cancelled.");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (options.position) {
|
|
419
|
+
const positionKey = new PublicKey(options.position);
|
|
420
|
+
if (!options.json) printInfo("Adding liquidity to existing DLMM position...");
|
|
421
|
+
|
|
422
|
+
const tx = await dlmm.addLiquidityByStrategy({
|
|
423
|
+
positionPubKey: positionKey,
|
|
424
|
+
totalXAmount, totalYAmount,
|
|
425
|
+
user: wallet.publicKey,
|
|
426
|
+
slippage: slippageBps / 100,
|
|
427
|
+
strategy: { maxBinId: activeBin.binId + 50, minBinId: activeBin.binId - 50, strategyType: 0 },
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const txs = Array.isArray(tx) ? tx : [tx];
|
|
431
|
+
for (const t of txs) {
|
|
432
|
+
t.feePayer = wallet.publicKey;
|
|
433
|
+
t.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
434
|
+
t.sign(wallet);
|
|
435
|
+
sig = await connection.sendRawTransaction(t.serialize(), { skipPreflight: true });
|
|
436
|
+
}
|
|
437
|
+
sig = sig!;
|
|
438
|
+
} else {
|
|
439
|
+
if (!options.json) printInfo("Creating position and adding liquidity to DLMM...");
|
|
440
|
+
|
|
441
|
+
const tx = await dlmm.initializePositionAndAddLiquidityByStrategy({
|
|
442
|
+
totalXAmount, totalYAmount,
|
|
443
|
+
user: wallet.publicKey,
|
|
444
|
+
slippage: slippageBps / 100,
|
|
445
|
+
strategy: { maxBinId: activeBin.binId + 50, minBinId: activeBin.binId - 50, strategyType: 0 },
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
tx.feePayer = wallet.publicKey;
|
|
449
|
+
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
450
|
+
tx.sign(wallet);
|
|
451
|
+
sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (options.json) {
|
|
456
|
+
formatOutput({ signature: sig, poolType, pool }, true);
|
|
457
|
+
} else {
|
|
458
|
+
printSuccess("Liquidity added!");
|
|
459
|
+
console.log(` Transaction: ${sig}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
464
|
+
// CREATE POOL
|
|
465
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
466
|
+
|
|
467
|
+
async function handleCreateCpAmm(
|
|
468
|
+
options: GlobalOptions & {
|
|
469
|
+
tokenA: string; tokenB: string; config: string;
|
|
470
|
+
price: string; amountA: string; amountB: string;
|
|
471
|
+
tokenAProgram?: string; tokenBProgram?: string;
|
|
472
|
+
minPrice?: string; maxPrice?: string;
|
|
473
|
+
}
|
|
474
|
+
) {
|
|
475
|
+
const { CpAmm } = await import("@meteora-ag/cp-amm-sdk");
|
|
476
|
+
const { Keypair: SolKeypair } = await import("@solana/web3.js");
|
|
477
|
+
const { TOKEN_PROGRAM_ID } = await import("@solana/spl-token");
|
|
478
|
+
|
|
479
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
480
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
481
|
+
const wallet = await loadWallet(options.wallet);
|
|
482
|
+
|
|
483
|
+
const tokenAMint = new PublicKey(options.tokenA);
|
|
484
|
+
const tokenBMint = new PublicKey(options.tokenB);
|
|
485
|
+
const configKey = new PublicKey(options.config);
|
|
486
|
+
const positionNft = SolKeypair.generate();
|
|
487
|
+
|
|
488
|
+
const tokenAProgram = options.tokenAProgram ? new PublicKey(options.tokenAProgram) : TOKEN_PROGRAM_ID;
|
|
489
|
+
const tokenBProgram = options.tokenBProgram ? new PublicKey(options.tokenBProgram) : TOKEN_PROGRAM_ID;
|
|
490
|
+
|
|
491
|
+
const decA = await getMintDecimals(connection, tokenAMint);
|
|
492
|
+
const decB = await getMintDecimals(connection, tokenBMint);
|
|
493
|
+
|
|
494
|
+
const price = parseFloat(options.price);
|
|
495
|
+
const decDiff = decB - decA;
|
|
496
|
+
const sqrtPrice = Math.sqrt(price * 10 ** decDiff);
|
|
497
|
+
const initSqrtPrice = new BN(Math.floor(sqrtPrice * 2 ** 64).toString());
|
|
498
|
+
|
|
499
|
+
const tokenAAmount = new BN(Math.floor(parseFloat(options.amountA) * 10 ** decA).toString());
|
|
500
|
+
const tokenBAmount = new BN(Math.floor(parseFloat(options.amountB) * 10 ** decB).toString());
|
|
501
|
+
|
|
502
|
+
const cpAmm = new CpAmm(connection);
|
|
503
|
+
const isConcentrated = options.minPrice && options.maxPrice;
|
|
504
|
+
|
|
505
|
+
if (isConcentrated) {
|
|
506
|
+
// Concentrated liquidity — custom pool with price range
|
|
507
|
+
const minP = parseFloat(options.minPrice!);
|
|
508
|
+
const maxP = parseFloat(options.maxPrice!);
|
|
509
|
+
const sqrtMinPrice = new BN(Math.floor(Math.sqrt(minP * 10 ** decDiff) * 2 ** 64).toString());
|
|
510
|
+
const sqrtMaxPrice = new BN(Math.floor(Math.sqrt(maxP * 10 ** decDiff) * 2 ** 64).toString());
|
|
511
|
+
|
|
512
|
+
if (!options.json) printInfo("Creating DAMM v2 pool (concentrated liquidity)...");
|
|
513
|
+
|
|
514
|
+
const { tx, pool, position } = await cpAmm.createCustomPoolWithDynamicConfig({
|
|
515
|
+
payer: wallet.publicKey, creator: wallet.publicKey,
|
|
516
|
+
positionNft: positionNft.publicKey,
|
|
517
|
+
tokenAMint, tokenBMint,
|
|
518
|
+
tokenAAmount, tokenBAmount,
|
|
519
|
+
sqrtMinPrice, sqrtMaxPrice,
|
|
520
|
+
liquidityDelta: new BN(0),
|
|
521
|
+
initSqrtPrice,
|
|
522
|
+
poolFees: { baseFee: { cliffFeeNumerator: new BN(2500000), numberOfPeriod: 0, reductionFactor: new BN(0), periodFrequency: new BN(0), feeSchedulerMode: 0 }, protocolFeePercent: 20, partnerFeePercent: 0, referralFeePercent: 20, dynamicFee: null },
|
|
523
|
+
hasAlphaVault: false,
|
|
524
|
+
activationType: 0,
|
|
525
|
+
collectFeeMode: 0,
|
|
526
|
+
activationPoint: null,
|
|
527
|
+
tokenAProgram, tokenBProgram,
|
|
528
|
+
config: configKey,
|
|
529
|
+
poolCreatorAuthority: wallet.publicKey,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
tx.feePayer = wallet.publicKey;
|
|
533
|
+
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
534
|
+
tx.sign(wallet, positionNft);
|
|
535
|
+
const sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true });
|
|
536
|
+
|
|
537
|
+
if (options.json) {
|
|
538
|
+
formatOutput({ signature: sig, type: "cpamm", mode: "concentrated", pool: pool.toBase58(), position: position.toBase58() }, true);
|
|
539
|
+
} else {
|
|
540
|
+
printSuccess("DAMM v2 pool created (concentrated)!");
|
|
541
|
+
console.log(` Transaction: ${sig}`);
|
|
542
|
+
console.log(` Pool: ${pool.toBase58()}`);
|
|
543
|
+
console.log(` Price range: ${options.minPrice} - ${options.maxPrice}`);
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
// Full-range liquidity
|
|
547
|
+
if (!options.json) printInfo("Creating DAMM v2 pool (full range)...");
|
|
548
|
+
|
|
549
|
+
const txBuilder = cpAmm.createPool({
|
|
550
|
+
creator: wallet.publicKey, payer: wallet.publicKey,
|
|
551
|
+
config: configKey, positionNft: positionNft.publicKey,
|
|
552
|
+
tokenAMint, tokenBMint,
|
|
553
|
+
initSqrtPrice, liquidityDelta: new BN(0),
|
|
554
|
+
tokenAAmount, tokenBAmount,
|
|
555
|
+
activationPoint: null,
|
|
556
|
+
tokenAProgram, tokenBProgram,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const tx = await txBuilder.transaction();
|
|
560
|
+
tx.feePayer = wallet.publicKey;
|
|
561
|
+
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
562
|
+
tx.sign(wallet, positionNft);
|
|
563
|
+
const sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true });
|
|
564
|
+
|
|
565
|
+
if (options.json) {
|
|
566
|
+
formatOutput({ signature: sig, type: "cpamm", mode: "full-range" }, true);
|
|
567
|
+
} else {
|
|
568
|
+
printSuccess("DAMM v2 pool created (full range)!");
|
|
569
|
+
console.log(` Transaction: ${sig}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function handleCreateDlmm(
|
|
575
|
+
options: GlobalOptions & {
|
|
576
|
+
tokenX: string; tokenY: string;
|
|
577
|
+
binStep: string; activeId: string; presetParameter: string;
|
|
578
|
+
}
|
|
579
|
+
) {
|
|
580
|
+
const { default: DLMM } = await import("@meteora-ag/dlmm");
|
|
581
|
+
|
|
582
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
583
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
584
|
+
const wallet = await loadWallet(options.wallet);
|
|
585
|
+
|
|
586
|
+
const tokenX = new PublicKey(options.tokenX);
|
|
587
|
+
const tokenY = new PublicKey(options.tokenY);
|
|
588
|
+
const presetParameter = new PublicKey(options.presetParameter);
|
|
589
|
+
const binStep = new BN(options.binStep);
|
|
590
|
+
const activeId = new BN(options.activeId);
|
|
591
|
+
|
|
592
|
+
if (!options.json) printInfo("Creating DLMM pool...");
|
|
593
|
+
|
|
594
|
+
const tx = await DLMM.createLbPair2(
|
|
595
|
+
connection, wallet.publicKey,
|
|
596
|
+
tokenX, tokenY, presetParameter, activeId,
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
tx.feePayer = wallet.publicKey;
|
|
600
|
+
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
601
|
+
tx.sign(wallet);
|
|
602
|
+
const sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true });
|
|
603
|
+
|
|
604
|
+
if (options.json) {
|
|
605
|
+
formatOutput({ signature: sig, type: "dlmm" }, true);
|
|
606
|
+
} else {
|
|
607
|
+
printSuccess("DLMM pool created!");
|
|
608
|
+
console.log(` Transaction: ${sig}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function handleCreateDbc(
|
|
613
|
+
options: GlobalOptions & {
|
|
614
|
+
config: string; baseMint: string;
|
|
615
|
+
name: string; symbol: string; uri: string;
|
|
616
|
+
}
|
|
617
|
+
) {
|
|
618
|
+
const { DynamicBondingCurveClient } = await import("@meteora-ag/dynamic-bonding-curve-sdk");
|
|
619
|
+
|
|
620
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
621
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
622
|
+
const wallet = await loadWallet(options.wallet);
|
|
623
|
+
|
|
624
|
+
const configKey = new PublicKey(options.config);
|
|
625
|
+
const baseMint = new PublicKey(options.baseMint);
|
|
626
|
+
|
|
627
|
+
if (!options.json) printInfo("Creating DBC pool...");
|
|
628
|
+
|
|
629
|
+
const client = DynamicBondingCurveClient.create(connection);
|
|
630
|
+
const tx = await client.pool.createPool({
|
|
631
|
+
payer: wallet.publicKey, poolCreator: wallet.publicKey,
|
|
632
|
+
config: configKey, baseMint,
|
|
633
|
+
name: options.name, symbol: options.symbol, uri: options.uri,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
tx.feePayer = wallet.publicKey;
|
|
637
|
+
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
638
|
+
tx.sign(wallet);
|
|
639
|
+
const sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true });
|
|
640
|
+
|
|
641
|
+
if (options.json) {
|
|
642
|
+
formatOutput({ signature: sig, type: "dbc" }, true);
|
|
643
|
+
} else {
|
|
644
|
+
printSuccess("DBC pool created!");
|
|
645
|
+
console.log(` Transaction: ${sig}`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
650
|
+
// REGISTER
|
|
651
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
652
|
+
|
|
653
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
654
|
+
// LIST POSITIONS
|
|
655
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
656
|
+
|
|
657
|
+
async function handleListPositions(
|
|
658
|
+
options: GlobalOptions & { owner?: string }
|
|
659
|
+
) {
|
|
660
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
661
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
662
|
+
let userPubkey: PublicKey;
|
|
663
|
+
if (options.owner) {
|
|
664
|
+
userPubkey = new PublicKey(options.owner);
|
|
665
|
+
} else {
|
|
666
|
+
const wallet = await loadWallet(options.wallet);
|
|
667
|
+
userPubkey = wallet.publicKey;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const allPositions: any[] = [];
|
|
671
|
+
|
|
672
|
+
// ── DAMM v2 (CP-AMM) ──
|
|
673
|
+
if (!options.json) printInfo("Fetching DAMM v2 positions...");
|
|
674
|
+
try {
|
|
675
|
+
const {
|
|
676
|
+
CpAmm, getAllPositionNftAccountByOwner, derivePositionAddress,
|
|
677
|
+
getReservesAmountForConcentratedLiquidity, getUnClaimLpFee,
|
|
678
|
+
} = await import("@meteora-ag/cp-amm-sdk");
|
|
679
|
+
const cpAmm = new CpAmm(connection);
|
|
680
|
+
const nfts = await getAllPositionNftAccountByOwner(connection, userPubkey);
|
|
681
|
+
|
|
682
|
+
for (const nft of nfts) {
|
|
683
|
+
try {
|
|
684
|
+
const positionPk = derivePositionAddress(nft.positionNft);
|
|
685
|
+
const posState = await cpAmm.fetchPositionState(positionPk);
|
|
686
|
+
const poolState = await cpAmm.fetchPoolState(posState.pool);
|
|
687
|
+
const decA = await getMintDecimals(connection, poolState.tokenAMint);
|
|
688
|
+
const decB = await getMintDecimals(connection, poolState.tokenBMint);
|
|
689
|
+
|
|
690
|
+
const totalLiq = new BN(posState.unlockedLiquidity?.toString() || "0")
|
|
691
|
+
.add(new BN(posState.vestedLiquidity?.toString() || "0"))
|
|
692
|
+
.add(new BN(posState.permanentLockedLiquidity?.toString() || "0"));
|
|
693
|
+
|
|
694
|
+
let amountA = "0", amountB = "0";
|
|
695
|
+
try {
|
|
696
|
+
const [resA, resB] = getReservesAmountForConcentratedLiquidity(
|
|
697
|
+
poolState.sqrtPrice, poolState.sqrtMinPrice, poolState.sqrtMaxPrice, totalLiq
|
|
698
|
+
);
|
|
699
|
+
amountA = (Number(resA.toString()) / 10 ** decA).toFixed(4);
|
|
700
|
+
amountB = (Number(resB.toString()) / 10 ** decB).toFixed(4);
|
|
701
|
+
} catch {}
|
|
702
|
+
|
|
703
|
+
let feeA = "0", feeB = "0";
|
|
704
|
+
try {
|
|
705
|
+
const fees = getUnClaimLpFee(poolState, posState);
|
|
706
|
+
feeA = (Number(fees.feeTokenA.toString()) / 10 ** decA).toFixed(4);
|
|
707
|
+
feeB = (Number(fees.feeTokenB.toString()) / 10 ** decB).toFixed(4);
|
|
708
|
+
} catch {}
|
|
709
|
+
|
|
710
|
+
allPositions.push({
|
|
711
|
+
type: "DAMM v2",
|
|
712
|
+
position: positionPk.toBase58(),
|
|
713
|
+
pool: posState.pool.toBase58(),
|
|
714
|
+
tokenA: poolState.tokenAMint.toBase58(),
|
|
715
|
+
tokenB: poolState.tokenBMint.toBase58(),
|
|
716
|
+
amountA, amountB, feeA, feeB,
|
|
717
|
+
});
|
|
718
|
+
} catch {}
|
|
719
|
+
}
|
|
720
|
+
} catch {}
|
|
721
|
+
|
|
722
|
+
// ── DLMM ──
|
|
723
|
+
if (!options.json) printInfo("Fetching DLMM positions...");
|
|
724
|
+
try {
|
|
725
|
+
const { default: DLMM } = await import("@meteora-ag/dlmm");
|
|
726
|
+
const posMap = await DLMM.getAllLbPairPositionsByUser(connection, userPubkey);
|
|
727
|
+
|
|
728
|
+
for (const [pairKey, info] of posMap) {
|
|
729
|
+
const decX = info.tokenX.decimal;
|
|
730
|
+
const decY = info.tokenY.decimal;
|
|
731
|
+
|
|
732
|
+
for (const lbPos of info.lbPairPositionsData) {
|
|
733
|
+
const pd = lbPos.positionData;
|
|
734
|
+
const amountX = (Number(pd.totalXAmount) / 10 ** decX).toFixed(4);
|
|
735
|
+
const amountY = (Number(pd.totalYAmount) / 10 ** decY).toFixed(4);
|
|
736
|
+
const feeX = (Number(pd.feeX.toString()) / 10 ** decX).toFixed(4);
|
|
737
|
+
const feeY = (Number(pd.feeY.toString()) / 10 ** decY).toFixed(4);
|
|
738
|
+
|
|
739
|
+
allPositions.push({
|
|
740
|
+
type: "DLMM",
|
|
741
|
+
position: lbPos.publicKey.toBase58(),
|
|
742
|
+
pool: pairKey,
|
|
743
|
+
tokenX: info.tokenX.publicKey.toBase58(),
|
|
744
|
+
tokenY: info.tokenY.publicKey.toBase58(),
|
|
745
|
+
amountX, amountY, feeX, feeY,
|
|
746
|
+
binRange: `${pd.lowerBinId} - ${pd.upperBinId}`,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
} catch {}
|
|
751
|
+
|
|
752
|
+
if (options.json) {
|
|
753
|
+
formatOutput(allPositions, true);
|
|
754
|
+
} else {
|
|
755
|
+
if (allPositions.length === 0) {
|
|
756
|
+
printInfo("No liquidity positions found.");
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
console.log("");
|
|
760
|
+
for (const p of allPositions) {
|
|
761
|
+
console.log(` [${p.type}] Position: ${p.position}`);
|
|
762
|
+
console.log(` Pool: ${p.pool}`);
|
|
763
|
+
if (p.type === "DAMM v2") {
|
|
764
|
+
console.log(` Token A: ${p.amountA} (fee: ${p.feeA}) — ${p.tokenA}`);
|
|
765
|
+
console.log(` Token B: ${p.amountB} (fee: ${p.feeB}) — ${p.tokenB}`);
|
|
766
|
+
} else {
|
|
767
|
+
console.log(` Token X: ${p.amountX} (fee: ${p.feeX}) — ${p.tokenX}`);
|
|
768
|
+
console.log(` Token Y: ${p.amountY} (fee: ${p.feeY}) — ${p.tokenY}`);
|
|
769
|
+
console.log(` Bin range: ${p.binRange}`);
|
|
770
|
+
}
|
|
771
|
+
console.log("");
|
|
772
|
+
}
|
|
773
|
+
console.log(` Total: ${allPositions.length} position(s)`);
|
|
774
|
+
console.log("");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
779
|
+
// REMOVE LIQUIDITY
|
|
780
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
781
|
+
|
|
782
|
+
async function handleRemoveLiquidity(
|
|
783
|
+
pool: string,
|
|
784
|
+
position: string,
|
|
785
|
+
options: GlobalOptions & { bps?: string; all?: boolean }
|
|
786
|
+
) {
|
|
787
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
788
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
789
|
+
const wallet = await loadWallet(options.wallet);
|
|
790
|
+
const poolAddress = new PublicKey(pool);
|
|
791
|
+
const positionKey = new PublicKey(position);
|
|
792
|
+
|
|
793
|
+
if (!options.json) printInfo("Detecting pool type...");
|
|
794
|
+
const accountInfo = await connection.getAccountInfo(poolAddress);
|
|
795
|
+
if (!accountInfo) { printError("Pool account not found"); process.exit(1); }
|
|
796
|
+
|
|
797
|
+
const poolType = identifyPoolType(accountInfo.owner.toBase58());
|
|
798
|
+
if (!poolType) { printError(`Unrecognized pool. Owner: ${accountInfo.owner.toBase58()}`); process.exit(1); }
|
|
799
|
+
|
|
800
|
+
if (poolType === "dbc") {
|
|
801
|
+
printError("DBC pools do not support removing liquidity via CLI.");
|
|
802
|
+
process.exit(1);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
let sig: string;
|
|
806
|
+
|
|
807
|
+
if (poolType === "cpamm") {
|
|
808
|
+
const { CpAmm } = await import("@meteora-ag/cp-amm-sdk");
|
|
809
|
+
const { getAssociatedTokenAddress } = await import("@solana/spl-token");
|
|
810
|
+
const cpAmm = new CpAmm(connection);
|
|
811
|
+
const poolState = await cpAmm.fetchPoolState(poolAddress);
|
|
812
|
+
const positionNftAccount = await getAssociatedTokenAddress(positionKey, wallet.publicKey);
|
|
813
|
+
|
|
814
|
+
if (!options.json) printInfo(options.all ? "Removing all liquidity..." : "Removing liquidity...");
|
|
815
|
+
|
|
816
|
+
const commonParams = {
|
|
817
|
+
owner: wallet.publicKey,
|
|
818
|
+
position: positionKey,
|
|
819
|
+
pool: poolAddress,
|
|
820
|
+
positionNftAccount,
|
|
821
|
+
tokenAAmountThreshold: new BN(0),
|
|
822
|
+
tokenBAmountThreshold: new BN(0),
|
|
823
|
+
tokenAMint: poolState.tokenAMint,
|
|
824
|
+
tokenBMint: poolState.tokenBMint,
|
|
825
|
+
tokenAProgram: poolState.tokenAProgram,
|
|
826
|
+
tokenBProgram: poolState.tokenBProgram,
|
|
827
|
+
vestings: [],
|
|
828
|
+
currentPoint: poolState.currentPoint,
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
let txBuilder;
|
|
832
|
+
if (options.all) {
|
|
833
|
+
txBuilder = cpAmm.removeAllLiquidity(commonParams);
|
|
834
|
+
} else {
|
|
835
|
+
// Get position state to calculate liquidity delta from bps
|
|
836
|
+
const positions = await cpAmm.getPositionsByUser(wallet.publicKey);
|
|
837
|
+
const pos = positions.find(p => p.position.equals(positionKey));
|
|
838
|
+
if (!pos) { printError("Position not found"); process.exit(1); }
|
|
839
|
+
|
|
840
|
+
const bps = options.bps ? parseInt(options.bps) : 10000;
|
|
841
|
+
const totalLiquidity = pos.positionState.liquidity;
|
|
842
|
+
const liquidityDelta = totalLiquidity.mul(new BN(bps)).div(new BN(10000));
|
|
843
|
+
|
|
844
|
+
txBuilder = cpAmm.removeLiquidity({ ...commonParams, liquidityDelta });
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const tx = await txBuilder.transaction();
|
|
848
|
+
tx.feePayer = wallet.publicKey;
|
|
849
|
+
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
850
|
+
tx.sign(wallet);
|
|
851
|
+
sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true });
|
|
852
|
+
} else {
|
|
853
|
+
// DLMM
|
|
854
|
+
const { default: DLMM } = await import("@meteora-ag/dlmm");
|
|
855
|
+
const dlmm = await DLMM.create(connection, poolAddress);
|
|
856
|
+
|
|
857
|
+
const positionInfo = await dlmm.getPosition(positionKey);
|
|
858
|
+
const bps = options.all ? new BN(10000) : new BN(options.bps ?? "10000");
|
|
859
|
+
|
|
860
|
+
if (!options.json) printInfo(options.all ? "Removing all liquidity..." : `Removing ${bps.toString()} bps of liquidity...`);
|
|
861
|
+
|
|
862
|
+
const txs = await dlmm.removeLiquidity({
|
|
863
|
+
user: wallet.publicKey,
|
|
864
|
+
position: positionKey,
|
|
865
|
+
fromBinId: positionInfo.positionData.lowerBinId,
|
|
866
|
+
toBinId: positionInfo.positionData.upperBinId,
|
|
867
|
+
bps,
|
|
868
|
+
shouldClaimAndClose: options.all,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
for (const tx of txs) {
|
|
872
|
+
tx.feePayer = wallet.publicKey;
|
|
873
|
+
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
874
|
+
tx.sign(wallet);
|
|
875
|
+
sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true });
|
|
876
|
+
}
|
|
877
|
+
sig = sig!;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (options.json) {
|
|
881
|
+
formatOutput({ signature: sig, poolType, pool, position }, true);
|
|
882
|
+
} else {
|
|
883
|
+
printSuccess("Liquidity removed!");
|
|
884
|
+
console.log(` Transaction: ${sig}`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
889
|
+
// CLAIM FEE
|
|
890
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
891
|
+
|
|
892
|
+
async function handleClaimFee(
|
|
893
|
+
pool: string, position: string,
|
|
894
|
+
options: GlobalOptions
|
|
895
|
+
) {
|
|
896
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
897
|
+
const connection = new Connection(rpcUrl, "confirmed");
|
|
898
|
+
const wallet = await loadWallet(options.wallet);
|
|
899
|
+
const poolAddress = new PublicKey(pool);
|
|
900
|
+
const positionKey = new PublicKey(position);
|
|
901
|
+
|
|
902
|
+
if (!options.json) printInfo("Detecting pool type...");
|
|
903
|
+
const accountInfo = await connection.getAccountInfo(poolAddress);
|
|
904
|
+
if (!accountInfo) { printError("Pool account not found"); process.exit(1); }
|
|
905
|
+
|
|
906
|
+
const poolType = identifyPoolType(accountInfo.owner.toBase58());
|
|
907
|
+
if (!poolType) { printError(`Unrecognized pool. Owner: ${accountInfo.owner.toBase58()}`); process.exit(1); }
|
|
908
|
+
|
|
909
|
+
if (poolType === "dbc") {
|
|
910
|
+
printError("DBC pools do not support fee claiming via CLI.");
|
|
911
|
+
process.exit(1);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
let sig: string;
|
|
915
|
+
|
|
916
|
+
if (poolType === "cpamm") {
|
|
917
|
+
const { CpAmm } = await import("@meteora-ag/cp-amm-sdk");
|
|
918
|
+
const { getAssociatedTokenAddress } = await import("@solana/spl-token");
|
|
919
|
+
const cpAmm = new CpAmm(connection);
|
|
920
|
+
const poolState = await cpAmm.fetchPoolState(poolAddress);
|
|
921
|
+
const positionNftAccount = await getAssociatedTokenAddress(positionKey, wallet.publicKey);
|
|
922
|
+
|
|
923
|
+
if (!options.json) printInfo("Claiming position fees...");
|
|
924
|
+
|
|
925
|
+
const txBuilder = cpAmm.claimPositionFee2({
|
|
926
|
+
owner: wallet.publicKey,
|
|
927
|
+
position: positionKey,
|
|
928
|
+
pool: poolAddress,
|
|
929
|
+
positionNftAccount,
|
|
930
|
+
tokenAMint: poolState.tokenAMint,
|
|
931
|
+
tokenBMint: poolState.tokenBMint,
|
|
932
|
+
tokenAVault: poolState.tokenAVault,
|
|
933
|
+
tokenBVault: poolState.tokenBVault,
|
|
934
|
+
tokenAProgram: poolState.tokenAProgram,
|
|
935
|
+
tokenBProgram: poolState.tokenBProgram,
|
|
936
|
+
receiver: wallet.publicKey,
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
const tx = await txBuilder.transaction();
|
|
940
|
+
tx.feePayer = wallet.publicKey;
|
|
941
|
+
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
942
|
+
tx.sign(wallet);
|
|
943
|
+
sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true });
|
|
944
|
+
} else {
|
|
945
|
+
// DLMM
|
|
946
|
+
const { default: DLMM } = await import("@meteora-ag/dlmm");
|
|
947
|
+
const dlmm = await DLMM.create(connection, poolAddress);
|
|
948
|
+
const positionInfo = await dlmm.getPosition(positionKey);
|
|
949
|
+
|
|
950
|
+
if (!options.json) printInfo("Claiming swap fees...");
|
|
951
|
+
|
|
952
|
+
const txs = await dlmm.claimSwapFee({
|
|
953
|
+
owner: wallet.publicKey,
|
|
954
|
+
position: positionInfo,
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
for (const tx of txs) {
|
|
958
|
+
tx.feePayer = wallet.publicKey;
|
|
959
|
+
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
960
|
+
tx.sign(wallet);
|
|
961
|
+
sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true });
|
|
962
|
+
}
|
|
963
|
+
sig = sig!;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (options.json) {
|
|
967
|
+
formatOutput({ signature: sig, poolType, pool, position }, true);
|
|
968
|
+
} else {
|
|
969
|
+
printSuccess("Fees claimed!");
|
|
970
|
+
console.log(` Transaction: ${sig}`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
975
|
+
// REGISTER
|
|
976
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
977
|
+
|
|
978
|
+
export function registerDexCommands(program: Command): void {
|
|
979
|
+
const dex = program
|
|
980
|
+
.command("dex", { hidden: true })
|
|
981
|
+
.description("DEX operations — swap tokens and create pools on Meteora (DAMM v2 / DLMM / DBC)");
|
|
982
|
+
|
|
983
|
+
// dex swap
|
|
984
|
+
dex
|
|
985
|
+
.command("swap <pool> <input-token-mint> <amount>")
|
|
986
|
+
.description("Swap tokens on a Meteora pool (auto-detects pool type)")
|
|
987
|
+
.option("--slippage <percent>", "Slippage tolerance in percent (default: 1)")
|
|
988
|
+
.addHelpText("after", `
|
|
989
|
+
Examples:
|
|
990
|
+
npx naracli dex swap <pool-address> <input-mint> 10
|
|
991
|
+
npx naracli dex swap <pool-address> <input-mint> 10 --slippage 0.5`)
|
|
992
|
+
.action(async (pool: string, inputToken: string, amount: string, opts: any, cmd: Command) => {
|
|
993
|
+
try {
|
|
994
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
|
|
995
|
+
await handleSwap(pool, inputToken, amount, { ...globalOpts, slippage: opts.slippage });
|
|
996
|
+
} catch (error: any) {
|
|
997
|
+
printError(error.message);
|
|
998
|
+
process.exit(1);
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
// dex add-liquidity
|
|
1003
|
+
dex
|
|
1004
|
+
.command("add-liquidity <pool> <token-mint> <amount>")
|
|
1005
|
+
.description("Add liquidity to a Meteora pool (DAMM v2 / DLMM). Calculates the paired token amount from pool price.")
|
|
1006
|
+
.option("--amount-b <number>", "Explicitly set paired token amount (skip price calculation)")
|
|
1007
|
+
.option("--position <address>", "Existing position address (creates new if omitted)")
|
|
1008
|
+
.option("--slippage <percent>", "Slippage tolerance in percent (default: 1)")
|
|
1009
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
1010
|
+
.action(async (pool: string, tokenMint: string, amount: string, opts: any, cmd: Command) => {
|
|
1011
|
+
try {
|
|
1012
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
|
|
1013
|
+
await handleAddLiquidity(pool, tokenMint, amount, {
|
|
1014
|
+
...globalOpts, slippage: opts.slippage, position: opts.position,
|
|
1015
|
+
yes: opts.yes, amountB: opts.amountB,
|
|
1016
|
+
});
|
|
1017
|
+
} catch (error: any) {
|
|
1018
|
+
printError(error.message);
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
// dex liquidity-positions
|
|
1024
|
+
dex
|
|
1025
|
+
.command("liquidity-positions [owner-address]")
|
|
1026
|
+
.description("List all liquidity positions across DAMM v2 and DLMM pools. Defaults to your wallet.")
|
|
1027
|
+
.action(async (ownerAddress: string | undefined, _opts: any, cmd: Command) => {
|
|
1028
|
+
try {
|
|
1029
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
|
|
1030
|
+
await handleListPositions({ ...globalOpts, owner: ownerAddress });
|
|
1031
|
+
} catch (error: any) {
|
|
1032
|
+
printError(error.message);
|
|
1033
|
+
process.exit(1);
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
// dex remove-liquidity
|
|
1038
|
+
dex
|
|
1039
|
+
.command("remove-liquidity <pool> <position>")
|
|
1040
|
+
.description("Remove liquidity from a Meteora pool position (DAMM v2 / DLMM)")
|
|
1041
|
+
.option("--bps <number>", "Basis points to remove (10000 = 100%, default: 10000)")
|
|
1042
|
+
.option("--all", "Remove all liquidity and close position")
|
|
1043
|
+
.action(async (pool: string, position: string, opts: any, cmd: Command) => {
|
|
1044
|
+
try {
|
|
1045
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
|
|
1046
|
+
await handleRemoveLiquidity(pool, position, { ...globalOpts, bps: opts.bps, all: opts.all });
|
|
1047
|
+
} catch (error: any) {
|
|
1048
|
+
printError(error.message);
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
// dex claim-fee
|
|
1054
|
+
dex
|
|
1055
|
+
.command("claim-fee <pool> <position>")
|
|
1056
|
+
.description("Claim accumulated trading fees from a position (DAMM v2 / DLMM)")
|
|
1057
|
+
.action(async (pool: string, position: string, _opts: any, cmd: Command) => {
|
|
1058
|
+
try {
|
|
1059
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
|
|
1060
|
+
await handleClaimFee(pool, position, globalOpts);
|
|
1061
|
+
} catch (error: any) {
|
|
1062
|
+
printError(error.message);
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
// dex create-pool
|
|
1068
|
+
const createPool = dex
|
|
1069
|
+
.command("create-pool")
|
|
1070
|
+
.description("Create a new liquidity pool on Meteora");
|
|
1071
|
+
|
|
1072
|
+
// dex create-pool cpamm
|
|
1073
|
+
createPool
|
|
1074
|
+
.command("cpamm")
|
|
1075
|
+
.description("Create a DAMM v2 (CP-AMM) pool. Full-range by default, add --min-price/--max-price for concentrated liquidity.")
|
|
1076
|
+
.requiredOption("--token-a <mint>", "Token A mint address")
|
|
1077
|
+
.requiredOption("--token-b <mint>", "Token B mint address")
|
|
1078
|
+
.requiredOption("--config <address>", "Pool config account address")
|
|
1079
|
+
.requiredOption("--price <number>", "Initial price (token B per token A)")
|
|
1080
|
+
.requiredOption("--amount-a <number>", "Initial token A amount")
|
|
1081
|
+
.requiredOption("--amount-b <number>", "Initial token B amount")
|
|
1082
|
+
.option("--min-price <number>", "Min price for concentrated liquidity range")
|
|
1083
|
+
.option("--max-price <number>", "Max price for concentrated liquidity range")
|
|
1084
|
+
.option("--token-a-program <id>", "Token A program (default: SPL Token)")
|
|
1085
|
+
.option("--token-b-program <id>", "Token B program (default: SPL Token)")
|
|
1086
|
+
.action(async (opts: any, cmd: Command) => {
|
|
1087
|
+
try {
|
|
1088
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
|
|
1089
|
+
await handleCreateCpAmm({ ...globalOpts, ...opts });
|
|
1090
|
+
} catch (error: any) {
|
|
1091
|
+
printError(error.message);
|
|
1092
|
+
process.exit(1);
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
// dex create-pool dlmm
|
|
1097
|
+
createPool
|
|
1098
|
+
.command("dlmm")
|
|
1099
|
+
.description("Create a DLMM (Liquidity Book) pool")
|
|
1100
|
+
.requiredOption("--token-x <mint>", "Token X mint address")
|
|
1101
|
+
.requiredOption("--token-y <mint>", "Token Y mint address")
|
|
1102
|
+
.requiredOption("--bin-step <number>", "Bin step size")
|
|
1103
|
+
.requiredOption("--active-id <number>", "Initial active bin ID (starting price)")
|
|
1104
|
+
.requiredOption("--preset-parameter <address>", "Preset parameter account address")
|
|
1105
|
+
.action(async (opts: any, cmd: Command) => {
|
|
1106
|
+
try {
|
|
1107
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
|
|
1108
|
+
await handleCreateDlmm({ ...globalOpts, ...opts });
|
|
1109
|
+
} catch (error: any) {
|
|
1110
|
+
printError(error.message);
|
|
1111
|
+
process.exit(1);
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
// dex create-pool dbc
|
|
1116
|
+
createPool
|
|
1117
|
+
.command("dbc")
|
|
1118
|
+
.description("Create a Dynamic Bonding Curve pool")
|
|
1119
|
+
.requiredOption("--config <address>", "Pool config account address")
|
|
1120
|
+
.requiredOption("--base-mint <mint>", "Base token mint address")
|
|
1121
|
+
.requiredOption("--name <string>", "Token name")
|
|
1122
|
+
.requiredOption("--symbol <string>", "Token symbol")
|
|
1123
|
+
.requiredOption("--uri <string>", "Token metadata URI")
|
|
1124
|
+
.action(async (opts: any, cmd: Command) => {
|
|
1125
|
+
try {
|
|
1126
|
+
const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
|
|
1127
|
+
await handleCreateDbc({ ...globalOpts, ...opts });
|
|
1128
|
+
} catch (error: any) {
|
|
1129
|
+
printError(error.message);
|
|
1130
|
+
process.exit(1);
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
}
|