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
15pub type Result<T> = std::result::Result<T, Error>;
17
18fn quote_api_url() -> String {
20 env::var("QUOTE_API_URL").unwrap_or_else(|_| "https://api.jup.ag/swap/v1".to_string())
21}
22
23#[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#[derive(Clone, Debug)]
114pub struct Swap {
115 pub swap_transaction: VersionedTransaction,
116 pub last_valid_block_height: u64,
117}
118
119#[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
138pub 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
196pub 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 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
303pub 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
331pub 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
354pub 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}