@flipflop-sdk/node
Version:
FlipFlop Node.js SDK for programmatic token operations
259 lines • 12.1 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.addLiquidity = void 0;
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 config_1 = require("../config");
const bn_js_1 = __importDefault(require("bn.js"));
const display_pool_1 = require("./display-pool");
const utils_1 = require("../utils");
const constants_1 = require("../constants");
const addLiquidity = async (options) => {
// Validate required parameters
if (!options.rpc) {
return {
success: false,
message: "Missing rpc parameter",
};
}
if (!options.mint) {
return {
success: false,
message: "Missing mint parameter",
};
}
if (!options.tokenAmount || options.tokenAmount <= 0) {
return {
success: false,
message: "Invalid tokenAmount parameter",
};
}
if (options.slippage === undefined || options.slippage < 0) {
return {
success: false,
message: "Invalid slippage parameter",
};
}
if (!options.payer) {
return {
success: false,
message: "Missing provider parameter",
};
}
const connection = new web3_js_1.Connection(options.rpc, "confirmed");
const networkType = (0, config_1.getNetworkType)(options.rpc);
const config = config_1.CONFIGS[networkType];
try {
// Initialize Raydium SDK with proper cluster
const raydium = await raydium_sdk_v2_1.Raydium.load({
owner: options.payer,
connection,
cluster: networkType,
disableFeatureCheck: true,
disableLoadToken: true,
blockhashCommitment: "finalized",
});
// Get token mint
const mintPubkey = new web3_js_1.PublicKey(options.mint);
// Check token balance
const tokenAccounts = await connection.getTokenAccountsByOwner(options.payer.publicKey, { mint: mintPubkey });
if (tokenAccounts.value.length === 0) {
return {
success: false,
message: `No token account found for mint ${options.mint}`,
};
}
// Parse token account data
const tokenAccountInfo = spl_token_1.AccountLayout.decode(tokenAccounts.value[0].account.data);
const availableTokenBalance = new bn_js_1.default(tokenAccountInfo.amount.toString())
.div(new bn_js_1.default(web3_js_1.LAMPORTS_PER_SOL))
.toNumber();
if (availableTokenBalance < options.tokenAmount) {
return {
success: false,
message: `Insufficient token balance. Available: ${availableTokenBalance}, Required: ${options.tokenAmount}`,
};
}
// Fallback to mint-based discovery using API
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;
// Calculate required SOL amount based on pool ratio
const tokenAmountBN = new bn_js_1.default(options.tokenAmount).mul(new bn_js_1.default(web3_js_1.LAMPORTS_PER_SOL));
// Ensure we have valid reserve values
const mintAmountA = String(poolInfoData.baseReserve || "0");
const mintAmountB = String(poolInfoData.quoteReserve || "0");
const cleanMintAmountA = mintAmountA.replace(/[^0-9]/g, "") || "1000000000";
const cleanMintAmountB = mintAmountB.replace(/[^0-9]/g, "") || "1000000000";
// Determine token and SOL reserves based on which mint is which
let tokenReserve;
let solReserve;
const tokenMint = new web3_js_1.PublicKey(options.mint);
if (poolInfoData.mintA.equals(tokenMint)) {
tokenReserve = new bn_js_1.default(cleanMintAmountA);
solReserve = new bn_js_1.default(cleanMintAmountB);
}
else {
tokenReserve = new bn_js_1.default(cleanMintAmountB);
solReserve = new bn_js_1.default(cleanMintAmountA);
}
// Handle zero reserves
if (tokenReserve.isZero()) {
return {
success: false,
message: "Cannot calculate SOL amount: token reserve is zero",
};
}
// Calculate required SOL amount
const requiredSolAmountBN = tokenAmountBN.mul(solReserve).div(tokenReserve);
// Check SOL balance
const solBalance = await connection.getBalance(options.payer.publicKey);
// console.log("SOL balance:", solBalance / 1e9);
// console.log("Required SOL:", requiredSolAmount);
if (solBalance < requiredSolAmountBN.toNumber()) {
return {
success: false,
message: `Insufficient SOL balance. Available: ${solBalance / 1e9}, Required: ${requiredSolAmountBN.toNumber() / 1e9}`,
};
}
// Create slippage percentage
const slippagePercent = new raydium_sdk_v2_1.Percent(options.slippage, 100);
const result = await doAddLiquidityInstruction(connection, poolInfoData, new web3_js_1.PublicKey(options.mint), requiredSolAmountBN, tokenAmountBN, options.payer, slippagePercent);
return {
success: true,
data: result,
};
}
catch (error) {
return {
success: false,
message: `Failed to add liquidity: ${error instanceof Error ? error.message : "Unknown error"}`,
};
}
};
exports.addLiquidity = addLiquidity;
async function doAddLiquidityInstruction(connection, poolInfo, mint, solAmount, tokenAmount, payer, slippagePercent) {
const [mint0, mint1] = (0, utils_1.compareMints)(spl_token_1.NATIVE_MINT, mint) < 0
? [spl_token_1.NATIVE_MINT, mint]
: [mint, spl_token_1.NATIVE_MINT];
const isSolFirst = mint0.equals(spl_token_1.NATIVE_MINT);
const userSolAccount = await (0, spl_token_1.getAssociatedTokenAddress)(spl_token_1.NATIVE_MINT, payer.publicKey);
const userTokenAccount = await (0, spl_token_1.getAssociatedTokenAddress)(mint, payer.publicKey);
const mintA = mint0;
const mintB = mint1;
const tokenAccountA = isSolFirst ? userSolAccount : userTokenAccount;
const tokenAccountB = isSolFirst ? userTokenAccount : userSolAccount;
const vaultA = isSolFirst
? new web3_js_1.PublicKey(poolInfo.vaultA)
: new web3_js_1.PublicKey(poolInfo.vaultB);
const vaultB = isSolFirst
? new web3_js_1.PublicKey(poolInfo.vaultB)
: new web3_js_1.PublicKey(poolInfo.vaultA);
const userLpAccount = await (0, spl_token_1.getAssociatedTokenAddress)(new web3_js_1.PublicKey(poolInfo.mintLp), payer.publicKey);
const [authority] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from(constants_1.AUTH_SEED)], new web3_js_1.PublicKey(poolInfo.programId));
const slippageMultiplier = new bn_js_1.default(10000 +
(slippagePercent.numerator.toNumber() * 10000) /
slippagePercent.denominator.toNumber());
const maxSolAmount = solAmount.mul(slippageMultiplier).div(new bn_js_1.default(10000));
const maxTokenAmount = tokenAmount.mul(slippageMultiplier).div(new bn_js_1.default(10000));
const amountMaxA = isSolFirst ? maxSolAmount : maxTokenAmount;
const amountMaxB = isSolFirst ? maxTokenAmount : maxSolAmount;
// 添加指令数组
const instructions = [];
// 检查并创建 WSOL 账户
try {
const wsolAccountInfo = await (0, spl_token_1.getAccount)(connection, userSolAccount);
// 检查 WSOL 账户余额是否足够
if (new bn_js_1.default(wsolAccountInfo.amount.toString()).lt(maxSolAmount)) {
// WSOL 余额不足,需要包装更多 SOL
const additionalSolNeeded = maxSolAmount.sub(new bn_js_1.default(wsolAccountInfo.amount.toString()));
instructions.push(web3_js_1.SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: userSolAccount,
lamports: additionalSolNeeded.toNumber(),
}), (0, spl_token_1.createSyncNativeInstruction)(userSolAccount));
}
}
catch (error) {
// WSOL 账户不存在,创建账户并包装所需的 SOL
instructions.push((0, spl_token_1.createAssociatedTokenAccountInstruction)(payer.publicKey, userSolAccount, payer.publicKey, spl_token_1.NATIVE_MINT), web3_js_1.SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: userSolAccount,
lamports: maxSolAmount.toNumber(),
}), (0, spl_token_1.createSyncNativeInstruction)(userSolAccount));
}
// 检查并创建目标代币账户
try {
await (0, spl_token_1.getAccount)(connection, userTokenAccount);
}
catch (error) {
instructions.push((0, spl_token_1.createAssociatedTokenAccountInstruction)(payer.publicKey, userTokenAccount, payer.publicKey, mint));
}
// 检查并创建 LP 代币账户
try {
await (0, spl_token_1.getAccount)(connection, userLpAccount);
}
catch (error) {
instructions.push((0, spl_token_1.createAssociatedTokenAccountInstruction)(payer.publicKey, userLpAccount, payer.publicKey, new web3_js_1.PublicKey(poolInfo.mintLp)));
}
let estimatedLpAmount;
if (new bn_js_1.default(poolInfo.lpAmount).isZero()) {
estimatedLpAmount = bn_js_1.default.min(solAmount, tokenAmount);
}
else {
const poolSolReserve = new bn_js_1.default(poolInfo.baseReserve || "0");
const totalLpSupply = new bn_js_1.default(poolInfo.lpAmount);
if (poolSolReserve.gt(new bn_js_1.default(0)) && totalLpSupply.gt(new bn_js_1.default(0))) {
estimatedLpAmount = solAmount.mul(totalLpSupply).div(poolSolReserve);
}
else {
estimatedLpAmount = bn_js_1.default.min(solAmount, tokenAmount);
}
}
// estimatedLpAmount = estimatedLpAmount.mul(new BN(98)).div(new BN(100));
// 添加计算预算指令
instructions.push(web3_js_1.ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }), web3_js_1.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1000 }));
const addLiquidityIx = (0, raydium_sdk_v2_1.makeDepositCpmmInInstruction)(new web3_js_1.PublicKey(poolInfo.programId), // programId
payer.publicKey, // owner
authority, // authority
new web3_js_1.PublicKey(poolInfo.poolAddress), // poolId
userLpAccount, // lpTokenAccount
tokenAccountA, // tokenAccountA (SOL)
tokenAccountB, // tokenAccountB (Token)
vaultA, vaultB, mintA, mintB, new web3_js_1.PublicKey(poolInfo.mintLp), estimatedLpAmount, amountMaxA, amountMaxB);
instructions.push(addLiquidityIx);
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");
return {
signature: sig,
mintAddress: mint,
tokenAmount: tokenAmount,
solAmount: solAmount,
lpTokenAmount: estimatedLpAmount,
poolAddress: poolInfo.poolAddress,
};
}
//# sourceMappingURL=add-liquidity.js.map