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