Skip to main content

zombienet_configuration/shared/
node.rs

1use std::{cell::RefCell, error::Error, fmt::Display, marker::PhantomData, path::PathBuf, rc::Rc};
2
3use multiaddr::Multiaddr;
4use serde::{ser::SerializeStruct, Deserialize, Serialize};
5
6use super::{
7    errors::FieldError,
8    helpers::{
9        ensure_port_unique, ensure_value_is_not_empty, generate_unique_node_name,
10        generate_unique_node_name_from_names, merge_errors, merge_errors_vecs,
11    },
12    macros::states,
13    resources::ResourcesBuilder,
14    types::{AssetLocation, ChainDefaultContext, Command, Image, ValidationContext, U128},
15};
16use crate::{
17    shared::{
18        resources::Resources,
19        types::{Arg, Port},
20    },
21    types::JamNodeMode,
22    utils::{default_as_true, default_initial_balance},
23};
24
25states! {
26    Buildable,
27    Initial
28}
29
30/// An environment variable with a name and a value.
31/// It can be constructed from a `(&str, &str)`.
32///
33/// # Examples:
34///
35/// ```
36/// use zombienet_configuration::shared::node::EnvVar;
37///
38/// let simple_var: EnvVar = ("FOO", "BAR").into();
39///
40/// assert_eq!(
41///     simple_var,
42///     EnvVar {
43///         name: "FOO".into(),
44///         value: "BAR".into()
45///     }
46/// )
47/// ```
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub struct EnvVar {
50    /// The name of the environment variable.
51    pub name: String,
52
53    /// The value of the environment variable.
54    pub value: String,
55}
56
57impl From<(&str, &str)> for EnvVar {
58    fn from((name, value): (&str, &str)) -> Self {
59        Self {
60            name: name.to_owned(),
61            value: value.to_owned(),
62        }
63    }
64}
65
66/// Basic configuration for long running process (e.g nodes)
67#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
68pub struct BaseNodeConfig {
69    pub(crate) name: String,
70    pub(crate) image: Option<Image>,
71    pub(crate) command: Option<Command>,
72    pub(crate) subcommand: Option<Command>,
73    #[serde(default)]
74    pub(crate) args: Vec<Arg>,
75    #[serde(default)]
76    pub(crate) env: Vec<EnvVar>,
77    pub(crate) resources: Option<Resources>,
78    #[serde(default)]
79    // used to skip serialization of fields with defaults to avoid duplication
80    pub(crate) chain_context: ChainDefaultContext,
81}
82
83impl Serialize for BaseNodeConfig {
84    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
85    where
86        S: serde::Serializer,
87    {
88        let mut state = serializer.serialize_struct("BaseNodeConfig", 1)?;
89        state.serialize_field("name", &self.name)?;
90
91        if self.image == self.chain_context.default_image {
92            state.skip_field("image")?;
93        } else {
94            state.serialize_field("image", &self.image)?;
95        }
96
97        if self.command == self.chain_context.default_command {
98            state.skip_field("command")?;
99        } else {
100            state.serialize_field("command", &self.command)?;
101        }
102
103        if self.subcommand.is_none() {
104            state.skip_field("subcommand")?;
105        } else {
106            state.serialize_field("subcommand", &self.subcommand)?;
107        }
108
109        if self.args.is_empty() || self.args == self.chain_context.default_args {
110            state.skip_field("args")?;
111        } else {
112            state.serialize_field("args", &self.args)?;
113        }
114
115        if self.env.is_empty() {
116            state.skip_field("env")?;
117        } else {
118            state.serialize_field("env", &self.env)?;
119        }
120
121        if self.resources == self.chain_context.default_resources {
122            state.skip_field("resources")?;
123        } else {
124            state.serialize_field("resources", &self.resources)?;
125        }
126
127        state.skip_field("chain_context")?;
128        state.end()
129    }
130}
131
132/// Basic configuration for long running process (e.g nodes)
133#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
134pub struct JamNodeConfig {
135    #[serde(flatten)]
136    pub(crate) base_config: BaseNodeConfig,
137    /// Mode (validator / ordinary / proxy )
138    pub(crate) mode: JamNodeMode,
139    /// RPC port
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub(crate) rpc_port: Option<Port>,
142    /// Telemetry endpoint.
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub(crate) telemetry_endpoint: Option<String>,
145}
146
147impl JamNodeConfig {
148    /// Node name (should be unique).
149    pub fn name(&self) -> &str {
150        &self.base_config.name
151    }
152
153    /// Image to run (only podman/k8s).
154    pub fn image(&self) -> Option<&Image> {
155        self.base_config.image.as_ref()
156    }
157
158    /// Command to run the node.
159    pub fn command(&self) -> Option<&Command> {
160        self.base_config.command.as_ref()
161    }
162
163    /// Subcommand to run the node.
164    pub fn subcommand(&self) -> Option<&Command> {
165        self.base_config.subcommand.as_ref()
166    }
167
168    /// Arguments to use for node.
169    pub fn args(&self) -> Vec<&Arg> {
170        self.base_config.args.iter().collect()
171    }
172
173    // /// Arguments to use for node.
174    // pub(crate) fn set_args(&mut self, args: Vec<Arg>) {
175    //     self.base_config.args = args;
176    // }
177
178    /// Default resources.
179    pub fn resources(&self) -> Option<&Resources> {
180        self.base_config.resources.as_ref()
181    }
182
183    /// Environment variables to set (inside pod for podman/k8s, inside shell for native).
184    pub fn env(&self) -> Vec<&EnvVar> {
185        self.base_config.env.iter().collect()
186    }
187
188    /// Whether the node is a validator.
189    pub fn mode(&self) -> &JamNodeMode {
190        &self.mode
191    }
192
193    /// RPC port to use.
194    pub fn rpc_port(&self) -> Option<u16> {
195        self.rpc_port
196    }
197
198    /// Telemetry endpoint to use.
199    pub fn telemetry_endpoint(&self) -> Option<&str> {
200        self.telemetry_endpoint.as_deref()
201    }
202}
203/// A node configuration, with fine-grained configuration options.
204#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
205pub struct NodeConfig {
206    pub(crate) name: String,
207    pub(crate) image: Option<Image>,
208    pub(crate) command: Option<Command>,
209    pub(crate) subcommand: Option<Command>,
210    #[serde(default)]
211    args: Vec<Arg>,
212    #[serde(alias = "validator", default = "default_as_true")]
213    pub(crate) is_validator: bool,
214    #[serde(alias = "invulnerable", default = "default_as_true")]
215    pub(crate) is_invulnerable: bool,
216    #[serde(alias = "bootnode", default)]
217    pub(crate) is_bootnode: bool,
218    #[serde(alias = "balance")]
219    #[serde(default = "default_initial_balance")]
220    initial_balance: U128,
221    #[serde(default)]
222    env: Vec<EnvVar>,
223    #[serde(default)]
224    bootnodes_addresses: Vec<Multiaddr>,
225    pub(crate) resources: Option<Resources>,
226    ws_port: Option<Port>,
227    rpc_port: Option<Port>,
228    prometheus_port: Option<Port>,
229    p2p_port: Option<Port>,
230    p2p_cert_hash: Option<String>,
231    pub(crate) db_snapshot: Option<AssetLocation>,
232    /// Optional override for the automatically generated EVM (eth) session key.
233    /// When set, override the auto-generated key so the seed will not be part of the resulting zombie.json
234    #[serde(default, skip_serializing_if = "Option::is_none")]
235    override_eth_key: Option<String>,
236    #[serde(default)]
237    // used to skip serialization of fields with defaults to avoid duplication
238    pub(crate) chain_context: ChainDefaultContext,
239    pub(crate) node_log_path: Option<PathBuf>,
240    // optional node keystore path override
241    keystore_path: Option<PathBuf>,
242    /// Keystore key types to generate.
243    /// Supports short form (e.g., "audi") using predefined schemas,
244    /// or long form (e.g., "audi_sr") with explicit schema (sr, ed, ec).
245    #[serde(default)]
246    keystore_key_types: Vec<String>,
247    /// Chain spec session key types to inject.
248    /// Supports short form (e.g., "aura") using predefined schemas,
249    /// or long form (e.g., "aura_sr") with explicit schema (sr, ed, ec).
250    /// When empty, uses the default session keys from the chain spec.
251    #[serde(default)]
252    chain_spec_key_types: Vec<String>,
253}
254
255impl Serialize for NodeConfig {
256    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
257    where
258        S: serde::Serializer,
259    {
260        let mut state = serializer.serialize_struct("NodeConfig", 19)?;
261        state.serialize_field("name", &self.name)?;
262
263        if self.image == self.chain_context.default_image {
264            state.skip_field("image")?;
265        } else {
266            state.serialize_field("image", &self.image)?;
267        }
268
269        if self.command == self.chain_context.default_command {
270            state.skip_field("command")?;
271        } else {
272            state.serialize_field("command", &self.command)?;
273        }
274
275        if self.subcommand.is_none() {
276            state.skip_field("subcommand")?;
277        } else {
278            state.serialize_field("subcommand", &self.subcommand)?;
279        }
280
281        if self.args.is_empty() || self.args == self.chain_context.default_args {
282            state.skip_field("args")?;
283        } else {
284            state.serialize_field("args", &self.args)?;
285        }
286
287        state.serialize_field("validator", &self.is_validator)?;
288        state.serialize_field("invulnerable", &self.is_invulnerable)?;
289        state.serialize_field("bootnode", &self.is_bootnode)?;
290        state.serialize_field("balance", &self.initial_balance)?;
291
292        if self.env.is_empty() {
293            state.skip_field("env")?;
294        } else {
295            state.serialize_field("env", &self.env)?;
296        }
297
298        if self.bootnodes_addresses.is_empty() {
299            state.skip_field("bootnodes_addresses")?;
300        } else {
301            state.serialize_field("bootnodes_addresses", &self.bootnodes_addresses)?;
302        }
303
304        if self.resources == self.chain_context.default_resources {
305            state.skip_field("resources")?;
306        } else {
307            state.serialize_field("resources", &self.resources)?;
308        }
309
310        state.serialize_field("ws_port", &self.ws_port)?;
311        state.serialize_field("rpc_port", &self.rpc_port)?;
312        state.serialize_field("prometheus_port", &self.prometheus_port)?;
313        state.serialize_field("p2p_port", &self.p2p_port)?;
314        state.serialize_field("p2p_cert_hash", &self.p2p_cert_hash)?;
315        state.serialize_field("override_eth_key", &self.override_eth_key)?;
316
317        if self.db_snapshot == self.chain_context.default_db_snapshot {
318            state.skip_field("db_snapshot")?;
319        } else {
320            state.serialize_field("db_snapshot", &self.db_snapshot)?;
321        }
322
323        if self.node_log_path.is_none() {
324            state.skip_field("node_log_path")?;
325        } else {
326            state.serialize_field("node_log_path", &self.node_log_path)?;
327        }
328
329        if self.keystore_path.is_none() {
330            state.skip_field("keystore_path")?;
331        } else {
332            state.serialize_field("keystore_path", &self.keystore_path)?;
333        }
334
335        if self.keystore_key_types.is_empty() {
336            state.skip_field("keystore_key_types")?;
337        } else {
338            state.serialize_field("keystore_key_types", &self.keystore_key_types)?;
339        }
340
341        if self.chain_spec_key_types.is_empty() {
342            state.skip_field("chain_spec_key_typese")?;
343        } else {
344            state.serialize_field("chain_spec_key_types", &self.chain_spec_key_types)?;
345        }
346
347        state.skip_field("chain_context")?;
348        state.end()
349    }
350}
351
352/// A group of nodes configuration
353#[derive(Debug, Clone, PartialEq, Deserialize)]
354pub struct GroupNodeConfig {
355    #[serde(flatten)]
356    pub(crate) base_config: NodeConfig,
357    pub(crate) count: usize,
358}
359
360impl GroupNodeConfig {
361    /// Expands the group into individual node configs.
362    /// Each node will have the same base configuration, but with unique names and log paths.
363    pub fn expand_group_configs(&self) -> Vec<NodeConfig> {
364        let mut used_names = std::collections::HashSet::new();
365
366        (0..self.count)
367            .map(|i| {
368                let mut node = self.base_config.clone();
369                // append count sufix
370                let node_name = format!("{}-{i}", node.name);
371
372                let unique_name = generate_unique_node_name_from_names(node_name, &mut used_names);
373                node.name = unique_name;
374
375                // If base config has a log path, generate unique log path for each node
376                if let Some(ref base_log_path) = node.node_log_path {
377                    let unique_log_path = if let Some(parent) = base_log_path.parent() {
378                        parent.join(format!("{}.log", node.name))
379                    } else {
380                        PathBuf::from(format!("{}.log", node.name))
381                    };
382                    node.node_log_path = Some(unique_log_path);
383                }
384
385                node
386            })
387            .collect()
388    }
389}
390
391impl Serialize for GroupNodeConfig {
392    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
393    where
394        S: serde::Serializer,
395    {
396        let mut state = serializer.serialize_struct("GroupNodeConfig", 18)?;
397        state.serialize_field("NodeConfig", &self.base_config)?;
398        state.serialize_field("count", &self.count)?;
399        state.end()
400    }
401}
402
403impl NodeConfig {
404    /// Node name (should be unique).
405    pub fn name(&self) -> &str {
406        &self.name
407    }
408
409    /// Image to run (only podman/k8s).
410    pub fn image(&self) -> Option<&Image> {
411        self.image.as_ref()
412    }
413
414    /// Command to run the node.
415    pub fn command(&self) -> Option<&Command> {
416        self.command.as_ref()
417    }
418
419    /// Subcommand to run the node.
420    pub fn subcommand(&self) -> Option<&Command> {
421        self.subcommand.as_ref()
422    }
423
424    /// Arguments to use for node.
425    pub fn args(&self) -> Vec<&Arg> {
426        self.args.iter().collect()
427    }
428
429    /// Arguments to use for node.
430    pub(crate) fn set_args(&mut self, args: Vec<Arg>) {
431        self.args = args;
432    }
433
434    /// Whether the node is a validator.
435    pub fn is_validator(&self) -> bool {
436        self.is_validator
437    }
438
439    /// Whether the node keys must be added to invulnerables.
440    pub fn is_invulnerable(&self) -> bool {
441        self.is_invulnerable
442    }
443
444    /// Whether the node is a bootnode.
445    pub fn is_bootnode(&self) -> bool {
446        self.is_bootnode
447    }
448
449    /// Node initial balance present in genesis.
450    pub fn initial_balance(&self) -> u128 {
451        self.initial_balance.0
452    }
453
454    /// Environment variables to set (inside pod for podman/k8s, inside shell for native).
455    pub fn env(&self) -> Vec<&EnvVar> {
456        self.env.iter().collect()
457    }
458
459    /// List of node's bootnodes addresses to use.
460    pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
461        self.bootnodes_addresses.iter().collect()
462    }
463
464    /// Default resources.
465    pub fn resources(&self) -> Option<&Resources> {
466        self.resources.as_ref()
467    }
468
469    /// Websocket port to use.
470    pub fn ws_port(&self) -> Option<u16> {
471        self.ws_port
472    }
473
474    /// RPC port to use.
475    pub fn rpc_port(&self) -> Option<u16> {
476        self.rpc_port
477    }
478
479    /// Prometheus port to use.
480    pub fn prometheus_port(&self) -> Option<u16> {
481        self.prometheus_port
482    }
483
484    /// P2P port to use.
485    pub fn p2p_port(&self) -> Option<u16> {
486        self.p2p_port
487    }
488
489    /// `libp2p` cert hash to use with `WebRTC` transport.
490    pub fn p2p_cert_hash(&self) -> Option<&str> {
491        self.p2p_cert_hash.as_deref()
492    }
493
494    /// Database snapshot.
495    pub fn db_snapshot(&self) -> Option<&AssetLocation> {
496        self.db_snapshot.as_ref()
497    }
498
499    /// Node log path
500    pub fn node_log_path(&self) -> Option<&PathBuf> {
501        self.node_log_path.as_ref()
502    }
503
504    /// Keystore path
505    pub fn keystore_path(&self) -> Option<&PathBuf> {
506        self.keystore_path.as_ref()
507    }
508
509    /// Override EVM session key to use for the node
510    pub fn override_eth_key(&self) -> Option<&str> {
511        self.override_eth_key.as_deref()
512    }
513
514    /// Keystore key types to generate.
515    /// Returns the list of key type specifications (short form like "audi" or long form like "audi_sr").
516    pub fn keystore_key_types(&self) -> Vec<&str> {
517        self.keystore_key_types.iter().map(String::as_str).collect()
518    }
519
520    /// Chain spec session key types to inject.
521    /// Returns the list of key type specifications (short form like "aura" or long form like "aura_sr").
522    pub fn chain_spec_key_types(&self) -> Vec<&str> {
523        self.chain_spec_key_types
524            .iter()
525            .map(String::as_str)
526            .collect()
527    }
528}
529
530/// A node configuration builder, used to build a [`NodeConfig`] declaratively with fields validation.
531pub struct NodeConfigBuilder<S> {
532    config: NodeConfig,
533    validation_context: Rc<RefCell<ValidationContext>>,
534    errors: Vec<anyhow::Error>,
535    _state: PhantomData<S>,
536}
537
538impl Default for NodeConfigBuilder<Initial> {
539    fn default() -> Self {
540        Self {
541            config: NodeConfig {
542                name: "".into(),
543                image: None,
544                command: None,
545                subcommand: None,
546                args: vec![],
547                is_validator: true,
548                is_invulnerable: true,
549                is_bootnode: false,
550                initial_balance: 2_000_000_000_000.into(),
551                env: vec![],
552                bootnodes_addresses: vec![],
553                resources: None,
554                ws_port: None,
555                rpc_port: None,
556                prometheus_port: None,
557                p2p_port: None,
558                p2p_cert_hash: None,
559                db_snapshot: None,
560                override_eth_key: None,
561                chain_context: Default::default(),
562                node_log_path: None,
563                keystore_path: None,
564                keystore_key_types: vec![],
565                chain_spec_key_types: vec![],
566            },
567            validation_context: Default::default(),
568            errors: vec![],
569            _state: PhantomData,
570        }
571    }
572}
573
574impl<A> NodeConfigBuilder<A> {
575    fn transition<B>(
576        config: NodeConfig,
577        validation_context: Rc<RefCell<ValidationContext>>,
578        errors: Vec<anyhow::Error>,
579    ) -> NodeConfigBuilder<B> {
580        NodeConfigBuilder {
581            config,
582            validation_context,
583            errors,
584            _state: PhantomData,
585        }
586    }
587}
588
589impl NodeConfigBuilder<Initial> {
590    pub fn new(
591        chain_context: ChainDefaultContext,
592        validation_context: Rc<RefCell<ValidationContext>>,
593    ) -> Self {
594        Self::transition(
595            NodeConfig {
596                command: chain_context.default_command.clone(),
597                image: chain_context.default_image.clone(),
598                resources: chain_context.default_resources.clone(),
599                db_snapshot: chain_context.default_db_snapshot.clone(),
600                args: chain_context.default_args.clone(),
601                chain_context,
602                ..Self::default().config
603            },
604            validation_context,
605            vec![],
606        )
607    }
608
609    /// Set the name of the node.
610    pub fn with_name<T: Into<String> + Copy>(self, name: T) -> NodeConfigBuilder<Buildable> {
611        let name: String = generate_unique_node_name(name, self.validation_context.clone());
612
613        match ensure_value_is_not_empty(&name) {
614            Ok(_) => Self::transition(
615                NodeConfig {
616                    name,
617                    ..self.config
618                },
619                self.validation_context,
620                self.errors,
621            ),
622            Err(e) => Self::transition(
623                NodeConfig {
624                    // we still set the name in error case to display error path
625                    name,
626                    ..self.config
627                },
628                self.validation_context,
629                merge_errors(self.errors, FieldError::Name(e).into()),
630            ),
631        }
632    }
633}
634
635impl NodeConfigBuilder<Buildable> {
636    /// Set the command that will be executed to launch the node. Override the default.
637    pub fn with_command<T>(self, command: T) -> Self
638    where
639        T: TryInto<Command>,
640        T::Error: Error + Send + Sync + 'static,
641    {
642        match command.try_into() {
643            Ok(command) => Self::transition(
644                NodeConfig {
645                    command: Some(command),
646                    ..self.config
647                },
648                self.validation_context,
649                self.errors,
650            ),
651            Err(error) => Self::transition(
652                self.config,
653                self.validation_context,
654                merge_errors(self.errors, FieldError::Command(error.into()).into()),
655            ),
656        }
657    }
658
659    /// Set the subcommand that will be executed to launch the node.
660    pub fn with_subcommand<T>(self, subcommand: T) -> Self
661    where
662        T: TryInto<Command>,
663        T::Error: Error + Send + Sync + 'static,
664    {
665        match subcommand.try_into() {
666            Ok(subcommand) => Self::transition(
667                NodeConfig {
668                    subcommand: Some(subcommand),
669                    ..self.config
670                },
671                self.validation_context,
672                self.errors,
673            ),
674            Err(error) => Self::transition(
675                self.config,
676                self.validation_context,
677                merge_errors(self.errors, FieldError::Command(error.into()).into()),
678            ),
679        }
680    }
681
682    /// Set the image that will be used for the node (only podman/k8s). Override the default.
683    pub fn with_image<T>(self, image: T) -> Self
684    where
685        T: TryInto<Image>,
686        T::Error: Error + Send + Sync + 'static,
687    {
688        match image.try_into() {
689            Ok(image) => Self::transition(
690                NodeConfig {
691                    image: Some(image),
692                    ..self.config
693                },
694                self.validation_context,
695                self.errors,
696            ),
697            Err(error) => Self::transition(
698                self.config,
699                self.validation_context,
700                merge_errors(self.errors, FieldError::Image(error.into()).into()),
701            ),
702        }
703    }
704
705    /// Set the arguments that will be used when launching the node. Override the default_args of the chain context.
706    pub fn with_args(self, args: Vec<Arg>) -> Self {
707        Self::transition(
708            NodeConfig {
709                args,
710                ..self.config
711            },
712            self.validation_context,
713            self.errors,
714        )
715    }
716
717    /// Set whether the node is a validator.
718    pub fn validator(self, choice: bool) -> Self {
719        Self::transition(
720            NodeConfig {
721                is_validator: choice,
722                ..self.config
723            },
724            self.validation_context,
725            self.errors,
726        )
727    }
728
729    /// Set whether the node is invulnerable.
730    pub fn invulnerable(self, choice: bool) -> Self {
731        Self::transition(
732            NodeConfig {
733                is_invulnerable: choice,
734                ..self.config
735            },
736            self.validation_context,
737            self.errors,
738        )
739    }
740
741    /// Set whether the node is a bootnode.
742    pub fn bootnode(self, choice: bool) -> Self {
743        Self::transition(
744            NodeConfig {
745                is_bootnode: choice,
746                ..self.config
747            },
748            self.validation_context,
749            self.errors,
750        )
751    }
752
753    /// Override the EVM session key to use for the node
754    pub fn with_override_eth_key(self, session_key: impl Into<String>) -> Self {
755        Self::transition(
756            NodeConfig {
757                override_eth_key: Some(session_key.into()),
758                ..self.config
759            },
760            self.validation_context,
761            self.errors,
762        )
763    }
764
765    /// Set the node initial balance.
766    pub fn with_initial_balance(self, initial_balance: u128) -> Self {
767        Self::transition(
768            NodeConfig {
769                initial_balance: initial_balance.into(),
770                ..self.config
771            },
772            self.validation_context,
773            self.errors,
774        )
775    }
776
777    /// Set the node environment variables that will be used when launched. Override the default.
778    pub fn with_env(self, env: Vec<impl Into<EnvVar>>) -> Self {
779        let env = env.into_iter().map(|var| var.into()).collect::<Vec<_>>();
780
781        Self::transition(
782            NodeConfig { env, ..self.config },
783            self.validation_context,
784            self.errors,
785        )
786    }
787
788    /// Set the bootnodes addresses that the node will try to connect to. Override the default.
789    ///
790    /// Note: Bootnode address replacements are NOT supported here.
791    /// Only arguments (`args`) support dynamic replacements. Bootnode addresses must be a valid address.
792    pub fn with_raw_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
793    where
794        T: TryInto<Multiaddr> + Display + Copy,
795        T::Error: Error + Send + Sync + 'static,
796    {
797        let mut addrs = vec![];
798        let mut errors = vec![];
799
800        for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
801            match addr.try_into() {
802                Ok(addr) => addrs.push(addr),
803                Err(error) => errors.push(
804                    FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
805                ),
806            }
807        }
808
809        Self::transition(
810            NodeConfig {
811                bootnodes_addresses: addrs,
812                ..self.config
813            },
814            self.validation_context,
815            merge_errors_vecs(self.errors, errors),
816        )
817    }
818
819    /// Set the resources limits what will be used for the node (only podman/k8s). Override the default.
820    pub fn with_resources(self, f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder) -> Self {
821        match f(ResourcesBuilder::new()).build() {
822            Ok(resources) => Self::transition(
823                NodeConfig {
824                    resources: Some(resources),
825                    ..self.config
826                },
827                self.validation_context,
828                self.errors,
829            ),
830            Err(errors) => Self::transition(
831                self.config,
832                self.validation_context,
833                merge_errors_vecs(
834                    self.errors,
835                    errors
836                        .into_iter()
837                        .map(|error| FieldError::Resources(error).into())
838                        .collect::<Vec<_>>(),
839                ),
840            ),
841        }
842    }
843
844    /// Set the websocket port that will be exposed. Uniqueness across config will be checked.
845    pub fn with_ws_port(self, ws_port: Port) -> Self {
846        match ensure_port_unique(ws_port, self.validation_context.clone()) {
847            Ok(_) => Self::transition(
848                NodeConfig {
849                    ws_port: Some(ws_port),
850                    ..self.config
851                },
852                self.validation_context,
853                self.errors,
854            ),
855            Err(error) => Self::transition(
856                self.config,
857                self.validation_context,
858                merge_errors(self.errors, FieldError::WsPort(error).into()),
859            ),
860        }
861    }
862
863    /// Set the RPC port that will be exposed. Uniqueness across config will be checked.
864    pub fn with_rpc_port(self, rpc_port: Port) -> Self {
865        match ensure_port_unique(rpc_port, self.validation_context.clone()) {
866            Ok(_) => Self::transition(
867                NodeConfig {
868                    rpc_port: Some(rpc_port),
869                    ..self.config
870                },
871                self.validation_context,
872                self.errors,
873            ),
874            Err(error) => Self::transition(
875                self.config,
876                self.validation_context,
877                merge_errors(self.errors, FieldError::RpcPort(error).into()),
878            ),
879        }
880    }
881
882    /// Set the Prometheus port that will be exposed for metrics. Uniqueness across config will be checked.
883    pub fn with_prometheus_port(self, prometheus_port: Port) -> Self {
884        match ensure_port_unique(prometheus_port, self.validation_context.clone()) {
885            Ok(_) => Self::transition(
886                NodeConfig {
887                    prometheus_port: Some(prometheus_port),
888                    ..self.config
889                },
890                self.validation_context,
891                self.errors,
892            ),
893            Err(error) => Self::transition(
894                self.config,
895                self.validation_context,
896                merge_errors(self.errors, FieldError::PrometheusPort(error).into()),
897            ),
898        }
899    }
900
901    /// Set the P2P port that will be exposed. Uniqueness across config will be checked.
902    pub fn with_p2p_port(self, p2p_port: Port) -> Self {
903        match ensure_port_unique(p2p_port, self.validation_context.clone()) {
904            Ok(_) => Self::transition(
905                NodeConfig {
906                    p2p_port: Some(p2p_port),
907                    ..self.config
908                },
909                self.validation_context,
910                self.errors,
911            ),
912            Err(error) => Self::transition(
913                self.config,
914                self.validation_context,
915                merge_errors(self.errors, FieldError::P2pPort(error).into()),
916            ),
917        }
918    }
919
920    /// Set the P2P cert hash that will be used as part of the multiaddress
921    /// if and only if the multiaddress is set to use `webrtc`.
922    pub fn with_p2p_cert_hash(self, p2p_cert_hash: impl Into<String>) -> Self {
923        Self::transition(
924            NodeConfig {
925                p2p_cert_hash: Some(p2p_cert_hash.into()),
926                ..self.config
927            },
928            self.validation_context,
929            self.errors,
930        )
931    }
932
933    /// Set the database snapshot that will be used to launch the node. Override the default.
934    pub fn with_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
935        Self::transition(
936            NodeConfig {
937                db_snapshot: Some(location.into()),
938                ..self.config
939            },
940            self.validation_context,
941            self.errors,
942        )
943    }
944
945    /// Like [`Self::with_db_snapshot`], but a no-op when `location` is
946    /// `None`. Lets a caller parametrise one node builder over both
947    /// "fresh" (`None`) and "from snapshot" (`Some`) without branching.
948    pub fn with_optional_db_snapshot(self, location: Option<impl Into<AssetLocation>>) -> Self {
949        match location {
950            Some(location) => self.with_db_snapshot(location),
951            None => self,
952        }
953    }
954
955    /// Set the node log path that will be used to launch the node.
956    pub fn with_log_path(self, log_path: impl Into<PathBuf>) -> Self {
957        Self::transition(
958            NodeConfig {
959                node_log_path: Some(log_path.into()),
960                ..self.config
961            },
962            self.validation_context,
963            self.errors,
964        )
965    }
966
967    /// Set the keystore path override.
968    pub fn with_keystore_path(self, keystore_path: impl Into<PathBuf>) -> Self {
969        Self::transition(
970            NodeConfig {
971                keystore_path: Some(keystore_path.into()),
972                ..self.config
973            },
974            self.validation_context,
975            self.errors,
976        )
977    }
978
979    /// Set the keystore key types to generate.
980    ///
981    /// Each key type can be specified in short form (e.g., "audi") using predefined schemas
982    /// (defaults to `sr` if no predefined schema exists for the key type),
983    /// or in long form (e.g., "audi_sr") with an explicit schema (sr, ed, ec).
984    ///
985    /// # Examples
986    ///
987    /// ```
988    /// use zombienet_configuration::shared::{node::NodeConfigBuilder, types::ChainDefaultContext};
989    ///
990    /// let config = NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
991    ///     .with_name("node")
992    ///     .with_keystore_key_types(vec!["audi", "gran", "cust_sr"])
993    ///     .build()
994    ///     .unwrap();
995    ///
996    /// assert_eq!(config.keystore_key_types(), &["audi", "gran", "cust_sr"]);
997    /// ```
998    pub fn with_keystore_key_types(self, key_types: Vec<impl Into<String>>) -> Self {
999        Self::transition(
1000            NodeConfig {
1001                keystore_key_types: key_types.into_iter().map(|k| k.into()).collect(),
1002                ..self.config
1003            },
1004            self.validation_context,
1005            self.errors,
1006        )
1007    }
1008
1009    /// Set the chain spec session key types to inject.
1010    ///
1011    /// Each key type can be specified in short form (e.g., "aura") using predefined schemas
1012    /// (defaults to `sr` if no predefined schema exists for the key type),
1013    /// or in long form (e.g., "aura_sr") with an explicit schema (sr, ed, ec).
1014    ///
1015    /// When specified, only these keys will be injected into the chain spec session keys.
1016    /// When empty, uses the default session keys from the chain spec.
1017    ///
1018    /// # Examples
1019    ///
1020    /// ```
1021    /// use zombienet_configuration::shared::{node::NodeConfigBuilder, types::ChainDefaultContext};
1022    ///
1023    /// let config = NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1024    ///     .with_name("node")
1025    ///     .with_chain_spec_key_types(vec!["aura", "grandpa", "babe_sr"])
1026    ///     .build()
1027    ///     .unwrap();
1028    ///
1029    /// assert_eq!(
1030    ///     config.chain_spec_key_types(),
1031    ///     &["aura", "grandpa", "babe_sr"]
1032    /// );
1033    /// ```
1034    pub fn with_chain_spec_key_types(self, key_types: Vec<impl Into<String>>) -> Self {
1035        Self::transition(
1036            NodeConfig {
1037                chain_spec_key_types: key_types.into_iter().map(|k| k.into()).collect(),
1038                ..self.config
1039            },
1040            self.validation_context,
1041            self.errors,
1042        )
1043    }
1044
1045    /// Seals the builder and returns a [`NodeConfig`] if there are no validation errors, else returns errors.
1046    pub fn build(self) -> Result<NodeConfig, (String, Vec<anyhow::Error>)> {
1047        if !self.errors.is_empty() {
1048            return Err((self.config.name.clone(), self.errors));
1049        }
1050
1051        Ok(self.config)
1052    }
1053}
1054
1055/// A group node configuration builder, used to build a [`GroupNodeConfig`] declaratively with fields validation.
1056pub struct GroupNodeConfigBuilder<S> {
1057    base_config: NodeConfig,
1058    count: usize,
1059    validation_context: Rc<RefCell<ValidationContext>>,
1060    errors: Vec<anyhow::Error>,
1061    _state: PhantomData<S>,
1062}
1063
1064impl GroupNodeConfigBuilder<Initial> {
1065    pub fn new(
1066        chain_context: ChainDefaultContext,
1067        validation_context: Rc<RefCell<ValidationContext>>,
1068    ) -> Self {
1069        let (errors, base_config) =
1070            match NodeConfigBuilder::new(chain_context.clone(), validation_context.clone())
1071                .with_name(" ") // placeholder
1072                .build()
1073            {
1074                Ok(base_config) => (vec![], base_config),
1075                Err((_name, errors)) => (errors, NodeConfig::default()),
1076            };
1077
1078        Self {
1079            base_config,
1080            count: 1,
1081            validation_context,
1082            errors,
1083            _state: PhantomData,
1084        }
1085    }
1086
1087    /// Set the base node config using a closure.
1088    pub fn with_base_node(
1089        mut self,
1090        f: impl FnOnce(NodeConfigBuilder<Initial>) -> NodeConfigBuilder<Buildable>,
1091    ) -> GroupNodeConfigBuilder<Buildable> {
1092        match f(NodeConfigBuilder::new(
1093            ChainDefaultContext::default(),
1094            self.validation_context.clone(),
1095        ))
1096        .build()
1097        {
1098            Ok(node) => {
1099                self.base_config = node;
1100                GroupNodeConfigBuilder {
1101                    base_config: self.base_config,
1102                    count: self.count,
1103                    validation_context: self.validation_context,
1104                    errors: self.errors,
1105                    _state: PhantomData,
1106                }
1107            },
1108            Err((_name, errors)) => {
1109                self.errors.extend(errors);
1110                GroupNodeConfigBuilder {
1111                    base_config: self.base_config,
1112                    count: self.count,
1113                    validation_context: self.validation_context,
1114                    errors: self.errors,
1115                    _state: PhantomData,
1116                }
1117            },
1118        }
1119    }
1120
1121    /// Set the number of nodes in the group.
1122    pub fn with_count(mut self, count: usize) -> Self {
1123        self.count = count;
1124        self
1125    }
1126}
1127
1128impl GroupNodeConfigBuilder<Buildable> {
1129    /// Set the number of nodes in the group.
1130    pub fn with_count(mut self, count: usize) -> Self {
1131        self.count = count;
1132        self
1133    }
1134
1135    pub fn build(self) -> Result<GroupNodeConfig, (String, Vec<anyhow::Error>)> {
1136        if self.count == 0 {
1137            return Err((
1138                self.base_config.name().to_string(),
1139                vec![anyhow::anyhow!("Count cannot be zero")],
1140            ));
1141        }
1142
1143        if !self.errors.is_empty() {
1144            return Err((self.base_config.name().to_string(), self.errors));
1145        }
1146
1147        Ok(GroupNodeConfig {
1148            base_config: self.base_config,
1149            count: self.count,
1150        })
1151    }
1152}
1153
1154// JAM
1155/// A node configuration builder, used to build a [`JamNodeConfig`] declaratively with fields validation.
1156pub struct JamNodeConfigBuilder<S> {
1157    config: JamNodeConfig,
1158    validation_context: Rc<RefCell<ValidationContext>>,
1159    errors: Vec<anyhow::Error>,
1160    _state: PhantomData<S>,
1161}
1162
1163impl Default for JamNodeConfigBuilder<Initial> {
1164    fn default() -> Self {
1165        Self {
1166            config: JamNodeConfig {
1167                rpc_port: None,
1168                base_config: BaseNodeConfig {
1169                    name: "".into(), // placeholder
1170                    image: None,
1171                    command: None,
1172                    subcommand: None,
1173                    args: vec![],
1174                    env: vec![],
1175                    resources: None,
1176                    chain_context: Default::default(),
1177                },
1178                mode: JamNodeMode::Validator, //
1179                telemetry_endpoint: None,
1180            },
1181            validation_context: Default::default(),
1182            errors: vec![],
1183            _state: PhantomData,
1184        }
1185    }
1186}
1187
1188impl<A> JamNodeConfigBuilder<A> {
1189    fn transition<B>(
1190        config: JamNodeConfig,
1191        validation_context: Rc<RefCell<ValidationContext>>,
1192        errors: Vec<anyhow::Error>,
1193    ) -> JamNodeConfigBuilder<B> {
1194        JamNodeConfigBuilder {
1195            config,
1196            validation_context,
1197            errors,
1198            _state: PhantomData,
1199        }
1200    }
1201}
1202
1203impl JamNodeConfigBuilder<Initial> {
1204    pub fn new(
1205        chain_context: ChainDefaultContext,
1206        validation_context: Rc<RefCell<ValidationContext>>,
1207    ) -> Self {
1208        let base_config = BaseNodeConfig {
1209            command: chain_context.default_command.clone(),
1210            image: chain_context.default_image.clone(),
1211            resources: chain_context.default_resources.clone(),
1212            args: chain_context.default_args.clone(),
1213            chain_context,
1214            ..Default::default()
1215        };
1216
1217        Self::transition(
1218            JamNodeConfig {
1219                base_config,
1220                ..JamNodeConfig::default()
1221            },
1222            validation_context,
1223            vec![],
1224        )
1225    }
1226
1227    /// Set the name of the node.
1228    pub fn with_name<T: Into<String> + Copy>(self, name: T) -> JamNodeConfigBuilder<Buildable> {
1229        let name: String = generate_unique_node_name(name, self.validation_context.clone());
1230
1231        match ensure_value_is_not_empty(&name) {
1232            Ok(_) => Self::transition(
1233                JamNodeConfig {
1234                    base_config: BaseNodeConfig {
1235                        name,
1236                        ..self.config.base_config
1237                    },
1238                    ..self.config
1239                },
1240                self.validation_context,
1241                self.errors,
1242            ),
1243            Err(e) => Self::transition(
1244                JamNodeConfig {
1245                    // we still set the name in error case to display error path
1246                    base_config: BaseNodeConfig {
1247                        name,
1248                        ..self.config.base_config
1249                    },
1250                    ..self.config
1251                },
1252                self.validation_context,
1253                merge_errors(self.errors, FieldError::Name(e).into()),
1254            ),
1255        }
1256    }
1257}
1258
1259impl JamNodeConfigBuilder<Buildable> {
1260    /// Set the command that will be executed to launch the node. Override the default.
1261    pub fn with_command<T>(self, command: T) -> Self
1262    where
1263        T: TryInto<Command>,
1264        T::Error: Error + Send + Sync + 'static,
1265    {
1266        match command.try_into() {
1267            Ok(command) => Self::transition(
1268                JamNodeConfig {
1269                    base_config: BaseNodeConfig {
1270                        command: Some(command),
1271                        ..self.config.base_config
1272                    },
1273                    ..self.config
1274                },
1275                self.validation_context,
1276                self.errors,
1277            ),
1278            Err(error) => Self::transition(
1279                self.config,
1280                self.validation_context,
1281                merge_errors(self.errors, FieldError::Command(error.into()).into()),
1282            ),
1283        }
1284    }
1285
1286    /// Set the subcommand that will be executed to launch the node.
1287    pub fn with_subcommand<T>(self, subcommand: T) -> Self
1288    where
1289        T: TryInto<Command>,
1290        T::Error: Error + Send + Sync + 'static,
1291    {
1292        match subcommand.try_into() {
1293            Ok(subcommand) => Self::transition(
1294                JamNodeConfig {
1295                    base_config: BaseNodeConfig {
1296                        subcommand: Some(subcommand),
1297                        ..self.config.base_config
1298                    },
1299                    ..self.config
1300                },
1301                self.validation_context,
1302                self.errors,
1303            ),
1304            Err(error) => Self::transition(
1305                self.config,
1306                self.validation_context,
1307                merge_errors(self.errors, FieldError::Command(error.into()).into()),
1308            ),
1309        }
1310    }
1311
1312    /// Set the image that will be used for the node (only podman/k8s). Override the default.
1313    pub fn with_image<T>(self, image: T) -> Self
1314    where
1315        T: TryInto<Image>,
1316        T::Error: Error + Send + Sync + 'static,
1317    {
1318        match image.try_into() {
1319            Ok(image) => Self::transition(
1320                JamNodeConfig {
1321                    base_config: BaseNodeConfig {
1322                        image: Some(image),
1323                        ..self.config.base_config
1324                    },
1325                    ..self.config
1326                },
1327                self.validation_context,
1328                self.errors,
1329            ),
1330            Err(error) => Self::transition(
1331                self.config,
1332                self.validation_context,
1333                merge_errors(self.errors, FieldError::Image(error.into()).into()),
1334            ),
1335        }
1336    }
1337
1338    /// Set the arguments that will be used when launching the node. Override the default_args of the chain context.
1339    pub fn with_args(self, args: Vec<Arg>) -> Self {
1340        Self::transition(
1341            JamNodeConfig {
1342                base_config: BaseNodeConfig {
1343                    args,
1344                    ..self.config.base_config
1345                },
1346                ..self.config
1347            },
1348            self.validation_context,
1349            self.errors,
1350        )
1351    }
1352
1353    /// Set the resources limits what will be used for the node (only podman/k8s). Override the default.
1354    pub fn with_resources(self, f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder) -> Self {
1355        match f(ResourcesBuilder::new()).build() {
1356            Ok(resources) => Self::transition(
1357                JamNodeConfig {
1358                    base_config: BaseNodeConfig {
1359                        resources: Some(resources),
1360                        ..self.config.base_config
1361                    },
1362                    ..self.config
1363                },
1364                self.validation_context,
1365                self.errors,
1366            ),
1367            Err(errors) => Self::transition(
1368                self.config,
1369                self.validation_context,
1370                merge_errors_vecs(
1371                    self.errors,
1372                    errors
1373                        .into_iter()
1374                        .map(|error| FieldError::Resources(error).into())
1375                        .collect::<Vec<_>>(),
1376                ),
1377            ),
1378        }
1379    }
1380
1381    /// Set the node environment variables that will be used when launched. Override the default.
1382    pub fn with_env(self, env: Vec<impl Into<EnvVar>>) -> Self {
1383        let env = env.into_iter().map(|var| var.into()).collect::<Vec<_>>();
1384
1385        Self::transition(
1386            JamNodeConfig {
1387                base_config: BaseNodeConfig {
1388                    env,
1389                    ..self.config.base_config
1390                },
1391                ..self.config
1392            },
1393            self.validation_context,
1394            self.errors,
1395        )
1396    }
1397
1398    /// Set the RPC port that will be exposed. Uniqueness across config will be checked.
1399    pub fn with_rpc_port(self, rpc_port: Port) -> Self {
1400        match ensure_port_unique(rpc_port, self.validation_context.clone()) {
1401            Ok(_) => Self::transition(
1402                JamNodeConfig {
1403                    rpc_port: Some(rpc_port),
1404                    ..self.config
1405                },
1406                self.validation_context,
1407                self.errors,
1408            ),
1409            Err(error) => Self::transition(
1410                self.config,
1411                self.validation_context,
1412                merge_errors(self.errors, FieldError::RpcPort(error).into()),
1413            ),
1414        }
1415    }
1416
1417    /// Set the Mode of the node.
1418    pub fn with_mode(self, mode: JamNodeMode) -> Self {
1419        Self::transition(
1420            JamNodeConfig {
1421                mode,
1422                ..self.config
1423            },
1424            self.validation_context,
1425            self.errors,
1426        )
1427    }
1428
1429    /// Set the Mode of the node.
1430    pub fn with_telemetry_endpoint(self, tel_endpoint: impl Into<String>) -> Self {
1431        Self::transition(
1432            JamNodeConfig {
1433                telemetry_endpoint: Some(tel_endpoint.into()),
1434                ..self.config
1435            },
1436            self.validation_context,
1437            self.errors,
1438        )
1439    }
1440
1441    /// Seals the builder and returns a [`JamNodeConfig`] if there are no validation errors, else returns errors.
1442    pub fn build(self) -> Result<JamNodeConfig, (String, Vec<anyhow::Error>)> {
1443        if !self.errors.is_empty() {
1444            return Err((self.config.base_config.name.clone(), self.errors));
1445        }
1446
1447        Ok(self.config)
1448    }
1449}
1450#[cfg(test)]
1451mod tests {
1452    use std::{collections::HashSet, println};
1453
1454    use super::*;
1455
1456    #[test]
1457    fn jam_default_node_serialize() {
1458        let jam_node = JamNodeConfig::default();
1459        let s = serde_json::to_string_pretty(&jam_node);
1460        println!("{:?}", s);
1461    }
1462
1463    #[test]
1464    fn jam_default_node_deserialize() {
1465        let toml_text = r#"
1466        name = "alice"
1467        mode = "validator"
1468        "#;
1469        let jam_node: JamNodeConfig = toml::from_str(toml_text).unwrap();
1470
1471        println!("{:?}", jam_node);
1472    }
1473
1474    #[test]
1475    fn node_config_builder_should_succeeds_and_returns_a_node_config() {
1476        let node_config =
1477            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1478                .with_name("node")
1479                .with_command("mycommand")
1480                .with_image("myrepo:myimage")
1481                .with_args(vec![("--arg1", "value1").into(), "--option2".into()])
1482                .validator(true)
1483                .invulnerable(true)
1484                .bootnode(true)
1485                .with_override_eth_key("0x0123456789abcdef0123456789abcdef01234567")
1486                .with_initial_balance(100_000_042)
1487                .with_env(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")])
1488                .with_raw_bootnodes_addresses(vec![
1489                    "/ip4/10.41.122.55/tcp/45421",
1490                    "/ip4/51.144.222.10/tcp/2333",
1491                ])
1492                .with_resources(|resources| {
1493                    resources
1494                        .with_request_cpu("200M")
1495                        .with_request_memory("500M")
1496                        .with_limit_cpu("1G")
1497                        .with_limit_memory("2G")
1498                })
1499                .with_ws_port(5000)
1500                .with_rpc_port(6000)
1501                .with_prometheus_port(7000)
1502                .with_p2p_port(8000)
1503                .with_p2p_cert_hash(
1504                    "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f",
1505                )
1506                .with_db_snapshot("/tmp/mysnapshot")
1507                .with_keystore_path("/tmp/mykeystore")
1508                .build()
1509                .unwrap();
1510
1511        assert_eq!(node_config.name(), "node");
1512        assert_eq!(node_config.command().unwrap().as_str(), "mycommand");
1513        assert_eq!(node_config.image().unwrap().as_str(), "myrepo:myimage");
1514        let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
1515        assert_eq!(node_config.args(), args.iter().collect::<Vec<_>>());
1516        assert!(node_config.is_validator());
1517        assert!(node_config.is_invulnerable());
1518        assert!(node_config.is_bootnode());
1519        assert_eq!(
1520            node_config.override_eth_key(),
1521            Some("0x0123456789abcdef0123456789abcdef01234567")
1522        );
1523        assert_eq!(node_config.initial_balance(), 100_000_042);
1524        let env: Vec<EnvVar> = vec![("VAR1", "VALUE1").into(), ("VAR2", "VALUE2").into()];
1525        assert_eq!(node_config.env(), env.iter().collect::<Vec<_>>());
1526        let bootnodes_addresses: Vec<Multiaddr> = vec![
1527            "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
1528            "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
1529        ];
1530        assert_eq!(
1531            node_config.bootnodes_addresses(),
1532            bootnodes_addresses.iter().collect::<Vec<_>>()
1533        );
1534        let resources = node_config.resources().unwrap();
1535        assert_eq!(resources.request_cpu().unwrap().as_str(), "200M");
1536        assert_eq!(resources.request_memory().unwrap().as_str(), "500M");
1537        assert_eq!(resources.limit_cpu().unwrap().as_str(), "1G");
1538        assert_eq!(resources.limit_memory().unwrap().as_str(), "2G");
1539        assert_eq!(node_config.ws_port().unwrap(), 5000);
1540        assert_eq!(node_config.rpc_port().unwrap(), 6000);
1541        assert_eq!(node_config.prometheus_port().unwrap(), 7000);
1542        assert_eq!(node_config.p2p_port().unwrap(), 8000);
1543        assert_eq!(
1544            node_config.p2p_cert_hash().unwrap(),
1545            "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f"
1546        );
1547        assert!(matches!(
1548            node_config.db_snapshot().unwrap(), AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/mysnapshot"
1549        ));
1550        assert!(matches!(
1551            node_config.keystore_path().unwrap().to_str().unwrap(),
1552            "/tmp/mykeystore"
1553        ));
1554    }
1555
1556    #[test]
1557    fn with_optional_db_snapshot_applies_when_some() {
1558        let node_config =
1559            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1560                .with_name("node")
1561                .with_command("mycommand")
1562                .with_optional_db_snapshot(Some("/tmp/mysnapshot"))
1563                .build()
1564                .unwrap();
1565        assert!(matches!(
1566            node_config.db_snapshot().unwrap(),
1567            AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/mysnapshot"
1568        ));
1569    }
1570
1571    #[test]
1572    fn with_optional_db_snapshot_is_noop_when_none() {
1573        let node_config =
1574            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1575                .with_name("node")
1576                .with_command("mycommand")
1577                .with_optional_db_snapshot(None::<&str>)
1578                .build()
1579                .unwrap();
1580        assert!(node_config.db_snapshot().is_none());
1581    }
1582
1583    #[test]
1584    fn node_config_builder_should_use_unique_name_if_node_name_already_used() {
1585        let mut used_nodes_names = HashSet::new();
1586        used_nodes_names.insert("mynode".into());
1587        let validation_context = Rc::new(RefCell::new(ValidationContext {
1588            used_nodes_names,
1589            ..Default::default()
1590        }));
1591        let node_config =
1592            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1593                .with_name("mynode")
1594                .build()
1595                .unwrap();
1596
1597        assert_eq!(node_config.name, "mynode-1");
1598    }
1599
1600    #[test]
1601    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_command_is_invalid() {
1602        let (node_name, errors) =
1603            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1604                .with_name("node")
1605                .with_command("invalid command")
1606                .build()
1607                .unwrap_err();
1608
1609        assert_eq!(node_name, "node");
1610        assert_eq!(errors.len(), 1);
1611        assert_eq!(
1612            errors.first().unwrap().to_string(),
1613            "command: 'invalid command' shouldn't contains whitespace"
1614        );
1615    }
1616
1617    #[test]
1618    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_image_is_invalid() {
1619        let (node_name, errors) =
1620            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1621                .with_name("node")
1622                .with_image("myinvalid.image")
1623                .build()
1624                .unwrap_err();
1625
1626        assert_eq!(node_name, "node");
1627        assert_eq!(errors.len(), 1);
1628        assert_eq!(
1629            errors.first().unwrap().to_string(),
1630            "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1631        );
1632    }
1633
1634    #[test]
1635    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_one_bootnode_address_is_invalid(
1636    ) {
1637        let (node_name, errors) =
1638            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1639                .with_name("node")
1640                .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
1641                .build()
1642                .unwrap_err();
1643
1644        assert_eq!(node_name, "node");
1645        assert_eq!(errors.len(), 1);
1646        assert_eq!(
1647            errors.first().unwrap().to_string(),
1648            "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1649        );
1650    }
1651
1652    #[test]
1653    fn node_config_builder_should_fails_and_returns_mulitle_errors_and_node_name_if_multiple_bootnode_address_are_invalid(
1654    ) {
1655        let (node_name, errors) =
1656            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1657                .with_name("node")
1658                .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
1659                .build()
1660                .unwrap_err();
1661
1662        assert_eq!(node_name, "node");
1663        assert_eq!(errors.len(), 2);
1664        assert_eq!(
1665            errors.first().unwrap().to_string(),
1666            "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1667        );
1668        assert_eq!(
1669            errors.get(1).unwrap().to_string(),
1670            "bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
1671        );
1672    }
1673
1674    #[test]
1675    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_resources_has_an_error(
1676    ) {
1677        let (node_name, errors) =
1678            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1679                .with_name("node")
1680                .with_resources(|resources| resources.with_limit_cpu("invalid"))
1681                .build()
1682                .unwrap_err();
1683
1684        assert_eq!(node_name, "node");
1685        assert_eq!(errors.len(), 1);
1686        assert_eq!(
1687            errors.first().unwrap().to_string(),
1688            r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1689        );
1690    }
1691
1692    #[test]
1693    fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_resources_has_multiple_errors(
1694    ) {
1695        let (node_name, errors) =
1696            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1697                .with_name("node")
1698                .with_resources(|resources| {
1699                    resources
1700                        .with_limit_cpu("invalid")
1701                        .with_request_memory("invalid")
1702                })
1703                .build()
1704                .unwrap_err();
1705
1706        assert_eq!(node_name, "node");
1707        assert_eq!(errors.len(), 2);
1708        assert_eq!(
1709            errors.first().unwrap().to_string(),
1710            r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1711        );
1712        assert_eq!(
1713            errors.get(1).unwrap().to_string(),
1714            r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1715        );
1716    }
1717
1718    #[test]
1719    fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_multiple_fields_have_errors(
1720    ) {
1721        let (node_name, errors) =
1722            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1723                .with_name("node")
1724                .with_command("invalid command")
1725                .with_image("myinvalid.image")
1726                .with_resources(|resources| {
1727                    resources
1728                        .with_limit_cpu("invalid")
1729                        .with_request_memory("invalid")
1730                })
1731                .build()
1732                .unwrap_err();
1733
1734        assert_eq!(node_name, "node");
1735        assert_eq!(errors.len(), 4);
1736        assert_eq!(
1737            errors.first().unwrap().to_string(),
1738            "command: 'invalid command' shouldn't contains whitespace"
1739        );
1740        assert_eq!(
1741            errors.get(1).unwrap().to_string(),
1742            "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1743        );
1744        assert_eq!(
1745            errors.get(2).unwrap().to_string(),
1746            r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1747        );
1748        assert_eq!(
1749            errors.get(3).unwrap().to_string(),
1750            r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1751        );
1752    }
1753
1754    #[test]
1755    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_ws_port_is_already_used(
1756    ) {
1757        let validation_context = Rc::new(RefCell::new(ValidationContext {
1758            used_ports: vec![30333],
1759            ..Default::default()
1760        }));
1761        let (node_name, errors) =
1762            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1763                .with_name("node")
1764                .with_ws_port(30333)
1765                .build()
1766                .unwrap_err();
1767
1768        assert_eq!(node_name, "node");
1769        assert_eq!(errors.len(), 1);
1770        assert_eq!(
1771            errors.first().unwrap().to_string(),
1772            "ws_port: '30333' is already used across config"
1773        );
1774    }
1775
1776    #[test]
1777    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_rpc_port_is_already_used(
1778    ) {
1779        let validation_context = Rc::new(RefCell::new(ValidationContext {
1780            used_ports: vec![4444],
1781            ..Default::default()
1782        }));
1783        let (node_name, errors) =
1784            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1785                .with_name("node")
1786                .with_rpc_port(4444)
1787                .build()
1788                .unwrap_err();
1789
1790        assert_eq!(node_name, "node");
1791        assert_eq!(errors.len(), 1);
1792        assert_eq!(
1793            errors.first().unwrap().to_string(),
1794            "rpc_port: '4444' is already used across config"
1795        );
1796    }
1797
1798    #[test]
1799    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_prometheus_port_is_already_used(
1800    ) {
1801        let validation_context = Rc::new(RefCell::new(ValidationContext {
1802            used_ports: vec![9089],
1803            ..Default::default()
1804        }));
1805        let (node_name, errors) =
1806            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1807                .with_name("node")
1808                .with_prometheus_port(9089)
1809                .build()
1810                .unwrap_err();
1811
1812        assert_eq!(node_name, "node");
1813        assert_eq!(errors.len(), 1);
1814        assert_eq!(
1815            errors.first().unwrap().to_string(),
1816            "prometheus_port: '9089' is already used across config"
1817        );
1818    }
1819
1820    #[test]
1821    fn node_config_builder_should_fails_and_returns_and_error_and_node_name_if_p2p_port_is_already_used(
1822    ) {
1823        let validation_context = Rc::new(RefCell::new(ValidationContext {
1824            used_ports: vec![45093],
1825            ..Default::default()
1826        }));
1827        let (node_name, errors) =
1828            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1829                .with_name("node")
1830                .with_p2p_port(45093)
1831                .build()
1832                .unwrap_err();
1833
1834        assert_eq!(node_name, "node");
1835        assert_eq!(errors.len(), 1);
1836        assert_eq!(
1837            errors.first().unwrap().to_string(),
1838            "p2p_port: '45093' is already used across config"
1839        );
1840    }
1841
1842    #[test]
1843    fn node_config_builder_should_fails_if_node_name_is_empty() {
1844        let validation_context = Rc::new(RefCell::new(ValidationContext {
1845            ..Default::default()
1846        }));
1847
1848        let (_, errors) =
1849            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1850                .with_name("")
1851                .build()
1852                .unwrap_err();
1853
1854        assert_eq!(errors.len(), 1);
1855        assert_eq!(errors.first().unwrap().to_string(), "name: can't be empty");
1856    }
1857
1858    #[test]
1859    fn group_default_base_node() {
1860        let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1861
1862        let group_config =
1863            GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1864                .with_base_node(|node| node.with_name("validator"))
1865                .build()
1866                .unwrap();
1867
1868        // Check group config
1869        assert_eq!(group_config.count, 1);
1870        assert_eq!(group_config.base_config.name(), "validator");
1871    }
1872
1873    #[test]
1874    fn group_custom_base_node() {
1875        let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1876        let node_config =
1877            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1878                .with_name("node")
1879                .with_command("some_command")
1880                .with_image("repo:image")
1881                .validator(true)
1882                .invulnerable(true)
1883                .bootnode(true);
1884
1885        let group_config =
1886            GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1887                .with_count(5)
1888                .with_base_node(|_node| node_config)
1889                .build()
1890                .unwrap();
1891
1892        // Check group config
1893        assert_eq!(group_config.count, 5);
1894
1895        assert_eq!(group_config.base_config.name(), "node");
1896        assert_eq!(
1897            group_config.base_config.command().unwrap().as_str(),
1898            "some_command"
1899        );
1900        assert_eq!(
1901            group_config.base_config.image().unwrap().as_str(),
1902            "repo:image"
1903        );
1904        assert!(group_config.base_config.is_validator());
1905        assert!(group_config.base_config.is_invulnerable());
1906        assert!(group_config.base_config.is_bootnode());
1907    }
1908
1909    #[test]
1910    fn ensure_default_args_are_overrided() {
1911        let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1912        let chain_context = ChainDefaultContext {
1913            default_args: vec!["-lruntime=trace".into()],
1914            ..Default::default()
1915        };
1916        let node_config = NodeConfigBuilder::new(chain_context, validation_context)
1917            .with_name("node")
1918            .with_args(vec!["-lruntime=info".into()])
1919            .build()
1920            .unwrap();
1921
1922        assert_eq!(node_config.args, vec!["-lruntime=info".into()]);
1923    }
1924}