listen_kit/
jup.rs

1use std::str::FromStr;
2
3use base64::prelude::BASE64_STANDARD;
4use base64::Engine;
5use serde::{Deserialize, Serialize};
6use solana_client::nonblocking::rpc_client::RpcClient;
7use solana_sdk::transaction::Transaction;
8use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer};
9
10use crate::jito::send_jito_tx;
11
12#[derive(Serialize, Deserialize, Debug)]
13pub struct PlatformFee {
14    pub amount: String,
15    #[serde(rename = "feeBps")]
16    pub fee_bps: i32,
17}
18
19#[derive(Serialize, Deserialize, Debug)]
20pub struct DynamicSlippage {
21    #[serde(rename = "minBps")]
22    pub min_bps: i32,
23    #[serde(rename = "maxBps")]
24    pub max_bps: i32,
25}
26
27#[derive(Serialize, Deserialize, Debug)]
28pub struct RoutePlan {
29    #[serde(rename = "swapInfo")]
30    pub swap_info: SwapInfo,
31    pub percent: i32,
32}
33
34#[derive(Serialize, Deserialize, Debug)]
35pub struct QuoteResponse {
36    #[serde(rename = "inputMint")]
37    pub input_mint: String,
38    #[serde(rename = "inAmount")]
39    pub in_amount: String,
40    #[serde(rename = "outputMint")]
41    pub output_mint: String,
42    #[serde(rename = "outAmount")]
43    pub out_amount: String,
44    #[serde(rename = "otherAmountThreshold")]
45    pub other_amount_threshold: String,
46    #[serde(rename = "swapMode")]
47    pub swap_mode: String,
48    #[serde(rename = "slippageBps")]
49    pub slippage_bps: i32,
50    #[serde(rename = "platformFee")]
51    pub platform_fee: Option<PlatformFee>,
52    #[serde(rename = "priceImpactPct")]
53    pub price_impact_pct: String,
54    #[serde(rename = "routePlan")]
55    pub route_plan: Vec<RoutePlan>,
56    #[serde(rename = "contextSlot")]
57    pub context_slot: u64,
58    #[serde(rename = "timeTaken")]
59    pub time_taken: f64,
60}
61
62#[derive(Serialize, Deserialize, Debug)]
63pub struct SwapInfo {
64    #[serde(rename = "ammKey")]
65    pub amm_key: String,
66    pub label: Option<String>,
67    #[serde(rename = "inputMint")]
68    pub input_mint: String,
69    #[serde(rename = "outputMint")]
70    pub output_mint: String,
71    #[serde(rename = "inAmount")]
72    pub in_amount: String,
73    #[serde(rename = "outAmount")]
74    pub out_amount: String,
75    #[serde(rename = "feeAmount")]
76    pub fee_amount: String,
77    #[serde(rename = "feeMint")]
78    pub fee_mint: String,
79}
80
81#[derive(Serialize)]
82pub struct SwapRequest {
83    #[serde(rename = "userPublicKey")]
84    pub user_public_key: String,
85    #[serde(rename = "wrapAndUnwrapSol")]
86    pub wrap_and_unwrap_sol: bool,
87    #[serde(rename = "useSharedAccounts")]
88    pub use_shared_accounts: bool,
89    #[serde(rename = "feeAccount")]
90    pub fee_account: Option<String>,
91    #[serde(rename = "trackingAccount")]
92    pub tracking_account: Option<String>,
93    #[serde(rename = "computeUnitPriceMicroLamports")]
94    pub compute_unit_price_micro_lamports: Option<u64>,
95    #[serde(rename = "prioritizationFeeLamports")]
96    pub prioritization_fee_lamports: Option<u64>,
97    #[serde(rename = "asLegacyTransaction")]
98    pub as_legacy_transaction: bool,
99    #[serde(rename = "useTokenLedger")]
100    pub use_token_ledger: bool,
101    #[serde(rename = "destinationTokenAccount")]
102    pub destination_token_account: Option<String>,
103    #[serde(rename = "dynamicComputeUnitLimit")]
104    pub dynamic_compute_unit_limit: bool,
105    #[serde(rename = "skipUserAccountsRpcCalls")]
106    pub skip_user_accounts_rpc_calls: bool,
107    #[serde(rename = "dynamicSlippage")]
108    pub dynamic_slippage: Option<DynamicSlippage>,
109    #[serde(rename = "quoteResponse")]
110    pub quote_response: QuoteResponse,
111}
112
113#[derive(Deserialize, Debug)]
114pub struct SwapInstructionsResponse {
115    #[serde(rename = "tokenLedgerInstruction")]
116    pub token_ledger_instruction: Option<InstructionData>,
117    #[serde(rename = "computeBudgetInstructions")]
118    pub compute_budget_instructions: Option<Vec<InstructionData>>,
119    #[serde(rename = "setupInstructions")]
120    pub setup_instructions: Vec<InstructionData>,
121    #[serde(rename = "swapInstruction")]
122    pub swap_instruction: InstructionData,
123    #[serde(rename = "cleanupInstruction")]
124    pub cleanup_instruction: Option<InstructionData>,
125    #[serde(rename = "addressLookupTableAddresses")]
126    pub address_lookup_table_addresses: Vec<String>,
127}
128
129#[derive(Deserialize, Debug)]
130pub struct InstructionData {
131    #[serde(rename = "programId")]
132    pub program_id: String,
133    pub accounts: Vec<AccountMeta>,
134    pub data: String,
135}
136
137#[derive(Deserialize, Debug)]
138pub struct AccountMeta {
139    pub pubkey: String,
140    #[serde(rename = "isSigner")]
141    pub is_signer: bool,
142    #[serde(rename = "isWritable")]
143    pub is_writable: bool,
144}
145
146pub struct Jupiter;
147
148impl Jupiter {
149    pub async fn fetch_quote(
150        input_mint: &str,
151        output_mint: &str,
152        amount: u64,
153        slippage: u16,
154    ) -> Result<QuoteResponse, Box<dyn std::error::Error>> {
155        let url = format!(
156            "https://quote-api.jup.ag/v6/quote?inputMint={}&outputMint={}&amount={}&slippageBps={}&onlyDirectRoutes=true", // TODO remove the onlyDirectRoutes query param after fixing jito issue
157            input_mint, output_mint, amount, slippage
158        );
159
160        let response =
161            reqwest::get(&url).await?.json::<QuoteResponse>().await?;
162        Ok(response)
163    }
164
165    pub async fn swap(
166        quote_response: QuoteResponse,
167        signer: &Keypair,
168    ) -> Result<String, Box<dyn std::error::Error>> {
169        let swap_request = SwapRequest {
170            user_public_key: signer.pubkey().to_string(),
171            wrap_and_unwrap_sol: true,
172            use_shared_accounts: true,
173            fee_account: None,
174            tracking_account: None,
175            compute_unit_price_micro_lamports: None,
176            prioritization_fee_lamports: None,
177            as_legacy_transaction: false,
178            use_token_ledger: false,
179            destination_token_account: None,
180            dynamic_compute_unit_limit: true,
181            skip_user_accounts_rpc_calls: true,
182            dynamic_slippage: None,
183            quote_response,
184        };
185
186        let client = reqwest::Client::new();
187        let raw_res = client
188            .post("https://quote-api.jup.ag/v6/swap-instructions")
189            .json(&swap_request)
190            .send()
191            .await?;
192        if !raw_res.status().is_success() {
193            let error = raw_res.text().await?;
194            return Err(error.into());
195        }
196        let response = raw_res.json::<SwapInstructionsResponse>().await?;
197
198        let rpc_client = RpcClient::new(std::env::var("RPC_URL")?);
199        let recent_blockhash = rpc_client.get_latest_blockhash().await?;
200
201        let mut instructions = Vec::new();
202
203        // Add token ledger instruction if present
204        if let Some(token_ledger_ix) = response.token_ledger_instruction {
205            instructions
206                .push(Self::convert_instruction_data(token_ledger_ix)?);
207        }
208
209        if let Some(compute_budget_instructions) =
210            response.compute_budget_instructions
211        {
212            for ix_data in compute_budget_instructions {
213                instructions.push(Self::convert_instruction_data(ix_data)?);
214            }
215        }
216
217        // Add setup instructions
218        for ix_data in response.setup_instructions {
219            instructions.push(Self::convert_instruction_data(ix_data)?);
220        }
221
222        // Add swap instruction
223        instructions
224            .push(Self::convert_instruction_data(response.swap_instruction)?);
225
226        // Add cleanup instruction if present
227        if let Some(cleanup_ix) = response.cleanup_instruction {
228            instructions.push(Self::convert_instruction_data(cleanup_ix)?);
229        }
230
231        // Create and sign transaction
232        let mut tx =
233            Transaction::new_with_payer(&instructions, Some(&signer.pubkey()));
234        tx.sign(&[signer], recent_blockhash);
235
236        let result = send_jito_tx(tx).await?;
237
238        Ok(result)
239    }
240
241    fn convert_instruction_data(
242        ix_data: InstructionData,
243    ) -> Result<solana_sdk::instruction::Instruction, Box<dyn std::error::Error>>
244    {
245        let program_id = Pubkey::from_str(&ix_data.program_id)?;
246
247        let accounts = ix_data
248            .accounts
249            .into_iter()
250            .map(|acc| {
251                Ok(solana_sdk::instruction::AccountMeta {
252                    pubkey: Pubkey::from_str(&acc.pubkey)?,
253                    is_signer: acc.is_signer,
254                    is_writable: acc.is_writable,
255                })
256            })
257            .collect::<Result<Vec<_>, Box<dyn std::error::Error>>>()?;
258
259        let data = BASE64_STANDARD.decode(ix_data.data)?;
260
261        Ok(solana_sdk::instruction::Instruction {
262            program_id,
263            accounts,
264            data,
265        })
266    }
267}