1#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8#[macro_use]
9extern crate tracing;
10
11use crate::cache::StorageCachingConfig;
12use alloy_primitives::{Address, B256, FixedBytes, U256, address, map::AddressHashMap};
13use eyre::{ContextCompat, WrapErr};
14use figment::{
15 Error, Figment, Metadata, Profile, Provider,
16 providers::{Env, Format, Serialized, Toml},
17 value::{Dict, Map, Value},
18};
19use filter::GlobMatcher;
20use foundry_compilers::{
21 ArtifactOutput, ConfigurableArtifacts, Graph, Project, ProjectPathsConfig,
22 RestrictionsWithVersion, VyperLanguage,
23 artifacts::{
24 BytecodeHash, DebuggingSettings, EvmVersion, Libraries, ModelCheckerSettings,
25 ModelCheckerTarget, Optimizer, OptimizerDetails, RevertStrings, Settings, SettingsMetadata,
26 Severity,
27 output_selection::{ContractOutputSelection, OutputSelection},
28 remappings::{RelativeRemapping, Remapping},
29 serde_helpers,
30 },
31 cache::SOLIDITY_FILES_CACHE_FILENAME,
32 compilers::{
33 Compiler,
34 multi::{MultiCompiler, MultiCompilerSettings, SolidityCompiler},
35 solc::{Solc, SolcCompiler},
36 vyper::{Vyper, VyperSettings},
37 },
38 error::SolcError,
39 multi::{MultiCompilerParsedSource, MultiCompilerRestrictions},
40 resolc::Resolc,
41 solc::{CliSettings, SolcSettings},
42};
43use regex::Regex;
44use revm::primitives::hardfork::SpecId;
45use semver::Version;
46use serde::{Deserialize, Serialize, Serializer};
47use std::{
48 borrow::Cow,
49 collections::BTreeMap,
50 fs,
51 path::{Path, PathBuf},
52 str::FromStr,
53};
54
55mod macros;
56
57pub mod utils;
58pub use utils::*;
59
60mod endpoints;
61pub use endpoints::{
62 ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl, RpcEndpoints,
63};
64
65mod etherscan;
66use etherscan::{
67 EtherscanConfigError, EtherscanConfigs, EtherscanEnvProvider, ResolvedEtherscanConfig,
68};
69
70mod resolve;
71pub use resolve::UnresolvedEnvVarError;
72
73pub mod cache;
74use cache::{Cache, ChainCache};
75
76pub mod fmt;
77pub use fmt::FormatterConfig;
78
79pub mod lint;
80pub use lint::{LinterConfig, Severity as LintSeverity};
81
82pub mod fs_permissions;
83pub use fs_permissions::FsPermissions;
84use fs_permissions::PathPermission;
85
86pub mod error;
87use error::ExtractConfigError;
88pub use error::SolidityErrorCode;
89
90pub mod doc;
91pub use doc::DocConfig;
92
93pub mod filter;
94pub use filter::SkipBuildFilters;
95
96mod warning;
97pub use warning::*;
98
99pub mod fix;
100
101pub use alloy_chains::{Chain, NamedChain};
103pub use figment;
104use foundry_block_explorers::EtherscanApiVersion;
105
106pub mod providers;
107pub use providers::Remappings;
108use providers::*;
109
110mod fuzz;
111pub use fuzz::{FuzzConfig, FuzzDictionaryConfig};
112
113mod invariant;
114pub use invariant::InvariantConfig;
115
116mod inline;
117pub use inline::{InlineConfig, InlineConfigError, NatSpec};
118
119pub mod soldeer;
120use soldeer::{SoldeerConfig, SoldeerDependencyConfig};
121
122mod vyper;
123pub use vyper::VyperConfig;
124
125mod bind_json;
126use bind_json::BindJsonConfig;
127
128mod compilation;
129pub use compilation::{CompilationRestrictions, SettingsOverrides};
130
131pub mod revive;
132use revive::PolkadotConfig;
133
134#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
166pub struct Config {
167 #[serde(skip)]
174 pub profile: Profile,
175 #[serde(skip)]
179 pub profiles: Vec<Profile>,
180
181 #[serde(default = "root_default", skip_serializing)]
186 pub root: PathBuf,
187
188 pub src: PathBuf,
190 pub test: PathBuf,
192 pub script: PathBuf,
194 pub out: PathBuf,
196 pub libs: Vec<PathBuf>,
198 pub remappings: Vec<RelativeRemapping>,
200 pub auto_detect_remappings: bool,
202 pub libraries: Vec<String>,
204 pub cache: bool,
206 pub dynamic_test_linking: bool,
208 pub cache_path: PathBuf,
210 pub snapshots: PathBuf,
212 pub gas_snapshot_check: bool,
214 pub gas_snapshot_emit: bool,
216 pub broadcast: PathBuf,
218 pub allow_paths: Vec<PathBuf>,
220 pub include_paths: Vec<PathBuf>,
222 pub skip: Vec<GlobMatcher>,
224 pub force: bool,
226 #[serde(with = "from_str_lowercase")]
228 pub evm_version: EvmVersion,
229 pub gas_reports: Vec<String>,
231 pub gas_reports_ignore: Vec<String>,
233 pub gas_reports_include_tests: bool,
235 #[doc(hidden)]
245 pub solc: Option<SolcReq>,
246 pub auto_detect_solc: bool,
248 pub offline: bool,
255 pub optimizer: Option<bool>,
257 pub optimizer_runs: Option<usize>,
268 pub optimizer_details: Option<OptimizerDetails>,
272 pub model_checker: Option<ModelCheckerSettings>,
274 pub verbosity: u8,
276 pub eth_rpc_url: Option<String>,
278 pub eth_rpc_accept_invalid_certs: bool,
280 pub eth_rpc_jwt: Option<String>,
282 pub eth_rpc_timeout: Option<u64>,
284 pub eth_rpc_headers: Option<Vec<String>>,
293 pub etherscan_api_key: Option<String>,
295 pub etherscan_api_version: Option<EtherscanApiVersion>,
297 #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
299 pub etherscan: EtherscanConfigs,
300 pub ignored_error_codes: Vec<SolidityErrorCode>,
302 #[serde(rename = "ignored_warnings_from")]
304 pub ignored_file_paths: Vec<PathBuf>,
305 pub deny_warnings: bool,
307 #[serde(rename = "match_test")]
309 pub test_pattern: Option<RegexWrapper>,
310 #[serde(rename = "no_match_test")]
312 pub test_pattern_inverse: Option<RegexWrapper>,
313 #[serde(rename = "match_contract")]
315 pub contract_pattern: Option<RegexWrapper>,
316 #[serde(rename = "no_match_contract")]
318 pub contract_pattern_inverse: Option<RegexWrapper>,
319 #[serde(rename = "match_path", with = "from_opt_glob")]
321 pub path_pattern: Option<globset::Glob>,
322 #[serde(rename = "no_match_path", with = "from_opt_glob")]
324 pub path_pattern_inverse: Option<globset::Glob>,
325 #[serde(rename = "no_match_coverage")]
327 pub coverage_pattern_inverse: Option<RegexWrapper>,
328 pub test_failures_file: PathBuf,
330 pub threads: Option<usize>,
332 pub show_progress: bool,
334 pub fuzz: FuzzConfig,
336 pub invariant: InvariantConfig,
338 pub ffi: bool,
340 pub allow_internal_expect_revert: bool,
342 pub always_use_create_2_factory: bool,
344 pub prompt_timeout: u64,
346 pub sender: Address,
348 pub tx_origin: Address,
350 pub initial_balance: U256,
352 #[serde(
354 deserialize_with = "crate::deserialize_u64_to_u256",
355 serialize_with = "crate::serialize_u64_or_u256"
356 )]
357 pub block_number: U256,
358 pub fork_block_number: Option<u64>,
360 #[serde(rename = "chain_id", alias = "chain")]
362 pub chain: Option<Chain>,
363 pub gas_limit: GasLimit,
365 pub code_size_limit: Option<usize>,
367 pub gas_price: Option<u64>,
372 pub block_base_fee_per_gas: u64,
374 pub block_coinbase: Address,
376 #[serde(
378 deserialize_with = "crate::deserialize_u64_to_u256",
379 serialize_with = "crate::serialize_u64_or_u256"
380 )]
381 pub block_timestamp: U256,
382 pub block_difficulty: u64,
384 pub block_prevrandao: B256,
386 pub block_gas_limit: Option<GasLimit>,
388 pub memory_limit: u64,
393 #[serde(default)]
410 pub extra_output: Vec<ContractOutputSelection>,
411 #[serde(default)]
422 pub extra_output_files: Vec<ContractOutputSelection>,
423 pub names: bool,
425 pub sizes: bool,
427 pub via_ir: bool,
430 pub ast: bool,
432 pub rpc_storage_caching: StorageCachingConfig,
434 pub no_storage_caching: bool,
437 pub no_rpc_rate_limit: bool,
440 #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
442 pub rpc_endpoints: RpcEndpoints,
443 pub use_literal_content: bool,
445 #[serde(with = "from_str_lowercase")]
449 pub bytecode_hash: BytecodeHash,
450 pub cbor_metadata: bool,
455 #[serde(with = "serde_helpers::display_from_str_opt")]
457 pub revert_strings: Option<RevertStrings>,
458 pub sparse_mode: bool,
463 pub build_info: bool,
466 pub build_info_path: Option<PathBuf>,
468 pub fmt: FormatterConfig,
470 pub lint: LinterConfig,
472 pub doc: DocConfig,
474 pub bind_json: BindJsonConfig,
476 pub fs_permissions: FsPermissions,
480
481 pub isolate: bool,
485
486 pub disable_block_gas_limit: bool,
488
489 pub labels: AddressHashMap<String>,
491
492 pub unchecked_cheatcode_artifacts: bool,
495
496 pub create2_library_salt: B256,
498
499 pub create2_deployer: Address,
501
502 pub vyper: VyperConfig,
504
505 pub dependencies: Option<SoldeerDependencyConfig>,
507
508 pub soldeer: Option<SoldeerConfig>,
510
511 pub assertions_revert: bool,
515
516 pub legacy_assertions: bool,
518
519 #[serde(default, skip_serializing_if = "Vec::is_empty")]
521 pub extra_args: Vec<String>,
522
523 #[serde(alias = "alphanet")]
525 pub odyssey: bool,
526
527 pub transaction_timeout: u64,
529
530 #[serde(rename = "__warnings", default, skip_serializing)]
532 pub warnings: Vec<Warning>,
533
534 #[serde(default)]
536 pub additional_compiler_profiles: Vec<SettingsOverrides>,
537
538 #[serde(default)]
540 pub compilation_restrictions: Vec<CompilationRestrictions>,
541
542 pub script_execution_protection: bool,
544
545 #[doc(hidden)]
554 #[serde(skip)]
555 pub _non_exhaustive: (),
556 pub polkadot: PolkadotConfig,
558}
559
560pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
562
563pub const DEPRECATIONS: &[(&str, &str)] = &[("cancun", "evm_version = Cancun")];
567
568impl Config {
569 pub const DEFAULT_PROFILE: Profile = Profile::Default;
571
572 pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
574
575 pub const PROFILE_SECTION: &'static str = "profile";
577
578 pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
580 "rpc_endpoints",
581 "etherscan",
582 "fmt",
583 "lint",
584 "doc",
585 "fuzz",
586 "invariant",
587 "labels",
588 "dependencies",
589 "soldeer",
590 "vyper",
591 "bind_json",
592 ];
593
594 pub const FILE_NAME: &'static str = "foundry.toml";
596
597 pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
599
600 pub const DEFAULT_SENDER: Address = address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
604
605 pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;
607
608 pub const DEFAULT_CREATE2_DEPLOYER: Address =
610 address!("0x4e59b44847b379578588920ca78fbf26c0b4956c");
611
612 pub fn load() -> Result<Self, ExtractConfigError> {
616 Self::from_provider(Self::figment())
617 }
618
619 pub fn load_with_providers(providers: FigmentProviders) -> Result<Self, ExtractConfigError> {
623 Self::from_provider(Self::default().to_figment(providers))
624 }
625
626 #[track_caller]
630 pub fn load_with_root(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
631 Self::from_provider(Self::figment_with_root(root.as_ref()))
632 }
633
634 #[doc(alias = "try_from")]
649 pub fn from_provider<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
650 trace!("load config with provider: {:?}", provider.metadata());
651 Self::from_figment(Figment::from(provider))
652 }
653
654 #[doc(hidden)]
655 #[deprecated(note = "use `Config::from_provider` instead")]
656 pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
657 Self::from_provider(provider)
658 }
659
660 fn from_figment(figment: Figment) -> Result<Self, ExtractConfigError> {
661 let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
662 config.profile = figment.profile().clone();
663
664 let mut add_profile = |profile: &Profile| {
666 if !config.profiles.contains(profile) {
667 config.profiles.push(profile.clone());
668 }
669 };
670 let figment = figment.select(Self::PROFILE_SECTION);
671 if let Ok(data) = figment.data()
672 && let Some(profiles) = data.get(&Profile::new(Self::PROFILE_SECTION))
673 {
674 for profile in profiles.keys() {
675 add_profile(&Profile::new(profile));
676 }
677 }
678 add_profile(&Self::DEFAULT_PROFILE);
679 add_profile(&config.profile);
680
681 config.normalize_optimizer_settings();
682
683 Ok(config)
684 }
685
686 pub fn to_figment(&self, providers: FigmentProviders) -> Figment {
691 if providers.is_none() {
694 return Figment::from(self);
695 }
696
697 let root = self.root.as_path();
698 let profile = Self::selected_profile();
699 let mut figment = Figment::default().merge(DappHardhatDirProvider(root));
700
701 if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
703 figment = Self::merge_toml_provider(
704 figment,
705 TomlFileProvider::new(None, global_toml).cached(),
706 profile.clone(),
707 );
708 }
709 figment = Self::merge_toml_provider(
711 figment,
712 TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.join(Self::FILE_NAME)).cached(),
713 profile.clone(),
714 );
715
716 figment = figment
718 .merge(
719 Env::prefixed("DAPP_")
720 .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
721 .global(),
722 )
723 .merge(
724 Env::prefixed("DAPP_TEST_")
725 .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
726 .global(),
727 )
728 .merge(DappEnvCompatProvider)
729 .merge(EtherscanEnvProvider::default())
730 .merge(
731 Env::prefixed("FOUNDRY_")
732 .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
733 .map(|key| {
734 let key = key.as_str();
735 if Self::STANDALONE_SECTIONS.iter().any(|section| {
736 key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
737 }) {
738 key.replacen('_', ".", 1).into()
739 } else {
740 key.into()
741 }
742 })
743 .global(),
744 )
745 .select(profile.clone());
746
747 if providers.is_all() {
749 let remappings = RemappingsProvider {
753 auto_detect_remappings: figment
754 .extract_inner::<bool>("auto_detect_remappings")
755 .unwrap_or(true),
756 lib_paths: figment
757 .extract_inner::<Vec<PathBuf>>("libs")
758 .map(Cow::Owned)
759 .unwrap_or_else(|_| Cow::Borrowed(&self.libs)),
760 root,
761 remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
762 };
763 figment = figment.merge(remappings);
764 }
765
766 figment = self.normalize_defaults(figment);
768
769 Figment::from(self).merge(figment).select(profile)
770 }
771
772 #[must_use]
777 pub fn canonic(self) -> Self {
778 let root = self.root.clone();
779 self.canonic_at(root)
780 }
781
782 #[must_use]
800 pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
801 let root = canonic(root);
802
803 fn p(root: &Path, rem: &Path) -> PathBuf {
804 canonic(root.join(rem))
805 }
806
807 self.src = p(&root, &self.src);
808 self.test = p(&root, &self.test);
809 self.script = p(&root, &self.script);
810 self.out = p(&root, &self.out);
811 self.broadcast = p(&root, &self.broadcast);
812 self.cache_path = p(&root, &self.cache_path);
813 self.snapshots = p(&root, &self.snapshots);
814
815 if let Some(build_info_path) = self.build_info_path {
816 self.build_info_path = Some(p(&root, &build_info_path));
817 }
818
819 self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
820
821 self.remappings =
822 self.remappings.into_iter().map(|r| RelativeRemapping::new(r.into(), &root)).collect();
823
824 self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
825
826 self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
827
828 self.fs_permissions.join_all(&root);
829
830 if let Some(model_checker) = &mut self.model_checker {
831 model_checker.contracts = std::mem::take(&mut model_checker.contracts)
832 .into_iter()
833 .map(|(path, contracts)| {
834 (format!("{}", p(&root, path.as_ref()).display()), contracts)
835 })
836 .collect();
837 }
838
839 self
840 }
841
842 pub fn normalized_evm_version(mut self) -> Self {
844 self.normalize_evm_version();
845 self
846 }
847
848 pub fn normalized_optimizer_settings(mut self) -> Self {
851 self.normalize_optimizer_settings();
852 self
853 }
854
855 pub fn normalize_evm_version(&mut self) {
857 self.evm_version = self.get_normalized_evm_version();
858 }
859
860 pub fn normalize_optimizer_settings(&mut self) {
865 match (self.optimizer, self.optimizer_runs) {
866 (None, None) => {
868 self.optimizer = Some(false);
869 self.optimizer_runs = Some(200);
870 }
871 (Some(_), None) => self.optimizer_runs = Some(200),
873 (None, Some(runs)) => self.optimizer = Some(runs > 0),
875 _ => {}
876 }
877 }
878
879 pub fn get_normalized_evm_version(&self) -> EvmVersion {
881 if let Some(version) = self.solc_version()
882 && let Some(evm_version) = self.evm_version.normalize_version_solc(&version)
883 {
884 return evm_version;
885 }
886 self.evm_version
887 }
888
889 #[must_use]
894 pub fn sanitized(self) -> Self {
895 let mut config = self.canonic();
896
897 config.sanitize_remappings();
898
899 config.libs.sort_unstable();
900 config.libs.dedup();
901
902 config
903 }
904
905 pub fn sanitize_remappings(&mut self) {
909 #[cfg(target_os = "windows")]
910 {
911 use path_slash::PathBufExt;
913 self.remappings.iter_mut().for_each(|r| {
914 r.path.path = r.path.path.to_slash_lossy().into_owned().into();
915 });
916 }
917 }
918
919 pub fn install_lib_dir(&self) -> &Path {
923 self.libs
924 .iter()
925 .find(|p| !p.ends_with("node_modules"))
926 .map(|p| p.as_path())
927 .unwrap_or_else(|| Path::new("lib"))
928 }
929
930 pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
945 self.create_project(self.cache, false)
946 }
947
948 pub fn ephemeral_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
951 self.create_project(false, true)
952 }
953
954 fn additional_settings(
956 &self,
957 base: &MultiCompilerSettings,
958 ) -> BTreeMap<String, MultiCompilerSettings> {
959 let mut map = BTreeMap::new();
960
961 for profile in &self.additional_compiler_profiles {
962 let mut settings = base.clone();
963 profile.apply(&mut settings);
964 map.insert(profile.name.clone(), settings);
965 }
966
967 map
968 }
969
970 #[expect(clippy::disallowed_macros)]
972 fn restrictions(
973 &self,
974 paths: &ProjectPathsConfig,
975 ) -> Result<BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>>, SolcError>
976 {
977 let mut map = BTreeMap::new();
978 if self.compilation_restrictions.is_empty() {
979 return Ok(BTreeMap::new());
980 }
981
982 let graph = Graph::<MultiCompilerParsedSource>::resolve(paths)?;
983 let (sources, _) = graph.into_sources();
984
985 for res in &self.compilation_restrictions {
986 for source in sources.keys().filter(|path| {
987 if res.paths.is_match(path) {
988 true
989 } else if let Ok(path) = path.strip_prefix(&paths.root) {
990 res.paths.is_match(path)
991 } else {
992 false
993 }
994 }) {
995 let res: RestrictionsWithVersion<_> =
996 res.clone().try_into().map_err(SolcError::msg)?;
997 if !map.contains_key(source) {
998 map.insert(source.clone(), res);
999 } else {
1000 let value = map.remove(source.as_path()).unwrap();
1001 if let Some(merged) = value.clone().merge(res) {
1002 map.insert(source.clone(), merged);
1003 } else {
1004 eprintln!(
1006 "{}",
1007 yansi::Paint::yellow(&format!(
1008 "Failed to merge compilation restrictions for {}",
1009 source.display()
1010 ))
1011 );
1012 map.insert(source.clone(), value);
1013 }
1014 }
1015 }
1016 }
1017
1018 Ok(map)
1019 }
1020
1021 pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
1025 let settings = self.compiler_settings()?;
1026 let paths = if self.polkadot.resolc_compile {
1027 PolkadotConfig::project_paths(self)
1028 } else {
1029 self.project_paths()
1030 };
1031 let mut builder = Project::builder()
1032 .artifacts(self.configured_artifacts_handler())
1033 .additional_settings(self.additional_settings(&settings))
1034 .restrictions(self.restrictions(&paths)?)
1035 .settings(settings)
1036 .paths(paths)
1037 .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
1038 .ignore_paths(self.ignored_file_paths.clone())
1039 .set_compiler_severity_filter(if self.deny_warnings {
1040 Severity::Warning
1041 } else {
1042 Severity::Error
1043 })
1044 .set_offline(self.offline)
1045 .set_cached(cached)
1046 .set_build_info(!no_artifacts && self.build_info)
1047 .set_no_artifacts(no_artifacts);
1048
1049 if !self.skip.is_empty() {
1050 let filter = SkipBuildFilters::new(self.skip.clone(), self.root.clone());
1051 builder = builder.sparse_output(filter);
1052 }
1053
1054 let project = builder.build(self.compiler()?)?;
1055
1056 if self.force {
1057 self.cleanup(&project)?;
1058 }
1059
1060 Ok(project)
1061 }
1062
1063 pub fn cleanup<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>>(
1065 &self,
1066 project: &Project<C, T>,
1067 ) -> Result<(), SolcError> {
1068 project.cleanup()?;
1069
1070 let _ = fs::remove_file(&self.test_failures_file);
1072
1073 let remove_test_dir = |test_dir: &Option<PathBuf>| {
1075 if let Some(test_dir) = test_dir {
1076 let path = project.root().join(test_dir);
1077 if path.exists() {
1078 let _ = fs::remove_dir_all(&path);
1079 }
1080 }
1081 };
1082 remove_test_dir(&self.fuzz.failure_persist_dir);
1083 remove_test_dir(&self.invariant.corpus_dir);
1084 remove_test_dir(&self.invariant.failure_persist_dir);
1085
1086 Ok(())
1087 }
1088
1089 fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
1096 if let Some(solc) = &self.solc {
1097 let solc = match solc {
1098 SolcReq::Version(version) => {
1099 if let Some(solc) = Solc::find_svm_installed_version(version)? {
1100 solc
1101 } else {
1102 if self.offline {
1103 return Err(SolcError::msg(format!(
1104 "can't install missing solc {version} in offline mode"
1105 )));
1106 }
1107 Solc::blocking_install(version)?
1108 }
1109 }
1110 SolcReq::Local(solc) => {
1111 if !solc.is_file() {
1112 return Err(SolcError::msg(format!(
1113 "`solc` {} does not exist",
1114 solc.display()
1115 )));
1116 }
1117 Solc::new(solc)?
1118 }
1119 };
1120 return Ok(Some(solc));
1121 }
1122
1123 Ok(None)
1124 }
1125
1126 #[inline]
1128 pub fn evm_spec_id(&self) -> SpecId {
1129 evm_spec_id(self.evm_version, self.odyssey)
1130 }
1131
1132 pub fn is_auto_detect(&self) -> bool {
1137 if self.solc.is_some() {
1138 return false;
1139 }
1140 self.auto_detect_solc
1141 }
1142
1143 pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
1145 !self.no_storage_caching
1146 && self.rpc_storage_caching.enable_for_chain_id(chain_id.into())
1147 && self.rpc_storage_caching.enable_for_endpoint(endpoint)
1148 }
1149
1150 pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
1165 let mut builder = ProjectPathsConfig::builder()
1166 .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
1167 .sources(&self.src)
1168 .tests(&self.test)
1169 .scripts(&self.script)
1170 .artifacts(&self.out)
1171 .libs(self.libs.iter())
1172 .remappings(self.get_all_remappings())
1173 .allowed_path(&self.root)
1174 .allowed_paths(&self.libs)
1175 .allowed_paths(&self.allow_paths)
1176 .include_paths(&self.include_paths);
1177
1178 if let Some(build_info_path) = &self.build_info_path {
1179 builder = builder.build_infos(build_info_path);
1180 }
1181
1182 builder.build_with_root(&self.root)
1183 }
1184
1185 pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
1187 if let Some(solc) = self.ensure_solc()? {
1188 Ok(SolcCompiler::Specific(solc))
1189 } else {
1190 Ok(SolcCompiler::AutoDetect)
1191 }
1192 }
1193
1194 pub fn solc_version(&self) -> Option<Version> {
1196 self.solc.as_ref().and_then(|solc| solc.try_version().ok())
1197 }
1198
1199 pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
1201 if !self.project_paths::<VyperLanguage>().has_input_files() {
1203 return Ok(None);
1204 }
1205 let vyper = if let Some(path) = &self.vyper.path {
1206 Some(Vyper::new(path)?)
1207 } else {
1208 Vyper::new("vyper").ok()
1209 };
1210 Ok(vyper)
1211 }
1212
1213 pub fn resolc_compiler(&self) -> Result<Resolc, SolcError> {
1215 let solc_compiler = self.solc_compiler()?;
1216 match &self.polkadot.resolc {
1217 Some(SolcReq::Local(path)) => {
1218 if !path.is_file() {
1219 return Err(SolcError::msg(format!(
1220 "`resolc` {} does not exist",
1221 path.display()
1222 )));
1223 }
1224 Resolc::new(path, solc_compiler)
1225 }
1226
1227 Some(SolcReq::Version(v)) => {
1228 if let Some(resolc) = Resolc::find_installed(v, solc_compiler.clone())? {
1229 Ok(resolc)
1230 } else {
1231 if self.offline {
1232 return Err(SolcError::msg(format!(
1233 "can't install missing resolc with version requirement {v} in offline mode"
1234 )));
1235 }
1236 Resolc::find_or_install(v, solc_compiler)
1237 }
1238 }
1239 None => {
1240 if self.offline {
1241 Resolc::new("resolc", solc_compiler)
1242 } else {
1243 Resolc::install(None, solc_compiler)
1244 }
1245 }
1246 }
1247 }
1248
1249 pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
1251 Ok(MultiCompiler {
1252 solidity: if self.polkadot.resolc_compile {
1253 SolidityCompiler::Resolc(self.resolc_compiler()?)
1254 } else {
1255 SolidityCompiler::Solc(self.solc_compiler()?)
1256 },
1257 vyper: self.vyper_compiler()?,
1258 })
1259 }
1260
1261 pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
1263 Ok(MultiCompilerSettings {
1264 solc: if self.polkadot.resolc_compile {
1265 PolkadotConfig::resolc_settings(self)?
1266 } else {
1267 self.solc_settings()?
1268 },
1269 vyper: self.vyper_settings()?,
1270 })
1271 }
1272
1273 pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
1275 self.remappings.iter().map(|m| m.clone().into())
1276 }
1277
1278 pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
1293 Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
1294 }
1295
1296 pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1312 let maybe_alias = self.eth_rpc_url.as_ref().or(self.etherscan_api_key.as_ref())?;
1313 if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
1314 Some(alias)
1315 } else {
1316 Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
1317 }
1318 }
1319
1320 pub fn get_rpc_url_with_alias(
1345 &self,
1346 maybe_alias: &str,
1347 ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1348 let mut endpoints = self.rpc_endpoints.clone().resolved();
1349 if let Some(endpoint) = endpoints.remove(maybe_alias) {
1350 return Some(endpoint.url().map(Cow::Owned));
1351 }
1352
1353 if let Some(mesc_url) = self.get_rpc_url_from_mesc(maybe_alias) {
1354 return Some(Ok(Cow::Owned(mesc_url)));
1355 }
1356
1357 None
1358 }
1359
1360 pub fn get_rpc_url_from_mesc(&self, maybe_alias: &str) -> Option<String> {
1362 let mesc_config = mesc::load::load_config_data()
1365 .inspect_err(|err| debug!(%err, "failed to load mesc config"))
1366 .ok()?;
1367
1368 if let Ok(Some(endpoint)) =
1369 mesc::query::get_endpoint_by_query(&mesc_config, maybe_alias, Some("foundry"))
1370 {
1371 return Some(endpoint.url);
1372 }
1373
1374 if maybe_alias.chars().all(|c| c.is_numeric()) {
1375 if let Ok(Some(endpoint)) =
1381 mesc::query::get_endpoint_by_network(&mesc_config, maybe_alias, Some("foundry"))
1382 {
1383 return Some(endpoint.url);
1384 }
1385 }
1386
1387 None
1388 }
1389
1390 pub fn get_rpc_url_or<'a>(
1402 &'a self,
1403 fallback: impl Into<Cow<'a, str>>,
1404 ) -> Result<Cow<'a, str>, UnresolvedEnvVarError> {
1405 if let Some(url) = self.get_rpc_url() { url } else { Ok(fallback.into()) }
1406 }
1407
1408 pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1420 self.get_rpc_url_or("http://localhost:8545")
1421 }
1422
1423 pub fn get_etherscan_config(
1443 &self,
1444 ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
1445 self.get_etherscan_config_with_chain(None).transpose()
1446 }
1447
1448 pub fn get_etherscan_config_with_chain(
1455 &self,
1456 chain: Option<Chain>,
1457 ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
1458 let default_api_version = self.etherscan_api_version.unwrap_or_default();
1459
1460 if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref())
1461 && self.etherscan.contains_key(maybe_alias)
1462 {
1463 return self
1464 .etherscan
1465 .clone()
1466 .resolved(default_api_version)
1467 .remove(maybe_alias)
1468 .transpose();
1469 }
1470
1471 if let Some(res) = chain.or(self.chain).and_then(|chain| {
1473 self.etherscan.clone().resolved(default_api_version).find_chain(chain)
1474 }) {
1475 match (res, self.etherscan_api_key.as_ref()) {
1476 (Ok(mut config), Some(key)) => {
1477 config.key.clone_from(key);
1480 return Ok(Some(config));
1481 }
1482 (Ok(config), None) => return Ok(Some(config)),
1483 (Err(err), None) => return Err(err),
1484 (Err(_), Some(_)) => {
1485 }
1487 }
1488 }
1489
1490 if let Some(key) = self.etherscan_api_key.as_ref() {
1492 return Ok(ResolvedEtherscanConfig::create(
1493 key,
1494 chain.or(self.chain).unwrap_or_default(),
1495 default_api_version,
1496 ));
1497 }
1498 Ok(None)
1499 }
1500
1501 pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
1507 self.get_etherscan_config_with_chain(chain).ok().flatten().map(|c| c.key)
1508 }
1509
1510 pub fn get_etherscan_api_version(&self, chain: Option<Chain>) -> EtherscanApiVersion {
1514 self.get_etherscan_config_with_chain(chain)
1515 .ok()
1516 .flatten()
1517 .map(|c| c.api_version)
1518 .unwrap_or_default()
1519 }
1520
1521 pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
1528 get_dir_remapping(&self.src)
1529 }
1530
1531 pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
1533 if self.root.join(&self.test).exists() { get_dir_remapping(&self.test) } else { None }
1534 }
1535
1536 pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
1538 if self.root.join(&self.script).exists() { get_dir_remapping(&self.script) } else { None }
1539 }
1540
1541 pub fn optimizer(&self) -> Optimizer {
1547 Optimizer {
1548 enabled: self.optimizer,
1549 runs: self.optimizer_runs,
1550 details: self.optimizer_details.clone(),
1553 }
1554 }
1555
1556 pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
1559 let mut extra_output = self.extra_output.clone();
1560
1561 if !extra_output.contains(&ContractOutputSelection::Metadata) {
1567 extra_output.push(ContractOutputSelection::Metadata);
1568 }
1569
1570 ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().copied())
1571 }
1572
1573 pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
1576 Libraries::parse(&self.libraries)
1577 }
1578
1579 pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
1581 let paths: ProjectPathsConfig = self.project_paths();
1582 Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
1583 }
1584
1585 pub fn solc_settings(&self) -> Result<SolcSettings, SolcError> {
1590 let mut model_checker = self.model_checker.clone();
1594 if let Some(model_checker_settings) = &mut model_checker
1595 && model_checker_settings.targets.is_none()
1596 {
1597 model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
1598 }
1599
1600 let mut settings = Settings {
1601 libraries: self.libraries_with_remappings()?,
1602 optimizer: self.optimizer(),
1603 evm_version: Some(self.evm_version),
1604 metadata: Some(SettingsMetadata {
1605 use_literal_content: Some(self.use_literal_content),
1606 bytecode_hash: if self.polkadot.resolc_compile {
1607 Some(BytecodeHash::None)
1609 } else {
1610 Some(self.bytecode_hash)
1611 },
1612 cbor_metadata: Some(self.cbor_metadata),
1613 }),
1614 debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
1615 revert_strings: Some(revert_strings),
1616 debug_info: Vec::new(),
1618 }),
1619 model_checker,
1620 via_ir: Some(self.via_ir),
1621 stop_after: None,
1623 remappings: Vec::new(),
1625 output_selection: Default::default(),
1627 }
1628 .with_extra_output(self.configured_artifacts_handler().output_selection());
1629
1630 if self.ast || self.build_info {
1632 settings = settings.with_ast();
1633 }
1634
1635 let cli_settings =
1636 CliSettings { extra_args: self.extra_args.clone(), ..Default::default() };
1637
1638 Ok(SolcSettings { settings, cli_settings, ..Default::default() })
1639 }
1640
1641 pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
1644 Ok(VyperSettings {
1645 evm_version: Some(self.evm_version),
1646 optimize: self.vyper.optimize,
1647 bytecode_metadata: None,
1648 output_selection: OutputSelection::common_output_selection([
1651 "abi".to_string(),
1652 "evm.bytecode".to_string(),
1653 "evm.deployedBytecode".to_string(),
1654 ]),
1655 search_paths: None,
1656 experimental_codegen: self.vyper.experimental_codegen,
1657 })
1658 }
1659
1660 pub fn figment() -> Figment {
1681 Self::default().into()
1682 }
1683
1684 pub fn figment_with_root(root: impl AsRef<Path>) -> Figment {
1696 Self::with_root(root.as_ref()).into()
1697 }
1698
1699 #[doc(hidden)]
1700 #[track_caller]
1701 pub fn figment_with_root_opt(root: Option<&Path>) -> Figment {
1702 let root = match root {
1703 Some(root) => root,
1704 None => &find_project_root(None).expect("could not determine project root"),
1705 };
1706 Self::figment_with_root(root)
1707 }
1708
1709 pub fn with_root(root: impl AsRef<Path>) -> Self {
1718 Self::_with_root(root.as_ref())
1719 }
1720
1721 fn _with_root(root: &Path) -> Self {
1722 let paths = ProjectPathsConfig::builder().build_with_root::<()>(root);
1724 let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
1725 Self {
1726 root: paths.root,
1727 src: paths.sources.file_name().unwrap().into(),
1728 out: artifacts.clone(),
1729 libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(),
1730 remappings: paths
1731 .remappings
1732 .into_iter()
1733 .map(|r| RelativeRemapping::new(r, root))
1734 .collect(),
1735 fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
1736 ..Self::default()
1737 }
1738 }
1739
1740 pub fn hardhat() -> Self {
1742 Self {
1743 src: "contracts".into(),
1744 out: "artifacts".into(),
1745 libs: vec!["node_modules".into()],
1746 ..Self::default()
1747 }
1748 }
1749
1750 pub fn dapptools() -> Self {
1752 Self {
1753 chain: Some(Chain::from_id(99)),
1754 block_timestamp: U256::ZERO,
1755 block_number: U256::ZERO,
1756 ..Self::default()
1757 }
1758 }
1759
1760 pub fn into_basic(self) -> BasicConfig {
1769 BasicConfig {
1770 profile: self.profile,
1771 src: self.src,
1772 out: self.out,
1773 libs: self.libs,
1774 remappings: self.remappings,
1775 }
1776 }
1777
1778 pub fn update_at<F>(root: &Path, f: F) -> eyre::Result<()>
1783 where
1784 F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
1785 {
1786 let config = Self::load_with_root(root)?.sanitized();
1787 config.update(|doc| f(&config, doc))
1788 }
1789
1790 pub fn update<F>(&self, f: F) -> eyre::Result<()>
1795 where
1796 F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
1797 {
1798 let file_path = self.get_config_path();
1799 if !file_path.exists() {
1800 return Ok(());
1801 }
1802 let contents = fs::read_to_string(&file_path)?;
1803 let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
1804 if f(&mut doc) {
1805 fs::write(file_path, doc.to_string())?;
1806 }
1807 Ok(())
1808 }
1809
1810 pub fn update_libs(&self) -> eyre::Result<()> {
1816 self.update(|doc| {
1817 let profile = self.profile.as_str().as_str();
1818 let root = &self.root;
1819 let libs: toml_edit::Value = self
1820 .libs
1821 .iter()
1822 .map(|path| {
1823 let path =
1824 if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
1825 toml_edit::Value::from(&*path.to_string_lossy())
1826 })
1827 .collect();
1828 let libs = toml_edit::value(libs);
1829 doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
1830 true
1831 })
1832 }
1833
1834 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
1846 let mut value = toml::Value::try_from(self)?;
1848 let value_table = value.as_table_mut().unwrap();
1850 let standalone_sections = Self::STANDALONE_SECTIONS
1852 .iter()
1853 .filter_map(|section| {
1854 let section = section.to_string();
1855 value_table.remove(§ion).map(|value| (section, value))
1856 })
1857 .collect::<Vec<_>>();
1858 let mut wrapping_table = [(
1860 Self::PROFILE_SECTION.into(),
1861 toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
1862 )]
1863 .into_iter()
1864 .collect::<toml::map::Map<_, _>>();
1865 for (section, value) in standalone_sections {
1867 wrapping_table.insert(section, value);
1868 }
1869 toml::to_string_pretty(&toml::Value::Table(wrapping_table))
1871 }
1872
1873 pub fn get_config_path(&self) -> PathBuf {
1875 self.root.join(Self::FILE_NAME)
1876 }
1877
1878 pub fn selected_profile() -> Profile {
1882 #[cfg(test)]
1884 {
1885 Self::force_selected_profile()
1886 }
1887 #[cfg(not(test))]
1888 {
1889 static CACHE: std::sync::OnceLock<Profile> = std::sync::OnceLock::new();
1890 CACHE.get_or_init(Self::force_selected_profile).clone()
1891 }
1892 }
1893
1894 fn force_selected_profile() -> Profile {
1895 Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
1896 }
1897
1898 pub fn foundry_dir_toml() -> Option<PathBuf> {
1900 Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
1901 }
1902
1903 pub fn foundry_dir() -> Option<PathBuf> {
1905 dirs::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
1906 }
1907
1908 pub fn foundry_cache_dir() -> Option<PathBuf> {
1910 Self::foundry_dir().map(|p| p.join("cache"))
1911 }
1912
1913 pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
1915 Some(Self::foundry_cache_dir()?.join("rpc"))
1916 }
1917 pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1919 Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
1920 }
1921
1922 pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
1924 Some(Self::foundry_cache_dir()?.join("etherscan"))
1925 }
1926
1927 pub fn foundry_keystores_dir() -> Option<PathBuf> {
1929 Some(Self::foundry_dir()?.join("keystores"))
1930 }
1931
1932 pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1935 Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
1936 }
1937
1938 pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1941 Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
1942 }
1943
1944 pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1947 Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
1948 }
1949
1950 pub fn data_dir() -> eyre::Result<PathBuf> {
1958 let path = dirs::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
1959 std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
1960 Ok(path)
1961 }
1962
1963 pub fn find_config_file() -> Option<PathBuf> {
1970 fn find(path: &Path) -> Option<PathBuf> {
1971 if path.is_absolute() {
1972 return match path.is_file() {
1973 true => Some(path.to_path_buf()),
1974 false => None,
1975 };
1976 }
1977 let cwd = std::env::current_dir().ok()?;
1978 let mut cwd = cwd.as_path();
1979 loop {
1980 let file_path = cwd.join(path);
1981 if file_path.is_file() {
1982 return Some(file_path);
1983 }
1984 cwd = cwd.parent()?;
1985 }
1986 }
1987 find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
1988 .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
1989 }
1990
1991 pub fn clean_foundry_cache() -> eyre::Result<()> {
1993 if let Some(cache_dir) = Self::foundry_cache_dir() {
1994 let path = cache_dir.as_path();
1995 let _ = fs::remove_dir_all(path);
1996 } else {
1997 eyre::bail!("failed to get foundry_cache_dir");
1998 }
1999
2000 Ok(())
2001 }
2002
2003 pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<()> {
2005 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2006 let path = cache_dir.as_path();
2007 let _ = fs::remove_dir_all(path);
2008 } else {
2009 eyre::bail!("failed to get foundry_chain_cache_dir");
2010 }
2011
2012 Ok(())
2013 }
2014
2015 pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<()> {
2017 if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
2018 let path = cache_dir.as_path();
2019 let _ = fs::remove_dir_all(path);
2020 } else {
2021 eyre::bail!("failed to get foundry_block_cache_dir");
2022 }
2023
2024 Ok(())
2025 }
2026
2027 pub fn clean_foundry_etherscan_cache() -> eyre::Result<()> {
2029 if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
2030 let path = cache_dir.as_path();
2031 let _ = fs::remove_dir_all(path);
2032 } else {
2033 eyre::bail!("failed to get foundry_etherscan_cache_dir");
2034 }
2035
2036 Ok(())
2037 }
2038
2039 pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<()> {
2041 if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
2042 let path = cache_dir.as_path();
2043 let _ = fs::remove_dir_all(path);
2044 } else {
2045 eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
2046 }
2047
2048 Ok(())
2049 }
2050
2051 pub fn list_foundry_cache() -> eyre::Result<Cache> {
2053 if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
2054 let mut cache = Cache { chains: vec![] };
2055 if !cache_dir.exists() {
2056 return Ok(cache);
2057 }
2058 if let Ok(entries) = cache_dir.as_path().read_dir() {
2059 for entry in entries.flatten().filter(|x| x.path().is_dir()) {
2060 match Chain::from_str(&entry.file_name().to_string_lossy()) {
2061 Ok(chain) => cache.chains.push(Self::list_foundry_chain_cache(chain)?),
2062 Err(_) => continue,
2063 }
2064 }
2065 Ok(cache)
2066 } else {
2067 eyre::bail!("failed to access foundry_cache_dir");
2068 }
2069 } else {
2070 eyre::bail!("failed to get foundry_cache_dir");
2071 }
2072 }
2073
2074 pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
2076 let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
2077 Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
2078 None => {
2079 warn!("failed to access foundry_etherscan_chain_cache_dir");
2080 0
2081 }
2082 };
2083
2084 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2085 let blocks = Self::get_cached_blocks(&cache_dir)?;
2086 Ok(ChainCache {
2087 name: chain.to_string(),
2088 blocks,
2089 block_explorer: block_explorer_data_size,
2090 })
2091 } else {
2092 eyre::bail!("failed to get foundry_chain_cache_dir");
2093 }
2094 }
2095
2096 fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
2098 let mut blocks = vec![];
2099 if !chain_path.exists() {
2100 return Ok(blocks);
2101 }
2102 for block in chain_path.read_dir()?.flatten() {
2103 let file_type = block.file_type()?;
2104 let file_name = block.file_name();
2105 let filepath = if file_type.is_dir() {
2106 block.path().join("storage.json")
2107 } else if file_type.is_file()
2108 && file_name.to_string_lossy().chars().all(char::is_numeric)
2109 {
2110 block.path()
2111 } else {
2112 continue;
2113 };
2114 blocks.push((file_name.to_string_lossy().into_owned(), fs::metadata(filepath)?.len()));
2115 }
2116 Ok(blocks)
2117 }
2118
2119 fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
2121 if !chain_path.exists() {
2122 return Ok(0);
2123 }
2124
2125 fn dir_size_recursive(mut dir: fs::ReadDir) -> eyre::Result<u64> {
2126 dir.try_fold(0, |acc, file| {
2127 let file = file?;
2128 let size = match file.metadata()? {
2129 data if data.is_dir() => dir_size_recursive(fs::read_dir(file.path())?)?,
2130 data => data.len(),
2131 };
2132 Ok(acc + size)
2133 })
2134 }
2135
2136 dir_size_recursive(fs::read_dir(chain_path)?)
2137 }
2138
2139 fn merge_toml_provider(
2140 mut figment: Figment,
2141 toml_provider: impl Provider,
2142 profile: Profile,
2143 ) -> Figment {
2144 figment = figment.select(profile.clone());
2145
2146 figment = {
2148 let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
2149 figment.merge(warnings)
2150 };
2151
2152 let mut profiles = vec![Self::DEFAULT_PROFILE];
2154 if profile != Self::DEFAULT_PROFILE {
2155 profiles.push(profile.clone());
2156 }
2157 let provider = toml_provider.strict_select(profiles);
2158
2159 let provider = &BackwardsCompatTomlProvider(ForcedSnakeCaseData(provider));
2161
2162 if profile != Self::DEFAULT_PROFILE {
2164 figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
2165 }
2166 for standalone_key in Self::STANDALONE_SECTIONS {
2168 if let Some((_, fallback)) =
2169 STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
2170 {
2171 figment = figment.merge(
2172 provider
2173 .fallback(standalone_key, fallback)
2174 .wrap(profile.clone(), standalone_key),
2175 );
2176 } else {
2177 figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
2178 }
2179 }
2180 figment = figment.merge(provider);
2182 figment
2183 }
2184
2185 fn normalize_defaults(&self, mut figment: Figment) -> Figment {
2191 if figment.contains("evm_version") {
2193 return figment;
2194 }
2195
2196 if let Ok(solc) = figment.extract_inner::<SolcReq>("solc")
2198 && let Some(version) = solc
2199 .try_version()
2200 .ok()
2201 .and_then(|version| self.evm_version.normalize_version_solc(&version))
2202 {
2203 figment = figment.merge(("evm_version", version));
2204 }
2205
2206 figment
2207 }
2208}
2209
2210impl From<Config> for Figment {
2211 fn from(c: Config) -> Self {
2212 (&c).into()
2213 }
2214}
2215impl From<&Config> for Figment {
2216 fn from(c: &Config) -> Self {
2217 c.to_figment(FigmentProviders::All)
2218 }
2219}
2220
2221#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2223pub enum FigmentProviders {
2224 #[default]
2226 All,
2227 Cast,
2231 Anvil,
2235 None,
2237}
2238
2239impl FigmentProviders {
2240 pub const fn is_all(&self) -> bool {
2242 matches!(self, Self::All)
2243 }
2244
2245 pub const fn is_cast(&self) -> bool {
2247 matches!(self, Self::Cast)
2248 }
2249
2250 pub const fn is_anvil(&self) -> bool {
2252 matches!(self, Self::Anvil)
2253 }
2254
2255 pub const fn is_none(&self) -> bool {
2257 matches!(self, Self::None)
2258 }
2259}
2260
2261#[derive(Clone, Debug, Serialize, Deserialize)]
2263#[serde(transparent)]
2264pub struct RegexWrapper {
2265 #[serde(with = "serde_regex")]
2266 inner: regex::Regex,
2267}
2268
2269impl std::ops::Deref for RegexWrapper {
2270 type Target = regex::Regex;
2271
2272 fn deref(&self) -> &Self::Target {
2273 &self.inner
2274 }
2275}
2276
2277impl std::cmp::PartialEq for RegexWrapper {
2278 fn eq(&self, other: &Self) -> bool {
2279 self.as_str() == other.as_str()
2280 }
2281}
2282
2283impl Eq for RegexWrapper {}
2284
2285impl From<RegexWrapper> for regex::Regex {
2286 fn from(wrapper: RegexWrapper) -> Self {
2287 wrapper.inner
2288 }
2289}
2290
2291impl From<regex::Regex> for RegexWrapper {
2292 fn from(re: Regex) -> Self {
2293 Self { inner: re }
2294 }
2295}
2296
2297mod serde_regex {
2298 use regex::Regex;
2299 use serde::{Deserialize, Deserializer, Serializer};
2300
2301 pub(crate) fn serialize<S>(value: &Regex, serializer: S) -> Result<S::Ok, S::Error>
2302 where
2303 S: Serializer,
2304 {
2305 serializer.serialize_str(value.as_str())
2306 }
2307
2308 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
2309 where
2310 D: Deserializer<'de>,
2311 {
2312 let s = String::deserialize(deserializer)?;
2313 Regex::new(&s).map_err(serde::de::Error::custom)
2314 }
2315}
2316
2317pub(crate) mod from_opt_glob {
2319 use serde::{Deserialize, Deserializer, Serializer};
2320
2321 pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
2322 where
2323 S: Serializer,
2324 {
2325 match value {
2326 Some(glob) => serializer.serialize_str(glob.glob()),
2327 None => serializer.serialize_none(),
2328 }
2329 }
2330
2331 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
2332 where
2333 D: Deserializer<'de>,
2334 {
2335 let s: Option<String> = Option::deserialize(deserializer)?;
2336 if let Some(s) = s {
2337 return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?));
2338 }
2339 Ok(None)
2340 }
2341}
2342
2343pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
2354 s: &str,
2355) -> Result<Option<(Profile, T)>, Error> {
2356 let figment = Config::merge_toml_provider(
2357 Figment::new(),
2358 Toml::string(s).nested(),
2359 Config::DEFAULT_PROFILE,
2360 );
2361 if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
2362 Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
2363 } else {
2364 Ok(None)
2365 }
2366}
2367
2368impl Provider for Config {
2369 fn metadata(&self) -> Metadata {
2370 Metadata::named("Foundry Config")
2371 }
2372
2373 #[track_caller]
2374 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2375 let mut data = Serialized::defaults(self).data()?;
2376 if let Some(entry) = data.get_mut(&self.profile) {
2377 entry.insert("root".to_string(), Value::serialize(self.root.clone())?);
2378 }
2379 Ok(data)
2380 }
2381
2382 fn profile(&self) -> Option<Profile> {
2383 Some(self.profile.clone())
2384 }
2385}
2386
2387impl Default for Config {
2388 fn default() -> Self {
2389 Self {
2390 profile: Self::DEFAULT_PROFILE,
2391 profiles: vec![Self::DEFAULT_PROFILE],
2392 fs_permissions: FsPermissions::new([PathPermission::read("out")]),
2393 isolate: cfg!(feature = "isolate-by-default"),
2394 root: root_default(),
2395 src: "src".into(),
2396 test: "test".into(),
2397 script: "script".into(),
2398 out: "out".into(),
2399 libs: vec!["lib".into()],
2400 cache: true,
2401 dynamic_test_linking: false,
2402 cache_path: "cache".into(),
2403 broadcast: "broadcast".into(),
2404 snapshots: "snapshots".into(),
2405 gas_snapshot_check: false,
2406 gas_snapshot_emit: true,
2407 allow_paths: vec![],
2408 include_paths: vec![],
2409 force: false,
2410 evm_version: EvmVersion::Cancun,
2411 gas_reports: vec!["*".to_string()],
2412 gas_reports_ignore: vec![],
2413 gas_reports_include_tests: false,
2414 solc: None,
2415 vyper: Default::default(),
2416 auto_detect_solc: true,
2417 offline: false,
2418 optimizer: None,
2419 optimizer_runs: None,
2420 optimizer_details: None,
2421 model_checker: None,
2422 extra_output: Default::default(),
2423 extra_output_files: Default::default(),
2424 names: false,
2425 sizes: false,
2426 test_pattern: None,
2427 test_pattern_inverse: None,
2428 contract_pattern: None,
2429 contract_pattern_inverse: None,
2430 path_pattern: None,
2431 path_pattern_inverse: None,
2432 coverage_pattern_inverse: None,
2433 test_failures_file: "cache/test-failures".into(),
2434 threads: None,
2435 show_progress: false,
2436 fuzz: FuzzConfig::new("cache/fuzz".into()),
2437 invariant: InvariantConfig::new("cache/invariant".into()),
2438 always_use_create_2_factory: false,
2439 ffi: false,
2440 allow_internal_expect_revert: false,
2441 prompt_timeout: 120,
2442 sender: Self::DEFAULT_SENDER,
2443 tx_origin: Self::DEFAULT_SENDER,
2444 initial_balance: U256::from((1u128 << 96) - 1),
2445 block_number: U256::from(1),
2446 fork_block_number: None,
2447 chain: None,
2448 gas_limit: (1u64 << 30).into(), code_size_limit: None,
2450 gas_price: None,
2451 block_base_fee_per_gas: 0,
2452 block_coinbase: Address::ZERO,
2453 block_timestamp: U256::from(1),
2454 block_difficulty: 0,
2455 block_prevrandao: Default::default(),
2456 block_gas_limit: None,
2457 disable_block_gas_limit: false,
2458 memory_limit: 1 << 27, eth_rpc_url: None,
2460 eth_rpc_accept_invalid_certs: false,
2461 eth_rpc_jwt: None,
2462 eth_rpc_timeout: None,
2463 eth_rpc_headers: None,
2464 etherscan_api_key: None,
2465 etherscan_api_version: None,
2466 verbosity: 0,
2467 remappings: vec![],
2468 auto_detect_remappings: true,
2469 libraries: vec![],
2470 ignored_error_codes: vec![
2471 SolidityErrorCode::SpdxLicenseNotProvided,
2472 SolidityErrorCode::ContractExceeds24576Bytes,
2473 SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
2474 SolidityErrorCode::TransientStorageUsed,
2475 ],
2476 ignored_file_paths: vec![],
2477 deny_warnings: false,
2478 via_ir: false,
2479 ast: false,
2480 rpc_storage_caching: Default::default(),
2481 rpc_endpoints: Default::default(),
2482 etherscan: Default::default(),
2483 no_storage_caching: false,
2484 no_rpc_rate_limit: false,
2485 use_literal_content: false,
2486 bytecode_hash: BytecodeHash::Ipfs,
2487 cbor_metadata: true,
2488 revert_strings: None,
2489 sparse_mode: false,
2490 build_info: false,
2491 build_info_path: None,
2492 fmt: Default::default(),
2493 lint: Default::default(),
2494 doc: Default::default(),
2495 bind_json: Default::default(),
2496 labels: Default::default(),
2497 unchecked_cheatcode_artifacts: false,
2498 create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
2499 create2_deployer: Self::DEFAULT_CREATE2_DEPLOYER,
2500 skip: vec![],
2501 dependencies: Default::default(),
2502 soldeer: Default::default(),
2503 assertions_revert: true,
2504 legacy_assertions: false,
2505 warnings: vec![],
2506 extra_args: vec![],
2507 odyssey: false,
2508 transaction_timeout: 120,
2509 additional_compiler_profiles: Default::default(),
2510 compilation_restrictions: Default::default(),
2511 script_execution_protection: true,
2512 _non_exhaustive: (),
2513 polkadot: Default::default(),
2514 }
2515 }
2516}
2517
2518#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
2524pub struct GasLimit(#[serde(deserialize_with = "crate::deserialize_u64_or_max")] pub u64);
2525
2526impl From<u64> for GasLimit {
2527 fn from(gas: u64) -> Self {
2528 Self(gas)
2529 }
2530}
2531
2532impl From<GasLimit> for u64 {
2533 fn from(gas: GasLimit) -> Self {
2534 gas.0
2535 }
2536}
2537
2538impl Serialize for GasLimit {
2539 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2540 where
2541 S: Serializer,
2542 {
2543 if self.0 == u64::MAX {
2544 serializer.serialize_str("max")
2545 } else if self.0 > i64::MAX as u64 {
2546 serializer.serialize_str(&self.0.to_string())
2547 } else {
2548 serializer.serialize_u64(self.0)
2549 }
2550 }
2551}
2552
2553#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2555#[serde(untagged)]
2556pub enum SolcReq {
2557 Version(Version),
2560 Local(PathBuf),
2562}
2563
2564impl SolcReq {
2565 fn try_version(&self) -> Result<Version, SolcError> {
2570 match self {
2571 Self::Version(version) => Ok(version.clone()),
2572 Self::Local(path) => Solc::new(path).map(|solc| solc.version),
2573 }
2574 }
2575}
2576
2577impl<T: AsRef<str>> From<T> for SolcReq {
2578 fn from(s: T) -> Self {
2579 let s = s.as_ref();
2580 if let Ok(v) = Version::from_str(s) { Self::Version(v) } else { Self::Local(s.into()) }
2581 }
2582}
2583
2584#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2596pub struct BasicConfig {
2597 #[serde(skip)]
2599 pub profile: Profile,
2600 pub src: PathBuf,
2602 pub out: PathBuf,
2604 pub libs: Vec<PathBuf>,
2606 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2608 pub remappings: Vec<RelativeRemapping>,
2609}
2610
2611impl BasicConfig {
2612 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2616 let s = toml::to_string_pretty(self)?;
2617 Ok(format!(
2618 "\
2619[profile.{}]
2620{s}
2621# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n",
2622 self.profile
2623 ))
2624 }
2625}
2626
2627pub(crate) mod from_str_lowercase {
2628 use serde::{Deserialize, Deserializer, Serializer};
2629 use std::str::FromStr;
2630
2631 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
2632 where
2633 T: std::fmt::Display,
2634 S: Serializer,
2635 {
2636 serializer.collect_str(&value.to_string().to_lowercase())
2637 }
2638
2639 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
2640 where
2641 D: Deserializer<'de>,
2642 T: FromStr,
2643 T::Err: std::fmt::Display,
2644 {
2645 String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
2646 }
2647}
2648
2649fn canonic(path: impl Into<PathBuf>) -> PathBuf {
2650 let path = path.into();
2651 foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
2652}
2653
2654fn root_default() -> PathBuf {
2655 ".".into()
2656}
2657
2658#[cfg(test)]
2659mod tests {
2660 use super::*;
2661 use crate::{
2662 cache::{CachedChains, CachedEndpoints},
2663 endpoints::RpcEndpointType,
2664 etherscan::ResolvedEtherscanConfigs,
2665 fmt::IndentStyle,
2666 };
2667 use NamedChain::Moonbeam;
2668 use endpoints::{RpcAuth, RpcEndpointConfig};
2669 use figment::error::Kind::InvalidType;
2670 use foundry_compilers::artifacts::{
2671 ModelCheckerEngine, YulDetails, vyper::VyperOptimizationMode,
2672 };
2673 use similar_asserts::assert_eq;
2674 use soldeer_core::remappings::RemappingsLocation;
2675 use std::{fs::File, io::Write};
2676 use tempfile::tempdir;
2677
2678 fn clear_warning(config: &mut Config) {
2681 config.warnings = vec![];
2682 }
2683
2684 #[test]
2685 fn default_sender() {
2686 assert_eq!(Config::DEFAULT_SENDER, address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38"));
2687 }
2688
2689 #[test]
2690 fn test_caching() {
2691 let mut config = Config::default();
2692 let chain_id = NamedChain::Mainnet;
2693 let url = "https://eth-mainnet.alchemyapi";
2694 assert!(config.enable_caching(url, chain_id));
2695
2696 config.no_storage_caching = true;
2697 assert!(!config.enable_caching(url, chain_id));
2698
2699 config.no_storage_caching = false;
2700 assert!(!config.enable_caching(url, NamedChain::Dev));
2701 }
2702
2703 #[test]
2704 fn test_install_dir() {
2705 figment::Jail::expect_with(|jail| {
2706 let config = Config::load().unwrap();
2707 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2708 jail.create_file(
2709 "foundry.toml",
2710 r"
2711 [profile.default]
2712 libs = ['node_modules', 'lib']
2713 ",
2714 )?;
2715 let config = Config::load().unwrap();
2716 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2717
2718 jail.create_file(
2719 "foundry.toml",
2720 r"
2721 [profile.default]
2722 libs = ['custom', 'node_modules', 'lib']
2723 ",
2724 )?;
2725 let config = Config::load().unwrap();
2726 assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
2727
2728 Ok(())
2729 });
2730 }
2731
2732 #[test]
2733 fn test_figment_is_default() {
2734 figment::Jail::expect_with(|_| {
2735 let mut default: Config = Config::figment().extract()?;
2736 let default2 = Config::default();
2737 default.profile = default2.profile.clone();
2738 default.profiles = default2.profiles.clone();
2739 assert_eq!(default, default2);
2740 Ok(())
2741 });
2742 }
2743
2744 #[test]
2745 fn figment_profiles() {
2746 figment::Jail::expect_with(|jail| {
2747 jail.create_file(
2748 "foundry.toml",
2749 r"
2750 [foo.baz]
2751 libs = ['node_modules', 'lib']
2752
2753 [profile.default]
2754 libs = ['node_modules', 'lib']
2755
2756 [profile.ci]
2757 libs = ['node_modules', 'lib']
2758
2759 [profile.local]
2760 libs = ['node_modules', 'lib']
2761 ",
2762 )?;
2763
2764 let config = crate::Config::load().unwrap();
2765 let expected: &[figment::Profile] = &["ci".into(), "default".into(), "local".into()];
2766 assert_eq!(config.profiles, expected);
2767
2768 Ok(())
2769 });
2770 }
2771
2772 #[test]
2773 fn test_default_round_trip() {
2774 figment::Jail::expect_with(|_| {
2775 let original = Config::figment();
2776 let roundtrip = Figment::from(Config::from_provider(&original).unwrap());
2777 for figment in &[original, roundtrip] {
2778 let config = Config::from_provider(figment).unwrap();
2779 assert_eq!(config, Config::default().normalized_optimizer_settings());
2780 }
2781 Ok(())
2782 });
2783 }
2784
2785 #[test]
2786 fn ffi_env_disallowed() {
2787 figment::Jail::expect_with(|jail| {
2788 jail.set_env("FOUNDRY_FFI", "true");
2789 jail.set_env("FFI", "true");
2790 jail.set_env("DAPP_FFI", "true");
2791 let config = Config::load().unwrap();
2792 assert!(!config.ffi);
2793
2794 Ok(())
2795 });
2796 }
2797
2798 #[test]
2799 fn test_profile_env() {
2800 figment::Jail::expect_with(|jail| {
2801 jail.set_env("FOUNDRY_PROFILE", "default");
2802 let figment = Config::figment();
2803 assert_eq!(figment.profile(), "default");
2804
2805 jail.set_env("FOUNDRY_PROFILE", "hardhat");
2806 let figment: Figment = Config::hardhat().into();
2807 assert_eq!(figment.profile(), "hardhat");
2808
2809 jail.create_file(
2810 "foundry.toml",
2811 r"
2812 [profile.default]
2813 libs = ['lib']
2814 [profile.local]
2815 libs = ['modules']
2816 ",
2817 )?;
2818 jail.set_env("FOUNDRY_PROFILE", "local");
2819 let config = Config::load().unwrap();
2820 assert_eq!(config.libs, vec![PathBuf::from("modules")]);
2821
2822 Ok(())
2823 });
2824 }
2825
2826 #[test]
2827 fn test_default_test_path() {
2828 figment::Jail::expect_with(|_| {
2829 let config = Config::default();
2830 let paths_config = config.project_paths::<Solc>();
2831 assert_eq!(paths_config.tests, PathBuf::from(r"test"));
2832 Ok(())
2833 });
2834 }
2835
2836 #[test]
2837 fn test_default_libs() {
2838 figment::Jail::expect_with(|jail| {
2839 let config = Config::load().unwrap();
2840 assert_eq!(config.libs, vec![PathBuf::from("lib")]);
2841
2842 fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
2843 let config = Config::load().unwrap();
2844 assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
2845
2846 fs::create_dir_all(jail.directory().join("lib")).unwrap();
2847 let config = Config::load().unwrap();
2848 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2849
2850 Ok(())
2851 });
2852 }
2853
2854 #[test]
2855 fn test_inheritance_from_default_test_path() {
2856 figment::Jail::expect_with(|jail| {
2857 jail.create_file(
2858 "foundry.toml",
2859 r#"
2860 [profile.default]
2861 test = "defaulttest"
2862 src = "defaultsrc"
2863 libs = ['lib', 'node_modules']
2864
2865 [profile.custom]
2866 src = "customsrc"
2867 "#,
2868 )?;
2869
2870 let config = Config::load().unwrap();
2871 assert_eq!(config.src, PathBuf::from("defaultsrc"));
2872 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2873
2874 jail.set_env("FOUNDRY_PROFILE", "custom");
2875 let config = Config::load().unwrap();
2876 assert_eq!(config.src, PathBuf::from("customsrc"));
2877 assert_eq!(config.test, PathBuf::from("defaulttest"));
2878 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2879
2880 Ok(())
2881 });
2882 }
2883
2884 #[test]
2885 fn test_custom_test_path() {
2886 figment::Jail::expect_with(|jail| {
2887 jail.create_file(
2888 "foundry.toml",
2889 r#"
2890 [profile.default]
2891 test = "mytest"
2892 "#,
2893 )?;
2894
2895 let config = Config::load().unwrap();
2896 let paths_config = config.project_paths::<Solc>();
2897 assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
2898 Ok(())
2899 });
2900 }
2901
2902 #[test]
2903 fn test_remappings() {
2904 figment::Jail::expect_with(|jail| {
2905 jail.create_file(
2906 "foundry.toml",
2907 r#"
2908 [profile.default]
2909 src = "some-source"
2910 out = "some-out"
2911 cache = true
2912 "#,
2913 )?;
2914 let config = Config::load().unwrap();
2915 assert!(config.remappings.is_empty());
2916
2917 jail.create_file(
2918 "remappings.txt",
2919 r"
2920 file-ds-test/=lib/ds-test/
2921 file-other/=lib/other/
2922 ",
2923 )?;
2924
2925 let config = Config::load().unwrap();
2926 assert_eq!(
2927 config.remappings,
2928 vec![
2929 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
2930 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
2931 ],
2932 );
2933
2934 jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
2935 let config = Config::load().unwrap();
2936
2937 assert_eq!(
2938 config.remappings,
2939 vec![
2940 Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
2942 Remapping::from_str("other/=lib/other/").unwrap().into(),
2943 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
2945 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
2946 ],
2947 );
2948
2949 Ok(())
2950 });
2951 }
2952
2953 #[test]
2954 fn test_remappings_override() {
2955 figment::Jail::expect_with(|jail| {
2956 jail.create_file(
2957 "foundry.toml",
2958 r#"
2959 [profile.default]
2960 src = "some-source"
2961 out = "some-out"
2962 cache = true
2963 "#,
2964 )?;
2965 let config = Config::load().unwrap();
2966 assert!(config.remappings.is_empty());
2967
2968 jail.create_file(
2969 "remappings.txt",
2970 r"
2971 ds-test/=lib/ds-test/
2972 other/=lib/other/
2973 ",
2974 )?;
2975
2976 let config = Config::load().unwrap();
2977 assert_eq!(
2978 config.remappings,
2979 vec![
2980 Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
2981 Remapping::from_str("other/=lib/other/").unwrap().into(),
2982 ],
2983 );
2984
2985 jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
2986 let config = Config::load().unwrap();
2987
2988 assert_eq!(
2993 config.remappings,
2994 vec![
2995 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
2996 Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
2997 Remapping::from_str("other/=lib/other/").unwrap().into(),
2998 ],
2999 );
3000
3001 assert_eq!(
3003 config.get_all_remappings().collect::<Vec<_>>(),
3004 vec![
3005 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
3006 Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
3007 Remapping::from_str("other/=lib/other/").unwrap(),
3008 ],
3009 );
3010
3011 Ok(())
3012 });
3013 }
3014
3015 #[test]
3016 fn test_can_update_libs() {
3017 figment::Jail::expect_with(|jail| {
3018 jail.create_file(
3019 "foundry.toml",
3020 r#"
3021 [profile.default]
3022 libs = ["node_modules"]
3023 "#,
3024 )?;
3025
3026 let mut config = Config::load().unwrap();
3027 config.libs.push("libs".into());
3028 config.update_libs().unwrap();
3029
3030 let config = Config::load().unwrap();
3031 assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
3032 Ok(())
3033 });
3034 }
3035
3036 #[test]
3037 fn test_large_gas_limit() {
3038 figment::Jail::expect_with(|jail| {
3039 let gas = u64::MAX;
3040 jail.create_file(
3041 "foundry.toml",
3042 &format!(
3043 r#"
3044 [profile.default]
3045 gas_limit = "{gas}"
3046 "#
3047 ),
3048 )?;
3049
3050 let config = Config::load().unwrap();
3051 assert_eq!(
3052 config,
3053 Config {
3054 gas_limit: gas.into(),
3055 ..Config::default().normalized_optimizer_settings()
3056 }
3057 );
3058
3059 Ok(())
3060 });
3061 }
3062
3063 #[test]
3064 #[should_panic]
3065 fn test_toml_file_parse_failure() {
3066 figment::Jail::expect_with(|jail| {
3067 jail.create_file(
3068 "foundry.toml",
3069 r#"
3070 [profile.default]
3071 eth_rpc_url = "https://example.com/
3072 "#,
3073 )?;
3074
3075 let _config = Config::load().unwrap();
3076
3077 Ok(())
3078 });
3079 }
3080
3081 #[test]
3082 #[should_panic]
3083 fn test_toml_file_non_existing_config_var_failure() {
3084 figment::Jail::expect_with(|jail| {
3085 jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
3086
3087 let _config = Config::load().unwrap();
3088
3089 Ok(())
3090 });
3091 }
3092
3093 #[test]
3094 fn test_resolve_etherscan_with_chain() {
3095 figment::Jail::expect_with(|jail| {
3096 let env_key = "__BSC_ETHERSCAN_API_KEY";
3097 let env_value = "env value";
3098 jail.create_file(
3099 "foundry.toml",
3100 r#"
3101 [profile.default]
3102
3103 [etherscan]
3104 bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
3105 "#,
3106 )?;
3107
3108 let config = Config::load().unwrap();
3109 assert!(
3110 config
3111 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3112 .is_err()
3113 );
3114
3115 unsafe {
3116 std::env::set_var(env_key, env_value);
3117 }
3118
3119 assert_eq!(
3120 config
3121 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3122 .unwrap()
3123 .unwrap()
3124 .key,
3125 env_value
3126 );
3127
3128 let mut with_key = config;
3129 with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
3130
3131 assert_eq!(
3132 with_key
3133 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3134 .unwrap()
3135 .unwrap()
3136 .key,
3137 "via etherscan_api_key"
3138 );
3139
3140 unsafe {
3141 std::env::remove_var(env_key);
3142 }
3143 Ok(())
3144 });
3145 }
3146
3147 #[test]
3148 fn test_resolve_etherscan() {
3149 figment::Jail::expect_with(|jail| {
3150 jail.create_file(
3151 "foundry.toml",
3152 r#"
3153 [profile.default]
3154
3155 [etherscan]
3156 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3157 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
3158 "#,
3159 )?;
3160
3161 let config = Config::load().unwrap();
3162
3163 assert!(config.etherscan.clone().resolved(EtherscanApiVersion::V2).has_unresolved());
3164
3165 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3166
3167 let configs = config.etherscan.resolved(EtherscanApiVersion::V2);
3168 assert!(!configs.has_unresolved());
3169
3170 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3171 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3172 assert_eq!(
3173 configs,
3174 ResolvedEtherscanConfigs::new([
3175 (
3176 "mainnet",
3177 ResolvedEtherscanConfig {
3178 api_url: mainnet_urls.0.to_string(),
3179 chain: Some(NamedChain::Mainnet.into()),
3180 browser_url: Some(mainnet_urls.1.to_string()),
3181 api_version: EtherscanApiVersion::V2,
3182 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3183 }
3184 ),
3185 (
3186 "moonbeam",
3187 ResolvedEtherscanConfig {
3188 api_url: mb_urls.0.to_string(),
3189 chain: Some(Moonbeam.into()),
3190 browser_url: Some(mb_urls.1.to_string()),
3191 api_version: EtherscanApiVersion::V2,
3192 key: "123456789".to_string(),
3193 }
3194 ),
3195 ])
3196 );
3197
3198 Ok(())
3199 });
3200 }
3201
3202 #[test]
3203 fn test_resolve_etherscan_with_versions() {
3204 figment::Jail::expect_with(|jail| {
3205 jail.create_file(
3206 "foundry.toml",
3207 r#"
3208 [profile.default]
3209
3210 [etherscan]
3211 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN", api_version = "v2" }
3212 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}", api_version = "v1" }
3213 "#,
3214 )?;
3215
3216 let config = Config::load().unwrap();
3217
3218 assert!(config.etherscan.clone().resolved(EtherscanApiVersion::V2).has_unresolved());
3219
3220 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3221
3222 let configs = config.etherscan.resolved(EtherscanApiVersion::V2);
3223 assert!(!configs.has_unresolved());
3224
3225 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3226 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3227 assert_eq!(
3228 configs,
3229 ResolvedEtherscanConfigs::new([
3230 (
3231 "mainnet",
3232 ResolvedEtherscanConfig {
3233 api_url: mainnet_urls.0.to_string(),
3234 chain: Some(NamedChain::Mainnet.into()),
3235 browser_url: Some(mainnet_urls.1.to_string()),
3236 api_version: EtherscanApiVersion::V2,
3237 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3238 }
3239 ),
3240 (
3241 "moonbeam",
3242 ResolvedEtherscanConfig {
3243 api_url: mb_urls.0.to_string(),
3244 chain: Some(Moonbeam.into()),
3245 browser_url: Some(mb_urls.1.to_string()),
3246 api_version: EtherscanApiVersion::V1,
3247 key: "123456789".to_string(),
3248 }
3249 ),
3250 ])
3251 );
3252
3253 Ok(())
3254 });
3255 }
3256
3257 #[test]
3258 fn test_resolve_etherscan_chain_id() {
3259 figment::Jail::expect_with(|jail| {
3260 jail.create_file(
3261 "foundry.toml",
3262 r#"
3263 [profile.default]
3264 chain_id = "sepolia"
3265
3266 [etherscan]
3267 sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3268 "#,
3269 )?;
3270
3271 let config = Config::load().unwrap();
3272 let etherscan = config.get_etherscan_config().unwrap().unwrap();
3273 assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
3274 assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");
3275
3276 Ok(())
3277 });
3278 }
3279
3280 #[test]
3281 fn test_resolve_rpc_url() {
3282 figment::Jail::expect_with(|jail| {
3283 jail.create_file(
3284 "foundry.toml",
3285 r#"
3286 [profile.default]
3287 [rpc_endpoints]
3288 optimism = "https://example.com/"
3289 mainnet = "${_CONFIG_MAINNET}"
3290 "#,
3291 )?;
3292 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3293
3294 let mut config = Config::load().unwrap();
3295 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3296
3297 config.eth_rpc_url = Some("mainnet".to_string());
3298 assert_eq!(
3299 "https://eth-mainnet.alchemyapi.io/v2/123455",
3300 config.get_rpc_url_or_localhost_http().unwrap()
3301 );
3302
3303 config.eth_rpc_url = Some("optimism".to_string());
3304 assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
3305
3306 Ok(())
3307 })
3308 }
3309
3310 #[test]
3311 fn test_resolve_rpc_url_if_etherscan_set() {
3312 figment::Jail::expect_with(|jail| {
3313 jail.create_file(
3314 "foundry.toml",
3315 r#"
3316 [profile.default]
3317 etherscan_api_key = "dummy"
3318 [rpc_endpoints]
3319 optimism = "https://example.com/"
3320 "#,
3321 )?;
3322
3323 let config = Config::load().unwrap();
3324 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3325
3326 Ok(())
3327 })
3328 }
3329
3330 #[test]
3331 fn test_resolve_rpc_url_alias() {
3332 figment::Jail::expect_with(|jail| {
3333 jail.create_file(
3334 "foundry.toml",
3335 r#"
3336 [profile.default]
3337 [rpc_endpoints]
3338 polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
3339 "#,
3340 )?;
3341 let mut config = Config::load().unwrap();
3342 config.eth_rpc_url = Some("polygonAmoy".to_string());
3343 assert!(config.get_rpc_url().unwrap().is_err());
3344
3345 jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
3346
3347 let mut config = Config::load().unwrap();
3348 config.eth_rpc_url = Some("polygonAmoy".to_string());
3349 assert_eq!(
3350 "https://polygon-amoy.g.alchemy.com/v2/123455",
3351 config.get_rpc_url().unwrap().unwrap()
3352 );
3353
3354 Ok(())
3355 })
3356 }
3357
3358 #[test]
3359 fn test_resolve_rpc_aliases() {
3360 figment::Jail::expect_with(|jail| {
3361 jail.create_file(
3362 "foundry.toml",
3363 r#"
3364 [profile.default]
3365 [etherscan]
3366 arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
3367 [rpc_endpoints]
3368 arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
3369 "#,
3370 )?;
3371
3372 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
3373 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");
3374
3375 let config = Config::load().unwrap();
3376
3377 let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
3378 assert!(config.is_err());
3379 assert_eq!(
3380 config.unwrap_err().to_string(),
3381 "At least one of `url` or `chain` must be present for Etherscan config with unknown alias `arbitrum_alias`"
3382 );
3383
3384 Ok(())
3385 });
3386 }
3387
3388 #[test]
3389 fn test_resolve_rpc_config() {
3390 figment::Jail::expect_with(|jail| {
3391 jail.create_file(
3392 "foundry.toml",
3393 r#"
3394 [rpc_endpoints]
3395 optimism = "https://example.com/"
3396 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
3397 "#,
3398 )?;
3399 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3400
3401 let config = Config::load().unwrap();
3402 assert_eq!(
3403 RpcEndpoints::new([
3404 (
3405 "optimism",
3406 RpcEndpointType::String(RpcEndpointUrl::Url(
3407 "https://example.com/".to_string()
3408 ))
3409 ),
3410 (
3411 "mainnet",
3412 RpcEndpointType::Config(RpcEndpoint {
3413 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3414 config: RpcEndpointConfig {
3415 retries: Some(3),
3416 retry_backoff: Some(1000),
3417 compute_units_per_second: Some(1000),
3418 },
3419 auth: None,
3420 })
3421 ),
3422 ]),
3423 config.rpc_endpoints
3424 );
3425
3426 let resolved = config.rpc_endpoints.resolved();
3427 assert_eq!(
3428 RpcEndpoints::new([
3429 (
3430 "optimism",
3431 RpcEndpointType::String(RpcEndpointUrl::Url(
3432 "https://example.com/".to_string()
3433 ))
3434 ),
3435 (
3436 "mainnet",
3437 RpcEndpointType::Config(RpcEndpoint {
3438 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3439 config: RpcEndpointConfig {
3440 retries: Some(3),
3441 retry_backoff: Some(1000),
3442 compute_units_per_second: Some(1000),
3443 },
3444 auth: None,
3445 })
3446 ),
3447 ])
3448 .resolved(),
3449 resolved
3450 );
3451 Ok(())
3452 })
3453 }
3454
3455 #[test]
3456 fn test_resolve_auth() {
3457 figment::Jail::expect_with(|jail| {
3458 jail.create_file(
3459 "foundry.toml",
3460 r#"
3461 [profile.default]
3462 eth_rpc_url = "optimism"
3463 [rpc_endpoints]
3464 optimism = "https://example.com/"
3465 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000, auth = "Bearer ${_CONFIG_AUTH}" }
3466 "#,
3467 )?;
3468
3469 let config = Config::load().unwrap();
3470
3471 jail.set_env("_CONFIG_AUTH", "123456");
3472 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3473
3474 assert_eq!(
3475 RpcEndpoints::new([
3476 (
3477 "optimism",
3478 RpcEndpointType::String(RpcEndpointUrl::Url(
3479 "https://example.com/".to_string()
3480 ))
3481 ),
3482 (
3483 "mainnet",
3484 RpcEndpointType::Config(RpcEndpoint {
3485 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3486 config: RpcEndpointConfig {
3487 retries: Some(3),
3488 retry_backoff: Some(1000),
3489 compute_units_per_second: Some(1000)
3490 },
3491 auth: Some(RpcAuth::Env("Bearer ${_CONFIG_AUTH}".to_string())),
3492 })
3493 ),
3494 ]),
3495 config.rpc_endpoints
3496 );
3497 let resolved = config.rpc_endpoints.resolved();
3498 assert_eq!(
3499 RpcEndpoints::new([
3500 (
3501 "optimism",
3502 RpcEndpointType::String(RpcEndpointUrl::Url(
3503 "https://example.com/".to_string()
3504 ))
3505 ),
3506 (
3507 "mainnet",
3508 RpcEndpointType::Config(RpcEndpoint {
3509 endpoint: RpcEndpointUrl::Url(
3510 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3511 ),
3512 config: RpcEndpointConfig {
3513 retries: Some(3),
3514 retry_backoff: Some(1000),
3515 compute_units_per_second: Some(1000)
3516 },
3517 auth: Some(RpcAuth::Raw("Bearer 123456".to_string())),
3518 })
3519 ),
3520 ])
3521 .resolved(),
3522 resolved
3523 );
3524
3525 Ok(())
3526 });
3527 }
3528
3529 #[test]
3530 fn test_resolve_endpoints() {
3531 figment::Jail::expect_with(|jail| {
3532 jail.create_file(
3533 "foundry.toml",
3534 r#"
3535 [profile.default]
3536 eth_rpc_url = "optimism"
3537 [rpc_endpoints]
3538 optimism = "https://example.com/"
3539 mainnet = "${_CONFIG_MAINNET}"
3540 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
3541 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
3542 "#,
3543 )?;
3544
3545 let config = Config::load().unwrap();
3546
3547 assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
3548
3549 assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
3550
3551 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3552 jail.set_env("_CONFIG_API_KEY1", "123456");
3553 jail.set_env("_CONFIG_API_KEY2", "98765");
3554
3555 let endpoints = config.rpc_endpoints.resolved();
3556
3557 assert!(!endpoints.has_unresolved());
3558
3559 assert_eq!(
3560 endpoints,
3561 RpcEndpoints::new([
3562 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3563 (
3564 "mainnet",
3565 RpcEndpointUrl::Url(
3566 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3567 )
3568 ),
3569 (
3570 "mainnet_2",
3571 RpcEndpointUrl::Url(
3572 "https://eth-mainnet.alchemyapi.io/v2/123456".to_string()
3573 )
3574 ),
3575 (
3576 "mainnet_3",
3577 RpcEndpointUrl::Url(
3578 "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
3579 )
3580 ),
3581 ])
3582 .resolved()
3583 );
3584
3585 Ok(())
3586 });
3587 }
3588
3589 #[test]
3590 fn test_extract_etherscan_config() {
3591 figment::Jail::expect_with(|jail| {
3592 jail.create_file(
3593 "foundry.toml",
3594 r#"
3595 [profile.default]
3596 etherscan_api_key = "optimism"
3597
3598 [etherscan]
3599 optimism = { key = "https://etherscan-optimism.com/" }
3600 amoy = { key = "https://etherscan-amoy.com/" }
3601 "#,
3602 )?;
3603
3604 let mut config = Config::load().unwrap();
3605
3606 let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
3607 assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
3608
3609 config.etherscan_api_key = Some("amoy".to_string());
3610
3611 let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
3612 assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
3613
3614 Ok(())
3615 });
3616 }
3617
3618 #[test]
3619 fn test_extract_etherscan_config_by_chain() {
3620 figment::Jail::expect_with(|jail| {
3621 jail.create_file(
3622 "foundry.toml",
3623 r#"
3624 [profile.default]
3625
3626 [etherscan]
3627 amoy = { key = "https://etherscan-amoy.com/", chain = 80002 }
3628 "#,
3629 )?;
3630
3631 let config = Config::load().unwrap();
3632
3633 let amoy = config
3634 .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
3635 .unwrap()
3636 .unwrap();
3637 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3638
3639 Ok(())
3640 });
3641 }
3642
3643 #[test]
3644 fn test_extract_etherscan_config_by_chain_with_url() {
3645 figment::Jail::expect_with(|jail| {
3646 jail.create_file(
3647 "foundry.toml",
3648 r#"
3649 [profile.default]
3650
3651 [etherscan]
3652 amoy = { key = "https://etherscan-amoy.com/", chain = 80002 , url = "https://verifier-url.com/"}
3653 "#,
3654 )?;
3655
3656 let config = Config::load().unwrap();
3657
3658 let amoy = config
3659 .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
3660 .unwrap()
3661 .unwrap();
3662 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3663 assert_eq!(amoy.api_url, "https://verifier-url.com/".to_string());
3664
3665 Ok(())
3666 });
3667 }
3668
3669 #[test]
3670 fn test_extract_etherscan_config_by_chain_and_alias() {
3671 figment::Jail::expect_with(|jail| {
3672 jail.create_file(
3673 "foundry.toml",
3674 r#"
3675 [profile.default]
3676 eth_rpc_url = "amoy"
3677
3678 [etherscan]
3679 amoy = { key = "https://etherscan-amoy.com/" }
3680
3681 [rpc_endpoints]
3682 amoy = "https://polygon-amoy.g.alchemy.com/v2/amoy"
3683 "#,
3684 )?;
3685
3686 let config = Config::load().unwrap();
3687
3688 let amoy = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
3689 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3690
3691 let amoy_rpc = config.get_rpc_url().unwrap().unwrap();
3692 assert_eq!(amoy_rpc, "https://polygon-amoy.g.alchemy.com/v2/amoy");
3693 Ok(())
3694 });
3695 }
3696
3697 #[test]
3698 fn test_toml_file() {
3699 figment::Jail::expect_with(|jail| {
3700 jail.create_file(
3701 "foundry.toml",
3702 r#"
3703 [profile.default]
3704 src = "some-source"
3705 out = "some-out"
3706 cache = true
3707 eth_rpc_url = "https://example.com/"
3708 verbosity = 3
3709 remappings = ["ds-test=lib/ds-test/"]
3710 via_ir = true
3711 rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
3712 use_literal_content = false
3713 bytecode_hash = "ipfs"
3714 cbor_metadata = true
3715 revert_strings = "strip"
3716 allow_paths = ["allow", "paths"]
3717 build_info_path = "build-info"
3718 always_use_create_2_factory = true
3719
3720 [rpc_endpoints]
3721 optimism = "https://example.com/"
3722 mainnet = "${RPC_MAINNET}"
3723 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3724 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3725 "#,
3726 )?;
3727
3728 let config = Config::load().unwrap();
3729 assert_eq!(
3730 config,
3731 Config {
3732 src: "some-source".into(),
3733 out: "some-out".into(),
3734 cache: true,
3735 eth_rpc_url: Some("https://example.com/".to_string()),
3736 remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
3737 verbosity: 3,
3738 via_ir: true,
3739 rpc_storage_caching: StorageCachingConfig {
3740 chains: CachedChains::Chains(vec![
3741 Chain::mainnet(),
3742 Chain::optimism_mainnet(),
3743 Chain::from_id(999999)
3744 ]),
3745 endpoints: CachedEndpoints::All,
3746 },
3747 use_literal_content: false,
3748 bytecode_hash: BytecodeHash::Ipfs,
3749 cbor_metadata: true,
3750 revert_strings: Some(RevertStrings::Strip),
3751 allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
3752 rpc_endpoints: RpcEndpoints::new([
3753 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3754 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
3755 (
3756 "mainnet_2",
3757 RpcEndpointUrl::Env(
3758 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3759 )
3760 ),
3761 (
3762 "mainnet_3",
3763 RpcEndpointUrl::Env(
3764 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3765 .to_string()
3766 )
3767 ),
3768 ]),
3769 build_info_path: Some("build-info".into()),
3770 always_use_create_2_factory: true,
3771 ..Config::default().normalized_optimizer_settings()
3772 }
3773 );
3774
3775 Ok(())
3776 });
3777 }
3778
3779 #[test]
3780 fn test_load_remappings() {
3781 figment::Jail::expect_with(|jail| {
3782 jail.create_file(
3783 "foundry.toml",
3784 r"
3785 [profile.default]
3786 remappings = ['nested/=lib/nested/']
3787 ",
3788 )?;
3789
3790 let config = Config::load_with_root(jail.directory()).unwrap();
3791 assert_eq!(
3792 config.remappings,
3793 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3794 );
3795
3796 Ok(())
3797 });
3798 }
3799
3800 #[test]
3801 fn test_load_full_toml() {
3802 figment::Jail::expect_with(|jail| {
3803 jail.create_file(
3804 "foundry.toml",
3805 r#"
3806 [profile.default]
3807 auto_detect_solc = true
3808 block_base_fee_per_gas = 0
3809 block_coinbase = '0x0000000000000000000000000000000000000000'
3810 block_difficulty = 0
3811 block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
3812 block_number = 1
3813 block_timestamp = 1
3814 use_literal_content = false
3815 bytecode_hash = 'ipfs'
3816 cbor_metadata = true
3817 cache = true
3818 cache_path = 'cache'
3819 evm_version = 'london'
3820 extra_output = []
3821 extra_output_files = []
3822 always_use_create_2_factory = false
3823 ffi = false
3824 force = false
3825 gas_limit = 9223372036854775807
3826 gas_price = 0
3827 gas_reports = ['*']
3828 ignored_error_codes = [1878]
3829 ignored_warnings_from = ["something"]
3830 deny_warnings = false
3831 initial_balance = '0xffffffffffffffffffffffff'
3832 libraries = []
3833 libs = ['lib']
3834 memory_limit = 134217728
3835 names = false
3836 no_storage_caching = false
3837 no_rpc_rate_limit = false
3838 offline = false
3839 optimizer = true
3840 optimizer_runs = 200
3841 out = 'out'
3842 remappings = ['nested/=lib/nested/']
3843 sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3844 sizes = false
3845 sparse_mode = false
3846 src = 'src'
3847 test = 'test'
3848 tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3849 verbosity = 0
3850 via_ir = false
3851
3852 [profile.default.rpc_storage_caching]
3853 chains = 'all'
3854 endpoints = 'all'
3855
3856 [rpc_endpoints]
3857 optimism = "https://example.com/"
3858 mainnet = "${RPC_MAINNET}"
3859 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3860
3861 [fuzz]
3862 runs = 256
3863 seed = '0x3e8'
3864 max_test_rejects = 65536
3865
3866 [invariant]
3867 runs = 256
3868 depth = 500
3869 fail_on_revert = false
3870 call_override = false
3871 shrink_run_limit = 5000
3872 "#,
3873 )?;
3874
3875 let config = Config::load_with_root(jail.directory()).unwrap();
3876
3877 assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
3878 assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
3879 assert_eq!(
3880 config.remappings,
3881 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3882 );
3883
3884 assert_eq!(
3885 config.rpc_endpoints,
3886 RpcEndpoints::new([
3887 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3888 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
3889 (
3890 "mainnet_2",
3891 RpcEndpointUrl::Env(
3892 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3893 )
3894 ),
3895 ]),
3896 );
3897
3898 Ok(())
3899 });
3900 }
3901
3902 #[test]
3903 fn test_solc_req() {
3904 figment::Jail::expect_with(|jail| {
3905 jail.create_file(
3906 "foundry.toml",
3907 r#"
3908 [profile.default]
3909 solc_version = "0.8.12"
3910 "#,
3911 )?;
3912
3913 let config = Config::load().unwrap();
3914 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3915
3916 jail.create_file(
3917 "foundry.toml",
3918 r#"
3919 [profile.default]
3920 solc = "0.8.12"
3921 "#,
3922 )?;
3923
3924 let config = Config::load().unwrap();
3925 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3926
3927 jail.create_file(
3928 "foundry.toml",
3929 r#"
3930 [profile.default]
3931 solc = "path/to/local/solc"
3932 "#,
3933 )?;
3934
3935 let config = Config::load().unwrap();
3936 assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
3937
3938 jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
3939 let config = Config::load().unwrap();
3940 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
3941 Ok(())
3942 });
3943 }
3944
3945 #[test]
3947 fn test_backwards_solc_version() {
3948 figment::Jail::expect_with(|jail| {
3949 jail.create_file(
3950 "foundry.toml",
3951 r#"
3952 [default]
3953 solc = "0.8.12"
3954 solc_version = "0.8.20"
3955 "#,
3956 )?;
3957
3958 let config = Config::load().unwrap();
3959 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3960
3961 Ok(())
3962 });
3963
3964 figment::Jail::expect_with(|jail| {
3965 jail.create_file(
3966 "foundry.toml",
3967 r#"
3968 [default]
3969 solc_version = "0.8.20"
3970 "#,
3971 )?;
3972
3973 let config = Config::load().unwrap();
3974 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));
3975
3976 Ok(())
3977 });
3978 }
3979
3980 #[test]
3981 fn test_toml_casing_file() {
3982 figment::Jail::expect_with(|jail| {
3983 jail.create_file(
3984 "foundry.toml",
3985 r#"
3986 [profile.default]
3987 src = "some-source"
3988 out = "some-out"
3989 cache = true
3990 eth-rpc-url = "https://example.com/"
3991 evm-version = "berlin"
3992 auto-detect-solc = false
3993 "#,
3994 )?;
3995
3996 let config = Config::load().unwrap();
3997 assert_eq!(
3998 config,
3999 Config {
4000 src: "some-source".into(),
4001 out: "some-out".into(),
4002 cache: true,
4003 eth_rpc_url: Some("https://example.com/".to_string()),
4004 auto_detect_solc: false,
4005 evm_version: EvmVersion::Berlin,
4006 ..Config::default().normalized_optimizer_settings()
4007 }
4008 );
4009
4010 Ok(())
4011 });
4012 }
4013
4014 #[test]
4015 fn test_output_selection() {
4016 figment::Jail::expect_with(|jail| {
4017 jail.create_file(
4018 "foundry.toml",
4019 r#"
4020 [profile.default]
4021 extra_output = ["metadata", "ir-optimized"]
4022 extra_output_files = ["metadata"]
4023 "#,
4024 )?;
4025
4026 let config = Config::load().unwrap();
4027
4028 assert_eq!(
4029 config.extra_output,
4030 vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
4031 );
4032 assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
4033
4034 Ok(())
4035 });
4036 }
4037
4038 #[test]
4039 fn test_precedence() {
4040 figment::Jail::expect_with(|jail| {
4041 jail.create_file(
4042 "foundry.toml",
4043 r#"
4044 [profile.default]
4045 src = "mysrc"
4046 out = "myout"
4047 verbosity = 3
4048 "#,
4049 )?;
4050
4051 let config = Config::load().unwrap();
4052 assert_eq!(
4053 config,
4054 Config {
4055 src: "mysrc".into(),
4056 out: "myout".into(),
4057 verbosity: 3,
4058 ..Config::default().normalized_optimizer_settings()
4059 }
4060 );
4061
4062 jail.set_env("FOUNDRY_SRC", r"other-src");
4063 let config = Config::load().unwrap();
4064 assert_eq!(
4065 config,
4066 Config {
4067 src: "other-src".into(),
4068 out: "myout".into(),
4069 verbosity: 3,
4070 ..Config::default().normalized_optimizer_settings()
4071 }
4072 );
4073
4074 jail.set_env("FOUNDRY_PROFILE", "foo");
4075 let val: Result<String, _> = Config::figment().extract_inner("profile");
4076 assert!(val.is_err());
4077
4078 Ok(())
4079 });
4080 }
4081
4082 #[test]
4083 fn test_extract_basic() {
4084 figment::Jail::expect_with(|jail| {
4085 jail.create_file(
4086 "foundry.toml",
4087 r#"
4088 [profile.default]
4089 src = "mysrc"
4090 out = "myout"
4091 verbosity = 3
4092 evm_version = 'berlin'
4093
4094 [profile.other]
4095 src = "other-src"
4096 "#,
4097 )?;
4098 let loaded = Config::load().unwrap();
4099 assert_eq!(loaded.evm_version, EvmVersion::Berlin);
4100 let base = loaded.into_basic();
4101 let default = Config::default();
4102 assert_eq!(
4103 base,
4104 BasicConfig {
4105 profile: Config::DEFAULT_PROFILE,
4106 src: "mysrc".into(),
4107 out: "myout".into(),
4108 libs: default.libs.clone(),
4109 remappings: default.remappings.clone(),
4110 }
4111 );
4112 jail.set_env("FOUNDRY_PROFILE", r"other");
4113 let base = Config::figment().extract::<BasicConfig>().unwrap();
4114 assert_eq!(
4115 base,
4116 BasicConfig {
4117 profile: Config::DEFAULT_PROFILE,
4118 src: "other-src".into(),
4119 out: "myout".into(),
4120 libs: default.libs.clone(),
4121 remappings: default.remappings,
4122 }
4123 );
4124 Ok(())
4125 });
4126 }
4127
4128 #[test]
4129 #[should_panic]
4130 fn test_parse_invalid_fuzz_weight() {
4131 figment::Jail::expect_with(|jail| {
4132 jail.create_file(
4133 "foundry.toml",
4134 r"
4135 [fuzz]
4136 dictionary_weight = 101
4137 ",
4138 )?;
4139 let _config = Config::load().unwrap();
4140 Ok(())
4141 });
4142 }
4143
4144 #[test]
4145 fn test_fallback_provider() {
4146 figment::Jail::expect_with(|jail| {
4147 jail.create_file(
4148 "foundry.toml",
4149 r"
4150 [fuzz]
4151 runs = 1
4152 include_storage = false
4153 dictionary_weight = 99
4154
4155 [invariant]
4156 runs = 420
4157
4158 [profile.ci.fuzz]
4159 dictionary_weight = 5
4160
4161 [profile.ci.invariant]
4162 runs = 400
4163 ",
4164 )?;
4165
4166 let invariant_default = InvariantConfig::default();
4167 let config = Config::load().unwrap();
4168
4169 assert_ne!(config.invariant.runs, config.fuzz.runs);
4170 assert_eq!(config.invariant.runs, 420);
4171
4172 assert_ne!(
4173 config.fuzz.dictionary.include_storage,
4174 invariant_default.dictionary.include_storage
4175 );
4176 assert_eq!(
4177 config.invariant.dictionary.include_storage,
4178 config.fuzz.dictionary.include_storage
4179 );
4180
4181 assert_ne!(
4182 config.fuzz.dictionary.dictionary_weight,
4183 invariant_default.dictionary.dictionary_weight
4184 );
4185 assert_eq!(
4186 config.invariant.dictionary.dictionary_weight,
4187 config.fuzz.dictionary.dictionary_weight
4188 );
4189
4190 jail.set_env("FOUNDRY_PROFILE", "ci");
4191 let ci_config = Config::load().unwrap();
4192 assert_eq!(ci_config.fuzz.runs, 1);
4193 assert_eq!(ci_config.invariant.runs, 400);
4194 assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
4195 assert_eq!(
4196 ci_config.invariant.dictionary.dictionary_weight,
4197 config.fuzz.dictionary.dictionary_weight
4198 );
4199
4200 Ok(())
4201 })
4202 }
4203
4204 #[test]
4205 fn test_standalone_profile_sections() {
4206 figment::Jail::expect_with(|jail| {
4207 jail.create_file(
4208 "foundry.toml",
4209 r"
4210 [fuzz]
4211 runs = 100
4212
4213 [invariant]
4214 runs = 120
4215
4216 [profile.ci.fuzz]
4217 runs = 420
4218
4219 [profile.ci.invariant]
4220 runs = 500
4221 ",
4222 )?;
4223
4224 let config = Config::load().unwrap();
4225 assert_eq!(config.fuzz.runs, 100);
4226 assert_eq!(config.invariant.runs, 120);
4227
4228 jail.set_env("FOUNDRY_PROFILE", "ci");
4229 let config = Config::load().unwrap();
4230 assert_eq!(config.fuzz.runs, 420);
4231 assert_eq!(config.invariant.runs, 500);
4232
4233 Ok(())
4234 });
4235 }
4236
4237 #[test]
4238 fn can_handle_deviating_dapp_aliases() {
4239 figment::Jail::expect_with(|jail| {
4240 let addr = Address::ZERO;
4241 jail.set_env("DAPP_TEST_NUMBER", 1337);
4242 jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
4243 jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
4244 jail.set_env("DAPP_TEST_DEPTH", 20);
4245 jail.set_env("DAPP_FORK_BLOCK", 100);
4246 jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
4247 jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
4248
4249 let config = Config::load().unwrap();
4250
4251 assert_eq!(config.block_number, U256::from(1337));
4252 assert_eq!(config.sender, addr);
4253 assert_eq!(config.fuzz.runs, 420);
4254 assert_eq!(config.invariant.depth, 20);
4255 assert_eq!(config.fork_block_number, Some(100));
4256 assert_eq!(config.optimizer_runs, Some(999));
4257 assert!(!config.optimizer.unwrap());
4258
4259 Ok(())
4260 });
4261 }
4262
4263 #[test]
4264 fn can_parse_libraries() {
4265 figment::Jail::expect_with(|jail| {
4266 jail.set_env(
4267 "DAPP_LIBRARIES",
4268 "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
4269 );
4270 let config = Config::load().unwrap();
4271 assert_eq!(
4272 config.libraries,
4273 vec![
4274 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4275 .to_string()
4276 ]
4277 );
4278
4279 jail.set_env(
4280 "DAPP_LIBRARIES",
4281 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4282 );
4283 let config = Config::load().unwrap();
4284 assert_eq!(
4285 config.libraries,
4286 vec![
4287 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4288 .to_string(),
4289 ]
4290 );
4291
4292 jail.set_env(
4293 "DAPP_LIBRARIES",
4294 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4295 );
4296 let config = Config::load().unwrap();
4297 assert_eq!(
4298 config.libraries,
4299 vec![
4300 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4301 .to_string(),
4302 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4303 .to_string()
4304 ]
4305 );
4306
4307 Ok(())
4308 });
4309 }
4310
4311 #[test]
4312 fn test_parse_many_libraries() {
4313 figment::Jail::expect_with(|jail| {
4314 jail.create_file(
4315 "foundry.toml",
4316 r"
4317 [profile.default]
4318 libraries= [
4319 './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4320 './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4321 './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4322 './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4323 './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4324 ]
4325 ",
4326 )?;
4327 let config = Config::load().unwrap();
4328
4329 let libs = config.parsed_libraries().unwrap().libs;
4330
4331 similar_asserts::assert_eq!(
4332 libs,
4333 BTreeMap::from([
4334 (
4335 PathBuf::from("./src/SizeAuctionDiscount.sol"),
4336 BTreeMap::from([
4337 (
4338 "Chainlink".to_string(),
4339 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4340 ),
4341 (
4342 "Math".to_string(),
4343 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4344 )
4345 ])
4346 ),
4347 (
4348 PathBuf::from("./src/SizeAuction.sol"),
4349 BTreeMap::from([
4350 (
4351 "ChainlinkTWAP".to_string(),
4352 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4353 ),
4354 (
4355 "Math".to_string(),
4356 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4357 )
4358 ])
4359 ),
4360 (
4361 PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
4362 BTreeMap::from([(
4363 "ChainlinkTWAP".to_string(),
4364 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4365 )])
4366 ),
4367 ])
4368 );
4369
4370 Ok(())
4371 });
4372 }
4373
4374 #[test]
4375 fn config_roundtrip() {
4376 figment::Jail::expect_with(|jail| {
4377 let default = Config::default().normalized_optimizer_settings();
4378 let basic = default.clone().into_basic();
4379 jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
4380
4381 let mut other = Config::load().unwrap();
4382 clear_warning(&mut other);
4383 assert_eq!(default, other);
4384
4385 let other = other.into_basic();
4386 assert_eq!(basic, other);
4387
4388 jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
4389 let mut other = Config::load().unwrap();
4390 clear_warning(&mut other);
4391 assert_eq!(default, other);
4392
4393 Ok(())
4394 });
4395 }
4396
4397 #[test]
4398 fn test_fs_permissions() {
4399 figment::Jail::expect_with(|jail| {
4400 jail.create_file(
4401 "foundry.toml",
4402 r#"
4403 [profile.default]
4404 fs_permissions = [{ access = "read-write", path = "./"}]
4405 "#,
4406 )?;
4407 let loaded = Config::load().unwrap();
4408
4409 assert_eq!(
4410 loaded.fs_permissions,
4411 FsPermissions::new(vec![PathPermission::read_write("./")])
4412 );
4413
4414 jail.create_file(
4415 "foundry.toml",
4416 r#"
4417 [profile.default]
4418 fs_permissions = [{ access = "none", path = "./"}]
4419 "#,
4420 )?;
4421 let loaded = Config::load().unwrap();
4422 assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
4423
4424 Ok(())
4425 });
4426 }
4427
4428 #[test]
4429 fn test_optimizer_settings_basic() {
4430 figment::Jail::expect_with(|jail| {
4431 jail.create_file(
4432 "foundry.toml",
4433 r"
4434 [profile.default]
4435 optimizer = true
4436
4437 [profile.default.optimizer_details]
4438 yul = false
4439
4440 [profile.default.optimizer_details.yulDetails]
4441 stackAllocation = true
4442 ",
4443 )?;
4444 let mut loaded = Config::load().unwrap();
4445 clear_warning(&mut loaded);
4446 assert_eq!(
4447 loaded.optimizer_details,
4448 Some(OptimizerDetails {
4449 yul: Some(false),
4450 yul_details: Some(YulDetails {
4451 stack_allocation: Some(true),
4452 ..Default::default()
4453 }),
4454 ..Default::default()
4455 })
4456 );
4457
4458 let s = loaded.to_string_pretty().unwrap();
4459 jail.create_file("foundry.toml", &s)?;
4460
4461 let mut reloaded = Config::load().unwrap();
4462 clear_warning(&mut reloaded);
4463 assert_eq!(loaded, reloaded);
4464
4465 Ok(())
4466 });
4467 }
4468
4469 #[test]
4470 fn test_model_checker_settings_basic() {
4471 figment::Jail::expect_with(|jail| {
4472 jail.create_file(
4473 "foundry.toml",
4474 r"
4475 [profile.default]
4476
4477 [profile.default.model_checker]
4478 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4479 engine = 'chc'
4480 targets = [ 'assert', 'outOfBounds' ]
4481 timeout = 10000
4482 ",
4483 )?;
4484 let mut loaded = Config::load().unwrap();
4485 clear_warning(&mut loaded);
4486 assert_eq!(
4487 loaded.model_checker,
4488 Some(ModelCheckerSettings {
4489 contracts: BTreeMap::from([
4490 ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
4491 ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
4492 ]),
4493 engine: Some(ModelCheckerEngine::CHC),
4494 targets: Some(vec![
4495 ModelCheckerTarget::Assert,
4496 ModelCheckerTarget::OutOfBounds
4497 ]),
4498 timeout: Some(10000),
4499 invariants: None,
4500 show_unproved: None,
4501 div_mod_with_slacks: None,
4502 solvers: None,
4503 show_unsupported: None,
4504 show_proved_safe: None,
4505 })
4506 );
4507
4508 let s = loaded.to_string_pretty().unwrap();
4509 jail.create_file("foundry.toml", &s)?;
4510
4511 let mut reloaded = Config::load().unwrap();
4512 clear_warning(&mut reloaded);
4513 assert_eq!(loaded, reloaded);
4514
4515 Ok(())
4516 });
4517 }
4518
4519 #[test]
4520 fn test_model_checker_settings_relative_paths() {
4521 figment::Jail::expect_with(|jail| {
4522 jail.create_file(
4523 "foundry.toml",
4524 r"
4525 [profile.default]
4526
4527 [profile.default.model_checker]
4528 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4529 engine = 'chc'
4530 targets = [ 'assert', 'outOfBounds' ]
4531 timeout = 10000
4532 ",
4533 )?;
4534 let loaded = Config::load().unwrap().sanitized();
4535
4536 let dir = foundry_compilers::utils::canonicalize(jail.directory())
4541 .expect("Could not canonicalize jail path");
4542 assert_eq!(
4543 loaded.model_checker,
4544 Some(ModelCheckerSettings {
4545 contracts: BTreeMap::from([
4546 (
4547 format!("{}", dir.join("a.sol").display()),
4548 vec!["A1".to_string(), "A2".to_string()]
4549 ),
4550 (
4551 format!("{}", dir.join("b.sol").display()),
4552 vec!["B1".to_string(), "B2".to_string()]
4553 ),
4554 ]),
4555 engine: Some(ModelCheckerEngine::CHC),
4556 targets: Some(vec![
4557 ModelCheckerTarget::Assert,
4558 ModelCheckerTarget::OutOfBounds
4559 ]),
4560 timeout: Some(10000),
4561 invariants: None,
4562 show_unproved: None,
4563 div_mod_with_slacks: None,
4564 solvers: None,
4565 show_unsupported: None,
4566 show_proved_safe: None,
4567 })
4568 );
4569
4570 Ok(())
4571 });
4572 }
4573
4574 #[test]
4575 fn test_fmt_config() {
4576 figment::Jail::expect_with(|jail| {
4577 jail.create_file(
4578 "foundry.toml",
4579 r#"
4580 [fmt]
4581 line_length = 100
4582 tab_width = 2
4583 bracket_spacing = true
4584 style = "space"
4585 "#,
4586 )?;
4587 let loaded = Config::load().unwrap().sanitized();
4588 assert_eq!(
4589 loaded.fmt,
4590 FormatterConfig {
4591 line_length: 100,
4592 tab_width: 2,
4593 bracket_spacing: true,
4594 style: IndentStyle::Space,
4595 ..Default::default()
4596 }
4597 );
4598
4599 Ok(())
4600 });
4601 }
4602
4603 #[test]
4604 fn test_lint_config() {
4605 figment::Jail::expect_with(|jail| {
4606 jail.create_file(
4607 "foundry.toml",
4608 r"
4609 [lint]
4610 severity = ['high', 'medium']
4611 exclude_lints = ['incorrect-shift']
4612 ",
4613 )?;
4614 let loaded = Config::load().unwrap().sanitized();
4615 assert_eq!(
4616 loaded.lint,
4617 LinterConfig {
4618 severity: vec![LintSeverity::High, LintSeverity::Med],
4619 exclude_lints: vec!["incorrect-shift".into()],
4620 ..Default::default()
4621 }
4622 );
4623
4624 Ok(())
4625 });
4626 }
4627
4628 #[test]
4629 fn test_invariant_config() {
4630 figment::Jail::expect_with(|jail| {
4631 jail.create_file(
4632 "foundry.toml",
4633 r"
4634 [invariant]
4635 runs = 512
4636 depth = 10
4637 ",
4638 )?;
4639
4640 let loaded = Config::load().unwrap().sanitized();
4641 assert_eq!(
4642 loaded.invariant,
4643 InvariantConfig {
4644 runs: 512,
4645 depth: 10,
4646 failure_persist_dir: Some(PathBuf::from("cache/invariant")),
4647 corpus_dir: None,
4648 ..Default::default()
4649 }
4650 );
4651
4652 Ok(())
4653 });
4654 }
4655
4656 #[test]
4657 fn test_standalone_sections_env() {
4658 figment::Jail::expect_with(|jail| {
4659 jail.create_file(
4660 "foundry.toml",
4661 r"
4662 [fuzz]
4663 runs = 100
4664
4665 [invariant]
4666 depth = 1
4667 ",
4668 )?;
4669
4670 jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
4671 jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
4672 jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
4673
4674 let config = Config::load().unwrap();
4675 assert_eq!(config.fmt.line_length, 95);
4676 assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
4677 assert_eq!(config.invariant.depth, 5);
4678
4679 Ok(())
4680 });
4681 }
4682
4683 #[test]
4684 fn test_parse_with_profile() {
4685 let foundry_str = r"
4686 [profile.default]
4687 src = 'src'
4688 out = 'out'
4689 libs = ['lib']
4690
4691 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
4692 ";
4693 assert_eq!(
4694 parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
4695 (
4696 Config::DEFAULT_PROFILE,
4697 BasicConfig {
4698 profile: Config::DEFAULT_PROFILE,
4699 src: "src".into(),
4700 out: "out".into(),
4701 libs: vec!["lib".into()],
4702 remappings: vec![]
4703 }
4704 )
4705 );
4706 }
4707
4708 #[test]
4709 fn test_implicit_profile_loads() {
4710 figment::Jail::expect_with(|jail| {
4711 jail.create_file(
4712 "foundry.toml",
4713 r"
4714 [default]
4715 src = 'my-src'
4716 out = 'my-out'
4717 ",
4718 )?;
4719 let loaded = Config::load().unwrap().sanitized();
4720 assert_eq!(loaded.src.file_name().unwrap(), "my-src");
4721 assert_eq!(loaded.out.file_name().unwrap(), "my-out");
4722 assert_eq!(
4723 loaded.warnings,
4724 vec![Warning::UnknownSection {
4725 unknown_section: Profile::new("default"),
4726 source: Some("foundry.toml".into())
4727 }]
4728 );
4729
4730 Ok(())
4731 });
4732 }
4733
4734 #[test]
4735 fn test_etherscan_api_key() {
4736 figment::Jail::expect_with(|jail| {
4737 jail.create_file(
4738 "foundry.toml",
4739 r"
4740 [default]
4741 ",
4742 )?;
4743 jail.set_env("ETHERSCAN_API_KEY", "");
4744 let loaded = Config::load().unwrap().sanitized();
4745 assert!(loaded.etherscan_api_key.is_none());
4746
4747 jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
4748 let loaded = Config::load().unwrap().sanitized();
4749 assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));
4750
4751 Ok(())
4752 });
4753 }
4754
4755 #[test]
4756 fn test_etherscan_api_key_figment() {
4757 figment::Jail::expect_with(|jail| {
4758 jail.create_file(
4759 "foundry.toml",
4760 r"
4761 [default]
4762 etherscan_api_key = 'DUMMY'
4763 ",
4764 )?;
4765 jail.set_env("ETHERSCAN_API_KEY", "ETHER");
4766
4767 let figment = Config::figment_with_root(jail.directory())
4768 .merge(("etherscan_api_key", "USER_KEY"));
4769
4770 let loaded = Config::from_provider(figment).unwrap();
4771 assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));
4772
4773 Ok(())
4774 });
4775 }
4776
4777 #[test]
4778 fn test_normalize_defaults() {
4779 figment::Jail::expect_with(|jail| {
4780 jail.create_file(
4781 "foundry.toml",
4782 r"
4783 [default]
4784 solc = '0.8.13'
4785 ",
4786 )?;
4787
4788 let loaded = Config::load().unwrap().sanitized();
4789 assert_eq!(loaded.evm_version, EvmVersion::London);
4790 Ok(())
4791 });
4792 }
4793
4794 #[expect(clippy::disallowed_macros)]
4796 #[test]
4797 #[ignore]
4798 fn print_config() {
4799 let config = Config {
4800 optimizer_details: Some(OptimizerDetails {
4801 peephole: None,
4802 inliner: None,
4803 jumpdest_remover: None,
4804 order_literals: None,
4805 deduplicate: None,
4806 cse: None,
4807 constant_optimizer: Some(true),
4808 yul: Some(true),
4809 yul_details: Some(YulDetails {
4810 stack_allocation: None,
4811 optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
4812 }),
4813 simple_counter_for_loop_unchecked_increment: None,
4814 }),
4815 ..Default::default()
4816 };
4817 println!("{}", config.to_string_pretty().unwrap());
4818 }
4819
4820 #[test]
4821 fn can_use_impl_figment_macro() {
4822 #[derive(Default, Serialize)]
4823 struct MyArgs {
4824 #[serde(skip_serializing_if = "Option::is_none")]
4825 root: Option<PathBuf>,
4826 }
4827 impl_figment_convert!(MyArgs);
4828
4829 impl Provider for MyArgs {
4830 fn metadata(&self) -> Metadata {
4831 Metadata::default()
4832 }
4833
4834 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
4835 let value = Value::serialize(self)?;
4836 let error = InvalidType(value.to_actual(), "map".into());
4837 let dict = value.into_dict().ok_or(error)?;
4838 Ok(Map::from([(Config::selected_profile(), dict)]))
4839 }
4840 }
4841
4842 let _figment: Figment = From::from(&MyArgs::default());
4843
4844 #[derive(Default)]
4845 struct Outer {
4846 start: MyArgs,
4847 other: MyArgs,
4848 another: MyArgs,
4849 }
4850 impl_figment_convert!(Outer, start, other, another);
4851
4852 let _figment: Figment = From::from(&Outer::default());
4853 }
4854
4855 #[test]
4856 fn list_cached_blocks() -> eyre::Result<()> {
4857 fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
4858 let block_path = chain_path.join(block_number);
4859 fs::create_dir(block_path.as_path()).unwrap();
4860 let file_path = block_path.join("storage.json");
4861 let mut file = File::create(file_path).unwrap();
4862 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4863 }
4864
4865 fn fake_block_cache_block_path_as_file(
4866 chain_path: &Path,
4867 block_number: &str,
4868 size_bytes: usize,
4869 ) {
4870 let block_path = chain_path.join(block_number);
4871 let mut file = File::create(block_path).unwrap();
4872 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4873 }
4874
4875 let chain_dir = tempdir()?;
4876
4877 fake_block_cache(chain_dir.path(), "1", 100);
4878 fake_block_cache(chain_dir.path(), "2", 500);
4879 fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
4880 let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
4882 writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
4883
4884 let result = Config::get_cached_blocks(chain_dir.path())?;
4885
4886 assert_eq!(result.len(), 3);
4887 let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
4888 let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
4889 let block3 = &result.iter().find(|x| x.0 == "3").unwrap();
4890
4891 assert_eq!(block1.0, "1");
4892 assert_eq!(block1.1, 100);
4893 assert_eq!(block2.0, "2");
4894 assert_eq!(block2.1, 500);
4895 assert_eq!(block3.0, "3");
4896 assert_eq!(block3.1, 900);
4897
4898 chain_dir.close()?;
4899 Ok(())
4900 }
4901
4902 #[test]
4903 fn list_etherscan_cache() -> eyre::Result<()> {
4904 fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
4905 let metadata_path = chain_path.join("sources");
4906 let abi_path = chain_path.join("abi");
4907 let _ = fs::create_dir(metadata_path.as_path());
4908 let _ = fs::create_dir(abi_path.as_path());
4909
4910 let metadata_file_path = metadata_path.join(address);
4911 let mut metadata_file = File::create(metadata_file_path).unwrap();
4912 writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4913 .unwrap();
4914
4915 let abi_file_path = abi_path.join(address);
4916 let mut abi_file = File::create(abi_file_path).unwrap();
4917 writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4918 .unwrap();
4919 }
4920
4921 let chain_dir = tempdir()?;
4922
4923 fake_etherscan_cache(chain_dir.path(), "1", 100);
4924 fake_etherscan_cache(chain_dir.path(), "2", 500);
4925
4926 let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
4927
4928 assert_eq!(result, 600);
4929
4930 chain_dir.close()?;
4931 Ok(())
4932 }
4933
4934 #[test]
4935 fn test_parse_error_codes() {
4936 figment::Jail::expect_with(|jail| {
4937 jail.create_file(
4938 "foundry.toml",
4939 r#"
4940 [default]
4941 ignored_error_codes = ["license", "unreachable", 1337]
4942 "#,
4943 )?;
4944
4945 let config = Config::load().unwrap();
4946 assert_eq!(
4947 config.ignored_error_codes,
4948 vec![
4949 SolidityErrorCode::SpdxLicenseNotProvided,
4950 SolidityErrorCode::Unreachable,
4951 SolidityErrorCode::Other(1337)
4952 ]
4953 );
4954
4955 Ok(())
4956 });
4957 }
4958
4959 #[test]
4960 fn test_parse_file_paths() {
4961 figment::Jail::expect_with(|jail| {
4962 jail.create_file(
4963 "foundry.toml",
4964 r#"
4965 [default]
4966 ignored_warnings_from = ["something"]
4967 "#,
4968 )?;
4969
4970 let config = Config::load().unwrap();
4971 assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);
4972
4973 Ok(())
4974 });
4975 }
4976
4977 #[test]
4978 fn test_parse_optimizer_settings() {
4979 figment::Jail::expect_with(|jail| {
4980 jail.create_file(
4981 "foundry.toml",
4982 r"
4983 [default]
4984 [profile.default.optimizer_details]
4985 ",
4986 )?;
4987
4988 let config = Config::load().unwrap();
4989 assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
4990
4991 Ok(())
4992 });
4993 }
4994
4995 #[test]
4996 fn test_parse_labels() {
4997 figment::Jail::expect_with(|jail| {
4998 jail.create_file(
4999 "foundry.toml",
5000 r#"
5001 [labels]
5002 0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
5003 0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
5004 "#,
5005 )?;
5006
5007 let config = Config::load().unwrap();
5008 assert_eq!(
5009 config.labels,
5010 AddressHashMap::from_iter(vec![
5011 (
5012 address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
5013 "Uniswap V3: Factory".to_string()
5014 ),
5015 (
5016 address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
5017 "Uniswap V3: Positions NFT".to_string()
5018 ),
5019 ])
5020 );
5021
5022 Ok(())
5023 });
5024 }
5025
5026 #[test]
5027 fn test_parse_vyper() {
5028 figment::Jail::expect_with(|jail| {
5029 jail.create_file(
5030 "foundry.toml",
5031 r#"
5032 [vyper]
5033 optimize = "codesize"
5034 path = "/path/to/vyper"
5035 experimental_codegen = true
5036 "#,
5037 )?;
5038
5039 let config = Config::load().unwrap();
5040 assert_eq!(
5041 config.vyper,
5042 VyperConfig {
5043 optimize: Some(VyperOptimizationMode::Codesize),
5044 path: Some("/path/to/vyper".into()),
5045 experimental_codegen: Some(true),
5046 }
5047 );
5048
5049 Ok(())
5050 });
5051 }
5052
5053 #[test]
5054 fn test_parse_soldeer() {
5055 figment::Jail::expect_with(|jail| {
5056 jail.create_file(
5057 "foundry.toml",
5058 r#"
5059 [soldeer]
5060 remappings_generate = true
5061 remappings_regenerate = false
5062 remappings_version = true
5063 remappings_prefix = "@"
5064 remappings_location = "txt"
5065 recursive_deps = true
5066 "#,
5067 )?;
5068
5069 let config = Config::load().unwrap();
5070
5071 assert_eq!(
5072 config.soldeer,
5073 Some(SoldeerConfig {
5074 remappings_generate: true,
5075 remappings_regenerate: false,
5076 remappings_version: true,
5077 remappings_prefix: "@".to_string(),
5078 remappings_location: RemappingsLocation::Txt,
5079 recursive_deps: true,
5080 })
5081 );
5082
5083 Ok(())
5084 });
5085 }
5086
5087 #[test]
5089 fn test_resolve_mesc_by_chain_id() {
5090 let s = r#"{
5091 "mesc_version": "0.2.1",
5092 "default_endpoint": null,
5093 "endpoints": {
5094 "sophon_50104": {
5095 "name": "sophon_50104",
5096 "url": "https://rpc.sophon.xyz",
5097 "chain_id": "50104",
5098 "endpoint_metadata": {}
5099 }
5100 },
5101 "network_defaults": {
5102 },
5103 "network_names": {},
5104 "profiles": {
5105 "foundry": {
5106 "name": "foundry",
5107 "default_endpoint": "local_ethereum",
5108 "network_defaults": {
5109 "50104": "sophon_50104"
5110 },
5111 "profile_metadata": {},
5112 "use_mesc": true
5113 }
5114 },
5115 "global_metadata": {}
5116}"#;
5117
5118 let config = serde_json::from_str(s).unwrap();
5119 let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5120 .unwrap()
5121 .unwrap();
5122 assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5123
5124 let s = r#"{
5125 "mesc_version": "0.2.1",
5126 "default_endpoint": null,
5127 "endpoints": {
5128 "sophon_50104": {
5129 "name": "sophon_50104",
5130 "url": "https://rpc.sophon.xyz",
5131 "chain_id": "50104",
5132 "endpoint_metadata": {}
5133 }
5134 },
5135 "network_defaults": {
5136 "50104": "sophon_50104"
5137 },
5138 "network_names": {},
5139 "profiles": {},
5140 "global_metadata": {}
5141}"#;
5142
5143 let config = serde_json::from_str(s).unwrap();
5144 let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5145 .unwrap()
5146 .unwrap();
5147 assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5148 }
5149
5150 #[test]
5151 fn test_get_etherscan_config_with_unknown_chain() {
5152 figment::Jail::expect_with(|jail| {
5153 jail.create_file(
5154 "foundry.toml",
5155 r#"
5156 [etherscan]
5157 mainnet = { chain = 3658348, key = "api-key"}
5158 "#,
5159 )?;
5160 let config = Config::load().unwrap();
5161 let unknown_chain = Chain::from_id(3658348);
5162 let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5163 assert!(result.is_err());
5164 let error_msg = result.unwrap_err().to_string();
5165 assert!(error_msg.contains("No known Etherscan API URL for chain `3658348`"));
5166 assert!(error_msg.contains("Specify a `url`"));
5167 assert!(error_msg.contains("Verify the chain `3658348` is correct"));
5168
5169 Ok(())
5170 });
5171 }
5172
5173 #[test]
5174 fn test_get_etherscan_config_with_existing_chain_and_url() {
5175 figment::Jail::expect_with(|jail| {
5176 jail.create_file(
5177 "foundry.toml",
5178 r#"
5179 [etherscan]
5180 mainnet = { chain = 1, key = "api-key" }
5181 "#,
5182 )?;
5183 let config = Config::load().unwrap();
5184 let unknown_chain = Chain::from_id(1);
5185 let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5186 assert!(result.is_ok());
5187 Ok(())
5188 });
5189 }
5190}