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

Fix close position: handle already-closed position from shouldClaimAndClose

When closing a follower position, removeLiquidity(bps=10000) with
shouldClaimAndClose=true already closes the position account on-chain.
The subsequent copyClosePosition call then fails because the position
no longer exists.

Changes:
- executor: use removeLiquidity result as the final tx, only fall back
  to closePosition if remove fails (e.g. position already empty)
- dlmm-client: check if position account exists on-chain before
  attempting close, return 'already-closed' gracefully if gone

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
lushdog@outlook.com 3 тижнів тому
батько
коміт
1782064bd0
2 змінених файлів з 34 додано та 12 видалено
  1. 18 0
      src/lib/meteora/dlmm-client.ts
  2. 16 12
      src/worker/executor.ts

+ 18 - 0
src/lib/meteora/dlmm-client.ts

@@ -327,6 +327,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
@@ -339,6 +351,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`
 		)

+ 16 - 12
src/worker/executor.ts

@@ -311,9 +311,12 @@ export class CopyExecutor {
 		)
 
 		try {
-			// First remove all liquidity
+			// Remove all liquidity with shouldClaimAndClose=true.
+			// This removes liquidity, claims fees, AND closes the position account
+			// in one step — no separate closePosition call needed.
+			let followerTx: string
 			try {
-				await copyRemoveLiquidity(
+				followerTx = await copyRemoveLiquidity(
 					this.connection,
 					this.follower,
 					new PublicKey(followerInfo.positionAddress),
@@ -321,18 +324,19 @@ export class CopyExecutor {
 					10000,
 					this.settings
 				)
-			} catch {
-				// Liquidity might already be removed
+			} catch (removeErr) {
+				// If remove fails (e.g. position already empty), try direct close
+				console.log(
+					`[Executor] Remove liquidity failed (${removeErr}), trying direct close...`
+				)
+				followerTx = await copyClosePosition(
+					this.connection,
+					this.follower,
+					new PublicKey(followerInfo.positionAddress),
+					new PublicKey(followerInfo.lbPairAddress)
+				)
 			}
 
-			// Then close the position
-			const followerTx = await copyClosePosition(
-				this.connection,
-				this.follower,
-				new PublicKey(followerInfo.positionAddress),
-				new PublicKey(followerInfo.lbPairAddress)
-			)
-
 			await closeFollowerPosition(followerInfo.positionAddress)
 			await updateCopyTradeStatus(copyTrade.id, 'SUCCESS', {
 				followerTxSignature: followerTx,