1use alloy_dyn_abi::{DynSolValue, Specifier};
2use alloy_json_abi::{Constructor, JsonAbi};
3use eyre::{Result, WrapErr};
4use foundry_common::{
5 TestFunctionExt, fmt::parse_tokens, fs, fs::json_files, selectors::SelectorKind, shell,
6};
7use foundry_compilers::{
8 Artifact, ArtifactId, ProjectCompileOutput, artifacts::CompactBytecode, utils::read_json_file,
9};
10use foundry_config::{
11 Chain, Config, NamedChain,
12 error::ExtractConfigError,
13 figment::{Figment, Provider},
14};
15use foundry_evm::{
16 core::evm::FoundryEvmNetwork,
17 executors::{DeployResult, EvmError, RawCallResult},
18 opts::EvmOpts,
19 traces::{
20 CallTraceDecoder, TraceKind, Traces, decode_trace_arena, identifier::SignaturesCache,
21 prune_trace_depth, render_trace_arena_inner, trace_arena_at_depth,
22 },
23};
24use std::{
25 fmt::Write,
26 path::{Path, PathBuf},
27};
28use yansi::Paint;
29
30#[track_caller]
33pub fn find_contract_artifacts(
34 output: ProjectCompileOutput,
35 path: &Path,
36 name: &str,
37) -> Result<(JsonAbi, CompactBytecode, ArtifactId)> {
38 let mut other = Vec::new();
39 let Some((id, contract)) = output.into_artifacts().find_map(|(id, artifact)| {
40 if id.name == name && id.source == path {
41 Some((id, artifact))
42 } else {
43 other.push(id.name);
44 None
45 }
46 }) else {
47 let mut err = format!("could not find artifact: `{name}`");
48 if let Some(suggestion) = super::did_you_mean(name, other).pop()
49 && suggestion != name
50 {
51 err = format!(
52 r#"{err}
53
54 Did you mean `{suggestion}`?"#
55 );
56 }
57 eyre::bail!(err);
58 };
59
60 let abi = contract
61 .get_abi()
62 .ok_or_else(|| eyre::eyre!("contract {} does not contain abi", name))?
63 .into_owned();
64
65 let bin = contract
66 .get_bytecode()
67 .ok_or_else(|| eyre::eyre!("contract {} does not contain bytecode", name))?
68 .into_owned();
69
70 Ok((abi, bin, id))
71}
72
73pub fn ensure_clean_constructor(abi: &JsonAbi) -> Result<()> {
75 if let Some(constructor) = &abi.constructor
76 && !constructor.inputs.is_empty()
77 {
78 eyre::bail!(
79 "Contract constructor should have no arguments. Add those arguments to `run(...)` instead, and call it with `--sig run(...)`."
80 );
81 }
82 Ok(())
83}
84
85pub fn needs_setup(abi: &JsonAbi) -> bool {
86 let setup_fns: Vec<_> = abi.functions().filter(|func| func.name.is_setup()).collect();
87
88 for setup_fn in &setup_fns {
89 if setup_fn.name != "setUp" {
90 let _ = sh_warn!(
91 "Found invalid setup function \"{}\" did you mean \"setUp()\"?",
92 setup_fn.signature()
93 );
94 }
95 }
96
97 setup_fns.len() == 1 && setup_fns[0].name == "setUp"
98}
99
100pub fn eta_key(state: &indicatif::ProgressState, f: &mut dyn Write) {
101 write!(f, "{:.1}s", state.eta().as_secs_f64()).unwrap()
102}
103
104pub fn init_progress(len: u64, label: &str) -> indicatif::ProgressBar {
105 let pb = indicatif::ProgressBar::new(len);
106 let mut template =
107 "{prefix}{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} "
108 .to_string();
109 write!(template, "{label}").unwrap();
110 template += " ({eta})";
111 pb.set_style(
112 indicatif::ProgressStyle::with_template(&template)
113 .unwrap()
114 .with_key("eta", crate::utils::eta_key)
115 .progress_chars("#>-"),
116 );
117 pb
118}
119
120pub fn has_different_gas_calc(chain_id: u64) -> bool {
122 let chain = Chain::from(chain_id);
123 if let Some(chain) = chain.named() {
124 return chain.is_tempo()
125 || chain.is_arbitrum()
126 || chain.is_elastic()
127 || matches!(
128 chain,
129 NamedChain::Acala
130 | NamedChain::AcalaMandalaTestnet
131 | NamedChain::AcalaTestnet
132 | NamedChain::Etherlink
133 | NamedChain::EtherlinkShadownet
134 | NamedChain::Karura
135 | NamedChain::KaruraTestnet
136 | NamedChain::Kusama
137 | NamedChain::Mantle
138 | NamedChain::MantleSepolia
139 | NamedChain::MegaEth
140 | NamedChain::MegaEthTestnet
141 | NamedChain::Metis
142 | NamedChain::Monad
143 | NamedChain::MonadTestnet
144 | NamedChain::Moonbase
145 | NamedChain::Moonbeam
146 | NamedChain::MoonbeamDev
147 | NamedChain::Moonriver
148 | NamedChain::Plume
149 | NamedChain::PlumeTestnet
150 | NamedChain::Polkadot
151 | NamedChain::PolkadotTestnet
152 );
153 }
154 false
155}
156
157pub fn has_batch_support(chain_id: u64) -> bool {
159 if let Some(chain) = Chain::from(chain_id).named() {
160 return !chain.is_arbitrum();
161 }
162 true
163}
164
165pub trait LoadConfig {
173 fn figment(&self) -> Figment;
175
176 fn load_config(&self) -> Result<Config, ExtractConfigError> {
178 load_config_from_provider(self.figment())
179 }
180
181 fn load_config_with_dependencies(&self) -> Result<Config, ExtractConfigError> {
183 let mut config = self.load_config()?;
184 self.install_missing_dependencies(&mut config)?;
185 Ok(config)
186 }
187
188 fn install_missing_dependencies(&self, config: &mut Config) -> Result<(), ExtractConfigError> {
190 crate::install::install_missing_dependencies(config, || self.load_config())
191 }
192
193 fn load_config_no_warnings(&self) -> Result<Config, ExtractConfigError> {
195 self.load_config_unsanitized_no_warnings().map(Config::sanitized)
196 }
197
198 fn load_config_unsanitized(&self) -> Result<Config, ExtractConfigError> {
200 self.load_config_unsanitized_no_warnings().inspect(emit_warnings)
201 }
202
203 fn load_config_unsanitized_no_warnings(&self) -> Result<Config, ExtractConfigError> {
205 Config::from_provider(self.figment())
206 }
207
208 fn load_config_and_evm_opts(&self) -> Result<(Config, EvmOpts)> {
210 self.load_config_and_evm_opts_no_warnings().inspect(|(config, _)| emit_warnings(config))
211 }
212
213 fn load_config_and_evm_opts_no_warnings(&self) -> Result<(Config, EvmOpts)> {
215 let figment = self.figment();
216
217 let mut evm_opts = figment.extract::<EvmOpts>().map_err(ExtractConfigError::new)?;
218 let config = Config::from_provider(figment)?.sanitized();
219
220 if config.networks != Default::default() {
221 evm_opts.networks = config.networks;
222 }
223
224 if let Some(fork_url) = config.get_rpc_url() {
226 trace!(target: "forge::config", ?fork_url, "Update EvmOpts fork url");
227 evm_opts.fork_url = Some(fork_url?.into_owned());
228 }
229
230 Ok((config, evm_opts))
231 }
232}
233
234pub fn load_config_from_provider<T: Provider>(provider: T) -> Result<Config, ExtractConfigError> {
236 Config::from_provider(provider).map(Config::sanitized).inspect(emit_warnings)
237}
238
239impl<T> LoadConfig for T
240where
241 for<'a> Figment: From<&'a T>,
242{
243 fn figment(&self) -> Figment {
244 self.into()
245 }
246}
247
248fn emit_warnings(config: &Config) {
249 for warning in &config.warnings {
250 let _ = sh_warn!("{warning}");
251 }
252}
253
254pub fn read_constructor_args_file(constructor_args_path: PathBuf) -> Result<Vec<String>> {
256 if !constructor_args_path.exists() {
257 eyre::bail!("Constructor args file \"{}\" not found", constructor_args_path.display());
258 }
259 let args = if constructor_args_path.extension() == Some(std::ffi::OsStr::new("json")) {
260 read_json_file(&constructor_args_path).wrap_err(format!(
261 "Constructor args file \"{}\" must encode a json array",
262 constructor_args_path.display(),
263 ))?
264 } else {
265 fs::read_to_string(constructor_args_path)?.split_whitespace().map(str::to_string).collect()
266 };
267 Ok(args)
268}
269
270pub fn parse_constructor_args(
272 constructor: &Constructor,
273 constructor_args: &[String],
274) -> Result<Vec<DynSolValue>> {
275 if constructor.inputs.len() != constructor_args.len() {
276 eyre::bail!(
277 "Constructor argument count mismatch: expected {} but got {}",
278 constructor.inputs.len(),
279 constructor_args.len()
280 );
281 }
282
283 let mut params = Vec::with_capacity(constructor.inputs.len());
284 for (input, arg) in constructor.inputs.iter().zip(constructor_args) {
285 let ty = input
286 .resolve()
287 .wrap_err_with(|| format!("Could not resolve constructor arg: input={input}"))?;
288 params.push((ty, arg));
289 }
290 let params = params.iter().map(|(ty, arg)| (ty, arg.as_str()));
291 parse_tokens(params).map_err(Into::into)
292}
293
294#[derive(Debug)]
296pub struct TraceResult {
297 pub success: bool,
298 pub traces: Option<Traces>,
299 pub gas_used: u64,
300}
301
302impl TraceResult {
303 pub fn from_raw<FEN: FoundryEvmNetwork>(
305 raw: RawCallResult<FEN>,
306 trace_kind: TraceKind,
307 ) -> Self {
308 let RawCallResult { gas_used, traces, reverted, .. } = raw;
309 Self { success: !reverted, traces: traces.map(|arena| vec![(trace_kind, arena)]), gas_used }
310 }
311}
312
313impl<FEN: FoundryEvmNetwork> From<DeployResult<FEN>> for TraceResult {
314 fn from(result: DeployResult<FEN>) -> Self {
315 Self::from_raw(result.raw, TraceKind::Deployment)
316 }
317}
318
319impl<FEN: FoundryEvmNetwork> TryFrom<Result<DeployResult<FEN>, EvmError<FEN>>> for TraceResult {
320 type Error = EvmError<FEN>;
321
322 fn try_from(value: Result<DeployResult<FEN>, EvmError<FEN>>) -> Result<Self, Self::Error> {
323 match value {
324 Ok(result) => Ok(Self::from(result)),
325 Err(EvmError::Execution(err)) => Ok(Self::from_raw(err.raw, TraceKind::Deployment)),
326 Err(err) => Err(err),
327 }
328 }
329}
330
331impl<FEN: FoundryEvmNetwork> From<RawCallResult<FEN>> for TraceResult {
332 fn from(result: RawCallResult<FEN>) -> Self {
333 Self::from_raw(result, TraceKind::Execution)
334 }
335}
336
337pub async fn print_traces(
338 result: &mut TraceResult,
339 decoder: &CallTraceDecoder,
340 verbose: bool,
341 state_changes: bool,
342 trace_depth: Option<usize>,
343) -> Result<()> {
344 let traces = result.traces.as_mut().expect("No traces found");
345
346 if !shell::is_json() {
347 sh_println!("Traces:")?;
348 }
349
350 for (_, arena) in traces {
351 decode_trace_arena(arena, decoder).await;
352
353 if shell::is_json()
354 && let Some(trace_depth) = trace_depth
355 {
356 let arena = trace_arena_at_depth(arena, trace_depth);
357 sh_println!("{}", render_trace_arena_inner(&arena, verbose, state_changes))?;
358 } else {
359 if let Some(trace_depth) = trace_depth {
360 prune_trace_depth(arena, trace_depth);
361 }
362 sh_println!("{}", render_trace_arena_inner(arena, verbose, state_changes))?;
363 }
364 }
365
366 if shell::is_json() {
367 return Ok(());
368 }
369
370 sh_println!()?;
371 if result.success {
372 sh_println!("{}", "Transaction successfully executed.".green())?;
373 } else {
374 sh_err!("Transaction failed.")?;
375 }
376 sh_println!("Gas used: {}", result.gas_used)?;
377
378 Ok(())
379}
380
381pub fn cache_local_signatures(output: &ProjectCompileOutput) -> Result<()> {
384 let Some(cache_dir) = Config::foundry_cache_dir() else {
385 eyre::bail!("Failed to get `cache_dir` to generate local signatures.");
386 };
387 let path = cache_dir.join("signatures");
388 let mut signatures = SignaturesCache::load(&path);
389 for (_, artifact) in output.artifacts() {
390 if let Some(abi) = &artifact.abi {
391 signatures.extend_from_abi(abi);
392 }
393
394 if let Some(method_identifiers) = &artifact.method_identifiers {
396 signatures.extend(method_identifiers.iter().filter_map(|(signature, selector)| {
397 Some((SelectorKind::Function(selector.parse().ok()?), signature.clone()))
398 }));
399 }
400 }
401 signatures.save(&path);
402 Ok(())
403}
404
405pub fn cache_signatures_from_abis(folder_path: impl AsRef<Path>) -> Result<()> {
408 let Some(cache_dir) = Config::foundry_cache_dir() else {
409 eyre::bail!("Failed to get `cache_dir` to generate local signatures.");
410 };
411 let path = cache_dir.join("signatures");
412 let mut signatures = SignaturesCache::load(&path);
413
414 json_files(folder_path.as_ref())
415 .filter_map(|path| std::fs::read_to_string(&path).ok())
416 .filter_map(|content| serde_json::from_str::<JsonAbi>(&content).ok())
417 .for_each(|json_abi| signatures.extend_from_abi(&json_abi));
418
419 signatures.save(&path);
420 Ok(())
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use foundry_config::TracingConfig;
427 use std::fs;
428 use tempfile::tempdir;
429
430 struct TracingConfigArgs;
431
432 impl LoadConfig for TracingConfigArgs {
433 fn figment(&self) -> Figment {
434 Config::figment()
435 .merge(("verbosity", 2u8))
436 .merge(("tracing", TracingConfig { verbosity: 4, ..Default::default() }))
437 }
438 }
439
440 #[test]
441 fn tracing_verbosity_is_independent_from_evm_opts() {
442 let (config, evm_opts) = TracingConfigArgs.load_config_and_evm_opts_no_warnings().unwrap();
443
444 assert_eq!(config.verbosity, 2);
445 assert_eq!(config.tracing.verbosity, 4);
446 assert_eq!(evm_opts.verbosity, 2);
447 }
448
449 #[test]
450 fn test_cache_signatures_from_abis() {
451 let temp_dir = tempdir().unwrap();
452 let abi_json = r#"[
453 {
454 "type": "function",
455 "name": "myCustomFunction",
456 "inputs": [{"name": "amount", "type": "uint256"}],
457 "outputs": [],
458 "stateMutability": "nonpayable"
459 },
460 {
461 "type": "event",
462 "name": "MyCustomEvent",
463 "inputs": [{"name": "value", "type": "uint256", "indexed": false}],
464 "anonymous": false
465 },
466 {
467 "type": "error",
468 "name": "MyCustomError",
469 "inputs": [{"name": "code", "type": "uint256"}]
470 }
471 ]"#;
472
473 let abi_path = temp_dir.path().join("test.json");
474 fs::write(&abi_path, abi_json).unwrap();
475
476 cache_signatures_from_abis(temp_dir.path()).unwrap();
477
478 let cache_dir = Config::foundry_cache_dir().unwrap();
479 let cache_path = cache_dir.join("signatures");
480 let cache = SignaturesCache::load(&cache_path);
481
482 let func_selector: alloy_primitives::Selector = "0x2e2dbaf7".parse().unwrap();
483 assert!(cache.contains_key(&SelectorKind::Function(func_selector)));
484
485 let event_selector: alloy_primitives::B256 =
486 "0x8cc20c47f3a2463817352f75dec0dbf43a7a771b5f6817a92bd5724c1f4aa745".parse().unwrap();
487 assert!(cache.contains_key(&SelectorKind::Event(event_selector)));
488
489 let error_selector: alloy_primitives::Selector = "0xd35f45de".parse().unwrap();
490 assert!(cache.contains_key(&SelectorKind::Error(error_selector)));
491 }
492}