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::Moonbase
182 | NamedChain::Moonbeam
183 | NamedChain::MoonbeamDev
184 | NamedChain::Moonriver
185 | NamedChain::Metis
186 );
187 }
188 false
189}
190
191pub fn has_batch_support(chain_id: u64) -> bool {
193 if let Some(chain) = Chain::from(chain_id).named() {
194 return !chain.is_arbitrum();
195 }
196 true
197}
198
199pub trait LoadConfig {
207 fn figment(&self) -> Figment;
209
210 fn load_config(&self) -> Result<Config, ExtractConfigError> {
212 self.load_config_no_warnings().inspect(emit_warnings)
213 }
214
215 fn load_config_no_warnings(&self) -> Result<Config, ExtractConfigError> {
217 self.load_config_unsanitized_no_warnings().map(Config::sanitized)
218 }
219
220 fn load_config_unsanitized(&self) -> Result<Config, ExtractConfigError> {
222 self.load_config_unsanitized_no_warnings().inspect(emit_warnings)
223 }
224
225 fn load_config_unsanitized_no_warnings(&self) -> Result<Config, ExtractConfigError> {
227 Config::from_provider(self.figment())
228 }
229
230 fn load_config_and_evm_opts(&self) -> Result<(Config, EvmOpts)> {
232 self.load_config_and_evm_opts_no_warnings().inspect(|(config, _)| emit_warnings(config))
233 }
234
235 fn load_config_and_evm_opts_no_warnings(&self) -> Result<(Config, EvmOpts)> {
237 let figment = self.figment();
238
239 let mut evm_opts = figment.extract::<EvmOpts>().map_err(ExtractConfigError::new)?;
240 let config = Config::from_provider(figment)?.sanitized();
241
242 if let Some(fork_url) = config.get_rpc_url() {
244 trace!(target: "forge::config", ?fork_url, "Update EvmOpts fork url");
245 evm_opts.fork_url = Some(fork_url?.into_owned());
246 }
247
248 Ok((config, evm_opts))
249 }
250}
251
252impl<T> LoadConfig for T
253where
254 for<'a> Figment: From<&'a T>,
255{
256 fn figment(&self) -> Figment {
257 self.into()
258 }
259}
260
261fn emit_warnings(config: &Config) {
262 for warning in &config.warnings {
263 let _ = sh_warn!("{warning}");
264 }
265}
266
267pub fn read_constructor_args_file(constructor_args_path: PathBuf) -> Result<Vec<String>> {
269 if !constructor_args_path.exists() {
270 eyre::bail!("Constructor args file \"{}\" not found", constructor_args_path.display());
271 }
272 let args = if constructor_args_path.extension() == Some(std::ffi::OsStr::new("json")) {
273 read_json_file(&constructor_args_path).wrap_err(format!(
274 "Constructor args file \"{}\" must encode a json array",
275 constructor_args_path.display(),
276 ))?
277 } else {
278 fs::read_to_string(constructor_args_path)?.split_whitespace().map(str::to_string).collect()
279 };
280 Ok(args)
281}
282
283#[derive(Debug)]
285pub struct TraceResult {
286 pub success: bool,
287 pub traces: Option<Traces>,
288 pub gas_used: u64,
289}
290
291impl TraceResult {
292 pub fn from_raw(raw: RawCallResult, trace_kind: TraceKind) -> Self {
294 let RawCallResult { gas_used, traces, reverted, .. } = raw;
295 Self { success: !reverted, traces: traces.map(|arena| vec![(trace_kind, arena)]), gas_used }
296 }
297}
298
299impl From<DeployResult> for TraceResult {
300 fn from(result: DeployResult) -> Self {
301 Self::from_raw(result.raw, TraceKind::Deployment)
302 }
303}
304
305impl TryFrom<Result<DeployResult, EvmError>> for TraceResult {
306 type Error = EvmError;
307
308 fn try_from(value: Result<DeployResult, EvmError>) -> Result<Self, Self::Error> {
309 match value {
310 Ok(result) => Ok(Self::from(result)),
311 Err(EvmError::Execution(err)) => Ok(Self::from_raw(err.raw, TraceKind::Deployment)),
312 Err(err) => Err(err),
313 }
314 }
315}
316
317impl From<RawCallResult> for TraceResult {
318 fn from(result: RawCallResult) -> Self {
319 Self::from_raw(result, TraceKind::Execution)
320 }
321}
322
323impl TryFrom<Result<RawCallResult>> for TraceResult {
324 type Error = EvmError;
325
326 fn try_from(value: Result<RawCallResult>) -> Result<Self, Self::Error> {
327 match value {
328 Ok(result) => Ok(Self::from(result)),
329 Err(err) => Err(EvmError::from(err)),
330 }
331 }
332}
333
334#[expect(clippy::too_many_arguments)]
336pub async fn handle_traces(
337 mut result: TraceResult,
338 config: &Config,
339 chain: Option<Chain>,
340 contracts_bytecode: &HashMap<Address, Bytes>,
341 labels: Vec<String>,
342 with_local_artifacts: bool,
343 debug: bool,
344 decode_internal: bool,
345 disable_label: bool,
346) -> Result<()> {
347 let (known_contracts, mut sources) = if with_local_artifacts {
348 let _ = sh_println!("Compiling project to generate artifacts");
349 let project = config.project()?;
350 let compiler = ProjectCompiler::new();
351 let output = compiler.compile(&project)?;
352 (
353 Some(ContractsByArtifact::new(
354 output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
355 )),
356 ContractSources::from_project_output(&output, project.root(), None)?,
357 )
358 } else {
359 (None, ContractSources::default())
360 };
361
362 let labels = labels.iter().filter_map(|label_str| {
363 let mut iter = label_str.split(':');
364
365 if let Some(addr) = iter.next()
366 && let (Ok(address), Some(label)) = (Address::from_str(addr), iter.next())
367 {
368 return Some((address, label.to_string()));
369 }
370 None
371 });
372 let config_labels = config.labels.clone().into_iter();
373
374 let mut builder = CallTraceDecoderBuilder::new()
375 .with_labels(labels.chain(config_labels))
376 .with_signature_identifier(SignaturesIdentifier::from_config(config)?)
377 .with_label_disabled(disable_label);
378 let mut identifier = TraceIdentifiers::new().with_etherscan(config, chain)?;
379 if let Some(contracts) = &known_contracts {
380 builder = builder.with_known_contracts(contracts);
381 identifier = identifier.with_local_and_bytecodes(contracts, contracts_bytecode);
382 }
383
384 let mut decoder = builder.build();
385
386 for (_, trace) in result.traces.as_deref_mut().unwrap_or_default() {
387 decoder.identify(trace, &mut identifier);
388 }
389
390 if decode_internal || debug {
391 if let Some(ref etherscan_identifier) = identifier.etherscan {
392 sources.merge(etherscan_identifier.get_compiled_contracts().await?);
393 }
394
395 if debug {
396 let mut debugger = Debugger::builder()
397 .traces(result.traces.expect("missing traces"))
398 .decoder(&decoder)
399 .sources(sources)
400 .build();
401 debugger.try_run_tui()?;
402 return Ok(());
403 }
404
405 decoder.debug_identifier = Some(DebugTraceIdentifier::new(sources));
406 }
407
408 print_traces(&mut result, &decoder, shell::verbosity() > 0, shell::verbosity() > 4).await?;
409
410 Ok(())
411}
412
413pub async fn print_traces(
414 result: &mut TraceResult,
415 decoder: &CallTraceDecoder,
416 verbose: bool,
417 state_changes: bool,
418) -> Result<()> {
419 let traces = result.traces.as_mut().expect("No traces found");
420
421 if !shell::is_json() {
422 sh_println!("Traces:")?;
423 }
424
425 for (_, arena) in traces {
426 decode_trace_arena(arena, decoder).await;
427 sh_println!("{}", render_trace_arena_inner(arena, verbose, state_changes))?;
428 }
429
430 if shell::is_json() {
431 return Ok(());
432 }
433
434 sh_println!()?;
435 if result.success {
436 sh_println!("{}", "Transaction successfully executed.".green())?;
437 } else {
438 sh_err!("Transaction failed.")?;
439 }
440 sh_println!("Gas used: {}", result.gas_used)?;
441
442 Ok(())
443}
444
445pub fn cache_local_signatures(output: &ProjectCompileOutput) -> Result<()> {
448 let Some(cache_dir) = Config::foundry_cache_dir() else {
449 eyre::bail!("Failed to get `cache_dir` to generate local signatures.");
450 };
451 let path = cache_dir.join("signatures");
452 let mut signatures = SignaturesCache::load(&path);
453 for (_, artifact) in output.artifacts() {
454 if let Some(abi) = &artifact.abi {
455 signatures.extend_from_abi(abi);
456 }
457
458 if let Some(method_identifiers) = &artifact.method_identifiers {
460 signatures.extend(method_identifiers.iter().filter_map(|(signature, selector)| {
461 Some((SelectorKind::Function(selector.parse().ok()?), signature.clone()))
462 }));
463 }
464 }
465 signatures.save(&path);
466 Ok(())
467}