Skip to main content

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