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) = match NodeConfigBuilder::new(
1070            chain_context.clone(),
1071            validation_context.clone(),
1072        )
1073        .with_name(" ") // placeholder
1074        .build()
1075        {
1076            Ok(base_config) => (vec![], base_config),
1077            Err((_name, errors)) => (errors, NodeConfig::default()),
1078        };
1079
1080        Self {
1081            base_config,
1082            count: 1,
1083            validation_context,
1084            errors,
1085            _state: PhantomData,
1086        }
1087    }
1088
1089    /// Set the base node config using a closure.
1090    pub fn with_base_node(
1091        mut self,
1092        f: impl FnOnce(NodeConfigBuilder<Initial>) -> NodeConfigBuilder<Buildable>,
1093    ) -> GroupNodeConfigBuilder<Buildable> {
1094        match f(NodeConfigBuilder::new(
1095            ChainDefaultContext::default(),
1096            self.validation_context.clone(),
1097        ))
1098        .build()
1099        {
1100            Ok(node) => {
1101                self.base_config = node;
1102                GroupNodeConfigBuilder {
1103                    base_config: self.base_config,
1104                    count: self.count,
1105                    validation_context: self.validation_context,
1106                    errors: self.errors,
1107                    _state: PhantomData,
1108                }
1109            },
1110            Err((_name, errors)) => {
1111                self.errors.extend(errors);
1112                GroupNodeConfigBuilder {
1113                    base_config: self.base_config,
1114                    count: self.count,
1115                    validation_context: self.validation_context,
1116                    errors: self.errors,
1117                    _state: PhantomData,
1118                }
1119            },
1120        }
1121    }
1122
1123    /// Set the number of nodes in the group.
1124    pub fn with_count(mut self, count: usize) -> Self {
1125        self.count = count;
1126        self
1127    }
1128}
1129
1130impl GroupNodeConfigBuilder<Buildable> {
1131    /// Set the number of nodes in the group.
1132    pub fn with_count(mut self, count: usize) -> Self {
1133        self.count = count;
1134        self
1135    }
1136
1137    pub fn build(self) -> Result<GroupNodeConfig, (String, Vec<anyhow::Error>)> {
1138        if self.count == 0 {
1139            return Err((
1140                self.base_config.name().to_string(),
1141                vec![anyhow::anyhow!("Count cannot be zero")],
1142            ));
1143        }
1144
1145        if !self.errors.is_empty() {
1146            return Err((self.base_config.name().to_string(), self.errors));
1147        }
1148
1149        Ok(GroupNodeConfig {
1150            base_config: self.base_config,
1151            count: self.count,
1152        })
1153    }
1154}
1155
1156// JAM
1157/// A node configuration builder, used to build a [`JamNodeConfig`] declaratively with fields validation.
1158pub struct JamNodeConfigBuilder<S> {
1159    config: JamNodeConfig,
1160    validation_context: Rc<RefCell<ValidationContext>>,
1161    errors: Vec<anyhow::Error>,
1162    _state: PhantomData<S>,
1163}
1164
1165impl Default for JamNodeConfigBuilder<Initial> {
1166    fn default() -> Self {
1167        Self {
1168            config: JamNodeConfig {
1169                rpc_port: None,
1170                base_config: BaseNodeConfig {
1171                    name: "".into(), // placeholder
1172                    image: None,
1173                    command: None,
1174                    subcommand: None,
1175                    args: vec![],
1176                    env: vec![],
1177                    resources: None,
1178                    chain_context: Default::default(),
1179                },
1180                mode: JamNodeMode::Validator, //
1181                telemetry_endpoint: None,
1182            },
1183            validation_context: Default::default(),
1184            errors: vec![],
1185            _state: PhantomData,
1186        }
1187    }
1188}
1189
1190impl<A> JamNodeConfigBuilder<A> {
1191    fn transition<B>(
1192        config: JamNodeConfig,
1193        validation_context: Rc<RefCell<ValidationContext>>,
1194        errors: Vec<anyhow::Error>,
1195    ) -> JamNodeConfigBuilder<B> {
1196        JamNodeConfigBuilder {
1197            config,
1198            validation_context,
1199            errors,
1200            _state: PhantomData,
1201        }
1202    }
1203}
1204
1205impl JamNodeConfigBuilder<Initial> {
1206    pub fn new(
1207        chain_context: ChainDefaultContext,
1208        validation_context: Rc<RefCell<ValidationContext>>,
1209    ) -> Self {
1210        let base_config = BaseNodeConfig {
1211            command: chain_context.default_command.clone(),
1212            image: chain_context.default_image.clone(),
1213            resources: chain_context.default_resources.clone(),
1214            args: chain_context.default_args.clone(),
1215            chain_context,
1216            ..Default::default()
1217        };
1218
1219        Self::transition(
1220            JamNodeConfig {
1221                base_config,
1222                ..JamNodeConfig::default()
1223            },
1224            validation_context,
1225            vec![],
1226        )
1227    }
1228
1229    /// Set the name of the node.
1230    pub fn with_name<T: Into<String> + Copy>(self, name: T) -> JamNodeConfigBuilder<Buildable> {
1231        let name: String = generate_unique_node_name(name, self.validation_context.clone());
1232
1233        match ensure_value_is_not_empty(&name) {
1234            Ok(_) => Self::transition(
1235                JamNodeConfig {
1236                    base_config: BaseNodeConfig {
1237                        name,
1238                        ..self.config.base_config
1239                    },
1240                    ..self.config
1241                },
1242                self.validation_context,
1243                self.errors,
1244            ),
1245            Err(e) => Self::transition(
1246                JamNodeConfig {
1247                    // we still set the name in error case to display error path
1248                    base_config: BaseNodeConfig {
1249                        name,
1250                        ..self.config.base_config
1251                    },
1252                    ..self.config
1253                },
1254                self.validation_context,
1255                merge_errors(self.errors, FieldError::Name(e).into()),
1256            ),
1257        }
1258    }
1259}
1260
1261impl JamNodeConfigBuilder<Buildable> {
1262    /// Set the command that will be executed to launch the node. Override the default.
1263    pub fn with_command<T>(self, command: T) -> Self
1264    where
1265        T: TryInto<Command>,
1266        T::Error: Error + Send + Sync + 'static,
1267    {
1268        match command.try_into() {
1269            Ok(command) => Self::transition(
1270                JamNodeConfig {
1271                    base_config: BaseNodeConfig {
1272                        command: Some(command),
1273                        ..self.config.base_config
1274                    },
1275                    ..self.config
1276                },
1277                self.validation_context,
1278                self.errors,
1279            ),
1280            Err(error) => Self::transition(
1281                self.config,
1282                self.validation_context,
1283                merge_errors(self.errors, FieldError::Command(error.into()).into()),
1284            ),
1285        }
1286    }
1287
1288    /// Set the subcommand that will be executed to launch the node.
1289    pub fn with_subcommand<T>(self, subcommand: T) -> Self
1290    where
1291        T: TryInto<Command>,
1292        T::Error: Error + Send + Sync + 'static,
1293    {
1294        match subcommand.try_into() {
1295            Ok(subcommand) => Self::transition(
1296                JamNodeConfig {
1297                    base_config: BaseNodeConfig {
1298                        subcommand: Some(subcommand),
1299                        ..self.config.base_config
1300                    },
1301                    ..self.config
1302                },
1303                self.validation_context,
1304                self.errors,
1305            ),
1306            Err(error) => Self::transition(
1307                self.config,
1308                self.validation_context,
1309                merge_errors(self.errors, FieldError::Command(error.into()).into()),
1310            ),
1311        }
1312    }
1313
1314    /// Set the image that will be used for the node (only podman/k8s). Override the default.
1315    pub fn with_image<T>(self, image: T) -> Self
1316    where
1317        T: TryInto<Image>,
1318        T::Error: Error + Send + Sync + 'static,
1319    {
1320        match image.try_into() {
1321            Ok(image) => Self::transition(
1322                JamNodeConfig {
1323                    base_config: BaseNodeConfig {
1324                        image: Some(image),
1325                        ..self.config.base_config
1326                    },
1327                    ..self.config
1328                },
1329                self.validation_context,
1330                self.errors,
1331            ),
1332            Err(error) => Self::transition(
1333                self.config,
1334                self.validation_context,
1335                merge_errors(self.errors, FieldError::Image(error.into()).into()),
1336            ),
1337        }
1338    }
1339
1340    /// Set the arguments that will be used when launching the node. Override the default_args of the chain context.
1341    pub fn with_args(self, args: Vec<Arg>) -> Self {
1342        Self::transition(
1343            JamNodeConfig {
1344                base_config: BaseNodeConfig {
1345                    args,
1346                    ..self.config.base_config
1347                },
1348                ..self.config
1349            },
1350            self.validation_context,
1351            self.errors,
1352        )
1353    }
1354
1355    /// Set the resources limits what will be used for the node (only podman/k8s). Override the default.
1356    pub fn with_resources(self, f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder) -> Self {
1357        match f(ResourcesBuilder::new()).build() {
1358            Ok(resources) => Self::transition(
1359                JamNodeConfig {
1360                    base_config: BaseNodeConfig {
1361                        resources: Some(resources),
1362                        ..self.config.base_config
1363                    },
1364                    ..self.config
1365                },
1366                self.validation_context,
1367                self.errors,
1368            ),
1369            Err(errors) => Self::transition(
1370                self.config,
1371                self.validation_context,
1372                merge_errors_vecs(
1373                    self.errors,
1374                    errors
1375                        .into_iter()
1376                        .map(|error| FieldError::Resources(error).into())
1377                        .collect::<Vec<_>>(),
1378                ),
1379            ),
1380        }
1381    }
1382
1383    /// Set the node environment variables that will be used when launched. Override the default.
1384    pub fn with_env(self, env: Vec<impl Into<EnvVar>>) -> Self {
1385        let env = env.into_iter().map(|var| var.into()).collect::<Vec<_>>();
1386
1387        Self::transition(
1388            JamNodeConfig {
1389                base_config: BaseNodeConfig {
1390                    env,
1391                    ..self.config.base_config
1392                },
1393                ..self.config
1394            },
1395            self.validation_context,
1396            self.errors,
1397        )
1398    }
1399
1400    /// Set the RPC port that will be exposed. Uniqueness across config will be checked.
1401    pub fn with_rpc_port(self, rpc_port: Port) -> Self {
1402        match ensure_port_unique(rpc_port, self.validation_context.clone()) {
1403            Ok(_) => Self::transition(
1404                JamNodeConfig {
1405                    rpc_port: Some(rpc_port),
1406                    ..self.config
1407                },
1408                self.validation_context,
1409                self.errors,
1410            ),
1411            Err(error) => Self::transition(
1412                self.config,
1413                self.validation_context,
1414                merge_errors(self.errors, FieldError::RpcPort(error).into()),
1415            ),
1416        }
1417    }
1418
1419    /// Set the Mode of the node.
1420    pub fn with_mode(self, mode: JamNodeMode) -> Self {
1421        Self::transition(
1422            JamNodeConfig {
1423                mode,
1424                ..self.config
1425            },
1426            self.validation_context,
1427            self.errors,
1428        )
1429    }
1430
1431    /// Set the Mode of the node.
1432    pub fn with_telemetry_endpoint(self, tel_endpoint: impl Into<String>) -> Self {
1433        Self::transition(
1434            JamNodeConfig {
1435                telemetry_endpoint: Some(tel_endpoint.into()),
1436                ..self.config
1437            },
1438            self.validation_context,
1439            self.errors,
1440        )
1441    }
1442
1443    /// Seals the builder and returns a [`JamNodeConfig`] if there are no validation errors, else returns errors.
1444    pub fn build(self) -> Result<JamNodeConfig, (String, Vec<anyhow::Error>)> {
1445        if !self.errors.is_empty() {
1446            return Err((self.config.base_config.name.clone(), self.errors));
1447        }
1448
1449        Ok(self.config)
1450    }
1451}
1452#[cfg(test)]
1453mod tests {
1454    use std::{collections::HashSet, println};
1455
1456    use super::*;
1457
1458    #[test]
1459    fn jam_default_node_serialize() {
1460        let jam_node = JamNodeConfig::default();
1461        let s = serde_json::to_string_pretty(&jam_node);
1462        println!("{:?}", s);
1463    }
1464
1465    #[test]
1466    fn jam_default_node_deserialize() {
1467        let toml_text = r#"
1468        name = "alice"
1469        mode = "validator"
1470        "#;
1471        let jam_node: JamNodeConfig = toml::from_str(toml_text).unwrap();
1472
1473        println!("{:?}", jam_node);
1474    }
1475
1476    #[test]
1477    fn node_config_builder_should_succeeds_and_returns_a_node_config() {
1478        let node_config =
1479            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1480                .with_name("node")
1481                .with_command("mycommand")
1482                .with_image("myrepo:myimage")
1483                .with_args(vec![("--arg1", "value1").into(), "--option2".into()])
1484                .validator(true)
1485                .invulnerable(true)
1486                .bootnode(true)
1487                .with_override_eth_key("0x0123456789abcdef0123456789abcdef01234567")
1488                .with_initial_balance(100_000_042)
1489                .with_env(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")])
1490                .with_raw_bootnodes_addresses(vec![
1491                    "/ip4/10.41.122.55/tcp/45421",
1492                    "/ip4/51.144.222.10/tcp/2333",
1493                ])
1494                .with_resources(|resources| {
1495                    resources
1496                        .with_request_cpu("200M")
1497                        .with_request_memory("500M")
1498                        .with_limit_cpu("1G")
1499                        .with_limit_memory("2G")
1500                })
1501                .with_ws_port(5000)
1502                .with_rpc_port(6000)
1503                .with_prometheus_port(7000)
1504                .with_p2p_port(8000)
1505                .with_p2p_cert_hash(
1506                    "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f",
1507                )
1508                .with_db_snapshot("/tmp/mysnapshot")
1509                .with_keystore_path("/tmp/mykeystore")
1510                .build()
1511                .unwrap();
1512
1513        assert_eq!(node_config.name(), "node");
1514        assert_eq!(node_config.command().unwrap().as_str(), "mycommand");
1515        assert_eq!(node_config.image().unwrap().as_str(), "myrepo:myimage");
1516        let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
1517        assert_eq!(node_config.args(), args.iter().collect::<Vec<_>>());
1518        assert!(node_config.is_validator());
1519        assert!(node_config.is_invulnerable());
1520        assert!(node_config.is_bootnode());
1521        assert_eq!(
1522            node_config.override_eth_key(),
1523            Some("0x0123456789abcdef0123456789abcdef01234567")
1524        );
1525        assert_eq!(node_config.initial_balance(), 100_000_042);
1526        let env: Vec<EnvVar> = vec![("VAR1", "VALUE1").into(), ("VAR2", "VALUE2").into()];
1527        assert_eq!(node_config.env(), env.iter().collect::<Vec<_>>());
1528        let bootnodes_addresses: Vec<Multiaddr> = vec![
1529            "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
1530            "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
1531        ];
1532        assert_eq!(
1533            node_config.bootnodes_addresses(),
1534            bootnodes_addresses.iter().collect::<Vec<_>>()
1535        );
1536        let resources = node_config.resources().unwrap();
1537        assert_eq!(resources.request_cpu().unwrap().as_str(), "200M");
1538        assert_eq!(resources.request_memory().unwrap().as_str(), "500M");
1539        assert_eq!(resources.limit_cpu().unwrap().as_str(), "1G");
1540        assert_eq!(resources.limit_memory().unwrap().as_str(), "2G");
1541        assert_eq!(node_config.ws_port().unwrap(), 5000);
1542        assert_eq!(node_config.rpc_port().unwrap(), 6000);
1543        assert_eq!(node_config.prometheus_port().unwrap(), 7000);
1544        assert_eq!(node_config.p2p_port().unwrap(), 8000);
1545        assert_eq!(
1546            node_config.p2p_cert_hash().unwrap(),
1547            "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f"
1548        );
1549        assert!(matches!(
1550            node_config.db_snapshot().unwrap(), AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/mysnapshot"
1551        ));
1552        assert!(matches!(
1553            node_config.keystore_path().unwrap().to_str().unwrap(),
1554            "/tmp/mykeystore"
1555        ));
1556    }
1557
1558    #[test]
1559    fn with_optional_db_snapshot_applies_when_some() {
1560        let node_config =
1561            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1562                .with_name("node")
1563                .with_command("mycommand")
1564                .with_optional_db_snapshot(Some("/tmp/mysnapshot"))
1565                .build()
1566                .unwrap();
1567        assert!(matches!(
1568            node_config.db_snapshot().unwrap(),
1569            AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/mysnapshot"
1570        ));
1571    }
1572
1573    #[test]
1574    fn with_optional_db_snapshot_is_noop_when_none() {
1575        let node_config =
1576            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1577                .with_name("node")
1578                .with_command("mycommand")
1579                .with_optional_db_snapshot(None::<&str>)
1580                .build()
1581                .unwrap();
1582        assert!(node_config.db_snapshot().is_none());
1583    }
1584
1585    #[test]
1586    fn node_config_builder_should_use_unique_name_if_node_name_already_used() {
1587        let mut used_nodes_names = HashSet::new();
1588        used_nodes_names.insert("mynode".into());
1589        let validation_context = Rc::new(RefCell::new(ValidationContext {
1590            used_nodes_names,
1591            ..Default::default()
1592        }));
1593        let node_config =
1594            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1595                .with_name("mynode")
1596                .build()
1597                .unwrap();
1598
1599        assert_eq!(node_config.name, "mynode-1");
1600    }
1601
1602    #[test]
1603    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_command_is_invalid() {
1604        let (node_name, errors) =
1605            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1606                .with_name("node")
1607                .with_command("invalid command")
1608                .build()
1609                .unwrap_err();
1610
1611        assert_eq!(node_name, "node");
1612        assert_eq!(errors.len(), 1);
1613        assert_eq!(
1614            errors.first().unwrap().to_string(),
1615            "command: 'invalid command' shouldn't contains whitespace"
1616        );
1617    }
1618
1619    #[test]
1620    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_image_is_invalid() {
1621        let (node_name, errors) =
1622            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1623                .with_name("node")
1624                .with_image("myinvalid.image")
1625                .build()
1626                .unwrap_err();
1627
1628        assert_eq!(node_name, "node");
1629        assert_eq!(errors.len(), 1);
1630        assert_eq!(
1631            errors.first().unwrap().to_string(),
1632            "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1633        );
1634    }
1635
1636    #[test]
1637    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_one_bootnode_address_is_invalid(
1638    ) {
1639        let (node_name, errors) =
1640            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1641                .with_name("node")
1642                .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
1643                .build()
1644                .unwrap_err();
1645
1646        assert_eq!(node_name, "node");
1647        assert_eq!(errors.len(), 1);
1648        assert_eq!(
1649            errors.first().unwrap().to_string(),
1650            "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1651        );
1652    }
1653
1654    #[test]
1655    fn node_config_builder_should_fails_and_returns_mulitle_errors_and_node_name_if_multiple_bootnode_address_are_invalid(
1656    ) {
1657        let (node_name, errors) =
1658            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1659                .with_name("node")
1660                .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
1661                .build()
1662                .unwrap_err();
1663
1664        assert_eq!(node_name, "node");
1665        assert_eq!(errors.len(), 2);
1666        assert_eq!(
1667            errors.first().unwrap().to_string(),
1668            "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1669        );
1670        assert_eq!(
1671            errors.get(1).unwrap().to_string(),
1672            "bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
1673        );
1674    }
1675
1676    #[test]
1677    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_resources_has_an_error(
1678    ) {
1679        let (node_name, errors) =
1680            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1681                .with_name("node")
1682                .with_resources(|resources| resources.with_limit_cpu("invalid"))
1683                .build()
1684                .unwrap_err();
1685
1686        assert_eq!(node_name, "node");
1687        assert_eq!(errors.len(), 1);
1688        assert_eq!(
1689            errors.first().unwrap().to_string(),
1690            r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1691        );
1692    }
1693
1694    #[test]
1695    fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_resources_has_multiple_errors(
1696    ) {
1697        let (node_name, errors) =
1698            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1699                .with_name("node")
1700                .with_resources(|resources| {
1701                    resources
1702                        .with_limit_cpu("invalid")
1703                        .with_request_memory("invalid")
1704                })
1705                .build()
1706                .unwrap_err();
1707
1708        assert_eq!(node_name, "node");
1709        assert_eq!(errors.len(), 2);
1710        assert_eq!(
1711            errors.first().unwrap().to_string(),
1712            r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1713        );
1714        assert_eq!(
1715            errors.get(1).unwrap().to_string(),
1716            r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1717        );
1718    }
1719
1720    #[test]
1721    fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_multiple_fields_have_errors(
1722    ) {
1723        let (node_name, errors) =
1724            NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1725                .with_name("node")
1726                .with_command("invalid command")
1727                .with_image("myinvalid.image")
1728                .with_resources(|resources| {
1729                    resources
1730                        .with_limit_cpu("invalid")
1731                        .with_request_memory("invalid")
1732                })
1733                .build()
1734                .unwrap_err();
1735
1736        assert_eq!(node_name, "node");
1737        assert_eq!(errors.len(), 4);
1738        assert_eq!(
1739            errors.first().unwrap().to_string(),
1740            "command: 'invalid command' shouldn't contains whitespace"
1741        );
1742        assert_eq!(
1743            errors.get(1).unwrap().to_string(),
1744            "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1745        );
1746        assert_eq!(
1747            errors.get(2).unwrap().to_string(),
1748            r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1749        );
1750        assert_eq!(
1751            errors.get(3).unwrap().to_string(),
1752            r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1753        );
1754    }
1755
1756    #[test]
1757    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_ws_port_is_already_used(
1758    ) {
1759        let validation_context = Rc::new(RefCell::new(ValidationContext {
1760            used_ports: vec![30333],
1761            ..Default::default()
1762        }));
1763        let (node_name, errors) =
1764            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1765                .with_name("node")
1766                .with_ws_port(30333)
1767                .build()
1768                .unwrap_err();
1769
1770        assert_eq!(node_name, "node");
1771        assert_eq!(errors.len(), 1);
1772        assert_eq!(
1773            errors.first().unwrap().to_string(),
1774            "ws_port: '30333' is already used across config"
1775        );
1776    }
1777
1778    #[test]
1779    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_rpc_port_is_already_used(
1780    ) {
1781        let validation_context = Rc::new(RefCell::new(ValidationContext {
1782            used_ports: vec![4444],
1783            ..Default::default()
1784        }));
1785        let (node_name, errors) =
1786            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1787                .with_name("node")
1788                .with_rpc_port(4444)
1789                .build()
1790                .unwrap_err();
1791
1792        assert_eq!(node_name, "node");
1793        assert_eq!(errors.len(), 1);
1794        assert_eq!(
1795            errors.first().unwrap().to_string(),
1796            "rpc_port: '4444' is already used across config"
1797        );
1798    }
1799
1800    #[test]
1801    fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_prometheus_port_is_already_used(
1802    ) {
1803        let validation_context = Rc::new(RefCell::new(ValidationContext {
1804            used_ports: vec![9089],
1805            ..Default::default()
1806        }));
1807        let (node_name, errors) =
1808            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1809                .with_name("node")
1810                .with_prometheus_port(9089)
1811                .build()
1812                .unwrap_err();
1813
1814        assert_eq!(node_name, "node");
1815        assert_eq!(errors.len(), 1);
1816        assert_eq!(
1817            errors.first().unwrap().to_string(),
1818            "prometheus_port: '9089' is already used across config"
1819        );
1820    }
1821
1822    #[test]
1823    fn node_config_builder_should_fails_and_returns_and_error_and_node_name_if_p2p_port_is_already_used(
1824    ) {
1825        let validation_context = Rc::new(RefCell::new(ValidationContext {
1826            used_ports: vec![45093],
1827            ..Default::default()
1828        }));
1829        let (node_name, errors) =
1830            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1831                .with_name("node")
1832                .with_p2p_port(45093)
1833                .build()
1834                .unwrap_err();
1835
1836        assert_eq!(node_name, "node");
1837        assert_eq!(errors.len(), 1);
1838        assert_eq!(
1839            errors.first().unwrap().to_string(),
1840            "p2p_port: '45093' is already used across config"
1841        );
1842    }
1843
1844    #[test]
1845    fn node_config_builder_should_fails_if_node_name_is_empty() {
1846        let validation_context = Rc::new(RefCell::new(ValidationContext {
1847            ..Default::default()
1848        }));
1849
1850        let (_, errors) =
1851            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1852                .with_name("")
1853                .build()
1854                .unwrap_err();
1855
1856        assert_eq!(errors.len(), 1);
1857        assert_eq!(errors.first().unwrap().to_string(), "name: can't be empty");
1858    }
1859
1860    #[test]
1861    fn group_default_base_node() {
1862        let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1863
1864        let group_config =
1865            GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1866                .with_base_node(|node| node.with_name("validator"))
1867                .build()
1868                .unwrap();
1869
1870        // Check group config
1871        assert_eq!(group_config.count, 1);
1872        assert_eq!(group_config.base_config.name(), "validator");
1873    }
1874
1875    #[test]
1876    fn group_custom_base_node() {
1877        let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1878        let node_config =
1879            NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1880                .with_name("node")
1881                .with_command("some_command")
1882                .with_image("repo:image")
1883                .validator(true)
1884                .invulnerable(true)
1885                .bootnode(true);
1886
1887        let group_config =
1888            GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1889                .with_count(5)
1890                .with_base_node(|_node| node_config)
1891                .build()
1892                .unwrap();
1893
1894        // Check group config
1895        assert_eq!(group_config.count, 5);
1896
1897        assert_eq!(group_config.base_config.name(), "node");
1898        assert_eq!(
1899            group_config.base_config.command().unwrap().as_str(),
1900            "some_command"
1901        );
1902        assert_eq!(
1903            group_config.base_config.image().unwrap().as_str(),
1904            "repo:image"
1905        );
1906        assert!(group_config.base_config.is_validator());
1907        assert!(group_config.base_config.is_invulnerable());
1908        assert!(group_config.base_config.is_bootnode());
1909    }
1910
1911    #[test]
1912    fn ensure_default_args_are_overrided() {
1913        let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1914        let chain_context = ChainDefaultContext {
1915            default_args: vec!["-lruntime=trace".into()],
1916            ..Default::default()
1917        };
1918        let node_config = NodeConfigBuilder::new(chain_context, validation_context)
1919            .with_name("node")
1920            .with_args(vec!["-lruntime=info".into()])
1921            .build()
1922            .unwrap();
1923
1924        assert_eq!(node_config.args, vec!["-lruntime=info".into()]);
1925    }
1926}