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::*;
26use crate::NetworkType;
27use pool::PeerPool;
28pub use pool::PeerRequirement;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum OptAnswer<T> {
42 Found(T),
49 CorroboratedAbsent,
51 UncorroboratedAbsent,
57}
58
59pub struct PeerBackend {
64 pool: PeerPool,
65 network: NetworkType,
66 request_timeout: Duration,
67}
68
69impl PeerBackend {
70 pub async fn new(
71 network: crate::NetworkType,
72 tls: Connector,
73 max_peers: usize,
74 requirement: PeerRequirement,
75 connect_timeout: Duration,
76 request_timeout: Duration,
77 ) -> Result<Self, ChiaQueryError> {
78 let pool = PeerPool::new(network, tls, max_peers, requirement, connect_timeout).await?;
79 Ok(Self {
80 pool,
81 network,
82 request_timeout,
83 })
84 }
85
86 pub fn constants(&self) -> &ConsensusConstants {
88 match self.network {
89 NetworkType::Mainnet => &MAINNET_CONSTANTS,
90 NetworkType::Testnet11 => &TESTNET11_CONSTANTS,
91 }
92 }
93
94 fn genesis_challenge(&self) -> Bytes32 {
98 self.constants().genesis_challenge
99 }
100
101 pub async fn has_peers(&self) -> bool {
102 self.pool.has_peers().await
103 }
104
105 pub async fn peer_count(&self) -> usize {
107 self.pool.peer_count().await
108 }
109
110 pub async fn independent_peer_count(&self) -> usize {
112 self.pool.independent_peer_count().await
113 }
114
115 async fn pick(&self) -> Result<(Peer, SocketAddr), ChiaQueryError> {
120 self.pool.try_refill().await;
122
123 self.pool
124 .select_peer()
125 .await
126 .ok_or_else(|| ChiaQueryError::PeerConnection("no peers available".into()))
127 }
128
129 pub async fn try_get_coin_record_by_name(
135 &self,
136 name: &str,
137 ) -> Result<CoinRecord, ChiaQueryError> {
138 let (peer, addr) = self.pick().await?;
139 let res = self.do_get_coin_record_by_name(&peer, name).await;
140 if res.is_err() {
141 self.pool.eject_peer(addr).await;
142 }
143 res
144 }
145
146 pub async fn try_get_coin_record_by_name_opt(
154 &self,
155 name: &str,
156 ) -> Result<OptAnswer<CoinRecord>, ChiaQueryError> {
157 self.read_opt_corroborated(|peer| async move {
158 self.do_get_coin_record_by_name_opt(&peer, name).await
159 })
160 .await
161 }
162
163 async fn read_opt_corroborated<T, F, Fut>(
182 &self,
183 read: F,
184 ) -> Result<OptAnswer<T>, ChiaQueryError>
185 where
186 F: Fn(Peer) -> Fut,
187 Fut: std::future::Future<Output = Result<Option<T>, ChiaQueryError>>,
188 {
189 let (peer, addr) = self.pick().await?;
190 let first = match read(peer).await {
191 Ok(v) => v,
192 Err(e) => {
193 self.pool.eject_peer(addr).await;
194 return Err(e);
195 }
196 };
197 if let Some(found) = first {
198 return Ok(OptAnswer::Found(found));
199 }
200
201 let Some((other, other_addr)) = self.pool.select_corroborating_peer(addr).await else {
202 log::debug!("absence reported by {addr} has no independent corroborator available");
203 return Ok(OptAnswer::UncorroboratedAbsent);
204 };
205
206 match read(other).await {
207 Ok(None) => Ok(OptAnswer::CorroboratedAbsent),
208 Ok(Some(_)) => Err(ChiaQueryError::SourcesDisagree(format!(
209 "peer {addr} reports absent, peer {other_addr} reports present"
210 ))),
211 Err(e) => {
212 self.pool.eject_peer(other_addr).await;
217 log::debug!("corroborator {other_addr} failed: {e}");
218 Ok(OptAnswer::UncorroboratedAbsent)
219 }
220 }
221 }
222
223 pub async fn try_get_coin_spend_opt(
230 &self,
231 coin_id: &str,
232 ) -> Result<OptAnswer<CoinSpend>, ChiaQueryError> {
233 self.read_opt_corroborated(|peer| async move {
234 self.do_get_coin_spend_opt(&peer, coin_id).await
235 })
236 .await
237 }
238
239 pub async fn try_get_coin_records_by_puzzle_hash(
240 &self,
241 puzzle_hash: &str,
242 start_height: Option<u32>,
243 end_height: Option<u32>,
244 include_spent: bool,
245 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
246 let (peer, addr) = self.pick().await?;
247 let res = self
248 .do_puzzle_hash_query(
249 &peer,
250 &[puzzle_hash],
251 start_height,
252 end_height,
253 include_spent,
254 false,
255 )
256 .await;
257 if res.is_err() {
258 self.pool.eject_peer(addr).await;
259 }
260 res
261 }
262
263 pub async fn try_get_coin_records_by_puzzle_hashes(
264 &self,
265 puzzle_hashes: &[String],
266 start_height: Option<u32>,
267 end_height: Option<u32>,
268 include_spent: bool,
269 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
270 let hashes: Vec<&str> = puzzle_hashes.iter().map(String::as_str).collect();
271 let (peer, addr) = self.pick().await?;
272 let res = self
273 .do_puzzle_hash_query(
274 &peer,
275 &hashes,
276 start_height,
277 end_height,
278 include_spent,
279 false,
280 )
281 .await;
282 if res.is_err() {
283 self.pool.eject_peer(addr).await;
284 }
285 res
286 }
287
288 pub async fn try_get_coin_records_by_hint(
289 &self,
290 hint: &str,
291 start_height: Option<u32>,
292 end_height: Option<u32>,
293 include_spent: bool,
294 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
295 let (peer, addr) = self.pick().await?;
296 let res = self
297 .do_puzzle_hash_query(
298 &peer,
299 &[hint],
300 start_height,
301 end_height,
302 include_spent,
303 true,
304 )
305 .await;
306 if res.is_err() {
307 self.pool.eject_peer(addr).await;
308 }
309 res
310 }
311
312 pub async fn try_get_coin_records_by_hints(
313 &self,
314 hints: &[String],
315 start_height: Option<u32>,
316 end_height: Option<u32>,
317 include_spent: bool,
318 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
319 let hs: Vec<&str> = hints.iter().map(String::as_str).collect();
320 let (peer, addr) = self.pick().await?;
321 let res = self
322 .do_puzzle_hash_query(&peer, &hs, start_height, end_height, include_spent, true)
323 .await;
324 if res.is_err() {
325 self.pool.eject_peer(addr).await;
326 }
327 res
328 }
329
330 pub async fn try_get_coin_records_by_names(
331 &self,
332 names: &[String],
333 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
334 let (peer, addr) = self.pick().await?;
335 let res = self.do_coin_ids_query(&peer, names).await;
336 if res.is_err() {
337 self.pool.eject_peer(addr).await;
338 }
339 res
340 }
341
342 pub async fn try_get_puzzle_and_solution(
343 &self,
344 coin_id: &str,
345 height: u32,
346 ) -> Result<CoinSpend, ChiaQueryError> {
347 let (peer, addr) = self.pick().await?;
348 let res = self
349 .do_get_puzzle_and_solution(&peer, coin_id, height)
350 .await;
351 if res.is_err() {
352 self.pool.eject_peer(addr).await;
353 }
354 res
355 }
356
357 pub async fn try_get_fee_estimate(
358 &self,
359 target_times: &[u64],
360 ) -> Result<FeeEstimate, ChiaQueryError> {
361 let (peer, addr) = self.pick().await?;
362 let res = self.do_get_fee_estimate(&peer, target_times).await;
363 if res.is_err() {
364 self.pool.eject_peer(addr).await;
365 }
366 res
367 }
368
369 pub async fn try_push_tx(&self, bundle: &SpendBundle) -> Result<TxStatus, ChiaQueryError> {
370 let (peer, addr) = self.pick().await?;
371 let res = self.do_push_tx(&peer, bundle).await;
372 if res.is_err() {
373 self.pool.eject_peer(addr).await;
374 }
375 res
376 }
377
378 pub async fn try_get_block_record_by_height(
381 &self,
382 height: u32,
383 ) -> Result<BlockRecord, ChiaQueryError> {
384 let (peer, addr) = self.pick().await?;
385 let res = self.do_get_block_record_by_height(&peer, height).await;
386 if res.is_err() {
387 self.pool.eject_peer(addr).await;
388 }
389 res
390 }
391
392 #[allow(dead_code)]
398 pub async fn try_get_additions_and_removals(
399 &self,
400 height: u32,
401 header_hash: &str,
402 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
403 let (peer, addr) = self.pick().await?;
404 let res = self
405 .do_get_additions_and_removals(&peer, height, header_hash)
406 .await;
407 if res.is_err() {
408 self.pool.eject_peer(addr).await;
409 }
410 res
411 }
412
413 pub async fn try_get_children(
416 &self,
417 parent_id: &str,
418 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
419 let (peer, addr) = self.pick().await?;
420 let res = self.do_get_children(&peer, parent_id).await;
421 if res.is_err() {
422 self.pool.eject_peer(addr).await;
423 }
424 res
425 }
426
427 pub async fn try_get_block_by_height(
430 &self,
431 height: u32,
432 ) -> Result<serde_json::Value, ChiaQueryError> {
433 let (peer, addr) = self.pick().await?;
434 let res = self.do_get_block_by_height(&peer, height).await;
435 if res.is_err() {
436 self.pool.eject_peer(addr).await;
437 }
438 res
439 }
440
441 pub async fn try_get_additions_and_removals_from_block(
444 &self,
445 height: u32,
446 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
447 let (peer, addr) = self.pick().await?;
448 let res = self
449 .do_get_additions_and_removals_from_block(&peer, height)
450 .await;
451 if res.is_err() {
452 self.pool.eject_peer(addr).await;
453 }
454 res
455 }
456
457 pub async fn try_get_block_spends_by_height(
460 &self,
461 height: u32,
462 ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
463 let (peer, addr) = self.pick().await?;
464 let res = self.do_get_block_spends(&peer, height).await;
465 if res.is_err() {
466 self.pool.eject_peer(addr).await;
467 }
468 res
469 }
470
471 pub async fn try_get_block_spends_with_conditions(
474 &self,
475 height: u32,
476 ) -> Result<Vec<CoinSpendWithConditions>, ChiaQueryError> {
477 let (peer, addr) = self.pick().await?;
478 let proto_block = self.fetch_full_block(&peer, height).await;
479 if proto_block.is_err() {
480 self.pool.eject_peer(addr).await;
481 }
482 let proto_block = proto_block?;
483 block::block_spends_with_conditions(&proto_block, self.constants())
484 }
485
486 pub async fn try_get_puzzle_and_solution_auto(
489 &self,
490 coin_id: &str,
491 ) -> Result<CoinSpend, ChiaQueryError> {
492 let (peer, addr) = self.pick().await?;
494 let id = translate::parse_bytes32(coin_id)?;
495
496 let state_resp = tokio::time::timeout(self.request_timeout, {
497 peer.request_coin_state(vec![id], None, self.genesis_challenge(), false)
498 })
499 .await
500 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
501 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
502 .map_err(|_| ChiaQueryError::PeerRejection("coin state rejected".into()))?;
503
504 let cs = state_resp
505 .coin_states
506 .first()
507 .ok_or_else(|| ChiaQueryError::PeerRejection("coin not found".into()))?;
508 let spent_height = cs
509 .spent_height
510 .ok_or_else(|| ChiaQueryError::PeerRejection("coin is not spent".into()))?;
511
512 let res = self
513 .do_get_puzzle_and_solution(&peer, coin_id, spent_height)
514 .await;
515 if res.is_err() {
516 self.pool.eject_peer(addr).await;
517 }
518 res
519 }
520
521 pub async fn try_get_block_records(
524 &self,
525 start: u32,
526 end: u32,
527 ) -> Result<Vec<BlockRecord>, ChiaQueryError> {
528 let mut records = Vec::with_capacity((end - start) as usize);
529 for height in start..end {
530 records.push(self.try_get_block_record_by_height(height).await?);
531 }
532 Ok(records)
533 }
534
535 pub async fn try_get_blocks_range(
538 &self,
539 start: u32,
540 end: u32,
541 ) -> Result<Vec<serde_json::Value>, ChiaQueryError> {
542 let mut blocks = Vec::with_capacity((end - start) as usize);
543 for height in start..end {
544 blocks.push(self.try_get_block_by_height(height).await?);
545 }
546 Ok(blocks)
547 }
548
549 pub fn network_info(&self) -> NetworkInfo {
552 let c = self.constants();
553 NetworkInfo {
554 network_name: self.network.network_id().to_string(),
555 network_prefix: match self.network {
556 NetworkType::Mainnet => "xch".to_string(),
557 NetworkType::Testnet11 => "txch".to_string(),
558 },
559 genesis_challenge: format!("0x{}", hex::encode(c.genesis_challenge)),
560 }
561 }
562
563 pub fn aggsig_additional_data(&self) -> String {
566 format!(
567 "0x{}",
568 hex::encode(self.constants().agg_sig_me_additional_data)
569 )
570 }
571
572 pub fn peak_height(&self) -> u32 {
575 self.pool.peak_height()
576 }
577
578 async fn do_get_coin_record_by_name(
583 &self,
584 peer: &Peer,
585 name: &str,
586 ) -> Result<CoinRecord, ChiaQueryError> {
587 let coin_id = translate::parse_bytes32(name)?;
588
589 let response = tokio::time::timeout(self.request_timeout, {
590 peer.request_coin_state(vec![coin_id], None, self.genesis_challenge(), false)
591 })
592 .await
593 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
594 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
595 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
596
597 response
598 .coin_states
599 .first()
600 .map(translate::coin_state_to_record)
601 .ok_or_else(|| ChiaQueryError::PeerRejection("coin not found".into()))
602 }
603
604 async fn do_get_coin_record_by_name_opt(
607 &self,
608 peer: &Peer,
609 name: &str,
610 ) -> Result<Option<CoinRecord>, ChiaQueryError> {
611 let coin_id = translate::parse_bytes32(name)?;
612
613 let response = tokio::time::timeout(self.request_timeout, {
614 peer.request_coin_state(vec![coin_id], None, self.genesis_challenge(), false)
615 })
616 .await
617 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
618 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
619 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
620
621 Ok(response
626 .coin_states
627 .first()
628 .map(translate::coin_state_to_record))
629 }
630
631 async fn do_get_coin_spend_opt(
634 &self,
635 peer: &Peer,
636 coin_id: &str,
637 ) -> Result<Option<CoinSpend>, ChiaQueryError> {
638 let id = translate::parse_bytes32(coin_id)?;
639
640 let state_resp = tokio::time::timeout(self.request_timeout, {
641 peer.request_coin_state(vec![id], None, self.genesis_challenge(), false)
642 })
643 .await
644 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
645 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
646 .map_err(|_| ChiaQueryError::PeerRejection("coin state rejected".into()))?;
647
648 let Some(cs) = state_resp.coin_states.first() else {
650 return Ok(None);
651 };
652 let Some(spent_height) = cs.spent_height else {
653 return Ok(None);
654 };
655
656 let spend = self
657 .do_get_puzzle_and_solution(peer, coin_id, spent_height)
658 .await?;
659
660 let spend = CoinSpend {
667 coin: Coin::from_protocol(&cs.coin),
668 ..spend
669 };
670 Ok(Some(spend))
671 }
672
673 async fn do_puzzle_hash_query(
674 &self,
675 peer: &Peer,
676 hashes: &[&str],
677 start_height: Option<u32>,
678 end_height: Option<u32>,
679 include_spent: bool,
680 include_hinted: bool,
681 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
682 let puzzle_hashes: Vec<Bytes32> = hashes
683 .iter()
684 .map(|h| translate::parse_bytes32(h))
685 .collect::<Result<_, _>>()?;
686
687 let filters = CoinStateFilters {
688 include_spent,
689 include_unspent: true,
690 include_hinted,
691 min_amount: 0,
692 };
693
694 let mut all_states = Vec::new();
695 let mut prev_height: Option<u32> = None;
701 let mut prev_header = self.genesis_challenge();
702
703 loop {
704 let response = tokio::time::timeout(self.request_timeout, {
705 peer.request_puzzle_state(
706 puzzle_hashes.clone(),
707 prev_height,
708 prev_header,
709 filters.clone(),
710 false,
711 )
712 })
713 .await
714 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
715 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
716 .map_err(|_| ChiaQueryError::PeerRejection("puzzle state request rejected".into()))?;
717
718 all_states.extend(response.coin_states.iter().cloned());
719
720 if response.is_finished {
721 break;
722 }
723 prev_height = Some(response.height);
724 prev_header = response.header_hash;
725 }
726
727 let records: Vec<CoinRecord> = all_states
729 .iter()
730 .filter(|cs| {
731 let h = cs.created_height.unwrap_or(0);
732 let above_start = start_height.is_none_or(|s| h >= s);
733 let below_end = end_height.is_none_or(|e| h <= e);
734 above_start && below_end
735 })
736 .map(translate::coin_state_to_record)
737 .collect();
738
739 Ok(records)
740 }
741
742 async fn do_coin_ids_query(
743 &self,
744 peer: &Peer,
745 names: &[String],
746 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
747 let ids: Vec<Bytes32> = names
748 .iter()
749 .map(|n| translate::parse_bytes32(n))
750 .collect::<Result<_, _>>()?;
751
752 let response = tokio::time::timeout(self.request_timeout, {
753 peer.request_coin_state(ids, None, self.genesis_challenge(), false)
754 })
755 .await
756 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
757 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
758 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
759
760 Ok(translate::coin_states_to_records(&response.coin_states))
761 }
762
763 async fn do_get_puzzle_and_solution(
764 &self,
765 peer: &Peer,
766 coin_id: &str,
767 height: u32,
768 ) -> Result<CoinSpend, ChiaQueryError> {
769 let id = translate::parse_bytes32(coin_id)?;
770
771 let response = tokio::time::timeout(self.request_timeout, {
772 peer.request_puzzle_and_solution(id, height)
773 })
774 .await
775 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
776 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
777 .map_err(|_| ChiaQueryError::PeerRejection("puzzle solution rejected".into()))?;
778
779 Ok(translate::make_coin_spend(
780 &chia_protocol::Coin {
786 parent_coin_info: response.coin_name,
787 puzzle_hash: Bytes32::default(),
788 amount: 0,
789 },
790 &response.puzzle,
791 &response.solution,
792 ))
793 }
794
795 async fn do_get_fee_estimate(
796 &self,
797 peer: &Peer,
798 target_times: &[u64],
799 ) -> Result<FeeEstimate, ChiaQueryError> {
800 let request = RequestFeeEstimates {
801 time_targets: target_times.to_vec(),
802 };
803
804 let response: RespondFeeEstimates =
805 tokio::time::timeout(self.request_timeout, peer.request_infallible(request))
806 .await
807 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
808 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
809
810 let estimates: Vec<f64> = response
811 .estimates
812 .estimates
813 .iter()
814 .map(|e| e.estimated_fee_rate.mojos_per_clvm_cost as f64)
815 .collect();
816
817 Ok(translate::make_fee_estimate(
818 estimates,
819 target_times.to_vec(),
820 ))
821 }
822
823 async fn do_push_tx(
824 &self,
825 peer: &Peer,
826 bundle: &SpendBundle,
827 ) -> Result<TxStatus, ChiaQueryError> {
828 let proto = to_protocol_spend_bundle(bundle)?;
829
830 let ack = tokio::time::timeout(self.request_timeout, peer.send_transaction(proto))
831 .await
832 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
833 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
834
835 Ok(translate::ack_to_tx_status(ack.status))
836 }
837
838 async fn do_get_block_by_height(
841 &self,
842 peer: &Peer,
843 height: u32,
844 ) -> Result<serde_json::Value, ChiaQueryError> {
845 let proto_block = self.fetch_full_block(peer, height).await?;
846 serde_json::to_value(&proto_block)
847 .map_err(|e| ChiaQueryError::PeerConnection(format!("serialize block: {e}")))
848 }
849
850 async fn do_get_additions_and_removals_from_block(
853 &self,
854 peer: &Peer,
855 height: u32,
856 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
857 let proto_block = self.fetch_full_block(peer, height).await?;
858 block::block_additions_and_removals(&proto_block, height, self.constants())
859 }
860
861 async fn do_get_block_spends(
864 &self,
865 peer: &Peer,
866 height: u32,
867 ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
868 let proto_block = self.fetch_full_block(peer, height).await?;
869 block::block_spends(&proto_block, self.constants())
870 }
871
872 async fn fetch_full_block(
875 &self,
876 peer: &Peer,
877 height: u32,
878 ) -> Result<ProtoFullBlock, ChiaQueryError> {
879 let response = tokio::time::timeout(self.request_timeout, {
880 peer.request_fallible::<RespondBlock, RejectBlock, _>(RequestBlock {
881 height,
882 include_transaction_block: true,
883 })
884 })
885 .await
886 .map_err(|_| ChiaQueryError::PeerConnection("block request timed out".into()))?
887 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
888 .map_err(|_| ChiaQueryError::PeerRejection("block request rejected".into()))?;
889
890 Ok(response.block)
891 }
892
893 async fn do_get_block_record_by_height(
896 &self,
897 peer: &Peer,
898 height: u32,
899 ) -> Result<BlockRecord, ChiaQueryError> {
900 let response = tokio::time::timeout(self.request_timeout, {
901 peer.request_fallible::<RespondBlockHeader, RejectHeaderRequest, _>(
902 RequestBlockHeader { height },
903 )
904 })
905 .await
906 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
907 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
908 .map_err(|_| ChiaQueryError::PeerRejection("header request rejected".into()))?;
909
910 Ok(translate::header_block_to_block_record(
911 &response.header_block,
912 ))
913 }
914
915 async fn do_get_additions_and_removals(
918 &self,
919 peer: &Peer,
920 height: u32,
921 header_hash_hex: &str,
922 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
923 let header_hash = translate::parse_bytes32(header_hash_hex)?;
924
925 let (adds_result, rems_result) = tokio::join!(
927 tokio::time::timeout(self.request_timeout, {
928 peer.request_fallible::<RespondAdditions, RejectAdditionsRequest, _>(
929 RequestAdditions {
930 height,
931 header_hash: Some(header_hash),
932 puzzle_hashes: None,
933 },
934 )
935 }),
936 tokio::time::timeout(self.request_timeout, {
937 peer.request_fallible::<RespondRemovals, RejectRemovalsRequest, _>(
938 RequestRemovals {
939 height,
940 header_hash,
941 coin_names: None,
942 },
943 )
944 }),
945 );
946
947 let adds = adds_result
948 .map_err(|_| ChiaQueryError::PeerConnection("additions request timed out".into()))?
949 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
950 .map_err(|_| ChiaQueryError::PeerRejection("additions rejected".into()))?;
951
952 let rems = rems_result
953 .map_err(|_| ChiaQueryError::PeerConnection("removals request timed out".into()))?
954 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
955 .map_err(|_| ChiaQueryError::PeerRejection("removals rejected".into()))?;
956
957 Ok(translate::additions_removals_to_response(
958 &adds, &rems, height,
959 ))
960 }
961
962 async fn do_get_children(
965 &self,
966 peer: &Peer,
967 parent_id: &str,
968 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
969 let coin_name = translate::parse_bytes32(parent_id)?;
970
971 let response = tokio::time::timeout(self.request_timeout, peer.request_children(coin_name))
972 .await
973 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
974 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
975
976 Ok(translate::coin_states_to_records(&response.coin_states))
977 }
978}
979
980fn to_protocol_spend_bundle(bundle: &SpendBundle) -> Result<ProtoBundle, ChiaQueryError> {
985 let coin_spends: Vec<chia_protocol::CoinSpend> = bundle
986 .coin_spends
987 .iter()
988 .map(|cs| {
989 Ok(chia_protocol::CoinSpend {
990 coin: chia_protocol::Coin {
991 parent_coin_info: translate::parse_bytes32(&cs.coin.parent_coin_info)?,
992 puzzle_hash: translate::parse_bytes32(&cs.coin.puzzle_hash)?,
993 amount: cs.coin.amount,
994 },
995 puzzle_reveal: chia_protocol::Program::from(chia_protocol::Bytes::from(
996 translate::parse_hex(&cs.puzzle_reveal)?,
997 )),
998 solution: chia_protocol::Program::from(chia_protocol::Bytes::from(
999 translate::parse_hex(&cs.solution)?,
1000 )),
1001 })
1002 })
1003 .collect::<Result<_, ChiaQueryError>>()?;
1004
1005 let sig_bytes = translate::parse_hex(&bundle.aggregated_signature)?;
1006 let sig_arr: [u8; 96] = sig_bytes
1007 .try_into()
1008 .map_err(|_| ChiaQueryError::InvalidRequest("signature must be 96 bytes".into()))?;
1009 let aggregated_signature = chia_bls::Signature::from_bytes(&sig_arr)
1010 .map_err(|e| ChiaQueryError::InvalidRequest(format!("bad BLS signature: {e}")))?;
1011
1012 Ok(ProtoBundle {
1013 coin_spends,
1014 aggregated_signature,
1015 })
1016}