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