1use crate::pocket_ic::common::rest::{
3 AutoProgressConfig, BlobCompression, BlobId, CanisterHttpRequest, CreateHttpGatewayResponse,
4 CreateInstanceResponse, ExtendedSubnetConfigSet, HttpGatewayBackend, HttpGatewayConfig,
5 HttpGatewayInfo, HttpsConfig, IcpConfig, IcpFeatures, InitialTime, InstanceConfig,
6 InstanceHttpGatewayConfig, InstanceId, MockCanisterHttpResponse,
7 MockFlexibleCanisterHttpResponse, RawAddCycles, RawCanisterCall, RawCanisterHttpRequest,
8 RawCanisterId, RawCanisterResult, RawCanisterSnapshotDownload, RawCanisterSnapshotId,
9 RawCanisterSnapshotUpload, RawCycles, RawEffectivePrincipal, RawIngressStatusArgs,
10 RawMessageId, RawMockCanisterHttpResponse, RawMockFlexibleCanisterHttpResponse, RawPrincipalId,
11 RawSenderInfo, RawSetStableMemory, RawStableMemory, RawSubnetId, RawTickConfigs, RawTime,
12 RawVerifyCanisterSigArg, SubnetId, Topology,
13};
14#[cfg(windows)]
15use crate::pocket_ic::wsl_path;
16use crate::pocket_ic::{ClientError, Operation, RequestContext, http::Exchange};
17use crate::pocket_ic::{
18 CreateCanisterParams, CreateCanisterPlacement, IngressStatusResult, PocketIcBuilder,
19 PocketIcState, RejectResponse, TickConfigs, Time, copy_dir,
20};
21use backon::{BackoffBuilder, ExponentialBuilder};
22use candid::{
23 Principal, decode_args, encode_args,
24 utils::{ArgumentDecoder, ArgumentEncoder},
25};
26use ic_management_canister_types::{
27 CanisterId, CanisterIdRecord, CanisterInstallMode, CanisterLogRecord, CanisterSettings,
28 CanisterStatusResult, ChunkHash, DeleteCanisterSnapshotArgs, FetchCanisterLogsResult,
29 InstallChunkedCodeArgs, InstallCodeArgs, LoadCanisterSnapshotArgs,
30 ProvisionalCreateCanisterWithCyclesArgs, Snapshot, StoredChunksResult,
31 TakeCanisterSnapshotArgs, UpdateSettingsArgs, UpgradeFlags, UploadChunkArgs, UploadChunkResult,
32 WasmMemoryPersistence,
33};
34use ic_platform::certification::cbor::decode_certificate;
35use ic_platform::certification::{Certificate, Label, LookupResult};
36use ic_platform::ic_protocol::Envelope;
37use ic_platform::ic_protocol::EnvelopeContent::ReadState;
38use ic_platform::ic_protocol::{
39 ReadStateResponse, SubnetMetrics, decode_read_state_response, decode_subnet_metrics,
40 encode_envelope,
41};
42use reqwest::{StatusCode, Url};
43use serde::{Serialize, de::DeserializeOwned};
44use sha2::{Digest, Sha256};
45use slog::Level;
46use std::fs::read_dir;
47use std::future::Future;
48use std::net::{IpAddr, SocketAddr};
49use std::path::PathBuf;
50use std::time::Duration;
51use tracing::{debug, instrument};
52use tracing_appender::non_blocking::WorkerGuard;
53use tracing_subscriber::EnvFilter;
54
55mod snapshot;
58
59const LOG_DIR_PATH_ENV_NAME: &str = "POCKET_IC_LOG_DIR";
60const LOG_DIR_LEVELS_ENV_NAME: &str = "POCKET_IC_LOG_DIR_LEVELS";
61
62const LOCALHOST: &str = "localhost";
63
64const INSTALL_CHUNKED_CODE_THRESHOLD: usize = 2_000_000; const INSTALL_CODE_CHUNK_SIZE: usize = 1 << 20; enum HttpMethod {
82 Get,
83 Post,
84}
85
86pub struct PocketIc {
88 pub instance_id: InstanceId,
90 max_request_time_ms: Option<u64>,
92 http_gateway: Option<HttpGatewayInfo>,
93 server_url: Url,
94 reqwest_client: reqwest::Client,
95 owns_instance: bool,
97 state_dir: Option<PocketIcState>,
98 _log_guard: Option<WorkerGuard>,
99}
100
101impl PocketIc {
102 pub async fn new(server_url: Url) -> Result<Self, ClientError> {
105 PocketIcBuilder::new()
106 .with_server_url(server_url)
107 .with_application_subnet()
108 .build()
109 .await
110 }
111
112 pub fn new_from_existing_instance(
117 server_url: Url,
118 instance_id: InstanceId,
119 max_request_time_ms: Option<u64>,
120 ) -> Self {
121 let test_driver_pid = std::process::id();
122 let log_guard = setup_tracing(test_driver_pid);
123
124 let reqwest_client = reqwest::Client::new();
125 debug!("instance_id={} Reusing existing instance.", instance_id);
126
127 Self {
128 instance_id,
129 max_request_time_ms,
130 http_gateway: None,
131 server_url,
132 reqwest_client,
133 owns_instance: false,
134 state_dir: None,
135 _log_guard: log_guard,
136 }
137 }
138
139 #[expect(
140 clippy::too_many_arguments,
141 reason = "retained upstream client method signature"
142 )]
143 pub(crate) async fn from_components(
144 subnet_config_set: impl Into<ExtendedSubnetConfigSet>,
145 server_url: Url,
146 max_request_time_ms: Option<u64>,
147 read_only_state_dir: Option<PathBuf>,
148 mut state_dir: Option<PocketIcState>,
149 icp_config: IcpConfig,
150 log_level: Option<Level>,
151 bitcoind_addr: Option<Vec<SocketAddr>>,
152 dogecoind_addr: Option<Vec<SocketAddr>>,
153 icp_features: IcpFeatures,
154 initial_time: Option<InitialTime>,
155 http_gateway_config: Option<InstanceHttpGatewayConfig>,
156 mainnet_nns_subnet_id: Option<bool>,
157 disable_ingress_validation: Option<bool>,
158 ) -> Result<Self, ClientError> {
159 let subnet_config_set: ExtendedSubnetConfigSet = subnet_config_set.into();
160
161 if let Some(read_only_state_dir) = read_only_state_dir {
164 if let Some(ref state_dir) = state_dir {
165 let mut state_dir_contents = read_dir(state_dir.state_dir())?;
166 if state_dir_contents.next().is_some() {
167 return Err(ClientError::Protocol(
168 "PocketIC instance state must be empty if a read-only state is mounted."
169 .into(),
170 ));
171 }
172 } else {
173 state_dir = Some(PocketIcState::new());
174 };
175 copy_dir(
176 read_only_state_dir,
177 state_dir
178 .as_ref()
179 .map(|state_dir| state_dir.state_dir())
180 .unwrap(),
181 )?;
182 };
183
184 let instance_config = InstanceConfig {
185 subnet_config_set,
186 http_gateway_config,
187 #[cfg(not(windows))]
188 state_dir: state_dir.as_ref().map(|state_dir| state_dir.state_dir()),
189 #[cfg(windows)]
190 state_dir: state_dir
191 .as_ref()
192 .map(|state_dir| wsl_path(&state_dir.state_dir(), "state directory").into()),
193 icp_config: Some(icp_config),
194 log_level: log_level.map(|l| l.to_string()),
195 bitcoind_addr,
196 dogecoind_addr,
197 icp_features: Some(icp_features),
198 incomplete_state: None,
199 initial_time,
200 mainnet_nns_subnet_id,
201 disable_ingress_validation,
202 };
203
204 let test_driver_pid = std::process::id();
205 let log_guard = setup_tracing(test_driver_pid);
206
207 let reqwest_client = reqwest::Client::new();
208 let url = server_url.join("instances").unwrap();
209 let mut exchange = Exchange::new(&url, max_request_time_ms);
210 let response: CreateInstanceResponse = exchange
211 .json(
212 &reqwest_client,
213 reqwest_client.post(url.clone()).json(&instance_config),
214 )
215 .await?;
216 let (instance_id, http_gateway_info) = match response {
217 CreateInstanceResponse::Created {
218 instance_id,
219 http_gateway_info,
220 ..
221 } => (instance_id, http_gateway_info),
222 CreateInstanceResponse::Error { message } => {
223 return Err(ClientError::Server {
224 context: Box::new(exchange.context.clone()),
225 status: exchange.context.status.unwrap_or(StatusCode::OK),
226 message,
227 });
228 }
229 };
230 debug!("instance_id={} New instance created.", instance_id);
231
232 Ok(Self {
233 instance_id,
234 max_request_time_ms,
235 http_gateway: http_gateway_info,
236 server_url,
237 reqwest_client,
238 owns_instance: true,
239 state_dir,
240 _log_guard: log_guard,
241 })
242 }
243
244 pub async fn drop_and_take_state(&mut self) -> Result<Option<PocketIcState>, ClientError> {
245 self.do_drop().await?;
246 Ok(self.state_dir.take())
247 }
248
249 pub fn get_server_url(&self) -> Url {
251 self.server_url.clone()
252 }
253
254 pub async fn topology(&self) -> Result<Topology, ClientError> {
256 let endpoint = "read/topology";
257 self.get(endpoint).await
258 }
259
260 #[instrument(ret, skip(self, blob), fields(instance_id=self.instance_id, blob_len = %blob.len(), compression = ?compression))]
262 pub async fn upload_blob(
263 &self,
264 blob: Vec<u8>,
265 compression: BlobCompression,
266 ) -> Result<BlobId, ClientError> {
267 let url = self.server_url.join("blobstore/").unwrap();
268 let mut request = self.reqwest_client.post(url.clone()).body(blob);
269 if let BlobCompression::Gzip = compression {
270 request = request.header(reqwest::header::CONTENT_ENCODING, "gzip");
271 }
272 let mut exchange = Exchange::new(&url, self.max_request_time_ms);
273 let (status, bytes) = exchange.bytes(&self.reqwest_client, request).await?;
274 if !status.is_success() {
275 return Err(exchange.server_error(status, &bytes));
276 }
277 let hash = hex::decode(bytes).map_err(|error| ClientError::Protocol(error.to_string()))?;
278 Ok(BlobId(hash))
279 }
280
281 #[instrument(skip(self, data), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), data_len = %data.len(), compression = ?compression))]
284 pub async fn set_stable_memory(
285 &self,
286 canister_id: CanisterId,
287 data: Vec<u8>,
288 compression: BlobCompression,
289 ) -> Result<(), ClientError> {
290 let blob_id = self.upload_blob(data, compression).await?;
291 let endpoint = "update/set_stable_memory";
292 self.post::<(), _>(
293 endpoint,
294 RawSetStableMemory {
295 canister_id: canister_id.as_slice().to_vec(),
296 blob_id,
297 },
298 )
299 .await?;
300
301 Ok(())
302 }
303
304 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))]
306 pub async fn get_stable_memory(&self, canister_id: CanisterId) -> Result<Vec<u8>, ClientError> {
307 let endpoint = "read/get_stable_memory";
308 let RawStableMemory { blob } = self
309 .post(
310 endpoint,
311 RawCanisterId {
312 canister_id: canister_id.as_slice().to_vec(),
313 },
314 )
315 .await?;
316 Ok(blob)
317 }
318
319 #[instrument(ret)]
321 pub async fn list_instances(server_url: Url) -> Result<Vec<String>, ClientError> {
322 let url = server_url.join("instances").unwrap();
323 let client = reqwest::Client::new();
324 Exchange::new(&url, None)
325 .json(&client, client.get(url.clone()))
326 .await
327 }
328
329 #[instrument(skip_all, fields(instance_id=self.instance_id))]
331 pub async fn verify_canister_signature(
332 &self,
333 msg: Vec<u8>,
334 sig: Vec<u8>,
335 pubkey: Vec<u8>,
336 root_pubkey: Vec<u8>,
337 ) -> Result<(), ClientError> {
338 let url = self.server_url.join("verify_signature").unwrap();
339 let result: Result<(), String> = Exchange::new(&url, self.max_request_time_ms)
340 .json(
341 &self.reqwest_client,
342 self.reqwest_client
343 .post(url.clone())
344 .json(&RawVerifyCanisterSigArg {
345 msg,
346 sig,
347 pubkey,
348 root_pubkey,
349 }),
350 )
351 .await?;
352 result.map_err(ClientError::Protocol)
353 }
354
355 #[instrument(skip(self), fields(instance_id=self.instance_id))]
360 pub async fn tick(&self) -> Result<(), ClientError> {
361 self.tick_with_configs(TickConfigs::default()).await?;
362
363 Ok(())
364 }
365
366 #[instrument(skip(self), fields(instance_id=self.instance_id))]
369 pub async fn tick_with_configs(&self, configs: TickConfigs) -> Result<(), ClientError> {
370 let endpoint = "update/tick";
371 self.post::<(), RawTickConfigs>(endpoint, configs.into())
372 .await?;
373
374 Ok(())
375 }
376
377 #[instrument(skip(self), fields(instance_id=self.instance_id))]
385 pub async fn auto_progress(&self) -> Result<Url, ClientError> {
386 let endpoint = "auto_progress";
387 let auto_progress_config = AutoProgressConfig {
388 artificial_delay_ms: None,
389 };
390 self.post::<(), _>(endpoint, auto_progress_config).await?;
391 Ok(self.instance_url())
392 }
393
394 #[instrument(skip(self), fields(instance_id=self.instance_id))]
396 pub async fn auto_progress_enabled(&self) -> Result<bool, ClientError> {
397 self.get("auto_progress").await
398 }
399
400 pub(crate) fn instance_url(&self) -> Url {
401 self.server_url
402 .join("/instances/")
403 .unwrap()
404 .join(&format!("{}/", self.instance_id))
405 .unwrap()
406 }
407
408 #[instrument(skip(self), fields(instance_id=self.instance_id))]
410 pub async fn stop_progress(&self) -> Result<(), ClientError> {
411 let endpoint = "stop_progress";
412 self.post::<(), _>(endpoint, "").await?;
413
414 Ok(())
415 }
416
417 pub fn url(&self) -> Option<Url> {
421 self.http_gateway
422 .as_ref()
423 .map(|res| Url::parse(&format!("http://{}:{}/", LOCALHOST, res.port)).unwrap())
424 }
425
426 #[instrument(skip(self), fields(instance_id=self.instance_id))]
437 pub async fn make_live(&mut self, listen_at: Option<u16>) -> Result<Url, ClientError> {
438 self.make_live_with_params(None, listen_at, None, None)
439 .await
440 }
441
442 #[instrument(skip(self), fields(instance_id=self.instance_id))]
455 pub async fn make_live_with_params(
456 &mut self,
457 ip_addr: Option<IpAddr>,
458 listen_at: Option<u16>,
459 domains: Option<Vec<String>>,
460 https_config: Option<HttpsConfig>,
461 ) -> Result<Url, ClientError> {
462 if let Some(url) = self.url() {
463 return Ok(url);
464 }
465 if !self.auto_progress_enabled().await? {
466 self.auto_progress().await?;
467 }
468 self.start_http_gateway(
469 ip_addr.map(|ip_addr| ip_addr.to_string()),
470 listen_at,
471 domains,
472 https_config,
473 )
474 .await
475 }
476
477 async fn start_http_gateway(
478 &mut self,
479 ip_addr: Option<String>,
480 port: Option<u16>,
481 domains: Option<Vec<String>>,
482 https_config: Option<HttpsConfig>,
483 ) -> Result<Url, ClientError> {
484 let endpoint = self.server_url.join("http_gateway").unwrap();
485 let http_gateway_config = HttpGatewayConfig {
486 ip_addr,
487 port,
488 forward_to: HttpGatewayBackend::PocketIcInstance(self.instance_id),
489 domains: domains.clone(),
490 https_config: https_config.clone(),
491 domain_custom_provider_local_file: None,
492 };
493 let mut exchange = Exchange::new(&endpoint, self.max_request_time_ms);
494 let res: CreateHttpGatewayResponse = exchange
495 .json(
496 &self.reqwest_client,
497 self.reqwest_client
498 .post(endpoint.clone())
499 .json(&http_gateway_config),
500 )
501 .await?;
502 Ok(match res {
503 CreateHttpGatewayResponse::Created(info) => {
504 let port = info.port;
505 self.http_gateway = Some(info);
506 let proto = if https_config.is_some() {
507 "https"
508 } else {
509 "http"
510 };
511 Url::parse(&format!(
512 "{}://{}:{}/",
513 proto,
514 domains
515 .unwrap_or_default()
516 .into_iter()
517 .next()
518 .unwrap_or(LOCALHOST.to_string()),
519 port
520 ))
521 .unwrap()
522 }
523 CreateHttpGatewayResponse::Error { message } => {
524 return Err(ClientError::Server {
525 context: Box::new(exchange.context.clone()),
526 status: exchange.context.status.unwrap_or(StatusCode::OK),
527 message,
528 });
529 }
530 })
531 }
532
533 async fn stop_http_gateway(&mut self) -> Result<(), ClientError> {
534 if let Some(info) = &self.http_gateway {
535 let url = self
536 .server_url
537 .join(&format!("http_gateway/{}/stop", info.instance_id))
538 .unwrap();
539 Exchange::new(&url, self.max_request_time_ms)
540 .json::<()>(&self.reqwest_client, self.reqwest_client.post(url.clone()))
541 .await?;
542 self.http_gateway = None;
543 }
544 Ok(())
545 }
546
547 #[instrument(skip(self), fields(instance_id=self.instance_id))]
550 pub async fn stop_live(&mut self) -> Result<(), ClientError> {
551 self.stop_http_gateway().await?;
552 self.stop_progress().await?;
553
554 Ok(())
555 }
556
557 #[instrument(skip(self), fields(instance_id=self.instance_id))]
559 pub async fn root_key(&self) -> Result<Option<Vec<u8>>, ClientError> {
560 let Some(subnet_id) = self.topology().await?.get_nns() else {
561 return Ok(None);
562 };
563 let subnet_id: RawSubnetId = subnet_id.into();
564 let endpoint = "read/pub_key";
565 let res = self.post::<Vec<u8>, _>(endpoint, subnet_id).await?;
566 Ok(Some(res))
567 }
568
569 #[instrument(ret, skip(self), fields(instance_id=self.instance_id))]
571 pub async fn get_time(&self) -> Result<Time, ClientError> {
572 let endpoint = "read/get_time";
573 let result: RawTime = self.get(endpoint).await?;
574 Ok(Time::from_nanos_since_unix_epoch(result.nanos_since_epoch))
575 }
576
577 #[instrument(skip(self), fields(instance_id=self.instance_id, time = ?time))]
579 pub async fn set_time(&self, time: Time) -> Result<(), ClientError> {
580 let endpoint = "update/set_time";
581 self.post::<(), _>(
582 endpoint,
583 RawTime {
584 nanos_since_epoch: time.as_nanos_since_unix_epoch(),
585 },
586 )
587 .await?;
588
589 Ok(())
590 }
591
592 #[instrument(skip(self), fields(instance_id=self.instance_id, time = ?time))]
594 pub async fn set_certified_time(&self, time: Time) -> Result<(), ClientError> {
595 let endpoint = "update/set_certified_time";
596 self.post::<(), _>(
597 endpoint,
598 RawTime {
599 nanos_since_epoch: time.as_nanos_since_unix_epoch(),
600 },
601 )
602 .await?;
603
604 Ok(())
605 }
606
607 #[instrument(skip(self), fields(instance_id=self.instance_id, duration = ?duration))]
609 pub async fn advance_time(&self, duration: Duration) -> Result<(), ClientError> {
610 let now = self.get_time().await?;
611 self.set_time(now + duration).await?;
612
613 Ok(())
614 }
615
616 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))]
619 pub async fn get_controllers(
620 &self,
621 canister_id: CanisterId,
622 ) -> Result<Vec<Principal>, ClientError> {
623 let endpoint = "read/get_controllers";
624 let result: Vec<RawPrincipalId> = self
625 .post(
626 endpoint,
627 RawCanisterId {
628 canister_id: canister_id.as_slice().to_vec(),
629 },
630 )
631 .await?;
632 Ok(result.into_iter().map(|p| p.into()).collect())
633 }
634
635 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))]
637 pub async fn cycle_balance(&self, canister_id: CanisterId) -> Result<u128, ClientError> {
638 let endpoint = "read/get_cycles";
639 let result: RawCycles = self
640 .post(
641 endpoint,
642 RawCanisterId {
643 canister_id: canister_id.as_slice().to_vec(),
644 },
645 )
646 .await?;
647 Ok(result.cycles)
648 }
649
650 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), amount = %amount))]
652 pub async fn add_cycles(
653 &self,
654 canister_id: CanisterId,
655 amount: u128,
656 ) -> Result<u128, ClientError> {
657 let endpoint = "update/add_cycles";
658 let result: RawCycles = self
659 .post(
660 endpoint,
661 RawAddCycles {
662 canister_id: canister_id.as_slice().to_vec(),
663 amount,
664 },
665 )
666 .await?;
667 Ok(result.cycles)
668 }
669
670 pub async fn submit_call(
672 &self,
673 canister_id: CanisterId,
674 sender: Principal,
675 method: &str,
676 payload: Vec<u8>,
677 ) -> Result<RawMessageId, ClientError> {
678 self.submit_call_with_effective_principal(
679 canister_id,
680 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
681 sender,
682 method,
683 payload,
684 )
685 .await
686 }
687
688 async fn submit_call_with_effective_principal_and_sender_info_inner(
689 &self,
690 canister_id: CanisterId,
691 effective_principal: RawEffectivePrincipal,
692 sender: Principal,
693 method: &str,
694 payload: Vec<u8>,
695 sender_info: Option<RawSenderInfo>,
696 ) -> Result<RawMessageId, ClientError> {
697 let endpoint = "update/submit_ingress_message";
698 let raw_canister_call = RawCanisterCall {
699 sender: sender.as_slice().to_vec(),
700 canister_id: canister_id.as_slice().to_vec(),
701 method: method.to_string(),
702 payload,
703 effective_principal,
704 sender_info,
705 };
706 self.post::<Result<RawMessageId, RejectResponse>, _>(endpoint, raw_canister_call)
707 .await?
708 .map_err(ClientError::from)
709 }
710
711 pub async fn submit_call_with_effective_principal(
713 &self,
714 canister_id: CanisterId,
715 effective_principal: RawEffectivePrincipal,
716 sender: Principal,
717 method: &str,
718 payload: Vec<u8>,
719 ) -> Result<RawMessageId, ClientError> {
720 self.submit_call_with_effective_principal_and_sender_info_inner(
721 canister_id,
722 effective_principal,
723 sender,
724 method,
725 payload,
726 None,
727 )
728 .await
729 }
730
731 pub async fn submit_call_with_effective_principal_and_sender_info(
733 &self,
734 canister_id: CanisterId,
735 effective_principal: RawEffectivePrincipal,
736 sender: Principal,
737 method: &str,
738 payload: Vec<u8>,
739 sender_info: RawSenderInfo,
740 ) -> Result<RawMessageId, ClientError> {
741 self.submit_call_with_effective_principal_and_sender_info_inner(
742 canister_id,
743 effective_principal,
744 sender,
745 method,
746 payload,
747 Some(sender_info),
748 )
749 .await
750 }
751
752 pub async fn submit_call_with_sender_info(
754 &self,
755 canister_id: CanisterId,
756 sender: Principal,
757 method: &str,
758 payload: Vec<u8>,
759 sender_info: RawSenderInfo,
760 ) -> Result<RawMessageId, ClientError> {
761 self.submit_call_with_effective_principal_and_sender_info(
762 canister_id,
763 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
764 sender,
765 method,
766 payload,
767 sender_info,
768 )
769 .await
770 }
771
772 pub async fn await_call(&self, message_id: RawMessageId) -> Result<Vec<u8>, ClientError> {
774 let endpoint = "update/await_ingress_message";
775 let result: RawCanisterResult = self
776 .post(endpoint, &message_id)
777 .await
778 .map_err(|error| error.with_ingress(message_id))?;
779 Result::<Vec<u8>, RejectResponse>::from(result).map_err(ClientError::from)
780 }
781
782 pub async fn ingress_status(
786 &self,
787 raw_message_id: RawMessageId,
788 ) -> Result<Option<Result<Vec<u8>, RejectResponse>>, ClientError> {
789 let status = self.ingress_status_as_caller(raw_message_id, None).await?;
790 Ok(match status {
791 IngressStatusResult::NotAvailable => None,
792 IngressStatusResult::Success(status) => Some(status),
793 IngressStatusResult::Forbidden(err) => return Err(ClientError::Protocol(err)),
794 })
795 }
796
797 pub async fn ingress_status_as(
802 &self,
803 raw_message_id: RawMessageId,
804 caller: Principal,
805 ) -> Result<IngressStatusResult, ClientError> {
806 self.ingress_status_as_caller(raw_message_id, Some(caller))
807 .await
808 }
809
810 async fn ingress_status_as_caller(
811 &self,
812 raw_message_id: RawMessageId,
813 caller: Option<Principal>,
814 ) -> Result<IngressStatusResult, ClientError> {
815 let args = RawIngressStatusArgs {
816 raw_message_id: raw_message_id.clone(),
817 raw_caller: caller.map(Into::into),
818 };
819 match self
820 .post::<Option<RawCanisterResult>, _>("read/ingress_status", args)
821 .await
822 {
823 Ok(None) => Ok(IngressStatusResult::NotAvailable),
824 Ok(Some(result)) => Ok(IngressStatusResult::Success(result.into())),
825 Err(ClientError::Server {
826 status: StatusCode::FORBIDDEN,
827 message,
828 ..
829 }) => Ok(IngressStatusResult::Forbidden(message)),
830 Err(error) => Err(error.with_ingress(raw_message_id)),
831 }
832 }
833
834 pub async fn await_call_no_ticks(
838 &self,
839 message_id: RawMessageId,
840 ) -> Result<Vec<u8>, ClientError> {
841 let mut retry_policy = ExponentialBuilder::new()
842 .with_min_delay(Duration::from_millis(10))
843 .with_max_delay(Duration::from_secs(1))
844 .with_factor(2.0)
845 .with_jitter()
846 .with_total_delay(Some(Duration::from_secs(15 * 60)))
847 .without_max_times()
848 .build();
849 Ok(loop {
850 if let Some(ingress_status) = self.ingress_status(message_id.clone()).await? {
851 break ingress_status.map_err(ClientError::from)?;
852 }
853 let delay = retry_policy.next().ok_or_else(|| ClientError::Timeout {
854 context: Box::new(RequestContext {
855 url: self.instance_url().to_string(),
856 operation: None,
857 ingress: Some(message_id.clone()),
858 may_have_executed: true,
859 status: None,
860 }),
861 })?;
862 tokio::time::sleep(delay).await;
863 })
864 }
865
866 #[instrument(skip(self, payload), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.to_string(), method = %method, payload_len = %payload.len()))]
868 pub async fn update_call(
869 &self,
870 canister_id: CanisterId,
871 sender: Principal,
872 method: &str,
873 payload: Vec<u8>,
874 ) -> Result<Vec<u8>, ClientError> {
875 self.update_call_with_effective_principal(
876 canister_id,
877 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
878 sender,
879 method,
880 payload,
881 )
882 .await
883 }
884
885 async fn update_call_with_effective_principal_and_sender_info_inner(
886 &self,
887 canister_id: CanisterId,
888 effective_principal: RawEffectivePrincipal,
889 sender: Principal,
890 method: &str,
891 payload: Vec<u8>,
892 sender_info: Option<RawSenderInfo>,
893 ) -> Result<Vec<u8>, ClientError> {
894 let message_id = self
895 .submit_call_with_effective_principal_and_sender_info_inner(
896 canister_id,
897 effective_principal,
898 sender,
899 method,
900 payload,
901 sender_info,
902 )
903 .await?;
904 self.await_call(message_id).await
905 }
906
907 pub async fn update_call_with_effective_principal_and_sender_info(
909 &self,
910 canister_id: CanisterId,
911 effective_principal: RawEffectivePrincipal,
912 sender: Principal,
913 method: &str,
914 payload: Vec<u8>,
915 sender_info: RawSenderInfo,
916 ) -> Result<Vec<u8>, ClientError> {
917 self.update_call_with_effective_principal_and_sender_info_inner(
918 canister_id,
919 effective_principal,
920 sender,
921 method,
922 payload,
923 Some(sender_info),
924 )
925 .await
926 }
927
928 pub async fn update_call_with_sender_info(
930 &self,
931 canister_id: CanisterId,
932 sender: Principal,
933 method: &str,
934 payload: Vec<u8>,
935 sender_info: RawSenderInfo,
936 ) -> Result<Vec<u8>, ClientError> {
937 self.update_call_with_effective_principal_and_sender_info(
938 canister_id,
939 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
940 sender,
941 method,
942 payload,
943 sender_info,
944 )
945 .await
946 }
947
948 #[instrument(skip(self, payload), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.to_string(), method = %method, payload_len = %payload.len()))]
950 pub async fn query_call(
951 &self,
952 canister_id: CanisterId,
953 sender: Principal,
954 method: &str,
955 payload: Vec<u8>,
956 ) -> Result<Vec<u8>, ClientError> {
957 self.query_call_with_effective_principal(
958 canister_id,
959 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
960 sender,
961 method,
962 payload,
963 )
964 .await
965 }
966
967 #[instrument(skip(self, payload), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), effective_principal = %effective_principal.to_string(), sender = %sender.to_string(), method = %method, payload_len = %payload.len()))]
971 pub async fn query_call_with_effective_principal(
972 &self,
973 canister_id: CanisterId,
974 effective_principal: RawEffectivePrincipal,
975 sender: Principal,
976 method: &str,
977 payload: Vec<u8>,
978 ) -> Result<Vec<u8>, ClientError> {
979 let endpoint = "read/query";
980 self.canister_call(
981 endpoint,
982 effective_principal,
983 canister_id,
984 sender,
985 method,
986 payload,
987 None,
988 )
989 .await
990 }
991
992 pub async fn query_call_with_effective_principal_and_sender_info(
994 &self,
995 canister_id: CanisterId,
996 effective_principal: RawEffectivePrincipal,
997 sender: Principal,
998 method: &str,
999 payload: Vec<u8>,
1000 sender_info: RawSenderInfo,
1001 ) -> Result<Vec<u8>, ClientError> {
1002 let endpoint = "read/query";
1003 self.canister_call(
1004 endpoint,
1005 effective_principal,
1006 canister_id,
1007 sender,
1008 method,
1009 payload,
1010 Some(sender_info),
1011 )
1012 .await
1013 }
1014
1015 pub async fn query_call_with_sender_info(
1017 &self,
1018 canister_id: CanisterId,
1019 sender: Principal,
1020 method: &str,
1021 payload: Vec<u8>,
1022 sender_info: RawSenderInfo,
1023 ) -> Result<Vec<u8>, ClientError> {
1024 self.query_call_with_effective_principal_and_sender_info(
1025 canister_id,
1026 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1027 sender,
1028 method,
1029 payload,
1030 sender_info,
1031 )
1032 .await
1033 }
1034
1035 pub async fn fetch_canister_logs(
1037 &self,
1038 canister_id: CanisterId,
1039 sender: Principal,
1040 ) -> Result<Vec<CanisterLogRecord>, ClientError> {
1041 Ok(with_candid::<_, (FetchCanisterLogsResult,), _>(
1042 (CanisterIdRecord { canister_id },),
1043 |payload| async {
1044 self.query_call_with_effective_principal(
1045 Principal::management_canister(),
1046 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1047 sender,
1048 "fetch_canister_logs",
1049 payload,
1050 )
1051 .await
1052 },
1053 )
1054 .await?
1055 .0
1056 .canister_log_records)
1057 }
1058
1059 #[instrument(skip(self), fields(instance_id=self.instance_id, sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1061 pub async fn canister_status(
1062 &self,
1063 canister_id: CanisterId,
1064 sender: Option<Principal>,
1065 ) -> Result<CanisterStatusResult, ClientError> {
1066 Ok(
1067 call_candid_as::<(CanisterIdRecord,), (CanisterStatusResult,)>(
1068 self,
1069 Principal::management_canister(),
1070 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1071 sender.unwrap_or(Principal::anonymous()),
1072 "canister_status",
1073 (CanisterIdRecord { canister_id },),
1074 )
1075 .await?
1076 .0,
1077 )
1078 }
1079
1080 #[instrument(ret, skip(self), fields(instance_id=self.instance_id))]
1083 pub async fn create_canister(&self) -> Result<CanisterId, ClientError> {
1084 self.create_canister_with_params(None, CreateCanisterParams::default())
1085 .await
1086 }
1087
1088 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, settings = ?settings, sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1091 pub async fn create_canister_with_settings(
1092 &self,
1093 sender: Option<Principal>,
1094 settings: Option<CanisterSettings>,
1095 ) -> Result<CanisterId, ClientError> {
1096 self.create_canister_with_params(
1097 sender,
1098 CreateCanisterParams {
1099 settings,
1100 ..Default::default()
1101 },
1102 )
1103 .await
1104 }
1105
1106 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, sender = %sender.unwrap_or(Principal::anonymous()).to_string(), settings = ?settings, canister_id = %canister_id.to_string()))]
1115 pub async fn create_canister_with_id(
1116 &self,
1117 sender: Option<Principal>,
1118 settings: Option<CanisterSettings>,
1119 canister_id: CanisterId,
1120 ) -> Result<CanisterId, ClientError> {
1121 self.create_canister_with_params(
1122 sender,
1123 CreateCanisterParams {
1124 settings,
1125 placement: Some(CreateCanisterPlacement::CanisterId(canister_id)),
1126 ..Default::default()
1127 },
1128 )
1129 .await
1130 }
1131
1132 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, sender = %sender.unwrap_or(Principal::anonymous()).to_string(), settings = ?settings, subnet_id = %subnet_id.to_string()))]
1135 pub async fn create_canister_on_subnet(
1136 &self,
1137 sender: Option<Principal>,
1138 settings: Option<CanisterSettings>,
1139 subnet_id: SubnetId,
1140 ) -> Result<CanisterId, ClientError> {
1141 self.create_canister_with_params(
1142 sender,
1143 CreateCanisterParams {
1144 settings,
1145 placement: Some(CreateCanisterPlacement::SubnetId(subnet_id)),
1146 ..Default::default()
1147 },
1148 )
1149 .await
1150 }
1151
1152 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1157 pub async fn create_canister_with_params(
1158 &self,
1159 sender: Option<Principal>,
1160 params: CreateCanisterParams,
1161 ) -> Result<CanisterId, ClientError> {
1162 let cycles = params.cycles.unwrap_or(100_000_000_000_000);
1163 let (effective_principal, specified_id) = match params.placement {
1164 None => (RawEffectivePrincipal::None, None),
1165 Some(CreateCanisterPlacement::SubnetId(subnet_id)) => (
1166 RawEffectivePrincipal::SubnetId(subnet_id.as_slice().to_vec()),
1167 None,
1168 ),
1169 Some(CreateCanisterPlacement::CanisterId(canister_id)) => (
1170 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1171 Some(canister_id),
1172 ),
1173 };
1174 Ok(call_candid_as::<_, (CanisterIdRecord,)>(
1175 self,
1176 Principal::management_canister(),
1177 effective_principal,
1178 sender.unwrap_or(Principal::anonymous()),
1179 "provisional_create_canister_with_cycles",
1180 (ProvisionalCreateCanisterWithCyclesArgs {
1181 settings: params.settings,
1182 amount: Some(cycles.into()),
1183 specified_id,
1184 sender_canister_version: None,
1185 },),
1186 )
1187 .await?
1188 .0
1189 .canister_id)
1190 }
1191
1192 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1195 pub async fn upload_chunk(
1196 &self,
1197 canister_id: CanisterId,
1198 sender: Option<Principal>,
1199 chunk: Vec<u8>,
1200 ) -> Result<Vec<u8>, ClientError> {
1201 Ok(call_candid_as::<_, (UploadChunkResult,)>(
1202 self,
1203 Principal::management_canister(),
1204 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1205 sender.unwrap_or(Principal::anonymous()),
1206 "upload_chunk",
1207 (UploadChunkArgs { canister_id, chunk },),
1208 )
1209 .await?
1210 .0
1211 .hash)
1212 }
1213
1214 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1216 pub async fn stored_chunks(
1217 &self,
1218 canister_id: CanisterId,
1219 sender: Option<Principal>,
1220 ) -> Result<Vec<Vec<u8>>, ClientError> {
1221 Ok(call_candid_as::<_, (StoredChunksResult,)>(
1222 self,
1223 Principal::management_canister(),
1224 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1225 sender.unwrap_or(Principal::anonymous()),
1226 "stored_chunks",
1227 (CanisterIdRecord { canister_id },),
1228 )
1229 .await?
1230 .0
1231 .into_iter()
1232 .map(|chunk| chunk.hash)
1233 .collect())
1234 }
1235
1236 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1238 pub async fn clear_chunk_store(
1239 &self,
1240 canister_id: CanisterId,
1241 sender: Option<Principal>,
1242 ) -> Result<(), ClientError> {
1243 call_candid_as(
1244 self,
1245 Principal::management_canister(),
1246 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1247 sender.unwrap_or(Principal::anonymous()),
1248 "clear_chunk_store",
1249 (CanisterIdRecord { canister_id },),
1250 )
1251 .await
1252 }
1253
1254 #[instrument(skip(self, mode, chunk_hashes_list, wasm_module_hash, arg), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string(), store_canister_id = %store_canister_id.to_string(), arg_len = %arg.len()))]
1256 pub async fn install_chunked_canister(
1257 &self,
1258 canister_id: CanisterId,
1259 sender: Option<Principal>,
1260 mode: CanisterInstallMode,
1261 store_canister_id: CanisterId,
1262 chunk_hashes_list: Vec<Vec<u8>>,
1263 wasm_module_hash: Vec<u8>,
1264 arg: Vec<u8>,
1265 ) -> Result<(), ClientError> {
1266 call_candid_as(
1267 self,
1268 Principal::management_canister(),
1269 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1270 sender.unwrap_or(Principal::anonymous()),
1271 "install_chunked_code",
1272 (InstallChunkedCodeArgs {
1273 mode,
1274 target_canister: canister_id,
1275 store_canister: Some(store_canister_id),
1276 chunk_hashes_list: chunk_hashes_list
1277 .into_iter()
1278 .map(|hash| ChunkHash { hash })
1279 .collect(),
1280 wasm_module_hash,
1281 arg,
1282 sender_canister_version: None,
1283 },),
1284 )
1285 .await
1286 }
1287
1288 async fn install_canister_helper(
1289 &self,
1290 mode: CanisterInstallMode,
1291 canister_id: CanisterId,
1292 wasm_module: Vec<u8>,
1293 arg: Vec<u8>,
1294 sender: Option<Principal>,
1295 ) -> Result<(), ClientError> {
1296 if wasm_module.len() + arg.len() < INSTALL_CHUNKED_CODE_THRESHOLD {
1297 call_candid_as::<(InstallCodeArgs,), ()>(
1298 self,
1299 Principal::management_canister(),
1300 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1301 sender.unwrap_or(Principal::anonymous()),
1302 "install_code",
1303 (InstallCodeArgs {
1304 mode,
1305 canister_id,
1306 wasm_module,
1307 arg,
1308 sender_canister_version: None,
1309 },),
1310 )
1311 .await?
1312 } else {
1313 self.clear_chunk_store(canister_id, sender).await?;
1314 let chunks: Vec<_> = wasm_module.chunks(INSTALL_CODE_CHUNK_SIZE).collect();
1315 let mut hashes = vec![];
1316 for chunk in chunks {
1317 let hash = self
1318 .upload_chunk(canister_id, sender, chunk.to_vec())
1319 .await?;
1320 hashes.push(hash);
1321 }
1322 let mut hasher = Sha256::new();
1323 hasher.update(wasm_module);
1324 let wasm_module_hash = hasher.finalize().to_vec();
1325 self.install_chunked_canister(
1326 canister_id,
1327 sender,
1328 mode,
1329 canister_id,
1330 hashes,
1331 wasm_module_hash,
1332 arg,
1333 )
1334 .await?
1335 };
1336 Ok(())
1337 }
1338
1339 #[instrument(skip(self, wasm_module, arg), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), wasm_module_len = %wasm_module.len(), arg_len = %arg.len(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1341 pub async fn install_canister(
1342 &self,
1343 canister_id: CanisterId,
1344 wasm_module: Vec<u8>,
1345 arg: Vec<u8>,
1346 sender: Option<Principal>,
1347 ) -> Result<(), ClientError> {
1348 self.install_canister_helper(
1349 CanisterInstallMode::Install,
1350 canister_id,
1351 wasm_module,
1352 arg,
1353 sender,
1354 )
1355 .await
1356 }
1357
1358 #[instrument(skip(self, wasm_module, arg), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), wasm_module_len = %wasm_module.len(), arg_len = %arg.len(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1360 pub async fn upgrade_canister(
1361 &self,
1362 canister_id: CanisterId,
1363 wasm_module: Vec<u8>,
1364 arg: Vec<u8>,
1365 sender: Option<Principal>,
1366 ) -> Result<(), ClientError> {
1367 self.install_canister_helper(
1368 CanisterInstallMode::Upgrade(None),
1369 canister_id,
1370 wasm_module,
1371 arg,
1372 sender,
1373 )
1374 .await
1375 }
1376
1377 #[instrument(skip(self, wasm_module, arg), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), wasm_module_len = %wasm_module.len(), arg_len = %arg.len(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1379 pub async fn upgrade_eop_canister(
1380 &self,
1381 canister_id: CanisterId,
1382 wasm_module: Vec<u8>,
1383 arg: Vec<u8>,
1384 sender: Option<Principal>,
1385 ) -> Result<(), ClientError> {
1386 self.install_canister_helper(
1387 CanisterInstallMode::Upgrade(Some(UpgradeFlags {
1388 wasm_memory_persistence: Some(WasmMemoryPersistence::Keep),
1389 skip_pre_upgrade: None,
1390 })),
1391 canister_id,
1392 wasm_module,
1393 arg,
1394 sender,
1395 )
1396 .await
1397 }
1398
1399 #[instrument(skip(self, wasm_module, arg), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), wasm_module_len = %wasm_module.len(), arg_len = %arg.len(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1401 pub async fn reinstall_canister(
1402 &self,
1403 canister_id: CanisterId,
1404 wasm_module: Vec<u8>,
1405 arg: Vec<u8>,
1406 sender: Option<Principal>,
1407 ) -> Result<(), ClientError> {
1408 self.install_canister_helper(
1409 CanisterInstallMode::Reinstall,
1410 canister_id,
1411 wasm_module,
1412 arg,
1413 sender,
1414 )
1415 .await
1416 }
1417
1418 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1420 pub async fn uninstall_canister(
1421 &self,
1422 canister_id: CanisterId,
1423 sender: Option<Principal>,
1424 ) -> Result<(), ClientError> {
1425 call_candid_as::<(CanisterIdRecord,), ()>(
1426 self,
1427 Principal::management_canister(),
1428 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1429 sender.unwrap_or(Principal::anonymous()),
1430 "uninstall_code",
1431 (CanisterIdRecord { canister_id },),
1432 )
1433 .await
1434 }
1435
1436 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1438 pub async fn take_canister_snapshot(
1439 &self,
1440 canister_id: CanisterId,
1441 sender: Option<Principal>,
1442 replace_snapshot: Option<Vec<u8>>,
1443 ) -> Result<Snapshot, ClientError> {
1444 Ok(call_candid_as::<_, (Snapshot,)>(
1445 self,
1446 Principal::management_canister(),
1447 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1448 sender.unwrap_or(Principal::anonymous()),
1449 "take_canister_snapshot",
1450 (TakeCanisterSnapshotArgs {
1451 canister_id,
1452 replace_snapshot,
1453 uninstall_code: None,
1454 sender_canister_version: None,
1455 },),
1456 )
1457 .await?
1458 .0)
1459 }
1460
1461 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1463 pub async fn load_canister_snapshot(
1464 &self,
1465 canister_id: CanisterId,
1466 sender: Option<Principal>,
1467 snapshot_id: Vec<u8>,
1468 ) -> Result<(), ClientError> {
1469 call_candid_as(
1470 self,
1471 Principal::management_canister(),
1472 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1473 sender.unwrap_or(Principal::anonymous()),
1474 "load_canister_snapshot",
1475 (LoadCanisterSnapshotArgs {
1476 canister_id,
1477 snapshot_id,
1478 sender_canister_version: None,
1479 },),
1480 )
1481 .await
1482 }
1483
1484 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1486 pub async fn list_canister_snapshots(
1487 &self,
1488 canister_id: CanisterId,
1489 sender: Option<Principal>,
1490 ) -> Result<Vec<Snapshot>, ClientError> {
1491 Ok(call_candid_as::<_, (Vec<Snapshot>,)>(
1492 self,
1493 Principal::management_canister(),
1494 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1495 sender.unwrap_or(Principal::anonymous()),
1496 "list_canister_snapshots",
1497 (CanisterIdRecord { canister_id },),
1498 )
1499 .await?
1500 .0)
1501 }
1502
1503 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1505 pub async fn delete_canister_snapshot(
1506 &self,
1507 canister_id: CanisterId,
1508 sender: Option<Principal>,
1509 snapshot_id: Vec<u8>,
1510 ) -> Result<(), ClientError> {
1511 call_candid_as(
1512 self,
1513 Principal::management_canister(),
1514 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1515 sender.unwrap_or(Principal::anonymous()),
1516 "delete_canister_snapshot",
1517 (DeleteCanisterSnapshotArgs {
1518 canister_id,
1519 snapshot_id,
1520 },),
1521 )
1522 .await
1523 }
1524
1525 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1527 pub async fn update_canister_settings(
1528 &self,
1529 canister_id: CanisterId,
1530 sender: Option<Principal>,
1531 settings: CanisterSettings,
1532 ) -> Result<(), ClientError> {
1533 call_candid_as::<_, ()>(
1534 self,
1535 Principal::management_canister(),
1536 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1537 sender.unwrap_or(Principal::anonymous()),
1538 "update_settings",
1539 (UpdateSettingsArgs {
1540 canister_id,
1541 settings,
1542 sender_canister_version: None,
1543 },),
1544 )
1545 .await
1546 }
1547
1548 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1550 pub async fn set_controllers(
1551 &self,
1552 canister_id: CanisterId,
1553 sender: Option<Principal>,
1554 new_controllers: Vec<Principal>,
1555 ) -> Result<(), ClientError> {
1556 let settings = CanisterSettings {
1557 controllers: Some(new_controllers),
1558 ..CanisterSettings::default()
1559 };
1560 call_candid_as::<(UpdateSettingsArgs,), ()>(
1561 self,
1562 Principal::management_canister(),
1563 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1564 sender.unwrap_or(Principal::anonymous()),
1565 "update_settings",
1566 (UpdateSettingsArgs {
1567 canister_id,
1568 settings,
1569 sender_canister_version: None,
1570 },),
1571 )
1572 .await
1573 }
1574
1575 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1577 pub async fn start_canister(
1578 &self,
1579 canister_id: CanisterId,
1580 sender: Option<Principal>,
1581 ) -> Result<(), ClientError> {
1582 call_candid_as::<(CanisterIdRecord,), ()>(
1583 self,
1584 Principal::management_canister(),
1585 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1586 sender.unwrap_or(Principal::anonymous()),
1587 "start_canister",
1588 (CanisterIdRecord { canister_id },),
1589 )
1590 .await
1591 }
1592
1593 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1595 pub async fn stop_canister(
1596 &self,
1597 canister_id: CanisterId,
1598 sender: Option<Principal>,
1599 ) -> Result<(), ClientError> {
1600 call_candid_as::<(CanisterIdRecord,), ()>(
1601 self,
1602 Principal::management_canister(),
1603 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1604 sender.unwrap_or(Principal::anonymous()),
1605 "stop_canister",
1606 (CanisterIdRecord { canister_id },),
1607 )
1608 .await
1609 }
1610
1611 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1613 pub async fn delete_canister(
1614 &self,
1615 canister_id: CanisterId,
1616 sender: Option<Principal>,
1617 ) -> Result<(), ClientError> {
1618 call_candid_as::<(CanisterIdRecord,), ()>(
1619 self,
1620 Principal::management_canister(),
1621 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1622 sender.unwrap_or(Principal::anonymous()),
1623 "delete_canister",
1624 (CanisterIdRecord { canister_id },),
1625 )
1626 .await
1627 }
1628
1629 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))]
1631 pub async fn canister_exists(&self, canister_id: CanisterId) -> Result<bool, ClientError> {
1632 Ok(self.get_subnet(canister_id).await?.is_some())
1633 }
1634
1635 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, subnet_id = %subnet_id.to_string()))]
1637 pub async fn delete_subnet(&self, subnet_id: SubnetId) -> Result<(), ClientError> {
1638 let endpoint = "update/delete_subnet";
1639 self.post::<(), RawSubnetId>(
1640 endpoint,
1641 RawSubnetId {
1642 subnet_id: subnet_id.as_slice().to_vec(),
1643 },
1644 )
1645 .await?;
1646
1647 Ok(())
1648 }
1649
1650 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))]
1652 pub async fn get_subnet(
1653 &self,
1654 canister_id: CanisterId,
1655 ) -> Result<Option<SubnetId>, ClientError> {
1656 let endpoint = "read/get_subnet";
1657 let result: Option<RawSubnetId> = self
1658 .post(
1659 endpoint,
1660 RawCanisterId {
1661 canister_id: canister_id.as_slice().to_vec(),
1662 },
1663 )
1664 .await?;
1665 Ok(result.map(|RawSubnetId { subnet_id }| SubnetId::from_slice(&subnet_id)))
1666 }
1667
1668 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, subnet_id = %subnet_id.to_string()))]
1670 pub async fn get_subnet_metrics(
1671 &self,
1672 subnet_id: Principal,
1673 ) -> Result<Option<SubnetMetrics>, ClientError> {
1674 let path = vec![
1675 "subnet".into(),
1676 Label::from_bytes(subnet_id.as_slice()),
1677 "metrics".into(),
1678 ];
1679 let paths = vec![path.clone()];
1680 let content = ReadState {
1681 ingress_expiry: self.get_time().await?.as_nanos_since_unix_epoch() + 240_000_000_000,
1682 sender: Principal::anonymous(),
1683 paths,
1684 };
1685 let envelope = Envelope {
1686 content: std::borrow::Cow::Borrowed(&content),
1687 sender_pubkey: None,
1688 sender_sig: None,
1689 sender_delegation: None,
1690 };
1691
1692 let serialized_bytes = encode_envelope(&envelope, usize::MAX)?;
1693
1694 let endpoint = format!("api/v2/subnet/{}/read_state", subnet_id.to_text());
1695 let url = self.instance_url().join(&endpoint).unwrap();
1696 let mut exchange = Exchange::new(&url, self.max_request_time_ms);
1697 let (status, bytes) = exchange
1698 .bytes(
1699 &self.reqwest_client,
1700 self.reqwest_client
1701 .post(url.clone())
1702 .header(reqwest::header::CONTENT_TYPE, "application/cbor")
1703 .body(serialized_bytes),
1704 )
1705 .await?;
1706 if !status.is_success() {
1707 return Err(exchange.server_error(status, &bytes));
1708 }
1709 let read_state_response: ReadStateResponse =
1710 decode_read_state_response(&bytes, bytes.len())?;
1711 let cert: Certificate = decode_certificate(
1712 &read_state_response.certificate,
1713 read_state_response.certificate.len(),
1714 )?;
1715
1716 let metrics = match cert.tree.lookup_path(path) {
1717 LookupResult::Found(value) => value,
1718 _ => return Ok(None),
1719 };
1720 decode_subnet_metrics(metrics, metrics.len()).map_err(ClientError::from)
1721 }
1722
1723 pub async fn drop(mut self) -> Result<(), ClientError> {
1726 self.do_drop().await?;
1727
1728 Ok(())
1729 }
1730
1731 pub(crate) async fn do_drop(&mut self) -> Result<(), ClientError> {
1732 if self.owns_instance {
1733 let url = self.instance_url();
1734 let mut exchange = Exchange::new(&url, self.max_request_time_ms);
1735 let (status, bytes) = exchange
1736 .bytes(
1737 &self.reqwest_client,
1738 self.reqwest_client.delete(url.clone()),
1739 )
1740 .await?;
1741 if !status.is_success() {
1742 return Err(exchange.server_error(status, &bytes));
1743 }
1744 self.owns_instance = false;
1745 Ok(())
1746 } else {
1747 self.stop_http_gateway().await
1748 }
1749 }
1750
1751 async fn get<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, ClientError> {
1752 self.request(HttpMethod::Get, endpoint, ()).await
1753 }
1754
1755 async fn post<T: DeserializeOwned, B: Serialize>(
1756 &self,
1757 endpoint: &str,
1758 body: B,
1759 ) -> Result<T, ClientError> {
1760 self.request(HttpMethod::Post, endpoint, body).await
1761 }
1762
1763 async fn request<T: DeserializeOwned, B: Serialize>(
1764 &self,
1765 method: HttpMethod,
1766 endpoint: &str,
1767 body: B,
1768 ) -> Result<T, ClientError> {
1769 self.request_limited(method, endpoint, body, None).await
1770 }
1771
1772 async fn request_limited<T: DeserializeOwned, B: Serialize>(
1773 &self,
1774 method: HttpMethod,
1775 endpoint: &str,
1776 body: B,
1777 response_limit: Option<usize>,
1778 ) -> Result<T, ClientError> {
1779 let url = self.instance_url().join(endpoint).unwrap();
1780 let mut exchange = Exchange::new(&url, self.max_request_time_ms);
1781 exchange.response_limit = response_limit;
1782 loop {
1783 let builder = if let Some(operation) = &exchange.context.operation {
1784 self.reqwest_client.get(
1785 self.server_url
1786 .join(&format!(
1787 "/read_graph/{}/{}",
1788 operation.state_label, operation.op_id
1789 ))
1790 .unwrap(),
1791 )
1792 } else {
1793 match method {
1794 HttpMethod::Get => self.reqwest_client.get(url.clone()),
1795 HttpMethod::Post => self.reqwest_client.post(url.clone()).json(&body),
1796 }
1797 };
1798 let (status, bytes) = exchange.bytes(&self.reqwest_client, builder).await?;
1799 match status {
1800 StatusCode::OK => {
1801 let value = exchange.decode(&bytes)?;
1802 if let Some(operation) = &exchange.context.operation {
1803 let request = self.reqwest_client.delete(
1805 self.server_url
1806 .join(&format!(
1807 "/prune_graph/{}/{}",
1808 operation.state_label, operation.op_id
1809 ))
1810 .unwrap(),
1811 );
1812 tokio::spawn(async move {
1813 let _ = request.send().await;
1814 });
1815 }
1816 return Ok(value);
1817 }
1818 StatusCode::ACCEPTED | StatusCode::CONFLICT => {
1819 let receipt: crate::pocket_ic::common::rest::StartedOrBusyResponse =
1820 exchange.decode(&bytes)?;
1821 if exchange.context.operation.is_none() {
1822 if status == StatusCode::ACCEPTED {
1823 exchange.started(Operation {
1824 state_label: receipt.state_label,
1825 op_id: receipt.op_id,
1826 });
1827 } else {
1828 exchange.context.may_have_executed = false;
1830 }
1831 }
1832 }
1835 StatusCode::NOT_FOUND if exchange.context.operation.is_some() => {}
1836 _ => return Err(exchange.server_error(status, &bytes)),
1837 }
1838 exchange.pause().await?;
1839 }
1840 }
1841
1842 async fn canister_call(
1843 &self,
1844 endpoint: &str,
1845 effective_principal: RawEffectivePrincipal,
1846 canister_id: CanisterId,
1847 sender: Principal,
1848 method: &str,
1849 payload: Vec<u8>,
1850 sender_info: Option<RawSenderInfo>,
1851 ) -> Result<Vec<u8>, ClientError> {
1852 let raw_canister_call = RawCanisterCall {
1853 sender: sender.as_slice().to_vec(),
1854 canister_id: canister_id.as_slice().to_vec(),
1855 method: method.to_string(),
1856 payload,
1857 effective_principal,
1858 sender_info,
1859 };
1860
1861 let result: RawCanisterResult = self.post(endpoint, raw_canister_call).await?;
1862 Result::<Vec<u8>, RejectResponse>::from(result).map_err(ClientError::from)
1863 }
1864
1865 pub async fn update_call_with_effective_principal(
1866 &self,
1867 canister_id: CanisterId,
1868 effective_principal: RawEffectivePrincipal,
1869 sender: Principal,
1870 method: &str,
1871 payload: Vec<u8>,
1872 ) -> Result<Vec<u8>, ClientError> {
1873 self.update_call_with_effective_principal_and_sender_info_inner(
1874 canister_id,
1875 effective_principal,
1876 sender,
1877 method,
1878 payload,
1879 None,
1880 )
1881 .await
1882 }
1883
1884 #[instrument(ret, skip(self), fields(instance_id=self.instance_id))]
1898 pub async fn get_canister_http(&self) -> Result<Vec<CanisterHttpRequest>, ClientError> {
1899 let endpoint = "read/get_canister_http";
1900 let res: Vec<RawCanisterHttpRequest> = self.get(endpoint).await?;
1901 Ok(res.into_iter().map(|r| r.into()).collect())
1902 }
1903
1904 #[instrument(ret, skip(self), fields(instance_id=self.instance_id))]
1910 pub async fn mock_canister_http_response(
1911 &self,
1912 mock_canister_http_response: MockCanisterHttpResponse,
1913 ) -> Result<(), ClientError> {
1914 let endpoint = "update/mock_canister_http";
1915 let raw_mock_canister_http_response: RawMockCanisterHttpResponse =
1916 mock_canister_http_response.into();
1917 self.post(endpoint, raw_mock_canister_http_response).await
1918 }
1919
1920 #[instrument(ret, skip(self), fields(instance_id=self.instance_id))]
1936 pub async fn mock_flexible_canister_http_response(
1937 &self,
1938 mock_flexible_canister_http_response: MockFlexibleCanisterHttpResponse,
1939 ) -> Result<(), ClientError> {
1940 let endpoint = "update/mock_flexible_canister_http";
1941 let raw_mock_flexible_canister_http_response: RawMockFlexibleCanisterHttpResponse =
1942 mock_flexible_canister_http_response.into();
1943 self.post(endpoint, raw_mock_flexible_canister_http_response)
1944 .await
1945 }
1946
1947 #[instrument(ret, skip(self), fields(instance_id=self.instance_id))]
1951 pub async fn canister_snapshot_download(
1952 &self,
1953 canister_id: CanisterId,
1954 sender: Principal,
1955 snapshot_id: Vec<u8>,
1956 snapshot_dir: PathBuf,
1957 ) -> Result<(), ClientError> {
1958 let endpoint = "update/canister_snapshot_download";
1959 #[cfg(not(windows))]
1960 let snapshot_dir = snapshot_dir;
1961 #[cfg(windows)]
1962 let snapshot_dir = wsl_path(&snapshot_dir, "snapshot directory").into();
1963 let raw_canister_snapshot_download = RawCanisterSnapshotDownload {
1964 sender: sender.into(),
1965 canister_id: canister_id.into(),
1966 snapshot_id,
1967 snapshot_dir,
1968 };
1969 self.post(endpoint, raw_canister_snapshot_download).await
1970 }
1971
1972 #[instrument(ret, skip(self), fields(instance_id=self.instance_id))]
1976 pub async fn canister_snapshot_upload(
1977 &self,
1978 canister_id: CanisterId,
1979 sender: Principal,
1980 replace_snapshot: Option<Vec<u8>>,
1981 snapshot_dir: PathBuf,
1982 ) -> Result<Vec<u8>, ClientError> {
1983 let endpoint = "update/canister_snapshot_upload";
1984 let replace_snapshot =
1985 replace_snapshot.map(|snapshot_id| RawCanisterSnapshotId { snapshot_id });
1986 #[cfg(not(windows))]
1987 let snapshot_dir = snapshot_dir;
1988 #[cfg(windows)]
1989 let snapshot_dir = wsl_path(&snapshot_dir, "snapshot directory").into();
1990 let raw_canister_snapshot_upload = RawCanisterSnapshotUpload {
1991 sender: sender.into(),
1992 canister_id: canister_id.into(),
1993 replace_snapshot,
1994 snapshot_dir,
1995 };
1996 Ok(self
1997 .post::<RawCanisterSnapshotId, _>(endpoint, raw_canister_snapshot_upload)
1998 .await?
1999 .snapshot_id)
2000 }
2001}
2002
2003pub async fn call_candid_as<Input, Output>(
2007 env: &PocketIc,
2008 canister_id: CanisterId,
2009 effective_principal: RawEffectivePrincipal,
2010 sender: Principal,
2011 method: &str,
2012 input: Input,
2013) -> Result<Output, ClientError>
2014where
2015 Input: ArgumentEncoder,
2016 Output: for<'a> ArgumentDecoder<'a>,
2017{
2018 with_candid(input, |payload| async {
2019 env.update_call_with_effective_principal(
2020 canister_id,
2021 effective_principal,
2022 sender,
2023 method,
2024 payload,
2025 )
2026 .await
2027 })
2028 .await
2029}
2030
2031pub async fn call_candid<Input, Output>(
2034 env: &PocketIc,
2035 canister_id: CanisterId,
2036 effective_principal: RawEffectivePrincipal,
2037 method: &str,
2038 input: Input,
2039) -> Result<Output, ClientError>
2040where
2041 Input: ArgumentEncoder,
2042 Output: for<'a> ArgumentDecoder<'a>,
2043{
2044 call_candid_as(
2045 env,
2046 canister_id,
2047 effective_principal,
2048 Principal::anonymous(),
2049 method,
2050 input,
2051 )
2052 .await
2053}
2054
2055pub async fn query_candid<Input, Output>(
2057 env: &PocketIc,
2058 canister_id: CanisterId,
2059 method: &str,
2060 input: Input,
2061) -> Result<Output, ClientError>
2062where
2063 Input: ArgumentEncoder,
2064 Output: for<'a> ArgumentDecoder<'a>,
2065{
2066 query_candid_as(env, canister_id, Principal::anonymous(), method, input).await
2067}
2068
2069pub async fn query_candid_as<Input, Output>(
2072 env: &PocketIc,
2073 canister_id: CanisterId,
2074 sender: Principal,
2075 method: &str,
2076 input: Input,
2077) -> Result<Output, ClientError>
2078where
2079 Input: ArgumentEncoder,
2080 Output: for<'a> ArgumentDecoder<'a>,
2081{
2082 with_candid(input, |bytes| async {
2083 env.query_call(canister_id, sender, method, bytes).await
2084 })
2085 .await
2086}
2087
2088pub async fn update_candid<Input, Output>(
2090 env: &PocketIc,
2091 canister_id: CanisterId,
2092 method: &str,
2093 input: Input,
2094) -> Result<Output, ClientError>
2095where
2096 Input: ArgumentEncoder,
2097 Output: for<'a> ArgumentDecoder<'a>,
2098{
2099 update_candid_as(env, canister_id, Principal::anonymous(), method, input).await
2100}
2101
2102pub async fn update_candid_as<Input, Output>(
2105 env: &PocketIc,
2106 canister_id: CanisterId,
2107 sender: Principal,
2108 method: &str,
2109 input: Input,
2110) -> Result<Output, ClientError>
2111where
2112 Input: ArgumentEncoder,
2113 Output: for<'a> ArgumentDecoder<'a>,
2114{
2115 with_candid(input, |bytes| async {
2116 env.update_call(canister_id, sender, method, bytes).await
2117 })
2118 .await
2119}
2120
2121pub async fn with_candid<Input, Output, Fut>(
2124 input: Input,
2125 f: impl FnOnce(Vec<u8>) -> Fut,
2126) -> Result<Output, ClientError>
2127where
2128 Input: ArgumentEncoder,
2129 Output: for<'a> ArgumentDecoder<'a>,
2130 Fut: Future<Output = Result<Vec<u8>, ClientError>>,
2131{
2132 let input = encode_args(input)?;
2133 let output = f(input).await?;
2134 Ok(decode_args(&output)?)
2135}
2136
2137fn setup_tracing(pid: u32) -> Option<WorkerGuard> {
2138 use tracing_subscriber::prelude::*;
2139 match std::env::var(LOG_DIR_PATH_ENV_NAME).map(std::path::PathBuf::from) {
2140 Ok(p) => {
2141 std::fs::create_dir_all(&p).expect("Could not create directory");
2142
2143 let file_name = format!("pocket_ic_client_{pid}.log");
2144 let appender = tracing_appender::rolling::never(&p, file_name);
2145 let (non_blocking_appender, guard) = tracing_appender::non_blocking(appender);
2146 let log_dir_filter: EnvFilter =
2147 tracing_subscriber::EnvFilter::try_from_env(LOG_DIR_LEVELS_ENV_NAME)
2148 .unwrap_or_else(|_| "trace".parse().unwrap());
2149
2150 let layers = vec![
2151 tracing_subscriber::fmt::layer()
2152 .with_writer(non_blocking_appender)
2153 .with_ansi(false)
2155 .with_filter(log_dir_filter)
2156 .boxed(),
2157 ];
2158 let _ = tracing_subscriber::registry().with(layers).try_init();
2159 Some(guard)
2160 }
2161 _ => None,
2162 }
2163}
2164
2165pub async fn get_default_effective_canister_id(
2173 pocket_ic_url: String,
2174) -> Result<Principal, ClientError> {
2175 let url = Url::parse(&format!(
2176 "{}/_/topology",
2177 pocket_ic_url.trim_end_matches('/')
2178 ))
2179 .map_err(|e| ClientError::Protocol(e.to_string()))?;
2180 let client = reqwest::Client::new();
2181 let mut exchange = Exchange::new(&url, Some(crate::pocket_ic::DEFAULT_MAX_REQUEST_TIME_MS));
2182 loop {
2183 let (status, bytes) = exchange.bytes(&client, client.get(url.clone())).await?;
2184 if status == StatusCode::CONFLICT {
2185 exchange.pause().await?;
2186 continue;
2187 }
2188 if !status.is_success() {
2189 return Err(exchange.server_error(status, &bytes));
2190 }
2191 let topology: Topology = exchange.decode(&bytes)?;
2192 return Principal::try_from_slice(&topology.default_effective_canister_id.canister_id)
2193 .map_err(|e| ClientError::Protocol(e.to_string()));
2194 }
2195}