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