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