- 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
491 lines
14 KiB
TypeScript
491 lines
14 KiB
TypeScript
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
|