Skip to main content

chia_query/
router.rs

1//! QueryRouter -- dispatches each request to the peer backend first (with one
2//! retry on a different peer) and falls back to the coinset.org HTTP API if
3//! both peer attempts fail.
4
5use 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
18// ---------------------------------------------------------------------------
19// Puzzle condition extraction helper
20// ---------------------------------------------------------------------------
21
22/// Run a puzzle against its solution (from a CoinSpend) and extract the CLVM
23/// output conditions.  Used by `get_puzzle_and_solution_with_conditions`.
24fn 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
57/// Decide what one peer's uncorroborated absence becomes, given whatever the coinset tier said.
58///
59/// `coinset` is `None` when the coinset tier was not consulted at all because the fallback is
60/// disabled — the distinction matters, since "nobody else was asked" and "somebody else was asked
61/// and agreed" are the two facts this whole change exists to keep apart.
62///
63/// Absence is only ever reported when a SECOND source says it too. Everything else is an error,
64/// and a contradiction is surfaced rather than broken in favour of either source: nothing in two
65/// contradictory answers says which one to believe.
66fn 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
91// ---------------------------------------------------------------------------
92// Internal helpers
93// ---------------------------------------------------------------------------
94
95impl QueryRouter {
96    /// Try `peer_fn` twice (each call will select a different peer because the
97    /// first failure ejects the peer).  If both fail, fall back to `coinset_fn`.
98    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        // First peer attempt
105        match peer_fn.await {
106            Ok(v) => return Ok(v),
107            Err(e) => log::debug!("peer attempt 1 failed: {e}"),
108        }
109
110        // Retry on a different peer
111        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                // Fall back to coinset
118                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    /// Absence-aware variant of [`peer_then_coinset`](Self::peer_then_coinset).
129    ///
130    /// `Ok(None)` from this router means **corroborated absence**: two independent sources were
131    /// asked and both said the thing does not exist. Absence that only one source will vouch for
132    /// is [`UncorroboratedAbsence`](ChiaQueryError::UncorroboratedAbsence) — an error, because a
133    /// caller reading `None` as "the chain provably does not have this" would otherwise be told a
134    /// falsehood by one anonymous peer's empty list (dig_ecosystem#2456).
135    ///
136    /// The peer tier grades its own answer (see
137    /// [`PeerBackend::read_opt_corroborated`](crate::peer::PeerBackend)); this method decides what
138    /// an ungraded absence becomes once the coinset tier is available to be a second voice:
139    ///
140    /// | peer tier | coinset | result |
141    /// |---|---|---|
142    /// | found | not asked | `Ok(Some)` — presence is self-verifying |
143    /// | both peers absent | not asked | `Ok(None)` |
144    /// | one peer absent | absent | `Ok(None)` — two independent sources agree |
145    /// | one peer absent | found | [`SourcesDisagree`](ChiaQueryError::SourcesDisagree) |
146    /// | one peer absent | unreachable or disabled | [`UncorroboratedAbsence`](ChiaQueryError::UncorroboratedAbsence) |
147    /// | peer read failed | any | the retry, then the coinset fallback, as before |
148    ///
149    /// **The stated limit.** When no peer answers at all, the coinset tier is the only source
150    /// there is, and its absence is returned as `Ok(None)` on its own — the behaviour a
151    /// coinset-only client has always had. That is one source, so it is one source's word; what
152    /// this method removes is absence resting on an *anonymous, unauthenticated* peer, not the
153    /// weaker claim that a single named HTTPS endpoint is infallible.
154    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    /// Turn the peer tier's graded answer into the router's contract, consulting coinset as a
189    /// second voice when — and only when — the peer tier could not find one itself.
190    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    /// For endpoints that have no peer protocol equivalent.
210    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
219// ---------------------------------------------------------------------------
220// Blocks (all coinset-only)
221// ---------------------------------------------------------------------------
222
223impl QueryRouter {
224    /// Peer-backed: fetches the full block by header_hash (via coinset to
225    /// resolve the height), then parses additions/removals from the CLVM
226    /// generator.  Falls back to the coinset endpoint on failure.
227    pub async fn get_additions_and_removals(
228        &self,
229        header_hash: &str,
230    ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
231        // The peer protocol needs a height.  Resolve it from the block record.
232        if let Ok(record) = self.get_block_record(header_hash).await {
233            // Try parsing via peer + CLVM.
234            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        // Fallback to coinset.
244        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        // Resolve height from block record, try peer first.
255        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    /// Fetch a full block by height.  Peer-backed via `RequestBlock`.
271    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                // Coinset has no direct by-height endpoint for full blocks, so
277                // resolve the header_hash first.
278                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    /// Peer-backed via `RequestBlockHeader` / `RespondBlockHeader` (pattern
296    /// from chia-block-listener).
297    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    /// Peer-backed: fetches the full block, then runs the CLVM generator to
323    /// extract every coin spend with its puzzle_reveal and solution.
324    pub async fn get_block_spends(
325        &self,
326        header_hash: &str,
327    ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
328        // Resolve height from block record.
329        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    /// Peer-backed: fetches full block, runs CLVM generator, then runs each
349    /// puzzle(solution) to extract parsed conditions.
350    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
399// ---------------------------------------------------------------------------
400// Coins (peer-backed with coinset fallback)
401// ---------------------------------------------------------------------------
402
403impl 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    /// Absence-aware [`get_coin_record_by_name`](Self::get_coin_record_by_name): a PROVABLE absence
414    /// is `Ok(None)`, every transport/rejection/parse failure is `Err`. A successful peer or coinset
415    /// response that reports no such coin is the authoritative absence; only when the peer read
416    /// itself fails does the router fall back to coinset (SPEC §3).
417    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    /// Absence-aware read of the spend that spent `coin_id`: `Ok(None)` when the coin is provably
430    /// unspent/unknown, `Err` when the read could not be completed.
431    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    /// The current peak height, or `Ok(None)` when no source exposes a peak; `Err` on failure.
444    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    /// The Unix timestamp of the block at `height`: `Ok(None)` when no such block exists or the
450    /// block carries no timestamp; `Err` on failure.
451    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    /// Absence-aware block-record read used by [`block_timestamp_opt`](Self::block_timestamp_opt).
457    async fn get_block_record_by_height_opt(
458        &self,
459        height: u32,
460    ) -> Result<Option<BlockRecord>, ChiaQueryError> {
461        // A successful peer read is authoritative; only a peer FAILURE falls through to coinset,
462        // whose null block_record is provable absence.
463        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    /// Peer-backed via `RequestChildren` / `RespondChildren` which returns
565    /// child coin states for a given parent coin ID.  Falls back to coinset
566    /// for batched queries or when peers fail.
567    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        // Try peer: query each parent ID via RequestChildren, combine results.
575        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            // Apply client-side height and spent filters.
582            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    /// No peer equivalent -- always coinset.
681    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            // No height provided -- peer can resolve it via coin state.
700            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    /// Peer-backed: get puzzle & solution, then run puzzle(solution) to extract
710    /// parsed conditions.
711    pub async fn get_puzzle_and_solution_with_conditions(
712        &self,
713        coin_id: &str,
714        height: Option<u32>,
715    ) -> Result<CoinSpendWithConditions, ChiaQueryError> {
716        // Try to get the spend via peer first.
717        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        // Run puzzle(solution) to extract conditions.
733        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
750// ---------------------------------------------------------------------------
751// Fees (peer-backed with coinset fallback)
752// ---------------------------------------------------------------------------
753
754impl 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
772// ---------------------------------------------------------------------------
773// Full node / network (all coinset-only)
774// ---------------------------------------------------------------------------
775
776impl QueryRouter {
777    /// Peer-backed: derived from the chia consensus constants for the
778    /// configured network.
779    pub async fn get_aggsig_additional_data(&self) -> Result<String, ChiaQueryError> {
780        Ok(self.peer.aggsig_additional_data())
781    }
782
783    /// Peer-backed: derived from the chia consensus constants for the
784    /// configured network.
785    pub async fn get_network_info(&self) -> Result<NetworkInfo, ChiaQueryError> {
786        Ok(self.peer.network_info())
787    }
788
789    /// Peer-backed partially: peak height is tracked from `NewPeakWallet`
790    /// messages received from peers.  Full state comes from coinset.
791    pub async fn get_blockchain_state(&self) -> Result<BlockchainState, ChiaQueryError> {
792        // Try coinset first for full state.
793        if self.coinset_fallback_enabled {
794            if let Ok(state) = self.coinset.get_blockchain_state().await {
795                return Ok(state);
796            }
797        }
798        // Fallback: return a minimal state from the peer-tracked peak.
799        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
832// ---------------------------------------------------------------------------
833// Mempool (all coinset-only)
834// ---------------------------------------------------------------------------
835
836impl 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}