diff --git a/examples/src/ultra.rs b/examples/src/ultra.rs index c3e55ba..31dfb19 100644 --- a/examples/src/ultra.rs +++ b/examples/src/ultra.rs @@ -49,3 +49,16 @@ pub async fn ultra() { println!("✅ Transaction submitted: {}", tx_signature); } + +pub async fn ultra_token_search() { + let client = JupiterClient::new("https:://lite-api.jup.ag"); + + let mints = vec![String::from("JUP")]; + + let data = client + .ultra_token_search(&mints) + .await + .expect("failed to get token info"); + + println!("data: {:?}", data); +} diff --git a/jup-ag-sdk/src/client/ultra_api.rs b/jup-ag-sdk/src/client/ultra_api.rs index 353b5ae..ae6027b 100644 --- a/jup-ag-sdk/src/client/ultra_api.rs +++ b/jup-ag-sdk/src/client/ultra_api.rs @@ -1,8 +1,8 @@ use crate::{ error::{JupiterClientError, handle_response}, types::{ - Router, Shield, TokenBalancesResponse, UltraExecuteOrderRequest, UltraExecuteOrderResponse, - UltraOrderRequest, UltraOrderResponse, + Router, Shield, TokenBalancesResponse, TokenInfo, UltraExecuteOrderRequest, + UltraExecuteOrderResponse, UltraOrderRequest, UltraOrderResponse, }, }; @@ -190,6 +190,57 @@ impl JupiterClient { } } + /// search for a token and its information by its symbol, name or mint address + /// + /// Limit to 100 mint addresses in query + /// + /// # Arguments + /// + /// * `mints` - A slice of mint addresses (`&[String]`) to inspect. + /// + /// # Returns + /// + /// * `Ok(Vec)` containing token safety metadata. + /// * `Err` if the request or deserialization fails. + /// + /// # Jupiter API Reference + /// + /// - [Search Endpoint](https://dev.jup.ag/docs/api/ultra-api/search) + /// + /// # Example + /// + /// ``` + /// let mints = vec![ + /// String::from("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), + /// String::from("JUP") + /// ]; + /// let token_info = client.ultra_token_search(&mints).await?; + /// ``` + pub async fn ultra_token_search( + &self, + mints: &[String], + ) -> Result, JupiterClientError> { + let query_params = vec![("query", mints.join(","))]; + + let response = match self + .client + .get(format!("{}/ultra/v1/search", self.base_url)) + .query(&query_params) + .send() + .await + { + Ok(resp) => resp, + Err(e) => return Err(JupiterClientError::RequestError(e)), + }; + + let response = handle_response(response).await?; + + match response.json::>().await { + Ok(data) => Ok(data), + Err(e) => Err(JupiterClientError::DeserializationError(e.to_string())), + } + } + /// Request for the list of routers available in the routing engine of Ultra, which is Juno pub async fn routers(&self) -> Result, JupiterClientError> { let response = match self diff --git a/jup-ag-sdk/src/types/ultra.rs b/jup-ag-sdk/src/types/ultra.rs index 216aef0..3e5d652 100644 --- a/jup-ag-sdk/src/types/ultra.rs +++ b/jup-ag-sdk/src/types/ultra.rs @@ -324,3 +324,112 @@ pub struct Router { pub name: String, pub icon: String, } + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenStats { + pub price_change: Option, + pub holder_change: Option, + pub liquidity_change: Option, + pub volume_change: Option, + pub buy_volume: Option, + pub sell_volume: Option, + pub buy_organic_volume: Option, + pub sell_organic_volume: Option, + pub num_buys: Option, + pub num_sells: Option, + pub num_traders: Option, + pub num_organic_buyers: Option, + pub num_net_buyers: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FirstPool { + pub id: String, + pub created_at: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Audit { + #[serde(default)] + pub is_sus: Option, + #[serde(default)] + pub mint_authority_disabled: Option, + #[serde(default)] + pub freeze_authority_disabled: Option, + #[serde(default)] + pub top_holders_percentage: Option, + #[serde(default)] + pub dev_balance_percentage: Option, + #[serde(default)] + pub dev_migrations: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenInfo { + pub id: String, + pub name: String, + pub symbol: String, + #[serde(default)] + pub icon: Option, + pub decimals: u8, + #[serde(default)] + pub twitter: Option, + #[serde(default)] + pub telegram: Option, + #[serde(default)] + pub website: Option, + #[serde(default)] + pub dev: Option, + pub circ_supply: f64, + pub total_supply: f64, + pub token_program: String, + + #[serde(default)] + pub launchpad: Option, + #[serde(default)] + pub partner_config: Option, + #[serde(default)] + pub graduated_pool: Option, + #[serde(default)] + pub graduated_at: Option, + #[serde(default)] + pub mint_authority: Option, + #[serde(default)] + pub freeze_authority: Option, + + pub first_pool: FirstPool, + pub holder_count: u64, + #[serde(default)] + pub audit: Option, + pub organic_score: f64, + pub organic_score_label: String, + #[serde(default)] + pub is_verified: Option, + #[serde(default)] + pub cexes: Vec, + #[serde(default)] + pub tags: Vec, + pub fdv: f64, + pub mcap: f64, + pub usd_price: f64, + pub price_block_id: u64, + pub liquidity: f64, + + #[serde(default)] + pub stats5m: Option, + #[serde(default)] + pub stats1h: Option, + #[serde(default)] + pub stats6h: Option, + #[serde(default)] + pub stats24h: Option, + #[serde(default)] + pub ct_likes: Option, + #[serde(default)] + pub smart_ct_likes: Option, + pub updated_at: String, +} diff --git a/tests/src/trigger.rs b/tests/src/trigger.rs index 679e4ae..a54ab97 100644 --- a/tests/src/trigger.rs +++ b/tests/src/trigger.rs @@ -1,5 +1,5 @@ #[cfg(test)] -mod ultra_tests { +mod trigger_tests { use jup_ag_sdk::types::{ CreateTriggerOrder, ExecuteTriggerOrder, GetTriggerOrders, OrderStatus, }; diff --git a/tests/src/ultra.rs b/tests/src/ultra.rs index ec9064a..5ae5f41 100644 --- a/tests/src/ultra.rs +++ b/tests/src/ultra.rs @@ -131,6 +131,32 @@ mod ultra_tests { ); } + #[tokio::test] + async fn test_ultra_token_search() { + let client = create_test_client(); + + let mints = vec!["JUP".to_string()]; + + let tokens = client + .ultra_token_search(&mints) + .await + .expect("Failed to search tokens"); + + let token = &tokens[0]; + assert_eq!(token.id, JUP_MINT, "Token ID should match JUP"); + assert!(!token.id.is_empty()); + assert!(!token.name.is_empty()); + assert!(!token.symbol.is_empty()); + assert!(token.decimals == 6); + assert!(token.circ_supply > 0.0); + assert!(token.total_supply > 0.0); + assert!(token.holder_count > 0); + assert!(token.fdv > 0.0); + assert!(token.mcap > 0.0); + assert!(token.usd_price > 0.0); + assert!(token.liquidity > 0.0); + } + #[tokio::test] async fn test_routers() { let client = create_test_client();