Skip to main content

scematica_executor/
jupiter.rs

1use crate::SwapInstructionBuilder;
2use anyhow::Result;
3use async_trait::async_trait;
4use base64::{Engine as _, engine::general_purpose};
5use scematica_core::types::DexKind;
6use solana_sdk::{instruction::Instruction, pubkey::Pubkey, transaction::VersionedTransaction};
7
8/// Jupiter V6 aggregator swap builder
9/// Uses Jupiter's REST API to get the optimal route and swap transaction
10pub struct JupiterBuilder {
11    http_client: reqwest::Client,
12    api_url: String,
13}
14
15impl JupiterBuilder {
16    pub fn new() -> Self {
17        Self {
18            http_client: reqwest::Client::new(),
19            api_url: "https://quote-api.jup.ag/v6".into(),
20        }
21    }
22
23    /// Get a swap quote from Jupiter API
24    pub async fn get_quote(
25        &self,
26        input_mint: &Pubkey,
27        output_mint: &Pubkey,
28        amount: u64,
29        slippage_bps: u16,
30    ) -> Result<serde_json::Value> {
31        let url = format!(
32            "{}/quote?inputMint={}&outputMint={}&amount={}&slippageBps={}",
33            self.api_url, input_mint, output_mint, amount, slippage_bps
34        );
35        let resp = self.http_client.get(&url).send().await?.json().await?;
36        Ok(resp)
37    }
38
39    /// Get a swap transaction from Jupiter API
40    pub async fn get_swap_transaction(
41        &self,
42        quote: &serde_json::Value,
43        user_public_key: &Pubkey,
44    ) -> Result<Vec<u8>> {
45        let payload = serde_json::json!({
46            "quoteResponse": quote,
47            "userPublicKey": user_public_key.to_string(),
48            "wrapAndUnwrapSol": true,
49            "dynamicComputeUnitLimit": true,
50            "prioritizationFeeLamports": "auto"
51        });
52
53        let response = self
54            .http_client
55            .post(format!("{}/swap", self.api_url))
56            .json(&payload)
57            .send()
58            .await?;
59
60        let status = response.status();
61        if !status.is_success() {
62            let body = response.text().await.unwrap_or_default();
63            anyhow::bail!(
64                "Jupiter /swap returned HTTP {}: {}",
65                status.as_u16(),
66                body
67            );
68        }
69
70        let resp: serde_json::Value = response.json().await?;
71
72        let tx_b64 = resp["swapTransaction"]
73            .as_str()
74            .ok_or_else(|| anyhow::anyhow!("No swapTransaction in Jupiter response"))?;
75
76        Ok(general_purpose::STANDARD.decode(tx_b64)?)
77    }
78
79    /// Deserialize a bincode-encoded `VersionedTransaction` from raw bytes.
80    ///
81    /// Returns `Err` on empty input, malformed bytes, or any bincode decode failure (Req 8.3).
82    pub fn deserialize_transaction(&self, tx_bytes: &[u8]) -> Result<VersionedTransaction> {
83        bincode::deserialize::<VersionedTransaction>(tx_bytes).map_err(Into::into)
84    }
85}
86
87#[async_trait]
88impl SwapInstructionBuilder for JupiterBuilder {
89    fn dex(&self) -> DexKind {
90        DexKind::Jupiter
91    }
92
93    async fn build_swap(
94        &self,
95        _pool: &Pubkey,
96        _owner: &Pubkey,
97        _token_in: &Pubkey,
98        _token_out: &Pubkey,
99        _ata_in: &Pubkey,
100        _ata_out: &Pubkey,
101        _amount_in: u64,
102        _min_amount_out: u64,
103    ) -> Result<Vec<Instruction>> {
104        // Jupiter returns a full versioned transaction, not individual instructions.
105        // For arb use, we prefer direct DEX instructions to avoid Jupiter's overhead.
106        // This builder is provided for single-hop swaps via the sniper.
107        // Returns empty — caller should use get_swap_transaction() directly.
108        tracing::warn!("JupiterBuilder::build_swap called — use get_swap_transaction() for Jupiter swaps");
109        Ok(vec![])
110    }
111}