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