1pub mod block;
2pub mod connect;
3pub mod pool;
4pub mod translate;
5
6#[cfg(test)]
7mod corroboration_tests;
8#[cfg(test)]
9pub(crate) mod test_support;
10
11use std::net::SocketAddr;
12use std::time::Duration;
13
14use chia_consensus::consensus_constants::ConsensusConstants;
15use chia_protocol::{
16 Bytes32, CoinStateFilters, FullBlock as ProtoFullBlock, RejectAdditionsRequest, RejectBlock,
17 RejectHeaderRequest, RejectRemovalsRequest, RequestAdditions, RequestBlock, RequestBlockHeader,
18 RequestFeeEstimates, RequestRemovals, RespondAdditions, RespondBlock, RespondBlockHeader,
19 RespondFeeEstimates, RespondRemovals, SpendBundle as ProtoBundle,
20};
21use chia_wallet_sdk::client::Peer;
22use chia_wallet_sdk::types::{MAINNET_CONSTANTS, TESTNET11_CONSTANTS};
23use tokio_tungstenite::Connector;
24
25use crate::types::*;
26
27use crate::NetworkType;
28use pool::PeerPool;
29pub use pool::PeerRequirement;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum OptAnswer<T> {
43 Found(T),
52 UncorroboratedFound(T),
58 CorroboratedAbsent,
60 UncorroboratedAbsent,
66}
67
68#[cfg(test)]
79impl PeerBackend {
80 pub(crate) fn for_tests() -> Self {
81 Self {
82 pool: pool::PeerPool::for_tests(0),
83 network: NetworkType::Mainnet,
84 request_timeout: Duration::from_millis(1),
85 }
86 }
87}
88
89pub struct PeerBackend {
90 pool: PeerPool,
91 network: NetworkType,
92 request_timeout: Duration,
93}
94
95impl PeerBackend {
96 pub async fn new(
97 network: crate::NetworkType,
98 tls: Connector,
99 max_peers: usize,
100 requirement: PeerRequirement,
101 connect_timeout: Duration,
102 request_timeout: Duration,
103 ) -> Result<Self, ChiaQueryError> {
104 let pool = PeerPool::new(network, tls, max_peers, requirement, connect_timeout).await?;
105 Ok(Self {
106 pool,
107 network,
108 request_timeout,
109 })
110 }
111
112 pub fn constants(&self) -> &ConsensusConstants {
114 match self.network {
115 NetworkType::Mainnet => &MAINNET_CONSTANTS,
116 NetworkType::Testnet11 => &TESTNET11_CONSTANTS,
117 }
118 }
119
120 fn genesis_challenge(&self) -> Bytes32 {
124 self.constants().genesis_challenge
125 }
126
127 pub async fn has_peers(&self) -> bool {
128 self.pool.has_peers().await
129 }
130
131 pub async fn peer_count(&self) -> usize {
133 self.pool.peer_count().await
134 }
135
136 pub async fn independent_peer_count(&self) -> usize {
138 self.pool.independent_peer_count().await
139 }
140
141 async fn pick(&self) -> Result<(Peer, SocketAddr), ChiaQueryError> {
146 self.pool.try_refill().await;
148
149 self.pool
150 .select_peer()
151 .await
152 .ok_or_else(|| ChiaQueryError::PeerConnection("no peers available".into()))
153 }
154
155 pub async fn try_get_coin_record_by_name(
161 &self,
162 name: &str,
163 ) -> Result<CoinRecord, ChiaQueryError> {
164 let (peer, addr) = self.pick().await?;
165 let res = self.do_get_coin_record_by_name(&peer, name).await;
166 if res.is_err() {
167 self.pool.eject_peer(addr).await;
168 }
169 res
170 }
171
172 pub async fn try_get_coin_record_by_name_opt(
180 &self,
181 name: &str,
182 ) -> Result<OptAnswer<CoinRecord>, ChiaQueryError> {
183 self.read_opt_corroborated(|peer| async move {
184 self.do_get_coin_record_by_name_opt(&peer, name).await
185 })
186 .await
187 }
188
189 async fn read_opt_corroborated<T, F, Fut>(
219 &self,
220 read: F,
221 ) -> Result<OptAnswer<T>, ChiaQueryError>
222 where
223 T: ChainClaim,
224 F: Fn(Peer) -> Fut,
225 Fut: std::future::Future<Output = Result<Option<T>, ChiaQueryError>>,
226 {
227 let (peer, addr) = self.pick().await?;
228 let first = match read(peer).await {
229 Ok(v) => v,
230 Err(e) => {
231 self.pool.eject_peer(addr).await;
232 return Err(e);
233 }
234 };
235
236 match first {
237 Some(found) => self.corroborate_presence(found, addr, &read).await,
238 None => self.corroborate_absence(addr, &read).await,
239 }
240 }
241
242 async fn corroborate_presence<T, F, Fut>(
244 &self,
245 found: T,
246 addr: SocketAddr,
247 read: &F,
248 ) -> Result<OptAnswer<T>, ChiaQueryError>
249 where
250 T: ChainClaim,
251 F: Fn(Peer) -> Fut,
252 Fut: std::future::Future<Output = Result<Option<T>, ChiaQueryError>>,
253 {
254 let corroborators = self.pool.select_corroborating_peers(addr).await;
255 if corroborators.is_empty() {
256 log::debug!("presence reported by {addr} has no independent corroborator available");
257 return Ok(OptAnswer::UncorroboratedFound(found));
258 }
259
260 let answers =
264 futures_util::future::join_all(corroborators.into_iter().map(|(peer, peer_addr)| {
265 let answer = read(peer);
266 async move { (peer_addr, answer.await) }
267 }))
268 .await;
269
270 let claim = found.chain_claim();
271 let mut agreed = 0usize;
272 let mut disagreement: Option<String> = None;
273 let mut failed: Vec<SocketAddr> = Vec::new();
274
275 for (peer_addr, answer) in answers {
276 match answer {
277 Ok(Some(other)) if other.chain_claim() == claim => agreed += 1,
278 Ok(Some(other)) => {
279 disagreement.get_or_insert_with(|| {
280 format!(
281 "peer {addr} claims `{claim}`, peer {peer_addr} claims `{}`",
282 other.chain_claim()
283 )
284 });
285 }
286 Ok(None) => {
287 disagreement.get_or_insert_with(|| {
288 format!("peer {addr} reports present, peer {peer_addr} reports absent")
289 });
290 }
291 Err(e) => {
292 log::debug!("corroborator {peer_addr} failed: {e}");
293 failed.push(peer_addr);
294 }
295 }
296 }
297
298 for peer_addr in failed {
301 self.pool.eject_peer(peer_addr).await;
302 }
303
304 if let Some(detail) = disagreement {
308 return Err(ChiaQueryError::SourcesDisagree(detail));
309 }
310
311 if agreed == 0 {
312 log::debug!("presence reported by {addr} was corroborated by nobody who could answer");
313 return Ok(OptAnswer::UncorroboratedFound(found));
314 }
315 Ok(OptAnswer::Found(found))
316 }
317
318 async fn corroborate_absence<T, F, Fut>(
320 &self,
321 addr: SocketAddr,
322 read: &F,
323 ) -> Result<OptAnswer<T>, ChiaQueryError>
324 where
325 T: ChainClaim,
326 F: Fn(Peer) -> Fut,
327 Fut: std::future::Future<Output = Result<Option<T>, ChiaQueryError>>,
328 {
329 let Some((other, other_addr)) = self.pool.select_corroborating_peer(addr).await else {
330 log::debug!("absence reported by {addr} has no independent corroborator available");
331 return Ok(OptAnswer::UncorroboratedAbsent);
332 };
333
334 match read(other).await {
335 Ok(None) => Ok(OptAnswer::CorroboratedAbsent),
336 Ok(Some(_)) => Err(ChiaQueryError::SourcesDisagree(format!(
337 "peer {addr} reports absent, peer {other_addr} reports present"
338 ))),
339 Err(e) => {
340 self.pool.eject_peer(other_addr).await;
345 log::debug!("corroborator {other_addr} failed: {e}");
346 Ok(OptAnswer::UncorroboratedAbsent)
347 }
348 }
349 }
350
351 pub async fn try_get_coin_spend_opt(
358 &self,
359 coin_id: &str,
360 ) -> Result<OptAnswer<CoinSpend>, ChiaQueryError> {
361 self.read_opt_corroborated(|peer| async move {
362 self.do_get_coin_spend_opt(&peer, coin_id).await
363 })
364 .await
365 }
366
367 pub async fn try_get_coin_records_by_puzzle_hash(
368 &self,
369 puzzle_hash: &str,
370 start_height: Option<u32>,
371 end_height: Option<u32>,
372 include_spent: bool,
373 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
374 let (peer, addr) = self.pick().await?;
375 let res = self
376 .do_puzzle_hash_query(
377 &peer,
378 &[puzzle_hash],
379 start_height,
380 end_height,
381 include_spent,
382 false,
383 )
384 .await;
385 if res.is_err() {
386 self.pool.eject_peer(addr).await;
387 }
388 res
389 }
390
391 pub async fn try_get_coin_records_by_puzzle_hashes(
392 &self,
393 puzzle_hashes: &[String],
394 start_height: Option<u32>,
395 end_height: Option<u32>,
396 include_spent: bool,
397 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
398 let hashes: Vec<&str> = puzzle_hashes.iter().map(String::as_str).collect();
399 let (peer, addr) = self.pick().await?;
400 let res = self
401 .do_puzzle_hash_query(
402 &peer,
403 &hashes,
404 start_height,
405 end_height,
406 include_spent,
407 false,
408 )
409 .await;
410 if res.is_err() {
411 self.pool.eject_peer(addr).await;
412 }
413 res
414 }
415
416 pub async fn try_get_coin_records_by_hint(
417 &self,
418 hint: &str,
419 start_height: Option<u32>,
420 end_height: Option<u32>,
421 include_spent: bool,
422 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
423 let (peer, addr) = self.pick().await?;
424 let res = self
425 .do_puzzle_hash_query(
426 &peer,
427 &[hint],
428 start_height,
429 end_height,
430 include_spent,
431 true,
432 )
433 .await;
434 if res.is_err() {
435 self.pool.eject_peer(addr).await;
436 }
437 res
438 }
439
440 pub async fn try_get_coin_records_by_hints(
441 &self,
442 hints: &[String],
443 start_height: Option<u32>,
444 end_height: Option<u32>,
445 include_spent: bool,
446 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
447 let hs: Vec<&str> = hints.iter().map(String::as_str).collect();
448 let (peer, addr) = self.pick().await?;
449 let res = self
450 .do_puzzle_hash_query(&peer, &hs, start_height, end_height, include_spent, true)
451 .await;
452 if res.is_err() {
453 self.pool.eject_peer(addr).await;
454 }
455 res
456 }
457
458 pub async fn try_get_coin_records_by_names(
459 &self,
460 names: &[String],
461 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
462 let (peer, addr) = self.pick().await?;
463 let res = self.do_coin_ids_query(&peer, names).await;
464 if res.is_err() {
465 self.pool.eject_peer(addr).await;
466 }
467 res
468 }
469
470 pub async fn try_get_puzzle_and_solution(
471 &self,
472 coin_id: &str,
473 height: u32,
474 ) -> Result<CoinSpend, ChiaQueryError> {
475 let (peer, addr) = self.pick().await?;
476 let res = self
477 .do_get_puzzle_and_solution(&peer, coin_id, height)
478 .await;
479 if res.is_err() {
480 self.pool.eject_peer(addr).await;
481 }
482 res
483 }
484
485 pub async fn try_get_fee_estimate(
486 &self,
487 target_times: &[u64],
488 ) -> Result<FeeEstimate, ChiaQueryError> {
489 let (peer, addr) = self.pick().await?;
490 let res = self.do_get_fee_estimate(&peer, target_times).await;
491 if res.is_err() {
492 self.pool.eject_peer(addr).await;
493 }
494 res
495 }
496
497 pub async fn try_push_tx(&self, bundle: &SpendBundle) -> Result<TxStatus, ChiaQueryError> {
498 let (peer, addr) = self.pick().await?;
499 let res = self.do_push_tx(&peer, bundle).await;
500 if res.is_err() {
501 self.pool.eject_peer(addr).await;
502 }
503 res
504 }
505
506 pub async fn try_get_block_record_by_height(
509 &self,
510 height: u32,
511 ) -> Result<BlockRecord, ChiaQueryError> {
512 let (peer, addr) = self.pick().await?;
513 let res = self.do_get_block_record_by_height(&peer, height).await;
514 if res.is_err() {
515 self.pool.eject_peer(addr).await;
516 }
517 res
518 }
519
520 #[allow(dead_code)]
526 pub async fn try_get_additions_and_removals(
527 &self,
528 height: u32,
529 header_hash: &str,
530 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
531 let (peer, addr) = self.pick().await?;
532 let res = self
533 .do_get_additions_and_removals(&peer, height, header_hash)
534 .await;
535 if res.is_err() {
536 self.pool.eject_peer(addr).await;
537 }
538 res
539 }
540
541 pub async fn try_get_children(
544 &self,
545 parent_id: &str,
546 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
547 let (peer, addr) = self.pick().await?;
548 let res = self.do_get_children(&peer, parent_id).await;
549 if res.is_err() {
550 self.pool.eject_peer(addr).await;
551 }
552 res
553 }
554
555 pub async fn try_get_block_by_height(
558 &self,
559 height: u32,
560 ) -> Result<serde_json::Value, ChiaQueryError> {
561 let (peer, addr) = self.pick().await?;
562 let res = self.do_get_block_by_height(&peer, height).await;
563 if res.is_err() {
564 self.pool.eject_peer(addr).await;
565 }
566 res
567 }
568
569 pub async fn try_get_additions_and_removals_from_block(
572 &self,
573 height: u32,
574 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
575 let (peer, addr) = self.pick().await?;
576 let res = self
577 .do_get_additions_and_removals_from_block(&peer, height)
578 .await;
579 if res.is_err() {
580 self.pool.eject_peer(addr).await;
581 }
582 res
583 }
584
585 pub async fn try_get_block_spends_by_height(
588 &self,
589 height: u32,
590 ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
591 let (peer, addr) = self.pick().await?;
592 let res = self.do_get_block_spends(&peer, height).await;
593 if res.is_err() {
594 self.pool.eject_peer(addr).await;
595 }
596 res
597 }
598
599 pub async fn try_get_block_spends_with_conditions(
602 &self,
603 height: u32,
604 ) -> Result<Vec<CoinSpendWithConditions>, ChiaQueryError> {
605 let (peer, addr) = self.pick().await?;
606 let proto_block = self.fetch_full_block(&peer, height).await;
607 if proto_block.is_err() {
608 self.pool.eject_peer(addr).await;
609 }
610 let proto_block = proto_block?;
611 block::block_spends_with_conditions(&proto_block, self.constants())
612 }
613
614 pub async fn try_get_puzzle_and_solution_auto(
617 &self,
618 coin_id: &str,
619 ) -> Result<CoinSpend, ChiaQueryError> {
620 let (peer, addr) = self.pick().await?;
622 let id = translate::parse_bytes32(coin_id)?;
623
624 let state_resp = tokio::time::timeout(self.request_timeout, {
625 peer.request_coin_state(vec![id], None, self.genesis_challenge(), false)
626 })
627 .await
628 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
629 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
630 .map_err(|_| ChiaQueryError::PeerRejection("coin state rejected".into()))?;
631
632 let cs = state_resp
633 .coin_states
634 .first()
635 .ok_or_else(|| ChiaQueryError::PeerRejection("coin not found".into()))?;
636 let spent_height = cs
637 .spent_height
638 .ok_or_else(|| ChiaQueryError::PeerRejection("coin is not spent".into()))?;
639
640 let res = self
641 .do_get_puzzle_and_solution(&peer, coin_id, spent_height)
642 .await;
643 if res.is_err() {
644 self.pool.eject_peer(addr).await;
645 }
646 res
647 }
648
649 pub async fn try_get_block_records(
652 &self,
653 start: u32,
654 end: u32,
655 ) -> Result<Vec<BlockRecord>, ChiaQueryError> {
656 let mut records = Vec::with_capacity((end - start) as usize);
657 for height in start..end {
658 records.push(self.try_get_block_record_by_height(height).await?);
659 }
660 Ok(records)
661 }
662
663 pub async fn try_get_blocks_range(
666 &self,
667 start: u32,
668 end: u32,
669 ) -> Result<Vec<serde_json::Value>, ChiaQueryError> {
670 let mut blocks = Vec::with_capacity((end - start) as usize);
671 for height in start..end {
672 blocks.push(self.try_get_block_by_height(height).await?);
673 }
674 Ok(blocks)
675 }
676
677 pub fn network_info(&self) -> NetworkInfo {
680 let c = self.constants();
681 NetworkInfo {
682 network_name: self.network.network_id().to_string(),
683 network_prefix: match self.network {
684 NetworkType::Mainnet => "xch".to_string(),
685 NetworkType::Testnet11 => "txch".to_string(),
686 },
687 genesis_challenge: format!("0x{}", hex::encode(c.genesis_challenge)),
688 }
689 }
690
691 pub fn aggsig_additional_data(&self) -> String {
694 format!(
695 "0x{}",
696 hex::encode(self.constants().agg_sig_me_additional_data)
697 )
698 }
699
700 pub fn peak_height(&self) -> u32 {
703 self.pool.peak_height()
704 }
705
706 async fn do_get_coin_record_by_name(
711 &self,
712 peer: &Peer,
713 name: &str,
714 ) -> Result<CoinRecord, ChiaQueryError> {
715 let coin_id = translate::parse_bytes32(name)?;
716
717 let response = tokio::time::timeout(self.request_timeout, {
718 peer.request_coin_state(vec![coin_id], None, self.genesis_challenge(), false)
719 })
720 .await
721 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
722 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
723 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
724
725 response
726 .coin_states
727 .first()
728 .map(translate::coin_state_to_record)
729 .ok_or_else(|| ChiaQueryError::PeerRejection("coin not found".into()))
730 }
731
732 async fn do_get_coin_record_by_name_opt(
735 &self,
736 peer: &Peer,
737 name: &str,
738 ) -> Result<Option<CoinRecord>, ChiaQueryError> {
739 let coin_id = translate::parse_bytes32(name)?;
740
741 let response = tokio::time::timeout(self.request_timeout, {
742 peer.request_coin_state(vec![coin_id], None, self.genesis_challenge(), false)
743 })
744 .await
745 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
746 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
747 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
748
749 Ok(response
754 .coin_states
755 .first()
756 .map(translate::coin_state_to_record))
757 }
758
759 async fn do_get_coin_spend_opt(
762 &self,
763 peer: &Peer,
764 coin_id: &str,
765 ) -> Result<Option<CoinSpend>, ChiaQueryError> {
766 let id = translate::parse_bytes32(coin_id)?;
767
768 let state_resp = tokio::time::timeout(self.request_timeout, {
769 peer.request_coin_state(vec![id], None, self.genesis_challenge(), false)
770 })
771 .await
772 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
773 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
774 .map_err(|_| ChiaQueryError::PeerRejection("coin state rejected".into()))?;
775
776 let Some(cs) = state_resp.coin_states.first() else {
778 return Ok(None);
779 };
780 let Some(spent_height) = cs.spent_height else {
781 return Ok(None);
782 };
783
784 let spend = self
785 .do_get_puzzle_and_solution(peer, coin_id, spent_height)
786 .await?;
787
788 let spend = CoinSpend {
795 coin: Coin::from_protocol(&cs.coin),
796 ..spend
797 };
798 Ok(Some(spend))
799 }
800
801 async fn do_puzzle_hash_query(
802 &self,
803 peer: &Peer,
804 hashes: &[&str],
805 start_height: Option<u32>,
806 end_height: Option<u32>,
807 include_spent: bool,
808 include_hinted: bool,
809 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
810 let puzzle_hashes: Vec<Bytes32> = hashes
811 .iter()
812 .map(|h| translate::parse_bytes32(h))
813 .collect::<Result<_, _>>()?;
814
815 let filters = CoinStateFilters {
816 include_spent,
817 include_unspent: true,
818 include_hinted,
819 min_amount: 0,
820 };
821
822 let mut all_states = Vec::new();
823 let mut prev_height: Option<u32> = None;
829 let mut prev_header = self.genesis_challenge();
830
831 loop {
832 let response = tokio::time::timeout(self.request_timeout, {
833 peer.request_puzzle_state(
834 puzzle_hashes.clone(),
835 prev_height,
836 prev_header,
837 filters.clone(),
838 false,
839 )
840 })
841 .await
842 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
843 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
844 .map_err(|_| ChiaQueryError::PeerRejection("puzzle state request rejected".into()))?;
845
846 all_states.extend(response.coin_states.iter().cloned());
847
848 if response.is_finished {
849 break;
850 }
851 prev_height = Some(response.height);
852 prev_header = response.header_hash;
853 }
854
855 let records: Vec<CoinRecord> = all_states
857 .iter()
858 .filter(|cs| {
859 let h = cs.created_height.unwrap_or(0);
860 let above_start = start_height.is_none_or(|s| h >= s);
861 let below_end = end_height.is_none_or(|e| h <= e);
862 above_start && below_end
863 })
864 .map(translate::coin_state_to_record)
865 .collect();
866
867 Ok(records)
868 }
869
870 async fn do_coin_ids_query(
871 &self,
872 peer: &Peer,
873 names: &[String],
874 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
875 let ids: Vec<Bytes32> = names
876 .iter()
877 .map(|n| translate::parse_bytes32(n))
878 .collect::<Result<_, _>>()?;
879
880 let response = tokio::time::timeout(self.request_timeout, {
881 peer.request_coin_state(ids, None, self.genesis_challenge(), false)
882 })
883 .await
884 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
885 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
886 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
887
888 Ok(translate::coin_states_to_records(&response.coin_states))
889 }
890
891 async fn do_get_puzzle_and_solution(
892 &self,
893 peer: &Peer,
894 coin_id: &str,
895 height: u32,
896 ) -> Result<CoinSpend, ChiaQueryError> {
897 let id = translate::parse_bytes32(coin_id)?;
898
899 let response = tokio::time::timeout(self.request_timeout, {
900 peer.request_puzzle_and_solution(id, height)
901 })
902 .await
903 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
904 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
905 .map_err(|_| ChiaQueryError::PeerRejection("puzzle solution rejected".into()))?;
906
907 Ok(translate::make_coin_spend(
908 &chia_protocol::Coin {
914 parent_coin_info: response.coin_name,
915 puzzle_hash: Bytes32::default(),
916 amount: 0,
917 },
918 &response.puzzle,
919 &response.solution,
920 ))
921 }
922
923 async fn do_get_fee_estimate(
924 &self,
925 peer: &Peer,
926 target_times: &[u64],
927 ) -> Result<FeeEstimate, ChiaQueryError> {
928 let request = RequestFeeEstimates {
929 time_targets: target_times.to_vec(),
930 };
931
932 let response: RespondFeeEstimates =
933 tokio::time::timeout(self.request_timeout, peer.request_infallible(request))
934 .await
935 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
936 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
937
938 let estimates: Vec<f64> = response
939 .estimates
940 .estimates
941 .iter()
942 .map(|e| e.estimated_fee_rate.mojos_per_clvm_cost as f64)
943 .collect();
944
945 Ok(translate::make_fee_estimate(
946 estimates,
947 target_times.to_vec(),
948 ))
949 }
950
951 async fn do_push_tx(
952 &self,
953 peer: &Peer,
954 bundle: &SpendBundle,
955 ) -> Result<TxStatus, ChiaQueryError> {
956 let proto = to_protocol_spend_bundle(bundle)?;
957
958 let ack = tokio::time::timeout(self.request_timeout, peer.send_transaction(proto))
959 .await
960 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
961 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
962
963 Ok(translate::ack_to_tx_status(ack.status))
964 }
965
966 async fn do_get_block_by_height(
969 &self,
970 peer: &Peer,
971 height: u32,
972 ) -> Result<serde_json::Value, ChiaQueryError> {
973 let proto_block = self.fetch_full_block(peer, height).await?;
974 serde_json::to_value(&proto_block)
975 .map_err(|e| ChiaQueryError::PeerConnection(format!("serialize block: {e}")))
976 }
977
978 async fn do_get_additions_and_removals_from_block(
981 &self,
982 peer: &Peer,
983 height: u32,
984 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
985 let proto_block = self.fetch_full_block(peer, height).await?;
986 block::block_additions_and_removals(&proto_block, height, self.constants())
987 }
988
989 async fn do_get_block_spends(
992 &self,
993 peer: &Peer,
994 height: u32,
995 ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
996 let proto_block = self.fetch_full_block(peer, height).await?;
997 block::block_spends(&proto_block, self.constants())
998 }
999
1000 async fn fetch_full_block(
1003 &self,
1004 peer: &Peer,
1005 height: u32,
1006 ) -> Result<ProtoFullBlock, ChiaQueryError> {
1007 let response = tokio::time::timeout(self.request_timeout, {
1008 peer.request_fallible::<RespondBlock, RejectBlock, _>(RequestBlock {
1009 height,
1010 include_transaction_block: true,
1011 })
1012 })
1013 .await
1014 .map_err(|_| ChiaQueryError::PeerConnection("block request timed out".into()))?
1015 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
1016 .map_err(|_| ChiaQueryError::PeerRejection("block request rejected".into()))?;
1017
1018 Ok(response.block)
1019 }
1020
1021 async fn do_get_block_record_by_height(
1024 &self,
1025 peer: &Peer,
1026 height: u32,
1027 ) -> Result<BlockRecord, ChiaQueryError> {
1028 let response = tokio::time::timeout(self.request_timeout, {
1029 peer.request_fallible::<RespondBlockHeader, RejectHeaderRequest, _>(
1030 RequestBlockHeader { height },
1031 )
1032 })
1033 .await
1034 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
1035 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
1036 .map_err(|_| ChiaQueryError::PeerRejection("header request rejected".into()))?;
1037
1038 Ok(translate::header_block_to_block_record(
1039 &response.header_block,
1040 ))
1041 }
1042
1043 async fn do_get_additions_and_removals(
1046 &self,
1047 peer: &Peer,
1048 height: u32,
1049 header_hash_hex: &str,
1050 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
1051 let header_hash = translate::parse_bytes32(header_hash_hex)?;
1052
1053 let (adds_result, rems_result) = tokio::join!(
1055 tokio::time::timeout(self.request_timeout, {
1056 peer.request_fallible::<RespondAdditions, RejectAdditionsRequest, _>(
1057 RequestAdditions {
1058 height,
1059 header_hash: Some(header_hash),
1060 puzzle_hashes: None,
1061 },
1062 )
1063 }),
1064 tokio::time::timeout(self.request_timeout, {
1065 peer.request_fallible::<RespondRemovals, RejectRemovalsRequest, _>(
1066 RequestRemovals {
1067 height,
1068 header_hash,
1069 coin_names: None,
1070 },
1071 )
1072 }),
1073 );
1074
1075 let adds = adds_result
1076 .map_err(|_| ChiaQueryError::PeerConnection("additions request timed out".into()))?
1077 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
1078 .map_err(|_| ChiaQueryError::PeerRejection("additions rejected".into()))?;
1079
1080 let rems = rems_result
1081 .map_err(|_| ChiaQueryError::PeerConnection("removals request timed out".into()))?
1082 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
1083 .map_err(|_| ChiaQueryError::PeerRejection("removals rejected".into()))?;
1084
1085 Ok(translate::additions_removals_to_response(
1086 &adds, &rems, height,
1087 ))
1088 }
1089
1090 async fn do_get_children(
1093 &self,
1094 peer: &Peer,
1095 parent_id: &str,
1096 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
1097 let coin_name = translate::parse_bytes32(parent_id)?;
1098
1099 let response = tokio::time::timeout(self.request_timeout, peer.request_children(coin_name))
1100 .await
1101 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
1102 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
1103
1104 Ok(translate::coin_states_to_records(&response.coin_states))
1105 }
1106}
1107
1108fn to_protocol_spend_bundle(bundle: &SpendBundle) -> Result<ProtoBundle, ChiaQueryError> {
1113 let coin_spends: Vec<chia_protocol::CoinSpend> = bundle
1114 .coin_spends
1115 .iter()
1116 .map(|cs| {
1117 Ok(chia_protocol::CoinSpend {
1118 coin: chia_protocol::Coin {
1119 parent_coin_info: translate::parse_bytes32(&cs.coin.parent_coin_info)?,
1120 puzzle_hash: translate::parse_bytes32(&cs.coin.puzzle_hash)?,
1121 amount: cs.coin.amount,
1122 },
1123 puzzle_reveal: chia_protocol::Program::from(chia_protocol::Bytes::from(
1124 translate::parse_hex(&cs.puzzle_reveal)?,
1125 )),
1126 solution: chia_protocol::Program::from(chia_protocol::Bytes::from(
1127 translate::parse_hex(&cs.solution)?,
1128 )),
1129 })
1130 })
1131 .collect::<Result<_, ChiaQueryError>>()?;
1132
1133 let sig_bytes = translate::parse_hex(&bundle.aggregated_signature)?;
1134 let sig_arr: [u8; 96] = sig_bytes
1135 .try_into()
1136 .map_err(|_| ChiaQueryError::InvalidRequest("signature must be 96 bytes".into()))?;
1137 let aggregated_signature = chia_bls::Signature::from_bytes(&sig_arr)
1138 .map_err(|e| ChiaQueryError::InvalidRequest(format!("bad BLS signature: {e}")))?;
1139
1140 Ok(ProtoBundle {
1141 coin_spends,
1142 aggregated_signature,
1143 })
1144}