canic_host/replica_query/
mod.rs1mod cbor;
8mod status;
9#[cfg(test)]
10mod tests;
11mod transport;
12
13use self::transport::local_query;
14use std::path::Path;
15
16use candid::{CandidType, Decode, Deserialize, Encode};
17use canic_core::{
18 dto::{
19 error::Error as CanicError,
20 role::CycleBalanceStatusResponse,
21 runtime::{CanicReadinessStatus, ReadinessStatus},
22 },
23 ids::BuildNetwork,
24 protocol::CANIC_STATUS,
25};
26use thiserror::Error as ThisError;
27
28use crate::icp_config::{
29 IcpConfigError, resolve_current_canic_icp_root, resolve_icp_build_network_from_root,
30};
31
32pub use self::status::local_replica_status_reachable_from_root;
33fn decode_cycle_balance_response(bytes: &[u8]) -> Result<u128, ReplicaQueryError> {
34 let result = Decode!(bytes, Result<RoleStatusResponse, CanicError>)
35 .map_err(ReplicaQueryError::Candid)?;
36 match result.map_err(ReplicaQueryError::Canister)? {
37 RoleStatusResponse::CycleBalance(response) => Ok(response.cycles),
38 RoleStatusResponse::Readiness(_) => Err(unexpected_role_status_response()),
39 }
40}
41
42#[derive(CandidType)]
43enum RoleStatusRequest {
44 CycleBalance,
45 Readiness,
46}
47
48#[derive(CandidType, Deserialize)]
49enum RoleStatusResponse {
50 CycleBalance(CycleBalanceStatusResponse),
51 Readiness(CanicReadinessStatus),
52}
53
54#[derive(Debug, ThisError)]
59pub enum ReplicaQueryError {
60 #[error(transparent)]
61 Candid(candid::Error),
62
63 #[error("{0}")]
64 Canister(CanicError),
65
66 #[error("{0}")]
67 Cbor(String),
68
69 #[error(transparent)]
70 Io(#[from] std::io::Error),
71
72 #[error("{0}")]
73 Query(String),
74
75 #[error("local replica rejected query: code={code} message={message}")]
76 Rejected { code: u64, message: String },
77}
78
79impl From<cbor::CborError> for ReplicaQueryError {
80 fn from(err: cbor::CborError) -> Self {
82 Self::Cbor(err.to_string())
83 }
84}
85
86pub fn uses_local_replica_transport(
88 environment: Option<&str>,
89 icp_root: Option<&Path>,
90) -> Result<bool, IcpConfigError> {
91 let Some(environment) = environment else {
92 return Ok(true);
93 };
94 if environment.starts_with("http://") {
95 return Ok(true);
96 }
97
98 let discovered_root;
99 let root = if let Some(root) = icp_root {
100 root
101 } else {
102 discovered_root = resolve_current_canic_icp_root()?;
103 &discovered_root
104 };
105 Ok(resolve_icp_build_network_from_root(root, environment)? == BuildNetwork::Local)
106}
107
108pub(crate) fn query_ready(
110 environment: Option<&str>,
111 canister: &str,
112 icp_root: Option<&Path>,
113) -> Result<bool, ReplicaQueryError> {
114 let arg = Encode!(&RoleStatusRequest::Readiness).map_err(ReplicaQueryError::Candid)?;
115 let bytes = local_query(environment, canister, CANIC_STATUS, &arg, icp_root)?;
116 let result = Decode!(&bytes, Result<RoleStatusResponse, CanicError>)
117 .map_err(ReplicaQueryError::Candid)?;
118 match result.map_err(ReplicaQueryError::Canister)? {
119 RoleStatusResponse::Readiness(response) => Ok(response.status == ReadinessStatus::Ready),
120 RoleStatusResponse::CycleBalance(_) => Err(unexpected_role_status_response()),
121 }
122}
123
124pub(crate) fn query_cycle_balance(
126 environment: Option<&str>,
127 canister: &str,
128 icp_root: Option<&Path>,
129) -> Result<u128, ReplicaQueryError> {
130 let arg = Encode!(&RoleStatusRequest::CycleBalance).map_err(ReplicaQueryError::Candid)?;
131 let bytes = local_query(environment, canister, CANIC_STATUS, &arg, icp_root)?;
132 decode_cycle_balance_response(&bytes)
133}
134
135fn unexpected_role_status_response() -> ReplicaQueryError {
136 ReplicaQueryError::Query("role returned a differently correlated status response".to_string())
137}