foundry_cli/utils/
cmd.rs

1use alloy_json_abi::JsonAbi;
2use eyre::{Result, WrapErr};
3use foundry_common::{TestFunctionExt, fs, selectors::SelectorKind, shell};
4use foundry_compilers::{
5    Artifact, ArtifactId, ProjectCompileOutput,
6    artifacts::{CompactBytecode, Settings},
7    cache::{CacheEntry, CompilerCache},
8    utils::read_json_file,
9};
10use foundry_config::{Chain, Config, NamedChain, error::ExtractConfigError, figment::Figment};
11use foundry_evm::{
12    executors::{DeployResult, EvmError, RawCallResult},
13    opts::EvmOpts,
14    traces::{
15        CallTraceDecoder, TraceKind, Traces, decode_trace_arena, identifier::SignaturesCache,
16        render_trace_arena_inner,
17    },
18};
19use std::{
20    fmt::Write,
21    path::{Path, PathBuf},
22};
23use yansi::Paint;
24
25/// Given a `Project`'s output, removes the matching ABI, Bytecode and
26/// Runtime Bytecode of the given contract.
27#[track_caller]
28pub fn remove_contract(
29    output: ProjectCompileOutput,
30    path: &Path,
31    name: &str,
32) -> Result<(JsonAbi, CompactBytecode, ArtifactId)> {
33    let mut other = Vec::new();
34    let Some((id, contract)) = output.into_artifacts().find_map(|(id, artifact)| {
35        if id.name == name && id.source == path {
36            Some((id, artifact))
37        } else {
38            other.push(id.name);
39            None
40        }
41    }) else {
42        let mut err = format!("could not find artifact: `{name}`");
43        if let Some(suggestion) = super::did_you_mean(name, other).pop()
44            && suggestion != name
45        {
46            err = format!(
47                r#"{err}
48
49        Did you mean `{suggestion}`?"#
50            );
51        }
52        eyre::bail!(err)
53    };
54
55    let abi = contract
56        .get_abi()
57        .ok_or_else(|| eyre::eyre!("contract {} does not contain abi", name))?
58        .into_owned();
59
60    let bin = contract
61        .get_bytecode()
62        .ok_or_else(|| eyre::eyre!("contract {} does not contain bytecode", name))?
63        .into_owned();
64
65    Ok((abi, bin, id))
66}
67
68/// Helper function for finding a contract by ContractName
69// TODO: Is there a better / more ergonomic way to get the artifacts given a project and a
70// contract name?
71pub fn get_cached_entry_by_name(
72    cache: &CompilerCache<Settings>,
73    name: &str,
74) -> Result<(PathBuf, CacheEntry)> {
75    let mut cached_entry = None;
76    let mut alternatives = Vec::new();
77
78    for (abs_path, entry) in &cache.files {
79        for artifact_name in entry.artifacts.keys() {
80            if artifact_name == name {
81                if cached_entry.is_some() {
82                    eyre::bail!(
83                        "contract with duplicate name `{}`. please pass the path instead",
84                        name
85                    )
86                }
87                cached_entry = Some((abs_path.to_owned(), entry.to_owned()));
88            } else {
89                alternatives.push(artifact_name);
90            }
91        }
92    }
93
94    if let Some(entry) = cached_entry {
95        return Ok(entry);
96    }
97
98    let mut err = format!("could not find artifact: `{name}`");
99    if let Some(suggestion) = super::did_you_mean(name, &alternatives).pop() {
100        err = format!(
101            r#"{err}
102
103        Did you mean `{suggestion}`?"#
104        );
105    }
106    eyre::bail!(err)
107}
108
109/// Returns error if constructor has arguments.
110pub fn ensure_clean_constructor(abi: &JsonAbi) -> Result<()> {
111    if let Some(constructor) = &abi.constructor
112        && !constructor.inputs.is_empty()
113    {
114        eyre::bail!(
115            "Contract constructor should have no arguments. Add those arguments to  `run(...)` instead, and call it with `--sig run(...)`."
116        );
117    }
118    Ok(())
119}
120
121pub fn needs_setup(abi: &JsonAbi) -> bool {
122    let setup_fns: Vec<_> = abi.functions().filter(|func| func.name.is_setup()).collect();
123
124    for setup_fn in &setup_fns {
125        if setup_fn.name != "setUp" {
126            let _ = sh_warn!(
127                "Found invalid setup function \"{}\" did you mean \"setUp()\"?",
128                setup_fn.signature()
129            );
130        }
131    }
132
133    setup_fns.len() == 1 && setup_fns[0].name == "setUp"
134}
135
136pub fn eta_key(state: &indicatif::ProgressState, f: &mut dyn Write) {
137    write!(f, "{:.1}s", state.eta().as_secs_f64()).unwrap()
138}
139
140pub fn init_progress(len: u64, label: &str) -> indicatif::ProgressBar {
141    let pb = indicatif::ProgressBar::new(len);
142    let mut template =
143        "{prefix}{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} "
144            .to_string();
145    write!(template, "{label}").unwrap();
146    template += " ({eta})";
147    pb.set_style(
148        indicatif::ProgressStyle::with_template(&template)
149            .unwrap()
150            .with_key("eta", crate::utils::eta_key)
151            .progress_chars("#>-"),
152    );
153    pb
154}
155
156/// True if the network calculates gas costs differently.
157pub fn has_different_gas_calc(chain_id: u64) -> bool {
158    if let Some(chain) = Chain::from(chain_id).named() {
159        return chain.is_arbitrum()
160            || chain.is_elastic()
161            || matches!(
162                chain,
163                NamedChain::Acala
164                    | NamedChain::AcalaMandalaTestnet
165                    | NamedChain::AcalaTestnet
166                    | NamedChain::Etherlink
167                    | NamedChain::EtherlinkTestnet
168                    | NamedChain::Karura
169                    | NamedChain::KaruraTestnet
170                    | NamedChain::Mantle
171                    | NamedChain::MantleSepolia
172                    | NamedChain::Moonbase
173                    | NamedChain::Moonbeam
174                    | NamedChain::MoonbeamDev
175                    | NamedChain::Moonriver
176                    | NamedChain::Metis
177            );
178    }
179    false
180}
181
182/// True if it supports broadcasting in batches.
183pub fn has_batch_support(chain_id: u64) -> bool {
184    if let Some(chain) = Chain::from(chain_id).named() {
185        return !chain.is_arbitrum();
186    }
187    true
188}
189
190/// Helpers for loading configuration.
191///
192/// This is usually implemented through the macros defined in [`foundry_config`]. See
193/// [`foundry_config::impl_figment_convert`] for more details.
194///
195/// By default each function will emit warnings generated during loading, unless the `_no_warnings`
196/// variant is used.
197pub trait LoadConfig {
198    /// Load the [`Config`] based on the options provided in self.
199    fn figment(&self) -> Figment;
200
201    /// Load and sanitize the [`Config`] based on the options provided in self.
202    fn load_config(&self) -> Result<Config, ExtractConfigError> {
203        self.load_config_no_warnings().inspect(emit_warnings)
204    }
205
206    /// Same as [`LoadConfig::load_config`] but does not emit warnings.
207    fn load_config_no_warnings(&self) -> Result<Config, ExtractConfigError> {
208        self.load_config_unsanitized_no_warnings().map(Config::sanitized)
209    }
210
211    /// Load [`Config`] but do not sanitize. See [`Config::sanitized`] for more information.
212    fn load_config_unsanitized(&self) -> Result<Config, ExtractConfigError> {
213        self.load_config_unsanitized_no_warnings().inspect(emit_warnings)
214    }
215
216    /// Same as [`LoadConfig::load_config_unsanitized`] but also emits warnings generated
217    fn load_config_unsanitized_no_warnings(&self) -> Result<Config, ExtractConfigError> {
218        Config::from_provider(self.figment())
219    }
220
221    /// Load and sanitize the [`Config`], as well as extract [`EvmOpts`] from self
222    fn load_config_and_evm_opts(&self) -> Result<(Config, EvmOpts)> {
223        self.load_config_and_evm_opts_no_warnings().inspect(|(config, _)| emit_warnings(config))
224    }
225
226    /// Same as [`LoadConfig::load_config_and_evm_opts`] but also emits warnings generated
227    fn load_config_and_evm_opts_no_warnings(&self) -> Result<(Config, EvmOpts)> {
228        let figment = self.figment();
229
230        let mut evm_opts = figment.extract::<EvmOpts>().map_err(ExtractConfigError::new)?;
231        let config = Config::from_provider(figment)?.sanitized();
232
233        // update the fork url if it was an alias
234        if let Some(fork_url) = config.get_rpc_url() {
235            trace!(target: "forge::config", ?fork_url, "Update EvmOpts fork url");
236            evm_opts.fork_url = Some(fork_url?.into_owned());
237        }
238
239        Ok((config, evm_opts))
240    }
241}
242
243impl<T> LoadConfig for T
244where
245    for<'a> Figment: From<&'a T>,
246{
247    fn figment(&self) -> Figment {
248        self.into()
249    }
250}
251
252fn emit_warnings(config: &Config) {
253    for warning in &config.warnings {
254        let _ = sh_warn!("{warning}");
255    }
256}
257
258/// Read contract constructor arguments from the given file.
259pub fn read_constructor_args_file(constructor_args_path: PathBuf) -> Result<Vec<String>> {
260    if !constructor_args_path.exists() {
261        eyre::bail!("Constructor args file \"{}\" not found", constructor_args_path.display());
262    }
263    let args = if constructor_args_path.extension() == Some(std::ffi::OsStr::new("json")) {
264        read_json_file(&constructor_args_path).wrap_err(format!(
265            "Constructor args file \"{}\" must encode a json array",
266            constructor_args_path.display(),
267        ))?
268    } else {
269        fs::read_to_string(constructor_args_path)?.split_whitespace().map(str::to_string).collect()
270    };
271    Ok(args)
272}
273
274/// A slimmed down return from the executor used for returning minimal trace + gas metering info
275#[derive(Debug)]
276pub struct TraceResult {
277    pub success: bool,
278    pub traces: Option<Traces>,
279    pub gas_used: u64,
280}
281
282impl TraceResult {
283    /// Create a new [`TraceResult`] from a [`RawCallResult`].
284    pub fn from_raw(raw: RawCallResult, trace_kind: TraceKind) -> Self {
285        let RawCallResult { gas_used, traces, reverted, .. } = raw;
286        Self { success: !reverted, traces: traces.map(|arena| vec![(trace_kind, arena)]), gas_used }
287    }
288}
289
290impl From<DeployResult> for TraceResult {
291    fn from(result: DeployResult) -> Self {
292        Self::from_raw(result.raw, TraceKind::Deployment)
293    }
294}
295
296impl TryFrom<Result<DeployResult, EvmError>> for TraceResult {
297    type Error = EvmError;
298
299    fn try_from(value: Result<DeployResult, EvmError>) -> Result<Self, Self::Error> {
300        match value {
301            Ok(result) => Ok(Self::from(result)),
302            Err(EvmError::Execution(err)) => Ok(Self::from_raw(err.raw, TraceKind::Deployment)),
303            Err(err) => Err(err),
304        }
305    }
306}
307
308impl From<RawCallResult> for TraceResult {
309    fn from(result: RawCallResult) -> Self {
310        Self::from_raw(result, TraceKind::Execution)
311    }
312}
313
314impl TryFrom<Result<RawCallResult>> for TraceResult {
315    type Error = EvmError;
316
317    fn try_from(value: Result<RawCallResult>) -> Result<Self, Self::Error> {
318        match value {
319            Ok(result) => Ok(Self::from(result)),
320            Err(err) => Err(EvmError::from(err)),
321        }
322    }
323}
324
325pub async fn print_traces(
326    result: &mut TraceResult,
327    decoder: &CallTraceDecoder,
328    verbose: bool,
329    state_changes: bool,
330) -> Result<()> {
331    let traces = result.traces.as_mut().expect("No traces found");
332
333    if !shell::is_json() {
334        sh_println!("Traces:")?;
335    }
336
337    for (_, arena) in traces {
338        decode_trace_arena(arena, decoder).await;
339        sh_println!("{}", render_trace_arena_inner(arena, verbose, state_changes))?;
340    }
341
342    if shell::is_json() {
343        return Ok(());
344    }
345
346    sh_println!()?;
347    if result.success {
348        sh_println!("{}", "Transaction successfully executed.".green())?;
349    } else {
350        sh_err!("Transaction failed.")?;
351    }
352    sh_println!("Gas used: {}", result.gas_used)?;
353
354    Ok(())
355}
356
357/// Traverse the artifacts in the project to generate local signatures and merge them into the cache
358/// file.
359pub fn cache_local_signatures(output: &ProjectCompileOutput) -> Result<()> {
360    let Some(cache_dir) = Config::foundry_cache_dir() else {
361        eyre::bail!("Failed to get `cache_dir` to generate local signatures.");
362    };
363    let path = cache_dir.join("signatures");
364    let mut signatures = SignaturesCache::load(&path);
365    for (_, artifact) in output.artifacts() {
366        if let Some(abi) = &artifact.abi {
367            signatures.extend_from_abi(abi);
368        }
369
370        // External libraries don't have functions included in the ABI, but `methodIdentifiers`.
371        if let Some(method_identifiers) = &artifact.method_identifiers {
372            signatures.extend(method_identifiers.iter().filter_map(|(signature, selector)| {
373                Some((SelectorKind::Function(selector.parse().ok()?), signature.clone()))
374            }));
375        }
376    }
377    signatures.save(&path);
378    Ok(())
379}