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 pub async fn peer_count(&self) -> usize {
73 self.pool.peer_count().await
74 }
75
76 pub async fn independent_peer_count(&self) -> usize {
78 self.pool.independent_peer_count().await
79 }
80
81 async fn pick(&self) -> Result<(Peer, SocketAddr), ChiaQueryError> {
86 self.pool.try_refill().await;
88
89 self.pool
90 .select_peer()
91 .await
92 .ok_or_else(|| ChiaQueryError::PeerConnection("no peers available".into()))
93 }
94
95 pub async fn try_get_coin_record_by_name(
101 &self,
102 name: &str,
103 ) -> Result<CoinRecord, ChiaQueryError> {
104 let (peer, addr) = self.pick().await?;
105 let res = self.do_get_coin_record_by_name(&peer, name).await;
106 if res.is_err() {
107 self.pool.eject_peer(addr).await;
108 }
109 res
110 }
111
112 pub async fn try_get_coin_record_by_name_opt(
118 &self,
119 name: &str,
120 ) -> Result<Option<CoinRecord>, ChiaQueryError> {
121 let (peer, addr) = self.pick().await?;
122 let res = self.do_get_coin_record_by_name_opt(&peer, name).await;
123 if res.is_err() {
124 self.pool.eject_peer(addr).await;
125 }
126 res
127 }
128
129 pub async fn try_get_coin_spend_opt(
135 &self,
136 coin_id: &str,
137 ) -> Result<Option<CoinSpend>, ChiaQueryError> {
138 let (peer, addr) = self.pick().await?;
139 let res = self.do_get_coin_spend_opt(&peer, coin_id).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_records_by_puzzle_hash(
147 &self,
148 puzzle_hash: &str,
149 start_height: Option<u32>,
150 end_height: Option<u32>,
151 include_spent: bool,
152 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
153 let (peer, addr) = self.pick().await?;
154 let res = self
155 .do_puzzle_hash_query(
156 &peer,
157 &[puzzle_hash],
158 start_height,
159 end_height,
160 include_spent,
161 false,
162 )
163 .await;
164 if res.is_err() {
165 self.pool.eject_peer(addr).await;
166 }
167 res
168 }
169
170 pub async fn try_get_coin_records_by_puzzle_hashes(
171 &self,
172 puzzle_hashes: &[String],
173 start_height: Option<u32>,
174 end_height: Option<u32>,
175 include_spent: bool,
176 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
177 let hashes: Vec<&str> = puzzle_hashes.iter().map(String::as_str).collect();
178 let (peer, addr) = self.pick().await?;
179 let res = self
180 .do_puzzle_hash_query(
181 &peer,
182 &hashes,
183 start_height,
184 end_height,
185 include_spent,
186 false,
187 )
188 .await;
189 if res.is_err() {
190 self.pool.eject_peer(addr).await;
191 }
192 res
193 }
194
195 pub async fn try_get_coin_records_by_hint(
196 &self,
197 hint: &str,
198 start_height: Option<u32>,
199 end_height: Option<u32>,
200 include_spent: bool,
201 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
202 let (peer, addr) = self.pick().await?;
203 let res = self
204 .do_puzzle_hash_query(
205 &peer,
206 &[hint],
207 start_height,
208 end_height,
209 include_spent,
210 true,
211 )
212 .await;
213 if res.is_err() {
214 self.pool.eject_peer(addr).await;
215 }
216 res
217 }
218
219 pub async fn try_get_coin_records_by_hints(
220 &self,
221 hints: &[String],
222 start_height: Option<u32>,
223 end_height: Option<u32>,
224 include_spent: bool,
225 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
226 let hs: Vec<&str> = hints.iter().map(String::as_str).collect();
227 let (peer, addr) = self.pick().await?;
228 let res = self
229 .do_puzzle_hash_query(&peer, &hs, start_height, end_height, include_spent, true)
230 .await;
231 if res.is_err() {
232 self.pool.eject_peer(addr).await;
233 }
234 res
235 }
236
237 pub async fn try_get_coin_records_by_names(
238 &self,
239 names: &[String],
240 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
241 let (peer, addr) = self.pick().await?;
242 let res = self.do_coin_ids_query(&peer, names).await;
243 if res.is_err() {
244 self.pool.eject_peer(addr).await;
245 }
246 res
247 }
248
249 pub async fn try_get_puzzle_and_solution(
250 &self,
251 coin_id: &str,
252 height: u32,
253 ) -> Result<CoinSpend, ChiaQueryError> {
254 let (peer, addr) = self.pick().await?;
255 let res = self
256 .do_get_puzzle_and_solution(&peer, coin_id, height)
257 .await;
258 if res.is_err() {
259 self.pool.eject_peer(addr).await;
260 }
261 res
262 }
263
264 pub async fn try_get_fee_estimate(
265 &self,
266 target_times: &[u64],
267 ) -> Result<FeeEstimate, ChiaQueryError> {
268 let (peer, addr) = self.pick().await?;
269 let res = self.do_get_fee_estimate(&peer, target_times).await;
270 if res.is_err() {
271 self.pool.eject_peer(addr).await;
272 }
273 res
274 }
275
276 pub async fn try_push_tx(&self, bundle: &SpendBundle) -> Result<TxStatus, ChiaQueryError> {
277 let (peer, addr) = self.pick().await?;
278 let res = self.do_push_tx(&peer, bundle).await;
279 if res.is_err() {
280 self.pool.eject_peer(addr).await;
281 }
282 res
283 }
284
285 pub async fn try_get_block_record_by_height(
288 &self,
289 height: u32,
290 ) -> Result<BlockRecord, ChiaQueryError> {
291 let (peer, addr) = self.pick().await?;
292 let res = self.do_get_block_record_by_height(&peer, height).await;
293 if res.is_err() {
294 self.pool.eject_peer(addr).await;
295 }
296 res
297 }
298
299 #[allow(dead_code)]
305 pub async fn try_get_additions_and_removals(
306 &self,
307 height: u32,
308 header_hash: &str,
309 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
310 let (peer, addr) = self.pick().await?;
311 let res = self
312 .do_get_additions_and_removals(&peer, height, header_hash)
313 .await;
314 if res.is_err() {
315 self.pool.eject_peer(addr).await;
316 }
317 res
318 }
319
320 pub async fn try_get_children(
323 &self,
324 parent_id: &str,
325 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
326 let (peer, addr) = self.pick().await?;
327 let res = self.do_get_children(&peer, parent_id).await;
328 if res.is_err() {
329 self.pool.eject_peer(addr).await;
330 }
331 res
332 }
333
334 pub async fn try_get_block_by_height(
337 &self,
338 height: u32,
339 ) -> Result<serde_json::Value, ChiaQueryError> {
340 let (peer, addr) = self.pick().await?;
341 let res = self.do_get_block_by_height(&peer, height).await;
342 if res.is_err() {
343 self.pool.eject_peer(addr).await;
344 }
345 res
346 }
347
348 pub async fn try_get_additions_and_removals_from_block(
351 &self,
352 height: u32,
353 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
354 let (peer, addr) = self.pick().await?;
355 let res = self
356 .do_get_additions_and_removals_from_block(&peer, height)
357 .await;
358 if res.is_err() {
359 self.pool.eject_peer(addr).await;
360 }
361 res
362 }
363
364 pub async fn try_get_block_spends_by_height(
367 &self,
368 height: u32,
369 ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
370 let (peer, addr) = self.pick().await?;
371 let res = self.do_get_block_spends(&peer, height).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_spends_with_conditions(
381 &self,
382 height: u32,
383 ) -> Result<Vec<CoinSpendWithConditions>, ChiaQueryError> {
384 let (peer, addr) = self.pick().await?;
385 let proto_block = self.fetch_full_block(&peer, height).await;
386 if proto_block.is_err() {
387 self.pool.eject_peer(addr).await;
388 }
389 let proto_block = proto_block?;
390 block::block_spends_with_conditions(&proto_block, self.constants())
391 }
392
393 pub async fn try_get_puzzle_and_solution_auto(
396 &self,
397 coin_id: &str,
398 ) -> Result<CoinSpend, ChiaQueryError> {
399 let (peer, addr) = self.pick().await?;
401 let id = translate::parse_bytes32(coin_id)?;
402
403 let state_resp = tokio::time::timeout(self.request_timeout, {
404 peer.request_coin_state(vec![id], None, self.genesis_challenge(), false)
405 })
406 .await
407 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
408 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
409 .map_err(|_| ChiaQueryError::PeerRejection("coin state rejected".into()))?;
410
411 let cs = state_resp
412 .coin_states
413 .first()
414 .ok_or_else(|| ChiaQueryError::PeerRejection("coin not found".into()))?;
415 let spent_height = cs
416 .spent_height
417 .ok_or_else(|| ChiaQueryError::PeerRejection("coin is not spent".into()))?;
418
419 let res = self
420 .do_get_puzzle_and_solution(&peer, coin_id, spent_height)
421 .await;
422 if res.is_err() {
423 self.pool.eject_peer(addr).await;
424 }
425 res
426 }
427
428 pub async fn try_get_block_records(
431 &self,
432 start: u32,
433 end: u32,
434 ) -> Result<Vec<BlockRecord>, ChiaQueryError> {
435 let mut records = Vec::with_capacity((end - start) as usize);
436 for height in start..end {
437 records.push(self.try_get_block_record_by_height(height).await?);
438 }
439 Ok(records)
440 }
441
442 pub async fn try_get_blocks_range(
445 &self,
446 start: u32,
447 end: u32,
448 ) -> Result<Vec<serde_json::Value>, ChiaQueryError> {
449 let mut blocks = Vec::with_capacity((end - start) as usize);
450 for height in start..end {
451 blocks.push(self.try_get_block_by_height(height).await?);
452 }
453 Ok(blocks)
454 }
455
456 pub fn network_info(&self) -> NetworkInfo {
459 let c = self.constants();
460 NetworkInfo {
461 network_name: self.network.network_id().to_string(),
462 network_prefix: match self.network {
463 NetworkType::Mainnet => "xch".to_string(),
464 NetworkType::Testnet11 => "txch".to_string(),
465 },
466 genesis_challenge: format!("0x{}", hex::encode(c.genesis_challenge)),
467 }
468 }
469
470 pub fn aggsig_additional_data(&self) -> String {
473 format!(
474 "0x{}",
475 hex::encode(self.constants().agg_sig_me_additional_data)
476 )
477 }
478
479 pub fn peak_height(&self) -> u32 {
482 self.pool.peak_height()
483 }
484
485 async fn do_get_coin_record_by_name(
490 &self,
491 peer: &Peer,
492 name: &str,
493 ) -> Result<CoinRecord, ChiaQueryError> {
494 let coin_id = translate::parse_bytes32(name)?;
495
496 let response = tokio::time::timeout(self.request_timeout, {
497 peer.request_coin_state(vec![coin_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 request rejected".into()))?;
503
504 response
505 .coin_states
506 .first()
507 .map(translate::coin_state_to_record)
508 .ok_or_else(|| ChiaQueryError::PeerRejection("coin not found".into()))
509 }
510
511 async fn do_get_coin_record_by_name_opt(
514 &self,
515 peer: &Peer,
516 name: &str,
517 ) -> Result<Option<CoinRecord>, ChiaQueryError> {
518 let coin_id = translate::parse_bytes32(name)?;
519
520 let response = tokio::time::timeout(self.request_timeout, {
521 peer.request_coin_state(vec![coin_id], None, self.genesis_challenge(), false)
522 })
523 .await
524 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
525 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
526 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
527
528 Ok(response
530 .coin_states
531 .first()
532 .map(translate::coin_state_to_record))
533 }
534
535 async fn do_get_coin_spend_opt(
538 &self,
539 peer: &Peer,
540 coin_id: &str,
541 ) -> Result<Option<CoinSpend>, ChiaQueryError> {
542 let id = translate::parse_bytes32(coin_id)?;
543
544 let state_resp = tokio::time::timeout(self.request_timeout, {
545 peer.request_coin_state(vec![id], None, self.genesis_challenge(), false)
546 })
547 .await
548 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
549 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
550 .map_err(|_| ChiaQueryError::PeerRejection("coin state rejected".into()))?;
551
552 let Some(cs) = state_resp.coin_states.first() else {
554 return Ok(None);
555 };
556 let Some(spent_height) = cs.spent_height else {
557 return Ok(None);
558 };
559
560 let spend = self
561 .do_get_puzzle_and_solution(peer, coin_id, spent_height)
562 .await?;
563
564 let spend = CoinSpend {
571 coin: Coin::from_protocol(&cs.coin),
572 ..spend
573 };
574 Ok(Some(spend))
575 }
576
577 async fn do_puzzle_hash_query(
578 &self,
579 peer: &Peer,
580 hashes: &[&str],
581 start_height: Option<u32>,
582 end_height: Option<u32>,
583 include_spent: bool,
584 include_hinted: bool,
585 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
586 let puzzle_hashes: Vec<Bytes32> = hashes
587 .iter()
588 .map(|h| translate::parse_bytes32(h))
589 .collect::<Result<_, _>>()?;
590
591 let filters = CoinStateFilters {
592 include_spent,
593 include_unspent: true,
594 include_hinted,
595 min_amount: 0,
596 };
597
598 let mut all_states = Vec::new();
599 let mut prev_height: Option<u32> = None;
605 let mut prev_header = self.genesis_challenge();
606
607 loop {
608 let response = tokio::time::timeout(self.request_timeout, {
609 peer.request_puzzle_state(
610 puzzle_hashes.clone(),
611 prev_height,
612 prev_header,
613 filters.clone(),
614 false,
615 )
616 })
617 .await
618 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
619 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
620 .map_err(|_| ChiaQueryError::PeerRejection("puzzle state request rejected".into()))?;
621
622 all_states.extend(response.coin_states.iter().cloned());
623
624 if response.is_finished {
625 break;
626 }
627 prev_height = Some(response.height);
628 prev_header = response.header_hash;
629 }
630
631 let records: Vec<CoinRecord> = all_states
633 .iter()
634 .filter(|cs| {
635 let h = cs.created_height.unwrap_or(0);
636 let above_start = start_height.is_none_or(|s| h >= s);
637 let below_end = end_height.is_none_or(|e| h <= e);
638 above_start && below_end
639 })
640 .map(translate::coin_state_to_record)
641 .collect();
642
643 Ok(records)
644 }
645
646 async fn do_coin_ids_query(
647 &self,
648 peer: &Peer,
649 names: &[String],
650 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
651 let ids: Vec<Bytes32> = names
652 .iter()
653 .map(|n| translate::parse_bytes32(n))
654 .collect::<Result<_, _>>()?;
655
656 let response = tokio::time::timeout(self.request_timeout, {
657 peer.request_coin_state(ids, None, self.genesis_challenge(), false)
658 })
659 .await
660 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
661 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
662 .map_err(|_| ChiaQueryError::PeerRejection("coin state request rejected".into()))?;
663
664 Ok(translate::coin_states_to_records(&response.coin_states))
665 }
666
667 async fn do_get_puzzle_and_solution(
668 &self,
669 peer: &Peer,
670 coin_id: &str,
671 height: u32,
672 ) -> Result<CoinSpend, ChiaQueryError> {
673 let id = translate::parse_bytes32(coin_id)?;
674
675 let response = tokio::time::timeout(self.request_timeout, {
676 peer.request_puzzle_and_solution(id, height)
677 })
678 .await
679 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
680 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
681 .map_err(|_| ChiaQueryError::PeerRejection("puzzle solution rejected".into()))?;
682
683 Ok(translate::make_coin_spend(
684 &chia::protocol::Coin {
690 parent_coin_info: response.coin_name,
691 puzzle_hash: Bytes32::default(),
692 amount: 0,
693 },
694 &response.puzzle,
695 &response.solution,
696 ))
697 }
698
699 async fn do_get_fee_estimate(
700 &self,
701 peer: &Peer,
702 target_times: &[u64],
703 ) -> Result<FeeEstimate, ChiaQueryError> {
704 let request = RequestFeeEstimates {
705 time_targets: target_times.to_vec(),
706 };
707
708 let response: RespondFeeEstimates =
709 tokio::time::timeout(self.request_timeout, peer.request_infallible(request))
710 .await
711 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
712 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
713
714 let estimates: Vec<f64> = response
715 .estimates
716 .estimates
717 .iter()
718 .map(|e| e.estimated_fee_rate.mojos_per_clvm_cost as f64)
719 .collect();
720
721 Ok(translate::make_fee_estimate(
722 estimates,
723 target_times.to_vec(),
724 ))
725 }
726
727 async fn do_push_tx(
728 &self,
729 peer: &Peer,
730 bundle: &SpendBundle,
731 ) -> Result<TxStatus, ChiaQueryError> {
732 let proto = to_protocol_spend_bundle(bundle)?;
733
734 let ack = tokio::time::timeout(self.request_timeout, peer.send_transaction(proto))
735 .await
736 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
737 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
738
739 Ok(translate::ack_to_tx_status(ack.status))
740 }
741
742 async fn do_get_block_by_height(
745 &self,
746 peer: &Peer,
747 height: u32,
748 ) -> Result<serde_json::Value, ChiaQueryError> {
749 let proto_block = self.fetch_full_block(peer, height).await?;
750 serde_json::to_value(&proto_block)
751 .map_err(|e| ChiaQueryError::PeerConnection(format!("serialize block: {e}")))
752 }
753
754 async fn do_get_additions_and_removals_from_block(
757 &self,
758 peer: &Peer,
759 height: u32,
760 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
761 let proto_block = self.fetch_full_block(peer, height).await?;
762 block::block_additions_and_removals(&proto_block, height, self.constants())
763 }
764
765 async fn do_get_block_spends(
768 &self,
769 peer: &Peer,
770 height: u32,
771 ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
772 let proto_block = self.fetch_full_block(peer, height).await?;
773 block::block_spends(&proto_block, self.constants())
774 }
775
776 async fn fetch_full_block(
779 &self,
780 peer: &Peer,
781 height: u32,
782 ) -> Result<ProtoFullBlock, ChiaQueryError> {
783 let response = tokio::time::timeout(self.request_timeout, {
784 peer.request_fallible::<RespondBlock, RejectBlock, _>(RequestBlock {
785 height,
786 include_transaction_block: true,
787 })
788 })
789 .await
790 .map_err(|_| ChiaQueryError::PeerConnection("block request timed out".into()))?
791 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
792 .map_err(|_| ChiaQueryError::PeerRejection("block request rejected".into()))?;
793
794 Ok(response.block)
795 }
796
797 async fn do_get_block_record_by_height(
800 &self,
801 peer: &Peer,
802 height: u32,
803 ) -> Result<BlockRecord, ChiaQueryError> {
804 let response = tokio::time::timeout(self.request_timeout, {
805 peer.request_fallible::<RespondBlockHeader, RejectHeaderRequest, _>(
806 RequestBlockHeader { height },
807 )
808 })
809 .await
810 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
811 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
812 .map_err(|_| ChiaQueryError::PeerRejection("header request rejected".into()))?;
813
814 Ok(translate::header_block_to_block_record(
815 &response.header_block,
816 ))
817 }
818
819 async fn do_get_additions_and_removals(
822 &self,
823 peer: &Peer,
824 height: u32,
825 header_hash_hex: &str,
826 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
827 let header_hash = translate::parse_bytes32(header_hash_hex)?;
828
829 let (adds_result, rems_result) = tokio::join!(
831 tokio::time::timeout(self.request_timeout, {
832 peer.request_fallible::<RespondAdditions, RejectAdditionsRequest, _>(
833 RequestAdditions {
834 height,
835 header_hash: Some(header_hash),
836 puzzle_hashes: None,
837 },
838 )
839 }),
840 tokio::time::timeout(self.request_timeout, {
841 peer.request_fallible::<RespondRemovals, RejectRemovalsRequest, _>(
842 RequestRemovals {
843 height,
844 header_hash,
845 coin_names: None,
846 },
847 )
848 }),
849 );
850
851 let adds = adds_result
852 .map_err(|_| ChiaQueryError::PeerConnection("additions request timed out".into()))?
853 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
854 .map_err(|_| ChiaQueryError::PeerRejection("additions rejected".into()))?;
855
856 let rems = rems_result
857 .map_err(|_| ChiaQueryError::PeerConnection("removals request timed out".into()))?
858 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?
859 .map_err(|_| ChiaQueryError::PeerRejection("removals rejected".into()))?;
860
861 Ok(translate::additions_removals_to_response(
862 &adds, &rems, height,
863 ))
864 }
865
866 async fn do_get_children(
869 &self,
870 peer: &Peer,
871 parent_id: &str,
872 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
873 let coin_name = translate::parse_bytes32(parent_id)?;
874
875 let response = tokio::time::timeout(self.request_timeout, peer.request_children(coin_name))
876 .await
877 .map_err(|_| ChiaQueryError::PeerConnection("request timed out".into()))?
878 .map_err(|e| ChiaQueryError::PeerConnection(e.to_string()))?;
879
880 Ok(translate::coin_states_to_records(&response.coin_states))
881 }
882}
883
884fn to_protocol_spend_bundle(bundle: &SpendBundle) -> Result<ProtoBundle, ChiaQueryError> {
889 let coin_spends: Vec<chia::protocol::CoinSpend> = bundle
890 .coin_spends
891 .iter()
892 .map(|cs| {
893 Ok(chia::protocol::CoinSpend {
894 coin: chia::protocol::Coin {
895 parent_coin_info: translate::parse_bytes32(&cs.coin.parent_coin_info)?,
896 puzzle_hash: translate::parse_bytes32(&cs.coin.puzzle_hash)?,
897 amount: cs.coin.amount,
898 },
899 puzzle_reveal: chia::protocol::Program::from(chia::protocol::Bytes::from(
900 translate::parse_hex(&cs.puzzle_reveal)?,
901 )),
902 solution: chia::protocol::Program::from(chia::protocol::Bytes::from(
903 translate::parse_hex(&cs.solution)?,
904 )),
905 })
906 })
907 .collect::<Result<_, ChiaQueryError>>()?;
908
909 let sig_bytes = translate::parse_hex(&bundle.aggregated_signature)?;
910 let sig_arr: [u8; 96] = sig_bytes
911 .try_into()
912 .map_err(|_| ChiaQueryError::InvalidRequest("signature must be 96 bytes".into()))?;
913 let aggregated_signature = chia::bls::Signature::from_bytes(&sig_arr)
914 .map_err(|e| ChiaQueryError::InvalidRequest(format!("bad BLS signature: {e}")))?;
915
916 Ok(ProtoBundle {
917 coin_spends,
918 aggregated_signature,
919 })
920}