#![cfg(not(target_family = "wasm"))]
#![expect(
missing_docs,
clippy::too_many_arguments,
clippy::pedantic,
clippy::nursery,
reason = "initial upstream source transfer preserves public signatures and protocol names; see upstream/NOTICE.md"
)]
use self::common::rest::{
AutoProgressConfig, IcpFeatures, InitialTime, InstanceConfig, InstanceHttpGatewayConfig,
RawSubnetBlockmakers, RawTickConfigs, RawTime, SubnetId,
};
use candid::Principal;
pub use ic_management_canister_types::{
CanisterId, CanisterInstallMode, CanisterLogRecord, CanisterSettings, CanisterStatusResult,
EnvironmentVariable, Snapshot,
};
pub use ic_platform::ic_protocol::SubnetMetrics;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use std::{
path::PathBuf,
time::{Duration, SystemTime, UNIX_EPOCH},
};
mod error;
mod http;
pub use error::{ClientError, Operation, RequestContext};
pub mod common;
mod nonblocking;
pub use nonblocking::{
PocketIc, call_candid, call_candid_as, get_default_effective_canister_id, query_candid,
query_candid_as, update_candid, update_candid_as, with_candid,
};
pub mod snapshot;
pub const LATEST_SERVER_VERSION: &str = "16.1.0";
const DEFAULT_MAX_REQUEST_TIME_MS: u64 = 300_000;
pub struct PocketIcBuilder {
config: InstanceConfig,
server_url: Option<Url>,
max_request_time_ms: Option<u64>,
}
#[expect(
clippy::new_without_default,
reason = "preserve the imported PocketIC contract without an unrelated API change"
)]
impl PocketIcBuilder {
pub fn new() -> Self {
Self {
config: InstanceConfig {
icp_features: Some(IcpFeatures::default()),
..InstanceConfig::default()
},
server_url: None,
max_request_time_ms: Some(DEFAULT_MAX_REQUEST_TIME_MS),
}
}
pub async fn build(self) -> Result<PocketIc, ClientError> {
PocketIc::from_config(
self.server_url.ok_or(ClientError::MissingServerUrl)?,
self.max_request_time_ms,
self.config,
)
.await
}
pub fn with_server_url(mut self, server_url: Url) -> Self {
self.server_url = Some(server_url);
self
}
pub fn with_max_request_time_ms(mut self, max_request_time_ms: Option<u64>) -> Self {
self.max_request_time_ms = max_request_time_ms;
self
}
pub fn with_state_dir(mut self, state_dir: PathBuf) -> Self {
self.config.state_dir = Some(state_dir);
self
}
pub fn with_nns_subnet(mut self) -> Self {
self.config.subnet_config_set.nns.get_or_insert_default();
self
}
pub fn with_ii_subnet(mut self) -> Self {
self.config.subnet_config_set.ii.get_or_insert_default();
self
}
pub fn with_system_subnet(mut self) -> Self {
self.config
.subnet_config_set
.system
.push(Default::default());
self
}
pub fn with_application_subnet(mut self) -> Self {
self.config
.subnet_config_set
.application
.push(Default::default());
self
}
pub fn with_icp_features(mut self, icp_features: IcpFeatures) -> Self {
self.config.icp_features = Some(icp_features);
self
}
pub fn with_initial_time(mut self, initial_time: Time) -> Self {
self.config.initial_time = Some(InitialTime::Timestamp(RawTime {
nanos_since_epoch: initial_time.as_nanos_since_unix_epoch(),
}));
self
}
pub fn with_auto_progress(mut self) -> Self {
self.config.initial_time = Some(InitialTime::AutoProgress(AutoProgressConfig {
artificial_delay_ms: None,
}));
self
}
pub fn with_http_gateway(mut self, http_gateway_config: InstanceHttpGatewayConfig) -> Self {
self.config.http_gateway_config = Some(http_gateway_config);
self
}
}
#[derive(Copy, Clone, PartialEq, PartialOrd)]
pub struct Time(Duration);
impl Time {
pub fn as_nanos_since_unix_epoch(&self) -> u64 {
self.0.as_nanos().try_into().unwrap()
}
pub const fn from_nanos_since_unix_epoch(nanos: u64) -> Self {
Time(Duration::from_nanos(nanos))
}
}
impl std::fmt::Debug for Time {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let nanos_since_unix_epoch = self.as_nanos_since_unix_epoch();
write!(f, "{nanos_since_unix_epoch}")
}
}
impl std::ops::Add<Duration> for Time {
type Output = Time;
fn add(self, dur: Duration) -> Time {
Time(self.0 + dur)
}
}
impl From<SystemTime> for Time {
fn from(time: SystemTime) -> Self {
Self::from_nanos_since_unix_epoch(
time.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
.try_into()
.unwrap(),
)
}
}
impl TryFrom<Time> for SystemTime {
type Error = String;
fn try_from(time: Time) -> Result<SystemTime, String> {
let nanos = time.as_nanos_since_unix_epoch();
let system_time = UNIX_EPOCH + Duration::from_nanos(nanos);
let roundtrip: Time = system_time.into();
if roundtrip.as_nanos_since_unix_epoch() == nanos {
Ok(system_time)
} else {
Err(format!(
"Converting UNIX timestamp {nanos} in nanoseconds to SystemTime failed due to losing precision"
))
}
}
}
#[derive(Clone, Debug)]
pub enum CreateCanisterPlacement {
SubnetId(SubnetId),
CanisterId(CanisterId),
}
#[derive(Clone, Debug, Default)]
pub struct CreateCanisterParams {
pub cycles: Option<u128>,
pub settings: Option<CanisterSettings>,
pub placement: Option<CreateCanisterPlacement>,
}
#[derive(Clone, Copy, Debug)]
pub enum TryFromError {
ValueOutOfRange(u64),
}
#[derive(PartialOrd, Ord, Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(test, derive(strum::EnumIter))]
pub enum ErrorCode {
SubnetOversubscribed = 101,
MaxNumberOfCanistersReached = 102,
CanisterQueueFull = 201,
IngressMessageTimeout = 202,
CanisterQueueNotEmpty = 203,
IngressHistoryFull = 204,
CanisterIdAlreadyExists = 205,
StopCanisterRequestTimeout = 206,
CanisterOutOfCycles = 207,
CertifiedStateUnavailable = 208,
CanisterInstallCodeRateLimited = 209,
CanisterHeapDeltaRateLimited = 210,
SubnetCoolingDown = 211,
CanisterNotFound = 301,
CanisterSnapshotNotFound = 305,
InsufficientMemoryAllocation = 402,
InsufficientCyclesForCreateCanister = 403,
SubnetNotFound = 404,
CanisterNotHostedBySubnet = 405,
CanisterRejectedMessage = 406,
UnknownManagementMessage = 407,
InvalidManagementPayload = 408,
CanisterSnapshotImmutable = 409,
InvalidSubnetAdmin = 410,
CanisterTrapped = 502,
CanisterCalledTrap = 503,
CanisterContractViolation = 504,
CanisterInvalidWasm = 505,
CanisterDidNotReply = 506,
CanisterOutOfMemory = 507,
CanisterStopped = 508,
CanisterStopping = 509,
CanisterNotStopped = 510,
CanisterStoppingCancelled = 511,
CanisterInvalidController = 512,
CanisterFunctionNotFound = 513,
CanisterNonEmpty = 514,
QueryCallGraphLoopDetected = 517,
InsufficientCyclesInCall = 520,
CanisterWasmEngineError = 521,
CanisterInstructionLimitExceeded = 522,
CanisterMemoryAccessLimitExceeded = 524,
QueryCallGraphTooDeep = 525,
QueryCallGraphTotalInstructionLimitExceeded = 526,
CompositeQueryCalledInReplicatedMode = 527,
QueryTimeLimitExceeded = 528,
QueryCallGraphInternal = 529,
InsufficientCyclesInComputeAllocation = 530,
InsufficientCyclesInMemoryAllocation = 531,
InsufficientCyclesInMemoryGrow = 532,
ReservedCyclesLimitExceededInMemoryAllocation = 533,
ReservedCyclesLimitExceededInMemoryGrow = 534,
InsufficientCyclesInMessageMemoryGrow = 535,
CanisterMethodNotFound = 536,
CanisterWasmModuleNotFound = 537,
CanisterAlreadyInstalled = 538,
CanisterWasmMemoryLimitExceeded = 539,
ReservedCyclesLimitIsTooLow = 540,
CanisterInvalidControllerOrSubnetAdmin = 541,
CanisterStatusAccessDenied = 542,
DeadlineExpired = 601,
ResponseDropped = 602,
}
impl TryFrom<u64> for ErrorCode {
type Error = TryFromError;
fn try_from(err: u64) -> Result<ErrorCode, Self::Error> {
match err {
101 => Ok(ErrorCode::SubnetOversubscribed),
102 => Ok(ErrorCode::MaxNumberOfCanistersReached),
201 => Ok(ErrorCode::CanisterQueueFull),
202 => Ok(ErrorCode::IngressMessageTimeout),
203 => Ok(ErrorCode::CanisterQueueNotEmpty),
204 => Ok(ErrorCode::IngressHistoryFull),
205 => Ok(ErrorCode::CanisterIdAlreadyExists),
206 => Ok(ErrorCode::StopCanisterRequestTimeout),
207 => Ok(ErrorCode::CanisterOutOfCycles),
208 => Ok(ErrorCode::CertifiedStateUnavailable),
209 => Ok(ErrorCode::CanisterInstallCodeRateLimited),
210 => Ok(ErrorCode::CanisterHeapDeltaRateLimited),
211 => Ok(ErrorCode::SubnetCoolingDown),
301 => Ok(ErrorCode::CanisterNotFound),
305 => Ok(ErrorCode::CanisterSnapshotNotFound),
402 => Ok(ErrorCode::InsufficientMemoryAllocation),
403 => Ok(ErrorCode::InsufficientCyclesForCreateCanister),
404 => Ok(ErrorCode::SubnetNotFound),
405 => Ok(ErrorCode::CanisterNotHostedBySubnet),
406 => Ok(ErrorCode::CanisterRejectedMessage),
407 => Ok(ErrorCode::UnknownManagementMessage),
408 => Ok(ErrorCode::InvalidManagementPayload),
409 => Ok(ErrorCode::CanisterSnapshotImmutable),
410 => Ok(ErrorCode::InvalidSubnetAdmin),
502 => Ok(ErrorCode::CanisterTrapped),
503 => Ok(ErrorCode::CanisterCalledTrap),
504 => Ok(ErrorCode::CanisterContractViolation),
505 => Ok(ErrorCode::CanisterInvalidWasm),
506 => Ok(ErrorCode::CanisterDidNotReply),
507 => Ok(ErrorCode::CanisterOutOfMemory),
508 => Ok(ErrorCode::CanisterStopped),
509 => Ok(ErrorCode::CanisterStopping),
510 => Ok(ErrorCode::CanisterNotStopped),
511 => Ok(ErrorCode::CanisterStoppingCancelled),
512 => Ok(ErrorCode::CanisterInvalidController),
513 => Ok(ErrorCode::CanisterFunctionNotFound),
514 => Ok(ErrorCode::CanisterNonEmpty),
517 => Ok(ErrorCode::QueryCallGraphLoopDetected),
520 => Ok(ErrorCode::InsufficientCyclesInCall),
521 => Ok(ErrorCode::CanisterWasmEngineError),
522 => Ok(ErrorCode::CanisterInstructionLimitExceeded),
524 => Ok(ErrorCode::CanisterMemoryAccessLimitExceeded),
525 => Ok(ErrorCode::QueryCallGraphTooDeep),
526 => Ok(ErrorCode::QueryCallGraphTotalInstructionLimitExceeded),
527 => Ok(ErrorCode::CompositeQueryCalledInReplicatedMode),
528 => Ok(ErrorCode::QueryTimeLimitExceeded),
529 => Ok(ErrorCode::QueryCallGraphInternal),
530 => Ok(ErrorCode::InsufficientCyclesInComputeAllocation),
531 => Ok(ErrorCode::InsufficientCyclesInMemoryAllocation),
532 => Ok(ErrorCode::InsufficientCyclesInMemoryGrow),
533 => Ok(ErrorCode::ReservedCyclesLimitExceededInMemoryAllocation),
534 => Ok(ErrorCode::ReservedCyclesLimitExceededInMemoryGrow),
535 => Ok(ErrorCode::InsufficientCyclesInMessageMemoryGrow),
536 => Ok(ErrorCode::CanisterMethodNotFound),
537 => Ok(ErrorCode::CanisterWasmModuleNotFound),
538 => Ok(ErrorCode::CanisterAlreadyInstalled),
539 => Ok(ErrorCode::CanisterWasmMemoryLimitExceeded),
540 => Ok(ErrorCode::ReservedCyclesLimitIsTooLow),
541 => Ok(ErrorCode::CanisterInvalidControllerOrSubnetAdmin),
542 => Ok(ErrorCode::CanisterStatusAccessDenied),
601 => Ok(ErrorCode::DeadlineExpired),
602 => Ok(ErrorCode::ResponseDropped),
_ => Err(TryFromError::ValueOutOfRange(err)),
}
}
}
impl std::fmt::Display for ErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "IC{:04}", *self as i32)
}
}
#[derive(PartialOrd, Ord, Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(test, derive(strum::EnumIter))]
pub enum RejectCode {
SysFatal = 1,
SysTransient = 2,
DestinationInvalid = 3,
CanisterReject = 4,
CanisterError = 5,
SysUnknown = 6,
}
impl TryFrom<u64> for RejectCode {
type Error = TryFromError;
fn try_from(err: u64) -> Result<RejectCode, Self::Error> {
match err {
1 => Ok(RejectCode::SysFatal),
2 => Ok(RejectCode::SysTransient),
3 => Ok(RejectCode::DestinationInvalid),
4 => Ok(RejectCode::CanisterReject),
5 => Ok(RejectCode::CanisterError),
6 => Ok(RejectCode::SysUnknown),
_ => Err(TryFromError::ValueOutOfRange(err)),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct RejectResponse {
pub reject_code: RejectCode,
pub reject_message: String,
pub error_code: ErrorCode,
pub certified: bool,
}
impl std::fmt::Display for RejectResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"PocketIC returned a rejection error: reject code {:?}, reject message {}, error code {:?}",
self.reject_code, self.reject_message, self.error_code
)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum IngressStatusResult {
NotAvailable,
Forbidden(String),
Success(Result<Vec<u8>, RejectResponse>),
}
#[derive(Clone, Debug, Default)]
pub struct TickConfigs {
pub blockmakers: Option<Vec<SubnetBlockmakers>>,
}
impl From<TickConfigs> for RawTickConfigs {
fn from(tick_configs: TickConfigs) -> Self {
Self {
blockmakers: tick_configs.blockmakers.map(|blockmakers| {
blockmakers
.into_iter()
.map(|blockmaker| blockmaker.into())
.collect()
}),
}
}
}
#[derive(Clone, Debug)]
pub struct SubnetBlockmakers {
pub subnet: Principal,
pub blockmaker: Principal,
pub failed_blockmakers: Vec<Principal>,
}
impl From<SubnetBlockmakers> for RawSubnetBlockmakers {
fn from(blockmaker: SubnetBlockmakers) -> Self {
Self {
subnet: blockmaker.subnet.into(),
blockmaker: blockmaker.blockmaker.into(),
failed_blockmakers: blockmaker
.failed_blockmakers
.into_iter()
.map(|p| p.into())
.collect(),
}
}
}
#[cfg_attr(
all(not(windows), not(test)),
expect(
dead_code,
reason = "the retained WSL converter is used by Windows implementations"
)
)]
fn windows_to_wsl(path: &str) -> Result<String, String> {
let path = path.strip_prefix(r"\\?\").unwrap_or(path);
let mut chars = path.chars();
let drive = match (chars.next(), chars.next()) {
(Some(drive), Some(':')) if drive.is_ascii_alphabetic() => drive.to_ascii_lowercase(),
_ => {
return Err(format!(
"`{path}` does not start with a drive prefix such as `C:`"
));
}
};
let rest = chars.as_str();
if !rest.is_empty() && !rest.starts_with(['\\', '/']) {
return Err(format!("`{path}` is a drive-relative path"));
}
Ok(format!("/mnt/{drive}{}", rest.replace('\\', "/")))
}
#[cfg(windows)]
fn wsl_path(path: &PathBuf, desc: &str) -> String {
windows_to_wsl(
path.as_os_str()
.to_str()
.unwrap_or_else(|| panic!("Could not convert {} path ({:?}) to String", desc, path)),
)
.unwrap_or_else(|e| {
panic!(
"Could not convert {} path ({:?}) to WSL path: {:?}",
desc, path, e
)
})
}
#[cfg(test)]
mod tests;