1pub use reqwest;
2pub use rust_decimal;
3pub use solana_account_decoder;
4pub use solana_sdk;
5
6use anyhow::{Result, anyhow};
7use reqwest::{Client, ClientBuilder};
8use rust_decimal::Decimal;
9use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
10use serde_json::Value;
11use solana_account_decoder::UiAccount;
12use solana_sdk::{
13 instruction::{AccountMeta, Instruction},
14 pubkey::Pubkey,
15};
16use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};
17
18pub mod field_as_string {
19 use {
20 serde::{Deserialize, Serialize},
21 serde::{Deserializer, Serializer, de},
22 std::str::FromStr,
23 };
24
25 pub fn serialize<T, S>(t: &T, serializer: S) -> Result<S::Ok, S::Error>
26 where
27 T: ToString,
28 S: Serializer,
29 {
30 t.to_string().serialize(serializer)
31 }
32
33 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
34 where
35 T: FromStr,
36 D: Deserializer<'de>,
37 <T as FromStr>::Err: std::fmt::Debug,
38 {
39 let s: String = String::deserialize(deserializer)?;
40 s.parse()
41 .map_err(|e| de::Error::custom(format!("Parse error: {:?}", e)))
42 }
43}
44
45pub mod option_field_as_string {
46 use {
47 serde::{Deserialize, Deserializer, Serialize, Serializer, de},
48 std::str::FromStr,
49 };
50
51 pub fn serialize<T, S>(t: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
52 where
53 T: ToString,
54 S: Serializer,
55 {
56 if let Some(t) = t {
57 t.to_string().serialize(serializer)
58 } else {
59 serializer.serialize_none()
60 }
61 }
62
63 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
64 where
65 T: FromStr,
66 D: Deserializer<'de>,
67 <T as FromStr>::Err: std::fmt::Debug,
68 {
69 let opt: Option<String> = Option::deserialize(deserializer)?;
70 match opt {
71 Some(s) => s
72 .parse()
73 .map(Some)
74 .map_err(|e| de::Error::custom(format!("Parse error: {:?}", e))),
75 None => Ok(None),
76 }
77 }
78}
79
80pub mod base64_serialize_deserialize {
81 use base64::{Engine, engine::general_purpose::STANDARD};
82 use serde::{Deserializer, Serializer, de};
83
84 use super::*;
85 pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
86 let base58 = STANDARD.encode(v);
87 String::serialize(&base58, s)
88 }
89
90 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
91 where
92 D: Deserializer<'de>,
93 {
94 let field_string = String::deserialize(deserializer)?;
95 STANDARD
96 .decode(field_string)
97 .map_err(|e| de::Error::custom(format!("base64 decoding error: {:?}", e)))
98 }
99}
100
101#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
104#[serde(rename_all = "camelCase")]
105#[serde(untagged)]
106pub enum ComputeUnitPriceMicroLamports {
107 MicroLamports(u64),
108 #[serde(deserialize_with = "deserialize_auto")]
109 Auto,
110}
111
112fn deserialize_auto<'de, D>(deserializer: D) -> Result<(), D::Error>
113where
114 D: Deserializer<'de>,
115{
116 #[derive(Deserialize)]
117 enum Helper {
118 #[serde(rename = "auto")]
119 Variant,
120 }
121 Helper::deserialize(deserializer)?;
122 Ok(())
123}
124
125#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
126#[serde(rename_all = "camelCase")]
127pub enum PriorityLevel {
128 Medium,
129 High,
130 VeryHigh,
131}
132
133#[derive(Deserialize, Debug, PartialEq, Copy, Clone, Default)]
134#[serde(rename_all = "camelCase")]
135pub enum PrioritizationFeeLamports {
136 AutoMultiplier(u32),
137 JitoTipLamports(u64),
138 #[serde(rename_all = "camelCase")]
139 PriorityLevelWithMaxLamports {
140 priority_level: PriorityLevel,
141 max_lamports: u64,
142 #[serde(default)]
143 global: bool,
144 },
145 #[default]
146 #[serde(untagged, deserialize_with = "deserialize_auto")]
147 Auto,
148 #[serde(untagged)]
149 Lamports(u64),
150 #[serde(untagged, deserialize_with = "deserialize_disabled")]
151 Disabled,
152}
153
154fn deserialize_disabled<'de, D>(deserializer: D) -> Result<(), D::Error>
155where
156 D: Deserializer<'de>,
157{
158 #[derive(Deserialize)]
159 enum Helper {
160 #[serde(rename = "disabled")]
161 Variant,
162 }
163 Helper::deserialize(deserializer)?;
164 Ok(())
165}
166
167impl Serialize for PrioritizationFeeLamports {
168 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
169 where
170 S: Serializer,
171 {
172 #[derive(Serialize)]
173 #[serde(rename_all = "camelCase")]
174 struct AutoMultiplier {
175 auto_multiplier: u32,
176 }
177
178 #[derive(Serialize)]
179 #[serde(rename_all = "camelCase")]
180 struct JitoTip {
181 jito_tip_lamports: u64,
182 }
183
184 #[derive(Serialize)]
185 #[serde(rename_all = "camelCase")]
186 struct PriorityWrapper<'a> {
187 priority_level_with_max_lamports: PriorityLevelWithMaxLamports<'a>,
188 }
189
190 #[derive(Serialize)]
191 #[serde(rename_all = "camelCase")]
192 struct PriorityLevelWithMaxLamports<'a> {
193 priority_level: &'a PriorityLevel,
194 max_lamports: &'a u64,
195 global: &'a bool,
196 }
197
198 match self {
199 Self::AutoMultiplier(v) => AutoMultiplier {
200 auto_multiplier: *v,
201 }
202 .serialize(serializer),
203 Self::JitoTipLamports(v) => JitoTip {
204 jito_tip_lamports: *v,
205 }
206 .serialize(serializer),
207 Self::Auto => serializer.serialize_str("auto"),
208 Self::Lamports(v) => serializer.serialize_u64(*v),
209 Self::Disabled => serializer.serialize_str("disabled"),
210 Self::PriorityLevelWithMaxLamports {
211 priority_level,
212 max_lamports,
213 global,
214 } => PriorityWrapper {
215 priority_level_with_max_lamports: PriorityLevelWithMaxLamports {
216 priority_level,
217 max_lamports,
218 global,
219 },
220 }
221 .serialize(serializer),
222 }
223 }
224}
225
226#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
229#[serde(rename_all = "camelCase")]
230pub struct DynamicSlippageSettings {
231 pub min_bps: Option<u16>,
232 pub max_bps: Option<u16>,
233}
234
235#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
236#[serde(rename_all = "camelCase")]
237#[serde(default)]
238pub struct TransactionConfig {
239 pub wrap_and_unwrap_sol: bool,
240 pub allow_optimized_wrapped_sol_token_account: bool,
241 #[serde(with = "option_field_as_string")]
242 pub fee_account: Option<Pubkey>,
243 #[serde(with = "option_field_as_string")]
244 pub destination_token_account: Option<Pubkey>,
245 #[serde(with = "option_field_as_string")]
246 pub tracking_account: Option<Pubkey>,
247 pub compute_unit_price_micro_lamports: Option<ComputeUnitPriceMicroLamports>,
248 pub prioritization_fee_lamports: Option<PrioritizationFeeLamports>,
249 pub dynamic_compute_unit_limit: bool,
250 pub as_legacy_transaction: bool,
251 pub use_shared_accounts: bool,
252 pub use_token_ledger: bool,
253 pub skip_user_accounts_rpc_calls: bool,
254 pub keyed_ui_accounts: Option<Vec<KeyedUiAccount>>,
255 pub program_authority_id: Option<u8>,
256 pub dynamic_slippage: Option<DynamicSlippageSettings>,
257}
258
259impl Default for TransactionConfig {
260 fn default() -> Self {
261 Self {
262 wrap_and_unwrap_sol: true,
263 allow_optimized_wrapped_sol_token_account: false,
264 fee_account: None,
265 destination_token_account: None,
266 tracking_account: None,
267 compute_unit_price_micro_lamports: None,
268 prioritization_fee_lamports: Some(
269 PrioritizationFeeLamports::PriorityLevelWithMaxLamports {
270 priority_level: PriorityLevel::VeryHigh,
271 max_lamports: 4_000_000,
272 global: false,
273 },
274 ),
275 dynamic_compute_unit_limit: false,
276 as_legacy_transaction: false,
277 use_shared_accounts: true,
278 use_token_ledger: false,
279 skip_user_accounts_rpc_calls: false,
280 keyed_ui_accounts: None,
281 program_authority_id: None,
282 dynamic_slippage: None,
283 }
284 }
285}
286
287#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
288pub struct KeyedUiAccount {
289 pub pubkey: String,
290 #[serde(flatten)]
291 pub ui_account: UiAccount,
292 pub params: Option<Value>,
293}
294
295#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
296#[serde(rename_all = "camelCase")]
297pub struct SwapInfo {
298 #[serde(with = "field_as_string")]
299 pub amm_key: Pubkey,
300 pub label: String,
301 #[serde(with = "field_as_string")]
302 pub input_mint: Pubkey,
303 #[serde(with = "field_as_string")]
304 pub output_mint: Pubkey,
305 #[serde(with = "field_as_string")]
306 pub in_amount: u64,
307 #[serde(with = "field_as_string")]
308 pub out_amount: u64,
309 #[serde(default, with = "option_field_as_string")]
310 pub fee_amount: Option<u64>,
311 #[serde(default, with = "option_field_as_string")]
312 pub fee_mint: Option<Pubkey>,
313 }
320
321pub type RoutePlanWithMetadata = Vec<RoutePlanStep>;
322
323#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
324#[serde(rename_all = "camelCase")]
325pub struct RoutePlanStep {
326 pub swap_info: SwapInfo,
327 pub percent: u8,
328}
329
330#[derive(Serialize, Deserialize, Default, PartialEq, Clone, Debug)]
331pub enum SwapMode {
332 #[default]
333 ExactIn,
334 ExactOut,
335}
336
337impl FromStr for SwapMode {
338 type Err = anyhow::Error;
339 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
340 match s {
341 "ExactIn" => Ok(Self::ExactIn),
342 "ExactOut" => Ok(Self::ExactOut),
343 _ => Err(anyhow!("Invalid SwapMode: {}", s)),
344 }
345 }
346}
347
348#[derive(Serialize, Debug, Default, Clone)]
349#[serde(rename_all = "camelCase")]
350pub struct QuoteRequest {
351 #[serde(with = "field_as_string")]
352 pub input_mint: Pubkey,
353 #[serde(with = "field_as_string")]
354 pub output_mint: Pubkey,
355 #[serde(with = "field_as_string")]
356 pub amount: u64,
357 pub swap_mode: Option<SwapMode>,
358 pub slippage_bps: u16,
359 pub auto_slippage: Option<bool>,
360 pub max_auto_slippage_bps: Option<u16>,
361 pub compute_auto_slippage: bool,
362 pub auto_slippage_collision_usd_value: Option<u32>,
363 pub minimize_slippage: Option<bool>,
364 pub platform_fee_bps: Option<u8>,
365 pub dexes: Option<String>,
366 pub excluded_dexes: Option<String>,
367 pub only_direct_routes: Option<bool>,
368 pub as_legacy_transaction: Option<bool>,
369 pub restrict_intermediate_tokens: Option<bool>,
370 pub max_accounts: Option<usize>,
371 pub quote_type: Option<String>,
372 pub quote_args: Option<HashMap<String, String>>,
373 pub prefer_liquid_dexes: Option<bool>,
374}
375
376#[derive(Serialize, Debug, Default, Clone)]
377#[serde(rename_all = "camelCase")]
378pub struct InternalQuoteRequest {
379 #[serde(with = "field_as_string")]
380 pub input_mint: Pubkey,
381 #[serde(with = "field_as_string")]
382 pub output_mint: Pubkey,
383 #[serde(with = "field_as_string")]
384 pub amount: u64,
385 pub swap_mode: Option<SwapMode>,
386 pub slippage_bps: u16,
387 pub auto_slippage: Option<bool>,
388 pub max_auto_slippage_bps: Option<u16>,
389 pub compute_auto_slippage: bool,
390 pub auto_slippage_collision_usd_value: Option<u32>,
391 pub minimize_slippage: Option<bool>,
392 pub platform_fee_bps: Option<u8>,
393 pub dexes: Option<String>,
394 pub excluded_dexes: Option<String>,
395 pub only_direct_routes: Option<bool>,
396 pub as_legacy_transaction: Option<bool>,
397 pub restrict_intermediate_tokens: Option<bool>,
398 pub max_accounts: Option<usize>,
399 pub quote_type: Option<String>,
400 pub prefer_liquid_dexes: Option<bool>,
401}
402
403impl From<QuoteRequest> for InternalQuoteRequest {
404 fn from(req: QuoteRequest) -> Self {
405 Self {
406 input_mint: req.input_mint,
407 output_mint: req.output_mint,
408 amount: req.amount,
409 swap_mode: req.swap_mode,
410 slippage_bps: req.slippage_bps,
411 auto_slippage: req.auto_slippage,
412 max_auto_slippage_bps: req.max_auto_slippage_bps,
413 compute_auto_slippage: req.compute_auto_slippage,
414 auto_slippage_collision_usd_value: req.auto_slippage_collision_usd_value,
415 minimize_slippage: req.minimize_slippage,
416 platform_fee_bps: req.platform_fee_bps,
417 dexes: req.dexes,
418 excluded_dexes: req.excluded_dexes,
419 only_direct_routes: req.only_direct_routes,
420 as_legacy_transaction: req.as_legacy_transaction,
421 restrict_intermediate_tokens: req.restrict_intermediate_tokens,
422 max_accounts: req.max_accounts,
423 quote_type: req.quote_type,
424 prefer_liquid_dexes: req.prefer_liquid_dexes,
425 }
426 }
427}
428
429#[derive(Serialize, Deserialize, Clone, Debug)]
430#[serde(rename_all = "camelCase")]
431pub struct PlatformFee {
432 #[serde(with = "field_as_string")]
433 pub amount: u64,
434 pub fee_bps: u8,
435}
436
437#[derive(Serialize, Deserialize, Clone, Debug)]
438#[serde(rename_all = "camelCase")]
439pub struct QuoteResponse {
440 #[serde(with = "field_as_string")]
441 pub input_mint: Pubkey,
442 #[serde(with = "field_as_string")]
443 pub in_amount: u64,
444 #[serde(with = "field_as_string")]
445 pub output_mint: Pubkey,
446 #[serde(with = "field_as_string")]
447 pub out_amount: u64,
448 #[serde(with = "field_as_string")]
449 pub other_amount_threshold: u64,
450 pub swap_mode: SwapMode,
451 pub slippage_bps: u16,
452 #[serde(skip_serializing_if = "Option::is_none")]
453 pub computed_auto_slippage: Option<u16>,
454 #[serde(skip_serializing_if = "Option::is_none")]
455 pub uses_quote_minimizing_slippage: Option<bool>,
456 pub platform_fee: Option<PlatformFee>,
457 pub price_impact_pct: Decimal,
458 pub route_plan: RoutePlanWithMetadata,
459 #[serde(default)]
460 pub context_slot: u64,
461 #[serde(default)]
462 pub time_taken: f64,
463}
464
465#[derive(Serialize, Deserialize, Clone, Debug)]
466#[serde(rename_all = "camelCase")]
467pub struct SwapRequest {
468 #[serde(with = "field_as_string")]
469 pub user_public_key: Pubkey,
470 pub quote_response: QuoteResponse,
471 #[serde(flatten)]
472 pub config: TransactionConfig,
473}
474
475#[derive(Serialize, Deserialize, Debug, Clone)]
476#[serde(rename_all = "camelCase")]
477pub enum PrioritizationType {
478 #[serde(rename_all = "camelCase")]
479 Jito { lamports: u64 },
480 #[serde(rename_all = "camelCase")]
481 ComputeBudget {
482 micro_lamports: u64,
483 estimated_micro_lamports: Option<u64>,
484 },
485}
486
487#[derive(Serialize, Deserialize, Debug, Clone)]
488#[serde(rename_all = "camelCase")]
489pub struct DynamicSlippageReport {
490 pub slippage_bps: u16,
491 pub other_amount: Option<u64>,
492 pub simulated_incurred_slippage_bps: Option<i16>,
493 pub amplification_ratio: Option<Decimal>,
494}
495
496#[derive(Serialize, Deserialize, Debug, Clone)]
497#[serde(rename_all = "camelCase")]
498pub struct UiSimulationError {
499 error_code: String,
500 error: String,
501}
502
503#[derive(Serialize, Deserialize, Clone, Debug)]
504#[serde(rename_all = "camelCase")]
505pub struct SwapResponse {
506 #[serde(with = "base64_serialize_deserialize")]
507 pub swap_transaction: Vec<u8>,
508 pub last_valid_block_height: u64,
509 pub prioritization_fee_lamports: u64,
510 pub compute_unit_limit: u32,
511 pub prioritization_type: Option<PrioritizationType>,
512 pub dynamic_slippage_report: Option<DynamicSlippageReport>,
513 pub simulation_error: Option<UiSimulationError>,
514}
515
516#[derive(Serialize, Deserialize, Debug, Clone)]
517pub struct SwapInstructionsResponse {
518 pub token_ledger_instruction: Option<Instruction>,
519 pub compute_budget_instructions: Vec<Instruction>,
520 pub setup_instructions: Vec<Instruction>,
521 pub swap_instruction: Instruction,
522 pub cleanup_instruction: Option<Instruction>,
523 pub other_instructions: Vec<Instruction>,
524 pub address_lookup_table_addresses: Vec<Pubkey>,
525 pub prioritization_fee_lamports: u64,
526 pub compute_unit_limit: u32,
527 pub prioritization_type: Option<PrioritizationType>,
528 pub dynamic_slippage_report: Option<DynamicSlippageReport>,
529 pub simulation_error: Option<UiSimulationError>,
530}
531
532#[derive(Deserialize, Debug, Clone)]
534#[serde(rename_all = "camelCase")]
535struct InstructionInternal {
536 #[serde(with = "field_as_string")]
537 program_id: Pubkey,
538 accounts: Vec<AccountMetaInternal>,
539 #[serde(with = "base64_serialize_deserialize")]
540 data: Vec<u8>,
541}
542
543#[derive(Deserialize, Debug, Clone)]
544#[serde(rename_all = "camelCase")]
545struct AccountMetaInternal {
546 #[serde(with = "field_as_string")]
547 pubkey: Pubkey,
548 is_signer: bool,
549 is_writable: bool,
550}
551
552impl From<AccountMetaInternal> for AccountMeta {
553 fn from(a: AccountMetaInternal) -> Self {
554 AccountMeta {
555 pubkey: a.pubkey,
556 is_signer: a.is_signer,
557 is_writable: a.is_writable,
558 }
559 }
560}
561
562impl From<InstructionInternal> for Instruction {
563 fn from(i: InstructionInternal) -> Self {
564 Instruction {
565 program_id: i.program_id,
566 accounts: i.accounts.into_iter().map(Into::into).collect(),
567 data: i.data,
568 }
569 }
570}
571
572#[derive(Deserialize, Debug, Clone)]
573#[serde(rename_all = "camelCase")]
574struct SwapInstructionsResponseInternal {
575 token_ledger_instruction: Option<InstructionInternal>,
576 compute_budget_instructions: Vec<InstructionInternal>,
577 setup_instructions: Vec<InstructionInternal>,
578 swap_instruction: InstructionInternal,
579 cleanup_instruction: Option<InstructionInternal>,
580 other_instructions: Vec<InstructionInternal>,
581 address_lookup_table_addresses: Vec<PubkeyInternal>,
582 prioritization_fee_lamports: u64,
583 compute_unit_limit: u32,
584 prioritization_type: Option<PrioritizationType>,
585 dynamic_slippage_report: Option<DynamicSlippageReport>,
586 simulation_error: Option<UiSimulationError>,
587}
588
589#[derive(Deserialize, Debug, Clone)]
590struct PubkeyInternal(#[serde(with = "field_as_string")] Pubkey);
591
592impl From<SwapInstructionsResponseInternal> for SwapInstructionsResponse {
593 fn from(v: SwapInstructionsResponseInternal) -> Self {
594 Self {
595 token_ledger_instruction: v.token_ledger_instruction.map(Into::into),
596 compute_budget_instructions: v
597 .compute_budget_instructions
598 .into_iter()
599 .map(Into::into)
600 .collect(),
601 setup_instructions: v.setup_instructions.into_iter().map(Into::into).collect(),
602 swap_instruction: v.swap_instruction.into(),
603 cleanup_instruction: v.cleanup_instruction.map(Into::into),
604 other_instructions: v.other_instructions.into_iter().map(Into::into).collect(),
605 address_lookup_table_addresses: v
606 .address_lookup_table_addresses
607 .into_iter()
608 .map(|p| p.0)
609 .collect(),
610 prioritization_fee_lamports: v.prioritization_fee_lamports,
611 compute_unit_limit: v.compute_unit_limit,
612 prioritization_type: v.prioritization_type,
613 dynamic_slippage_report: v.dynamic_slippage_report,
614 simulation_error: v.simulation_error,
615 }
616 }
617}
618
619#[derive(Serialize, Deserialize, Clone, Debug)]
620#[serde(rename_all = "camelCase")]
621pub struct OrderResponse {
622 pub route_plan: RoutePlanWithMetadata,
623 #[serde(with = "field_as_string")]
624 pub input_mint: Pubkey,
625 #[serde(with = "field_as_string")]
626 pub output_mint: Pubkey,
627 #[serde(with = "field_as_string")]
628 pub in_amount: u64,
629 #[serde(with = "field_as_string")]
630 pub out_amount: u64,
631 #[serde(with = "field_as_string")]
632 pub other_amount_threshold: u64,
633 pub swap_mode: SwapMode,
634 pub transaction: String,
635 pub request_id: String,
636 pub error_message: Option<String>,
637 pub router: String,
638 pub slippage_bps: u64,
639}
640
641#[derive(Clone)]
642struct JupiterClientRef {
643 client: Client,
644 base_path: String,
645 api_key: Option<String>,
646}
647
648#[derive(Clone)]
649pub struct JupiterClient {
650 inner: Arc<JupiterClientRef>,
651}
652
653async fn check_is_success(response: reqwest::Response) -> Result<reqwest::Response> {
654 if !response.status().is_success() {
655 let status = response.status();
656 let text = response.text().await.ok();
657 return Err(anyhow!("Request failed: {}, body: {:?}", status, text));
658 }
659
660 Ok(response)
661}
662
663async fn check_and_deserialize<T: DeserializeOwned>(response: reqwest::Response) -> Result<T> {
664 check_is_success(response)
665 .await?
666 .json::<T>()
667 .await
668 .map_err(Into::into)
669}
670
671impl JupiterClient {
672 fn build_inner(base_path: impl AsRef<str>, client: Client, api_key: Option<String>) -> Self {
673 Self {
674 inner: Arc::new(JupiterClientRef {
675 client,
676 base_path: base_path.as_ref().to_string(),
677 api_key,
678 }),
679 }
680 }
681
682 pub fn new(base_path: impl AsRef<str>) -> anyhow::Result<Self> {
683 Ok(Self::build_inner(
684 base_path,
685 ClientBuilder::new().build()?,
686 None,
687 ))
688 }
689
690 pub fn new_with_apikey(
691 base_path: impl AsRef<str>,
692 api_key: impl AsRef<str>,
693 ) -> anyhow::Result<Self> {
694 Ok(Self::build_inner(
695 base_path,
696 ClientBuilder::new().build()?,
697 Some(api_key.as_ref().to_string()),
698 ))
699 }
700
701 pub fn new_with_timeout(base_path: impl AsRef<str>, timeout: Duration) -> anyhow::Result<Self> {
702 let client = ClientBuilder::new().timeout(timeout).build()?;
703
704 Ok(Self::build_inner(base_path, client, None))
705 }
706
707 pub fn new_with_timeout_and_apikey(
708 base_path: impl AsRef<str>,
709 timeout: Duration,
710 api_key: impl AsRef<str>,
711 ) -> anyhow::Result<Self> {
712 let client = ClientBuilder::new().timeout(timeout).build()?;
713
714 Ok(Self::build_inner(
715 base_path,
716 client,
717 Some(api_key.as_ref().to_string()),
718 ))
719 }
720
721 pub fn new_with_client(base_path: impl AsRef<str>, client: Client) -> Self {
722 Self::build_inner(base_path, client, None)
723 }
724
725 pub fn new_with_client_and_apikey(
726 base_path: impl AsRef<str>,
727 client: Client,
728 api_key: impl AsRef<str>,
729 ) -> Self {
730 Self::build_inner(base_path, client, Some(api_key.as_ref().to_string()))
731 }
732
733 pub fn base_path(&self) -> &str {
734 &self.inner.base_path
735 }
736
737 pub fn api_key(&self) -> Option<&str> {
738 self.inner.api_key.as_deref()
739 }
740
741 pub async fn request(
742 &self,
743 method: reqwest::Method,
744 path: impl AsRef<str>,
745 ) -> Result<reqwest::Response> {
746 let url = format!("{}{}", self.inner.base_path, path.as_ref());
747 let mut builder = self.inner.client.request(method, &url);
748
749 if let Some(ref api_key) = self.inner.api_key {
750 builder = builder.header("x-api-key", api_key);
751 }
752
753 Ok(builder.send().await?)
754 }
755
756 pub async fn quote(&self, request: &QuoteRequest) -> Result<QuoteResponse> {
757 let url = format!("{}/quote", self.inner.base_path);
758 let internal = InternalQuoteRequest::from(request.clone());
759
760 let mut builder = self
761 .inner
762 .client
763 .get(&url)
764 .query(&internal)
765 .query(&request.quote_args);
766
767 if let Some(ref api_key) = self.inner.api_key {
768 builder = builder.header("x-api-key", api_key);
769 }
770
771 check_and_deserialize(builder.send().await?).await
772 }
773
774 pub async fn quote_raw(&self, request: &QuoteRequest) -> Result<reqwest::Response> {
775 let url = format!("{}/quote", self.inner.base_path);
776 let internal = InternalQuoteRequest::from(request.clone());
777
778 let mut builder = self
779 .inner
780 .client
781 .get(&url)
782 .query(&internal)
783 .query(&request.quote_args);
784
785 if let Some(ref api_key) = self.inner.api_key {
786 builder = builder.header("x-api-key", api_key);
787 }
788
789 Ok(builder.send().await?)
790 }
791
792 pub async fn swap(
793 &self,
794 swap_request: &SwapRequest,
795 extra_args: Option<HashMap<String, String>>,
796 ) -> Result<SwapResponse> {
797 let mut builder = self
798 .inner
799 .client
800 .post(format!("{}/swap", self.inner.base_path))
801 .query(&extra_args)
802 .json(swap_request);
803
804 if let Some(ref api_key) = self.inner.api_key {
805 builder = builder.header("x-api-key", api_key);
806 }
807
808 check_and_deserialize(builder.send().await?).await
809 }
810
811 pub async fn swap_instructions(
812 &self,
813 swap_request: &SwapRequest,
814 ) -> Result<SwapInstructionsResponse> {
815 let mut builder = self
816 .inner
817 .client
818 .post(format!("{}/swap-instructions", self.inner.base_path))
819 .json(swap_request);
820
821 if let Some(ref api_key) = self.inner.api_key {
822 builder = builder.header("x-api-key", api_key);
823 }
824
825 check_and_deserialize::<SwapInstructionsResponseInternal>(builder.send().await?)
826 .await
827 .map(Into::into)
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use super::solana_sdk::pubkey;
834 use super::*;
835
836 #[tokio::test]
837 async fn test_swap() {
838 let client = JupiterClient::new_with_apikey(
839 "https://api.jup.ag/swap/v1",
840 "6fec26c0-9178-4d63-abe2-e29f8a10107f",
841 )
842 .unwrap();
843
844 let quote = client
845 .quote(&QuoteRequest {
846 input_mint: pubkey!("So11111111111111111111111111111111111111112"),
847 output_mint: pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
848 amount: 1_000_000_000,
849 ..Default::default()
850 })
851 .await
852 .unwrap();
853
854 println!("{:#?}", quote);
855 }
856}