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