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
552 let spend = CoinSpend {
559 coin: Coin::from_protocol(&cs.coin),
560 ..spend
561 };
562 Ok(Some(spend))
563 }
564
565 async fn do_puzzle_hash_query(
566 &self,
567 peer: &Peer,
568 hashes: &[&str],
569 start_height: Option<u32>,
570 end_height: Option<u32>,
571 include_spent: bool,
572 include_hinted: bool,
573 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
574 let puzzle_hashes: Vec<Bytes32> = hashes
575 .iter()
576 .map(|h| translate::parse_bytes32(h))
577 .collect::<Result<_, _>>()?;
578
579 let filters = CoinStateFilters {
580 include_spent,
581 include_unspent: true,
582 include_hinted,
583 min_amount: 0,
584 };
585
586 let mut all_states = Vec::new();
587 let mut prev_height: Option<u32> = None;
593 let mut prev_header = self.genesis_challenge();
594
595 loop {
596 let response = tokio::time::timeout(self.request_timeout, {
597 peer.request_puzzle_state(
598 puzzle_hashes.clone(),
599 prev_height,
600 prev_header,
601 filters.clone(),
602 false,
603 )
604 })
605 .await
606 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
607 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
608 .map_err(|_| ChiaQueryError::PeerRejection("puzzle state request rejected".into()))?;
609
610 all_states.extend(response.coin_states.iter().cloned());
611
612 if response.is_finished {
613 break;
614 }
615 prev_height = Some(response.height);
616 prev_header = response.header_hash;
617 }
618
619 let records: Vec<CoinRecord> = all_states
621 .iter()
622 .filter(|cs| {
623 let h = cs.created_height.unwrap_or(0);
624 let above_start = start_height.is_none_or(|s| h >= s);
625 let below_end = end_height.is_none_or(|e| h <= e);
626 above_start && below_end
627 })
628 .map(translate::coin_state_to_record)
629 .collect();
630
631 Ok(records)
632 }
633
634 async fn do_coin_ids_query(
635 &self,
636 peer: &Peer,
637 names: &[String],
638 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
639 let ids: Vec<Bytes32> = names
640 .iter()
641 .map(|n| translate::parse_bytes32(n))
642 .collect::<Result<_, _>>()?;
643
644 let response = tokio::time::timeout(self.request_timeout, {
645 peer.request_coin_state(ids, None, self.genesis_challenge(), false)
646 })
647 .await
648 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
649 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
650 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
651
652 Ok(translate::coin_states_to_records(&response.coin_states))
653 }
654
655 async fn do_get_puzzle_and_solution(
656 &self,
657 peer: &Peer,
658 coin_id: &str,
659 height: u32,
660 ) -> Result<CoinSpend, ChiaQueryError> {
661 let id = translate::parse_bytes32(coin_id)?;
662
663 let response = tokio::time::timeout(self.request_timeout, {
664 peer.request_puzzle_and_solution(id, height)
665 })
666 .await
667 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
668 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
669 .map_err(|_| ChiaQueryError::PeerRejection("puzzle solution rejected".into()))?;
670
671 Ok(translate::make_coin_spend(
672 &chia::protocol::Coin {
678 parent_coin_info: response.coin_name,
679 puzzle_hash: Bytes32::default(),
680 amount: 0,
681 },
682 &response.puzzle,
683 &response.solution,
684 ))
685 }
686
687 async fn do_get_fee_estimate(
688 &self,
689 peer: &Peer,
690 target_times: &[u64],
691 ) -> Result<FeeEstimate, ChiaQueryError> {
692 let request = RequestFeeEstimates {
693 time_targets: target_times.to_vec(),
694 };
695
696 let response: RespondFeeEstimates =
697 tokio::time::timeout(self.request_timeout, peer.request_infallible(request))
698 .await
699 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
700 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
701
702 let estimates: Vec<f64> = response
703 .estimates
704 .estimates
705 .iter()
706 .map(|e| e.estimated_fee_rate.mojos_per_clvm_cost as f64)
707 .collect();
708
709 Ok(translate::make_fee_estimate(
710 estimates,
711 target_times.to_vec(),
712 ))
713 }
714
715 async fn do_push_tx(
716 &self,
717 peer: &Peer,
718 bundle: &SpendBundle,
719 ) -> Result<TxStatus, ChiaQueryError> {
720 let proto = to_protocol_spend_bundle(bundle)?;
721
722 let ack = tokio::time::timeout(self.request_timeout, peer.send_transaction(proto))
723 .await
724 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
725 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
726
727 Ok(translate::ack_to_tx_status(ack.status))
728 }
729
730 async fn do_get_block_by_height(
733 &self,
734 peer: &Peer,
735 height: u32,
736 ) -> Result<serde_json::Value, ChiaQueryError> {
737 let proto_block = self.fetch_full_block(peer, height).await?;
738 serde_json::to_value(&proto_block)
739 .map_err(|e| ChiaQueryError::PeerConnection(format!("serialize block: {e}")))
740 }
741
742 async fn do_get_additions_and_removals_from_block(
745 &self,
746 peer: &Peer,
747 height: u32,
748 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
749 let proto_block = self.fetch_full_block(peer, height).await?;
750 block::block_additions_and_removals(&proto_block, height, self.constants())
751 }
752
753 async fn do_get_block_spends(
756 &self,
757 peer: &Peer,
758 height: u32,
759 ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
760 let proto_block = self.fetch_full_block(peer, height).await?;
761 block::block_spends(&proto_block, self.constants())
762 }
763
764 async fn fetch_full_block(
767 &self,
768 peer: &Peer,
769 height: u32,
770 ) -> Result<ProtoFullBlock, ChiaQueryError> {
771 let response = tokio::time::timeout(self.request_timeout, {
772 peer.request_fallible::<RespondBlock, RejectBlock, _>(RequestBlock {
773 height,
774 include_transaction_block: true,
775 })
776 })
777 .await
778 .map_err(|_| ChiaQueryError::PeerConnection("block request timed out".into()))?
779 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
780 .map_err(|_| ChiaQueryError::PeerRejection("block request rejected".into()))?;
781
782 Ok(response.block)
783 }
784
785 async fn do_get_block_record_by_height(
788 &self,
789 peer: &Peer,
790 height: u32,
791 ) -> Result<BlockRecord, ChiaQueryError> {
792 let response = tokio::time::timeout(self.request_timeout, {
793 peer.request_fallible::<RespondBlockHeader, RejectHeaderRequest, _>(
794 RequestBlockHeader { height },
795 )
796 })
797 .await
798 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
799 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
800 .map_err(|_| ChiaQueryError::PeerRejection("header request rejected".into()))?;
801
802 Ok(translate::header_block_to_block_record(
803 &response.header_block,
804 ))
805 }
806
807 async fn do_get_additions_and_removals(
810 &self,
811 peer: &Peer,
812 height: u32,
813 header_hash_hex: &str,
814 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
815 let header_hash = translate::parse_bytes32(header_hash_hex)?;
816
817 let (adds_result, rems_result) = tokio::join!(
819 tokio::time::timeout(self.request_timeout, {
820 peer.request_fallible::<RespondAdditions, RejectAdditionsRequest, _>(
821 RequestAdditions {
822 height,
823 header_hash: Some(header_hash),
824 puzzle_hashes: None,
825 },
826 )
827 }),
828 tokio::time::timeout(self.request_timeout, {
829 peer.request_fallible::<RespondRemovals, RejectRemovalsRequest, _>(
830 RequestRemovals {
831 height,
832 header_hash,
833 coin_names: None,
834 },
835 )
836 }),
837 );
838
839 let adds = adds_result
840 .map_err(|_| ChiaQueryError::PeerConnection("additions request timed out".into()))?
841 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
842 .map_err(|_| ChiaQueryError::PeerRejection("additions rejected".into()))?;
843
844 let rems = rems_result
845 .map_err(|_| ChiaQueryError::PeerConnection("removals request timed out".into()))?
846 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
847 .map_err(|_| ChiaQueryError::PeerRejection("removals rejected".into()))?;
848
849 Ok(translate::additions_removals_to_response(
850 &adds, &rems, height,
851 ))
852 }
853
854 async fn do_get_children(
857 &self,
858 peer: &Peer,
859 parent_id: &str,
860 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
861 let coin_name = translate::parse_bytes32(parent_id)?;
862
863 let response = tokio::time::timeout(self.request_timeout, peer.request_children(coin_name))
864 .await
865 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
866 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
867
868 Ok(translate::coin_states_to_records(&response.coin_states))
869 }
870}
871
872fn to_protocol_spend_bundle(bundle: &SpendBundle) -> Result<ProtoBundle, ChiaQueryError> {
877 let coin_spends: Vec<chia::protocol::CoinSpend> = bundle
878 .coin_spends
879 .iter()
880 .map(|cs| {
881 Ok(chia::protocol::CoinSpend {
882 coin: chia::protocol::Coin {
883 parent_coin_info: translate::parse_bytes32(&cs.coin.parent_coin_info)?,
884 puzzle_hash: translate::parse_bytes32(&cs.coin.puzzle_hash)?,
885 amount: cs.coin.amount,
886 },
887 puzzle_reveal: chia::protocol::Program::from(chia::protocol::Bytes::from(
888 translate::parse_hex(&cs.puzzle_reveal)?,
889 )),
890 solution: chia::protocol::Program::from(chia::protocol::Bytes::from(
891 translate::parse_hex(&cs.solution)?,
892 )),
893 })
894 })
895 .collect::<Result<_, ChiaQueryError>>()?;
896
897 let sig_bytes = translate::parse_hex(&bundle.aggregated_signature)?;
898 let sig_arr: [u8; 96] = sig_bytes
899 .try_into()
900 .map_err(|_| ChiaQueryError::InvalidRequest("signature must be 96 bytes".into()))?;
901 let aggregated_signature = chia::bls::Signature::from_bytes(&sig_arr)
902 .map_err(|e| ChiaQueryError::InvalidRequest(format!("bad BLS signature: {e}")))?;
903
904 Ok(ProtoBundle {
905 coin_spends,
906 aggregated_signature,
907 })
908}