#![warn(missing_docs, unused_crate_dependencies)]
use crate::cache::StorageCachingConfig;
use ethers_core::types::{Address, Chain::Mainnet, H160, H256, U256};
pub use ethers_solc::{self, artifacts::OptimizerDetails};
use ethers_solc::{
artifacts::{
output_selection::ContractOutputSelection, serde_helpers, BytecodeHash, DebuggingSettings,
Libraries, ModelCheckerSettings, ModelCheckerTarget, Optimizer, RevertStrings, Settings,
SettingsMetadata, Severity,
},
cache::SOLIDITY_FILES_CACHE_FILENAME,
error::SolcError,
remappings::{RelativeRemapping, Remapping},
ConfigurableArtifacts, EvmVersion, Project, ProjectPathsConfig, Solc, SolcConfig,
};
use eyre::{ContextCompat, WrapErr};
use figment::{
providers::{Env, Format, Serialized, Toml},
value::{Dict, Map, Value},
Error, Figment, Metadata, Profile, Provider,
};
use inflector::Inflector;
use once_cell::sync::Lazy;
use regex::Regex;
use semver::Version;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::{
borrow::Cow,
collections::HashMap,
fs,
path::{Path, PathBuf},
str::FromStr,
};
pub(crate) use tracing::trace;
mod macros;
pub mod utils;
pub use crate::utils::*;
mod endpoints;
pub use endpoints::{ResolvedRpcEndpoints, RpcEndpoint, RpcEndpoints};
mod etherscan;
mod resolve;
pub use resolve::UnresolvedEnvVarError;
pub mod cache;
use cache::{Cache, ChainCache};
mod chain;
pub use chain::Chain;
pub mod fmt;
pub use fmt::FormatterConfig;
pub mod fs_permissions;
pub use crate::fs_permissions::FsPermissions;
pub mod error;
pub use error::SolidityErrorCode;
pub mod doc;
pub use doc::DocConfig;
mod warning;
pub use warning::*;
pub mod fix;
pub use figment;
use revm_primitives::SpecId;
use tracing::warn;
pub mod providers;
use crate::{
error::ExtractConfigError,
etherscan::{EtherscanConfigError, EtherscanConfigs, ResolvedEtherscanConfig},
};
use providers::*;
mod fuzz;
pub use fuzz::{FuzzConfig, FuzzDictionaryConfig};
mod invariant;
use crate::fs_permissions::PathPermission;
pub use invariant::InvariantConfig;
use providers::remappings::RemappingsProvider;
mod inline;
pub use inline::{validate_profiles, InlineConfig, InlineConfigError, InlineConfigParser, NatSpec};
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct Config {
#[serde(skip)]
pub profile: Profile,
pub src: PathBuf,
pub test: PathBuf,
pub script: PathBuf,
pub out: PathBuf,
pub libs: Vec<PathBuf>,
pub remappings: Vec<RelativeRemapping>,
pub auto_detect_remappings: bool,
pub libraries: Vec<String>,
pub cache: bool,
pub cache_path: PathBuf,
pub broadcast: PathBuf,
pub allow_paths: Vec<PathBuf>,
pub include_paths: Vec<PathBuf>,
pub force: bool,
#[serde(with = "from_str_lowercase")]
pub evm_version: EvmVersion,
pub gas_reports: Vec<String>,
pub gas_reports_ignore: Vec<String>,
pub solc: Option<SolcReq>,
pub auto_detect_solc: bool,
pub offline: bool,
pub optimizer: bool,
pub optimizer_runs: usize,
pub optimizer_details: Option<OptimizerDetails>,
pub model_checker: Option<ModelCheckerSettings>,
pub verbosity: u8,
pub eth_rpc_url: Option<String>,
pub eth_rpc_jwt: Option<String>,
pub etherscan_api_key: Option<String>,
#[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
pub etherscan: EtherscanConfigs,
pub ignored_error_codes: Vec<SolidityErrorCode>,
pub deny_warnings: bool,
#[serde(rename = "match_test")]
pub test_pattern: Option<RegexWrapper>,
#[serde(rename = "no_match_test")]
pub test_pattern_inverse: Option<RegexWrapper>,
#[serde(rename = "match_contract")]
pub contract_pattern: Option<RegexWrapper>,
#[serde(rename = "no_match_contract")]
pub contract_pattern_inverse: Option<RegexWrapper>,
#[serde(rename = "match_path", with = "from_opt_glob")]
pub path_pattern: Option<globset::Glob>,
#[serde(rename = "no_match_path", with = "from_opt_glob")]
pub path_pattern_inverse: Option<globset::Glob>,
pub fuzz: FuzzConfig,
pub invariant: InvariantConfig,
pub ffi: bool,
pub sender: Address,
pub tx_origin: Address,
pub initial_balance: U256,
pub block_number: u64,
pub fork_block_number: Option<u64>,
pub chain_id: Option<Chain>,
pub gas_limit: GasLimit,
pub code_size_limit: Option<usize>,
pub gas_price: Option<u64>,
pub block_base_fee_per_gas: u64,
pub block_coinbase: Address,
pub block_timestamp: u64,
pub block_difficulty: u64,
pub block_prevrandao: H256,
pub block_gas_limit: Option<GasLimit>,
pub memory_limit: u64,
#[serde(default)]
pub extra_output: Vec<ContractOutputSelection>,
#[serde(default)]
pub extra_output_files: Vec<ContractOutputSelection>,
pub names: bool,
pub sizes: bool,
pub via_ir: bool,
pub rpc_storage_caching: StorageCachingConfig,
pub no_storage_caching: bool,
pub no_rpc_rate_limit: bool,
#[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
pub rpc_endpoints: RpcEndpoints,
pub use_literal_content: bool,
#[serde(with = "from_str_lowercase")]
pub bytecode_hash: BytecodeHash,
pub cbor_metadata: bool,
#[serde(with = "serde_helpers::display_from_str_opt")]
pub revert_strings: Option<RevertStrings>,
pub sparse_mode: bool,
pub build_info: bool,
pub build_info_path: Option<PathBuf>,
pub fmt: FormatterConfig,
pub doc: DocConfig,
pub fs_permissions: FsPermissions,
pub cancun: bool,
#[doc(hidden)]
#[serde(rename = "root", default, skip_serializing)]
pub __root: RootPath,
#[doc(hidden)]
#[serde(skip)]
pub __non_exhaustive: (),
#[serde(default, skip_serializing)]
pub __warnings: Vec<Warning>,
}
pub static STANDALONE_FALLBACK_SECTIONS: Lazy<HashMap<&'static str, &'static str>> =
Lazy::new(|| HashMap::from([("invariant", "fuzz")]));
pub static DEPRECATIONS: Lazy<HashMap<String, String>> = Lazy::new(|| HashMap::from([]));
impl Config {
pub const DEFAULT_PROFILE: Profile = Profile::const_new("default");
pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
pub const PROFILE_SECTION: &'static str = "profile";
pub const STANDALONE_SECTIONS: &'static [&'static str] =
&["rpc_endpoints", "etherscan", "fmt", "doc", "fuzz", "invariant"];
pub const FILE_NAME: &'static str = "foundry.toml";
pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
pub const DEFAULT_SENDER: H160 = H160([
0x18, 0x04, 0xc8, 0xAB, 0x1F, 0x12, 0xE6, 0xbb, 0xF3, 0x89, 0x4D, 0x40, 0x83, 0xF3, 0x3E,
0x07, 0x30, 0x9D, 0x1F, 0x38,
]);
#[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 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)
}
#[must_use]
pub fn canonic(self) -> Self {
let root = self.__root.0.clone();
self.canonic_at(root)
}
#[must_use]
pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
let root = canonic(root);
fn p(root: &Path, rem: &Path) -> PathBuf {
canonic(root.join(rem))
}
self.src = p(&root, &self.src);
self.test = p(&root, &self.test);
self.script = p(&root, &self.script);
self.out = p(&root, &self.out);
self.broadcast = p(&root, &self.broadcast);
self.cache_path = p(&root, &self.cache_path);
if let Some(build_info_path) = self.build_info_path {
self.build_info_path = Some(p(&root, &build_info_path));
}
self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
self.remappings =
self.remappings.into_iter().map(|r| RelativeRemapping::new(r.into(), &root)).collect();
self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
self.fs_permissions.join_all(&root);
if let Some(ref mut model_checker) = self.model_checker {
model_checker.contracts = std::mem::take(&mut model_checker.contracts)
.into_iter()
.map(|(path, contracts)| {
(format!("{}", p(&root, path.as_ref()).display()), contracts)
})
.collect();
}
self
}
#[must_use]
pub fn sanitized(self) -> Self {
let mut config = self.canonic();
config.sanitize_remappings();
config.libs.sort_unstable();
config.libs.dedup();
config
}
pub fn sanitize_remappings(&mut self) {
#[cfg(target_os = "windows")]
{
use path_slash::PathBufExt;
self.remappings.iter_mut().for_each(|r| {
r.path.path = r.path.path.to_slash_lossy().into_owned().into();
});
}
}
pub fn install_lib_dir(&self) -> &Path {
self.libs
.iter()
.find(|p| !p.ends_with("node_modules"))
.map(|p| p.as_path())
.unwrap_or_else(|| Path::new("lib"))
}
pub fn project(&self) -> Result<Project, SolcError> {
self.create_project(true, false)
}
pub fn ephemeral_no_artifacts_project(&self) -> Result<Project, SolcError> {
self.create_project(false, true)
}
fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
let mut project = Project::builder()
.artifacts(self.configured_artifacts_handler())
.paths(self.project_paths())
.allowed_path(&self.__root.0)
.allowed_paths(&self.libs)
.allowed_paths(&self.allow_paths)
.include_paths(&self.include_paths)
.solc_config(SolcConfig::builder().settings(self.solc_settings()?).build())
.ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
.set_compiler_severity_filter(if self.deny_warnings {
Severity::Warning
} else {
Severity::Error
})
.set_auto_detect(self.is_auto_detect())
.set_offline(self.offline)
.set_cached(cached)
.set_build_info(cached & self.build_info)
.set_no_artifacts(no_artifacts)
.build()?;
if self.force {
project.cleanup()?;
}
if let Some(solc) = self.ensure_solc()? {
project.solc = solc;
}
Ok(project)
}
fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
if let Some(ref solc) = self.solc {
let solc = match solc {
SolcReq::Version(version) => {
let v = version.to_string();
let mut solc = Solc::find_svm_installed_version(&v)?;
if solc.is_none() {
if self.offline {
return Err(SolcError::msg(format!(
"can't install missing solc {version} in offline mode"
)))
}
Solc::blocking_install(version)?;
solc = Solc::find_svm_installed_version(&v)?;
}
solc
}
SolcReq::Local(solc) => {
if !solc.is_file() {
return Err(SolcError::msg(format!(
"`solc` {} does not exist",
solc.display()
)))
}
Some(Solc::new(solc))
}
};
return Ok(solc)
}
Ok(None)
}
#[inline]
pub fn evm_spec_id(&self) -> SpecId {
if self.cancun {
return SpecId::CANCUN
}
evm_spec_id(&self.evm_version)
}
pub fn is_auto_detect(&self) -> bool {
if self.solc.is_some() {
return false
}
self.auto_detect_solc
}
pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
!self.no_storage_caching &&
self.rpc_storage_caching.enable_for_chain_id(chain_id.into()) &&
self.rpc_storage_caching.enable_for_endpoint(endpoint)
}
pub fn project_paths(&self) -> ProjectPathsConfig {
let mut builder = ProjectPathsConfig::builder()
.cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
.sources(&self.src)
.tests(&self.test)
.scripts(&self.script)
.artifacts(&self.out)
.libs(self.libs.clone())
.remappings(self.get_all_remappings());
if let Some(build_info_path) = &self.build_info_path {
builder = builder.build_infos(build_info_path);
}
builder.build_with_root(&self.__root.0)
}
pub fn get_all_remappings(&self) -> Vec<Remapping> {
self.remappings.iter().map(|m| m.clone().into()).collect()
}
pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<str>>, UnresolvedEnvVarError> {
Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
}
pub fn get_rpc_url(&self) -> Option<Result<Cow<str>, UnresolvedEnvVarError>> {
let maybe_alias = self.eth_rpc_url.as_ref().or(self.etherscan_api_key.as_ref())?;
if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
Some(alias)
} else {
Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
}
}
pub fn get_rpc_url_with_alias(
&self,
maybe_alias: &str,
) -> Option<Result<Cow<str>, UnresolvedEnvVarError>> {
let mut endpoints = self.rpc_endpoints.clone().resolved();
Some(endpoints.remove(maybe_alias)?.map(Cow::Owned))
}
pub fn get_rpc_url_or<'a>(
&'a self,
fallback: impl Into<Cow<'a, str>>,
) -> Result<Cow<str>, UnresolvedEnvVarError> {
if let Some(url) = self.get_rpc_url() {
url
} else {
Ok(fallback.into())
}
}
pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<str>, UnresolvedEnvVarError> {
self.get_rpc_url_or("http://localhost:8545")
}
pub fn get_etherscan_config(
&self,
) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
let maybe_alias = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref())?;
if self.etherscan.contains_key(maybe_alias) {
let mut resolved = self.etherscan.clone().resolved();
return resolved.remove(maybe_alias)
}
let chain = self.chain_id.unwrap_or(Chain::Named(Mainnet));
let api_key = self.etherscan_api_key.as_ref()?;
ResolvedEtherscanConfig::create(api_key, chain).map(Ok)
}
pub fn get_etherscan_config_with_chain(
&self,
chain: Option<impl Into<Chain>>,
) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
let chain = chain.map(Into::into);
if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref()) {
if self.etherscan.contains_key(maybe_alias) {
return self.etherscan.clone().resolved().remove(maybe_alias).transpose()
}
}
if let Some(res) =
chain.and_then(|chain| self.etherscan.clone().resolved().find_chain(chain))
{
match (res, self.etherscan_api_key.as_ref()) {
(Ok(mut config), Some(key)) => {
config.key = key.clone();
return Ok(Some(config))
}
(Ok(config), None) => return Ok(Some(config)),
(Err(err), None) => return Err(err),
(Err(_), Some(_)) => {
}
}
}
if let Some(key) = self.etherscan_api_key.as_ref() {
let chain = chain.or(self.chain_id).unwrap_or_default();
return Ok(ResolvedEtherscanConfig::create(key, chain))
}
Ok(None)
}
pub fn get_etherscan_api_key(&self, chain: Option<impl Into<Chain>>) -> Option<String> {
self.get_etherscan_config_with_chain(chain).ok().flatten().map(|c| c.key)
}
pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
get_dir_remapping(&self.src)
}
pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
if self.__root.0.join(&self.test).exists() {
get_dir_remapping(&self.test)
} else {
None
}
}
pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
if self.__root.0.join(&self.script).exists() {
get_dir_remapping(&self.script)
} else {
None
}
}
pub fn optimizer(&self) -> Optimizer {
let details = if self.optimizer { self.optimizer_details.clone() } else { None };
Optimizer { enabled: Some(self.optimizer), runs: Some(self.optimizer_runs), details }
}
pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
let mut extra_output = self.extra_output.clone();
if !extra_output.contains(&ContractOutputSelection::Metadata) {
extra_output.push(ContractOutputSelection::Metadata);
}
ConfigurableArtifacts::new(extra_output, self.extra_output_files.clone())
}
pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
Libraries::parse(&self.libraries)
}
pub fn solc_settings(&self) -> Result<Settings, SolcError> {
let libraries = self.parsed_libraries()?.with_applied_remappings(&self.project_paths());
let optimizer = self.optimizer();
let mut model_checker = self.model_checker.clone();
if let Some(ref mut model_checker_settings) = model_checker {
if model_checker_settings.targets.is_none() {
model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
}
}
let mut settings = Settings {
optimizer,
evm_version: Some(self.evm_version),
libraries,
metadata: Some(SettingsMetadata {
use_literal_content: Some(self.use_literal_content),
bytecode_hash: Some(self.bytecode_hash),
cbor_metadata: Some(self.cbor_metadata),
}),
debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
revert_strings: Some(revert_strings),
debug_info: Vec::new(),
}),
model_checker,
..Default::default()
}
.with_extra_output(self.configured_artifacts_handler().output_selection())
.with_ast();
if self.via_ir {
settings = settings.with_via_ir();
}
Ok(settings)
}
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 {
let root = root.into();
let paths = ProjectPathsConfig::builder().build_with_root(&root);
let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
Config {
__root: paths.root.into(),
src: paths.sources.file_name().unwrap().into(),
out: artifacts.clone(),
libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(),
remappings: paths
.remappings
.into_iter()
.map(|r| RelativeRemapping::new(r, &root))
.collect(),
fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
..Config::default()
}
}
pub fn hardhat() -> Self {
Config {
src: "contracts".into(),
out: "artifacts".into(),
libs: vec!["node_modules".into()],
..Config::default()
}
}
pub fn dapptools() -> Self {
Config {
chain_id: Some(Chain::Id(99)),
block_timestamp: 0,
block_number: 0,
..Config::default()
}
}
pub fn into_basic(self) -> BasicConfig {
BasicConfig {
profile: self.profile,
src: self.src,
out: self.out,
libs: self.libs,
remappings: self.remappings,
}
}
pub fn update_at<F>(root: impl Into<PathBuf>, f: F) -> eyre::Result<()>
where
F: FnOnce(&Config, &mut toml_edit::Document) -> bool,
{
let config = Self::load_with_root(root).sanitized();
config.update(|doc| f(&config, doc))
}
pub fn update<F>(&self, f: F) -> eyre::Result<()>
where
F: FnOnce(&mut toml_edit::Document) -> bool,
{
let file_path = self.get_config_path();
if !file_path.exists() {
return Ok(())
}
let contents = fs::read_to_string(&file_path)?;
let mut doc = contents.parse::<toml_edit::Document>()?;
if f(&mut doc) {
fs::write(file_path, doc.to_string())?;
}
Ok(())
}
pub fn update_libs(&self) -> eyre::Result<()> {
self.update(|doc| {
let profile = self.profile.as_str().as_str();
let root = &self.__root.0;
let libs: toml_edit::Value = self
.libs
.iter()
.map(|path| {
let path =
if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
toml_edit::Value::from(&*path.to_string_lossy())
})
.collect();
let libs = toml_edit::value(libs);
doc[Config::PROFILE_SECTION][profile]["libs"] = libs;
true
})
}
pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
let mut value = toml::Value::try_from(self)?;
let value_table = value.as_table_mut().unwrap();
let standalone_sections = Config::STANDALONE_SECTIONS
.iter()
.filter_map(|section| {
let section = section.to_string();
value_table.remove(§ion).map(|value| (section, value))
})
.collect::<Vec<_>>();
let mut wrapping_table = [(
Config::PROFILE_SECTION.into(),
toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
)]
.into_iter()
.collect::<toml::map::Map<_, _>>();
for (section, value) in standalone_sections {
wrapping_table.insert(section, value);
}
toml::to_string_pretty(&toml::Value::Table(wrapping_table))
}
pub fn get_config_path(&self) -> PathBuf {
self.__root.0.join(Config::FILE_NAME)
}
pub fn selected_profile() -> Profile {
Profile::from_env_or("FOUNDRY_PROFILE", Config::DEFAULT_PROFILE)
}
pub fn foundry_dir_toml() -> Option<PathBuf> {
Self::foundry_dir().map(|p| p.join(Config::FILE_NAME))
}
pub fn foundry_dir() -> Option<PathBuf> {
dirs_next::home_dir().map(|p| p.join(Config::FOUNDRY_DIR_NAME))
}
pub fn foundry_cache_dir() -> Option<PathBuf> {
Self::foundry_dir().map(|p| p.join("cache"))
}
pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
Some(Self::foundry_cache_dir()?.join("rpc"))
}
pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
}
pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
Some(Self::foundry_cache_dir()?.join("etherscan"))
}
pub fn foundry_keystores_dir() -> Option<PathBuf> {
Some(Self::foundry_dir()?.join("keystores"))
}
pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
}
pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
}
pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
}
#[doc = r#"Returns the path to `foundry`'s data directory inside the user's data directory
|Platform | Value | Example |
| ------- | ------------------------------------- | -------------------------------- |
| Linux | `$XDG_CONFIG_HOME` or `$HOME`/.config/foundry | /home/alice/.config/foundry|
| macOS | `$HOME`/Library/Application Support/foundry | /Users/Alice/Library/Application Support/foundry |
| Windows | `{FOLDERID_RoamingAppData}/foundry` | C:\Users\Alice\AppData\Roaming/foundry |
"#]
pub fn data_dir() -> eyre::Result<PathBuf> {
let path = dirs_next::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
Ok(path)
}
pub fn find_config_file() -> Option<PathBuf> {
fn find(path: &Path) -> Option<PathBuf> {
if path.is_absolute() {
return match path.is_file() {
true => Some(path.to_path_buf()),
false => None,
}
}
let cwd = std::env::current_dir().ok()?;
let mut cwd = cwd.as_path();
loop {
let file_path = cwd.join(path);
if file_path.is_file() {
return Some(file_path)
}
cwd = cwd.parent()?;
}
}
find(Env::var_or("FOUNDRY_CONFIG", Config::FILE_NAME).as_ref())
.or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
}
pub fn clean_foundry_cache() -> eyre::Result<()> {
if let Some(cache_dir) = Config::foundry_cache_dir() {
let path = cache_dir.as_path();
let _ = fs::remove_dir_all(path);
} else {
eyre::bail!("failed to get foundry_cache_dir");
}
Ok(())
}
pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<()> {
if let Some(cache_dir) = Config::foundry_chain_cache_dir(chain) {
let path = cache_dir.as_path();
let _ = fs::remove_dir_all(path);
} else {
eyre::bail!("failed to get foundry_chain_cache_dir");
}
Ok(())
}
pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<()> {
if let Some(cache_dir) = Config::foundry_block_cache_dir(chain, block) {
let path = cache_dir.as_path();
let _ = fs::remove_dir_all(path);
} else {
eyre::bail!("failed to get foundry_block_cache_dir");
}
Ok(())
}
pub fn clean_foundry_etherscan_cache() -> eyre::Result<()> {
if let Some(cache_dir) = Config::foundry_etherscan_cache_dir() {
let path = cache_dir.as_path();
let _ = fs::remove_dir_all(path);
} else {
eyre::bail!("failed to get foundry_etherscan_cache_dir");
}
Ok(())
}
pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<()> {
if let Some(cache_dir) = Config::foundry_etherscan_chain_cache_dir(chain) {
let path = cache_dir.as_path();
let _ = fs::remove_dir_all(path);
} else {
eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
}
Ok(())
}
pub fn list_foundry_cache() -> eyre::Result<Cache> {
if let Some(cache_dir) = Config::foundry_rpc_cache_dir() {
let mut cache = Cache { chains: vec![] };
if !cache_dir.exists() {
return Ok(cache)
}
if let Ok(entries) = cache_dir.as_path().read_dir() {
for entry in entries.flatten().filter(|x| x.path().is_dir()) {
match Chain::from_str(&entry.file_name().to_string_lossy()) {
Ok(chain) => cache.chains.push(Self::list_foundry_chain_cache(chain)?),
Err(_) => continue,
}
}
Ok(cache)
} else {
eyre::bail!("failed to access foundry_cache_dir");
}
} else {
eyre::bail!("failed to get foundry_cache_dir");
}
}
pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
let block_explorer_data_size = match Config::foundry_etherscan_chain_cache_dir(chain) {
Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
None => {
warn!("failed to access foundry_etherscan_chain_cache_dir");
0
}
};
if let Some(cache_dir) = Config::foundry_chain_cache_dir(chain) {
let blocks = Self::get_cached_blocks(&cache_dir)?;
Ok(ChainCache {
name: chain.to_string(),
blocks,
block_explorer: block_explorer_data_size,
})
} else {
eyre::bail!("failed to get foundry_chain_cache_dir");
}
}
fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
let mut blocks = vec![];
if !chain_path.exists() {
return Ok(blocks)
}
for block in chain_path.read_dir()?.flatten().filter(|x| x.file_type().unwrap().is_dir()) {
let filepath = block.path().join("storage.json");
blocks.push((
block.file_name().to_string_lossy().into_owned(),
fs::metadata(filepath)?.len(),
));
}
Ok(blocks)
}
fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
if !chain_path.exists() {
return Ok(0)
}
fn dir_size_recursive(mut dir: fs::ReadDir) -> eyre::Result<u64> {
dir.try_fold(0, |acc, file| {
let file = file?;
let size = match file.metadata()? {
data if data.is_dir() => dir_size_recursive(fs::read_dir(file.path())?)?,
data => data.len(),
};
Ok(acc + size)
})
}
dir_size_recursive(fs::read_dir(chain_path)?)
}
fn merge_toml_provider(
mut figment: Figment,
toml_provider: impl Provider,
profile: Profile,
) -> Figment {
figment = figment.select(profile.clone());
figment = {
let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
figment.merge(warnings)
};
let mut profiles = vec![Config::DEFAULT_PROFILE];
if profile != Config::DEFAULT_PROFILE {
profiles.push(profile.clone());
}
let provider = toml_provider.strict_select(profiles);
let provider = BackwardsCompatTomlProvider(ForcedSnakeCaseData(provider));
if profile != Config::DEFAULT_PROFILE {
figment = figment.merge(provider.rename(Config::DEFAULT_PROFILE, profile.clone()));
}
for standalone_key in Config::STANDALONE_SECTIONS {
if let Some(fallback) = STANDALONE_FALLBACK_SECTIONS.get(standalone_key) {
figment = figment.merge(
provider
.fallback(standalone_key, fallback)
.wrap(profile.clone(), standalone_key),
);
} else {
figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
}
}
figment = figment.merge(provider);
figment
}
}
impl From<Config> for Figment {
fn from(c: Config) -> Figment {
let profile = Config::selected_profile();
let mut figment = Figment::default().merge(DappHardhatDirProvider(&c.__root.0));
if let Some(global_toml) = Config::foundry_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("FOUNDRY_CONFIG"), c.__root.0.join(Config::FILE_NAME))
.cached(),
profile.clone(),
);
figment = figment
.merge(
Env::prefixed("DAPP_")
.ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
.global(),
)
.merge(
Env::prefixed("DAPP_TEST_")
.ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
.global(),
)
.merge(DappEnvCompatProvider)
.merge(Env::raw().only(&["ETHERSCAN_API_KEY"]))
.merge(
Env::prefixed("FOUNDRY_")
.ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
.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());
let remappings = RemappingsProvider {
auto_detect_remappings: figment
.extract_inner::<bool>("auto_detect_remappings")
.unwrap_or(true),
lib_paths: figment
.extract_inner::<Vec<PathBuf>>("libs")
.map(Cow::Owned)
.unwrap_or_else(|_| Cow::Borrowed(&c.libs)),
root: &c.__root.0,
remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
};
let merge = figment.merge(remappings);
Figment::from(c).merge(merge).select(profile)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(transparent)]
pub struct RegexWrapper {
#[serde(with = "serde_regex")]
inner: regex::Regex,
}
impl std::ops::Deref for RegexWrapper {
type Target = regex::Regex;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::cmp::PartialEq for RegexWrapper {
fn eq(&self, other: &Self) -> bool {
self.as_str() == other.as_str()
}
}
impl From<RegexWrapper> for regex::Regex {
fn from(wrapper: RegexWrapper) -> Self {
wrapper.inner
}
}
impl From<regex::Regex> for RegexWrapper {
fn from(re: Regex) -> Self {
RegexWrapper { inner: re }
}
}
pub(crate) mod from_opt_glob {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(glob) => serializer.serialize_str(glob.glob()),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
if let Some(s) = s {
return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?))
}
Ok(None)
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(transparent)]
pub struct RootPath(pub PathBuf);
impl Default for RootPath {
fn default() -> Self {
".".into()
}
}
impl<P: Into<PathBuf>> From<P> for RootPath {
fn from(p: P) -> Self {
RootPath(p.into())
}
}
impl AsRef<Path> for RootPath {
fn as_ref(&self) -> &Path {
&self.0
}
}
pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
s: &str,
) -> Result<Option<(Profile, T)>, Error> {
let figment = Config::merge_toml_provider(
Figment::new(),
Toml::string(s).nested(),
Config::DEFAULT_PROFILE,
);
if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
} else {
Ok(None)
}
}
impl Provider for Config {
fn metadata(&self) -> Metadata {
Metadata::named("Foundry 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 Default for Config {
fn default() -> Self {
Self {
profile: Self::DEFAULT_PROFILE,
fs_permissions: FsPermissions::new([PathPermission::read("out")]),
cancun: false,
__root: Default::default(),
src: "src".into(),
test: "test".into(),
script: "script".into(),
out: "out".into(),
libs: vec!["lib".into()],
cache: true,
cache_path: "cache".into(),
broadcast: "broadcast".into(),
allow_paths: vec![],
include_paths: vec![],
force: false,
evm_version: EvmVersion::Paris,
gas_reports: vec!["*".to_string()],
gas_reports_ignore: vec![],
solc: None,
auto_detect_solc: true,
offline: false,
optimizer: true,
optimizer_runs: 200,
optimizer_details: None,
model_checker: None,
extra_output: Default::default(),
extra_output_files: Default::default(),
names: false,
sizes: false,
test_pattern: None,
test_pattern_inverse: None,
contract_pattern: None,
contract_pattern_inverse: None,
path_pattern: None,
path_pattern_inverse: None,
fuzz: Default::default(),
invariant: Default::default(),
ffi: false,
sender: Config::DEFAULT_SENDER,
tx_origin: Config::DEFAULT_SENDER,
initial_balance: U256::from(0xffffffffffffffffffffffffu128),
block_number: 1,
fork_block_number: None,
chain_id: None,
gas_limit: i64::MAX.into(),
code_size_limit: None,
gas_price: None,
block_base_fee_per_gas: 0,
block_coinbase: Address::zero(),
block_timestamp: 1,
block_difficulty: 0,
block_prevrandao: Default::default(),
block_gas_limit: None,
memory_limit: 2u64.pow(25),
eth_rpc_url: None,
eth_rpc_jwt: None,
etherscan_api_key: None,
verbosity: 0,
remappings: vec![],
auto_detect_remappings: true,
libraries: vec![],
ignored_error_codes: vec![
SolidityErrorCode::SpdxLicenseNotProvided,
SolidityErrorCode::ContractExceeds24576Bytes,
SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
],
deny_warnings: false,
via_ir: false,
rpc_storage_caching: Default::default(),
rpc_endpoints: Default::default(),
etherscan: Default::default(),
no_storage_caching: false,
no_rpc_rate_limit: false,
use_literal_content: false,
bytecode_hash: BytecodeHash::Ipfs,
cbor_metadata: true,
revert_strings: None,
sparse_mode: false,
build_info: false,
build_info_path: None,
fmt: Default::default(),
doc: Default::default(),
__non_exhaustive: (),
__warnings: vec![],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GasLimit(pub u64);
impl From<u64> for GasLimit {
fn from(gas: u64) -> Self {
Self(gas)
}
}
impl From<i64> for GasLimit {
fn from(gas: i64) -> Self {
Self(gas as u64)
}
}
impl From<i32> for GasLimit {
fn from(gas: i32) -> Self {
Self(gas as u64)
}
}
impl From<u32> for GasLimit {
fn from(gas: u32) -> Self {
Self(gas as u64)
}
}
impl From<GasLimit> for u64 {
fn from(gas: GasLimit) -> Self {
gas.0
}
}
impl Serialize for GasLimit {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if self.0 > i64::MAX as u64 {
serializer.serialize_str(&self.0.to_string())
} else {
serializer.serialize_u64(self.0)
}
}
}
impl<'de> Deserialize<'de> for GasLimit {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
#[serde(untagged)]
enum Gas {
Number(u64),
Text(String),
}
let gas = match Gas::deserialize(deserializer)? {
Gas::Number(num) => GasLimit(num),
Gas::Text(s) => match s.as_str() {
"max" | "MAX" | "Max" | "u64::MAX" | "u64::Max" => GasLimit(u64::MAX),
s => GasLimit(s.parse().map_err(D::Error::custom)?),
},
};
Ok(gas)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SolcReq {
Version(Version),
Local(PathBuf),
}
impl<T: AsRef<str>> From<T> for SolcReq {
fn from(s: T) -> Self {
let s = s.as_ref();
if let Ok(v) = Version::from_str(s) {
SolcReq::Version(v)
} else {
SolcReq::Local(s.into())
}
}
}
struct TomlFileProvider {
pub env_var: Option<&'static str>,
pub default: PathBuf,
pub cache: Option<Result<Map<Profile, Dict>, Error>>,
}
impl TomlFileProvider {
fn new(env_var: Option<&'static str>, default: impl Into<PathBuf>) -> Self {
Self { env_var, default: default.into(), cache: None }
}
fn env_val(&self) -> Option<String> {
self.env_var.and_then(Env::var)
}
fn file(&self) -> PathBuf {
self.env_val().map(PathBuf::from).unwrap_or_else(|| self.default.clone())
}
fn is_missing(&self) -> bool {
if let Some(file) = self.env_val() {
let path = Path::new(&file);
if !path.exists() {
return true
}
}
false
}
pub fn cached(mut self) -> Self {
self.cache = Some(self.read());
self
}
fn read(&self) -> Result<Map<Profile, Dict>, Error> {
use serde::de::Error as _;
if let Some(file) = self.env_val() {
let path = Path::new(&file);
if !path.exists() {
return Err(Error::custom(format!(
"Config file `{}` set in env var `{}` does not exist",
file,
self.env_var.unwrap()
)))
}
Toml::file(file)
} else {
Toml::file(&self.default)
}
.nested()
.data()
}
}
impl Provider for TomlFileProvider {
fn metadata(&self) -> Metadata {
if self.is_missing() {
Metadata::named("TOML file provider")
} else {
Toml::file(self.file()).nested().metadata()
}
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
if let Some(cache) = self.cache.as_ref() {
cache.clone()
} else {
self.read()
}
}
}
struct ForcedSnakeCaseData<P>(P);
impl<P: Provider> Provider for ForcedSnakeCaseData<P> {
fn metadata(&self) -> Metadata {
self.0.metadata()
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
let mut map = Map::new();
for (profile, dict) in self.0.data()? {
if Config::STANDALONE_SECTIONS.contains(&profile.as_ref()) {
map.insert(profile, dict);
continue
}
map.insert(profile, dict.into_iter().map(|(k, v)| (k.to_snake_case(), v)).collect());
}
Ok(map)
}
}
struct BackwardsCompatTomlProvider<P>(P);
impl<P: Provider> Provider for BackwardsCompatTomlProvider<P> {
fn metadata(&self) -> Metadata {
self.0.metadata()
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
let mut map = Map::new();
let solc_env = std::env::var("FOUNDRY_SOLC_VERSION")
.or_else(|_| std::env::var("DAPP_SOLC_VERSION"))
.map(Value::from)
.ok();
for (profile, mut dict) in self.0.data()? {
if let Some(v) = solc_env.clone().or_else(|| dict.remove("solc_version")) {
dict.insert("solc".to_string(), v);
}
map.insert(profile, dict);
}
Ok(map)
}
}
struct DappHardhatDirProvider<'a>(&'a Path);
impl<'a> Provider for DappHardhatDirProvider<'a> {
fn metadata(&self) -> Metadata {
Metadata::named("Dapp Hardhat dir compat")
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
let mut dict = Dict::new();
dict.insert(
"src".to_string(),
ProjectPathsConfig::find_source_dir(self.0)
.file_name()
.unwrap()
.to_string_lossy()
.to_string()
.into(),
);
dict.insert(
"out".to_string(),
ProjectPathsConfig::find_artifacts_dir(self.0)
.file_name()
.unwrap()
.to_string_lossy()
.to_string()
.into(),
);
let mut libs = vec![];
let node_modules = self.0.join("node_modules");
let lib = self.0.join("lib");
if node_modules.exists() {
if lib.exists() {
libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
}
libs.push(node_modules.file_name().unwrap().to_string_lossy().to_string());
} else {
libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
}
dict.insert("libs".to_string(), libs.into());
Ok(Map::from([(Config::selected_profile(), dict)]))
}
}
struct DappEnvCompatProvider;
impl Provider for DappEnvCompatProvider {
fn metadata(&self) -> Metadata {
Metadata::named("Dapp env compat")
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
use serde::de::Error as _;
use std::env;
let mut dict = Dict::new();
if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
dict.insert(
"block_number".to_string(),
val.parse::<u64>().map_err(figment::Error::custom)?.into(),
);
}
if let Ok(val) = env::var("DAPP_TEST_ADDRESS") {
dict.insert("sender".to_string(), val.into());
}
if let Ok(val) = env::var("DAPP_FORK_BLOCK") {
dict.insert(
"fork_block_number".to_string(),
val.parse::<u64>().map_err(figment::Error::custom)?.into(),
);
} else if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
dict.insert(
"fork_block_number".to_string(),
val.parse::<u64>().map_err(figment::Error::custom)?.into(),
);
}
if let Ok(val) = env::var("DAPP_TEST_TIMESTAMP") {
dict.insert(
"block_timestamp".to_string(),
val.parse::<u64>().map_err(figment::Error::custom)?.into(),
);
}
if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE_RUNS") {
dict.insert(
"optimizer_runs".to_string(),
val.parse::<u64>().map_err(figment::Error::custom)?.into(),
);
}
if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE") {
let val = val.parse::<u8>().map_err(figment::Error::custom)?;
if val > 1 {
return Err(
format!("Invalid $DAPP_BUILD_OPTIMIZE value `{val}`, expected 0 or 1").into()
)
}
dict.insert("optimizer".to_string(), (val == 1).into());
}
if let Ok(val) = env::var("DAPP_LIBRARIES").or_else(|_| env::var("FOUNDRY_LIBRARIES")) {
dict.insert("libraries".to_string(), utils::to_array_value(&val)?);
}
let mut fuzz_dict = Dict::new();
if let Ok(val) = env::var("DAPP_TEST_FUZZ_RUNS") {
fuzz_dict.insert(
"runs".to_string(),
val.parse::<u32>().map_err(figment::Error::custom)?.into(),
);
}
dict.insert("fuzz".to_string(), fuzz_dict.into());
let mut invariant_dict = Dict::new();
if let Ok(val) = env::var("DAPP_TEST_DEPTH") {
invariant_dict.insert(
"depth".to_string(),
val.parse::<u32>().map_err(figment::Error::custom)?.into(),
);
}
dict.insert("invariant".to_string(), invariant_dict.into());
Ok(Map::from([(Config::selected_profile(), dict)]))
}
}
struct RenameProfileProvider<P> {
provider: P,
from: Profile,
to: Profile,
}
impl<P> RenameProfileProvider<P> {
pub fn new(provider: P, from: impl Into<Profile>, to: impl Into<Profile>) -> Self {
Self { provider, from: from.into(), to: to.into() }
}
}
impl<P: Provider> Provider for RenameProfileProvider<P> {
fn metadata(&self) -> Metadata {
self.provider.metadata()
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
let mut data = self.provider.data()?;
if let Some(data) = data.remove(&self.from) {
return Ok(Map::from([(self.to.clone(), data)]))
}
Ok(Default::default())
}
fn profile(&self) -> Option<Profile> {
Some(self.to.clone())
}
}
struct UnwrapProfileProvider<P> {
provider: P,
wrapping_key: Profile,
profile: Profile,
}
impl<P> UnwrapProfileProvider<P> {
pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
}
}
impl<P: Provider> Provider for UnwrapProfileProvider<P> {
fn metadata(&self) -> Metadata {
self.provider.metadata()
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
self.provider.data().and_then(|mut data| {
if let Some(profiles) = data.remove(&self.wrapping_key) {
for (profile_str, profile_val) in profiles {
let profile = Profile::new(&profile_str);
if profile != self.profile {
continue
}
match profile_val {
Value::Dict(_, dict) => return Ok(profile.collect(dict)),
bad_val => {
let mut err = Error::from(figment::error::Kind::InvalidType(
bad_val.to_actual(),
"dict".into(),
));
err.metadata = Some(self.provider.metadata());
err.profile = Some(self.profile.clone());
return Err(err)
}
}
}
}
Ok(Default::default())
})
}
fn profile(&self) -> Option<Profile> {
Some(self.profile.clone())
}
}
struct WrapProfileProvider<P> {
provider: P,
wrapping_key: Profile,
profile: Profile,
}
impl<P> WrapProfileProvider<P> {
pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
}
}
impl<P: Provider> Provider for WrapProfileProvider<P> {
fn metadata(&self) -> Metadata {
self.provider.metadata()
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
if let Some(inner) = self.provider.data()?.remove(&self.profile) {
let value = Value::from(inner);
let dict = [(self.profile.to_string().to_snake_case(), value)].into_iter().collect();
Ok(self.wrapping_key.collect(dict))
} else {
Ok(Default::default())
}
}
fn profile(&self) -> Option<Profile> {
Some(self.profile.clone())
}
}
struct OptionalStrictProfileProvider<P> {
provider: P,
profiles: Vec<Profile>,
}
impl<P> OptionalStrictProfileProvider<P> {
pub const PROFILE_PROFILE: Profile = Profile::const_new("profile");
pub fn new(provider: P, profiles: impl IntoIterator<Item = impl Into<Profile>>) -> Self {
Self { provider, profiles: profiles.into_iter().map(|profile| profile.into()).collect() }
}
}
impl<P: Provider> Provider for OptionalStrictProfileProvider<P> {
fn metadata(&self) -> Metadata {
self.provider.metadata()
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
let mut figment = Figment::from(&self.provider);
for profile in &self.profiles {
figment = figment.merge(UnwrapProfileProvider::new(
&self.provider,
Self::PROFILE_PROFILE,
profile.clone(),
));
}
figment.data().map_err(|err| {
if let Err(root_err) = self.provider.data() {
return root_err
}
err
})
}
fn profile(&self) -> Option<Profile> {
self.profiles.last().cloned()
}
}
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)
}
fn fallback(
&self,
profile: impl Into<Profile>,
fallback: impl Into<Profile>,
) -> FallbackProfileProvider<&Self> {
FallbackProfileProvider::new(self, profile, fallback)
}
}
impl<P: Provider> ProviderExt for P {}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct BasicConfig {
#[serde(skip)]
pub profile: Profile,
pub src: PathBuf,
pub out: PathBuf,
pub libs: Vec<PathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub remappings: Vec<RelativeRemapping>,
}
impl BasicConfig {
pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
let s = toml::to_string_pretty(self)?;
Ok(format!(
"\
[profile.{}]
{s}
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n",
self.profile
))
}
}
pub(crate) mod from_str_lowercase {
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: std::fmt::Display,
S: Serializer,
{
serializer.collect_str(&value.to_string().to_lowercase())
}
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: std::fmt::Display,
{
String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
}
}
fn canonic(path: impl Into<PathBuf>) -> PathBuf {
let path = path.into();
ethers_solc::utils::canonicalize(&path).unwrap_or(path)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
cache::{CachedChains, CachedEndpoints},
endpoints::RpcEndpoint,
etherscan::ResolvedEtherscanConfigs,
fs_permissions::PathPermission,
};
use ethers_core::types::Chain::Moonbeam;
use ethers_solc::artifacts::{ModelCheckerEngine, YulDetails};
use figment::{error::Kind::InvalidType, value::Value, Figment};
use pretty_assertions::assert_eq;
use std::{collections::BTreeMap, fs::File, io::Write, str::FromStr};
use tempfile::tempdir;
fn clear_warning(config: &mut Config) {
config.__warnings = vec![];
}
#[test]
fn default_sender() {
assert_eq!(
Config::DEFAULT_SENDER,
"0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38".parse().unwrap()
);
}
#[test]
fn test_caching() {
let mut config = Config::default();
let chain_id = ethers_core::types::Chain::Mainnet;
let url = "https://eth-mainnet.alchemyapi";
assert!(config.enable_caching(url, chain_id));
config.no_storage_caching = true;
assert!(!config.enable_caching(url, chain_id));
config.no_storage_caching = false;
assert!(!config.enable_caching(url, ethers_core::types::Chain::Dev));
}
#[test]
fn test_install_dir() {
figment::Jail::expect_with(|jail| {
let config = Config::load();
assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
jail.create_file(
"foundry.toml",
r#"
[profile.default]
libs = ['node_modules', 'lib']
"#,
)?;
let config = Config::load();
assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
jail.create_file(
"foundry.toml",
r#"
[profile.default]
libs = ['custom', 'node_modules', 'lib']
"#,
)?;
let config = Config::load();
assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
Ok(())
});
}
#[test]
fn test_figment_is_default() {
figment::Jail::expect_with(|_| {
let mut default: Config = Config::figment().extract().unwrap();
default.profile = Config::default().profile;
assert_eq!(default, Config::default());
Ok(())
});
}
#[test]
fn test_default_round_trip() {
figment::Jail::expect_with(|_| {
let original = Config::figment();
let roundtrip = Figment::from(Config::from_provider(&original));
for figment in &[original, roundtrip] {
let config = Config::from_provider(figment);
assert_eq!(config, Config::default());
}
Ok(())
});
}
#[test]
fn ffi_env_disallowed() {
figment::Jail::expect_with(|jail| {
jail.set_env("FOUNDRY_FFI", "true");
jail.set_env("FFI", "true");
jail.set_env("DAPP_FFI", "true");
let config = Config::load();
assert!(!config.ffi);
Ok(())
});
}
#[test]
fn test_profile_env() {
figment::Jail::expect_with(|jail| {
jail.set_env("FOUNDRY_PROFILE", "default");
let figment = Config::figment();
assert_eq!(figment.profile(), "default");
jail.set_env("FOUNDRY_PROFILE", "hardhat");
let figment: Figment = Config::hardhat().into();
assert_eq!(figment.profile(), "hardhat");
jail.create_file(
"foundry.toml",
r#"
[profile.default]
libs = ['lib']
[profile.local]
libs = ['modules']
"#,
)?;
jail.set_env("FOUNDRY_PROFILE", "local");
let config = Config::load();
assert_eq!(config.libs, vec![PathBuf::from("modules")]);
Ok(())
});
}
#[test]
fn test_default_test_path() {
figment::Jail::expect_with(|_| {
let config = Config::default();
let paths_config = config.project_paths();
assert_eq!(paths_config.tests, PathBuf::from(r"test"));
Ok(())
});
}
#[test]
fn test_default_libs() {
figment::Jail::expect_with(|jail| {
let config = Config::load();
assert_eq!(config.libs, vec![PathBuf::from("lib")]);
fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
let config = Config::load();
assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
fs::create_dir_all(jail.directory().join("lib")).unwrap();
let config = Config::load();
assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
Ok(())
});
}
#[test]
fn test_inheritance_from_default_test_path() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
test = "defaulttest"
src = "defaultsrc"
libs = ['lib', 'node_modules']
[profile.custom]
src = "customsrc"
"#,
)?;
let config = Config::load();
assert_eq!(config.src, PathBuf::from("defaultsrc"));
assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
jail.set_env("FOUNDRY_PROFILE", "custom");
let config = Config::load();
assert_eq!(config.src, PathBuf::from("customsrc"));
assert_eq!(config.test, PathBuf::from("defaulttest"));
assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
Ok(())
});
}
#[test]
fn test_custom_test_path() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
test = "mytest"
"#,
)?;
let config = Config::load();
let paths_config = config.project_paths();
assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
Ok(())
});
}
#[test]
fn test_remappings() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
src = "some-source"
out = "some-out"
cache = true
"#,
)?;
let config = Config::load();
assert!(config.remappings.is_empty());
jail.create_file(
"remappings.txt",
r#"
file-ds-test/=lib/ds-test/
file-other/=lib/other/
"#,
)?;
let config = Config::load();
assert_eq!(
config.remappings,
vec![
Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
Remapping::from_str("file-other/=lib/other/").unwrap().into(),
],
);
jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
let config = Config::load();
assert_eq!(
config.remappings,
vec![
Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
Remapping::from_str("other/=lib/other/").unwrap().into(),
Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
Remapping::from_str("file-other/=lib/other/").unwrap().into(),
],
);
Ok(())
});
}
#[test]
fn test_remappings_override() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
src = "some-source"
out = "some-out"
cache = true
"#,
)?;
let config = Config::load();
assert!(config.remappings.is_empty());
jail.create_file(
"remappings.txt",
r#"
ds-test/=lib/ds-test/
other/=lib/other/
"#,
)?;
let config = Config::load();
assert_eq!(
config.remappings,
vec![
Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
Remapping::from_str("other/=lib/other/").unwrap().into(),
],
);
jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
let config = Config::load();
assert_eq!(
config.remappings,
vec![
Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
Remapping::from_str("other/=lib/other/").unwrap().into(),
],
);
assert_eq!(
config.get_all_remappings(),
vec![
Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
Remapping::from_str("other/=lib/other/").unwrap(),
],
);
Ok(())
});
}
#[test]
fn test_can_update_libs() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
libs = ["node_modules"]
"#,
)?;
let mut config = Config::load();
config.libs.push("libs".into());
config.update_libs().unwrap();
let config = Config::load();
assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
Ok(())
});
}
#[test]
fn test_large_gas_limit() {
figment::Jail::expect_with(|jail| {
let gas = u64::MAX;
jail.create_file(
"foundry.toml",
&format!(
r#"
[profile.default]
gas_limit = "{gas}"
"#
),
)?;
let config = Config::load();
assert_eq!(config, Config { gas_limit: gas.into(), ..Config::default() });
Ok(())
});
}
#[test]
#[should_panic]
fn test_toml_file_parse_failure() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
eth_rpc_url = "https://example.com/
"#,
)?;
let _config = Config::load();
Ok(())
});
}
#[test]
#[should_panic]
fn test_toml_file_non_existing_config_var_failure() {
figment::Jail::expect_with(|jail| {
jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
let _config = Config::load();
Ok(())
});
}
#[test]
fn test_resolve_etherscan_with_chain() {
figment::Jail::expect_with(|jail| {
let env_key = "__BSC_ETHERSCAN_API_KEY";
let env_value = "env value";
jail.create_file(
"foundry.toml",
r#"
[profile.default]
[etherscan]
bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
"#,
)?;
let config = Config::load();
assert!(config.get_etherscan_config_with_chain(None::<u64>).unwrap().is_none());
assert!(config
.get_etherscan_config_with_chain(Some(ethers_core::types::Chain::BinanceSmartChain))
.is_err());
std::env::set_var(env_key, env_value);
assert_eq!(
config
.get_etherscan_config_with_chain(Some(
ethers_core::types::Chain::BinanceSmartChain
))
.unwrap()
.unwrap()
.key,
env_value
);
let mut with_key = config;
with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
assert_eq!(
with_key
.get_etherscan_config_with_chain(Some(
ethers_core::types::Chain::BinanceSmartChain
))
.unwrap()
.unwrap()
.key,
"via etherscan_api_key"
);
std::env::remove_var(env_key);
Ok(())
});
}
#[test]
fn test_resolve_etherscan() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
[etherscan]
mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
"#,
)?;
let config = Config::load();
assert!(config.etherscan.clone().resolved().has_unresolved());
jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
let configs = config.etherscan.resolved();
assert!(!configs.has_unresolved());
let mb_urls = Moonbeam.etherscan_urls().unwrap();
let mainnet_urls = Mainnet.etherscan_urls().unwrap();
assert_eq!(
configs,
ResolvedEtherscanConfigs::new([
(
"mainnet",
ResolvedEtherscanConfig {
api_url: mainnet_urls.0.to_string(),
chain: Some(Mainnet.into()),
browser_url: Some(mainnet_urls.1.to_string()),
key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
}
),
(
"moonbeam",
ResolvedEtherscanConfig {
api_url: mb_urls.0.to_string(),
chain: Some(Moonbeam.into()),
browser_url: Some(mb_urls.1.to_string()),
key: "123456789".to_string(),
}
),
])
);
Ok(())
});
}
#[test]
fn test_resolve_rpc_url() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
[rpc_endpoints]
optimism = "https://example.com/"
mainnet = "${_CONFIG_MAINNET}"
"#,
)?;
jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
let mut config = Config::load();
assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
config.eth_rpc_url = Some("mainnet".to_string());
assert_eq!(
"https://eth-mainnet.alchemyapi.io/v2/123455",
config.get_rpc_url_or_localhost_http().unwrap()
);
config.eth_rpc_url = Some("optimism".to_string());
assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
Ok(())
})
}
#[test]
fn test_resolve_rpc_url_if_etherscan_set() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
etherscan_api_key = "dummy"
[rpc_endpoints]
optimism = "https://example.com/"
"#,
)?;
let config = Config::load();
assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
Ok(())
})
}
#[test]
fn test_resolve_rpc_url_alias() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
[rpc_endpoints]
polygonMumbai = "https://polygon-mumbai.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
"#,
)?;
let mut config = Config::load();
config.eth_rpc_url = Some("polygonMumbai".to_string());
assert!(config.get_rpc_url().unwrap().is_err());
jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
let mut config = Config::load();
config.eth_rpc_url = Some("polygonMumbai".to_string());
assert_eq!(
"https://polygon-mumbai.g.alchemy.com/v2/123455",
config.get_rpc_url().unwrap().unwrap()
);
Ok(())
})
}
#[test]
fn test_resolve_endpoints() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
eth_rpc_url = "optimism"
[rpc_endpoints]
optimism = "https://example.com/"
mainnet = "${_CONFIG_MAINNET}"
mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
"#,
)?;
let config = Config::load();
assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
jail.set_env("_CONFIG_API_KEY1", "123456");
jail.set_env("_CONFIG_API_KEY2", "98765");
let endpoints = config.rpc_endpoints.resolved();
assert!(!endpoints.has_unresolved());
assert_eq!(
endpoints,
RpcEndpoints::new([
("optimism", RpcEndpoint::Url("https://example.com/".to_string())),
(
"mainnet",
RpcEndpoint::Url("https://eth-mainnet.alchemyapi.io/v2/123455".to_string())
),
(
"mainnet_2",
RpcEndpoint::Url("https://eth-mainnet.alchemyapi.io/v2/123456".to_string())
),
(
"mainnet_3",
RpcEndpoint::Url(
"https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
)
),
])
.resolved()
);
Ok(())
});
}
#[test]
fn test_extract_etherscan_config() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
etherscan_api_key = "optimism"
[etherscan]
optimism = { key = "https://etherscan-optimism.com/" }
mumbai = { key = "https://etherscan-mumbai.com/" }
"#,
)?;
let mut config = Config::load();
let optimism = config.get_etherscan_api_key(Some(ethers_core::types::Chain::Optimism));
assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
config.etherscan_api_key = Some("mumbai".to_string());
let mumbai =
config.get_etherscan_api_key(Some(ethers_core::types::Chain::PolygonMumbai));
assert_eq!(mumbai, Some("https://etherscan-mumbai.com/".to_string()));
Ok(())
});
}
#[test]
fn test_extract_etherscan_config_by_chain() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
[etherscan]
mumbai = { key = "https://etherscan-mumbai.com/", chain = 80001 }
"#,
)?;
let config = Config::load();
let mumbai = config
.get_etherscan_config_with_chain(Some(ethers_core::types::Chain::PolygonMumbai))
.unwrap()
.unwrap();
assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
Ok(())
});
}
#[test]
fn test_extract_etherscan_config_by_chain_with_url() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
[etherscan]
mumbai = { key = "https://etherscan-mumbai.com/", chain = 80001 , url = "https://verifier-url.com/"}
"#,
)?;
let config = Config::load();
let mumbai = config
.get_etherscan_config_with_chain(Some(ethers_core::types::Chain::PolygonMumbai))
.unwrap()
.unwrap();
assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
assert_eq!(mumbai.api_url, "https://verifier-url.com/".to_string());
Ok(())
});
}
#[test]
fn test_extract_etherscan_config_by_chain_and_alias() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
eth_rpc_url = "mumbai"
[etherscan]
mumbai = { key = "https://etherscan-mumbai.com/" }
[rpc_endpoints]
mumbai = "https://polygon-mumbai.g.alchemy.com/v2/mumbai"
"#,
)?;
let config = Config::load();
let mumbai =
config.get_etherscan_config_with_chain(Option::<u64>::None).unwrap().unwrap();
assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
let mumbai_rpc = config.get_rpc_url().unwrap().unwrap();
assert_eq!(mumbai_rpc, "https://polygon-mumbai.g.alchemy.com/v2/mumbai");
Ok(())
});
}
#[test]
fn test_toml_file() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
src = "some-source"
out = "some-out"
cache = true
eth_rpc_url = "https://example.com/"
verbosity = 3
remappings = ["ds-test=lib/ds-test/"]
via_ir = true
rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
use_literal_content = false
bytecode_hash = "ipfs"
cbor_metadata = true
revert_strings = "strip"
allow_paths = ["allow", "paths"]
build_info_path = "build-info"
[rpc_endpoints]
optimism = "https://example.com/"
mainnet = "${RPC_MAINNET}"
mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
"#,
)?;
let config = Config::load();
assert_eq!(
config,
Config {
src: "some-source".into(),
out: "some-out".into(),
cache: true,
eth_rpc_url: Some("https://example.com/".to_string()),
remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
verbosity: 3,
via_ir: true,
rpc_storage_caching: StorageCachingConfig {
chains: CachedChains::Chains(vec![
Chain::Named(ethers_core::types::Chain::Mainnet),
Chain::Named(ethers_core::types::Chain::Optimism),
Chain::Id(999999)
]),
endpoints: CachedEndpoints::All
},
use_literal_content: false,
bytecode_hash: BytecodeHash::Ipfs,
cbor_metadata: true,
revert_strings: Some(RevertStrings::Strip),
allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
rpc_endpoints: RpcEndpoints::new([
("optimism", RpcEndpoint::Url("https://example.com/".to_string())),
("mainnet", RpcEndpoint::Env("${RPC_MAINNET}".to_string())),
(
"mainnet_2",
RpcEndpoint::Env(
"https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
)
),
(
"mainnet_3",
RpcEndpoint::Env(
"https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
.to_string()
)
),
]),
build_info_path: Some("build-info".into()),
..Config::default()
}
);
Ok(())
});
}
#[test]
fn test_load_remappings() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
remappings = ['nested/=lib/nested/']
"#,
)?;
let config = Config::load_with_root(jail.directory());
assert_eq!(
config.remappings,
vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
);
Ok(())
});
}
#[test]
fn test_load_full_toml() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
auto_detect_solc = true
block_base_fee_per_gas = 0
block_coinbase = '0x0000000000000000000000000000000000000000'
block_difficulty = 0
block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
block_number = 1
block_timestamp = 1
use_literal_content = false
bytecode_hash = 'ipfs'
cbor_metadata = true
cache = true
cache_path = 'cache'
evm_version = 'london'
extra_output = []
extra_output_files = []
ffi = false
force = false
gas_limit = 9223372036854775807
gas_price = 0
gas_reports = ['*']
ignored_error_codes = [1878]
deny_warnings = false
initial_balance = '0xffffffffffffffffffffffff'
libraries = []
libs = ['lib']
memory_limit = 33554432
names = false
no_storage_caching = false
no_rpc_rate_limit = false
offline = false
optimizer = true
optimizer_runs = 200
out = 'out'
remappings = ['nested/=lib/nested/']
sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
sizes = false
sparse_mode = false
src = 'src'
test = 'test'
tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
verbosity = 0
via_ir = false
[profile.default.rpc_storage_caching]
chains = 'all'
endpoints = 'all'
[rpc_endpoints]
optimism = "https://example.com/"
mainnet = "${RPC_MAINNET}"
mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
[fuzz]
runs = 256
seed = '0x3e8'
max_test_rejects = 65536
[invariant]
runs = 256
depth = 15
fail_on_revert = false
call_override = false
shrink_sequence = true
"#,
)?;
let config = Config::load_with_root(jail.directory());
assert_eq!(config.fuzz.seed, Some(1000.into()));
assert_eq!(
config.remappings,
vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
);
assert_eq!(
config.rpc_endpoints,
RpcEndpoints::new([
("optimism", RpcEndpoint::Url("https://example.com/".to_string())),
("mainnet", RpcEndpoint::Env("${RPC_MAINNET}".to_string())),
(
"mainnet_2",
RpcEndpoint::Env(
"https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
)
),
]),
);
Ok(())
});
}
#[test]
fn test_solc_req() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
solc_version = "0.8.12"
"#,
)?;
let config = Config::load();
assert_eq!(config.solc, Some(SolcReq::Version("0.8.12".parse().unwrap())));
jail.create_file(
"foundry.toml",
r#"
[profile.default]
solc = "0.8.12"
"#,
)?;
let config = Config::load();
assert_eq!(config.solc, Some(SolcReq::Version("0.8.12".parse().unwrap())));
jail.create_file(
"foundry.toml",
r#"
[profile.default]
solc = "path/to/local/solc"
"#,
)?;
let config = Config::load();
assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
let config = Config::load();
assert_eq!(config.solc, Some(SolcReq::Version("0.6.6".parse().unwrap())));
Ok(())
});
}
#[test]
fn test_toml_casing_file() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
src = "some-source"
out = "some-out"
cache = true
eth-rpc-url = "https://example.com/"
evm-version = "berlin"
auto-detect-solc = false
"#,
)?;
let config = Config::load();
assert_eq!(
config,
Config {
src: "some-source".into(),
out: "some-out".into(),
cache: true,
eth_rpc_url: Some("https://example.com/".to_string()),
auto_detect_solc: false,
evm_version: EvmVersion::Berlin,
..Config::default()
}
);
Ok(())
});
}
#[test]
fn test_output_selection() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
extra_output = ["metadata", "ir-optimized"]
extra_output_files = ["metadata"]
"#,
)?;
let config = Config::load();
assert_eq!(
config.extra_output,
vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
);
assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
Ok(())
});
}
#[test]
fn test_precedence() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
src = "mysrc"
out = "myout"
verbosity = 3
"#,
)?;
let config = Config::load();
assert_eq!(
config,
Config {
src: "mysrc".into(),
out: "myout".into(),
verbosity: 3,
..Config::default()
}
);
jail.set_env("FOUNDRY_SRC", r#"other-src"#);
let config = Config::load();
assert_eq!(
config,
Config {
src: "other-src".into(),
out: "myout".into(),
verbosity: 3,
..Config::default()
}
);
jail.set_env("FOUNDRY_PROFILE", "foo");
let val: Result<String, _> = Config::figment().extract_inner("profile");
assert!(val.is_err());
Ok(())
});
}
#[test]
fn test_extract_basic() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
src = "mysrc"
out = "myout"
verbosity = 3
evm_version = 'berlin'
[profile.other]
src = "other-src"
"#,
)?;
let loaded = Config::load();
assert_eq!(loaded.evm_version, EvmVersion::Berlin);
let base = loaded.into_basic();
let default = Config::default();
assert_eq!(
base,
BasicConfig {
profile: Config::DEFAULT_PROFILE,
src: "mysrc".into(),
out: "myout".into(),
libs: default.libs.clone(),
remappings: default.remappings.clone(),
}
);
jail.set_env("FOUNDRY_PROFILE", r#"other"#);
let base = Config::figment().extract::<BasicConfig>().unwrap();
assert_eq!(
base,
BasicConfig {
profile: Config::DEFAULT_PROFILE,
src: "other-src".into(),
out: "myout".into(),
libs: default.libs.clone(),
remappings: default.remappings,
}
);
Ok(())
});
}
#[test]
#[should_panic]
fn test_parse_invalid_fuzz_weight() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[fuzz]
dictionary_weight = 101
"#,
)?;
let _config = Config::load();
Ok(())
});
}
#[test]
fn test_fallback_provider() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[fuzz]
runs = 1
include_storage = false
dictionary_weight = 99
[invariant]
runs = 420
[profile.ci.fuzz]
dictionary_weight = 5
[profile.ci.invariant]
runs = 400
"#,
)?;
let invariant_default = InvariantConfig::default();
let config = Config::load();
assert_ne!(config.invariant.runs, config.fuzz.runs);
assert_eq!(config.invariant.runs, 420);
assert_ne!(
config.fuzz.dictionary.include_storage,
invariant_default.dictionary.include_storage
);
assert_eq!(
config.invariant.dictionary.include_storage,
config.fuzz.dictionary.include_storage
);
assert_ne!(
config.fuzz.dictionary.dictionary_weight,
invariant_default.dictionary.dictionary_weight
);
assert_eq!(
config.invariant.dictionary.dictionary_weight,
config.fuzz.dictionary.dictionary_weight
);
jail.set_env("FOUNDRY_PROFILE", "ci");
let ci_config = Config::load();
assert_eq!(ci_config.fuzz.runs, 1);
assert_eq!(ci_config.invariant.runs, 400);
assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
assert_eq!(
ci_config.invariant.dictionary.dictionary_weight,
config.fuzz.dictionary.dictionary_weight
);
Ok(())
})
}
#[test]
fn test_standalone_profile_sections() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[fuzz]
runs = 100
[invariant]
runs = 120
[profile.ci.fuzz]
runs = 420
[profile.ci.invariant]
runs = 500
"#,
)?;
let config = Config::load();
assert_eq!(config.fuzz.runs, 100);
assert_eq!(config.invariant.runs, 120);
jail.set_env("FOUNDRY_PROFILE", "ci");
let config = Config::load();
assert_eq!(config.fuzz.runs, 420);
assert_eq!(config.invariant.runs, 500);
Ok(())
});
}
#[test]
fn can_handle_deviating_dapp_aliases() {
figment::Jail::expect_with(|jail| {
let addr = Address::random();
jail.set_env("DAPP_TEST_NUMBER", 1337);
jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
jail.set_env("DAPP_TEST_DEPTH", 20);
jail.set_env("DAPP_FORK_BLOCK", 100);
jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
let config = Config::load();
assert_eq!(config.block_number, 1337);
assert_eq!(config.sender, addr);
assert_eq!(config.fuzz.runs, 420);
assert_eq!(config.invariant.depth, 20);
assert_eq!(config.fork_block_number, Some(100));
assert_eq!(config.optimizer_runs, 999);
assert!(!config.optimizer);
Ok(())
});
}
#[test]
fn can_parse_libraries() {
figment::Jail::expect_with(|jail| {
jail.set_env(
"DAPP_LIBRARIES",
"[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
);
let config = Config::load();
assert_eq!(
config.libraries,
vec!["src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
.to_string()]
);
jail.set_env(
"DAPP_LIBRARIES",
"src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
);
let config = Config::load();
assert_eq!(
config.libraries,
vec!["src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
.to_string(),]
);
jail.set_env(
"DAPP_LIBRARIES",
"src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
);
let config = Config::load();
assert_eq!(
config.libraries,
vec![
"src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
.to_string(),
"src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
.to_string()
]
);
Ok(())
});
}
#[test]
fn test_parse_many_libraries() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
libraries= [
'./src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
'./src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
'./src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
'./src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
'./src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
]
"#,
)?;
let config = Config::load();
let libs = config.parsed_libraries().unwrap().libs;
pretty_assertions::assert_eq!(
libs,
BTreeMap::from([
(
PathBuf::from("./src/SizeAuctionDiscount.sol"),
BTreeMap::from([
(
"Chainlink".to_string(),
"0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
),
(
"Math".to_string(),
"0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
)
])
),
(
PathBuf::from("./src/SizeAuction.sol"),
BTreeMap::from([
(
"ChainlinkTWAP".to_string(),
"0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
),
(
"Math".to_string(),
"0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
)
])
),
(
PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
BTreeMap::from([(
"ChainlinkTWAP".to_string(),
"0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
)])
),
])
);
Ok(())
});
}
#[test]
fn config_roundtrip() {
figment::Jail::expect_with(|jail| {
let default = Config::default();
let basic = default.clone().into_basic();
jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
let mut other = Config::load();
clear_warning(&mut other);
assert_eq!(default, other);
let other = other.into_basic();
assert_eq!(basic, other);
jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
let mut other = Config::load();
clear_warning(&mut other);
assert_eq!(default, other);
Ok(())
});
}
#[test]
fn test_fs_permissions() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
fs_permissions = [{ access = "read-write", path = "./"}]
"#,
)?;
let loaded = Config::load();
assert_eq!(
loaded.fs_permissions,
FsPermissions::new(vec![PathPermission::read_write("./")])
);
jail.create_file(
"foundry.toml",
r#"
[profile.default]
fs_permissions = [{ access = "none", path = "./"}]
"#,
)?;
let loaded = Config::load();
assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
Ok(())
});
}
#[test]
fn test_optimizer_settings_basic() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
optimizer = true
[profile.default.optimizer_details]
yul = false
[profile.default.optimizer_details.yulDetails]
stackAllocation = true
"#,
)?;
let mut loaded = Config::load();
clear_warning(&mut loaded);
assert_eq!(
loaded.optimizer_details,
Some(OptimizerDetails {
yul: Some(false),
yul_details: Some(YulDetails {
stack_allocation: Some(true),
..Default::default()
}),
..Default::default()
})
);
let s = loaded.to_string_pretty().unwrap();
jail.create_file("foundry.toml", &s)?;
let mut reloaded = Config::load();
clear_warning(&mut reloaded);
assert_eq!(loaded, reloaded);
Ok(())
});
}
#[test]
fn test_model_checker_settings_basic() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
[profile.default.model_checker]
contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
engine = 'chc'
targets = [ 'assert', 'outOfBounds' ]
timeout = 10000
"#,
)?;
let mut loaded = Config::load();
clear_warning(&mut loaded);
assert_eq!(
loaded.model_checker,
Some(ModelCheckerSettings {
contracts: BTreeMap::from([
("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
]),
engine: Some(ModelCheckerEngine::CHC),
targets: Some(vec![
ModelCheckerTarget::Assert,
ModelCheckerTarget::OutOfBounds
]),
timeout: Some(10000),
invariants: None,
show_unproved: None,
div_mod_with_slacks: None,
solvers: None,
show_unsupported: None,
show_proved_safe: None,
})
);
let s = loaded.to_string_pretty().unwrap();
jail.create_file("foundry.toml", &s)?;
let mut reloaded = Config::load();
clear_warning(&mut reloaded);
assert_eq!(loaded, reloaded);
Ok(())
});
}
#[test]
fn test_model_checker_settings_relative_paths() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]
[profile.default.model_checker]
contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
engine = 'chc'
targets = [ 'assert', 'outOfBounds' ]
timeout = 10000
"#,
)?;
let loaded = Config::load().sanitized();
let dir = ethers_solc::utils::canonicalize(jail.directory())
.expect("Could not canonicalize jail path");
assert_eq!(
loaded.model_checker,
Some(ModelCheckerSettings {
contracts: BTreeMap::from([
(
format!("{}", dir.join("a.sol").display()),
vec!["A1".to_string(), "A2".to_string()]
),
(
format!("{}", dir.join("b.sol").display()),
vec!["B1".to_string(), "B2".to_string()]
),
]),
engine: Some(ModelCheckerEngine::CHC),
targets: Some(vec![
ModelCheckerTarget::Assert,
ModelCheckerTarget::OutOfBounds
]),
timeout: Some(10000),
invariants: None,
show_unproved: None,
div_mod_with_slacks: None,
solvers: None,
show_unsupported: None,
show_proved_safe: None,
})
);
Ok(())
});
}
#[test]
fn test_fmt_config() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[fmt]
line_length = 100
tab_width = 2
bracket_spacing = true
"#,
)?;
let loaded = Config::load().sanitized();
assert_eq!(
loaded.fmt,
FormatterConfig {
line_length: 100,
tab_width: 2,
bracket_spacing: true,
..Default::default()
}
);
Ok(())
});
}
#[test]
fn test_invariant_config() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[invariant]
runs = 512
depth = 10
"#,
)?;
let loaded = Config::load().sanitized();
assert_eq!(
loaded.invariant,
InvariantConfig { runs: 512, depth: 10, ..Default::default() }
);
Ok(())
});
}
#[test]
fn test_standalone_sections_env() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[fuzz]
runs = 100
[invariant]
depth = 1
"#,
)?;
jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
let config = Config::load();
assert_eq!(config.fmt.line_length, 95);
assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
assert_eq!(config.invariant.depth, 5);
Ok(())
});
}
#[test]
fn test_parse_with_profile() {
let foundry_str = r#"
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
"#;
assert_eq!(
parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
(
Config::DEFAULT_PROFILE,
BasicConfig {
profile: Config::DEFAULT_PROFILE,
src: "src".into(),
out: "out".into(),
libs: vec!["lib".into()],
remappings: vec![]
}
)
);
}
#[test]
fn test_implicit_profile_loads() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[default]
src = 'my-src'
out = 'my-out'
"#,
)?;
let loaded = Config::load().sanitized();
assert_eq!(loaded.src.file_name().unwrap(), "my-src");
assert_eq!(loaded.out.file_name().unwrap(), "my-out");
assert_eq!(
loaded.__warnings,
vec![Warning::UnknownSection {
unknown_section: Profile::new("default"),
source: Some("foundry.toml".into())
}]
);
Ok(())
});
}
#[test]
#[ignore]
fn print_config() {
let config = Config {
optimizer_details: Some(OptimizerDetails {
peephole: None,
inliner: None,
jumpdest_remover: None,
order_literals: None,
deduplicate: None,
cse: None,
constant_optimizer: Some(true),
yul: Some(true),
yul_details: Some(YulDetails {
stack_allocation: None,
optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
}),
}),
..Default::default()
};
println!("{}", config.to_string_pretty().unwrap());
}
#[test]
fn can_use_impl_figment_macro() {
#[derive(Default, Serialize)]
struct MyArgs {
#[serde(skip_serializing_if = "Option::is_none")]
root: Option<PathBuf>,
}
impl_figment_convert!(MyArgs);
impl Provider for MyArgs {
fn metadata(&self) -> Metadata {
Metadata::default()
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
let value = Value::serialize(self)?;
let error = InvalidType(value.to_actual(), "map".into());
let dict = value.into_dict().ok_or(error)?;
Ok(Map::from([(Config::selected_profile(), dict)]))
}
}
let _figment: Figment = From::from(&MyArgs::default());
let _config: Config = From::from(&MyArgs::default());
#[derive(Default)]
struct Outer {
start: MyArgs,
other: MyArgs,
another: MyArgs,
}
impl_figment_convert!(Outer, start, other, another);
let _figment: Figment = From::from(&Outer::default());
let _config: Config = From::from(&Outer::default());
}
#[test]
fn list_cached_blocks() -> eyre::Result<()> {
fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
let block_path = chain_path.join(block_number);
fs::create_dir(block_path.as_path()).unwrap();
let file_path = block_path.join("storage.json");
let mut file = File::create(file_path).unwrap();
writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
}
let chain_dir = tempdir()?;
fake_block_cache(chain_dir.path(), "1", 100);
fake_block_cache(chain_dir.path(), "2", 500);
let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
let result = Config::get_cached_blocks(chain_dir.path())?;
assert_eq!(result.len(), 2);
let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
assert_eq!(block1.0, "1");
assert_eq!(block1.1, 100);
assert_eq!(block2.0, "2");
assert_eq!(block2.1, 500);
chain_dir.close()?;
Ok(())
}
#[test]
fn list_etherscan_cache() -> eyre::Result<()> {
fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
let metadata_path = chain_path.join("sources");
let abi_path = chain_path.join("abi");
let _ = fs::create_dir(metadata_path.as_path());
let _ = fs::create_dir(abi_path.as_path());
let metadata_file_path = metadata_path.join(address);
let mut metadata_file = File::create(metadata_file_path).unwrap();
writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
.unwrap();
let abi_file_path = abi_path.join(address);
let mut abi_file = File::create(abi_file_path).unwrap();
writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
.unwrap();
}
let chain_dir = tempdir()?;
fake_etherscan_cache(chain_dir.path(), "1", 100);
fake_etherscan_cache(chain_dir.path(), "2", 500);
let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
assert_eq!(result, 600);
chain_dir.close()?;
Ok(())
}
#[test]
fn test_parse_error_codes() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[default]
ignored_error_codes = ["license", "unreachable", 1337]
"#,
)?;
let config = Config::load();
assert_eq!(
config.ignored_error_codes,
vec![
SolidityErrorCode::SpdxLicenseNotProvided,
SolidityErrorCode::Unreachable,
SolidityErrorCode::Other(1337)
]
);
Ok(())
});
}
#[test]
fn test_parse_optimizer_settings() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[default]
[profile.default.optimizer_details]
"#,
)?;
let config = Config::load();
assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
Ok(())
});
}
}