1use crate::pocket_ic::common::rest::{
3 AutoProgressConfig, BlobCompression, BlobId, CanisterHttpRequest, CreateHttpGatewayResponse,
4 CreateInstanceResponse, HttpGatewayBackend, HttpGatewayConfig, HttpGatewayInfo, HttpsConfig,
5 InstanceConfig, InstanceId, MockCanisterHttpResponse, RawAddCycles, RawCanisterCall,
6 RawCanisterHttpRequest, RawCanisterId, RawCanisterResult, RawCanisterSnapshotDownload,
7 RawCanisterSnapshotId, RawCanisterSnapshotUpload, RawCycles, RawEffectivePrincipal,
8 RawIngressStatusArgs, RawMessageId, RawMockCanisterHttpResponse, RawPrincipalId,
9 RawSetStableMemory, RawStableMemory, RawSubnetId, RawTickConfigs, RawTime, SubnetId, Topology,
10};
11#[cfg(windows)]
12use crate::pocket_ic::wsl_path;
13use crate::pocket_ic::{ClientError, Operation, RequestContext, http::Exchange};
14use crate::pocket_ic::{
15 CreateCanisterParams, CreateCanisterPlacement, IngressStatusResult, PocketIcBuilder,
16 RejectResponse, TickConfigs, Time,
17};
18use backon::{BackoffBuilder, ExponentialBuilder};
19use candid::{
20 Principal, decode_args, encode_args,
21 utils::{ArgumentDecoder, ArgumentEncoder},
22};
23use ic_management_canister_types::{
24 CanisterId, CanisterIdRecord, CanisterInstallMode, CanisterLogRecord, CanisterSettings,
25 CanisterStatusResult, ChunkHash, DeleteCanisterSnapshotArgs, FetchCanisterLogsResult,
26 InstallChunkedCodeArgs, InstallCodeArgs, LoadCanisterSnapshotArgs,
27 ProvisionalCreateCanisterWithCyclesArgs, Snapshot, StoredChunksResult,
28 TakeCanisterSnapshotArgs, UpdateSettingsArgs, UploadChunkArgs, UploadChunkResult,
29};
30use ic_platform::certification::cbor::decode_certificate;
31use ic_platform::certification::{Certificate, Label, LookupResult};
32use ic_platform::ic_protocol::Envelope;
33use ic_platform::ic_protocol::EnvelopeContent::ReadState;
34use ic_platform::ic_protocol::{
35 ReadStateResponse, SubnetMetrics, decode_read_state_response, decode_subnet_metrics,
36 encode_envelope,
37};
38use reqwest::{StatusCode, Url};
39use serde::{Serialize, de::DeserializeOwned};
40use sha2::{Digest, Sha256};
41use std::future::Future;
42use std::net::IpAddr;
43use std::path::PathBuf;
44use std::time::Duration;
45use tracing::{debug, instrument};
46
47mod snapshot;
50
51const LOCALHOST: &str = "localhost";
52
53const INSTALL_CHUNKED_CODE_THRESHOLD: usize = 2_000_000; const INSTALL_CODE_CHUNK_SIZE: usize = 1 << 20; enum HttpMethod {
71 Get,
72 Post,
73}
74
75pub struct PocketIc {
77 pub instance_id: InstanceId,
79 max_request_time_ms: Option<u64>,
81 http_gateway: Option<HttpGatewayInfo>,
82 server_url: Url,
83 reqwest_client: reqwest::Client,
84 owns_instance: bool,
86}
87
88impl PocketIc {
89 pub async fn new(server_url: Url) -> Result<Self, ClientError> {
92 PocketIcBuilder::new()
93 .with_server_url(server_url)
94 .with_application_subnet()
95 .build()
96 .await
97 }
98
99 pub fn new_from_existing_instance(
104 server_url: Url,
105 instance_id: InstanceId,
106 max_request_time_ms: Option<u64>,
107 ) -> Self {
108 let reqwest_client = reqwest::Client::new();
109 debug!("instance_id={} Reusing existing instance.", instance_id);
110
111 Self {
112 instance_id,
113 max_request_time_ms,
114 http_gateway: None,
115 server_url,
116 reqwest_client,
117 owns_instance: false,
118 }
119 }
120
121 pub(crate) async fn from_config(
122 server_url: Url,
123 max_request_time_ms: Option<u64>,
124 instance_config: InstanceConfig,
125 ) -> Result<Self, ClientError> {
126 #[cfg(windows)]
127 let instance_config = InstanceConfig {
128 state_dir: instance_config
129 .state_dir
130 .map(|path| wsl_path(&path, "state directory").into()),
131 ..instance_config
132 };
133
134 let reqwest_client = reqwest::Client::new();
135 let url = server_url.join("instances").unwrap();
136 let mut exchange = Exchange::new(&url, max_request_time_ms);
137 let response: CreateInstanceResponse = exchange
138 .json(
139 &reqwest_client,
140 reqwest_client.post(url.clone()).json(&instance_config),
141 )
142 .await?;
143 let (instance_id, http_gateway_info) = match response {
144 CreateInstanceResponse::Created {
145 instance_id,
146 http_gateway_info,
147 ..
148 } => (instance_id, http_gateway_info),
149 CreateInstanceResponse::Error { message } => {
150 return Err(ClientError::Server {
151 context: Box::new(exchange.context.clone()),
152 status: exchange.context.status.unwrap_or(StatusCode::OK),
153 message,
154 });
155 }
156 };
157 debug!("instance_id={} New instance created.", instance_id);
158
159 Ok(Self {
160 instance_id,
161 max_request_time_ms,
162 http_gateway: http_gateway_info,
163 server_url,
164 reqwest_client,
165 owns_instance: true,
166 })
167 }
168
169 pub fn get_server_url(&self) -> Url {
171 self.server_url.clone()
172 }
173
174 pub async fn topology(&self) -> Result<Topology, ClientError> {
176 let endpoint = "read/topology";
177 self.get(endpoint).await
178 }
179
180 #[instrument(ret, skip(self, blob), fields(instance_id=self.instance_id, blob_len = %blob.len(), compression = ?compression))]
182 pub async fn upload_blob(
183 &self,
184 blob: Vec<u8>,
185 compression: BlobCompression,
186 ) -> Result<BlobId, ClientError> {
187 let url = self.server_url.join("blobstore/").unwrap();
188 let mut request = self.reqwest_client.post(url.clone()).body(blob);
189 if let BlobCompression::Gzip = compression {
190 request = request.header(reqwest::header::CONTENT_ENCODING, "gzip");
191 }
192 let mut exchange = Exchange::new(&url, self.max_request_time_ms);
193 let (status, bytes) = exchange.bytes(&self.reqwest_client, request).await?;
194 if !status.is_success() {
195 return Err(exchange.server_error(status, &bytes));
196 }
197 let hash = hex::decode(bytes).map_err(|error| ClientError::Protocol(error.to_string()))?;
198 Ok(BlobId(hash))
199 }
200
201 #[instrument(skip(self, data), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), data_len = %data.len(), compression = ?compression))]
204 pub async fn set_stable_memory(
205 &self,
206 canister_id: CanisterId,
207 data: Vec<u8>,
208 compression: BlobCompression,
209 ) -> Result<(), ClientError> {
210 let blob_id = self.upload_blob(data, compression).await?;
211 let endpoint = "update/set_stable_memory";
212 self.post::<(), _>(
213 endpoint,
214 RawSetStableMemory {
215 canister_id: canister_id.as_slice().to_vec(),
216 blob_id,
217 },
218 )
219 .await?;
220
221 Ok(())
222 }
223
224 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))]
226 pub async fn get_stable_memory(&self, canister_id: CanisterId) -> Result<Vec<u8>, ClientError> {
227 let endpoint = "read/get_stable_memory";
228 let RawStableMemory { blob } = self
229 .post(
230 endpoint,
231 RawCanisterId {
232 canister_id: canister_id.as_slice().to_vec(),
233 },
234 )
235 .await?;
236 Ok(blob)
237 }
238
239 #[instrument(ret)]
241 pub async fn list_instances(server_url: Url) -> Result<Vec<String>, ClientError> {
242 let url = server_url.join("instances").unwrap();
243 let client = reqwest::Client::new();
244 Exchange::new(&url, None)
245 .json(&client, client.get(url.clone()))
246 .await
247 }
248
249 #[instrument(skip(self), fields(instance_id=self.instance_id))]
254 pub async fn tick(&self) -> Result<(), ClientError> {
255 self.tick_with_configs(TickConfigs::default()).await?;
256
257 Ok(())
258 }
259
260 #[instrument(skip(self), fields(instance_id=self.instance_id))]
263 pub async fn tick_with_configs(&self, configs: TickConfigs) -> Result<(), ClientError> {
264 let endpoint = "update/tick";
265 self.post::<(), RawTickConfigs>(endpoint, configs.into())
266 .await?;
267
268 Ok(())
269 }
270
271 #[instrument(skip(self), fields(instance_id=self.instance_id))]
279 pub async fn auto_progress(&self) -> Result<Url, ClientError> {
280 let endpoint = "auto_progress";
281 let auto_progress_config = AutoProgressConfig {
282 artificial_delay_ms: None,
283 };
284 self.post::<(), _>(endpoint, auto_progress_config).await?;
285 Ok(self.instance_url())
286 }
287
288 #[instrument(skip(self), fields(instance_id=self.instance_id))]
290 pub async fn auto_progress_enabled(&self) -> Result<bool, ClientError> {
291 self.get("auto_progress").await
292 }
293
294 pub(crate) fn instance_url(&self) -> Url {
295 self.server_url
296 .join("/instances/")
297 .unwrap()
298 .join(&format!("{}/", self.instance_id))
299 .unwrap()
300 }
301
302 #[instrument(skip(self), fields(instance_id=self.instance_id))]
304 pub async fn stop_progress(&self) -> Result<(), ClientError> {
305 let endpoint = "stop_progress";
306 self.post::<(), _>(endpoint, "").await?;
307
308 Ok(())
309 }
310
311 pub fn url(&self) -> Option<Url> {
315 self.http_gateway
316 .as_ref()
317 .map(|res| Url::parse(&format!("http://{}:{}/", LOCALHOST, res.port)).unwrap())
318 }
319
320 #[instrument(skip(self), fields(instance_id=self.instance_id))]
331 pub async fn make_live(&mut self, listen_at: Option<u16>) -> Result<Url, ClientError> {
332 self.make_live_with_params(None, listen_at, None, None)
333 .await
334 }
335
336 #[instrument(skip(self), fields(instance_id=self.instance_id))]
349 pub async fn make_live_with_params(
350 &mut self,
351 ip_addr: Option<IpAddr>,
352 listen_at: Option<u16>,
353 domains: Option<Vec<String>>,
354 https_config: Option<HttpsConfig>,
355 ) -> Result<Url, ClientError> {
356 if let Some(url) = self.url() {
357 return Ok(url);
358 }
359 if !self.auto_progress_enabled().await? {
360 self.auto_progress().await?;
361 }
362 self.start_http_gateway(
363 ip_addr.map(|ip_addr| ip_addr.to_string()),
364 listen_at,
365 domains,
366 https_config,
367 )
368 .await
369 }
370
371 async fn start_http_gateway(
372 &mut self,
373 ip_addr: Option<String>,
374 port: Option<u16>,
375 domains: Option<Vec<String>>,
376 https_config: Option<HttpsConfig>,
377 ) -> Result<Url, ClientError> {
378 let endpoint = self.server_url.join("http_gateway").unwrap();
379 let http_gateway_config = HttpGatewayConfig {
380 ip_addr,
381 port,
382 forward_to: HttpGatewayBackend::PocketIcInstance(self.instance_id),
383 domains: domains.clone(),
384 https_config: https_config.clone(),
385 domain_custom_provider_local_file: None,
386 };
387 let mut exchange = Exchange::new(&endpoint, self.max_request_time_ms);
388 let res: CreateHttpGatewayResponse = exchange
389 .json(
390 &self.reqwest_client,
391 self.reqwest_client
392 .post(endpoint.clone())
393 .json(&http_gateway_config),
394 )
395 .await?;
396 Ok(match res {
397 CreateHttpGatewayResponse::Created(info) => {
398 let port = info.port;
399 self.http_gateway = Some(info);
400 let proto = if https_config.is_some() {
401 "https"
402 } else {
403 "http"
404 };
405 Url::parse(&format!(
406 "{}://{}:{}/",
407 proto,
408 domains
409 .unwrap_or_default()
410 .into_iter()
411 .next()
412 .unwrap_or(LOCALHOST.to_string()),
413 port
414 ))
415 .unwrap()
416 }
417 CreateHttpGatewayResponse::Error { message } => {
418 return Err(ClientError::Server {
419 context: Box::new(exchange.context.clone()),
420 status: exchange.context.status.unwrap_or(StatusCode::OK),
421 message,
422 });
423 }
424 })
425 }
426
427 async fn stop_http_gateway(&mut self) -> Result<(), ClientError> {
428 if let Some(info) = &self.http_gateway {
429 let url = self
430 .server_url
431 .join(&format!("http_gateway/{}/stop", info.instance_id))
432 .unwrap();
433 Exchange::new(&url, self.max_request_time_ms)
434 .json::<()>(&self.reqwest_client, self.reqwest_client.post(url.clone()))
435 .await?;
436 self.http_gateway = None;
437 }
438 Ok(())
439 }
440
441 #[instrument(skip(self), fields(instance_id=self.instance_id))]
444 pub async fn stop_live(&mut self) -> Result<(), ClientError> {
445 self.stop_http_gateway().await?;
446 self.stop_progress().await?;
447
448 Ok(())
449 }
450
451 #[instrument(skip(self), fields(instance_id=self.instance_id))]
453 pub async fn root_key(&self) -> Result<Option<Vec<u8>>, ClientError> {
454 let Some(subnet_id) = self.topology().await?.get_nns() else {
455 return Ok(None);
456 };
457 let subnet_id: RawSubnetId = subnet_id.into();
458 let endpoint = "read/pub_key";
459 let res = self.post::<Vec<u8>, _>(endpoint, subnet_id).await?;
460 Ok(Some(res))
461 }
462
463 #[instrument(ret, skip(self), fields(instance_id=self.instance_id))]
465 pub async fn get_time(&self) -> Result<Time, ClientError> {
466 let endpoint = "read/get_time";
467 let result: RawTime = self.get(endpoint).await?;
468 Ok(Time::from_nanos_since_unix_epoch(result.nanos_since_epoch))
469 }
470
471 #[instrument(skip(self), fields(instance_id=self.instance_id, time = ?time))]
473 pub async fn set_time(&self, time: Time) -> Result<(), ClientError> {
474 let endpoint = "update/set_time";
475 self.post::<(), _>(
476 endpoint,
477 RawTime {
478 nanos_since_epoch: time.as_nanos_since_unix_epoch(),
479 },
480 )
481 .await?;
482
483 Ok(())
484 }
485
486 #[instrument(skip(self), fields(instance_id=self.instance_id, time = ?time))]
488 pub async fn set_certified_time(&self, time: Time) -> Result<(), ClientError> {
489 let endpoint = "update/set_certified_time";
490 self.post::<(), _>(
491 endpoint,
492 RawTime {
493 nanos_since_epoch: time.as_nanos_since_unix_epoch(),
494 },
495 )
496 .await?;
497
498 Ok(())
499 }
500
501 #[instrument(skip(self), fields(instance_id=self.instance_id, duration = ?duration))]
503 pub async fn advance_time(&self, duration: Duration) -> Result<(), ClientError> {
504 let now = self.get_time().await?;
505 self.set_time(now + duration).await?;
506
507 Ok(())
508 }
509
510 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))]
513 pub async fn get_controllers(
514 &self,
515 canister_id: CanisterId,
516 ) -> Result<Vec<Principal>, ClientError> {
517 let endpoint = "read/get_controllers";
518 let result: Vec<RawPrincipalId> = self
519 .post(
520 endpoint,
521 RawCanisterId {
522 canister_id: canister_id.as_slice().to_vec(),
523 },
524 )
525 .await?;
526 Ok(result.into_iter().map(|p| p.into()).collect())
527 }
528
529 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))]
531 pub async fn cycle_balance(&self, canister_id: CanisterId) -> Result<u128, ClientError> {
532 let endpoint = "read/get_cycles";
533 let result: RawCycles = self
534 .post(
535 endpoint,
536 RawCanisterId {
537 canister_id: canister_id.as_slice().to_vec(),
538 },
539 )
540 .await?;
541 Ok(result.cycles)
542 }
543
544 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), amount = %amount))]
546 pub async fn add_cycles(
547 &self,
548 canister_id: CanisterId,
549 amount: u128,
550 ) -> Result<u128, ClientError> {
551 let endpoint = "update/add_cycles";
552 let result: RawCycles = self
553 .post(
554 endpoint,
555 RawAddCycles {
556 canister_id: canister_id.as_slice().to_vec(),
557 amount,
558 },
559 )
560 .await?;
561 Ok(result.cycles)
562 }
563
564 pub async fn submit_call(
566 &self,
567 canister_id: CanisterId,
568 sender: Principal,
569 method: &str,
570 payload: Vec<u8>,
571 ) -> Result<RawMessageId, ClientError> {
572 self.submit_call_with_effective_principal(
573 canister_id,
574 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
575 sender,
576 method,
577 payload,
578 )
579 .await
580 }
581
582 pub async fn submit_call_with_effective_principal(
584 &self,
585 canister_id: CanisterId,
586 effective_principal: RawEffectivePrincipal,
587 sender: Principal,
588 method: &str,
589 payload: Vec<u8>,
590 ) -> Result<RawMessageId, ClientError> {
591 let endpoint = "update/submit_ingress_message";
592 let raw_canister_call = RawCanisterCall {
593 sender: sender.as_slice().to_vec(),
594 canister_id: canister_id.as_slice().to_vec(),
595 method: method.to_string(),
596 payload,
597 effective_principal,
598 sender_info: None,
599 };
600 self.post::<Result<RawMessageId, RejectResponse>, _>(endpoint, raw_canister_call)
601 .await?
602 .map_err(ClientError::from)
603 }
604
605 pub async fn await_call(&self, message_id: RawMessageId) -> Result<Vec<u8>, ClientError> {
607 let endpoint = "update/await_ingress_message";
608 let result: RawCanisterResult = self
609 .post(endpoint, &message_id)
610 .await
611 .map_err(|error| error.with_ingress(message_id))?;
612 Result::<Vec<u8>, RejectResponse>::from(result).map_err(ClientError::from)
613 }
614
615 pub async fn ingress_status(
619 &self,
620 raw_message_id: RawMessageId,
621 ) -> Result<Option<Result<Vec<u8>, RejectResponse>>, ClientError> {
622 let status = self.ingress_status_as_caller(raw_message_id, None).await?;
623 Ok(match status {
624 IngressStatusResult::NotAvailable => None,
625 IngressStatusResult::Success(status) => Some(status),
626 IngressStatusResult::Forbidden(err) => return Err(ClientError::Protocol(err)),
627 })
628 }
629
630 pub async fn ingress_status_as(
635 &self,
636 raw_message_id: RawMessageId,
637 caller: Principal,
638 ) -> Result<IngressStatusResult, ClientError> {
639 self.ingress_status_as_caller(raw_message_id, Some(caller))
640 .await
641 }
642
643 async fn ingress_status_as_caller(
644 &self,
645 raw_message_id: RawMessageId,
646 caller: Option<Principal>,
647 ) -> Result<IngressStatusResult, ClientError> {
648 let args = RawIngressStatusArgs {
649 raw_message_id: raw_message_id.clone(),
650 raw_caller: caller.map(Into::into),
651 };
652 match self
653 .post::<Option<RawCanisterResult>, _>("read/ingress_status", args)
654 .await
655 {
656 Ok(None) => Ok(IngressStatusResult::NotAvailable),
657 Ok(Some(result)) => Ok(IngressStatusResult::Success(result.into())),
658 Err(ClientError::Server {
659 status: StatusCode::FORBIDDEN,
660 message,
661 ..
662 }) => Ok(IngressStatusResult::Forbidden(message)),
663 Err(error) => Err(error.with_ingress(raw_message_id)),
664 }
665 }
666
667 pub async fn await_call_no_ticks(
671 &self,
672 message_id: RawMessageId,
673 ) -> Result<Vec<u8>, ClientError> {
674 let mut retry_policy = ExponentialBuilder::new()
675 .with_min_delay(Duration::from_millis(10))
676 .with_max_delay(Duration::from_secs(1))
677 .with_factor(2.0)
678 .with_jitter()
679 .with_total_delay(Some(Duration::from_secs(15 * 60)))
680 .without_max_times()
681 .build();
682 Ok(loop {
683 if let Some(ingress_status) = self.ingress_status(message_id.clone()).await? {
684 break ingress_status.map_err(ClientError::from)?;
685 }
686 let delay = retry_policy.next().ok_or_else(|| ClientError::Timeout {
687 context: Box::new(RequestContext {
688 url: self.instance_url().to_string(),
689 operation: None,
690 ingress: Some(message_id.clone()),
691 may_have_executed: true,
692 status: None,
693 }),
694 })?;
695 tokio::time::sleep(delay).await;
696 })
697 }
698
699 #[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()))]
701 pub async fn update_call(
702 &self,
703 canister_id: CanisterId,
704 sender: Principal,
705 method: &str,
706 payload: Vec<u8>,
707 ) -> Result<Vec<u8>, ClientError> {
708 self.update_call_with_effective_principal(
709 canister_id,
710 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
711 sender,
712 method,
713 payload,
714 )
715 .await
716 }
717
718 pub async fn update_call_with_effective_principal(
720 &self,
721 canister_id: CanisterId,
722 effective_principal: RawEffectivePrincipal,
723 sender: Principal,
724 method: &str,
725 payload: Vec<u8>,
726 ) -> Result<Vec<u8>, ClientError> {
727 let message_id = self
728 .submit_call_with_effective_principal(
729 canister_id,
730 effective_principal,
731 sender,
732 method,
733 payload,
734 )
735 .await?;
736 self.await_call(message_id).await
737 }
738
739 #[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()))]
741 pub async fn query_call(
742 &self,
743 canister_id: CanisterId,
744 sender: Principal,
745 method: &str,
746 payload: Vec<u8>,
747 ) -> Result<Vec<u8>, ClientError> {
748 self.query_call_with_effective_principal(
749 canister_id,
750 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
751 sender,
752 method,
753 payload,
754 )
755 .await
756 }
757
758 #[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()))]
762 pub async fn query_call_with_effective_principal(
763 &self,
764 canister_id: CanisterId,
765 effective_principal: RawEffectivePrincipal,
766 sender: Principal,
767 method: &str,
768 payload: Vec<u8>,
769 ) -> Result<Vec<u8>, ClientError> {
770 let endpoint = "read/query";
771 let raw_canister_call = RawCanisterCall {
772 sender: sender.as_slice().to_vec(),
773 canister_id: canister_id.as_slice().to_vec(),
774 method: method.to_string(),
775 payload,
776 effective_principal,
777 sender_info: None,
778 };
779 let result: RawCanisterResult = self.post(endpoint, raw_canister_call).await?;
780 Result::<Vec<u8>, RejectResponse>::from(result).map_err(ClientError::from)
781 }
782
783 pub async fn fetch_canister_logs(
785 &self,
786 canister_id: CanisterId,
787 sender: Principal,
788 ) -> Result<Vec<CanisterLogRecord>, ClientError> {
789 Ok(with_candid::<_, (FetchCanisterLogsResult,), _>(
790 (CanisterIdRecord { canister_id },),
791 |payload| async {
792 self.query_call_with_effective_principal(
793 Principal::management_canister(),
794 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
795 sender,
796 "fetch_canister_logs",
797 payload,
798 )
799 .await
800 },
801 )
802 .await?
803 .0
804 .canister_log_records)
805 }
806
807 #[instrument(skip(self), fields(instance_id=self.instance_id, sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
809 pub async fn canister_status(
810 &self,
811 canister_id: CanisterId,
812 sender: Option<Principal>,
813 ) -> Result<CanisterStatusResult, ClientError> {
814 Ok(
815 call_candid_as::<(CanisterIdRecord,), (CanisterStatusResult,)>(
816 self,
817 Principal::management_canister(),
818 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
819 sender.unwrap_or(Principal::anonymous()),
820 "canister_status",
821 (CanisterIdRecord { canister_id },),
822 )
823 .await?
824 .0,
825 )
826 }
827
828 #[instrument(ret, skip(self), fields(instance_id=self.instance_id))]
831 pub async fn create_canister(&self) -> Result<CanisterId, ClientError> {
832 self.create_canister_with_params(None, CreateCanisterParams::default())
833 .await
834 }
835
836 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, settings = ?settings, sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
839 pub async fn create_canister_with_settings(
840 &self,
841 sender: Option<Principal>,
842 settings: Option<CanisterSettings>,
843 ) -> Result<CanisterId, ClientError> {
844 self.create_canister_with_params(
845 sender,
846 CreateCanisterParams {
847 settings,
848 ..Default::default()
849 },
850 )
851 .await
852 }
853
854 #[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()))]
863 pub async fn create_canister_with_id(
864 &self,
865 sender: Option<Principal>,
866 settings: Option<CanisterSettings>,
867 canister_id: CanisterId,
868 ) -> Result<CanisterId, ClientError> {
869 self.create_canister_with_params(
870 sender,
871 CreateCanisterParams {
872 settings,
873 placement: Some(CreateCanisterPlacement::CanisterId(canister_id)),
874 ..Default::default()
875 },
876 )
877 .await
878 }
879
880 #[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()))]
883 pub async fn create_canister_on_subnet(
884 &self,
885 sender: Option<Principal>,
886 settings: Option<CanisterSettings>,
887 subnet_id: SubnetId,
888 ) -> Result<CanisterId, ClientError> {
889 self.create_canister_with_params(
890 sender,
891 CreateCanisterParams {
892 settings,
893 placement: Some(CreateCanisterPlacement::SubnetId(subnet_id)),
894 ..Default::default()
895 },
896 )
897 .await
898 }
899
900 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
905 pub async fn create_canister_with_params(
906 &self,
907 sender: Option<Principal>,
908 params: CreateCanisterParams,
909 ) -> Result<CanisterId, ClientError> {
910 let cycles = params.cycles.unwrap_or(100_000_000_000_000);
911 let (effective_principal, specified_id) = match params.placement {
912 None => (RawEffectivePrincipal::None, None),
913 Some(CreateCanisterPlacement::SubnetId(subnet_id)) => (
914 RawEffectivePrincipal::SubnetId(subnet_id.as_slice().to_vec()),
915 None,
916 ),
917 Some(CreateCanisterPlacement::CanisterId(canister_id)) => (
918 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
919 Some(canister_id),
920 ),
921 };
922 Ok(call_candid_as::<_, (CanisterIdRecord,)>(
923 self,
924 Principal::management_canister(),
925 effective_principal,
926 sender.unwrap_or(Principal::anonymous()),
927 "provisional_create_canister_with_cycles",
928 (ProvisionalCreateCanisterWithCyclesArgs {
929 settings: params.settings,
930 amount: Some(cycles.into()),
931 specified_id,
932 sender_canister_version: None,
933 },),
934 )
935 .await?
936 .0
937 .canister_id)
938 }
939
940 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
943 pub async fn upload_chunk(
944 &self,
945 canister_id: CanisterId,
946 sender: Option<Principal>,
947 chunk: Vec<u8>,
948 ) -> Result<Vec<u8>, ClientError> {
949 Ok(call_candid_as::<_, (UploadChunkResult,)>(
950 self,
951 Principal::management_canister(),
952 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
953 sender.unwrap_or(Principal::anonymous()),
954 "upload_chunk",
955 (UploadChunkArgs { canister_id, chunk },),
956 )
957 .await?
958 .0
959 .hash)
960 }
961
962 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
964 pub async fn stored_chunks(
965 &self,
966 canister_id: CanisterId,
967 sender: Option<Principal>,
968 ) -> Result<Vec<Vec<u8>>, ClientError> {
969 Ok(call_candid_as::<_, (StoredChunksResult,)>(
970 self,
971 Principal::management_canister(),
972 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
973 sender.unwrap_or(Principal::anonymous()),
974 "stored_chunks",
975 (CanisterIdRecord { canister_id },),
976 )
977 .await?
978 .0
979 .into_iter()
980 .map(|chunk| chunk.hash)
981 .collect())
982 }
983
984 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
986 pub async fn clear_chunk_store(
987 &self,
988 canister_id: CanisterId,
989 sender: Option<Principal>,
990 ) -> Result<(), ClientError> {
991 call_candid_as(
992 self,
993 Principal::management_canister(),
994 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
995 sender.unwrap_or(Principal::anonymous()),
996 "clear_chunk_store",
997 (CanisterIdRecord { canister_id },),
998 )
999 .await
1000 }
1001
1002 #[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()))]
1004 pub async fn install_chunked_canister(
1005 &self,
1006 canister_id: CanisterId,
1007 sender: Option<Principal>,
1008 mode: CanisterInstallMode,
1009 store_canister_id: CanisterId,
1010 chunk_hashes_list: Vec<Vec<u8>>,
1011 wasm_module_hash: Vec<u8>,
1012 arg: Vec<u8>,
1013 ) -> Result<(), ClientError> {
1014 call_candid_as(
1015 self,
1016 Principal::management_canister(),
1017 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1018 sender.unwrap_or(Principal::anonymous()),
1019 "install_chunked_code",
1020 (InstallChunkedCodeArgs {
1021 mode,
1022 target_canister: canister_id,
1023 store_canister: Some(store_canister_id),
1024 chunk_hashes_list: chunk_hashes_list
1025 .into_iter()
1026 .map(|hash| ChunkHash { hash })
1027 .collect(),
1028 wasm_module_hash,
1029 arg,
1030 sender_canister_version: None,
1031 },),
1032 )
1033 .await
1034 }
1035
1036 async fn install_canister_helper(
1037 &self,
1038 mode: CanisterInstallMode,
1039 canister_id: CanisterId,
1040 wasm_module: Vec<u8>,
1041 arg: Vec<u8>,
1042 sender: Option<Principal>,
1043 ) -> Result<(), ClientError> {
1044 if wasm_module.len() + arg.len() < INSTALL_CHUNKED_CODE_THRESHOLD {
1045 call_candid_as::<(InstallCodeArgs,), ()>(
1046 self,
1047 Principal::management_canister(),
1048 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1049 sender.unwrap_or(Principal::anonymous()),
1050 "install_code",
1051 (InstallCodeArgs {
1052 mode,
1053 canister_id,
1054 wasm_module,
1055 arg,
1056 sender_canister_version: None,
1057 },),
1058 )
1059 .await?
1060 } else {
1061 self.clear_chunk_store(canister_id, sender).await?;
1062 let chunks: Vec<_> = wasm_module.chunks(INSTALL_CODE_CHUNK_SIZE).collect();
1063 let mut hashes = vec![];
1064 for chunk in chunks {
1065 let hash = self
1066 .upload_chunk(canister_id, sender, chunk.to_vec())
1067 .await?;
1068 hashes.push(hash);
1069 }
1070 let mut hasher = Sha256::new();
1071 hasher.update(wasm_module);
1072 let wasm_module_hash = hasher.finalize().to_vec();
1073 self.install_chunked_canister(
1074 canister_id,
1075 sender,
1076 mode,
1077 canister_id,
1078 hashes,
1079 wasm_module_hash,
1080 arg,
1081 )
1082 .await?
1083 };
1084 Ok(())
1085 }
1086
1087 #[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()))]
1089 pub async fn install_canister(
1090 &self,
1091 canister_id: CanisterId,
1092 wasm_module: Vec<u8>,
1093 arg: Vec<u8>,
1094 sender: Option<Principal>,
1095 ) -> Result<(), ClientError> {
1096 self.install_canister_helper(
1097 CanisterInstallMode::Install,
1098 canister_id,
1099 wasm_module,
1100 arg,
1101 sender,
1102 )
1103 .await
1104 }
1105
1106 #[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()))]
1108 pub async fn upgrade_canister(
1109 &self,
1110 canister_id: CanisterId,
1111 wasm_module: Vec<u8>,
1112 arg: Vec<u8>,
1113 sender: Option<Principal>,
1114 ) -> Result<(), ClientError> {
1115 self.install_canister_helper(
1116 CanisterInstallMode::Upgrade(None),
1117 canister_id,
1118 wasm_module,
1119 arg,
1120 sender,
1121 )
1122 .await
1123 }
1124
1125 #[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()))]
1127 pub async fn reinstall_canister(
1128 &self,
1129 canister_id: CanisterId,
1130 wasm_module: Vec<u8>,
1131 arg: Vec<u8>,
1132 sender: Option<Principal>,
1133 ) -> Result<(), ClientError> {
1134 self.install_canister_helper(
1135 CanisterInstallMode::Reinstall,
1136 canister_id,
1137 wasm_module,
1138 arg,
1139 sender,
1140 )
1141 .await
1142 }
1143
1144 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1146 pub async fn uninstall_canister(
1147 &self,
1148 canister_id: CanisterId,
1149 sender: Option<Principal>,
1150 ) -> Result<(), ClientError> {
1151 call_candid_as::<(CanisterIdRecord,), ()>(
1152 self,
1153 Principal::management_canister(),
1154 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1155 sender.unwrap_or(Principal::anonymous()),
1156 "uninstall_code",
1157 (CanisterIdRecord { canister_id },),
1158 )
1159 .await
1160 }
1161
1162 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1164 pub async fn take_canister_snapshot(
1165 &self,
1166 canister_id: CanisterId,
1167 sender: Option<Principal>,
1168 replace_snapshot: Option<Vec<u8>>,
1169 ) -> Result<Snapshot, ClientError> {
1170 Ok(call_candid_as::<_, (Snapshot,)>(
1171 self,
1172 Principal::management_canister(),
1173 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1174 sender.unwrap_or(Principal::anonymous()),
1175 "take_canister_snapshot",
1176 (TakeCanisterSnapshotArgs {
1177 canister_id,
1178 replace_snapshot,
1179 uninstall_code: None,
1180 sender_canister_version: None,
1181 },),
1182 )
1183 .await?
1184 .0)
1185 }
1186
1187 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1189 pub async fn load_canister_snapshot(
1190 &self,
1191 canister_id: CanisterId,
1192 sender: Option<Principal>,
1193 snapshot_id: Vec<u8>,
1194 ) -> Result<(), ClientError> {
1195 call_candid_as(
1196 self,
1197 Principal::management_canister(),
1198 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1199 sender.unwrap_or(Principal::anonymous()),
1200 "load_canister_snapshot",
1201 (LoadCanisterSnapshotArgs {
1202 canister_id,
1203 snapshot_id,
1204 sender_canister_version: None,
1205 },),
1206 )
1207 .await
1208 }
1209
1210 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1212 pub async fn list_canister_snapshots(
1213 &self,
1214 canister_id: CanisterId,
1215 sender: Option<Principal>,
1216 ) -> Result<Vec<Snapshot>, ClientError> {
1217 Ok(call_candid_as::<_, (Vec<Snapshot>,)>(
1218 self,
1219 Principal::management_canister(),
1220 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1221 sender.unwrap_or(Principal::anonymous()),
1222 "list_canister_snapshots",
1223 (CanisterIdRecord { canister_id },),
1224 )
1225 .await?
1226 .0)
1227 }
1228
1229 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1231 pub async fn delete_canister_snapshot(
1232 &self,
1233 canister_id: CanisterId,
1234 sender: Option<Principal>,
1235 snapshot_id: Vec<u8>,
1236 ) -> Result<(), ClientError> {
1237 call_candid_as(
1238 self,
1239 Principal::management_canister(),
1240 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1241 sender.unwrap_or(Principal::anonymous()),
1242 "delete_canister_snapshot",
1243 (DeleteCanisterSnapshotArgs {
1244 canister_id,
1245 snapshot_id,
1246 },),
1247 )
1248 .await
1249 }
1250
1251 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1253 pub async fn update_canister_settings(
1254 &self,
1255 canister_id: CanisterId,
1256 sender: Option<Principal>,
1257 settings: CanisterSettings,
1258 ) -> Result<(), ClientError> {
1259 call_candid_as::<_, ()>(
1260 self,
1261 Principal::management_canister(),
1262 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1263 sender.unwrap_or(Principal::anonymous()),
1264 "update_settings",
1265 (UpdateSettingsArgs {
1266 canister_id,
1267 settings,
1268 sender_canister_version: None,
1269 },),
1270 )
1271 .await
1272 }
1273
1274 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1276 pub async fn set_controllers(
1277 &self,
1278 canister_id: CanisterId,
1279 sender: Option<Principal>,
1280 new_controllers: Vec<Principal>,
1281 ) -> Result<(), ClientError> {
1282 let settings = CanisterSettings {
1283 controllers: Some(new_controllers),
1284 ..CanisterSettings::default()
1285 };
1286 call_candid_as::<(UpdateSettingsArgs,), ()>(
1287 self,
1288 Principal::management_canister(),
1289 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1290 sender.unwrap_or(Principal::anonymous()),
1291 "update_settings",
1292 (UpdateSettingsArgs {
1293 canister_id,
1294 settings,
1295 sender_canister_version: None,
1296 },),
1297 )
1298 .await
1299 }
1300
1301 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1303 pub async fn start_canister(
1304 &self,
1305 canister_id: CanisterId,
1306 sender: Option<Principal>,
1307 ) -> Result<(), ClientError> {
1308 call_candid_as::<(CanisterIdRecord,), ()>(
1309 self,
1310 Principal::management_canister(),
1311 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1312 sender.unwrap_or(Principal::anonymous()),
1313 "start_canister",
1314 (CanisterIdRecord { canister_id },),
1315 )
1316 .await
1317 }
1318
1319 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1321 pub async fn stop_canister(
1322 &self,
1323 canister_id: CanisterId,
1324 sender: Option<Principal>,
1325 ) -> Result<(), ClientError> {
1326 call_candid_as::<(CanisterIdRecord,), ()>(
1327 self,
1328 Principal::management_canister(),
1329 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1330 sender.unwrap_or(Principal::anonymous()),
1331 "stop_canister",
1332 (CanisterIdRecord { canister_id },),
1333 )
1334 .await
1335 }
1336
1337 #[instrument(skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string(), sender = %sender.unwrap_or(Principal::anonymous()).to_string()))]
1339 pub async fn delete_canister(
1340 &self,
1341 canister_id: CanisterId,
1342 sender: Option<Principal>,
1343 ) -> Result<(), ClientError> {
1344 call_candid_as::<(CanisterIdRecord,), ()>(
1345 self,
1346 Principal::management_canister(),
1347 RawEffectivePrincipal::CanisterId(canister_id.as_slice().to_vec()),
1348 sender.unwrap_or(Principal::anonymous()),
1349 "delete_canister",
1350 (CanisterIdRecord { canister_id },),
1351 )
1352 .await
1353 }
1354
1355 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))]
1357 pub async fn canister_exists(&self, canister_id: CanisterId) -> Result<bool, ClientError> {
1358 Ok(self.get_subnet(canister_id).await?.is_some())
1359 }
1360
1361 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, subnet_id = %subnet_id.to_string()))]
1363 pub async fn delete_subnet(&self, subnet_id: SubnetId) -> Result<(), ClientError> {
1364 let endpoint = "update/delete_subnet";
1365 self.post::<(), RawSubnetId>(
1366 endpoint,
1367 RawSubnetId {
1368 subnet_id: subnet_id.as_slice().to_vec(),
1369 },
1370 )
1371 .await?;
1372
1373 Ok(())
1374 }
1375
1376 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))]
1378 pub async fn get_subnet(
1379 &self,
1380 canister_id: CanisterId,
1381 ) -> Result<Option<SubnetId>, ClientError> {
1382 let endpoint = "read/get_subnet";
1383 let result: Option<RawSubnetId> = self
1384 .post(
1385 endpoint,
1386 RawCanisterId {
1387 canister_id: canister_id.as_slice().to_vec(),
1388 },
1389 )
1390 .await?;
1391 Ok(result.map(|RawSubnetId { subnet_id }| SubnetId::from_slice(&subnet_id)))
1392 }
1393
1394 #[instrument(ret, skip(self), fields(instance_id=self.instance_id, subnet_id = %subnet_id.to_string()))]
1396 pub async fn get_subnet_metrics(
1397 &self,
1398 subnet_id: Principal,
1399 ) -> Result<Option<SubnetMetrics>, ClientError> {
1400 let path = vec![
1401 "subnet".into(),
1402 Label::from_bytes(subnet_id.as_slice()),
1403 "metrics".into(),
1404 ];
1405 let paths = vec![path.clone()];
1406 let content = ReadState {
1407 ingress_expiry: self.get_time().await?.as_nanos_since_unix_epoch() + 240_000_000_000,
1408 sender: Principal::anonymous(),
1409 paths,
1410 };
1411 let envelope = Envelope {
1412 content: std::borrow::Cow::Borrowed(&content),
1413 sender_pubkey: None,
1414 sender_sig: None,
1415 sender_delegation: None,
1416 };
1417
1418 let serialized_bytes = encode_envelope(&envelope, usize::MAX)?;
1419
1420 let endpoint = format!("api/v2/subnet/{}/read_state", subnet_id.to_text());
1421 let url = self.instance_url().join(&endpoint).unwrap();
1422 let mut exchange = Exchange::new(&url, self.max_request_time_ms);
1423 let (status, bytes) = exchange
1424 .bytes(
1425 &self.reqwest_client,
1426 self.reqwest_client
1427 .post(url.clone())
1428 .header(reqwest::header::CONTENT_TYPE, "application/cbor")
1429 .body(serialized_bytes),
1430 )
1431 .await?;
1432 if !status.is_success() {
1433 return Err(exchange.server_error(status, &bytes));
1434 }
1435 let read_state_response: ReadStateResponse =
1436 decode_read_state_response(&bytes, bytes.len())?;
1437 let cert: Certificate = decode_certificate(
1438 &read_state_response.certificate,
1439 read_state_response.certificate.len(),
1440 )?;
1441
1442 let metrics = match cert.tree.lookup_path(path) {
1443 LookupResult::Found(value) => value,
1444 _ => return Ok(None),
1445 };
1446 decode_subnet_metrics(metrics, metrics.len()).map_err(ClientError::from)
1447 }
1448
1449 pub async fn drop(mut self) -> Result<(), ClientError> {
1452 self.do_drop().await?;
1453
1454 Ok(())
1455 }
1456
1457 pub(crate) async fn do_drop(&mut self) -> Result<(), ClientError> {
1458 if self.owns_instance {
1459 let url = self.instance_url();
1460 let mut exchange = Exchange::new(&url, self.max_request_time_ms);
1461 let (status, bytes) = exchange
1462 .bytes(
1463 &self.reqwest_client,
1464 self.reqwest_client.delete(url.clone()),
1465 )
1466 .await?;
1467 if !status.is_success() {
1468 return Err(exchange.server_error(status, &bytes));
1469 }
1470 self.owns_instance = false;
1471 Ok(())
1472 } else {
1473 self.stop_http_gateway().await
1474 }
1475 }
1476
1477 async fn get<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, ClientError> {
1478 self.request(HttpMethod::Get, endpoint, ()).await
1479 }
1480
1481 async fn post<T: DeserializeOwned, B: Serialize>(
1482 &self,
1483 endpoint: &str,
1484 body: B,
1485 ) -> Result<T, ClientError> {
1486 self.request(HttpMethod::Post, endpoint, body).await
1487 }
1488
1489 async fn request<T: DeserializeOwned, B: Serialize>(
1490 &self,
1491 method: HttpMethod,
1492 endpoint: &str,
1493 body: B,
1494 ) -> Result<T, ClientError> {
1495 self.request_limited(method, endpoint, body, None).await
1496 }
1497
1498 async fn request_limited<T: DeserializeOwned, B: Serialize>(
1499 &self,
1500 method: HttpMethod,
1501 endpoint: &str,
1502 body: B,
1503 response_limit: Option<usize>,
1504 ) -> Result<T, ClientError> {
1505 let url = self.instance_url().join(endpoint).unwrap();
1506 let mut exchange = Exchange::new(&url, self.max_request_time_ms);
1507 exchange.response_limit = response_limit;
1508 loop {
1509 let builder = if let Some(operation) = &exchange.context.operation {
1510 self.reqwest_client.get(
1511 self.server_url
1512 .join(&format!(
1513 "/read_graph/{}/{}",
1514 operation.state_label, operation.op_id
1515 ))
1516 .unwrap(),
1517 )
1518 } else {
1519 match method {
1520 HttpMethod::Get => self.reqwest_client.get(url.clone()),
1521 HttpMethod::Post => self.reqwest_client.post(url.clone()).json(&body),
1522 }
1523 };
1524 let (status, bytes) = exchange.bytes(&self.reqwest_client, builder).await?;
1525 match status {
1526 StatusCode::OK => {
1527 let value = exchange.decode(&bytes)?;
1528 if let Some(operation) = &exchange.context.operation {
1529 let request = self.reqwest_client.delete(
1531 self.server_url
1532 .join(&format!(
1533 "/prune_graph/{}/{}",
1534 operation.state_label, operation.op_id
1535 ))
1536 .unwrap(),
1537 );
1538 tokio::spawn(async move {
1539 let _ = request.send().await;
1540 });
1541 }
1542 return Ok(value);
1543 }
1544 StatusCode::ACCEPTED | StatusCode::CONFLICT => {
1545 let receipt: crate::pocket_ic::common::rest::StartedOrBusyResponse =
1546 exchange.decode(&bytes)?;
1547 if exchange.context.operation.is_none() {
1548 if status == StatusCode::ACCEPTED {
1549 exchange.started(Operation {
1550 state_label: receipt.state_label,
1551 op_id: receipt.op_id,
1552 });
1553 } else {
1554 exchange.context.may_have_executed = false;
1556 }
1557 }
1558 }
1561 StatusCode::NOT_FOUND if exchange.context.operation.is_some() => {}
1562 _ => return Err(exchange.server_error(status, &bytes)),
1563 }
1564 exchange.pause().await?;
1565 }
1566 }
1567
1568 #[instrument(ret, skip(self), fields(instance_id=self.instance_id))]
1581 pub async fn get_canister_http(&self) -> Result<Vec<CanisterHttpRequest>, ClientError> {
1582 let endpoint = "read/get_canister_http";
1583 let res: Vec<RawCanisterHttpRequest> = self.get(endpoint).await?;
1584 Ok(res.into_iter().map(|r| r.into()).collect())
1585 }
1586
1587 #[instrument(ret, skip(self), fields(instance_id=self.instance_id))]
1591 pub async fn mock_canister_http_response(
1592 &self,
1593 mock_canister_http_response: MockCanisterHttpResponse,
1594 ) -> Result<(), ClientError> {
1595 let endpoint = "update/mock_canister_http";
1596 let raw_mock_canister_http_response: RawMockCanisterHttpResponse =
1597 mock_canister_http_response.into();
1598 self.post(endpoint, raw_mock_canister_http_response).await
1599 }
1600
1601 #[instrument(ret, skip(self), fields(instance_id=self.instance_id))]
1605 pub async fn canister_snapshot_download(
1606 &self,
1607 canister_id: CanisterId,
1608 sender: Principal,
1609 snapshot_id: Vec<u8>,
1610 snapshot_dir: PathBuf,
1611 ) -> Result<(), ClientError> {
1612 let endpoint = "update/canister_snapshot_download";
1613 #[cfg(not(windows))]
1614 let snapshot_dir = snapshot_dir;
1615 #[cfg(windows)]
1616 let snapshot_dir = wsl_path(&snapshot_dir, "snapshot directory").into();
1617 let raw_canister_snapshot_download = RawCanisterSnapshotDownload {
1618 sender: sender.into(),
1619 canister_id: canister_id.into(),
1620 snapshot_id,
1621 snapshot_dir,
1622 };
1623 self.post(endpoint, raw_canister_snapshot_download).await
1624 }
1625
1626 #[instrument(ret, skip(self), fields(instance_id=self.instance_id))]
1630 pub async fn canister_snapshot_upload(
1631 &self,
1632 canister_id: CanisterId,
1633 sender: Principal,
1634 replace_snapshot: Option<Vec<u8>>,
1635 snapshot_dir: PathBuf,
1636 ) -> Result<Vec<u8>, ClientError> {
1637 let endpoint = "update/canister_snapshot_upload";
1638 let replace_snapshot =
1639 replace_snapshot.map(|snapshot_id| RawCanisterSnapshotId { snapshot_id });
1640 #[cfg(not(windows))]
1641 let snapshot_dir = snapshot_dir;
1642 #[cfg(windows)]
1643 let snapshot_dir = wsl_path(&snapshot_dir, "snapshot directory").into();
1644 let raw_canister_snapshot_upload = RawCanisterSnapshotUpload {
1645 sender: sender.into(),
1646 canister_id: canister_id.into(),
1647 replace_snapshot,
1648 snapshot_dir,
1649 };
1650 Ok(self
1651 .post::<RawCanisterSnapshotId, _>(endpoint, raw_canister_snapshot_upload)
1652 .await?
1653 .snapshot_id)
1654 }
1655}
1656
1657pub async fn call_candid_as<Input, Output>(
1661 env: &PocketIc,
1662 canister_id: CanisterId,
1663 effective_principal: RawEffectivePrincipal,
1664 sender: Principal,
1665 method: &str,
1666 input: Input,
1667) -> Result<Output, ClientError>
1668where
1669 Input: ArgumentEncoder,
1670 Output: for<'a> ArgumentDecoder<'a>,
1671{
1672 with_candid(input, |payload| async {
1673 env.update_call_with_effective_principal(
1674 canister_id,
1675 effective_principal,
1676 sender,
1677 method,
1678 payload,
1679 )
1680 .await
1681 })
1682 .await
1683}
1684
1685pub async fn call_candid<Input, Output>(
1688 env: &PocketIc,
1689 canister_id: CanisterId,
1690 effective_principal: RawEffectivePrincipal,
1691 method: &str,
1692 input: Input,
1693) -> Result<Output, ClientError>
1694where
1695 Input: ArgumentEncoder,
1696 Output: for<'a> ArgumentDecoder<'a>,
1697{
1698 call_candid_as(
1699 env,
1700 canister_id,
1701 effective_principal,
1702 Principal::anonymous(),
1703 method,
1704 input,
1705 )
1706 .await
1707}
1708
1709pub async fn query_candid<Input, Output>(
1711 env: &PocketIc,
1712 canister_id: CanisterId,
1713 method: &str,
1714 input: Input,
1715) -> Result<Output, ClientError>
1716where
1717 Input: ArgumentEncoder,
1718 Output: for<'a> ArgumentDecoder<'a>,
1719{
1720 query_candid_as(env, canister_id, Principal::anonymous(), method, input).await
1721}
1722
1723pub async fn query_candid_as<Input, Output>(
1726 env: &PocketIc,
1727 canister_id: CanisterId,
1728 sender: Principal,
1729 method: &str,
1730 input: Input,
1731) -> Result<Output, ClientError>
1732where
1733 Input: ArgumentEncoder,
1734 Output: for<'a> ArgumentDecoder<'a>,
1735{
1736 with_candid(input, |bytes| async {
1737 env.query_call(canister_id, sender, method, bytes).await
1738 })
1739 .await
1740}
1741
1742pub async fn update_candid<Input, Output>(
1744 env: &PocketIc,
1745 canister_id: CanisterId,
1746 method: &str,
1747 input: Input,
1748) -> Result<Output, ClientError>
1749where
1750 Input: ArgumentEncoder,
1751 Output: for<'a> ArgumentDecoder<'a>,
1752{
1753 update_candid_as(env, canister_id, Principal::anonymous(), method, input).await
1754}
1755
1756pub async fn update_candid_as<Input, Output>(
1759 env: &PocketIc,
1760 canister_id: CanisterId,
1761 sender: Principal,
1762 method: &str,
1763 input: Input,
1764) -> Result<Output, ClientError>
1765where
1766 Input: ArgumentEncoder,
1767 Output: for<'a> ArgumentDecoder<'a>,
1768{
1769 with_candid(input, |bytes| async {
1770 env.update_call(canister_id, sender, method, bytes).await
1771 })
1772 .await
1773}
1774
1775pub async fn with_candid<Input, Output, Fut>(
1778 input: Input,
1779 f: impl FnOnce(Vec<u8>) -> Fut,
1780) -> Result<Output, ClientError>
1781where
1782 Input: ArgumentEncoder,
1783 Output: for<'a> ArgumentDecoder<'a>,
1784 Fut: Future<Output = Result<Vec<u8>, ClientError>>,
1785{
1786 let input = encode_args(input)?;
1787 let output = f(input).await?;
1788 Ok(decode_args(&output)?)
1789}
1790
1791pub async fn get_default_effective_canister_id(
1799 pocket_ic_url: String,
1800) -> Result<Principal, ClientError> {
1801 let url = Url::parse(&format!(
1802 "{}/_/topology",
1803 pocket_ic_url.trim_end_matches('/')
1804 ))
1805 .map_err(|e| ClientError::Protocol(e.to_string()))?;
1806 let client = reqwest::Client::new();
1807 let mut exchange = Exchange::new(&url, Some(crate::pocket_ic::DEFAULT_MAX_REQUEST_TIME_MS));
1808 loop {
1809 let (status, bytes) = exchange.bytes(&client, client.get(url.clone())).await?;
1810 if status == StatusCode::CONFLICT {
1811 exchange.pause().await?;
1812 continue;
1813 }
1814 if !status.is_success() {
1815 return Err(exchange.server_error(status, &bytes));
1816 }
1817 let topology: Topology = exchange.decode(&bytes)?;
1818 return Principal::try_from_slice(&topology.default_effective_canister_id.canister_id)
1819 .map_err(|e| ClientError::Protocol(e.to_string()));
1820 }
1821}