foundry_cli/utils/
cmd.rs

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/// Given a `Project`'s output, removes the matching ABI, Bytecode and
35/// Runtime Bytecode of the given contract.
36#[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
77/// Helper function for finding a contract by ContractName
78// TODO: Is there a better / more ergonomic way to get the artifacts given a project and a
79// contract name?
80pub 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
118/// Returns error if constructor has arguments.
119pub 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
165/// True if the network calculates gas costs differently.
166pub 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
192/// True if it supports broadcasting in batches.
193pub 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
200/// Helpers for loading configuration.
201///
202/// This is usually implemented through the macros defined in [`foundry_config`]. See
203/// [`foundry_config::impl_figment_convert`] for more details.
204///
205/// By default each function will emit warnings generated during loading, unless the `_no_warnings`
206/// variant is used.
207pub trait LoadConfig {
208    /// Load the [`Config`] based on the options provided in self.
209    fn figment(&self) -> Figment;
210
211    /// Load and sanitize the [`Config`] based on the options provided in self.
212    fn load_config(&self) -> Result<Config, ExtractConfigError> {
213        self.load_config_no_warnings().inspect(emit_warnings)
214    }
215
216    /// Same as [`LoadConfig::load_config`] but does not emit warnings.
217    fn load_config_no_warnings(&self) -> Result<Config, ExtractConfigError> {
218        self.load_config_unsanitized_no_warnings().map(Config::sanitized)
219    }
220
221    /// Load [`Config`] but do not sanitize. See [`Config::sanitized`] for more information.
222    fn load_config_unsanitized(&self) -> Result<Config, ExtractConfigError> {
223        self.load_config_unsanitized_no_warnings().inspect(emit_warnings)
224    }
225
226    /// Same as [`LoadConfig::load_config_unsanitized`] but also emits warnings generated
227    fn load_config_unsanitized_no_warnings(&self) -> Result<Config, ExtractConfigError> {
228        Config::from_provider(self.figment())
229    }
230
231    /// Load and sanitize the [`Config`], as well as extract [`EvmOpts`] from self
232    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    /// Same as [`LoadConfig::load_config_and_evm_opts`] but also emits warnings generated
237    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        // update the fork url if it was an alias
244        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
268/// Read contract constructor arguments from the given file.
269pub 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/// A slimmed down return from the executor used for returning minimal trace + gas metering info
285#[derive(Debug)]
286pub struct TraceResult {
287    pub success: bool,
288    pub traces: Option<Traces>,
289    pub gas_used: u64,
290}
291
292impl TraceResult {
293    /// Create a new [`TraceResult`] from a [`RawCallResult`].
294    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/// labels the traces, conditionally prints them or opens the debugger
336#[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
446/// Traverse the artifacts in the project to generate local signatures and merge them into the cache
447/// file.
448pub 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        // External libraries don't have functions included in the ABI, but `methodIdentifiers`.
460        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}