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", 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 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 for ix_data in response.setup_instructions {
219 instructions.push(Self::convert_instruction_data(ix_data)?);
220 }
221
222 instructions
224 .push(Self::convert_instruction_data(response.swap_instruction)?);
225
226 if let Some(cleanup_ix) = response.cleanup_instruction {
228 instructions.push(Self::convert_instruction_data(cleanup_ix)?);
229 }
230
231 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}