import { NextResponse } from 'next/server' import { executeWithFailover, getRpcStatus } from '../../../../lib/rpc-failover.js' export async function POST() { try { console.log('๐Ÿงน Starting orphaned order cleanup...') // Log RPC status const rpcStatus = getRpcStatus() console.log('๐ŸŒ RPC Status:', rpcStatus) // Check if environment is configured if (!process.env.SOLANA_PRIVATE_KEY) { return NextResponse.json({ success: false, error: 'Drift not configured - missing SOLANA_PRIVATE_KEY' }, { status: 400 }) } // Execute cleanup with RPC failover const result = await executeWithFailover(async (connection) => { // Import Drift SDK components const { DriftClient, initialize } = await import('@drift-labs/sdk') const { Keypair } = await import('@solana/web3.js') const { AnchorProvider } = await import('@coral-xyz/anchor') const privateKeyArray = JSON.parse(process.env.SOLANA_PRIVATE_KEY) const keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyArray)) // Use the correct Wallet class const { default: NodeWallet } = await import('@coral-xyz/anchor/dist/cjs/nodewallet.js') const wallet = new NodeWallet(keypair) // Initialize Drift SDK const env = 'mainnet-beta' const sdkConfig = initialize({ env }) const driftClient = new DriftClient({ connection, wallet, programID: sdkConfig.DRIFT_PROGRAM_ID, opts: { commitment: 'confirmed', }, }) try { await driftClient.subscribe() console.log('โœ… Connected to Drift for cleanup') // Get user account let userAccount try { userAccount = await driftClient.getUserAccount() } catch (accountError) { await driftClient.unsubscribe() throw new Error('No Drift user account found. Please initialize your account first.') } // Get current positions const perpPositions = userAccount.perpPositions || [] const activePositions = perpPositions.filter(pos => pos.baseAssetAmount && !pos.baseAssetAmount.isZero() ) // Get current orders const orders = userAccount.orders || [] // Filter for active orders - handle both numeric and object status formats const activeOrders = orders.filter(order => { if (order.baseAssetAmount.isZero()) return false // Handle object-based status (new format) if (typeof order.status === 'object') { return order.status.hasOwnProperty('open') } // Handle numeric status (old format) return order.status === 0 }) console.log(`๐Ÿ“‹ Raw orders in cleanup: ${orders.length}`); orders.forEach((order, index) => { if (!order.baseAssetAmount.isZero()) { console.log(`๐Ÿ“‹ Cleanup Order ${index}:`, { orderId: order.orderId, status: order.status, baseAssetAmount: order.baseAssetAmount.toString() }); } }); console.log(`๐Ÿ“Š Analysis: ${activePositions.length} active positions, ${activeOrders.length} active orders`) // Map positions by market index const positionMarkets = new Set(activePositions.map(pos => pos.marketIndex)) // Find orphaned orders (orders for markets where we have no position) const orphanedOrders = activeOrders.filter(order => { // Check if this order is for a market where we have no position const hasPosition = positionMarkets.has(order.marketIndex) // Also check if it's a reduce-only order (these should be canceled if no position) const isReduceOnly = order.reduceOnly return !hasPosition || (isReduceOnly && !hasPosition) }) // Additionally, find lingering SL/TP orders when position has changed significantly const conflictingOrders = [] for (const order of activeOrders) { // Find corresponding position const position = activePositions.find(pos => pos.marketIndex === order.marketIndex) if (position) { const positionSide = Number(position.baseAssetAmount) > 0 ? 'long' : 'short' const orderDirection = order.direction === 0 ? 'long' : 'short' // Check for conflicting reduce-only orders if (order.reduceOnly) { // Reduce-only order should be opposite direction to position const correctDirection = positionSide === 'long' ? 'short' : 'long' if (orderDirection !== correctDirection) { console.log(`โš ๏ธ Found conflicting reduce-only order: ${orderDirection} order for ${positionSide} position`) conflictingOrders.push(order) } } } } const ordersToCancel = [...orphanedOrders, ...conflictingOrders] console.log(`๐ŸŽฏ Found ${orphanedOrders.length} orphaned orders and ${conflictingOrders.length} conflicting orders`) const cancelResults = [] if (ordersToCancel.length > 0) { console.log('๐Ÿงน Canceling orphaned/conflicting orders...') for (const order of ordersToCancel) { try { const marketIndex = order.marketIndex const orderId = order.orderId // Get market symbol for logging const marketSymbols = { 0: 'SOL-PERP', 1: 'BTC-PERP', 2: 'ETH-PERP', 3: 'APT-PERP', 4: 'BNB-PERP' } const symbol = marketSymbols[marketIndex] || `MARKET-${marketIndex}` console.log(`โŒ Canceling order: ${symbol} Order ID ${orderId}`) // Cancel the order const txSig = await driftClient.cancelOrder(orderId) console.log(`โœ… Canceled order ${orderId} for ${symbol}, tx: ${txSig}`) cancelResults.push({ orderId: orderId, marketIndex: marketIndex, symbol: symbol, txSignature: txSig, success: true, reason: orphanedOrders.includes(order) ? 'orphaned' : 'conflicting' }) // Small delay between cancellations to avoid rate limits await new Promise(resolve => setTimeout(resolve, 100)) } catch (cancelError) { console.error(`โŒ Failed to cancel order ${order.orderId}:`, cancelError) cancelResults.push({ orderId: order.orderId, marketIndex: order.marketIndex, success: false, error: cancelError.message, reason: orphanedOrders.includes(order) ? 'orphaned' : 'conflicting' }) } } } else { console.log('โœ… No orphaned or conflicting orders found') } await driftClient.unsubscribe() const cleanupResult = { success: true, summary: { activePositions: activePositions.length, activeOrders: activeOrders.length, orphanedOrders: orphanedOrders.length, conflictingOrders: conflictingOrders.length, totalCanceled: cancelResults.filter(r => r.success).length, totalFailed: cancelResults.filter(r => !r.success).length }, canceledOrders: cancelResults, timestamp: Date.now(), rpcEndpoint: getRpcStatus().currentEndpoint } console.log('๐Ÿงน Cleanup completed:', cleanupResult.summary) return cleanupResult } catch (driftError) { console.error('โŒ Drift cleanup error:', driftError) try { await driftClient.unsubscribe() } catch (cleanupError) { console.warn('โš ๏ธ Cleanup error:', cleanupError.message) } throw driftError } }, 3) // Max 3 retries across different RPCs return NextResponse.json(result) } catch (error) { console.error('โŒ Orphaned order cleanup API error:', error) return NextResponse.json({ success: false, error: 'Failed to cleanup orphaned orders', details: error.message, rpcStatus: getRpcStatus() }, { status: 500 }) } } export async function GET() { return NextResponse.json({ message: 'Drift Orphaned Order Cleanup API', description: 'Automatically cancels orphaned orders when SL/TP hits but leaves opposite orders open', usage: 'POST /api/drift/cleanup-orders', features: [ 'Detects orphaned orders (orders for markets with no position)', 'Finds conflicting reduce-only orders', 'Automatically cancels problematic orders', 'Prevents manual cleanup requirement' ] }) }