foundry_config/
lib.rs

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