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