jito_client/
lib.rs

1use anyhow::{Context, anyhow, bail};
2use base64::prelude::*;
3use futures::future::{join_all, select_ok};
4use load_balancer::{LoadBalancer, time::TimeLoadBalancer};
5use reqwest::header::HeaderName;
6use reqwest::{Client, ClientBuilder, Response};
7use serde::{Deserialize, Serialize};
8use serde_json::{Value, json};
9use std::{net::IpAddr, sync::Arc, time::Duration};
10
11pub use get_if_addrs::get_if_addrs;
12pub use load_balancer;
13pub use reqwest;
14pub use reqwest::Proxy;
15pub use reqwest::StatusCode;
16pub use reqwest::header::HeaderMap;
17pub use serde_json;
18
19/// Builder for configuring and creating a `JitoClient`.
20pub struct JitoClientBuilder {
21    url: Vec<String>,
22    broadcast: bool,
23    interval: Duration,
24    timeout: Option<Duration>,
25    proxy: Option<Proxy>,
26    headers: Option<HeaderMap>,
27    ip: Vec<IpAddr>,
28    headers_with_separator: Option<String>,
29    error_read_headers: bool,
30    error_read_body: bool,
31}
32
33impl JitoClientBuilder {
34    /// Creates a new `JitoClientBuilder` with default settings.
35    pub fn new() -> Self {
36        Self {
37            url: vec!["https://mainnet.block-engine.jito.wtf".to_string()],
38            broadcast: false,
39            interval: Duration::ZERO,
40            timeout: None,
41            proxy: None,
42            headers: None,
43            ip: Vec::new(),
44            headers_with_separator: None,
45            error_read_headers: false,
46            error_read_body: false,
47        }
48    }
49
50    /// Sets the target URLs for the client.
51    pub fn url<T: IntoIterator<Item = impl AsRef<str>>>(mut self, url: T) -> Self {
52        self.url = url.into_iter().map(|v| v.as_ref().to_string()).collect();
53        self
54    }
55
56    /// Sets the interval duration between requests (0 = unlimited)
57    /// For example, 5 requests per second = 200 ms interval.
58    pub fn interval(mut self, interval: Duration) -> Self {
59        self.interval = interval;
60        self
61    }
62
63    /// Sets the local IP addresses to bind outgoing requests to.
64    pub fn ip<T: IntoIterator<Item = IpAddr>>(mut self, ip: T) -> Self {
65        self.ip = ip.into_iter().collect();
66        self
67    }
68
69    /// Broadcast each request to all configured URLs.
70    pub fn broadcast(mut self, broadcast: bool) -> Self {
71        self.broadcast = broadcast;
72        self
73    }
74
75    /// Sets a timeout duration for requests.
76    pub fn timeout(mut self, timeout: Duration) -> Self {
77        self.timeout = Some(timeout);
78        self
79    }
80
81    /// Sets a proxy for the client.
82    pub fn proxy(mut self, proxy: Proxy) -> Self {
83        self.proxy = Some(proxy);
84        self
85    }
86
87    /// Sets headers for the client.
88    pub fn headers(mut self, headers: HeaderMap) -> Self {
89        self.headers = Some(headers);
90        self
91    }
92
93    /// Whether to include HTTP response headers in error messages when the request fails.
94    pub fn error_read_headers(mut self, error_read_headers: bool) -> Self {
95        self.error_read_headers = error_read_headers;
96        self
97    }
98
99    /// Whether to include HTTP response body in error messages when the request fails.
100    pub fn error_read_body(mut self, error_read_body: bool) -> Self {
101        self.error_read_body = error_read_body;
102        self
103    }
104
105    /// Sets custom headers encoded in the URL using a separator.
106    ///
107    /// Example: `.headers_with_separator(":").url(&["https://google.com:key1=value1&key2=value2"])`
108    ///
109    /// parses `key1=value1&key2=value2` as headers.
110    pub fn headers_with_separator(mut self, headers_with_separator: impl AsRef<str>) -> Self {
111        self.headers_with_separator = Some(headers_with_separator.as_ref().to_string());
112        self
113    }
114
115    /// Builds the `JitoClient` with the configured options.
116    pub fn build(self) -> anyhow::Result<JitoClient> {
117        let default_ip = self.ip.is_empty();
118
119        let inner = if self.broadcast {
120            let mut entries = Vec::new();
121
122            if default_ip {
123                let mut cb = ClientBuilder::new();
124
125                if let Some(v) = self.timeout {
126                    cb = cb.timeout(v);
127                }
128
129                if let Some(v) = self.proxy {
130                    cb = cb.proxy(v);
131                }
132
133                if let Some(v) = self.headers {
134                    cb = cb.default_headers(v);
135                }
136
137                entries.push((
138                    self.interval,
139                    Arc::new((self.url.clone(), cb.build()?, "127.0.0.1".parse()?)),
140                ));
141            } else {
142                for ip in &self.ip {
143                    let mut cb = ClientBuilder::new();
144
145                    if let Some(v) = self.timeout {
146                        cb = cb.timeout(v);
147                    }
148
149                    if let Some(v) = self.proxy.clone() {
150                        cb = cb.proxy(v);
151                    }
152
153                    if let Some(v) = self.headers.clone() {
154                        cb = cb.default_headers(v);
155                    }
156
157                    cb = cb.local_address(*ip);
158
159                    entries.push((
160                        self.interval,
161                        Arc::new((self.url.clone(), cb.build()?, *ip)),
162                    ));
163                }
164            }
165
166            JitoClientRef {
167                lb: TimeLoadBalancer::new(entries),
168                error_read_body: self.error_read_body,
169                error_read_headers: self.error_read_headers,
170                headers_with_separator: self.headers_with_separator,
171            }
172        } else {
173            let mut entries = Vec::new();
174
175            if default_ip {
176                for url in &self.url {
177                    let mut cb = ClientBuilder::new();
178
179                    if let Some(v) = self.timeout {
180                        cb = cb.timeout(v);
181                    }
182
183                    if let Some(v) = self.proxy.clone() {
184                        cb = cb.proxy(v);
185                    }
186
187                    if let Some(v) = self.headers.clone() {
188                        cb = cb.default_headers(v);
189                    }
190
191                    entries.push((
192                        self.interval,
193                        Arc::new((vec![url.clone()], cb.build()?, "127.0.0.1".parse()?)),
194                    ));
195                }
196            } else {
197                for url in &self.url {
198                    for ip in &self.ip {
199                        let mut cb = ClientBuilder::new();
200
201                        if let Some(v) = self.timeout {
202                            cb = cb.timeout(v);
203                        }
204
205                        if let Some(v) = self.proxy.clone() {
206                            cb = cb.proxy(v);
207                        }
208
209                        if let Some(v) = self.headers.clone() {
210                            cb = cb.default_headers(v);
211                        }
212
213                        cb = cb.local_address(*ip);
214
215                        entries.push((
216                            self.interval,
217                            Arc::new((vec![url.clone()], cb.build()?, *ip)),
218                        ));
219                    }
220                }
221            }
222
223            JitoClientRef {
224                lb: TimeLoadBalancer::new(entries),
225                error_read_body: self.error_read_body,
226                error_read_headers: self.error_read_headers,
227                headers_with_separator: self.headers_with_separator,
228            }
229        };
230
231        Ok(JitoClient {
232            inner: inner.into(),
233        })
234    }
235}
236
237/// (url, client, ip)
238///
239/// If `url.len() > 1`, the request is broadcast.
240pub type Entry = (Vec<String>, Client, IpAddr);
241
242struct JitoClientRef {
243    lb: TimeLoadBalancer<Arc<Entry>>,
244    error_read_body: bool,
245    error_read_headers: bool,
246    headers_with_separator: Option<String>,
247}
248
249/// Jito client for sending transactions and bundles.
250#[derive(Clone)]
251pub struct JitoClient {
252    inner: Arc<JitoClientRef>,
253}
254
255#[derive(Clone)]
256pub struct JitoClientOnce {
257    inner: Arc<JitoClientRef>,
258    entry: Arc<Entry>,
259}
260
261impl JitoClientOnce {
262    async fn handle_response(&self, response: Response) -> anyhow::Result<Response> {
263        if response.status().is_success() {
264            return Ok(response);
265        }
266
267        let status = response.status();
268
269        let headers = if self.inner.error_read_headers {
270            format!("{:#?}", response.headers())
271        } else {
272            String::new()
273        };
274
275        let body = if self.inner.error_read_body {
276            response.text().await.unwrap_or_default()
277        } else {
278            String::new()
279        };
280
281        match (self.inner.error_read_headers, self.inner.error_read_body) {
282            (true, true) => bail!("{}\n{}\n{}", status, headers, body),
283            (true, false) => bail!("{}\n{}", status, headers),
284            (false, true) => bail!("{}\n{}", status, body),
285            (false, false) => bail!("{}", status),
286        }
287    }
288
289    fn split_url<'a>(&'a self, url: &'a str) -> anyhow::Result<(&'a str, HeaderMap)> {
290        let mut headers = HeaderMap::new();
291
292        if let Some(v) = &self.inner.headers_with_separator {
293            if let Some((a, b)) = url.split_once(v) {
294                for (k, v) in form_urlencoded::parse(b.as_bytes()) {
295                    headers.insert(HeaderName::from_bytes(k.as_bytes())?, v.parse()?);
296                }
297
298                return Ok((a, headers));
299            }
300        }
301
302        Ok((url, headers))
303    }
304
305    pub fn entry(&self) -> Arc<Entry> {
306        self.entry.clone()
307    }
308
309    /// Sends a raw request.
310    pub async fn raw_send(&self, body: &Value) -> anyhow::Result<Response> {
311        let (ref url, ref client, ..) = *self.entry;
312
313        if url.len() > 1 {
314            Ok(select_ok(url.iter().map(|v| {
315                Box::pin(async move {
316                    let (url, headers) = self.split_url(v)?;
317                    let response = client.post(url).headers(headers).json(&body).send().await?;
318                    let response = self.handle_response(response).await?;
319
320                    anyhow::Ok(response)
321                })
322            }))
323            .await?
324            .0)
325        } else {
326            let (url, headers) = self.split_url(&url[0])?;
327            let response = client.post(url).headers(headers).json(body).send().await?;
328
329            self.handle_response(response).await
330        }
331    }
332
333    /// Sends a raw request, use base_url + api_url.
334    pub async fn raw_send_api(
335        &self,
336        api_url: impl AsRef<str>,
337        body: &Value,
338    ) -> anyhow::Result<Response> {
339        let (ref url, ref client, ..) = *self.entry;
340        let api_url = api_url.as_ref();
341
342        if url.len() > 1 {
343            Ok(select_ok(url.iter().map(|v| {
344                Box::pin(async move {
345                    let (base, headers) = self.split_url(v)?;
346
347                    anyhow::Ok(
348                        self.handle_response(
349                            client
350                                .post(&format!("{}{}", base, api_url))
351                                .headers(headers)
352                                .json(&body)
353                                .send()
354                                .await?,
355                        )
356                        .await?,
357                    )
358                })
359            }))
360            .await?
361            .0)
362        } else {
363            let (base, headers) = self.split_url(&url[0])?;
364
365            self.handle_response(
366                client
367                    .post(&format!("{}{}", base, api_url))
368                    .headers(headers)
369                    .json(body)
370                    .send()
371                    .await?,
372            )
373            .await
374        }
375    }
376
377    /// Sends a single transaction and returns the HTTP response.
378    pub async fn send_transaction(&self, tx: impl Serialize) -> anyhow::Result<Response> {
379        let data = serialize_tx_checked(tx)?;
380
381        let body = &json!({
382            "id": 1,
383            "jsonrpc": "2.0",
384            "method": "sendTransaction",
385            "params": [
386                data, { "encoding": "base64" }
387            ]
388        });
389
390        let (ref url, ref client, ..) = *self.entry;
391
392        if url.len() > 1 {
393            Ok(select_ok(url.iter().map(|v| {
394                Box::pin(async move {
395                    let (base, headers) = self.split_url(v)?;
396
397                    anyhow::Ok(
398                        self.handle_response(
399                            client
400                                .post(&format!("{}/api/v1/transactions", base))
401                                .headers(headers)
402                                .query(&[("bundleOnly", "true")])
403                                .json(body)
404                                .send()
405                                .await?,
406                        )
407                        .await?,
408                    )
409                })
410            }))
411            .await?
412            .0)
413        } else {
414            let (base, headers) = self.split_url(&url[0])?;
415
416            let response = client
417                .post(&format!("{}/api/v1/transactions", base))
418                .headers(headers)
419                .query(&[("bundleOnly", "true")])
420                .json(body)
421                .send()
422                .await?;
423
424            self.handle_response(response).await
425        }
426    }
427
428    /// Sends a transaction and returns the bundle ID from the response headers.
429    pub async fn send_transaction_bid(&self, tx: impl Serialize) -> anyhow::Result<String> {
430        let response = self.send_transaction(tx).await?;
431
432        Ok(response
433            .headers()
434            .get("x-bundle-id")
435            .ok_or_else(|| anyhow!("missing `x-bundle-id` header"))?
436            .to_str()
437            .map_err(|v| anyhow!("invalid `x-bundle-id` header: {}", v))?
438            .to_string())
439    }
440
441    /// Sends a transaction without `bundleOnly` flag.
442    pub async fn send_transaction_no_bundle_only(
443        &self,
444        tx: impl Serialize,
445    ) -> anyhow::Result<Response> {
446        let data = serialize_tx_checked(tx)?;
447        let body = &json!({
448            "id": 1,
449            "jsonrpc": "2.0",
450            "method": "sendTransaction",
451            "params": [
452                data, { "encoding": "base64" }
453            ]
454        });
455
456        let (ref url, ref client, ..) = *self.entry;
457
458        if url.len() > 1 {
459            Ok(select_ok(url.iter().map(|v| {
460                Box::pin(async move {
461                    let (base, headers) = self.split_url(v)?;
462
463                    anyhow::Ok(
464                        self.handle_response(
465                            client
466                                .post(&format!("{}/api/v1/transactions", base))
467                                .headers(headers)
468                                .json(body)
469                                .send()
470                                .await?,
471                        )
472                        .await?,
473                    )
474                })
475            }))
476            .await?
477            .0)
478        } else {
479            let (base, headers) = self.split_url(&url[0])?;
480
481            self.handle_response(
482                client
483                    .post(&format!("{}/api/v1/transactions", base))
484                    .headers(headers)
485                    .json(body)
486                    .send()
487                    .await?,
488            )
489            .await
490        }
491    }
492
493    /// Sends multiple transactions as a bundle.
494    pub async fn send_bundle<T: IntoIterator<Item = impl Serialize>>(
495        &self,
496        tx: T,
497    ) -> anyhow::Result<Response> {
498        let data = serialize_tx_vec_checked(tx)?;
499
500        let body = &json!({
501            "id": 1,
502            "jsonrpc": "2.0",
503            "method": "sendBundle",
504            "params": [ data, { "encoding": "base64" } ]
505        });
506
507        let (ref url, ref client, ..) = *self.entry;
508
509        if url.len() > 1 {
510            Ok(select_ok(url.iter().map(|v| {
511                Box::pin(async move {
512                    let (base, headers) = self.split_url(v)?;
513
514                    anyhow::Ok(
515                        self.handle_response(
516                            client
517                                .post(&format!("{}/api/v1/bundles", base))
518                                .headers(headers)
519                                .json(body)
520                                .send()
521                                .await?,
522                        )
523                        .await?,
524                    )
525                })
526            }))
527            .await?
528            .0)
529        } else {
530            let (base, headers) = self.split_url(&url[0])?;
531
532            self.handle_response(
533                client
534                    .post(&format!("{}/api/v1/bundles", base))
535                    .headers(headers)
536                    .json(body)
537                    .send()
538                    .await?,
539            )
540            .await
541        }
542    }
543
544    /// Sends a bundle and returns its bundle ID from the JSON response.
545    pub async fn send_bundle_bid<T: IntoIterator<Item = impl Serialize>>(
546        &self,
547        tx: T,
548    ) -> anyhow::Result<String> {
549        self.send_bundle(tx)
550            .await?
551            .error_for_status()?
552            .json::<Value>()
553            .await?["result"]
554            .as_str()
555            .map(|v| v.to_string())
556            .ok_or_else(|| anyhow::anyhow!("missing bundle result"))
557    }
558
559    /// Sends a single transaction with pre-serialized data and returns the HTTP response.
560    pub async fn send_transaction_str(&self, tx: impl AsRef<str>) -> anyhow::Result<Response> {
561        let data = tx.as_ref();
562
563        let body = &json!({
564            "id": 1,
565            "jsonrpc": "2.0",
566            "method": "sendTransaction",
567            "params": [
568                data, { "encoding": "base64" }
569            ]
570        });
571
572        let (ref url, ref client, ..) = *self.entry;
573
574        if url.len() > 1 {
575            Ok(select_ok(url.iter().map(|v| {
576                Box::pin(async move {
577                    let (base, headers) = self.split_url(v)?;
578
579                    anyhow::Ok(
580                        self.handle_response(
581                            client
582                                .post(&format!("{}/api/v1/transactions", base))
583                                .headers(headers)
584                                .query(&[("bundleOnly", "true")])
585                                .json(body)
586                                .send()
587                                .await?,
588                        )
589                        .await?,
590                    )
591                })
592            }))
593            .await?
594            .0)
595        } else {
596            let (base, headers) = self.split_url(&url[0])?;
597
598            let response = client
599                .post(&format!("{}/api/v1/transactions", base))
600                .headers(headers)
601                .query(&[("bundleOnly", "true")])
602                .json(body)
603                .send()
604                .await?;
605
606            self.handle_response(response).await
607        }
608    }
609
610    /// Sends a transaction with pre-serialized data and returns the bundle ID from the response headers.
611    pub async fn send_transaction_bid_str(&self, tx: impl AsRef<str>) -> anyhow::Result<String> {
612        let response = self.send_transaction_str(tx).await?;
613
614        Ok(response
615            .headers()
616            .get("x-bundle-id")
617            .ok_or_else(|| anyhow!("missing `x-bundle-id` header"))?
618            .to_str()
619            .map_err(|v| anyhow!("invalid `x-bundle-id` header: {}", v))?
620            .to_string())
621    }
622
623    /// Sends a transaction with pre-serialized data without `bundleOnly` flag.
624    pub async fn send_transaction_no_bundle_only_str(
625        &self,
626        tx: impl AsRef<str>,
627    ) -> anyhow::Result<Response> {
628        let data = tx.as_ref();
629        let body = &json!({
630            "id": 1,
631            "jsonrpc": "2.0",
632            "method": "sendTransaction",
633            "params": [
634                data, { "encoding": "base64" }
635            ]
636        });
637
638        let (ref url, ref client, ..) = *self.entry;
639
640        if url.len() > 1 {
641            Ok(select_ok(url.iter().map(|v| {
642                Box::pin(async move {
643                    let (base, headers) = self.split_url(v)?;
644
645                    anyhow::Ok(
646                        self.handle_response(
647                            client
648                                .post(&format!("{}/api/v1/transactions", base))
649                                .headers(headers)
650                                .json(body)
651                                .send()
652                                .await?,
653                        )
654                        .await?,
655                    )
656                })
657            }))
658            .await?
659            .0)
660        } else {
661            let (base, headers) = self.split_url(&url[0])?;
662
663            self.handle_response(
664                client
665                    .post(&format!("{}/api/v1/transactions", base))
666                    .headers(headers)
667                    .json(body)
668                    .send()
669                    .await?,
670            )
671            .await
672        }
673    }
674
675    /// Sends multiple pre-serialized transactions as a bundle.
676    pub async fn send_bundle_str<T: IntoIterator<Item = impl AsRef<str>>>(
677        &self,
678        tx: T,
679    ) -> anyhow::Result<Response> {
680        let data = tx
681            .into_iter()
682            .map(|x| x.as_ref().to_string())
683            .collect::<Vec<String>>();
684
685        let body = &json!({
686            "id": 1,
687            "jsonrpc": "2.0",
688            "method": "sendBundle",
689            "params": [ data, { "encoding": "base64" } ]
690        });
691
692        let (ref url, ref client, ..) = *self.entry;
693
694        if url.len() > 1 {
695            Ok(select_ok(url.iter().map(|v| {
696                Box::pin(async move {
697                    let (base, headers) = self.split_url(v)?;
698
699                    anyhow::Ok(
700                        self.handle_response(
701                            client
702                                .post(&format!("{}/api/v1/bundles", base))
703                                .headers(headers)
704                                .json(body)
705                                .send()
706                                .await?,
707                        )
708                        .await?,
709                    )
710                })
711            }))
712            .await?
713            .0)
714        } else {
715            let (base, headers) = self.split_url(&url[0])?;
716
717            self.handle_response(
718                client
719                    .post(&format!("{}/api/v1/bundles", base))
720                    .headers(headers)
721                    .json(body)
722                    .send()
723                    .await?,
724            )
725            .await
726        }
727    }
728
729    /// Sends a bundle with pre-serialized data and returns its bundle ID from the JSON response.
730    pub async fn send_bundle_bid_str<T: IntoIterator<Item = impl AsRef<str>>>(
731        &self,
732        tx: T,
733    ) -> anyhow::Result<String> {
734        self.send_bundle_str(tx)
735            .await?
736            .error_for_status()?
737            .json::<Value>()
738            .await?["result"]
739            .as_str()
740            .map(|v| v.to_string())
741            .ok_or_else(|| anyhow::anyhow!("missing bundle result"))
742    }
743}
744
745impl JitoClient {
746    /// Creates a new client with default settings.
747    pub fn new() -> Self {
748        JitoClientBuilder::new().build().unwrap()
749    }
750
751    // Alloc a new client with Load Balancer.
752    pub async fn alloc(&self) -> JitoClientOnce {
753        JitoClientOnce {
754            inner: self.inner.clone(),
755            entry: self.inner.lb.alloc().await,
756        }
757    }
758
759    // Try alloc a new client with Load Balancer.
760    pub fn try_alloc(&self) -> Option<JitoClientOnce> {
761        Some(JitoClientOnce {
762            inner: self.inner.clone(),
763            entry: self.inner.lb.try_alloc()?,
764        })
765    }
766
767    /// Sends a raw request.
768    pub async fn raw_send(&self, body: &Value) -> anyhow::Result<Response> {
769        self.alloc().await.raw_send(body).await
770    }
771
772    /// Sends a raw request, use base_url + api_url.
773    pub async fn raw_send_api(
774        &self,
775        api_url: impl AsRef<str>,
776        body: &Value,
777    ) -> anyhow::Result<Response> {
778        self.alloc().await.raw_send_api(api_url, body).await
779    }
780
781    /// Sends a single transaction and returns the HTTP response.
782    pub async fn send_transaction(&self, tx: impl Serialize) -> anyhow::Result<Response> {
783        self.alloc().await.send_transaction(tx).await
784    }
785
786    /// Sends a transaction and returns the bundle ID from the response headers.
787    pub async fn send_transaction_bid(&self, tx: impl Serialize) -> anyhow::Result<String> {
788        self.alloc().await.send_transaction_bid(tx).await
789    }
790
791    /// Sends a transaction without `bundleOnly` flag.
792    pub async fn send_transaction_no_bundle_only(
793        &self,
794        tx: impl Serialize,
795    ) -> anyhow::Result<Response> {
796        self.alloc().await.send_transaction_no_bundle_only(tx).await
797    }
798
799    /// Sends multiple transactions as a bundle.
800    pub async fn send_bundle<T: IntoIterator<Item = impl Serialize>>(
801        &self,
802        tx: T,
803    ) -> anyhow::Result<Response> {
804        self.alloc().await.send_bundle(tx).await
805    }
806
807    /// Sends a bundle and returns its bundle ID from the JSON response.
808    pub async fn send_bundle_bid<T: IntoIterator<Item = impl Serialize>>(
809        &self,
810        tx: T,
811    ) -> anyhow::Result<String> {
812        self.alloc().await.send_bundle_bid(tx).await
813    }
814
815    /// Sends a single transaction with pre-serialized data and returns the HTTP response.
816    pub async fn send_transaction_str(&self, tx: impl AsRef<str>) -> anyhow::Result<Response> {
817        self.alloc().await.send_transaction_str(tx).await
818    }
819
820    /// Sends a transaction with pre-serialized data and returns the bundle ID from the response headers.
821    pub async fn send_transaction_bid_str(&self, tx: impl AsRef<str>) -> anyhow::Result<String> {
822        self.alloc().await.send_transaction_bid_str(tx).await
823    }
824
825    /// Sends a transaction with pre-serialized data without `bundleOnly` flag.
826    pub async fn send_transaction_no_bundle_only_str(
827        &self,
828        tx: impl AsRef<str>,
829    ) -> anyhow::Result<Response> {
830        self.alloc()
831            .await
832            .send_transaction_no_bundle_only_str(tx)
833            .await
834    }
835
836    /// Sends multiple pre-serialized transactions as a bundle.
837    pub async fn send_bundle_str<T: IntoIterator<Item = impl AsRef<str>>>(
838        &self,
839        tx: T,
840    ) -> anyhow::Result<Response> {
841        self.alloc().await.send_bundle_str(tx).await
842    }
843
844    /// Sends a bundle with pre-serialized data and returns its bundle ID from the JSON response.
845    pub async fn send_bundle_bid_str<T: IntoIterator<Item = impl AsRef<str>>>(
846        &self,
847        tx: T,
848    ) -> anyhow::Result<String> {
849        self.alloc().await.send_bundle_bid_str(tx).await
850    }
851}
852
853/// Represents Jito tip data.
854#[derive(Debug, Clone, Deserialize)]
855pub struct JitoTip {
856    pub landed_tips_25th_percentile: f64,
857    pub landed_tips_50th_percentile: f64,
858    pub landed_tips_75th_percentile: f64,
859    pub landed_tips_95th_percentile: f64,
860    pub landed_tips_99th_percentile: f64,
861    pub ema_landed_tips_50th_percentile: f64,
862}
863
864/// Fetches the current Jito tip from the public API.
865pub async fn get_jito_tip(client: Client) -> anyhow::Result<JitoTip> {
866    Ok(client
867        .get("https://bundles.jito.wtf/api/v1/bundles/tip_floor")
868        .send()
869        .await?
870        .json::<Vec<JitoTip>>()
871        .await?
872        .get(0)
873        .context("get_jito_tip: empty response")?
874        .clone())
875}
876
877/// Represents the result of querying bundle statuses.
878#[derive(Debug, Deserialize)]
879pub struct BundleResult {
880    pub context: Value,
881    pub value: Option<Vec<BundleStatus>>,
882}
883
884#[derive(Debug, Deserialize)]
885pub struct BundleStatus {
886    pub bundle_id: String,
887    pub transactions: Option<Vec<String>>,
888    pub slot: Option<u64>,
889    pub confirmation_status: Option<String>,
890    pub err: Option<Value>,
891}
892
893/// Fetches statuses of multiple bundles.
894pub async fn get_bundle_statuses<T: IntoIterator<Item = impl AsRef<str>>>(
895    client: Client,
896    bundle: T,
897) -> anyhow::Result<BundleResult> {
898    #[derive(Debug, Deserialize)]
899    struct RpcResponse {
900        result: BundleResult,
901    }
902
903    let payload = json!({
904        "jsonrpc": "2.0",
905        "id": 1,
906        "method": "getBundleStatuses",
907        "params": [bundle.into_iter().map(|v| v.as_ref().to_string()).collect::<Vec<_>>()],
908    });
909
910    Ok(client
911        .post("https://mainnet.block-engine.jito.wtf/api/v1/getBundleStatuses")
912        .json(&payload)
913        .send()
914        .await?
915        .json::<RpcResponse>()
916        .await?
917        .result)
918}
919
920/// Represents in-flight bundle status.
921#[derive(Debug, Deserialize)]
922pub struct InflightBundleStatus {
923    pub bundle_id: String,
924    pub status: String,
925    pub landed_slot: Option<u64>,
926}
927
928#[derive(Debug, Deserialize)]
929pub struct InflightBundleResult {
930    pub context: Value,
931    pub value: Option<Vec<InflightBundleStatus>>,
932}
933
934/// Fetches statuses of in-flight bundles.
935pub async fn get_inflight_bundle_statuses<T: IntoIterator<Item = impl AsRef<str>>>(
936    client: Client,
937    bundle: T,
938) -> anyhow::Result<InflightBundleResult> {
939    #[derive(Debug, Deserialize)]
940    struct InflightRpcResponse {
941        result: InflightBundleResult,
942    }
943
944    let payload = json!({
945        "jsonrpc": "2.0",
946        "id": 1,
947        "method": "getInflightBundleStatuses",
948        "params": [bundle.into_iter().map(|v| v.as_ref().to_string()).collect::<Vec<_>>()],
949    });
950
951    Ok(client
952        .post("https://mainnet.block-engine.jito.wtf/api/v1/getInflightBundleStatuses")
953        .json(&payload)
954        .send()
955        .await?
956        .json::<InflightRpcResponse>()
957        .await?
958        .result)
959}
960
961/// Get all non-loopback IP addresses of the machine.
962pub fn get_ip_list() -> anyhow::Result<Vec<IpAddr>> {
963    Ok(get_if_addrs()?
964        .into_iter()
965        .filter(|v| !v.is_loopback())
966        .map(|v| v.ip())
967        .collect::<Vec<_>>())
968}
969
970/// Get all non-loopback IPv4 addresses of the machine.
971pub fn get_ipv4_list() -> anyhow::Result<Vec<IpAddr>> {
972    Ok(get_if_addrs()?
973        .into_iter()
974        .filter(|v| !v.is_loopback() && v.ip().is_ipv4())
975        .map(|v| v.ip())
976        .collect::<Vec<_>>())
977}
978
979/// Get all non-loopback IPv6 addresses of the machine.
980pub fn get_ipv6_list() -> anyhow::Result<Vec<IpAddr>> {
981    Ok(get_if_addrs()?
982        .into_iter()
983        .filter(|v| !v.is_loopback() && v.ip().is_ipv6())
984        .map(|v| v.ip())
985        .collect::<Vec<_>>())
986}
987
988pub async fn test_ip(ip: IpAddr) -> anyhow::Result<IpAddr> {
989    reqwest::ClientBuilder::new()
990        .timeout(Duration::from_secs(3))
991        .local_address(ip)
992        .build()?
993        .get("https://apple.com")
994        .send()
995        .await?;
996
997    Ok(ip)
998}
999
1000pub async fn test_all_ip() -> Vec<anyhow::Result<IpAddr>> {
1001    match get_ip_list() {
1002        Ok(v) => {
1003            join_all(
1004                v.into_iter()
1005                    .map(|v| async move { test_ip(v).await.context(v) }),
1006            )
1007            .await
1008        }
1009        Err(_) => Vec::new(),
1010    }
1011}
1012
1013pub async fn test_all_ipv4() -> Vec<anyhow::Result<IpAddr>> {
1014    match get_ipv4_list() {
1015        Ok(v) => {
1016            join_all(
1017                v.into_iter()
1018                    .map(|v| async move { test_ip(v).await.context(v) }),
1019            )
1020            .await
1021        }
1022        Err(_) => Vec::new(),
1023    }
1024}
1025
1026pub async fn test_all_ipv6() -> Vec<anyhow::Result<IpAddr>> {
1027    match get_ipv6_list() {
1028        Ok(v) => {
1029            join_all(
1030                v.into_iter()
1031                    .map(|v| async move { test_ip(v).await.context(v) }),
1032            )
1033            .await
1034        }
1035        Err(_) => Vec::new(),
1036    }
1037}
1038
1039pub fn serialize_tx(tx: impl Serialize) -> anyhow::Result<String> {
1040    Ok(BASE64_STANDARD.encode(bincode::serialize(&tx)?))
1041}
1042
1043pub fn serialize_tx_vec<T: IntoIterator<Item = impl Serialize>>(
1044    tx: T,
1045) -> anyhow::Result<Vec<String>> {
1046    tx.into_iter()
1047        .map(|tx| Ok(BASE64_STANDARD.encode(bincode::serialize(&tx)?)))
1048        .collect::<anyhow::Result<Vec<_>>>()
1049}
1050
1051pub fn serialize_tx_checked(tx: impl Serialize) -> anyhow::Result<String> {
1052    let data = bincode::serialize(&tx)?;
1053
1054    anyhow::ensure!(data.len() <= 1232);
1055
1056    Ok(BASE64_STANDARD.encode(data))
1057}
1058
1059pub fn serialize_tx_vec_checked<T: IntoIterator<Item = impl Serialize>>(
1060    tx: T,
1061) -> anyhow::Result<Vec<String>> {
1062    let mut result = Vec::new();
1063
1064    for i in tx.into_iter() {
1065        let data = bincode::serialize(&i)?;
1066
1067        anyhow::ensure!(data.len() <= 1232);
1068
1069        result.push(BASE64_STANDARD.encode(data));
1070    }
1071
1072    Ok(result)
1073}