foundry_config/
lib.rs

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