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, PostSubmitPaladinRequest, GetRecentBlockHashResponseV2};
13use crate::provider::utils::timestamp_rfc3339;
14
15use crate::{
16    common::{
17        get_base_url_from_env, http_endpoint, is_submit_only_endpoint,
18        signing::{sign_transaction, SubmitParams},
19        BaseConfig,
20    },
21    provider::utils::convert_string_enums,
22};
23
24use super::utils::IntoTransactionMessage;
25
26pub struct HTTPClient {
27    client: Client,
28    base_url: String,
29    keypair: Option<Keypair>,
30    pub public_key: Option<Pubkey>,
31}
32
33impl HTTPClient {
34    pub fn get_keypair(&self) -> Result<&Keypair> {
35        Ok(self.keypair.as_ref().unwrap())
36    }
37
38    pub fn new(endpoint: Option<String>) -> Result<Self> {
39        let base = BaseConfig::try_from_env()?;
40        let (default_base_url, secure) = get_base_url_from_env();
41        let final_base_url = endpoint.unwrap_or(default_base_url);
42        let endpoint = http_endpoint(&final_base_url, secure);
43
44        is_submit_only_endpoint(&final_base_url);
45
46        let headers = Self::build_headers(&base.auth_header)?;
47        let client = Client::builder()
48            .default_headers(headers)
49            .build()
50            .map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;
51
52        Ok(Self {
53            client,
54            base_url: endpoint,
55            keypair: base.keypair,
56            public_key: base.public_key,
57        })
58    }
59
60    fn build_headers(auth_header: &str) -> Result<HeaderMap> {
61        let mut headers = HeaderMap::new();
62        headers.insert(
63            "Authorization",
64            HeaderValue::from_str(auth_header)
65                .map_err(|e| anyhow!("Invalid auth header: {}", e))?,
66        );
67        headers.insert("x-sdk", HeaderValue::from_static("rust-client"));
68        headers.insert(
69            "x-sdk-version",
70            HeaderValue::from_static(env!("CARGO_PKG_VERSION")),
71        );
72        Ok(headers)
73    }
74
75    async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
76        println!("Response: {:?}", response);
77        if !response.status().is_success() {
78            let error_text = response
79                .text()
80                .await
81                .unwrap_or_else(|_| "Failed to read error response".into());
82            return Err(anyhow::anyhow!("HTTP request failed: {}", error_text));
83        }
84
85        let res = response.text().await?;
86
87        let mut value = serde_json::from_str(&res)
88            .map_err(|e| anyhow::anyhow!("Failed to parse response as JSON: {}", e))?;
89
90        convert_string_enums(&mut value);
91
92        serde_json::from_value(value)
93            .map_err(|e| anyhow::anyhow!("Failed to parse response into desired type: {}", e))
94    }
95
96    pub async fn sign_and_submit<T: IntoTransactionMessage + Clone>(
97        &self,
98        txs: Vec<T>,
99        submit_opts: SubmitParams,
100        use_bundle: bool,
101    ) -> Result<Vec<String>> {
102        let keypair = self.get_keypair()?;
103
104        // TODO: refactor once this endpoint is defined
105        let response = self
106            .client
107            .get(format!(
108                "{}/api/v2/system/blockhash?offset={}",
109                self.base_url, 0
110            ))
111            .send()
112            .await?;
113
114        let res: GetRecentBlockHashResponseV2 = self.handle_response(response).await?;
115
116        if txs.len() == 1 {
117            let signed_tx = sign_transaction(&txs[0], keypair, res.block_hash).await?;
118
119            let request_json = json!({
120                "transaction": { "content": signed_tx.content, "isCleanup": signed_tx.is_cleanup },
121                "skipPreFlight": submit_opts.skip_pre_flight,
122                "frontRunningProtection": submit_opts.front_running_protection,
123                "useStakedRPCs": submit_opts.use_staked_rpcs,
124                "fastBestEffort": submit_opts.fast_best_effort
125            });
126
127            let response = self
128                .client
129                .post(format!("{}/api/v2/submit", self.base_url))
130                .json(&request_json)
131                .send()
132                .await?;
133
134            let result: serde_json::Value = self.handle_response(response).await?;
135            return Ok(vec![result
136                .get("signature")
137                .and_then(|s| s.as_str())
138                .map(String::from)
139                .ok_or_else(|| anyhow!("Missing signature in response"))?]);
140        }
141
142        let mut entries = Vec::with_capacity(txs.len());
143        for tx in txs {
144            let signed_tx = sign_transaction(&tx, keypair, res.block_hash.clone()).await?;
145            entries.push(json!({
146                "transaction": {
147                    "content": signed_tx.content,
148                    "isCleanup": signed_tx.is_cleanup
149                },
150                "skipPreFlight": submit_opts.skip_pre_flight,
151                "frontRunningProtection": submit_opts.front_running_protection,
152                "useStakedRPCs": submit_opts.use_staked_rpcs,
153                "fastBestEffort": submit_opts.fast_best_effort
154            }));
155        }
156
157        let request_json = json!({
158            "entries": entries,
159            "useBundle": use_bundle,
160            "submitStrategy": submit_opts.submit_strategy
161        });
162
163        let response = self
164            .client
165            .post(format!("{}/api/v2/submit/batch", self.base_url))
166            .json(&request_json)
167            .send()
168            .await?;
169
170        let result: serde_json::Value = self.handle_response(response).await?;
171
172        let signatures = result["transactions"]
173            .as_array()
174            .ok_or_else(|| anyhow!("Invalid response format"))?
175            .iter()
176            .filter(|entry| entry["submitted"].as_bool().unwrap_or(false))
177            .filter_map(|entry| entry["signature"].as_str().map(String::from))
178            .collect();
179
180        Ok(signatures)
181    }
182
183    pub async fn post_submit_batch(
184        &self,
185        entries: Vec<api::PostSubmitRequestEntry>,
186        submit_strategy: api::SubmitStrategy,
187        use_bundle: Option<bool>,
188        front_running_protection: Option<bool>
189    ) -> anyhow::Result<api::PostSubmitBatchResponse> {
190        let url = format!("{}/api/v1/trade/submit-batch", self.base_url);
191        println!("{}", url);
192        
193        let request_json = json!({
194            "entries": entries,
195            "submitStrategy": submit_strategy,
196            "useBundle": use_bundle,
197            "frontRunningProtection": front_running_protection,
198            "timestamp": timestamp_rfc3339()
199        });
200        
201        let response = self
202            .client
203            .post(&url)
204            .json(&request_json)
205            .send()
206            .await?;
207            
208        let result: api::PostSubmitBatchResponse = self.handle_response(response).await?;
209        
210        Ok(result)
211    }
212
213    pub async fn post_submit_batch_v2(
214        &self,
215        entries: Vec<api::PostSubmitRequestEntry>,
216        submit_strategy: api::SubmitStrategy,
217        use_bundle: Option<bool>,
218        front_running_protection: Option<bool>
219    ) -> anyhow::Result<api::PostSubmitBatchResponse> {
220        let url = format!("{}/api/v2/submit-batch", self.base_url);
221        println!("{}", url);
222        
223        let request_json = json!({
224            "entries": entries,
225            "submitStrategy": submit_strategy,
226            "useBundle": use_bundle,
227            "frontRunningProtection": front_running_protection,
228            "timestamp": timestamp_rfc3339()
229        });
230        
231        let response = self
232            .client
233            .post(&url)
234            .json(&request_json)
235            .send()
236            .await?;
237            
238        let result: api::PostSubmitBatchResponse = self.handle_response(response).await?;
239        
240        Ok(result)
241    }
242
243    pub async fn post_submit_paladin_v2(
244        &self,
245        request: &PostSubmitPaladinRequest
246    ) -> anyhow::Result<api::PostSubmitResponse> {
247        let url = format!("{}/api/v2/submit-paladin", self.base_url);
248        println!("{}", url);
249        
250        let request_json = json!({
251            "transaction": request.transaction,
252            "revertProtection": request.revert_protection,
253            "timestamp": timestamp_rfc3339()
254        });
255        
256        let response = self
257            .client
258            .post(&url)
259            .json(&request_json)
260            .send()
261            .await?;
262            
263        let result: api::PostSubmitResponse = self.handle_response(response).await?;
264        
265        Ok(result)
266    }
267
268    pub async fn sign_and_submit_snipe<T: IntoTransactionMessage + Clone>(
269        &self,
270        txs: Vec<T>,
271        use_staked_rpcs: bool,
272    ) -> Result<Vec<String>> {
273        let keypair = self.get_keypair()?;
274
275        // Get recent blockhash
276        let response = self
277            .client
278            .get(format!(
279                "{}/api/v2/system/blockhash?offset={}",
280                self.base_url, 0
281            ))
282            .send()
283            .await?;
284
285        let res: GetRecentBlockHashResponseV2 = self.handle_response(response).await?;
286
287        // Build entries for each transaction
288        let mut entries = Vec::with_capacity(txs.len());
289        for tx in txs {
290            let signed_tx = sign_transaction(&tx, keypair, res.block_hash.clone()).await?;
291            entries.push(json!({
292                "transaction": {
293                    "content": signed_tx.content,
294                    "isCleanup": signed_tx.is_cleanup
295                },
296                "skipPreFlight": false
297            }));
298        }
299
300        let request_json = json!({
301            "entries": entries,
302            "useStakedRPCs": use_staked_rpcs,
303            "timestamp": timestamp_rfc3339()
304        });
305
306        let response = self
307            .client
308            .post(format!("{}/api/v2/submit-snipe", self.base_url))
309            .json(&request_json)
310            .send()
311            .await?;
312
313        let result: serde_json::Value = self.handle_response(response).await?;
314
315        let signatures = result["transactions"]
316            .as_array()
317            .ok_or_else(|| anyhow!("Invalid response format"))?
318            .iter()
319            .filter(|entry| entry["submitted"].as_bool().unwrap_or(false))
320            .filter_map(|entry| entry["signature"].as_str().map(String::from))
321            .collect();
322
323        Ok(signatures)
324    }
325
326    pub async fn post_submit_snipe_v2(
327        &self,
328        entries: Vec<api::PostSubmitRequestEntry>,
329        use_staked_rpcs: Option<bool>
330    ) -> anyhow::Result<api::PostSubmitSnipeResponse> {
331        let url = format!("{}/api/v2/submit-snipe", self.base_url);
332        println!("{}", url);
333        
334        let request_json = json!({
335            "entries": entries,
336            "useStakedRPCs": use_staked_rpcs,
337            "timestamp": timestamp_rfc3339()
338        });
339        
340        let response = self
341            .client
342            .post(&url)
343            .json(&request_json)
344            .send()
345            .await?;
346            
347        let result: api::PostSubmitSnipeResponse = self.handle_response(response).await?;
348        
349        Ok(result)
350    }
351    
352    pub async fn post_submit_v2(
353        &self,
354        transaction: api::TransactionMessage,
355        skip_pre_flight: bool,
356        front_running_protection: Option<bool>,
357        tip: Option<u64>,
358        use_staked_rpcs: Option<bool>,
359        fast_best_effort: Option<bool>,
360        allow_back_run: Option<bool>,
361        revenue_address: Option<String>,
362        sniping: Option<bool>
363    ) -> anyhow::Result<api::PostSubmitResponse> {
364        let url = format!("{}/api/v2/submit", self.base_url);
365        println!("{}", url);
366        
367        let request_json = json!({
368            "transaction": transaction,
369            "skipPreFlight": skip_pre_flight,
370            "frontRunningProtection": front_running_protection,
371            "tip": tip,
372            "useStakedRPCs": use_staked_rpcs,
373            "fastBestEffort": fast_best_effort,
374            "allowBackRun": allow_back_run,
375            "revenueAddress": revenue_address,
376            "sniping": sniping,
377            "timestamp": timestamp_rfc3339()
378        });
379        
380        let response = self
381            .client
382            .post(&url)
383            .json(&request_json)
384            .send()
385            .await?;
386            
387        let result: api::PostSubmitResponse = self.handle_response(response).await?;
388        
389        Ok(result)
390    }
391
392    pub async fn sign_and_submit_paladin<T: IntoTransactionMessage + Clone>(
393        &self,
394        tx: T,
395        revert_protection: bool,
396    ) -> Result<String> {
397        let response = self
398            .client
399            .get(format!(
400                "{}/api/v2/system/blockhash?offset={}",
401                self.base_url, 0
402            ))
403            .send()
404            .await?;
405
406        let res: GetRecentBlockHashResponseV2 = self.handle_response(response).await?;
407        let keypair = self.get_keypair()?;
408        let signed_tx = sign_transaction(&tx, keypair, res.block_hash).await?;
409
410        let request_json = json!({
411            "transaction": {
412                "content": signed_tx.content
413            },
414            "revertProtection": revert_protection
415        });
416
417        let response = self
418            .client
419            .post(format!("{}/api/v2/submit-paladin", self.base_url))
420            .json(&request_json)
421            .send()
422            .await?;
423
424        let result: serde_json::Value = self.handle_response(response).await?;
425        let signature = result
426            .get("signature")
427            .and_then(|s| s.as_str())
428            .map(String::from)
429            .ok_or_else(|| anyhow!("Missing signature in response"))?;
430
431        Ok(signature)
432    }
433
434    pub async fn get_transaction(
435        &self,
436        request: &api::GetTransactionRequest,
437    ) -> anyhow::Result<api::GetTransactionResponse> {
438        let url = format!(
439            "{}/api/v2/transaction?signature={}",
440            self.base_url, request.signature
441        );
442
443        println!("{}", url);
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        let response_text = response.text().await?;
453
454        println!("{}", response_text);
455
456        // let mut value: serde_json::Value = serde_json::from_str(&response_text)
457        //     .map_err(|e| anyhow::anyhow!("Failed to parse response as JSON: {}", e))?;
458        //
459        // convert_string_enums(&mut value);
460        //
461        // serde_json::from_value(value)
462        //     .map_err(|e| anyhow::anyhow!("Failed to parse response into GetTransactionResponse: {}", e))
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_recent_block_hash(&self) -> anyhow::Result<api::GetRecentBlockHashResponse> {
475        let url = format!("{}/api/v1/system/blockhash", self.base_url);
476
477        println!("{}", url);
478
479        let response = self
480            .client
481            .get(&url)
482            .send()
483            .await
484            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
485
486        self.handle_response(response).await
487    }
488
489    pub async fn get_server_time(&self) -> anyhow::Result<api::GetServerTimeResponse> {
490        let url = format!("{}/api/v1/system/time", self.base_url);
491
492        println!("{}", url);
493
494        let response = self
495            .client
496            .get(&url)
497            .send()
498            .await
499            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
500
501        self.handle_response(response).await
502    }
503
504    pub async fn post_submit(
505        &self, 
506        transaction: api::TransactionMessage,
507        skip_pre_flight: bool,
508        front_running_protection: Option<bool>,
509        tip: Option<u64>,
510        use_staked_rpcs: Option<bool>,
511        fast_best_effort: Option<bool>,
512        allow_back_run: Option<bool>,
513        revenue_address: Option<String>,
514        sniping: Option<bool>
515    ) -> anyhow::Result<api::PostSubmitResponse> {
516        let url = format!("{}/api/v1/trade/submit", self.base_url);
517        println!("{}", url);
518        
519        let request_json = json!({
520            "transaction": transaction,
521            "skipPreFlight": skip_pre_flight,
522            "frontRunningProtection": front_running_protection,
523            "tip": tip,
524            "useStakedRPCs": use_staked_rpcs,
525            "fastBestEffort": fast_best_effort,
526            "allowBackRun": allow_back_run,
527            "revenueAddress": revenue_address,
528            "sniping": sniping,
529            "timestamp": timestamp_rfc3339()
530        });
531        
532        let response = self
533            .client
534            .post(format!("{}/api/v1/trade/submit", self.base_url))
535            .json(&request_json)
536            .send()
537            .await?;
538            
539        let result: api::PostSubmitResponse = self.handle_response(response).await?;
540        
541        Ok(result)
542    }
543
544    pub async fn get_recent_block_hash_v2(
545        &self,
546        request: &api::GetRecentBlockHashRequestV2,
547    ) -> anyhow::Result<api::GetRecentBlockHashResponseV2> {
548        let url = format!(
549            "{}/api/v2/system/blockhash?offset={}",
550            self.base_url, request.offset
551        );
552
553        println!("{}", url);
554
555        let response = self
556            .client
557            .get(&url)
558            .send()
559            .await
560            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
561
562        self.handle_response(response).await
563    }
564
565    pub async fn get_rate_limit(&self) -> anyhow::Result<api::GetRateLimitResponse> {
566        let url = format!("{}/api/v2/rate-limit", self.base_url);
567
568        println!("{}", url);
569
570        let response = self
571            .client
572            .get(&url)
573            .send()
574            .await
575            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
576
577        self.handle_response(response).await
578    }
579
580    pub async fn get_account_balance_v2(
581        &self,
582        request: api::GetAccountBalanceRequest,
583    ) -> anyhow::Result<api::GetAccountBalanceResponse> {
584        println!("here1");
585
586        let url = format!(
587            "{}/api/v2/balance?ownerAddress={}",
588            self.base_url, request.owner_address
589        );
590
591        let response = self
592            .client
593            .get(&url)
594            .send()
595            .await
596            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
597
598        self.handle_response(response).await
599    }
600
601    pub async fn get_priority_fee(
602        &self,
603        project: api::Project,
604        percentile: Option<f64>,
605    ) -> Result<api::GetPriorityFeeResponse> {
606        let mut url = format!(
607            "{}/api/v2/system/priority-fee?project={}",
608            self.base_url, project as i32
609        );
610        if let Some(p) = percentile {
611            url = format!(
612                "{}/api/v2/system/priority-fee?project={}&percentile={}",
613                self.base_url, project as i32, p
614            );
615        }
616
617        let response = self
618            .client
619            .get(&url)
620            .send()
621            .await
622            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
623
624        self.handle_response(response).await
625    }
626
627    pub async fn get_priority_fee_by_program(
628        &self,
629        programs: Vec<String>,
630    ) -> Result<api::GetPriorityFeeByProgramResponse> {
631        let url = format!(
632            "{}/api/v2/system/priority-fee-by-program?programs={}",
633            self.base_url,
634            programs.join("&programs=")
635        );
636
637        let response: reqwest::Response = self
638            .client
639            .get(&url)
640            .send()
641            .await
642            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
643
644        self.handle_response(response).await
645    }
646
647    pub async fn get_token_accounts(
648        &self,
649        owner_address: String,
650    ) -> Result<api::GetTokenAccountsResponse> {
651        let url = format!(
652            "{}/api/v1/account/token-accounts?ownerAddress={}",
653            self.base_url, owner_address
654        );
655
656        let response = self
657            .client
658            .get(&url)
659            .send()
660            .await
661            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
662
663        self.handle_response(response).await
664    }
665
666    pub async fn get_account_balance(
667        &self,
668        owner_address: String,
669    ) -> Result<api::GetAccountBalanceResponse> {
670        let url = format!(
671            "{}/api/v2/balance?ownerAddress={}",
672            self.base_url, owner_address
673        );
674
675        let response = self
676            .client
677            .get(&url)
678            .send()
679            .await
680            .map_err(|e| anyhow!("HTTP GET request failed: {}", e))?;
681
682        self.handle_response(response).await
683    }
684}