use std::fmt::Display;
use std::marker::PhantomData;
use std::path::PathBuf;
use eyre::Result;
use figment::{
providers::{Env, Serialized},
value::{Dict, Map, Value},
Figment, Metadata, Profile, Provider,
};
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use tracing::trace;
use op_primitives::{ChallengerAgent, L1Client, L2Client, MonorepoConfig, RollupClient};
use crate::providers::{
error::ExtractConfigError, optional::OptionalStrictProfileProvider,
rename::RenameProfileProvider, toml::TomlFileProvider, wraps::WrapProfileProvider,
};
use crate::root::RootPath;
pub const L1_URL: &str = "http://localhost:8545";
pub const L1_PORT: u16 = 8545;
pub const L2_URL: &str = "http://localhost:9545";
pub const L2_PORT: u16 = 9545;
pub const ROLLUP_URL: &str = "http://localhost:7545";
pub const ROLLUP_PORT: u16 = 7545;
pub const DEPLOYER_PRIVATE_KEY: &str =
"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Config<'a> {
#[serde(skip)]
_phantom: std::marker::PhantomData<&'a ()>,
#[serde(skip)]
pub profile: Profile,
pub artifacts: PathBuf,
pub monorepo: MonorepoConfig,
pub l1_client: L1Client,
pub l2_client: L2Client,
pub rollup_client: RollupClient,
pub l1_client_url: Option<String>,
pub l1_client_port: Option<u16>,
pub l2_client_url: Option<String>,
pub l2_client_port: Option<u16>,
pub rollup_client_url: Option<String>,
pub rollup_client_port: Option<u16>,
pub deployer: Option<String>,
pub challenger: ChallengerAgent,
pub enable_sequencing: bool,
pub enable_fault_proofs: bool,
pub eth_rpc_jwt: Option<String>,
#[doc(hidden)]
#[serde(rename = "root", default, skip_serializing)]
pub __root: RootPath,
}
impl Display for Config<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[macro_export]
macro_rules! make_selection {
($name:ident, $prompt:expr, $options:expr) => {
let $name = inquire::Select::new($prompt, $options)
.without_help_message()
.prompt()?
.to_string();
};
}
impl Config<'_> {
pub const DEFAULT_PROFILE: Profile = Profile::const_new("default");
pub const PROFILE_SECTION: &'static str = "profile";
pub const FILE_NAME: &'static str = "stack.toml";
pub const STACK_DIR_NAME: &'static str = ".stack";
pub const STANDALONE_SECTIONS: &'static [&'static str] = &[];
#[track_caller]
pub fn load() -> Self {
Config::from_provider(Config::figment())
}
#[track_caller]
pub fn load_with_root(root: impl Into<PathBuf>) -> Self {
Config::from_provider(Config::figment_with_root(root))
}
#[track_caller]
pub fn from_provider<T: Provider>(provider: T) -> Self {
trace!("load config with provider: {:?}", provider.metadata());
Self::try_from(provider).unwrap_or_else(|err| panic!("{}", err))
}
pub fn from_toml(path: impl Into<PathBuf>) -> Result<Self, ExtractConfigError> {
let figment = Config::figment().merge(TomlFileProvider::new(None, path));
Self::try_from(figment)
}
pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
let figment = Figment::from(provider);
let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
config.profile = figment.profile().clone();
Ok(config)
}
pub fn figment() -> Figment {
Config::default().into()
}
pub fn figment_with_root(root: impl Into<PathBuf>) -> Figment {
Self::with_root(root).into()
}
pub fn with_root(root: impl Into<PathBuf>) -> Self {
Config {
__root: RootPath(root.into()),
..Config::default()
}
}
pub fn create_artifacts_dir(&self) -> Result<()> {
if !self.artifacts.exists() {
std::fs::create_dir_all(&self.artifacts)?;
}
Ok(())
}
pub fn selected_profile() -> Profile {
Profile::from_env_or("STACK_PROFILE", Config::DEFAULT_PROFILE)
}
pub fn stack_dir_toml() -> Option<PathBuf> {
Self::stack_dir().map(|p| p.join(Config::FILE_NAME))
}
pub fn stack_dir() -> Option<PathBuf> {
dirs_next::home_dir().map(|p| p.join(Config::STACK_DIR_NAME))
}
pub fn force_overwrites(mut self, force: bool) -> Self {
self.monorepo.force = force;
self
}
pub fn set_l1_client(&mut self) -> Result<()> {
make_selection!(
l1_client,
"Which L1 execution client would you like to use?",
L1Client::iter().collect::<Vec<_>>()
);
self.l1_client = l1_client.parse()?;
tracing::debug!(target: "stack", "Nice l1 client choice! You've got great taste ✨");
Ok(())
}
pub fn set_l2_client(&mut self) -> Result<()> {
make_selection!(
l2_client,
"Which L2 execution client would you like to use?",
L2Client::iter().collect::<Vec<_>>()
);
self.l2_client = l2_client.parse()?;
tracing::debug!(target: "stack", "Nice l2 client choice! You've got great taste ✨");
Ok(())
}
pub fn set_rollup_client(&mut self) -> Result<()> {
make_selection!(
rollup_client,
"Which rollup client would you like to use?",
RollupClient::iter().collect::<Vec<_>>()
);
self.rollup_client = rollup_client.parse()?;
tracing::debug!(target: "stack", "Nice rollup choice! You've got great taste ✨");
Ok(())
}
pub fn set_challenger(&mut self) -> Result<()> {
make_selection!(
challenger,
"Which challenger agent would you like to use?",
ChallengerAgent::iter().collect::<Vec<_>>()
);
self.challenger = challenger.parse()?;
tracing::debug!(target: "stack", "Nice challenger choice! You've got great taste ✨");
Ok(())
}
fn merge_toml_provider(
mut figment: Figment,
toml_provider: impl Provider,
profile: Profile,
) -> Figment {
figment = figment.select(profile.clone());
let mut profiles = vec![Config::DEFAULT_PROFILE];
if profile != Config::DEFAULT_PROFILE {
profiles.push(profile.clone());
}
let provider = toml_provider;
if profile != Config::DEFAULT_PROFILE {
figment = figment.merge(provider.rename(Config::DEFAULT_PROFILE, profile.clone()));
}
figment = figment.merge(provider);
figment
}
}
impl Provider for Config<'_> {
fn metadata(&self) -> Metadata {
Metadata::named("OP Stack Config")
}
#[track_caller]
fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
let mut data = Serialized::defaults(self).data()?;
if let Some(entry) = data.get_mut(&self.profile) {
entry.insert("root".to_string(), Value::serialize(self.__root.clone())?);
}
Ok(data)
}
fn profile(&self) -> Option<Profile> {
Some(self.profile.clone())
}
}
impl From<Config<'_>> for Figment {
fn from(c: Config<'_>) -> Figment {
let profile = Config::selected_profile();
let mut figment = Figment::default();
if let Some(global_toml) = Config::stack_dir_toml().filter(|p| p.exists()) {
figment = Config::merge_toml_provider(
figment,
TomlFileProvider::new(None, global_toml).cached(),
profile.clone(),
);
}
figment = Config::merge_toml_provider(
figment,
TomlFileProvider::new(Some("OP_STACK_CONFIG"), c.__root.0.join(Config::FILE_NAME))
.cached(),
profile.clone(),
);
figment = figment
.merge(
Env::prefixed("OP_STACK_")
.ignore(&[
"PROFILE",
"L1_CLIENT",
"L2_CLIENT",
"ROLLUP_CLIENT",
"CHALLENGER",
])
.map(|key| {
let key = key.as_str();
if Config::STANDALONE_SECTIONS.iter().any(|section| {
key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
}) {
key.replacen('_', ".", 1).into()
} else {
key.into()
}
})
.global(),
)
.select(profile.clone());
Figment::from(c).merge(figment).select(profile)
}
}
impl Default for Config<'_> {
fn default() -> Self {
Self {
_phantom: PhantomData,
profile: Self::DEFAULT_PROFILE,
artifacts: PathBuf::from(Self::STACK_DIR_NAME),
monorepo: MonorepoConfig::default(),
l1_client: L1Client::default(),
l2_client: L2Client::default(),
l1_client_url: Some(L1_URL.to_string()),
l1_client_port: Some(L1_PORT),
l2_client_url: Some(L2_URL.to_string()),
l2_client_port: Some(L2_PORT),
deployer: Some(DEPLOYER_PRIVATE_KEY.to_string()),
rollup_client_url: Some(ROLLUP_URL.to_string()),
rollup_client_port: Some(ROLLUP_PORT),
rollup_client: RollupClient::default(),
challenger: ChallengerAgent::default(),
enable_sequencing: false,
enable_fault_proofs: false,
eth_rpc_jwt: None,
__root: RootPath::default(),
}
}
}
trait ProviderExt: Provider {
fn rename(
&self,
from: impl Into<Profile>,
to: impl Into<Profile>,
) -> RenameProfileProvider<&Self> {
RenameProfileProvider::new(self, from, to)
}
fn wrap(
&self,
wrapping_key: impl Into<Profile>,
profile: impl Into<Profile>,
) -> WrapProfileProvider<&Self> {
WrapProfileProvider::new(self, wrapping_key, profile)
}
fn strict_select(
&self,
profiles: impl IntoIterator<Item = impl Into<Profile>>,
) -> OptionalStrictProfileProvider<&Self> {
OptionalStrictProfileProvider::new(self, profiles)
}
}
impl<P: Provider> ProviderExt for P {}