solana_trader_client_rust/provider/http/
mod.rs

1pub mod quote;
2pub mod swap;
3
4use anyhow::{anyhow, Result};
5use reqwest::{
6    header::{HeaderMap, HeaderValue},
7    Client,
8};
9use serde::de::DeserializeOwned;
10use serde_json::json;
11use solana_sdk::{pubkey::Pubkey, signature::Keypair};
12use solana_trader_proto::api::{self, GetRecentBlockHashResponseV2};
13
14use crate::{
15    common::{
16        get_base_url_from_env, http_endpoint, is_submit_only_endpoint,
17        signing::{sign_transaction, SubmitParams},
18        BaseConfig,
19    },
20    provider::utils::convert_string_enums,
21};
22
23use super::utils::IntoTransactionMessage;
24
25pub struct HTTPClient {
26    client: Client,
27    base_url: String,
28    keypair: Option<Keypair>,
29    pub public_key: Option<Pubkey>,
30}
31
32impl HTTPClient {
33    pub fn get_keypair(&self) -> Result<&Keypair> {
34        Ok(self.keypair.as_ref().unwrap())
35    }
36
37    pub fn new(endpoint: Option<String>) -> Result<Self> {
38        let base = BaseConfig::try_from_env()?;
39        let (default_base_url, secure) = get_base_url_from_env();
40        let final_base_url = endpoint.unwrap_or(default_base_url);
41        let endpoint = http_endpoint(&final_base_url, secure);
42
43        is_submit_only_endpoint(&final_base_url);
44
45        let headers = Self::build_headers(&base.auth_header)?;
46        let client = Client::builder()
47            .default_headers(headers)
48            .build()
49            .map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;
50
51        Ok(Self {
52            client,
53            base_url: endpoint,
54            keypair: base.keypair,
55            public_key: base.public_key,
56        })
57    }
58
59    fn build_headers(auth_header: &str) -> Result<HeaderMap> {
60        let mut headers = HeaderMap::new();
61        headers.insert(
62            "Authorization",
63            HeaderValue::from_str(auth_header)
64                .map_err(|e| anyhow!("Invalid auth header: {}", e))?,
65        );
66        headers.insert("x-sdk", HeaderValue::from_static("rust-client"));
67        headers.insert(
68            "x-sdk-version",
69            HeaderValue::from_static(env!("CARGO_PKG_VERSION")),
70        );
71        Ok(headers)
72    }
73
74    async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
75        if !response.status().is_success() {
76            let error_text = response
77                .text()
78                .await
79                .unwrap_or_else(|_| "Failed to read error response".into());
80            return Err(anyhow::anyhow!("HTTP request failed: {}", error_text));
81        }
82
83        let res = response.text().await?;
84
85        let mut value = serde_json::from_str(&res)
86            .map_err(|e| anyhow::anyhow!("Failed to parse response as JSON: {}", e))?;
87
88        convert_string_enums(&mut value);
89
90        serde_json::from_value(value)
91            .map_err(|e| anyhow::anyhow!("Failed to parse response into desired type: {}", e))
92    }
93
94    pub async fn sign_and_submit<T: IntoTransactionMessage + Clone>(
95        &self,
96        txs: Vec<T>,
97        submit_opts: SubmitParams,
98        use_bundle: bool,
99    ) -> Result<Vec<String>> {
100        let keypair = self.get_keypair()?;
101
102        // TODO: refactor once this endpoint is defined
103        let response = self
104            .client
105            .get(format!(
106                "{}/api/v2/system/blockhash?offset={}",
107                self.base_url, 0
108            ))
109            .send()
110            .await?;
111
112        let res: GetRecentBlockHashResponseV2 = self.handle_response(response).await?;
113
114        if txs.len() == 1 {
115            let signed_tx = sign_transaction(&txs[0], keypair, res.block_hash).await?;
116
117            let request_json = json!({
118                "transaction": { "content": signed_tx.content, "isCleanup": signed_tx.is_cleanup },
119                "skipPreFlight": submit_opts.skip_pre_flight,
120                "frontRunningProtection": submit_opts.front_running_protection,
121                "useStakedRPCs": submit_opts.use_staked_rpcs,
122                "fastBestEffort": submit_opts.fast_best_effort
123            });
124
125            let response = self
126                .client
127                .post(format!("{}/api/v2/submit", self.base_url))
128                .json(&request_json)
129                .send()
130                .await?;
131
132            let result: serde_json::Value = self.handle_response(response).await?;
133            return Ok(vec![result
134                .get("signature")
135                .and_then(|s| s.as_str())
136                .map(String::from)
137                .ok_or_else(|| anyhow!("Missing signature in response"))?]);
138        }
139
140        let mut entries = Vec::with_capacity(txs.len());
141        for tx in txs {
142            let signed_tx = sign_transaction(&tx, keypair, res.block_hash.clone()).await?;
143            entries.push(json!({
144                "transaction": {
145                    "content": signed_tx.content,
146                    "isCleanup": signed_tx.is_cleanup
147                },
148                "skipPreFlight": submit_opts.skip_pre_flight,
149                "frontRunningProtection": submit_opts.front_running_protection,
150                "useStakedRPCs": submit_opts.use_staked_rpcs,
151                "fastBestEffort": submit_opts.fast_best_effort
152            }));
153        }
154
155        let request_json = json!({
156            "entries": entries,
157            "useBundle": use_bundle,
158            "submitStrategy": submit_opts.submit_strategy
159        });
160
161        let response = self
162            .client
163            .post(format!("{}/api/v2/submit/batch", self.base_url))
164            .json(&request_json)
165            .send()
166            .await?;
167
168        let result: serde_json::Value = self.handle_response(response).await?;
169
170        let signatures = result["transactions"]
171            .as_array()
172            .ok_or_else(|| anyhow!("Invalid response format"))?
173            .iter()
174            .filter(|entry| entry["submitted"].as_bool().unwrap_or(false))
175            .filter_map(|entry| entry["signature"].as_str().map(String::from))
176            .collect();
177
178        Ok(signatures)
179    }
180
181    pub async fn sign_and_submit_snipe<T: IntoTransactionMessage + Clone>(
182        &self,
183        txs: Vec<T>,
184        use_staked_rpcs: bool,
185    ) -> Result<Vec<String>> {
186        let keypair = self.get_keypair()?;
187
188        // Get recent blockhash
189        let response = self
190            .client
191            .get(format!(
192                "{}/api/v2/system/blockhash?offset={}",
193                self.base_url, 0
194            ))
195            .send()
196            .await?;
197
198        let res: GetRecentBlockHashResponseV2 = self.handle_response(response).await?;
199
200        // Build entries for each transaction
201        let mut entries = Vec::with_capacity(txs.len());
202        for tx in txs {
203            let signed_tx = sign_transaction(&tx, keypair, res.block_hash.clone()).await?;
204            entries.push(json!({
205                "transaction": {
206                    "content": signed_tx.content,
207                    "isCleanup": signed_tx.is_cleanup
208                },
209                "skipPreFlight": false
210            }));
211        }
212
213        let request_json = json!({
214            "entries": entries,
215            "useStakedRPCs": use_staked_rpcs
216        });
217
218        let response = self
219            .client
220            .post(format!("{}/api/v2/submit-snipe", self.base_url))
221            .json(&request_json)
222            .send()
223            .await?;
224
225        let result: serde_json::Value = self.handle_response(response).await?;
226
227        let signatures = result["transactions"]
228            .as_array()
229            .ok_or_else(|| anyhow!("Invalid response format"))?
230            .iter()
231            .filter(|entry| entry["submitted"].as_bool().unwrap_or(false))
232            .filter_map(|entry| entry["signature"].as_str().map(String::from))
233            .collect();
234
235        Ok(signatures)
236    }
237
238    pub async fn sign_and_submit_paladin<T: IntoTransactionMessage + Clone>(
239        &self,
240        tx: T,
241    ) -> Result<String> {
242        let response = self
243            .client
244            .get(format!(
245                "{}/api/v2/system/blockhash?offset={}",
246                self.base_url, 0
247            ))
248            .send()
249            .await?;
250
251        let res: GetRecentBlockHashResponseV2 = self.handle_response(response).await?;
252        let keypair = self.get_keypair()?;
253        let signed_tx = sign_transaction(&tx, keypair, res.block_hash).await?;
254
255        let request_json = json!({
256            "transaction": {
257                "content": signed_tx.content,
258                "isCleanup": signed_tx.is_cleanup
259            }
260        });
261
262        let response = self
263            .client
264            .post(format!("{}/api/v2/submit-paladin", self.base_url))
265            .json(&request_json)
266            .send()
267            .await?;
268
269        let result: serde_json::Value = self.handle_response(response).await?;
270        let signature = result
271            .get("signature")
272            .and_then(|s| s.as_str())
273            .map(String::from)
274            .ok_or_else(|| anyhow!("Missing signature in response"))?;
275
276        Ok(signature)
277    }
278
279    pub async fn get_transaction(
280        &self,
281        request: &api::GetTransactionRequest,
282    ) -> anyhow::Result<api::GetTransactionResponse> {
283        let url = format!(
284            "{}/api/v2/transaction?signature={}",
285            self.base_url, request.signature
286        );
287
288        println!("{}", url);
289
290        let response = self
291            .client
292            .get(&url)
293            .send()
294            .await
295            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
296
297        let response_text = response.text().await?;
298
299        println!("{}", response_text);
300
301        // let mut value: serde_json::Value = serde_json::from_str(&response_text)
302        //     .map_err(|e| anyhow::anyhow!("Failed to parse response as JSON: {}", e))?;
303        //
304        // convert_string_enums(&mut value);
305        //
306        // serde_json::from_value(value)
307        //     .map_err(|e| anyhow::anyhow!("Failed to parse response into GetTransactionResponse: {}", e))
308
309        let response = self
310            .client
311            .get(&url)
312            .send()
313            .await
314            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
315
316        self.handle_response(response).await
317    }
318    pub async fn get_recent_block_hash(&self) -> anyhow::Result<api::GetRecentBlockHashResponse> {
319        let url = format!("{}/api/v1/system/blockhash", self.base_url);
320
321        println!("{}", url);
322
323        let response = self
324            .client
325            .get(&url)
326            .send()
327            .await
328            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
329
330        self.handle_response(response).await
331    }
332
333    pub async fn get_recent_block_hash_v2(
334        &self,
335        request: &api::GetRecentBlockHashRequestV2,
336    ) -> anyhow::Result<api::GetRecentBlockHashResponseV2> {
337        let url = format!(
338            "{}/api/v2/system/blockhash?offset={}",
339            self.base_url, request.offset
340        );
341
342        println!("{}", url);
343
344        let response = self
345            .client
346            .get(&url)
347            .send()
348            .await
349            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
350
351        self.handle_response(response).await
352    }
353
354    pub async fn get_rate_limit(&self) -> anyhow::Result<api::GetRateLimitResponse> {
355        let url = format!("{}/api/v2/rate-limit", self.base_url);
356
357        println!("{}", url);
358
359        let response = self
360            .client
361            .get(&url)
362            .send()
363            .await
364            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
365
366        self.handle_response(response).await
367    }
368
369    pub async fn get_account_balance_v2(
370        &self,
371        request: api::GetAccountBalanceRequest,
372    ) -> anyhow::Result<api::GetAccountBalanceResponse> {
373        println!("here1");
374
375        let url = format!(
376            "{}/api/v2/balance?ownerAddress={}",
377            self.base_url, request.owner_address
378        );
379
380        let response = self
381            .client
382            .get(&url)
383            .send()
384            .await
385            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
386
387        self.handle_response(response).await
388    }
389
390    pub async fn get_priority_fee(
391        &self,
392        project: api::Project,
393        percentile: Option<f64>,
394    ) -> Result<api::GetPriorityFeeResponse> {
395        let mut url = format!(
396            "{}/api/v2/system/priority-fee?project={}",
397            self.base_url, project as i32
398        );
399        if let Some(p) = percentile {
400            url = format!(
401                "{}/api/v2/system/priority-fee?project={}&percentile={}",
402                self.base_url, project as i32, p
403            );
404        }
405
406        let response = self
407            .client
408            .get(&url)
409            .send()
410            .await
411            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
412
413        self.handle_response(response).await
414    }
415
416    pub async fn get_priority_fee_by_program(
417        &self,
418        programs: Vec<String>,
419    ) -> Result<api::GetPriorityFeeByProgramResponse> {
420        let url = format!(
421            "{}/api/v2/system/priority-fee-by-program?programs={}",
422            self.base_url,
423            programs.join("&programs=")
424        );
425
426        let response: reqwest::Response = self
427            .client
428            .get(&url)
429            .send()
430            .await
431            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
432
433        self.handle_response(response).await
434    }
435
436    pub async fn get_token_accounts(
437        &self,
438        owner_address: String,
439    ) -> Result<api::GetTokenAccountsResponse> {
440        let url = format!(
441            "{}/api/v1/account/token-accounts?ownerAddress={}",
442            self.base_url, owner_address
443        );
444
445        let response = self
446            .client
447            .get(&url)
448            .send()
449            .await
450            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
451
452        self.handle_response(response).await
453    }
454
455    pub async fn get_account_balance(
456        &self,
457        owner_address: String,
458    ) -> Result<api::GetAccountBalanceResponse> {
459        let url = format!(
460            "{}/api/v2/balance?ownerAddress={}",
461            self.base_url, owner_address
462        );
463
464        let response = self
465            .client
466            .get(&url)
467            .send()
468            .await
469            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
470
471        self.handle_response(response).await
472    }
473
474    pub async fn get_leader_schedule(
475        &self,
476        max_slots: u64,
477    ) -> Result<api::GetLeaderScheduleResponse> {
478        let url = format!(
479            "{}/api/v2/system/leader-schedule?maxSlots={}",
480            self.base_url, max_slots
481        );
482
483        let response = self
484            .client
485            .get(&url)
486            .send()
487            .await
488            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
489
490        self.handle_response(response).await
491    }
492}