Skip to main content

pocket_ic/common/
rest.rs

1//! The PocketIC server and the PocketIc library interface with HTTP/JSON.
2//! The types in this module are used to serialize and deserialize data
3//! from and to JSON, and are used by both crates.
4
5use crate::RejectResponse;
6use candid::Principal;
7use hex;
8use reqwest::Response;
9use schemars::JsonSchema;
10use serde::de::DeserializeOwned;
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeMap;
13use std::net::SocketAddr;
14use std::path::PathBuf;
15use strum::IntoEnumIterator;
16use strum_macros::EnumIter;
17
18pub type InstanceId = usize;
19
20#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
21pub struct AutoProgressConfig {
22    pub artificial_delay_ms: Option<u64>,
23}
24
25#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
26pub enum HttpGatewayBackend {
27    Replica(String),
28    PocketIcInstance(InstanceId),
29}
30
31#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
32pub struct HttpsConfig {
33    pub cert_path: String,
34    pub key_path: String,
35}
36
37#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
38pub struct InstanceHttpGatewayConfig {
39    pub ip_addr: Option<String>,
40    pub port: Option<u16>,
41    pub domains: Option<Vec<String>>,
42    pub https_config: Option<HttpsConfig>,
43    pub domain_custom_provider_local_file: Option<String>,
44}
45
46#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
47pub struct HttpGatewayConfig {
48    pub ip_addr: Option<String>,
49    pub port: Option<u16>,
50    pub forward_to: HttpGatewayBackend,
51    pub domains: Option<Vec<String>>,
52    pub https_config: Option<HttpsConfig>,
53    pub domain_custom_provider_local_file: Option<String>,
54}
55
56#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
57pub struct HttpGatewayDetails {
58    pub instance_id: InstanceId,
59    pub port: u16,
60    pub forward_to: HttpGatewayBackend,
61    pub domains: Option<Vec<String>>,
62    pub https_config: Option<HttpsConfig>,
63}
64
65#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
66pub struct HttpGatewayInfo {
67    pub instance_id: InstanceId,
68    pub port: u16,
69}
70
71#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
72pub enum CreateHttpGatewayResponse {
73    Created(HttpGatewayInfo),
74    Error { message: String },
75}
76
77#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
78pub enum CreateInstanceResponse {
79    Created {
80        instance_id: InstanceId,
81        topology: Topology,
82        http_gateway_info: Option<HttpGatewayInfo>,
83    },
84    Error {
85        message: String,
86    },
87}
88
89#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
90pub struct RawTime {
91    pub nanos_since_epoch: u64,
92}
93
94/// Relevant for calls to the management canister. If a subnet ID is
95/// provided, the call will be sent to the management canister of that subnet.
96/// If a canister ID is provided, the call will be sent to the management
97/// canister of the subnet where the canister is on.
98/// If None, the call will be sent to any management canister.
99#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, PartialEq, Eq, Hash)]
100pub enum RawEffectivePrincipal {
101    None,
102    SubnetId(
103        #[serde(deserialize_with = "base64::deserialize")]
104        #[serde(serialize_with = "base64::serialize")]
105        Vec<u8>,
106    ),
107    CanisterId(
108        #[serde(deserialize_with = "base64::deserialize")]
109        #[serde(serialize_with = "base64::serialize")]
110        Vec<u8>,
111    ),
112}
113
114impl std::fmt::Display for RawEffectivePrincipal {
115    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
116        match self {
117            RawEffectivePrincipal::None => write!(f, "None"),
118            RawEffectivePrincipal::SubnetId(subnet_id) => {
119                let principal = Principal::from_slice(subnet_id);
120                write!(f, "SubnetId({principal})")
121            }
122            RawEffectivePrincipal::CanisterId(canister_id) => {
123                let principal = Principal::from_slice(canister_id);
124                write!(f, "CanisterId({principal})")
125            }
126        }
127    }
128}
129
130#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
131pub struct RawMessageId {
132    pub effective_principal: RawEffectivePrincipal,
133    #[serde(deserialize_with = "base64::deserialize")]
134    #[serde(serialize_with = "base64::serialize")]
135    pub message_id: Vec<u8>,
136}
137
138#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
139pub struct RawIngressStatusArgs {
140    pub raw_message_id: RawMessageId,
141    pub raw_caller: Option<RawPrincipalId>,
142}
143
144#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
145pub struct RawSenderInfo {
146    #[serde(deserialize_with = "base64::deserialize")]
147    #[serde(serialize_with = "base64::serialize")]
148    pub info: Vec<u8>,
149    #[serde(deserialize_with = "base64::deserialize")]
150    #[serde(serialize_with = "base64::serialize")]
151    pub signer: Vec<u8>,
152}
153
154#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
155pub struct RawCanisterCall {
156    #[serde(deserialize_with = "base64::deserialize")]
157    #[serde(serialize_with = "base64::serialize")]
158    pub sender: Vec<u8>,
159    #[serde(deserialize_with = "base64::deserialize")]
160    #[serde(serialize_with = "base64::serialize")]
161    pub canister_id: Vec<u8>,
162    pub effective_principal: RawEffectivePrincipal,
163    pub method: String,
164    #[serde(deserialize_with = "base64::deserialize")]
165    #[serde(serialize_with = "base64::serialize")]
166    pub payload: Vec<u8>,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub sender_info: Option<RawSenderInfo>,
169}
170
171#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
172pub enum RawCanisterResult {
173    Ok(
174        #[serde(deserialize_with = "base64::deserialize")]
175        #[serde(serialize_with = "base64::serialize")]
176        Vec<u8>,
177    ),
178    Err(RejectResponse),
179}
180
181impl From<Result<Vec<u8>, RejectResponse>> for RawCanisterResult {
182    fn from(result: Result<Vec<u8>, RejectResponse>) -> Self {
183        match result {
184            Ok(data) => RawCanisterResult::Ok(data),
185            Err(reject_response) => RawCanisterResult::Err(reject_response),
186        }
187    }
188}
189
190impl From<RawCanisterResult> for Result<Vec<u8>, RejectResponse> {
191    fn from(result: RawCanisterResult) -> Self {
192        match result {
193            RawCanisterResult::Ok(data) => Ok(data),
194            RawCanisterResult::Err(reject_response) => Err(reject_response),
195        }
196    }
197}
198
199#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
200pub struct RawSetStableMemory {
201    #[serde(deserialize_with = "base64::deserialize")]
202    #[serde(serialize_with = "base64::serialize")]
203    pub canister_id: Vec<u8>,
204    pub blob_id: BlobId,
205}
206
207#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
208pub struct RawStableMemory {
209    #[serde(deserialize_with = "base64::deserialize")]
210    #[serde(serialize_with = "base64::serialize")]
211    pub blob: Vec<u8>,
212}
213
214#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
215pub struct ApiError {
216    message: String,
217}
218
219#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
220pub struct StartedOrBusyResponse {
221    pub state_label: String,
222    pub op_id: String,
223}
224
225#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
226#[serde(untagged)]
227pub enum ApiResponse<T> {
228    Success(T),
229    Busy { state_label: String, op_id: String },
230    Started { state_label: String, op_id: String },
231    Error { message: String },
232}
233
234impl<T: DeserializeOwned> ApiResponse<T> {
235    pub async fn from_response(resp: Response) -> Self {
236        match resp.status() {
237            reqwest::StatusCode::OK => {
238                let result = resp.json::<T>().await;
239                match result {
240                    Ok(t) => ApiResponse::Success(t),
241                    Err(e) => ApiResponse::Error {
242                        message: format!("Could not parse response: {e}"),
243                    },
244                }
245            }
246            reqwest::StatusCode::ACCEPTED => {
247                let result = resp.json::<StartedOrBusyResponse>().await;
248                match result {
249                    Ok(StartedOrBusyResponse { state_label, op_id }) => {
250                        ApiResponse::Started { state_label, op_id }
251                    }
252                    Err(e) => ApiResponse::Error {
253                        message: format!("Could not parse response: {e}"),
254                    },
255                }
256            }
257            reqwest::StatusCode::CONFLICT => {
258                let result = resp.json::<StartedOrBusyResponse>().await;
259                match result {
260                    Ok(StartedOrBusyResponse { state_label, op_id }) => {
261                        ApiResponse::Busy { state_label, op_id }
262                    }
263                    Err(e) => ApiResponse::Error {
264                        message: format!("Could not parse response: {e}"),
265                    },
266                }
267            }
268            _ => {
269                let result = resp.json::<ApiError>().await;
270                match result {
271                    Ok(e) => ApiResponse::Error { message: e.message },
272                    Err(e) => ApiResponse::Error {
273                        message: format!("Could not parse error: {e}"),
274                    },
275                }
276            }
277        }
278    }
279}
280
281#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
282pub struct RawAddCycles {
283    #[serde(deserialize_with = "base64::deserialize")]
284    #[serde(serialize_with = "base64::serialize")]
285    pub canister_id: Vec<u8>,
286    pub amount: u128,
287}
288
289#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
290pub struct RawCycles {
291    pub cycles: u128,
292}
293
294#[derive(
295    Clone, Serialize, Hash, Eq, PartialEq, Ord, PartialOrd, Deserialize, Debug, JsonSchema,
296)]
297pub struct RawPrincipalId {
298    // raw bytes of the principal
299    #[serde(deserialize_with = "base64::deserialize")]
300    #[serde(serialize_with = "base64::serialize")]
301    pub principal_id: Vec<u8>,
302}
303
304impl From<Principal> for RawPrincipalId {
305    fn from(principal: Principal) -> Self {
306        Self {
307            principal_id: principal.as_slice().to_vec(),
308        }
309    }
310}
311
312impl From<RawPrincipalId> for Principal {
313    fn from(raw_principal_id: RawPrincipalId) -> Self {
314        Principal::from_slice(&raw_principal_id.principal_id)
315    }
316}
317
318#[derive(Clone, Serialize, Eq, PartialEq, Ord, PartialOrd, Deserialize, Debug, JsonSchema)]
319pub struct RawCanisterId {
320    // raw bytes of the principal
321    #[serde(deserialize_with = "base64::deserialize")]
322    #[serde(serialize_with = "base64::serialize")]
323    pub canister_id: Vec<u8>,
324}
325
326impl From<Principal> for RawCanisterId {
327    fn from(principal: Principal) -> Self {
328        Self {
329            canister_id: principal.as_slice().to_vec(),
330        }
331    }
332}
333
334impl From<RawCanisterId> for Principal {
335    fn from(raw_canister_id: RawCanisterId) -> Self {
336        Principal::from_slice(&raw_canister_id.canister_id)
337    }
338}
339
340#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, PartialEq, Eq, Hash)]
341pub struct RawSubnetId {
342    #[serde(deserialize_with = "base64::deserialize")]
343    #[serde(serialize_with = "base64::serialize")]
344    pub subnet_id: Vec<u8>,
345}
346
347pub type SubnetId = Principal;
348
349impl From<Principal> for RawSubnetId {
350    fn from(principal: Principal) -> Self {
351        Self {
352            subnet_id: principal.as_slice().to_vec(),
353        }
354    }
355}
356
357impl From<RawSubnetId> for Principal {
358    fn from(val: RawSubnetId) -> Self {
359        Principal::from_slice(&val.subnet_id)
360    }
361}
362
363#[derive(
364    Clone, Serialize, Deserialize, Debug, JsonSchema, PartialEq, Eq, PartialOrd, Ord, Hash,
365)]
366pub struct RawNodeId {
367    #[serde(deserialize_with = "base64::deserialize")]
368    #[serde(serialize_with = "base64::serialize")]
369    pub node_id: Vec<u8>,
370}
371
372impl From<RawNodeId> for Principal {
373    fn from(val: RawNodeId) -> Self {
374        Principal::from_slice(&val.node_id)
375    }
376}
377
378impl From<Principal> for RawNodeId {
379    fn from(principal: Principal) -> Self {
380        Self {
381            node_id: principal.as_slice().to_vec(),
382        }
383    }
384}
385
386#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
387pub struct RawTickConfigs {
388    pub blockmakers: Option<Vec<RawSubnetBlockmakers>>,
389}
390
391#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
392pub struct RawSubnetBlockmakers {
393    pub subnet: RawSubnetId,
394    pub blockmaker: RawNodeId,
395    pub failed_blockmakers: Vec<RawNodeId>,
396}
397
398#[derive(Serialize, Deserialize, JsonSchema)]
399pub struct RawVerifyCanisterSigArg {
400    #[serde(deserialize_with = "base64::deserialize")]
401    #[serde(serialize_with = "base64::serialize")]
402    pub msg: Vec<u8>,
403    #[serde(deserialize_with = "base64::deserialize")]
404    #[serde(serialize_with = "base64::serialize")]
405    pub sig: Vec<u8>,
406    #[serde(deserialize_with = "base64::deserialize")]
407    #[serde(serialize_with = "base64::serialize")]
408    pub pubkey: Vec<u8>,
409    #[serde(deserialize_with = "base64::deserialize")]
410    #[serde(serialize_with = "base64::serialize")]
411    pub root_pubkey: Vec<u8>,
412}
413
414#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, JsonSchema)]
415pub struct BlobId(
416    #[serde(deserialize_with = "base64::deserialize")]
417    #[serde(serialize_with = "base64::serialize")]
418    pub Vec<u8>,
419);
420
421impl std::fmt::Display for BlobId {
422    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
423        write!(f, "BlobId{{{}}}", hex::encode(self.0.clone()))
424    }
425}
426
427#[derive(Clone, Debug)]
428pub struct BinaryBlob {
429    pub data: Vec<u8>,
430    pub compression: BlobCompression,
431}
432
433#[derive(Clone, Copy, Debug, Eq, PartialEq, JsonSchema)]
434pub enum BlobCompression {
435    Gzip,
436    NoCompression,
437}
438
439// By default, serde serializes Vec<u8> to a list of numbers, which is inefficient.
440// This enables serializing Vec<u8> to a compact base64 representation.
441pub mod base64 {
442    use ::base64::prelude::*;
443    use serde::{Deserialize, Serialize};
444    use serde::{Deserializer, Serializer};
445
446    pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
447        let base64 = BASE64_STANDARD.encode(v);
448        String::serialize(&base64, s)
449    }
450
451    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
452        let base64 = String::deserialize(d)?;
453        BASE64_STANDARD
454            .decode(base64.as_bytes())
455            .map_err(serde::de::Error::custom)
456    }
457}
458
459// ================================================================================================================= //
460
461#[derive(
462    Debug,
463    Clone,
464    Copy,
465    Eq,
466    Hash,
467    PartialEq,
468    Ord,
469    PartialOrd,
470    Serialize,
471    Deserialize,
472    JsonSchema,
473    EnumIter,
474)]
475pub enum SubnetKind {
476    Application,
477    Bitcoin,
478    CloudEngine,
479    Fiduciary,
480    II,
481    NNS,
482    SNS,
483    System,
484    TestThresholdKeys,
485    VerifiedApplication,
486}
487
488impl SubnetKind {
489    pub fn is_named(self) -> bool {
490        NamedSubnet::try_from(self).is_ok()
491    }
492}
493
494/// Named subnets have a fixed canister ID range on the IC mainnet and at most one
495/// instance per PocketIC instance.
496#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, EnumIter)]
497pub enum NamedSubnet {
498    NNS,
499    SNS,
500    II,
501    Fiduciary,
502    Bitcoin,
503    TestThresholdKeys,
504}
505
506impl From<NamedSubnet> for SubnetKind {
507    fn from(named: NamedSubnet) -> Self {
508        match named {
509            NamedSubnet::NNS => SubnetKind::NNS,
510            NamedSubnet::SNS => SubnetKind::SNS,
511            NamedSubnet::II => SubnetKind::II,
512            NamedSubnet::Fiduciary => SubnetKind::Fiduciary,
513            NamedSubnet::Bitcoin => SubnetKind::Bitcoin,
514            NamedSubnet::TestThresholdKeys => SubnetKind::TestThresholdKeys,
515        }
516    }
517}
518
519/// The exhaustive match over `SubnetKind` enforces structural consistency: adding a new
520/// `SubnetKind` variant is a compile error until it is explicitly placed in either the named
521/// or unnamed arm here and in `From<NamedSubnet> for SubnetKind`.
522impl TryFrom<SubnetKind> for NamedSubnet {
523    type Error = ();
524    fn try_from(kind: SubnetKind) -> Result<Self, Self::Error> {
525        match kind {
526            SubnetKind::NNS => Ok(NamedSubnet::NNS),
527            SubnetKind::SNS => Ok(NamedSubnet::SNS),
528            SubnetKind::II => Ok(NamedSubnet::II),
529            SubnetKind::Fiduciary => Ok(NamedSubnet::Fiduciary),
530            SubnetKind::Bitcoin => Ok(NamedSubnet::Bitcoin),
531            SubnetKind::TestThresholdKeys => Ok(NamedSubnet::TestThresholdKeys),
532            SubnetKind::Application
533            | SubnetKind::CloudEngine
534            | SubnetKind::System
535            | SubnetKind::VerifiedApplication => Err(()),
536        }
537    }
538}
539
540/// This represents which named subnets the user wants to create, and how
541/// many of the general app/system subnets, which are indistinguishable.
542#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
543pub struct SubnetConfigSet {
544    pub nns: bool,
545    pub sns: bool,
546    pub ii: bool,
547    pub fiduciary: bool,
548    pub bitcoin: bool,
549    pub test_threshold_keys: bool,
550    pub system: usize,
551    pub application: usize,
552    pub cloud_engine: usize,
553    pub verified_application: usize,
554}
555
556impl SubnetConfigSet {
557    pub fn validate(&self) -> Result<(), String> {
558        if self.system > 0
559            || self.application > 0
560            || self.cloud_engine > 0
561            || self.verified_application > 0
562            || self.nns
563            || self.sns
564            || self.ii
565            || self.fiduciary
566            || self.bitcoin
567            || self.test_threshold_keys
568        {
569            return Ok(());
570        }
571        Err("SubnetConfigSet must contain at least one subnet".to_owned())
572    }
573}
574
575impl From<SubnetConfigSet> for ExtendedSubnetConfigSet {
576    fn from(
577        SubnetConfigSet {
578            nns,
579            sns,
580            ii,
581            fiduciary: fid,
582            bitcoin,
583            test_threshold_keys,
584            system,
585            application,
586            cloud_engine,
587            verified_application,
588        }: SubnetConfigSet,
589    ) -> Self {
590        ExtendedSubnetConfigSet {
591            nns: if nns {
592                Some(SubnetSpec::default())
593            } else {
594                None
595            },
596            sns: if sns {
597                Some(SubnetSpec::default())
598            } else {
599                None
600            },
601            ii: if ii {
602                Some(SubnetSpec::default())
603            } else {
604                None
605            },
606            fiduciary: if fid {
607                Some(SubnetSpec::default())
608            } else {
609                None
610            },
611            bitcoin: if bitcoin {
612                Some(SubnetSpec::default())
613            } else {
614                None
615            },
616            test_threshold_keys: if test_threshold_keys {
617                Some(SubnetSpec::default())
618            } else {
619                None
620            },
621            system: vec![SubnetSpec::default(); system],
622            application: vec![SubnetSpec::default(); application],
623            cloud_engine: vec![SubnetSpec::default(); cloud_engine],
624            verified_application: vec![SubnetSpec::default(); verified_application],
625        }
626    }
627}
628
629#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
630pub enum IcpConfigFlag {
631    Disabled,
632    Enabled,
633}
634
635/// Specifies ICP config of this instance.
636#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
637pub struct IcpConfig {
638    /// Beta features (disabled on the ICP mainnet).
639    pub beta_features: Option<IcpConfigFlag>,
640    /// Limits on function name length in canister WASM (enabled on the ICP mainnet).
641    pub function_name_length_limits: Option<IcpConfigFlag>,
642    /// Rate-limiting of canister execution (enabled on the ICP mainnet).
643    /// Canister execution refers to instructions and memory writes here.
644    pub canister_execution_rate_limiting: Option<IcpConfigFlag>,
645}
646
647#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
648pub enum IcpFeaturesConfig {
649    /// Default configuration of an ICP feature resembling mainnet configuration as closely as possible.
650    #[default]
651    DefaultConfig,
652}
653
654/// Specifies ICP features enabled by deploying their corresponding system canisters
655/// when creating a PocketIC instance and keeping them up to date
656/// during the PocketIC instance lifetime.
657/// The subnets to which the corresponding system canisters are deployed must be empty,
658/// i.e., their corresponding field in `ExtendedSubnetConfigSet` must be `None`
659/// or `Some(config)` with `config.state_config = SubnetStateConfig::New`.
660/// An ICP feature is enabled if its `IcpFeaturesConfig` is provided, i.e.,
661/// if the corresponding field is not `None`.
662#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
663pub struct IcpFeatures {
664    /// Deploys the NNS registry canister and keeps its content in sync with registry used internally by PocketIC.
665    /// Subnets: NNS.
666    pub registry: Option<IcpFeaturesConfig>,
667    /// Deploys the NNS cycles minting canister, sets ICP/XDR conversion rate, and keeps its subnet lists in sync with PocketIC topology.
668    /// If the `cycles_minting` feature is enabled, then the default timestamp of a PocketIC instance is set to 10 May 2021 10:00:01 AM CEST (the smallest value that is strictly larger than the default timestamp hard-coded in the CMC state).
669    /// Subnets: NNS.
670    pub cycles_minting: Option<IcpFeaturesConfig>,
671    /// Deploys the ICP ledger and index canisters and initializes the ICP account of the anonymous principal with 1,000,000,000 ICP.
672    /// Subnets: NNS.
673    pub icp_token: Option<IcpFeaturesConfig>,
674    /// Deploys the cycles ledger and index canisters and initializes the cycles account of the anonymous principal with 2^127 cycles.
675    /// Subnets: II.
676    pub cycles_token: Option<IcpFeaturesConfig>,
677    /// Deploys the NNS governance and root canisters and sets up an initial NNS neuron with 1 ICP stake.
678    /// The initial NNS neuron is controlled by the anonymous principal.
679    /// Subnets: NNS.
680    pub nns_governance: Option<IcpFeaturesConfig>,
681    /// Deploys the SNS-W and aggregator canisters, sets up the SNS subnet list in the SNS-W canister according to PocketIC topology,
682    /// and uploads the SNS canister WASMs to the SNS-W canister.
683    /// Subnets: NNS, SNS.
684    pub sns: Option<IcpFeaturesConfig>,
685    /// Deploys the Internet Identity canister.
686    /// Subnets: II.
687    pub ii: Option<IcpFeaturesConfig>,
688    /// Deploys the NNS frontend dapp. The HTTP gateway must be specified via `http_gateway_config` in `InstanceConfig`
689    /// and the ICP features `cycles_minting`, `icp_token`, `nns_governance`, `sns`, `ii` must all be enabled.
690    /// Subnets: NNS.
691    pub nns_ui: Option<IcpFeaturesConfig>,
692    /// Deploys the Bitcoin canister under the testnet canister ID `g4xu7-jiaaa-aaaan-aaaaq-cai` and configured for the regtest network.
693    /// Subnets: Bitcoin.
694    pub bitcoin: Option<IcpFeaturesConfig>,
695    /// Deploys the Dogecoin canister under the mainnet canister ID `gordg-fyaaa-aaaan-aaadq-cai` and configured for the regtest network.
696    /// Subnets: Bitcoin.
697    pub dogecoin: Option<IcpFeaturesConfig>,
698    /// Deploys the canister migration orchestrator canister.
699    /// Subnets: NNS.
700    pub canister_migration: Option<IcpFeaturesConfig>,
701}
702
703#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
704pub enum InitialTime {
705    /// Sets the initial timestamp of the new instance to the provided value which must be at least
706    /// - 10 May 2021 10:00:01 AM CEST if the `cycles_minting` feature is enabled in `icp_features`;
707    /// - 06 May 2021 21:17:10 CEST otherwise.
708    Timestamp(RawTime),
709    /// Configures the new instance to make progress automatically,
710    /// i.e., periodically update the time of the IC instance
711    /// to the real time and execute rounds on the subnets.
712    /// Creating the instance only returns after the certified time
713    /// of the IC instance has been updated for the first time.
714    AutoProgress(AutoProgressConfig),
715}
716
717#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
718pub enum IncompleteStateFlag {
719    #[default]
720    Disabled,
721    Enabled,
722}
723
724#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
725pub struct InstanceConfig {
726    pub subnet_config_set: ExtendedSubnetConfigSet,
727    pub http_gateway_config: Option<InstanceHttpGatewayConfig>,
728    pub state_dir: Option<PathBuf>,
729    pub icp_config: Option<IcpConfig>,
730    pub log_level: Option<String>,
731    pub bitcoind_addr: Option<Vec<SocketAddr>>,
732    pub dogecoind_addr: Option<Vec<SocketAddr>>,
733    pub icp_features: Option<IcpFeatures>,
734    pub incomplete_state: Option<IncompleteStateFlag>,
735    pub initial_time: Option<InitialTime>,
736    pub mainnet_nns_subnet_id: Option<bool>,
737    pub disable_ingress_validation: Option<bool>,
738}
739
740#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
741pub struct ExtendedSubnetConfigSet {
742    pub nns: Option<SubnetSpec>,
743    pub sns: Option<SubnetSpec>,
744    pub ii: Option<SubnetSpec>,
745    pub fiduciary: Option<SubnetSpec>,
746    pub bitcoin: Option<SubnetSpec>,
747    pub test_threshold_keys: Option<SubnetSpec>,
748    pub system: Vec<SubnetSpec>,
749    pub application: Vec<SubnetSpec>,
750    #[serde(default)]
751    pub cloud_engine: Vec<SubnetSpec>,
752    pub verified_application: Vec<SubnetSpec>,
753}
754
755/// Specifies various configurations for a subnet.
756#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
757pub struct SubnetSpec {
758    state_config: SubnetStateConfig,
759    instruction_config: SubnetInstructionConfig,
760    subnet_admins: Option<Vec<RawPrincipalId>>,
761    #[serde(default)]
762    cost_schedule: CanisterCyclesCostSchedule,
763}
764
765impl SubnetSpec {
766    pub fn with_state_dir(mut self, path: PathBuf) -> SubnetSpec {
767        self.state_config = SubnetStateConfig::FromPath(path);
768        self
769    }
770
771    pub fn with_benchmarking_instruction_config(mut self) -> SubnetSpec {
772        self.instruction_config = SubnetInstructionConfig::Benchmarking;
773        self
774    }
775
776    pub fn get_state_path(&self) -> Option<PathBuf> {
777        self.state_config.get_path()
778    }
779
780    pub fn get_instruction_config(&self) -> SubnetInstructionConfig {
781        self.instruction_config.clone()
782    }
783
784    pub fn is_supported(&self) -> bool {
785        match &self.state_config {
786            SubnetStateConfig::New => true,
787            SubnetStateConfig::FromPath(..) => true,
788            SubnetStateConfig::FromBlobStore(..) => false,
789        }
790    }
791
792    pub fn with_subnet_admins(mut self, subnet_admins: Vec<Principal>) -> SubnetSpec {
793        self.subnet_admins = Some(
794            subnet_admins
795                .into_iter()
796                .map(RawPrincipalId::from)
797                .collect(),
798        );
799        self
800    }
801
802    pub fn with_cost_schedule(mut self, cost_schedule: CanisterCyclesCostSchedule) -> SubnetSpec {
803        self.cost_schedule = cost_schedule;
804        self
805    }
806
807    pub fn get_subnet_admins(&self) -> Option<Vec<Principal>> {
808        self.subnet_admins
809            .clone()
810            .map(|subnet_admins| subnet_admins.into_iter().map(Principal::from).collect())
811    }
812
813    pub fn get_cost_schedule(&self) -> CanisterCyclesCostSchedule {
814        self.cost_schedule
815    }
816}
817
818impl Default for SubnetSpec {
819    fn default() -> Self {
820        Self {
821            state_config: SubnetStateConfig::New,
822            instruction_config: SubnetInstructionConfig::Production,
823            subnet_admins: None,
824            cost_schedule: Default::default(),
825        }
826    }
827}
828
829/// Specifies instruction limits for canister execution on this subnet.
830#[derive(
831    Debug, Clone, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema,
832)]
833pub enum SubnetInstructionConfig {
834    /// Use default instruction limits as in production.
835    Production,
836    /// Use very high instruction limits useful for asymptotic canister benchmarking.
837    Benchmarking,
838}
839
840/// Specifies whether the subnet should be created from scratch or loaded
841/// from a path.
842#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
843pub enum SubnetStateConfig {
844    /// Create new subnet with empty state.
845    New,
846    /// Load existing subnet state from the given path.
847    /// The path must be on a filesystem accessible to the server process.
848    FromPath(PathBuf),
849    /// Load existing subnet state from blobstore. Needs to be uploaded first!
850    /// Not implemented!
851    FromBlobStore(BlobId),
852}
853
854impl SubnetStateConfig {
855    pub fn get_path(&self) -> Option<PathBuf> {
856        match self {
857            SubnetStateConfig::FromPath(path) => Some(path.clone()),
858            SubnetStateConfig::FromBlobStore(_) => None,
859            SubnetStateConfig::New => None,
860        }
861    }
862}
863
864impl ExtendedSubnetConfigSet {
865    fn named_subnet_spec(&self, named: NamedSubnet) -> Option<SubnetSpec> {
866        match named {
867            NamedSubnet::NNS => self.nns.clone(),
868            NamedSubnet::SNS => self.sns.clone(),
869            NamedSubnet::II => self.ii.clone(),
870            NamedSubnet::Fiduciary => self.fiduciary.clone(),
871            NamedSubnet::Bitcoin => self.bitcoin.clone(),
872            NamedSubnet::TestThresholdKeys => self.test_threshold_keys.clone(),
873        }
874    }
875
876    // Return the configured named subnets in order.
877    #[allow(clippy::type_complexity)]
878    pub fn get_named(&self) -> Vec<(SubnetKind, Option<PathBuf>, SubnetInstructionConfig)> {
879        NamedSubnet::iter()
880            .filter_map(|named| {
881                self.named_subnet_spec(named).map(|spec| {
882                    (
883                        SubnetKind::from(named),
884                        spec.get_state_path(),
885                        spec.get_instruction_config(),
886                    )
887                })
888            })
889            .collect()
890    }
891
892    pub fn validate(&self) -> Result<(), String> {
893        let all_but_application_and_cloud_engine = self
894            .nns
895            .iter()
896            .chain(&self.sns)
897            .chain(&self.ii)
898            .chain(&self.fiduciary)
899            .chain(&self.bitcoin)
900            .chain(&self.test_threshold_keys)
901            .chain(&self.system)
902            .chain(&self.verified_application);
903
904        let all = all_but_application_and_cloud_engine
905            .clone()
906            .chain(&self.application)
907            .chain(&self.cloud_engine);
908
909        // 1. Check for invalid admins using a clone of the iterator (to prevent its consumption).
910        if all_but_application_and_cloud_engine
911            .clone()
912            .any(|spec| spec.subnet_admins.is_some())
913        {
914            return Err(
915                "Subnet admins can only be specified for subnet of kind `Application` or `CloudEngine`".into(),
916            );
917        }
918
919        // 2. Check for invalid cost schedules using a clone of the iterator (to prevent its consumption).
920        if all_but_application_and_cloud_engine
921            .clone()
922            .any(|spec| spec.cost_schedule != Default::default())
923        {
924            return Err("Non-default cost schedule can only be specified for subnet of kind `Application` or `CloudEngine`".into());
925        }
926
927        // 3. Check for invalid combinations of subnet admins and cost schedule using a clone of the iterator (to prevent its consumption).
928        if all.clone().any(|spec| {
929            spec.subnet_admins.is_some() && spec.cost_schedule != CanisterCyclesCostSchedule::Free
930        }) {
931            return Err(
932                "Subnet admins can only be specified for subnet with cost schedule of kind `Free`"
933                    .into(),
934            );
935        }
936
937        // 4. Cloud engines must have a "free" cost schedule.
938        if self
939            .cloud_engine
940            .iter()
941            .any(|spec| spec.cost_schedule != CanisterCyclesCostSchedule::Free)
942        {
943            return Err(
944                "Every subnet of kind `CloudEngine` must have cost schedule of kind `Free`".into(),
945            );
946        }
947
948        // 5. Check for existence across everything (again cloning the iterator to prevent its consumption).
949        let has_any = all.clone().next().is_some();
950
951        if !has_any {
952            return Err("ExtendedSubnetConfigSet must contain at least one subnet".into());
953        }
954
955        Ok(())
956    }
957
958    pub fn try_with_icp_features(mut self, icp_features: &IcpFeatures) -> Result<Self, String> {
959        let check_empty_subnet = |subnet: &Option<SubnetSpec>, subnet_desc, icp_feature| {
960            if let Some(config) = subnet
961                && !matches!(config.state_config, SubnetStateConfig::New)
962            {
963                return Err(format!(
964                    "The {subnet_desc} subnet must be empty when specifying the `{icp_feature}` ICP feature."
965                ));
966            }
967            Ok(())
968        };
969        // using `let IcpFeatures { }` with explicit field names
970        // to force an update after adding a new field to `IcpFeatures`
971        let IcpFeatures {
972            registry,
973            cycles_minting,
974            icp_token,
975            cycles_token,
976            nns_governance,
977            sns,
978            ii,
979            nns_ui,
980            bitcoin,
981            dogecoin,
982            canister_migration,
983        } = icp_features;
984        // NNS canisters
985        for (flag, icp_feature_str) in [
986            (registry, "registry"),
987            (cycles_minting, "cycles_minting"),
988            (icp_token, "icp_token"),
989            (nns_governance, "nns_governance"),
990            (sns, "sns"),
991            (nns_ui, "nns_ui"),
992            (canister_migration, "canister_migration"),
993        ] {
994            if flag.is_some() {
995                check_empty_subnet(&self.nns, "NNS", icp_feature_str)?;
996                self.nns = Some(self.nns.unwrap_or_default());
997            }
998        }
999        // canisters on the II subnet
1000        for (flag, icp_feature_str) in [(cycles_token, "cycles_token"), (ii, "ii")] {
1001            if flag.is_some() {
1002                check_empty_subnet(&self.ii, "II", icp_feature_str)?;
1003                self.ii = Some(self.ii.unwrap_or_default());
1004            }
1005        }
1006        // canisters on the SNS subnet
1007        if sns.is_some() {
1008            check_empty_subnet(&self.sns, "SNS", "sns")?;
1009            self.sns = Some(self.sns.unwrap_or_default());
1010        }
1011        // canisters on the Bitcoin subnet
1012        for (flag, icp_feature_str) in [(bitcoin, "bitcoin"), (dogecoin, "dogecoin")] {
1013            if flag.is_some() {
1014                check_empty_subnet(&self.bitcoin, "Bitcoin", icp_feature_str)?;
1015                self.bitcoin = Some(self.bitcoin.unwrap_or_default());
1016            }
1017        }
1018        Ok(self)
1019    }
1020}
1021
1022/// Specifies how to charge canisters for their use of computational resources (such as
1023/// executing instructions, storing data, network, etc.).
1024#[derive(
1025    Debug,
1026    Default,
1027    Hash,
1028    Clone,
1029    Copy,
1030    Eq,
1031    PartialEq,
1032    Ord,
1033    PartialOrd,
1034    Serialize,
1035    Deserialize,
1036    JsonSchema,
1037)]
1038pub enum CanisterCyclesCostSchedule {
1039    #[default]
1040    Normal,
1041    Free,
1042}
1043
1044/// Configuration details for a subnet, returned by PocketIc server
1045#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema)]
1046pub struct SubnetConfig {
1047    pub subnet_kind: SubnetKind,
1048    pub subnet_admins: Option<Vec<RawPrincipalId>>,
1049    pub cost_schedule: CanisterCyclesCostSchedule,
1050    pub subnet_seed: [u8; 32],
1051    /// Instruction limits for canister execution on this subnet.
1052    pub instruction_config: SubnetInstructionConfig,
1053    /// Some mainnet subnets have several disjunct canister ranges.
1054    pub canister_ranges: Vec<CanisterIdRange>,
1055}
1056
1057#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema)]
1058pub struct CanisterIdRange {
1059    pub start: RawCanisterId,
1060    pub end: RawCanisterId,
1061}
1062
1063impl CanisterIdRange {
1064    fn contains(&self, canister_id: Principal) -> bool {
1065        Principal::from_slice(&self.start.canister_id) <= canister_id
1066            && canister_id <= Principal::from_slice(&self.end.canister_id)
1067    }
1068}
1069
1070#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema)]
1071pub struct Topology {
1072    pub subnet_configs: BTreeMap<SubnetId, SubnetConfig>,
1073    pub default_effective_canister_id: RawCanisterId,
1074}
1075
1076impl Topology {
1077    pub fn get_subnet(&self, canister_id: Principal) -> Option<SubnetId> {
1078        self.subnet_configs
1079            .iter()
1080            .find(|(_, config)| {
1081                config
1082                    .canister_ranges
1083                    .iter()
1084                    .any(|r| r.contains(canister_id))
1085            })
1086            .map(|(subnet_id, _)| subnet_id)
1087            .copied()
1088    }
1089
1090    pub fn get_app_subnets(&self) -> Vec<SubnetId> {
1091        self.find_subnets(SubnetKind::Application, None)
1092    }
1093
1094    pub fn get_cloud_engines(&self) -> Vec<SubnetId> {
1095        self.find_subnets(SubnetKind::CloudEngine, None)
1096    }
1097
1098    pub fn get_verified_app_subnets(&self) -> Vec<SubnetId> {
1099        self.find_subnets(SubnetKind::VerifiedApplication, None)
1100    }
1101
1102    pub fn get_benchmarking_app_subnets(&self) -> Vec<SubnetId> {
1103        self.find_subnets(
1104            SubnetKind::Application,
1105            Some(SubnetInstructionConfig::Benchmarking),
1106        )
1107    }
1108
1109    pub fn get_bitcoin(&self) -> Option<SubnetId> {
1110        self.find_subnet(SubnetKind::Bitcoin)
1111    }
1112
1113    pub fn get_fiduciary(&self) -> Option<SubnetId> {
1114        self.find_subnet(SubnetKind::Fiduciary)
1115    }
1116
1117    pub fn get_ii(&self) -> Option<SubnetId> {
1118        self.find_subnet(SubnetKind::II)
1119    }
1120
1121    pub fn get_nns(&self) -> Option<SubnetId> {
1122        self.find_subnet(SubnetKind::NNS)
1123    }
1124
1125    pub fn get_sns(&self) -> Option<SubnetId> {
1126        self.find_subnet(SubnetKind::SNS)
1127    }
1128
1129    pub fn get_system_subnets(&self) -> Vec<SubnetId> {
1130        self.find_subnets(SubnetKind::System, None)
1131    }
1132
1133    fn find_subnets(
1134        &self,
1135        kind: SubnetKind,
1136        instruction_config: Option<SubnetInstructionConfig>,
1137    ) -> Vec<SubnetId> {
1138        self.subnet_configs
1139            .iter()
1140            .filter(|(_, config)| {
1141                config.subnet_kind == kind
1142                    && instruction_config
1143                        .as_ref()
1144                        .map(|instruction_config| config.instruction_config == *instruction_config)
1145                        .unwrap_or(true)
1146            })
1147            .map(|(id, _)| *id)
1148            .collect()
1149    }
1150
1151    fn find_subnet(&self, kind: SubnetKind) -> Option<SubnetId> {
1152        self.subnet_configs
1153            .iter()
1154            .find(|(_, config)| config.subnet_kind == kind)
1155            .map(|(id, _)| *id)
1156    }
1157}
1158
1159#[derive(
1160    Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, JsonSchema,
1161)]
1162pub enum CanisterHttpMethod {
1163    GET,
1164    POST,
1165    HEAD,
1166    PUT,
1167    DELETE,
1168    PATCH,
1169}
1170
1171#[derive(
1172    Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, JsonSchema,
1173)]
1174pub struct CanisterHttpHeader {
1175    pub name: String,
1176    pub value: String,
1177}
1178
1179/// How a canister HTTP outcall is replicated across the nodes of its subnet.
1180#[derive(
1181    Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, JsonSchema,
1182)]
1183pub enum CanisterHttpReplication {
1184    /// Every node of the subnet performs the outcall and a response is delivered
1185    /// once `n - f` of them agree on it. Too many differing responses make the
1186    /// outcall fail with a "no consensus could be reached" rejection instead.
1187    /// Mock such an outcall with `PocketIc::mock_canister_http_response`, whose
1188    /// `additional_responses` are what lets a test produce either outcome.
1189    FullyReplicated,
1190    /// One node of the subnet performs the outcall (`is_replicated = false`) and
1191    /// its response is the one delivered. PocketIC does not report which node was
1192    /// picked, so mock such an outcall with
1193    /// `PocketIc::mock_canister_http_response` like a fully replicated one: give it
1194    /// a single `response`, which every node then reports, and it is delivered no
1195    /// matter which node was picked. Differing `additional_responses` would instead
1196    /// make the delivered response unpredictable.
1197    NonReplicated,
1198    /// A committee of `total_requests` nodes performs the outcall and between
1199    /// `min_responses` and `max_responses` of their (potentially differing)
1200    /// responses are delivered to the calling canister. Mock such an outcall
1201    /// with `PocketIc::mock_flexible_canister_http_response`.
1202    Flexible {
1203        /// The number of nodes performing the outcall.
1204        total_requests: u32,
1205        /// The number of responses required to deliver a result.
1206        min_responses: u32,
1207        /// The largest number of responses that may be delivered.
1208        max_responses: u32,
1209    },
1210}
1211
1212/// The pricing model applied to a canister HTTP outcall.
1213#[derive(
1214    Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, JsonSchema,
1215)]
1216pub enum CanisterHttpPricingVersion {
1217    /// The whole cost of the outcall is charged up front, based on the largest
1218    /// response it could receive.
1219    Legacy,
1220    /// The outcall is charged for the resources it actually consumes: a base fee is
1221    /// charged up front, a per-replica cycles allowance is withheld from the
1222    /// payment, and whatever the responding nodes do not spend is refunded.
1223    PayAsYouGo,
1224}
1225
1226#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
1227pub struct RawCanisterHttpRequest {
1228    pub subnet_id: RawSubnetId,
1229    pub request_id: u64,
1230    pub http_method: CanisterHttpMethod,
1231    pub url: String,
1232    pub headers: Vec<CanisterHttpHeader>,
1233    #[serde(deserialize_with = "base64::deserialize")]
1234    #[serde(serialize_with = "base64::serialize")]
1235    pub body: Vec<u8>,
1236    pub max_response_bytes: Option<u64>,
1237    pub replication: CanisterHttpReplication,
1238    pub pricing_version: CanisterHttpPricingVersion,
1239}
1240
1241#[derive(Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
1242pub struct CanisterHttpRequest {
1243    pub subnet_id: Principal,
1244    pub request_id: u64,
1245    pub http_method: CanisterHttpMethod,
1246    pub url: String,
1247    pub headers: Vec<CanisterHttpHeader>,
1248    #[serde(deserialize_with = "base64::deserialize")]
1249    #[serde(serialize_with = "base64::serialize")]
1250    pub body: Vec<u8>,
1251    pub max_response_bytes: Option<u64>,
1252    pub replication: CanisterHttpReplication,
1253    pub pricing_version: CanisterHttpPricingVersion,
1254}
1255
1256impl From<RawCanisterHttpRequest> for CanisterHttpRequest {
1257    fn from(raw_canister_http_request: RawCanisterHttpRequest) -> Self {
1258        Self {
1259            subnet_id: candid::Principal::from_slice(
1260                &raw_canister_http_request.subnet_id.subnet_id,
1261            ),
1262            request_id: raw_canister_http_request.request_id,
1263            http_method: raw_canister_http_request.http_method,
1264            url: raw_canister_http_request.url,
1265            headers: raw_canister_http_request.headers,
1266            body: raw_canister_http_request.body,
1267            max_response_bytes: raw_canister_http_request.max_response_bytes,
1268            replication: raw_canister_http_request.replication,
1269            pricing_version: raw_canister_http_request.pricing_version,
1270        }
1271    }
1272}
1273
1274impl From<CanisterHttpRequest> for RawCanisterHttpRequest {
1275    fn from(canister_http_request: CanisterHttpRequest) -> Self {
1276        Self {
1277            subnet_id: canister_http_request.subnet_id.into(),
1278            request_id: canister_http_request.request_id,
1279            http_method: canister_http_request.http_method,
1280            url: canister_http_request.url,
1281            headers: canister_http_request.headers,
1282            body: canister_http_request.body,
1283            max_response_bytes: canister_http_request.max_response_bytes,
1284            replication: canister_http_request.replication,
1285            pricing_version: canister_http_request.pricing_version,
1286        }
1287    }
1288}
1289
1290#[derive(
1291    Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, JsonSchema,
1292)]
1293pub struct CanisterHttpReply {
1294    pub status: u16,
1295    pub headers: Vec<CanisterHttpHeader>,
1296    #[serde(deserialize_with = "base64::deserialize")]
1297    #[serde(serialize_with = "base64::serialize")]
1298    pub body: Vec<u8>,
1299}
1300
1301#[derive(
1302    Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, JsonSchema,
1303)]
1304pub struct CanisterHttpReject {
1305    pub reject_code: u64,
1306    /// Bounded by the 1 KiB a node truncates its reject messages to.
1307    pub message: String,
1308}
1309
1310#[derive(
1311    Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, JsonSchema,
1312)]
1313pub enum CanisterHttpResponse {
1314    CanisterHttpReply(CanisterHttpReply),
1315    CanisterHttpReject(CanisterHttpReject),
1316}
1317
1318#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
1319pub struct RawMockCanisterHttpResponse {
1320    pub subnet_id: RawSubnetId,
1321    pub request_id: u64,
1322    pub response: CanisterHttpResponse,
1323    pub additional_responses: Vec<CanisterHttpResponse>,
1324}
1325
1326/// Mocked responses to a pending canister HTTP outcall made through the
1327/// `http_request` management canister endpoint — whether fully replicated or
1328/// non-replicated (see [`CanisterHttpReplication`]). A *flexible* outcall is
1329/// answered with [`MockFlexibleCanisterHttpResponse`] instead.
1330///
1331/// This answers for every node of the subnet, which is what an `http_request`
1332/// outcall needs: a fully replicated one is performed by all of them, and a
1333/// non-replicated one by a single node that is not exposed. It is therefore also
1334/// the only shape that can make the nodes of a fully replicated outcall disagree
1335/// (see `additional_responses`).
1336///
1337/// An `http_request` outcall answered this way reports one of:
1338/// - the mocked response — as a reply, or as a rejection of the
1339///   calling canister's call carrying the mocked code and message;
1340/// - `SysTransient` and "No consensus could be reached" if its nodes disagree (see
1341///   `additional_responses`; a non-replicated outcall has no such outcome, it just
1342///   delivers its designated node's response);
1343/// - `SysTransient` and "Out of cycles" if what the nodes left unspent of their
1344///   per-replica cycles allowances no longer covers putting a response into a
1345///   block;
1346/// - `SysTransient` and "Canister http request timed out" once the outcall is
1347///   older than its 60 second timeout.
1348#[derive(Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
1349pub struct MockCanisterHttpResponse {
1350    pub subnet_id: Principal,
1351    pub request_id: u64,
1352    /// The response every node of the subnet reports, unless
1353    /// `additional_responses` is non-empty — in which case this is the response of
1354    /// one node and `additional_responses` holds the rest.
1355    ///
1356    /// With this response alone, all nodes agree, so a fully replicated outcall
1357    /// delivers it (see [`CanisterHttpReplication::FullyReplicated`]).
1358    pub response: CanisterHttpResponse,
1359    /// The responses of the remaining nodes of the subnet. Either empty — then
1360    /// `response` is used for every node — or exactly one short of the subnet size,
1361    /// so that together with `response` there is one response per node.
1362    ///
1363    /// This is how a test makes the nodes of a *fully replicated* outcall disagree:
1364    /// unless `n - f` of the `n` responses are equal, it fails with a "no consensus
1365    /// could be reached" rejection.
1366    ///
1367    /// A *non-replicated* outcall has nothing to disagree about — only its
1368    /// designated node's response is delivered, and PocketIC does not report which
1369    /// node that is — so differing responses merely make the delivered one
1370    /// unpredictable. Leave this empty unless the disagreement is what is under
1371    /// test.
1372    pub additional_responses: Vec<CanisterHttpResponse>,
1373}
1374
1375impl From<RawMockCanisterHttpResponse> for MockCanisterHttpResponse {
1376    fn from(raw_mock_canister_http_response: RawMockCanisterHttpResponse) -> Self {
1377        Self {
1378            subnet_id: candid::Principal::from_slice(
1379                &raw_mock_canister_http_response.subnet_id.subnet_id,
1380            ),
1381            request_id: raw_mock_canister_http_response.request_id,
1382            response: raw_mock_canister_http_response.response,
1383            additional_responses: raw_mock_canister_http_response.additional_responses,
1384        }
1385    }
1386}
1387
1388impl From<MockCanisterHttpResponse> for RawMockCanisterHttpResponse {
1389    fn from(mock_canister_http_response: MockCanisterHttpResponse) -> Self {
1390        Self {
1391            subnet_id: RawSubnetId {
1392                subnet_id: mock_canister_http_response.subnet_id.as_slice().to_vec(),
1393            },
1394            request_id: mock_canister_http_response.request_id,
1395            response: mock_canister_http_response.response,
1396            additional_responses: mock_canister_http_response.additional_responses,
1397        }
1398    }
1399}
1400
1401#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
1402pub struct RawMockFlexibleCanisterHttpResponse {
1403    pub subnet_id: RawSubnetId,
1404    pub request_id: u64,
1405    pub responses: Vec<CanisterHttpResponse>,
1406}
1407
1408/// Mocked responses to a pending *flexible* canister HTTP outcall, i.e. one made
1409/// through the `flexible_http_request` management canister endpoint.
1410///
1411/// A flexible outcall is performed by a committee of `total_requests` nodes whose
1412/// differing responses are all legitimate, several of which are delivered, and which
1413/// need not all arrive (see [`CanisterHttpReplication::Flexible`]). Leaving some of them
1414/// silent is what models a slow or unresponsive committee.
1415#[derive(Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
1416pub struct MockFlexibleCanisterHttpResponse {
1417    pub subnet_id: Principal,
1418    pub request_id: u64,
1419    /// One response per committee node that responded. Each response is attributed
1420    /// to a different committee node.
1421    ///
1422    /// There may be at most `total_requests` responses; providing fewer models a
1423    /// committee whose remaining nodes never respond. What the outcall reports is
1424    /// then decided in this order:
1425    /// - once the outcall is older than its 60 second timeout it reports a
1426    ///   `timeout`;
1427    /// - at least `min_responses` replies deliver a result — unless they cannot be
1428    ///   delivered, in which case one of the outcomes below applies instead;
1429    /// - more than `total_requests - min_responses` rejects — i.e. so
1430    ///   many that `min_responses` replies can no longer be reached — deliver a
1431    ///   `too_many_rejects` error naming the rejecting nodes;
1432    /// - if the smallest replies that would have to be delivered together do not
1433    ///   fit into the 2 MiB a block has for HTTP outcall responses, a
1434    ///   `responses_too_large` error (each response may still be within the 2 MB
1435    ///   cap on a single one);
1436    /// - if what the nodes left unspent of their per-replica cycles allowances no
1437    ///   longer covers putting a response into a block, an `out_of_cycles`;
1438    /// - anything else leaves the outcall pending, so that advancing the time past
1439    ///   its timeout delivers a `timeout` error.
1440    ///
1441    /// All of these are replies to the calling canister, not rejections of its
1442    /// call.
1443    pub responses: Vec<CanisterHttpResponse>,
1444}
1445
1446impl From<RawMockFlexibleCanisterHttpResponse> for MockFlexibleCanisterHttpResponse {
1447    fn from(raw_mock_flexible_canister_http_response: RawMockFlexibleCanisterHttpResponse) -> Self {
1448        Self {
1449            subnet_id: candid::Principal::from_slice(
1450                &raw_mock_flexible_canister_http_response.subnet_id.subnet_id,
1451            ),
1452            request_id: raw_mock_flexible_canister_http_response.request_id,
1453            responses: raw_mock_flexible_canister_http_response.responses,
1454        }
1455    }
1456}
1457
1458impl From<MockFlexibleCanisterHttpResponse> for RawMockFlexibleCanisterHttpResponse {
1459    fn from(mock_flexible_canister_http_response: MockFlexibleCanisterHttpResponse) -> Self {
1460        Self {
1461            subnet_id: RawSubnetId {
1462                subnet_id: mock_flexible_canister_http_response
1463                    .subnet_id
1464                    .as_slice()
1465                    .to_vec(),
1466            },
1467            request_id: mock_flexible_canister_http_response.request_id,
1468            responses: mock_flexible_canister_http_response.responses,
1469        }
1470    }
1471}
1472
1473#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
1474pub struct RawCanisterSnapshotDownload {
1475    pub sender: RawPrincipalId,
1476    pub canister_id: RawCanisterId,
1477    #[serde(deserialize_with = "base64::deserialize")]
1478    #[serde(serialize_with = "base64::serialize")]
1479    pub snapshot_id: Vec<u8>,
1480    pub snapshot_dir: PathBuf,
1481}
1482
1483#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
1484pub struct RawCanisterSnapshotUpload {
1485    pub sender: RawPrincipalId,
1486    pub canister_id: RawCanisterId,
1487    pub replace_snapshot: Option<RawCanisterSnapshotId>,
1488    pub snapshot_dir: PathBuf,
1489}
1490
1491#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
1492pub struct RawCanisterSnapshotId {
1493    #[serde(deserialize_with = "base64::deserialize")]
1494    #[serde(serialize_with = "base64::serialize")]
1495    pub snapshot_id: Vec<u8>,
1496}