jup_ag_nova/
lib.rs

1use {
2    serde::{Deserialize, Serialize},
3    solana_sdk::{
4        pubkey::{ParsePubkeyError, Pubkey},
5        transaction::Transaction,
6    },
7    std::collections::HashMap,
8};
9
10mod field_as_string;
11
12/// A `Result` alias where the `Err` case is `jup_ag::Error`.
13pub type Result<T> = std::result::Result<T, Error>;
14
15/// The Errors that may occur while using this crate
16#[derive(thiserror::Error, Debug)]
17pub enum Error {
18    #[error("reqwest: {0}")]
19    Reqwest(#[from] reqwest::Error),
20
21    #[error("invalid pubkey in response data: {0}")]
22    ParsePubkey(#[from] ParsePubkeyError),
23
24    #[error("base64: {0}")]
25    Base64Decode(#[from] base64::DecodeError),
26
27    #[error("bincode: {0}")]
28    Bincode(#[from] bincode::Error),
29
30    #[error("Jupiter API: {0}")]
31    JupiterApi(String),
32
33    #[error("serde_json: {0}")]
34    SerdeJson(#[from] serde_json::Error),
35}
36
37/// Generic response with timing information
38#[derive(Debug, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct Response<T> {
41    pub data: T,
42    pub time_taken: f64,
43}
44
45#[derive(Clone, Debug, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct Price {
48    #[serde(with = "field_as_string", rename = "id")]
49    pub input_mint: Pubkey,
50    #[serde(rename = "mintSymbol")]
51    pub input_symbol: String,
52    #[serde(with = "field_as_string", rename = "vsToken")]
53    pub output_mint: Pubkey,
54    #[serde(rename = "vsTokenSymbol")]
55    pub output_symbol: String,
56    pub price: f64,
57}
58
59#[derive(Clone, Debug, Deserialize, Serialize)]
60#[serde(rename_all = "camelCase")]
61pub struct Quote {
62    pub in_amount: u64,
63    pub out_amount: u64,
64    pub out_amount_with_slippage: u64,
65    pub price_impact_pct: f64,
66    pub market_infos: Vec<MarketInfo>,
67}
68
69#[derive(Clone, Debug, Deserialize, Serialize)]
70#[serde(rename_all = "camelCase")]
71pub struct MarketInfo {
72    pub id: String,
73    pub label: String,
74    #[serde(with = "field_as_string")]
75    pub input_mint: Pubkey,
76    #[serde(with = "field_as_string")]
77    pub output_mint: Pubkey,
78    pub not_enough_liquidity: bool,
79    pub in_amount: u64,
80    pub out_amount: u64,
81    pub price_impact_pct: f64,
82    pub lp_fee: FeeInfo,
83    pub platform_fee: FeeInfo,
84}
85
86#[derive(Clone, Debug, Deserialize, Serialize)]
87#[serde(rename_all = "camelCase")]
88pub struct FeeInfo {
89    pub amount: f64,
90    #[serde(with = "field_as_string")]
91    pub mint: Pubkey,
92    pub pct: f64,
93}
94
95/// Partially signed transactions required to execute a swap
96#[derive(Clone, Debug)]
97pub struct Swap {
98    pub setup: Option<Transaction>,
99    pub swap: Transaction,
100    pub cleanup: Option<Transaction>,
101}
102
103/// Hashmap of possible swap routes from input mint to an array of output mints
104pub type RouteMap = HashMap<Pubkey, Vec<Pubkey>>;
105
106fn maybe_jupiter_api_error<T>(value: serde_json::Value) -> Result<T>
107where
108    T: serde::de::DeserializeOwned,
109{
110    #[derive(Deserialize)]
111    struct ErrorResponse {
112        error: String,
113    }
114    if let Ok(ErrorResponse { error }) = serde_json::from_value::<ErrorResponse>(value.clone()) {
115        Err(Error::JupiterApi(error))
116    } else {
117        serde_json::from_value(value).map_err(|err| err.into())
118    }
119}
120
121/// Get simple price for a given input mint, output mint and amount
122pub async fn price(
123    input_mint: Pubkey,
124    output_mint: Pubkey,
125    ui_amount: f64,
126) -> Result<Response<Price>> {
127    let url = format!(
128        "https://quote-api.jup.ag/v1/price?id={}&vsToken={}&amount={}",
129        input_mint, output_mint, ui_amount,
130    );
131    maybe_jupiter_api_error(reqwest::get(url).await?.json().await?)
132}
133
134/// Get quote for a given input mint, output mint and amount
135pub async fn quote(
136    input_mint: Pubkey,
137    output_mint: Pubkey,
138    amount: u64,
139    only_direct_routes: bool,
140    slippage: Option<f64>,
141    fees_bps: Option<f64>,
142) -> Result<Response<Vec<Quote>>> {
143    let url = format!(
144        "https://quote-api.jup.ag/v1/quote?inputMint={}&outputMint={}&amount={}&onlyDirectRoutes={}&{}{}",
145        input_mint,
146        output_mint,
147        amount,
148        only_direct_routes,
149        slippage
150            .map(|slippage| format!("&slippage={}", slippage))
151            .unwrap_or_default(),
152        fees_bps
153            .map(|fees_bps| format!("&feesBps={}", fees_bps))
154            .unwrap_or_default(),
155    );
156
157    maybe_jupiter_api_error(reqwest::get(url).await?.json().await?)
158}
159
160#[derive(Default)]
161pub struct SwapConfig {
162    pub wrap_unwrap_sol: Option<bool>,
163    pub fee_account: Option<Pubkey>,
164    pub token_ledger: Option<Pubkey>,
165}
166
167/// Get swap serialized transactions for a quote
168pub async fn swap_with_config(
169    route: Quote,
170    user_public_key: Pubkey,
171    swap_config: SwapConfig,
172) -> Result<Swap> {
173    let url = "https://quote-api.jup.ag/v1/swap";
174
175    #[derive(Debug, Serialize)]
176    #[serde(rename_all = "camelCase")]
177    #[allow(non_snake_case)]
178    struct SwapRequest {
179        route: Quote,
180        wrap_unwrap_SOL: Option<bool>,
181        fee_account: Option<String>,
182        token_ledger: Option<String>,
183        #[serde(with = "field_as_string")]
184        user_public_key: Pubkey,
185    }
186
187    #[derive(Debug, Deserialize)]
188    #[serde(rename_all = "camelCase")]
189    struct SwapResponse {
190        setup_transaction: Option<String>,
191        swap_transaction: String,
192        cleanup_transaction: Option<String>,
193    }
194
195    let request = SwapRequest {
196        route,
197        wrap_unwrap_SOL: swap_config.wrap_unwrap_sol,
198        fee_account: swap_config.fee_account.map(|x| x.to_string()),
199        token_ledger: swap_config.token_ledger.map(|x| x.to_string()),
200        user_public_key,
201    };
202
203    let response = maybe_jupiter_api_error::<SwapResponse>(
204        reqwest::Client::builder()
205            .build()?
206            .post(url)
207            .json(&request)
208            .send()
209            .await?
210            .error_for_status()?
211            .json()
212            .await?,
213    )?;
214
215    fn decode(base64_transaction: String) -> Result<Transaction> {
216        bincode::deserialize(&base64::decode(base64_transaction)?).map_err(|err| err.into())
217    }
218
219    Ok(Swap {
220        setup: response.setup_transaction.map(decode).transpose()?,
221        swap: decode(response.swap_transaction)?,
222        cleanup: response.cleanup_transaction.map(decode).transpose()?,
223    })
224}
225
226/// Get swap serialized transactions for a quote using `SwapConfig` defaults
227pub async fn swap(route: Quote, user_public_key: Pubkey) -> Result<Swap> {
228    swap_with_config(route, user_public_key, SwapConfig::default()).await
229}
230
231/// Returns a hash map, input mint as key and an array of valid output mint as values
232pub async fn route_map(only_direct_routes: bool) -> Result<RouteMap> {
233    let url = format!(
234        "https://quote-api.jup.ag/v1/indexed-route-map?onlyDirectRoutes={}",
235        only_direct_routes
236    );
237
238    #[derive(Debug, Deserialize)]
239    #[serde(rename_all = "camelCase")]
240    struct IndexedRouteMap {
241        mint_keys: Vec<String>,
242        indexed_route_map: HashMap<usize, Vec<usize>>,
243    }
244
245    let response = reqwest::get(url).await?.json::<IndexedRouteMap>().await?;
246
247    let mint_keys = response
248        .mint_keys
249        .into_iter()
250        .map(|x| x.parse::<Pubkey>().map_err(|err| err.into()))
251        .collect::<Result<Vec<Pubkey>>>()?;
252
253    let mut route_map = HashMap::new();
254    for (from_index, to_indices) in response.indexed_route_map {
255        route_map.insert(
256            mint_keys[from_index],
257            to_indices.into_iter().map(|i| mint_keys[i]).collect(),
258        );
259    }
260
261    Ok(route_map)
262}