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::Moonbase
182                    | NamedChain::Moonbeam
183                    | NamedChain::MoonbeamDev
184                    | NamedChain::Moonriver
185                    | NamedChain::Metis
186            );
187    }
188    false
189}
190
191/// True if it supports broadcasting in batches.
192pub 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
199/// Helpers for loading configuration.
200///
201/// This is usually implemented through the macros defined in [`foundry_config`]. See
202/// [`foundry_config::impl_figment_convert`] for more details.
203///
204/// By default each function will emit warnings generated during loading, unless the `_no_warnings`
205/// variant is used.
206pub trait LoadConfig {
207    /// Load the [`Config`] based on the options provided in self.
208    fn figment(&self) -> Figment;
209
210    /// Load and sanitize the [`Config`] based on the options provided in self.
211    fn load_config(&self) -> Result<Config, ExtractConfigError> {
212        self.load_config_no_warnings().inspect(emit_warnings)
213    }
214
215    /// Same as [`LoadConfig::load_config`] but does not emit warnings.
216    fn load_config_no_warnings(&self) -> Result<Config, ExtractConfigError> {
217        self.load_config_unsanitized_no_warnings().map(Config::sanitized)
218    }
219
220    /// Load [`Config`] but do not sanitize. See [`Config::sanitized`] for more information.
221    fn load_config_unsanitized(&self) -> Result<Config, ExtractConfigError> {
222        self.load_config_unsanitized_no_warnings().inspect(emit_warnings)
223    }
224
225    /// Same as [`LoadConfig::load_config_unsanitized`] but also emits warnings generated
226    fn load_config_unsanitized_no_warnings(&self) -> Result<Config, ExtractConfigError> {
227        Config::from_provider(self.figment())
228    }
229
230    /// Load and sanitize the [`Config`], as well as extract [`EvmOpts`] from self
231    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    /// Same as [`LoadConfig::load_config_and_evm_opts`] but also emits warnings generated
236    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        // update the fork url if it was an alias
243        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
267/// Read contract constructor arguments from the given file.
268pub 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/// A slimmed down return from the executor used for returning minimal trace + gas metering info
284#[derive(Debug)]
285pub struct TraceResult {
286    pub success: bool,
287    pub traces: Option<Traces>,
288    pub gas_used: u64,
289}
290
291impl TraceResult {
292    /// Create a new [`TraceResult`] from a [`RawCallResult`].
293    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/// labels the traces, conditionally prints them or opens the debugger
335#[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
445/// Traverse the artifacts in the project to generate local signatures and merge them into the cache
446/// file.
447pub 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        // External libraries don't have functions included in the ABI, but `methodIdentifiers`.
459        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}