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