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    /// For endpoints that have no peer protocol equivalent.
98    fn require_coinset(&self, endpoint: &str) -> Result<(), ChiaQueryError> {
99        if !self.coinset_fallback_enabled {
100            Err(ChiaQueryError::UnsupportedWithoutCoinset(endpoint.into()))
101        } else {
102            Ok(())
103        }
104    }
105}
106
107// ---------------------------------------------------------------------------
108// Blocks (all coinset-only)
109// ---------------------------------------------------------------------------
110
111impl QueryRouter {
112    /// Peer-backed: fetches the full block by header_hash (via coinset to
113    /// resolve the height), then parses additions/removals from the CLVM
114    /// generator.  Falls back to the coinset endpoint on failure.
115    pub async fn get_additions_and_removals(
116        &self,
117        header_hash: &str,
118    ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
119        // The peer protocol needs a height.  Resolve it from the block record.
120        if let Ok(record) = self.get_block_record(header_hash).await {
121            // Try parsing via peer + CLVM.
122            match self
123                .peer
124                .try_get_additions_and_removals_from_block(record.height)
125                .await
126            {
127                Ok(r) => return Ok(r),
128                Err(e) => log::debug!("peer additions_and_removals failed: {e}"),
129            }
130        }
131        // Fallback to coinset.
132        if self.coinset_fallback_enabled {
133            self.coinset.get_additions_and_removals(header_hash).await
134        } else {
135            Err(ChiaQueryError::UnsupportedWithoutCoinset(
136                "get_additions_and_removals".into(),
137            ))
138        }
139    }
140
141    pub async fn get_block(&self, header_hash: &str) -> Result<FullBlock, ChiaQueryError> {
142        // Resolve height from block record, try peer first.
143        if let Ok(record) = self.get_block_record(header_hash).await {
144            match self.peer.try_get_block_by_height(record.height).await {
145                Ok(b) => return Ok(b),
146                Err(e) => log::debug!("peer get_block failed: {e}"),
147            }
148        }
149        if self.coinset_fallback_enabled {
150            self.coinset.get_block(header_hash).await
151        } else {
152            Err(ChiaQueryError::UnsupportedWithoutCoinset(
153                "get_block".into(),
154            ))
155        }
156    }
157
158    /// Fetch a full block by height.  Peer-backed via `RequestBlock`.
159    pub async fn get_block_by_height(&self, height: u32) -> Result<FullBlock, ChiaQueryError> {
160        self.peer_then_coinset(
161            self.peer.try_get_block_by_height(height),
162            self.peer.try_get_block_by_height(height),
163            async {
164                // Coinset has no direct by-height endpoint for full blocks, so
165                // resolve the header_hash first.
166                let record = self.coinset.get_block_record_by_height(height).await?;
167                self.coinset.get_block(&record.header_hash).await
168            },
169        )
170        .await
171    }
172
173    pub async fn get_block_count_metrics(&self) -> Result<BlockCountMetrics, ChiaQueryError> {
174        self.require_coinset("get_block_count_metrics")?;
175        self.coinset.get_block_count_metrics().await
176    }
177
178    pub async fn get_block_record(&self, header_hash: &str) -> Result<BlockRecord, ChiaQueryError> {
179        self.require_coinset("get_block_record")?;
180        self.coinset.get_block_record(header_hash).await
181    }
182
183    /// Peer-backed via `RequestBlockHeader` / `RespondBlockHeader` (pattern
184    /// from chia-block-listener).
185    pub async fn get_block_record_by_height(
186        &self,
187        height: u32,
188    ) -> Result<BlockRecord, ChiaQueryError> {
189        self.peer_then_coinset(
190            self.peer.try_get_block_record_by_height(height),
191            self.peer.try_get_block_record_by_height(height),
192            self.coinset.get_block_record_by_height(height),
193        )
194        .await
195    }
196
197    pub async fn get_block_records(
198        &self,
199        start: u32,
200        end: u32,
201    ) -> Result<Vec<BlockRecord>, ChiaQueryError> {
202        self.peer_then_coinset(
203            self.peer.try_get_block_records(start, end),
204            self.peer.try_get_block_records(start, end),
205            self.coinset.get_block_records(start, end),
206        )
207        .await
208    }
209
210    /// Peer-backed: fetches the full block, then runs the CLVM generator to
211    /// extract every coin spend with its puzzle_reveal and solution.
212    pub async fn get_block_spends(
213        &self,
214        header_hash: &str,
215    ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
216        // Resolve height from block record.
217        if let Ok(record) = self.get_block_record(header_hash).await {
218            match self
219                .peer
220                .try_get_block_spends_by_height(record.height)
221                .await
222            {
223                Ok(r) => return Ok(r),
224                Err(e) => log::debug!("peer block_spends failed: {e}"),
225            }
226        }
227        if self.coinset_fallback_enabled {
228            self.coinset.get_block_spends(header_hash).await
229        } else {
230            Err(ChiaQueryError::UnsupportedWithoutCoinset(
231                "get_block_spends".into(),
232            ))
233        }
234    }
235
236    /// Peer-backed: fetches full block, runs CLVM generator, then runs each
237    /// puzzle(solution) to extract parsed conditions.
238    pub async fn get_block_spends_with_conditions(
239        &self,
240        header_hash: &str,
241    ) -> Result<Vec<CoinSpendWithConditions>, ChiaQueryError> {
242        if let Ok(record) = self.get_block_record(header_hash).await {
243            match self
244                .peer
245                .try_get_block_spends_with_conditions(record.height)
246                .await
247            {
248                Ok(r) => return Ok(r),
249                Err(e) => log::debug!("peer block_spends_with_conditions failed: {e}"),
250            }
251        }
252        if self.coinset_fallback_enabled {
253            self.coinset
254                .get_block_spends_with_conditions(header_hash)
255                .await
256        } else {
257            Err(ChiaQueryError::UnsupportedWithoutCoinset(
258                "get_block_spends_with_conditions".into(),
259            ))
260        }
261    }
262
263    pub async fn get_blocks(
264        &self,
265        start: u32,
266        end: u32,
267        exclude_header_hash: bool,
268        exclude_reorged: bool,
269    ) -> Result<Vec<FullBlock>, ChiaQueryError> {
270        self.peer_then_coinset(
271            self.peer.try_get_blocks_range(start, end),
272            self.peer.try_get_blocks_range(start, end),
273            self.coinset
274                .get_blocks(start, end, exclude_header_hash, exclude_reorged),
275        )
276        .await
277    }
278
279    pub async fn get_unfinished_block_headers(
280        &self,
281    ) -> Result<Vec<UnfinishedBlockHeader>, ChiaQueryError> {
282        self.require_coinset("get_unfinished_block_headers")?;
283        self.coinset.get_unfinished_block_headers().await
284    }
285}
286
287// ---------------------------------------------------------------------------
288// Coins (peer-backed with coinset fallback)
289// ---------------------------------------------------------------------------
290
291impl QueryRouter {
292    pub async fn get_coin_record_by_name(&self, name: &str) -> Result<CoinRecord, ChiaQueryError> {
293        self.peer_then_coinset(
294            self.peer.try_get_coin_record_by_name(name),
295            self.peer.try_get_coin_record_by_name(name),
296            self.coinset.get_coin_record_by_name(name),
297        )
298        .await
299    }
300
301    pub async fn get_coin_records_by_hint(
302        &self,
303        hint: &str,
304        start_height: Option<u32>,
305        end_height: Option<u32>,
306        include_spent_coins: bool,
307    ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
308        self.peer_then_coinset(
309            self.peer.try_get_coin_records_by_hint(
310                hint,
311                start_height,
312                end_height,
313                include_spent_coins,
314            ),
315            self.peer.try_get_coin_records_by_hint(
316                hint,
317                start_height,
318                end_height,
319                include_spent_coins,
320            ),
321            self.coinset.get_coin_records_by_hint(
322                hint,
323                start_height,
324                end_height,
325                include_spent_coins,
326            ),
327        )
328        .await
329    }
330
331    pub async fn get_coin_records_by_hints(
332        &self,
333        hints: &[String],
334        start_height: Option<u32>,
335        end_height: Option<u32>,
336        include_spent_coins: bool,
337    ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
338        self.peer_then_coinset(
339            self.peer.try_get_coin_records_by_hints(
340                hints,
341                start_height,
342                end_height,
343                include_spent_coins,
344            ),
345            self.peer.try_get_coin_records_by_hints(
346                hints,
347                start_height,
348                end_height,
349                include_spent_coins,
350            ),
351            self.coinset.get_coin_records_by_hints(
352                hints,
353                start_height,
354                end_height,
355                include_spent_coins,
356            ),
357        )
358        .await
359    }
360
361    pub async fn get_coin_records_by_names(
362        &self,
363        names: &[String],
364        start_height: Option<u32>,
365        end_height: Option<u32>,
366        include_spent_coins: bool,
367    ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
368        self.peer_then_coinset(
369            self.peer.try_get_coin_records_by_names(names),
370            self.peer.try_get_coin_records_by_names(names),
371            self.coinset.get_coin_records_by_names(
372                names,
373                start_height,
374                end_height,
375                include_spent_coins,
376            ),
377        )
378        .await
379    }
380
381    /// Peer-backed via `RequestChildren` / `RespondChildren` which returns
382    /// child coin states for a given parent coin ID.  Falls back to coinset
383    /// for batched queries or when peers fail.
384    pub async fn get_coin_records_by_parent_ids(
385        &self,
386        parent_ids: &[String],
387        start_height: Option<u32>,
388        end_height: Option<u32>,
389        include_spent_coins: bool,
390    ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
391        // Try peer: query each parent ID via RequestChildren, combine results.
392        let peer_attempt = async {
393            let mut all_records = Vec::new();
394            for parent_id in parent_ids {
395                let children = self.peer.try_get_children(parent_id).await?;
396                all_records.extend(children);
397            }
398            // Apply client-side height and spent filters.
399            all_records.retain(|r| {
400                let height_ok = match (start_height, end_height) {
401                    (Some(s), Some(e)) => {
402                        r.confirmed_block_index >= s && r.confirmed_block_index <= e
403                    }
404                    (Some(s), None) => r.confirmed_block_index >= s,
405                    (None, Some(e)) => r.confirmed_block_index <= e,
406                    (None, None) => true,
407                };
408                let spent_ok = include_spent_coins || !r.spent;
409                height_ok && spent_ok
410            });
411            Ok(all_records)
412        };
413
414        match peer_attempt.await {
415            Ok(r) => Ok(r),
416            Err(peer_err) => {
417                if self.coinset_fallback_enabled {
418                    self.coinset
419                        .get_coin_records_by_parent_ids(
420                            parent_ids,
421                            start_height,
422                            end_height,
423                            include_spent_coins,
424                        )
425                        .await
426                        .map_err(|ce| ChiaQueryError::AllSourcesFailed {
427                            peer_error: Box::new(peer_err),
428                            coinset_error: Some(Box::new(ce)),
429                        })
430                } else {
431                    Err(peer_err)
432                }
433            }
434        }
435    }
436
437    pub async fn get_coin_records_by_puzzle_hash(
438        &self,
439        puzzle_hash: &str,
440        start_height: Option<u32>,
441        end_height: Option<u32>,
442        include_spent_coins: bool,
443    ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
444        self.peer_then_coinset(
445            self.peer.try_get_coin_records_by_puzzle_hash(
446                puzzle_hash,
447                start_height,
448                end_height,
449                include_spent_coins,
450            ),
451            self.peer.try_get_coin_records_by_puzzle_hash(
452                puzzle_hash,
453                start_height,
454                end_height,
455                include_spent_coins,
456            ),
457            self.coinset.get_coin_records_by_puzzle_hash(
458                puzzle_hash,
459                start_height,
460                end_height,
461                include_spent_coins,
462            ),
463        )
464        .await
465    }
466
467    pub async fn get_coin_records_by_puzzle_hashes(
468        &self,
469        puzzle_hashes: &[String],
470        start_height: Option<u32>,
471        end_height: Option<u32>,
472        include_spent_coins: bool,
473    ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
474        self.peer_then_coinset(
475            self.peer.try_get_coin_records_by_puzzle_hashes(
476                puzzle_hashes,
477                start_height,
478                end_height,
479                include_spent_coins,
480            ),
481            self.peer.try_get_coin_records_by_puzzle_hashes(
482                puzzle_hashes,
483                start_height,
484                end_height,
485                include_spent_coins,
486            ),
487            self.coinset.get_coin_records_by_puzzle_hashes(
488                puzzle_hashes,
489                start_height,
490                end_height,
491                include_spent_coins,
492            ),
493        )
494        .await
495    }
496
497    /// No peer equivalent -- always coinset.
498    pub async fn get_memos_by_coin_name(&self, name: &str) -> Result<Value, ChiaQueryError> {
499        self.require_coinset("get_memos_by_coin_name")?;
500        self.coinset.get_memos_by_coin_name(name).await
501    }
502
503    pub async fn get_puzzle_and_solution(
504        &self,
505        coin_id: &str,
506        height: Option<u32>,
507    ) -> Result<CoinSpend, ChiaQueryError> {
508        if let Some(h) = height {
509            self.peer_then_coinset(
510                self.peer.try_get_puzzle_and_solution(coin_id, h),
511                self.peer.try_get_puzzle_and_solution(coin_id, h),
512                self.coinset.get_puzzle_and_solution(coin_id, height),
513            )
514            .await
515        } else {
516            // No height provided -- peer can resolve it via coin state.
517            self.peer_then_coinset(
518                self.peer.try_get_puzzle_and_solution_auto(coin_id),
519                self.peer.try_get_puzzle_and_solution_auto(coin_id),
520                self.coinset.get_puzzle_and_solution(coin_id, None),
521            )
522            .await
523        }
524    }
525
526    /// Peer-backed: get puzzle & solution, then run puzzle(solution) to extract
527    /// parsed conditions.
528    pub async fn get_puzzle_and_solution_with_conditions(
529        &self,
530        coin_id: &str,
531        height: Option<u32>,
532    ) -> Result<CoinSpendWithConditions, ChiaQueryError> {
533        // Try to get the spend via peer first.
534        let spend = match self.get_puzzle_and_solution(coin_id, height).await {
535            Ok(s) => s,
536            Err(_) => {
537                if self.coinset_fallback_enabled {
538                    return self
539                        .coinset
540                        .get_puzzle_and_solution_with_conditions(coin_id, height)
541                        .await;
542                }
543                return Err(ChiaQueryError::PeerRejection(
544                    "could not retrieve puzzle and solution".into(),
545                ));
546            }
547        };
548
549        // Run puzzle(solution) to extract conditions.
550        let conditions = run_puzzle_conditions(&spend, self.peer.constants());
551        Ok(CoinSpendWithConditions {
552            coin_spend: spend,
553            conditions,
554        })
555    }
556
557    pub async fn push_tx(&self, bundle: &SpendBundle) -> Result<TxStatus, ChiaQueryError> {
558        self.peer_then_coinset(
559            self.peer.try_push_tx(bundle),
560            self.peer.try_push_tx(bundle),
561            self.coinset.push_tx(bundle),
562        )
563        .await
564    }
565}
566
567// ---------------------------------------------------------------------------
568// Fees (peer-backed with coinset fallback)
569// ---------------------------------------------------------------------------
570
571impl QueryRouter {
572    pub async fn get_fee_estimate(
573        &self,
574        spend_bundle: Option<&SpendBundle>,
575        target_times: Option<&[u64]>,
576        spend_count: Option<u64>,
577    ) -> Result<FeeEstimate, ChiaQueryError> {
578        let times = target_times.unwrap_or(&[60, 120, 300]);
579        self.peer_then_coinset(
580            self.peer.try_get_fee_estimate(times),
581            self.peer.try_get_fee_estimate(times),
582            self.coinset
583                .get_fee_estimate(spend_bundle, target_times, spend_count),
584        )
585        .await
586    }
587}
588
589// ---------------------------------------------------------------------------
590// Full node / network (all coinset-only)
591// ---------------------------------------------------------------------------
592
593impl QueryRouter {
594    /// Peer-backed: derived from the chia consensus constants for the
595    /// configured network.
596    pub async fn get_aggsig_additional_data(&self) -> Result<String, ChiaQueryError> {
597        Ok(self.peer.aggsig_additional_data())
598    }
599
600    /// Peer-backed: derived from the chia consensus constants for the
601    /// configured network.
602    pub async fn get_network_info(&self) -> Result<NetworkInfo, ChiaQueryError> {
603        Ok(self.peer.network_info())
604    }
605
606    /// Peer-backed partially: peak height is tracked from `NewPeakWallet`
607    /// messages received from peers.  Full state comes from coinset.
608    pub async fn get_blockchain_state(&self) -> Result<BlockchainState, ChiaQueryError> {
609        // Try coinset first for full state.
610        if self.coinset_fallback_enabled {
611            if let Ok(state) = self.coinset.get_blockchain_state().await {
612                return Ok(state);
613            }
614        }
615        // Fallback: return a minimal state from the peer-tracked peak.
616        let peak = self.peer.peak_height();
617        if peak == 0 {
618            return Err(ChiaQueryError::PeerConnection(
619                "no peak observed from peers yet".into(),
620            ));
621        }
622        Ok(BlockchainState {
623            peak: Some(BlockRecord {
624                height: peak,
625                ..Default::default()
626            }),
627            sync: Some(SyncState {
628                synced: true,
629                sync_mode: false,
630                sync_progress_height: peak,
631                sync_tip_height: peak,
632            }),
633            ..Default::default()
634        })
635    }
636
637    pub async fn get_network_space(
638        &self,
639        newer_block_header_hash: &str,
640        older_block_header_hash: &str,
641    ) -> Result<u64, ChiaQueryError> {
642        self.require_coinset("get_network_space")?;
643        self.coinset
644            .get_network_space(newer_block_header_hash, older_block_header_hash)
645            .await
646    }
647}
648
649// ---------------------------------------------------------------------------
650// Mempool (all coinset-only)
651// ---------------------------------------------------------------------------
652
653impl QueryRouter {
654    pub async fn get_all_mempool_items(
655        &self,
656    ) -> Result<HashMap<String, MempoolItem>, ChiaQueryError> {
657        self.require_coinset("get_all_mempool_items")?;
658        self.coinset.get_all_mempool_items().await
659    }
660
661    pub async fn get_all_mempool_tx_ids(&self) -> Result<Vec<String>, ChiaQueryError> {
662        self.require_coinset("get_all_mempool_tx_ids")?;
663        self.coinset.get_all_mempool_tx_ids().await
664    }
665
666    pub async fn get_mempool_item_by_tx_id(
667        &self,
668        tx_id: &str,
669    ) -> Result<MempoolItem, ChiaQueryError> {
670        self.require_coinset("get_mempool_item_by_tx_id")?;
671        self.coinset.get_mempool_item_by_tx_id(tx_id).await
672    }
673
674    pub async fn get_mempool_items_by_coin_name(
675        &self,
676        coin_name: &str,
677        include_spent_coins: Option<bool>,
678    ) -> Result<Vec<MempoolItem>, ChiaQueryError> {
679        self.require_coinset("get_mempool_items_by_coin_name")?;
680        self.coinset
681            .get_mempool_items_by_coin_name(coin_name, include_spent_coins)
682            .await
683    }
684}