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        prune_trace_depth, 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, finds the contract by path and name and returns its
26/// ABI, creation bytecode, and `ArtifactId`.
27#[track_caller]
28pub fn find_contract_artifacts(
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::Monad
173                    | NamedChain::MonadTestnet
174                    | NamedChain::Moonbase
175                    | NamedChain::Moonbeam
176                    | NamedChain::MoonbeamDev
177                    | NamedChain::Moonriver
178                    | NamedChain::Metis
179            );
180    }
181    false
182}
183
184/// True if it supports broadcasting in batches.
185pub fn has_batch_support(chain_id: u64) -> bool {
186    if let Some(chain) = Chain::from(chain_id).named() {
187        return !chain.is_arbitrum();
188    }
189    true
190}
191
192/// Helpers for loading configuration.
193///
194/// This is usually implemented through the macros defined in [`foundry_config`]. See
195/// [`foundry_config::impl_figment_convert`] for more details.
196///
197/// By default each function will emit warnings generated during loading, unless the `_no_warnings`
198/// variant is used.
199pub trait LoadConfig {
200    /// Load the [`Config`] based on the options provided in self.
201    fn figment(&self) -> Figment;
202
203    /// Load and sanitize the [`Config`] based on the options provided in self.
204    fn load_config(&self) -> Result<Config, ExtractConfigError> {
205        self.load_config_no_warnings().inspect(emit_warnings)
206    }
207
208    /// Same as [`LoadConfig::load_config`] but does not emit warnings.
209    fn load_config_no_warnings(&self) -> Result<Config, ExtractConfigError> {
210        self.load_config_unsanitized_no_warnings().map(Config::sanitized)
211    }
212
213    /// Load [`Config`] but do not sanitize. See [`Config::sanitized`] for more information.
214    fn load_config_unsanitized(&self) -> Result<Config, ExtractConfigError> {
215        self.load_config_unsanitized_no_warnings().inspect(emit_warnings)
216    }
217
218    /// Same as [`LoadConfig::load_config_unsanitized`] but also emits warnings generated
219    fn load_config_unsanitized_no_warnings(&self) -> Result<Config, ExtractConfigError> {
220        Config::from_provider(self.figment())
221    }
222
223    /// Load and sanitize the [`Config`], as well as extract [`EvmOpts`] from self
224    fn load_config_and_evm_opts(&self) -> Result<(Config, EvmOpts)> {
225        self.load_config_and_evm_opts_no_warnings().inspect(|(config, _)| emit_warnings(config))
226    }
227
228    /// Same as [`LoadConfig::load_config_and_evm_opts`] but also emits warnings generated
229    fn load_config_and_evm_opts_no_warnings(&self) -> Result<(Config, EvmOpts)> {
230        let figment = self.figment();
231
232        let mut evm_opts = figment.extract::<EvmOpts>().map_err(ExtractConfigError::new)?;
233        let config = Config::from_provider(figment)?.sanitized();
234
235        // update the fork url if it was an alias
236        if let Some(fork_url) = config.get_rpc_url() {
237            trace!(target: "forge::config", ?fork_url, "Update EvmOpts fork url");
238            evm_opts.fork_url = Some(fork_url?.into_owned());
239        }
240
241        Ok((config, evm_opts))
242    }
243}
244
245impl<T> LoadConfig for T
246where
247    for<'a> Figment: From<&'a T>,
248{
249    fn figment(&self) -> Figment {
250        self.into()
251    }
252}
253
254fn emit_warnings(config: &Config) {
255    for warning in &config.warnings {
256        let _ = sh_warn!("{warning}");
257    }
258}
259
260/// Read contract constructor arguments from the given file.
261pub fn read_constructor_args_file(constructor_args_path: PathBuf) -> Result<Vec<String>> {
262    if !constructor_args_path.exists() {
263        eyre::bail!("Constructor args file \"{}\" not found", constructor_args_path.display());
264    }
265    let args = if constructor_args_path.extension() == Some(std::ffi::OsStr::new("json")) {
266        read_json_file(&constructor_args_path).wrap_err(format!(
267            "Constructor args file \"{}\" must encode a json array",
268            constructor_args_path.display(),
269        ))?
270    } else {
271        fs::read_to_string(constructor_args_path)?.split_whitespace().map(str::to_string).collect()
272    };
273    Ok(args)
274}
275
276/// A slimmed down return from the executor used for returning minimal trace + gas metering info
277#[derive(Debug)]
278pub struct TraceResult {
279    pub success: bool,
280    pub traces: Option<Traces>,
281    pub gas_used: u64,
282}
283
284impl TraceResult {
285    /// Create a new [`TraceResult`] from a [`RawCallResult`].
286    pub fn from_raw(raw: RawCallResult, trace_kind: TraceKind) -> Self {
287        let RawCallResult { gas_used, traces, reverted, .. } = raw;
288        Self { success: !reverted, traces: traces.map(|arena| vec![(trace_kind, arena)]), gas_used }
289    }
290}
291
292impl From<DeployResult> for TraceResult {
293    fn from(result: DeployResult) -> Self {
294        Self::from_raw(result.raw, TraceKind::Deployment)
295    }
296}
297
298impl TryFrom<Result<DeployResult, EvmError>> for TraceResult {
299    type Error = EvmError;
300
301    fn try_from(value: Result<DeployResult, EvmError>) -> Result<Self, Self::Error> {
302        match value {
303            Ok(result) => Ok(Self::from(result)),
304            Err(EvmError::Execution(err)) => Ok(Self::from_raw(err.raw, TraceKind::Deployment)),
305            Err(err) => Err(err),
306        }
307    }
308}
309
310impl From<RawCallResult> for TraceResult {
311    fn from(result: RawCallResult) -> Self {
312        Self::from_raw(result, TraceKind::Execution)
313    }
314}
315
316impl TryFrom<Result<RawCallResult>> for TraceResult {
317    type Error = EvmError;
318
319    fn try_from(value: Result<RawCallResult>) -> Result<Self, Self::Error> {
320        match value {
321            Ok(result) => Ok(Self::from(result)),
322            Err(err) => Err(EvmError::from(err)),
323        }
324    }
325}
326
327pub async fn print_traces(
328    result: &mut TraceResult,
329    decoder: &CallTraceDecoder,
330    verbose: bool,
331    state_changes: bool,
332    trace_depth: Option<usize>,
333) -> Result<()> {
334    let traces = result.traces.as_mut().expect("No traces found");
335
336    if !shell::is_json() {
337        sh_println!("Traces:")?;
338    }
339
340    for (_, arena) in traces {
341        decode_trace_arena(arena, decoder).await;
342
343        if let Some(trace_depth) = trace_depth {
344            prune_trace_depth(arena, trace_depth);
345        }
346
347        sh_println!("{}", render_trace_arena_inner(arena, verbose, state_changes))?;
348    }
349
350    if shell::is_json() {
351        return Ok(());
352    }
353
354    sh_println!()?;
355    if result.success {
356        sh_println!("{}", "Transaction successfully executed.".green())?;
357    } else {
358        sh_err!("Transaction failed.")?;
359    }
360    sh_println!("Gas used: {}", result.gas_used)?;
361
362    Ok(())
363}
364
365/// Traverse the artifacts in the project to generate local signatures and merge them into the cache
366/// file.
367pub fn cache_local_signatures(output: &ProjectCompileOutput) -> Result<()> {
368    let Some(cache_dir) = Config::foundry_cache_dir() else {
369        eyre::bail!("Failed to get `cache_dir` to generate local signatures.");
370    };
371    let path = cache_dir.join("signatures");
372    let mut signatures = SignaturesCache::load(&path);
373    for (_, artifact) in output.artifacts() {
374        if let Some(abi) = &artifact.abi {
375            signatures.extend_from_abi(abi);
376        }
377
378        // External libraries don't have functions included in the ABI, but `methodIdentifiers`.
379        if let Some(method_identifiers) = &artifact.method_identifiers {
380            signatures.extend(method_identifiers.iter().filter_map(|(signature, selector)| {
381                Some((SelectorKind::Function(selector.parse().ok()?), signature.clone()))
382            }));
383        }
384    }
385    signatures.save(&path);
386    Ok(())
387}