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