@flipflop-sdk/node
Version:
FlipFlop Node.js SDK for programmatic token operations
277 lines • 12.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getLpTokenAmount = void 0;
exports.removeLiquidity = removeLiquidity;
const web3_js_1 = require("@solana/web3.js");
const raydium_sdk_v2_1 = require("@raydium-io/raydium-sdk-v2");
const spl_token_1 = require("@solana/spl-token");
const bn_js_1 = __importDefault(require("bn.js"));
const display_pool_1 = require("./display-pool");
const config_1 = require("../config");
const utils_1 = require("../utils");
const constants_1 = require("../constants");
async function removeLiquidity(options) {
try {
const { rpc, payer, mint, slippage = 1 } = options;
// Validate inputs
if (!mint) {
return {
success: false,
message: "Token mints are required",
};
}
const connection = new web3_js_1.Connection(options.rpc, "confirmed");
const networkType = (0, config_1.getNetworkType)(options.rpc);
const config = config_1.CONFIGS[networkType];
// Initialize Raydium SDK
const raydium = await raydium_sdk_v2_1.Raydium.load({
owner: options.payer,
connection,
cluster: networkType,
disableFeatureCheck: true,
disableLoadToken: true,
blockhashCommitment: "finalized",
});
const poolInfo = await (0, display_pool_1.getPoolInfoByRpc)(connection, raydium, spl_token_1.NATIVE_MINT, options.mint, options.rpc);
if (!poolInfo) {
return {
success: false,
message: `No CPMM pool found for token ${options.mint}. You can specify poolAddress parameter to use a specific pool.`,
};
}
if (!poolInfo.success) {
return {
success: false,
message: poolInfo.message || "Unknown error",
};
}
const poolInfoData = poolInfo.data;
// Get LP token amount
const lpTokenMint = new web3_js_1.PublicKey(poolInfoData.mintLp);
const lpTokenInfo = await connection.getAccountInfo(lpTokenMint);
if (!lpTokenInfo) {
return {
success: false,
message: "Failed to get LP token information",
};
}
const lpToken = await (0, exports.getLpTokenAmount)(connection, payer.publicKey, lpTokenMint);
if (!lpToken.success || !lpToken.data) {
return {
success: false,
message: lpToken.message || "Unknown error",
};
}
const lpTokenAmount = lpToken.data.amount
.mul(new bn_js_1.default(options.removePercentage))
.div(new bn_js_1.default(100));
const result = await doRemoveLiquidityInstruction(connection, poolInfoData, lpTokenAmount, payer, new raydium_sdk_v2_1.Percent(slippage, 100));
// console.log("Liquidity removed successfully. Transaction ID:", result.signature);
// console.log("Pool ID:", poolInfo.poolAddress);
return {
success: true,
data: {
signature: result.signature,
tokenAAmount: result.tokenAAmount,
tokenBAmount: result.tokenBAmount,
},
};
}
catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : "Unknown error occurred",
};
}
}
// 在 doRemoveLiquidityInstruction 函数中修复
async function doRemoveLiquidityInstruction(connection, poolInfo, lpTokenAmountBN, payer, slippagePercent) {
const [authority] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from(constants_1.AUTH_SEED)], new web3_js_1.PublicKey(poolInfo.programId));
// 确保mint排序正确
const mintA = new web3_js_1.PublicKey(poolInfo.mintA);
const mintB = new web3_js_1.PublicKey(poolInfo.mintB);
const [mint0, mint1] = (0, utils_1.compareMints)(mintA, mintB) < 0 ? [mintA, mintB] : [mintB, mintA];
const isAFirst = mint0.equals(mintA);
// 获取正确的用户代币账户
const userTokenAccountA = await (0, spl_token_1.getAssociatedTokenAddress)(mint0, payer.publicKey);
const userTokenAccountB = await (0, spl_token_1.getAssociatedTokenAddress)(mint1, payer.publicKey);
const userLpAccount = await (0, spl_token_1.getAssociatedTokenAddress)(new web3_js_1.PublicKey(poolInfo.mintLp), payer.publicKey);
// 计算预期获得的代币数量(基于池子比例)
const poolTokenAReserve = new bn_js_1.default(poolInfo.baseReserve || "0");
const poolTokenBReserve = new bn_js_1.default(poolInfo.quoteReserve || "0");
const totalLpSupply = new bn_js_1.default(poolInfo.lpAmount);
let expectedTokenAAmount = new bn_js_1.default(0);
let expectedTokenBAmount = new bn_js_1.default(0);
if (totalLpSupply.gt(new bn_js_1.default(0))) {
expectedTokenAAmount = lpTokenAmountBN
.mul(poolTokenAReserve)
.div(totalLpSupply);
expectedTokenBAmount = lpTokenAmountBN
.mul(poolTokenBReserve)
.div(totalLpSupply);
}
// 根据排序调整预期数量
const expectedAmount0 = isAFirst
? expectedTokenAAmount
: expectedTokenBAmount;
const expectedAmount1 = isAFirst
? expectedTokenBAmount
: expectedTokenAAmount;
const slippageMultiplier = new bn_js_1.default(10000 -
(slippagePercent.numerator.toNumber() * 10000) /
slippagePercent.denominator.toNumber());
const minAmount0 = expectedAmount0.mul(slippageMultiplier).div(new bn_js_1.default(10000));
const minAmount1 = expectedAmount1.mul(slippageMultiplier).div(new bn_js_1.default(10000));
// 根据排序调整vault
const vault0 = isAFirst
? new web3_js_1.PublicKey(poolInfo.vaultA)
: new web3_js_1.PublicKey(poolInfo.vaultB);
const vault1 = isAFirst
? new web3_js_1.PublicKey(poolInfo.vaultB)
: new web3_js_1.PublicKey(poolInfo.vaultA);
// console.log("Remove liquidity parameters:", {
// lpTokenAmount: lpTokenAmountBN.toString(),
// expectedAmount0: expectedAmount0.toString(),
// expectedAmount1: expectedAmount1.toString(),
// minAmount0: minAmount0.toString(),
// minAmount1: minAmount1.toString(),
// mint0: mint0.toString(),
// mint1: mint1.toString(),
// });
// 检查并创建必要的代币账户
const instructions = [];
// 检查 tokenA 账户(可能是 WSOL)
try {
await (0, spl_token_1.getAccount)(connection, userTokenAccountA);
}
catch (error) {
if (error.name === "TokenAccountNotFoundError") {
instructions.push((0, spl_token_1.createAssociatedTokenAccountInstruction)(payer.publicKey, userTokenAccountA, payer.publicKey, mint0));
}
}
// 检查 tokenB 账户(可能是 WSOL)
try {
await (0, spl_token_1.getAccount)(connection, userTokenAccountB);
}
catch (error) {
if (error.name === "TokenAccountNotFoundError") {
instructions.push((0, spl_token_1.createAssociatedTokenAccountInstruction)(payer.publicKey, userTokenAccountB, payer.publicKey, mint1));
}
}
// 创建withdraw指令
const withdrawIx = (0, raydium_sdk_v2_1.makeWithdrawCpmmInInstruction)(new web3_js_1.PublicKey(poolInfo.programId), // programId
payer.publicKey, // owner
authority, // authority
new web3_js_1.PublicKey(poolInfo.poolAddress), // poolId
userLpAccount, // lpTokenAccount
userTokenAccountA, // tokenAccountA (正确的账户)
userTokenAccountB, // tokenAccountB (正确的账户)
vault0, // vaultA (正确排序)
vault1, // vaultB (正确排序)
mint0, // mintA (正确排序)
mint1, // mintB (正确排序)
new web3_js_1.PublicKey(poolInfo.mintLp), // lpMint
lpTokenAmountBN, // lpAmount
minAmount0, // minimumAmountA (正确排序)
minAmount1 // minimumAmountB (正确排序)
);
instructions.push(withdrawIx);
// 构建并发送交易
const { blockhash } = await connection.getLatestBlockhash();
const message = new web3_js_1.TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions, // 使用包含创建账户指令的数组
}).compileToV0Message();
const tx = new web3_js_1.VersionedTransaction(message);
tx.sign([payer]);
const sig = await connection.sendTransaction(tx);
await connection.confirmTransaction(sig, "confirmed");
// 添加 WSOL 清理逻辑
try {
// 检查是否有 WSOL 需要清理
const wsolAccount = mint0.equals(spl_token_1.NATIVE_MINT) ? userTokenAccountA :
mint1.equals(spl_token_1.NATIVE_MINT) ? userTokenAccountB : null;
if (wsolAccount) {
const wsolAccountInfo = await (0, spl_token_1.getAccount)(connection, wsolAccount);
const wsolBalance = new bn_js_1.default(wsolAccountInfo.amount.toString());
if (wsolBalance.gt(new bn_js_1.default(0))) {
// 创建关闭 WSOL 账户的指令,将剩余 WSOL 转换回 SOL
const closeInstructions = [
(0, spl_token_1.createCloseAccountInstruction)(wsolAccount, payer.publicKey, payer.publicKey)
];
const { blockhash: closeBlockhash } = await connection.getLatestBlockhash();
const closeMessage = new web3_js_1.TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: closeBlockhash,
instructions: closeInstructions,
}).compileToV0Message();
const closeTx = new web3_js_1.VersionedTransaction(closeMessage);
closeTx.sign([payer]);
const closeSig = await connection.sendTransaction(closeTx);
await connection.confirmTransaction(closeSig, "confirmed");
}
}
}
catch (error) {
console.warn(`Warning: Failed to cleanup WSOL account: ${error.message}`);
// 不抛出错误,因为主要的移除流动性操作已经成功
}
return {
signature: sig,
tokenAAmount: expectedTokenAAmount,
tokenBAmount: expectedTokenBAmount,
lpTokenAmount: lpTokenAmountBN,
poolAddress: new web3_js_1.PublicKey(poolInfo.poolAddress),
};
}
const getLpTokenAmount = async (connection, owner, lpTokenMint) => {
const lpTokenInfo = await connection.getAccountInfo(lpTokenMint);
if (!lpTokenInfo) {
return {
success: false,
message: "Failed to get LP token information",
};
}
// 获取用户的LP代币账户地址
const userLpTokenAccount = await (0, spl_token_1.getAssociatedTokenAddress)(lpTokenMint, owner);
// 获取用户LP代币账户信息
const userLpAccountInfo = await connection.getAccountInfo(userLpTokenAccount);
if (!userLpAccountInfo) {
return {
success: false,
message: "User does not have LP tokens for this pool",
};
}
// 解析LP代币余额
const userLpAccountData = spl_token_1.AccountLayout.decode(userLpAccountInfo.data);
const userLpBalance = new bn_js_1.default(userLpAccountData.amount.toString());
// 获取LP代币的小数位数
const lpMintAccountInfo = await connection.getAccountInfo(lpTokenMint);
if (!lpMintAccountInfo) {
return {
success: false,
message: "Failed to get LP mint information",
};
}
const lpMintData = spl_token_1.MintLayout.decode(lpMintAccountInfo.data);
const lpDecimals = lpMintData.decimals;
if (userLpBalance.lt(new bn_js_1.default(0))) {
return {
success: false,
message: "Insufficient LP token balance",
};
}
return {
success: true,
data: {
amount: userLpBalance,
decimals: lpDecimals,
},
};
};
exports.getLpTokenAmount = getLpTokenAmount;
//# sourceMappingURL=remove-liquidity.js.map