UNPKG

bnbchain-mcp

Version:

---

177 lines (176 loc) 8.3 kB
import { parseAbi, } from 'viem'; import { Pool, Position, nearestUsableTick } from '@pancakeswap/v3-sdk'; import { CurrencyAmount, Percent } from '@pancakeswap/sdk'; import dotenv from 'dotenv'; import { account, client } from '../config.js'; dotenv.config(); // PancakeSwap V3 contract addresses (BSC) const POSITION_MANAGER_ADDRESS = '0x46A15B0b27311cedF172AB29E4f4766fbE7F4364'; const FACTORY_ADDRESS = '0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865'; // Contract ABI definitions const FACTORY_ABI = parseAbi([ 'function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool)' ]); const POOL_ABI = parseAbi([ 'function liquidity() external view returns (uint128)', 'function slot0() external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)' ]); const ERC20_ABI = parseAbi([ 'function allowance(address owner, address spender) external view returns (uint256)', 'function approve(address spender, uint256 amount) external returns (bool)', 'function balanceOf(address account) external view returns (uint256)' ]); const POSITION_MANAGER_ABI = [ { "inputs": [{ "components": [{ "internalType": "address", "name": "token0", "type": "address" }, { "internalType": "address", "name": "token1", "type": "address" }, { "internalType": "uint24", "name": "fee", "type": "uint24" }, { "internalType": "int24", "name": "tickLower", "type": "int24" }, { "internalType": "int24", "name": "tickUpper", "type": "int24" }, { "internalType": "uint256", "name": "amount0Desired", "type": "uint256" }, { "internalType": "uint256", "name": "amount1Desired", "type": "uint256" }, { "internalType": "uint256", "name": "amount0Min", "type": "uint256" }, { "internalType": "uint256", "name": "amount1Min", "type": "uint256" }, { "internalType": "address", "name": "recipient", "type": "address" }, { "internalType": "uint256", "name": "deadline", "type": "uint256" }], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple" }], "name": "mint", "outputs": [{ "internalType": "uint256", "name": "tokenId", "type": "uint256" }, { "internalType": "uint128", "name": "liquidity", "type": "uint128" }, { "internalType": "uint256", "name": "amount0", "type": "uint256" }, { "internalType": "uint256", "name": "amount1", "type": "uint256" }], "stateMutability": "payable", "type": "function" } ]; async function approveTokensIfNeeded(token, spender, amount) { if (!token.isNative) { const tokenAddress = token.address; const accountAddress = account.address; const allowance = await client.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'allowance', args: [accountAddress, spender] }); if (BigInt(allowance.toString()) < BigInt(amount)) { const hash = await client.writeContract({ chain: client.chain, account: account, address: tokenAddress, abi: ERC20_ABI, functionName: 'approve', args: [spender, BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')], }); await client.waitForTransactionReceipt({ hash }); console.log(`Approved ${token.symbol}`); } } } function sortTokens(tokenA, tokenB, amountA, amountB) { let token0 = tokenA.isNative ? tokenA.wrapped : tokenA; let token1 = tokenB.isNative ? tokenB.wrapped : tokenB; if (token0.sortsBefore(token1)) { return [token0, token1, amountA, amountB]; } else { return [token1, token0, amountB, amountA]; } } async function checkBalance(token, amount) { const accountAddress = account.address; if (token.isNative) { const balance = await client.getBalance({ address: accountAddress }); const balanceAmount = CurrencyAmount.fromRawAmount(token, balance.toString()); if (balanceAmount.lessThan(amount)) { throw new Error(`Insufficient balance of ${token.symbol}`); } return; } const balance = await client.readContract({ address: token.address, abi: ERC20_ABI, functionName: 'balanceOf', args: [accountAddress] }); const balanceAmount = CurrencyAmount.fromRawAmount(token, balance.toString()); if (balanceAmount.lessThan(amount)) { throw new Error(`Insufficient balance of ${token.symbol}`); } } /** * Add V3 liquidity * @param tokenA first token * @param tokenB second token * @param fee fee tier * @param amountA amount of tokenA * @param amountB amount of tokenB * @param recipient address to receive LP NFT * @param slippageTolerance slippage tolerance * @param deadline transaction deadline * @param priceLower lower price bound percentage (default: 80% of current price) * @param priceUpper upper price bound percentage (default: 120% of current price) * @returns transaction receipt */ export async function addLiquidityV3(tokenA, tokenB, fee, amountA, amountB, recipient, slippageTolerance = new Percent('50', '10000'), // default 0.5% deadline = Math.floor(Date.now() / 1000) + 20 * 60, // default 20 minutes priceLower = 0.8, priceUpper = 1.2) { await Promise.all([ approveTokensIfNeeded(tokenA, POSITION_MANAGER_ADDRESS, amountA.quotient.toString()), approveTokensIfNeeded(tokenB, POSITION_MANAGER_ADDRESS, amountB.quotient.toString()), ]); await checkBalance(tokenA, amountA); await checkBalance(tokenB, amountB); const [token0, token1, amount0, amount1] = sortTokens(tokenA, tokenB, amountA, amountB); const poolAddress = await client.readContract({ address: FACTORY_ADDRESS, abi: FACTORY_ABI, functionName: 'getPool', args: [ token0.address, token1.address, fee ] }); if (!poolAddress || poolAddress === '0x0000000000000000000000000000000000000000') { throw new Error(`Pool for ${tokenA.symbol}/${tokenB.symbol} not found`); } const [liquidity, slot0] = await Promise.all([ client.readContract({ address: poolAddress, abi: POOL_ABI, functionName: 'liquidity' }), client.readContract({ address: poolAddress, abi: POOL_ABI, functionName: 'slot0' }) ]); const pool = new Pool(token0, token1, fee, slot0[0].toString(), // sqrtPriceX96 liquidity.toString(), slot0[1] // tick ); const tickSpacing = fee / 50; const currentTick = pool.tickCurrent; const lowerPriceTick = currentTick - Math.floor(Math.abs(Math.log(priceLower) / Math.log(1.0001))); const upperPriceTick = currentTick + Math.floor(Math.abs(Math.log(priceUpper) / Math.log(1.0001))); const tickLower = nearestUsableTick(lowerPriceTick, tickSpacing); const tickUpper = nearestUsableTick(upperPriceTick, tickSpacing); const position = Position.fromAmounts({ pool, tickLower, tickUpper, amount0: amount0.quotient.toString(), amount1: amount1.quotient.toString(), useFullPrecision: true }); const { amount0: amount0Min, amount1: amount1Min } = position.mintAmountsWithSlippage(slippageTolerance); const value = tokenA.isNative ? amountA.quotient.toString() : tokenB.isNative ? amountB.quotient.toString() : '0'; const mintParams = { token0: token0.address, token1: token1.address, fee, tickLower, tickUpper, amount0Desired: BigInt(amount0.quotient.toString()), amount1Desired: BigInt(amount1.quotient.toString()), amount0Min: BigInt(amount0Min.toString()), amount1Min: BigInt(amount1Min.toString()), recipient, deadline: BigInt(deadline) }; const hash = await client.writeContract({ chain: client.chain, address: POSITION_MANAGER_ADDRESS, abi: POSITION_MANAGER_ABI, functionName: 'mint', args: [mintParams], value: BigInt(value), account: account }); return hash; }