jup_ag/
lib.rs

1use {
2    base64::prelude::{Engine as _, BASE64_STANDARD},
3    itertools::Itertools,
4    serde::{Deserialize, Serialize},
5    solana_pubkey::{ParsePubkeyError, Pubkey},
6    solana_sdk::{instruction::Instruction, transaction::VersionedTransaction},
7    std::{collections::HashMap, env, fmt, str::FromStr},
8};
9
10mod field_as_string;
11mod field_instruction;
12mod field_prioritization_fee;
13mod field_pubkey;
14
15/// A `Result` alias where the `Err` case is `jup_ag::Error`.
16pub type Result<T> = std::result::Result<T, Error>;
17
18// Reference: https://dev.jup.ag/docs/api/swap-api/quote
19fn quote_api_url() -> String {
20    env::var("QUOTE_API_URL").unwrap_or_else(|_| "https://api.jup.ag/swap/v1".to_string())
21}
22
23/// The Errors that may occur while using this crate
24#[derive(thiserror::Error, Debug)]
25pub enum Error {
26    #[error("reqwest: {0}")]
27    Reqwest(#[from] reqwest::Error),
28
29    #[error("invalid pubkey in response data: {0}")]
30    ParsePubkey(#[from] ParsePubkeyError),
31
32    #[error("base64: {0}")]
33    Base64Decode(#[from] base64::DecodeError),
34
35    #[error("bincode: {0}")]
36    Bincode(#[from] bincode::Error),
37
38    #[error("Jupiter API: {0}")]
39    JupiterApi(String),
40
41    #[error("serde_json: {0}")]
42    SerdeJson(#[from] serde_json::Error),
43
44    #[error("parse SwapMode: Invalid value `{value}`")]
45    ParseSwapMode { value: String },
46}
47
48#[derive(Clone, Debug, Deserialize, Serialize)]
49#[serde(rename_all = "camelCase")]
50pub struct Quote {
51    #[serde(with = "field_as_string")]
52    pub input_mint: Pubkey,
53    #[serde(with = "field_as_string")]
54    pub in_amount: u64,
55    #[serde(with = "field_as_string")]
56    pub output_mint: Pubkey,
57    #[serde(with = "field_as_string")]
58    pub out_amount: u64,
59    #[serde(with = "field_as_string")]
60    pub other_amount_threshold: u64,
61    pub swap_mode: String,
62    pub slippage_bps: u64,
63    #[serde(with = "field_as_string")]
64    pub price_impact_pct: f64,
65    pub route_plan: Vec<RoutePlan>,
66    pub platform_fee: Option<PlatformFee>,
67    pub context_slot: Option<u64>,
68    pub time_taken: Option<f64>,
69}
70
71#[derive(Clone, Debug, Deserialize, Serialize)]
72#[serde(rename_all = "camelCase")]
73pub struct PlatformFee {
74    #[serde(with = "field_as_string")]
75    pub amount: u64,
76    pub fee_bps: u64,
77}
78
79#[derive(Clone, Debug, Deserialize, Serialize)]
80#[serde(rename_all = "camelCase")]
81pub struct RoutePlan {
82    pub swap_info: SwapInfo,
83    pub percent: u8,
84}
85
86#[derive(Clone, Debug, Deserialize, Serialize)]
87#[serde(rename_all = "camelCase")]
88pub struct SwapInfo {
89    #[serde(with = "field_as_string")]
90    pub amm_key: Pubkey,
91    #[serde(with = "field_as_string")]
92    pub in_amount: u64,
93    #[serde(with = "field_as_string")]
94    pub input_mint: Pubkey,
95    pub label: Option<String>,
96    #[serde(with = "field_as_string")]
97    pub out_amount: u64,
98    #[serde(with = "field_as_string")]
99    pub output_mint: Pubkey,
100}
101
102#[derive(Clone, Debug, Deserialize, Serialize)]
103#[serde(rename_all = "camelCase")]
104pub struct FeeInfo {
105    #[serde(with = "field_as_string")]
106    pub amount: u64,
107    #[serde(with = "field_as_string")]
108    pub mint: Pubkey,
109    pub pct: f64,
110}
111
112/// Partially signed transactions required to execute a swap
113#[derive(Clone, Debug)]
114pub struct Swap {
115    pub swap_transaction: VersionedTransaction,
116    pub last_valid_block_height: u64,
117}
118
119/// Swap instructions
120#[derive(Clone, Debug, Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct SwapInstructions {
123    #[serde(with = "field_instruction::option_instruction")]
124    pub token_ledger_instruction: Option<Instruction>,
125    #[serde(with = "field_instruction::vec_instruction")]
126    pub compute_budget_instructions: Vec<Instruction>,
127    #[serde(with = "field_instruction::vec_instruction")]
128    pub setup_instructions: Vec<Instruction>,
129    #[serde(with = "field_instruction::instruction")]
130    pub swap_instruction: Instruction,
131    #[serde(with = "field_instruction::option_instruction")]
132    pub cleanup_instruction: Option<Instruction>,
133    #[serde(with = "field_pubkey::vec")]
134    pub address_lookup_table_addresses: Vec<Pubkey>,
135    pub prioritization_fee_lamports: u64,
136}
137
138/// Hashmap of possible swap routes from input mint to an array of output mints
139pub type RouteMap = HashMap<Pubkey, Vec<Pubkey>>;
140
141fn maybe_jupiter_api_error<T>(value: serde_json::Value) -> Result<T>
142where
143    T: serde::de::DeserializeOwned,
144{
145    #[derive(Deserialize)]
146    struct ErrorResponse {
147        error: String,
148    }
149    if let Ok(ErrorResponse { error }) = serde_json::from_value::<ErrorResponse>(value.clone()) {
150        Err(Error::JupiterApi(error))
151    } else {
152        serde_json::from_value(value).map_err(|err| err.into())
153    }
154}
155
156#[derive(Serialize, Deserialize, Default, PartialEq, Clone, Debug)]
157pub enum SwapMode {
158    #[default]
159    ExactIn,
160    ExactOut,
161}
162
163impl FromStr for SwapMode {
164    type Err = Error;
165
166    fn from_str(s: &str) -> Result<Self> {
167        match s {
168            "ExactIn" => Ok(Self::ExactIn),
169            "ExactOut" => Ok(Self::ExactOut),
170            _ => Err(Error::ParseSwapMode { value: s.into() }),
171        }
172    }
173}
174
175impl fmt::Display for SwapMode {
176    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
177        match *self {
178            Self::ExactIn => write!(f, "ExactIn"),
179            Self::ExactOut => write!(f, "ExactOut"),
180        }
181    }
182}
183
184#[derive(Default)]
185pub struct QuoteConfig {
186    pub slippage_bps: Option<u64>,
187    pub swap_mode: Option<SwapMode>,
188    pub dexes: Option<Vec<String>>,
189    pub exclude_dexes: Option<Vec<String>>,
190    pub only_direct_routes: bool,
191    pub as_legacy_transaction: Option<bool>,
192    pub platform_fee_bps: Option<u64>,
193    pub max_accounts: Option<u64>,
194}
195
196/// Get quote for a given input mint, output mint, and amount
197pub async fn quote(
198    input_mint: Pubkey,
199    output_mint: Pubkey,
200    amount: u64,
201    quote_config: QuoteConfig,
202    api_key: String,
203) -> Result<Quote> {
204    let url = format!(
205        "{base_url}/quote?inputMint={input_mint}&outputMint={output_mint}&amount={amount}&onlyDirectRoutes={}&{}{}{}{}{}{}{}",
206        quote_config.only_direct_routes,
207        quote_config
208            .as_legacy_transaction
209            .map(|as_legacy_transaction| format!("&asLegacyTransaction={as_legacy_transaction}"))
210            .unwrap_or_default(),
211        quote_config
212            .swap_mode
213            .map(|swap_mode| format!("&swapMode={swap_mode}"))
214            .unwrap_or_default(),
215        quote_config
216            .slippage_bps
217            .map(|slippage_bps| format!("&slippageBps={slippage_bps}"))
218            .unwrap_or_default(),
219        quote_config
220            .platform_fee_bps
221            .map(|platform_fee_bps| format!("&feeBps={platform_fee_bps}"))
222            .unwrap_or_default(),
223        quote_config
224            .dexes
225            .map(|dexes| format!("&dexes={}", dexes.into_iter().join(",")))
226            .unwrap_or_default(),
227        quote_config
228            .exclude_dexes
229            .map(|exclude_dexes| format!("&excludeDexes={}", exclude_dexes.into_iter().join(",")))
230            .unwrap_or_default(),
231        quote_config
232            .max_accounts
233            .map(|max_accounts| format!("&maxAccounts={max_accounts}"))
234            .unwrap_or_default(),
235        base_url=quote_api_url(),
236    );
237
238    maybe_jupiter_api_error(
239        reqwest::Client::builder()
240            .build()?
241            .get(url)
242            .header("x-api-key", api_key)
243            .send()
244            .await?
245            .json()
246            .await?,
247    )
248}
249
250#[derive(Debug)]
251pub enum PrioritizationFeeLamports {
252    Auto,
253    Exact { lamports: u64 },
254}
255
256#[derive(Debug, Serialize)]
257#[serde(rename_all = "camelCase")]
258#[allow(non_snake_case)]
259pub struct SwapRequest {
260    #[serde(with = "field_as_string")]
261    pub user_public_key: Pubkey,
262    pub wrap_and_unwrap_sol: Option<bool>,
263    pub use_shared_accounts: Option<bool>,
264    #[serde(with = "field_pubkey::option")]
265    pub fee_account: Option<Pubkey>,
266    #[deprecated = "please use SwapRequest::prioritization_fee_lamports instead"]
267    pub compute_unit_price_micro_lamports: Option<u64>,
268    #[serde(with = "field_prioritization_fee")]
269    pub prioritization_fee_lamports: PrioritizationFeeLamports,
270    pub as_legacy_transaction: Option<bool>,
271    pub use_token_ledger: Option<bool>,
272    #[serde(with = "field_pubkey::option")]
273    pub destination_token_account: Option<Pubkey>,
274    pub quote_response: Quote,
275}
276
277impl SwapRequest {
278    /// Creates new SwapRequest with the given and default values
279    pub fn new(user_public_key: Pubkey, quote_response: Quote) -> Self {
280        #[allow(deprecated)]
281        SwapRequest {
282            user_public_key,
283            wrap_and_unwrap_sol: Some(true),
284            use_shared_accounts: Some(true),
285            fee_account: None,
286            compute_unit_price_micro_lamports: None,
287            prioritization_fee_lamports: PrioritizationFeeLamports::Auto,
288            as_legacy_transaction: Some(false),
289            use_token_ledger: Some(false),
290            destination_token_account: None,
291            quote_response,
292        }
293    }
294}
295
296#[derive(Debug, Deserialize)]
297#[serde(rename_all = "camelCase")]
298struct SwapResponse {
299    pub swap_transaction: String,
300    pub last_valid_block_height: u64,
301}
302
303/// Get swap serialized transactions for a quote
304pub async fn swap(swap_request: SwapRequest, api_key: String) -> Result<Swap> {
305    let url = format!("{}/swap", quote_api_url());
306
307    let response = maybe_jupiter_api_error::<SwapResponse>(
308        reqwest::Client::builder()
309            .build()?
310            .post(url)
311            .header("Accept", "application/json")
312            .header("x-api-key", api_key)
313            .json(&swap_request)
314            .send()
315            .await?
316            .error_for_status()?
317            .json()
318            .await?,
319    )?;
320
321    fn decode(base64_transaction: String) -> Result<VersionedTransaction> {
322        bincode::deserialize(&BASE64_STANDARD.decode(base64_transaction)?).map_err(|err| err.into())
323    }
324
325    Ok(Swap {
326        swap_transaction: decode(response.swap_transaction)?,
327        last_valid_block_height: response.last_valid_block_height,
328    })
329}
330
331/// Get swap serialized transaction instructions for a quote
332pub async fn swap_instructions(
333    swap_request: SwapRequest,
334    api_key: String,
335) -> Result<SwapInstructions> {
336    let url = format!("{}/swap-instructions", quote_api_url());
337
338    let response = reqwest::Client::builder()
339        .build()?
340        .post(url)
341        .header("Accept", "application/json")
342        .header("x-api-key", api_key)
343        .json(&swap_request)
344        .send()
345        .await?;
346
347    if !response.status().is_success() {
348        return Err(Error::JupiterApi(response.text().await?));
349    }
350
351    Ok(response.json::<SwapInstructions>().await?)
352}
353
354/// Returns a hash map, input mint as key and an array of valid output mint as values
355pub async fn route_map() -> Result<RouteMap> {
356    let url = format!(
357        "{}/indexed-route-map?onlyDirectRoutes=false",
358        quote_api_url()
359    );
360
361    #[derive(Debug, Deserialize)]
362    #[serde(rename_all = "camelCase")]
363    struct IndexedRouteMap {
364        mint_keys: Vec<String>,
365        indexed_route_map: HashMap<usize, Vec<usize>>,
366    }
367
368    let response = reqwest::get(url).await?.json::<IndexedRouteMap>().await?;
369
370    let mint_keys = response
371        .mint_keys
372        .into_iter()
373        .map(|x| x.parse::<Pubkey>().map_err(|err| err.into()))
374        .collect::<Result<Vec<Pubkey>>>()?;
375
376    let mut route_map = HashMap::new();
377    for (from_index, to_indices) in response.indexed_route_map {
378        route_map.insert(
379            mint_keys[from_index],
380            to_indices.into_iter().map(|i| mint_keys[i]).collect(),
381        );
382    }
383
384    Ok(route_map)
385}