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::PeerBackend;
13use crate::types::*;
14
15// ---------------------------------------------------------------------------
16// Puzzle condition extraction helper
17// ---------------------------------------------------------------------------
18
19/// Run a puzzle against its solution (from a CoinSpend) and extract the CLVM
20/// output conditions.  Used by `get_puzzle_and_solution_with_conditions`.
21fn run_puzzle_conditions(spend: &CoinSpend, constants: &ConsensusConstants) -> Vec<Condition> {
22    let flags = DONT_VALIDATE_SIGNATURE;
23    let Ok(puzzle_bytes) = crate::peer::translate::parse_hex(&spend.puzzle_reveal) else {
24        return Vec::new();
25    };
26    let Ok(solution_bytes) = crate::peer::translate::parse_hex(&spend.solution) else {
27        return Vec::new();
28    };
29
30    let mut allocator = chia_consensus::allocator::make_allocator(flags);
31
32    let Ok(puzzle_node) = clvmr::serde::node_from_bytes(&mut allocator, &puzzle_bytes) else {
33        return Vec::new();
34    };
35    let Ok(solution_node) = clvmr::serde::node_from_bytes(&mut allocator, &solution_bytes) else {
36        return Vec::new();
37    };
38
39    let dialect = clvmr::chia_dialect::ChiaDialect::new(flags);
40    match clvmr::run_program::run_program(
41        &mut allocator,
42        &dialect,
43        puzzle_node,
44        solution_node,
45        constants.max_block_cost_clvm,
46    ) {
47        Ok(clvmr::reduction::Reduction(_, output)) => {
48            crate::peer::block::parse_conditions_public(&allocator, output)
49        }
50        Err(_) => Vec::new(),
51    }
52}
53
54pub struct QueryRouter {
55    pub(crate) peer: PeerBackend,
56    pub(crate) coinset: CoinsetClient,
57    pub(crate) coinset_fallback_enabled: bool,
58}
59
60// ---------------------------------------------------------------------------
61// Internal helpers
62// ---------------------------------------------------------------------------
63
64impl QueryRouter {
65    /// Try `peer_fn` twice (each call will select a different peer because the
66    /// first failure ejects the peer).  If both fail, fall back to `coinset_fn`.
67    async fn peer_then_coinset<T>(
68        &self,
69        peer_fn: impl std::future::Future<Output = Result<T, ChiaQueryError>>,
70        peer_retry: impl std::future::Future<Output = Result<T, ChiaQueryError>>,
71        coinset_fn: impl std::future::Future<Output = Result<T, ChiaQueryError>>,
72    ) -> Result<T, ChiaQueryError> {
73        // First peer attempt
74        match peer_fn.await {
75            Ok(v) => return Ok(v),
76            Err(e) => log::debug!("peer attempt 1 failed: {e}"),
77        }
78
79        // Retry on a different peer
80        match peer_retry.await {
81            Ok(v) => Ok(v),
82            Err(peer_err) => {
83                if !self.coinset_fallback_enabled {
84                    return Err(peer_err);
85                }
86                // Fall back to coinset
87                coinset_fn
88                    .await
89                    .map_err(|ce| ChiaQueryError::AllSourcesFailed {
90                        peer_error: Box::new(peer_err),
91                        coinset_error: Some(Box::new(ce)),
92                    })
93            }
94        }
95    }
96
97    /// Absence-aware variant of [`peer_then_coinset`](Self::peer_then_coinset).
98    ///
99    /// A successful peer response — whether `Some` (found) or `None` (provable absence) — is
100    /// authoritative and returned immediately. Only a peer FAILURE falls through to the retry and
101    /// then the coinset fallback, so a genuine absence is never masked by a fallback and a failure
102    /// is never collapsed into `Ok(None)` (SPEC §3).
103    async fn peer_then_coinset_opt<T>(
104        &self,
105        peer_fn: impl std::future::Future<Output = Result<Option<T>, ChiaQueryError>>,
106        peer_retry: impl std::future::Future<Output = Result<Option<T>, ChiaQueryError>>,
107        coinset_fn: impl std::future::Future<Output = Result<Option<T>, ChiaQueryError>>,
108    ) -> Result<Option<T>, ChiaQueryError> {
109        match peer_fn.await {
110            Ok(v) => return Ok(v),
111            Err(e) => log::debug!("peer opt attempt 1 failed: {e}"),
112        }
113        match peer_retry.await {
114            Ok(v) => Ok(v),
115            Err(peer_err) => {
116                if !self.coinset_fallback_enabled {
117                    return Err(peer_err);
118                }
119                coinset_fn
120                    .await
121                    .map_err(|ce| ChiaQueryError::AllSourcesFailed {
122                        peer_error: Box::new(peer_err),
123                        coinset_error: Some(Box::new(ce)),
124                    })
125            }
126        }
127    }
128
129    /// For endpoints that have no peer protocol equivalent.
130    fn require_coinset(&self, endpoint: &str) -> Result<(), ChiaQueryError> {
131        if !self.coinset_fallback_enabled {
132            Err(ChiaQueryError::UnsupportedWithoutCoinset(endpoint.into()))
133        } else {
134            Ok(())
135        }
136    }
137}
138
139// ---------------------------------------------------------------------------
140// Blocks (all coinset-only)
141// ---------------------------------------------------------------------------
142
143impl QueryRouter {
144    /// Peer-backed: fetches the full block by header_hash (via coinset to
145    /// resolve the height), then parses additions/removals from the CLVM
146    /// generator.  Falls back to the coinset endpoint on failure.
147    pub async fn get_additions_and_removals(
148        &self,
149        header_hash: &str,
150    ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
151        // The peer protocol needs a height.  Resolve it from the block record.
152        if let Ok(record) = self.get_block_record(header_hash).await {
153            // Try parsing via peer + CLVM.
154            match self
155                .peer
156                .try_get_additions_and_removals_from_block(record.height)
157                .await
158            {
159                Ok(r) => return Ok(r),
160                Err(e) => log::debug!("peer additions_and_removals failed: {e}"),
161            }
162        }
163        // Fallback to coinset.
164        if self.coinset_fallback_enabled {
165            self.coinset.get_additions_and_removals(header_hash).await
166        } else {
167            Err(ChiaQueryError::UnsupportedWithoutCoinset(
168                "get_additions_and_removals".into(),
169            ))
170        }
171    }
172
173    pub async fn get_block(&self, header_hash: &str) -> Result<FullBlock, ChiaQueryError> {
174        // Resolve height from block record, try peer first.
175        if let Ok(record) = self.get_block_record(header_hash).await {
176            match self.peer.try_get_block_by_height(record.height).await {
177                Ok(b) => return Ok(b),
178                Err(e) => log::debug!("peer get_block failed: {e}"),
179            }
180        }
181        if self.coinset_fallback_enabled {
182            self.coinset.get_block(header_hash).await
183        } else {
184            Err(ChiaQueryError::UnsupportedWithoutCoinset(
185                "get_block".into(),
186            ))
187        }
188    }
189
190    /// Fetch a full block by height.  Peer-backed via `RequestBlock`.
191    pub async fn get_block_by_height(&self, height: u32) -> Result<FullBlock, ChiaQueryError> {
192        self.peer_then_coinset(
193            self.peer.try_get_block_by_height(height),
194            self.peer.try_get_block_by_height(height),
195            async {
196                // Coinset has no direct by-height endpoint for full blocks, so
197                // resolve the header_hash first.
198                let record = self.coinset.get_block_record_by_height(height).await?;
199                self.coinset.get_block(&record.header_hash).await
200            },
201        )
202        .await
203    }
204
205    pub async fn get_block_count_metrics(&self) -> Result<BlockCountMetrics, ChiaQueryError> {
206        self.require_coinset("get_block_count_metrics")?;
207        self.coinset.get_block_count_metrics().await
208    }
209
210    pub async fn get_block_record(&self, header_hash: &str) -> Result<BlockRecord, ChiaQueryError> {
211        self.require_coinset("get_block_record")?;
212        self.coinset.get_block_record(header_hash).await
213    }
214
215    /// Peer-backed via `RequestBlockHeader` / `RespondBlockHeader` (pattern
216    /// from chia-block-listener).
217    pub async fn get_block_record_by_height(
218        &self,
219        height: u32,
220    ) -> Result<BlockRecord, ChiaQueryError> {
221        self.peer_then_coinset(
222            self.peer.try_get_block_record_by_height(height),
223            self.peer.try_get_block_record_by_height(height),
224            self.coinset.get_block_record_by_height(height),
225        )
226        .await
227    }
228
229    pub async fn get_block_records(
230        &self,
231        start: u32,
232        end: u32,
233    ) -> Result<Vec<BlockRecord>, ChiaQueryError> {
234        self.peer_then_coinset(
235            self.peer.try_get_block_records(start, end),
236            self.peer.try_get_block_records(start, end),
237            self.coinset.get_block_records(start, end),
238        )
239        .await
240    }
241
242    /// Peer-backed: fetches the full block, then runs the CLVM generator to
243    /// extract every coin spend with its puzzle_reveal and solution.
244    pub async fn get_block_spends(
245        &self,
246        header_hash: &str,
247    ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
248        // Resolve height from block record.
249        if let Ok(record) = self.get_block_record(header_hash).await {
250            match self
251                .peer
252                .try_get_block_spends_by_height(record.height)
253                .await
254            {
255                Ok(r) => return Ok(r),
256                Err(e) => log::debug!("peer block_spends failed: {e}"),
257            }
258        }
259        if self.coinset_fallback_enabled {
260            self.coinset.get_block_spends(header_hash).await
261        } else {
262            Err(ChiaQueryError::UnsupportedWithoutCoinset(
263                "get_block_spends".into(),
264            ))
265        }
266    }
267
268    /// Peer-backed: fetches full block, runs CLVM generator, then runs each
269    /// puzzle(solution) to extract parsed conditions.
270    pub async fn get_block_spends_with_conditions(
271        &self,
272        header_hash: &str,
273    ) -> Result<Vec<CoinSpendWithConditions>, ChiaQueryError> {
274        if let Ok(record) = self.get_block_record(header_hash).await {
275            match self
276                .peer
277                .try_get_block_spends_with_conditions(record.height)
278                .await
279            {
280                Ok(r) => return Ok(r),
281                Err(e) => log::debug!("peer block_spends_with_conditions failed: {e}"),
282            }
283        }
284        if self.coinset_fallback_enabled {
285            self.coinset
286                .get_block_spends_with_conditions(header_hash)
287                .await
288        } else {
289            Err(ChiaQueryError::UnsupportedWithoutCoinset(
290                "get_block_spends_with_conditions".into(),
291            ))
292        }
293    }
294
295    pub async fn get_blocks(
296        &self,
297        start: u32,
298        end: u32,
299        exclude_header_hash: bool,
300        exclude_reorged: bool,
301    ) -> Result<Vec<FullBlock>, ChiaQueryError> {
302        self.peer_then_coinset(
303            self.peer.try_get_blocks_range(start, end),
304            self.peer.try_get_blocks_range(start, end),
305            self.coinset
306                .get_blocks(start, end, exclude_header_hash, exclude_reorged),
307        )
308        .await
309    }
310
311    pub async fn get_unfinished_block_headers(
312        &self,
313    ) -> Result<Vec<UnfinishedBlockHeader>, ChiaQueryError> {
314        self.require_coinset("get_unfinished_block_headers")?;
315        self.coinset.get_unfinished_block_headers().await
316    }
317}
318
319// ---------------------------------------------------------------------------
320// Coins (peer-backed with coinset fallback)
321// ---------------------------------------------------------------------------
322
323impl QueryRouter {
324    pub async fn get_coin_record_by_name(&self, name: &str) -> Result<CoinRecord, ChiaQueryError> {
325        self.peer_then_coinset(
326            self.peer.try_get_coin_record_by_name(name),
327            self.peer.try_get_coin_record_by_name(name),
328            self.coinset.get_coin_record_by_name(name),
329        )
330        .await
331    }
332
333    /// Absence-aware [`get_coin_record_by_name`](Self::get_coin_record_by_name): a PROVABLE absence
334    /// is `Ok(None)`, every transport/rejection/parse failure is `Err`. A successful peer or coinset
335    /// response that reports no such coin is the authoritative absence; only when the peer read
336    /// itself fails does the router fall back to coinset (SPEC §3).
337    pub async fn get_coin_record_by_name_opt(
338        &self,
339        name: &str,
340    ) -> Result<Option<CoinRecord>, ChiaQueryError> {
341        self.peer_then_coinset_opt(
342            self.peer.try_get_coin_record_by_name_opt(name),
343            self.peer.try_get_coin_record_by_name_opt(name),
344            self.coinset.get_coin_record_by_name_opt(name),
345        )
346        .await
347    }
348
349    /// Absence-aware read of the spend that spent `coin_id`: `Ok(None)` when the coin is provably
350    /// unspent/unknown, `Err` when the read could not be completed.
351    pub async fn get_coin_spend_opt(
352        &self,
353        coin_id: &str,
354    ) -> Result<Option<CoinSpend>, ChiaQueryError> {
355        self.peer_then_coinset_opt(
356            self.peer.try_get_coin_spend_opt(coin_id),
357            self.peer.try_get_coin_spend_opt(coin_id),
358            self.coinset.get_puzzle_and_solution_opt(coin_id, None),
359        )
360        .await
361    }
362
363    /// The current peak height, or `Ok(None)` when no source exposes a peak; `Err` on failure.
364    pub async fn peak_height_opt(&self) -> Result<Option<u32>, ChiaQueryError> {
365        let state = self.get_blockchain_state().await?;
366        Ok(state.peak.map(|p| p.height))
367    }
368
369    /// The Unix timestamp of the block at `height`: `Ok(None)` when no such block exists or the
370    /// block carries no timestamp; `Err` on failure.
371    pub async fn block_timestamp_opt(&self, height: u32) -> Result<Option<u64>, ChiaQueryError> {
372        let record = self.get_block_record_by_height_opt(height).await?;
373        Ok(record.and_then(|r| r.timestamp))
374    }
375
376    /// Absence-aware block-record read used by [`block_timestamp_opt`](Self::block_timestamp_opt).
377    async fn get_block_record_by_height_opt(
378        &self,
379        height: u32,
380    ) -> Result<Option<BlockRecord>, ChiaQueryError> {
381        // A successful peer read is authoritative; only a peer FAILURE falls through to coinset,
382        // whose null block_record is provable absence.
383        if let Ok(record) = self.peer.try_get_block_record_by_height(height).await {
384            return Ok(Some(record));
385        }
386        match self.peer.try_get_block_record_by_height(height).await {
387            Ok(record) => Ok(Some(record)),
388            Err(peer_err) => {
389                if self.coinset_fallback_enabled {
390                    self.coinset
391                        .get_block_record_by_height_opt(height)
392                        .await
393                        .map_err(|ce| ChiaQueryError::AllSourcesFailed {
394                            peer_error: Box::new(peer_err),
395                            coinset_error: Some(Box::new(ce)),
396                        })
397                } else {
398                    Err(peer_err)
399                }
400            }
401        }
402    }
403
404    pub async fn get_coin_records_by_hint(
405        &self,
406        hint: &str,
407        start_height: Option<u32>,
408        end_height: Option<u32>,
409        include_spent_coins: bool,
410    ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
411        self.peer_then_coinset(
412            self.peer.try_get_coin_records_by_hint(
413                hint,
414                start_height,
415                end_height,
416                include_spent_coins,
417            ),
418            self.peer.try_get_coin_records_by_hint(
419                hint,
420                start_height,
421                end_height,
422                include_spent_coins,
423            ),
424            self.coinset.get_coin_records_by_hint(
425                hint,
426                start_height,
427                end_height,
428                include_spent_coins,
429            ),
430        )
431        .await
432    }
433
434    pub async fn get_coin_records_by_hints(
435        &self,
436        hints: &[String],
437        start_height: Option<u32>,
438        end_height: Option<u32>,
439        include_spent_coins: bool,
440    ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
441        self.peer_then_coinset(
442            self.peer.try_get_coin_records_by_hints(
443                hints,
444                start_height,
445                end_height,
446                include_spent_coins,
447            ),
448            self.peer.try_get_coin_records_by_hints(
449                hints,
450                start_height,
451                end_height,
452                include_spent_coins,
453            ),
454            self.coinset.get_coin_records_by_hints(
455                hints,
456                start_height,
457                end_height,
458                include_spent_coins,
459            ),
460        )
461        .await
462    }
463
464    pub async fn get_coin_records_by_names(
465        &self,
466        names: &[String],
467        start_height: Option<u32>,
468        end_height: Option<u32>,
469        include_spent_coins: bool,
470    ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
471        self.peer_then_coinset(
472            self.peer.try_get_coin_records_by_names(names),
473            self.peer.try_get_coin_records_by_names(names),
474            self.coinset.get_coin_records_by_names(
475                names,
476                start_height,
477                end_height,
478                include_spent_coins,
479            ),
480        )
481        .await
482    }
483
484    /// Peer-backed via `RequestChildren` / `RespondChildren` which returns
485    /// child coin states for a given parent coin ID.  Falls back to coinset
486    /// for batched queries or when peers fail.
487    pub async fn get_coin_records_by_parent_ids(
488        &self,
489        parent_ids: &[String],
490        start_height: Option<u32>,
491        end_height: Option<u32>,
492        include_spent_coins: bool,
493    ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
494        // Try peer: query each parent ID via RequestChildren, combine results.
495        let peer_attempt = async {
496            let mut all_records = Vec::new();
497            for parent_id in parent_ids {
498                let children = self.peer.try_get_children(parent_id).await?;
499                all_records.extend(children);
500            }
501            // Apply client-side height and spent filters.
502            all_records.retain(|r| {
503                let height_ok = match (start_height, end_height) {
504                    (Some(s), Some(e)) => {
505                        r.confirmed_block_index >= s && r.confirmed_block_index <= e
506                    }
507                    (Some(s), None) => r.confirmed_block_index >= s,
508                    (None, Some(e)) => r.confirmed_block_index <= e,
509                    (None, None) => true,
510                };
511                let spent_ok = include_spent_coins || !r.spent;
512                height_ok && spent_ok
513            });
514            Ok(all_records)
515        };
516
517        match peer_attempt.await {
518            Ok(r) => Ok(r),
519            Err(peer_err) => {
520                if self.coinset_fallback_enabled {
521                    self.coinset
522                        .get_coin_records_by_parent_ids(
523                            parent_ids,
524                            start_height,
525                            end_height,
526                            include_spent_coins,
527                        )
528                        .await
529                        .map_err(|ce| ChiaQueryError::AllSourcesFailed {
530                            peer_error: Box::new(peer_err),
531                            coinset_error: Some(Box::new(ce)),
532                        })
533                } else {
534                    Err(peer_err)
535                }
536            }
537        }
538    }
539
540    pub async fn get_coin_records_by_puzzle_hash(
541        &self,
542        puzzle_hash: &str,
543        start_height: Option<u32>,
544        end_height: Option<u32>,
545        include_spent_coins: bool,
546    ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
547        self.peer_then_coinset(
548            self.peer.try_get_coin_records_by_puzzle_hash(
549                puzzle_hash,
550                start_height,
551                end_height,
552                include_spent_coins,
553            ),
554            self.peer.try_get_coin_records_by_puzzle_hash(
555                puzzle_hash,
556                start_height,
557                end_height,
558                include_spent_coins,
559            ),
560            self.coinset.get_coin_records_by_puzzle_hash(
561                puzzle_hash,
562                start_height,
563                end_height,
564                include_spent_coins,
565            ),
566        )
567        .await
568    }
569
570    pub async fn get_coin_records_by_puzzle_hashes(
571        &self,
572        puzzle_hashes: &[String],
573        start_height: Option<u32>,
574        end_height: Option<u32>,
575        include_spent_coins: bool,
576    ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
577        self.peer_then_coinset(
578            self.peer.try_get_coin_records_by_puzzle_hashes(
579                puzzle_hashes,
580                start_height,
581                end_height,
582                include_spent_coins,
583            ),
584            self.peer.try_get_coin_records_by_puzzle_hashes(
585                puzzle_hashes,
586                start_height,
587                end_height,
588                include_spent_coins,
589            ),
590            self.coinset.get_coin_records_by_puzzle_hashes(
591                puzzle_hashes,
592                start_height,
593                end_height,
594                include_spent_coins,
595            ),
596        )
597        .await
598    }
599
600    /// No peer equivalent -- always coinset.
601    pub async fn get_memos_by_coin_name(&self, name: &str) -> Result<Value, ChiaQueryError> {
602        self.require_coinset("get_memos_by_coin_name")?;
603        self.coinset.get_memos_by_coin_name(name).await
604    }
605
606    pub async fn get_puzzle_and_solution(
607        &self,
608        coin_id: &str,
609        height: Option<u32>,
610    ) -> Result<CoinSpend, ChiaQueryError> {
611        if let Some(h) = height {
612            self.peer_then_coinset(
613                self.peer.try_get_puzzle_and_solution(coin_id, h),
614                self.peer.try_get_puzzle_and_solution(coin_id, h),
615                self.coinset.get_puzzle_and_solution(coin_id, height),
616            )
617            .await
618        } else {
619            // No height provided -- peer can resolve it via coin state.
620            self.peer_then_coinset(
621                self.peer.try_get_puzzle_and_solution_auto(coin_id),
622                self.peer.try_get_puzzle_and_solution_auto(coin_id),
623                self.coinset.get_puzzle_and_solution(coin_id, None),
624            )
625            .await
626        }
627    }
628
629    /// Peer-backed: get puzzle & solution, then run puzzle(solution) to extract
630    /// parsed conditions.
631    pub async fn get_puzzle_and_solution_with_conditions(
632        &self,
633        coin_id: &str,
634        height: Option<u32>,
635    ) -> Result<CoinSpendWithConditions, ChiaQueryError> {
636        // Try to get the spend via peer first.
637        let spend = match self.get_puzzle_and_solution(coin_id, height).await {
638            Ok(s) => s,
639            Err(_) => {
640                if self.coinset_fallback_enabled {
641                    return self
642                        .coinset
643                        .get_puzzle_and_solution_with_conditions(coin_id, height)
644                        .await;
645                }
646                return Err(ChiaQueryError::PeerRejection(
647                    "could not retrieve puzzle and solution".into(),
648                ));
649            }
650        };
651
652        // Run puzzle(solution) to extract conditions.
653        let conditions = run_puzzle_conditions(&spend, self.peer.constants());
654        Ok(CoinSpendWithConditions {
655            coin_spend: spend,
656            conditions,
657        })
658    }
659
660    pub async fn push_tx(&self, bundle: &SpendBundle) -> Result<TxStatus, ChiaQueryError> {
661        self.peer_then_coinset(
662            self.peer.try_push_tx(bundle),
663            self.peer.try_push_tx(bundle),
664            self.coinset.push_tx(bundle),
665        )
666        .await
667    }
668}
669
670// ---------------------------------------------------------------------------
671// Fees (peer-backed with coinset fallback)
672// ---------------------------------------------------------------------------
673
674impl QueryRouter {
675    pub async fn get_fee_estimate(
676        &self,
677        spend_bundle: Option<&SpendBundle>,
678        target_times: Option<&[u64]>,
679        spend_count: Option<u64>,
680    ) -> Result<FeeEstimate, ChiaQueryError> {
681        let times = target_times.unwrap_or(&[60, 120, 300]);
682        self.peer_then_coinset(
683            self.peer.try_get_fee_estimate(times),
684            self.peer.try_get_fee_estimate(times),
685            self.coinset
686                .get_fee_estimate(spend_bundle, target_times, spend_count),
687        )
688        .await
689    }
690}
691
692// ---------------------------------------------------------------------------
693// Full node / network (all coinset-only)
694// ---------------------------------------------------------------------------
695
696impl QueryRouter {
697    /// Peer-backed: derived from the chia consensus constants for the
698    /// configured network.
699    pub async fn get_aggsig_additional_data(&self) -> Result<String, ChiaQueryError> {
700        Ok(self.peer.aggsig_additional_data())
701    }
702
703    /// Peer-backed: derived from the chia consensus constants for the
704    /// configured network.
705    pub async fn get_network_info(&self) -> Result<NetworkInfo, ChiaQueryError> {
706        Ok(self.peer.network_info())
707    }
708
709    /// Peer-backed partially: peak height is tracked from `NewPeakWallet`
710    /// messages received from peers.  Full state comes from coinset.
711    pub async fn get_blockchain_state(&self) -> Result<BlockchainState, ChiaQueryError> {
712        // Try coinset first for full state.
713        if self.coinset_fallback_enabled {
714            if let Ok(state) = self.coinset.get_blockchain_state().await {
715                return Ok(state);
716            }
717        }
718        // Fallback: return a minimal state from the peer-tracked peak.
719        let peak = self.peer.peak_height();
720        if peak == 0 {
721            return Err(ChiaQueryError::PeerConnection(
722                "no peak observed from peers yet".into(),
723            ));
724        }
725        Ok(BlockchainState {
726            peak: Some(BlockRecord {
727                height: peak,
728                ..Default::default()
729            }),
730            sync: Some(SyncState {
731                synced: true,
732                sync_mode: false,
733                sync_progress_height: peak,
734                sync_tip_height: peak,
735            }),
736            ..Default::default()
737        })
738    }
739
740    pub async fn get_network_space(
741        &self,
742        newer_block_header_hash: &str,
743        older_block_header_hash: &str,
744    ) -> Result<u64, ChiaQueryError> {
745        self.require_coinset("get_network_space")?;
746        self.coinset
747            .get_network_space(newer_block_header_hash, older_block_header_hash)
748            .await
749    }
750}
751
752// ---------------------------------------------------------------------------
753// Mempool (all coinset-only)
754// ---------------------------------------------------------------------------
755
756impl QueryRouter {
757    pub async fn get_all_mempool_items(
758        &self,
759    ) -> Result<HashMap<String, MempoolItem>, ChiaQueryError> {
760        self.require_coinset("get_all_mempool_items")?;
761        self.coinset.get_all_mempool_items().await
762    }
763
764    pub async fn get_all_mempool_tx_ids(&self) -> Result<Vec<String>, ChiaQueryError> {
765        self.require_coinset("get_all_mempool_tx_ids")?;
766        self.coinset.get_all_mempool_tx_ids().await
767    }
768
769    pub async fn get_mempool_item_by_tx_id(
770        &self,
771        tx_id: &str,
772    ) -> Result<MempoolItem, ChiaQueryError> {
773        self.require_coinset("get_mempool_item_by_tx_id")?;
774        self.coinset.get_mempool_item_by_tx_id(tx_id).await
775    }
776
777    pub async fn get_mempool_items_by_coin_name(
778        &self,
779        coin_name: &str,
780        include_spent_coins: Option<bool>,
781    ) -> Result<Vec<MempoolItem>, ChiaQueryError> {
782        self.require_coinset("get_mempool_items_by_coin_name")?;
783        self.coinset
784            .get_mempool_items_by_coin_name(coin_name, include_spent_coins)
785            .await
786    }
787}