foundry_cli/utils/
cmd.rs

1use alloy_json_abi::JsonAbi;
2use alloy_primitives::Address;
3use eyre::{Result, WrapErr};
4use foundry_common::{
5    compile::ProjectCompiler, fs, selectors::SelectorKind, shell, ContractsByArtifact,
6    TestFunctionExt,
7};
8use foundry_compilers::{
9    artifacts::{CompactBytecode, Settings},
10    cache::{CacheEntry, CompilerCache},
11    utils::read_json_file,
12    Artifact, ArtifactId, ProjectCompileOutput,
13};
14use foundry_config::{error::ExtractConfigError, figment::Figment, Chain, Config, NamedChain};
15use foundry_debugger::Debugger;
16use foundry_evm::{
17    executors::{DeployResult, EvmError, RawCallResult},
18    opts::EvmOpts,
19    traces::{
20        debug::{ContractSources, DebugTraceIdentifier},
21        decode_trace_arena,
22        identifier::{SignaturesCache, SignaturesIdentifier, TraceIdentifiers},
23        render_trace_arena_inner, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces,
24    },
25};
26use std::{
27    fmt::Write,
28    path::{Path, PathBuf},
29    str::FromStr,
30};
31use yansi::Paint;
32
33/// Given a `Project`'s output, removes the matching ABI, Bytecode and
34/// Runtime Bytecode of the given contract.
35#[track_caller]
36pub fn remove_contract(
37    output: ProjectCompileOutput,
38    path: &Path,
39    name: &str,
40) -> Result<(JsonAbi, CompactBytecode, ArtifactId)> {
41    let mut other = Vec::new();
42    let Some((id, contract)) = output.into_artifacts().find_map(|(id, artifact)| {
43        if id.name == name && id.source == path {
44            Some((id, artifact))
45        } else {
46            other.push(id.name);
47            None
48        }
49    }) else {
50        let mut err = format!("could not find artifact: `{name}`");
51        if let Some(suggestion) = super::did_you_mean(name, other).pop() {
52            if suggestion != name {
53                err = format!(
54                    r#"{err}
55
56        Did you mean `{suggestion}`?"#
57                );
58            }
59        }
60        eyre::bail!(err)
61    };
62
63    let abi = contract
64        .get_abi()
65        .ok_or_else(|| eyre::eyre!("contract {} does not contain abi", name))?
66        .into_owned();
67
68    let bin = contract
69        .get_bytecode()
70        .ok_or_else(|| eyre::eyre!("contract {} does not contain bytecode", name))?
71        .into_owned();
72
73    Ok((abi, bin, id))
74}
75
76/// Helper function for finding a contract by ContractName
77// TODO: Is there a better / more ergonomic way to get the artifacts given a project and a
78// contract name?
79pub fn get_cached_entry_by_name(
80    cache: &CompilerCache<Settings>,
81    name: &str,
82) -> Result<(PathBuf, CacheEntry)> {
83    let mut cached_entry = None;
84    let mut alternatives = Vec::new();
85
86    for (abs_path, entry) in &cache.files {
87        for artifact_name in entry.artifacts.keys() {
88            if artifact_name == name {
89                if cached_entry.is_some() {
90                    eyre::bail!(
91                        "contract with duplicate name `{}`. please pass the path instead",
92                        name
93                    )
94                }
95                cached_entry = Some((abs_path.to_owned(), entry.to_owned()));
96            } else {
97                alternatives.push(artifact_name);
98            }
99        }
100    }
101
102    if let Some(entry) = cached_entry {
103        return Ok(entry);
104    }
105
106    let mut err = format!("could not find artifact: `{name}`");
107    if let Some(suggestion) = super::did_you_mean(name, &alternatives).pop() {
108        err = format!(
109            r#"{err}
110
111        Did you mean `{suggestion}`?"#
112        );
113    }
114    eyre::bail!(err)
115}
116
117/// Returns error if constructor has arguments.
118pub fn ensure_clean_constructor(abi: &JsonAbi) -> Result<()> {
119    if let Some(constructor) = &abi.constructor {
120        if !constructor.inputs.is_empty() {
121            eyre::bail!("Contract constructor should have no arguments. Add those arguments to  `run(...)` instead, and call it with `--sig run(...)`.");
122        }
123    }
124    Ok(())
125}
126
127pub fn needs_setup(abi: &JsonAbi) -> bool {
128    let setup_fns: Vec<_> = abi.functions().filter(|func| func.name.is_setup()).collect();
129
130    for setup_fn in &setup_fns {
131        if setup_fn.name != "setUp" {
132            let _ = sh_warn!(
133                "Found invalid setup function \"{}\" did you mean \"setUp()\"?",
134                setup_fn.signature()
135            );
136        }
137    }
138
139    setup_fns.len() == 1 && setup_fns[0].name == "setUp"
140}
141
142pub fn eta_key(state: &indicatif::ProgressState, f: &mut dyn Write) {
143    write!(f, "{:.1}s", state.eta().as_secs_f64()).unwrap()
144}
145
146pub fn init_progress(len: u64, label: &str) -> indicatif::ProgressBar {
147    let pb = indicatif::ProgressBar::new(len);
148    let mut template =
149        "{prefix}{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} "
150            .to_string();
151    write!(template, "{label}").unwrap();
152    template += " ({eta})";
153    pb.set_style(
154        indicatif::ProgressStyle::with_template(&template)
155            .unwrap()
156            .with_key("eta", crate::utils::eta_key)
157            .progress_chars("#>-"),
158    );
159    pb
160}
161
162/// True if the network calculates gas costs differently.
163pub fn has_different_gas_calc(chain_id: u64) -> bool {
164    if let Some(chain) = Chain::from(chain_id).named() {
165        return chain.is_arbitrum() ||
166            matches!(
167                chain,
168                NamedChain::Acala |
169                    NamedChain::AcalaMandalaTestnet |
170                    NamedChain::AcalaTestnet |
171                    NamedChain::Etherlink |
172                    NamedChain::EtherlinkTestnet |
173                    NamedChain::Karura |
174                    NamedChain::KaruraTestnet |
175                    NamedChain::Mantle |
176                    NamedChain::MantleSepolia |
177                    NamedChain::MantleTestnet |
178                    NamedChain::Moonbase |
179                    NamedChain::Moonbeam |
180                    NamedChain::MoonbeamDev |
181                    NamedChain::Moonriver |
182                    NamedChain::Metis |
183                    NamedChain::Abstract |
184                    NamedChain::ZkSync |
185                    NamedChain::ZkSyncTestnet
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
335pub async fn handle_traces(
336    mut result: TraceResult,
337    config: &Config,
338    chain: Option<Chain>,
339    labels: Vec<String>,
340    with_local_artifacts: bool,
341    debug: bool,
342    decode_internal: bool,
343) -> Result<()> {
344    let (known_contracts, mut sources) = if with_local_artifacts {
345        let _ = sh_println!("Compiling project to generate artifacts");
346        let project = config.project()?;
347        let compiler = ProjectCompiler::new();
348        let output = compiler.compile(&project)?;
349        (
350            Some(ContractsByArtifact::new(
351                output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
352            )),
353            ContractSources::from_project_output(&output, project.root(), None)?,
354        )
355    } else {
356        (None, ContractSources::default())
357    };
358
359    let labels = labels.iter().filter_map(|label_str| {
360        let mut iter = label_str.split(':');
361
362        if let Some(addr) = iter.next() {
363            if let (Ok(address), Some(label)) = (Address::from_str(addr), iter.next()) {
364                return Some((address, label.to_string()));
365            }
366        }
367        None
368    });
369    let config_labels = config.labels.clone().into_iter();
370
371    let mut builder = CallTraceDecoderBuilder::new()
372        .with_labels(labels.chain(config_labels))
373        .with_signature_identifier(SignaturesIdentifier::from_config(config)?);
374    let mut identifier = TraceIdentifiers::new().with_etherscan(config, chain)?;
375    if let Some(contracts) = &known_contracts {
376        builder = builder.with_known_contracts(contracts);
377        identifier = identifier.with_local(contracts);
378    }
379
380    let mut decoder = builder.build();
381
382    for (_, trace) in result.traces.as_deref_mut().unwrap_or_default() {
383        decoder.identify(trace, &mut identifier);
384    }
385
386    if decode_internal || debug {
387        if let Some(ref etherscan_identifier) = identifier.etherscan {
388            sources.merge(etherscan_identifier.get_compiled_contracts().await?);
389        }
390
391        if debug {
392            let mut debugger = Debugger::builder()
393                .traces(result.traces.expect("missing traces"))
394                .decoder(&decoder)
395                .sources(sources)
396                .build();
397            debugger.try_run_tui()?;
398            return Ok(())
399        }
400
401        decoder.debug_identifier = Some(DebugTraceIdentifier::new(sources));
402    }
403
404    print_traces(&mut result, &decoder, shell::verbosity() > 0, shell::verbosity() > 4).await?;
405
406    Ok(())
407}
408
409pub async fn print_traces(
410    result: &mut TraceResult,
411    decoder: &CallTraceDecoder,
412    verbose: bool,
413    state_changes: bool,
414) -> Result<()> {
415    let traces = result.traces.as_mut().expect("No traces found");
416
417    if !shell::is_json() {
418        sh_println!("Traces:")?;
419    }
420
421    for (_, arena) in traces {
422        decode_trace_arena(arena, decoder).await;
423        sh_println!("{}", render_trace_arena_inner(arena, verbose, state_changes))?;
424    }
425
426    if shell::is_json() {
427        return Ok(());
428    }
429
430    sh_println!()?;
431    if result.success {
432        sh_println!("{}", "Transaction successfully executed.".green())?;
433    } else {
434        sh_err!("Transaction failed.")?;
435    }
436    sh_println!("Gas used: {}", result.gas_used)?;
437
438    Ok(())
439}
440
441/// Traverse the artifacts in the project to generate local signatures and merge them into the cache
442/// file.
443pub fn cache_local_signatures(output: &ProjectCompileOutput, cache_dir: &Path) -> Result<()> {
444    let path = cache_dir.join("signatures");
445    let mut signatures = SignaturesCache::load(&path);
446    for (_, artifact) in output.artifacts() {
447        if let Some(abi) = &artifact.abi {
448            signatures.extend_from_abi(abi);
449        }
450
451        // External libraries don't have functions included in the ABI, but `methodIdentifiers`.
452        if let Some(method_identifiers) = &artifact.method_identifiers {
453            signatures.extend(method_identifiers.iter().filter_map(|(signature, selector)| {
454                Some((SelectorKind::Function(selector.parse().ok()?), signature.clone()))
455            }));
456        }
457    }
458    signatures.save(&path);
459    Ok(())
460}