Skip to main content

canic_host/replica_query/
mod.rs

1//! Module: replica_query
2//!
3//! Responsibility: query maintained Canic endpoints through a direct local replica transport.
4//! Does not own: endpoint DTOs, topology projection, or ICP CLI command execution.
5//! Boundary: decodes canonical Candid responses and preserves typed transport and endpoint errors.
6
7mod cbor;
8mod status;
9#[cfg(test)]
10mod tests;
11mod transport;
12
13use self::transport::local_query;
14use std::path::Path;
15
16use candid::Decode;
17use canic_core::dto::error::Error as CanicError;
18use thiserror::Error as ThisError;
19
20pub use self::status::local_replica_status_reachable_from_root;
21pub(crate) use self::{
22    status::local_replica_root_key_from_root, transport::local_replica_endpoint_from_root,
23};
24
25fn nonempty_text(text: &str) -> Option<String> {
26    let trimmed = text.trim();
27    (!trimmed.is_empty()).then(|| trimmed.to_string())
28}
29
30fn decode_cycle_balance_response(bytes: &[u8]) -> Result<u128, ReplicaQueryError> {
31    let result = Decode!(bytes, Result<u128, CanicError>).map_err(ReplicaQueryError::Candid)?;
32    result.map_err(ReplicaQueryError::Canister)
33}
34
35///
36/// ReplicaQueryError
37///
38
39#[derive(Debug, ThisError)]
40pub enum ReplicaQueryError {
41    #[error(transparent)]
42    Candid(candid::Error),
43
44    #[error("{0}")]
45    Canister(CanicError),
46
47    #[error("{0}")]
48    Cbor(String),
49
50    #[error(transparent)]
51    Io(#[from] std::io::Error),
52
53    #[error("{0}")]
54    Query(String),
55
56    #[error("local replica rejected query: code={code} message={message}")]
57    Rejected { code: u64, message: String },
58}
59
60impl From<cbor::CborError> for ReplicaQueryError {
61    // Convert CBOR encode/decode failures.
62    fn from(err: cbor::CborError) -> Self {
63        Self::Cbor(err.to_string())
64    }
65}
66
67/// Return whether the selected environment should use direct local replica queries.
68#[must_use]
69pub fn should_use_local_replica_query(environment: Option<&str>) -> bool {
70    environment
71        .is_none_or(|environment| environment == "local" || environment.starts_with("http://"))
72}
73
74/// Query `canic_ready` directly through the local replica HTTP API.
75pub(crate) fn query_ready(
76    environment: Option<&str>,
77    canister: &str,
78    icp_root: Option<&Path>,
79) -> Result<bool, ReplicaQueryError> {
80    let bytes = local_query(environment, canister, "canic_ready", icp_root)?;
81    Decode!(&bytes, bool).map_err(ReplicaQueryError::Candid)
82}
83
84/// Query `canic_cycle_balance` directly through the local replica HTTP API.
85pub(crate) fn query_cycle_balance(
86    environment: Option<&str>,
87    canister: &str,
88    icp_root: Option<&Path>,
89) -> Result<u128, ReplicaQueryError> {
90    let bytes = local_query(environment, canister, "canic_cycle_balance", icp_root)?;
91    decode_cycle_balance_response(&bytes)
92}