Skip to main content

jupiter_client/
lib.rs

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// ====================== ComputeUnitPrice & Priority ======================
102
103#[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// ====================== TransactionConfig ======================
227
228#[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    // deprecated
314    //
315    // #[serde(default, with = "field_as_string")]
316    // pub fee_amount: u64,
317    // #[serde(default, with = "field_as_string")]
318    // pub fee_mint: Pubkey,
319}
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// Internal structs for deserialization
533#[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}