1use anyhow::{Context, anyhow};
2use base64::prelude::*;
3use load_balancer::{LoadBalancer, interval::IntervalLoadBalancer};
4use reqwest::{Client, ClientBuilder, Response};
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7use std::{net::IpAddr, sync::Arc, time::Duration};
8
9pub use load_balancer;
10pub use load_balancer::get_if_addrs;
11pub use load_balancer::ip::{get_ip_list, get_ipv4_list, get_ipv6_list};
12pub use reqwest;
13pub use reqwest::Proxy;
14pub use reqwest::header::HeaderMap;
15pub use serde_json;
16
17pub struct JitoClientBuilder {
19 url: Vec<String>,
20 broadcast: bool,
21 interval: Duration,
22 timeout: Option<Duration>,
23 proxy: Option<Proxy>,
24 headers: Option<HeaderMap>,
25 ip: Vec<IpAddr>,
26}
27
28impl JitoClientBuilder {
29 pub fn new() -> Self {
31 Self {
32 url: vec!["https://mainnet.block-engine.jito.wtf".to_string()],
33 broadcast: false,
34 interval: Duration::ZERO,
35 timeout: None,
36 proxy: None,
37 headers: None,
38 ip: Vec::new(),
39 }
40 }
41
42 pub fn url<T: IntoIterator<Item = impl AsRef<str>>>(mut self, url: T) -> Self {
44 self.url = url.into_iter().map(|v| v.as_ref().to_string()).collect();
45 self
46 }
47
48 pub fn interval(mut self, interval: Duration) -> Self {
51 self.interval = interval;
52 self
53 }
54
55 pub fn ip(mut self, ip: Vec<IpAddr>) -> Self {
57 self.ip = ip;
58 self
59 }
60
61 pub fn broadcast(mut self, broadcast: bool) -> Self {
63 self.broadcast = broadcast;
64 self
65 }
66
67 pub fn timeout(mut self, timeout: Duration) -> Self {
69 self.timeout = Some(timeout);
70 self
71 }
72
73 pub fn proxy(mut self, proxy: Proxy) -> Self {
75 self.proxy = Some(proxy);
76 self
77 }
78
79 pub fn headers(mut self, headers: HeaderMap) -> Self {
81 self.headers = Some(headers);
82 self
83 }
84
85 pub fn build(self) -> anyhow::Result<JitoClient> {
87 let default_ip = self.ip.is_empty();
88
89 let inner = if self.broadcast {
90 let mut entries = Vec::new();
91
92 if default_ip {
93 let mut cb = ClientBuilder::new();
94
95 if let Some(v) = self.timeout {
96 cb = cb.timeout(v);
97 }
98
99 if let Some(v) = self.proxy {
100 cb = cb.proxy(v);
101 }
102
103 if let Some(v) = self.headers {
104 cb = cb.default_headers(v);
105 }
106
107 entries.push((self.interval, Arc::new((self.url.clone(), cb.build()?))));
108 } else {
109 for ip in &self.ip {
110 let mut cb = ClientBuilder::new();
111
112 if let Some(v) = self.timeout {
113 cb = cb.timeout(v);
114 }
115
116 if let Some(v) = self.proxy.clone() {
117 cb = cb.proxy(v);
118 }
119
120 if let Some(v) = self.headers.clone() {
121 cb = cb.default_headers(v);
122 }
123
124 cb = cb.local_address(*ip);
125
126 entries.push((self.interval, Arc::new((self.url.clone(), cb.build()?))));
127 }
128 }
129
130 JitoClientRef {
131 broadcast: true,
132 lb: IntervalLoadBalancer::new(entries),
133 }
134 } else {
135 let mut entries = Vec::new();
136
137 if default_ip {
138 for url in &self.url {
139 let mut cb = ClientBuilder::new();
140
141 if let Some(v) = self.timeout {
142 cb = cb.timeout(v);
143 }
144
145 if let Some(v) = self.proxy.clone() {
146 cb = cb.proxy(v);
147 }
148
149 if let Some(v) = self.headers.clone() {
150 cb = cb.default_headers(v);
151 }
152
153 entries.push((self.interval, Arc::new((vec![url.clone()], cb.build()?))));
154 }
155 } else {
156 for url in &self.url {
157 for ip in &self.ip {
158 let mut cb = ClientBuilder::new();
159
160 if let Some(v) = self.timeout {
161 cb = cb.timeout(v);
162 }
163
164 if let Some(v) = self.proxy.clone() {
165 cb = cb.proxy(v);
166 }
167
168 if let Some(v) = self.headers.clone() {
169 cb = cb.default_headers(v);
170 }
171
172 cb = cb.local_address(*ip);
173
174 entries.push((self.interval, Arc::new((vec![url.clone()], cb.build()?))));
175 }
176 }
177 }
178
179 JitoClientRef {
180 broadcast: false,
181 lb: IntervalLoadBalancer::new(entries),
182 }
183 };
184
185 Ok(JitoClient {
186 inner: inner.into(),
187 })
188 }
189}
190
191struct JitoClientRef {
192 broadcast: bool,
193 lb: IntervalLoadBalancer<Arc<(Vec<String>, Client)>>,
194}
195
196#[derive(Clone)]
198pub struct JitoClient {
199 inner: Arc<JitoClientRef>,
200}
201
202impl JitoClient {
203 pub fn new() -> Self {
205 JitoClientBuilder::new().build().unwrap()
206 }
207
208 pub async fn raw_send(&mut self, body: &serde_json::Value) -> anyhow::Result<Response> {
210 let (ref url, ref client) = *self.inner.lb.alloc().await;
211
212 if self.inner.broadcast {
213 Ok(
214 futures::future::select_ok(url.iter().map(|v| client.post(v).json(body).send()))
215 .await?
216 .0,
217 )
218 } else {
219 Ok(client.post(&url[0]).json(body).send().await?)
220 }
221 }
222
223 pub async fn raw_send_api(
225 &mut self,
226 api_url: impl AsRef<str>,
227 body: &serde_json::Value,
228 ) -> anyhow::Result<Response> {
229 let (ref url, ref client) = *self.inner.lb.alloc().await;
230
231 if self.inner.broadcast {
232 Ok(futures::future::select_ok(url.iter().map(|v| {
233 client
234 .post(&format!("{}{}", v, api_url.as_ref()))
235 .json(body)
236 .send()
237 }))
238 .await?
239 .0)
240 } else {
241 Ok(client
242 .post(&format!("{}{}", url[0], api_url.as_ref()))
243 .json(body)
244 .send()
245 .await?)
246 }
247 }
248
249 pub async fn send_transaction(&self, tx: impl Serialize) -> anyhow::Result<Response> {
251 let data = BASE64_STANDARD.encode(bincode::serialize(&tx)?);
252 let body = json!({
253 "id": 1,
254 "jsonrpc": "2.0",
255 "method": "sendTransaction",
256 "params": [
257 data, { "encoding": "base64" }
258 ]
259 });
260
261 let (ref url, ref client) = *self.inner.lb.alloc().await;
262
263 if self.inner.broadcast {
264 Ok(futures::future::select_ok(url.iter().map(|v| {
265 client
266 .post(&format!("{}/api/v1/transactions", v))
267 .query(&["bundleOnly", "true"])
268 .json(&body)
269 .send()
270 }))
271 .await?
272 .0)
273 } else {
274 Ok(client
275 .post(&format!("{}/api/v1/transactions", url[0]))
276 .query(&["bundleOnly", "true"])
277 .json(&body)
278 .send()
279 .await?)
280 }
281 }
282
283 pub async fn send_transaction_bid(&self, tx: impl Serialize) -> anyhow::Result<String> {
285 Ok(self
286 .send_transaction(tx)
287 .await?
288 .error_for_status()?
289 .headers()
290 .get("x-bundle-id")
291 .ok_or_else(|| anyhow!("missing `x-bundle-id` header"))?
292 .to_str()
293 .map_err(|v| anyhow!("invalid `x-bundle-id` header: {}", v))?
294 .to_string())
295 }
296
297 pub async fn send_transaction_no_bundle_only(
299 &self,
300 tx: impl Serialize,
301 ) -> anyhow::Result<Response> {
302 let data = BASE64_STANDARD.encode(bincode::serialize(&tx)?);
303 let body = json!({
304 "id": 1,
305 "jsonrpc": "2.0",
306 "method": "sendTransaction",
307 "params": [
308 data, { "encoding": "base64" }
309 ]
310 });
311
312 let (ref url, ref client) = *self.inner.lb.alloc().await;
313
314 if self.inner.broadcast {
315 Ok(futures::future::select_ok(url.iter().map(|v| {
316 client
317 .post(&format!("{}/api/v1/transactions", v))
318 .json(&body)
319 .send()
320 }))
321 .await?
322 .0)
323 } else {
324 Ok(client
325 .post(&format!("{}/api/v1/transactions", url[0]))
326 .json(&body)
327 .send()
328 .await?)
329 }
330 }
331
332 pub async fn send_bundle<T: IntoIterator<Item = impl Serialize>>(
334 &self,
335 tx: T,
336 ) -> anyhow::Result<Response> {
337 let data = tx
338 .into_iter()
339 .map(|tx| {
340 Ok(BASE64_STANDARD.encode(
341 bincode::serialize(&tx)
342 .map_err(|v| anyhow::anyhow!("failed to serialize tx: {}", v))?,
343 ))
344 })
345 .collect::<anyhow::Result<Vec<_>>>()?;
346
347 let body = json!({
348 "id": 1,
349 "jsonrpc": "2.0",
350 "method": "sendBundle",
351 "params": [ data, { "encoding": "base64" } ]
352 });
353
354 let (ref url, ref client) = *self.inner.lb.alloc().await;
355
356 if self.inner.broadcast {
357 Ok(futures::future::select_ok(url.iter().map(|v| {
358 client
359 .post(&format!("{}/api/v1/bundles", v))
360 .json(&body)
361 .send()
362 }))
363 .await?
364 .0)
365 } else {
366 Ok(client
367 .post(&format!("{}/api/v1/bundles", url[0]))
368 .json(&body)
369 .send()
370 .await?)
371 }
372 }
373
374 pub async fn send_bundle_bid<T: IntoIterator<Item = impl Serialize>>(
376 &self,
377 tx: T,
378 ) -> anyhow::Result<String> {
379 self.send_bundle(tx)
380 .await?
381 .error_for_status()?
382 .json::<serde_json::Value>()
383 .await?["result"]
384 .as_str()
385 .map(|v| v.to_string())
386 .ok_or_else(|| anyhow::anyhow!("missing bundle result"))
387 }
388
389 pub async fn send_transaction_lazy<T>(
391 &self,
392 tx: impl Future<Output = anyhow::Result<T>>,
393 ) -> anyhow::Result<Response>
394 where
395 T: Serialize,
396 {
397 let (ref url, ref client) = *self.inner.lb.alloc().await;
398
399 let data = BASE64_STANDARD.encode(bincode::serialize(&tx.await?)?);
400
401 let body = json!({
402 "id": 1,
403 "jsonrpc": "2.0",
404 "method": "sendTransaction",
405 "params": [
406 data, { "encoding": "base64" }
407 ]
408 });
409
410 if self.inner.broadcast {
411 Ok(futures::future::select_ok(url.iter().map(|v| {
412 client
413 .post(&format!("{}/api/v1/transactions", v))
414 .query(&["bundleOnly", "true"])
415 .json(&body)
416 .send()
417 }))
418 .await?
419 .0)
420 } else {
421 Ok(client
422 .post(&format!("{}/api/v1/transactions", url[0]))
423 .query(&["bundleOnly", "true"])
424 .json(&body)
425 .send()
426 .await?)
427 }
428 }
429
430 pub async fn send_transaction_bid_lazy<T>(
432 &self,
433 tx: impl Future<Output = anyhow::Result<T>>,
434 ) -> anyhow::Result<String>
435 where
436 T: Serialize,
437 {
438 Ok(self
439 .send_transaction_lazy(tx)
440 .await?
441 .error_for_status()?
442 .headers()
443 .get("x-bundle-id")
444 .ok_or_else(|| anyhow!("missing `x-bundle-id` header"))?
445 .to_str()
446 .map_err(|v| anyhow!("invalid `x-bundle-id` header: {}", v))?
447 .to_string())
448 }
449
450 pub async fn send_transaction_no_bundle_only_lazy<T>(
452 &self,
453 tx: impl Future<Output = anyhow::Result<T>>,
454 ) -> anyhow::Result<Response>
455 where
456 T: Serialize,
457 {
458 let (ref url, ref client) = *self.inner.lb.alloc().await;
459
460 let data = BASE64_STANDARD.encode(bincode::serialize(&tx.await?)?);
461
462 let body = json!({
463 "id": 1,
464 "jsonrpc": "2.0",
465 "method": "sendTransaction",
466 "params": [
467 data, { "encoding": "base64" }
468 ]
469 });
470
471 if self.inner.broadcast {
472 Ok(futures::future::select_ok(url.iter().map(|v| {
473 client
474 .post(&format!("{}/api/v1/transactions", v))
475 .json(&body)
476 .send()
477 }))
478 .await?
479 .0)
480 } else {
481 Ok(client
482 .post(&format!("{}/api/v1/transactions", url[0]))
483 .json(&body)
484 .send()
485 .await?)
486 }
487 }
488
489 pub async fn send_bundle_lazy<T, S>(
491 &self,
492 tx: impl Future<Output = anyhow::Result<T>>,
493 ) -> anyhow::Result<Response>
494 where
495 T: IntoIterator<Item = S>,
496 S: Serialize,
497 {
498 let (ref url, ref client) = *self.inner.lb.alloc().await;
499
500 let data = tx
501 .await?
502 .into_iter()
503 .map(|tx| {
504 Ok(BASE64_STANDARD.encode(
505 bincode::serialize(&tx)
506 .map_err(|v| anyhow::anyhow!("failed to serialize tx: {}", v))?,
507 ))
508 })
509 .collect::<anyhow::Result<Vec<_>>>()?;
510
511 let body = json!({
512 "id": 1,
513 "jsonrpc": "2.0",
514 "method": "sendBundle",
515 "params": [ data, { "encoding": "base64" } ]
516 });
517
518 if self.inner.broadcast {
519 Ok(futures::future::select_ok(url.iter().map(|v| {
520 client
521 .post(&format!("{}/api/v1/bundles", v))
522 .json(&body)
523 .send()
524 }))
525 .await?
526 .0)
527 } else {
528 Ok(client
529 .post(&format!("{}/api/v1/bundles", url[0]))
530 .json(&body)
531 .send()
532 .await?)
533 }
534 }
535
536 pub async fn send_bundle_bid_lazy<T, S>(
538 &self,
539 tx: impl Future<Output = anyhow::Result<T>>,
540 ) -> anyhow::Result<String>
541 where
542 T: IntoIterator<Item = S>,
543 S: Serialize,
544 {
545 self.send_bundle_lazy(tx)
546 .await?
547 .error_for_status()?
548 .json::<serde_json::Value>()
549 .await?["result"]
550 .as_str()
551 .map(|v| v.to_string())
552 .ok_or_else(|| anyhow::anyhow!("missing bundle result"))
553 }
554
555 #[cfg(rustc_version_1_85_0)]
557 pub async fn send_transaction_lazy_fn<F, T>(&self, callback: F) -> anyhow::Result<Response>
558 where
559 F: AsyncFnOnce(&Vec<String>, &Client) -> anyhow::Result<T>,
560 T: Serialize,
561 {
562 let (ref url, ref client) = *self.inner.lb.alloc().await;
563
564 let data = BASE64_STANDARD.encode(bincode::serialize(&callback(url, client).await?)?);
565
566 let body = json!({
567 "id": 1,
568 "jsonrpc": "2.0",
569 "method": "sendTransaction",
570 "params": [
571 data, { "encoding": "base64" }
572 ]
573 });
574
575 if self.inner.broadcast {
576 Ok(futures::future::select_ok(url.iter().map(|v| {
577 client
578 .post(&format!("{}/api/v1/transactions", v))
579 .query(&["bundleOnly", "true"])
580 .json(&body)
581 .send()
582 }))
583 .await?
584 .0)
585 } else {
586 Ok(client
587 .post(&format!("{}/api/v1/transactions", url[0]))
588 .query(&["bundleOnly", "true"])
589 .json(&body)
590 .send()
591 .await?)
592 }
593 }
594
595 #[cfg(rustc_version_1_85_0)]
597 pub async fn send_transaction_bid_lazy_fn<F, T>(&self, callback: F) -> anyhow::Result<String>
598 where
599 F: AsyncFnOnce(&Vec<String>, &Client) -> anyhow::Result<T>,
600 T: Serialize,
601 {
602 Ok(self
603 .send_transaction_lazy_fn(callback)
604 .await?
605 .error_for_status()?
606 .headers()
607 .get("x-bundle-id")
608 .ok_or_else(|| anyhow!("missing `x-bundle-id` header"))?
609 .to_str()
610 .map_err(|v| anyhow!("invalid `x-bundle-id` header: {}", v))?
611 .to_string())
612 }
613
614 #[cfg(rustc_version_1_85_0)]
616 pub async fn send_transaction_no_bundle_only_lazy_fn<F, T>(
617 &self,
618 callback: F,
619 ) -> anyhow::Result<Response>
620 where
621 F: AsyncFnOnce(&Vec<String>, &Client) -> anyhow::Result<T>,
622 T: Serialize,
623 {
624 let (ref url, ref client) = *self.inner.lb.alloc().await;
625
626 let data = BASE64_STANDARD.encode(bincode::serialize(&callback(url, client).await?)?);
627
628 let body = json!({
629 "id": 1,
630 "jsonrpc": "2.0",
631 "method": "sendTransaction",
632 "params": [
633 data, { "encoding": "base64" }
634 ]
635 });
636
637 if self.inner.broadcast {
638 Ok(futures::future::select_ok(url.iter().map(|v| {
639 client
640 .post(&format!("{}/api/v1/transactions", v))
641 .json(&body)
642 .send()
643 }))
644 .await?
645 .0)
646 } else {
647 Ok(client
648 .post(&format!("{}/api/v1/transactions", url[0]))
649 .json(&body)
650 .send()
651 .await?)
652 }
653 }
654
655 #[cfg(rustc_version_1_85_0)]
657 pub async fn send_bundle_lazy_fn<F, T, S>(&self, callback: F) -> anyhow::Result<Response>
658 where
659 F: AsyncFnOnce(&Vec<String>, &Client) -> anyhow::Result<T>,
660 T: IntoIterator<Item = S>,
661 S: Serialize,
662 {
663 let (ref url, ref client) = *self.inner.lb.alloc().await;
664
665 let data = callback(url, client)
666 .await?
667 .into_iter()
668 .map(|tx| {
669 Ok(BASE64_STANDARD.encode(
670 bincode::serialize(&tx)
671 .map_err(|v| anyhow::anyhow!("failed to serialize tx: {}", v))?,
672 ))
673 })
674 .collect::<anyhow::Result<Vec<_>>>()?;
675
676 let body = json!({
677 "id": 1,
678 "jsonrpc": "2.0",
679 "method": "sendBundle",
680 "params": [ data, { "encoding": "base64" } ]
681 });
682
683 if self.inner.broadcast {
684 Ok(futures::future::select_ok(url.iter().map(|v| {
685 client
686 .post(&format!("{}/api/v1/bundles", v))
687 .json(&body)
688 .send()
689 }))
690 .await?
691 .0)
692 } else {
693 Ok(client
694 .post(&format!("{}/api/v1/bundles", url[0]))
695 .json(&body)
696 .send()
697 .await?)
698 }
699 }
700
701 #[cfg(rustc_version_1_85_0)]
703 pub async fn send_bundle_bid_lazy_fn<F, T, S>(&self, callback: F) -> anyhow::Result<String>
704 where
705 F: AsyncFnOnce(&Vec<String>, &Client) -> anyhow::Result<T>,
706 T: IntoIterator<Item = S>,
707 S: Serialize,
708 {
709 self.send_bundle_lazy_fn(callback)
710 .await?
711 .error_for_status()?
712 .json::<serde_json::Value>()
713 .await?["result"]
714 .as_str()
715 .map(|v| v.to_string())
716 .ok_or_else(|| anyhow::anyhow!("missing bundle result"))
717 }
718}
719
720#[derive(Debug, Clone, Deserialize)]
722pub struct JitoTip {
723 pub landed_tips_25th_percentile: f64,
724 pub landed_tips_50th_percentile: f64,
725 pub landed_tips_75th_percentile: f64,
726 pub landed_tips_95th_percentile: f64,
727 pub landed_tips_99th_percentile: f64,
728 pub ema_landed_tips_50th_percentile: f64,
729}
730
731pub async fn get_jito_tip(client: Client) -> anyhow::Result<JitoTip> {
733 Ok(client
734 .get("https://bundles.jito.wtf/api/v1/bundles/tip_floor")
735 .send()
736 .await?
737 .json::<Vec<JitoTip>>()
738 .await?
739 .get(0)
740 .context("get_jito_tip: empty response")?
741 .clone())
742}
743
744#[derive(Debug, Deserialize)]
746pub struct BundleResult {
747 pub context: serde_json::Value,
748 pub value: Option<Vec<BundleStatus>>,
749}
750
751#[derive(Debug, Deserialize)]
752pub struct BundleStatus {
753 pub bundle_id: String,
754 pub transactions: Option<Vec<String>>,
755 pub slot: Option<u64>,
756 pub confirmation_status: Option<String>,
757 pub err: Option<serde_json::Value>,
758}
759
760pub async fn get_bundle_statuses<T: IntoIterator<Item = impl AsRef<str>>>(
762 client: Client,
763 bundle: T,
764) -> anyhow::Result<BundleResult> {
765 #[derive(Debug, Deserialize)]
766 struct RpcResponse {
767 result: BundleResult,
768 }
769
770 let payload = json!({
771 "jsonrpc": "2.0",
772 "id": 1,
773 "method": "getBundleStatuses",
774 "params": [bundle.into_iter().map(|v| v.as_ref().to_string()).collect::<Vec<_>>()],
775 });
776
777 Ok(client
778 .post("https://mainnet.block-engine.jito.wtf/api/v1/getBundleStatuses")
779 .json(&payload)
780 .send()
781 .await?
782 .json::<RpcResponse>()
783 .await?
784 .result)
785}
786
787#[derive(Debug, Deserialize)]
789pub struct InflightBundleStatus {
790 pub bundle_id: String,
791 pub status: String,
792 pub landed_slot: Option<u64>,
793}
794
795#[derive(Debug, Deserialize)]
796pub struct InflightBundleResult {
797 pub context: serde_json::Value,
798 pub value: Option<Vec<InflightBundleStatus>>,
799}
800
801pub async fn get_inflight_bundle_statuses<T: IntoIterator<Item = impl AsRef<str>>>(
803 client: Client,
804 bundle: T,
805) -> anyhow::Result<InflightBundleResult> {
806 #[derive(Debug, Deserialize)]
807 struct InflightRpcResponse {
808 result: InflightBundleResult,
809 }
810
811 let payload = json!({
812 "jsonrpc": "2.0",
813 "id": 1,
814 "method": "getInflightBundleStatuses",
815 "params": [bundle.into_iter().map(|v| v.as_ref().to_string()).collect::<Vec<_>>()],
816 });
817
818 Ok(client
819 .post("https://mainnet.block-engine.jito.wtf/api/v1/getInflightBundleStatuses")
820 .json(&payload)
821 .send()
822 .await?
823 .json::<InflightRpcResponse>()
824 .await?
825 .result)
826}
827
828pub async fn test_ip(ip: IpAddr) -> anyhow::Result<IpAddr> {
829 reqwest::ClientBuilder::new()
830 .timeout(Duration::from_secs(3))
831 .local_address(ip)
832 .build()?
833 .get("https://crates.io")
834 .send()
835 .await?;
836
837 Ok(ip)
838}
839
840pub async fn test_all_ip() -> Vec<anyhow::Result<IpAddr>> {
841 match get_ip_list() {
842 Ok(v) => futures::future::join_all(v.into_iter().map(|v| test_ip(v))).await,
843 Err(_) => Vec::new(),
844 }
845}
846
847pub async fn test_all_ipv4() -> Vec<anyhow::Result<IpAddr>> {
848 match get_ipv4_list() {
849 Ok(v) => futures::future::join_all(v.into_iter().map(|v| test_ip(v))).await,
850 Err(_) => Vec::new(),
851 }
852}
853
854pub async fn test_all_ipv6() -> Vec<anyhow::Result<IpAddr>> {
855 match get_ipv6_list() {
856 Ok(v) => futures::future::join_all(v.into_iter().map(|v| test_ip(v))).await,
857 Err(_) => Vec::new(),
858 }
859}
860
861pub fn serialize_tx(tx: impl Serialize) -> anyhow::Result<String> {
862 Ok(BASE64_STANDARD.encode(bincode::serialize(&tx)?))
863}
864
865pub fn serialize_tx_vec<T: IntoIterator<Item = impl Serialize>>(
866 tx: T,
867) -> anyhow::Result<Vec<String>> {
868 tx.into_iter()
869 .map(|tx| {
870 Ok(BASE64_STANDARD.encode(
871 bincode::serialize(&tx)
872 .map_err(|v| anyhow::anyhow!("failed to serialize tx: {}", v))?,
873 ))
874 })
875 .collect::<anyhow::Result<Vec<_>>>()
876}