foundry_cli/utils/
mod.rs

1use alloy_json_abi::JsonAbi;
2use alloy_primitives::U256;
3use alloy_provider::{network::AnyNetwork, Provider};
4use eyre::{ContextCompat, Result};
5use foundry_common::{
6    provider::{ProviderBuilder, RetryProvider},
7    shell,
8};
9use foundry_config::{Chain, Config};
10use serde::de::DeserializeOwned;
11use std::{
12    ffi::OsStr,
13    future::Future,
14    path::{Path, PathBuf},
15    process::{Command, Output, Stdio},
16    time::{Duration, SystemTime, UNIX_EPOCH},
17};
18use tracing_subscriber::prelude::*;
19
20mod cmd;
21pub use cmd::*;
22
23mod suggestions;
24pub use suggestions::*;
25
26mod abi;
27pub use abi::*;
28
29mod allocator;
30pub use allocator::*;
31
32// reexport all `foundry_config::utils`
33#[doc(hidden)]
34pub use foundry_config::utils::*;
35
36/// Deterministic fuzzer seed used for gas snapshots and coverage reports.
37///
38/// The keccak256 hash of "foundry rulez"
39pub const STATIC_FUZZ_SEED: [u8; 32] = [
40    0x01, 0x00, 0xfa, 0x69, 0xa5, 0xf1, 0x71, 0x0a, 0x95, 0xcd, 0xef, 0x94, 0x88, 0x9b, 0x02, 0x84,
41    0x5d, 0x64, 0x0b, 0x19, 0xad, 0xf0, 0xe3, 0x57, 0xb8, 0xd4, 0xbe, 0x7d, 0x49, 0xee, 0x70, 0xe6,
42];
43
44/// Useful extensions to [`std::path::Path`].
45pub trait FoundryPathExt {
46    /// Returns true if the [`Path`] ends with `.t.sol`
47    fn is_sol_test(&self) -> bool;
48
49    /// Returns true if the  [`Path`] has a `sol` extension
50    fn is_sol(&self) -> bool;
51
52    /// Returns true if the  [`Path`] has a `yul` extension
53    fn is_yul(&self) -> bool;
54}
55
56impl<T: AsRef<Path>> FoundryPathExt for T {
57    fn is_sol_test(&self) -> bool {
58        self.as_ref()
59            .file_name()
60            .and_then(|s| s.to_str())
61            .map(|s| s.ends_with(".t.sol"))
62            .unwrap_or_default()
63    }
64
65    fn is_sol(&self) -> bool {
66        self.as_ref().extension() == Some(std::ffi::OsStr::new("sol"))
67    }
68
69    fn is_yul(&self) -> bool {
70        self.as_ref().extension() == Some(std::ffi::OsStr::new("yul"))
71    }
72}
73
74/// Initializes a tracing Subscriber for logging
75pub fn subscriber() {
76    let registry = tracing_subscriber::Registry::default()
77        .with(tracing_subscriber::EnvFilter::from_default_env());
78    #[cfg(feature = "tracy")]
79    let registry = registry.with(tracing_tracy::TracyLayer::default());
80    registry.with(tracing_subscriber::fmt::layer()).init()
81}
82
83pub fn abi_to_solidity(abi: &JsonAbi, name: &str) -> Result<String> {
84    let s = abi.to_sol(name, None);
85    let s = forge_fmt::format(&s)?;
86    Ok(s)
87}
88
89/// Returns a [RetryProvider] instantiated using [Config]'s
90/// RPC
91pub fn get_provider(config: &Config) -> Result<RetryProvider> {
92    get_provider_builder(config)?.build()
93}
94
95/// Returns a [ProviderBuilder] instantiated using [Config] values.
96///
97/// Defaults to `http://localhost:8545` and `Mainnet`.
98pub fn get_provider_builder(config: &Config) -> Result<ProviderBuilder> {
99    let url = config.get_rpc_url_or_localhost_http()?;
100    let mut builder = ProviderBuilder::new(url.as_ref());
101
102    if let Ok(chain) = config.chain.unwrap_or_default().try_into() {
103        builder = builder.chain(chain);
104    }
105
106    if let Some(jwt) = config.get_rpc_jwt_secret()? {
107        builder = builder.jwt(jwt.as_ref());
108    }
109
110    if let Some(rpc_timeout) = config.eth_rpc_timeout {
111        builder = builder.timeout(Duration::from_secs(rpc_timeout));
112    }
113
114    if let Some(rpc_headers) = config.eth_rpc_headers.clone() {
115        builder = builder.headers(rpc_headers);
116    }
117
118    Ok(builder)
119}
120
121pub async fn get_chain<P>(chain: Option<Chain>, provider: P) -> Result<Chain>
122where
123    P: Provider<AnyNetwork>,
124{
125    match chain {
126        Some(chain) => Ok(chain),
127        None => Ok(Chain::from_id(provider.get_chain_id().await?)),
128    }
129}
130
131/// Parses an ether value from a string.
132///
133/// The amount can be tagged with a unit, e.g. "1ether".
134///
135/// If the string represents an untagged amount (e.g. "100") then
136/// it is interpreted as wei.
137pub fn parse_ether_value(value: &str) -> Result<U256> {
138    Ok(if value.starts_with("0x") {
139        U256::from_str_radix(value, 16)?
140    } else {
141        alloy_dyn_abi::DynSolType::coerce_str(&alloy_dyn_abi::DynSolType::Uint(256), value)?
142            .as_uint()
143            .wrap_err("Could not parse ether value from string")?
144            .0
145    })
146}
147
148/// Parses a `T` from a string using [`serde_json::from_str`].
149pub fn parse_json<T: DeserializeOwned>(value: &str) -> serde_json::Result<T> {
150    serde_json::from_str(value)
151}
152
153/// Parses a `Duration` from a &str
154pub fn parse_delay(delay: &str) -> Result<Duration> {
155    let delay = if delay.ends_with("ms") {
156        let d: u64 = delay.trim_end_matches("ms").parse()?;
157        Duration::from_millis(d)
158    } else {
159        let d: f64 = delay.parse()?;
160        let delay = (d * 1000.0).round();
161        if delay.is_infinite() || delay.is_nan() || delay.is_sign_negative() {
162            eyre::bail!("delay must be finite and non-negative");
163        }
164
165        Duration::from_millis(delay as u64)
166    };
167    Ok(delay)
168}
169
170/// Returns the current time as a [`Duration`] since the Unix epoch.
171pub fn now() -> Duration {
172    SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards")
173}
174
175/// Runs the `future` in a new [`tokio::runtime::Runtime`]
176pub fn block_on<F: Future>(future: F) -> F::Output {
177    let rt = tokio::runtime::Runtime::new().expect("could not start tokio rt");
178    rt.block_on(future)
179}
180
181/// Loads a dotenv file, from the cwd and the project root, ignoring potential failure.
182///
183/// We could use `warn!` here, but that would imply that the dotenv file can't configure
184/// the logging behavior of Foundry.
185///
186/// Similarly, we could just use `eprintln!`, but colors are off limits otherwise dotenv is implied
187/// to not be able to configure the colors. It would also mess up the JSON output.
188pub fn load_dotenv() {
189    let load = |p: &Path| {
190        dotenvy::from_path(p.join(".env")).ok();
191    };
192
193    // we only want the .env file of the cwd and project root
194    // `find_project_root` calls `current_dir` internally so both paths are either both `Ok` or
195    // both `Err`
196    if let (Ok(cwd), Ok(prj_root)) = (std::env::current_dir(), find_project_root(None)) {
197        load(&prj_root);
198        if cwd != prj_root {
199            // prj root and cwd can be identical
200            load(&cwd);
201        }
202    };
203}
204
205/// Sets the default [`yansi`] color output condition.
206pub fn enable_paint() {
207    let enable = yansi::Condition::os_support() && yansi::Condition::tty_and_color_live();
208    yansi::whenever(yansi::Condition::cached(enable));
209}
210
211/// This force installs the default crypto provider.
212///
213/// This is necessary in case there are more than one available backends enabled in rustls (ring,
214/// aws-lc-rs).
215///
216/// This should be called high in the main fn.
217///
218/// See also:
219///   <https://github.com/snapview/tokio-tungstenite/issues/353#issuecomment-2455100010>
220///   <https://github.com/awslabs/aws-sdk-rust/discussions/1257>
221pub fn install_crypto_provider() {
222    // https://github.com/snapview/tokio-tungstenite/issues/353
223    rustls::crypto::ring::default_provider()
224        .install_default()
225        .expect("Failed to install default rustls crypto provider");
226}
227
228/// Useful extensions to [`std::process::Command`].
229pub trait CommandUtils {
230    /// Returns the command's output if execution is successful, otherwise, throws an error.
231    fn exec(&mut self) -> Result<Output>;
232
233    /// Returns the command's stdout if execution is successful, otherwise, throws an error.
234    fn get_stdout_lossy(&mut self) -> Result<String>;
235}
236
237impl CommandUtils for Command {
238    #[track_caller]
239    fn exec(&mut self) -> Result<Output> {
240        trace!(command=?self, "executing");
241
242        let output = self.output()?;
243
244        trace!(code=?output.status.code(), ?output);
245
246        if output.status.success() {
247            Ok(output)
248        } else {
249            let stdout = String::from_utf8_lossy(&output.stdout);
250            let stdout = stdout.trim();
251            let stderr = String::from_utf8_lossy(&output.stderr);
252            let stderr = stderr.trim();
253            let msg = if stdout.is_empty() {
254                stderr.to_string()
255            } else if stderr.is_empty() {
256                stdout.to_string()
257            } else {
258                format!("stdout:\n{stdout}\n\nstderr:\n{stderr}")
259            };
260
261            let mut name = self.get_program().to_string_lossy();
262            if let Some(arg) = self.get_args().next() {
263                let arg = arg.to_string_lossy();
264                if !arg.starts_with('-') {
265                    let name = name.to_mut();
266                    name.push(' ');
267                    name.push_str(&arg);
268                }
269            }
270
271            let mut err = match output.status.code() {
272                Some(code) => format!("{name} exited with code {code}"),
273                None => format!("{name} terminated by a signal"),
274            };
275            if !msg.is_empty() {
276                err.push(':');
277                err.push(if msg.lines().count() == 0 { ' ' } else { '\n' });
278                err.push_str(&msg);
279            }
280            Err(eyre::eyre!(err))
281        }
282    }
283
284    #[track_caller]
285    fn get_stdout_lossy(&mut self) -> Result<String> {
286        let output = self.exec()?;
287        let stdout = String::from_utf8_lossy(&output.stdout);
288        Ok(stdout.trim().into())
289    }
290}
291
292#[derive(Clone, Copy, Debug)]
293pub struct Git<'a> {
294    pub root: &'a Path,
295    pub quiet: bool,
296    pub shallow: bool,
297}
298
299impl<'a> Git<'a> {
300    #[inline]
301    pub fn new(root: &'a Path) -> Self {
302        Self { root, quiet: shell::is_quiet(), shallow: false }
303    }
304
305    #[inline]
306    pub fn from_config(config: &'a Config) -> Self {
307        Self::new(config.root.as_path())
308    }
309
310    pub fn root_of(relative_to: &Path) -> Result<PathBuf> {
311        let output = Self::cmd_no_root()
312            .current_dir(relative_to)
313            .args(["rev-parse", "--show-toplevel"])
314            .get_stdout_lossy()?;
315        Ok(PathBuf::from(output))
316    }
317
318    pub fn clone_with_branch(
319        shallow: bool,
320        from: impl AsRef<OsStr>,
321        branch: impl AsRef<OsStr>,
322        to: Option<impl AsRef<OsStr>>,
323    ) -> Result<()> {
324        Self::cmd_no_root()
325            .stderr(Stdio::inherit())
326            .args(["clone", "--recurse-submodules"])
327            .args(shallow.then_some("--depth=1"))
328            .args(shallow.then_some("--shallow-submodules"))
329            .arg("-b")
330            .arg(branch)
331            .arg(from)
332            .args(to)
333            .exec()
334            .map(drop)
335    }
336
337    pub fn clone(
338        shallow: bool,
339        from: impl AsRef<OsStr>,
340        to: Option<impl AsRef<OsStr>>,
341    ) -> Result<()> {
342        Self::cmd_no_root()
343            .stderr(Stdio::inherit())
344            .args(["clone", "--recurse-submodules"])
345            .args(shallow.then_some("--depth=1"))
346            .args(shallow.then_some("--shallow-submodules"))
347            .arg(from)
348            .args(to)
349            .exec()
350            .map(drop)
351    }
352
353    pub fn fetch(
354        self,
355        shallow: bool,
356        remote: impl AsRef<OsStr>,
357        branch: Option<impl AsRef<OsStr>>,
358    ) -> Result<()> {
359        self.cmd()
360            .stderr(Stdio::inherit())
361            .arg("fetch")
362            .args(shallow.then_some("--no-tags"))
363            .args(shallow.then_some("--depth=1"))
364            .arg(remote)
365            .args(branch)
366            .exec()
367            .map(drop)
368    }
369
370    #[inline]
371    pub fn root(self, root: &Path) -> Git<'_> {
372        Git { root, ..self }
373    }
374
375    #[inline]
376    pub fn quiet(self, quiet: bool) -> Self {
377        Self { quiet, ..self }
378    }
379
380    /// True to perform shallow clones
381    #[inline]
382    pub fn shallow(self, shallow: bool) -> Self {
383        Self { shallow, ..self }
384    }
385
386    pub fn checkout(self, recursive: bool, tag: impl AsRef<OsStr>) -> Result<()> {
387        self.cmd()
388            .arg("checkout")
389            .args(recursive.then_some("--recurse-submodules"))
390            .arg(tag)
391            .exec()
392            .map(drop)
393    }
394
395    pub fn init(self) -> Result<()> {
396        self.cmd().arg("init").exec().map(drop)
397    }
398
399    #[expect(clippy::should_implement_trait)] // this is not std::ops::Add clippy
400    pub fn add<I, S>(self, paths: I) -> Result<()>
401    where
402        I: IntoIterator<Item = S>,
403        S: AsRef<OsStr>,
404    {
405        self.cmd().arg("add").args(paths).exec().map(drop)
406    }
407
408    pub fn reset(self, hard: bool, tree: impl AsRef<OsStr>) -> Result<()> {
409        self.cmd().arg("reset").args(hard.then_some("--hard")).arg(tree).exec().map(drop)
410    }
411
412    pub fn commit_tree(
413        self,
414        tree: impl AsRef<OsStr>,
415        msg: Option<impl AsRef<OsStr>>,
416    ) -> Result<String> {
417        self.cmd()
418            .arg("commit-tree")
419            .arg(tree)
420            .args(msg.as_ref().is_some().then_some("-m"))
421            .args(msg)
422            .get_stdout_lossy()
423    }
424
425    pub fn rm<I, S>(self, force: bool, paths: I) -> Result<()>
426    where
427        I: IntoIterator<Item = S>,
428        S: AsRef<OsStr>,
429    {
430        self.cmd().arg("rm").args(force.then_some("--force")).args(paths).exec().map(drop)
431    }
432
433    pub fn commit(self, msg: &str) -> Result<()> {
434        let output = self
435            .cmd()
436            .args(["commit", "-m", msg])
437            .args(cfg!(any(test, debug_assertions)).then_some("--no-gpg-sign"))
438            .output()?;
439        if !output.status.success() {
440            let stdout = String::from_utf8_lossy(&output.stdout);
441            let stderr = String::from_utf8_lossy(&output.stderr);
442            // ignore "nothing to commit" error
443            let msg = "nothing to commit, working tree clean";
444            if !(stdout.contains(msg) || stderr.contains(msg)) {
445                return Err(eyre::eyre!(
446                    "failed to commit (code={:?}, stdout={:?}, stderr={:?})",
447                    output.status.code(),
448                    stdout.trim(),
449                    stderr.trim()
450                ));
451            }
452        }
453        Ok(())
454    }
455
456    pub fn is_in_repo(self) -> std::io::Result<bool> {
457        self.cmd().args(["rev-parse", "--is-inside-work-tree"]).status().map(|s| s.success())
458    }
459
460    pub fn is_clean(self) -> Result<bool> {
461        self.cmd().args(["status", "--porcelain"]).exec().map(|out| out.stdout.is_empty())
462    }
463
464    pub fn has_branch(self, branch: impl AsRef<OsStr>, at: &Path) -> Result<bool> {
465        self.cmd_at(at)
466            .args(["branch", "--list", "--no-color"])
467            .arg(branch)
468            .get_stdout_lossy()
469            .map(|stdout| !stdout.is_empty())
470    }
471
472    pub fn ensure_clean(self) -> Result<()> {
473        if self.is_clean()? {
474            Ok(())
475        } else {
476            Err(eyre::eyre!(
477                "\
478The target directory is a part of or on its own an already initialized git repository,
479and it requires clean working and staging areas, including no untracked files.
480
481Check the current git repository's status with `git status`.
482Then, you can track files with `git add ...` and then commit them with `git commit`,
483ignore them in the `.gitignore` file."
484            ))
485        }
486    }
487
488    pub fn commit_hash(self, short: bool, revision: &str) -> Result<String> {
489        self.cmd()
490            .arg("rev-parse")
491            .args(short.then_some("--short"))
492            .arg(revision)
493            .get_stdout_lossy()
494    }
495
496    pub fn tag(self) -> Result<String> {
497        self.cmd().arg("tag").get_stdout_lossy()
498    }
499
500    pub fn has_missing_dependencies<I, S>(self, paths: I) -> Result<bool>
501    where
502        I: IntoIterator<Item = S>,
503        S: AsRef<OsStr>,
504    {
505        self.cmd()
506            .args(["submodule", "status"])
507            .args(paths)
508            .get_stdout_lossy()
509            .map(|stdout| stdout.lines().any(|line| line.starts_with('-')))
510    }
511
512    /// Returns true if the given path has no submodules by checking `git submodule status`
513    pub fn has_submodules<I, S>(self, paths: I) -> Result<bool>
514    where
515        I: IntoIterator<Item = S>,
516        S: AsRef<OsStr>,
517    {
518        self.cmd()
519            .args(["submodule", "status"])
520            .args(paths)
521            .get_stdout_lossy()
522            .map(|stdout| stdout.trim().lines().next().is_some())
523    }
524
525    pub fn submodule_add(
526        self,
527        force: bool,
528        url: impl AsRef<OsStr>,
529        path: impl AsRef<OsStr>,
530    ) -> Result<()> {
531        self.cmd()
532            .stderr(self.stderr())
533            .args(["submodule", "add"])
534            .args(self.shallow.then_some("--depth=1"))
535            .args(force.then_some("--force"))
536            .arg(url)
537            .arg(path)
538            .exec()
539            .map(drop)
540    }
541
542    pub fn submodule_update<I, S>(
543        self,
544        force: bool,
545        remote: bool,
546        no_fetch: bool,
547        recursive: bool,
548        paths: I,
549    ) -> Result<()>
550    where
551        I: IntoIterator<Item = S>,
552        S: AsRef<OsStr>,
553    {
554        self.cmd()
555            .stderr(self.stderr())
556            .args(["submodule", "update", "--progress", "--init"])
557            .args(self.shallow.then_some("--depth=1"))
558            .args(force.then_some("--force"))
559            .args(remote.then_some("--remote"))
560            .args(no_fetch.then_some("--no-fetch"))
561            .args(recursive.then_some("--recursive"))
562            .args(paths)
563            .exec()
564            .map(drop)
565    }
566
567    pub fn submodule_foreach(self, recursive: bool, cmd: impl AsRef<OsStr>) -> Result<()> {
568        self.cmd()
569            .stderr(self.stderr())
570            .args(["submodule", "foreach"])
571            .args(recursive.then_some("--recursive"))
572            .arg(cmd)
573            .exec()
574            .map(drop)
575    }
576
577    pub fn submodule_init(self) -> Result<()> {
578        self.cmd().stderr(self.stderr()).args(["submodule", "init"]).exec().map(drop)
579    }
580
581    pub fn submodule_sync(self) -> Result<()> {
582        self.cmd().stderr(self.stderr()).args(["submodule", "sync"]).exec().map(drop)
583    }
584
585    pub fn cmd(self) -> Command {
586        let mut cmd = Self::cmd_no_root();
587        cmd.current_dir(self.root);
588        cmd
589    }
590
591    pub fn cmd_at(self, path: &Path) -> Command {
592        let mut cmd = Self::cmd_no_root();
593        cmd.current_dir(path);
594        cmd
595    }
596
597    pub fn cmd_no_root() -> Command {
598        let mut cmd = Command::new("git");
599        cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
600        cmd
601    }
602
603    // don't set this in cmd() because it's not wanted for all commands
604    fn stderr(self) -> Stdio {
605        if self.quiet {
606            Stdio::piped()
607        } else {
608            Stdio::inherit()
609        }
610    }
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616    use foundry_common::fs;
617    use std::{env, fs::File, io::Write};
618    use tempfile::tempdir;
619
620    #[test]
621    fn foundry_path_ext_works() {
622        let p = Path::new("contracts/MyTest.t.sol");
623        assert!(p.is_sol_test());
624        assert!(p.is_sol());
625        let p = Path::new("contracts/Greeter.sol");
626        assert!(!p.is_sol_test());
627    }
628
629    // loads .env from cwd and project dir, See [`find_project_root()`]
630    #[test]
631    fn can_load_dotenv() {
632        let temp = tempdir().unwrap();
633        Git::new(temp.path()).init().unwrap();
634        let cwd_env = temp.path().join(".env");
635        fs::create_file(temp.path().join("foundry.toml")).unwrap();
636        let nested = temp.path().join("nested");
637        fs::create_dir(&nested).unwrap();
638
639        let mut cwd_file = File::create(cwd_env).unwrap();
640        let mut prj_file = File::create(nested.join(".env")).unwrap();
641
642        cwd_file.write_all("TESTCWDKEY=cwd_val".as_bytes()).unwrap();
643        cwd_file.sync_all().unwrap();
644
645        prj_file.write_all("TESTPRJKEY=prj_val".as_bytes()).unwrap();
646        prj_file.sync_all().unwrap();
647
648        let cwd = env::current_dir().unwrap();
649        env::set_current_dir(nested).unwrap();
650        load_dotenv();
651        env::set_current_dir(cwd).unwrap();
652
653        assert_eq!(env::var("TESTCWDKEY").unwrap(), "cwd_val");
654        assert_eq!(env::var("TESTPRJKEY").unwrap(), "prj_val");
655    }
656}