use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tracing::{debug, warn};
use crate::errors::AppError;
const ULTRA_API_BASE: &str = "https://api.jup.ag/ultra/v1";
pub const SOL_MINT: &str = "So11111111111111111111111111111111111111112";
pub const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
pub const USDT_MINT: &str = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB";
pub struct JupiterSwapService {
http_client: Client,
company_wallet: String,
company_currency_mint: String,
api_key: Option<String>,
}
#[derive(Debug)]
pub struct OrderParams {
pub input_mint: String,
pub amount: u64,
pub taker: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SwapOrder {
pub input_mint: String,
pub output_mint: String,
pub in_amount: String,
pub out_amount: String,
#[serde(default)]
pub in_usd_value: Option<f64>,
#[serde(default)]
pub out_usd_value: Option<f64>,
#[serde(default)]
pub slippage_bps: Option<u32>,
pub transaction: Option<String>,
pub request_id: String,
#[serde(default)]
pub error_code: Option<u32>,
#[serde(default)]
pub error_message: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteResult {
pub status: String,
pub code: u32,
#[serde(default)]
pub signature: Option<String>,
#[serde(default)]
pub slot: Option<String>,
#[serde(default)]
pub error: Option<String>,
#[serde(default)]
pub total_input_amount: Option<String>,
#[serde(default)]
pub total_output_amount: Option<String>,
}
impl ExecuteResult {
pub fn is_success(&self) -> bool {
self.status == "Success"
}
}
pub fn mint_for_currency(currency: &str) -> Option<&'static str> {
match currency.to_uppercase().as_str() {
"SOL" => Some(SOL_MINT),
"USDC" => Some(USDC_MINT),
"USDT" => Some(USDT_MINT),
_ => None,
}
}
impl JupiterSwapService {
pub fn new(
company_wallet: String,
company_currency: &str,
api_key: Option<String>,
) -> Result<Self, AppError> {
let company_currency_mint = mint_for_currency(company_currency)
.ok_or_else(|| {
AppError::Internal(anyhow::anyhow!(
"Unknown company currency: {}",
company_currency
))
})?
.to_string();
let http_client = Client::builder()
.timeout(Duration::from_secs(30))
.build()
.map_err(|e| {
AppError::Internal(anyhow::anyhow!("Failed to create HTTP client: {}", e))
})?;
Ok(Self {
http_client,
company_wallet,
company_currency_mint,
api_key,
})
}
pub async fn get_order(&self, params: &OrderParams) -> Result<SwapOrder, AppError> {
let url = format!(
"{}/order?inputMint={}&outputMint={}&amount={}&taker={}&receiver={}",
ULTRA_API_BASE,
params.input_mint,
self.company_currency_mint,
params.amount,
params.taker,
self.company_wallet,
);
let mut request = self.http_client.get(&url);
if let Some(key) = &self.api_key {
request = request.header("x-api-key", key);
}
let response = request.send().await.map_err(|e| {
AppError::Internal(anyhow::anyhow!("Jupiter order request failed: {}", e))
})?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
warn!(status = %status, body = %body, "Jupiter order API error");
return Err(AppError::Internal(anyhow::anyhow!(
"Jupiter order API returned {}: {}",
status,
body
)));
}
let order: SwapOrder = response.json().await.map_err(|e| {
AppError::Internal(anyhow::anyhow!("Failed to parse Jupiter order: {}", e))
})?;
if order.transaction.is_none() {
let msg = order
.error_message
.as_deref()
.unwrap_or("No transaction returned");
return Err(AppError::Validation(format!(
"Jupiter could not create swap: {}",
msg
)));
}
debug!(
input_mint = %order.input_mint,
output_mint = %order.output_mint,
in_amount = %order.in_amount,
out_amount = %order.out_amount,
"Got Jupiter swap order"
);
Ok(order)
}
pub async fn execute_order(
&self,
signed_transaction: &str,
request_id: &str,
) -> Result<ExecuteResult, AppError> {
let url = format!("{}/execute", ULTRA_API_BASE);
let body = serde_json::json!({
"signedTransaction": signed_transaction,
"requestId": request_id,
});
let mut request = self.http_client.post(&url).json(&body);
if let Some(key) = &self.api_key {
request = request.header("x-api-key", key);
}
let response = request.send().await.map_err(|e| {
AppError::Internal(anyhow::anyhow!("Jupiter execute request failed: {}", e))
})?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
warn!(status = %status, body = %body, "Jupiter execute API error");
return Err(AppError::Internal(anyhow::anyhow!(
"Jupiter execute API returned {}: {}",
status,
body
)));
}
let result: ExecuteResult = response.json().await.map_err(|e| {
AppError::Internal(anyhow::anyhow!("Failed to parse Jupiter execute: {}", e))
})?;
if result.is_success() {
debug!(
signature = ?result.signature,
output = ?result.total_output_amount,
"Jupiter swap executed successfully"
);
} else {
warn!(
error = ?result.error,
code = result.code,
"Jupiter swap execution failed"
);
}
Ok(result)
}
pub fn company_currency_mint(&self) -> &str {
&self.company_currency_mint
}
}