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//! ```rust,no_run
7//! use chia_query::{ChiaQuery, ChiaQueryConfig};
8//!
9//! #[tokio::main]
10//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
11//!     let client = ChiaQuery::new(ChiaQueryConfig::default()).await?;
12//!     let record = client.get_coin_record_by_name("0xabc...").await?;
13//!     println!("{:?}", record);
14//!     Ok(())
15//! }
16//! ```
17
18pub mod coinset;
19pub mod drift;
20pub mod types;
21
22// The `@dignetwork/chia-query-wasm` bindings — only for the wasm coinset build.
23#[cfg(all(target_arch = "wasm32", feature = "coinset", not(feature = "native")))]
24pub mod wasm_api;
25
26// The peer WebSocket backend + the routing/CLVM layer are native-only: they
27// pull in `chia-wallet-sdk` (native-tls), `clvmr`, and tokio networking, none
28// of which belong in the wasm coinset-only build.
29#[cfg(feature = "native")]
30pub mod peer;
31#[cfg(feature = "native")]
32pub mod provider_registry;
33#[cfg(feature = "native")]
34pub mod router;
35
36pub use types::*;
37
38// Everything below — the full `ChiaQuery` client that races peers against the
39// coinset fallback — is the native surface. A wasm consumer uses
40// [`coinset::CoinsetClient`] directly with an injected `fetch` transport.
41#[cfg(feature = "native")]
42mod native_client {
43    use std::collections::HashMap;
44    use std::path::PathBuf;
45    use std::time::Duration;
46
47    use serde_json::Value;
48
49    use crate::types::*;
50    use crate::{coinset, peer, router};
51
52    // ---------------------------------------------------------------------------
53    // NetworkType
54    // ---------------------------------------------------------------------------
55
56    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
57    pub enum NetworkType {
58        Mainnet,
59        Testnet11,
60    }
61
62    impl NetworkType {
63        pub fn network_id(self) -> &'static str {
64            match self {
65                Self::Mainnet => "mainnet",
66                Self::Testnet11 => "testnet11",
67            }
68        }
69
70        fn default_cert_path(self) -> PathBuf {
71            let base = dirs_home().join(".chia");
72            match self {
73                Self::Mainnet => base.join("mainnet/config/ssl/wallet/wallet_node.crt"),
74                Self::Testnet11 => base.join("testnet11/config/ssl/wallet/wallet_node.crt"),
75            }
76        }
77
78        fn default_key_path(self) -> PathBuf {
79            let base = dirs_home().join(".chia");
80            match self {
81                Self::Mainnet => base.join("mainnet/config/ssl/wallet/wallet_node.key"),
82                Self::Testnet11 => base.join("testnet11/config/ssl/wallet/wallet_node.key"),
83            }
84        }
85    }
86
87    fn dirs_home() -> PathBuf {
88        #[cfg(target_os = "windows")]
89        {
90            std::env::var("USERPROFILE")
91                .map(PathBuf::from)
92                .unwrap_or_else(|_| PathBuf::from("C:\\"))
93        }
94        #[cfg(not(target_os = "windows"))]
95        {
96            std::env::var("HOME")
97                .map(PathBuf::from)
98                .unwrap_or_else(|_| PathBuf::from("/"))
99        }
100    }
101
102    // ---------------------------------------------------------------------------
103    // Configuration
104    // ---------------------------------------------------------------------------
105
106    pub struct ChiaQueryConfig {
107        pub network: NetworkType,
108        pub max_peers: usize,
109        pub coinset_base_url: String,
110        pub coinset_fallback_enabled: bool,
111        pub cert_path: PathBuf,
112        pub key_path: PathBuf,
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            let network = NetworkType::Mainnet;
121            Self {
122                network,
123                max_peers: 5,
124                coinset_base_url: "https://api.coinset.org".into(),
125                coinset_fallback_enabled: true,
126                cert_path: network.default_cert_path(),
127                key_path: network.default_key_path(),
128                peer_connect_timeout: Duration::from_secs(8),
129                peer_request_timeout: Duration::from_secs(30),
130                coinset_request_timeout: Duration::from_secs(30),
131            }
132        }
133    }
134
135    // ---------------------------------------------------------------------------
136    // ChiaQuery -- the public entry-point
137    // ---------------------------------------------------------------------------
138
139    pub struct ChiaQuery {
140        router: router::QueryRouter,
141    }
142
143    impl ChiaQuery {
144        /// Create a new client.  This will:
145        /// 1. Load TLS certificates from the configured paths.
146        /// 2. Discover peers via DNS and connect up to `max_peers` concurrently.
147        /// 3. Initialise the coinset.org HTTP client.
148        ///
149        /// At least one peer must connect successfully, otherwise this returns
150        /// [`ChiaQueryError::PeerDiscoveryFailed`].
151        pub async fn new(cfg: ChiaQueryConfig) -> Result<Self, ChiaQueryError> {
152            let tls = peer::connect::create_tls(&cfg.cert_path, &cfg.key_path)?;
153
154            let peer_backend = peer::PeerBackend::new(
155                cfg.network,
156                tls,
157                cfg.max_peers,
158                cfg.peer_connect_timeout,
159                cfg.peer_request_timeout,
160            )
161            .await?;
162
163            let coinset_client =
164                coinset::CoinsetClient::new(&cfg.coinset_base_url, cfg.coinset_request_timeout)?;
165
166            Ok(Self {
167                router: router::QueryRouter {
168                    peer: peer_backend,
169                    coinset: coinset_client,
170                    coinset_fallback_enabled: cfg.coinset_fallback_enabled,
171                },
172            })
173        }
174
175        // =======================================================================
176        // Blocks
177        // =======================================================================
178
179        pub async fn get_additions_and_removals(
180            &self,
181            header_hash: &str,
182        ) -> Result<AdditionsAndRemovals, ChiaQueryError> {
183            self.router.get_additions_and_removals(header_hash).await
184        }
185
186        pub async fn get_block(&self, header_hash: &str) -> Result<FullBlock, ChiaQueryError> {
187            self.router.get_block(header_hash).await
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.router.get_block_by_height(height).await
193        }
194
195        pub async fn get_block_count_metrics(&self) -> Result<BlockCountMetrics, ChiaQueryError> {
196            self.router.get_block_count_metrics().await
197        }
198
199        pub async fn get_block_record(
200            &self,
201            header_hash: &str,
202        ) -> Result<BlockRecord, ChiaQueryError> {
203            self.router.get_block_record(header_hash).await
204        }
205
206        pub async fn get_block_record_by_height(
207            &self,
208            height: u32,
209        ) -> Result<BlockRecord, ChiaQueryError> {
210            self.router.get_block_record_by_height(height).await
211        }
212
213        pub async fn get_block_records(
214            &self,
215            start: u32,
216            end: u32,
217        ) -> Result<Vec<BlockRecord>, ChiaQueryError> {
218            self.router.get_block_records(start, end).await
219        }
220
221        pub async fn get_block_spends(
222            &self,
223            header_hash: &str,
224        ) -> Result<Vec<CoinSpend>, ChiaQueryError> {
225            self.router.get_block_spends(header_hash).await
226        }
227
228        pub async fn get_block_spends_with_conditions(
229            &self,
230            header_hash: &str,
231        ) -> Result<Vec<CoinSpendWithConditions>, ChiaQueryError> {
232            self.router
233                .get_block_spends_with_conditions(header_hash)
234                .await
235        }
236
237        pub async fn get_blocks(
238            &self,
239            start: u32,
240            end: u32,
241            exclude_header_hash: bool,
242            exclude_reorged: bool,
243        ) -> Result<Vec<FullBlock>, ChiaQueryError> {
244            self.router
245                .get_blocks(start, end, exclude_header_hash, exclude_reorged)
246                .await
247        }
248
249        pub async fn get_unfinished_block_headers(
250            &self,
251        ) -> Result<Vec<UnfinishedBlockHeader>, ChiaQueryError> {
252            self.router.get_unfinished_block_headers().await
253        }
254
255        // =======================================================================
256        // Coins
257        // =======================================================================
258
259        pub async fn get_coin_record_by_name(
260            &self,
261            name: &str,
262        ) -> Result<CoinRecord, ChiaQueryError> {
263            self.router.get_coin_record_by_name(name).await
264        }
265
266        /// Absence-aware [`get_coin_record_by_name`](Self::get_coin_record_by_name): `Ok(None)` when
267        /// the coin provably does not exist, `Err` when the read could not be completed. Used by the
268        /// [`ChainSource`](dig_chainsource_interface::ChainSource) facade to honour the fail-closed
269        /// `Ok(None)`-vs-`Err` contract.
270        pub async fn get_coin_record_by_name_opt(
271            &self,
272            name: &str,
273        ) -> Result<Option<CoinRecord>, ChiaQueryError> {
274            self.router.get_coin_record_by_name_opt(name).await
275        }
276
277        /// Absence-aware read of the spend that spent `coin_id`: `Ok(None)` when the coin is
278        /// provably unspent/unknown, `Err` on failure.
279        pub async fn get_coin_spend_opt(
280            &self,
281            coin_id: &str,
282        ) -> Result<Option<CoinSpend>, ChiaQueryError> {
283            self.router.get_coin_spend_opt(coin_id).await
284        }
285
286        /// The current peak height (`Ok(None)` when unavailable), `Err` on failure.
287        pub async fn peak_height_opt(&self) -> Result<Option<u32>, ChiaQueryError> {
288            self.router.peak_height_opt().await
289        }
290
291        /// The Unix timestamp of the block at `height` (`Ok(None)` when absent), `Err` on failure.
292        pub async fn block_timestamp_opt(
293            &self,
294            height: u32,
295        ) -> Result<Option<u64>, ChiaQueryError> {
296            self.router.block_timestamp_opt(height).await
297        }
298
299        pub async fn get_coin_records_by_hint(
300            &self,
301            hint: &str,
302            start_height: Option<u32>,
303            end_height: Option<u32>,
304            include_spent_coins: bool,
305        ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
306            self.router
307                .get_coin_records_by_hint(hint, start_height, end_height, include_spent_coins)
308                .await
309        }
310
311        pub async fn get_coin_records_by_hints(
312            &self,
313            hints: &[String],
314            start_height: Option<u32>,
315            end_height: Option<u32>,
316            include_spent_coins: bool,
317        ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
318            self.router
319                .get_coin_records_by_hints(hints, start_height, end_height, include_spent_coins)
320                .await
321        }
322
323        pub async fn get_coin_records_by_names(
324            &self,
325            names: &[String],
326            start_height: Option<u32>,
327            end_height: Option<u32>,
328            include_spent_coins: bool,
329        ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
330            self.router
331                .get_coin_records_by_names(names, start_height, end_height, include_spent_coins)
332                .await
333        }
334
335        pub async fn get_coin_records_by_parent_ids(
336            &self,
337            parent_ids: &[String],
338            start_height: Option<u32>,
339            end_height: Option<u32>,
340            include_spent_coins: bool,
341        ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
342            self.router
343                .get_coin_records_by_parent_ids(
344                    parent_ids,
345                    start_height,
346                    end_height,
347                    include_spent_coins,
348                )
349                .await
350        }
351
352        pub async fn get_coin_records_by_puzzle_hash(
353            &self,
354            puzzle_hash: &str,
355            start_height: Option<u32>,
356            end_height: Option<u32>,
357            include_spent_coins: bool,
358        ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
359            self.router
360                .get_coin_records_by_puzzle_hash(
361                    puzzle_hash,
362                    start_height,
363                    end_height,
364                    include_spent_coins,
365                )
366                .await
367        }
368
369        pub async fn get_coin_records_by_puzzle_hashes(
370            &self,
371            puzzle_hashes: &[String],
372            start_height: Option<u32>,
373            end_height: Option<u32>,
374            include_spent_coins: bool,
375        ) -> Result<Vec<CoinRecord>, ChiaQueryError> {
376            self.router
377                .get_coin_records_by_puzzle_hashes(
378                    puzzle_hashes,
379                    start_height,
380                    end_height,
381                    include_spent_coins,
382                )
383                .await
384        }
385
386        pub async fn get_memos_by_coin_name(&self, name: &str) -> Result<Value, ChiaQueryError> {
387            self.router.get_memos_by_coin_name(name).await
388        }
389
390        pub async fn get_puzzle_and_solution(
391            &self,
392            coin_id: &str,
393            height: Option<u32>,
394        ) -> Result<CoinSpend, ChiaQueryError> {
395            self.router.get_puzzle_and_solution(coin_id, height).await
396        }
397
398        pub async fn get_puzzle_and_solution_with_conditions(
399            &self,
400            coin_id: &str,
401            height: Option<u32>,
402        ) -> Result<CoinSpendWithConditions, ChiaQueryError> {
403            self.router
404                .get_puzzle_and_solution_with_conditions(coin_id, height)
405                .await
406        }
407
408        pub async fn push_tx(
409            &self,
410            spend_bundle: &SpendBundle,
411        ) -> Result<TxStatus, ChiaQueryError> {
412            self.router.push_tx(spend_bundle).await
413        }
414
415        // =======================================================================
416        // Fees
417        // =======================================================================
418
419        pub async fn get_fee_estimate(
420            &self,
421            spend_bundle: Option<&SpendBundle>,
422            target_times: Option<&[u64]>,
423            spend_count: Option<u64>,
424        ) -> Result<FeeEstimate, ChiaQueryError> {
425            self.router
426                .get_fee_estimate(spend_bundle, target_times, spend_count)
427                .await
428        }
429
430        // =======================================================================
431        // Full node / network
432        // =======================================================================
433
434        pub async fn get_aggsig_additional_data(&self) -> Result<String, ChiaQueryError> {
435            self.router.get_aggsig_additional_data().await
436        }
437
438        pub async fn get_network_info(&self) -> Result<NetworkInfo, ChiaQueryError> {
439            self.router.get_network_info().await
440        }
441
442        pub async fn get_blockchain_state(&self) -> Result<BlockchainState, ChiaQueryError> {
443            self.router.get_blockchain_state().await
444        }
445
446        pub async fn get_network_space(
447            &self,
448            newer_block_header_hash: &str,
449            older_block_header_hash: &str,
450        ) -> Result<u64, ChiaQueryError> {
451            self.router
452                .get_network_space(newer_block_header_hash, older_block_header_hash)
453                .await
454        }
455
456        // =======================================================================
457        // Mempool
458        // =======================================================================
459
460        pub async fn get_all_mempool_items(
461            &self,
462        ) -> Result<HashMap<String, MempoolItem>, ChiaQueryError> {
463            self.router.get_all_mempool_items().await
464        }
465
466        pub async fn get_all_mempool_tx_ids(&self) -> Result<Vec<String>, ChiaQueryError> {
467            self.router.get_all_mempool_tx_ids().await
468        }
469
470        pub async fn get_mempool_item_by_tx_id(
471            &self,
472            tx_id: &str,
473        ) -> Result<MempoolItem, ChiaQueryError> {
474            self.router.get_mempool_item_by_tx_id(tx_id).await
475        }
476
477        pub async fn get_mempool_items_by_coin_name(
478            &self,
479            coin_name: &str,
480            include_spent_coins: Option<bool>,
481        ) -> Result<Vec<MempoolItem>, ChiaQueryError> {
482            self.router
483                .get_mempool_items_by_coin_name(coin_name, include_spent_coins)
484                .await
485        }
486
487        // =======================================================================
488        // Convenience helpers
489        // =======================================================================
490
491        /// Poll the blockchain until a coin appears on-chain (confirmed) or the
492        /// timeout elapses.
493        ///
494        /// Returns the [`CoinRecord`] once the coin is found with a non-zero
495        /// `confirmed_block_index`.  Returns an error if the timeout expires
496        /// before the coin is confirmed.
497        ///
498        /// ```rust,no_run
499        /// # use chia_query::{ChiaQuery, ChiaQueryConfig};
500        /// # use std::time::Duration;
501        /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
502        /// let client = ChiaQuery::new(ChiaQueryConfig::default()).await?;
503        /// let record = client.wait_for_confirmation(
504        ///     "0xabc...",
505        ///     Duration::from_secs(5),   // poll every 5 seconds
506        ///     Duration::from_secs(300), // give up after 5 minutes
507        /// ).await?;
508        /// println!("confirmed at height {}", record.confirmed_block_index);
509        /// # Ok(())
510        /// # }
511        /// ```
512        pub async fn wait_for_confirmation(
513            &self,
514            coin_id: &str,
515            poll_interval: Duration,
516            timeout: Duration,
517        ) -> Result<CoinRecord, ChiaQueryError> {
518            let deadline = tokio::time::Instant::now() + timeout;
519
520            loop {
521                match self.get_coin_record_by_name(coin_id).await {
522                    Ok(record) if record.confirmed_block_index > 0 => {
523                        return Ok(record);
524                    }
525                    Ok(_) => {
526                        // Coin exists but confirmed_block_index is 0 -- not
527                        // confirmed yet, keep polling.
528                    }
529                    Err(ChiaQueryError::PeerRejection(_))
530                    | Err(ChiaQueryError::CoinsetApiError(_)) => {
531                        // Coin not found yet -- keep polling.
532                    }
533                    Err(e) => {
534                        // Transient connection errors -- log and keep trying.
535                        log::debug!("wait_for_confirmation poll error: {e}");
536                    }
537                }
538
539                if tokio::time::Instant::now() + poll_interval > deadline {
540                    return Err(ChiaQueryError::PeerConnection(format!(
541                        "coin {coin_id} not confirmed within {timeout:?}"
542                    )));
543                }
544
545                tokio::time::sleep(poll_interval).await;
546            }
547        }
548    }
549} // mod native_client
550
551#[cfg(feature = "native")]
552pub use native_client::{ChiaQuery, ChiaQueryConfig, NetworkType};