import { Connection, Keypair, PublicKey } from '@solana/web3.js' import { DriftClient, Wallet, OrderType, PositionDirection, MarketType, convertToNumber, BASE_PRECISION, PRICE_PRECISION, QUOTE_PRECISION, BN, ZERO, type PerpPosition, type SpotPosition, getUserAccountPublicKey, DRIFT_PROGRAM_ID } from '@drift-labs/sdk' export interface TradeParams { symbol: string side: 'BUY' | 'SELL' amount: number // USD amount orderType?: 'MARKET' | 'LIMIT' price?: number stopLoss?: number takeProfit?: number stopLossType?: 'PRICE' | 'PERCENTAGE' takeProfitType?: 'PRICE' | 'PERCENTAGE' } export interface TradeResult { success: boolean txId?: string error?: string executedPrice?: number executedAmount?: number conditionalOrders?: string[] } export interface Position { symbol: string side: 'LONG' | 'SHORT' size: number entryPrice: number markPrice: number unrealizedPnl: number marketIndex: number marketType: 'PERP' | 'SPOT' } export interface AccountBalance { totalCollateral: number freeCollateral: number marginRequirement: number accountValue: number leverage: number availableBalance: number netUsdValue: number unrealizedPnl: number } export interface TradeHistory { id: string symbol: string side: 'BUY' | 'SELL' amount: number price: number status: 'FILLED' | 'PENDING' | 'CANCELLED' executedAt: string pnl?: number txId?: string } export interface LoginStatus { isLoggedIn: boolean publicKey: string userAccountExists: boolean error?: string } export class DriftTradingService { private connection: Connection private wallet: Wallet private driftClient: DriftClient | null = null private isInitialized = false private publicKey: PublicKey constructor() { const rpcUrl = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com' const secret = process.env.SOLANA_PRIVATE_KEY if (!secret) throw new Error('Missing SOLANA_PRIVATE_KEY in env') try { const keypair = Keypair.fromSecretKey(Buffer.from(JSON.parse(secret))) this.connection = new Connection(rpcUrl, 'confirmed') this.wallet = new Wallet(keypair) this.publicKey = keypair.publicKey } catch (error) { throw new Error(`Failed to initialize wallet: ${error}`) } } async login(): Promise { try { console.log('🔧 Starting Drift login process...') // First, verify the account exists without SDK console.log('🔍 Pre-checking user account existence...') const userAccountPublicKey = await getUserAccountPublicKey( new PublicKey(DRIFT_PROGRAM_ID), this.publicKey, 0 ) const userAccountInfo = await this.connection.getAccountInfo(userAccountPublicKey) if (!userAccountInfo) { return { isLoggedIn: false, publicKey: this.publicKey.toString(), userAccountExists: false, error: 'User account does not exist. Please initialize your Drift account at app.drift.trade first.' } } console.log('✅ User account confirmed to exist') // Skip SDK subscription entirely and mark as "connected" since account exists console.log('🎯 Using direct account access instead of SDK subscription...') try { // Create client but don't subscribe - just for occasional use this.driftClient = new DriftClient({ connection: this.connection, wallet: this.wallet, env: 'mainnet-beta', opts: { commitment: 'confirmed', preflightCommitment: 'processed' } }) // Mark as initialized without subscription this.isInitialized = true console.log('✅ Drift client created successfully (no subscription needed)') return { isLoggedIn: true, publicKey: this.publicKey.toString(), userAccountExists: true } } catch (error: any) { console.log('⚠️ SDK creation failed, using fallback mode:', error.message) // Even if SDK fails, we can still show as "connected" since account exists this.isInitialized = false return { isLoggedIn: true, // Account exists, so we're "connected" publicKey: this.publicKey.toString(), userAccountExists: true, error: 'Limited mode: Account verified but SDK unavailable. Basic info only.' } } } catch (error: any) { console.error('❌ Login failed:', error.message) return { isLoggedIn: false, publicKey: this.publicKey.toString(), userAccountExists: false, error: `Login failed: ${error.message}` } } } private async disconnect(): Promise { if (this.driftClient) { try { await this.driftClient.unsubscribe() } catch (error) { console.error('Error during disconnect:', error) } this.driftClient = null } this.isInitialized = false } async getAccountBalance(): Promise { try { if (this.isInitialized && this.driftClient) { // Subscribe to user account to access balance data try { console.log('🔍 Subscribing to user account for balance...') await this.driftClient.subscribe() const user = this.driftClient.getUser() // Get account equity and collateral information using proper SDK methods const totalCollateral = convertToNumber( user.getTotalCollateral(), QUOTE_PRECISION ) const freeCollateral = convertToNumber( user.getFreeCollateral(), QUOTE_PRECISION ) // Try to get net USD value using more comprehensive methods let calculatedNetUsdValue = totalCollateral try { // Check if there's a direct method for net USD value or equity // Try different possible method names let directNetValue = null if ('getNetUsdValue' in user) { directNetValue = convertToNumber((user as any).getNetUsdValue(), QUOTE_PRECISION) } else if ('getEquity' in user) { directNetValue = convertToNumber((user as any).getEquity(), QUOTE_PRECISION) } else if ('getTotalAccountValue' in user) { directNetValue = convertToNumber((user as any).getTotalAccountValue(), QUOTE_PRECISION) } if (directNetValue !== null) { calculatedNetUsdValue = directNetValue console.log(`📊 Direct net USD value: $${calculatedNetUsdValue.toFixed(2)}`) } else { console.log('⚠️ No direct net USD method found, will calculate manually') } } catch (e) { console.log('⚠️ Direct net USD method failed:', (e as Error).message) } // Try to get unsettled PnL and funding let unsettledBalance = 0 try { // Try different approaches to get unsettled amounts if ('getUnsettledPnl' in user) { unsettledBalance += convertToNumber((user as any).getUnsettledPnl(), QUOTE_PRECISION) } if ('getPendingFundingPayments' in user) { unsettledBalance += convertToNumber((user as any).getPendingFundingPayments(), QUOTE_PRECISION) } if (unsettledBalance !== 0) { console.log(`📊 Unsettled balance: $${unsettledBalance.toFixed(2)}`) } } catch (e) { console.log('⚠️ Unsettled balance calculation failed:', (e as Error).message) } // Calculate margin requirement using proper method let marginRequirement = 0 try { // According to docs, getMarginRequirement requires MarginCategory parameter marginRequirement = convertToNumber( user.getMarginRequirement('Initial'), QUOTE_PRECISION ) } catch { // Fallback calculation if the method signature is different marginRequirement = Math.max(0, totalCollateral - freeCollateral) } const accountValue = totalCollateral const leverage = marginRequirement > 0 ? totalCollateral / marginRequirement : 1 const availableBalance = freeCollateral // Calculate unrealized PnL from all positions let totalUnrealizedPnl = 0 try { // Get all perp positions to calculate total unrealized PnL const mainMarkets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] // Check more markets for PnL for (const marketIndex of mainMarkets) { try { const position = user.getPerpPosition(marketIndex) if (!position || position.baseAssetAmount.isZero()) continue // Calculate unrealized PnL manually const marketData = this.driftClient.getPerpMarketAccount(marketIndex) const markPrice = convertToNumber(marketData?.amm.lastMarkPriceTwap || new BN(0), PRICE_PRECISION) const entryPrice = convertToNumber(position.quoteEntryAmount.abs(), PRICE_PRECISION) / convertToNumber(position.baseAssetAmount.abs(), BASE_PRECISION) const size = convertToNumber(position.baseAssetAmount.abs(), BASE_PRECISION) const isLong = position.baseAssetAmount.gt(new BN(0)) const unrealizedPnl = isLong ? (markPrice - entryPrice) * size : (entryPrice - markPrice) * size totalUnrealizedPnl += unrealizedPnl } catch (e) { // Skip markets that don't exist continue } } } catch (e) { console.warn('Could not calculate unrealized PnL:', e) } // Net USD Value calculation with enhanced accuracy let finalNetUsdValue = calculatedNetUsdValue // If we got a direct value, use it, otherwise calculate manually if (calculatedNetUsdValue === totalCollateral) { // Manual calculation: Total Collateral + Unrealized PnL + Unsettled finalNetUsdValue = totalCollateral + totalUnrealizedPnl + unsettledBalance console.log(`📊 Manual calculation: Collateral($${totalCollateral.toFixed(2)}) + PnL($${totalUnrealizedPnl.toFixed(2)}) + Unsettled($${unsettledBalance.toFixed(2)}) = $${finalNetUsdValue.toFixed(2)}`) } console.log(`💰 Account balance: $${accountValue.toFixed(2)}, Net USD: $${finalNetUsdValue.toFixed(2)}, PnL: $${totalUnrealizedPnl.toFixed(2)}`) return { totalCollateral, freeCollateral, marginRequirement, accountValue, leverage, availableBalance, netUsdValue: finalNetUsdValue, unrealizedPnl: totalUnrealizedPnl } } catch (sdkError: any) { console.log('⚠️ SDK balance method failed, using fallback:', sdkError.message) // Fall through to fallback method } finally { // Always unsubscribe to clean up if (this.driftClient) { try { await this.driftClient.unsubscribe() } catch (e) { // Ignore unsubscribe errors } } } } // Fallback: Return basic account info console.log('📊 Using fallback balance method - fetching basic account data') const balance = await this.connection.getBalance(this.publicKey) return { totalCollateral: 0, freeCollateral: 0, marginRequirement: 0, accountValue: balance / 1e9, // SOL balance leverage: 0, availableBalance: 0, netUsdValue: balance / 1e9, // Use SOL balance as fallback unrealizedPnl: 0 } } catch (error: any) { throw new Error(`Failed to get account balance: ${error.message}`) } } async executeTrade(params: TradeParams): Promise { if (!this.driftClient || !this.isInitialized) { throw new Error('Client not logged in. Call login() first.') } try { await this.driftClient.subscribe() const marketIndex = await this.getMarketIndex(params.symbol) const direction = params.side === 'BUY' ? PositionDirection.LONG : PositionDirection.SHORT const orderType = params.orderType === 'LIMIT' ? OrderType.LIMIT : OrderType.MARKET const price = params.price ? new BN(Math.round(params.price * PRICE_PRECISION.toNumber())) : undefined const baseAmount = new BN(Math.round(params.amount * BASE_PRECISION.toNumber())) // Place the main order const txSig = await this.driftClient.placeAndTakePerpOrder({ marketIndex, direction, baseAssetAmount: baseAmount, orderType, price, marketType: MarketType.PERP }) console.log(`✅ Main order placed: ${txSig}`) // Place stop loss and take profit orders if specified const conditionalOrders: string[] = [] if (params.stopLoss && params.stopLoss > 0) { try { const stopLossPrice = new BN(Math.round(params.stopLoss * PRICE_PRECISION.toNumber())) const stopLossDirection = direction === PositionDirection.LONG ? PositionDirection.SHORT : PositionDirection.LONG const stopLossTxSig = await this.driftClient.placeAndTakePerpOrder({ marketIndex, direction: stopLossDirection, baseAssetAmount: baseAmount, orderType: OrderType.LIMIT, price: stopLossPrice, marketType: MarketType.PERP, // Add conditional trigger postOnly: false, reduceOnly: true // This ensures it only closes positions }) conditionalOrders.push(stopLossTxSig) console.log(`🛑 Stop loss order placed: ${stopLossTxSig} at $${params.stopLoss}`) } catch (e: any) { console.warn(`⚠️ Failed to place stop loss order: ${e.message}`) } } if (params.takeProfit && params.takeProfit > 0) { try { const takeProfitPrice = new BN(Math.round(params.takeProfit * PRICE_PRECISION.toNumber())) const takeProfitDirection = direction === PositionDirection.LONG ? PositionDirection.SHORT : PositionDirection.LONG const takeProfitTxSig = await this.driftClient.placeAndTakePerpOrder({ marketIndex, direction: takeProfitDirection, baseAssetAmount: baseAmount, orderType: OrderType.LIMIT, price: takeProfitPrice, marketType: MarketType.PERP, postOnly: false, reduceOnly: true // This ensures it only closes positions }) conditionalOrders.push(takeProfitTxSig) console.log(`🎯 Take profit order placed: ${takeProfitTxSig} at $${params.takeProfit}`) } catch (e: any) { console.warn(`⚠️ Failed to place take profit order: ${e.message}`) } } const result = { success: true, txId: txSig, conditionalOrders: conditionalOrders.length > 0 ? conditionalOrders : undefined } // Store the trade in local database for history tracking try { const { default: prisma } = await import('./prisma') // Get current market price (simplified - using a default for now) let currentPrice = 160; // Default SOL price try { // Try to get actual market price from the market const perpMarket = this.driftClient.getPerpMarketAccount(marketIndex) if (perpMarket && perpMarket.amm) { // Use oracle price or mark price if available const oraclePrice = perpMarket.amm.historicalOracleData?.lastOraclePrice || perpMarket.amm.lastMarkPriceTwap || new BN(160 * PRICE_PRECISION.toNumber()) currentPrice = convertToNumber(oraclePrice, PRICE_PRECISION) } } catch (priceError) { console.log('⚠️ Could not get current market price, using default') } await prisma.trade.create({ data: { userId: 'default-user', // TODO: Implement proper user management symbol: params.symbol, side: params.side, amount: params.amount, price: currentPrice, status: 'FILLED', executedAt: new Date(), driftTxId: txSig } }) console.log(`💾 Trade saved to database: ${params.side} ${params.amount} ${params.symbol} at $${currentPrice}`) } catch (dbError) { console.log('⚠️ Failed to save trade to database:', (dbError as Error).message) // Don't fail the trade if database save fails } return result } catch (e: any) { return { success: false, error: e.message } } finally { if (this.driftClient) { await this.driftClient.unsubscribe() } } } async closePosition(symbol: string, amount?: number): Promise { if (!this.driftClient || !this.isInitialized) { throw new Error('Client not logged in. Call login() first.') } try { await this.driftClient.subscribe() const marketIndex = await this.getMarketIndex(symbol) // Get current position to determine the size and direction to close const user = this.driftClient.getUser() const perpPosition = user.getPerpPosition(marketIndex) if (!perpPosition || perpPosition.baseAssetAmount.eq(ZERO)) { return { success: false, error: 'No position found for this symbol' } } const positionSize = Math.abs(perpPosition.baseAssetAmount.toNumber()) / BASE_PRECISION.toNumber() const isLong = perpPosition.baseAssetAmount.gt(ZERO) // Determine amount to close (default to full position) const closeAmount = amount && amount > 0 && amount <= positionSize ? amount : positionSize const baseAmount = new BN(Math.round(closeAmount * BASE_PRECISION.toNumber())) // Close position by taking opposite direction const direction = isLong ? PositionDirection.SHORT : PositionDirection.LONG const txSig = await this.driftClient.placeAndTakePerpOrder({ marketIndex, direction, baseAssetAmount: baseAmount, orderType: OrderType.MARKET, marketType: MarketType.PERP, reduceOnly: true // This ensures it only closes the position }) console.log(`✅ Position closed: ${txSig}`) return { success: true, txId: txSig } } catch (e: any) { console.error(`❌ Failed to close position: ${e.message}`) return { success: false, error: e.message } } finally { if (this.driftClient) { await this.driftClient.unsubscribe() } } } async getPositions(): Promise { try { if (this.isInitialized && this.driftClient) { // Subscribe to user account to access positions try { console.log('🔍 Subscribing to user account for positions...') await this.driftClient.subscribe() const user = this.driftClient.getUser() // Get all available markets const positions: Position[] = [] // Check perp positions - limit to main markets to avoid timeouts const mainMarkets = [0, 1, 2, 3, 4, 5]; // SOL, BTC, ETH and a few others for (const marketIndex of mainMarkets) { try { const p = user.getPerpPosition(marketIndex) if (!p || p.baseAssetAmount.isZero()) continue // Get market price const marketData = this.driftClient.getPerpMarketAccount(marketIndex) const markPrice = convertToNumber(marketData?.amm.lastMarkPriceTwap || new BN(0), PRICE_PRECISION) // Calculate unrealized PnL const entryPrice = convertToNumber(p.quoteEntryAmount.abs(), PRICE_PRECISION) / convertToNumber(p.baseAssetAmount.abs(), BASE_PRECISION) const size = convertToNumber(p.baseAssetAmount.abs(), BASE_PRECISION) const isLong = p.baseAssetAmount.gt(new BN(0)) const unrealizedPnl = isLong ? (markPrice - entryPrice) * size : (entryPrice - markPrice) * size positions.push({ symbol: this.getSymbolFromMarketIndex(marketIndex), side: isLong ? 'LONG' : 'SHORT', size, entryPrice, markPrice, unrealizedPnl, marketIndex, marketType: 'PERP' }) console.log(`✅ Found position: ${this.getSymbolFromMarketIndex(marketIndex)} ${isLong ? 'LONG' : 'SHORT'} ${size}`) } catch (error) { // Skip markets that don't exist or have errors continue } } console.log(`📊 Found ${positions.length} total positions`) return positions } catch (sdkError: any) { console.log('⚠️ SDK positions method failed, using fallback:', sdkError.message) // Fall through to fallback method } finally { // Always unsubscribe to clean up if (this.driftClient) { try { await this.driftClient.unsubscribe() } catch (e) { // Ignore unsubscribe errors } } } } // Fallback: Return empty array instead of demo data console.log('📊 Using fallback positions method - returning empty positions') return [] } catch (error: any) { console.error('❌ Error getting positions:', error) return [] // Return empty array instead of throwing error } } async getTradingHistory(limit: number = 50): Promise { try { console.log('📊 Fetching trading history from Drift...') if (!this.driftClient || !this.isInitialized) { console.log('⚠️ Drift client not initialized, trying local database...') return await this.getLocalTradingHistory(limit) } try { // Subscribe to get access to user data await this.driftClient.subscribe() const user = this.driftClient.getUser() console.log('📊 Getting user order records from Drift SDK...') console.log('📊 Getting user account data from Drift SDK...') // Get user account which contains order and trade history const userAccount = user.getUserAccount() console.log(`📊 User account found with ${userAccount.orders?.length || 0} orders`) // Convert orders to trade history const trades: TradeHistory[] = [] if (userAccount.orders) { for (const order of userAccount.orders.slice(0, limit)) { try { // Only include filled orders (status 2 = filled) if (order.status === 2) { const marketIndex = order.marketIndex const symbol = this.getSymbolFromMarketIndex(marketIndex) const side = order.direction === 0 ? 'BUY' : 'SELL' // 0 = PositionDirection.LONG const baseAmount = order.baseAssetAmountFilled || order.baseAssetAmount const quoteAmount = order.quoteAssetAmountFilled || order.quoteAssetAmount // Calculate executed price from filled amounts const amount = Number(baseAmount.toString()) / 1e9 // Convert from base precision const totalValue = Number(quoteAmount.toString()) / 1e6 // Convert from quote precision const price = amount > 0 ? totalValue / amount : 0 const trade: TradeHistory = { id: order.orderId?.toString() || `order_${Date.now()}_${trades.length}`, symbol, side, amount, price, status: 'FILLED', executedAt: new Date().toISOString(), // Use current time as fallback txId: order.orderId?.toString() || '', pnl: 0 // PnL calculation would require more complex logic } trades.push(trade) console.log(`✅ Processed trade: ${symbol} ${side} ${amount.toFixed(4)} @ $${price.toFixed(2)}`) } } catch (orderError) { console.warn('⚠️ Error processing order:', orderError) continue } } } // Sort by execution time (newest first) trades.sort((a, b) => new Date(b.executedAt).getTime() - new Date(a.executedAt).getTime()) console.log(`✅ Successfully fetched ${trades.length} trades from Drift`) return trades } catch (sdkError: any) { console.error('❌ Error fetching from Drift SDK:', sdkError.message) return await this.getLocalTradingHistory(limit) } finally { if (this.driftClient) { try { await this.driftClient.unsubscribe() } catch (e) { // Ignore unsubscribe errors } } } } catch (error: any) { console.error('❌ Error getting trading history:', error) return await this.getLocalTradingHistory(limit) } } private async getLocalTradingHistory(limit: number): Promise { try { console.log('📊 Checking local trade database...') const { default: prisma } = await import('./prisma') const localTrades = await prisma.trade.findMany({ orderBy: { executedAt: 'desc' }, take: limit }) if (localTrades.length > 0) { console.log(`📊 Found ${localTrades.length} trades in local database`) return localTrades.map((trade: any) => ({ id: trade.id.toString(), symbol: trade.symbol, side: trade.side as 'BUY' | 'SELL', amount: trade.amount, price: trade.price, status: trade.status as 'FILLED' | 'PENDING' | 'CANCELLED', executedAt: trade.executedAt.toISOString(), pnl: trade.pnl || 0, txId: trade.driftTxId || trade.txId || '' })) } console.log('📊 No local trades found') return [] } catch (prismaError) { console.log('⚠️ Local database not available:', (prismaError as Error).message) return [] } } // Helper: map symbol to market index using Drift market data private async getMarketIndex(symbol: string): Promise { if (!this.driftClient) { throw new Error('Client not initialized') } // Common market mappings for Drift const marketMap: { [key: string]: number } = { 'SOLUSD': 0, 'BTCUSD': 1, 'ETHUSD': 2, 'DOTUSD': 3, 'AVAXUSD': 4, 'ADAUSD': 5, 'MATICUSD': 6, 'LINKUSD': 7, 'ATOMUSD': 8, 'NEARUSD': 9, 'APTUSD': 10, 'ORBSUSD': 11, 'RNDUSD': 12, 'WIFUSD': 13, 'JUPUSD': 14, 'TNSUSD': 15, 'DOGEUSD': 16, 'PEPE1KUSD': 17, 'POPCATUSD': 18, 'BOMERUSD': 19 } const marketIndex = marketMap[symbol.toUpperCase()] if (marketIndex === undefined) { throw new Error(`Unknown symbol: ${symbol}. Available symbols: ${Object.keys(marketMap).join(', ')}`) } return marketIndex } // Helper: map market index to symbol private getSymbolFromMarketIndex(index: number): string { const indexMap: { [key: number]: string } = { 0: 'SOLUSD', 1: 'BTCUSD', 2: 'ETHUSD', 3: 'DOTUSD', 4: 'AVAXUSD', 5: 'ADAUSD', 6: 'MATICUSD', 7: 'LINKUSD', 8: 'ATOMUSD', 9: 'NEARUSD', 10: 'APTUSD', 11: 'ORBSUSD', 12: 'RNDUSD', 13: 'WIFUSD', 14: 'JUPUSD', 15: 'TNSUSD', 16: 'DOGEUSD', 17: 'PEPE1KUSD', 18: 'POPCATUSD', 19: 'BOMERUSD' } return indexMap[index] || `MARKET_${index}` } } export const driftTradingService = new DriftTradingService()