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 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 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 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 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}