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
12pub type Result<T> = std::result::Result<T, Error>;
14
15#[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#[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#[derive(Clone, Debug)]
97pub struct Swap {
98 pub setup: Option<Transaction>,
99 pub swap: Transaction,
100 pub cleanup: Option<Transaction>,
101}
102
103pub 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
121pub 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
134pub 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
167pub 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
226pub async fn swap(route: Quote, user_public_key: Pubkey) -> Result<Swap> {
228 swap_with_config(route, user_public_key, SwapConfig::default()).await
229}
230
231pub 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}