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