fix: timeframe handling and progress tracking improvements
- Fix timeframe parameter handling in enhanced-screenshot API route - Support both 'timeframe' (singular) and 'timeframes' (array) parameters - Add proper sessionId propagation for real-time progress tracking - Enhance MACD analysis prompt with detailed crossover definitions - Add progress tracker service with Server-Sent Events support - Fix Next.js build errors in chart components (module variable conflicts) - Change dev environment port from 9000:3000 to 9001:3000 - Improve AI analysis layout detection logic - Add comprehensive progress tracking through all service layers
This commit is contained in:
490
lib/jupiter-trigger-service.ts
Normal file
490
lib/jupiter-trigger-service.ts
Normal file
@@ -0,0 +1,490 @@
|
||||
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<TriggerOrder[]> {
|
||||
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
|
||||
Reference in New Issue
Block a user