1use color_eyre::{eyre::eyre, Result};
10use libp2p::Multiaddr;
11use service_manager::{ServiceInstallCtx, ServiceLabel};
12use sn_evm::{EvmNetwork, RewardsAddress};
13use sn_logging::LogFormat;
14use std::{
15 ffi::OsString,
16 net::{Ipv4Addr, SocketAddr},
17 path::PathBuf,
18 str::FromStr,
19};
20
21#[derive(Clone, Debug)]
22pub enum PortRange {
23 Single(u16),
24 Range(u16, u16),
25}
26
27impl PortRange {
28 pub fn parse(s: &str) -> Result<Self> {
29 if let Ok(port) = u16::from_str(s) {
30 Ok(Self::Single(port))
31 } else {
32 let parts: Vec<&str> = s.split('-').collect();
33 if parts.len() != 2 {
34 return Err(eyre!("Port range must be in the format 'start-end'"));
35 }
36 let start = parts[0].parse::<u16>()?;
37 let end = parts[1].parse::<u16>()?;
38 if start >= end {
39 return Err(eyre!("End port must be greater than start port"));
40 }
41 Ok(Self::Range(start, end))
42 }
43 }
44
45 pub fn validate(&self, count: u16) -> Result<()> {
47 match self {
48 Self::Single(_) => {
49 if count != 1 {
50 error!("The count ({count}) does not match the number of ports (1)");
51 return Err(eyre!(
52 "The count ({count}) does not match the number of ports (1)"
53 ));
54 }
55 }
56 Self::Range(start, end) => {
57 let port_count = end - start + 1;
58 if count != port_count {
59 error!("The count ({count}) does not match the number of ports ({port_count})");
60 return Err(eyre!(
61 "The count ({count}) does not match the number of ports ({port_count})"
62 ));
63 }
64 }
65 }
66 Ok(())
67 }
68}
69
70#[derive(Debug, PartialEq)]
71pub struct InstallNodeServiceCtxBuilder {
72 pub autostart: bool,
73 pub bootstrap_peers: Vec<Multiaddr>,
74 pub data_dir_path: PathBuf,
75 pub env_variables: Option<Vec<(String, String)>>,
76 pub evm_network: EvmNetwork,
77 pub genesis: bool,
78 pub home_network: bool,
79 pub local: bool,
80 pub log_dir_path: PathBuf,
81 pub log_format: Option<LogFormat>,
82 pub name: String,
83 pub max_archived_log_files: Option<usize>,
84 pub max_log_files: Option<usize>,
85 pub metrics_port: Option<u16>,
86 pub node_ip: Option<Ipv4Addr>,
87 pub node_port: Option<u16>,
88 pub owner: Option<String>,
89 pub rewards_address: RewardsAddress,
90 pub rpc_socket_addr: SocketAddr,
91 pub safenode_path: PathBuf,
92 pub service_user: Option<String>,
93 pub upnp: bool,
94}
95
96impl InstallNodeServiceCtxBuilder {
97 pub fn build(self) -> Result<ServiceInstallCtx> {
98 let label: ServiceLabel = self.name.parse()?;
99 let mut args = vec![
100 OsString::from("--rpc"),
101 OsString::from(self.rpc_socket_addr.to_string()),
102 OsString::from("--root-dir"),
103 OsString::from(self.data_dir_path.to_string_lossy().to_string()),
104 OsString::from("--log-output-dest"),
105 OsString::from(self.log_dir_path.to_string_lossy().to_string()),
106 ];
107
108 if self.genesis {
109 args.push(OsString::from("--first"));
110 }
111 if self.home_network {
112 args.push(OsString::from("--home-network"));
113 }
114 if self.local {
115 args.push(OsString::from("--local"));
116 }
117 if let Some(log_format) = self.log_format {
118 args.push(OsString::from("--log-format"));
119 args.push(OsString::from(log_format.as_str()));
120 }
121 if self.upnp {
122 args.push(OsString::from("--upnp"));
123 }
124 if let Some(node_ip) = self.node_ip {
125 args.push(OsString::from("--ip"));
126 args.push(OsString::from(node_ip.to_string()));
127 }
128 if let Some(node_port) = self.node_port {
129 args.push(OsString::from("--port"));
130 args.push(OsString::from(node_port.to_string()));
131 }
132 if let Some(metrics_port) = self.metrics_port {
133 args.push(OsString::from("--metrics-server-port"));
134 args.push(OsString::from(metrics_port.to_string()));
135 }
136 if let Some(owner) = self.owner {
137 args.push(OsString::from("--owner"));
138 args.push(OsString::from(owner));
139 }
140 if let Some(log_files) = self.max_archived_log_files {
141 args.push(OsString::from("--max-archived-log-files"));
142 args.push(OsString::from(log_files.to_string()));
143 }
144 if let Some(log_files) = self.max_log_files {
145 args.push(OsString::from("--max-log-files"));
146 args.push(OsString::from(log_files.to_string()));
147 }
148
149 if !self.bootstrap_peers.is_empty() {
150 let peers_str = self
151 .bootstrap_peers
152 .iter()
153 .map(|peer| peer.to_string())
154 .collect::<Vec<_>>()
155 .join(",");
156 args.push(OsString::from("--peer"));
157 args.push(OsString::from(peers_str));
158 }
159
160 args.push(OsString::from("--rewards-address"));
161 args.push(OsString::from(self.rewards_address.to_string()));
162
163 args.push(OsString::from(self.evm_network.to_string()));
164 if let EvmNetwork::Custom(custom_network) = &self.evm_network {
165 args.push(OsString::from("--rpc-url"));
166 args.push(OsString::from(custom_network.rpc_url_http.to_string()));
167 args.push(OsString::from("--payment-token-address"));
168 args.push(OsString::from(
169 custom_network.payment_token_address.to_string(),
170 ));
171 args.push(OsString::from("--data-payments-address"));
172 args.push(OsString::from(
173 custom_network.data_payments_address.to_string(),
174 ));
175 }
176
177 Ok(ServiceInstallCtx {
178 args,
179 autostart: self.autostart,
180 contents: None,
181 environment: self.env_variables,
182 label: label.clone(),
183 program: self.safenode_path.to_path_buf(),
184 username: self.service_user.clone(),
185 working_directory: None,
186 })
187 }
188}
189
190pub struct AddNodeServiceOptions {
191 pub auto_restart: bool,
192 pub auto_set_nat_flags: bool,
193 pub bootstrap_peers: Vec<Multiaddr>,
194 pub count: Option<u16>,
195 pub delete_safenode_src: bool,
196 pub enable_metrics_server: bool,
197 pub env_variables: Option<Vec<(String, String)>>,
198 pub evm_network: EvmNetwork,
199 pub genesis: bool,
200 pub home_network: bool,
201 pub local: bool,
202 pub log_format: Option<LogFormat>,
203 pub max_archived_log_files: Option<usize>,
204 pub max_log_files: Option<usize>,
205 pub metrics_port: Option<PortRange>,
206 pub node_ip: Option<Ipv4Addr>,
207 pub node_port: Option<PortRange>,
208 pub owner: Option<String>,
209 pub rewards_address: RewardsAddress,
210 pub rpc_address: Option<Ipv4Addr>,
211 pub rpc_port: Option<PortRange>,
212 pub safenode_src_path: PathBuf,
213 pub safenode_dir_path: PathBuf,
214 pub service_data_dir_path: PathBuf,
215 pub service_log_dir_path: PathBuf,
216 pub upnp: bool,
217 pub user: Option<String>,
218 pub user_mode: bool,
219 pub version: String,
220}
221
222#[derive(Debug, PartialEq)]
223pub struct InstallAuditorServiceCtxBuilder {
224 pub auditor_path: PathBuf,
225 pub beta_encryption_key: Option<String>,
226 pub bootstrap_peers: Vec<Multiaddr>,
227 pub env_variables: Option<Vec<(String, String)>>,
228 pub log_dir_path: PathBuf,
229 pub name: String,
230 pub service_user: String,
231}
232
233impl InstallAuditorServiceCtxBuilder {
234 pub fn build(self) -> Result<ServiceInstallCtx> {
235 let mut args = vec![
236 OsString::from("--log-output-dest"),
237 OsString::from(self.log_dir_path.to_string_lossy().to_string()),
238 ];
239
240 if !self.bootstrap_peers.is_empty() {
241 let peers_str = self
242 .bootstrap_peers
243 .iter()
244 .map(|peer| peer.to_string())
245 .collect::<Vec<_>>()
246 .join(",");
247 args.push(OsString::from("--peer"));
248 args.push(OsString::from(peers_str));
249 }
250 if let Some(beta_encryption_key) = self.beta_encryption_key {
251 args.push(OsString::from("--beta-encryption-key"));
252 args.push(OsString::from(beta_encryption_key));
253 }
254
255 Ok(ServiceInstallCtx {
256 args,
257 autostart: true,
258 contents: None,
259 environment: self.env_variables,
260 label: self.name.parse()?,
261 program: self.auditor_path.to_path_buf(),
262 username: Some(self.service_user.to_string()),
263 working_directory: None,
264 })
265 }
266}
267
268#[derive(Debug, PartialEq)]
269pub struct InstallFaucetServiceCtxBuilder {
270 pub bootstrap_peers: Vec<Multiaddr>,
271 pub env_variables: Option<Vec<(String, String)>>,
272 pub faucet_path: PathBuf,
273 pub local: bool,
274 pub log_dir_path: PathBuf,
275 pub name: String,
276 pub service_user: String,
277}
278
279impl InstallFaucetServiceCtxBuilder {
280 pub fn build(self) -> Result<ServiceInstallCtx> {
281 let mut args = vec![
282 OsString::from("--log-output-dest"),
283 OsString::from(self.log_dir_path.to_string_lossy().to_string()),
284 ];
285
286 if !self.bootstrap_peers.is_empty() {
287 let peers_str = self
288 .bootstrap_peers
289 .iter()
290 .map(|peer| peer.to_string())
291 .collect::<Vec<_>>()
292 .join(",");
293 args.push(OsString::from("--peer"));
294 args.push(OsString::from(peers_str));
295 }
296
297 args.push(OsString::from("server"));
298
299 Ok(ServiceInstallCtx {
300 args,
301 autostart: true,
302 contents: None,
303 environment: self.env_variables,
304 label: self.name.parse()?,
305 program: self.faucet_path.to_path_buf(),
306 username: Some(self.service_user.to_string()),
307 working_directory: None,
308 })
309 }
310}
311
312pub struct AddAuditorServiceOptions {
313 pub auditor_install_bin_path: PathBuf,
314 pub auditor_src_bin_path: PathBuf,
315 pub beta_encryption_key: Option<String>,
316 pub bootstrap_peers: Vec<Multiaddr>,
317 pub env_variables: Option<Vec<(String, String)>>,
318 pub service_log_dir_path: PathBuf,
319 pub user: String,
320 pub version: String,
321}
322
323pub struct AddFaucetServiceOptions {
324 pub bootstrap_peers: Vec<Multiaddr>,
325 pub env_variables: Option<Vec<(String, String)>>,
326 pub faucet_install_bin_path: PathBuf,
327 pub faucet_src_bin_path: PathBuf,
328 pub local: bool,
329 pub service_data_dir_path: PathBuf,
330 pub service_log_dir_path: PathBuf,
331 pub user: String,
332 pub version: String,
333}
334
335pub struct AddDaemonServiceOptions {
336 pub address: Ipv4Addr,
337 pub env_variables: Option<Vec<(String, String)>>,
338 pub daemon_install_bin_path: PathBuf,
339 pub daemon_src_bin_path: PathBuf,
340 pub port: u16,
341 pub user: String,
342 pub version: String,
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348 use sn_evm::{CustomNetwork, RewardsAddress};
349 use std::net::{IpAddr, Ipv4Addr};
350
351 fn create_default_builder() -> InstallNodeServiceCtxBuilder {
352 InstallNodeServiceCtxBuilder {
353 autostart: true,
354 bootstrap_peers: vec![],
355 data_dir_path: PathBuf::from("/data"),
356 env_variables: None,
357 evm_network: EvmNetwork::ArbitrumOne,
358 genesis: false,
359 home_network: false,
360 local: false,
361 log_dir_path: PathBuf::from("/logs"),
362 log_format: None,
363 name: "test-node".to_string(),
364 max_archived_log_files: None,
365 max_log_files: None,
366 metrics_port: None,
367 node_ip: None,
368 node_port: None,
369 owner: None,
370 rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
371 .unwrap(),
372 rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
373 safenode_path: PathBuf::from("/bin/safenode"),
374 service_user: None,
375 upnp: false,
376 }
377 }
378
379 fn create_custom_evm_network_builder() -> InstallNodeServiceCtxBuilder {
380 InstallNodeServiceCtxBuilder {
381 autostart: true,
382 bootstrap_peers: vec![],
383 data_dir_path: PathBuf::from("/data"),
384 env_variables: None,
385 evm_network: EvmNetwork::Custom(CustomNetwork {
386 rpc_url_http: "http://localhost:8545".parse().unwrap(),
387 payment_token_address: RewardsAddress::from_str(
388 "0x5FbDB2315678afecb367f032d93F642f64180aa3",
389 )
390 .unwrap(),
391 data_payments_address: RewardsAddress::from_str(
392 "0x8464135c8F25Da09e49BC8782676a84730C318bC",
393 )
394 .unwrap(),
395 }),
396 genesis: false,
397 home_network: false,
398 local: false,
399 log_dir_path: PathBuf::from("/logs"),
400 log_format: None,
401 name: "test-node".to_string(),
402 max_archived_log_files: None,
403 max_log_files: None,
404 metrics_port: None,
405 node_ip: None,
406 node_port: None,
407 owner: None,
408 rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
409 .unwrap(),
410 rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
411 safenode_path: PathBuf::from("/bin/safenode"),
412 service_user: None,
413 upnp: false,
414 }
415 }
416
417 fn create_builder_with_all_options_enabled() -> InstallNodeServiceCtxBuilder {
418 InstallNodeServiceCtxBuilder {
419 autostart: true,
420 bootstrap_peers: vec![],
421 data_dir_path: PathBuf::from("/data"),
422 env_variables: None,
423 evm_network: EvmNetwork::Custom(CustomNetwork {
424 rpc_url_http: "http://localhost:8545".parse().unwrap(),
425 payment_token_address: RewardsAddress::from_str(
426 "0x5FbDB2315678afecb367f032d93F642f64180aa3",
427 )
428 .unwrap(),
429 data_payments_address: RewardsAddress::from_str(
430 "0x8464135c8F25Da09e49BC8782676a84730C318bC",
431 )
432 .unwrap(),
433 }),
434 genesis: false,
435 home_network: false,
436 local: false,
437 log_dir_path: PathBuf::from("/logs"),
438 log_format: None,
439 name: "test-node".to_string(),
440 max_archived_log_files: Some(10),
441 max_log_files: Some(10),
442 metrics_port: None,
443 node_ip: None,
444 node_port: None,
445 owner: None,
446 rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
447 .unwrap(),
448 rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
449 safenode_path: PathBuf::from("/bin/safenode"),
450 service_user: None,
451 upnp: false,
452 }
453 }
454
455 #[test]
456 fn build_should_assign_expected_values_when_mandatory_options_are_provided() {
457 let builder = create_default_builder();
458 let result = builder.build().unwrap();
459
460 assert_eq!(result.label.to_string(), "test-node");
461 assert_eq!(result.program, PathBuf::from("/bin/safenode"));
462 assert!(result.autostart);
463 assert_eq!(result.username, None);
464 assert_eq!(result.working_directory, None);
465
466 let expected_args = vec![
467 "--rpc",
468 "127.0.0.1:8080",
469 "--root-dir",
470 "/data",
471 "--log-output-dest",
472 "/logs",
473 "--rewards-address",
474 "0x03B770D9cD32077cC0bF330c13C114a87643B124",
475 "evm-arbitrum-one",
476 ];
477 assert_eq!(
478 result
479 .args
480 .iter()
481 .map(|os| os.to_str().unwrap())
482 .collect::<Vec<_>>(),
483 expected_args
484 );
485 }
486
487 #[test]
488 fn build_should_assign_expected_values_when_a_custom_evm_network_is_provided() {
489 let builder = create_custom_evm_network_builder();
490 let result = builder.build().unwrap();
491
492 assert_eq!(result.label.to_string(), "test-node");
493 assert_eq!(result.program, PathBuf::from("/bin/safenode"));
494 assert!(result.autostart);
495 assert_eq!(result.username, None);
496 assert_eq!(result.working_directory, None);
497
498 let expected_args = vec![
499 "--rpc",
500 "127.0.0.1:8080",
501 "--root-dir",
502 "/data",
503 "--log-output-dest",
504 "/logs",
505 "--rewards-address",
506 "0x03B770D9cD32077cC0bF330c13C114a87643B124",
507 "evm-custom",
508 "--rpc-url",
509 "http://localhost:8545/",
510 "--payment-token-address",
511 "0x5FbDB2315678afecb367f032d93F642f64180aa3",
512 "--data-payments-address",
513 "0x8464135c8F25Da09e49BC8782676a84730C318bC",
514 ];
515 assert_eq!(
516 result
517 .args
518 .iter()
519 .map(|os| os.to_str().unwrap())
520 .collect::<Vec<_>>(),
521 expected_args
522 );
523 }
524
525 #[test]
526 fn build_should_assign_expected_values_when_all_options_are_enabled() {
527 let mut builder = create_builder_with_all_options_enabled();
528 builder.genesis = true;
529 builder.home_network = true;
530 builder.local = true;
531 builder.log_format = Some(LogFormat::Json);
532 builder.upnp = true;
533 builder.node_ip = Some(Ipv4Addr::new(192, 168, 1, 1));
534 builder.node_port = Some(12345);
535 builder.metrics_port = Some(9090);
536 builder.owner = Some("test-owner".to_string());
537 builder.bootstrap_peers = vec![
538 "/ip4/127.0.0.1/tcp/8080".parse().unwrap(),
539 "/ip4/192.168.1.1/tcp/8081".parse().unwrap(),
540 ];
541 builder.service_user = Some("safenode-user".to_string());
542
543 let result = builder.build().unwrap();
544
545 let expected_args = vec![
546 "--rpc",
547 "127.0.0.1:8080",
548 "--root-dir",
549 "/data",
550 "--log-output-dest",
551 "/logs",
552 "--first",
553 "--home-network",
554 "--local",
555 "--log-format",
556 "json",
557 "--upnp",
558 "--ip",
559 "192.168.1.1",
560 "--port",
561 "12345",
562 "--metrics-server-port",
563 "9090",
564 "--owner",
565 "test-owner",
566 "--max-archived-log-files",
567 "10",
568 "--max-log-files",
569 "10",
570 "--peer",
571 "/ip4/127.0.0.1/tcp/8080,/ip4/192.168.1.1/tcp/8081",
572 "--rewards-address",
573 "0x03B770D9cD32077cC0bF330c13C114a87643B124",
574 "evm-custom",
575 "--rpc-url",
576 "http://localhost:8545/",
577 "--payment-token-address",
578 "0x5FbDB2315678afecb367f032d93F642f64180aa3",
579 "--data-payments-address",
580 "0x8464135c8F25Da09e49BC8782676a84730C318bC",
581 ];
582 assert_eq!(
583 result
584 .args
585 .iter()
586 .map(|os| os.to_str().unwrap())
587 .collect::<Vec<_>>(),
588 expected_args
589 );
590 assert_eq!(result.username, Some("safenode-user".to_string()));
591 }
592
593 #[test]
594 fn build_should_assign_expected_values_when_environment_variables_are_provided() {
595 let mut builder = create_default_builder();
596 builder.env_variables = Some(vec![
597 ("VAR1".to_string(), "value1".to_string()),
598 ("VAR2".to_string(), "value2".to_string()),
599 ]);
600
601 let result = builder.build().unwrap();
602
603 assert_eq!(
604 result.environment,
605 Some(vec![
606 ("VAR1".to_string(), "value1".to_string()),
607 ("VAR2".to_string(), "value2".to_string()),
608 ])
609 );
610 }
611}