Skip to main content

chia_query/
lib.rs

1//! # chia-query
2//!
3//! Query the Chia blockchain through decentralized peer connections with
4//! automatic fallback to the [coinset.org](https://api.coinset.org) HTTP API.
5//!
6//! No Chia installation is required. The peer TLS client identity is generated in
7//! memory by default ([`TlsIdentity::Generated`]), so a client works from a service
8//! account with no home directory of its own.
9//!
10//! ```rust,no_run
11//! use chia_query::{ChiaQuery, ChiaQueryConfig};
12//!
13//! #[tokio::main]
14//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
15//!     let client = ChiaQuery::new(ChiaQueryConfig::default()).await?;
16//!     let record = client.get_coin_record_by_name("0xabc...").await?;
17//!     println!("{:?}", record);
18//!     Ok(())
19//! }
20//! ```
21
22pub mod coinset;
23pub mod drift;
24pub mod types;
25
26// The `@dignetwork/chia-query-wasm` bindings — only for the wasm coinset build.
27#[cfg(all(target_arch = "wasm32", feature = "coinset", not(feature = "native")))]
28pub mod wasm_api;
29
30// The peer WebSocket backend + the routing/CLVM layer are native-only: they
31// pull in `chia-wallet-sdk` (native-tls), `clvmr`, and tokio networking, none
32// of which belong in the wasm coinset-only build.
33#[cfg(feature = "native")]
34pub mod peer;
35#[cfg(feature = "native")]
36pub mod provider_registry;
37#[cfg(feature = "native")]
38pub mod router;
39
40pub use types::*;
41
42// Everything below — the full `ChiaQuery` client that races peers against the
43// coinset fallback — is the native surface. A wasm consumer uses
44// [`coinset::CoinsetClient`] directly with an injected `fetch` transport.
45#[cfg(feature = "native")]
46mod native_client {
47    use std::collections::HashMap;
48    use std::path::PathBuf;
49    use std::time::Duration;
50
51    use serde_json::Value;
52
53    use crate::types::*;
54    use crate::{coinset, peer, router};
55
56    // ---------------------------------------------------------------------------
57    // NetworkType
58    // ---------------------------------------------------------------------------
59
60    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
61    pub enum NetworkType {
62        Mainnet,
63        Testnet11,
64    }
65
66    impl NetworkType {
67        pub fn network_id(self) -> &'static str {
68            match self {
69                Self::Mainnet => "mainnet",
70                Self::Testnet11 => "testnet11",
71            }
72        }
73    }
74
75    // ---------------------------------------------------------------------------
76    // Configuration
77    // ---------------------------------------------------------------------------
78
79    /// Where the peer-protocol TLS client identity comes from.
80    ///
81    /// Modelled as one choice rather than a pair of optional paths so a half-configured
82    /// identity — a certificate without its key — cannot be expressed.
83    #[derive(Debug, Clone, PartialEq, Eq)]
84    pub enum TlsIdentity {
85        /// Generate a fresh self-signed certificate in memory (the default).
86        ///
87        /// Chia full nodes accept any well-formed client certificate, so nothing is
88        /// gained by requiring one on disk and a great deal is lost: a service account
89        /// has no populated `~/.chia`, which is what made every balance read fail in
90        /// dig_ecosystem#2210. See [`peer::connect::create_generated_tls`] for why the
91        /// certificate is not persisted.
92        Generated,
93
94        /// Load an existing certificate/key pair, e.g. a real Chia node's wallet cert.
95        Files {
96            cert_path: PathBuf,
97            key_path: PathBuf,
98        },
99    }
100
101    pub struct ChiaQueryConfig {
102        pub network: NetworkType,
103        /// How many peer sessions the pool holds.
104        ///
105        /// Defaults to [`peer::plurality::default_max_peers`], which is DERIVED from the sample a
106        /// corroborated read needs to leave standing rather than picked. Lowering it below that
107        /// does not make corroboration weaker quietly — the pool refuses rather than degrading —
108        /// but it does make it unavailable.
109        pub max_peers: usize,
110        pub coinset_base_url: String,
111        pub coinset_fallback_enabled: bool,
112        pub tls_identity: TlsIdentity,
113        pub peer_connect_timeout: Duration,
114        pub peer_request_timeout: Duration,
115        pub coinset_request_timeout: Duration,
116    }
117
118    impl Default for ChiaQueryConfig {
119        fn default() -> Self {
120            Self {
121                network: NetworkType::Mainnet,
122                max_peers: peer::plurality::default_max_peers(),
123                coinset_base_url: "https://api.coinset.org".into(),
124                coinset_fallback_enabled: true,
125                tls_identity: TlsIdentity::Generated,
126                peer_connect_timeout: Duration::from_secs(8),
127                peer_request_timeout: Duration::from_secs(30),
128                coinset_request_timeout: Duration::from_secs(30),
129            }
130        }
131    }
132
133    // ---------------------------------------------------------------------------
134    // ChiaQuery -- the public entry-point
135    // ---------------------------------------------------------------------------
136
137    pub struct ChiaQuery {
138        router: router::QueryRouter,
139    }
140
141    impl ChiaQuery {
142        /// Create a new client.  This will:
143        /// 1. Establish the peer TLS identity (generated by default — no files needed).
144        /// 2. Discover peers via DNS and connect up to `max_peers` concurrently.
145        /// 3. Initialise the coinset.org HTTP client.
146        ///
147        /// Peer discovery failing is fatal ([`ChiaQueryError::PeerDiscoveryFailed`])
148        /// ONLY when the coinset fallback is disabled; with the fallback enabled the
149        /// client is usable immediately and the peer pool refills in the background.
150        pub async fn new(cfg: ChiaQueryConfig) -> Result<Self, ChiaQueryError> {
151            let tls = match &cfg.tls_identity {
152                TlsIdentity::Generated => peer::connect::create_generated_tls()?,
153                TlsIdentity::Files {
154                    cert_path,
155                    key_path,
156                } => peer::connect::create_tls(cert_path, key_path)?,
157            };
158
159            // The coinset tier is plain HTTP and needs neither a credential nor a peer,
160            // so a peer-tier problem must not deny a reader the fallback that exists
161            // for exactly that case (dig_ecosystem#2210).
162            let peer_requirement = if cfg.coinset_fallback_enabled {
163                peer::PeerRequirement::Optional
164            } else {
165                peer::PeerRequirement::Required
166            };
167
168            let peer_backend = peer::PeerBackend::new(
169                cfg.network,
170                tls,
171                cfg.max_peers,
172                peer_requirement,
173                cfg.peer_connect_timeout,
174                cfg.peer_request_timeout,
175            )
176            .await?;
177
178            let coinset_client =
179                coinset::CoinsetClient::new(&cfg.coinset_base_url, cfg.coinset_request_timeout)?;
180
181            Ok(Self {
182                router: router::QueryRouter {
183                    peer: std::sync::Arc::new(peer_backend),
184                    coinset: coinset_client,
185                    coinset_fallback_enabled: cfg.coinset_fallback_enabled,
186                },
187            })
188        }
189
190        // =======================================================================
191        // Blocks
192        // =======================================================================
193
194        pub async fn get_additions_and_removals(
195            &self,
196            header_hash: &str,
197        ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
198            self.router.get_additions_and_removals(header_hash).await
199        }
200
201        pub async fn get_block(&self, header_hash: &str) -> Result<FullBlock, ChiaQueryError> {
202            self.router.get_block(header_hash).await
203        }
204
205        /// Fetch a full block by height.  Peer-backed via `RequestBlock`.
206        pub async fn get_block_by_height(&self, height: u32) -> Result<FullBlock, ChiaQueryError> {
207            self.router.get_block_by_height(height).await
208        }
209
210        pub async fn get_block_count_metrics(&self) -> Result<BlockCountMetrics, ChiaQueryError> {
211            self.router.get_block_count_metrics().await
212        }
213
214        pub async fn get_block_record(
215            &self,
216            header_hash: &str,
217        ) -> Result<BlockRecord, ChiaQueryError> {
218            self.router.get_block_record(header_hash).await
219        }
220
221        pub async fn get_block_record_by_height(
222            &self,
223            height: u32,
224        ) -> Result<BlockRecord, ChiaQueryError> {
225            self.router.get_block_record_by_height(height).await
226        }
227
228        pub async fn get_block_records(
229            &self,
230            start: u32,
231            end: u32,
232        ) -> Result<Vec<BlockRecord>, ChiaQueryError> {
233            self.router.get_block_records(start, end).await
234        }
235
236        pub async fn get_block_spends(
237            &self,
238            header_hash: &str,
239        ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
240            self.router.get_block_spends(header_hash).await
241        }
242
243        pub async fn get_block_spends_with_conditions(
244            &self,
245            header_hash: &str,
246        ) -> Result<Vec<CoinSpendWithConditions>, ChiaQueryError> {
247            self.router
248                .get_block_spends_with_conditions(header_hash)
249                .await
250        }
251
252        pub async fn get_blocks(
253            &self,
254            start: u32,
255            end: u32,
256            exclude_header_hash: bool,
257            exclude_reorged: bool,
258        ) -> Result<Vec<FullBlock>, ChiaQueryError> {
259            self.router
260                .get_blocks(start, end, exclude_header_hash, exclude_reorged)
261                .await
262        }
263
264        pub async fn get_unfinished_block_headers(
265            &self,
266        ) -> Result<Vec<UnfinishedBlockHeader>, ChiaQueryError> {
267            self.router.get_unfinished_block_headers().await
268        }
269
270        // =======================================================================
271        // Coins
272        // =======================================================================
273
274        pub async fn get_coin_record_by_name(
275            &self,
276            name: &str,
277        ) -> Result<CoinRecord, ChiaQueryError> {
278            self.router.get_coin_record_by_name(name).await
279        }
280
281        /// Absence-aware [`get_coin_record_by_name`](Self::get_coin_record_by_name).
282        ///
283        /// `Ok(None)` means **corroborated absence**: two independent sources were asked and both
284        /// reported no such coin. Absence that only one source will vouch for is
285        /// [`UncorroboratedAbsence`](ChiaQueryError::UncorroboratedAbsence), and two sources that
286        /// contradict each other are [`SourcesDisagree`](ChiaQueryError::SourcesDisagree) — both
287        /// errors, because neither is a fact about the chain (dig_ecosystem#2456).
288        ///
289        /// `Ok(Some(record))` means **corroborated presence**, and means it for the same reason:
290        /// the coin-id binding authenticates the coin's identity only, so `confirmed_block_index`
291        /// and `spent_block_index` are put to a second independent source before they are
292        /// reported. A record only one source will vouch for is
293        /// [`UncorroboratedPresence`](ChiaQueryError::UncorroboratedPresence)
294        /// (dig_ecosystem#2462).
295        ///
296        /// Used by the [`ChainSource`](dig_chainsource_interface::ChainSource) facade to honour the
297        /// fail-closed `Ok(None)`-vs-`Err` contract.
298        pub async fn get_coin_record_by_name_opt(
299            &self,
300            name: &str,
301        ) -> Result<Option<CoinRecord>, ChiaQueryError> {
302            self.router.get_coin_record_by_name_opt(name).await
303        }
304
305        /// Absence-aware read of the spend that spent `coin_id`.
306        ///
307        /// `Ok(None)` when two independent sources agree the coin is unspent or unknown,
308        /// `Ok(Some(spend))` when two agree on the spend; `Err` on failure, and on an answer in
309        /// either direction that only one source will vouch for — see
310        /// [`get_coin_record_by_name_opt`](Self::get_coin_record_by_name_opt).
311        pub async fn get_coin_spend_opt(
312            &self,
313            coin_id: &str,
314        ) -> Result<Option<CoinSpend>, ChiaQueryError> {
315            self.router.get_coin_spend_opt(coin_id).await
316        }
317
318        /// The current peak height (`Ok(None)` when unavailable), `Err` on failure.
319        pub async fn peak_height_opt(&self) -> Result<Option<u32>, ChiaQueryError> {
320            self.router.peak_height_opt().await
321        }
322
323        /// How many Chia full-node peers this client HOLDS right now.
324        ///
325        /// Exposed because a consumer that presents itself as a light client has to be able to
326        /// SAY how many peers it is a client of, and until now the pool's size was observable
327        /// only as the boolean [`has_peers`](peer::PeerBackend::has_peers). A count is not
328        /// derivable from that, and a consumer with no way to read it is left either silent or
329        /// quoting [`ChiaQueryConfig::max_peers`] — an intention presented as a measurement.
330        ///
331        /// It is the LIVE count, never the target: a filling pool reports the smaller number.
332        /// See [`peer::pool::PeerPool::peer_count`] for what "held" means with respect to a peer
333        /// that has died without being used since.
334        pub async fn peer_count(&self) -> usize {
335            self.router.peer.peer_count().await
336        }
337
338        /// How many held peers count as INDEPENDENT opinions about the chain.
339        ///
340        /// Never larger than [`peer_count`](Self::peer_count), and smaller by the peers reached
341        /// from a preferred address — an operator's `TRUSTED_FULLNODE`, or a full node on this
342        /// machine. Those are the fastest peers to read from and the worst possible witnesses to
343        /// each other: a local process is a source a local attacker can supply, so a count of
344        /// agreeing sources that includes one is not a count of independent sources.
345        ///
346        /// Use this, not `peer_count`, for any decision of the form "do enough separate sources
347        /// agree" (dig_ecosystem#2648). Use `peer_count` to tell a user how many peers are held.
348        pub async fn independent_peer_count(&self) -> usize {
349            self.router.peer.independent_peer_count().await
350        }
351
352        /// The peak height this client's OWN peers have reported, or `None` when they have
353        /// reported none yet.
354        ///
355        /// Distinct from [`peak_height_opt`](Self::peak_height_opt), which answers "what is the
356        /// chain's peak" and consults coinset FIRST — so its figure is a third party's view of
357        /// the chain even on a client holding peers. This one answers "what have MY peers told
358        /// me", which is the only form of the question a light client can demonstrate, and it
359        /// makes no network call at all: the pool tracks it from inbound `NewPeakWallet`
360        /// messages.
361        ///
362        /// `None` is UNKNOWN, never height zero. The pool spells an unobserved peak `0`
363        /// internally, and every block is trivially above zero, so returning it would silently
364        /// satisfy any "is this buried yet" comparison a caller makes.
365        pub async fn peer_peak_height(&self) -> Option<u32> {
366            observed_peak(self.router.peer.peak_height())
367        }
368
369        /// A subscribing light client over THIS client's peer pool.
370        ///
371        /// The returned [`ChiaLightClient`](peer::ChiaLightClient) borrows the same held sessions
372        /// this client reads through, so a consumer that needs both coin subscriptions and ordinary
373        /// queries holds ONE set of peers with ONE notion of the peak. Building a second
374        /// `ChiaQuery` to get a light client would recreate exactly the split this replaces.
375        ///
376        /// `request_timeout` bounds each of the light client's own requests; it does not affect
377        /// this client's.
378        pub async fn light_client(&self, request_timeout: Duration) -> peer::ChiaLightClient {
379            peer::ChiaLightClient::new(self.router.peer.clone(), request_timeout).await
380        }
381
382        /// The Unix timestamp of the block at `height` (`Ok(None)` when absent), `Err` on failure.
383        pub async fn block_timestamp_opt(
384            &self,
385            height: u32,
386        ) -> Result<Option<u64>, ChiaQueryError> {
387            self.router.block_timestamp_opt(height).await
388        }
389
390        pub async fn get_coin_records_by_hint(
391            &self,
392            hint: &str,
393            start_height: Option<u32>,
394            end_height: Option<u32>,
395            include_spent_coins: bool,
396        ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
397            self.router
398                .get_coin_records_by_hint(hint, start_height, end_height, include_spent_coins)
399                .await
400        }
401
402        pub async fn get_coin_records_by_hints(
403            &self,
404            hints: &[String],
405            start_height: Option<u32>,
406            end_height: Option<u32>,
407            include_spent_coins: bool,
408        ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
409            self.router
410                .get_coin_records_by_hints(hints, start_height, end_height, include_spent_coins)
411                .await
412        }
413
414        pub async fn get_coin_records_by_names(
415            &self,
416            names: &[String],
417            start_height: Option<u32>,
418            end_height: Option<u32>,
419            include_spent_coins: bool,
420        ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
421            self.router
422                .get_coin_records_by_names(names, start_height, end_height, include_spent_coins)
423                .await
424        }
425
426        pub async fn get_coin_records_by_parent_ids(
427            &self,
428            parent_ids: &[String],
429            start_height: Option<u32>,
430            end_height: Option<u32>,
431            include_spent_coins: bool,
432        ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
433            self.router
434                .get_coin_records_by_parent_ids(
435                    parent_ids,
436                    start_height,
437                    end_height,
438                    include_spent_coins,
439                )
440                .await
441        }
442
443        pub async fn get_coin_records_by_puzzle_hash(
444            &self,
445            puzzle_hash: &str,
446            start_height: Option<u32>,
447            end_height: Option<u32>,
448            include_spent_coins: bool,
449        ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
450            self.router
451                .get_coin_records_by_puzzle_hash(
452                    puzzle_hash,
453                    start_height,
454                    end_height,
455                    include_spent_coins,
456                )
457                .await
458        }
459
460        pub async fn get_coin_records_by_puzzle_hashes(
461            &self,
462            puzzle_hashes: &[String],
463            start_height: Option<u32>,
464            end_height: Option<u32>,
465            include_spent_coins: bool,
466        ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
467            self.router
468                .get_coin_records_by_puzzle_hashes(
469                    puzzle_hashes,
470                    start_height,
471                    end_height,
472                    include_spent_coins,
473                )
474                .await
475        }
476
477        pub async fn get_memos_by_coin_name(&self, name: &str) -> Result<Value, ChiaQueryError> {
478            self.router.get_memos_by_coin_name(name).await
479        }
480
481        pub async fn get_puzzle_and_solution(
482            &self,
483            coin_id: &str,
484            height: Option<u32>,
485        ) -> Result<CoinSpend, ChiaQueryError> {
486            self.router.get_puzzle_and_solution(coin_id, height).await
487        }
488
489        pub async fn get_puzzle_and_solution_with_conditions(
490            &self,
491            coin_id: &str,
492            height: Option<u32>,
493        ) -> Result<CoinSpendWithConditions, ChiaQueryError> {
494            self.router
495                .get_puzzle_and_solution_with_conditions(coin_id, height)
496                .await
497        }
498
499        pub async fn push_tx(
500            &self,
501            spend_bundle: &SpendBundle,
502        ) -> Result<TxStatus, ChiaQueryError> {
503            self.router.push_tx(spend_bundle).await
504        }
505
506        // =======================================================================
507        // Fees
508        // =======================================================================
509
510        pub async fn get_fee_estimate(
511            &self,
512            spend_bundle: Option<&SpendBundle>,
513            target_times: Option<&[u64]>,
514            spend_count: Option<u64>,
515        ) -> Result<FeeEstimate, ChiaQueryError> {
516            self.router
517                .get_fee_estimate(spend_bundle, target_times, spend_count)
518                .await
519        }
520
521        // =======================================================================
522        // Full node / network
523        // =======================================================================
524
525        pub async fn get_aggsig_additional_data(&self) -> Result<String, ChiaQueryError> {
526            self.router.get_aggsig_additional_data().await
527        }
528
529        pub async fn get_network_info(&self) -> Result<NetworkInfo, ChiaQueryError> {
530            self.router.get_network_info().await
531        }
532
533        pub async fn get_blockchain_state(&self) -> Result<BlockchainState, ChiaQueryError> {
534            self.router.get_blockchain_state().await
535        }
536
537        pub async fn get_network_space(
538            &self,
539            newer_block_header_hash: &str,
540            older_block_header_hash: &str,
541        ) -> Result<u64, ChiaQueryError> {
542            self.router
543                .get_network_space(newer_block_header_hash, older_block_header_hash)
544                .await
545        }
546
547        // =======================================================================
548        // Mempool
549        // =======================================================================
550
551        pub async fn get_all_mempool_items(
552            &self,
553        ) -> Result<HashMap<String, MempoolItem>, ChiaQueryError> {
554            self.router.get_all_mempool_items().await
555        }
556
557        pub async fn get_all_mempool_tx_ids(&self) -> Result<Vec<String>, ChiaQueryError> {
558            self.router.get_all_mempool_tx_ids().await
559        }
560
561        pub async fn get_mempool_item_by_tx_id(
562            &self,
563            tx_id: &str,
564        ) -> Result<MempoolItem, ChiaQueryError> {
565            self.router.get_mempool_item_by_tx_id(tx_id).await
566        }
567
568        pub async fn get_mempool_items_by_coin_name(
569            &self,
570            coin_name: &str,
571            include_spent_coins: Option<bool>,
572        ) -> Result<Vec<MempoolItem>, ChiaQueryError> {
573            self.router
574                .get_mempool_items_by_coin_name(coin_name, include_spent_coins)
575                .await
576        }
577
578        // =======================================================================
579        // Convenience helpers
580        // =======================================================================
581
582        /// Poll the blockchain until a coin appears on-chain (confirmed) or the
583        /// timeout elapses.
584        ///
585        /// Returns the [`CoinRecord`] once the coin is found with a non-zero
586        /// `confirmed_block_index`.  Returns an error if the timeout expires
587        /// before the coin is confirmed.
588        ///
589        /// ```rust,no_run
590        /// # use chia_query::{ChiaQuery, ChiaQueryConfig};
591        /// # use std::time::Duration;
592        /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
593        /// let client = ChiaQuery::new(ChiaQueryConfig::default()).await?;
594        /// let record = client.wait_for_confirmation(
595        ///     "0xabc...",
596        ///     Duration::from_secs(5),   // poll every 5 seconds
597        ///     Duration::from_secs(300), // give up after 5 minutes
598        /// ).await?;
599        /// println!("confirmed at height {}", record.confirmed_block_index);
600        /// # Ok(())
601        /// # }
602        /// ```
603        pub async fn wait_for_confirmation(
604            &self,
605            coin_id: &str,
606            poll_interval: Duration,
607            timeout: Duration,
608        ) -> Result<CoinRecord, ChiaQueryError> {
609            let deadline = tokio::time::Instant::now() + timeout;
610
611            loop {
612                match self.get_coin_record_by_name(coin_id).await {
613                    Ok(record) if record.confirmed_block_index > 0 => {
614                        return Ok(record);
615                    }
616                    Ok(_) => {
617                        // Coin exists but confirmed_block_index is 0 -- not
618                        // confirmed yet, keep polling.
619                    }
620                    Err(ChiaQueryError::PeerRejection(_))
621                    | Err(ChiaQueryError::CoinsetApiError(_)) => {
622                        // Coin not found yet -- keep polling.
623                    }
624                    Err(e) => {
625                        // Transient connection errors -- log and keep trying.
626                        log::debug!("wait_for_confirmation poll error: {e}");
627                    }
628                }
629
630                if tokio::time::Instant::now() + poll_interval > deadline {
631                    return Err(ChiaQueryError::PeerConnection(format!(
632                        "coin {coin_id} not confirmed within {timeout:?}"
633                    )));
634                }
635
636                tokio::time::sleep(poll_interval).await;
637            }
638        }
639    }
640
641    /// The pool's peak sentinel as an honest optional height.
642    ///
643    /// Kept as a named pure function rather than inlined, because the rule it encodes — an
644    /// unobserved peak is UNKNOWN and not height zero — is the whole reason
645    /// [`ChiaQuery::peer_peak_height`] returns an `Option`, and inline it is unreachable from a
646    /// test on a machine with no peers.
647    fn observed_peak(raw: u32) -> Option<u32> {
648        (raw != 0).then_some(raw)
649    }
650
651    #[cfg(test)]
652    mod tests {
653        use super::observed_peak;
654
655        /// **An unobserved peak is unknown, never zero.** The pool spells "no peer has told me a
656        /// peak" as `0`, and every block is trivially above zero — so a caller asking "is this
657        /// coin buried yet" against a leaked `0` gets a confident yes about a chain nobody has
658        /// looked at.
659        #[test]
660        fn an_unobserved_peak_is_unknown_and_a_real_height_survives() {
661            assert_eq!(observed_peak(0), None);
662            assert_eq!(observed_peak(1), Some(1));
663            assert_eq!(observed_peak(9_139_211), Some(9_139_211));
664        }
665    }
666} // mod native_client
667
668#[cfg(feature = "native")]
669pub use native_client::{ChiaQuery, ChiaQueryConfig, NetworkType, TlsIdentity};