Переглянути джерело

Fix rebalance: use follower's actual token balances instead of leader's safety caps

The rebalance_liquidity IDL params contain max_deposit_x/y_amount which are
safety caps (often u64::MAX), not actual deposit amounts. Using these caused
"insufficient lamports" errors with absurdly large amounts (368934881474191032).

Changes:
- Parser: fix field name from data.rebalance_liquidity_params to data.params
- Parser: remove max_deposit amount extraction, add comment explaining they're caps
- dlmm-client: query follower's actual SOL/SPL token balances after removing
  liquidity, with 0.01 SOL reserve for tx fees
- dlmm-client: add bin range validation to catch extraction failures early

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
lushdog@outlook.com 3 тижнів тому
батько
коміт
983ebfca04
2 змінених файлів з 88 додано та 10 видалено
  1. 76 9
      src/lib/meteora/dlmm-client.ts
  2. 12 1
      src/lib/solana/transaction-parser.ts

+ 76 - 9
src/lib/meteora/dlmm-client.ts

@@ -203,7 +203,8 @@ export async function copyRebalanceLiquidity(
 	let minBinId = leaderOp.minBinId ?? 0
 	let maxBinId = leaderOp.maxBinId ?? 0
 
-	// If we have delta-based adds, compute absolute bin IDs from current active bin
+	// If we have delta-based adds, compute absolute bin IDs from the current active bin
+	// (not the leader's active bin, since it may have moved)
 	if (leaderOp.rebalanceAdds && leaderOp.rebalanceAdds.length > 0) {
 		const allMinBins = leaderOp.rebalanceAdds.map(
 			(a) => activeId + a.minDeltaId
@@ -215,22 +216,88 @@ export async function copyRebalanceLiquidity(
 		maxBinId = Math.max(...allMaxBins)
 	}
 
+	// Validate bin range — if both are 0, the parameter extraction failed
+	if (minBinId === 0 && maxBinId === 0 && activeId !== 0) {
+		throw new Error(
+			`Rebalance failed: could not determine target bin range (activeBin=${activeId}). ` +
+				`Leader rebalanceAdds: ${JSON.stringify(leaderOp.rebalanceAdds)}`
+		)
+	}
+
 	console.log(
 		`[DLMM] Rebalancing: new bins [${minBinId}, ${maxBinId}], activeBin ${activeId}`
 	)
 
-	// Scale amounts by copy ratio
-	const followerAmountX = scaleAmount(
-		leaderOp.totalXAmount ?? new BN(0),
-		settings.copyRatio
-	)
-	const followerAmountY = scaleAmount(
-		leaderOp.totalYAmount ?? new BN(0),
-		settings.copyRatio
+	// For rebalance, use the follower's actual available token balances
+	// (not the leader's amounts — those are just safety caps from the IDL).
+	// After step 1 removed all liquidity, the tokens are in the follower's accounts.
+	const WSOL_MINT = new PublicKey(
+		'So11111111111111111111111111111111111111112'
 	)
+	const tokenXMint = dlmmPool.tokenX.publicKey
+	const tokenYMint = dlmmPool.tokenY.publicKey
+
+	const SOL_RESERVE = 10_000_000 // Keep 0.01 SOL for tx fees
+
+	let followerAmountX = new BN(0)
+	let followerAmountY = new BN(0)
+
+	// Token X balance
+	if (tokenXMint.equals(WSOL_MINT)) {
+		const balance = await connection.getBalance(follower.publicKey)
+		followerAmountX = new BN(Math.max(0, balance - SOL_RESERVE))
+	} else {
+		const xAccounts = await connection.getParsedTokenAccountsByOwner(
+			follower.publicKey,
+			{ mint: tokenXMint }
+		)
+		for (const { account } of xAccounts.value) {
+			followerAmountX = followerAmountX.add(
+				new BN(
+					(
+						account.data.parsed as {
+							info: { tokenAmount: { amount: string } }
+						}
+					).info.tokenAmount.amount
+				)
+			)
+		}
+	}
+
+	// Token Y balance
+	if (tokenYMint.equals(WSOL_MINT)) {
+		const balance = await connection.getBalance(follower.publicKey)
+		followerAmountY = new BN(Math.max(0, balance - SOL_RESERVE))
+	} else {
+		const yAccounts = await connection.getParsedTokenAccountsByOwner(
+			follower.publicKey,
+			{ mint: tokenYMint }
+		)
+		for (const { account } of yAccounts.value) {
+			followerAmountY = followerAmountY.add(
+				new BN(
+					(
+						account.data.parsed as {
+							info: { tokenAmount: { amount: string } }
+						}
+					).info.tokenAmount.amount
+				)
+			)
+		}
+	}
+
+	if (followerAmountX.isZero() && followerAmountY.isZero()) {
+		throw new Error(
+			'Rebalance failed: follower has no token balance to re-deposit after removing liquidity.'
+		)
+	}
 
 	const sdkStrategyType = toSdkStrategyType(leaderOp.strategyType)
 
+	console.log(
+		`[DLMM] Rebalance addLiquidity: strategy ${sdkStrategyType}, amountX ${followerAmountX.toString()}, amountY ${followerAmountY.toString()}`
+	)
+
 	const addLiqTx = await dlmmPool.addLiquidityByStrategy({
 		positionPubKey: followerPositionAddress,
 		user: follower.publicKey,

+ 12 - 1
src/lib/solana/transaction-parser.ts

@@ -250,9 +250,11 @@ export function parseTransaction(
 			}
 
 			// Extract rebalance liquidity parameters
+			// IDL args: { params: RebalanceLiquidityParams, remaining_accounts_info: ... }
 			if (action === 'REBALANCE_LIQUIDITY') {
 				const data = decoded.data as Record<string, unknown>
-				const rebalanceParams = data.rebalance_liquidity_params as
+				// The IDL arg name is "params", not "rebalance_liquidity_params"
+				const rebalanceParams = data.params as
 					| Record<string, unknown>
 					| undefined
 				if (rebalanceParams) {
@@ -288,6 +290,15 @@ export function parseTransaction(
 							bpsToRemove: rem.bps_to_remove as number,
 						}))
 					}
+
+					// Note: max_deposit_x_amount / max_deposit_y_amount are safety caps
+					// (often set to u64::MAX), NOT actual deposit amounts. Do not use
+					// them as totalXAmount/totalYAmount — the rebalance executor
+					// queries the follower's actual token balances instead.
+				} else {
+					console.warn(
+						`[Parser] Could not extract rebalance params. Keys: ${Object.keys(data).join(', ')}`
+					)
 				}
 			}