import { Connection, Keypair, VersionedTransaction } from '@solana/web3.js' import fetch from 'cross-fetch' export interface TriggerOrder { orderId: string inputMint: string outputMint: string makingAmount: string takingAmount: string targetPrice: number side: 'BUY' | 'SELL' orderType: 'STOP_LOSS' | 'TAKE_PROFIT' | 'LIMIT' status: 'PENDING' | 'EXECUTED' | 'CANCELLED' | 'EXPIRED' createdAt: number executedAt?: number txId?: string requestId?: string } class JupiterTriggerService { private connection: Connection private keypair: Keypair | null = null private activeOrders: TriggerOrder[] = [] // Token mint addresses private tokens = { SOL: 'So11111111111111111111111111111111111111112', // Wrapped SOL USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', USDT: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', } constructor() { const rpcUrl = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com' this.connection = new Connection(rpcUrl, 'confirmed') this.initializeWallet() } private initializeWallet() { try { if (process.env.SOLANA_PRIVATE_KEY) { const privateKeyArray = JSON.parse(process.env.SOLANA_PRIVATE_KEY) this.keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyArray)) console.log('✅ Jupiter Trigger wallet initialized:', this.keypair.publicKey.toString()) } else { console.warn('⚠️ No SOLANA_PRIVATE_KEY found for Jupiter Trigger') } } catch (error) { console.error('❌ Failed to initialize Jupiter Trigger wallet:', error) } } /** * Create a stop loss order * When current price drops to stopPrice, sell the token */ async createStopLossOrder(params: { tokenSymbol: string amount: number // Amount of tokens to sell stopPrice: number // Price at which to trigger the sale slippageBps?: number // Optional slippage (default 0 for exact execution) expiredAt?: number // Optional expiry timestamp }): Promise<{ success: boolean orderId?: string requestId?: string transaction?: string error?: string }> { if (!this.keypair) { return { success: false, error: 'Wallet not initialized' } } try { const { tokenSymbol, amount, stopPrice, slippageBps = 0, expiredAt } = params console.log('🛑 Creating stop loss order:', params) // Determine mint addresses const inputMint = tokenSymbol === 'SOL' ? this.tokens.SOL : this.tokens.USDC const outputMint = tokenSymbol === 'SOL' ? this.tokens.USDC : this.tokens.SOL // Calculate amounts const makingAmount = tokenSymbol === 'SOL' ? Math.floor(amount * 1_000_000_000).toString() // SOL has 9 decimals : Math.floor(amount * 1_000_000).toString() // USDC has 6 decimals const takingAmount = tokenSymbol === 'SOL' ? Math.floor(amount * stopPrice * 1_000_000).toString() // Convert to USDC : Math.floor(amount / stopPrice * 1_000_000_000).toString() // Convert to SOL const orderParams: any = { inputMint, outputMint, maker: this.keypair.publicKey.toString(), payer: this.keypair.publicKey.toString(), params: { makingAmount, takingAmount, }, computeUnitPrice: "auto" } // Add optional parameters if (slippageBps > 0) { orderParams.params.slippageBps = slippageBps.toString() } if (expiredAt) { orderParams.params.expiredAt = expiredAt.toString() } // Create the trigger order const response = await fetch('https://api.jup.ag/trigger/v1/createOrder', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(orderParams) }) if (!response.ok) { const error = await response.json() throw new Error(`Trigger API error: ${error.error || response.status}`) } const result = await response.json() // Store the order locally const order: TriggerOrder = { orderId: result.order, inputMint, outputMint, makingAmount, takingAmount, targetPrice: stopPrice, side: 'SELL', orderType: 'STOP_LOSS', status: 'PENDING', createdAt: Date.now(), requestId: result.requestId } this.activeOrders.push(order) console.log('✅ Stop loss order created:', result.order) return { success: true, orderId: result.order, requestId: result.requestId, transaction: result.transaction } } catch (error: any) { console.error('❌ Failed to create stop loss order:', error) return { success: false, error: error.message } } } /** * Create a take profit order * When current price rises to targetPrice, sell the token */ async createTakeProfitOrder(params: { tokenSymbol: string amount: number // Amount of tokens to sell targetPrice: number // Price at which to trigger the sale slippageBps?: number // Optional slippage (default 0 for exact execution) expiredAt?: number // Optional expiry timestamp }): Promise<{ success: boolean orderId?: string requestId?: string transaction?: string error?: string }> { if (!this.keypair) { return { success: false, error: 'Wallet not initialized' } } try { const { tokenSymbol, amount, targetPrice, slippageBps = 0, expiredAt } = params console.log('🎯 Creating take profit order:', params) // Determine mint addresses const inputMint = tokenSymbol === 'SOL' ? this.tokens.SOL : this.tokens.USDC const outputMint = tokenSymbol === 'SOL' ? this.tokens.USDC : this.tokens.SOL // Calculate amounts const makingAmount = tokenSymbol === 'SOL' ? Math.floor(amount * 1_000_000_000).toString() // SOL has 9 decimals : Math.floor(amount * 1_000_000).toString() // USDC has 6 decimals const takingAmount = tokenSymbol === 'SOL' ? Math.floor(amount * targetPrice * 1_000_000).toString() // Convert to USDC : Math.floor(amount / targetPrice * 1_000_000_000).toString() // Convert to SOL const orderParams: any = { inputMint, outputMint, maker: this.keypair.publicKey.toString(), payer: this.keypair.publicKey.toString(), params: { makingAmount, takingAmount, }, computeUnitPrice: "auto" } // Add optional parameters if (slippageBps > 0) { orderParams.params.slippageBps = slippageBps.toString() } if (expiredAt) { orderParams.params.expiredAt = expiredAt.toString() } // Create the trigger order const response = await fetch('https://api.jup.ag/trigger/v1/createOrder', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(orderParams) }) if (!response.ok) { const error = await response.json() throw new Error(`Trigger API error: ${error.error || response.status}`) } const result = await response.json() // Store the order locally const order: TriggerOrder = { orderId: result.order, inputMint, outputMint, makingAmount, takingAmount, targetPrice: targetPrice, side: 'SELL', orderType: 'TAKE_PROFIT', status: 'PENDING', createdAt: Date.now(), requestId: result.requestId } this.activeOrders.push(order) console.log('✅ Take profit order created:', result.order) return { success: true, orderId: result.order, requestId: result.requestId, transaction: result.transaction } } catch (error: any) { console.error('❌ Failed to create take profit order:', error) return { success: false, error: error.message } } } /** * Execute (sign and send) a trigger order transaction */ async executeOrder(transaction: string, requestId: string): Promise<{ success: boolean txId?: string error?: string }> { if (!this.keypair) { return { success: false, error: 'Wallet not initialized' } } try { console.log('⚡ Executing trigger order transaction') // Deserialize and sign transaction const transactionBuf = Buffer.from(transaction, 'base64') const versionedTransaction = VersionedTransaction.deserialize(transactionBuf) versionedTransaction.sign([this.keypair]) // Send via Jupiter's execute endpoint const response = await fetch('https://api.jup.ag/trigger/v1/execute', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ requestId, transaction: Buffer.from(versionedTransaction.serialize()).toString('base64') }) }) if (!response.ok) { const error = await response.json() throw new Error(`Execute API error: ${error.error || response.status}`) } const result = await response.json() console.log('✅ Trigger order executed:', result.txId) // Update local order status const order = this.activeOrders.find(o => o.requestId === requestId) if (order) { order.status = 'PENDING' order.txId = result.txId } return { success: true, txId: result.txId } } catch (error: any) { console.error('❌ Failed to execute trigger order:', error) return { success: false, error: error.message } } } /** * Cancel a trigger order */ async cancelOrder(orderId: string): Promise<{ success: boolean txId?: string error?: string }> { if (!this.keypair) { return { success: false, error: 'Wallet not initialized' } } try { console.log('❌ Cancelling trigger order:', orderId) const response = await fetch('https://api.jup.ag/trigger/v1/cancelOrder', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ order: orderId, owner: this.keypair.publicKey.toString(), computeUnitPrice: "auto" }) }) if (!response.ok) { const error = await response.json() throw new Error(`Cancel API error: ${error.error || response.status}`) } const result = await response.json() // Sign and send the cancel transaction const transactionBuf = Buffer.from(result.transaction, 'base64') const versionedTransaction = VersionedTransaction.deserialize(transactionBuf) versionedTransaction.sign([this.keypair]) const txId = await this.connection.sendTransaction(versionedTransaction) const confirmation = await this.connection.confirmTransaction(txId, 'confirmed') if (confirmation.value.err) { throw new Error(`Cancel transaction failed: ${confirmation.value.err}`) } // Update local order status const order = this.activeOrders.find(o => o.orderId === orderId) if (order) { order.status = 'CANCELLED' } console.log('✅ Trigger order cancelled:', txId) return { success: true, txId } } catch (error: any) { console.error('❌ Failed to cancel trigger order:', error) return { success: false, error: error.message } } } /** * Get active trigger orders for the wallet */ async getTriggerOrders(): Promise { if (!this.keypair) { return [] } try { const response = await fetch(`https://api.jup.ag/trigger/v1/getTriggerOrders?wallet=${this.keypair.publicKey.toString()}&active=true`) if (!response.ok) { throw new Error(`Get orders API error: ${response.status}`) } const result = await response.json() console.log('📊 Retrieved trigger orders:', result.orders?.length || 0) return result.orders || [] } catch (error: any) { console.error('❌ Failed to get trigger orders:', error) return this.activeOrders // Fallback to local orders } } /** * Create both stop loss and take profit orders for a trade */ async createTradingOrders(params: { tokenSymbol: string amount: number stopLoss?: number takeProfit?: number slippageBps?: number expiredAt?: number }): Promise<{ success: boolean stopLossOrder?: string takeProfitOrder?: string transactions?: string[] error?: string }> { const { tokenSymbol, amount, stopLoss, takeProfit, slippageBps, expiredAt } = params const results: any = { success: true, transactions: [] } try { // Create stop loss order if (stopLoss) { const slResult = await this.createStopLossOrder({ tokenSymbol, amount, stopPrice: stopLoss, slippageBps, expiredAt }) if (slResult.success) { results.stopLossOrder = slResult.orderId results.transactions.push(slResult.transaction) } else { console.warn('⚠️ Failed to create stop loss order:', slResult.error) } } // Create take profit order if (takeProfit) { const tpResult = await this.createTakeProfitOrder({ tokenSymbol, amount, targetPrice: takeProfit, slippageBps, expiredAt }) if (tpResult.success) { results.takeProfitOrder = tpResult.orderId results.transactions.push(tpResult.transaction) } else { console.warn('⚠️ Failed to create take profit order:', tpResult.error) } } return results } catch (error: any) { console.error('❌ Failed to create trading orders:', error) return { success: false, error: error.message } } } getLocalOrders(): TriggerOrder[] { return this.activeOrders } isConfigured(): boolean { return this.keypair !== null } } export const jupiterTriggerService = new JupiterTriggerService() export default JupiterTriggerService