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