1use alloy_json_abi::JsonAbi;
2use alloy_primitives::{Address, Bytes, map::HashMap};
3use eyre::{Result, WrapErr};
4use foundry_common::{
5 ContractsByArtifact, TestFunctionExt, compile::ProjectCompiler, fs, selectors::SelectorKind,
6 shell,
7};
8use foundry_compilers::{
9 Artifact, ArtifactId, ProjectCompileOutput,
10 artifacts::{CompactBytecode, Settings},
11 cache::{CacheEntry, CompilerCache},
12 utils::read_json_file,
13};
14use foundry_config::{Chain, Config, NamedChain, error::ExtractConfigError, figment::Figment};
15use foundry_debugger::Debugger;
16use foundry_evm::{
17 executors::{DeployResult, EvmError, RawCallResult},
18 opts::EvmOpts,
19 traces::{
20 CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces,
21 debug::{ContractSources, DebugTraceIdentifier},
22 decode_trace_arena,
23 identifier::{SignaturesCache, SignaturesIdentifier, TraceIdentifiers},
24 render_trace_arena_inner,
25 },
26};
27use std::{
28 fmt::Write,
29 path::{Path, PathBuf},
30 str::FromStr,
31};
32use yansi::Paint;
33
34#[track_caller]
37pub fn remove_contract(
38 output: ProjectCompileOutput,
39 path: &Path,
40 name: &str,
41) -> Result<(JsonAbi, CompactBytecode, ArtifactId)> {
42 let mut other = Vec::new();
43 let Some((id, contract)) = output.into_artifacts().find_map(|(id, artifact)| {
44 if id.name == name && id.source == path {
45 Some((id, artifact))
46 } else {
47 other.push(id.name);
48 None
49 }
50 }) else {
51 let mut err = format!("could not find artifact: `{name}`");
52 if let Some(suggestion) = super::did_you_mean(name, other).pop()
53 && suggestion != name
54 {
55 err = format!(
56 r#"{err}
57
58 Did you mean `{suggestion}`?"#
59 );
60 }
61 eyre::bail!(err)
62 };
63
64 let abi = contract
65 .get_abi()
66 .ok_or_else(|| eyre::eyre!("contract {} does not contain abi", name))?
67 .into_owned();
68
69 let bin = contract
70 .get_bytecode()
71 .ok_or_else(|| eyre::eyre!("contract {} does not contain bytecode", name))?
72 .into_owned();
73
74 Ok((abi, bin, id))
75}
76
77pub fn get_cached_entry_by_name(
81 cache: &CompilerCache<Settings>,
82 name: &str,
83) -> Result<(PathBuf, CacheEntry)> {
84 let mut cached_entry = None;
85 let mut alternatives = Vec::new();
86
87 for (abs_path, entry) in &cache.files {
88 for artifact_name in entry.artifacts.keys() {
89 if artifact_name == name {
90 if cached_entry.is_some() {
91 eyre::bail!(
92 "contract with duplicate name `{}`. please pass the path instead",
93 name
94 )
95 }
96 cached_entry = Some((abs_path.to_owned(), entry.to_owned()));
97 } else {
98 alternatives.push(artifact_name);
99 }
100 }
101 }
102
103 if let Some(entry) = cached_entry {
104 return Ok(entry);
105 }
106
107 let mut err = format!("could not find artifact: `{name}`");
108 if let Some(suggestion) = super::did_you_mean(name, &alternatives).pop() {
109 err = format!(
110 r#"{err}
111
112 Did you mean `{suggestion}`?"#
113 );
114 }
115 eyre::bail!(err)
116}
117
118pub fn ensure_clean_constructor(abi: &JsonAbi) -> Result<()> {
120 if let Some(constructor) = &abi.constructor
121 && !constructor.inputs.is_empty()
122 {
123 eyre::bail!(
124 "Contract constructor should have no arguments. Add those arguments to `run(...)` instead, and call it with `--sig run(...)`."
125 );
126 }
127 Ok(())
128}
129
130pub fn needs_setup(abi: &JsonAbi) -> bool {
131 let setup_fns: Vec<_> = abi.functions().filter(|func| func.name.is_setup()).collect();
132
133 for setup_fn in &setup_fns {
134 if setup_fn.name != "setUp" {
135 let _ = sh_warn!(
136 "Found invalid setup function \"{}\" did you mean \"setUp()\"?",
137 setup_fn.signature()
138 );
139 }
140 }
141
142 setup_fns.len() == 1 && setup_fns[0].name == "setUp"
143}
144
145pub fn eta_key(state: &indicatif::ProgressState, f: &mut dyn Write) {
146 write!(f, "{:.1}s", state.eta().as_secs_f64()).unwrap()
147}
148
149pub fn init_progress(len: u64, label: &str) -> indicatif::ProgressBar {
150 let pb = indicatif::ProgressBar::new(len);
151 let mut template =
152 "{prefix}{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} "
153 .to_string();
154 write!(template, "{label}").unwrap();
155 template += " ({eta})";
156 pb.set_style(
157 indicatif::ProgressStyle::with_template(&template)
158 .unwrap()
159 .with_key("eta", crate::utils::eta_key)
160 .progress_chars("#>-"),
161 );
162 pb
163}
164
165pub fn has_different_gas_calc(chain_id: u64) -> bool {
167 if let Some(chain) = Chain::from(chain_id).named() {
168 return chain.is_arbitrum()
169 || chain.is_elastic()
170 || matches!(
171 chain,
172 NamedChain::Acala
173 | NamedChain::AcalaMandalaTestnet
174 | NamedChain::AcalaTestnet
175 | NamedChain::Etherlink
176 | NamedChain::EtherlinkTestnet
177 | NamedChain::Karura
178 | NamedChain::KaruraTestnet
179 | NamedChain::Mantle
180 | NamedChain::MantleSepolia
181 | NamedChain::MantleTestnet
182 | NamedChain::Moonbase
183 | NamedChain::Moonbeam
184 | NamedChain::MoonbeamDev
185 | NamedChain::Moonriver
186 | NamedChain::Metis
187 );
188 }
189 false
190}
191
192pub fn has_batch_support(chain_id: u64) -> bool {
194 if let Some(chain) = Chain::from(chain_id).named() {
195 return !chain.is_arbitrum();
196 }
197 true
198}
199
200pub trait LoadConfig {
208 fn figment(&self) -> Figment;
210
211 fn load_config(&self) -> Result<Config, ExtractConfigError> {
213 self.load_config_no_warnings().inspect(emit_warnings)
214 }
215
216 fn load_config_no_warnings(&self) -> Result<Config, ExtractConfigError> {
218 self.load_config_unsanitized_no_warnings().map(Config::sanitized)
219 }
220
221 fn load_config_unsanitized(&self) -> Result<Config, ExtractConfigError> {
223 self.load_config_unsanitized_no_warnings().inspect(emit_warnings)
224 }
225
226 fn load_config_unsanitized_no_warnings(&self) -> Result<Config, ExtractConfigError> {
228 Config::from_provider(self.figment())
229 }
230
231 fn load_config_and_evm_opts(&self) -> Result<(Config, EvmOpts)> {
233 self.load_config_and_evm_opts_no_warnings().inspect(|(config, _)| emit_warnings(config))
234 }
235
236 fn load_config_and_evm_opts_no_warnings(&self) -> Result<(Config, EvmOpts)> {
238 let figment = self.figment();
239
240 let mut evm_opts = figment.extract::<EvmOpts>().map_err(ExtractConfigError::new)?;
241 let config = Config::from_provider(figment)?.sanitized();
242
243 if let Some(fork_url) = config.get_rpc_url() {
245 trace!(target: "forge::config", ?fork_url, "Update EvmOpts fork url");
246 evm_opts.fork_url = Some(fork_url?.into_owned());
247 }
248
249 Ok((config, evm_opts))
250 }
251}
252
253impl<T> LoadConfig for T
254where
255 for<'a> Figment: From<&'a T>,
256{
257 fn figment(&self) -> Figment {
258 self.into()
259 }
260}
261
262fn emit_warnings(config: &Config) {
263 for warning in &config.warnings {
264 let _ = sh_warn!("{warning}");
265 }
266}
267
268pub fn read_constructor_args_file(constructor_args_path: PathBuf) -> Result<Vec<String>> {
270 if !constructor_args_path.exists() {
271 eyre::bail!("Constructor args file \"{}\" not found", constructor_args_path.display());
272 }
273 let args = if constructor_args_path.extension() == Some(std::ffi::OsStr::new("json")) {
274 read_json_file(&constructor_args_path).wrap_err(format!(
275 "Constructor args file \"{}\" must encode a json array",
276 constructor_args_path.display(),
277 ))?
278 } else {
279 fs::read_to_string(constructor_args_path)?.split_whitespace().map(str::to_string).collect()
280 };
281 Ok(args)
282}
283
284#[derive(Debug)]
286pub struct TraceResult {
287 pub success: bool,
288 pub traces: Option<Traces>,
289 pub gas_used: u64,
290}
291
292impl TraceResult {
293 pub fn from_raw(raw: RawCallResult, trace_kind: TraceKind) -> Self {
295 let RawCallResult { gas_used, traces, reverted, .. } = raw;
296 Self { success: !reverted, traces: traces.map(|arena| vec![(trace_kind, arena)]), gas_used }
297 }
298}
299
300impl From<DeployResult> for TraceResult {
301 fn from(result: DeployResult) -> Self {
302 Self::from_raw(result.raw, TraceKind::Deployment)
303 }
304}
305
306impl TryFrom<Result<DeployResult, EvmError>> for TraceResult {
307 type Error = EvmError;
308
309 fn try_from(value: Result<DeployResult, EvmError>) -> Result<Self, Self::Error> {
310 match value {
311 Ok(result) => Ok(Self::from(result)),
312 Err(EvmError::Execution(err)) => Ok(Self::from_raw(err.raw, TraceKind::Deployment)),
313 Err(err) => Err(err),
314 }
315 }
316}
317
318impl From<RawCallResult> for TraceResult {
319 fn from(result: RawCallResult) -> Self {
320 Self::from_raw(result, TraceKind::Execution)
321 }
322}
323
324impl TryFrom<Result<RawCallResult>> for TraceResult {
325 type Error = EvmError;
326
327 fn try_from(value: Result<RawCallResult>) -> Result<Self, Self::Error> {
328 match value {
329 Ok(result) => Ok(Self::from(result)),
330 Err(err) => Err(EvmError::from(err)),
331 }
332 }
333}
334
335#[expect(clippy::too_many_arguments)]
337pub async fn handle_traces(
338 mut result: TraceResult,
339 config: &Config,
340 chain: Option<Chain>,
341 contracts_bytecode: &HashMap<Address, Bytes>,
342 labels: Vec<String>,
343 with_local_artifacts: bool,
344 debug: bool,
345 decode_internal: bool,
346 disable_label: bool,
347) -> Result<()> {
348 let (known_contracts, mut sources) = if with_local_artifacts {
349 let _ = sh_println!("Compiling project to generate artifacts");
350 let project = config.project()?;
351 let compiler = ProjectCompiler::new();
352 let output = compiler.compile(&project)?;
353 (
354 Some(ContractsByArtifact::new(
355 output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
356 )),
357 ContractSources::from_project_output(&output, project.root(), None)?,
358 )
359 } else {
360 (None, ContractSources::default())
361 };
362
363 let labels = labels.iter().filter_map(|label_str| {
364 let mut iter = label_str.split(':');
365
366 if let Some(addr) = iter.next()
367 && let (Ok(address), Some(label)) = (Address::from_str(addr), iter.next())
368 {
369 return Some((address, label.to_string()));
370 }
371 None
372 });
373 let config_labels = config.labels.clone().into_iter();
374
375 let mut builder = CallTraceDecoderBuilder::new()
376 .with_labels(labels.chain(config_labels))
377 .with_signature_identifier(SignaturesIdentifier::from_config(config)?)
378 .with_label_disabled(disable_label);
379 let mut identifier = TraceIdentifiers::new().with_etherscan(config, chain)?;
380 if let Some(contracts) = &known_contracts {
381 builder = builder.with_known_contracts(contracts);
382 identifier = identifier.with_local_and_bytecodes(contracts, contracts_bytecode);
383 }
384
385 let mut decoder = builder.build();
386
387 for (_, trace) in result.traces.as_deref_mut().unwrap_or_default() {
388 decoder.identify(trace, &mut identifier);
389 }
390
391 if decode_internal || debug {
392 if let Some(ref etherscan_identifier) = identifier.etherscan {
393 sources.merge(etherscan_identifier.get_compiled_contracts().await?);
394 }
395
396 if debug {
397 let mut debugger = Debugger::builder()
398 .traces(result.traces.expect("missing traces"))
399 .decoder(&decoder)
400 .sources(sources)
401 .build();
402 debugger.try_run_tui()?;
403 return Ok(());
404 }
405
406 decoder.debug_identifier = Some(DebugTraceIdentifier::new(sources));
407 }
408
409 print_traces(&mut result, &decoder, shell::verbosity() > 0, shell::verbosity() > 4).await?;
410
411 Ok(())
412}
413
414pub async fn print_traces(
415 result: &mut TraceResult,
416 decoder: &CallTraceDecoder,
417 verbose: bool,
418 state_changes: bool,
419) -> Result<()> {
420 let traces = result.traces.as_mut().expect("No traces found");
421
422 if !shell::is_json() {
423 sh_println!("Traces:")?;
424 }
425
426 for (_, arena) in traces {
427 decode_trace_arena(arena, decoder).await;
428 sh_println!("{}", render_trace_arena_inner(arena, verbose, state_changes))?;
429 }
430
431 if shell::is_json() {
432 return Ok(());
433 }
434
435 sh_println!()?;
436 if result.success {
437 sh_println!("{}", "Transaction successfully executed.".green())?;
438 } else {
439 sh_err!("Transaction failed.")?;
440 }
441 sh_println!("Gas used: {}", result.gas_used)?;
442
443 Ok(())
444}
445
446pub fn cache_local_signatures(output: &ProjectCompileOutput) -> Result<()> {
449 let Some(cache_dir) = Config::foundry_cache_dir() else {
450 eyre::bail!("Failed to get `cache_dir` to generate local signatures.");
451 };
452 let path = cache_dir.join("signatures");
453 let mut signatures = SignaturesCache::load(&path);
454 for (_, artifact) in output.artifacts() {
455 if let Some(abi) = &artifact.abi {
456 signatures.extend_from_abi(abi);
457 }
458
459 if let Some(method_identifiers) = &artifact.method_identifiers {
461 signatures.extend(method_identifiers.iter().filter_map(|(signature, selector)| {
462 Some((SelectorKind::Function(selector.parse().ok()?), signature.clone()))
463 }));
464 }
465 }
466 signatures.save(&path);
467 Ok(())
468}