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