Skip to main content

dfx_core/config/model/
dfinity.rs

1#![allow(dead_code)]
2#![allow(clippy::should_implement_trait)] // for from_str.  why now?
3use crate::config::directories::get_user_dfx_config_dir;
4use crate::config::model::bitcoin_adapter::BitcoinAdapterLogLevel;
5use crate::config::model::canister_http_adapter::HttpAdapterLogLevel;
6use crate::config::model::extension_canister_type::apply_extension_canister_types;
7use crate::error::config::{GetOutputEnvFileError, GetTempPathError};
8use crate::error::dfx_config::AddDependenciesError::CanisterCircularDependency;
9use crate::error::dfx_config::GetCanisterNamesWithDependenciesError::AddDependenciesFailed;
10use crate::error::dfx_config::GetComputeAllocationError::GetComputeAllocationFailed;
11use crate::error::dfx_config::GetFreezingThresholdError::GetFreezingThresholdFailed;
12use crate::error::dfx_config::GetLogVisibilityError::GetLogVisibilityFailed;
13use crate::error::dfx_config::GetMemoryAllocationError::GetMemoryAllocationFailed;
14use crate::error::dfx_config::GetPullCanistersError::PullCanistersSameId;
15use crate::error::dfx_config::GetRemoteCanisterIdError::GetRemoteCanisterIdFailed;
16use crate::error::dfx_config::GetReservedCyclesLimitError::GetReservedCyclesLimitFailed;
17use crate::error::dfx_config::GetSpecifiedIdError::GetSpecifiedIdFailed;
18use crate::error::dfx_config::GetWasmMemoryLimitError::GetWasmMemoryLimitFailed;
19use crate::error::dfx_config::GetWasmMemoryThresholdError::GetWasmMemoryThresholdFailed;
20use crate::error::dfx_config::{
21    AddDependenciesError, GetCanisterConfigError, GetCanisterNamesWithDependenciesError,
22    GetComputeAllocationError, GetFreezingThresholdError, GetLogVisibilityError,
23    GetMemoryAllocationError, GetPullCanistersError, GetRemoteCanisterIdError,
24    GetReservedCyclesLimitError, GetSpecifiedIdError, GetWasmMemoryLimitError,
25    GetWasmMemoryThresholdError,
26};
27use crate::error::fs::CanonicalizePathError;
28use crate::error::load_dfx_config::LoadDfxConfigError;
29use crate::error::load_dfx_config::LoadDfxConfigError::{
30    DetermineCurrentWorkingDirFailed, ResolveConfigPath,
31};
32use crate::error::load_networks_config::LoadNetworksConfigError;
33use crate::error::load_networks_config::LoadNetworksConfigError::{
34    GetConfigPathFailed as GetNetworkConfigPathFailed,
35    LoadConfigFromFileFailed as LoadNetworkConfigFromFileFailed,
36};
37use crate::error::load_tool_config::ToolConfigError;
38use crate::error::load_tool_config::ToolConfigError::{
39    GetConfigPathFailed as GetToolConfigPathFailed,
40    LoadConfigFromFileFailed as LoadToolConfigFromFileFailed, SaveDefaultConfigFailed,
41};
42use crate::error::socket_addr_conversion::SocketAddrConversionError;
43use crate::error::socket_addr_conversion::SocketAddrConversionError::{
44    EmptyIterator, ParseSocketAddrFailed,
45};
46use crate::error::structured_file::StructuredFileError;
47use crate::error::structured_file::StructuredFileError::DeserializeJsonFileFailed;
48use crate::extension::manager::ExtensionManager;
49use crate::fs::create_dir_all;
50use crate::json::structure::{PossiblyStr, SerdeVec};
51use crate::json::{load_json_file, save_json_file};
52use crate::util::ByteSchema;
53use byte_unit::Byte;
54use candid::Principal;
55use clap::ValueEnum;
56use ic_utils::interfaces::management_canister::LogVisibility;
57use schemars::JsonSchema;
58use serde::de::{Error as _, MapAccess, Visitor};
59use serde::{Deserialize, Deserializer, Serialize};
60use serde_json::Value;
61use std::collections::{BTreeMap, BTreeSet, HashSet};
62use std::default::Default;
63use std::fmt::{self, Debug, Display, Formatter};
64use std::net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs};
65use std::path::{Path, PathBuf};
66use std::time::Duration;
67
68use super::network_descriptor::MOTOKO_PLAYGROUND_CANISTER_TIMEOUT_SECONDS;
69
70pub const CONFIG_FILE_NAME: &str = "dfx.json";
71
72pub const BUILTIN_CANISTER_TYPES: [&str; 5] = ["rust", "motoko", "assets", "custom", "pull"];
73
74const EMPTY_CONFIG_DEFAULTS: ConfigDefaults = ConfigDefaults {
75    bitcoin: None,
76    dogecoin: None,
77    bootstrap: None,
78    build: None,
79    canister_http: None,
80    proxy: None,
81    replica: None,
82};
83
84const EMPTY_CONFIG_DEFAULTS_BUILD: ConfigDefaultsBuild = ConfigDefaultsBuild {
85    packtool: None,
86    args: None,
87};
88
89/// # Remote Canister Configuration
90/// This field allows canisters to be marked 'remote' for certain networks.
91/// On networks where this canister contains a remote ID, the canister is not deployed.
92/// Instead it is assumed to exist already under control of a different project.
93#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
94pub struct ConfigCanistersCanisterRemote {
95    /// # Remote Candid File
96    /// On networks where this canister is marked 'remote', this candid file is used instead of the one declared in the canister settings.
97    pub candid: Option<PathBuf>,
98
99    /// # Network to Remote ID Mapping
100    /// This field contains mappings from network names to remote canister IDs (Principals).
101    /// For all networks listed here, this canister is considered 'remote'.
102    #[schemars(with = "BTreeMap<String, String>")]
103    pub id: BTreeMap<String, Principal>,
104}
105
106/// # Wasm Optimization Levels
107/// Wasm optimization levels that are passed to `wasm-opt`. "cycles" defaults to O3, "size" defaults to Oz.
108/// O4 through O0 focus on performance (with O0 performing no optimizations), and Oz and Os focus on reducing binary size, where Oz is more aggressive than Os.
109/// O3 and Oz empirically give best cycle savings and code size savings respectively.
110#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
111pub enum WasmOptLevel {
112    #[serde(rename = "cycles")]
113    Cycles,
114    #[serde(rename = "size")]
115    Size,
116    O4,
117    O3,
118    O2,
119    O1,
120    O0,
121    Oz,
122    Os,
123}
124impl Display for WasmOptLevel {
125    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
126        Debug::fmt(self, f)
127    }
128}
129
130#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
131#[serde(rename_all = "lowercase")]
132pub enum MetadataVisibility {
133    /// Anyone can query the metadata
134    #[default]
135    Public,
136
137    /// Only the controllers of the canister can query the metadata.
138    Private,
139}
140
141/// # Canister Metadata Configuration
142/// Configures a custom metadata section for the canister wasm.
143/// dfx uses the first definition of a given name matching the current network, ignoring any of the same name that follow.
144#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
145pub struct CanisterMetadataSection {
146    /// # Name
147    /// The name of the wasm section
148    pub name: String,
149
150    /// # Visibility
151    #[serde(default)]
152    pub visibility: MetadataVisibility,
153
154    /// # Networks
155    /// Networks this section applies to.
156    /// If this field is absent, then it applies to all networks.
157    /// An empty array means this element will not apply to any network.
158    pub networks: Option<BTreeSet<String>>,
159
160    /// # Path
161    /// Path to file containing section contents.
162    /// Conflicts with `content`.
163    /// For sections with name=`candid:service`, this field is optional, and if not specified, dfx will use
164    /// the canister's candid definition.
165    /// If specified for a Motoko canister, the service defined in the specified path must be a valid subtype of the canister's
166    /// actual candid service definition.
167    pub path: Option<PathBuf>,
168
169    /// # Content
170    /// Content of this metadata section.
171    /// Conflicts with `path`.
172    pub content: Option<String>,
173}
174
175impl CanisterMetadataSection {
176    pub fn applies_to_network(&self, network: &str) -> bool {
177        self.networks
178            .as_ref()
179            .map(|networks| networks.contains(network))
180            .unwrap_or(true)
181    }
182}
183
184#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
185pub struct Pullable {
186    /// # wasm_url
187    /// The Url to download canister wasm.
188    pub wasm_url: String,
189    /// # wasm_hash
190    /// SHA256 hash of the wasm module located at wasm_url.
191    /// Only define this if the on-chain canister wasm is expected not to match the wasm at wasm_url.
192    /// The hash can also be specified via a URL using the `wasm_hash_url` field.
193    /// If both are defined, the `wasm_hash_url` field will be ignored.
194    pub wasm_hash: Option<String>,
195    /// # wasm_hash_url
196    /// Specify the SHA256 hash of the wasm module via this URL.
197    /// Only define this if the on-chain canister wasm is expected not to match the wasm at wasm_url.
198    /// The hash can also be specified directly using the `wasm_hash` field.
199    /// If both are defined, the `wasm_hash_url` field will be ignored.
200    pub wasm_hash_url: Option<String>,
201    /// # dependencies
202    /// Canister IDs (Principal) of direct dependencies.
203    #[schemars(with = "Vec::<String>")]
204    pub dependencies: Vec<Principal>,
205    /// # init_guide
206    /// A message to guide consumers how to initialize the canister.
207    pub init_guide: String,
208    /// # init_arg
209    /// A default initialization argument for the canister that consumers can use.
210    pub init_arg: Option<String>,
211}
212
213pub type TechStackCategoryMap = BTreeMap<String, BTreeMap<String, String>>;
214
215/// # Tech Stack
216/// The tech stack used to build a canister.
217#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
218pub struct TechStack {
219    /// # cdk
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub cdk: Option<TechStackCategoryMap>,
222    /// # language
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub language: Option<TechStackCategoryMap>,
225    /// # lib
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub lib: Option<TechStackCategoryMap>,
228    /// # tool
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub tool: Option<TechStackCategoryMap>,
231    /// # other
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub other: Option<TechStackCategoryMap>,
234}
235
236pub const DEFAULT_SHARED_LOCAL_BIND: &str = "127.0.0.1:4943"; // hex for "IC"
237pub const DEFAULT_PROJECT_LOCAL_BIND: &str = "127.0.0.1:8000";
238pub const DEFAULT_IC_GATEWAY: &str = "https://icp0.io";
239pub const DEFAULT_IC_GATEWAY_TRAILING_SLASH: &str = "https://icp0.io/";
240pub const DEFAULT_REPLICA_PORT: u16 = 8080;
241
242/// # Canister Configuration
243/// Configurations for a single canister.
244#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
245pub struct ConfigCanistersCanister {
246    /// # Declarations Configuration
247    /// Defines which canister interface declarations to generate,
248    /// and where to generate them.
249    #[serde(default)]
250    pub declarations: CanisterDeclarationsConfig,
251
252    /// # Remote Configuration
253    /// Used to mark the canister as 'remote' on certain networks.
254    #[serde(default)]
255    pub remote: Option<ConfigCanistersCanisterRemote>,
256
257    /// # Canister-Specific Build Argument
258    /// This field defines an additional argument to pass to the Motoko compiler when building the canister.
259    pub args: Option<String>,
260
261    /// # Resource Allocation Settings
262    /// Defines initial values for resource allocation settings.
263    #[serde(default)]
264    pub initialization_values: InitializationValues,
265
266    /// # Dependencies
267    /// Defines on which canisters this canister depends on.
268    #[serde(default)]
269    pub dependencies: Vec<String>,
270
271    /// # Force Frontend URL
272    /// Mostly unused.
273    /// If this value is not null, a frontend URL is displayed after deployment even if the canister type is not 'asset'.
274    pub frontend: Option<BTreeMap<String, String>>,
275
276    /// # Type-Specific Canister Properties
277    /// Depending on the canister type, different fields are required.
278    /// These are defined in this object.
279    #[serde(flatten)]
280    pub type_specific: CanisterTypeProperties,
281
282    /// # Pre-Install Commands
283    /// One or more commands to run pre canister installation.
284    /// These commands are executed in the root of the project.
285    #[serde(default)]
286    pub pre_install: SerdeVec<String>,
287
288    /// # Post-Install Commands
289    /// One or more commands to run post canister installation.
290    /// These commands are executed in the root of the project.
291    #[serde(default)]
292    pub post_install: SerdeVec<String>,
293
294    /// # Path to Canister Entry Point
295    /// Entry point for e.g. Motoko Compiler.
296    pub main: Option<PathBuf>,
297
298    /// # Shrink Canister Wasm
299    /// Whether run `ic-wasm shrink` after building the Canister.
300    /// Enabled by default for Rust/Motoko canisters.
301    /// Disabled by default for custom canisters.
302    pub shrink: Option<bool>,
303
304    /// # Optimize Canister Wasm
305    /// Invoke wasm level optimizations after building the canister. Optimization level can be set to "cycles" to optimize for cycle usage, "size" to optimize for binary size, or any of "O4, O3, O2, O1, O0, Oz, Os".
306    /// Disabled by default.
307    /// If this option is specified, the `shrink` option will be ignored.
308    #[serde(default)]
309    pub optimize: Option<WasmOptLevel>,
310
311    /// # Metadata
312    /// Defines metadata sections to set in the canister .wasm
313    #[serde(default)]
314    pub metadata: Vec<CanisterMetadataSection>,
315
316    /// # Pullable
317    /// Defines required properties so that this canister is ready for `dfx deps pull` by other projects.
318    #[serde(default)]
319    pub pullable: Option<Pullable>,
320
321    /// # Tech Stack
322    /// Defines the tech stack used to build this canister.
323    #[serde(default)]
324    pub tech_stack: Option<TechStack>,
325
326    /// # Gzip Canister Wasm
327    /// Disabled by default.
328    pub gzip: Option<bool>,
329
330    /// # Specified Canister ID
331    /// Attempts to create the canister with this Canister ID.
332    /// This option only works with non-mainnet replica.
333    /// If the `--specified-id` argument is also provided, this `specified_id` field will be ignored.
334    #[schemars(with = "Option<String>")]
335    pub specified_id: Option<Principal>,
336
337    /// # Init Arg
338    /// The Candid initialization argument for installing the canister.
339    /// If the `--argument` or `--argument-file` argument is also provided, this `init_arg` field will be ignored.
340    pub init_arg: Option<String>,
341
342    /// # Init Arg File
343    /// The Candid initialization argument file for installing the canister.
344    /// If the `--argument` or `--argument-file` argument is also provided, this `init_arg_file` field will be ignored.
345    pub init_arg_file: Option<String>,
346}
347
348#[derive(Clone, Debug, Serialize, JsonSchema)]
349#[serde(tag = "type", rename_all = "snake_case")]
350pub enum CanisterTypeProperties {
351    /// # Rust-Specific Properties
352    Rust {
353        /// # Package Name
354        /// Name of the Rust package that compiles this canister's Wasm.
355        package: String,
356
357        /// # Crate name
358        /// Name of the Rust crate that compiles to this canister's Wasm.
359        /// If left unspecified, defaults to the crate with the same name as the package.
360        #[serde(rename = "crate")]
361        crate_name: Option<String>,
362
363        /// # Candid File
364        /// Path of this canister's candid interface declaration.
365        candid: PathBuf,
366
367        /// # `cargo-audit` check
368        /// If set to true, does not run `cargo audit` before building.
369        #[serde(default)]
370        skip_cargo_audit: bool,
371    },
372    /// # Asset-Specific Properties
373    Assets {
374        /// # Asset Source Folder
375        /// Folders from which assets are uploaded.
376        source: Vec<PathBuf>,
377
378        /// # Build Commands
379        /// Commands that are executed in order to produce this canister's assets.
380        /// Expected to produce assets in one of the paths specified by the 'source' field.
381        /// Optional if there is no build necessary or the assets can be built using the default `npm run build` command.
382        #[schemars(default)]
383        build: SerdeVec<String>,
384
385        /// # NPM workspace
386        /// The workspace in package.json that this canister is in, if it is not in the root workspace.
387        workspace: Option<String>,
388    },
389    /// # Custom-Specific Properties
390    Custom {
391        /// # Wasm Path
392        /// Path to Wasm to be installed. URLs to a Wasm module are also acceptable.
393        /// A canister that has a URL to a Wasm module can not also have `build` steps.
394        wasm: String,
395
396        /// # Candid File
397        /// Path to this canister's candid interface declaration.  A URL to a candid file is also acceptable.
398        candid: String,
399
400        /// # Build Commands
401        /// Commands that are executed in order to produce this canister's Wasm module.
402        /// Expected to produce the Wasm in the path specified by the 'wasm' field.
403        /// No build commands are allowed if the `wasm` field is a URL.
404        /// These commands are executed in the root of the project.
405        #[schemars(default)]
406        build: SerdeVec<String>,
407    },
408    /// # Motoko-Specific Properties
409    Motoko,
410    /// # Pull-Specific Properties
411    Pull {
412        /// # Canister ID
413        /// Principal of the canister on the ic network.
414        #[schemars(with = "String")]
415        id: Principal,
416    },
417}
418
419impl CanisterTypeProperties {
420    pub fn name(&self) -> &'static str {
421        match self {
422            Self::Rust { .. } => "rust",
423            Self::Motoko { .. } => "motoko",
424            Self::Assets { .. } => "assets",
425            Self::Custom { .. } => "custom",
426            Self::Pull { .. } => "pull",
427        }
428    }
429}
430
431#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
432#[serde(rename_all = "snake_case")]
433pub enum CanisterLogVisibility {
434    #[default]
435    Controllers,
436    Public,
437    #[schemars(with = "Vec::<String>")]
438    AllowedViewers(Vec<Principal>),
439}
440
441impl From<CanisterLogVisibility> for LogVisibility {
442    fn from(value: CanisterLogVisibility) -> Self {
443        match value {
444            CanisterLogVisibility::Controllers => LogVisibility::Controllers,
445            CanisterLogVisibility::Public => LogVisibility::Public,
446            CanisterLogVisibility::AllowedViewers(viewers) => {
447                LogVisibility::AllowedViewers(viewers)
448            }
449        }
450    }
451}
452
453/// # Initial Resource Allocations
454#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
455#[serde(default)]
456pub struct InitializationValues {
457    /// # Compute Allocation
458    /// Must be a number between 0 and 100, inclusively.
459    /// It indicates how much compute power should be guaranteed to this canister, expressed as a percentage of the maximum compute power that a single canister can allocate.
460    pub compute_allocation: Option<PossiblyStr<u64>>,
461
462    /// # Memory Allocation
463    /// Maximum memory (in bytes) this canister is allowed to occupy.
464    /// Can be specified as an integer, or as an SI unit string (e.g. "4KB", "2 MiB")
465    #[schemars(with = "Option<ByteSchema>")]
466    pub memory_allocation: Option<Byte>,
467
468    /// # Freezing Threshold
469    /// Freezing threshould of the canister, measured in seconds.
470    /// Valid inputs are numbers (seconds) or strings parsable by humantime (e.g. "15days 2min 2s").
471    #[serde(with = "humantime_serde")]
472    #[schemars(with = "Option<String>")]
473    pub freezing_threshold: Option<Duration>,
474
475    /// # Reserved Cycles Limit
476    /// Specifies the upper limit of the canister's reserved cycles balance.
477    ///
478    /// Reserved cycles are cycles that the system sets aside for future use by the canister.
479    /// If a subnet's storage exceeds 750 GiB, then every time a canister allocates new storage bytes,
480    /// the system sets aside some amount of cycles from the main balance of the canister.
481    /// These reserved cycles will be used to cover future payments for the newly allocated bytes.
482    /// The reserved cycles are not transferable and the amount of reserved cycles depends on how full the subnet is.
483    ///
484    /// A setting of 0 means that the canister will trap if it tries to allocate new storage while the subnet's memory usage exceeds 750 GiB.
485    #[schemars(with = "Option<u128>")]
486    pub reserved_cycles_limit: Option<u128>,
487
488    /// # Wasm Memory Limit
489    /// Specifies a soft limit (in bytes) on the Wasm memory usage of the canister.
490    ///
491    /// Update calls, timers, heartbeats, installs, and post-upgrades fail if the
492    /// Wasm memory usage exceeds this limit. The main purpose of this setting is
493    /// to protect against the case when the canister reaches the hard 4GiB
494    /// limit.
495    ///
496    /// Must be a number of bytes between 0 and 2^48 (i.e. 256 TiB), inclusive.
497    /// Can be specified as an integer, or as an SI unit string (e.g. "4KB", "2 MiB")
498    #[schemars(with = "Option<ByteSchema>")]
499    pub wasm_memory_limit: Option<Byte>,
500
501    /// # Wasm Memory Threshold
502    ///
503    /// Specifies a threshold (in bytes) on the Wasm memory usage of the canister,
504    /// as a distance from `wasm_memory_limit`.
505    ///
506    /// When the remaining memory before the limit drops below this threshold, its
507    /// `on_low_wasm_memory` hook will be invoked. This enables it to self-optimize,
508    /// or raise an alert, or otherwise attempt to prevent itself from reaching
509    /// `wasm_memory_limit`.
510    ///
511    /// Must be a number of bytes between 0 and 2^48 (i.e. 256 TiB), inclusive.
512    /// Can be specified as an integer, or as an SI unit string (e.g. "4KB", "2 MiB")
513    #[schemars(with = "Option<ByteSchema>")]
514    pub wasm_memory_threshold: Option<Byte>,
515
516    /// # Log Visibility
517    /// Specifies who is allowed to read the canister's logs.
518    ///
519    /// Can be "public", "controllers" or "allowed_viewers" with a list of principals.
520    #[schemars(with = "Option<CanisterLogVisibility>")]
521    pub log_visibility: Option<CanisterLogVisibility>,
522}
523
524/// # Declarations Configuration
525/// Configurations about which canister interface declarations to generate,
526/// and where to generate them.
527#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
528pub struct CanisterDeclarationsConfig {
529    /// # Declaration Output Directory
530    /// Directory to place declarations for that canister.
531    /// Default is 'src/declarations/<canister_name>'.
532    pub output: Option<PathBuf>,
533
534    /// # Languages to generate
535    /// A list of languages to generate type declarations.
536    /// Supported options are 'js', 'ts', 'did', 'mo'.
537    /// Default is ['js', 'ts', 'did'].
538    pub bindings: Option<Vec<String>>,
539
540    /// # Canister ID ENV Override
541    /// A string that will replace process.env.CANISTER_ID_{canister_name_uppercase}
542    /// in the 'src/dfx/assets/language_bindings/canister.js' template.
543    pub env_override: Option<String>,
544
545    /// # Node compatibility flag
546    /// Flag to pre-populate generated declarations with better defaults for various types of projects
547    /// Default is false
548    #[serde(default)]
549    pub node_compatibility: bool,
550}
551
552/// # Bitcoin Adapter Configuration
553#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
554pub struct ConfigDefaultsBitcoin {
555    /// # Enable Bitcoin Adapter
556    #[serde(default)]
557    pub enabled: bool,
558
559    /// # Available Nodes
560    /// Addresses of nodes to connect to (in case discovery from seeds is not possible/sufficient).
561    #[serde(default)]
562    pub nodes: Option<Vec<SocketAddr>>,
563
564    /// # Logging Level
565    /// The logging level of the adapter.
566    #[serde(default = "default_bitcoin_log_level")]
567    pub log_level: BitcoinAdapterLogLevel,
568
569    /// # Initialization Argument
570    /// The initialization argument for the bitcoin canister.
571    #[serde(default = "default_bitcoin_canister_init_arg")]
572    pub canister_init_arg: String,
573}
574
575pub fn default_bitcoin_log_level() -> BitcoinAdapterLogLevel {
576    BitcoinAdapterLogLevel::Info
577}
578
579pub fn default_bitcoin_canister_init_arg() -> String {
580    "(record { stability_threshold = 0 : nat; network = variant { regtest }; blocks_source = principal \"aaaaa-aa\"; fees = record { get_utxos_base = 50000000 : nat; get_utxos_cycles_per_ten_instructions = 10 : nat; get_utxos_maximum = 10000000000 : nat; get_balance = 10000000 : nat; get_balance_maximum = 100000000 : nat; get_block_headers_base = 50000000 : nat; get_block_headers_cycles_per_ten_instructions = 10 : nat; get_block_headers_maximum = 10000000000 : nat; get_current_fee_percentiles = 10000000 : nat; get_current_fee_percentiles_maximum = 100000000 : nat; send_transaction_base = 5000000000 : nat; send_transaction_per_byte = 20000000 : nat; }; syncing = variant { enabled }; api_access = variant { enabled }; disable_api_if_not_fully_synced = variant { enabled }})".to_string()
581}
582
583impl Default for ConfigDefaultsBitcoin {
584    fn default() -> Self {
585        ConfigDefaultsBitcoin {
586            enabled: false,
587            nodes: None,
588            log_level: default_bitcoin_log_level(),
589            canister_init_arg: default_bitcoin_canister_init_arg(),
590        }
591    }
592}
593
594/// # Dogecoin Adapter Configuration
595#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
596pub struct ConfigDefaultsDogecoin {
597    /// # Enable Dogecoin Adapter
598    #[serde(default)]
599    pub enabled: bool,
600
601    /// # Available Nodes
602    /// Addresses of nodes to connect to (in case discovery from seeds is not possible/sufficient).
603    #[serde(default)]
604    pub nodes: Option<Vec<SocketAddr>>,
605}
606
607/// # HTTP Adapter Configuration
608#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
609pub struct ConfigDefaultsCanisterHttp {
610    /// # Enable HTTP Adapter
611    #[serde(default = "default_as_true")]
612    pub enabled: bool,
613
614    /// # Logging Level
615    /// The logging level of the adapter.
616    #[serde(default)]
617    pub log_level: HttpAdapterLogLevel,
618}
619
620impl Default for ConfigDefaultsCanisterHttp {
621    fn default() -> Self {
622        ConfigDefaultsCanisterHttp {
623            enabled: true,
624            log_level: HttpAdapterLogLevel::default(),
625        }
626    }
627}
628
629fn default_as_true() -> bool {
630    // sigh https://github.com/serde-rs/serde/issues/368
631    true
632}
633
634/// # Bootstrap Server Configuration
635/// The bootstrap command has been removed.  All of these fields are ignored.
636#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
637pub struct ConfigDefaultsBootstrap {
638    /// Specifies the IP address that the bootstrap server listens on. Defaults to 127.0.0.1.
639    #[serde(default = "default_bootstrap_ip")]
640    pub ip: IpAddr,
641
642    /// Specifies the port number that the bootstrap server listens on. Defaults to 8081.
643    #[serde(default = "default_bootstrap_port")]
644    pub port: u16,
645
646    /// Specifies the maximum number of seconds that the bootstrap server
647    /// will wait for upstream requests to complete. Defaults to 30.
648    #[serde(default = "default_bootstrap_timeout")]
649    pub timeout: u64,
650}
651
652pub fn default_bootstrap_ip() -> IpAddr {
653    IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))
654}
655pub fn default_bootstrap_port() -> u16 {
656    8081
657}
658pub fn default_bootstrap_timeout() -> u64 {
659    30
660}
661
662impl Default for ConfigDefaultsBootstrap {
663    fn default() -> Self {
664        ConfigDefaultsBootstrap {
665            ip: default_bootstrap_ip(),
666            port: default_bootstrap_port(),
667            timeout: default_bootstrap_timeout(),
668        }
669    }
670}
671
672/// # Build Process Configuration
673#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
674pub struct ConfigDefaultsBuild {
675    /// Main command to run the packtool.
676    /// This command is executed in the root of the project.
677    pub packtool: Option<String>,
678
679    /// Arguments for packtool.
680    pub args: Option<String>,
681}
682
683#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
684#[serde(rename_all = "lowercase")]
685pub enum ReplicaLogLevel {
686    Critical,
687    Error,
688    Warning,
689    Info,
690    Debug,
691    Trace,
692}
693
694impl Default for ReplicaLogLevel {
695    fn default() -> Self {
696        Self::Error
697    }
698}
699
700impl ReplicaLogLevel {
701    pub fn to_ic_starter_string(&self) -> String {
702        match self {
703            Self::Critical => "critical".to_string(),
704            Self::Error => "error".to_string(),
705            Self::Warning => "warning".to_string(),
706            Self::Info => "info".to_string(),
707            Self::Debug => "debug".to_string(),
708            Self::Trace => "trace".to_string(),
709        }
710    }
711    pub fn to_pocketic_string(&self) -> String {
712        match self {
713            Self::Critical => "CRITICAL".to_string(),
714            Self::Error => "ERROR".to_string(),
715            Self::Warning => "WARN".to_string(),
716            Self::Info => "INFO".to_string(),
717            Self::Debug => "DEBUG".to_string(),
718            Self::Trace => "TRACE".to_string(),
719        }
720    }
721}
722
723/// # Local Replica Configuration
724#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
725pub struct ConfigDefaultsReplica {
726    /// Port the replica listens on.
727    pub port: Option<u16>,
728
729    /// # Subnet Type
730    /// Determines the subnet type the replica will run as.
731    /// Affects things like cycles accounting, message size limits, cycle limits.
732    /// Defaults to 'application'.
733    pub subnet_type: Option<ReplicaSubnetType>,
734
735    /// Run replica with the provided log level. Default is 'error'. Debug prints still get displayed
736    pub log_level: Option<ReplicaLogLevel>,
737}
738
739/// Configuration for the HTTP gateway.
740#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
741pub struct ConfigDefaultsProxy {
742    /// A list of domains that can be served. These are used for canister resolution [default: localhost]
743    pub domain: Option<SerdeVec<String>>,
744}
745
746// Schemars doesn't add the enum value's docstrings. Therefore the explanations have to be up here.
747/// # Network Type
748/// Type 'ephemeral' is used for networks that are regularly reset.
749/// Type 'persistent' is used for networks that last for a long time and where it is preferred that canister IDs get stored in source control.
750#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
751#[serde(rename_all = "lowercase")]
752pub enum NetworkType {
753    // We store ephemeral canister ids in .dfx/{network}/canister_ids.json
754    #[default]
755    Ephemeral,
756
757    // We store persistent canister ids in canister_ids.json (adjacent to dfx.json)
758    Persistent,
759}
760
761impl NetworkType {
762    fn ephemeral() -> Self {
763        NetworkType::Ephemeral
764    }
765    fn persistent() -> Self {
766        NetworkType::Persistent
767    }
768}
769
770#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
771#[serde(rename_all = "lowercase")]
772pub enum ReplicaSubnetType {
773    System,
774    #[default]
775    Application,
776    VerifiedApplication,
777}
778
779impl ReplicaSubnetType {
780    /// Converts the value to the string expected by ic-starter for its --subnet-type argument
781    pub fn as_ic_starter_string(&self) -> String {
782        match self {
783            ReplicaSubnetType::System => "system".to_string(),
784            ReplicaSubnetType::Application => "application".to_string(),
785            ReplicaSubnetType::VerifiedApplication => "verified_application".to_string(),
786        }
787    }
788}
789
790fn default_playground_timeout_seconds() -> u64 {
791    MOTOKO_PLAYGROUND_CANISTER_TIMEOUT_SECONDS
792}
793
794/// Playground config to borrow canister from instead of creating new canisters.
795#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
796pub struct PlaygroundConfig {
797    /// Canister ID of the playground canister
798    pub playground_canister: String,
799
800    /// How many seconds a canister can be borrowed for
801    #[serde(default = "default_playground_timeout_seconds")]
802    pub timeout_seconds: u64,
803}
804
805/// # Custom Network Configuration
806#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
807pub struct ConfigNetworkProvider {
808    /// The URL(s) this network can be reached at.
809    pub providers: Vec<String>,
810
811    /// Persistence type of this network.
812    #[serde(default = "NetworkType::persistent")]
813    pub r#type: NetworkType,
814    pub playground: Option<PlaygroundConfig>,
815}
816
817/// # Local Replica Configuration
818#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
819pub struct ConfigLocalProvider {
820    /// Bind address for the webserver.
821    /// For the shared local network, the default is 127.0.0.1:4943.
822    /// For project-specific local networks, the default is 127.0.0.1:8000.
823    pub bind: Option<String>,
824
825    /// Persistence type of this network.
826    #[serde(default = "NetworkType::ephemeral")]
827    pub r#type: NetworkType,
828
829    pub bitcoin: Option<ConfigDefaultsBitcoin>,
830    pub dogecoin: Option<ConfigDefaultsDogecoin>,
831    pub bootstrap: Option<ConfigDefaultsBootstrap>,
832    pub canister_http: Option<ConfigDefaultsCanisterHttp>,
833    pub replica: Option<ConfigDefaultsReplica>,
834    pub playground: Option<PlaygroundConfig>,
835    pub proxy: Option<ConfigDefaultsProxy>,
836}
837
838#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
839#[serde(untagged)]
840pub enum ConfigNetwork {
841    ConfigNetworkProvider(ConfigNetworkProvider),
842    ConfigLocalProvider(ConfigLocalProvider),
843}
844
845#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
846pub enum Profile {
847    // debug is for development only
848    Debug,
849    // release is for production
850    Release,
851}
852
853/// Defaults to use on dfx start.
854#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
855pub struct ConfigDefaults {
856    pub bitcoin: Option<ConfigDefaultsBitcoin>,
857    pub dogecoin: Option<ConfigDefaultsDogecoin>,
858    pub bootstrap: Option<ConfigDefaultsBootstrap>,
859    pub build: Option<ConfigDefaultsBuild>,
860    pub canister_http: Option<ConfigDefaultsCanisterHttp>,
861    pub proxy: Option<ConfigDefaultsProxy>,
862    pub replica: Option<ConfigDefaultsReplica>,
863}
864
865/// # dfx.json
866#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
867pub struct ConfigInterface {
868    pub profile: Option<Profile>,
869
870    /// Used to keep track of dfx.json versions.
871    pub version: Option<u32>,
872
873    /// # dfx version
874    /// Pins the dfx version for this project.
875    pub dfx: Option<String>,
876
877    /// Mapping between canisters and their settings.
878    pub canisters: Option<BTreeMap<String, ConfigCanistersCanister>>,
879
880    /// Defaults for dfx start.
881    pub defaults: Option<ConfigDefaults>,
882
883    /// Mapping between network names and their configurations.
884    /// Networks 'ic' and 'local' are implicitly defined.
885    pub networks: Option<BTreeMap<String, ConfigNetwork>>,
886
887    /// If set, environment variables will be output to this file (without overwriting any user-defined variables, if the file already exists).
888    pub output_env_file: Option<PathBuf>,
889}
890
891pub type TopLevelConfigNetworks = BTreeMap<String, ConfigNetwork>;
892
893#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
894pub struct NetworksConfigInterface {
895    pub networks: TopLevelConfigNetworks,
896}
897
898impl ConfigCanistersCanister {}
899
900pub fn to_socket_addr(s: &str) -> Result<SocketAddr, SocketAddrConversionError> {
901    match s.to_socket_addrs() {
902        Ok(mut a) => match a.next() {
903            Some(res) => Ok(res),
904            None => Err(EmptyIterator(s.to_string())),
905        },
906        Err(err) => Err(ParseSocketAddrFailed(s.to_string(), err)),
907    }
908}
909
910impl ConfigDefaultsBuild {
911    pub fn get_packtool(&self) -> Option<String> {
912        match &self.packtool {
913            Some(v) if !v.is_empty() => self.packtool.to_owned(),
914            _ => None,
915        }
916    }
917    pub fn get_args(&self) -> Option<String> {
918        match &self.args {
919            Some(v) if !v.is_empty() => self.args.to_owned(),
920            _ => None,
921        }
922    }
923}
924
925impl ConfigDefaults {
926    pub fn get_build(&self) -> &ConfigDefaultsBuild {
927        match &self.build {
928            Some(x) => x,
929            None => &EMPTY_CONFIG_DEFAULTS_BUILD,
930        }
931    }
932}
933
934impl NetworksConfigInterface {
935    pub fn get_network(&self, name: &str) -> Option<&ConfigNetwork> {
936        self.networks.get(name)
937    }
938}
939
940impl ConfigInterface {
941    pub fn get_defaults(&self) -> &ConfigDefaults {
942        match &self.defaults {
943            Some(v) => v,
944            _ => &EMPTY_CONFIG_DEFAULTS,
945        }
946    }
947
948    pub fn get_network(&self, name: &str) -> Option<&ConfigNetwork> {
949        self.networks
950            .as_ref()
951            .and_then(|networks| networks.get(name))
952    }
953
954    pub fn get_version(&self) -> u32 {
955        self.version.unwrap_or(1)
956    }
957    pub fn get_dfx(&self) -> Option<String> {
958        self.dfx.to_owned()
959    }
960
961    /// Return the names of the specified canister and all of its dependencies.
962    /// If none specified, return the names of all canisters.
963    pub fn get_canister_names_with_dependencies(
964        &self,
965        some_canister: Option<&str>,
966    ) -> Result<Vec<String>, GetCanisterNamesWithDependenciesError> {
967        self.canisters
968            .as_ref()
969            .ok_or(GetCanisterNamesWithDependenciesError::CanistersFieldDoesNotExist())
970            .and_then(|canister_map| match some_canister {
971                Some(specific_canister) => {
972                    let mut names = HashSet::new();
973                    let mut path = vec![];
974                    add_dependencies(canister_map, &mut names, &mut path, specific_canister)
975                        .map(|_| names.into_iter().collect())
976                        .map_err(|err| AddDependenciesFailed(specific_canister.to_string(), err))
977                }
978                None => Ok(canister_map.keys().cloned().collect()),
979            })
980    }
981
982    pub fn get_remote_canister_id(
983        &self,
984        canister: &str,
985        network: &str,
986    ) -> Result<Option<Principal>, GetRemoteCanisterIdError> {
987        let maybe_principal = self
988            .get_canister_config(canister)
989            .map_err(|e| {
990                GetRemoteCanisterIdFailed(
991                    Box::new(canister.to_string()),
992                    Box::new(network.to_string()),
993                    e,
994                )
995            })?
996            .remote
997            .as_ref()
998            .and_then(|r| r.id.get(network))
999            .copied();
1000
1001        Ok(maybe_principal)
1002    }
1003
1004    pub fn is_remote_canister(
1005        &self,
1006        canister: &str,
1007        network: &str,
1008    ) -> Result<bool, GetRemoteCanisterIdError> {
1009        Ok(self.get_remote_canister_id(canister, network)?.is_some())
1010    }
1011
1012    pub fn get_compute_allocation(
1013        &self,
1014        canister_name: &str,
1015    ) -> Result<Option<u64>, GetComputeAllocationError> {
1016        Ok(self
1017            .get_canister_config(canister_name)
1018            .map_err(|e| GetComputeAllocationFailed(canister_name.to_string(), e))?
1019            .initialization_values
1020            .compute_allocation
1021            .map(|x| x.0))
1022    }
1023
1024    pub fn get_memory_allocation(
1025        &self,
1026        canister_name: &str,
1027    ) -> Result<Option<Byte>, GetMemoryAllocationError> {
1028        Ok(self
1029            .get_canister_config(canister_name)
1030            .map_err(|e| GetMemoryAllocationFailed(canister_name.to_string(), e))?
1031            .initialization_values
1032            .memory_allocation)
1033    }
1034
1035    pub fn get_freezing_threshold(
1036        &self,
1037        canister_name: &str,
1038    ) -> Result<Option<Duration>, GetFreezingThresholdError> {
1039        Ok(self
1040            .get_canister_config(canister_name)
1041            .map_err(|e| GetFreezingThresholdFailed(canister_name.to_string(), e))?
1042            .initialization_values
1043            .freezing_threshold)
1044    }
1045
1046    pub fn get_reserved_cycles_limit(
1047        &self,
1048        canister_name: &str,
1049    ) -> Result<Option<u128>, GetReservedCyclesLimitError> {
1050        Ok(self
1051            .get_canister_config(canister_name)
1052            .map_err(|e| GetReservedCyclesLimitFailed(canister_name.to_string(), e))?
1053            .initialization_values
1054            .reserved_cycles_limit)
1055    }
1056
1057    pub fn get_wasm_memory_limit(
1058        &self,
1059        canister_name: &str,
1060    ) -> Result<Option<Byte>, GetWasmMemoryLimitError> {
1061        Ok(self
1062            .get_canister_config(canister_name)
1063            .map_err(|e| GetWasmMemoryLimitFailed(canister_name.to_string(), e))?
1064            .initialization_values
1065            .wasm_memory_limit)
1066    }
1067
1068    pub fn get_wasm_memory_threshold(
1069        &self,
1070        canister_name: &str,
1071    ) -> Result<Option<Byte>, GetWasmMemoryThresholdError> {
1072        Ok(self
1073            .get_canister_config(canister_name)
1074            .map_err(|e| GetWasmMemoryThresholdFailed(canister_name.to_string(), e))?
1075            .initialization_values
1076            .wasm_memory_threshold)
1077    }
1078
1079    pub fn get_log_visibility(
1080        &self,
1081        canister_name: &str,
1082    ) -> Result<Option<LogVisibility>, GetLogVisibilityError> {
1083        Ok(self
1084            .get_canister_config(canister_name)
1085            .map_err(|e| GetLogVisibilityFailed(canister_name.to_string(), e))?
1086            .initialization_values
1087            .log_visibility
1088            .clone()
1089            .map(|visibility| visibility.into()))
1090    }
1091
1092    fn get_canister_config(
1093        &self,
1094        canister_name: &str,
1095    ) -> Result<&ConfigCanistersCanister, GetCanisterConfigError> {
1096        self.canisters
1097            .as_ref()
1098            .ok_or(GetCanisterConfigError::CanistersFieldDoesNotExist())?
1099            .get(canister_name)
1100            .ok_or_else(|| GetCanisterConfigError::CanisterNotFound(canister_name.to_string()))
1101    }
1102
1103    pub fn get_pull_canisters(&self) -> Result<BTreeMap<String, Principal>, GetPullCanistersError> {
1104        let mut res = BTreeMap::new();
1105        let mut id_to_name: BTreeMap<Principal, &String> = BTreeMap::new();
1106        if let Some(map) = &self.canisters {
1107            for (k, v) in map {
1108                if let CanisterTypeProperties::Pull { id } = v.type_specific {
1109                    if let Some(other_name) = id_to_name.get(&id) {
1110                        return Err(PullCanistersSameId(other_name.to_string(), k.clone(), id));
1111                    }
1112                    res.insert(k.clone(), id);
1113                    id_to_name.insert(id, k);
1114                }
1115            }
1116        };
1117        Ok(res)
1118    }
1119
1120    pub fn get_specified_id(
1121        &self,
1122        canister_name: &str,
1123    ) -> Result<Option<Principal>, GetSpecifiedIdError> {
1124        Ok(self
1125            .get_canister_config(canister_name)
1126            .map_err(|e| GetSpecifiedIdFailed(canister_name.to_string(), e))?
1127            .specified_id)
1128    }
1129}
1130
1131fn add_dependencies(
1132    all_canisters: &BTreeMap<String, ConfigCanistersCanister>,
1133    names: &mut HashSet<String>,
1134    path: &mut Vec<String>,
1135    canister_name: &str,
1136) -> Result<(), AddDependenciesError> {
1137    let inserted = names.insert(String::from(canister_name));
1138
1139    if !inserted {
1140        return if path.contains(&String::from(canister_name)) {
1141            path.push(String::from(canister_name));
1142            Err(CanisterCircularDependency(path.clone()))
1143        } else {
1144            Ok(())
1145        };
1146    }
1147
1148    let canister_config = all_canisters
1149        .get(canister_name)
1150        .ok_or_else(|| AddDependenciesError::CanisterNotFound(canister_name.to_string()))?;
1151
1152    path.push(String::from(canister_name));
1153
1154    for canister in &canister_config.dependencies {
1155        add_dependencies(all_canisters, names, path, canister)?;
1156    }
1157
1158    path.pop();
1159
1160    Ok(())
1161}
1162
1163#[derive(Clone, Debug)]
1164pub struct Config {
1165    path: PathBuf,
1166    json: Value,
1167    // public interface to the config:
1168    pub config: ConfigInterface,
1169}
1170
1171#[allow(dead_code)]
1172impl Config {
1173    fn resolve_config_path(working_dir: &Path) -> Result<Option<PathBuf>, CanonicalizePathError> {
1174        let mut curr = crate::fs::canonicalize(working_dir)?;
1175        while curr.parent().is_some() {
1176            if curr.join(CONFIG_FILE_NAME).is_file() {
1177                return Ok(Some(curr.join(CONFIG_FILE_NAME)));
1178            } else {
1179                curr.pop();
1180            }
1181        }
1182
1183        // Have to check if the config could be in the root (e.g. on VMs / CI).
1184        if curr.join(CONFIG_FILE_NAME).is_file() {
1185            return Ok(Some(curr.join(CONFIG_FILE_NAME)));
1186        }
1187
1188        Ok(None)
1189    }
1190
1191    fn from_file(
1192        path: &Path,
1193        extension_manager: Option<&ExtensionManager>,
1194    ) -> Result<Config, LoadDfxConfigError> {
1195        let content = crate::fs::read(path)?;
1196        Config::from_slice(path.to_path_buf(), &content, extension_manager)
1197    }
1198
1199    pub fn from_dir(
1200        working_dir: &Path,
1201        extension_manager: Option<&ExtensionManager>,
1202    ) -> Result<Option<Config>, LoadDfxConfigError> {
1203        let path = Config::resolve_config_path(working_dir).map_err(ResolveConfigPath)?;
1204        path.map(|path| Config::from_file(&path, extension_manager))
1205            .transpose()
1206    }
1207
1208    pub fn from_current_dir(
1209        extension_manager: Option<&ExtensionManager>,
1210    ) -> Result<Option<Config>, LoadDfxConfigError> {
1211        let working_dir = std::env::current_dir().map_err(DetermineCurrentWorkingDirFailed)?;
1212        Config::from_dir(&working_dir, extension_manager)
1213    }
1214
1215    fn from_slice(
1216        path: PathBuf,
1217        content: &[u8],
1218        extension_manager: Option<&ExtensionManager>,
1219    ) -> Result<Config, LoadDfxConfigError> {
1220        let json: Value = serde_json::from_slice(content)
1221            .map_err(|e| LoadDfxConfigError::DeserializeValueFailed(Box::new(path.clone()), e))?;
1222        let effective_json = apply_extension_canister_types(json.clone(), extension_manager)?;
1223
1224        let config = serde_json::from_value(effective_json)
1225            .map_err(|e| LoadDfxConfigError::DeserializeValueFailed(Box::new(path.clone()), e))?;
1226        Ok(Config { path, json, config })
1227    }
1228
1229    /// Create a configuration from a string.
1230    #[cfg(test)]
1231    pub(crate) fn from_str(content: &str) -> Result<Config, StructuredFileError> {
1232        Ok(Config::from_slice(PathBuf::from("-"), content.as_bytes(), None).unwrap())
1233    }
1234
1235    #[cfg(test)]
1236    pub(crate) fn from_str_and_path(
1237        path: PathBuf,
1238        content: &str,
1239    ) -> Result<Config, StructuredFileError> {
1240        Ok(Config::from_slice(path, content.as_bytes(), None).unwrap())
1241    }
1242
1243    pub fn get_path(&self) -> &PathBuf {
1244        &self.path
1245    }
1246    pub fn get_temp_path(&self) -> Result<PathBuf, GetTempPathError> {
1247        let path = self.get_path().parent().unwrap().join(".dfx");
1248        create_dir_all(&path)?;
1249        Ok(path)
1250    }
1251    pub fn get_json(&self) -> &Value {
1252        &self.json
1253    }
1254    pub fn get_mut_json(&mut self) -> &mut Value {
1255        &mut self.json
1256    }
1257    pub fn get_config(&self) -> &ConfigInterface {
1258        &self.config
1259    }
1260
1261    pub fn get_project_root(&self) -> &Path {
1262        // a configuration path contains a file name specifically. As
1263        // such we should be returning at least root as parent. If
1264        // this is invariance is broken, we must fail.
1265        self.path.parent().expect(
1266            "An incorrect configuration path was set with no parent, i.e. did not include root",
1267        )
1268    }
1269
1270    // returns the path to the output env file if any, guaranteed to be
1271    // a child relative to the project root
1272    pub fn get_output_env_file(
1273        &self,
1274        from_cmdline: Option<PathBuf>,
1275    ) -> Result<Option<PathBuf>, GetOutputEnvFileError> {
1276        from_cmdline
1277            .or(self.config.output_env_file.clone())
1278            .map(|p| {
1279                if p.is_relative() {
1280                    let p = self.get_project_root().join(p);
1281
1282                    // cannot canonicalize a path that doesn't exist, but the parent should exist
1283                    let env_parent = crate::fs::parent(&p)?;
1284                    let env_parent = crate::fs::canonicalize(&env_parent)?;
1285                    if !env_parent.starts_with(self.get_project_root()) {
1286                        Err(GetOutputEnvFileError::OutputEnvFileMustBeInProjectRoot(p))
1287                    } else {
1288                        Ok(self.get_project_root().join(p))
1289                    }
1290                } else {
1291                    Err(GetOutputEnvFileError::OutputEnvFileMustBeRelative(p))
1292                }
1293            })
1294            .transpose()
1295    }
1296
1297    pub fn save(&self) -> Result<(), StructuredFileError> {
1298        save_json_file(&self.path, &self.json)
1299    }
1300}
1301
1302// grumble grumble https://github.com/serde-rs/serde/issues/2231
1303impl<'de> Deserialize<'de> for CanisterTypeProperties {
1304    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1305    where
1306        D: Deserializer<'de>,
1307    {
1308        deserializer.deserialize_map(PropertiesVisitor)
1309    }
1310}
1311
1312struct PropertiesVisitor;
1313
1314impl<'de> Visitor<'de> for PropertiesVisitor {
1315    type Value = CanisterTypeProperties;
1316    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1317        f.write_str("canister type metadata")
1318    }
1319    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1320    where
1321        A: MapAccess<'de>,
1322    {
1323        let missing_field = A::Error::missing_field;
1324        let mut wasm = None;
1325        let mut candid = None;
1326        let mut package = None;
1327        let mut skip_cargo_audit = None;
1328        let mut crate_name = None;
1329        let mut source = None;
1330        let mut build = None;
1331        let mut r#type = None;
1332        let mut id = None;
1333        let mut workspace = None;
1334        while let Some(key) = map.next_key::<String>()? {
1335            match &*key {
1336                "package" => package = Some(map.next_value()?),
1337                "crate" => crate_name = Some(map.next_value()?),
1338                "source" => source = Some(map.next_value()?),
1339                "candid" => candid = Some(map.next_value()?),
1340                "build" => build = Some(map.next_value()?),
1341                "wasm" => wasm = Some(map.next_value()?),
1342                "type" => r#type = Some(map.next_value::<String>()?),
1343                "id" => id = Some(map.next_value()?),
1344                "workspace" => workspace = Some(map.next_value()?),
1345                "skip_cargo_audit" => skip_cargo_audit = Some(map.next_value()?),
1346                _ => continue,
1347            }
1348        }
1349        let props = match r#type.as_deref() {
1350            Some("motoko") | None => CanisterTypeProperties::Motoko,
1351            Some("rust") => CanisterTypeProperties::Rust {
1352                candid: PathBuf::from(candid.ok_or_else(|| missing_field("candid"))?),
1353                package: package.ok_or_else(|| missing_field("package"))?,
1354                skip_cargo_audit: skip_cargo_audit.unwrap_or(false),
1355                crate_name,
1356            },
1357            Some("assets") => CanisterTypeProperties::Assets {
1358                source: source.ok_or_else(|| missing_field("source"))?,
1359                build: build.unwrap_or_default(),
1360                workspace,
1361            },
1362            Some("custom") => CanisterTypeProperties::Custom {
1363                build: build.unwrap_or_default(),
1364                candid: candid.ok_or_else(|| missing_field("candid"))?,
1365                wasm: wasm.ok_or_else(|| missing_field("wasm"))?,
1366            },
1367            Some("pull") => CanisterTypeProperties::Pull {
1368                id: id.ok_or_else(|| missing_field("id"))?,
1369            },
1370            Some(x) => return Err(A::Error::unknown_variant(x, &BUILTIN_CANISTER_TYPES)),
1371        };
1372        Ok(props)
1373    }
1374}
1375
1376#[derive(Clone)]
1377pub struct NetworksConfig {
1378    path: PathBuf,
1379    json: Value,
1380    // public interface to the networks config:
1381    networks_config: NetworksConfigInterface,
1382}
1383
1384impl NetworksConfig {
1385    pub fn get_path(&self) -> &PathBuf {
1386        &self.path
1387    }
1388    pub fn get_interface(&self) -> &NetworksConfigInterface {
1389        &self.networks_config
1390    }
1391
1392    pub fn new() -> Result<NetworksConfig, LoadNetworksConfigError> {
1393        let dir = get_user_dfx_config_dir().map_err(GetNetworkConfigPathFailed)?;
1394
1395        let path = dir.join("networks.json");
1396        if path.exists() {
1397            NetworksConfig::from_file(&path).map_err(LoadNetworkConfigFromFileFailed)
1398        } else {
1399            Ok(NetworksConfig {
1400                path,
1401                json: Default::default(),
1402                networks_config: NetworksConfigInterface {
1403                    networks: BTreeMap::new(),
1404                },
1405            })
1406        }
1407    }
1408
1409    fn from_file(path: &Path) -> Result<NetworksConfig, StructuredFileError> {
1410        let content = crate::fs::read(path)?;
1411
1412        let networks: BTreeMap<String, ConfigNetwork> = serde_json::from_slice(&content)
1413            .map_err(|e| DeserializeJsonFileFailed(Box::new(path.to_path_buf()), e))?;
1414        let networks_config = NetworksConfigInterface { networks };
1415        let json = serde_json::from_slice(&content)
1416            .map_err(|e| DeserializeJsonFileFailed(Box::new(path.to_path_buf()), e))?;
1417        let path = PathBuf::from(path);
1418        Ok(NetworksConfig {
1419            path,
1420            json,
1421            networks_config,
1422        })
1423    }
1424}
1425
1426pub struct ToolConfig {
1427    path: PathBuf,
1428    json: Value,
1429    tool_config: ToolConfigInterface,
1430}
1431
1432#[derive(Serialize, Deserialize, JsonSchema)]
1433pub struct ToolConfigInterface {
1434    pub telemetry: TelemetryState,
1435}
1436
1437impl ToolConfig {
1438    pub fn path(&self) -> &PathBuf {
1439        &self.path
1440    }
1441    pub fn interface(&self) -> &ToolConfigInterface {
1442        &self.tool_config
1443    }
1444
1445    pub fn interface_mut(&mut self) -> &mut ToolConfigInterface {
1446        &mut self.tool_config
1447    }
1448
1449    pub fn new() -> Result<Self, ToolConfigError> {
1450        let dir = get_user_dfx_config_dir().map_err(GetToolConfigPathFailed)?;
1451
1452        let path = dir.join("config.json");
1453        if path.exists() {
1454            Self::from_file(&path).map_err(LoadToolConfigFromFileFailed)
1455        } else {
1456            let default = Self {
1457                path,
1458                json: Default::default(),
1459                tool_config: ToolConfigInterface {
1460                    telemetry: TelemetryState::On,
1461                },
1462            };
1463            default.save()?;
1464            Ok(default)
1465        }
1466    }
1467
1468    pub fn save(&self) -> Result<(), ToolConfigError> {
1469        self.save_to_file(&self.path)
1470            .map_err(SaveDefaultConfigFailed)
1471    }
1472
1473    pub fn config_path(&self) -> &Path {
1474        &self.path
1475    }
1476
1477    fn from_file(path: &Path) -> Result<Self, StructuredFileError> {
1478        let json: Value = load_json_file(path)?;
1479        let tool_config: ToolConfigInterface = serde_json::from_value(json.clone())
1480            .map_err(|e| DeserializeJsonFileFailed(Box::new(path.to_path_buf()), e))?;
1481        let path = PathBuf::from(path);
1482        Ok(Self {
1483            path,
1484            json,
1485            tool_config,
1486        })
1487    }
1488
1489    fn save_to_file(&self, path: &Path) -> Result<(), StructuredFileError> {
1490        save_json_file(path, &self.tool_config)?;
1491        Ok(())
1492    }
1493}
1494
1495#[derive(Serialize, Deserialize, Copy, Clone, JsonSchema, PartialEq, Eq, ValueEnum)]
1496#[serde(rename_all = "snake_case")]
1497pub enum TelemetryState {
1498    On,
1499    Off,
1500    Local,
1501}
1502
1503impl TelemetryState {
1504    pub fn should_collect(&self) -> bool {
1505        *self != TelemetryState::Off
1506    }
1507    pub fn should_publish(&self) -> bool {
1508        *self == TelemetryState::On
1509    }
1510}
1511
1512impl Display for TelemetryState {
1513    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1514        Display::fmt(
1515            match self {
1516                Self::On => "on",
1517                Self::Off => "off",
1518                Self::Local => "local",
1519            },
1520            f,
1521        )
1522    }
1523}
1524
1525#[cfg(test)]
1526mod tests {
1527    use super::*;
1528
1529    #[test]
1530    fn find_dfinity_config_current_path() {
1531        let root_dir = tempfile::tempdir().unwrap();
1532        let root_path = root_dir.keep().canonicalize().unwrap();
1533        let config_path = root_path.join("foo/fah/bar").join(CONFIG_FILE_NAME);
1534
1535        std::fs::create_dir_all(config_path.parent().unwrap()).unwrap();
1536        std::fs::write(&config_path, "{}").unwrap();
1537
1538        assert_eq!(
1539            config_path,
1540            Config::resolve_config_path(config_path.parent().unwrap())
1541                .unwrap()
1542                .unwrap(),
1543        );
1544    }
1545
1546    #[test]
1547    fn find_dfinity_config_parent() {
1548        let root_dir = tempfile::tempdir().unwrap();
1549        let root_path = root_dir.keep().canonicalize().unwrap();
1550        let config_path = root_path.join("foo/fah/bar").join(CONFIG_FILE_NAME);
1551
1552        std::fs::create_dir_all(config_path.parent().unwrap()).unwrap();
1553        std::fs::write(&config_path, "{}").unwrap();
1554
1555        assert!(
1556            Config::resolve_config_path(config_path.parent().unwrap().parent().unwrap())
1557                .unwrap()
1558                .is_none()
1559        );
1560    }
1561
1562    #[test]
1563    fn find_dfinity_config_subdir() {
1564        let root_dir = tempfile::tempdir().unwrap();
1565        let root_path = root_dir.keep().canonicalize().unwrap();
1566        let config_path = root_path.join("foo/fah/bar").join(CONFIG_FILE_NAME);
1567        let subdir_path = config_path.parent().unwrap().join("baz/blue");
1568
1569        std::fs::create_dir_all(&subdir_path).unwrap();
1570        std::fs::write(&config_path, "{}").unwrap();
1571
1572        assert_eq!(
1573            config_path,
1574            Config::resolve_config_path(subdir_path.as_path())
1575                .unwrap()
1576                .unwrap(),
1577        );
1578    }
1579
1580    #[test]
1581    fn local_defaults_to_ephemeral() {
1582        let config = Config::from_str(
1583            r#"{
1584            "networks": {
1585                "local": {
1586                    "bind": "localhost:8000"
1587                }
1588            }
1589        }"#,
1590        )
1591        .unwrap();
1592
1593        let network = config.get_config().get_network("local").unwrap();
1594        if let ConfigNetwork::ConfigLocalProvider(local_network) = network {
1595            assert_eq!(local_network.r#type, NetworkType::Ephemeral);
1596        } else {
1597            panic!("not a local provider");
1598        }
1599    }
1600
1601    #[test]
1602    fn local_can_override_to_persistent() {
1603        let config = Config::from_str(
1604            r#"{
1605            "networks": {
1606                "local": {
1607                    "bind": "localhost:8000",
1608                    "type": "persistent"
1609                }
1610            }
1611        }"#,
1612        )
1613        .unwrap();
1614
1615        let network = config.get_config().get_network("local").unwrap();
1616        if let ConfigNetwork::ConfigLocalProvider(local_network) = network {
1617            assert_eq!(local_network.r#type, NetworkType::Persistent);
1618        } else {
1619            panic!("not a local provider");
1620        }
1621    }
1622
1623    #[test]
1624    fn network_defaults_to_persistent() {
1625        let config = Config::from_str(
1626            r#"{
1627            "networks": {
1628                "somewhere": {
1629                    "providers": [ "https://1.2.3.4:5000" ]
1630                }
1631            }
1632        }"#,
1633        )
1634        .unwrap();
1635
1636        let network = config.get_config().get_network("somewhere").unwrap();
1637        if let ConfigNetwork::ConfigNetworkProvider(network_provider) = network {
1638            assert_eq!(network_provider.r#type, NetworkType::Persistent);
1639        } else {
1640            panic!("not a network provider");
1641        }
1642    }
1643
1644    #[test]
1645    fn network_can_override_to_ephemeral() {
1646        let config = Config::from_str(
1647            r#"{
1648            "networks": {
1649                "staging": {
1650                    "providers": [ "https://1.2.3.4:5000" ],
1651                    "type": "ephemeral"
1652                }
1653            }
1654        }"#,
1655        )
1656        .unwrap();
1657
1658        let network = config.get_config().get_network("staging").unwrap();
1659        if let ConfigNetwork::ConfigNetworkProvider(network_provider) = network {
1660            assert_eq!(network_provider.r#type, NetworkType::Ephemeral);
1661        } else {
1662            panic!("not a network provider");
1663        }
1664
1665        assert_eq!(
1666            config.get_config().get_network("staging").unwrap(),
1667            &ConfigNetwork::ConfigNetworkProvider(ConfigNetworkProvider {
1668                providers: vec![String::from("https://1.2.3.4:5000")],
1669                r#type: NetworkType::Ephemeral,
1670                playground: None,
1671            })
1672        );
1673    }
1674
1675    #[test]
1676    fn get_correct_initialization_values() {
1677        let config = Config::from_str(
1678            r#"{
1679              "canisters": {
1680                "test_project": {
1681                  "initialization_values": {
1682                    "compute_allocation" : "100",
1683                    "memory_allocation": "8GB"
1684                  }
1685                }
1686              }
1687        }"#,
1688        )
1689        .unwrap();
1690
1691        let config_interface = config.get_config();
1692        let compute_allocation = config_interface
1693            .get_compute_allocation("test_project")
1694            .unwrap()
1695            .unwrap();
1696        assert_eq!(100, compute_allocation);
1697
1698        let memory_allocation = config_interface
1699            .get_memory_allocation("test_project")
1700            .unwrap()
1701            .unwrap();
1702        assert_eq!("8GB".parse::<Byte>().unwrap(), memory_allocation);
1703
1704        let config_no_values = Config::from_str(
1705            r#"{
1706              "canisters": {
1707                "test_project_two": {
1708                }
1709              }
1710        }"#,
1711        )
1712        .unwrap();
1713        let config_interface = config_no_values.get_config();
1714        let compute_allocation = config_interface
1715            .get_compute_allocation("test_project_two")
1716            .unwrap();
1717        let memory_allocation = config_interface
1718            .get_memory_allocation("test_project_two")
1719            .unwrap();
1720        assert_eq!(None, compute_allocation);
1721        assert_eq!(None, memory_allocation);
1722    }
1723
1724    #[test]
1725    fn tech_stack_category_deterministic_serialization() {
1726        let first = build_and_serialize();
1727        // Repeat `build and serialize` many times and assert equality
1728        for _ in 0..10 {
1729            let next = build_and_serialize();
1730            assert_eq!(first, next);
1731        }
1732    }
1733
1734    fn build_and_serialize() -> String {
1735        use super::TechStackCategoryMap;
1736        use std::collections::BTreeMap;
1737        let mut data: TechStackCategoryMap = BTreeMap::new();
1738        data.insert("typescript".into(), BTreeMap::new());
1739        data.insert("javascript".into(), BTreeMap::new());
1740        serde_json::to_string(&data).unwrap()
1741    }
1742}