1pub mod block;
2pub mod connect;
3pub mod pool;
4pub mod translate;
5
6use std::net::SocketAddr;
7use std::time::Duration;
8
9use chia_consensus::consensus_constants::ConsensusConstants;
10use chia_protocol::{
11 Bytes32, CoinStateFilters, FullBlock as ProtoFullBlock, RejectAdditionsRequest, RejectBlock,
12 RejectHeaderRequest, RejectRemovalsRequest, RequestAdditions, RequestBlock, RequestBlockHeader,
13 RequestFeeEstimates, RequestRemovals, RespondAdditions, RespondBlock, RespondBlockHeader,
14 RespondFeeEstimates, RespondRemovals, SpendBundle as ProtoBundle,
15};
16use chia_wallet_sdk::client::Peer;
17use chia_wallet_sdk::types::{MAINNET_CONSTANTS, TESTNET11_CONSTANTS};
18use tokio_tungstenite::Connector;
19
20use crate::types::*;
21use crate::NetworkType;
22use pool::PeerPool;
23pub use pool::PeerRequirement;
24
25pub struct PeerBackend {
30 pool: PeerPool,
31 network: NetworkType,
32 request_timeout: Duration,
33}
34
35impl PeerBackend {
36 pub async fn new(
37 network: crate::NetworkType,
38 tls: Connector,
39 max_peers: usize,
40 requirement: PeerRequirement,
41 connect_timeout: Duration,
42 request_timeout: Duration,
43 ) -> Result<Self, ChiaQueryError> {
44 let pool = PeerPool::new(network, tls, max_peers, requirement, connect_timeout).await?;
45 Ok(Self {
46 pool,
47 network,
48 request_timeout,
49 })
50 }
51
52 pub fn constants(&self) -> &ConsensusConstants {
54 match self.network {
55 NetworkType::Mainnet => &MAINNET_CONSTANTS,
56 NetworkType::Testnet11 => &TESTNET11_CONSTANTS,
57 }
58 }
59
60 fn genesis_challenge(&self) -> Bytes32 {
64 self.constants().genesis_challenge
65 }
66
67 pub async fn has_peers(&self) -> bool {
68 self.pool.has_peers().await
69 }
70
71 async fn pick(&self) -> Result<(Peer, SocketAddr), ChiaQueryError> {
76 self.pool.try_refill().await;
78
79 self.pool
80 .select_peer()
81 .await
82 .ok_or_else(|| ChiaQueryError::PeerConnection("no peers available".into()))
83 }
84
85 pub async fn try_get_coin_record_by_name(
91 &self,
92 name: &str,
93 ) -> Result<CoinRecord, ChiaQueryError> {
94 let (peer, addr) = self.pick().await?;
95 let res = self.do_get_coin_record_by_name(&peer, name).await;
96 if res.is_err() {
97 self.pool.eject_peer(addr).await;
98 }
99 res
100 }
101
102 pub async fn try_get_coin_record_by_name_opt(
108 &self,
109 name: &str,
110 ) -> Result<Option<CoinRecord>, ChiaQueryError> {
111 let (peer, addr) = self.pick().await?;
112 let res = self.do_get_coin_record_by_name_opt(&peer, name).await;
113 if res.is_err() {
114 self.pool.eject_peer(addr).await;
115 }
116 res
117 }
118
119 pub async fn try_get_coin_spend_opt(
125 &self,
126 coin_id: &str,
127 ) -> Result<Option<CoinSpend>, ChiaQueryError> {
128 let (peer, addr) = self.pick().await?;
129 let res = self.do_get_coin_spend_opt(&peer, coin_id).await;
130 if res.is_err() {
131 self.pool.eject_peer(addr).await;
132 }
133 res
134 }
135
136 pub async fn try_get_coin_records_by_puzzle_hash(
137 &self,
138 puzzle_hash: &str,
139 start_height: Option<u32>,
140 end_height: Option<u32>,
141 include_spent: bool,
142 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
143 let (peer, addr) = self.pick().await?;
144 let res = self
145 .do_puzzle_hash_query(
146 &peer,
147 &[puzzle_hash],
148 start_height,
149 end_height,
150 include_spent,
151 false,
152 )
153 .await;
154 if res.is_err() {
155 self.pool.eject_peer(addr).await;
156 }
157 res
158 }
159
160 pub async fn try_get_coin_records_by_puzzle_hashes(
161 &self,
162 puzzle_hashes: &[String],
163 start_height: Option<u32>,
164 end_height: Option<u32>,
165 include_spent: bool,
166 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
167 let hashes: Vec<&str> = puzzle_hashes.iter().map(String::as_str).collect();
168 let (peer, addr) = self.pick().await?;
169 let res = self
170 .do_puzzle_hash_query(
171 &peer,
172 &hashes,
173 start_height,
174 end_height,
175 include_spent,
176 false,
177 )
178 .await;
179 if res.is_err() {
180 self.pool.eject_peer(addr).await;
181 }
182 res
183 }
184
185 pub async fn try_get_coin_records_by_hint(
186 &self,
187 hint: &str,
188 start_height: Option<u32>,
189 end_height: Option<u32>,
190 include_spent: bool,
191 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
192 let (peer, addr) = self.pick().await?;
193 let res = self
194 .do_puzzle_hash_query(
195 &peer,
196 &[hint],
197 start_height,
198 end_height,
199 include_spent,
200 true,
201 )
202 .await;
203 if res.is_err() {
204 self.pool.eject_peer(addr).await;
205 }
206 res
207 }
208
209 pub async fn try_get_coin_records_by_hints(
210 &self,
211 hints: &[String],
212 start_height: Option<u32>,
213 end_height: Option<u32>,
214 include_spent: bool,
215 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
216 let hs: Vec<&str> = hints.iter().map(String::as_str).collect();
217 let (peer, addr) = self.pick().await?;
218 let res = self
219 .do_puzzle_hash_query(&peer, &hs, start_height, end_height, include_spent, true)
220 .await;
221 if res.is_err() {
222 self.pool.eject_peer(addr).await;
223 }
224 res
225 }
226
227 pub async fn try_get_coin_records_by_names(
228 &self,
229 names: &[String],
230 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
231 let (peer, addr) = self.pick().await?;
232 let res = self.do_coin_ids_query(&peer, names).await;
233 if res.is_err() {
234 self.pool.eject_peer(addr).await;
235 }
236 res
237 }
238
239 pub async fn try_get_puzzle_and_solution(
240 &self,
241 coin_id: &str,
242 height: u32,
243 ) -> Result<CoinSpend, ChiaQueryError> {
244 let (peer, addr) = self.pick().await?;
245 let res = self
246 .do_get_puzzle_and_solution(&peer, coin_id, height)
247 .await;
248 if res.is_err() {
249 self.pool.eject_peer(addr).await;
250 }
251 res
252 }
253
254 pub async fn try_get_fee_estimate(
255 &self,
256 target_times: &[u64],
257 ) -> Result<FeeEstimate, ChiaQueryError> {
258 let (peer, addr) = self.pick().await?;
259 let res = self.do_get_fee_estimate(&peer, target_times).await;
260 if res.is_err() {
261 self.pool.eject_peer(addr).await;
262 }
263 res
264 }
265
266 pub async fn try_push_tx(&self, bundle: &SpendBundle) -> Result<TxStatus, ChiaQueryError> {
267 let (peer, addr) = self.pick().await?;
268 let res = self.do_push_tx(&peer, bundle).await;
269 if res.is_err() {
270 self.pool.eject_peer(addr).await;
271 }
272 res
273 }
274
275 pub async fn try_get_block_record_by_height(
278 &self,
279 height: u32,
280 ) -> Result<BlockRecord, ChiaQueryError> {
281 let (peer, addr) = self.pick().await?;
282 let res = self.do_get_block_record_by_height(&peer, height).await;
283 if res.is_err() {
284 self.pool.eject_peer(addr).await;
285 }
286 res
287 }
288
289 #[allow(dead_code)]
295 pub async fn try_get_additions_and_removals(
296 &self,
297 height: u32,
298 header_hash: &str,
299 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
300 let (peer, addr) = self.pick().await?;
301 let res = self
302 .do_get_additions_and_removals(&peer, height, header_hash)
303 .await;
304 if res.is_err() {
305 self.pool.eject_peer(addr).await;
306 }
307 res
308 }
309
310 pub async fn try_get_children(
313 &self,
314 parent_id: &str,
315 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
316 let (peer, addr) = self.pick().await?;
317 let res = self.do_get_children(&peer, parent_id).await;
318 if res.is_err() {
319 self.pool.eject_peer(addr).await;
320 }
321 res
322 }
323
324 pub async fn try_get_block_by_height(
327 &self,
328 height: u32,
329 ) -> Result<serde_json::Value, ChiaQueryError> {
330 let (peer, addr) = self.pick().await?;
331 let res = self.do_get_block_by_height(&peer, height).await;
332 if res.is_err() {
333 self.pool.eject_peer(addr).await;
334 }
335 res
336 }
337
338 pub async fn try_get_additions_and_removals_from_block(
341 &self,
342 height: u32,
343 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
344 let (peer, addr) = self.pick().await?;
345 let res = self
346 .do_get_additions_and_removals_from_block(&peer, height)
347 .await;
348 if res.is_err() {
349 self.pool.eject_peer(addr).await;
350 }
351 res
352 }
353
354 pub async fn try_get_block_spends_by_height(
357 &self,
358 height: u32,
359 ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
360 let (peer, addr) = self.pick().await?;
361 let res = self.do_get_block_spends(&peer, height).await;
362 if res.is_err() {
363 self.pool.eject_peer(addr).await;
364 }
365 res
366 }
367
368 pub async fn try_get_block_spends_with_conditions(
371 &self,
372 height: u32,
373 ) -> Result<Vec<CoinSpendWithConditions>, ChiaQueryError> {
374 let (peer, addr) = self.pick().await?;
375 let proto_block = self.fetch_full_block(&peer, height).await;
376 if proto_block.is_err() {
377 self.pool.eject_peer(addr).await;
378 }
379 let proto_block = proto_block?;
380 block::block_spends_with_conditions(&proto_block, self.constants())
381 }
382
383 pub async fn try_get_puzzle_and_solution_auto(
386 &self,
387 coin_id: &str,
388 ) -> Result<CoinSpend, ChiaQueryError> {
389 let (peer, addr) = self.pick().await?;
391 let id = translate::parse_bytes32(coin_id)?;
392
393 let state_resp = tokio::time::timeout(self.request_timeout, {
394 peer.request_coin_state(vec![id], None, self.genesis_challenge(), false)
395 })
396 .await
397 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
398 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
399 .map_err(|_| ChiaQueryError::PeerRejection("coin state rejected".into()))?;
400
401 let cs = state_resp
402 .coin_states
403 .first()
404 .ok_or_else(|| ChiaQueryError::PeerRejection("coin not found".into()))?;
405 let spent_height = cs
406 .spent_height
407 .ok_or_else(|| ChiaQueryError::PeerRejection("coin is not spent".into()))?;
408
409 let res = self
410 .do_get_puzzle_and_solution(&peer, coin_id, spent_height)
411 .await;
412 if res.is_err() {
413 self.pool.eject_peer(addr).await;
414 }
415 res
416 }
417
418 pub async fn try_get_block_records(
421 &self,
422 start: u32,
423 end: u32,
424 ) -> Result<Vec<BlockRecord>, ChiaQueryError> {
425 let mut records = Vec::with_capacity((end - start) as usize);
426 for height in start..end {
427 records.push(self.try_get_block_record_by_height(height).await?);
428 }
429 Ok(records)
430 }
431
432 pub async fn try_get_blocks_range(
435 &self,
436 start: u32,
437 end: u32,
438 ) -> Result<Vec<serde_json::Value>, ChiaQueryError> {
439 let mut blocks = Vec::with_capacity((end - start) as usize);
440 for height in start..end {
441 blocks.push(self.try_get_block_by_height(height).await?);
442 }
443 Ok(blocks)
444 }
445
446 pub fn network_info(&self) -> NetworkInfo {
449 let c = self.constants();
450 NetworkInfo {
451 network_name: self.network.network_id().to_string(),
452 network_prefix: match self.network {
453 NetworkType::Mainnet => "xch".to_string(),
454 NetworkType::Testnet11 => "txch".to_string(),
455 },
456 genesis_challenge: format!("0x{}", hex::encode(c.genesis_challenge)),
457 }
458 }
459
460 pub fn aggsig_additional_data(&self) -> String {
463 format!(
464 "0x{}",
465 hex::encode(self.constants().agg_sig_me_additional_data)
466 )
467 }
468
469 pub fn peak_height(&self) -> u32 {
472 self.pool.peak_height()
473 }
474
475 async fn do_get_coin_record_by_name(
480 &self,
481 peer: &Peer,
482 name: &str,
483 ) -> Result<CoinRecord, ChiaQueryError> {
484 let coin_id = translate::parse_bytes32(name)?;
485
486 let response = tokio::time::timeout(self.request_timeout, {
487 peer.request_coin_state(vec![coin_id], None, self.genesis_challenge(), false)
488 })
489 .await
490 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
491 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
492 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
493
494 response
495 .coin_states
496 .first()
497 .map(translate::coin_state_to_record)
498 .ok_or_else(|| ChiaQueryError::PeerRejection("coin not found".into()))
499 }
500
501 async fn do_get_coin_record_by_name_opt(
504 &self,
505 peer: &Peer,
506 name: &str,
507 ) -> Result<Option<CoinRecord>, ChiaQueryError> {
508 let coin_id = translate::parse_bytes32(name)?;
509
510 let response = tokio::time::timeout(self.request_timeout, {
511 peer.request_coin_state(vec![coin_id], None, self.genesis_challenge(), false)
512 })
513 .await
514 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
515 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
516 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
517
518 Ok(response
520 .coin_states
521 .first()
522 .map(translate::coin_state_to_record))
523 }
524
525 async fn do_get_coin_spend_opt(
528 &self,
529 peer: &Peer,
530 coin_id: &str,
531 ) -> Result<Option<CoinSpend>, ChiaQueryError> {
532 let id = translate::parse_bytes32(coin_id)?;
533
534 let state_resp = tokio::time::timeout(self.request_timeout, {
535 peer.request_coin_state(vec![id], None, self.genesis_challenge(), false)
536 })
537 .await
538 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
539 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
540 .map_err(|_| ChiaQueryError::PeerRejection("coin state rejected".into()))?;
541
542 let Some(cs) = state_resp.coin_states.first() else {
544 return Ok(None);
545 };
546 let Some(spent_height) = cs.spent_height else {
547 return Ok(None);
548 };
549
550 let spend = self
551 .do_get_puzzle_and_solution(peer, coin_id, spent_height)
552 .await?;
553
554 let spend = CoinSpend {
561 coin: Coin::from_protocol(&cs.coin),
562 ..spend
563 };
564 Ok(Some(spend))
565 }
566
567 async fn do_puzzle_hash_query(
568 &self,
569 peer: &Peer,
570 hashes: &[&str],
571 start_height: Option<u32>,
572 end_height: Option<u32>,
573 include_spent: bool,
574 include_hinted: bool,
575 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
576 let puzzle_hashes: Vec<Bytes32> = hashes
577 .iter()
578 .map(|h| translate::parse_bytes32(h))
579 .collect::<Result<_, _>>()?;
580
581 let filters = CoinStateFilters {
582 include_spent,
583 include_unspent: true,
584 include_hinted,
585 min_amount: 0,
586 };
587
588 let mut all_states = Vec::new();
589 let mut prev_height: Option<u32> = None;
595 let mut prev_header = self.genesis_challenge();
596
597 loop {
598 let response = tokio::time::timeout(self.request_timeout, {
599 peer.request_puzzle_state(
600 puzzle_hashes.clone(),
601 prev_height,
602 prev_header,
603 filters.clone(),
604 false,
605 )
606 })
607 .await
608 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
609 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
610 .map_err(|_| ChiaQueryError::PeerRejection("puzzle state request rejected".into()))?;
611
612 all_states.extend(response.coin_states.iter().cloned());
613
614 if response.is_finished {
615 break;
616 }
617 prev_height = Some(response.height);
618 prev_header = response.header_hash;
619 }
620
621 let records: Vec<CoinRecord> = all_states
623 .iter()
624 .filter(|cs| {
625 let h = cs.created_height.unwrap_or(0);
626 let above_start = start_height.is_none_or(|s| h >= s);
627 let below_end = end_height.is_none_or(|e| h <= e);
628 above_start && below_end
629 })
630 .map(translate::coin_state_to_record)
631 .collect();
632
633 Ok(records)
634 }
635
636 async fn do_coin_ids_query(
637 &self,
638 peer: &Peer,
639 names: &[String],
640 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
641 let ids: Vec<Bytes32> = names
642 .iter()
643 .map(|n| translate::parse_bytes32(n))
644 .collect::<Result<_, _>>()?;
645
646 let response = tokio::time::timeout(self.request_timeout, {
647 peer.request_coin_state(ids, None, self.genesis_challenge(), false)
648 })
649 .await
650 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
651 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
652 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
653
654 Ok(translate::coin_states_to_records(&response.coin_states))
655 }
656
657 async fn do_get_puzzle_and_solution(
658 &self,
659 peer: &Peer,
660 coin_id: &str,
661 height: u32,
662 ) -> Result<CoinSpend, ChiaQueryError> {
663 let id = translate::parse_bytes32(coin_id)?;
664
665 let response = tokio::time::timeout(self.request_timeout, {
666 peer.request_puzzle_and_solution(id, height)
667 })
668 .await
669 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
670 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
671 .map_err(|_| ChiaQueryError::PeerRejection("puzzle solution rejected".into()))?;
672
673 Ok(translate::make_coin_spend(
674 &chia_protocol::Coin {
680 parent_coin_info: response.coin_name,
681 puzzle_hash: Bytes32::default(),
682 amount: 0,
683 },
684 &response.puzzle,
685 &response.solution,
686 ))
687 }
688
689 async fn do_get_fee_estimate(
690 &self,
691 peer: &Peer,
692 target_times: &[u64],
693 ) -> Result<FeeEstimate, ChiaQueryError> {
694 let request = RequestFeeEstimates {
695 time_targets: target_times.to_vec(),
696 };
697
698 let response: RespondFeeEstimates =
699 tokio::time::timeout(self.request_timeout, peer.request_infallible(request))
700 .await
701 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
702 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
703
704 let estimates: Vec<f64> = response
705 .estimates
706 .estimates
707 .iter()
708 .map(|e| e.estimated_fee_rate.mojos_per_clvm_cost as f64)
709 .collect();
710
711 Ok(translate::make_fee_estimate(
712 estimates,
713 target_times.to_vec(),
714 ))
715 }
716
717 async fn do_push_tx(
718 &self,
719 peer: &Peer,
720 bundle: &SpendBundle,
721 ) -> Result<TxStatus, ChiaQueryError> {
722 let proto = to_protocol_spend_bundle(bundle)?;
723
724 let ack = tokio::time::timeout(self.request_timeout, peer.send_transaction(proto))
725 .await
726 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
727 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
728
729 Ok(translate::ack_to_tx_status(ack.status))
730 }
731
732 async fn do_get_block_by_height(
735 &self,
736 peer: &Peer,
737 height: u32,
738 ) -> Result<serde_json::Value, ChiaQueryError> {
739 let proto_block = self.fetch_full_block(peer, height).await?;
740 serde_json::to_value(&proto_block)
741 .map_err(|e| ChiaQueryError::PeerConnection(format!("serialize block: {e}")))
742 }
743
744 async fn do_get_additions_and_removals_from_block(
747 &self,
748 peer: &Peer,
749 height: u32,
750 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
751 let proto_block = self.fetch_full_block(peer, height).await?;
752 block::block_additions_and_removals(&proto_block, height, self.constants())
753 }
754
755 async fn do_get_block_spends(
758 &self,
759 peer: &Peer,
760 height: u32,
761 ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
762 let proto_block = self.fetch_full_block(peer, height).await?;
763 block::block_spends(&proto_block, self.constants())
764 }
765
766 async fn fetch_full_block(
769 &self,
770 peer: &Peer,
771 height: u32,
772 ) -> Result<ProtoFullBlock, ChiaQueryError> {
773 let response = tokio::time::timeout(self.request_timeout, {
774 peer.request_fallible::<RespondBlock, RejectBlock, _>(RequestBlock {
775 height,
776 include_transaction_block: true,
777 })
778 })
779 .await
780 .map_err(|_| ChiaQueryError::PeerConnection("block request timed out".into()))?
781 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
782 .map_err(|_| ChiaQueryError::PeerRejection("block request rejected".into()))?;
783
784 Ok(response.block)
785 }
786
787 async fn do_get_block_record_by_height(
790 &self,
791 peer: &Peer,
792 height: u32,
793 ) -> Result<BlockRecord, ChiaQueryError> {
794 let response = tokio::time::timeout(self.request_timeout, {
795 peer.request_fallible::<RespondBlockHeader, RejectHeaderRequest, _>(
796 RequestBlockHeader { height },
797 )
798 })
799 .await
800 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
801 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
802 .map_err(|_| ChiaQueryError::PeerRejection("header request rejected".into()))?;
803
804 Ok(translate::header_block_to_block_record(
805 &response.header_block,
806 ))
807 }
808
809 async fn do_get_additions_and_removals(
812 &self,
813 peer: &Peer,
814 height: u32,
815 header_hash_hex: &str,
816 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
817 let header_hash = translate::parse_bytes32(header_hash_hex)?;
818
819 let (adds_result, rems_result) = tokio::join!(
821 tokio::time::timeout(self.request_timeout, {
822 peer.request_fallible::<RespondAdditions, RejectAdditionsRequest, _>(
823 RequestAdditions {
824 height,
825 header_hash: Some(header_hash),
826 puzzle_hashes: None,
827 },
828 )
829 }),
830 tokio::time::timeout(self.request_timeout, {
831 peer.request_fallible::<RespondRemovals, RejectRemovalsRequest, _>(
832 RequestRemovals {
833 height,
834 header_hash,
835 coin_names: None,
836 },
837 )
838 }),
839 );
840
841 let adds = adds_result
842 .map_err(|_| ChiaQueryError::PeerConnection("additions request timed out".into()))?
843 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
844 .map_err(|_| ChiaQueryError::PeerRejection("additions rejected".into()))?;
845
846 let rems = rems_result
847 .map_err(|_| ChiaQueryError::PeerConnection("removals request timed out".into()))?
848 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
849 .map_err(|_| ChiaQueryError::PeerRejection("removals rejected".into()))?;
850
851 Ok(translate::additions_removals_to_response(
852 &adds, &rems, height,
853 ))
854 }
855
856 async fn do_get_children(
859 &self,
860 peer: &Peer,
861 parent_id: &str,
862 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
863 let coin_name = translate::parse_bytes32(parent_id)?;
864
865 let response = tokio::time::timeout(self.request_timeout, peer.request_children(coin_name))
866 .await
867 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
868 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
869
870 Ok(translate::coin_states_to_records(&response.coin_states))
871 }
872}
873
874fn to_protocol_spend_bundle(bundle: &SpendBundle) -> Result<ProtoBundle, ChiaQueryError> {
879 let coin_spends: Vec<chia_protocol::CoinSpend> = bundle
880 .coin_spends
881 .iter()
882 .map(|cs| {
883 Ok(chia_protocol::CoinSpend {
884 coin: chia_protocol::Coin {
885 parent_coin_info: translate::parse_bytes32(&cs.coin.parent_coin_info)?,
886 puzzle_hash: translate::parse_bytes32(&cs.coin.puzzle_hash)?,
887 amount: cs.coin.amount,
888 },
889 puzzle_reveal: chia_protocol::Program::from(chia_protocol::Bytes::from(
890 translate::parse_hex(&cs.puzzle_reveal)?,
891 )),
892 solution: chia_protocol::Program::from(chia_protocol::Bytes::from(
893 translate::parse_hex(&cs.solution)?,
894 )),
895 })
896 })
897 .collect::<Result<_, ChiaQueryError>>()?;
898
899 let sig_bytes = translate::parse_hex(&bundle.aggregated_signature)?;
900 let sig_arr: [u8; 96] = sig_bytes
901 .try_into()
902 .map_err(|_| ChiaQueryError::InvalidRequest("signature must be 96 bytes".into()))?;
903 let aggregated_signature = chia_bls::Signature::from_bytes(&sig_arr)
904 .map_err(|e| ChiaQueryError::InvalidRequest(format!("bad BLS signature: {e}")))?;
905
906 Ok(ProtoBundle {
907 coin_spends,
908 aggregated_signature,
909 })
910}