1use std::{sync::Arc, time::Duration};
10
11use candid::{CandidType, Principal};
12use ic_agent::{
13 Agent, AgentError, Identity,
14 identity::{BasicIdentity, Prime256v1Identity, Secp256k1Identity},
15};
16use serde::{Deserialize, de::DeserializeOwned};
17use thiserror::Error as ThisError;
18
19use super::{
20 model::IcpCli,
21 run::{run_json, run_output, run_secret_output},
22};
23
24const MANAGEMENT_CANISTER_STATUS: &str = "canister_status";
25const MANAGEMENT_INGRESS_EXPIRY: Duration = Duration::from_mins(4);
26
27#[derive(Debug, Deserialize)]
28struct IcpNetworkStatus {
29 api_url: String,
30 root_key: String,
31}
32
33#[derive(Debug, ThisError)]
36pub enum IcpManagementCallError {
37 #[error("failed to encode the management-canister argument: {0}")]
38 CandidEncode(#[source] candid::Error),
39
40 #[error("failed to decode the management-canister response: {0}")]
41 CandidResponse(#[source] candid::Error),
42
43 #[error("failed to build the management-canister agent: {0}")]
44 AgentBuild(#[source] AgentError),
45
46 #[error("the effective-ID-correct management-canister call failed: {0}")]
47 AgentCall(#[source] AgentError),
48
49 #[error("failed to create the management-canister async runtime: {0}")]
50 AsyncRuntime(#[source] std::io::Error),
51
52 #[error(transparent)]
53 Icp(#[from] super::IcpCommandError),
54
55 #[error("ICP CLI reported an invalid network root key: {0}")]
56 NetworkRootKey(#[source] canic_core::cdk::utils::hash::DecodeHexError),
57
58 #[error("the selected ICP identity cannot be represented by a supported exported PEM")]
59 UnsupportedExportedIdentity,
60
61 #[error("failed to resolve the Principal of the exported ICP identity: {message}")]
62 ExportedIdentityPrincipal { message: String },
63
64 #[error(
65 "the exported ICP identity Principal {exported} conflicts with the active ICP identity {active}"
66 )]
67 ExportedIdentityConflict { active: String, exported: String },
68
69 #[error("an ICP environment is required for the management-canister call")]
70 MissingEnvironment,
71}
72
73impl IcpCli {
74 pub(crate) fn management_canister_status_candid<I, O>(
77 &self,
78 effective_canister_id: Principal,
79 input: &I,
80 ) -> Result<O, IcpManagementCallError>
81 where
82 I: CandidType,
83 O: CandidType + DeserializeOwned,
84 {
85 let argument = candid::encode_one(input).map_err(IcpManagementCallError::CandidEncode)?;
86 let agent = self.management_agent()?;
87 let response = call_management_update(
88 &LiveAgentUpdateBoundary { agent: &agent },
89 effective_canister_id,
90 MANAGEMENT_CANISTER_STATUS,
91 argument,
92 )?;
93 candid::decode_one(&response).map_err(IcpManagementCallError::CandidResponse)
94 }
95
96 fn management_agent(&self) -> Result<Agent, IcpManagementCallError> {
97 let environment = self
98 .environment
99 .as_deref()
100 .ok_or(IcpManagementCallError::MissingEnvironment)?;
101 let network = self.network_status(environment)?;
102 let identity = self.exported_active_identity()?;
103 let agent = Agent::builder()
104 .with_url(&network.api_url)
105 .with_arc_identity(identity)
106 .with_ingress_expiry(MANAGEMENT_INGRESS_EXPIRY)
107 .build()
108 .map_err(IcpManagementCallError::AgentBuild)?;
109 let root_key = canic_core::cdk::utils::hash::decode_hex(&network.root_key)
110 .map_err(IcpManagementCallError::NetworkRootKey)?;
111 agent.set_root_key(root_key);
112 Ok(agent)
113 }
114
115 fn network_status(
116 &self,
117 environment: &str,
118 ) -> Result<IcpNetworkStatus, IcpManagementCallError> {
119 let mut command = self.command();
120 command.args(["network", "status", "--environment", environment, "--json"]);
121 run_json(&mut command).map_err(Into::into)
122 }
123
124 fn exported_active_identity(&self) -> Result<Arc<dyn Identity>, IcpManagementCallError> {
125 let mut default_command = self.command();
126 default_command.args(["identity", "default"]);
127 let identity_name = run_output(&mut default_command)?;
128
129 let mut export_command = self.command();
130 export_command.args(["identity", "export", &identity_name]);
131 if let Some(password_file) = self.identity_password_file.as_deref() {
132 export_command.arg("--password-file").arg(password_file);
133 }
134 let mut pem = run_secret_output(&mut export_command)?;
135 let identity = parse_exported_identity(&pem);
136 pem.fill(0);
137 let identity = identity?;
138 let exported = identity
139 .sender()
140 .map_err(|message| IcpManagementCallError::ExportedIdentityPrincipal { message })?;
141 let active = self.identity_principal_text()?;
142 if exported.to_text() != active {
143 return Err(IcpManagementCallError::ExportedIdentityConflict {
144 active,
145 exported: exported.to_text(),
146 });
147 }
148 Ok(identity)
149 }
150}
151
152fn parse_exported_identity(pem: &[u8]) -> Result<Arc<dyn Identity>, IcpManagementCallError> {
153 if let Ok(identity) = BasicIdentity::from_pem(pem) {
154 return Ok(Arc::new(identity));
155 }
156 if let Ok(identity) = Secp256k1Identity::from_pem(pem) {
157 return Ok(Arc::new(identity));
158 }
159 if let Ok(identity) = Prime256v1Identity::from_pem(pem) {
160 return Ok(Arc::new(identity));
161 }
162 Err(IcpManagementCallError::UnsupportedExportedIdentity)
163}
164
165#[derive(Debug, Eq, PartialEq)]
166struct ManagementUpdateRequest {
167 argument: Vec<u8>,
168 canister_id: Principal,
169 effective_canister_id: Principal,
170 method: String,
171}
172
173trait AgentUpdateBoundary {
174 fn update(&self, request: ManagementUpdateRequest) -> Result<Vec<u8>, IcpManagementCallError>;
175}
176
177struct LiveAgentUpdateBoundary<'a> {
178 agent: &'a Agent,
179}
180
181impl AgentUpdateBoundary for LiveAgentUpdateBoundary<'_> {
182 fn update(&self, request: ManagementUpdateRequest) -> Result<Vec<u8>, IcpManagementCallError> {
183 let runtime = tokio::runtime::Builder::new_current_thread()
184 .enable_all()
185 .build()
186 .map_err(IcpManagementCallError::AsyncRuntime)?;
187 runtime
188 .block_on(
189 self.agent
190 .update(&request.canister_id, request.method)
191 .with_effective_canister_id(request.effective_canister_id)
192 .with_arg(request.argument)
193 .call_and_wait(),
194 )
195 .map_err(IcpManagementCallError::AgentCall)
196 }
197}
198
199fn call_management_update(
200 boundary: &impl AgentUpdateBoundary,
201 effective_canister_id: Principal,
202 method: &str,
203 argument: Vec<u8>,
204) -> Result<Vec<u8>, IcpManagementCallError> {
205 boundary.update(ManagementUpdateRequest {
206 argument,
207 canister_id: Principal::management_canister(),
208 effective_canister_id,
209 method: method.to_string(),
210 })
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use std::cell::RefCell;
217
218 struct RecordingAgentBoundary {
219 request: RefCell<Option<ManagementUpdateRequest>>,
220 }
221
222 impl AgentUpdateBoundary for RecordingAgentBoundary {
223 fn update(
224 &self,
225 request: ManagementUpdateRequest,
226 ) -> Result<Vec<u8>, IcpManagementCallError> {
227 self.request.replace(Some(request));
228 Ok(vec![1, 2, 3])
229 }
230 }
231
232 #[test]
233 fn management_status_routes_through_the_target_effective_canister_id() {
234 let target =
235 Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").expect("effective canister ID");
236 let boundary = RecordingAgentBoundary {
237 request: RefCell::new(None),
238 };
239 let response =
240 call_management_update(&boundary, target, MANAGEMENT_CANISTER_STATUS, vec![7, 8, 9])
241 .expect("record routed management update");
242
243 assert_eq!(response, vec![1, 2, 3]);
244 assert_eq!(
245 boundary.request.into_inner(),
246 Some(ManagementUpdateRequest {
247 argument: vec![7, 8, 9],
248 canister_id: Principal::management_canister(),
249 effective_canister_id: target,
250 method: MANAGEMENT_CANISTER_STATUS.to_string(),
251 })
252 );
253 }
254}