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#[doc(hidden)]
31pub use foundry_config::utils::*;
32
33pub 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
41pub trait FoundryPathExt {
43 fn is_sol_test(&self) -> bool;
45
46 fn is_sol(&self) -> bool;
48
49 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
71pub 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
86pub fn get_provider(config: &Config) -> Result<RetryProvider> {
89 get_provider_builder(config)?.build()
90}
91
92pub 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
128pub 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
145pub fn parse_json<T: DeserializeOwned>(value: &str) -> serde_json::Result<T> {
147 serde_json::from_str(value)
148}
149
150pub 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
167pub fn now() -> Duration {
169 SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards")
170}
171
172pub 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
178pub fn load_dotenv() {
186 let load = |p: &Path| {
187 dotenvy::from_path(p.join(".env")).ok();
188 };
189
190 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 load(&cwd);
198 }
199 };
200}
201
202pub 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
208pub trait CommandUtils {
210 fn exec(&mut self) -> Result<Output>;
212
213 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 #[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)] 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 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 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 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 #[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}