Skip to main content

dfx_core/network/
provider.rs

1use crate::config::directories::{
2    get_shared_network_data_directory, get_shared_wallet_config_path,
3};
4use crate::config::model::dfinity::{
5    Config, ConfigDefaults, ConfigLocalProvider, ConfigNetwork, DEFAULT_PROJECT_LOCAL_BIND,
6    DEFAULT_SHARED_LOCAL_BIND, NetworkType, NetworksConfig,
7};
8use crate::config::model::local_server_descriptor::{
9    LocalNetworkScopeDescriptor, LocalServerDescriptor,
10};
11use crate::config::model::network_descriptor::{
12    NetworkDescriptor, NetworkTypeDescriptor, PLAYGROUND_NETWORK_NAME,
13};
14use crate::error::network_config::NetworkConfigError::{
15    self, DetermineSharedNetworkDirectoryFailed, NetworkNotFound, NoNetworkContext,
16    NoProvidersForNetwork, ParsePortValueFailed, ParseProviderUrlFailed, ReadWebserverPortFailed,
17};
18use crate::identity::WALLET_CONFIG_FILENAME;
19use crate::util;
20use lazy_static::lazy_static;
21use serde_json::json;
22use slog::{Logger, debug, info, warn};
23use std::path::{Display, Path, PathBuf};
24use std::sync::{Arc, RwLock};
25use url::Url;
26
27lazy_static! {
28    static ref NETWORK_CONTEXT: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
29}
30
31fn set_network_context(network: Option<String>) {
32    let name = network.unwrap_or_else(|| "local".to_string());
33
34    let mut n = NETWORK_CONTEXT.write().unwrap();
35    *n = Some(name);
36}
37
38pub fn get_network_context() -> Result<String, NetworkConfigError> {
39    NETWORK_CONTEXT
40        .read()
41        .unwrap()
42        .clone()
43        .ok_or(NoNetworkContext())
44}
45
46pub enum LocalBindDetermination {
47    /// Use value from configuration
48    AsConfigured,
49
50    /// Get port of running server from webserver-port file
51    ApplyRunningWebserverPort,
52}
53
54fn config_network_to_network_descriptor(
55    network_name: &str,
56    config_network: &ConfigNetwork,
57    project_defaults: Option<&ConfigDefaults>,
58    data_directory: PathBuf,
59    local_scope: LocalNetworkScopeDescriptor,
60    ephemeral_wallet_config_path: Option<PathBuf>,
61    local_bind_determination: &LocalBindDetermination,
62    default_local_bind: &str,
63    legacy_pid_path: Option<PathBuf>,
64) -> Result<NetworkDescriptor, NetworkConfigError> {
65    match config_network {
66        ConfigNetwork::ConfigNetworkProvider(network_provider) => {
67            let providers = if !network_provider.providers.is_empty() {
68                network_provider
69                    .providers
70                    .iter()
71                    .map(|provider| parse_provider_url(provider))
72                    .collect::<Result<_, NetworkConfigError>>()
73            } else {
74                Err(NoProvidersForNetwork(network_name.to_string()))
75            }?;
76            let playground = network_provider.playground.clone();
77            let is_ic = NetworkDescriptor::is_ic(network_name, &providers);
78            Ok(NetworkDescriptor {
79                name: network_name.to_string(),
80                providers,
81                r#type: NetworkTypeDescriptor::new(
82                    network_provider.r#type,
83                    ephemeral_wallet_config_path,
84                    playground,
85                )?,
86                is_ic,
87                is_ad_hoc: false,
88                local_server_descriptor: None,
89            })
90        }
91        ConfigNetwork::ConfigLocalProvider(local_provider) => {
92            let bitcoin = local_provider
93                .bitcoin
94                .clone()
95                .or_else(|| project_defaults.and_then(|x| x.bitcoin.clone()))
96                .unwrap_or_default();
97            let dogecoin = local_provider
98                .dogecoin
99                .clone()
100                .or_else(|| project_defaults.and_then(|x| x.dogecoin.clone()))
101                .unwrap_or_default();
102            let canister_http = local_provider
103                .canister_http
104                .clone()
105                .or_else(|| project_defaults.and_then(|x| x.canister_http.clone()))
106                .unwrap_or_default();
107            let proxy = local_provider
108                .proxy
109                .clone()
110                .or_else(|| project_defaults.and_then(|x| x.proxy.clone()))
111                .unwrap_or_default();
112            let replica = local_provider
113                .replica
114                .clone()
115                .or_else(|| project_defaults.and_then(|x| x.replica.clone()))
116                .unwrap_or_default();
117            let playground = local_provider.playground.clone();
118
119            let network_type = NetworkTypeDescriptor::new(
120                local_provider.r#type,
121                ephemeral_wallet_config_path,
122                playground,
123            )?;
124            let bind_address = get_local_bind_address(
125                local_provider,
126                local_bind_determination,
127                &data_directory,
128                default_local_bind,
129            )?;
130            let provider_url = format!("http://{bind_address}");
131            let providers = vec![parse_provider_url(&provider_url)?];
132            let local_server_descriptor = LocalServerDescriptor::new(
133                data_directory,
134                bind_address,
135                bitcoin,
136                dogecoin,
137                canister_http,
138                proxy,
139                replica,
140                local_scope,
141                legacy_pid_path,
142            )?;
143            Ok(NetworkDescriptor {
144                name: network_name.to_string(),
145                providers,
146                r#type: network_type,
147                is_ic: false,
148                is_ad_hoc: false,
149                local_server_descriptor: Some(local_server_descriptor),
150            })
151        }
152    }
153}
154
155pub fn create_network_descriptor(
156    project_config: Option<Arc<Config>>,
157    shared_config: Arc<NetworksConfig>,
158    network: Option<String>,
159    logger: Option<Logger>,
160    local_bind_determination: LocalBindDetermination,
161) -> Result<NetworkDescriptor, NetworkConfigError> {
162    let logger = (logger.clone()).unwrap_or_else(|| Logger::root(slog::Discard, slog::o!()));
163
164    set_network_context(network);
165    let network_name = get_network_context()?;
166
167    create_mainnet_network_descriptor(&network_name, &logger)
168        .or_else(|| {
169            create_project_network_descriptor(
170                &network_name,
171                project_config.clone(),
172                &local_bind_determination,
173                &logger,
174            )
175        })
176        .or_else(|| {
177            let project_config_for_warnings_only = project_config;
178            create_shared_network_descriptor(
179                &network_name,
180                shared_config,
181                project_config_for_warnings_only,
182                &local_bind_determination,
183                &logger,
184            )
185        })
186        .or_else(|| create_default_network_from_name(&network_name, &logger).map(Ok))
187        .or_else(|| create_url_based_network_descriptor(&network_name))
188        .unwrap_or(Err(NetworkNotFound(network_name)))
189}
190
191fn create_mainnet_network_descriptor(
192    network_name: &str,
193    logger: &Logger,
194) -> Option<Result<NetworkDescriptor, NetworkConfigError>> {
195    if network_name == "ic" {
196        info!(
197            logger,
198            "Using built-in definition for network 'ic' (mainnet)"
199        );
200        Some(Ok(NetworkDescriptor::ic()))
201    } else {
202        None
203    }
204}
205
206fn create_url_based_network_descriptor(
207    network_name: &str,
208) -> Option<Result<NetworkDescriptor, NetworkConfigError>> {
209    parse_provider_url(network_name).ok().map(|url| {
210        // Replace any non-ascii-alphanumeric characters with `_`, to create an
211        // OS-friendly directory name for it.
212        let name = util::network_to_pathcompat(network_name);
213        let is_ic = NetworkDescriptor::is_ic(&name, &vec![url.to_string()]);
214        let data_directory = get_shared_network_data_directory(network_name)
215            .map_err(DetermineSharedNetworkDirectoryFailed)?;
216        let network_type = NetworkTypeDescriptor::new(
217            NetworkType::Ephemeral,
218            Some(data_directory.join(WALLET_CONFIG_FILENAME)),
219            None,
220        )?;
221        Ok(NetworkDescriptor {
222            name,
223            providers: vec![url],
224            r#type: network_type,
225            is_ic,
226            is_ad_hoc: true,
227            local_server_descriptor: None,
228        })
229    })
230}
231
232fn create_shared_network_descriptor(
233    network_name: &str,
234    shared_config: Arc<NetworksConfig>,
235    project_config_for_warnings_only: Option<Arc<Config>>,
236    local_bind_determination: &LocalBindDetermination,
237    logger: &Logger,
238) -> Option<Result<NetworkDescriptor, NetworkConfigError>> {
239    let shared_config_file_exists = shared_config.get_path().is_file();
240    let shared_config_display_path = shared_config.get_path().display();
241    let network = shared_config.get_interface().get_network(network_name);
242    let network = match (network_name, network) {
243        ("local", None) => {
244            info!(
245                logger,
246                "Using the default configuration for the local shared network."
247            );
248
249            Some(ConfigNetwork::ConfigLocalProvider(ConfigLocalProvider {
250                bind: Some(String::from(DEFAULT_SHARED_LOCAL_BIND)),
251                r#type: NetworkType::Ephemeral,
252                bitcoin: None,
253                dogecoin: None,
254                bootstrap: None,
255                canister_http: None,
256                replica: None,
257                playground: None,
258                proxy: None,
259            }))
260        }
261        (network_name, None) => {
262            if shared_config_file_exists {
263                debug!(
264                    logger,
265                    "There is no shared network '{}' defined in {}",
266                    &shared_config_display_path,
267                    network_name
268                );
269            } else {
270                debug!(
271                    logger,
272                    "There is no shared network '{}' because {} does not exist.",
273                    network_name,
274                    &shared_config_display_path
275                );
276            }
277            None
278        }
279        (network_name, Some(network)) => {
280            info!(
281                logger,
282                "Using shared network '{}' defined in {}",
283                network_name,
284                shared_config.get_path().display()
285            );
286            Some(network.clone())
287        }
288    };
289
290    network.as_ref().map(|config_network| {
291        warn_if_ignoring_project_defaults_in_shared_network(
292            network_name,
293            &shared_config_display_path,
294            project_config_for_warnings_only,
295            logger,
296        );
297
298        let data_directory = get_shared_network_data_directory(network_name)?;
299
300        let ephemeral_wallet_config_path = get_shared_wallet_config_path(network_name)?;
301
302        let local_scope = LocalNetworkScopeDescriptor::shared(&data_directory);
303        config_network_to_network_descriptor(
304            network_name,
305            config_network,
306            None,
307            data_directory,
308            local_scope,
309            ephemeral_wallet_config_path,
310            local_bind_determination,
311            DEFAULT_SHARED_LOCAL_BIND,
312            None,
313        )
314    })
315}
316
317fn warn_if_ignoring_project_defaults_in_shared_network(
318    network_name: &str,
319    shared_config_display_path: &Display,
320    project_config_for_warnings_only: Option<Arc<Config>>,
321    logger: &Logger,
322) {
323    if let Some(project_config_for_warnings_only) = project_config_for_warnings_only {
324        let defaults = project_config_for_warnings_only.get_json().get("defaults");
325        let bitcoin = defaults.and_then(|x| x.get("bitcoin")).cloned();
326        let replica = defaults.and_then(|x| x.get("replica")).cloned();
327        let canister_http = defaults.and_then(|x| x.get("canister_http")).cloned();
328
329        if bitcoin.is_some() || replica.is_some() || canister_http.is_some() {
330            let mut example_network_json = json!({});
331            if let Some(bitcoin) = bitcoin {
332                example_network_json["bitcoin"] = bitcoin;
333            }
334            if let Some(replica) = replica {
335                example_network_json["replica"] = replica;
336            }
337            if let Some(canister_http) = canister_http {
338                example_network_json["canister_http"] = canister_http;
339            }
340            let example_networks_json = json!({
341                network_name: example_network_json
342            });
343
344            let example_networks_json =
345                serde_json::to_string_pretty(&example_networks_json).unwrap();
346            warn!(
347                logger,
348                "Ignoring the 'defaults' field in dfx.json because project settings never apply to the shared network.\n\
349                    To apply these settings to the shared network, define them in {shared_config_display_path} like so:\n\
350                    {example_networks_json}",
351                shared_config_display_path = &shared_config_display_path,
352                example_networks_json = &example_networks_json
353            );
354        }
355    }
356}
357
358fn create_project_network_descriptor(
359    network_name: &str,
360    project_config: Option<Arc<Config>>,
361    local_bind_determination: &LocalBindDetermination,
362    logger: &Logger,
363) -> Option<Result<NetworkDescriptor, NetworkConfigError>> {
364    if let Some(config) = project_config {
365        if let Some(config_network) = config.get_config().get_network(network_name) {
366            info!(
367                logger,
368                "Using project-specific network '{}' defined in {}",
369                network_name,
370                config.get_path().display(),
371            );
372
373            let temp_path = match config.get_temp_path() {
374                Ok(temp_path) => temp_path,
375                Err(e) => {
376                    return Some(Err(NetworkConfigError::GetTempPath(e)));
377                }
378            };
379            let data_directory = temp_path.join("network").join(network_name);
380            let legacy_pid_path = Some(temp_path.join("pid"));
381            let ephemeral_wallet_config_path = temp_path.join("local").join(WALLET_CONFIG_FILENAME);
382            Some(config_network_to_network_descriptor(
383                network_name,
384                config_network,
385                Some(config.get_config().get_defaults()),
386                data_directory,
387                LocalNetworkScopeDescriptor::Project,
388                Some(ephemeral_wallet_config_path),
389                local_bind_determination,
390                DEFAULT_PROJECT_LOCAL_BIND,
391                legacy_pid_path,
392            ))
393        } else {
394            debug!(
395                logger,
396                "There is no project-specific network '{}' defined in {}",
397                network_name,
398                config.get_path().display()
399            );
400            None
401        }
402    } else {
403        debug!(
404            logger,
405            "There is no project-specific network '{}' because there is no project (no dfx.json).",
406            network_name
407        );
408        None
409    }
410}
411
412fn create_default_network_from_name(
413    network_name: &str,
414    logger: &Logger,
415) -> Option<NetworkDescriptor> {
416    match network_name {
417        PLAYGROUND_NETWORK_NAME => {
418            debug!(logger, "Using default definition for network 'playground'.");
419            Some(NetworkDescriptor::default_playground_network())
420        }
421        _ => None,
422    }
423}
424
425fn get_local_bind_address(
426    local_provider: &ConfigLocalProvider,
427    local_bind_determination: &LocalBindDetermination,
428    data_directory: &Path,
429    default_local_bind: &str,
430) -> Result<String, NetworkConfigError> {
431    match local_bind_determination {
432        LocalBindDetermination::AsConfigured => Ok(local_provider
433            .bind
434            .clone()
435            .unwrap_or_else(|| default_local_bind.to_string())),
436        LocalBindDetermination::ApplyRunningWebserverPort => {
437            get_running_webserver_bind_address(data_directory, local_provider, default_local_bind)
438        }
439    }
440}
441
442fn get_running_webserver_bind_address(
443    data_directory: &Path,
444    local_provider: &ConfigLocalProvider,
445    default_local_bind: &str,
446) -> Result<String, NetworkConfigError> {
447    let local_bind = local_provider
448        .bind
449        .clone()
450        .unwrap_or_else(|| default_local_bind.to_string());
451    let path = data_directory.join("webserver-port");
452    if path.exists() {
453        let s = crate::fs::read_to_string(&path).map_err(ReadWebserverPortFailed)?;
454        let s = s.trim();
455        if s.is_empty() {
456            Ok(local_bind)
457        } else {
458            let port = s
459                .parse::<u16>()
460                .map_err(|e| ParsePortValueFailed(Box::new(path), Box::new(e)))?;
461            // converting to a socket address, and then setting the port,
462            // will unfortunately transform "localhost:port" to "[::1]:{port}",
463            // which the agent fails to connect with.
464            let host = match local_bind.rfind(':') {
465                None => local_bind.clone(),
466                Some(index) => local_bind[0..index].to_string(),
467            };
468            Ok(format!("{host}:{port}"))
469        }
470    } else {
471        Ok(local_bind)
472    }
473}
474
475pub fn command_line_provider_to_url(s: &str) -> Result<String, NetworkConfigError> {
476    match parse_provider_url(s) {
477        Ok(url) => Ok(url),
478        Err(original_error) => {
479            let prefixed_with_http = format!("http://{s}");
480            parse_provider_url(&prefixed_with_http).or(Err(original_error))
481        }
482    }
483}
484
485pub fn parse_provider_url(url: &str) -> Result<String, NetworkConfigError> {
486    Url::parse(url)
487        .map(|_| String::from(url))
488        .map_err(|e| ParseProviderUrlFailed(Box::new(url.to_string()), e))
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use crate::config::model::bitcoin_adapter::BitcoinAdapterLogLevel;
495    use crate::config::model::canister_http_adapter::HttpAdapterLogLevel;
496    use crate::config::model::dfinity::ReplicaSubnetType::{System, VerifiedApplication};
497    use crate::config::model::dfinity::{
498        ConfigDefaultsBitcoin, ConfigDefaultsCanisterHttp, ConfigDefaultsReplica, ReplicaLogLevel,
499        to_socket_addr,
500    };
501    use std::fs;
502    use std::net::SocketAddr;
503    use std::str::FromStr;
504
505    #[test]
506    fn use_default_if_no_webserver_port_file() {
507        // no file - use default
508        test_with_webserver_port_file_contents(
509            LocalBindDetermination::ApplyRunningWebserverPort,
510            None,
511            "localhost:8000",
512        );
513    }
514
515    #[test]
516    fn ignore_running_webserver_port_if_not_requested() {
517        // port file present and populated, but not asked for: ignored
518        test_with_webserver_port_file_contents(
519            LocalBindDetermination::AsConfigured,
520            Some("1234"),
521            "localhost:8000",
522        );
523    }
524
525    #[test]
526    fn use_port_if_have_file() {
527        // port file present and populated: reflected in socket address
528        test_with_webserver_port_file_contents(
529            LocalBindDetermination::ApplyRunningWebserverPort,
530            Some("1234"),
531            "localhost:1234",
532        );
533    }
534
535    #[test]
536    fn ignore_port_if_not_requested() {
537        // port file present and populated, but not asked for: ignored
538        test_with_webserver_port_file_contents(
539            LocalBindDetermination::AsConfigured,
540            Some("1234"),
541            "localhost:8000",
542        );
543    }
544
545    #[test]
546    fn extra_whitespace_in_webserver_port_is_ok() {
547        // trailing newline is ok
548        test_with_webserver_port_file_contents(
549            LocalBindDetermination::ApplyRunningWebserverPort,
550            Some("  \n3456 \n"),
551            "localhost:3456",
552        );
553    }
554
555    #[test]
556    fn use_running_webserver_address() {
557        // no file - use default
558        test_with_webserver_port_file_contents(
559            LocalBindDetermination::ApplyRunningWebserverPort,
560            None,
561            "localhost:8000",
562        );
563    }
564
565    #[test]
566    fn ignore_empty_webserver_port_file() {
567        // empty is ok: ignore
568        test_with_webserver_port_file_contents(
569            LocalBindDetermination::ApplyRunningWebserverPort,
570            Some(""),
571            "localhost:8000",
572        );
573    }
574    #[test]
575    fn ignore_whitespace_only_webserver_port_file() {
576        // just whitespace is ok: ignore
577        test_with_webserver_port_file_contents(
578            LocalBindDetermination::ApplyRunningWebserverPort,
579            Some("\n"),
580            "localhost:8000",
581        );
582    }
583
584    fn test_with_webserver_port_file_contents(
585        local_bind_determination: LocalBindDetermination,
586        webserver_port_contents: Option<&str>,
587        expected_socket_addr: &str,
588    ) {
589        let temp_dir = tempfile::tempdir().unwrap();
590        let project_dir = temp_dir.path().join("project");
591        fs::create_dir_all(&project_dir).unwrap();
592        let project_dfx_json = project_dir.join("dfx.json");
593        std::fs::write(
594            project_dfx_json,
595            r#"{
596            "networks": {
597                "local": {
598                    "bind": "localhost:8000"
599                }
600            }
601        }"#,
602        )
603        .unwrap();
604
605        if let Some(webserver_port_contents) = webserver_port_contents {
606            let dot_dfx_dir = project_dir.join(".dfx");
607            let network_data_dir = dot_dfx_dir.join("network").join("local");
608            fs::create_dir_all(&network_data_dir).unwrap();
609            std::fs::write(
610                network_data_dir.join("webserver-port"),
611                webserver_port_contents,
612            )
613            .unwrap();
614        }
615
616        let config = Config::from_dir(&project_dir, None).unwrap().unwrap();
617        let network_descriptor = create_network_descriptor(
618            Some(Arc::new(config)),
619            Arc::new(NetworksConfig::new().unwrap()),
620            None,
621            None,
622            local_bind_determination,
623        )
624        .unwrap();
625
626        assert_eq!(
627            network_descriptor
628                .local_server_descriptor()
629                .unwrap()
630                .bind_address,
631            to_socket_addr(expected_socket_addr).unwrap()
632        );
633    }
634
635    #[test]
636    fn config_with_local_bind_addr() {
637        let config = Config::from_str(
638            r#"{
639            "networks": {
640                "local": {
641                    "bind": "localhost:8000"
642                }
643            }
644        }"#,
645        )
646        .unwrap();
647
648        let network_descriptor = create_network_descriptor(
649            Some(Arc::new(config)),
650            Arc::new(NetworksConfig::new().unwrap()),
651            None,
652            None,
653            LocalBindDetermination::AsConfigured,
654        )
655        .unwrap();
656
657        assert_eq!(
658            network_descriptor
659                .local_server_descriptor()
660                .unwrap()
661                .bind_address,
662            to_socket_addr("localhost:8000").unwrap()
663        );
664    }
665
666    #[test]
667    fn config_with_invalid_local_bind_addr() {
668        let config = Config::from_str(
669            r#"{
670            "networks": {
671                "local": {
672                    "bind": "not a valid bind address"
673                }
674            }
675        }"#,
676        )
677        .unwrap();
678
679        let result = create_network_descriptor(
680            Some(Arc::new(config)),
681            Arc::new(NetworksConfig::new().unwrap()),
682            None,
683            None,
684            LocalBindDetermination::AsConfigured,
685        );
686        assert!(result.is_err());
687    }
688
689    #[test]
690    fn config_returns_local_bind_address_if_no_local_network() {
691        let config = Config::from_str(
692            r#"{
693            "networks": {
694            }
695        }"#,
696        )
697        .unwrap();
698        let network_descriptor = create_network_descriptor(
699            Some(Arc::new(config)),
700            Arc::new(NetworksConfig::new().unwrap()),
701            None,
702            None,
703            LocalBindDetermination::AsConfigured,
704        )
705        .unwrap();
706
707        assert_eq!(
708            network_descriptor
709                .local_server_descriptor()
710                .unwrap()
711                .bind_address,
712            to_socket_addr("127.0.0.1:4943").unwrap()
713        );
714    }
715
716    #[test]
717    fn config_returns_local_bind_address_if_no_networks() {
718        let config = Config::from_str(
719            r#"{
720        }"#,
721        )
722        .unwrap();
723        let network_descriptor = create_network_descriptor(
724            Some(Arc::new(config)),
725            Arc::new(NetworksConfig::new().unwrap()),
726            None,
727            None,
728            LocalBindDetermination::AsConfigured,
729        )
730        .unwrap();
731
732        assert_eq!(
733            network_descriptor
734                .local_server_descriptor()
735                .unwrap()
736                .bind_address,
737            to_socket_addr("127.0.0.1:4943").unwrap()
738        );
739    }
740
741    #[test]
742    fn get_bitcoin_config() {
743        let config = Config::from_str(
744            r#"{
745              "defaults": {
746                "bitcoin": {
747                  "enabled": true,
748                  "nodes": ["127.0.0.1:18444"],
749                  "log_level": "info"
750                }
751              },
752              "networks": {
753                "local": {
754                    "bind": "localhost:8000"
755                }
756              }
757        }"#,
758        )
759        .unwrap();
760
761        let network_descriptor = create_network_descriptor(
762            Some(Arc::new(config)),
763            Arc::new(NetworksConfig::new().unwrap()),
764            None,
765            None,
766            LocalBindDetermination::AsConfigured,
767        )
768        .unwrap();
769        let bitcoin_config = &network_descriptor
770            .local_server_descriptor()
771            .unwrap()
772            .bitcoin;
773
774        assert_eq!(
775            bitcoin_config,
776            &ConfigDefaultsBitcoin {
777                enabled: true,
778                nodes: Some(vec![SocketAddr::from_str("127.0.0.1:18444").unwrap()]),
779                log_level: BitcoinAdapterLogLevel::Info,
780                ..ConfigDefaultsBitcoin::default()
781            }
782        );
783    }
784
785    #[test]
786    fn get_bitcoin_config_default_log_level() {
787        let config = Config::from_str(
788            r#"{
789              "defaults": {
790                "bitcoin": {
791                  "enabled": true,
792                  "nodes": ["127.0.0.1:18444"]
793                }
794              },
795              "networks": {
796                "local": {
797                    "bind": "localhost:8000"
798                }
799              }
800        }"#,
801        )
802        .unwrap();
803
804        let network_descriptor = create_network_descriptor(
805            Some(Arc::new(config)),
806            Arc::new(NetworksConfig::new().unwrap()),
807            None,
808            None,
809            LocalBindDetermination::AsConfigured,
810        )
811        .unwrap();
812        let bitcoin_config = &network_descriptor
813            .local_server_descriptor()
814            .unwrap()
815            .bitcoin;
816
817        assert_eq!(
818            bitcoin_config,
819            &ConfigDefaultsBitcoin {
820                enabled: true,
821                nodes: Some(vec![SocketAddr::from_str("127.0.0.1:18444").unwrap()]),
822                log_level: BitcoinAdapterLogLevel::Info, // A default log level of "info" is assumed
823                ..ConfigDefaultsBitcoin::default()
824            }
825        );
826    }
827
828    #[test]
829    fn get_bitcoin_config_debug_log_level() {
830        let config = Config::from_str(
831            r#"{
832              "defaults": {
833                "bitcoin": {
834                  "enabled": true,
835                  "log_level": "debug"
836                }
837              },
838              "networks": {
839                "local": {
840                    "bind": "localhost:8000"
841                }
842              }
843        }"#,
844        )
845        .unwrap();
846
847        let network_descriptor = create_network_descriptor(
848            Some(Arc::new(config)),
849            Arc::new(NetworksConfig::new().unwrap()),
850            None,
851            None,
852            LocalBindDetermination::AsConfigured,
853        )
854        .unwrap();
855        let bitcoin_config = &network_descriptor
856            .local_server_descriptor()
857            .unwrap()
858            .bitcoin;
859
860        assert_eq!(
861            bitcoin_config,
862            &ConfigDefaultsBitcoin {
863                enabled: true,
864                log_level: BitcoinAdapterLogLevel::Debug,
865                ..ConfigDefaultsBitcoin::default()
866            }
867        );
868    }
869
870    #[test]
871    fn bitcoin_config_on_local_network() {
872        let config = Config::from_str(
873            r#"{
874              "networks": {
875                "local": {
876                  "bind": "127.0.0.1:8000",
877                  "bitcoin": {
878                    "enabled": true,
879                    "nodes": ["127.0.0.1:18444"],
880                    "log_level": "info"
881                  }
882                }
883              }
884        }"#,
885        )
886        .unwrap();
887
888        let network_descriptor = create_network_descriptor(
889            Some(Arc::new(config)),
890            Arc::new(NetworksConfig::new().unwrap()),
891            None,
892            None,
893            LocalBindDetermination::AsConfigured,
894        )
895        .unwrap();
896        let bitcoin_config = &network_descriptor
897            .local_server_descriptor()
898            .unwrap()
899            .bitcoin;
900
901        assert_eq!(
902            bitcoin_config,
903            &ConfigDefaultsBitcoin {
904                enabled: true,
905                nodes: Some(vec![SocketAddr::from_str("127.0.0.1:18444").unwrap()]),
906                log_level: BitcoinAdapterLogLevel::Info,
907                ..ConfigDefaultsBitcoin::default()
908            }
909        );
910    }
911
912    #[test]
913    fn replica_config_on_local_network() {
914        let config = Config::from_str(
915            r#"{
916              "networks": {
917                "local": {
918                  "bind": "127.0.0.1:8000",
919                  "replica": {
920                    "subnet_type": "verifiedapplication",
921                    "port": 17001,
922                    "log_level": "trace"
923                  }
924                }
925              }
926        }"#,
927        )
928        .unwrap();
929
930        let network_descriptor = create_network_descriptor(
931            Some(Arc::new(config)),
932            Arc::new(NetworksConfig::new().unwrap()),
933            None,
934            None,
935            LocalBindDetermination::AsConfigured,
936        )
937        .unwrap();
938        let replica_config = &network_descriptor
939            .local_server_descriptor()
940            .unwrap()
941            .replica;
942
943        assert_eq!(
944            replica_config,
945            &ConfigDefaultsReplica {
946                subnet_type: Some(VerifiedApplication),
947                port: Some(17001),
948                log_level: Some(ReplicaLogLevel::Trace)
949            }
950        );
951    }
952
953    #[test]
954    fn replica_config_on_local_network_overrides_default() {
955        // Defaults are not combined.
956        // Here the 'default' level specifies a port, but it's ignored due to the
957        // network-level setting.
958        let config = Config::from_str(
959            r#"{
960              "defaults": {
961                "replica": {
962                  "port": 13131
963                }
964              },
965              "networks": {
966                "local": {
967                  "bind": "127.0.0.1:8000",
968                  "replica": {
969                    "subnet_type": "system"
970                  }
971                }
972              }
973        }"#,
974        )
975        .unwrap();
976
977        let network_descriptor = create_network_descriptor(
978            Some(Arc::new(config)),
979            Arc::new(NetworksConfig::new().unwrap()),
980            None,
981            None,
982            LocalBindDetermination::AsConfigured,
983        )
984        .unwrap();
985        let replica_config = &network_descriptor
986            .local_server_descriptor()
987            .unwrap()
988            .replica;
989
990        assert_eq!(
991            replica_config,
992            &ConfigDefaultsReplica {
993                subnet_type: Some(System),
994                port: None,
995                log_level: None
996            }
997        );
998    }
999
1000    #[test]
1001    // Creating nested directories under macOS temp paths (e.g. /var/folders/…/T/.tmpXXX/.config/dfx)
1002    // fails with "Invalid argument" on CI runners, likely due to the dot-prefixed temp dir name.
1003    #[cfg_attr(target_os = "macos", ignore)]
1004    fn canister_http_config_on_local_network() {
1005        let config = Config::from_str(
1006            r#"{
1007              "networks": {
1008                "local": {
1009                  "bind": "127.0.0.1:8000",
1010                  "canister_http": {
1011                    "enabled": true,
1012                    "log_level": "debug"
1013                  }
1014                }
1015              }
1016        }"#,
1017        )
1018        .unwrap();
1019
1020        let network_descriptor = create_network_descriptor(
1021            Some(Arc::new(config)),
1022            Arc::new(NetworksConfig::new().unwrap()),
1023            None,
1024            None,
1025            LocalBindDetermination::AsConfigured,
1026        )
1027        .unwrap();
1028        let canister_http_config = &network_descriptor
1029            .local_server_descriptor()
1030            .unwrap()
1031            .canister_http;
1032
1033        assert_eq!(
1034            canister_http_config,
1035            &ConfigDefaultsCanisterHttp {
1036                enabled: true,
1037                log_level: HttpAdapterLogLevel::Debug
1038            }
1039        );
1040    }
1041
1042    #[test]
1043    fn url_is_url() {
1044        assert_eq!(
1045            command_line_provider_to_url("http://127.0.0.1:8000").unwrap(),
1046            "http://127.0.0.1:8000"
1047        );
1048    }
1049
1050    #[test]
1051    fn addr_and_port_to_url() {
1052        assert_eq!(
1053            command_line_provider_to_url("127.0.0.1:8000").unwrap(),
1054            "http://127.0.0.1:8000"
1055        );
1056    }
1057}