1#![cfg(not(target_family = "wasm"))]
2#![expect(
3 missing_docs,
4 clippy::too_many_arguments,
5 clippy::pedantic,
6 clippy::nursery,
7 reason = "initial upstream source transfer preserves public signatures and protocol names; see upstream/NOTICE.md"
8)]
9use self::common::rest::{
15 AutoProgressConfig, IcpFeatures, InitialTime, InstanceConfig, InstanceHttpGatewayConfig,
16 RawSubnetBlockmakers, RawTickConfigs, RawTime, SubnetId,
17};
18use candid::Principal;
19pub use ic_management_canister_types::{
20 CanisterId, CanisterInstallMode, CanisterLogRecord, CanisterSettings, CanisterStatusResult,
21 EnvironmentVariable, Snapshot,
22};
23pub use ic_platform::ic_protocol::SubnetMetrics;
24use reqwest::Url;
25use serde::{Deserialize, Serialize};
26use std::{
27 path::PathBuf,
28 time::{Duration, SystemTime, UNIX_EPOCH},
29};
30
31mod error;
32mod http;
33pub use error::{ClientError, Operation, RequestContext};
34pub mod common;
35mod nonblocking;
36pub use nonblocking::{
37 PocketIc, call_candid, call_candid_as, get_default_effective_canister_id, query_candid,
38 query_candid_as, update_candid, update_candid_as, with_candid,
39};
40pub mod snapshot;
41
42pub const LATEST_SERVER_VERSION: &str = "16.1.0";
44
45const DEFAULT_MAX_REQUEST_TIME_MS: u64 = 300_000;
47
48pub struct PocketIcBuilder {
51 config: InstanceConfig,
52 server_url: Option<Url>,
53 max_request_time_ms: Option<u64>,
54}
55
56#[expect(
57 clippy::new_without_default,
58 reason = "preserve the imported PocketIC contract without an unrelated API change"
59)]
60impl PocketIcBuilder {
61 pub fn new() -> Self {
62 Self {
63 config: InstanceConfig {
64 icp_features: Some(IcpFeatures::default()),
65 ..InstanceConfig::default()
66 },
67 server_url: None,
68 max_request_time_ms: Some(DEFAULT_MAX_REQUEST_TIME_MS),
69 }
70 }
71
72 pub async fn build(self) -> Result<PocketIc, ClientError> {
73 PocketIc::from_config(
74 self.server_url.ok_or(ClientError::MissingServerUrl)?,
75 self.max_request_time_ms,
76 self.config,
77 )
78 .await
79 }
80
81 pub fn with_server_url(mut self, server_url: Url) -> Self {
83 self.server_url = Some(server_url);
84 self
85 }
86
87 pub fn with_max_request_time_ms(mut self, max_request_time_ms: Option<u64>) -> Self {
88 self.max_request_time_ms = max_request_time_ms;
89 self
90 }
91
92 pub fn with_state_dir(mut self, state_dir: PathBuf) -> Self {
95 self.config.state_dir = Some(state_dir);
96 self
97 }
98
99 pub fn with_nns_subnet(mut self) -> Self {
101 self.config.subnet_config_set.nns.get_or_insert_default();
102 self
103 }
104
105 pub fn with_ii_subnet(mut self) -> Self {
107 self.config.subnet_config_set.ii.get_or_insert_default();
108 self
109 }
110
111 pub fn with_system_subnet(mut self) -> Self {
112 self.config
113 .subnet_config_set
114 .system
115 .push(Default::default());
116 self
117 }
118
119 pub fn with_application_subnet(mut self) -> Self {
120 self.config
121 .subnet_config_set
122 .application
123 .push(Default::default());
124 self
125 }
126
127 pub fn with_icp_features(mut self, icp_features: IcpFeatures) -> Self {
129 self.config.icp_features = Some(icp_features);
130 self
131 }
132
133 pub fn with_initial_time(mut self, initial_time: Time) -> Self {
135 self.config.initial_time = Some(InitialTime::Timestamp(RawTime {
136 nanos_since_epoch: initial_time.as_nanos_since_unix_epoch(),
137 }));
138 self
139 }
140
141 pub fn with_auto_progress(mut self) -> Self {
143 self.config.initial_time = Some(InitialTime::AutoProgress(AutoProgressConfig {
144 artificial_delay_ms: None,
145 }));
146 self
147 }
148
149 pub fn with_http_gateway(mut self, http_gateway_config: InstanceHttpGatewayConfig) -> Self {
150 self.config.http_gateway_config = Some(http_gateway_config);
151 self
152 }
153}
154
155#[derive(Copy, Clone, PartialEq, PartialOrd)]
158pub struct Time(Duration);
159
160impl Time {
161 pub fn as_nanos_since_unix_epoch(&self) -> u64 {
163 self.0.as_nanos().try_into().unwrap()
164 }
165
166 pub const fn from_nanos_since_unix_epoch(nanos: u64) -> Self {
167 Time(Duration::from_nanos(nanos))
168 }
169}
170
171impl std::fmt::Debug for Time {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 let nanos_since_unix_epoch = self.as_nanos_since_unix_epoch();
174 write!(f, "{nanos_since_unix_epoch}")
175 }
176}
177
178impl std::ops::Add<Duration> for Time {
179 type Output = Time;
180 fn add(self, dur: Duration) -> Time {
181 Time(self.0 + dur)
182 }
183}
184
185impl From<SystemTime> for Time {
186 fn from(time: SystemTime) -> Self {
187 Self::from_nanos_since_unix_epoch(
188 time.duration_since(UNIX_EPOCH)
189 .unwrap()
190 .as_nanos()
191 .try_into()
192 .unwrap(),
193 )
194 }
195}
196
197impl TryFrom<Time> for SystemTime {
198 type Error = String;
199
200 fn try_from(time: Time) -> Result<SystemTime, String> {
201 let nanos = time.as_nanos_since_unix_epoch();
202 let system_time = UNIX_EPOCH + Duration::from_nanos(nanos);
203 let roundtrip: Time = system_time.into();
204 if roundtrip.as_nanos_since_unix_epoch() == nanos {
205 Ok(system_time)
206 } else {
207 Err(format!(
208 "Converting UNIX timestamp {nanos} in nanoseconds to SystemTime failed due to losing precision"
209 ))
210 }
211 }
212}
213
214#[derive(Clone, Debug)]
216pub enum CreateCanisterPlacement {
217 SubnetId(SubnetId),
219 CanisterId(CanisterId),
221}
222
223#[derive(Clone, Debug, Default)]
225pub struct CreateCanisterParams {
226 pub cycles: Option<u128>,
228 pub settings: Option<CanisterSettings>,
230 pub placement: Option<CreateCanisterPlacement>,
232}
233
234#[derive(Clone, Copy, Debug)]
236pub enum TryFromError {
237 ValueOutOfRange(u64),
238}
239
240#[derive(PartialOrd, Ord, Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
247#[cfg_attr(test, derive(strum::EnumIter))]
248pub enum ErrorCode {
249 SubnetOversubscribed = 101,
251 MaxNumberOfCanistersReached = 102,
252 CanisterQueueFull = 201,
254 IngressMessageTimeout = 202,
255 CanisterQueueNotEmpty = 203,
256 IngressHistoryFull = 204,
257 CanisterIdAlreadyExists = 205,
258 StopCanisterRequestTimeout = 206,
259 CanisterOutOfCycles = 207,
260 CertifiedStateUnavailable = 208,
261 CanisterInstallCodeRateLimited = 209,
262 CanisterHeapDeltaRateLimited = 210,
263 SubnetCoolingDown = 211,
264 CanisterNotFound = 301,
266 CanisterSnapshotNotFound = 305,
267 InsufficientMemoryAllocation = 402,
269 InsufficientCyclesForCreateCanister = 403,
270 SubnetNotFound = 404,
271 CanisterNotHostedBySubnet = 405,
272 CanisterRejectedMessage = 406,
273 UnknownManagementMessage = 407,
274 InvalidManagementPayload = 408,
275 CanisterSnapshotImmutable = 409,
276 InvalidSubnetAdmin = 410,
277 CanisterTrapped = 502,
279 CanisterCalledTrap = 503,
280 CanisterContractViolation = 504,
281 CanisterInvalidWasm = 505,
282 CanisterDidNotReply = 506,
283 CanisterOutOfMemory = 507,
284 CanisterStopped = 508,
285 CanisterStopping = 509,
286 CanisterNotStopped = 510,
287 CanisterStoppingCancelled = 511,
288 CanisterInvalidController = 512,
289 CanisterFunctionNotFound = 513,
290 CanisterNonEmpty = 514,
291 QueryCallGraphLoopDetected = 517,
292 InsufficientCyclesInCall = 520,
293 CanisterWasmEngineError = 521,
294 CanisterInstructionLimitExceeded = 522,
295 CanisterMemoryAccessLimitExceeded = 524,
296 QueryCallGraphTooDeep = 525,
297 QueryCallGraphTotalInstructionLimitExceeded = 526,
298 CompositeQueryCalledInReplicatedMode = 527,
299 QueryTimeLimitExceeded = 528,
300 QueryCallGraphInternal = 529,
301 InsufficientCyclesInComputeAllocation = 530,
302 InsufficientCyclesInMemoryAllocation = 531,
303 InsufficientCyclesInMemoryGrow = 532,
304 ReservedCyclesLimitExceededInMemoryAllocation = 533,
305 ReservedCyclesLimitExceededInMemoryGrow = 534,
306 InsufficientCyclesInMessageMemoryGrow = 535,
307 CanisterMethodNotFound = 536,
308 CanisterWasmModuleNotFound = 537,
309 CanisterAlreadyInstalled = 538,
310 CanisterWasmMemoryLimitExceeded = 539,
311 ReservedCyclesLimitIsTooLow = 540,
312 CanisterInvalidControllerOrSubnetAdmin = 541,
313 CanisterStatusAccessDenied = 542,
314 DeadlineExpired = 601,
316 ResponseDropped = 602,
317}
318
319impl TryFrom<u64> for ErrorCode {
320 type Error = TryFromError;
321 fn try_from(err: u64) -> Result<ErrorCode, Self::Error> {
322 match err {
323 101 => Ok(ErrorCode::SubnetOversubscribed),
325 102 => Ok(ErrorCode::MaxNumberOfCanistersReached),
326 201 => Ok(ErrorCode::CanisterQueueFull),
328 202 => Ok(ErrorCode::IngressMessageTimeout),
329 203 => Ok(ErrorCode::CanisterQueueNotEmpty),
330 204 => Ok(ErrorCode::IngressHistoryFull),
331 205 => Ok(ErrorCode::CanisterIdAlreadyExists),
332 206 => Ok(ErrorCode::StopCanisterRequestTimeout),
333 207 => Ok(ErrorCode::CanisterOutOfCycles),
334 208 => Ok(ErrorCode::CertifiedStateUnavailable),
335 209 => Ok(ErrorCode::CanisterInstallCodeRateLimited),
336 210 => Ok(ErrorCode::CanisterHeapDeltaRateLimited),
337 211 => Ok(ErrorCode::SubnetCoolingDown),
338 301 => Ok(ErrorCode::CanisterNotFound),
340 305 => Ok(ErrorCode::CanisterSnapshotNotFound),
341 402 => Ok(ErrorCode::InsufficientMemoryAllocation),
343 403 => Ok(ErrorCode::InsufficientCyclesForCreateCanister),
344 404 => Ok(ErrorCode::SubnetNotFound),
345 405 => Ok(ErrorCode::CanisterNotHostedBySubnet),
346 406 => Ok(ErrorCode::CanisterRejectedMessage),
347 407 => Ok(ErrorCode::UnknownManagementMessage),
348 408 => Ok(ErrorCode::InvalidManagementPayload),
349 409 => Ok(ErrorCode::CanisterSnapshotImmutable),
350 410 => Ok(ErrorCode::InvalidSubnetAdmin),
351 502 => Ok(ErrorCode::CanisterTrapped),
353 503 => Ok(ErrorCode::CanisterCalledTrap),
354 504 => Ok(ErrorCode::CanisterContractViolation),
355 505 => Ok(ErrorCode::CanisterInvalidWasm),
356 506 => Ok(ErrorCode::CanisterDidNotReply),
357 507 => Ok(ErrorCode::CanisterOutOfMemory),
358 508 => Ok(ErrorCode::CanisterStopped),
359 509 => Ok(ErrorCode::CanisterStopping),
360 510 => Ok(ErrorCode::CanisterNotStopped),
361 511 => Ok(ErrorCode::CanisterStoppingCancelled),
362 512 => Ok(ErrorCode::CanisterInvalidController),
363 513 => Ok(ErrorCode::CanisterFunctionNotFound),
364 514 => Ok(ErrorCode::CanisterNonEmpty),
365 517 => Ok(ErrorCode::QueryCallGraphLoopDetected),
366 520 => Ok(ErrorCode::InsufficientCyclesInCall),
367 521 => Ok(ErrorCode::CanisterWasmEngineError),
368 522 => Ok(ErrorCode::CanisterInstructionLimitExceeded),
369 524 => Ok(ErrorCode::CanisterMemoryAccessLimitExceeded),
370 525 => Ok(ErrorCode::QueryCallGraphTooDeep),
371 526 => Ok(ErrorCode::QueryCallGraphTotalInstructionLimitExceeded),
372 527 => Ok(ErrorCode::CompositeQueryCalledInReplicatedMode),
373 528 => Ok(ErrorCode::QueryTimeLimitExceeded),
374 529 => Ok(ErrorCode::QueryCallGraphInternal),
375 530 => Ok(ErrorCode::InsufficientCyclesInComputeAllocation),
376 531 => Ok(ErrorCode::InsufficientCyclesInMemoryAllocation),
377 532 => Ok(ErrorCode::InsufficientCyclesInMemoryGrow),
378 533 => Ok(ErrorCode::ReservedCyclesLimitExceededInMemoryAllocation),
379 534 => Ok(ErrorCode::ReservedCyclesLimitExceededInMemoryGrow),
380 535 => Ok(ErrorCode::InsufficientCyclesInMessageMemoryGrow),
381 536 => Ok(ErrorCode::CanisterMethodNotFound),
382 537 => Ok(ErrorCode::CanisterWasmModuleNotFound),
383 538 => Ok(ErrorCode::CanisterAlreadyInstalled),
384 539 => Ok(ErrorCode::CanisterWasmMemoryLimitExceeded),
385 540 => Ok(ErrorCode::ReservedCyclesLimitIsTooLow),
386 541 => Ok(ErrorCode::CanisterInvalidControllerOrSubnetAdmin),
387 542 => Ok(ErrorCode::CanisterStatusAccessDenied),
388 601 => Ok(ErrorCode::DeadlineExpired),
390 602 => Ok(ErrorCode::ResponseDropped),
391 _ => Err(TryFromError::ValueOutOfRange(err)),
392 }
393 }
394}
395
396impl std::fmt::Display for ErrorCode {
397 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398 write!(f, "IC{:04}", *self as i32)
400 }
401}
402
403#[derive(PartialOrd, Ord, Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
408#[cfg_attr(test, derive(strum::EnumIter))]
409pub enum RejectCode {
410 SysFatal = 1,
411 SysTransient = 2,
412 DestinationInvalid = 3,
413 CanisterReject = 4,
414 CanisterError = 5,
415 SysUnknown = 6,
416}
417
418impl TryFrom<u64> for RejectCode {
419 type Error = TryFromError;
420 fn try_from(err: u64) -> Result<RejectCode, Self::Error> {
421 match err {
422 1 => Ok(RejectCode::SysFatal),
423 2 => Ok(RejectCode::SysTransient),
424 3 => Ok(RejectCode::DestinationInvalid),
425 4 => Ok(RejectCode::CanisterReject),
426 5 => Ok(RejectCode::CanisterError),
427 6 => Ok(RejectCode::SysUnknown),
428 _ => Err(TryFromError::ValueOutOfRange(err)),
429 }
430 }
431}
432
433#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
435pub struct RejectResponse {
436 pub reject_code: RejectCode,
437 pub reject_message: String,
438 pub error_code: ErrorCode,
439 pub certified: bool,
440}
441
442impl std::fmt::Display for RejectResponse {
443 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444 write!(
446 f,
447 "PocketIC returned a rejection error: reject code {:?}, reject message {}, error code {:?}",
448 self.reject_code, self.reject_message, self.error_code
449 )
450 }
451}
452
453#[derive(Debug, Serialize, Deserialize)]
459pub enum IngressStatusResult {
460 NotAvailable,
461 Forbidden(String),
462 Success(Result<Vec<u8>, RejectResponse>),
463}
464
465#[derive(Clone, Debug, Default)]
466pub struct TickConfigs {
467 pub blockmakers: Option<Vec<SubnetBlockmakers>>,
468}
469
470impl From<TickConfigs> for RawTickConfigs {
471 fn from(tick_configs: TickConfigs) -> Self {
472 Self {
473 blockmakers: tick_configs.blockmakers.map(|blockmakers| {
474 blockmakers
475 .into_iter()
476 .map(|blockmaker| blockmaker.into())
477 .collect()
478 }),
479 }
480 }
481}
482
483#[derive(Clone, Debug)]
484pub struct SubnetBlockmakers {
485 pub subnet: Principal,
486 pub blockmaker: Principal,
487 pub failed_blockmakers: Vec<Principal>,
488}
489
490impl From<SubnetBlockmakers> for RawSubnetBlockmakers {
491 fn from(blockmaker: SubnetBlockmakers) -> Self {
492 Self {
493 subnet: blockmaker.subnet.into(),
494 blockmaker: blockmaker.blockmaker.into(),
495 failed_blockmakers: blockmaker
496 .failed_blockmakers
497 .into_iter()
498 .map(|p| p.into())
499 .collect(),
500 }
501 }
502}
503
504#[cfg_attr(
512 all(not(windows), not(test)),
513 expect(
514 dead_code,
515 reason = "the retained WSL converter is used by Windows implementations"
516 )
517)]
518fn windows_to_wsl(path: &str) -> Result<String, String> {
519 let path = path.strip_prefix(r"\\?\").unwrap_or(path);
520 let mut chars = path.chars();
521 let drive = match (chars.next(), chars.next()) {
522 (Some(drive), Some(':')) if drive.is_ascii_alphabetic() => drive.to_ascii_lowercase(),
523 _ => {
524 return Err(format!(
525 "`{path}` does not start with a drive prefix such as `C:`"
526 ));
527 }
528 };
529 let rest = chars.as_str();
530 if !rest.is_empty() && !rest.starts_with(['\\', '/']) {
531 return Err(format!("`{path}` is a drive-relative path"));
532 }
533 Ok(format!("/mnt/{drive}{}", rest.replace('\\', "/")))
534}
535
536#[cfg(windows)]
537fn wsl_path(path: &PathBuf, desc: &str) -> String {
538 windows_to_wsl(
539 path.as_os_str()
540 .to_str()
541 .unwrap_or_else(|| panic!("Could not convert {} path ({:?}) to String", desc, path)),
542 )
543 .unwrap_or_else(|e| {
544 panic!(
545 "Could not convert {} path ({:?}) to WSL path: {:?}",
546 desc, path, e
547 )
548 })
549}
550
551#[cfg(test)]
552mod tests;