Skip to main content

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