Skip to main content

ic_testkit/pocket_ic/
mod.rs

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)]
9// Copyright DFINITY Foundation. Licensed under Apache-2.0.
10//! Maintained PocketIC 16.1 client implementation; see ../upstream/NOTICE.md.
11//! The client is async-only and uses the caller's Tokio runtime.
12//! Build with an explicit server URL and await operations. Explicitly await
13//! `PocketIc::drop` before releasing a borrowed server; Rust Drop performs no I/O.
14use self::common::rest::{
15    AutoProgressConfig, ExtendedSubnetConfigSet, IcpConfig, IcpFeatures, InitialTime,
16    InstanceHttpGatewayConfig, RawSubnetBlockmakers, RawTickConfigs, RawTime, SubnetId, SubnetKind,
17    SubnetSpec,
18};
19use candid::Principal;
20pub use ic_management_canister_types::{
21    CanisterId, CanisterInstallMode, CanisterLogRecord, CanisterSettings, CanisterStatusResult,
22    EnvironmentVariable, Snapshot,
23};
24pub use ic_platform::ic_protocol::SubnetMetrics;
25use reqwest::Url;
26use schemars::JsonSchema;
27use serde::{Deserialize, Serialize};
28use slog::Level;
29use std::{
30    net::SocketAddr,
31    path::PathBuf,
32    time::{Duration, SystemTime, UNIX_EPOCH},
33};
34use strum_macros::EnumIter;
35use tempfile::TempDir;
36
37mod error;
38mod http;
39pub use error::{ClientError, Operation, RequestContext};
40pub mod common;
41mod nonblocking;
42pub use nonblocking::{
43    PocketIc, call_candid, call_candid_as, get_default_effective_canister_id, query_candid,
44    query_candid_as, update_candid, update_candid_as, with_candid,
45};
46pub mod snapshot;
47
48/// Server version associated with the imported PocketIC 16.1 client.
49pub const LATEST_SERVER_VERSION: &str = "16.1.0";
50
51// the default timeout of a PocketIC operation
52const DEFAULT_MAX_REQUEST_TIME_MS: u64 = 300_000;
53
54enum PocketIcStateKind {
55    /// A persistent state dir managed by the user.
56    StateDir(PathBuf),
57    /// A fresh temporary directory used if the user does not provide
58    /// a persistent state directory managed by the user.
59    /// The temporary directory is deleted when `PocketIcState` is dropped
60    /// unless `PocketIcState` is turned into a persistent state
61    /// at the path given by `PocketIcState::into_path`.
62    TempDir(TempDir),
63}
64
65pub struct PocketIcState {
66    state: PocketIcStateKind,
67}
68
69impl PocketIcState {
70    #[expect(
71        clippy::new_without_default,
72        reason = "preserve the imported PocketIC contract without an unrelated API change"
73    )]
74    pub fn new() -> Self {
75        let temp_dir = TempDir::new().unwrap();
76        Self {
77            state: PocketIcStateKind::TempDir(temp_dir),
78        }
79    }
80
81    pub fn new_from_path(state_dir: PathBuf) -> Self {
82        Self {
83            state: PocketIcStateKind::StateDir(state_dir),
84        }
85    }
86
87    pub fn into_path(self) -> PathBuf {
88        match self.state {
89            PocketIcStateKind::StateDir(state_dir) => state_dir,
90            PocketIcStateKind::TempDir(temp_dir) => temp_dir.keep(),
91        }
92    }
93
94    pub(crate) fn state_dir(&self) -> PathBuf {
95        match &self.state {
96            PocketIcStateKind::StateDir(state_dir) => state_dir.clone(),
97            PocketIcStateKind::TempDir(temp_dir) => temp_dir.path().to_path_buf(),
98        }
99    }
100}
101
102pub struct PocketIcBuilder {
103    config: Option<ExtendedSubnetConfigSet>,
104    http_gateway_config: Option<InstanceHttpGatewayConfig>,
105    server_url: Option<Url>,
106    max_request_time_ms: Option<u64>,
107    read_only_state_dir: Option<PathBuf>,
108    state_dir: Option<PocketIcState>,
109    icp_config: IcpConfig,
110    log_level: Option<Level>,
111    bitcoind_addr: Option<Vec<SocketAddr>>,
112    dogecoind_addr: Option<Vec<SocketAddr>>,
113    icp_features: IcpFeatures,
114    initial_time: Option<InitialTime>,
115    mainnet_nns_subnet_id: Option<bool>,
116    disable_ingress_validation: Option<bool>,
117}
118
119#[expect(
120    clippy::new_without_default,
121    reason = "preserve the imported PocketIC contract without an unrelated API change"
122)]
123impl PocketIcBuilder {
124    pub fn new() -> Self {
125        Self {
126            config: None,
127            http_gateway_config: None,
128            server_url: None,
129            max_request_time_ms: Some(DEFAULT_MAX_REQUEST_TIME_MS),
130            read_only_state_dir: None,
131            state_dir: None,
132            icp_config: IcpConfig::default(),
133            log_level: None,
134            bitcoind_addr: None,
135            dogecoind_addr: None,
136            icp_features: IcpFeatures::default(),
137            initial_time: None,
138            mainnet_nns_subnet_id: None,
139            disable_ingress_validation: None,
140        }
141    }
142
143    pub fn new_with_config(config: impl Into<ExtendedSubnetConfigSet>) -> Self {
144        let mut builder = Self::new();
145        builder.config = Some(config.into());
146        builder
147    }
148
149    pub async fn build(self) -> Result<PocketIc, ClientError> {
150        PocketIc::from_components(
151            self.config.unwrap_or_default(),
152            self.server_url.ok_or(ClientError::MissingServerUrl)?,
153            self.max_request_time_ms,
154            self.read_only_state_dir,
155            self.state_dir,
156            self.icp_config,
157            self.log_level,
158            self.bitcoind_addr,
159            self.dogecoind_addr,
160            self.icp_features,
161            self.initial_time,
162            self.http_gateway_config,
163            self.mainnet_nns_subnet_id,
164            self.disable_ingress_validation,
165        )
166        .await
167    }
168
169    /// Use an already running PocketIC server.
170    pub fn with_server_url(mut self, server_url: Url) -> Self {
171        self.server_url = Some(server_url);
172        self
173    }
174
175    pub fn with_max_request_time_ms(mut self, max_request_time_ms: Option<u64>) -> Self {
176        self.max_request_time_ms = max_request_time_ms;
177        self
178    }
179
180    pub fn with_state_dir(mut self, state_dir: PathBuf) -> Self {
181        self.state_dir = Some(PocketIcState::new_from_path(state_dir));
182        self
183    }
184
185    pub fn with_state(mut self, state_dir: PocketIcState) -> Self {
186        self.state_dir = Some(state_dir);
187        self
188    }
189
190    pub fn with_read_only_state(mut self, read_only_state_dir: &PocketIcState) -> Self {
191        self.read_only_state_dir = Some(read_only_state_dir.state_dir());
192        self
193    }
194
195    pub fn with_icp_config(mut self, icp_config: IcpConfig) -> Self {
196        self.icp_config = icp_config;
197        self
198    }
199
200    pub fn with_log_level(mut self, log_level: Level) -> Self {
201        self.log_level = Some(log_level);
202        self
203    }
204
205    pub fn with_bitcoind_addr(self, bitcoind_addr: SocketAddr) -> Self {
206        self.with_bitcoind_addrs(vec![bitcoind_addr])
207    }
208
209    pub fn with_bitcoind_addrs(self, bitcoind_addrs: Vec<SocketAddr>) -> Self {
210        Self {
211            bitcoind_addr: Some(bitcoind_addrs),
212            ..self
213        }
214    }
215
216    pub fn with_dogecoind_addrs(self, dogecoind_addrs: Vec<SocketAddr>) -> Self {
217        Self {
218            dogecoind_addr: Some(dogecoind_addrs),
219            ..self
220        }
221    }
222
223    /// Add an empty NNS subnet unless an NNS subnet has already been added.
224    pub fn with_nns_subnet(mut self) -> Self {
225        let mut config = self.config.unwrap_or_default();
226        config.nns = Some(config.nns.unwrap_or_default());
227        self.config = Some(config);
228        self
229    }
230
231    /// Add an NNS subnet with state loaded from the given state directory.
232    /// Note that the provided path must be accessible for the PocketIC server process.
233    ///
234    /// `path_to_state` should lead to a directory which is expected to have
235    /// the following structure:
236    ///
237    /// path_to_state/
238    ///  |-- backups
239    ///  |-- checkpoints
240    ///  |-- diverged_checkpoints
241    ///  |-- diverged_state_markers
242    ///  |-- fs_tmp
243    ///  |-- page_deltas
244    ///  |-- states_metadata.pbuf
245    ///  |-- tip
246    ///  `-- tmp
247    pub fn with_nns_state(self, path_to_state: PathBuf) -> Self {
248        self.with_subnet_state(SubnetKind::NNS, path_to_state)
249    }
250
251    /// Add a subnet with state loaded from the given state directory.
252    /// Note that the provided path must be accessible for the PocketIC server process.
253    ///
254    /// `path_to_state` should point to a directory which is expected to have
255    /// the following structure:
256    ///
257    /// path_to_state/
258    ///  |-- backups
259    ///  |-- checkpoints
260    ///  |-- diverged_checkpoints
261    ///  |-- diverged_state_markers
262    ///  |-- fs_tmp
263    ///  |-- page_deltas
264    ///  |-- states_metadata.pbuf
265    ///  |-- tip
266    ///  `-- tmp
267    pub fn with_subnet_state(mut self, subnet_kind: SubnetKind, path_to_state: PathBuf) -> Self {
268        let mut config = self.config.unwrap_or_default();
269        #[cfg(not(windows))]
270        let state_dir = path_to_state;
271        #[cfg(windows)]
272        let state_dir = wsl_path(&path_to_state, "subnet state").into();
273        let subnet_spec = SubnetSpec::default().with_state_dir(state_dir);
274        match subnet_kind {
275            SubnetKind::NNS => config.nns = Some(subnet_spec),
276            SubnetKind::SNS => config.sns = Some(subnet_spec),
277            SubnetKind::II => config.ii = Some(subnet_spec),
278            SubnetKind::Fiduciary => config.fiduciary = Some(subnet_spec),
279            SubnetKind::Bitcoin => config.bitcoin = Some(subnet_spec),
280            SubnetKind::TestThresholdKeys => config.test_threshold_keys = Some(subnet_spec),
281            SubnetKind::Application => config.application.push(subnet_spec),
282            SubnetKind::CloudEngine => config.cloud_engine.push(subnet_spec),
283            SubnetKind::System => config.system.push(subnet_spec),
284            SubnetKind::VerifiedApplication => config.verified_application.push(subnet_spec),
285        };
286        self.config = Some(config);
287        self
288    }
289
290    /// Add an empty sns subnet unless an SNS subnet has already been added.
291    pub fn with_sns_subnet(mut self) -> Self {
292        let mut config = self.config.unwrap_or_default();
293        config.sns = Some(config.sns.unwrap_or_default());
294        self.config = Some(config);
295        self
296    }
297
298    /// Add an empty II subnet unless an II subnet has already been added.
299    pub fn with_ii_subnet(mut self) -> Self {
300        let mut config = self.config.unwrap_or_default();
301        config.ii = Some(config.ii.unwrap_or_default());
302        self.config = Some(config);
303        self
304    }
305
306    /// Add an empty fiduciary subnet unless a fiduciary subnet has already been added.
307    pub fn with_fiduciary_subnet(mut self) -> Self {
308        let mut config = self.config.unwrap_or_default();
309        config.fiduciary = Some(config.fiduciary.unwrap_or_default());
310        self.config = Some(config);
311        self
312    }
313
314    /// Add an empty bitcoin subnet unless a bitcoin subnet has already been added.
315    pub fn with_bitcoin_subnet(mut self) -> Self {
316        let mut config = self.config.unwrap_or_default();
317        config.bitcoin = Some(config.bitcoin.unwrap_or_default());
318        self.config = Some(config);
319        self
320    }
321
322    /// Add an empty test threshold keys subnet unless a test threshold keys subnet has already been added.
323    pub fn with_test_threshold_keys_subnet(mut self) -> Self {
324        let mut config = self.config.unwrap_or_default();
325        config.test_threshold_keys = Some(config.test_threshold_keys.unwrap_or_default());
326        self.config = Some(config);
327        self
328    }
329
330    /// Add an empty generic system subnet.
331    pub fn with_system_subnet(mut self) -> Self {
332        let mut config = self.config.unwrap_or_default();
333        config.system.push(SubnetSpec::default());
334        self.config = Some(config);
335        self
336    }
337
338    /// Add an empty generic application subnet.
339    pub fn with_application_subnet(mut self) -> Self {
340        let mut config = self.config.unwrap_or_default();
341        config.application.push(SubnetSpec::default());
342        self.config = Some(config);
343        self
344    }
345
346    /// Add an empty generic verified application subnet.
347    pub fn with_verified_application_subnet(mut self) -> Self {
348        let mut config = self.config.unwrap_or_default();
349        config.verified_application.push(SubnetSpec::default());
350        self.config = Some(config);
351        self
352    }
353
354    /// Add an empty generic application subnet with benchmarking instruction configuration.
355    pub fn with_benchmarking_application_subnet(mut self) -> Self {
356        let mut config = self.config.unwrap_or_default();
357        config
358            .application
359            .push(SubnetSpec::default().with_benchmarking_instruction_config());
360        self.config = Some(config);
361        self
362    }
363
364    /// Add an empty generic system subnet with benchmarking instruction configuration.
365    pub fn with_benchmarking_system_subnet(mut self) -> Self {
366        let mut config = self.config.unwrap_or_default();
367        config
368            .system
369            .push(SubnetSpec::default().with_benchmarking_instruction_config());
370        self.config = Some(config);
371        self
372    }
373
374    /// Enables selected ICP features supported by PocketIC and implemented by system canisters
375    /// (deployed to the PocketIC instance automatically when creating a new PocketIC instance).
376    /// Subnets to which the system canisters are deployed are automatically declared as empty subnets,
377    /// e.g., `PocketIcBuilder::with_nns_subnet` is implicitly implied by specifying the `icp_token` feature.
378    pub fn with_icp_features(mut self, icp_features: IcpFeatures) -> Self {
379        self.icp_features = icp_features;
380        self
381    }
382
383    /// Sets the initial timestamp of the new instance to the provided value which must be at least
384    /// - 10 May 2021 10:00:01 AM CEST if the `cycles_minting` feature is enabled in `icp_features`;
385    /// - 06 May 2021 21:17:10 CEST otherwise.
386    #[deprecated(note = "Use `with_initial_time` instead")]
387    pub fn with_initial_timestamp(mut self, initial_timestamp_nanos: u64) -> Self {
388        self.initial_time = Some(InitialTime::Timestamp(RawTime {
389            nanos_since_epoch: initial_timestamp_nanos,
390        }));
391        self
392    }
393
394    /// Sets the initial time of the new instance to the provided value which must be at least
395    /// - 10 May 2021 10:00:01 AM CEST if the `cycles_minting` feature is enabled in `icp_features`;
396    /// - 06 May 2021 21:17:10 CEST otherwise.
397    pub fn with_initial_time(mut self, initial_time: Time) -> Self {
398        self.initial_time = Some(InitialTime::Timestamp(RawTime {
399            nanos_since_epoch: initial_time.as_nanos_since_unix_epoch(),
400        }));
401        self
402    }
403
404    /// Configures the new instance to make progress automatically,
405    /// i.e., periodically update the time of the IC instance
406    /// to the real time and execute rounds on the subnets.
407    /// Building the instance only returns after the certified time
408    /// of the IC instance has been updated for the first time.
409    pub fn with_auto_progress(mut self) -> Self {
410        let config = AutoProgressConfig {
411            artificial_delay_ms: None,
412        };
413        self.initial_time = Some(InitialTime::AutoProgress(config));
414        self
415    }
416
417    pub fn with_http_gateway(mut self, http_gateway_config: InstanceHttpGatewayConfig) -> Self {
418        self.http_gateway_config = Some(http_gateway_config);
419        self
420    }
421
422    pub fn with_mainnet_nns_subnet_id(mut self) -> Self {
423        self.mainnet_nns_subnet_id = Some(true);
424        self
425    }
426
427    pub fn disable_ingress_validation(mut self) -> Self {
428        self.disable_ingress_validation = Some(true);
429        self
430    }
431}
432
433/// Representation of system time as duration since UNIX epoch
434/// with cross-platform nanosecond precision.
435#[derive(Copy, Clone, PartialEq, PartialOrd)]
436pub struct Time(Duration);
437
438impl Time {
439    /// Number of nanoseconds since UNIX EPOCH.
440    pub fn as_nanos_since_unix_epoch(&self) -> u64 {
441        self.0.as_nanos().try_into().unwrap()
442    }
443
444    pub const fn from_nanos_since_unix_epoch(nanos: u64) -> Self {
445        Time(Duration::from_nanos(nanos))
446    }
447}
448
449impl std::fmt::Debug for Time {
450    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451        let nanos_since_unix_epoch = self.as_nanos_since_unix_epoch();
452        write!(f, "{nanos_since_unix_epoch}")
453    }
454}
455
456impl std::ops::Add<Duration> for Time {
457    type Output = Time;
458    fn add(self, dur: Duration) -> Time {
459        Time(self.0 + dur)
460    }
461}
462
463impl From<SystemTime> for Time {
464    fn from(time: SystemTime) -> Self {
465        Self::from_nanos_since_unix_epoch(
466            time.duration_since(UNIX_EPOCH)
467                .unwrap()
468                .as_nanos()
469                .try_into()
470                .unwrap(),
471        )
472    }
473}
474
475impl TryFrom<Time> for SystemTime {
476    type Error = String;
477
478    fn try_from(time: Time) -> Result<SystemTime, String> {
479        let nanos = time.as_nanos_since_unix_epoch();
480        let system_time = UNIX_EPOCH + Duration::from_nanos(nanos);
481        let roundtrip: Time = system_time.into();
482        if roundtrip.as_nanos_since_unix_epoch() == nanos {
483            Ok(system_time)
484        } else {
485            Err(format!(
486                "Converting UNIX timestamp {nanos} in nanoseconds to SystemTime failed due to losing precision"
487            ))
488        }
489    }
490}
491
492/// Specifies where to place a newly created canister.
493#[derive(Clone, Debug)]
494pub enum CreateCanisterPlacement {
495    /// Place the canister on the given subnet.
496    SubnetId(SubnetId),
497    /// Create the canister with the given specific canister ID.
498    CanisterId(CanisterId),
499}
500
501/// Parameters for [`PocketIc::create_canister_with_params`].
502#[derive(Clone, Debug, Default)]
503pub struct CreateCanisterParams {
504    /// Initial cycles balance; defaults to 100T if `None`.
505    pub cycles: Option<u128>,
506    /// Canister settings; defaults to default canister settings if `None`.
507    pub settings: Option<CanisterSettings>,
508    /// Canister placement (subnet or specific canister ID); a random application subnet is chosen if `None`.
509    pub placement: Option<CreateCanisterPlacement>,
510}
511
512/// Error type for [`TryFrom<u64>`].
513#[derive(Clone, Copy, Debug)]
514pub enum TryFromError {
515    ValueOutOfRange(u64),
516}
517
518/// User-facing error codes.
519///
520/// The error codes are currently assigned using an HTTP-like
521/// convention: the most significant digit is the corresponding reject
522/// code and the rest is just a sequentially assigned two-digit
523/// number.
524#[derive(
525    PartialOrd,
526    Ord,
527    Clone,
528    Copy,
529    Debug,
530    PartialEq,
531    Eq,
532    Hash,
533    Serialize,
534    Deserialize,
535    JsonSchema,
536    EnumIter,
537)]
538pub enum ErrorCode {
539    // 1xx -- `RejectCode::SysFatal`
540    SubnetOversubscribed = 101,
541    MaxNumberOfCanistersReached = 102,
542    // 2xx -- `RejectCode::SysTransient`
543    CanisterQueueFull = 201,
544    IngressMessageTimeout = 202,
545    CanisterQueueNotEmpty = 203,
546    IngressHistoryFull = 204,
547    CanisterIdAlreadyExists = 205,
548    StopCanisterRequestTimeout = 206,
549    CanisterOutOfCycles = 207,
550    CertifiedStateUnavailable = 208,
551    CanisterInstallCodeRateLimited = 209,
552    CanisterHeapDeltaRateLimited = 210,
553    SubnetCoolingDown = 211,
554    // 3xx -- `RejectCode::DestinationInvalid`
555    CanisterNotFound = 301,
556    CanisterSnapshotNotFound = 305,
557    // 4xx -- `RejectCode::CanisterReject`
558    InsufficientMemoryAllocation = 402,
559    InsufficientCyclesForCreateCanister = 403,
560    SubnetNotFound = 404,
561    CanisterNotHostedBySubnet = 405,
562    CanisterRejectedMessage = 406,
563    UnknownManagementMessage = 407,
564    InvalidManagementPayload = 408,
565    CanisterSnapshotImmutable = 409,
566    InvalidSubnetAdmin = 410,
567    // 5xx -- `RejectCode::CanisterError`
568    CanisterTrapped = 502,
569    CanisterCalledTrap = 503,
570    CanisterContractViolation = 504,
571    CanisterInvalidWasm = 505,
572    CanisterDidNotReply = 506,
573    CanisterOutOfMemory = 507,
574    CanisterStopped = 508,
575    CanisterStopping = 509,
576    CanisterNotStopped = 510,
577    CanisterStoppingCancelled = 511,
578    CanisterInvalidController = 512,
579    CanisterFunctionNotFound = 513,
580    CanisterNonEmpty = 514,
581    QueryCallGraphLoopDetected = 517,
582    InsufficientCyclesInCall = 520,
583    CanisterWasmEngineError = 521,
584    CanisterInstructionLimitExceeded = 522,
585    CanisterMemoryAccessLimitExceeded = 524,
586    QueryCallGraphTooDeep = 525,
587    QueryCallGraphTotalInstructionLimitExceeded = 526,
588    CompositeQueryCalledInReplicatedMode = 527,
589    QueryTimeLimitExceeded = 528,
590    QueryCallGraphInternal = 529,
591    InsufficientCyclesInComputeAllocation = 530,
592    InsufficientCyclesInMemoryAllocation = 531,
593    InsufficientCyclesInMemoryGrow = 532,
594    ReservedCyclesLimitExceededInMemoryAllocation = 533,
595    ReservedCyclesLimitExceededInMemoryGrow = 534,
596    InsufficientCyclesInMessageMemoryGrow = 535,
597    CanisterMethodNotFound = 536,
598    CanisterWasmModuleNotFound = 537,
599    CanisterAlreadyInstalled = 538,
600    CanisterWasmMemoryLimitExceeded = 539,
601    ReservedCyclesLimitIsTooLow = 540,
602    CanisterInvalidControllerOrSubnetAdmin = 541,
603    CanisterStatusAccessDenied = 542,
604    // 6xx -- `RejectCode::SysUnknown`
605    DeadlineExpired = 601,
606    ResponseDropped = 602,
607}
608
609impl TryFrom<u64> for ErrorCode {
610    type Error = TryFromError;
611    fn try_from(err: u64) -> Result<ErrorCode, Self::Error> {
612        match err {
613            // 1xx -- `RejectCode::SysFatal`
614            101 => Ok(ErrorCode::SubnetOversubscribed),
615            102 => Ok(ErrorCode::MaxNumberOfCanistersReached),
616            // 2xx -- `RejectCode::SysTransient`
617            201 => Ok(ErrorCode::CanisterQueueFull),
618            202 => Ok(ErrorCode::IngressMessageTimeout),
619            203 => Ok(ErrorCode::CanisterQueueNotEmpty),
620            204 => Ok(ErrorCode::IngressHistoryFull),
621            205 => Ok(ErrorCode::CanisterIdAlreadyExists),
622            206 => Ok(ErrorCode::StopCanisterRequestTimeout),
623            207 => Ok(ErrorCode::CanisterOutOfCycles),
624            208 => Ok(ErrorCode::CertifiedStateUnavailable),
625            209 => Ok(ErrorCode::CanisterInstallCodeRateLimited),
626            210 => Ok(ErrorCode::CanisterHeapDeltaRateLimited),
627            211 => Ok(ErrorCode::SubnetCoolingDown),
628            // 3xx -- `RejectCode::DestinationInvalid`
629            301 => Ok(ErrorCode::CanisterNotFound),
630            305 => Ok(ErrorCode::CanisterSnapshotNotFound),
631            // 4xx -- `RejectCode::CanisterReject`
632            402 => Ok(ErrorCode::InsufficientMemoryAllocation),
633            403 => Ok(ErrorCode::InsufficientCyclesForCreateCanister),
634            404 => Ok(ErrorCode::SubnetNotFound),
635            405 => Ok(ErrorCode::CanisterNotHostedBySubnet),
636            406 => Ok(ErrorCode::CanisterRejectedMessage),
637            407 => Ok(ErrorCode::UnknownManagementMessage),
638            408 => Ok(ErrorCode::InvalidManagementPayload),
639            409 => Ok(ErrorCode::CanisterSnapshotImmutable),
640            410 => Ok(ErrorCode::InvalidSubnetAdmin),
641            // 5xx -- `RejectCode::CanisterError`
642            502 => Ok(ErrorCode::CanisterTrapped),
643            503 => Ok(ErrorCode::CanisterCalledTrap),
644            504 => Ok(ErrorCode::CanisterContractViolation),
645            505 => Ok(ErrorCode::CanisterInvalidWasm),
646            506 => Ok(ErrorCode::CanisterDidNotReply),
647            507 => Ok(ErrorCode::CanisterOutOfMemory),
648            508 => Ok(ErrorCode::CanisterStopped),
649            509 => Ok(ErrorCode::CanisterStopping),
650            510 => Ok(ErrorCode::CanisterNotStopped),
651            511 => Ok(ErrorCode::CanisterStoppingCancelled),
652            512 => Ok(ErrorCode::CanisterInvalidController),
653            513 => Ok(ErrorCode::CanisterFunctionNotFound),
654            514 => Ok(ErrorCode::CanisterNonEmpty),
655            517 => Ok(ErrorCode::QueryCallGraphLoopDetected),
656            520 => Ok(ErrorCode::InsufficientCyclesInCall),
657            521 => Ok(ErrorCode::CanisterWasmEngineError),
658            522 => Ok(ErrorCode::CanisterInstructionLimitExceeded),
659            524 => Ok(ErrorCode::CanisterMemoryAccessLimitExceeded),
660            525 => Ok(ErrorCode::QueryCallGraphTooDeep),
661            526 => Ok(ErrorCode::QueryCallGraphTotalInstructionLimitExceeded),
662            527 => Ok(ErrorCode::CompositeQueryCalledInReplicatedMode),
663            528 => Ok(ErrorCode::QueryTimeLimitExceeded),
664            529 => Ok(ErrorCode::QueryCallGraphInternal),
665            530 => Ok(ErrorCode::InsufficientCyclesInComputeAllocation),
666            531 => Ok(ErrorCode::InsufficientCyclesInMemoryAllocation),
667            532 => Ok(ErrorCode::InsufficientCyclesInMemoryGrow),
668            533 => Ok(ErrorCode::ReservedCyclesLimitExceededInMemoryAllocation),
669            534 => Ok(ErrorCode::ReservedCyclesLimitExceededInMemoryGrow),
670            535 => Ok(ErrorCode::InsufficientCyclesInMessageMemoryGrow),
671            536 => Ok(ErrorCode::CanisterMethodNotFound),
672            537 => Ok(ErrorCode::CanisterWasmModuleNotFound),
673            538 => Ok(ErrorCode::CanisterAlreadyInstalled),
674            539 => Ok(ErrorCode::CanisterWasmMemoryLimitExceeded),
675            540 => Ok(ErrorCode::ReservedCyclesLimitIsTooLow),
676            541 => Ok(ErrorCode::CanisterInvalidControllerOrSubnetAdmin),
677            542 => Ok(ErrorCode::CanisterStatusAccessDenied),
678            // 6xx -- `RejectCode::SysUnknown`
679            601 => Ok(ErrorCode::DeadlineExpired),
680            602 => Ok(ErrorCode::ResponseDropped),
681            _ => Err(TryFromError::ValueOutOfRange(err)),
682        }
683    }
684}
685
686impl std::fmt::Display for ErrorCode {
687    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
688        // E.g. "IC0301"
689        write!(f, "IC{:04}", *self as i32)
690    }
691}
692
693/// User-facing reject codes.
694///
695/// They can be derived from the most significant digit of the
696/// corresponding error code.
697#[derive(
698    PartialOrd,
699    Ord,
700    Clone,
701    Copy,
702    Debug,
703    PartialEq,
704    Eq,
705    Hash,
706    Serialize,
707    Deserialize,
708    JsonSchema,
709    EnumIter,
710)]
711pub enum RejectCode {
712    SysFatal = 1,
713    SysTransient = 2,
714    DestinationInvalid = 3,
715    CanisterReject = 4,
716    CanisterError = 5,
717    SysUnknown = 6,
718}
719
720impl TryFrom<u64> for RejectCode {
721    type Error = TryFromError;
722    fn try_from(err: u64) -> Result<RejectCode, Self::Error> {
723        match err {
724            1 => Ok(RejectCode::SysFatal),
725            2 => Ok(RejectCode::SysTransient),
726            3 => Ok(RejectCode::DestinationInvalid),
727            4 => Ok(RejectCode::CanisterReject),
728            5 => Ok(RejectCode::CanisterError),
729            6 => Ok(RejectCode::SysUnknown),
730            _ => Err(TryFromError::ValueOutOfRange(err)),
731        }
732    }
733}
734
735/// User-facing type describing an unsuccessful (also called reject) call response.
736#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
737pub struct RejectResponse {
738    pub reject_code: RejectCode,
739    pub reject_message: String,
740    pub error_code: ErrorCode,
741    pub certified: bool,
742}
743
744impl std::fmt::Display for RejectResponse {
745    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
746        // Follows [agent-rs](https://github.com/dfinity/agent-rs/blob/a651dbbe69e61d4e8508c144cd60cfa3118eeb3a/ic-agent/src/agent/agent_error.rs#L54)
747        write!(
748            f,
749            "PocketIC returned a rejection error: reject code {:?}, reject message {}, error code {:?}",
750            self.reject_code, self.reject_message, self.error_code
751        )
752    }
753}
754
755/// This enum describes the result of retrieving ingress status.
756/// The `IngressStatusResult::Forbidden` variant is produced
757/// if an optional caller is provided and a corresponding read state request
758/// for the status of the same update call signed by that specified caller
759/// was rejected because the update call was submitted by a different caller.
760#[derive(Debug, Serialize, Deserialize)]
761pub enum IngressStatusResult {
762    NotAvailable,
763    Forbidden(String),
764    Success(Result<Vec<u8>, RejectResponse>),
765}
766
767#[derive(Clone, Debug, Default)]
768pub struct TickConfigs {
769    pub blockmakers: Option<Vec<SubnetBlockmakers>>,
770}
771
772impl From<TickConfigs> for RawTickConfigs {
773    fn from(tick_configs: TickConfigs) -> Self {
774        Self {
775            blockmakers: tick_configs.blockmakers.map(|blockmakers| {
776                blockmakers
777                    .into_iter()
778                    .map(|blockmaker| blockmaker.into())
779                    .collect()
780            }),
781        }
782    }
783}
784
785#[derive(Clone, Debug)]
786pub struct SubnetBlockmakers {
787    pub subnet: Principal,
788    pub blockmaker: Principal,
789    pub failed_blockmakers: Vec<Principal>,
790}
791
792impl From<SubnetBlockmakers> for RawSubnetBlockmakers {
793    fn from(blockmaker: SubnetBlockmakers) -> Self {
794        Self {
795            subnet: blockmaker.subnet.into(),
796            blockmaker: blockmaker.blockmaker.into(),
797            failed_blockmakers: blockmaker
798                .failed_blockmakers
799                .into_iter()
800                .map(|p| p.into())
801                .collect(),
802        }
803    }
804}
805
806/// Converts an absolute Windows path with a drive prefix (e.g. `C:\Users\x\y`)
807/// to the path under which WSL mounts it (e.g. `/mnt/c/Users/x/y`).
808///
809/// The drive letter is lower-cased and backslashes are replaced by forward slashes.
810/// A verbatim prefix (`\\?\`), as produced by `std::fs::canonicalize` on Windows,
811/// is stripped. Paths without a drive prefix (UNC paths, relative paths, ...) are
812/// rejected.
813#[cfg_attr(
814    all(not(windows), not(test)),
815    expect(
816        dead_code,
817        reason = "the retained WSL converter is used by Windows implementations"
818    )
819)]
820fn windows_to_wsl(path: &str) -> Result<String, String> {
821    let path = path.strip_prefix(r"\\?\").unwrap_or(path);
822    let mut chars = path.chars();
823    let drive = match (chars.next(), chars.next()) {
824        (Some(drive), Some(':')) if drive.is_ascii_alphabetic() => drive.to_ascii_lowercase(),
825        _ => {
826            return Err(format!(
827                "`{path}` does not start with a drive prefix such as `C:`"
828            ));
829        }
830    };
831    let rest = chars.as_str();
832    if !rest.is_empty() && !rest.starts_with(['\\', '/']) {
833        return Err(format!("`{path}` is a drive-relative path"));
834    }
835    Ok(format!("/mnt/{drive}{}", rest.replace('\\', "/")))
836}
837
838#[cfg(windows)]
839fn wsl_path(path: &PathBuf, desc: &str) -> String {
840    windows_to_wsl(
841        path.as_os_str()
842            .to_str()
843            .unwrap_or_else(|| panic!("Could not convert {} path ({:?}) to String", desc, path)),
844    )
845    .unwrap_or_else(|e| {
846        panic!(
847            "Could not convert {} path ({:?}) to WSL path: {:?}",
848            desc, path, e
849        )
850    })
851}
852
853pub fn copy_dir(
854    src: impl AsRef<std::path::Path>,
855    dst: impl AsRef<std::path::Path>,
856) -> std::io::Result<()> {
857    std::fs::create_dir_all(&dst)?;
858    for entry in std::fs::read_dir(src)? {
859        let entry = entry?;
860        let ty = entry.file_type()?;
861        if ty.is_dir() {
862            copy_dir(entry.path(), dst.as_ref().join(entry.file_name()))?;
863        } else {
864            std::fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
865        }
866    }
867    Ok(())
868}
869
870#[cfg(test)]
871mod tests;