Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 53 additions & 37 deletions jupiter-swap-api-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::sync::Arc;

use quote::{InternalQuoteRequest, QuoteRequest, QuoteResponse};
use reqwest::{Client, Response};
Expand All @@ -12,84 +13,99 @@ pub mod serde_helpers;
pub mod swap;
pub mod transaction_config;

#[derive(Clone)]
pub struct JupiterSwapApiClient {
pub base_path: String,
}

#[derive(Debug, Error)]
pub enum ClientError {
#[error("Network error: {0}")]
NetworkError(#[from] reqwest::Error),
#[error("Request failed with status {status}: {body}")]
RequestFailed {
status: reqwest::StatusCode,
body: String,
},
#[error("Failed to deserialize response: {0}")]
DeserializationError(#[from] reqwest::Error),
DeserializationError(String),
}

async fn check_is_success(response: Response) -> Result<Response, ClientError> {
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(ClientError::RequestFailed { status, body });
}
Ok(response)
}

async fn check_status_code_and_deserialize<T: DeserializeOwned>(
response: Response,
) -> Result<T, ClientError> {
let response = check_is_success(response).await?;
response
.json::<T>()
.await
.map_err(ClientError::DeserializationError)
#[derive(Clone)]
pub struct JupiterSwapApiClient {
pub base_path: String,
// Optimization: Shared HTTP client for connection pooling
client: Client,
}

impl JupiterSwapApiClient {
/// Creates a new Jupiter API client with connection pooling.
pub fn new(base_path: String) -> Self {
Self { base_path }
let client = Client::builder()
.pool_idle_timeout(std::time::Duration::from_secs(90))
.build()
.unwrap_or_else(|_| Client::new());

Self { base_path, client }
}

/// Internal helper to validate response and deserialize JSON.
async fn handle_response<T: DeserializeOwned>(&self, response: Response) -> Result<T, ClientError> {
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_else(|_| "Could not read error body".to_string());
return Err(ClientError::RequestFailed { status, body });
}

response
.json::<T>()
.await
.map_err(|e| ClientError::DeserializationError(e.to_string()))
}

/// Gets a quote for a swap.
pub async fn quote(&self, quote_request: &QuoteRequest) -> Result<QuoteResponse, ClientError> {
let url = format!("{}/quote", self.base_path);
let extra_args = quote_request.quote_args.clone();
let extra_args = &quote_request.quote_args;
let internal_quote_request = InternalQuoteRequest::from(quote_request.clone());
let response = Client::new()

let response = self.client
.get(url)
.query(&internal_quote_request)
.query(&extra_args)
.query(extra_args)
.send()
.await?;
check_status_code_and_deserialize(response).await

self.handle_response(response).await
}

/// Performs a swap.
pub async fn swap(
&self,
swap_request: &SwapRequest,
extra_args: Option<HashMap<String, String>>,
extra_args: Option<&HashMap<String, String>>,
) -> Result<SwapResponse, ClientError> {
let response = Client::new()
.post(format!("{}/swap", self.base_path))
let url = format!("{}/swap", self.base_path);

let response = self.client
.post(url)
.query(&extra_args)
.json(swap_request)
.send()
.await?;
check_status_code_and_deserialize(response).await

self.handle_response(response).await
}

/// Gets instructions for a swap without executing it.
pub async fn swap_instructions(
&self,
swap_request: &SwapRequest,
) -> Result<SwapInstructionsResponse, ClientError> {
let response = Client::new()
.post(format!("{}/swap-instructions", self.base_path))
let url = format!("{}/swap-instructions", self.base_path);

let response = self.client
.post(url)
.json(swap_request)
.send()
.await?;
check_status_code_and_deserialize::<SwapInstructionsResponseInternal>(response)
.await
.map(Into::into)

let internal_res: SwapInstructionsResponseInternal = self.handle_response(response).await?;
Ok(internal_res.into())
}
}