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#[doc(hidden)]
34pub use foundry_config::utils::*;
35
36pub 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
44pub trait FoundryPathExt {
46 fn is_sol_test(&self) -> bool;
48
49 fn is_sol(&self) -> bool;
51
52 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
74pub 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
89pub fn get_provider(config: &Config) -> Result<RetryProvider> {
92 get_provider_builder(config)?.build()
93}
94
95pub 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
131pub 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
148pub fn parse_json<T: DeserializeOwned>(value: &str) -> serde_json::Result<T> {
150 serde_json::from_str(value)
151}
152
153pub 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
170pub fn now() -> Duration {
172 SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards")
173}
174
175pub 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
181pub fn load_dotenv() {
189 let load = |p: &Path| {
190 dotenvy::from_path(p.join(".env")).ok();
191 };
192
193 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 load(&cwd);
201 }
202 };
203}
204
205pub 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
211pub fn install_crypto_provider() {
222 rustls::crypto::ring::default_provider()
224 .install_default()
225 .expect("Failed to install default rustls crypto provider");
226}
227
228pub trait CommandUtils {
230 fn exec(&mut self) -> Result<Output>;
232
233 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 #[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)] 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 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 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 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 #[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}