1use std::collections::HashMap;
6
7use chia_consensus::consensus_constants::ConsensusConstants;
8use chia_consensus::flags::DONT_VALIDATE_SIGNATURE;
9use serde_json::Value;
10
11use crate::coinset::CoinsetClient;
12use crate::peer::{OptAnswer, PeerBackend};
13use crate::types::*;
14
15#[cfg(test)]
16mod absence_tests;
17
18fn run_puzzle_conditions(spend: &CoinSpend, constants: &ConsensusConstants) -> Vec<Condition> {
25 let flags = DONT_VALIDATE_SIGNATURE;
26 let Ok(puzzle_bytes) = crate::peer::translate::parse_hex(&spend.puzzle_reveal) else {
27 return Vec::new();
28 };
29 let Ok(solution_bytes) = crate::peer::translate::parse_hex(&spend.solution) else {
30 return Vec::new();
31 };
32
33 let mut allocator = chia_consensus::allocator::make_allocator(flags);
34
35 let Ok(puzzle_node) = clvmr::serde::node_from_bytes(&mut allocator, &puzzle_bytes) else {
36 return Vec::new();
37 };
38 let Ok(solution_node) = clvmr::serde::node_from_bytes(&mut allocator, &solution_bytes) else {
39 return Vec::new();
40 };
41
42 let dialect = clvmr::chia_dialect::ChiaDialect::new(flags);
43 match clvmr::run_program::run_program(
44 &mut allocator,
45 &dialect,
46 puzzle_node,
47 solution_node,
48 constants.max_block_cost_clvm,
49 ) {
50 Ok(clvmr::reduction::Reduction(_, output)) => {
51 crate::peer::block::parse_conditions_public(&allocator, output)
52 }
53 Err(_) => Vec::new(),
54 }
55}
56
57fn settle_uncorroborated_absence<T>(
67 coinset: Option<Result<Option<T>, ChiaQueryError>>,
68) -> Result<Option<T>, ChiaQueryError> {
69 match coinset {
70 None => Err(ChiaQueryError::UncorroboratedAbsence(
71 "one peer reported absence, no second peer was available, and the coinset fallback is \
72 disabled"
73 .into(),
74 )),
75 Some(Ok(None)) => Ok(None),
76 Some(Ok(Some(_))) => Err(ChiaQueryError::SourcesDisagree(
77 "a peer reports absent, the coinset API reports present".into(),
78 )),
79 Some(Err(e)) => Err(ChiaQueryError::UncorroboratedAbsence(format!(
80 "one peer reported absence and the coinset API could not corroborate it: {e}"
81 ))),
82 }
83}
84
85pub struct QueryRouter {
86 pub(crate) peer: PeerBackend,
87 pub(crate) coinset: CoinsetClient,
88 pub(crate) coinset_fallback_enabled: bool,
89}
90
91impl QueryRouter {
96 async fn peer_then_coinset<T>(
99 &self,
100 peer_fn: impl std::future::Future<Output = Result<T, ChiaQueryError>>,
101 peer_retry: impl std::future::Future<Output = Result<T, ChiaQueryError>>,
102 coinset_fn: impl std::future::Future<Output = Result<T, ChiaQueryError>>,
103 ) -> Result<T, ChiaQueryError> {
104 match peer_fn.await {
106 Ok(v) => return Ok(v),
107 Err(e) => log::debug!("peer attempt 1 failed: {e}"),
108 }
109
110 match peer_retry.await {
112 Ok(v) => Ok(v),
113 Err(peer_err) => {
114 if !self.coinset_fallback_enabled {
115 return Err(peer_err);
116 }
117 coinset_fn
119 .await
120 .map_err(|ce| ChiaQueryError::AllSourcesFailed {
121 peer_error: Box::new(peer_err),
122 coinset_error: Some(Box::new(ce)),
123 })
124 }
125 }
126 }
127
128 async fn peer_then_coinset_opt<T>(
155 &self,
156 peer_fn: impl std::future::Future<Output = Result<OptAnswer<T>, ChiaQueryError>>,
157 peer_retry: impl std::future::Future<Output = Result<OptAnswer<T>, ChiaQueryError>>,
158 coinset_fn: impl std::future::Future<Output = Result<Option<T>, ChiaQueryError>>,
159 ) -> Result<Option<T>, ChiaQueryError> {
160 let first = match peer_fn.await {
161 Ok(answer) => Some(answer),
162 Err(e) => {
163 log::debug!("peer opt attempt 1 failed: {e}");
164 None
165 }
166 };
167
168 if let Some(answer) = first {
169 return self.settle_peer_answer(answer, coinset_fn).await;
170 }
171
172 match peer_retry.await {
173 Ok(answer) => self.settle_peer_answer(answer, coinset_fn).await,
174 Err(peer_err) => {
175 if !self.coinset_fallback_enabled {
176 return Err(peer_err);
177 }
178 coinset_fn
179 .await
180 .map_err(|ce| ChiaQueryError::AllSourcesFailed {
181 peer_error: Box::new(peer_err),
182 coinset_error: Some(Box::new(ce)),
183 })
184 }
185 }
186 }
187
188 async fn settle_peer_answer<T>(
191 &self,
192 answer: OptAnswer<T>,
193 coinset_fn: impl std::future::Future<Output = Result<Option<T>, ChiaQueryError>>,
194 ) -> Result<Option<T>, ChiaQueryError> {
195 match answer {
196 OptAnswer::Found(v) => Ok(Some(v)),
197 OptAnswer::CorroboratedAbsent => Ok(None),
198 OptAnswer::UncorroboratedAbsent => {
199 let coinset = if self.coinset_fallback_enabled {
200 Some(coinset_fn.await)
201 } else {
202 None
203 };
204 settle_uncorroborated_absence(coinset)
205 }
206 }
207 }
208
209 fn require_coinset(&self, endpoint: &str) -> Result<(), ChiaQueryError> {
211 if !self.coinset_fallback_enabled {
212 Err(ChiaQueryError::UnsupportedWithoutCoinset(endpoint.into()))
213 } else {
214 Ok(())
215 }
216 }
217}
218
219impl QueryRouter {
224 pub async fn get_additions_and_removals(
228 &self,
229 header_hash: &str,
230 ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
231 if let Ok(record) = self.get_block_record(header_hash).await {
233 match self
235 .peer
236 .try_get_additions_and_removals_from_block(record.height)
237 .await
238 {
239 Ok(r) => return Ok(r),
240 Err(e) => log::debug!("peer additions_and_removals failed: {e}"),
241 }
242 }
243 if self.coinset_fallback_enabled {
245 self.coinset.get_additions_and_removals(header_hash).await
246 } else {
247 Err(ChiaQueryError::UnsupportedWithoutCoinset(
248 "get_additions_and_removals".into(),
249 ))
250 }
251 }
252
253 pub async fn get_block(&self, header_hash: &str) -> Result<FullBlock, ChiaQueryError> {
254 if let Ok(record) = self.get_block_record(header_hash).await {
256 match self.peer.try_get_block_by_height(record.height).await {
257 Ok(b) => return Ok(b),
258 Err(e) => log::debug!("peer get_block failed: {e}"),
259 }
260 }
261 if self.coinset_fallback_enabled {
262 self.coinset.get_block(header_hash).await
263 } else {
264 Err(ChiaQueryError::UnsupportedWithoutCoinset(
265 "get_block".into(),
266 ))
267 }
268 }
269
270 pub async fn get_block_by_height(&self, height: u32) -> Result<FullBlock, ChiaQueryError> {
272 self.peer_then_coinset(
273 self.peer.try_get_block_by_height(height),
274 self.peer.try_get_block_by_height(height),
275 async {
276 let record = self.coinset.get_block_record_by_height(height).await?;
279 self.coinset.get_block(&record.header_hash).await
280 },
281 )
282 .await
283 }
284
285 pub async fn get_block_count_metrics(&self) -> Result<BlockCountMetrics, ChiaQueryError> {
286 self.require_coinset("get_block_count_metrics")?;
287 self.coinset.get_block_count_metrics().await
288 }
289
290 pub async fn get_block_record(&self, header_hash: &str) -> Result<BlockRecord, ChiaQueryError> {
291 self.require_coinset("get_block_record")?;
292 self.coinset.get_block_record(header_hash).await
293 }
294
295 pub async fn get_block_record_by_height(
298 &self,
299 height: u32,
300 ) -> Result<BlockRecord, ChiaQueryError> {
301 self.peer_then_coinset(
302 self.peer.try_get_block_record_by_height(height),
303 self.peer.try_get_block_record_by_height(height),
304 self.coinset.get_block_record_by_height(height),
305 )
306 .await
307 }
308
309 pub async fn get_block_records(
310 &self,
311 start: u32,
312 end: u32,
313 ) -> Result<Vec<BlockRecord>, ChiaQueryError> {
314 self.peer_then_coinset(
315 self.peer.try_get_block_records(start, end),
316 self.peer.try_get_block_records(start, end),
317 self.coinset.get_block_records(start, end),
318 )
319 .await
320 }
321
322 pub async fn get_block_spends(
325 &self,
326 header_hash: &str,
327 ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
328 if let Ok(record) = self.get_block_record(header_hash).await {
330 match self
331 .peer
332 .try_get_block_spends_by_height(record.height)
333 .await
334 {
335 Ok(r) => return Ok(r),
336 Err(e) => log::debug!("peer block_spends failed: {e}"),
337 }
338 }
339 if self.coinset_fallback_enabled {
340 self.coinset.get_block_spends(header_hash).await
341 } else {
342 Err(ChiaQueryError::UnsupportedWithoutCoinset(
343 "get_block_spends".into(),
344 ))
345 }
346 }
347
348 pub async fn get_block_spends_with_conditions(
351 &self,
352 header_hash: &str,
353 ) -> Result<Vec<CoinSpendWithConditions>, ChiaQueryError> {
354 if let Ok(record) = self.get_block_record(header_hash).await {
355 match self
356 .peer
357 .try_get_block_spends_with_conditions(record.height)
358 .await
359 {
360 Ok(r) => return Ok(r),
361 Err(e) => log::debug!("peer block_spends_with_conditions failed: {e}"),
362 }
363 }
364 if self.coinset_fallback_enabled {
365 self.coinset
366 .get_block_spends_with_conditions(header_hash)
367 .await
368 } else {
369 Err(ChiaQueryError::UnsupportedWithoutCoinset(
370 "get_block_spends_with_conditions".into(),
371 ))
372 }
373 }
374
375 pub async fn get_blocks(
376 &self,
377 start: u32,
378 end: u32,
379 exclude_header_hash: bool,
380 exclude_reorged: bool,
381 ) -> Result<Vec<FullBlock>, ChiaQueryError> {
382 self.peer_then_coinset(
383 self.peer.try_get_blocks_range(start, end),
384 self.peer.try_get_blocks_range(start, end),
385 self.coinset
386 .get_blocks(start, end, exclude_header_hash, exclude_reorged),
387 )
388 .await
389 }
390
391 pub async fn get_unfinished_block_headers(
392 &self,
393 ) -> Result<Vec<UnfinishedBlockHeader>, ChiaQueryError> {
394 self.require_coinset("get_unfinished_block_headers")?;
395 self.coinset.get_unfinished_block_headers().await
396 }
397}
398
399impl QueryRouter {
404 pub async fn get_coin_record_by_name(&self, name: &str) -> Result<CoinRecord, ChiaQueryError> {
405 self.peer_then_coinset(
406 self.peer.try_get_coin_record_by_name(name),
407 self.peer.try_get_coin_record_by_name(name),
408 self.coinset.get_coin_record_by_name(name),
409 )
410 .await
411 }
412
413 pub async fn get_coin_record_by_name_opt(
418 &self,
419 name: &str,
420 ) -> Result<Option<CoinRecord>, ChiaQueryError> {
421 self.peer_then_coinset_opt(
422 self.peer.try_get_coin_record_by_name_opt(name),
423 self.peer.try_get_coin_record_by_name_opt(name),
424 self.coinset.get_coin_record_by_name_opt(name),
425 )
426 .await
427 }
428
429 pub async fn get_coin_spend_opt(
432 &self,
433 coin_id: &str,
434 ) -> Result<Option<CoinSpend>, ChiaQueryError> {
435 self.peer_then_coinset_opt(
436 self.peer.try_get_coin_spend_opt(coin_id),
437 self.peer.try_get_coin_spend_opt(coin_id),
438 self.coinset.get_puzzle_and_solution_opt(coin_id, None),
439 )
440 .await
441 }
442
443 pub async fn peak_height_opt(&self) -> Result<Option<u32>, ChiaQueryError> {
445 let state = self.get_blockchain_state().await?;
446 Ok(state.peak.map(|p| p.height))
447 }
448
449 pub async fn block_timestamp_opt(&self, height: u32) -> Result<Option<u64>, ChiaQueryError> {
452 let record = self.get_block_record_by_height_opt(height).await?;
453 Ok(record.and_then(|r| r.timestamp))
454 }
455
456 async fn get_block_record_by_height_opt(
458 &self,
459 height: u32,
460 ) -> Result<Option<BlockRecord>, ChiaQueryError> {
461 if let Ok(record) = self.peer.try_get_block_record_by_height(height).await {
464 return Ok(Some(record));
465 }
466 match self.peer.try_get_block_record_by_height(height).await {
467 Ok(record) => Ok(Some(record)),
468 Err(peer_err) => {
469 if self.coinset_fallback_enabled {
470 self.coinset
471 .get_block_record_by_height_opt(height)
472 .await
473 .map_err(|ce| ChiaQueryError::AllSourcesFailed {
474 peer_error: Box::new(peer_err),
475 coinset_error: Some(Box::new(ce)),
476 })
477 } else {
478 Err(peer_err)
479 }
480 }
481 }
482 }
483
484 pub async fn get_coin_records_by_hint(
485 &self,
486 hint: &str,
487 start_height: Option<u32>,
488 end_height: Option<u32>,
489 include_spent_coins: bool,
490 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
491 self.peer_then_coinset(
492 self.peer.try_get_coin_records_by_hint(
493 hint,
494 start_height,
495 end_height,
496 include_spent_coins,
497 ),
498 self.peer.try_get_coin_records_by_hint(
499 hint,
500 start_height,
501 end_height,
502 include_spent_coins,
503 ),
504 self.coinset.get_coin_records_by_hint(
505 hint,
506 start_height,
507 end_height,
508 include_spent_coins,
509 ),
510 )
511 .await
512 }
513
514 pub async fn get_coin_records_by_hints(
515 &self,
516 hints: &[String],
517 start_height: Option<u32>,
518 end_height: Option<u32>,
519 include_spent_coins: bool,
520 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
521 self.peer_then_coinset(
522 self.peer.try_get_coin_records_by_hints(
523 hints,
524 start_height,
525 end_height,
526 include_spent_coins,
527 ),
528 self.peer.try_get_coin_records_by_hints(
529 hints,
530 start_height,
531 end_height,
532 include_spent_coins,
533 ),
534 self.coinset.get_coin_records_by_hints(
535 hints,
536 start_height,
537 end_height,
538 include_spent_coins,
539 ),
540 )
541 .await
542 }
543
544 pub async fn get_coin_records_by_names(
545 &self,
546 names: &[String],
547 start_height: Option<u32>,
548 end_height: Option<u32>,
549 include_spent_coins: bool,
550 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
551 self.peer_then_coinset(
552 self.peer.try_get_coin_records_by_names(names),
553 self.peer.try_get_coin_records_by_names(names),
554 self.coinset.get_coin_records_by_names(
555 names,
556 start_height,
557 end_height,
558 include_spent_coins,
559 ),
560 )
561 .await
562 }
563
564 pub async fn get_coin_records_by_parent_ids(
568 &self,
569 parent_ids: &[String],
570 start_height: Option<u32>,
571 end_height: Option<u32>,
572 include_spent_coins: bool,
573 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
574 let peer_attempt = async {
576 let mut all_records = Vec::new();
577 for parent_id in parent_ids {
578 let children = self.peer.try_get_children(parent_id).await?;
579 all_records.extend(children);
580 }
581 all_records.retain(|r| {
583 let height_ok = match (start_height, end_height) {
584 (Some(s), Some(e)) => {
585 r.confirmed_block_index >= s && r.confirmed_block_index <= e
586 }
587 (Some(s), None) => r.confirmed_block_index >= s,
588 (None, Some(e)) => r.confirmed_block_index <= e,
589 (None, None) => true,
590 };
591 let spent_ok = include_spent_coins || !r.spent;
592 height_ok && spent_ok
593 });
594 Ok(all_records)
595 };
596
597 match peer_attempt.await {
598 Ok(r) => Ok(r),
599 Err(peer_err) => {
600 if self.coinset_fallback_enabled {
601 self.coinset
602 .get_coin_records_by_parent_ids(
603 parent_ids,
604 start_height,
605 end_height,
606 include_spent_coins,
607 )
608 .await
609 .map_err(|ce| ChiaQueryError::AllSourcesFailed {
610 peer_error: Box::new(peer_err),
611 coinset_error: Some(Box::new(ce)),
612 })
613 } else {
614 Err(peer_err)
615 }
616 }
617 }
618 }
619
620 pub async fn get_coin_records_by_puzzle_hash(
621 &self,
622 puzzle_hash: &str,
623 start_height: Option<u32>,
624 end_height: Option<u32>,
625 include_spent_coins: bool,
626 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
627 self.peer_then_coinset(
628 self.peer.try_get_coin_records_by_puzzle_hash(
629 puzzle_hash,
630 start_height,
631 end_height,
632 include_spent_coins,
633 ),
634 self.peer.try_get_coin_records_by_puzzle_hash(
635 puzzle_hash,
636 start_height,
637 end_height,
638 include_spent_coins,
639 ),
640 self.coinset.get_coin_records_by_puzzle_hash(
641 puzzle_hash,
642 start_height,
643 end_height,
644 include_spent_coins,
645 ),
646 )
647 .await
648 }
649
650 pub async fn get_coin_records_by_puzzle_hashes(
651 &self,
652 puzzle_hashes: &[String],
653 start_height: Option<u32>,
654 end_height: Option<u32>,
655 include_spent_coins: bool,
656 ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
657 self.peer_then_coinset(
658 self.peer.try_get_coin_records_by_puzzle_hashes(
659 puzzle_hashes,
660 start_height,
661 end_height,
662 include_spent_coins,
663 ),
664 self.peer.try_get_coin_records_by_puzzle_hashes(
665 puzzle_hashes,
666 start_height,
667 end_height,
668 include_spent_coins,
669 ),
670 self.coinset.get_coin_records_by_puzzle_hashes(
671 puzzle_hashes,
672 start_height,
673 end_height,
674 include_spent_coins,
675 ),
676 )
677 .await
678 }
679
680 pub async fn get_memos_by_coin_name(&self, name: &str) -> Result<Value, ChiaQueryError> {
682 self.require_coinset("get_memos_by_coin_name")?;
683 self.coinset.get_memos_by_coin_name(name).await
684 }
685
686 pub async fn get_puzzle_and_solution(
687 &self,
688 coin_id: &str,
689 height: Option<u32>,
690 ) -> Result<CoinSpend, ChiaQueryError> {
691 if let Some(h) = height {
692 self.peer_then_coinset(
693 self.peer.try_get_puzzle_and_solution(coin_id, h),
694 self.peer.try_get_puzzle_and_solution(coin_id, h),
695 self.coinset.get_puzzle_and_solution(coin_id, height),
696 )
697 .await
698 } else {
699 self.peer_then_coinset(
701 self.peer.try_get_puzzle_and_solution_auto(coin_id),
702 self.peer.try_get_puzzle_and_solution_auto(coin_id),
703 self.coinset.get_puzzle_and_solution(coin_id, None),
704 )
705 .await
706 }
707 }
708
709 pub async fn get_puzzle_and_solution_with_conditions(
712 &self,
713 coin_id: &str,
714 height: Option<u32>,
715 ) -> Result<CoinSpendWithConditions, ChiaQueryError> {
716 let spend = match self.get_puzzle_and_solution(coin_id, height).await {
718 Ok(s) => s,
719 Err(_) => {
720 if self.coinset_fallback_enabled {
721 return self
722 .coinset
723 .get_puzzle_and_solution_with_conditions(coin_id, height)
724 .await;
725 }
726 return Err(ChiaQueryError::PeerRejection(
727 "could not retrieve puzzle and solution".into(),
728 ));
729 }
730 };
731
732 let conditions = run_puzzle_conditions(&spend, self.peer.constants());
734 Ok(CoinSpendWithConditions {
735 coin_spend: spend,
736 conditions,
737 })
738 }
739
740 pub async fn push_tx(&self, bundle: &SpendBundle) -> Result<TxStatus, ChiaQueryError> {
741 self.peer_then_coinset(
742 self.peer.try_push_tx(bundle),
743 self.peer.try_push_tx(bundle),
744 self.coinset.push_tx(bundle),
745 )
746 .await
747 }
748}
749
750impl QueryRouter {
755 pub async fn get_fee_estimate(
756 &self,
757 spend_bundle: Option<&SpendBundle>,
758 target_times: Option<&[u64]>,
759 spend_count: Option<u64>,
760 ) -> Result<FeeEstimate, ChiaQueryError> {
761 let times = target_times.unwrap_or(&[60, 120, 300]);
762 self.peer_then_coinset(
763 self.peer.try_get_fee_estimate(times),
764 self.peer.try_get_fee_estimate(times),
765 self.coinset
766 .get_fee_estimate(spend_bundle, target_times, spend_count),
767 )
768 .await
769 }
770}
771
772impl QueryRouter {
777 pub async fn get_aggsig_additional_data(&self) -> Result<String, ChiaQueryError> {
780 Ok(self.peer.aggsig_additional_data())
781 }
782
783 pub async fn get_network_info(&self) -> Result<NetworkInfo, ChiaQueryError> {
786 Ok(self.peer.network_info())
787 }
788
789 pub async fn get_blockchain_state(&self) -> Result<BlockchainState, ChiaQueryError> {
792 if self.coinset_fallback_enabled {
794 if let Ok(state) = self.coinset.get_blockchain_state().await {
795 return Ok(state);
796 }
797 }
798 let peak = self.peer.peak_height();
800 if peak == 0 {
801 return Err(ChiaQueryError::PeerConnection(
802 "no peak observed from peers yet".into(),
803 ));
804 }
805 Ok(BlockchainState {
806 peak: Some(BlockRecord {
807 height: peak,
808 ..Default::default()
809 }),
810 sync: Some(SyncState {
811 synced: true,
812 sync_mode: false,
813 sync_progress_height: peak,
814 sync_tip_height: peak,
815 }),
816 ..Default::default()
817 })
818 }
819
820 pub async fn get_network_space(
821 &self,
822 newer_block_header_hash: &str,
823 older_block_header_hash: &str,
824 ) -> Result<u64, ChiaQueryError> {
825 self.require_coinset("get_network_space")?;
826 self.coinset
827 .get_network_space(newer_block_header_hash, older_block_header_hash)
828 .await
829 }
830}
831
832impl QueryRouter {
837 pub async fn get_all_mempool_items(
838 &self,
839 ) -> Result<HashMap<String, MempoolItem>, ChiaQueryError> {
840 self.require_coinset("get_all_mempool_items")?;
841 self.coinset.get_all_mempool_items().await
842 }
843
844 pub async fn get_all_mempool_tx_ids(&self) -> Result<Vec<String>, ChiaQueryError> {
845 self.require_coinset("get_all_mempool_tx_ids")?;
846 self.coinset.get_all_mempool_tx_ids().await
847 }
848
849 pub async fn get_mempool_item_by_tx_id(
850 &self,
851 tx_id: &str,
852 ) -> Result<MempoolItem, ChiaQueryError> {
853 self.require_coinset("get_mempool_item_by_tx_id")?;
854 self.coinset.get_mempool_item_by_tx_id(tx_id).await
855 }
856
857 pub async fn get_mempool_items_by_coin_name(
858 &self,
859 coin_name: &str,
860 include_spent_coins: Option<bool>,
861 ) -> Result<Vec<MempoolItem>, ChiaQueryError> {
862 self.require_coinset("get_mempool_items_by_coin_name")?;
863 self.coinset
864 .get_mempool_items_by_coin_name(coin_name, include_spent_coins)
865 .await
866 }
867}