diff --git a/lib/drift/orders.ts b/lib/drift/orders.ts index 0f6ed2a..a64ca10 100644 --- a/lib/drift/orders.ts +++ b/lib/drift/orders.ts @@ -644,35 +644,48 @@ export async function closePosition( } } - // Prepare close order (opposite direction) - const orderParams = { - orderType: OrderType.MARKET, - marketIndex: marketConfig.driftMarketIndex, - direction: position.side === 'long' - ? PositionDirection.SHORT - : PositionDirection.LONG, - baseAssetAmount: new BN(Math.floor(sizeToClose * 1e9)), // 9 decimals - reduceOnly: true, // Important: only close existing position - } + // CRITICAL FIX (Jan 1, 2026 - Bug #91): Use SDK's closePosition() for 100% closes + // + // ROOT CAUSE: Our baseAssetAmount calculation uses: + // baseAssetAmount: new BN(Math.floor(sizeToClose * 1e9)) + // + // The Math.floor() TRUNCATES, leaving fractional remnants that never close! + // Example: Drift has 23.02145678 SOL, we try to close 23.02145670 SOL + // Remaining 0.00000008 SOL stays open forever! + // + // SOLUTION: For 100% closes, use SDK's driftClient.closePosition() which: + // 1. Gets EXACT position size from Drift as BN (no floating point) + // 2. Uses baseAssetAmount.abs() internally (no truncation) + // 3. Calls placeAndTakePerpOrder with exact amount + // + // This matches what the Drift UI "Market" button does vs "Close All" which + // may use a similar size calculation that leaves remnants. - // CRITICAL FIX (Dec 31, 2025 - Bug #89): Use placeAndTakePerpOrder instead of placePerpOrder - // - // ROOT CAUSE: placePerpOrder only PLACES an order on the order book. - // For MARKET orders, the transaction confirms when the order is placed, NOT when it's filled. - // This caused the flip operation to fail - close tx confirmed but position never closed. - // - // SOLUTION: placeAndTakePerpOrder places the order AND fills it against makers atomically. - // This guarantees the position is closed when the transaction confirms. - // - // Evidence: Solscan showed tx 5E713pD6... as "Place long 24.72 SOL perp order at price 0" - // - The order was placed on the book but never filled - // - Position remained open despite "confirmed" transaction - // - // See: https://github.com/drift-labs/drift-vaults/blob/main/tests/testHelpers.ts#L1542 - console.log('🚀 Placing market close order with IMMEDIATE FILL (placeAndTakePerpOrder)...') - const txSig = await retryWithBackoff(async () => { - return await driftClient.placeAndTakePerpOrder(orderParams) - }, 3, 8000) // 8s base delay, 3 max retries + let txSig: string + + if (params.percentToClose === 100) { + // USE SDK'S BUILT-IN CLOSE - Gets EXACT position size from Drift internally + console.log('🚀 Using SDK closePosition() for 100% close (exact BN, no truncation)...') + txSig = await retryWithBackoff(async () => { + return await driftClient.closePosition(marketConfig.driftMarketIndex) + }, 3, 8000) // 8s base delay, 3 max retries + } else { + // Partial close - use our calculation (partial closes are less critical) + const orderParams = { + orderType: OrderType.MARKET, + marketIndex: marketConfig.driftMarketIndex, + direction: position.side === 'long' + ? PositionDirection.SHORT + : PositionDirection.LONG, + baseAssetAmount: new BN(Math.floor(sizeToClose * 1e9)), // 9 decimals + reduceOnly: true, + } + + console.log('🚀 Placing partial close order with placeAndTakePerpOrder...') + txSig = await retryWithBackoff(async () => { + return await driftClient.placeAndTakePerpOrder(orderParams) + }, 3, 8000) + } console.log(`✅ Close order executed! Transaction: ${txSig}`)