cyfrin_foundry_config/
lib.rs

1//! # cyfrin-foundry-config
2//!
3//! Foundry configuration.
4
5#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
7
8#[macro_use]
9extern crate tracing;
10
11use crate::cache::StorageCachingConfig;
12use alloy_primitives::{address, Address, B256, U256};
13use eyre::{ContextCompat, WrapErr};
14use figment::{
15    providers::{Env, Format, Serialized, Toml},
16    value::{Dict, Map, Value},
17    Error, Figment, Metadata, Profile, Provider,
18};
19use foundry_compilers::{
20    artifacts::{
21        output_selection::{ContractOutputSelection, OutputSelection},
22        serde_helpers, BytecodeHash, DebuggingSettings, EvmVersion, Libraries,
23        ModelCheckerSettings, ModelCheckerTarget, Optimizer, OptimizerDetails, RevertStrings,
24        Settings, SettingsMetadata, Severity,
25    },
26    cache::SOLIDITY_FILES_CACHE_FILENAME,
27    compilers::{
28        multi::{MultiCompiler, MultiCompilerSettings},
29        solc::{Solc, SolcCompiler},
30        vyper::{Vyper, VyperSettings},
31        Compiler,
32    },
33    error::SolcError,
34    ConfigurableArtifacts, Project, ProjectPathsConfig,
35};
36
37// cyfrin downstream need public access to these
38pub use foundry_compilers::artifacts::remappings::{RelativeRemapping, Remapping};
39
40use inflector::Inflector;
41use regex::Regex;
42use revm_primitives::{FixedBytes, SpecId};
43use semver::Version;
44use serde::{Deserialize, Deserializer, Serialize, Serializer};
45use std::{
46    borrow::Cow,
47    collections::HashMap,
48    fs,
49    path::{Path, PathBuf},
50    str::FromStr,
51};
52
53mod macros;
54
55pub mod utils;
56pub use utils::*;
57
58mod endpoints;
59pub use endpoints::{ResolvedRpcEndpoints, RpcEndpoint, RpcEndpoints};
60
61mod etherscan;
62use etherscan::{
63    EtherscanConfigError, EtherscanConfigs, EtherscanEnvProvider, ResolvedEtherscanConfig,
64};
65
66mod resolve;
67pub use resolve::UnresolvedEnvVarError;
68
69pub mod cache;
70use cache::{Cache, ChainCache};
71
72pub mod fmt;
73pub use fmt::FormatterConfig;
74
75pub mod fs_permissions;
76pub use fs_permissions::FsPermissions;
77use fs_permissions::PathPermission;
78
79pub mod error;
80use error::ExtractConfigError;
81pub use error::SolidityErrorCode;
82
83pub mod doc;
84pub use doc::DocConfig;
85
86pub mod filter;
87pub use filter::SkipBuildFilters;
88
89mod warning;
90pub use warning::*;
91
92pub mod fix;
93
94// reexport so cli types can implement `figment::Provider` to easily merge compiler arguments
95pub use alloy_chains::{Chain, NamedChain};
96pub use figment;
97
98pub mod providers;
99use providers::{remappings::RemappingsProvider, FallbackProfileProvider, WarningsProvider};
100
101mod fuzz;
102pub use fuzz::{FuzzConfig, FuzzDictionaryConfig};
103
104mod invariant;
105pub use invariant::InvariantConfig;
106
107mod inline;
108pub use inline::{validate_profiles, InlineConfig, InlineConfigError, InlineConfigParser, NatSpec};
109
110pub mod soldeer;
111use soldeer::SoldeerConfig;
112
113mod vyper;
114use vyper::VyperConfig;
115
116/// Foundry configuration
117///
118/// # Defaults
119///
120/// All configuration values have a default, documented in the [fields](#fields)
121/// section below. [`Config::default()`] returns the default values for
122/// the default profile while [`Config::with_root()`] returns the values based on the given
123/// directory. [`Config::load()`] starts with the default profile and merges various providers into
124/// the config, same for [`Config::load_with_root()`], but there the default values are determined
125/// by [`Config::with_root()`]
126///
127/// # Provider Details
128///
129/// `Config` is a Figment [`Provider`] with the following characteristics:
130///
131///   * **Profile**
132///
133///     The profile is set to the value of the `profile` field.
134///
135///   * **Metadata**
136///
137///     This provider is named `Foundry Config`. It does not specify a
138///     [`Source`](figment::Source) and uses default interpolation.
139///
140///   * **Data**
141///
142///     The data emitted by this provider are the keys and values corresponding
143///     to the fields and values of the structure. The dictionary is emitted to
144///     the "default" meta-profile.
145///
146/// Note that these behaviors differ from those of [`Config::figment()`].
147#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
148pub struct Config {
149    /// The selected profile. **(default: _default_ `default`)**
150    ///
151    /// **Note:** This field is never serialized nor deserialized. When a
152    /// `Config` is merged into a `Figment` as a `Provider`, this profile is
153    /// selected on the `Figment`. When a `Config` is extracted, this field is
154    /// set to the extracting Figment's selected `Profile`.
155    #[serde(skip)]
156    pub profile: Profile,
157    /// path of the source contracts dir, like `src` or `contracts`
158    pub src: PathBuf,
159    /// path of the test dir
160    pub test: PathBuf,
161    /// path of the script dir
162    pub script: PathBuf,
163    /// path to where artifacts shut be written to
164    pub out: PathBuf,
165    /// all library folders to include, `lib`, `node_modules`
166    pub libs: Vec<PathBuf>,
167    /// `Remappings` to use for this repo
168    pub remappings: Vec<RelativeRemapping>,
169    /// Whether to autodetect remappings by scanning the `libs` folders recursively
170    pub auto_detect_remappings: bool,
171    /// library addresses to link
172    pub libraries: Vec<String>,
173    /// whether to enable cache
174    pub cache: bool,
175    /// where the cache is stored if enabled
176    pub cache_path: PathBuf,
177    /// where the broadcast logs are stored
178    pub broadcast: PathBuf,
179    /// additional solc allow paths for `--allow-paths`
180    pub allow_paths: Vec<PathBuf>,
181    /// additional solc include paths for `--include-path`
182    pub include_paths: Vec<PathBuf>,
183    /// glob patterns to skip
184    #[serde(with = "from_vec_glob")]
185    pub skip: Vec<globset::Glob>,
186    /// whether to force a `project.clean()`
187    pub force: bool,
188    /// evm version to use
189    #[serde(with = "from_str_lowercase")]
190    pub evm_version: EvmVersion,
191    /// list of contracts to report gas of
192    pub gas_reports: Vec<String>,
193    /// list of contracts to ignore for gas reports
194    pub gas_reports_ignore: Vec<String>,
195    /// The Solc instance to use if any.
196    ///
197    /// This takes precedence over `auto_detect_solc`, if a version is set then this overrides
198    /// auto-detection.
199    ///
200    /// **Note** for backwards compatibility reasons this also accepts solc_version from the toml
201    /// file, see `BackwardsCompatTomlProvider`.
202    pub solc: Option<SolcReq>,
203    /// whether to autodetect the solc compiler version to use
204    pub auto_detect_solc: bool,
205    /// Offline mode, if set, network access (downloading solc) is disallowed.
206    ///
207    /// Relationship with `auto_detect_solc`:
208    ///    - if `auto_detect_solc = true` and `offline = true`, the required solc version(s) will
209    ///      be auto detected but if the solc version is not installed, it will _not_ try to
210    ///      install it
211    pub offline: bool,
212    /// Whether to activate optimizer
213    pub optimizer: bool,
214    /// Sets the optimizer runs
215    pub optimizer_runs: usize,
216    /// Switch optimizer components on or off in detail.
217    /// The "enabled" switch above provides two defaults which can be
218    /// tweaked here. If "details" is given, "enabled" can be omitted.
219    pub optimizer_details: Option<OptimizerDetails>,
220    /// Model checker settings.
221    pub model_checker: Option<ModelCheckerSettings>,
222    /// verbosity to use
223    pub verbosity: u8,
224    /// url of the rpc server that should be used for any rpc calls
225    pub eth_rpc_url: Option<String>,
226    /// JWT secret that should be used for any rpc calls
227    pub eth_rpc_jwt: Option<String>,
228    /// etherscan API key, or alias for an `EtherscanConfig` in `etherscan` table
229    pub etherscan_api_key: Option<String>,
230    /// Multiple etherscan api configs and their aliases
231    #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
232    pub etherscan: EtherscanConfigs,
233    /// list of solidity error codes to always silence in the compiler output
234    pub ignored_error_codes: Vec<SolidityErrorCode>,
235    /// list of file paths to ignore
236    #[serde(rename = "ignored_warnings_from")]
237    pub ignored_file_paths: Vec<PathBuf>,
238    /// When true, compiler warnings are treated as errors
239    pub deny_warnings: bool,
240    /// Only run test functions matching the specified regex pattern.
241    #[serde(rename = "match_test")]
242    pub test_pattern: Option<RegexWrapper>,
243    /// Only run test functions that do not match the specified regex pattern.
244    #[serde(rename = "no_match_test")]
245    pub test_pattern_inverse: Option<RegexWrapper>,
246    /// Only run tests in contracts matching the specified regex pattern.
247    #[serde(rename = "match_contract")]
248    pub contract_pattern: Option<RegexWrapper>,
249    /// Only run tests in contracts that do not match the specified regex pattern.
250    #[serde(rename = "no_match_contract")]
251    pub contract_pattern_inverse: Option<RegexWrapper>,
252    /// Only run tests in source files matching the specified glob pattern.
253    #[serde(rename = "match_path", with = "from_opt_glob")]
254    pub path_pattern: Option<globset::Glob>,
255    /// Only run tests in source files that do not match the specified glob pattern.
256    #[serde(rename = "no_match_path", with = "from_opt_glob")]
257    pub path_pattern_inverse: Option<globset::Glob>,
258    /// Configuration for fuzz testing
259    pub fuzz: FuzzConfig,
260    /// Configuration for invariant testing
261    pub invariant: InvariantConfig,
262    /// Whether to allow ffi cheatcodes in test
263    pub ffi: bool,
264    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
265    pub always_use_create_2_factory: bool,
266    /// Sets a timeout in seconds for vm.prompt cheatcodes
267    pub prompt_timeout: u64,
268    /// The address which will be executing all tests
269    pub sender: Address,
270    /// The tx.origin value during EVM execution
271    pub tx_origin: Address,
272    /// the initial balance of each deployed test contract
273    pub initial_balance: U256,
274    /// the block.number value during EVM execution
275    pub block_number: u64,
276    /// pins the block number for the state fork
277    pub fork_block_number: Option<u64>,
278    /// The chain name or EIP-155 chain ID.
279    #[serde(rename = "chain_id", alias = "chain")]
280    pub chain: Option<Chain>,
281    /// Block gas limit.
282    pub gas_limit: GasLimit,
283    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests.
284    pub code_size_limit: Option<usize>,
285    /// `tx.gasprice` value during EVM execution.
286    ///
287    /// This is an Option, so we can determine in fork mode whether to use the config's gas price
288    /// (if set by user) or the remote client's gas price.
289    pub gas_price: Option<u64>,
290    /// The base fee in a block.
291    pub block_base_fee_per_gas: u64,
292    /// The `block.coinbase` value during EVM execution.
293    pub block_coinbase: Address,
294    /// The `block.timestamp` value during EVM execution.
295    pub block_timestamp: u64,
296    /// The `block.difficulty` value during EVM execution.
297    pub block_difficulty: u64,
298    /// Before merge the `block.max_hash`, after merge it is `block.prevrandao`.
299    pub block_prevrandao: B256,
300    /// the `block.gaslimit` value during EVM execution
301    pub block_gas_limit: Option<GasLimit>,
302    /// The memory limit per EVM execution in bytes.
303    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
304    ///
305    /// The default is 128MiB.
306    pub memory_limit: u64,
307    /// Additional output selection for all contracts, such as "ir", "devdoc", "storageLayout",
308    /// etc.
309    ///
310    /// See the [Solc Compiler Api](https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-api) for more information.
311    ///
312    /// The following values are always set because they're required by `forge`:
313    /// ```json
314    /// {
315    ///   "*": [
316    ///       "abi",
317    ///       "evm.bytecode",
318    ///       "evm.deployedBytecode",
319    ///       "evm.methodIdentifiers"
320    ///     ]
321    /// }
322    /// ```
323    #[serde(default)]
324    pub extra_output: Vec<ContractOutputSelection>,
325    /// If set, a separate JSON file will be emitted for every contract depending on the
326    /// selection, eg. `extra_output_files = ["metadata"]` will create a `metadata.json` for
327    /// each contract in the project.
328    ///
329    /// See [Contract Metadata](https://docs.soliditylang.org/en/latest/metadata.html) for more information.
330    ///
331    /// The difference between `extra_output = ["metadata"]` and
332    /// `extra_output_files = ["metadata"]` is that the former will include the
333    /// contract's metadata in the contract's json artifact, whereas the latter will emit the
334    /// output selection as separate files.
335    #[serde(default)]
336    pub extra_output_files: Vec<ContractOutputSelection>,
337    /// Whether to print the names of the compiled contracts.
338    pub names: bool,
339    /// Whether to print the sizes of the compiled contracts.
340    pub sizes: bool,
341    /// If set to true, changes compilation pipeline to go through the Yul intermediate
342    /// representation.
343    pub via_ir: bool,
344    /// Whether to include the AST as JSON in the compiler output.
345    pub ast: bool,
346    /// RPC storage caching settings determines what chains and endpoints to cache
347    pub rpc_storage_caching: StorageCachingConfig,
348    /// Disables storage caching entirely. This overrides any settings made in
349    /// `rpc_storage_caching`
350    pub no_storage_caching: bool,
351    /// Disables rate limiting entirely. This overrides any settings made in
352    /// `compute_units_per_second`
353    pub no_rpc_rate_limit: bool,
354    /// Multiple rpc endpoints and their aliases
355    #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
356    pub rpc_endpoints: RpcEndpoints,
357    /// Whether to store the referenced sources in the metadata as literal data.
358    pub use_literal_content: bool,
359    /// Whether to include the metadata hash.
360    ///
361    /// The metadata hash is machine dependent. By default, this is set to [BytecodeHash::None] to allow for deterministic code, See: <https://docs.soliditylang.org/en/latest/metadata.html>
362    #[serde(with = "from_str_lowercase")]
363    pub bytecode_hash: BytecodeHash,
364    /// Whether to append the metadata hash to the bytecode.
365    ///
366    /// If this is `false` and the `bytecode_hash` option above is not `None` solc will issue a
367    /// warning.
368    pub cbor_metadata: bool,
369    /// How to treat revert (and require) reason strings.
370    #[serde(with = "serde_helpers::display_from_str_opt")]
371    pub revert_strings: Option<RevertStrings>,
372    /// Whether to compile in sparse mode
373    ///
374    /// If this option is enabled, only the required contracts/files will be selected to be
375    /// included in solc's output selection, see also [`OutputSelection`].
376    pub sparse_mode: bool,
377    /// Generates additional build info json files for every new build, containing the
378    /// `CompilerInput` and `CompilerOutput`.
379    pub build_info: bool,
380    /// The path to the `build-info` directory that contains the build info json files.
381    pub build_info_path: Option<PathBuf>,
382    /// Configuration for `forge fmt`
383    pub fmt: FormatterConfig,
384    /// Configuration for `forge doc`
385    pub doc: DocConfig,
386    /// Configures the permissions of cheat codes that touch the file system.
387    ///
388    /// This includes what operations can be executed (read, write)
389    pub fs_permissions: FsPermissions,
390
391    /// Temporary config to enable [SpecId::PRAGUE]
392    ///
393    /// Should be removed once EvmVersion Prague is supported by solc
394    pub prague: bool,
395
396    /// Whether to enable call isolation.
397    ///
398    /// Useful for more correct gas accounting and EVM behavior in general.
399    pub isolate: bool,
400
401    /// Whether to disable the block gas limit.
402    pub disable_block_gas_limit: bool,
403
404    /// Address labels
405    pub labels: HashMap<Address, String>,
406
407    /// Whether to enable safety checks for `vm.getCode` and `vm.getDeployedCode` invocations.
408    /// If disabled, it is possible to access artifacts which were not recompiled or cached.
409    pub unchecked_cheatcode_artifacts: bool,
410
411    /// CREATE2 salt to use for the library deployment in scripts.
412    pub create2_library_salt: B256,
413
414    /// Configuration for Vyper compiler
415    pub vyper: VyperConfig,
416
417    /// Soldeer dependencies
418    pub dependencies: Option<SoldeerConfig>,
419
420    /// The root path where the config detection started from, [`Config::with_root`].
421    // We're skipping serialization here, so it won't be included in the [`Config::to_string()`]
422    // representation, but will be deserialized from the `Figment` so that forge commands can
423    // override it.
424    #[serde(default, skip_serializing)]
425    pub root: RootPath,
426
427    /// Warnings gathered when loading the Config. See [`WarningsProvider`] for more information
428    #[serde(rename = "__warnings", default, skip_serializing)]
429    pub warnings: Vec<Warning>,
430
431    /// PRIVATE: This structure may grow, As such, constructing this structure should
432    /// _always_ be done using a public constructor or update syntax:
433    ///
434    /// ```ignore
435    /// use cyfrin_foundry_config::Config;
436    ///
437    /// let config = Config { src: "other".into(), ..Default::default() };
438    /// ```
439    #[doc(hidden)]
440    #[serde(skip)]
441    pub _non_exhaustive: (),
442}
443
444/// Mapping of fallback standalone sections. See [`FallbackProfileProvider`]
445pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
446
447/// Deprecated keys and their replacements.
448///
449/// See [Warning::DeprecatedKey]
450pub const DEPRECATIONS: &[(&str, &str)] = &[("cancun", "evm_version = Cancun")];
451
452impl Config {
453    /// The default profile: "default"
454    pub const DEFAULT_PROFILE: Profile = Profile::const_new("default");
455
456    /// The hardhat profile: "hardhat"
457    pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
458
459    /// TOML section for profiles
460    pub const PROFILE_SECTION: &'static str = "profile";
461
462    /// Standalone sections in the config which get integrated into the selected profile
463    pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
464        "rpc_endpoints",
465        "etherscan",
466        "fmt",
467        "doc",
468        "fuzz",
469        "invariant",
470        "labels",
471        "dependencies",
472        "vyper",
473    ];
474
475    /// File name of config toml file
476    pub const FILE_NAME: &'static str = "foundry.toml";
477
478    /// The name of the directory foundry reserves for itself under the user's home directory: `~`
479    pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
480
481    /// Default address for tx.origin
482    ///
483    /// `0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38`
484    pub const DEFAULT_SENDER: Address = address!("1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
485
486    /// Default salt for create2 library deployments
487    pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;
488
489    /// Returns the current `Config`
490    ///
491    /// See `Config::figment`
492    #[track_caller]
493    pub fn load() -> Self {
494        Self::from_provider(Self::figment())
495    }
496
497    /// Returns the current `Config` with the given `providers` preset
498    ///
499    /// See `Config::to_figment`
500    #[track_caller]
501    pub fn load_with_providers(providers: FigmentProviders) -> Self {
502        Self::default().to_figment(providers).extract().unwrap()
503    }
504
505    /// Returns the current `Config`
506    ///
507    /// See `Config::figment_with_root`
508    #[track_caller]
509    pub fn load_with_root(root: impl Into<PathBuf>) -> Self {
510        Self::from_provider(Self::figment_with_root(root))
511    }
512
513    /// Extract a `Config` from `provider`, panicking if extraction fails.
514    ///
515    /// # Panics
516    ///
517    /// If extraction fails, prints an error message indicating the failure and
518    /// panics. For a version that doesn't panic, use [`Config::try_from()`].
519    ///
520    /// # Example
521    ///
522    /// ```no_run
523    /// use figment::providers::{Env, Format, Toml};
524    /// use cyfrin_foundry_config::Config;
525    ///
526    /// // Use foundry's default `Figment`, but allow values from `other.toml`
527    /// // to supersede its values.
528    /// let figment = Config::figment().merge(Toml::file("other.toml").nested());
529    ///
530    /// let config = Config::from_provider(figment);
531    /// ```
532    #[track_caller]
533    pub fn from_provider<T: Provider>(provider: T) -> Self {
534        trace!("load config with provider: {:?}", provider.metadata());
535        Self::try_from(provider).unwrap_or_else(|err| panic!("{}", err))
536    }
537
538    /// Attempts to extract a `Config` from `provider`, returning the result.
539    ///
540    /// # Example
541    ///
542    /// ```rust
543    /// use figment::providers::{Env, Format, Toml};
544    /// use cyfrin_foundry_config::Config;
545    ///
546    /// // Use foundry's default `Figment`, but allow values from `other.toml`
547    /// // to supersede its values.
548    /// let figment = Config::figment().merge(Toml::file("other.toml").nested());
549    ///
550    /// let config = Config::try_from(figment);
551    /// ```
552    pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
553        let figment = Figment::from(provider);
554        let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
555        config.profile = figment.profile().clone();
556        Ok(config)
557    }
558
559    /// Returns the populated [Figment] using the requested [FigmentProviders] preset.
560    ///
561    /// This will merge various providers, such as env,toml,remappings into the figment.
562    pub fn to_figment(self, providers: FigmentProviders) -> Figment {
563        let mut c = self;
564        let profile = Self::selected_profile();
565        let mut figment = Figment::default().merge(DappHardhatDirProvider(&c.root.0));
566
567        // merge global foundry.toml file
568        if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
569            figment = Self::merge_toml_provider(
570                figment,
571                TomlFileProvider::new(None, global_toml).cached(),
572                profile.clone(),
573            );
574        }
575        // merge local foundry.toml file
576        figment = Self::merge_toml_provider(
577            figment,
578            TomlFileProvider::new(Some("FOUNDRY_CONFIG"), c.root.0.join(Self::FILE_NAME)).cached(),
579            profile.clone(),
580        );
581
582        // merge environment variables
583        figment = figment
584            .merge(
585                Env::prefixed("DAPP_")
586                    .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
587                    .global(),
588            )
589            .merge(
590                Env::prefixed("DAPP_TEST_")
591                    .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
592                    .global(),
593            )
594            .merge(DappEnvCompatProvider)
595            .merge(EtherscanEnvProvider::default())
596            .merge(
597                Env::prefixed("FOUNDRY_")
598                    .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
599                    .map(|key| {
600                        let key = key.as_str();
601                        if Self::STANDALONE_SECTIONS.iter().any(|section| {
602                            key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
603                        }) {
604                            key.replacen('_', ".", 1).into()
605                        } else {
606                            key.into()
607                        }
608                    })
609                    .global(),
610            )
611            .select(profile.clone());
612
613        // only resolve remappings if all providers are requested
614        if providers.is_all() {
615            // we try to merge remappings after we've merged all other providers, this prevents
616            // redundant fs lookups to determine the default remappings that are eventually updated
617            // by other providers, like the toml file
618            let remappings = RemappingsProvider {
619                auto_detect_remappings: figment
620                    .extract_inner::<bool>("auto_detect_remappings")
621                    .unwrap_or(true),
622                lib_paths: figment
623                    .extract_inner::<Vec<PathBuf>>("libs")
624                    .map(Cow::Owned)
625                    .unwrap_or_else(|_| Cow::Borrowed(&c.libs)),
626                root: &c.root.0,
627                remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
628            };
629            figment = figment.merge(remappings);
630        }
631
632        // normalize defaults
633        figment = c.normalize_defaults(figment);
634
635        Figment::from(c).merge(figment).select(profile)
636    }
637
638    /// The config supports relative paths and tracks the root path separately see
639    /// `Config::with_root`
640    ///
641    /// This joins all relative paths with the current root and attempts to make them canonic
642    #[must_use]
643    pub fn canonic(self) -> Self {
644        let root = self.root.0.clone();
645        self.canonic_at(root)
646    }
647
648    /// Joins all relative paths with the given root so that paths that are defined as:
649    ///
650    /// ```toml
651    /// [profile.default]
652    /// src = "src"
653    /// out = "./out"
654    /// libs = ["lib", "/var/lib"]
655    /// ```
656    ///
657    /// Will be made canonic with the given root:
658    ///
659    /// ```toml
660    /// [profile.default]
661    /// src = "<root>/src"
662    /// out = "<root>/out"
663    /// libs = ["<root>/lib", "/var/lib"]
664    /// ```
665    #[must_use]
666    pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
667        let root = canonic(root);
668
669        fn p(root: &Path, rem: &Path) -> PathBuf {
670            canonic(root.join(rem))
671        }
672
673        self.src = p(&root, &self.src);
674        self.test = p(&root, &self.test);
675        self.script = p(&root, &self.script);
676        self.out = p(&root, &self.out);
677        self.broadcast = p(&root, &self.broadcast);
678        self.cache_path = p(&root, &self.cache_path);
679
680        if let Some(build_info_path) = self.build_info_path {
681            self.build_info_path = Some(p(&root, &build_info_path));
682        }
683
684        self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
685
686        self.remappings =
687            self.remappings.into_iter().map(|r| RelativeRemapping::new(r.into(), &root)).collect();
688
689        self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
690
691        self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
692
693        self.fs_permissions.join_all(&root);
694
695        if let Some(ref mut model_checker) = self.model_checker {
696            model_checker.contracts = std::mem::take(&mut model_checker.contracts)
697                .into_iter()
698                .map(|(path, contracts)| {
699                    (format!("{}", p(&root, path.as_ref()).display()), contracts)
700                })
701                .collect();
702        }
703
704        self
705    }
706
707    /// Normalizes the evm version if a [SolcReq] is set
708    pub fn normalized_evm_version(mut self) -> Self {
709        self.normalize_evm_version();
710        self
711    }
712
713    /// Normalizes the evm version if a [SolcReq] is set to a valid version.
714    pub fn normalize_evm_version(&mut self) {
715        self.evm_version = self.get_normalized_evm_version();
716    }
717
718    /// Returns the normalized [EvmVersion] if a [SolcReq] is set to a valid version or if the solc
719    /// path is a valid solc binary.
720    ///
721    /// Otherwise it returns the configured [EvmVersion].
722    pub fn get_normalized_evm_version(&self) -> EvmVersion {
723        if let Some(version) = self.solc.as_ref().and_then(|solc| solc.try_version().ok()) {
724            if let Some(evm_version) = self.evm_version.normalize_version_solc(&version) {
725                return evm_version;
726            }
727        }
728        self.evm_version
729    }
730
731    /// Returns a sanitized version of the Config where are paths are set correctly and potential
732    /// duplicates are resolved
733    ///
734    /// See [`Self::canonic`]
735    #[must_use]
736    pub fn sanitized(self) -> Self {
737        let mut config = self.canonic();
738
739        config.sanitize_remappings();
740
741        config.libs.sort_unstable();
742        config.libs.dedup();
743
744        config
745    }
746
747    /// Cleans up any duplicate `Remapping` and sorts them
748    ///
749    /// On windows this will convert any `\` in the remapping path into a `/`
750    pub fn sanitize_remappings(&mut self) {
751        #[cfg(target_os = "windows")]
752        {
753            // force `/` in remappings on windows
754            use path_slash::PathBufExt;
755            self.remappings.iter_mut().for_each(|r| {
756                r.path.path = r.path.path.to_slash_lossy().into_owned().into();
757            });
758        }
759    }
760
761    /// Returns the directory in which dependencies should be installed
762    ///
763    /// Returns the first dir from `libs` that is not `node_modules` or `lib` if `libs` is empty
764    pub fn install_lib_dir(&self) -> &Path {
765        self.libs
766            .iter()
767            .find(|p| !p.ends_with("node_modules"))
768            .map(|p| p.as_path())
769            .unwrap_or_else(|| Path::new("lib"))
770    }
771
772    /// Serves as the entrypoint for obtaining the project.
773    ///
774    /// Returns the `Project` configured with all `solc` and path related values.
775    ///
776    /// *Note*: this also _cleans_ [`Project::cleanup`] the workspace if `force` is set to true.
777    ///
778    /// # Example
779    ///
780    /// ```
781    /// use cyfrin_foundry_config::Config;
782    /// let config = Config::load_with_root(".").sanitized();
783    /// let project = config.project();
784    /// ```
785    pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
786        self.create_project(self.cache, false)
787    }
788
789    /// Same as [`Self::project()`] but sets configures the project to not emit artifacts and ignore
790    /// cache.
791    pub fn ephemeral_no_artifacts_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
792        self.create_project(false, true)
793    }
794
795    /// Creates a [Project] with the given `cached` and `no_artifacts` flags
796    pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
797        let mut builder = Project::builder()
798            .artifacts(self.configured_artifacts_handler())
799            .paths(self.project_paths())
800            .settings(self.compiler_settings()?)
801            .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
802            .ignore_paths(self.ignored_file_paths.clone())
803            .set_compiler_severity_filter(if self.deny_warnings {
804                Severity::Warning
805            } else {
806                Severity::Error
807            })
808            .set_offline(self.offline)
809            .set_cached(cached)
810            .set_build_info(!no_artifacts && self.build_info)
811            .set_no_artifacts(no_artifacts);
812
813        if !self.skip.is_empty() {
814            let filter = SkipBuildFilters::new(self.skip.clone(), self.root.0.clone());
815            builder = builder.sparse_output(filter);
816        }
817
818        let project = builder.build(self.compiler()?)?;
819
820        if self.force {
821            self.cleanup(&project)?;
822        }
823
824        Ok(project)
825    }
826
827    /// Cleans the project.
828    pub fn cleanup<C: Compiler>(&self, project: &Project<C>) -> Result<(), SolcError> {
829        project.cleanup()?;
830
831        // Remove fuzz and invariant cache directories.
832        let remove_test_dir = |test_dir: &Option<PathBuf>| {
833            if let Some(test_dir) = test_dir {
834                let path = project.root().join(test_dir);
835                if path.exists() {
836                    let _ = fs::remove_dir_all(&path);
837                }
838            }
839        };
840        remove_test_dir(&self.fuzz.failure_persist_dir);
841        remove_test_dir(&self.invariant.failure_persist_dir);
842
843        Ok(())
844    }
845
846    /// Ensures that the configured version is installed if explicitly set
847    ///
848    /// If `solc` is [`SolcReq::Version`] then this will download and install the solc version if
849    /// it's missing, unless the `offline` flag is enabled, in which case an error is thrown.
850    ///
851    /// If `solc` is [`SolcReq::Local`] then this will ensure that the path exists.
852    fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
853        if let Some(ref solc) = self.solc {
854            let solc = match solc {
855                SolcReq::Version(version) => {
856                    if let Some(solc) = Solc::find_svm_installed_version(version)? {
857                        solc
858                    } else {
859                        if self.offline {
860                            return Err(SolcError::msg(format!(
861                                "can't install missing solc {version} in offline mode"
862                            )))
863                        }
864                        Solc::blocking_install(version)?
865                    }
866                }
867                SolcReq::Local(solc) => {
868                    if !solc.is_file() {
869                        return Err(SolcError::msg(format!(
870                            "`solc` {} does not exist",
871                            solc.display()
872                        )))
873                    }
874                    Solc::new(solc)?
875                }
876            };
877            return Ok(Some(solc))
878        }
879
880        Ok(None)
881    }
882
883    /// Returns the [SpecId] derived from the configured [EvmVersion]
884    #[inline]
885    pub fn evm_spec_id(&self) -> SpecId {
886        if self.prague {
887            return SpecId::PRAGUE
888        }
889        evm_spec_id(&self.evm_version)
890    }
891
892    /// Returns whether the compiler version should be auto-detected
893    ///
894    /// Returns `false` if `solc_version` is explicitly set, otherwise returns the value of
895    /// `auto_detect_solc`
896    pub fn is_auto_detect(&self) -> bool {
897        if self.solc.is_some() {
898            return false
899        }
900        self.auto_detect_solc
901    }
902
903    /// Whether caching should be enabled for the given chain id
904    pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
905        !self.no_storage_caching &&
906            self.rpc_storage_caching.enable_for_chain_id(chain_id.into()) &&
907            self.rpc_storage_caching.enable_for_endpoint(endpoint)
908    }
909
910    /// Returns the `ProjectPathsConfig` sub set of the config.
911    ///
912    /// **NOTE**: this uses the paths as they are and does __not__ modify them, see
913    /// `[Self::sanitized]`
914    ///
915    /// # Example
916    ///
917    /// ```
918    /// use foundry_compilers::solc::Solc;
919    /// use cyfrin_foundry_config::Config;
920    /// let config = Config::load_with_root(".").sanitized();
921    /// let paths = config.project_paths::<Solc>();
922    /// ```
923    pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
924        let mut builder = ProjectPathsConfig::builder()
925            .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
926            .sources(&self.src)
927            .tests(&self.test)
928            .scripts(&self.script)
929            .artifacts(&self.out)
930            .libs(self.libs.iter())
931            .remappings(self.get_all_remappings())
932            .allowed_path(&self.root.0)
933            .allowed_paths(&self.libs)
934            .allowed_paths(&self.allow_paths)
935            .include_paths(&self.include_paths);
936
937        if let Some(build_info_path) = &self.build_info_path {
938            builder = builder.build_infos(build_info_path);
939        }
940
941        builder.build_with_root(&self.root.0)
942    }
943
944    /// Returns configuration for a compiler to use when setting up a [Project].
945    pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
946        if let Some(solc) = self.ensure_solc()? {
947            Ok(SolcCompiler::Specific(solc))
948        } else {
949            Ok(SolcCompiler::AutoDetect)
950        }
951    }
952
953    /// Returns configured [Vyper] compiler.
954    pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
955        let vyper = if let Some(path) = &self.vyper.path {
956            Some(Vyper::new(path)?)
957        } else {
958            Vyper::new("vyper").ok()
959        };
960
961        Ok(vyper)
962    }
963
964    /// Returns configuration for a compiler to use when setting up a [Project].
965    pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
966        Ok(MultiCompiler { solc: self.solc_compiler()?, vyper: self.vyper_compiler()? })
967    }
968
969    /// Returns configured [MultiCompilerSettings].
970    pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
971        Ok(MultiCompilerSettings { solc: self.solc_settings()?, vyper: self.vyper_settings()? })
972    }
973
974    /// Returns all configured remappings.
975    ///
976    /// **Note:** this will add an additional `<src>/=<src path>` remapping here, see
977    /// [Self::get_source_dir_remapping()]
978    ///
979    /// So that
980    ///
981    /// ```solidity
982    /// import "./math/math.sol";
983    /// import "contracts/tokens/token.sol";
984    /// ```
985    ///
986    /// in `contracts/contract.sol` are resolved to
987    ///
988    /// ```text
989    /// contracts/tokens/token.sol
990    /// contracts/math/math.sol
991    /// ```
992    pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
993        self.remappings.iter().map(|m| m.clone().into())
994    }
995
996    /// Returns the configured rpc jwt secret
997    ///
998    /// Returns:
999    ///    - The jwt secret, if configured
1000    ///
1001    /// # Example
1002    ///
1003    /// ```
1004    /// use cyfrin_foundry_config::Config;
1005    /// # fn t() {
1006    /// let config = Config::with_root("./");
1007    /// let rpc_jwt = config.get_rpc_jwt_secret().unwrap().unwrap();
1008    /// # }
1009    /// ```
1010    pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
1011        Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
1012    }
1013
1014    /// Returns the configured rpc url
1015    ///
1016    /// Returns:
1017    ///    - the matching, resolved url of  `rpc_endpoints` if `eth_rpc_url` is an alias
1018    ///    - the `eth_rpc_url` as-is if it isn't an alias
1019    ///
1020    /// # Example
1021    ///
1022    /// ```
1023    /// use cyfrin_foundry_config::Config;
1024    /// # fn t() {
1025    /// let config = Config::with_root("./");
1026    /// let rpc_url = config.get_rpc_url().unwrap().unwrap();
1027    /// # }
1028    /// ```
1029    pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1030        let maybe_alias = self.eth_rpc_url.as_ref().or(self.etherscan_api_key.as_ref())?;
1031        if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
1032            Some(alias)
1033        } else {
1034            Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
1035        }
1036    }
1037
1038    /// Resolves the given alias to a matching rpc url
1039    ///
1040    /// Returns:
1041    ///    - the matching, resolved url of  `rpc_endpoints` if `maybe_alias` is an alias
1042    ///    - None otherwise
1043    ///
1044    /// # Example
1045    ///
1046    /// ```
1047    /// use cyfrin_foundry_config::Config;
1048    /// # fn t() {
1049    /// let config = Config::with_root("./");
1050    /// let rpc_url = config.get_rpc_url_with_alias("mainnet").unwrap().unwrap();
1051    /// # }
1052    /// ```
1053    pub fn get_rpc_url_with_alias(
1054        &self,
1055        maybe_alias: &str,
1056    ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1057        let mut endpoints = self.rpc_endpoints.clone().resolved();
1058        Some(endpoints.remove(maybe_alias)?.map(Cow::Owned))
1059    }
1060
1061    /// Returns the configured rpc, or the fallback url
1062    ///
1063    /// # Example
1064    ///
1065    /// ```
1066    /// use cyfrin_foundry_config::Config;
1067    /// # fn t() {
1068    /// let config = Config::with_root("./");
1069    /// let rpc_url = config.get_rpc_url_or("http://localhost:8545").unwrap();
1070    /// # }
1071    /// ```
1072    pub fn get_rpc_url_or<'a>(
1073        &'a self,
1074        fallback: impl Into<Cow<'a, str>>,
1075    ) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1076        if let Some(url) = self.get_rpc_url() {
1077            url
1078        } else {
1079            Ok(fallback.into())
1080        }
1081    }
1082
1083    /// Returns the configured rpc or `"http://localhost:8545"` if no `eth_rpc_url` is set
1084    ///
1085    /// # Example
1086    ///
1087    /// ```
1088    /// use cyfrin_foundry_config::Config;
1089    /// # fn t() {
1090    /// let config = Config::with_root("./");
1091    /// let rpc_url = config.get_rpc_url_or_localhost_http().unwrap();
1092    /// # }
1093    /// ```
1094    pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1095        self.get_rpc_url_or("http://localhost:8545")
1096    }
1097
1098    /// Returns the `EtherscanConfig` to use, if any
1099    ///
1100    /// Returns
1101    ///  - the matching `ResolvedEtherscanConfig` of the `etherscan` table if `etherscan_api_key` is
1102    ///    an alias
1103    ///  - the matching `ResolvedEtherscanConfig` of the `etherscan` table if a `chain` is
1104    ///    configured. an alias
1105    ///  - the Mainnet  `ResolvedEtherscanConfig` if `etherscan_api_key` is set, `None` otherwise
1106    ///
1107    /// # Example
1108    ///
1109    /// ```
1110    /// use cyfrin_foundry_config::Config;
1111    /// # fn t() {
1112    /// let config = Config::with_root("./");
1113    /// let etherscan_config = config.get_etherscan_config().unwrap().unwrap();
1114    /// let client = etherscan_config.into_client().unwrap();
1115    /// # }
1116    /// ```
1117    pub fn get_etherscan_config(
1118        &self,
1119    ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
1120        self.get_etherscan_config_with_chain(None).transpose()
1121    }
1122
1123    /// Same as [`Self::get_etherscan_config()`] but optionally updates the config with the given
1124    /// `chain`, and `etherscan_api_key`
1125    ///
1126    /// If not matching alias was found, then this will try to find the first entry in the table
1127    /// with a matching chain id. If an etherscan_api_key is already set it will take precedence
1128    /// over the chain's entry in the table.
1129    pub fn get_etherscan_config_with_chain(
1130        &self,
1131        chain: Option<Chain>,
1132    ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
1133        if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref()) {
1134            if self.etherscan.contains_key(maybe_alias) {
1135                return self.etherscan.clone().resolved().remove(maybe_alias).transpose()
1136            }
1137        }
1138
1139        // try to find by comparing chain IDs after resolving
1140        if let Some(res) = chain
1141            .or(self.chain)
1142            .and_then(|chain| self.etherscan.clone().resolved().find_chain(chain))
1143        {
1144            match (res, self.etherscan_api_key.as_ref()) {
1145                (Ok(mut config), Some(key)) => {
1146                    // we update the key, because if an etherscan_api_key is set, it should take
1147                    // precedence over the entry, since this is usually set via env var or CLI args.
1148                    config.key.clone_from(key);
1149                    return Ok(Some(config))
1150                }
1151                (Ok(config), None) => return Ok(Some(config)),
1152                (Err(err), None) => return Err(err),
1153                (Err(_), Some(_)) => {
1154                    // use the etherscan key as fallback
1155                }
1156            }
1157        }
1158
1159        // etherscan fallback via API key
1160        if let Some(key) = self.etherscan_api_key.as_ref() {
1161            let chain = chain.or(self.chain).unwrap_or_default();
1162            return Ok(ResolvedEtherscanConfig::create(key, chain))
1163        }
1164
1165        Ok(None)
1166    }
1167
1168    /// Helper function to just get the API key
1169    ///
1170    /// Optionally updates the config with the given `chain`.
1171    ///
1172    /// See also [Self::get_etherscan_config_with_chain]
1173    pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
1174        self.get_etherscan_config_with_chain(chain).ok().flatten().map(|c| c.key)
1175    }
1176
1177    /// Returns the remapping for the project's _src_ directory
1178    ///
1179    /// **Note:** this will add an additional `<src>/=<src path>` remapping here so imports that
1180    /// look like `import {Foo} from "src/Foo.sol";` are properly resolved.
1181    ///
1182    /// This is due the fact that `solc`'s VFS resolves [direct imports](https://docs.soliditylang.org/en/develop/path-resolution.html#direct-imports) that start with the source directory's name.
1183    pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
1184        get_dir_remapping(&self.src)
1185    }
1186
1187    /// Returns the remapping for the project's _test_ directory, but only if it exists
1188    pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
1189        if self.root.0.join(&self.test).exists() {
1190            get_dir_remapping(&self.test)
1191        } else {
1192            None
1193        }
1194    }
1195
1196    /// Returns the remapping for the project's _script_ directory, but only if it exists
1197    pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
1198        if self.root.0.join(&self.script).exists() {
1199            get_dir_remapping(&self.script)
1200        } else {
1201            None
1202        }
1203    }
1204
1205    /// Returns the `Optimizer` based on the configured settings
1206    ///
1207    /// Note: optimizer details can be set independently of `enabled`
1208    /// See also: <https://github.com/foundry-rs/foundry/issues/7689>
1209    /// and  <https://github.com/ethereum/solidity/blob/bbb7f58be026fdc51b0b4694a6f25c22a1425586/docs/using-the-compiler.rst?plain=1#L293-L294>
1210    pub fn optimizer(&self) -> Optimizer {
1211        Optimizer {
1212            enabled: Some(self.optimizer),
1213            runs: Some(self.optimizer_runs),
1214            // we always set the details because `enabled` is effectively a specific details profile
1215            // that can still be modified
1216            details: self.optimizer_details.clone(),
1217        }
1218    }
1219
1220    /// returns the [`foundry_compilers::ConfigurableArtifacts`] for this config, that includes the
1221    /// `extra_output` fields
1222    pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
1223        let mut extra_output = self.extra_output.clone();
1224
1225        // Sourcify verification requires solc metadata output. Since, it doesn't
1226        // affect the UX & performance of the compiler, output the metadata files
1227        // by default.
1228        // For more info see: <https://github.com/foundry-rs/foundry/issues/2795>
1229        // Metadata is not emitted as separate file because this breaks typechain support: <https://github.com/foundry-rs/foundry/issues/2969>
1230        if !extra_output.contains(&ContractOutputSelection::Metadata) {
1231            extra_output.push(ContractOutputSelection::Metadata);
1232        }
1233
1234        ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().cloned())
1235    }
1236
1237    /// Parses all libraries in the form of
1238    /// `<file>:<lib>:<addr>`
1239    pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
1240        Libraries::parse(&self.libraries)
1241    }
1242
1243    /// Returns all libraries with applied remappings. Same as `self.solc_settings()?.libraries`.
1244    pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
1245        let paths: ProjectPathsConfig = self.project_paths();
1246        Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
1247    }
1248
1249    /// Returns the configured `solc` `Settings` that includes:
1250    /// - all libraries
1251    /// - the optimizer (including details, if configured)
1252    /// - evm version
1253    pub fn solc_settings(&self) -> Result<Settings, SolcError> {
1254        // By default if no targets are specifically selected the model checker uses all targets.
1255        // This might be too much here, so only enable assertion checks.
1256        // If users wish to enable all options they need to do so explicitly.
1257        let mut model_checker = self.model_checker.clone();
1258        if let Some(model_checker_settings) = &mut model_checker {
1259            if model_checker_settings.targets.is_none() {
1260                model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
1261            }
1262        }
1263
1264        let mut settings = Settings {
1265            libraries: self.libraries_with_remappings()?,
1266            optimizer: self.optimizer(),
1267            evm_version: Some(self.evm_version),
1268            metadata: Some(SettingsMetadata {
1269                use_literal_content: Some(self.use_literal_content),
1270                bytecode_hash: Some(self.bytecode_hash),
1271                cbor_metadata: Some(self.cbor_metadata),
1272            }),
1273            debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
1274                revert_strings: Some(revert_strings),
1275                // Not used.
1276                debug_info: Vec::new(),
1277            }),
1278            model_checker,
1279            via_ir: Some(self.via_ir),
1280            // Not used.
1281            stop_after: None,
1282            // Set in project paths.
1283            remappings: Vec::new(),
1284            // Set with `with_extra_output` below.
1285            output_selection: Default::default(),
1286        }
1287        .with_extra_output(self.configured_artifacts_handler().output_selection());
1288
1289        // We're keeping AST in `--build-info` for backwards compatibility with HardHat.
1290        if self.ast || self.build_info {
1291            settings = settings.with_ast();
1292        }
1293
1294        Ok(settings)
1295    }
1296
1297    /// Returns the configured [VyperSettings] that includes:
1298    /// - evm version
1299    pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
1300        Ok(VyperSettings {
1301            evm_version: Some(self.evm_version),
1302            optimize: self.vyper.optimize,
1303            bytecode_metadata: None,
1304            // TODO: We don't yet have a way to deserialize other outputs correctly, so request only
1305            // those for now. It should be enough to run tests and deploy contracts.
1306            output_selection: OutputSelection::common_output_selection([
1307                "abi".to_string(),
1308                "evm.bytecode".to_string(),
1309                "evm.deployedBytecode".to_string(),
1310            ]),
1311        })
1312    }
1313
1314    /// Returns the default figment
1315    ///
1316    /// The default figment reads from the following sources, in ascending
1317    /// priority order:
1318    ///
1319    ///   1. [`Config::default()`] (see [defaults](#defaults))
1320    ///   2. `foundry.toml` _or_ filename in `FOUNDRY_CONFIG` environment variable
1321    ///   3. `FOUNDRY_` prefixed environment variables
1322    ///
1323    /// The profile selected is the value set in the `FOUNDRY_PROFILE`
1324    /// environment variable. If it is not set, it defaults to `default`.
1325    ///
1326    /// # Example
1327    ///
1328    /// ```rust
1329    /// use cyfrin_foundry_config::Config;
1330    /// use serde::Deserialize;
1331    ///
1332    /// let my_config = Config::figment().extract::<Config>();
1333    /// ```
1334    pub fn figment() -> Figment {
1335        Self::default().into()
1336    }
1337
1338    /// Returns the default figment enhanced with additional context extracted from the provided
1339    /// root, like remappings and directories.
1340    ///
1341    /// # Example
1342    ///
1343    /// ```rust
1344    /// use cyfrin_foundry_config::Config;
1345    /// use serde::Deserialize;
1346    ///
1347    /// let my_config = Config::figment_with_root(".").extract::<Config>();
1348    /// ```
1349    pub fn figment_with_root(root: impl Into<PathBuf>) -> Figment {
1350        Self::with_root(root).into()
1351    }
1352
1353    /// Creates a new Config that adds additional context extracted from the provided root.
1354    ///
1355    /// # Example
1356    ///
1357    /// ```rust
1358    /// use cyfrin_foundry_config::Config;
1359    /// let my_config = Config::with_root(".");
1360    /// ```
1361    pub fn with_root(root: impl Into<PathBuf>) -> Self {
1362        // autodetect paths
1363        let root = root.into();
1364        let paths = ProjectPathsConfig::builder().build_with_root::<()>(&root);
1365        let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
1366        Self {
1367            root: paths.root.into(),
1368            src: paths.sources.file_name().unwrap().into(),
1369            out: artifacts.clone(),
1370            libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(),
1371            remappings: paths
1372                .remappings
1373                .into_iter()
1374                .map(|r| RelativeRemapping::new(r, &root))
1375                .collect(),
1376            fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
1377            ..Self::default()
1378        }
1379    }
1380
1381    /// Returns the default config but with hardhat paths
1382    pub fn hardhat() -> Self {
1383        Self {
1384            src: "contracts".into(),
1385            out: "artifacts".into(),
1386            libs: vec!["node_modules".into()],
1387            ..Self::default()
1388        }
1389    }
1390
1391    /// Returns the default config that uses dapptools style paths
1392    pub fn dapptools() -> Self {
1393        Self {
1394            chain: Some(Chain::from_id(99)),
1395            block_timestamp: 0,
1396            block_number: 0,
1397            ..Self::default()
1398        }
1399    }
1400
1401    /// Extracts a basic subset of the config, used for initialisations.
1402    ///
1403    /// # Example
1404    ///
1405    /// ```rust
1406    /// use cyfrin_foundry_config::Config;
1407    /// let my_config = Config::with_root(".").into_basic();
1408    /// ```
1409    pub fn into_basic(self) -> BasicConfig {
1410        BasicConfig {
1411            profile: self.profile,
1412            src: self.src,
1413            out: self.out,
1414            libs: self.libs,
1415            remappings: self.remappings,
1416        }
1417    }
1418
1419    /// Updates the `foundry.toml` file for the given `root` based on the provided closure.
1420    ///
1421    /// **Note:** the closure will only be invoked if the `foundry.toml` file exists, See
1422    /// [Self::get_config_path()] and if the closure returns `true`.
1423    pub fn update_at<F>(root: impl Into<PathBuf>, f: F) -> eyre::Result<()>
1424    where
1425        F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
1426    {
1427        let config = Self::load_with_root(root).sanitized();
1428        config.update(|doc| f(&config, doc))
1429    }
1430
1431    /// Updates the `foundry.toml` file this `Config` ias based on with the provided closure.
1432    ///
1433    /// **Note:** the closure will only be invoked if the `foundry.toml` file exists, See
1434    /// [Self::get_config_path()] and if the closure returns `true`
1435    pub fn update<F>(&self, f: F) -> eyre::Result<()>
1436    where
1437        F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
1438    {
1439        let file_path = self.get_config_path();
1440        if !file_path.exists() {
1441            return Ok(())
1442        }
1443        let contents = fs::read_to_string(&file_path)?;
1444        let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
1445        if f(&mut doc) {
1446            fs::write(file_path, doc.to_string())?;
1447        }
1448        Ok(())
1449    }
1450
1451    /// Sets the `libs` entry inside a `foundry.toml` file but only if it exists
1452    ///
1453    /// # Errors
1454    ///
1455    /// An error if the `foundry.toml` could not be parsed.
1456    pub fn update_libs(&self) -> eyre::Result<()> {
1457        self.update(|doc| {
1458            let profile = self.profile.as_str().as_str();
1459            let root = &self.root.0;
1460            let libs: toml_edit::Value = self
1461                .libs
1462                .iter()
1463                .map(|path| {
1464                    let path =
1465                        if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
1466                    toml_edit::Value::from(&*path.to_string_lossy())
1467                })
1468                .collect();
1469            let libs = toml_edit::value(libs);
1470            doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
1471            true
1472        })
1473    }
1474
1475    /// Serialize the config type as a String of TOML.
1476    ///
1477    /// This serializes to a table with the name of the profile
1478    ///
1479    /// ```toml
1480    /// [profile.default]
1481    /// src = "src"
1482    /// out = "out"
1483    /// libs = ["lib"]
1484    /// # ...
1485    /// ```
1486    pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
1487        // serializing to value first to prevent `ValueAfterTable` errors
1488        let mut value = toml::Value::try_from(self)?;
1489        // Config map always gets serialized as a table
1490        let value_table = value.as_table_mut().unwrap();
1491        // remove standalone sections from inner table
1492        let standalone_sections = Self::STANDALONE_SECTIONS
1493            .iter()
1494            .filter_map(|section| {
1495                let section = section.to_string();
1496                value_table.remove(&section).map(|value| (section, value))
1497            })
1498            .collect::<Vec<_>>();
1499        // wrap inner table in [profile.<profile>]
1500        let mut wrapping_table = [(
1501            Self::PROFILE_SECTION.into(),
1502            toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
1503        )]
1504        .into_iter()
1505        .collect::<toml::map::Map<_, _>>();
1506        // insert standalone sections
1507        for (section, value) in standalone_sections {
1508            wrapping_table.insert(section, value);
1509        }
1510        // stringify
1511        toml::to_string_pretty(&toml::Value::Table(wrapping_table))
1512    }
1513
1514    /// Returns the path to the `foundry.toml` of this `Config`.
1515    pub fn get_config_path(&self) -> PathBuf {
1516        self.root.0.join(Self::FILE_NAME)
1517    }
1518
1519    /// Returns the selected profile.
1520    ///
1521    /// If the `FOUNDRY_PROFILE` env variable is not set, this returns the `DEFAULT_PROFILE`.
1522    pub fn selected_profile() -> Profile {
1523        Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
1524    }
1525
1526    /// Returns the path to foundry's global TOML file: `~/.foundry/foundry.toml`.
1527    pub fn foundry_dir_toml() -> Option<PathBuf> {
1528        Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
1529    }
1530
1531    /// Returns the path to foundry's config dir: `~/.foundry/`.
1532    pub fn foundry_dir() -> Option<PathBuf> {
1533        dirs_next::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
1534    }
1535
1536    /// Returns the path to foundry's cache dir: `~/.foundry/cache`.
1537    pub fn foundry_cache_dir() -> Option<PathBuf> {
1538        Self::foundry_dir().map(|p| p.join("cache"))
1539    }
1540
1541    /// Returns the path to foundry rpc cache dir: `~/.foundry/cache/rpc`.
1542    pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
1543        Some(Self::foundry_cache_dir()?.join("rpc"))
1544    }
1545    /// Returns the path to foundry chain's cache dir: `~/.foundry/cache/rpc/<chain>`
1546    pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1547        Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
1548    }
1549
1550    /// Returns the path to foundry's etherscan cache dir: `~/.foundry/cache/etherscan`.
1551    pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
1552        Some(Self::foundry_cache_dir()?.join("etherscan"))
1553    }
1554
1555    /// Returns the path to foundry's keystores dir: `~/.foundry/keystores`.
1556    pub fn foundry_keystores_dir() -> Option<PathBuf> {
1557        Some(Self::foundry_dir()?.join("keystores"))
1558    }
1559
1560    /// Returns the path to foundry's etherscan cache dir for `chain_id`:
1561    /// `~/.foundry/cache/etherscan/<chain>`
1562    pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1563        Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
1564    }
1565
1566    /// Returns the path to the cache dir of the `block` on the `chain`:
1567    /// `~/.foundry/cache/rpc/<chain>/<block>`
1568    pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1569        Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
1570    }
1571
1572    /// Returns the path to the cache file of the `block` on the `chain`:
1573    /// `~/.foundry/cache/rpc/<chain>/<block>/storage.json`
1574    pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1575        Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
1576    }
1577
1578    /// Returns the path to `foundry`'s data directory inside the user's data directory.
1579    ///
1580    /// | Platform | Value                                         | Example                                          |
1581    /// | -------  | --------------------------------------------- | ------------------------------------------------ |
1582    /// | Linux    | `$XDG_CONFIG_HOME` or `$HOME`/.config/foundry | /home/alice/.config/foundry                      |
1583    /// | macOS    | `$HOME`/Library/Application Support/foundry   | /Users/Alice/Library/Application Support/foundry |
1584    /// | Windows  | `{FOLDERID_RoamingAppData}/foundry`           | C:\Users\Alice\AppData\Roaming/foundry           |
1585    pub fn data_dir() -> eyre::Result<PathBuf> {
1586        let path = dirs_next::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
1587        std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
1588        Ok(path)
1589    }
1590
1591    /// Returns the path to the `foundry.toml` file, the file is searched for in
1592    /// the current working directory and all parent directories until the root,
1593    /// and the first hit is used.
1594    ///
1595    /// If this search comes up empty, then it checks if a global `foundry.toml` exists at
1596    /// `~/.foundry/foundry.toml`, see [`Self::foundry_dir_toml`].
1597    pub fn find_config_file() -> Option<PathBuf> {
1598        fn find(path: &Path) -> Option<PathBuf> {
1599            if path.is_absolute() {
1600                return match path.is_file() {
1601                    true => Some(path.to_path_buf()),
1602                    false => None,
1603                }
1604            }
1605            let cwd = std::env::current_dir().ok()?;
1606            let mut cwd = cwd.as_path();
1607            loop {
1608                let file_path = cwd.join(path);
1609                if file_path.is_file() {
1610                    return Some(file_path)
1611                }
1612                cwd = cwd.parent()?;
1613            }
1614        }
1615        find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
1616            .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
1617    }
1618
1619    /// Clears the foundry cache.
1620    pub fn clean_foundry_cache() -> eyre::Result<()> {
1621        if let Some(cache_dir) = Self::foundry_cache_dir() {
1622            let path = cache_dir.as_path();
1623            let _ = fs::remove_dir_all(path);
1624        } else {
1625            eyre::bail!("failed to get foundry_cache_dir");
1626        }
1627
1628        Ok(())
1629    }
1630
1631    /// Clears the foundry cache for `chain`.
1632    pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<()> {
1633        if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
1634            let path = cache_dir.as_path();
1635            let _ = fs::remove_dir_all(path);
1636        } else {
1637            eyre::bail!("failed to get foundry_chain_cache_dir");
1638        }
1639
1640        Ok(())
1641    }
1642
1643    /// Clears the foundry cache for `chain` and `block`.
1644    pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<()> {
1645        if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
1646            let path = cache_dir.as_path();
1647            let _ = fs::remove_dir_all(path);
1648        } else {
1649            eyre::bail!("failed to get foundry_block_cache_dir");
1650        }
1651
1652        Ok(())
1653    }
1654
1655    /// Clears the foundry etherscan cache.
1656    pub fn clean_foundry_etherscan_cache() -> eyre::Result<()> {
1657        if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
1658            let path = cache_dir.as_path();
1659            let _ = fs::remove_dir_all(path);
1660        } else {
1661            eyre::bail!("failed to get foundry_etherscan_cache_dir");
1662        }
1663
1664        Ok(())
1665    }
1666
1667    /// Clears the foundry etherscan cache for `chain`.
1668    pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<()> {
1669        if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
1670            let path = cache_dir.as_path();
1671            let _ = fs::remove_dir_all(path);
1672        } else {
1673            eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
1674        }
1675
1676        Ok(())
1677    }
1678
1679    /// List the data in the foundry cache.
1680    pub fn list_foundry_cache() -> eyre::Result<Cache> {
1681        if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
1682            let mut cache = Cache { chains: vec![] };
1683            if !cache_dir.exists() {
1684                return Ok(cache)
1685            }
1686            if let Ok(entries) = cache_dir.as_path().read_dir() {
1687                for entry in entries.flatten().filter(|x| x.path().is_dir()) {
1688                    match Chain::from_str(&entry.file_name().to_string_lossy()) {
1689                        Ok(chain) => cache.chains.push(Self::list_foundry_chain_cache(chain)?),
1690                        Err(_) => continue,
1691                    }
1692                }
1693                Ok(cache)
1694            } else {
1695                eyre::bail!("failed to access foundry_cache_dir");
1696            }
1697        } else {
1698            eyre::bail!("failed to get foundry_cache_dir");
1699        }
1700    }
1701
1702    /// List the cached data for `chain`.
1703    pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
1704        let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
1705            Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
1706            None => {
1707                warn!("failed to access foundry_etherscan_chain_cache_dir");
1708                0
1709            }
1710        };
1711
1712        if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
1713            let blocks = Self::get_cached_blocks(&cache_dir)?;
1714            Ok(ChainCache {
1715                name: chain.to_string(),
1716                blocks,
1717                block_explorer: block_explorer_data_size,
1718            })
1719        } else {
1720            eyre::bail!("failed to get foundry_chain_cache_dir");
1721        }
1722    }
1723
1724    /// The path provided to this function should point to a cached chain folder.
1725    fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
1726        let mut blocks = vec![];
1727        if !chain_path.exists() {
1728            return Ok(blocks)
1729        }
1730        for block in chain_path.read_dir()?.flatten() {
1731            let file_type = block.file_type()?;
1732            let file_name = block.file_name();
1733            let filepath = if file_type.is_dir() {
1734                block.path().join("storage.json")
1735            } else if file_type.is_file() &&
1736                file_name.to_string_lossy().chars().all(char::is_numeric)
1737            {
1738                block.path()
1739            } else {
1740                continue
1741            };
1742            blocks.push((file_name.to_string_lossy().into_owned(), fs::metadata(filepath)?.len()));
1743        }
1744        Ok(blocks)
1745    }
1746
1747    /// The path provided to this function should point to the etherscan cache for a chain.
1748    fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
1749        if !chain_path.exists() {
1750            return Ok(0)
1751        }
1752
1753        fn dir_size_recursive(mut dir: fs::ReadDir) -> eyre::Result<u64> {
1754            dir.try_fold(0, |acc, file| {
1755                let file = file?;
1756                let size = match file.metadata()? {
1757                    data if data.is_dir() => dir_size_recursive(fs::read_dir(file.path())?)?,
1758                    data => data.len(),
1759                };
1760                Ok(acc + size)
1761            })
1762        }
1763
1764        dir_size_recursive(fs::read_dir(chain_path)?)
1765    }
1766
1767    fn merge_toml_provider(
1768        mut figment: Figment,
1769        toml_provider: impl Provider,
1770        profile: Profile,
1771    ) -> Figment {
1772        figment = figment.select(profile.clone());
1773
1774        // add warnings
1775        figment = {
1776            let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
1777            figment.merge(warnings)
1778        };
1779
1780        // use [profile.<profile>] as [<profile>]
1781        let mut profiles = vec![Self::DEFAULT_PROFILE];
1782        if profile != Self::DEFAULT_PROFILE {
1783            profiles.push(profile.clone());
1784        }
1785        let provider = toml_provider.strict_select(profiles);
1786
1787        // apply any key fixes
1788        let provider = BackwardsCompatTomlProvider(ForcedSnakeCaseData(provider));
1789
1790        // merge the default profile as a base
1791        if profile != Self::DEFAULT_PROFILE {
1792            figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
1793        }
1794        // merge special keys into config
1795        for standalone_key in Self::STANDALONE_SECTIONS {
1796            if let Some((_, fallback)) =
1797                STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
1798            {
1799                figment = figment.merge(
1800                    provider
1801                        .fallback(standalone_key, fallback)
1802                        .wrap(profile.clone(), standalone_key),
1803                );
1804            } else {
1805                figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
1806            }
1807        }
1808        // merge the profile
1809        figment = figment.merge(provider);
1810        figment
1811    }
1812
1813    /// Check if any defaults need to be normalized.
1814    ///
1815    /// This normalizes the default `evm_version` if a `solc` was provided in the config.
1816    ///
1817    /// See also <https://github.com/foundry-rs/foundry/issues/7014>
1818    fn normalize_defaults(&mut self, figment: Figment) -> Figment {
1819        if let Ok(solc) = figment.extract_inner::<SolcReq>("solc") {
1820            // check if evm_version is set
1821            // TODO: add a warning if evm_version is provided but incompatible
1822            if figment.find_value("evm_version").is_err() {
1823                if let Some(version) = solc
1824                    .try_version()
1825                    .ok()
1826                    .and_then(|version| self.evm_version.normalize_version_solc(&version))
1827                {
1828                    // normalize evm_version based on the provided solc version
1829                    self.evm_version = version;
1830                }
1831            }
1832        }
1833
1834        figment
1835    }
1836}
1837
1838impl From<Config> for Figment {
1839    fn from(c: Config) -> Self {
1840        c.to_figment(FigmentProviders::All)
1841    }
1842}
1843
1844/// Determines what providers should be used when loading the [Figment] for a [Config]
1845#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1846pub enum FigmentProviders {
1847    /// Include all providers
1848    #[default]
1849    All,
1850    /// Only include necessary providers that are useful for cast commands
1851    ///
1852    /// This will exclude more expensive providers such as remappings
1853    Cast,
1854    /// Only include necessary providers that are useful for anvil
1855    ///
1856    /// This will exclude more expensive providers such as remappings
1857    Anvil,
1858}
1859
1860impl FigmentProviders {
1861    /// Returns true if all providers should be included
1862    pub const fn is_all(&self) -> bool {
1863        matches!(self, Self::All)
1864    }
1865
1866    /// Returns true if this is the cast preset
1867    pub const fn is_cast(&self) -> bool {
1868        matches!(self, Self::Cast)
1869    }
1870}
1871
1872/// Wrapper type for `regex::Regex` that implements `PartialEq`
1873#[derive(Clone, Debug, Serialize, Deserialize)]
1874#[serde(transparent)]
1875pub struct RegexWrapper {
1876    #[serde(with = "serde_regex")]
1877    inner: regex::Regex,
1878}
1879
1880impl std::ops::Deref for RegexWrapper {
1881    type Target = regex::Regex;
1882
1883    fn deref(&self) -> &Self::Target {
1884        &self.inner
1885    }
1886}
1887
1888impl std::cmp::PartialEq for RegexWrapper {
1889    fn eq(&self, other: &Self) -> bool {
1890        self.as_str() == other.as_str()
1891    }
1892}
1893
1894impl From<RegexWrapper> for regex::Regex {
1895    fn from(wrapper: RegexWrapper) -> Self {
1896        wrapper.inner
1897    }
1898}
1899
1900impl From<regex::Regex> for RegexWrapper {
1901    fn from(re: Regex) -> Self {
1902        Self { inner: re }
1903    }
1904}
1905
1906/// Ser/de `globset::Glob` explicitly to handle `Option<Glob>` properly
1907pub(crate) mod from_opt_glob {
1908    use serde::{Deserialize, Deserializer, Serializer};
1909
1910    pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
1911    where
1912        S: Serializer,
1913    {
1914        match value {
1915            Some(glob) => serializer.serialize_str(glob.glob()),
1916            None => serializer.serialize_none(),
1917        }
1918    }
1919
1920    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
1921    where
1922        D: Deserializer<'de>,
1923    {
1924        let s: Option<String> = Option::deserialize(deserializer)?;
1925        if let Some(s) = s {
1926            return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?))
1927        }
1928        Ok(None)
1929    }
1930}
1931
1932/// Ser/de `globset::Glob` explicitly to handle `Option<Glob>` properly
1933pub(crate) mod from_vec_glob {
1934    use serde::{Deserialize, Deserializer, Serialize, Serializer};
1935
1936    pub fn serialize<S>(value: &[globset::Glob], serializer: S) -> Result<S::Ok, S::Error>
1937    where
1938        S: Serializer,
1939    {
1940        let value = value.iter().map(|g| g.glob()).collect::<Vec<_>>();
1941        value.serialize(serializer)
1942    }
1943
1944    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<globset::Glob>, D::Error>
1945    where
1946        D: Deserializer<'de>,
1947    {
1948        let s: Vec<String> = Vec::deserialize(deserializer)?;
1949        s.into_iter()
1950            .map(|s| globset::Glob::new(&s))
1951            .collect::<Result<Vec<_>, _>>()
1952            .map_err(serde::de::Error::custom)
1953    }
1954}
1955
1956/// A helper wrapper around the root path used during Config detection
1957#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1958#[serde(transparent)]
1959pub struct RootPath(pub PathBuf);
1960
1961impl Default for RootPath {
1962    fn default() -> Self {
1963        ".".into()
1964    }
1965}
1966
1967impl<P: Into<PathBuf>> From<P> for RootPath {
1968    fn from(p: P) -> Self {
1969        Self(p.into())
1970    }
1971}
1972
1973impl AsRef<Path> for RootPath {
1974    fn as_ref(&self) -> &Path {
1975        &self.0
1976    }
1977}
1978
1979/// Parses a config profile
1980///
1981/// All `Profile` date is ignored by serde, however the `Config::to_string_pretty` includes it and
1982/// returns a toml table like
1983///
1984/// ```toml
1985/// #[profile.default]
1986/// src = "..."
1987/// ```
1988/// This ignores the `#[profile.default]` part in the toml
1989pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
1990    s: &str,
1991) -> Result<Option<(Profile, T)>, Error> {
1992    let figment = Config::merge_toml_provider(
1993        Figment::new(),
1994        Toml::string(s).nested(),
1995        Config::DEFAULT_PROFILE,
1996    );
1997    if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
1998        Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
1999    } else {
2000        Ok(None)
2001    }
2002}
2003
2004impl Provider for Config {
2005    fn metadata(&self) -> Metadata {
2006        Metadata::named("Foundry Config")
2007    }
2008
2009    #[track_caller]
2010    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2011        let mut data = Serialized::defaults(self).data()?;
2012        if let Some(entry) = data.get_mut(&self.profile) {
2013            entry.insert("root".to_string(), Value::serialize(self.root.clone())?);
2014        }
2015        Ok(data)
2016    }
2017
2018    fn profile(&self) -> Option<Profile> {
2019        Some(self.profile.clone())
2020    }
2021}
2022
2023impl Default for Config {
2024    fn default() -> Self {
2025        Self {
2026            profile: Self::DEFAULT_PROFILE,
2027            fs_permissions: FsPermissions::new([PathPermission::read("out")]),
2028            prague: false,
2029            #[cfg(not(feature = "isolate-by-default"))]
2030            isolate: false,
2031            #[cfg(feature = "isolate-by-default")]
2032            isolate: true,
2033            root: Default::default(),
2034            src: "src".into(),
2035            test: "test".into(),
2036            script: "script".into(),
2037            out: "out".into(),
2038            libs: vec!["lib".into()],
2039            cache: true,
2040            cache_path: "cache".into(),
2041            broadcast: "broadcast".into(),
2042            allow_paths: vec![],
2043            include_paths: vec![],
2044            force: false,
2045            evm_version: EvmVersion::Paris,
2046            gas_reports: vec!["*".to_string()],
2047            gas_reports_ignore: vec![],
2048            solc: None,
2049            vyper: Default::default(),
2050            auto_detect_solc: true,
2051            offline: false,
2052            optimizer: true,
2053            optimizer_runs: 200,
2054            optimizer_details: None,
2055            model_checker: None,
2056            extra_output: Default::default(),
2057            extra_output_files: Default::default(),
2058            names: false,
2059            sizes: false,
2060            test_pattern: None,
2061            test_pattern_inverse: None,
2062            contract_pattern: None,
2063            contract_pattern_inverse: None,
2064            path_pattern: None,
2065            path_pattern_inverse: None,
2066            fuzz: FuzzConfig::new("cache/fuzz".into()),
2067            invariant: InvariantConfig::new("cache/invariant".into()),
2068            always_use_create_2_factory: false,
2069            ffi: false,
2070            prompt_timeout: 120,
2071            sender: Self::DEFAULT_SENDER,
2072            tx_origin: Self::DEFAULT_SENDER,
2073            initial_balance: U256::from(0xffffffffffffffffffffffffu128),
2074            block_number: 1,
2075            fork_block_number: None,
2076            chain: None,
2077            gas_limit: i64::MAX.into(),
2078            code_size_limit: None,
2079            gas_price: None,
2080            block_base_fee_per_gas: 0,
2081            block_coinbase: Address::ZERO,
2082            block_timestamp: 1,
2083            block_difficulty: 0,
2084            block_prevrandao: Default::default(),
2085            block_gas_limit: None,
2086            disable_block_gas_limit: false,
2087            memory_limit: 1 << 27, // 2**27 = 128MiB = 134_217_728 bytes
2088            eth_rpc_url: None,
2089            eth_rpc_jwt: None,
2090            etherscan_api_key: None,
2091            verbosity: 0,
2092            remappings: vec![],
2093            auto_detect_remappings: true,
2094            libraries: vec![],
2095            ignored_error_codes: vec![
2096                SolidityErrorCode::SpdxLicenseNotProvided,
2097                SolidityErrorCode::ContractExceeds24576Bytes,
2098                SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
2099                SolidityErrorCode::TransientStorageUsed,
2100            ],
2101            ignored_file_paths: vec![],
2102            deny_warnings: false,
2103            via_ir: false,
2104            ast: false,
2105            rpc_storage_caching: Default::default(),
2106            rpc_endpoints: Default::default(),
2107            etherscan: Default::default(),
2108            no_storage_caching: false,
2109            no_rpc_rate_limit: false,
2110            use_literal_content: false,
2111            bytecode_hash: BytecodeHash::Ipfs,
2112            cbor_metadata: true,
2113            revert_strings: None,
2114            sparse_mode: false,
2115            build_info: false,
2116            build_info_path: None,
2117            fmt: Default::default(),
2118            doc: Default::default(),
2119            labels: Default::default(),
2120            unchecked_cheatcode_artifacts: false,
2121            create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
2122            skip: vec![],
2123            dependencies: Default::default(),
2124            warnings: vec![],
2125            _non_exhaustive: (),
2126        }
2127    }
2128}
2129
2130/// Wrapper for the config's `gas_limit` value necessary because toml-rs can't handle larger number because integers are stored signed: <https://github.com/alexcrichton/toml-rs/issues/256>
2131///
2132/// Due to this limitation this type will be serialized/deserialized as String if it's larger than
2133/// `i64`
2134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2135pub struct GasLimit(pub u64);
2136
2137impl From<u64> for GasLimit {
2138    fn from(gas: u64) -> Self {
2139        Self(gas)
2140    }
2141}
2142impl From<i64> for GasLimit {
2143    fn from(gas: i64) -> Self {
2144        Self(gas as u64)
2145    }
2146}
2147impl From<i32> for GasLimit {
2148    fn from(gas: i32) -> Self {
2149        Self(gas as u64)
2150    }
2151}
2152impl From<u32> for GasLimit {
2153    fn from(gas: u32) -> Self {
2154        Self(gas as u64)
2155    }
2156}
2157
2158impl From<GasLimit> for u64 {
2159    fn from(gas: GasLimit) -> Self {
2160        gas.0
2161    }
2162}
2163
2164impl Serialize for GasLimit {
2165    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2166    where
2167        S: Serializer,
2168    {
2169        if self.0 > i64::MAX as u64 {
2170            serializer.serialize_str(&self.0.to_string())
2171        } else {
2172            serializer.serialize_u64(self.0)
2173        }
2174    }
2175}
2176
2177impl<'de> Deserialize<'de> for GasLimit {
2178    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2179    where
2180        D: Deserializer<'de>,
2181    {
2182        use serde::de::Error;
2183
2184        #[derive(Deserialize)]
2185        #[serde(untagged)]
2186        enum Gas {
2187            Number(u64),
2188            Text(String),
2189        }
2190
2191        let gas = match Gas::deserialize(deserializer)? {
2192            Gas::Number(num) => Self(num),
2193            Gas::Text(s) => match s.as_str() {
2194                "max" | "MAX" | "Max" | "u64::MAX" | "u64::Max" => Self(u64::MAX),
2195                s => Self(s.parse().map_err(D::Error::custom)?),
2196            },
2197        };
2198
2199        Ok(gas)
2200    }
2201}
2202
2203/// Variants for selecting the [`Solc`] instance
2204#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2205#[serde(untagged)]
2206pub enum SolcReq {
2207    /// Requires a specific solc version, that's either already installed (via `svm`) or will be
2208    /// auto installed (via `svm`)
2209    Version(Version),
2210    /// Path to an existing local solc installation
2211    Local(PathBuf),
2212}
2213
2214impl SolcReq {
2215    /// Tries to get the solc version from the `SolcReq`
2216    ///
2217    /// If the `SolcReq` is a `Version` it will return the version, if it's a path to a binary it
2218    /// will try to get the version from the binary.
2219    fn try_version(&self) -> Result<Version, SolcError> {
2220        match self {
2221            Self::Version(version) => Ok(version.clone()),
2222            Self::Local(path) => Solc::new(path).map(|solc| solc.version),
2223        }
2224    }
2225}
2226
2227impl<T: AsRef<str>> From<T> for SolcReq {
2228    fn from(s: T) -> Self {
2229        let s = s.as_ref();
2230        if let Ok(v) = Version::from_str(s) {
2231            Self::Version(v)
2232        } else {
2233            Self::Local(s.into())
2234        }
2235    }
2236}
2237
2238/// A convenience provider to retrieve a toml file.
2239/// This will return an error if the env var is set but the file does not exist
2240struct TomlFileProvider {
2241    pub env_var: Option<&'static str>,
2242    pub default: PathBuf,
2243    pub cache: Option<Result<Map<Profile, Dict>, Error>>,
2244}
2245
2246impl TomlFileProvider {
2247    fn new(env_var: Option<&'static str>, default: impl Into<PathBuf>) -> Self {
2248        Self { env_var, default: default.into(), cache: None }
2249    }
2250
2251    fn env_val(&self) -> Option<String> {
2252        self.env_var.and_then(Env::var)
2253    }
2254
2255    fn file(&self) -> PathBuf {
2256        self.env_val().map(PathBuf::from).unwrap_or_else(|| self.default.clone())
2257    }
2258
2259    fn is_missing(&self) -> bool {
2260        if let Some(file) = self.env_val() {
2261            let path = Path::new(&file);
2262            if !path.exists() {
2263                return true
2264            }
2265        }
2266        false
2267    }
2268
2269    pub fn cached(mut self) -> Self {
2270        self.cache = Some(self.read());
2271        self
2272    }
2273
2274    fn read(&self) -> Result<Map<Profile, Dict>, Error> {
2275        use serde::de::Error as _;
2276        if let Some(file) = self.env_val() {
2277            let path = Path::new(&file);
2278            if !path.exists() {
2279                return Err(Error::custom(format!(
2280                    "Config file `{}` set in env var `{}` does not exist",
2281                    file,
2282                    self.env_var.unwrap()
2283                )))
2284            }
2285            Toml::file(file)
2286        } else {
2287            Toml::file(&self.default)
2288        }
2289        .nested()
2290        .data()
2291    }
2292}
2293
2294impl Provider for TomlFileProvider {
2295    fn metadata(&self) -> Metadata {
2296        if self.is_missing() {
2297            Metadata::named("TOML file provider")
2298        } else {
2299            Toml::file(self.file()).nested().metadata()
2300        }
2301    }
2302
2303    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2304        if let Some(cache) = self.cache.as_ref() {
2305            cache.clone()
2306        } else {
2307            self.read()
2308        }
2309    }
2310}
2311
2312/// A Provider that ensures all keys are snake case if they're not standalone sections, See
2313/// `Config::STANDALONE_SECTIONS`
2314struct ForcedSnakeCaseData<P>(P);
2315
2316impl<P: Provider> Provider for ForcedSnakeCaseData<P> {
2317    fn metadata(&self) -> Metadata {
2318        self.0.metadata()
2319    }
2320
2321    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2322        let mut map = Map::new();
2323        for (profile, dict) in self.0.data()? {
2324            if Config::STANDALONE_SECTIONS.contains(&profile.as_ref()) {
2325                // don't force snake case for keys in standalone sections
2326                map.insert(profile, dict);
2327                continue
2328            }
2329            map.insert(profile, dict.into_iter().map(|(k, v)| (k.to_snake_case(), v)).collect());
2330        }
2331        Ok(map)
2332    }
2333}
2334
2335/// A Provider that handles breaking changes in toml files
2336struct BackwardsCompatTomlProvider<P>(P);
2337
2338impl<P: Provider> Provider for BackwardsCompatTomlProvider<P> {
2339    fn metadata(&self) -> Metadata {
2340        self.0.metadata()
2341    }
2342
2343    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2344        let mut map = Map::new();
2345        let solc_env = std::env::var("FOUNDRY_SOLC_VERSION")
2346            .or_else(|_| std::env::var("DAPP_SOLC_VERSION"))
2347            .map(Value::from)
2348            .ok();
2349        for (profile, mut dict) in self.0.data()? {
2350            if let Some(v) = solc_env.clone() {
2351                // ENV var takes precedence over config file
2352                dict.insert("solc".to_string(), v);
2353            } else if let Some(v) = dict.remove("solc_version") {
2354                // only insert older variant if not already included
2355                if !dict.contains_key("solc") {
2356                    dict.insert("solc".to_string(), v);
2357                }
2358            }
2359            map.insert(profile, dict);
2360        }
2361        Ok(map)
2362    }
2363}
2364
2365/// A provider that sets the `src` and `output` path depending on their existence.
2366struct DappHardhatDirProvider<'a>(&'a Path);
2367
2368impl<'a> Provider for DappHardhatDirProvider<'a> {
2369    fn metadata(&self) -> Metadata {
2370        Metadata::named("Dapp Hardhat dir compat")
2371    }
2372
2373    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2374        let mut dict = Dict::new();
2375        dict.insert(
2376            "src".to_string(),
2377            ProjectPathsConfig::find_source_dir(self.0)
2378                .file_name()
2379                .unwrap()
2380                .to_string_lossy()
2381                .to_string()
2382                .into(),
2383        );
2384        dict.insert(
2385            "out".to_string(),
2386            ProjectPathsConfig::find_artifacts_dir(self.0)
2387                .file_name()
2388                .unwrap()
2389                .to_string_lossy()
2390                .to_string()
2391                .into(),
2392        );
2393
2394        // detect libs folders:
2395        //   if `lib` _and_ `node_modules` exists: include both
2396        //   if only `node_modules` exists: include `node_modules`
2397        //   include `lib` otherwise
2398        let mut libs = vec![];
2399        let node_modules = self.0.join("node_modules");
2400        let lib = self.0.join("lib");
2401        if node_modules.exists() {
2402            if lib.exists() {
2403                libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
2404            }
2405            libs.push(node_modules.file_name().unwrap().to_string_lossy().to_string());
2406        } else {
2407            libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
2408        }
2409
2410        dict.insert("libs".to_string(), libs.into());
2411
2412        Ok(Map::from([(Config::selected_profile(), dict)]))
2413    }
2414}
2415
2416/// A provider that checks for DAPP_ env vars that are named differently than FOUNDRY_
2417struct DappEnvCompatProvider;
2418
2419impl Provider for DappEnvCompatProvider {
2420    fn metadata(&self) -> Metadata {
2421        Metadata::named("Dapp env compat")
2422    }
2423
2424    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2425        use serde::de::Error as _;
2426        use std::env;
2427
2428        let mut dict = Dict::new();
2429        if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
2430            dict.insert(
2431                "block_number".to_string(),
2432                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
2433            );
2434        }
2435        if let Ok(val) = env::var("DAPP_TEST_ADDRESS") {
2436            dict.insert("sender".to_string(), val.into());
2437        }
2438        if let Ok(val) = env::var("DAPP_FORK_BLOCK") {
2439            dict.insert(
2440                "fork_block_number".to_string(),
2441                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
2442            );
2443        } else if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
2444            dict.insert(
2445                "fork_block_number".to_string(),
2446                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
2447            );
2448        }
2449        if let Ok(val) = env::var("DAPP_TEST_TIMESTAMP") {
2450            dict.insert(
2451                "block_timestamp".to_string(),
2452                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
2453            );
2454        }
2455        if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE_RUNS") {
2456            dict.insert(
2457                "optimizer_runs".to_string(),
2458                val.parse::<u64>().map_err(figment::Error::custom)?.into(),
2459            );
2460        }
2461        if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE") {
2462            // Activate Solidity optimizer (0 or 1)
2463            let val = val.parse::<u8>().map_err(figment::Error::custom)?;
2464            if val > 1 {
2465                return Err(
2466                    format!("Invalid $DAPP_BUILD_OPTIMIZE value `{val}`, expected 0 or 1").into()
2467                )
2468            }
2469            dict.insert("optimizer".to_string(), (val == 1).into());
2470        }
2471
2472        // libraries in env vars either as `[..]` or single string separated by comma
2473        if let Ok(val) = env::var("DAPP_LIBRARIES").or_else(|_| env::var("FOUNDRY_LIBRARIES")) {
2474            dict.insert("libraries".to_string(), utils::to_array_value(&val)?);
2475        }
2476
2477        let mut fuzz_dict = Dict::new();
2478        if let Ok(val) = env::var("DAPP_TEST_FUZZ_RUNS") {
2479            fuzz_dict.insert(
2480                "runs".to_string(),
2481                val.parse::<u32>().map_err(figment::Error::custom)?.into(),
2482            );
2483        }
2484        dict.insert("fuzz".to_string(), fuzz_dict.into());
2485
2486        let mut invariant_dict = Dict::new();
2487        if let Ok(val) = env::var("DAPP_TEST_DEPTH") {
2488            invariant_dict.insert(
2489                "depth".to_string(),
2490                val.parse::<u32>().map_err(figment::Error::custom)?.into(),
2491            );
2492        }
2493        dict.insert("invariant".to_string(), invariant_dict.into());
2494
2495        Ok(Map::from([(Config::selected_profile(), dict)]))
2496    }
2497}
2498
2499/// Renames a profile from `from` to `to`.
2500///
2501/// For example given:
2502///
2503/// ```toml
2504/// [from]
2505/// key = "value"
2506/// ```
2507///
2508/// RenameProfileProvider will output
2509///
2510/// ```toml
2511/// [to]
2512/// key = "value"
2513/// ```
2514struct RenameProfileProvider<P> {
2515    provider: P,
2516    from: Profile,
2517    to: Profile,
2518}
2519
2520impl<P> RenameProfileProvider<P> {
2521    pub fn new(provider: P, from: impl Into<Profile>, to: impl Into<Profile>) -> Self {
2522        Self { provider, from: from.into(), to: to.into() }
2523    }
2524}
2525
2526impl<P: Provider> Provider for RenameProfileProvider<P> {
2527    fn metadata(&self) -> Metadata {
2528        self.provider.metadata()
2529    }
2530    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2531        let mut data = self.provider.data()?;
2532        if let Some(data) = data.remove(&self.from) {
2533            return Ok(Map::from([(self.to.clone(), data)]))
2534        }
2535        Ok(Default::default())
2536    }
2537    fn profile(&self) -> Option<Profile> {
2538        Some(self.to.clone())
2539    }
2540}
2541
2542/// Unwraps a profile reducing the key depth
2543///
2544/// For example given:
2545///
2546/// ```toml
2547/// [wrapping_key.profile]
2548/// key = "value"
2549/// ```
2550///
2551/// UnwrapProfileProvider will output:
2552///
2553/// ```toml
2554/// [profile]
2555/// key = "value"
2556/// ```
2557struct UnwrapProfileProvider<P> {
2558    provider: P,
2559    wrapping_key: Profile,
2560    profile: Profile,
2561}
2562
2563impl<P> UnwrapProfileProvider<P> {
2564    pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
2565        Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
2566    }
2567}
2568
2569impl<P: Provider> Provider for UnwrapProfileProvider<P> {
2570    fn metadata(&self) -> Metadata {
2571        self.provider.metadata()
2572    }
2573    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2574        self.provider.data().and_then(|mut data| {
2575            if let Some(profiles) = data.remove(&self.wrapping_key) {
2576                for (profile_str, profile_val) in profiles {
2577                    let profile = Profile::new(&profile_str);
2578                    if profile != self.profile {
2579                        continue
2580                    }
2581                    match profile_val {
2582                        Value::Dict(_, dict) => return Ok(profile.collect(dict)),
2583                        bad_val => {
2584                            let mut err = Error::from(figment::error::Kind::InvalidType(
2585                                bad_val.to_actual(),
2586                                "dict".into(),
2587                            ));
2588                            err.metadata = Some(self.provider.metadata());
2589                            err.profile = Some(self.profile.clone());
2590                            return Err(err)
2591                        }
2592                    }
2593                }
2594            }
2595            Ok(Default::default())
2596        })
2597    }
2598    fn profile(&self) -> Option<Profile> {
2599        Some(self.profile.clone())
2600    }
2601}
2602
2603/// Wraps a profile in another profile
2604///
2605/// For example given:
2606///
2607/// ```toml
2608/// [profile]
2609/// key = "value"
2610/// ```
2611///
2612/// WrapProfileProvider will output:
2613///
2614/// ```toml
2615/// [wrapping_key.profile]
2616/// key = "value"
2617/// ```
2618struct WrapProfileProvider<P> {
2619    provider: P,
2620    wrapping_key: Profile,
2621    profile: Profile,
2622}
2623
2624impl<P> WrapProfileProvider<P> {
2625    pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
2626        Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
2627    }
2628}
2629
2630impl<P: Provider> Provider for WrapProfileProvider<P> {
2631    fn metadata(&self) -> Metadata {
2632        self.provider.metadata()
2633    }
2634    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2635        if let Some(inner) = self.provider.data()?.remove(&self.profile) {
2636            let value = Value::from(inner);
2637            let dict = [(self.profile.to_string().to_snake_case(), value)].into_iter().collect();
2638            Ok(self.wrapping_key.collect(dict))
2639        } else {
2640            Ok(Default::default())
2641        }
2642    }
2643    fn profile(&self) -> Option<Profile> {
2644        Some(self.profile.clone())
2645    }
2646}
2647
2648/// Extracts the profile from the `profile` key and using the original key as backup, merging
2649/// values where necessary
2650///
2651/// For example given:
2652///
2653/// ```toml
2654/// [profile.cool]
2655/// key = "value"
2656///
2657/// [cool]
2658/// key2 = "value2"
2659/// ```
2660///
2661/// OptionalStrictProfileProvider will output:
2662///
2663/// ```toml
2664/// [cool]
2665/// key = "value"
2666/// key2 = "value2"
2667/// ```
2668///
2669/// And emit a deprecation warning
2670struct OptionalStrictProfileProvider<P> {
2671    provider: P,
2672    profiles: Vec<Profile>,
2673}
2674
2675impl<P> OptionalStrictProfileProvider<P> {
2676    pub const PROFILE_PROFILE: Profile = Profile::const_new("profile");
2677
2678    pub fn new(provider: P, profiles: impl IntoIterator<Item = impl Into<Profile>>) -> Self {
2679        Self { provider, profiles: profiles.into_iter().map(|profile| profile.into()).collect() }
2680    }
2681}
2682
2683impl<P: Provider> Provider for OptionalStrictProfileProvider<P> {
2684    fn metadata(&self) -> Metadata {
2685        self.provider.metadata()
2686    }
2687    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2688        let mut figment = Figment::from(&self.provider);
2689        for profile in &self.profiles {
2690            figment = figment.merge(UnwrapProfileProvider::new(
2691                &self.provider,
2692                Self::PROFILE_PROFILE,
2693                profile.clone(),
2694            ));
2695        }
2696        figment.data().map_err(|err| {
2697            // figment does tag metadata and tries to map metadata to an error, since we use a new
2698            // figment in this provider this new figment does not know about the metadata of the
2699            // provider and can't map the metadata to the error. Therefor we return the root error
2700            // if this error originated in the provider's data.
2701            if let Err(root_err) = self.provider.data() {
2702                return root_err
2703            }
2704            err
2705        })
2706    }
2707    fn profile(&self) -> Option<Profile> {
2708        self.profiles.last().cloned()
2709    }
2710}
2711
2712trait ProviderExt: Provider {
2713    fn rename(
2714        &self,
2715        from: impl Into<Profile>,
2716        to: impl Into<Profile>,
2717    ) -> RenameProfileProvider<&Self> {
2718        RenameProfileProvider::new(self, from, to)
2719    }
2720
2721    fn wrap(
2722        &self,
2723        wrapping_key: impl Into<Profile>,
2724        profile: impl Into<Profile>,
2725    ) -> WrapProfileProvider<&Self> {
2726        WrapProfileProvider::new(self, wrapping_key, profile)
2727    }
2728
2729    fn strict_select(
2730        &self,
2731        profiles: impl IntoIterator<Item = impl Into<Profile>>,
2732    ) -> OptionalStrictProfileProvider<&Self> {
2733        OptionalStrictProfileProvider::new(self, profiles)
2734    }
2735
2736    fn fallback(
2737        &self,
2738        profile: impl Into<Profile>,
2739        fallback: impl Into<Profile>,
2740    ) -> FallbackProfileProvider<&Self> {
2741        FallbackProfileProvider::new(self, profile, fallback)
2742    }
2743}
2744impl<P: Provider> ProviderExt for P {}
2745
2746/// A subset of the foundry `Config`
2747/// used to initialize a `foundry.toml` file
2748///
2749/// # Example
2750///
2751/// ```rust
2752/// use cyfrin_foundry_config::{BasicConfig, Config};
2753/// use serde::Deserialize;
2754///
2755/// let my_config = Config::figment().extract::<BasicConfig>();
2756/// ```
2757#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2758pub struct BasicConfig {
2759    /// the profile tag: `[profile.default]`
2760    #[serde(skip)]
2761    pub profile: Profile,
2762    /// path of the source contracts dir, like `src` or `contracts`
2763    pub src: PathBuf,
2764    /// path to where artifacts shut be written to
2765    pub out: PathBuf,
2766    /// all library folders to include, `lib`, `node_modules`
2767    pub libs: Vec<PathBuf>,
2768    /// `Remappings` to use for this repo
2769    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2770    pub remappings: Vec<RelativeRemapping>,
2771}
2772
2773impl BasicConfig {
2774    /// Serialize the config as a String of TOML.
2775    ///
2776    /// This serializes to a table with the name of the profile
2777    pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2778        let s = toml::to_string_pretty(self)?;
2779        Ok(format!(
2780            "\
2781[profile.{}]
2782{s}
2783# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n",
2784            self.profile
2785        ))
2786    }
2787}
2788
2789pub(crate) mod from_str_lowercase {
2790    use serde::{Deserialize, Deserializer, Serializer};
2791    use std::str::FromStr;
2792
2793    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
2794    where
2795        T: std::fmt::Display,
2796        S: Serializer,
2797    {
2798        serializer.collect_str(&value.to_string().to_lowercase())
2799    }
2800
2801    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
2802    where
2803        D: Deserializer<'de>,
2804        T: FromStr,
2805        T::Err: std::fmt::Display,
2806    {
2807        String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
2808    }
2809}
2810
2811fn canonic(path: impl Into<PathBuf>) -> PathBuf {
2812    let path = path.into();
2813    foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
2814}
2815
2816#[cfg(test)]
2817mod tests {
2818    use super::*;
2819    use crate::{
2820        cache::{CachedChains, CachedEndpoints},
2821        endpoints::{RpcEndpointConfig, RpcEndpointType},
2822        etherscan::ResolvedEtherscanConfigs,
2823    };
2824    use figment::error::Kind::InvalidType;
2825    use foundry_compilers::artifacts::{
2826        vyper::VyperOptimizationMode, ModelCheckerEngine, YulDetails,
2827    };
2828    use similar_asserts::assert_eq;
2829    use std::{collections::BTreeMap, fs::File, io::Write};
2830    use tempfile::tempdir;
2831    use NamedChain::Moonbeam;
2832
2833    // Helper function to clear `__warnings` in config, since it will be populated during loading
2834    // from file, causing testing problem when comparing to those created from `default()`, etc.
2835    fn clear_warning(config: &mut Config) {
2836        config.warnings = vec![];
2837    }
2838
2839    #[test]
2840    fn default_sender() {
2841        assert_eq!(
2842            Config::DEFAULT_SENDER,
2843            Address::from_str("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38").unwrap()
2844        );
2845    }
2846
2847    #[test]
2848    fn test_caching() {
2849        let mut config = Config::default();
2850        let chain_id = NamedChain::Mainnet;
2851        let url = "https://eth-mainnet.alchemyapi";
2852        assert!(config.enable_caching(url, chain_id));
2853
2854        config.no_storage_caching = true;
2855        assert!(!config.enable_caching(url, chain_id));
2856
2857        config.no_storage_caching = false;
2858        assert!(!config.enable_caching(url, NamedChain::Dev));
2859    }
2860
2861    #[test]
2862    fn test_install_dir() {
2863        figment::Jail::expect_with(|jail| {
2864            let config = Config::load();
2865            assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2866            jail.create_file(
2867                "foundry.toml",
2868                r"
2869                [profile.default]
2870                libs = ['node_modules', 'lib']
2871            ",
2872            )?;
2873            let config = Config::load();
2874            assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2875
2876            jail.create_file(
2877                "foundry.toml",
2878                r"
2879                [profile.default]
2880                libs = ['custom', 'node_modules', 'lib']
2881            ",
2882            )?;
2883            let config = Config::load();
2884            assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
2885
2886            Ok(())
2887        });
2888    }
2889
2890    #[test]
2891    fn test_figment_is_default() {
2892        figment::Jail::expect_with(|_| {
2893            let mut default: Config = Config::figment().extract().unwrap();
2894            default.profile = Config::default().profile;
2895            assert_eq!(default, Config::default());
2896            Ok(())
2897        });
2898    }
2899
2900    #[test]
2901    fn test_default_round_trip() {
2902        figment::Jail::expect_with(|_| {
2903            let original = Config::figment();
2904            let roundtrip = Figment::from(Config::from_provider(&original));
2905            for figment in &[original, roundtrip] {
2906                let config = Config::from_provider(figment);
2907                assert_eq!(config, Config::default());
2908            }
2909            Ok(())
2910        });
2911    }
2912
2913    #[test]
2914    fn ffi_env_disallowed() {
2915        figment::Jail::expect_with(|jail| {
2916            jail.set_env("FOUNDRY_FFI", "true");
2917            jail.set_env("FFI", "true");
2918            jail.set_env("DAPP_FFI", "true");
2919            let config = Config::load();
2920            assert!(!config.ffi);
2921
2922            Ok(())
2923        });
2924    }
2925
2926    #[test]
2927    fn test_profile_env() {
2928        figment::Jail::expect_with(|jail| {
2929            jail.set_env("FOUNDRY_PROFILE", "default");
2930            let figment = Config::figment();
2931            assert_eq!(figment.profile(), "default");
2932
2933            jail.set_env("FOUNDRY_PROFILE", "hardhat");
2934            let figment: Figment = Config::hardhat().into();
2935            assert_eq!(figment.profile(), "hardhat");
2936
2937            jail.create_file(
2938                "foundry.toml",
2939                r"
2940                [profile.default]
2941                libs = ['lib']
2942                [profile.local]
2943                libs = ['modules']
2944            ",
2945            )?;
2946            jail.set_env("FOUNDRY_PROFILE", "local");
2947            let config = Config::load();
2948            assert_eq!(config.libs, vec![PathBuf::from("modules")]);
2949
2950            Ok(())
2951        });
2952    }
2953
2954    #[test]
2955    fn test_default_test_path() {
2956        figment::Jail::expect_with(|_| {
2957            let config = Config::default();
2958            let paths_config = config.project_paths::<Solc>();
2959            assert_eq!(paths_config.tests, PathBuf::from(r"test"));
2960            Ok(())
2961        });
2962    }
2963
2964    #[test]
2965    fn test_default_libs() {
2966        figment::Jail::expect_with(|jail| {
2967            let config = Config::load();
2968            assert_eq!(config.libs, vec![PathBuf::from("lib")]);
2969
2970            fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
2971            let config = Config::load();
2972            assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
2973
2974            fs::create_dir_all(jail.directory().join("lib")).unwrap();
2975            let config = Config::load();
2976            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2977
2978            Ok(())
2979        });
2980    }
2981
2982    #[test]
2983    fn test_inheritance_from_default_test_path() {
2984        figment::Jail::expect_with(|jail| {
2985            jail.create_file(
2986                "foundry.toml",
2987                r#"
2988                [profile.default]
2989                test = "defaulttest"
2990                src  = "defaultsrc"
2991                libs = ['lib', 'node_modules']
2992
2993                [profile.custom]
2994                src = "customsrc"
2995            "#,
2996            )?;
2997
2998            let config = Config::load();
2999            assert_eq!(config.src, PathBuf::from("defaultsrc"));
3000            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3001
3002            jail.set_env("FOUNDRY_PROFILE", "custom");
3003            let config = Config::load();
3004
3005            assert_eq!(config.src, PathBuf::from("customsrc"));
3006            assert_eq!(config.test, PathBuf::from("defaulttest"));
3007            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3008
3009            Ok(())
3010        });
3011    }
3012
3013    #[test]
3014    fn test_custom_test_path() {
3015        figment::Jail::expect_with(|jail| {
3016            jail.create_file(
3017                "foundry.toml",
3018                r#"
3019                [profile.default]
3020                test = "mytest"
3021            "#,
3022            )?;
3023
3024            let config = Config::load();
3025            let paths_config = config.project_paths::<Solc>();
3026            assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
3027            Ok(())
3028        });
3029    }
3030
3031    #[test]
3032    fn test_remappings() {
3033        figment::Jail::expect_with(|jail| {
3034            jail.create_file(
3035                "foundry.toml",
3036                r#"
3037                [profile.default]
3038                src = "some-source"
3039                out = "some-out"
3040                cache = true
3041            "#,
3042            )?;
3043            let config = Config::load();
3044            assert!(config.remappings.is_empty());
3045
3046            jail.create_file(
3047                "remappings.txt",
3048                r"
3049                file-ds-test/=lib/ds-test/
3050                file-other/=lib/other/
3051            ",
3052            )?;
3053
3054            let config = Config::load();
3055            assert_eq!(
3056                config.remappings,
3057                vec![
3058                    Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3059                    Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3060                ],
3061            );
3062
3063            jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
3064            let config = Config::load();
3065
3066            assert_eq!(
3067                config.remappings,
3068                vec![
3069                    // From environment (should have precedence over remapping.txt)
3070                    Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
3071                    Remapping::from_str("other/=lib/other/").unwrap().into(),
3072                    // From remapping.txt (should have less precedence than remapping.txt)
3073                    Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3074                    Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3075                ],
3076            );
3077
3078            Ok(())
3079        });
3080    }
3081
3082    #[test]
3083    fn test_remappings_override() {
3084        figment::Jail::expect_with(|jail| {
3085            jail.create_file(
3086                "foundry.toml",
3087                r#"
3088                [profile.default]
3089                src = "some-source"
3090                out = "some-out"
3091                cache = true
3092            "#,
3093            )?;
3094            let config = Config::load();
3095            assert!(config.remappings.is_empty());
3096
3097            jail.create_file(
3098                "remappings.txt",
3099                r"
3100                ds-test/=lib/ds-test/
3101                other/=lib/other/
3102            ",
3103            )?;
3104
3105            let config = Config::load();
3106            assert_eq!(
3107                config.remappings,
3108                vec![
3109                    Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
3110                    Remapping::from_str("other/=lib/other/").unwrap().into(),
3111                ],
3112            );
3113
3114            jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
3115            let config = Config::load();
3116
3117            // Remappings should now be:
3118            // - ds-test from environment (lib/ds-test/src/)
3119            // - other from remappings.txt (lib/other/)
3120            // - env-lib from environment (lib/env-lib/)
3121            assert_eq!(
3122                config.remappings,
3123                vec![
3124                    Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
3125                    Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
3126                    Remapping::from_str("other/=lib/other/").unwrap().into(),
3127                ],
3128            );
3129
3130            // contains additional remapping to the source dir
3131            assert_eq!(
3132                config.get_all_remappings().collect::<Vec<_>>(),
3133                vec![
3134                    Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
3135                    Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
3136                    Remapping::from_str("other/=lib/other/").unwrap(),
3137                ],
3138            );
3139
3140            Ok(())
3141        });
3142    }
3143
3144    #[test]
3145    fn test_can_update_libs() {
3146        figment::Jail::expect_with(|jail| {
3147            jail.create_file(
3148                "foundry.toml",
3149                r#"
3150                [profile.default]
3151                libs = ["node_modules"]
3152            "#,
3153            )?;
3154
3155            let mut config = Config::load();
3156            config.libs.push("libs".into());
3157            config.update_libs().unwrap();
3158
3159            let config = Config::load();
3160            assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
3161            Ok(())
3162        });
3163    }
3164
3165    #[test]
3166    fn test_large_gas_limit() {
3167        figment::Jail::expect_with(|jail| {
3168            let gas = u64::MAX;
3169            jail.create_file(
3170                "foundry.toml",
3171                &format!(
3172                    r#"
3173                [profile.default]
3174                gas_limit = "{gas}"
3175            "#
3176                ),
3177            )?;
3178
3179            let config = Config::load();
3180            assert_eq!(config, Config { gas_limit: gas.into(), ..Config::default() });
3181
3182            Ok(())
3183        });
3184    }
3185
3186    #[test]
3187    #[should_panic]
3188    fn test_toml_file_parse_failure() {
3189        figment::Jail::expect_with(|jail| {
3190            jail.create_file(
3191                "foundry.toml",
3192                r#"
3193                [profile.default]
3194                eth_rpc_url = "https://example.com/
3195            "#,
3196            )?;
3197
3198            let _config = Config::load();
3199
3200            Ok(())
3201        });
3202    }
3203
3204    #[test]
3205    #[should_panic]
3206    fn test_toml_file_non_existing_config_var_failure() {
3207        figment::Jail::expect_with(|jail| {
3208            jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
3209
3210            let _config = Config::load();
3211
3212            Ok(())
3213        });
3214    }
3215
3216    #[test]
3217    fn test_resolve_etherscan_with_chain() {
3218        figment::Jail::expect_with(|jail| {
3219            let env_key = "__BSC_ETHERSCAN_API_KEY";
3220            let env_value = "env value";
3221            jail.create_file(
3222                "foundry.toml",
3223                r#"
3224                [profile.default]
3225
3226                [etherscan]
3227                bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
3228            "#,
3229            )?;
3230
3231            let config = Config::load();
3232            assert!(config
3233                .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3234                .is_err());
3235
3236            std::env::set_var(env_key, env_value);
3237
3238            assert_eq!(
3239                config
3240                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3241                    .unwrap()
3242                    .unwrap()
3243                    .key,
3244                env_value
3245            );
3246
3247            let mut with_key = config;
3248            with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
3249
3250            assert_eq!(
3251                with_key
3252                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3253                    .unwrap()
3254                    .unwrap()
3255                    .key,
3256                "via etherscan_api_key"
3257            );
3258
3259            std::env::remove_var(env_key);
3260            Ok(())
3261        });
3262    }
3263
3264    #[test]
3265    fn test_resolve_etherscan() {
3266        figment::Jail::expect_with(|jail| {
3267            jail.create_file(
3268                "foundry.toml",
3269                r#"
3270                [profile.default]
3271
3272                [etherscan]
3273                mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3274                moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
3275            "#,
3276            )?;
3277
3278            let config = Config::load();
3279
3280            assert!(config.etherscan.clone().resolved().has_unresolved());
3281
3282            jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3283
3284            let configs = config.etherscan.resolved();
3285            assert!(!configs.has_unresolved());
3286
3287            let mb_urls = Moonbeam.etherscan_urls().unwrap();
3288            let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3289            assert_eq!(
3290                configs,
3291                ResolvedEtherscanConfigs::new([
3292                    (
3293                        "mainnet",
3294                        ResolvedEtherscanConfig {
3295                            api_url: mainnet_urls.0.to_string(),
3296                            chain: Some(NamedChain::Mainnet.into()),
3297                            browser_url: Some(mainnet_urls.1.to_string()),
3298                            key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3299                        }
3300                    ),
3301                    (
3302                        "moonbeam",
3303                        ResolvedEtherscanConfig {
3304                            api_url: mb_urls.0.to_string(),
3305                            chain: Some(Moonbeam.into()),
3306                            browser_url: Some(mb_urls.1.to_string()),
3307                            key: "123456789".to_string(),
3308                        }
3309                    ),
3310                ])
3311            );
3312
3313            Ok(())
3314        });
3315    }
3316
3317    #[test]
3318    fn test_resolve_etherscan_chain_id() {
3319        figment::Jail::expect_with(|jail| {
3320            jail.create_file(
3321                "foundry.toml",
3322                r#"
3323                [profile.default]
3324                chain_id = "sepolia"
3325
3326                [etherscan]
3327                sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3328            "#,
3329            )?;
3330
3331            let config = Config::load();
3332            let etherscan = config.get_etherscan_config().unwrap().unwrap();
3333            assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
3334            assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");
3335
3336            Ok(())
3337        });
3338    }
3339
3340    #[test]
3341    fn test_resolve_rpc_url() {
3342        figment::Jail::expect_with(|jail| {
3343            jail.create_file(
3344                "foundry.toml",
3345                r#"
3346                [profile.default]
3347                [rpc_endpoints]
3348                optimism = "https://example.com/"
3349                mainnet = "${_CONFIG_MAINNET}"
3350            "#,
3351            )?;
3352            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3353
3354            let mut config = Config::load();
3355            assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3356
3357            config.eth_rpc_url = Some("mainnet".to_string());
3358            assert_eq!(
3359                "https://eth-mainnet.alchemyapi.io/v2/123455",
3360                config.get_rpc_url_or_localhost_http().unwrap()
3361            );
3362
3363            config.eth_rpc_url = Some("optimism".to_string());
3364            assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
3365
3366            Ok(())
3367        })
3368    }
3369
3370    #[test]
3371    fn test_resolve_rpc_url_if_etherscan_set() {
3372        figment::Jail::expect_with(|jail| {
3373            jail.create_file(
3374                "foundry.toml",
3375                r#"
3376                [profile.default]
3377                etherscan_api_key = "dummy"
3378                [rpc_endpoints]
3379                optimism = "https://example.com/"
3380            "#,
3381            )?;
3382
3383            let config = Config::load();
3384            assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3385
3386            Ok(())
3387        })
3388    }
3389
3390    #[test]
3391    fn test_resolve_rpc_url_alias() {
3392        figment::Jail::expect_with(|jail| {
3393            jail.create_file(
3394                "foundry.toml",
3395                r#"
3396                [profile.default]
3397                [rpc_endpoints]
3398                polygonMumbai = "https://polygon-mumbai.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
3399            "#,
3400            )?;
3401            let mut config = Config::load();
3402            config.eth_rpc_url = Some("polygonMumbai".to_string());
3403            assert!(config.get_rpc_url().unwrap().is_err());
3404
3405            jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
3406
3407            let mut config = Config::load();
3408            config.eth_rpc_url = Some("polygonMumbai".to_string());
3409            assert_eq!(
3410                "https://polygon-mumbai.g.alchemy.com/v2/123455",
3411                config.get_rpc_url().unwrap().unwrap()
3412            );
3413
3414            Ok(())
3415        })
3416    }
3417
3418    #[test]
3419    fn test_resolve_rpc_aliases() {
3420        figment::Jail::expect_with(|jail| {
3421            jail.create_file(
3422                "foundry.toml",
3423                r#"
3424               [profile.default]
3425               [etherscan]
3426               arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
3427               [rpc_endpoints]
3428               arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
3429            "#,
3430            )?;
3431
3432            jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
3433            jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");
3434
3435            let config = Config::load();
3436
3437            let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
3438            assert!(config.is_err());
3439            assert_eq!(config.unwrap_err().to_string(), "At least one of `url` or `chain` must be present for Etherscan config with unknown alias `arbitrum_alias`");
3440
3441            Ok(())
3442        });
3443    }
3444
3445    #[test]
3446    fn test_resolve_rpc_config() {
3447        figment::Jail::expect_with(|jail| {
3448            jail.create_file(
3449                "foundry.toml",
3450                r#"
3451                [rpc_endpoints]
3452                optimism = "https://example.com/"
3453                mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
3454            "#,
3455            )?;
3456            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3457
3458            let config = Config::load();
3459            assert_eq!(
3460                RpcEndpoints::new([
3461                    (
3462                        "optimism",
3463                        RpcEndpointType::String(RpcEndpoint::Url(
3464                            "https://example.com/".to_string()
3465                        ))
3466                    ),
3467                    (
3468                        "mainnet",
3469                        RpcEndpointType::Config(RpcEndpointConfig {
3470                            endpoint: RpcEndpoint::Env("${_CONFIG_MAINNET}".to_string()),
3471                            retries: Some(3),
3472                            retry_backoff: Some(1000),
3473                            compute_units_per_second: Some(1000),
3474                        })
3475                    ),
3476                ]),
3477                config.rpc_endpoints
3478            );
3479
3480            let resolved = config.rpc_endpoints.resolved();
3481            assert_eq!(
3482                RpcEndpoints::new([
3483                    ("optimism", RpcEndpoint::Url("https://example.com/".to_string())),
3484                    (
3485                        "mainnet",
3486                        RpcEndpoint::Url("https://eth-mainnet.alchemyapi.io/v2/123455".to_string())
3487                    ),
3488                ])
3489                .resolved(),
3490                resolved
3491            );
3492            Ok(())
3493        })
3494    }
3495
3496    #[test]
3497    fn test_resolve_endpoints() {
3498        figment::Jail::expect_with(|jail| {
3499            jail.create_file(
3500                "foundry.toml",
3501                r#"
3502                [profile.default]
3503                eth_rpc_url = "optimism"
3504                [rpc_endpoints]
3505                optimism = "https://example.com/"
3506                mainnet = "${_CONFIG_MAINNET}"
3507                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
3508                mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
3509            "#,
3510            )?;
3511
3512            let config = Config::load();
3513
3514            assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
3515
3516            assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
3517
3518            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3519            jail.set_env("_CONFIG_API_KEY1", "123456");
3520            jail.set_env("_CONFIG_API_KEY2", "98765");
3521
3522            let endpoints = config.rpc_endpoints.resolved();
3523
3524            assert!(!endpoints.has_unresolved());
3525
3526            assert_eq!(
3527                endpoints,
3528                RpcEndpoints::new([
3529                    ("optimism", RpcEndpoint::Url("https://example.com/".to_string())),
3530                    (
3531                        "mainnet",
3532                        RpcEndpoint::Url("https://eth-mainnet.alchemyapi.io/v2/123455".to_string())
3533                    ),
3534                    (
3535                        "mainnet_2",
3536                        RpcEndpoint::Url("https://eth-mainnet.alchemyapi.io/v2/123456".to_string())
3537                    ),
3538                    (
3539                        "mainnet_3",
3540                        RpcEndpoint::Url(
3541                            "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
3542                        )
3543                    ),
3544                ])
3545                .resolved()
3546            );
3547
3548            Ok(())
3549        });
3550    }
3551
3552    #[test]
3553    fn test_extract_etherscan_config() {
3554        figment::Jail::expect_with(|jail| {
3555            jail.create_file(
3556                "foundry.toml",
3557                r#"
3558                [profile.default]
3559                etherscan_api_key = "optimism"
3560
3561                [etherscan]
3562                optimism = { key = "https://etherscan-optimism.com/" }
3563                mumbai = { key = "https://etherscan-mumbai.com/" }
3564            "#,
3565            )?;
3566
3567            let mut config = Config::load();
3568
3569            let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
3570            assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
3571
3572            config.etherscan_api_key = Some("mumbai".to_string());
3573
3574            let mumbai = config.get_etherscan_api_key(Some(NamedChain::PolygonMumbai.into()));
3575            assert_eq!(mumbai, Some("https://etherscan-mumbai.com/".to_string()));
3576
3577            Ok(())
3578        });
3579    }
3580
3581    #[test]
3582    fn test_extract_etherscan_config_by_chain() {
3583        figment::Jail::expect_with(|jail| {
3584            jail.create_file(
3585                "foundry.toml",
3586                r#"
3587                [profile.default]
3588
3589                [etherscan]
3590                mumbai = { key = "https://etherscan-mumbai.com/", chain = 80001 }
3591            "#,
3592            )?;
3593
3594            let config = Config::load();
3595
3596            let mumbai = config
3597                .get_etherscan_config_with_chain(Some(NamedChain::PolygonMumbai.into()))
3598                .unwrap()
3599                .unwrap();
3600            assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3601
3602            Ok(())
3603        });
3604    }
3605
3606    #[test]
3607    fn test_extract_etherscan_config_by_chain_with_url() {
3608        figment::Jail::expect_with(|jail| {
3609            jail.create_file(
3610                "foundry.toml",
3611                r#"
3612                [profile.default]
3613
3614                [etherscan]
3615                mumbai = { key = "https://etherscan-mumbai.com/", chain = 80001 , url =  "https://verifier-url.com/"}
3616            "#,
3617            )?;
3618
3619            let config = Config::load();
3620
3621            let mumbai = config
3622                .get_etherscan_config_with_chain(Some(NamedChain::PolygonMumbai.into()))
3623                .unwrap()
3624                .unwrap();
3625            assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3626            assert_eq!(mumbai.api_url, "https://verifier-url.com/".to_string());
3627
3628            Ok(())
3629        });
3630    }
3631
3632    #[test]
3633    fn test_extract_etherscan_config_by_chain_and_alias() {
3634        figment::Jail::expect_with(|jail| {
3635            jail.create_file(
3636                "foundry.toml",
3637                r#"
3638                [profile.default]
3639                eth_rpc_url = "mumbai"
3640
3641                [etherscan]
3642                mumbai = { key = "https://etherscan-mumbai.com/" }
3643
3644                [rpc_endpoints]
3645                mumbai = "https://polygon-mumbai.g.alchemy.com/v2/mumbai"
3646            "#,
3647            )?;
3648
3649            let config = Config::load();
3650
3651            let mumbai = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
3652            assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3653
3654            let mumbai_rpc = config.get_rpc_url().unwrap().unwrap();
3655            assert_eq!(mumbai_rpc, "https://polygon-mumbai.g.alchemy.com/v2/mumbai");
3656            Ok(())
3657        });
3658    }
3659
3660    #[test]
3661    fn test_toml_file() {
3662        figment::Jail::expect_with(|jail| {
3663            jail.create_file(
3664                "foundry.toml",
3665                r#"
3666                [profile.default]
3667                src = "some-source"
3668                out = "some-out"
3669                cache = true
3670                eth_rpc_url = "https://example.com/"
3671                verbosity = 3
3672                remappings = ["ds-test=lib/ds-test/"]
3673                via_ir = true
3674                rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
3675                use_literal_content = false
3676                bytecode_hash = "ipfs"
3677                cbor_metadata = true
3678                revert_strings = "strip"
3679                allow_paths = ["allow", "paths"]
3680                build_info_path = "build-info"
3681                always_use_create_2_factory = true
3682
3683                [rpc_endpoints]
3684                optimism = "https://example.com/"
3685                mainnet = "${RPC_MAINNET}"
3686                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3687                mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3688            "#,
3689            )?;
3690
3691            let config = Config::load();
3692            assert_eq!(
3693                config,
3694                Config {
3695                    src: "some-source".into(),
3696                    out: "some-out".into(),
3697                    cache: true,
3698                    eth_rpc_url: Some("https://example.com/".to_string()),
3699                    remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
3700                    verbosity: 3,
3701                    via_ir: true,
3702                    rpc_storage_caching: StorageCachingConfig {
3703                        chains: CachedChains::Chains(vec![
3704                            Chain::mainnet(),
3705                            Chain::optimism_mainnet(),
3706                            Chain::from_id(999999)
3707                        ]),
3708                        endpoints: CachedEndpoints::All,
3709                    },
3710                    use_literal_content: false,
3711                    bytecode_hash: BytecodeHash::Ipfs,
3712                    cbor_metadata: true,
3713                    revert_strings: Some(RevertStrings::Strip),
3714                    allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
3715                    rpc_endpoints: RpcEndpoints::new([
3716                        ("optimism", RpcEndpoint::Url("https://example.com/".to_string())),
3717                        ("mainnet", RpcEndpoint::Env("${RPC_MAINNET}".to_string())),
3718                        (
3719                            "mainnet_2",
3720                            RpcEndpoint::Env(
3721                                "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3722                            )
3723                        ),
3724                        (
3725                            "mainnet_3",
3726                            RpcEndpoint::Env(
3727                                "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3728                                    .to_string()
3729                            )
3730                        ),
3731                    ]),
3732                    build_info_path: Some("build-info".into()),
3733                    always_use_create_2_factory: true,
3734                    ..Config::default()
3735                }
3736            );
3737
3738            Ok(())
3739        });
3740    }
3741
3742    #[test]
3743    fn test_load_remappings() {
3744        figment::Jail::expect_with(|jail| {
3745            jail.create_file(
3746                "foundry.toml",
3747                r"
3748                [profile.default]
3749                remappings = ['nested/=lib/nested/']
3750            ",
3751            )?;
3752
3753            let config = Config::load_with_root(jail.directory());
3754            assert_eq!(
3755                config.remappings,
3756                vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3757            );
3758
3759            Ok(())
3760        });
3761    }
3762
3763    #[test]
3764    fn test_load_full_toml() {
3765        figment::Jail::expect_with(|jail| {
3766            jail.create_file(
3767                "foundry.toml",
3768                r#"
3769                [profile.default]
3770                auto_detect_solc = true
3771                block_base_fee_per_gas = 0
3772                block_coinbase = '0x0000000000000000000000000000000000000000'
3773                block_difficulty = 0
3774                block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
3775                block_number = 1
3776                block_timestamp = 1
3777                use_literal_content = false
3778                bytecode_hash = 'ipfs'
3779                cbor_metadata = true
3780                cache = true
3781                cache_path = 'cache'
3782                evm_version = 'london'
3783                extra_output = []
3784                extra_output_files = []
3785                always_use_create_2_factory = false
3786                ffi = false
3787                force = false
3788                gas_limit = 9223372036854775807
3789                gas_price = 0
3790                gas_reports = ['*']
3791                ignored_error_codes = [1878]
3792                ignored_warnings_from = ["something"]
3793                deny_warnings = false
3794                initial_balance = '0xffffffffffffffffffffffff'
3795                libraries = []
3796                libs = ['lib']
3797                memory_limit = 134217728
3798                names = false
3799                no_storage_caching = false
3800                no_rpc_rate_limit = false
3801                offline = false
3802                optimizer = true
3803                optimizer_runs = 200
3804                out = 'out'
3805                remappings = ['nested/=lib/nested/']
3806                sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3807                sizes = false
3808                sparse_mode = false
3809                src = 'src'
3810                test = 'test'
3811                tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3812                verbosity = 0
3813                via_ir = false
3814
3815                [profile.default.rpc_storage_caching]
3816                chains = 'all'
3817                endpoints = 'all'
3818
3819                [rpc_endpoints]
3820                optimism = "https://example.com/"
3821                mainnet = "${RPC_MAINNET}"
3822                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3823
3824                [fuzz]
3825                runs = 256
3826                seed = '0x3e8'
3827                max_test_rejects = 65536
3828
3829                [invariant]
3830                runs = 256
3831                depth = 500
3832                fail_on_revert = false
3833                call_override = false
3834                shrink_run_limit = 5000
3835            "#,
3836            )?;
3837
3838            let config = Config::load_with_root(jail.directory());
3839
3840            assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
3841            assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
3842            assert_eq!(
3843                config.remappings,
3844                vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3845            );
3846
3847            assert_eq!(
3848                config.rpc_endpoints,
3849                RpcEndpoints::new([
3850                    ("optimism", RpcEndpoint::Url("https://example.com/".to_string())),
3851                    ("mainnet", RpcEndpoint::Env("${RPC_MAINNET}".to_string())),
3852                    (
3853                        "mainnet_2",
3854                        RpcEndpoint::Env(
3855                            "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3856                        )
3857                    ),
3858                ]),
3859            );
3860
3861            Ok(())
3862        });
3863    }
3864
3865    #[test]
3866    fn test_solc_req() {
3867        figment::Jail::expect_with(|jail| {
3868            jail.create_file(
3869                "foundry.toml",
3870                r#"
3871                [profile.default]
3872                solc_version = "0.8.12"
3873            "#,
3874            )?;
3875
3876            let config = Config::load();
3877            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3878
3879            jail.create_file(
3880                "foundry.toml",
3881                r#"
3882                [profile.default]
3883                solc = "0.8.12"
3884            "#,
3885            )?;
3886
3887            let config = Config::load();
3888            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3889
3890            jail.create_file(
3891                "foundry.toml",
3892                r#"
3893                [profile.default]
3894                solc = "path/to/local/solc"
3895            "#,
3896            )?;
3897
3898            let config = Config::load();
3899            assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
3900
3901            jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
3902            let config = Config::load();
3903            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
3904            Ok(())
3905        });
3906    }
3907
3908    // ensures the newer `solc` takes precedence over `solc_version`
3909    #[test]
3910    fn test_backwards_solc_version() {
3911        figment::Jail::expect_with(|jail| {
3912            jail.create_file(
3913                "foundry.toml",
3914                r#"
3915                [default]
3916                solc = "0.8.12"
3917                solc_version = "0.8.20"
3918            "#,
3919            )?;
3920
3921            let config = Config::load();
3922            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3923
3924            Ok(())
3925        });
3926
3927        figment::Jail::expect_with(|jail| {
3928            jail.create_file(
3929                "foundry.toml",
3930                r#"
3931                [default]
3932                solc_version = "0.8.20"
3933            "#,
3934            )?;
3935
3936            let config = Config::load();
3937            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));
3938
3939            Ok(())
3940        });
3941    }
3942
3943    #[test]
3944    fn test_toml_casing_file() {
3945        figment::Jail::expect_with(|jail| {
3946            jail.create_file(
3947                "foundry.toml",
3948                r#"
3949                [profile.default]
3950                src = "some-source"
3951                out = "some-out"
3952                cache = true
3953                eth-rpc-url = "https://example.com/"
3954                evm-version = "berlin"
3955                auto-detect-solc = false
3956            "#,
3957            )?;
3958
3959            let config = Config::load();
3960            assert_eq!(
3961                config,
3962                Config {
3963                    src: "some-source".into(),
3964                    out: "some-out".into(),
3965                    cache: true,
3966                    eth_rpc_url: Some("https://example.com/".to_string()),
3967                    auto_detect_solc: false,
3968                    evm_version: EvmVersion::Berlin,
3969                    ..Config::default()
3970                }
3971            );
3972
3973            Ok(())
3974        });
3975    }
3976
3977    #[test]
3978    fn test_output_selection() {
3979        figment::Jail::expect_with(|jail| {
3980            jail.create_file(
3981                "foundry.toml",
3982                r#"
3983                [profile.default]
3984                extra_output = ["metadata", "ir-optimized"]
3985                extra_output_files = ["metadata"]
3986            "#,
3987            )?;
3988
3989            let config = Config::load();
3990
3991            assert_eq!(
3992                config.extra_output,
3993                vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
3994            );
3995            assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
3996
3997            Ok(())
3998        });
3999    }
4000
4001    #[test]
4002    fn test_precedence() {
4003        figment::Jail::expect_with(|jail| {
4004            jail.create_file(
4005                "foundry.toml",
4006                r#"
4007                [profile.default]
4008                src = "mysrc"
4009                out = "myout"
4010                verbosity = 3
4011            "#,
4012            )?;
4013
4014            let config = Config::load();
4015            assert_eq!(
4016                config,
4017                Config {
4018                    src: "mysrc".into(),
4019                    out: "myout".into(),
4020                    verbosity: 3,
4021                    ..Config::default()
4022                }
4023            );
4024
4025            jail.set_env("FOUNDRY_SRC", r"other-src");
4026            let config = Config::load();
4027            assert_eq!(
4028                config,
4029                Config {
4030                    src: "other-src".into(),
4031                    out: "myout".into(),
4032                    verbosity: 3,
4033                    ..Config::default()
4034                }
4035            );
4036
4037            jail.set_env("FOUNDRY_PROFILE", "foo");
4038            let val: Result<String, _> = Config::figment().extract_inner("profile");
4039            assert!(val.is_err());
4040
4041            Ok(())
4042        });
4043    }
4044
4045    #[test]
4046    fn test_extract_basic() {
4047        figment::Jail::expect_with(|jail| {
4048            jail.create_file(
4049                "foundry.toml",
4050                r#"
4051                [profile.default]
4052                src = "mysrc"
4053                out = "myout"
4054                verbosity = 3
4055                evm_version = 'berlin'
4056
4057                [profile.other]
4058                src = "other-src"
4059            "#,
4060            )?;
4061            let loaded = Config::load();
4062            assert_eq!(loaded.evm_version, EvmVersion::Berlin);
4063            let base = loaded.into_basic();
4064            let default = Config::default();
4065            assert_eq!(
4066                base,
4067                BasicConfig {
4068                    profile: Config::DEFAULT_PROFILE,
4069                    src: "mysrc".into(),
4070                    out: "myout".into(),
4071                    libs: default.libs.clone(),
4072                    remappings: default.remappings.clone(),
4073                }
4074            );
4075            jail.set_env("FOUNDRY_PROFILE", r"other");
4076            let base = Config::figment().extract::<BasicConfig>().unwrap();
4077            assert_eq!(
4078                base,
4079                BasicConfig {
4080                    profile: Config::DEFAULT_PROFILE,
4081                    src: "other-src".into(),
4082                    out: "myout".into(),
4083                    libs: default.libs.clone(),
4084                    remappings: default.remappings,
4085                }
4086            );
4087            Ok(())
4088        });
4089    }
4090
4091    #[test]
4092    #[should_panic]
4093    fn test_parse_invalid_fuzz_weight() {
4094        figment::Jail::expect_with(|jail| {
4095            jail.create_file(
4096                "foundry.toml",
4097                r"
4098                [fuzz]
4099                dictionary_weight = 101
4100            ",
4101            )?;
4102            let _config = Config::load();
4103            Ok(())
4104        });
4105    }
4106
4107    #[test]
4108    fn test_fallback_provider() {
4109        figment::Jail::expect_with(|jail| {
4110            jail.create_file(
4111                "foundry.toml",
4112                r"
4113                [fuzz]
4114                runs = 1
4115                include_storage = false
4116                dictionary_weight = 99
4117
4118                [invariant]
4119                runs = 420
4120
4121                [profile.ci.fuzz]
4122                dictionary_weight = 5
4123
4124                [profile.ci.invariant]
4125                runs = 400
4126            ",
4127            )?;
4128
4129            let invariant_default = InvariantConfig::default();
4130            let config = Config::load();
4131
4132            assert_ne!(config.invariant.runs, config.fuzz.runs);
4133            assert_eq!(config.invariant.runs, 420);
4134
4135            assert_ne!(
4136                config.fuzz.dictionary.include_storage,
4137                invariant_default.dictionary.include_storage
4138            );
4139            assert_eq!(
4140                config.invariant.dictionary.include_storage,
4141                config.fuzz.dictionary.include_storage
4142            );
4143
4144            assert_ne!(
4145                config.fuzz.dictionary.dictionary_weight,
4146                invariant_default.dictionary.dictionary_weight
4147            );
4148            assert_eq!(
4149                config.invariant.dictionary.dictionary_weight,
4150                config.fuzz.dictionary.dictionary_weight
4151            );
4152
4153            jail.set_env("FOUNDRY_PROFILE", "ci");
4154            let ci_config = Config::load();
4155            assert_eq!(ci_config.fuzz.runs, 1);
4156            assert_eq!(ci_config.invariant.runs, 400);
4157            assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
4158            assert_eq!(
4159                ci_config.invariant.dictionary.dictionary_weight,
4160                config.fuzz.dictionary.dictionary_weight
4161            );
4162
4163            Ok(())
4164        })
4165    }
4166
4167    #[test]
4168    fn test_standalone_profile_sections() {
4169        figment::Jail::expect_with(|jail| {
4170            jail.create_file(
4171                "foundry.toml",
4172                r"
4173                [fuzz]
4174                runs = 100
4175
4176                [invariant]
4177                runs = 120
4178
4179                [profile.ci.fuzz]
4180                runs = 420
4181
4182                [profile.ci.invariant]
4183                runs = 500
4184            ",
4185            )?;
4186
4187            let config = Config::load();
4188            assert_eq!(config.fuzz.runs, 100);
4189            assert_eq!(config.invariant.runs, 120);
4190
4191            jail.set_env("FOUNDRY_PROFILE", "ci");
4192            let config = Config::load();
4193            assert_eq!(config.fuzz.runs, 420);
4194            assert_eq!(config.invariant.runs, 500);
4195
4196            Ok(())
4197        });
4198    }
4199
4200    #[test]
4201    fn can_handle_deviating_dapp_aliases() {
4202        figment::Jail::expect_with(|jail| {
4203            let addr = Address::ZERO;
4204            jail.set_env("DAPP_TEST_NUMBER", 1337);
4205            jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
4206            jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
4207            jail.set_env("DAPP_TEST_DEPTH", 20);
4208            jail.set_env("DAPP_FORK_BLOCK", 100);
4209            jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
4210            jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
4211
4212            let config = Config::load();
4213
4214            assert_eq!(config.block_number, 1337);
4215            assert_eq!(config.sender, addr);
4216            assert_eq!(config.fuzz.runs, 420);
4217            assert_eq!(config.invariant.depth, 20);
4218            assert_eq!(config.fork_block_number, Some(100));
4219            assert_eq!(config.optimizer_runs, 999);
4220            assert!(!config.optimizer);
4221
4222            Ok(())
4223        });
4224    }
4225
4226    #[test]
4227    fn can_parse_libraries() {
4228        figment::Jail::expect_with(|jail| {
4229            jail.set_env(
4230                "DAPP_LIBRARIES",
4231                "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
4232            );
4233            let config = Config::load();
4234            assert_eq!(
4235                config.libraries,
4236                vec!["src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4237                    .to_string()]
4238            );
4239
4240            jail.set_env(
4241                "DAPP_LIBRARIES",
4242                "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4243            );
4244            let config = Config::load();
4245            assert_eq!(
4246                config.libraries,
4247                vec!["src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4248                    .to_string(),]
4249            );
4250
4251            jail.set_env(
4252                "DAPP_LIBRARIES",
4253                "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4254            );
4255            let config = Config::load();
4256            assert_eq!(
4257                config.libraries,
4258                vec![
4259                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4260                        .to_string(),
4261                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4262                        .to_string()
4263                ]
4264            );
4265
4266            Ok(())
4267        });
4268    }
4269
4270    #[test]
4271    fn test_parse_many_libraries() {
4272        figment::Jail::expect_with(|jail| {
4273            jail.create_file(
4274                "foundry.toml",
4275                r"
4276                [profile.default]
4277               libraries= [
4278                        './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4279                        './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4280                        './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4281                        './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4282                        './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4283                    ]
4284            ",
4285            )?;
4286            let config = Config::load();
4287
4288            let libs = config.parsed_libraries().unwrap().libs;
4289
4290            similar_asserts::assert_eq!(
4291                libs,
4292                BTreeMap::from([
4293                    (
4294                        PathBuf::from("./src/SizeAuctionDiscount.sol"),
4295                        BTreeMap::from([
4296                            (
4297                                "Chainlink".to_string(),
4298                                "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4299                            ),
4300                            (
4301                                "Math".to_string(),
4302                                "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4303                            )
4304                        ])
4305                    ),
4306                    (
4307                        PathBuf::from("./src/SizeAuction.sol"),
4308                        BTreeMap::from([
4309                            (
4310                                "ChainlinkTWAP".to_string(),
4311                                "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4312                            ),
4313                            (
4314                                "Math".to_string(),
4315                                "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4316                            )
4317                        ])
4318                    ),
4319                    (
4320                        PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
4321                        BTreeMap::from([(
4322                            "ChainlinkTWAP".to_string(),
4323                            "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4324                        )])
4325                    ),
4326                ])
4327            );
4328
4329            Ok(())
4330        });
4331    }
4332
4333    #[test]
4334    fn config_roundtrip() {
4335        figment::Jail::expect_with(|jail| {
4336            let default = Config::default();
4337            let basic = default.clone().into_basic();
4338            jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
4339
4340            let mut other = Config::load();
4341            clear_warning(&mut other);
4342            assert_eq!(default, other);
4343
4344            let other = other.into_basic();
4345            assert_eq!(basic, other);
4346
4347            jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
4348            let mut other = Config::load();
4349            clear_warning(&mut other);
4350            assert_eq!(default, other);
4351
4352            Ok(())
4353        });
4354    }
4355
4356    #[test]
4357    fn test_fs_permissions() {
4358        figment::Jail::expect_with(|jail| {
4359            jail.create_file(
4360                "foundry.toml",
4361                r#"
4362                [profile.default]
4363                fs_permissions = [{ access = "read-write", path = "./"}]
4364            "#,
4365            )?;
4366            let loaded = Config::load();
4367
4368            assert_eq!(
4369                loaded.fs_permissions,
4370                FsPermissions::new(vec![PathPermission::read_write("./")])
4371            );
4372
4373            jail.create_file(
4374                "foundry.toml",
4375                r#"
4376                [profile.default]
4377                fs_permissions = [{ access = "none", path = "./"}]
4378            "#,
4379            )?;
4380            let loaded = Config::load();
4381            assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
4382
4383            Ok(())
4384        });
4385    }
4386
4387    #[test]
4388    fn test_optimizer_settings_basic() {
4389        figment::Jail::expect_with(|jail| {
4390            jail.create_file(
4391                "foundry.toml",
4392                r"
4393                [profile.default]
4394                optimizer = true
4395
4396                [profile.default.optimizer_details]
4397                yul = false
4398
4399                [profile.default.optimizer_details.yulDetails]
4400                stackAllocation = true
4401            ",
4402            )?;
4403            let mut loaded = Config::load();
4404            clear_warning(&mut loaded);
4405            assert_eq!(
4406                loaded.optimizer_details,
4407                Some(OptimizerDetails {
4408                    yul: Some(false),
4409                    yul_details: Some(YulDetails {
4410                        stack_allocation: Some(true),
4411                        ..Default::default()
4412                    }),
4413                    ..Default::default()
4414                })
4415            );
4416
4417            let s = loaded.to_string_pretty().unwrap();
4418            jail.create_file("foundry.toml", &s)?;
4419
4420            let mut reloaded = Config::load();
4421            clear_warning(&mut reloaded);
4422            assert_eq!(loaded, reloaded);
4423
4424            Ok(())
4425        });
4426    }
4427
4428    #[test]
4429    fn test_model_checker_settings_basic() {
4430        figment::Jail::expect_with(|jail| {
4431            jail.create_file(
4432                "foundry.toml",
4433                r"
4434                [profile.default]
4435
4436                [profile.default.model_checker]
4437                contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4438                engine = 'chc'
4439                targets = [ 'assert', 'outOfBounds' ]
4440                timeout = 10000
4441            ",
4442            )?;
4443            let mut loaded = Config::load();
4444            clear_warning(&mut loaded);
4445            assert_eq!(
4446                loaded.model_checker,
4447                Some(ModelCheckerSettings {
4448                    contracts: BTreeMap::from([
4449                        ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
4450                        ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
4451                    ]),
4452                    engine: Some(ModelCheckerEngine::CHC),
4453                    targets: Some(vec![
4454                        ModelCheckerTarget::Assert,
4455                        ModelCheckerTarget::OutOfBounds
4456                    ]),
4457                    timeout: Some(10000),
4458                    invariants: None,
4459                    show_unproved: None,
4460                    div_mod_with_slacks: None,
4461                    solvers: None,
4462                    show_unsupported: None,
4463                    show_proved_safe: None,
4464                })
4465            );
4466
4467            let s = loaded.to_string_pretty().unwrap();
4468            jail.create_file("foundry.toml", &s)?;
4469
4470            let mut reloaded = Config::load();
4471            clear_warning(&mut reloaded);
4472            assert_eq!(loaded, reloaded);
4473
4474            Ok(())
4475        });
4476    }
4477
4478    #[test]
4479    fn test_model_checker_settings_relative_paths() {
4480        figment::Jail::expect_with(|jail| {
4481            jail.create_file(
4482                "foundry.toml",
4483                r"
4484                [profile.default]
4485
4486                [profile.default.model_checker]
4487                contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4488                engine = 'chc'
4489                targets = [ 'assert', 'outOfBounds' ]
4490                timeout = 10000
4491            ",
4492            )?;
4493            let loaded = Config::load().sanitized();
4494
4495            // NOTE(onbjerg): We have to canonicalize the path here using dunce because figment will
4496            // canonicalize the jail path using the standard library. The standard library *always*
4497            // transforms Windows paths to some weird extended format, which none of our code base
4498            // does.
4499            let dir = foundry_compilers::utils::canonicalize(jail.directory())
4500                .expect("Could not canonicalize jail path");
4501            assert_eq!(
4502                loaded.model_checker,
4503                Some(ModelCheckerSettings {
4504                    contracts: BTreeMap::from([
4505                        (
4506                            format!("{}", dir.join("a.sol").display()),
4507                            vec!["A1".to_string(), "A2".to_string()]
4508                        ),
4509                        (
4510                            format!("{}", dir.join("b.sol").display()),
4511                            vec!["B1".to_string(), "B2".to_string()]
4512                        ),
4513                    ]),
4514                    engine: Some(ModelCheckerEngine::CHC),
4515                    targets: Some(vec![
4516                        ModelCheckerTarget::Assert,
4517                        ModelCheckerTarget::OutOfBounds
4518                    ]),
4519                    timeout: Some(10000),
4520                    invariants: None,
4521                    show_unproved: None,
4522                    div_mod_with_slacks: None,
4523                    solvers: None,
4524                    show_unsupported: None,
4525                    show_proved_safe: None,
4526                })
4527            );
4528
4529            Ok(())
4530        });
4531    }
4532
4533    #[test]
4534    fn test_fmt_config() {
4535        figment::Jail::expect_with(|jail| {
4536            jail.create_file(
4537                "foundry.toml",
4538                r"
4539                [fmt]
4540                line_length = 100
4541                tab_width = 2
4542                bracket_spacing = true
4543            ",
4544            )?;
4545            let loaded = Config::load().sanitized();
4546            assert_eq!(
4547                loaded.fmt,
4548                FormatterConfig {
4549                    line_length: 100,
4550                    tab_width: 2,
4551                    bracket_spacing: true,
4552                    ..Default::default()
4553                }
4554            );
4555
4556            Ok(())
4557        });
4558    }
4559
4560    #[test]
4561    fn test_invariant_config() {
4562        figment::Jail::expect_with(|jail| {
4563            jail.create_file(
4564                "foundry.toml",
4565                r"
4566                [invariant]
4567                runs = 512
4568                depth = 10
4569            ",
4570            )?;
4571
4572            let loaded = Config::load().sanitized();
4573            assert_eq!(
4574                loaded.invariant,
4575                InvariantConfig {
4576                    runs: 512,
4577                    depth: 10,
4578                    failure_persist_dir: Some(PathBuf::from("cache/invariant")),
4579                    ..Default::default()
4580                }
4581            );
4582
4583            Ok(())
4584        });
4585    }
4586
4587    #[test]
4588    fn test_standalone_sections_env() {
4589        figment::Jail::expect_with(|jail| {
4590            jail.create_file(
4591                "foundry.toml",
4592                r"
4593                [fuzz]
4594                runs = 100
4595
4596                [invariant]
4597                depth = 1
4598            ",
4599            )?;
4600
4601            jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
4602            jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
4603            jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
4604
4605            let config = Config::load();
4606            assert_eq!(config.fmt.line_length, 95);
4607            assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
4608            assert_eq!(config.invariant.depth, 5);
4609
4610            Ok(())
4611        });
4612    }
4613
4614    #[test]
4615    fn test_parse_with_profile() {
4616        let foundry_str = r"
4617            [profile.default]
4618            src = 'src'
4619            out = 'out'
4620            libs = ['lib']
4621
4622            # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
4623        ";
4624        assert_eq!(
4625            parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
4626            (
4627                Config::DEFAULT_PROFILE,
4628                BasicConfig {
4629                    profile: Config::DEFAULT_PROFILE,
4630                    src: "src".into(),
4631                    out: "out".into(),
4632                    libs: vec!["lib".into()],
4633                    remappings: vec![]
4634                }
4635            )
4636        );
4637    }
4638
4639    #[test]
4640    fn test_implicit_profile_loads() {
4641        figment::Jail::expect_with(|jail| {
4642            jail.create_file(
4643                "foundry.toml",
4644                r"
4645                [default]
4646                src = 'my-src'
4647                out = 'my-out'
4648            ",
4649            )?;
4650            let loaded = Config::load().sanitized();
4651            assert_eq!(loaded.src.file_name().unwrap(), "my-src");
4652            assert_eq!(loaded.out.file_name().unwrap(), "my-out");
4653            assert_eq!(
4654                loaded.warnings,
4655                vec![Warning::UnknownSection {
4656                    unknown_section: Profile::new("default"),
4657                    source: Some("foundry.toml".into())
4658                }]
4659            );
4660
4661            Ok(())
4662        });
4663    }
4664
4665    #[test]
4666    fn test_etherscan_api_key() {
4667        figment::Jail::expect_with(|jail| {
4668            jail.create_file(
4669                "foundry.toml",
4670                r"
4671                [default]
4672            ",
4673            )?;
4674            jail.set_env("ETHERSCAN_API_KEY", "");
4675            let loaded = Config::load().sanitized();
4676            assert!(loaded.etherscan_api_key.is_none());
4677
4678            jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
4679            let loaded = Config::load().sanitized();
4680            assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));
4681
4682            Ok(())
4683        });
4684    }
4685
4686    #[test]
4687    fn test_etherscan_api_key_figment() {
4688        figment::Jail::expect_with(|jail| {
4689            jail.create_file(
4690                "foundry.toml",
4691                r"
4692                [default]
4693                etherscan_api_key = 'DUMMY'
4694            ",
4695            )?;
4696            jail.set_env("ETHERSCAN_API_KEY", "ETHER");
4697
4698            let figment = Config::figment_with_root(jail.directory())
4699                .merge(("etherscan_api_key", "USER_KEY"));
4700
4701            let loaded = Config::from_provider(figment);
4702            assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));
4703
4704            Ok(())
4705        });
4706    }
4707
4708    #[test]
4709    fn test_normalize_defaults() {
4710        figment::Jail::expect_with(|jail| {
4711            jail.create_file(
4712                "foundry.toml",
4713                r"
4714                [default]
4715                solc = '0.8.13'
4716            ",
4717            )?;
4718
4719            let loaded = Config::load().sanitized();
4720            assert_eq!(loaded.evm_version, EvmVersion::London);
4721            Ok(())
4722        });
4723    }
4724
4725    // a test to print the config, mainly used to update the example config in the README
4726    #[test]
4727    #[ignore]
4728    fn print_config() {
4729        let config = Config {
4730            optimizer_details: Some(OptimizerDetails {
4731                peephole: None,
4732                inliner: None,
4733                jumpdest_remover: None,
4734                order_literals: None,
4735                deduplicate: None,
4736                cse: None,
4737                constant_optimizer: Some(true),
4738                yul: Some(true),
4739                yul_details: Some(YulDetails {
4740                    stack_allocation: None,
4741                    optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
4742                }),
4743                simple_counter_for_loop_unchecked_increment: None,
4744            }),
4745            ..Default::default()
4746        };
4747        println!("{}", config.to_string_pretty().unwrap());
4748    }
4749
4750    #[test]
4751    #[allow(unknown_lints, non_local_definitions)]
4752    fn can_use_impl_figment_macro() {
4753        #[derive(Default, Serialize)]
4754        struct MyArgs {
4755            #[serde(skip_serializing_if = "Option::is_none")]
4756            root: Option<PathBuf>,
4757        }
4758        impl_figment_convert!(MyArgs);
4759
4760        impl Provider for MyArgs {
4761            fn metadata(&self) -> Metadata {
4762                Metadata::default()
4763            }
4764
4765            fn data(&self) -> Result<Map<Profile, Dict>, Error> {
4766                let value = Value::serialize(self)?;
4767                let error = InvalidType(value.to_actual(), "map".into());
4768                let dict = value.into_dict().ok_or(error)?;
4769                Ok(Map::from([(Config::selected_profile(), dict)]))
4770            }
4771        }
4772
4773        let _figment: Figment = From::from(&MyArgs::default());
4774        let _config: Config = From::from(&MyArgs::default());
4775
4776        #[derive(Default)]
4777        struct Outer {
4778            start: MyArgs,
4779            other: MyArgs,
4780            another: MyArgs,
4781        }
4782        impl_figment_convert!(Outer, start, other, another);
4783
4784        let _figment: Figment = From::from(&Outer::default());
4785        let _config: Config = From::from(&Outer::default());
4786    }
4787
4788    #[test]
4789    fn list_cached_blocks() -> eyre::Result<()> {
4790        fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
4791            let block_path = chain_path.join(block_number);
4792            fs::create_dir(block_path.as_path()).unwrap();
4793            let file_path = block_path.join("storage.json");
4794            let mut file = File::create(file_path).unwrap();
4795            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4796        }
4797
4798        fn fake_block_cache_block_path_as_file(
4799            chain_path: &Path,
4800            block_number: &str,
4801            size_bytes: usize,
4802        ) {
4803            let block_path = chain_path.join(block_number);
4804            let mut file = File::create(block_path).unwrap();
4805            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4806        }
4807
4808        let chain_dir = tempdir()?;
4809
4810        fake_block_cache(chain_dir.path(), "1", 100);
4811        fake_block_cache(chain_dir.path(), "2", 500);
4812        fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
4813        // Pollution file that should not show up in the cached block
4814        let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
4815        writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
4816
4817        let result = Config::get_cached_blocks(chain_dir.path())?;
4818
4819        assert_eq!(result.len(), 3);
4820        let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
4821        let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
4822        let block3 = &result.iter().find(|x| x.0 == "3").unwrap();
4823
4824        assert_eq!(block1.0, "1");
4825        assert_eq!(block1.1, 100);
4826        assert_eq!(block2.0, "2");
4827        assert_eq!(block2.1, 500);
4828        assert_eq!(block3.0, "3");
4829        assert_eq!(block3.1, 900);
4830
4831        chain_dir.close()?;
4832        Ok(())
4833    }
4834
4835    #[test]
4836    fn list_etherscan_cache() -> eyre::Result<()> {
4837        fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
4838            let metadata_path = chain_path.join("sources");
4839            let abi_path = chain_path.join("abi");
4840            let _ = fs::create_dir(metadata_path.as_path());
4841            let _ = fs::create_dir(abi_path.as_path());
4842
4843            let metadata_file_path = metadata_path.join(address);
4844            let mut metadata_file = File::create(metadata_file_path).unwrap();
4845            writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4846                .unwrap();
4847
4848            let abi_file_path = abi_path.join(address);
4849            let mut abi_file = File::create(abi_file_path).unwrap();
4850            writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4851                .unwrap();
4852        }
4853
4854        let chain_dir = tempdir()?;
4855
4856        fake_etherscan_cache(chain_dir.path(), "1", 100);
4857        fake_etherscan_cache(chain_dir.path(), "2", 500);
4858
4859        let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
4860
4861        assert_eq!(result, 600);
4862
4863        chain_dir.close()?;
4864        Ok(())
4865    }
4866
4867    #[test]
4868    fn test_parse_error_codes() {
4869        figment::Jail::expect_with(|jail| {
4870            jail.create_file(
4871                "foundry.toml",
4872                r#"
4873                [default]
4874                ignored_error_codes = ["license", "unreachable", 1337]
4875            "#,
4876            )?;
4877
4878            let config = Config::load();
4879            assert_eq!(
4880                config.ignored_error_codes,
4881                vec![
4882                    SolidityErrorCode::SpdxLicenseNotProvided,
4883                    SolidityErrorCode::Unreachable,
4884                    SolidityErrorCode::Other(1337)
4885                ]
4886            );
4887
4888            Ok(())
4889        });
4890    }
4891
4892    #[test]
4893    fn test_parse_file_paths() {
4894        figment::Jail::expect_with(|jail| {
4895            jail.create_file(
4896                "foundry.toml",
4897                r#"
4898                [default]
4899                ignored_warnings_from = ["something"]
4900            "#,
4901            )?;
4902
4903            let config = Config::load();
4904            assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);
4905
4906            Ok(())
4907        });
4908    }
4909
4910    #[test]
4911    fn test_parse_optimizer_settings() {
4912        figment::Jail::expect_with(|jail| {
4913            jail.create_file(
4914                "foundry.toml",
4915                r"
4916                [default]
4917                [profile.default.optimizer_details]
4918            ",
4919            )?;
4920
4921            let config = Config::load();
4922            assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
4923
4924            Ok(())
4925        });
4926    }
4927
4928    #[test]
4929    fn test_parse_labels() {
4930        figment::Jail::expect_with(|jail| {
4931            jail.create_file(
4932                "foundry.toml",
4933                r#"
4934                [labels]
4935                0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
4936                0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
4937            "#,
4938            )?;
4939
4940            let config = Config::load();
4941            assert_eq!(
4942                config.labels,
4943                HashMap::from_iter(vec![
4944                    (
4945                        Address::from_str("0x1F98431c8aD98523631AE4a59f267346ea31F984").unwrap(),
4946                        "Uniswap V3: Factory".to_string()
4947                    ),
4948                    (
4949                        Address::from_str("0xC36442b4a4522E871399CD717aBDD847Ab11FE88").unwrap(),
4950                        "Uniswap V3: Positions NFT".to_string()
4951                    ),
4952                ])
4953            );
4954
4955            Ok(())
4956        });
4957    }
4958
4959    #[test]
4960    fn test_parse_vyper() {
4961        figment::Jail::expect_with(|jail| {
4962            jail.create_file(
4963                "foundry.toml",
4964                r#"
4965                [vyper]
4966                optimize = "codesize"
4967                path = "/path/to/vyper"
4968            "#,
4969            )?;
4970
4971            let config = Config::load();
4972            assert_eq!(
4973                config.vyper,
4974                VyperConfig {
4975                    optimize: Some(VyperOptimizationMode::Codesize),
4976                    path: Some("/path/to/vyper".into())
4977                }
4978            );
4979
4980            Ok(())
4981        });
4982    }
4983}