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;
33pub(crate) use self::{
34 status::local_replica_root_key_from_root, transport::local_replica_endpoint_from_root,
35};
36
37fn nonempty_text(text: &str) -> Option<String> {
38 let trimmed = text.trim();
39 (!trimmed.is_empty()).then(|| trimmed.to_string())
40}
41
42fn decode_cycle_balance_response(bytes: &[u8]) -> Result<u128, ReplicaQueryError> {
43 let result = Decode!(bytes, Result<RoleStatusResponse, CanicError>)
44 .map_err(ReplicaQueryError::Candid)?;
45 match result.map_err(ReplicaQueryError::Canister)? {
46 RoleStatusResponse::CycleBalance(response) => Ok(response.cycles),
47 RoleStatusResponse::Readiness(_) => Err(unexpected_role_status_response()),
48 }
49}
50
51#[derive(CandidType)]
52enum RoleStatusRequest {
53 CycleBalance,
54 Readiness,
55}
56
57#[derive(CandidType, Deserialize)]
58enum RoleStatusResponse {
59 CycleBalance(CycleBalanceStatusResponse),
60 Readiness(CanicReadinessStatus),
61}
62
63#[derive(Debug, ThisError)]
68pub enum ReplicaQueryError {
69 #[error(transparent)]
70 Candid(candid::Error),
71
72 #[error("{0}")]
73 Canister(CanicError),
74
75 #[error("{0}")]
76 Cbor(String),
77
78 #[error(transparent)]
79 Io(#[from] std::io::Error),
80
81 #[error("{0}")]
82 Query(String),
83
84 #[error("local replica rejected query: code={code} message={message}")]
85 Rejected { code: u64, message: String },
86}
87
88impl From<cbor::CborError> for ReplicaQueryError {
89 fn from(err: cbor::CborError) -> Self {
91 Self::Cbor(err.to_string())
92 }
93}
94
95pub fn uses_local_replica_transport(
97 environment: Option<&str>,
98 icp_root: Option<&Path>,
99) -> Result<bool, IcpConfigError> {
100 let Some(environment) = environment else {
101 return Ok(true);
102 };
103 if environment.starts_with("http://") {
104 return Ok(true);
105 }
106
107 let discovered_root;
108 let root = if let Some(root) = icp_root {
109 root
110 } else {
111 discovered_root = resolve_current_canic_icp_root()?;
112 &discovered_root
113 };
114 Ok(resolve_icp_build_network_from_root(root, environment)? == BuildNetwork::Local)
115}
116
117pub(crate) fn query_ready(
119 environment: Option<&str>,
120 canister: &str,
121 icp_root: Option<&Path>,
122) -> Result<bool, ReplicaQueryError> {
123 let arg = Encode!(&RoleStatusRequest::Readiness).map_err(ReplicaQueryError::Candid)?;
124 let bytes = local_query(environment, canister, CANIC_STATUS, &arg, icp_root)?;
125 let result = Decode!(&bytes, Result<RoleStatusResponse, CanicError>)
126 .map_err(ReplicaQueryError::Candid)?;
127 match result.map_err(ReplicaQueryError::Canister)? {
128 RoleStatusResponse::Readiness(response) => Ok(response.status == ReadinessStatus::Ready),
129 RoleStatusResponse::CycleBalance(_) => Err(unexpected_role_status_response()),
130 }
131}
132
133pub(crate) fn query_cycle_balance(
135 environment: Option<&str>,
136 canister: &str,
137 icp_root: Option<&Path>,
138) -> Result<u128, ReplicaQueryError> {
139 let arg = Encode!(&RoleStatusRequest::CycleBalance).map_err(ReplicaQueryError::Candid)?;
140 let bytes = local_query(environment, canister, CANIC_STATUS, &arg, icp_root)?;
141 decode_cycle_balance_response(&bytes)
142}
143
144fn unexpected_role_status_response() -> ReplicaQueryError {
145 ReplicaQueryError::Query("role returned a differently correlated status response".to_string())
146}