foundry_cli/utils/
cmd.rs

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