foundry_cli/utils/
cmd.rs

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/// Given a `Project`'s output, removes the matching ABI, Bytecode and
31/// Runtime Bytecode of the given contract.
32#[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
73/// Helper function for finding a contract by ContractName
74// TODO: Is there a better / more ergonomic way to get the artifacts given a project and a
75// contract name?
76pub 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
114/// Returns error if constructor has arguments.
115pub 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
159/// True if the network calculates gas costs differently.
160pub 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
185/// True if it supports broadcasting in batches.
186pub 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
193/// Helpers for loading configuration.
194///
195/// This is usually implemented through the macros defined in [`foundry_config`]. See
196/// [`foundry_config::impl_figment_convert`] for more details.
197///
198/// By default each function will emit warnings generated during loading, unless the `_no_warnings`
199/// variant is used.
200pub trait LoadConfig {
201    /// Load the [`Config`] based on the options provided in self.
202    fn figment(&self) -> Figment;
203
204    /// Load and sanitize the [`Config`] based on the options provided in self.
205    fn load_config(&self) -> Result<Config, ExtractConfigError> {
206        self.load_config_no_warnings().inspect(emit_warnings)
207    }
208
209    /// Same as [`LoadConfig::load_config`] but does not emit warnings.
210    fn load_config_no_warnings(&self) -> Result<Config, ExtractConfigError> {
211        self.load_config_unsanitized_no_warnings().map(Config::sanitized)
212    }
213
214    /// Load [`Config`] but do not sanitize. See [`Config::sanitized`] for more information.
215    fn load_config_unsanitized(&self) -> Result<Config, ExtractConfigError> {
216        self.load_config_unsanitized_no_warnings().inspect(emit_warnings)
217    }
218
219    /// Same as [`LoadConfig::load_config_unsanitized`] but also emits warnings generated
220    fn load_config_unsanitized_no_warnings(&self) -> Result<Config, ExtractConfigError> {
221        Config::from_provider(self.figment())
222    }
223
224    /// Load and sanitize the [`Config`], as well as extract [`EvmOpts`] from self
225    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    /// Same as [`LoadConfig::load_config_and_evm_opts`] but also emits warnings generated
230    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        // update the fork url if it was an alias
237        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
261/// Read contract constructor arguments from the given file.
262pub 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/// A slimmed down return from the executor used for returning minimal trace + gas metering info
278#[derive(Debug)]
279pub struct TraceResult {
280    pub success: bool,
281    pub traces: Option<Traces>,
282    pub gas_used: u64,
283}
284
285impl TraceResult {
286    /// Create a new [`TraceResult`] from a [`RawCallResult`].
287    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
328/// labels the traces, conditionally prints them or opens the debugger
329pub 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
438/// Traverse the artifacts in the project to generate local signatures and merge them into the cache
439/// file.
440pub 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            // External libraries doesn't have functions included in abi, but `methodIdentifiers`.
457            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}