import { MeteoraWrapper } from "./meteora"; import logger from "./utils/logger"; import { REBALANCE_THRESHOLD_PERCENT, COMPOUND_THRESHOLD_USD, JLP_USD_ESTIMATE, SOL_USD_ESTIMATE } from "./utils/config"; import { getSwapQuote, executeSwap } from "./swap"; import { PublicKey } from "@solana/web3.js"; import { BN } from "@coral-xyz/anchor"; export class DLMMBot { private meteora: MeteoraWrapper; constructor() { this.meteora = new MeteoraWrapper(); } async init(): Promise { await this.meteora.init(); } async rebalance(): Promise { const startTime = new Date().toISOString(); try { logger.info(`--- Portfolio Status Report [${startTime}] ---`); // Force refresh of pool data in each cycle await this.meteora.init(); const activeBin = await this.meteora.getActiveBin(); const positions = await this.meteora.getPositions(); logger.info(`Active Bin ID: ${activeBin.binId}`); logger.info(`Number of active positions: ${positions.length}`); if (positions.length === 0) { logger.info( "No active positions found. Checking for tokens to deposit..." ); await this.handleInitialDeposit(activeBin.binId); return; } let priceRangeRebalance = false; for (const position of positions) { const lowerBinId = position.positionData.lowerBinId; const upperBinId = position.positionData.upperBinId; const range = upperBinId - lowerBinId; const threshold = Math.floor(range * REBALANCE_THRESHOLD_PERCENT); const lowerBoundary = lowerBinId + threshold; const upperBoundary = upperBinId - threshold; const distanceToLower = activeBin.binId - lowerBoundary; const distanceToUpper = upperBoundary - activeBin.binId; logger.info(`Position ${position.publicKey.toBase58()}: Range [${lowerBinId}, ${upperBinId}], Threshold Boundary [${lowerBoundary}, ${upperBoundary}]`); logger.info(`Distance to boundaries: Lower=+${distanceToLower}, Upper=+${distanceToUpper}`); if (activeBin.binId <= lowerBoundary || activeBin.binId >= upperBoundary) { logger.info("Active bin reached boundary. Price range rebalance required."); priceRangeRebalance = true; break; } } let rebalanceRequired = priceRangeRebalance; let totalValueUSD = 0; let hasSubstantialBalance = false; if (!rebalanceRequired) { // Value-based compound check const { balanceX, balanceY } = await this.meteora.getBalances(); const tokenX = this.meteora.dlmmPool?.tokenX; const tokenY = this.meteora.dlmmPool?.tokenY; if (!tokenX || !tokenY) { logger.warn("Pool tokens not available for value calculation."); return; } const totalX = balanceX.toNumber() / 10 ** (tokenX.mint as any).decimals; const totalY = balanceY.toNumber() / 10 ** (tokenY.mint as any).decimals; const isXSol = tokenX.mint.address.toBase58() === "So11111111111111111111111111111111111111112"; const isYSol = tokenY.mint.address.toBase58() === "So11111111111111111111111111111111111111112"; let valueX = 0; let valueY = 0; if (isXSol) { valueX = totalX * SOL_USD_ESTIMATE; valueY = totalY * JLP_USD_ESTIMATE; } else if (isYSol) { valueY = totalY * SOL_USD_ESTIMATE; valueX = totalX * JLP_USD_ESTIMATE; } else { valueX = totalX * JLP_USD_ESTIMATE; valueY = totalY * JLP_USD_ESTIMATE; } totalValueUSD = valueX + valueY; hasSubstantialBalance = totalValueUSD >= COMPOUND_THRESHOLD_USD; if (hasSubstantialBalance) { logger.info(`Substantial wallet balance detected ($${totalValueUSD.toFixed(2)}). Triggering value-based compounding...`); rebalanceRequired = true; } else { logger.info(`Portfolio healthy. Uninvested balance ($${totalValueUSD.toFixed(2)}) is below threshold ($${COMPOUND_THRESHOLD_USD}).`); logger.info(`--- Cycle Complete [${new Date().toISOString()}] ---`); return; } } await this.meteora.withdrawAll(); // Claim fees after withdrawal to consolidate balances await this.meteora.claimFees(); const { balanceX, balanceY } = await this.meteora.getBalances(); if (balanceX.isZero() && balanceY.isZero()) { logger.warn("No balances available to re-deposit after withdrawal."); return; } // Financial Safety: Only skip if NOT a price-range rebalance and value is too low const estimatedFeeUSD = 0.005 * SOL_USD_ESTIMATE; if (!priceRangeRebalance && totalValueUSD < estimatedFeeUSD) { logger.warn(`Skipping compounding: Total uninvested value ($${totalValueUSD.toFixed(2)}) is less than estimated transaction fees ($${estimatedFeeUSD.toFixed(2)}).`); return; } await this.meteora.deposit(balanceX, balanceY, activeBin.binId); logger.info("Rebalance/Compounding completed successfully."); logger.info(`--- Cycle Complete [${new Date().toISOString()}] ---`); } catch (error: any) { logger.error(`CRITICAL: Error during rebalance cycle: ${error.message}`); } } private async handleInitialDeposit(activeBinId: number): Promise { let { balanceX, balanceY } = await this.meteora.getBalances(); if (balanceX.isZero() && balanceY.isZero()) { logger.info("No token balances found for initial deposit."); return; } // Balanced investment logic: if one side is zero, swap 50% const SOL_MINT = "So11111111111111111111111111111111111111112"; const pool = this.meteora.dlmmPool; if (!pool) { logger.error("Pool not initialized during handleInitialDeposit."); return; } const isXSol = pool.tokenX.mint.address.toBase58() === SOL_MINT; const isYSol = pool.tokenY.mint.address.toBase58() === SOL_MINT; if (balanceX.isZero() || balanceY.isZero()) { logger.info("Single asset detected. Performing 50/50 swap to balance the position..."); try { if (isXSol && !balanceX.isZero()) { const swapAmount = balanceX.div(new BN(2)).toNumber(); logger.info(`Swapping ${swapAmount / 1e9} SOL for TokenY...`); const quote = await getSwapQuote(SOL_MINT, pool.tokenY.mint.address.toBase58(), swapAmount); await executeSwap(quote); } else if (isYSol && !balanceY.isZero()) { const swapAmount = balanceY.div(new BN(2)).toNumber(); logger.info(`Swapping ${swapAmount / 1e9} SOL for TokenX...`); const quote = await getSwapQuote(SOL_MINT, pool.tokenX.mint.address.toBase58(), swapAmount); await executeSwap(quote); } else { logger.warn("Only non-SOL asset detected. Proceeding with single-sided deposit (no auto-swap for non-SOL yet)."); } // Refetch balances after swap ({ balanceX, balanceY } = await this.meteora.getBalances()); } catch (error: any) { logger.error(`Swap failed during initial deposit: ${error.message}. Proceeding with current balances.`); } } logger.info( `Initial deposit: X=${balanceX.toString()}, Y=${balanceY.toString()}` ); await this.meteora.deposit(balanceX, balanceY, activeBinId); } async compound(): Promise { logger.info("Starting compounding..."); await this.meteora.claimFees(); logger.info("Compounding finished."); } }