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