dfx_core/interface/
builder.rs

1use crate::{
2    DfxInterface,
3    config::cache::get_version_from_cache_path,
4    config::model::{
5        dfinity::{Config, NetworksConfig},
6        network_descriptor::NetworkDescriptor,
7    },
8    error::{
9        builder::{BuildAgentError, BuildDfxInterfaceError, BuildIdentityError},
10        extension::NewExtensionManagerError,
11        interface::NewExtensionManagerFromCachePathError,
12        network_config::NetworkConfigError,
13    },
14    extension::manager::ExtensionManager,
15    identity::{IdentityManager, identity_manager::InitializeIdentity},
16    network::{
17        provider::{LocalBindDetermination, create_network_descriptor},
18        root_key::fetch_root_key_when_non_mainnet_or_error,
19    },
20};
21use ic_agent::{Agent, Identity, agent::route_provider::RoundRobinRouteProvider};
22use reqwest::Client;
23use semver::Version;
24use std::path::Path;
25use std::sync::Arc;
26
27#[derive(PartialEq)]
28pub enum IdentityPicker {
29    Anonymous,
30    Selected,
31    Named(String),
32}
33
34#[derive(PartialEq)]
35pub enum NetworkPicker {
36    Local,
37    Mainnet,
38    Named(String),
39}
40
41pub struct DfxInterfaceBuilder {
42    identity: IdentityPicker,
43
44    network: NetworkPicker,
45
46    /// Force fetching of the root key.
47    /// This is insecure and should only be set for non-mainnet networks.
48    /// There is no need to set this for the local network, where the root key is fetched by default.
49    /// This would typically be set for a testnet, or an alias for the local network.
50    force_fetch_root_key_insecure_non_mainnet_only: bool,
51
52    extension_manager: Option<ExtensionManager>,
53}
54
55impl DfxInterfaceBuilder {
56    pub fn new() -> Self {
57        Self {
58            identity: IdentityPicker::Selected,
59            network: NetworkPicker::Local,
60            force_fetch_root_key_insecure_non_mainnet_only: false,
61            extension_manager: None,
62        }
63    }
64
65    pub fn anonymous(self) -> Self {
66        self.with_identity(IdentityPicker::Anonymous)
67    }
68
69    pub fn with_identity_named(self, name: &str) -> Self {
70        self.with_identity(IdentityPicker::Named(name.to_string()))
71    }
72
73    pub fn with_identity(self, identity: IdentityPicker) -> Self {
74        Self { identity, ..self }
75    }
76
77    pub fn mainnet(self) -> Self {
78        self.with_network(NetworkPicker::Mainnet)
79    }
80
81    pub fn with_network(self, network: NetworkPicker) -> Self {
82        Self { network, ..self }
83    }
84
85    pub fn with_network_named(self, name: &str) -> Self {
86        self.with_network(NetworkPicker::Named(name.to_string()))
87    }
88
89    pub fn with_extension_manager(
90        self,
91        version: Version,
92    ) -> Result<Self, NewExtensionManagerError> {
93        let extension_manager = Some(ExtensionManager::new(&version)?);
94        Ok(Self {
95            extension_manager,
96            ..self
97        })
98    }
99
100    pub fn with_extension_manager_from_cache_path(
101        self,
102        cache_path: &Path,
103    ) -> Result<Self, NewExtensionManagerFromCachePathError> {
104        let version = get_version_from_cache_path(cache_path)?;
105
106        Ok(self.with_extension_manager(version)?)
107    }
108
109    pub fn with_force_fetch_root_key_insecure_non_mainnet_only(self) -> Self {
110        Self {
111            force_fetch_root_key_insecure_non_mainnet_only: true,
112            ..self
113        }
114    }
115
116    pub async fn build(&self) -> Result<DfxInterface, BuildDfxInterfaceError> {
117        let fetch_root_key = self.network == NetworkPicker::Local
118            || self.force_fetch_root_key_insecure_non_mainnet_only;
119        let networks_config = NetworksConfig::new()?;
120        let config = Config::from_current_dir(self.extension_manager.as_ref())?.map(Arc::new);
121        let network_descriptor = self.build_network_descriptor(config.clone(), &networks_config)?;
122        let identity = self.build_identity()?;
123        let agent = self.build_agent(identity.clone(), &network_descriptor)?;
124
125        if fetch_root_key {
126            fetch_root_key_when_non_mainnet_or_error(&agent, &network_descriptor).await?;
127        }
128
129        Ok(DfxInterface {
130            config,
131            identity,
132            agent,
133            networks_config,
134            network_descriptor,
135        })
136    }
137
138    fn build_agent(
139        &self,
140        identity: Arc<dyn Identity>,
141        network_descriptor: &NetworkDescriptor,
142    ) -> Result<Agent, BuildAgentError> {
143        let route_provider = RoundRobinRouteProvider::new(network_descriptor.providers.clone())
144            .map_err(BuildAgentError::CreateRouteProvider)?;
145        let client = Client::builder()
146            .use_rustls_tls()
147            .build()
148            .map_err(BuildAgentError::CreateHttpClient)?;
149        let agent = Agent::builder()
150            .with_http_client(client)
151            .with_route_provider(route_provider)
152            .with_arc_identity(identity)
153            .build()
154            .map_err(BuildAgentError::CreateAgent)?;
155        Ok(agent)
156    }
157
158    fn build_identity(&self) -> Result<Arc<dyn Identity>, BuildIdentityError> {
159        if self.identity == IdentityPicker::Anonymous {
160            return Ok(Arc::new(ic_agent::identity::AnonymousIdentity));
161        }
162
163        let identity_override = match &self.identity {
164            IdentityPicker::Named(name) => Some(name.clone()),
165            IdentityPicker::Selected => None,
166            IdentityPicker::Anonymous => unreachable!(),
167        };
168
169        let logger = slog::Logger::root(slog::Discard, slog::o!());
170        let mut identity_manager = IdentityManager::new(
171            &logger,
172            identity_override.as_deref(),
173            InitializeIdentity::Disallow,
174        )?;
175        let identity: Box<dyn Identity> =
176            identity_manager.instantiate_selected_identity(&logger)?;
177        Ok(Arc::from(identity))
178    }
179
180    fn build_network_descriptor(
181        &self,
182        config: Option<Arc<Config>>,
183        networks_config: &NetworksConfig,
184    ) -> Result<NetworkDescriptor, NetworkConfigError> {
185        let network = match &self.network {
186            NetworkPicker::Local => None,
187            NetworkPicker::Mainnet => Some("ic".to_string()),
188            NetworkPicker::Named(name) => Some(name.clone()),
189        };
190        let logger = None;
191        create_network_descriptor(
192            config,
193            Arc::new(networks_config.clone()),
194            network,
195            logger,
196            LocalBindDetermination::ApplyRunningWebserverPort,
197        )
198    }
199}
200
201impl Default for DfxInterfaceBuilder {
202    fn default() -> Self {
203        Self::new()
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use crate::DfxInterface;
210    use crate::error::{
211        builder::{
212            BuildDfxInterfaceError,
213            BuildDfxInterfaceError::{FetchRootKey, NetworkConfig},
214            BuildIdentityError::NewIdentityManager,
215        },
216        identity::NewIdentityManagerError,
217        network_config::NetworkConfigError::NetworkNotFound,
218        root_key::FetchRootKeyError,
219        root_key::FetchRootKeyError::AgentError,
220    };
221    use crate::identity::{
222        IdentityCreationParameters, IdentityManager,
223        identity_manager::IdentityStorageMode::Plaintext,
224    };
225    use candid::Principal;
226    use futures::Future;
227    use ic_agent::{AgentError::TransportError, Identity};
228    use serde_json::json;
229    use std::path::Path;
230    use std::sync::Arc;
231    use tempfile::TempDir;
232    use tokio::sync::Semaphore;
233
234    lazy_static::lazy_static! {
235        static ref SEMAPHORE: Semaphore = Semaphore::new(1);
236    }
237
238    async fn run_test<F, Fut>(test_function: F)
239    where
240        F: FnOnce(Arc<TempDir>) -> Fut + Send,
241        Fut: Future<Output = ()> + Send,
242    {
243        run_test_with_settings(TestSettings { testnet: None }, test_function).await;
244    }
245
246    pub struct TestNetSettings {
247        pub name: String,
248        pub providers: Vec<String>,
249    }
250    pub struct TestSettings {
251        pub testnet: Option<TestNetSettings>,
252    }
253    async fn run_test_with_settings<F, Fut>(settings: TestSettings, test_function: F)
254    where
255        F: FnOnce(Arc<TempDir>) -> Fut + Send,
256        Fut: Future<Output = ()> + Send,
257    {
258        let _permit = SEMAPHORE.acquire().await.unwrap();
259
260        let temp_dir = TempDir::new().unwrap();
261
262        if let Some(testnet) = settings.testnet {
263            let networks_config = json!({
264                testnet.name: {
265                    "providers": testnet.providers,
266                }
267            });
268
269            let config_dir = temp_dir.path().join(".config/dfx");
270            let networks_config_path = config_dir.join("networks.json");
271            crate::fs::create_dir_all(&config_dir).unwrap();
272            crate::fs::write(networks_config_path, networks_config.to_string()).unwrap();
273        }
274
275        // so tests don't clobber each other in the environment
276        crate::config::directories::DFX_CONFIG_ROOT
277            .lock()
278            .unwrap()
279            .replace(temp_dir.path().to_path_buf().into_os_string());
280
281        let temp_dir = Arc::new(temp_dir);
282        test_function(temp_dir.clone()).await;
283    }
284
285    #[tokio::test]
286    async fn anonymous() {
287        run_test(|td| async move {
288            let d = DfxInterface::builder()
289                .anonymous()
290                .mainnet()
291                .build()
292                .await
293                .unwrap();
294            assert!(d.identity().public_key().is_none());
295            assert_eq!(d.identity().sender().unwrap(), Principal::anonymous());
296
297            let actual = all_children(td.path());
298            let expected: Vec<String> = vec![".config/".into(), ".config/dfx/".into()];
299            assert_eq!(actual, expected);
300        })
301        .await;
302    }
303
304    #[tokio::test]
305    async fn no_config_does_not_create_default_identity() {
306        run_test(|_| async {
307            assert!(matches!(
308                DfxInterface::builder().build().await,
309                Err(BuildDfxInterfaceError::BuildIdentity(NewIdentityManager(
310                    NewIdentityManagerError::NoIdentityConfigurationFound
311                )))
312            ));
313        })
314        .await;
315    }
316
317    #[tokio::test]
318    async fn default_identity() {
319        run_test(|_| async {
320            let default_principal = {
321                let logger = slog::Logger::root(slog::Discard, slog::o!());
322                let mut im = IdentityManager::new(
323                    &logger,
324                    None,
325                    crate::identity::identity_manager::InitializeIdentity::Allow,
326                )
327                .unwrap();
328                let id: Box<dyn Identity> = im.instantiate_selected_identity(&logger).unwrap();
329                id.sender().unwrap()
330            };
331            let d = DfxInterface::builder().mainnet().build().await.unwrap();
332            assert_eq!(d.identity.sender().unwrap(), default_principal);
333        })
334        .await;
335    }
336
337    #[tokio::test]
338    async fn select_identity_by_name() {
339        run_test(|_| async {
340            let alice = "alice";
341            let bob = "bob";
342            let (alice_principal_from_mgr, bob_principal_from_mgr) = {
343                let logger = slog::Logger::root(slog::Discard, slog::o!());
344                let mut im = IdentityManager::new(
345                    &logger,
346                    None,
347                    crate::identity::identity_manager::InitializeIdentity::Allow,
348                )
349                .unwrap();
350                im.create_new_identity(&logger, alice, plaintext(), false)
351                    .unwrap();
352                im.create_new_identity(&logger, bob, plaintext(), false)
353                    .unwrap();
354
355                let alice: Box<dyn Identity> =
356                    im.instantiate_identity_from_name(alice, &logger).unwrap();
357
358                let bob: Box<dyn Identity> =
359                    im.instantiate_identity_from_name(bob, &logger).unwrap();
360                (alice.sender().unwrap(), bob.sender().unwrap())
361            };
362            assert_ne!(alice_principal_from_mgr, bob_principal_from_mgr);
363            let alice_interface = DfxInterface::builder()
364                .with_identity_named(alice)
365                .mainnet()
366                .build()
367                .await
368                .unwrap();
369            assert_eq!(
370                alice_interface.identity.sender().unwrap(),
371                alice_principal_from_mgr
372            );
373
374            let bob_interface = DfxInterface::builder()
375                .with_identity_named(bob)
376                .mainnet()
377                .build()
378                .await
379                .unwrap();
380            assert_eq!(
381                bob_interface.identity.sender().unwrap(),
382                bob_principal_from_mgr
383            );
384        })
385        .await;
386    }
387
388    #[tokio::test]
389    async fn selected_non_default() {
390        run_test(|_| async {
391            let alice = "alice";
392            let bob = "bob";
393            let bob_principal_from_mgr = {
394                let logger = slog::Logger::root(slog::Discard, slog::o!());
395                let mut im = IdentityManager::new(
396                    &logger,
397                    None,
398                    crate::identity::identity_manager::InitializeIdentity::Allow,
399                )
400                .unwrap();
401                im.create_new_identity(&logger, alice, plaintext(), false)
402                    .unwrap();
403                im.create_new_identity(&logger, bob, plaintext(), false)
404                    .unwrap();
405
406                let _alice_identity: Box<dyn Identity> =
407                    im.instantiate_identity_from_name(alice, &logger).unwrap();
408
409                let bob_identity: Box<dyn Identity> =
410                    im.instantiate_identity_from_name(bob, &logger).unwrap();
411
412                im.use_identity_named(&logger, bob).unwrap();
413                bob_identity.sender().unwrap()
414            };
415
416            let selected_interface = DfxInterface::builder().mainnet().build().await.unwrap();
417            assert_eq!(
418                selected_interface.identity.sender().unwrap(),
419                bob_principal_from_mgr
420            );
421        })
422        .await;
423    }
424
425    fn plaintext() -> IdentityCreationParameters {
426        IdentityCreationParameters::Pem { mode: Plaintext }
427    }
428
429    #[tokio::test]
430    async fn local_network() {
431        run_test(|_| async {
432            match DfxInterface::builder().anonymous().build().await {
433                Ok(d) => {
434                    assert_eq!(d.network_descriptor.name, "local");
435                    assert!(!d.network_descriptor.is_ic);
436                    assert!(d.network_descriptor.local_server_descriptor.is_some());
437                }
438                Err(FetchRootKey(AgentError(TransportError(_)))) => {
439                    // local replica isn't running, so this is expected,
440                    // but we can't check anything else
441                }
442                Err(e) => panic!("unexpected error: {:?}", e),
443            }
444        })
445        .await;
446    }
447
448    #[tokio::test]
449    async fn mainnet() {
450        run_test(|_| async {
451            let d = DfxInterface::builder()
452                .anonymous()
453                .mainnet()
454                .build()
455                .await
456                .unwrap();
457            let network_descriptor = d.network_descriptor;
458            assert!(network_descriptor.is_ic);
459            assert_eq!(network_descriptor.name, "ic");
460        })
461        .await;
462    }
463
464    #[tokio::test]
465    async fn try_to_fetch_root_key_on_mainnet() {
466        run_test(|_| async {
467            assert!(matches!(
468                DfxInterface::builder()
469                    .anonymous()
470                    .mainnet()
471                    .with_force_fetch_root_key_insecure_non_mainnet_only()
472                    .build()
473                    .await,
474                Err(FetchRootKey(
475                    FetchRootKeyError::MustNotFetchRootKeyOnMainnet
476                ))
477            ));
478        })
479        .await;
480    }
481
482    #[tokio::test]
483    async fn named_network_not_found() {
484        run_test(|_| async {
485            assert!(
486                matches!(DfxInterface::builder().with_network_named("testnet").build().await,
487                Err(NetworkConfig(NetworkNotFound(network_name))) if network_name == "testnet")
488            );
489        })
490        .await;
491    }
492
493    #[tokio::test]
494    async fn named_network() {
495        let settings = TestSettings {
496            testnet: Some(TestNetSettings {
497                name: "testnet".to_string(),
498                providers: vec!["http://localhost:1234".to_string()],
499            }),
500        };
501        run_test_with_settings(settings, |_| async move {
502            let d = DfxInterface::builder()
503                .anonymous()
504                .with_network_named("testnet")
505                .build()
506                .await
507                .unwrap();
508            let network_descriptor = d.network_descriptor;
509            assert_eq!(network_descriptor.name, "testnet");
510            assert_eq!(network_descriptor.providers, vec!["http://localhost:1234"]);
511
512            // Notice that the above did not fail, because it did not try to fetch the root key.
513            // It only does so if we tell it to:
514            assert!(matches!(
515                DfxInterface::builder()
516                    .anonymous()
517                    .with_network_named("testnet")
518                    .with_force_fetch_root_key_insecure_non_mainnet_only()
519                    .build()
520                    .await,
521                Err(FetchRootKey(AgentError(TransportError(_))))
522            ));
523        })
524        .await;
525    }
526
527    // returns a vec of all children of a directory, recursively
528    // directories are suffixed with a '/'
529    fn all_children(dir: &Path) -> Vec<String> {
530        let mut result = vec![];
531        for entry in std::fs::read_dir(dir).unwrap() {
532            let entry = entry.unwrap();
533            let path = entry.path();
534            let filename = path
535                .file_name()
536                .unwrap()
537                .to_os_string()
538                .into_string()
539                .unwrap();
540
541            if path.is_dir() {
542                result.push(filename.clone() + "/");
543                let all_children = all_children(&path);
544                let all_children: Vec<_> = all_children
545                    .iter()
546                    .map(|c| format!("{}/{}", &filename, c))
547                    .collect();
548                result.extend(all_children);
549            } else {
550                result.push(filename);
551            }
552        }
553        result
554    }
555}