فهرست منبع

Merge branch 'claude/serene-banach' of maxmind/meteora-copy into main

maxmind 3 هفته پیش
والد
کامیت
2591ef9d4c
3فایلهای تغییر یافته به همراه119 افزوده شده و 15 حذف شده
  1. 98 10
      src/lib/meteora/dlmm-client.ts
  2. 12 1
      src/lib/solana/transaction-parser.ts
  3. 9 4
      src/worker/executor.ts

+ 98 - 10
src/lib/meteora/dlmm-client.ts

@@ -135,7 +135,10 @@ export async function copyRemoveLiquidity(
 		fromBinId: Math.min(...binIds),
 		toBinId: Math.max(...binIds),
 		bps: new BN(bpsToRemove),
-		shouldClaimAndClose: bpsToRemove >= 10000,
+		// Never use shouldClaimAndClose — its claim fee/reward instructions
+		// can fail with error 3007 (AccountOwnedByWrongProgram) on Token-2022
+		// pools. Close is handled separately by copyClosePosition instead.
+		shouldClaimAndClose: false,
 	})
 
 	let lastSignature = ''
@@ -203,7 +206,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 +219,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,
@@ -260,6 +330,18 @@ export async function copyClosePosition(
 	followerPositionAddress: PublicKey,
 	lbPairAddress: PublicKey
 ): Promise<string> {
+	// First check if the position account still exists on-chain.
+	// It may have been already closed by removeLiquidity(shouldClaimAndClose=true).
+	const positionAccountInfo = await connection.getAccountInfo(
+		followerPositionAddress
+	)
+	if (!positionAccountInfo) {
+		console.log(
+			`[DLMM] Position ${followerPositionAddress.toBase58()} already closed on-chain, skipping close`
+		)
+		return 'already-closed'
+	}
+
 	const dlmmPool = await DLMM.create(connection, lbPairAddress)
 
 	// Need full LbPosition object for closePosition
@@ -272,6 +354,12 @@ export async function copyClosePosition(
 	)
 
 	if (!position) {
+		// Position account exists but not found by SDK (edge case).
+		// This shouldn't happen if the account exists on-chain, but
+		// handle it gracefully.
+		console.warn(
+			`[DLMM] Position ${followerPositionAddress.toBase58()} exists on-chain but not returned by SDK`
+		)
 		throw new Error(
 			`Position ${followerPositionAddress.toBase58()} not found for close`
 		)

+ 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(', ')}`
+					)
 				}
 			}
 

+ 9 - 4
src/worker/executor.ts

@@ -311,7 +311,8 @@ export class CopyExecutor {
 		)
 
 		try {
-			// First remove all liquidity
+			// Step 1: Remove all liquidity (without shouldClaimAndClose to avoid
+			// error 3007 on Token-2022 pools)
 			try {
 				await copyRemoveLiquidity(
 					this.connection,
@@ -321,11 +322,15 @@ export class CopyExecutor {
 					10000,
 					this.settings
 				)
-			} catch {
-				// Liquidity might already be removed
+			} catch (removeErr) {
+				// Liquidity might already be removed (e.g. after a failed rebalance)
+				console.log(
+					`[Executor] Remove liquidity failed (${removeErr}), proceeding to close...`
+				)
 			}
 
-			// Then close the position
+			// Step 2: Close the empty position account (reclaims rent)
+			// copyClosePosition handles already-closed positions gracefully
 			const followerTx = await copyClosePosition(
 				this.connection,
 				this.follower,