diff --git a/examples/src/token.rs b/examples/src/token.rs index b236653..9e33f3c 100644 --- a/examples/src/token.rs +++ b/examples/src/token.rs @@ -1,4 +1,4 @@ -use jup_ag_sdk::{JupiterClient, types::TokenPriceRequest}; +use jup_ag_sdk::{JupiterClient, client}; pub async fn token_balances() { let client = JupiterClient::new("https://lite-api.jup.ag"); @@ -31,48 +31,22 @@ pub async fn token_price() { "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN".to_string(), ]; - let params = TokenPriceRequest::new(&token_mints); - - let price = client - .get_token_price(¶ms) - .await - .expect("Failed to get token price"); - - let sol_price = price - .data - .get(token_mints[0].as_str()) - .expect("SOL price not found"); - - println!("1 SOL price in USDC: {}", sol_price.price); - - let jup_price = price - .data - .get(token_mints[1].as_str()) - .expect("Jup Token price not found"); - - println!("1 JUP price USDC: {}", jup_price.price); - - let params = TokenPriceRequest::new(&token_mints) - .with_vs_token("So11111111111111111111111111111111111111112"); - let price = client - .get_token_price(¶ms) + .get_tokens_price(&token_mints) .await .expect("Failed to get token price"); let sol_price = price - .data .get(token_mints[0].as_str()) .expect("SOL price not found"); - println!("1 SOL price in SOL: {}", sol_price.price); + println!("1 SOL price in USDC: {}", sol_price.usd_price); let jup_price = price - .data .get(token_mints[1].as_str()) .expect("Jup Token price not found"); - println!("1 JUP price in SOL: {}", jup_price.price); + println!("1 JUP price USDC: {}", jup_price.usd_price); } pub async fn token_info() { @@ -115,3 +89,18 @@ pub async fn get_tokens_from_tags() { println!("mints: {}", mints.len()) } + +pub async fn get_trending_tokens() { + let client = JupiterClient::new("https:://lite-api.jup.ag"); + + let tokens = client + .get_tokens_by_category( + jup_ag_sdk::types::Category::TopTrending, + jup_ag_sdk::types::Interval::TwentyFourHours, + Some(10), + ) + .await + .expect("failed to get trending tokens"); + + println!("trending tokens: {:?}", tokens.len()); +} diff --git a/jup-ag-sdk/src/client/token_api.rs b/jup-ag-sdk/src/client/token_api.rs index db9abc9..34c899a 100644 --- a/jup-ag-sdk/src/client/token_api.rs +++ b/jup-ag-sdk/src/client/token_api.rs @@ -1,10 +1,210 @@ +use std::collections::HashMap; + use super::JupiterClient; use crate::{ error::{JupiterClientError, handle_response}, - types::{NewTokens, TokenInfoResponse, TokenPriceRequest, TokenPriceResponse}, + types::{ + Category, Interval, NewTokens, Price, TokenInfo, TokenInfoResponse, TokenPriceRequest, + TokenPriceResponse, + }, }; impl JupiterClient { + /// search for a token and its information by its symbol, name or mint address + /// + /// Limit to 100 mint addresses in query + /// Default to 20 mints in response when searching via symbol or name + /// + /// # 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.token_search(&mints).await?; + /// ``` + pub async fn token_search( + &self, + mints: &[String], + ) -> Result, JupiterClientError> { + let query_params = vec![("query", mints.join(","))]; + + let response = match self + .client + .get(format!("{}/tokens/v2/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())), + } + } + + /// Returns a list of mints with specified tag(s) along with their metadata. + /// tags: verified, lst, token-2022, etc + /// ``` + /// + /// let tags = vec![String::from("verified")]; + /// let tagged = client + /// .get_mints_by_tags(&tags) + /// .await + /// .expect("failed to get mints by tags"); + /// ``` + pub async fn get_mints_by_tags( + &self, + tags: &[String], + ) -> Result, JupiterClientError> { + let query_params = vec![("query", tags.join(","))]; + + let response = match self + .client + .get(format!("{}/tokens/v2/tag", 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(mints) => Ok(mints), + Err(e) => Err(JupiterClientError::DeserializationError(e.to_string())), + } + } + + /// Returns a list of mints and their information for the given category and time interval. + /// + /// # Parameters + /// - `category` (`Category`) — Required + /// The token ranking category. Possible values: + /// - `toporganicscore` — Top tokens by organic score + /// - `toptraded` — Top traded tokens + /// - `toptrending` — Top trending tokens + /// + /// - `interval` (`Interval`) — Required + /// Time interval for the ranking query. Possible values: + /// - `5m` — Last 5 minutes + /// - `1h` — Last 1 hour + /// - `6h` — Last 6 hours + /// - `24h` — Last 24 hours + /// + /// - `limit` (`Option`) — Optional + /// Maximum number of results to return (default is 50, maximum is 100). + /// Must be between 1 and 100 inclusive if provided. + /// ``` + /// let tokens = client + /// .get_mints_by_category(Category::TopTrending, Interval::OneHour, None) + /// .await.expect("failed to get tokens"); + /// ``` + pub async fn get_tokens_by_category( + &self, + category: Category, + interval: Interval, + limit: Option, + ) -> Result, JupiterClientError> { + let url = format!("{}/tokens/v2/{}/{}", self.base_url, category, interval); + + let mut request = self.client.get(url); + + if let Some(limit) = limit { + request = request.query(&[("limit", limit)]); + } + + let response = match request.send().await { + Ok(resp) => resp, + Err(e) => return Err(JupiterClientError::RequestError(e)), + }; + + let response = handle_response(response).await?; + + match response.json::>().await { + Ok(mints) => Ok(mints), + Err(e) => Err(JupiterClientError::DeserializationError(e.to_string())), + } + } + + /// Returns an vec of mints that recently had their first created pool + /// Default to 30 mints in response + pub async fn get_recent_tokens(&self) -> Result, JupiterClientError> { + let url = format!("{}/tokens/v2/recent", self.base_url); + + let response = match self.client.get(&url).send().await { + Ok(resp) => resp, + Err(e) => return Err(JupiterClientError::RequestError(e)), + }; + + let response = handle_response(response).await?; + + match response.json::>().await { + Ok(mints) => Ok(mints), + Err(e) => Err(JupiterClientError::DeserializationError(e.to_string())), + } + } + + /// Returns prices of specified tokens. + /// + /// ``` + /// let client = JupiterClient::new("https://lite-api.jup.ag"); + /// + /// let mints = vec![ + /// String::from("So11111111111111111111111111111111111111112"), + /// String::from("JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"), + /// ]; + /// + /// let price = client.get_tokens_price(&mints).await.expect("failed to get token price"); + /// let jup_price = price.get(&mints[1]).expect("jup not found").usd_price; + /// ``` + pub async fn get_tokens_price( + &self, + mints: &[String], + ) -> Result, JupiterClientError> { + let query_params = vec![("ids", mints.join(","))]; + + let response = match self + .client + .get(format!("{}/price/v3", 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(token_price) => Ok(token_price), + Err(e) => Err(JupiterClientError::DeserializationError(e.to_string())), + } + } + + #[deprecated(note = "This endpoint is deprecated. use `get_tokens_price` instead")] /// Returns prices of specified tokens. /// ``` /// let client = JupiterClient::new("https://lite-api.jup.ag") @@ -52,6 +252,7 @@ impl JupiterClient { } } + #[deprecated] /// Returns the specified mint address's token information and metadata. /// /// ``` @@ -78,6 +279,7 @@ impl JupiterClient { } } + #[deprecated] /// Returns the mints involved in a market. pub async fn get_market_mints( &self, @@ -100,6 +302,7 @@ impl JupiterClient { } } + #[deprecated] /// Returns a list of all mints tradable via Jupiter routing. /// This endpoint returns greater than 32MB amount of data. May take a while to complete. pub async fn get_tradable_mints(&self) -> Result, JupiterClientError> { @@ -117,34 +320,7 @@ impl JupiterClient { } } - /// Returns a list of mints with specified tag(s) along with their metadata. - /// tags: verified, lst, token-2022, etc - /// ``` - /// - /// let tags = vec![String::from("verified")]; - /// let tagged = client - /// .get_mints_by_tags(&tags) - /// .await - /// .expect("failed to get mints by tags"); - /// ``` - pub async fn get_mints_by_tags( - &self, - tags: &[String], - ) -> Result, JupiterClientError> { - let url = format!("{}/tokens/v1/tagged/{}", self.base_url, tags.join(",")); - let response = match self.client.get(&url).send().await { - Ok(resp) => resp, - Err(e) => return Err(JupiterClientError::RequestError(e)), - }; - - let response = handle_response(response).await?; - - match response.json::>().await { - Ok(mints) => Ok(mints), - Err(e) => Err(JupiterClientError::DeserializationError(e.to_string())), - } - } - + #[deprecated(note = "This fn is deprecated. Use `get_recent_tokens` instead.")] /// get new tokens with metadata, created at timestamp and markets. pub async fn get_new_tokens( &self, @@ -175,6 +351,7 @@ impl JupiterClient { } } + #[deprecated] /// Returns all tokens with all metadata. /// Do note that calling this endpoint's resource will return a large payload of 300+MB, which would introduce some latency in the call. /// Please use carefully and intentionally, else utilize the other endpoints. diff --git a/jup-ag-sdk/src/types/token.rs b/jup-ag-sdk/src/types/token.rs index 1e79de4..ed8eacf 100644 --- a/jup-ag-sdk/src/types/token.rs +++ b/jup-ag-sdk/src/types/token.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize, Serializer}; use std::collections::HashMap; +use std::fmt; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -99,3 +100,53 @@ pub struct NewTokens { pub mint_authority: Option, pub freeze_authority: Option, } + +#[derive(Debug, Serialize, Deserialize)] +pub enum Category { + TopOrganicScore, + TopTraded, + TopTrending, +} + +impl fmt::Display for Category { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Self::TopOrganicScore => "toporganicscore", + Self::TopTraded => "toptraded", + Self::TopTrending => "toptrending", + }; + write!(f, "{}", s) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum Interval { + FiveMinutes, + OneHour, + SixHours, + TwentyFourHours, +} + +impl fmt::Display for Interval { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Self::FiveMinutes => "5m", + Self::OneHour => "1h", + Self::SixHours => "6h", + Self::TwentyFourHours => "24h", + }; + write!(f, "{}", s) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Price { + pub usd_price: f64, + + pub block_id: u64, + + pub decimals: u8, + + pub price_change_24h: f64, +} diff --git a/jup-ag-sdk/src/types/ultra.rs b/jup-ag-sdk/src/types/ultra.rs index 3e5d652..a83bef2 100644 --- a/jup-ag-sdk/src/types/ultra.rs +++ b/jup-ag-sdk/src/types/ultra.rs @@ -353,17 +353,11 @@ pub struct FirstPool { #[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, } @@ -373,51 +367,43 @@ 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, + pub holder_count: Option, + #[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, + + pub fdv: Option, + pub mcap: Option, + pub usd_price: Option, + pub price_block_id: Option, + pub liquidity: Option, #[serde(default)] pub stats5m: Option, @@ -427,9 +413,8 @@ pub struct TokenInfo { 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, + pub updated_at: Option, } diff --git a/tests/src/token.rs b/tests/src/token.rs index 8e02ed9..3d6a62b 100644 --- a/tests/src/token.rs +++ b/tests/src/token.rs @@ -1,7 +1,5 @@ #[cfg(test)] mod token_tests { - use jup_ag_sdk::types::TokenPriceRequest; - use crate::common::{JUP_MINT, SOL_MINT, USDC_MINT, create_test_client}; #[tokio::test] @@ -25,40 +23,19 @@ mod token_tests { async fn test_get_token_prices() { let client = create_test_client(); let token_mints = vec![SOL_MINT.to_string(), USDC_MINT.to_string()]; - let req = TokenPriceRequest::new(&token_mints); - assert_eq!(req.token_mints.len(), 2, "mints should be 2"); - assert_eq!(req.token_mints[0], SOL_MINT); let res = client - .get_token_price(&req) + .get_tokens_price(&token_mints) .await .expect("failed to get token prices"); - let usdc_price: f64 = res - .data - .get(USDC_MINT) - .expect("usdc price not found") - .price - .parse() - .expect("failed to parse usdc price"); + let usdc_price: f64 = res.get(USDC_MINT).expect("usdc price not found").usd_price; assert!( (0.9..=1.1).contains(&usdc_price), "USDC price {} is out of range (0.9 to 1.1)", usdc_price ); - - let req = TokenPriceRequest::new(&token_mints).with_vs_token(SOL_MINT); - - let res = client - .get_token_price(&req) - .await - .expect("failed to get token prices"); - - assert_eq!( - res.data.get(SOL_MINT).expect("sol price not found").price, - "1" - ); } #[tokio::test] @@ -66,26 +43,13 @@ mod token_tests { let client = create_test_client(); let info = client - .get_token_info(JUP_MINT) + .token_search(&[JUP_MINT.to_string()]) .await .expect("failed to get token info"); - assert_eq!(info.decimals, 6, "JUP decimals should be 6"); - - assert_eq!(info.symbol, "JUP") - } - - #[tokio::test] - pub async fn test_get_market_mints() { - let client = create_test_client(); - - let mints = client - .get_market_mints("5rCf1DM8LjKTw4YqhnoLcngyZYeNnQqztScTogYHAS6") - .await - .expect("failed to get market mints"); + assert_eq!(info[0].decimals, 6, "JUP decimals should be 6"); - assert_eq!(mints[0], SOL_MINT, "First mint should be SOL"); - assert_eq!(mints[1], USDC_MINT, "Second mint should be USDC"); + assert_eq!(info[0].symbol, "JUP") } #[tokio::test] diff --git a/tests/src/ultra.rs b/tests/src/ultra.rs index 5ae5f41..ce42387 100644 --- a/tests/src/ultra.rs +++ b/tests/src/ultra.rs @@ -150,11 +150,11 @@ mod ultra_tests { 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); + assert!(token.holder_count.expect("holder_count") > 0); + assert!(token.fdv.expect("fdv") > 0.0); + assert!(token.mcap.expect("mcap") > 0.0); + assert!(token.usd_price.expect("usd price") > 0.0); + assert!(token.liquidity.expect("liquidity") > 0.0); } #[tokio::test]