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_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 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 }
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 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 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}