op_config/
stack.rs

1use std::fmt::Display;
2use std::marker::PhantomData;
3use std::path::PathBuf;
4
5use eyre::Result;
6use figment::{
7    providers::{Env, Serialized},
8    value::{Dict, Map, Value},
9    Figment, Metadata, Profile, Provider,
10};
11use serde::{Deserialize, Serialize};
12use strum::IntoEnumIterator;
13use tracing::trace;
14
15use op_primitives::{ChallengerAgent, L1Client, L2Client, MonorepoConfig, RollupClient};
16
17use crate::providers::{
18    error::ExtractConfigError, optional::OptionalStrictProfileProvider,
19    rename::RenameProfileProvider, toml::TomlFileProvider, wraps::WrapProfileProvider,
20};
21use crate::root::RootPath;
22
23/// L1 node url.
24pub const L1_URL: &str = "http://localhost:8545";
25
26/// L1 node port.
27pub const L1_PORT: u16 = 8545;
28
29/// L2 node url.
30pub const L2_URL: &str = "http://localhost:9545";
31
32/// L2 node port.
33pub const L2_PORT: u16 = 9545;
34
35/// Rollup node url.
36pub const ROLLUP_URL: &str = "http://localhost:7545";
37
38/// Rollup node port.
39pub const ROLLUP_PORT: u16 = 7545;
40
41/// Testing deployer private key.
42pub const DEPLOYER_PRIVATE_KEY: &str =
43    "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
44
45/// OP Stack Configuration
46///
47/// # Defaults
48///
49/// All configuration values have a default, documented in the [fields](#fields)
50/// section below. [`Config::default()`] returns the default values for
51/// the default profile while [`Config::with_root()`] returns the values based on the given
52/// directory. [`Config::load()`] starts with the default profile and merges various providers into
53/// the config, same for [`Config::load_with_root()`], but there the default values are determined
54/// by [`Config::with_root()`]
55///
56/// # Provider Details
57///
58/// `Config` is a Figment [`Provider`] with the following characteristics:
59///
60///   * **Profile**
61///
62///     The profile is set to the value of the `profile` field.
63///
64///   * **Metadata**
65///
66///     This provider is named `OP Stack Config`. It does not specify a
67///     [`Source`](figment::Source) and uses default interpolation.
68///
69///   * **Data**
70///
71///     The data emitted by this provider are the keys and values corresponding
72///     to the fields and values of the structure. The dictionary is emitted to
73///     the "default" meta-profile.
74///
75/// Note that these behaviors differ from those of [`Config::figment()`].
76#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
77#[serde(rename_all = "kebab-case")]
78pub struct Config<'a> {
79    /// Phantom data to ensure that the lifetime `'a` is used.
80    #[serde(skip)]
81    _phantom: std::marker::PhantomData<&'a ()>,
82
83    /// The selected profile. **(default: _default_ `default`)**
84    ///
85    /// **Note:** This field is never serialized nor deserialized. When a
86    /// `Config` is merged into a `Figment` as a `Provider`, this profile is
87    /// selected on the `Figment`. When a `Config` is extracted, this field is
88    /// set to the extracting Figment's selected `Profile`.
89    #[serde(skip)]
90    pub profile: Profile,
91
92    /// The path to the op stack artifact directory. **(default: _default_ `.stack`)**
93    pub artifacts: PathBuf,
94
95    /// The Optimism Monorepo configuration options.
96    pub monorepo: MonorepoConfig,
97
98    /// The type of L1 Client to use. **(default: _default_ `L1Client::Geth`)**
99    pub l1_client: L1Client,
100    /// The type of L2 Client to use. **(default: _default_ `L2Client::OpGeth`)**
101    pub l2_client: L2Client,
102    /// The type of Rollup Client to use. **(default: _default_ `RollupClient::OpNode`)**
103    pub rollup_client: RollupClient,
104
105    // todo: parse the urls properly as opposed to using strings
106    /// The L1 Client base URL.
107    pub l1_client_url: Option<String>,
108    /// The L1 Client port.
109    pub l1_client_port: Option<u16>,
110    /// The L2 Client URL.
111    pub l2_client_url: Option<String>,
112    /// The L2 Client port.
113    pub l2_client_port: Option<u16>,
114    /// The rollup client URL.
115    pub rollup_client_url: Option<String>,
116    /// The rollup client port.
117    pub rollup_client_port: Option<u16>,
118
119    /// Deployer is the contract deployer.
120    /// By default, this is a hardhat test account.
121    pub deployer: Option<String>,
122
123    /// The challenger agent to use. **(default: _default_ `ChallengerAgent::OpChallengerGo`)**
124    pub challenger: ChallengerAgent,
125
126    /// Enable Sequencing. **(default: _default_ `false`)**
127    pub enable_sequencing: bool,
128    /// Enable Fault Proofs. **(default: _default_ `false`)**
129    pub enable_fault_proofs: bool,
130
131    /// Stack Stage Components
132    ///
133    /// This is a table array of [StageConfig]s, each of which
134    /// represents a stage in the stack and is orchestrated by the
135    /// [StageManager].
136    ///
137    /// The parsing of [StageConfig]s is done by the [StageConfig::from_toml]
138    /// function. This allows for different configuration formats to be used
139    /// for each stage.
140    // pub stages: Vec<StageProvider<'a>>,
141
142    /// JWT secret that should be used for any rpc calls
143    pub eth_rpc_jwt: Option<String>,
144
145    /// The root path where the config detection started from, `Config::with_root`
146    #[doc(hidden)]
147    // Skip serialization here, so it won't be included in the [`Config::to_string()`]
148    // representation, but will be deserialized from `Figment` so that commands can
149    // override it.
150    #[serde(rename = "root", default, skip_serializing)]
151    pub __root: RootPath,
152}
153
154// impl<'a> Deserialize<'a> for Config<'a> {
155//     fn deserialize<D: serde::Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
156//         let mut config: Config<'a> = serde::Deserialize::deserialize(deserializer)?;
157//         config.__root = RootPath::default();
158//         Ok(config)
159//     }
160// }
161
162impl Display for Config<'_> {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        write!(f, "{:?}", self)
165    }
166}
167
168/// Macro to create a selection prompt.
169#[macro_export]
170macro_rules! make_selection {
171    ($name:ident, $prompt:expr, $options:expr) => {
172        let $name = inquire::Select::new($prompt, $options)
173            .without_help_message()
174            .prompt()?
175            .to_string();
176    };
177}
178
179impl Config<'_> {
180    /// The default profile: "default"
181    pub const DEFAULT_PROFILE: Profile = Profile::const_new("default");
182
183    /// TOML section for profiles
184    pub const PROFILE_SECTION: &'static str = "profile";
185
186    /// File name of config toml file
187    pub const FILE_NAME: &'static str = "stack.toml";
188
189    /// The name of the directory rollup reserves for itself under the user's home directory: `~`
190    pub const STACK_DIR_NAME: &'static str = ".stack";
191
192    /// Standalone sections in the config which get integrated into the selected profile
193    pub const STANDALONE_SECTIONS: &'static [&'static str] = &[];
194
195    /// Returns the current `Config`
196    ///
197    /// See `Config::figment`
198    #[track_caller]
199    pub fn load() -> Self {
200        Config::from_provider(Config::figment())
201    }
202
203    /// Returns the current `Config`
204    ///
205    /// See `Config::figment_with_root`
206    #[track_caller]
207    pub fn load_with_root(root: impl Into<PathBuf>) -> Self {
208        Config::from_provider(Config::figment_with_root(root))
209    }
210
211    /// Extract a `Config` from `provider`, panicking if extraction fails.
212    ///
213    /// # Panics
214    ///
215    /// If extraction fails, prints an error message indicating the failure and
216    /// panics. For a version that doesn't panic, use [`Config::try_from()`].
217    ///
218    /// # Example
219    ///
220    /// ```no_run
221    /// use op_config::Config;
222    /// use figment::providers::{Toml, Format, Env};
223    ///
224    /// // Use default `Figment`, but allow values from `other.toml`
225    /// // to supersede its values.
226    /// let figment = Config::figment()
227    ///     .merge(Toml::file("other.toml").nested());
228    ///
229    /// let config = Config::from_provider(figment);
230    /// ```
231    #[track_caller]
232    pub fn from_provider<T: Provider>(provider: T) -> Self {
233        trace!("load config with provider: {:?}", provider.metadata());
234        Self::try_from(provider).unwrap_or_else(|err| panic!("{}", err))
235    }
236
237    /// Attempts to build a `Config` using a [PathBuf] toml file.
238    /// If the file does not exist, it will be created with default values.
239    pub fn from_toml(path: impl Into<PathBuf>) -> Result<Self, ExtractConfigError> {
240        let figment = Config::figment().merge(TomlFileProvider::new(None, path));
241        Self::try_from(figment)
242    }
243
244    /// Attempts to extract a `Config` from `provider`, returning the result.
245    ///
246    /// # Example
247    ///
248    /// ```rust
249    /// use op_config::Config;
250    /// use figment::providers::{Toml, Format, Env};
251    ///
252    /// // Use default `Figment`, but allow values from `other.toml`
253    /// // to supersede its values.
254    /// let figment = Config::figment()
255    ///     .merge(Toml::file("other.toml").nested());
256    ///
257    /// let config = Config::try_from(figment);
258    /// ```
259    pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
260        let figment = Figment::from(provider);
261        let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
262        config.profile = figment.profile().clone();
263        Ok(config)
264    }
265
266    /// Returns the default figment
267    ///
268    /// The default figment reads from the following sources, in ascending
269    /// priority order:
270    ///
271    ///   1. [`Config::default()`] (see [defaults](#defaults))
272    ///   2. `stack.toml` _or_ filename in `OP_STACK_CONFIG` environment variable
273    ///   3. `OP_STACK_` prefixed environment variables
274    ///
275    /// The profile selected is the value set in the `OP_STACK_PROFILE`
276    /// environment variable. If it is not set, it defaults to `default`.
277    ///
278    /// # Example
279    ///
280    /// ```rust
281    /// use op_config::Config;
282    /// use serde::Deserialize;
283    ///
284    /// let my_config = Config::figment().extract::<Config>();
285    /// ```
286    pub fn figment() -> Figment {
287        Config::default().into()
288    }
289
290    /// Returns the default figment enhanced with additional context extracted from the provided
291    /// root, like remappings and directories.
292    ///
293    /// # Example
294    ///
295    /// ```rust
296    /// use op_config::Config;
297    /// use serde::Deserialize;
298    ///
299    /// let my_config = Config::figment_with_root(".").extract::<Config>();
300    /// ```
301    pub fn figment_with_root(root: impl Into<PathBuf>) -> Figment {
302        Self::with_root(root).into()
303    }
304
305    /// Creates a new Config that adds additional context extracted from the provided root.
306    ///
307    /// # Example
308    ///
309    /// ```rust
310    /// use op_config::Config;
311    /// let my_config = Config::with_root(".");
312    /// ```
313    pub fn with_root(root: impl Into<PathBuf>) -> Self {
314        Config {
315            __root: RootPath(root.into()),
316            ..Config::default()
317        }
318    }
319
320    /// Creates the artifacts directory if it doesn't exist.
321    pub fn create_artifacts_dir(&self) -> Result<()> {
322        if !self.artifacts.exists() {
323            std::fs::create_dir_all(&self.artifacts)?;
324        }
325        Ok(())
326    }
327
328    /// Returns the selected profile
329    ///
330    /// If the `STACK_PROFILE` env variable is not set, this returns the `DEFAULT_PROFILE`
331    pub fn selected_profile() -> Profile {
332        Profile::from_env_or("STACK_PROFILE", Config::DEFAULT_PROFILE)
333    }
334
335    /// Returns the path to the global toml file that's stored at `~/.stack/stack.toml`
336    pub fn stack_dir_toml() -> Option<PathBuf> {
337        Self::stack_dir().map(|p| p.join(Config::FILE_NAME))
338    }
339
340    /// Returns the path to the config dir `~/.stack/`
341    pub fn stack_dir() -> Option<PathBuf> {
342        dirs_next::home_dir().map(|p| p.join(Config::STACK_DIR_NAME))
343    }
344
345    /// Force monorepo artifact overwrites.
346    pub fn force_overwrites(mut self, force: bool) -> Self {
347        self.monorepo.force = force;
348        self
349    }
350
351    /// Sets the l1 client to use via a cli prompt.
352    pub fn set_l1_client(&mut self) -> Result<()> {
353        make_selection!(
354            l1_client,
355            "Which L1 execution client would you like to use?",
356            L1Client::iter().collect::<Vec<_>>()
357        );
358        self.l1_client = l1_client.parse()?;
359        tracing::debug!(target: "stack", "Nice l1 client choice! You've got great taste ✨");
360        Ok(())
361    }
362
363    /// Sets the l2 client to use via a cli prompt.
364    pub fn set_l2_client(&mut self) -> Result<()> {
365        make_selection!(
366            l2_client,
367            "Which L2 execution client would you like to use?",
368            L2Client::iter().collect::<Vec<_>>()
369        );
370        self.l2_client = l2_client.parse()?;
371        tracing::debug!(target: "stack", "Nice l2 client choice! You've got great taste ✨");
372        Ok(())
373    }
374
375    /// Sets the rollup client to use via a cli prompt.
376    pub fn set_rollup_client(&mut self) -> Result<()> {
377        make_selection!(
378            rollup_client,
379            "Which rollup client would you like to use?",
380            RollupClient::iter().collect::<Vec<_>>()
381        );
382        self.rollup_client = rollup_client.parse()?;
383        tracing::debug!(target: "stack", "Nice rollup choice! You've got great taste ✨");
384        Ok(())
385    }
386
387    /// Sets the challenger agent to use via a cli prompt.
388    pub fn set_challenger(&mut self) -> Result<()> {
389        make_selection!(
390            challenger,
391            "Which challenger agent would you like to use?",
392            ChallengerAgent::iter().collect::<Vec<_>>()
393        );
394        self.challenger = challenger.parse()?;
395        tracing::debug!(target: "stack", "Nice challenger choice! You've got great taste ✨");
396        Ok(())
397    }
398
399    fn merge_toml_provider(
400        mut figment: Figment,
401        toml_provider: impl Provider,
402        profile: Profile,
403    ) -> Figment {
404        figment = figment.select(profile.clone());
405
406        // use [profile.<profile>] as [<profile>]
407        let mut profiles = vec![Config::DEFAULT_PROFILE];
408        if profile != Config::DEFAULT_PROFILE {
409            profiles.push(profile.clone());
410        }
411        let provider = toml_provider; // toml_provider.strict_select(profiles);
412
413        // merge the default profile as a base
414        if profile != Config::DEFAULT_PROFILE {
415            figment = figment.merge(provider.rename(Config::DEFAULT_PROFILE, profile.clone()));
416        }
417
418        // merge the profile
419        figment = figment.merge(provider);
420        figment
421    }
422}
423
424impl Provider for Config<'_> {
425    fn metadata(&self) -> Metadata {
426        Metadata::named("OP Stack Config")
427    }
428
429    #[track_caller]
430    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
431        let mut data = Serialized::defaults(self).data()?;
432        if let Some(entry) = data.get_mut(&self.profile) {
433            entry.insert("root".to_string(), Value::serialize(self.__root.clone())?);
434        }
435        Ok(data)
436    }
437
438    fn profile(&self) -> Option<Profile> {
439        Some(self.profile.clone())
440    }
441}
442
443impl From<Config<'_>> for Figment {
444    fn from(c: Config<'_>) -> Figment {
445        let profile = Config::selected_profile();
446        let mut figment = Figment::default();
447
448        // merge global toml file
449        if let Some(global_toml) = Config::stack_dir_toml().filter(|p| p.exists()) {
450            figment = Config::merge_toml_provider(
451                figment,
452                TomlFileProvider::new(None, global_toml).cached(),
453                profile.clone(),
454            );
455        }
456        // merge local toml file
457        figment = Config::merge_toml_provider(
458            figment,
459            TomlFileProvider::new(Some("OP_STACK_CONFIG"), c.__root.0.join(Config::FILE_NAME))
460                .cached(),
461            profile.clone(),
462        );
463
464        // merge environment variables
465        figment = figment
466            .merge(
467                Env::prefixed("OP_STACK_")
468                    .ignore(&[
469                        "PROFILE",
470                        "L1_CLIENT",
471                        "L2_CLIENT",
472                        "ROLLUP_CLIENT",
473                        "CHALLENGER",
474                    ])
475                    .map(|key| {
476                        let key = key.as_str();
477                        if Config::STANDALONE_SECTIONS.iter().any(|section| {
478                            key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
479                        }) {
480                            key.replacen('_', ".", 1).into()
481                        } else {
482                            key.into()
483                        }
484                    })
485                    .global(),
486            )
487            .select(profile.clone());
488
489        Figment::from(c).merge(figment).select(profile)
490    }
491}
492
493impl Default for Config<'_> {
494    fn default() -> Self {
495        Self {
496            _phantom: PhantomData,
497            profile: Self::DEFAULT_PROFILE,
498            artifacts: PathBuf::from(Self::STACK_DIR_NAME),
499            monorepo: MonorepoConfig::default(),
500            l1_client: L1Client::default(),
501            l2_client: L2Client::default(),
502            l1_client_url: Some(L1_URL.to_string()),
503            l1_client_port: Some(L1_PORT),
504            l2_client_url: Some(L2_URL.to_string()),
505            l2_client_port: Some(L2_PORT),
506            deployer: Some(DEPLOYER_PRIVATE_KEY.to_string()),
507            rollup_client_url: Some(ROLLUP_URL.to_string()),
508            rollup_client_port: Some(ROLLUP_PORT),
509            rollup_client: RollupClient::default(),
510            challenger: ChallengerAgent::default(),
511            enable_sequencing: false,
512            enable_fault_proofs: false,
513            // stages: vec![],
514            eth_rpc_jwt: None,
515            __root: RootPath::default(),
516        }
517    }
518}
519
520trait ProviderExt: Provider {
521    fn rename(
522        &self,
523        from: impl Into<Profile>,
524        to: impl Into<Profile>,
525    ) -> RenameProfileProvider<&Self> {
526        RenameProfileProvider::new(self, from, to)
527    }
528
529    fn wrap(
530        &self,
531        wrapping_key: impl Into<Profile>,
532        profile: impl Into<Profile>,
533    ) -> WrapProfileProvider<&Self> {
534        WrapProfileProvider::new(self, wrapping_key, profile)
535    }
536
537    fn strict_select(
538        &self,
539        profiles: impl IntoIterator<Item = impl Into<Profile>>,
540    ) -> OptionalStrictProfileProvider<&Self> {
541        OptionalStrictProfileProvider::new(self, profiles)
542    }
543}
544impl<P: Provider> ProviderExt for P {}