1use alloy_json_abi::JsonAbi;
2use eyre::{Result, WrapErr};
3use foundry_common::{TestFunctionExt, fs, selectors::SelectorKind, shell};
4use foundry_compilers::{
5 Artifact, ArtifactId, ProjectCompileOutput,
6 artifacts::{CompactBytecode, Settings},
7 cache::{CacheEntry, CompilerCache},
8 utils::read_json_file,
9};
10use foundry_config::{Chain, Config, NamedChain, error::ExtractConfigError, figment::Figment};
11use foundry_evm::{
12 executors::{DeployResult, EvmError, RawCallResult},
13 opts::EvmOpts,
14 traces::{
15 CallTraceDecoder, TraceKind, Traces, decode_trace_arena, identifier::SignaturesCache,
16 prune_trace_depth, render_trace_arena_inner,
17 },
18};
19use std::{
20 fmt::Write,
21 path::{Path, PathBuf},
22};
23use yansi::Paint;
24
25#[track_caller]
28pub fn find_contract_artifacts(
29 output: ProjectCompileOutput,
30 path: &Path,
31 name: &str,
32) -> Result<(JsonAbi, CompactBytecode, ArtifactId)> {
33 let mut other = Vec::new();
34 let Some((id, contract)) = output.into_artifacts().find_map(|(id, artifact)| {
35 if id.name == name && id.source == path {
36 Some((id, artifact))
37 } else {
38 other.push(id.name);
39 None
40 }
41 }) else {
42 let mut err = format!("could not find artifact: `{name}`");
43 if let Some(suggestion) = super::did_you_mean(name, other).pop()
44 && suggestion != name
45 {
46 err = format!(
47 r#"{err}
48
49 Did you mean `{suggestion}`?"#
50 );
51 }
52 eyre::bail!(err)
53 };
54
55 let abi = contract
56 .get_abi()
57 .ok_or_else(|| eyre::eyre!("contract {} does not contain abi", name))?
58 .into_owned();
59
60 let bin = contract
61 .get_bytecode()
62 .ok_or_else(|| eyre::eyre!("contract {} does not contain bytecode", name))?
63 .into_owned();
64
65 Ok((abi, bin, id))
66}
67
68pub fn get_cached_entry_by_name(
72 cache: &CompilerCache<Settings>,
73 name: &str,
74) -> Result<(PathBuf, CacheEntry)> {
75 let mut cached_entry = None;
76 let mut alternatives = Vec::new();
77
78 for (abs_path, entry) in &cache.files {
79 for artifact_name in entry.artifacts.keys() {
80 if artifact_name == name {
81 if cached_entry.is_some() {
82 eyre::bail!(
83 "contract with duplicate name `{}`. please pass the path instead",
84 name
85 )
86 }
87 cached_entry = Some((abs_path.to_owned(), entry.to_owned()));
88 } else {
89 alternatives.push(artifact_name);
90 }
91 }
92 }
93
94 if let Some(entry) = cached_entry {
95 return Ok(entry);
96 }
97
98 let mut err = format!("could not find artifact: `{name}`");
99 if let Some(suggestion) = super::did_you_mean(name, &alternatives).pop() {
100 err = format!(
101 r#"{err}
102
103 Did you mean `{suggestion}`?"#
104 );
105 }
106 eyre::bail!(err)
107}
108
109pub fn ensure_clean_constructor(abi: &JsonAbi) -> Result<()> {
111 if let Some(constructor) = &abi.constructor
112 && !constructor.inputs.is_empty()
113 {
114 eyre::bail!(
115 "Contract constructor should have no arguments. Add those arguments to `run(...)` instead, and call it with `--sig run(...)`."
116 );
117 }
118 Ok(())
119}
120
121pub fn needs_setup(abi: &JsonAbi) -> bool {
122 let setup_fns: Vec<_> = abi.functions().filter(|func| func.name.is_setup()).collect();
123
124 for setup_fn in &setup_fns {
125 if setup_fn.name != "setUp" {
126 let _ = sh_warn!(
127 "Found invalid setup function \"{}\" did you mean \"setUp()\"?",
128 setup_fn.signature()
129 );
130 }
131 }
132
133 setup_fns.len() == 1 && setup_fns[0].name == "setUp"
134}
135
136pub fn eta_key(state: &indicatif::ProgressState, f: &mut dyn Write) {
137 write!(f, "{:.1}s", state.eta().as_secs_f64()).unwrap()
138}
139
140pub fn init_progress(len: u64, label: &str) -> indicatif::ProgressBar {
141 let pb = indicatif::ProgressBar::new(len);
142 let mut template =
143 "{prefix}{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} "
144 .to_string();
145 write!(template, "{label}").unwrap();
146 template += " ({eta})";
147 pb.set_style(
148 indicatif::ProgressStyle::with_template(&template)
149 .unwrap()
150 .with_key("eta", crate::utils::eta_key)
151 .progress_chars("#>-"),
152 );
153 pb
154}
155
156pub fn has_different_gas_calc(chain_id: u64) -> bool {
158 if let Some(chain) = Chain::from(chain_id).named() {
159 return chain.is_arbitrum()
160 || chain.is_elastic()
161 || matches!(
162 chain,
163 NamedChain::Acala
164 | NamedChain::AcalaMandalaTestnet
165 | NamedChain::AcalaTestnet
166 | NamedChain::Etherlink
167 | NamedChain::EtherlinkTestnet
168 | NamedChain::Karura
169 | NamedChain::KaruraTestnet
170 | NamedChain::Mantle
171 | NamedChain::MantleSepolia
172 | NamedChain::Monad
173 | NamedChain::MonadTestnet
174 | NamedChain::Moonbase
175 | NamedChain::Moonbeam
176 | NamedChain::MoonbeamDev
177 | NamedChain::Moonriver
178 | NamedChain::Metis
179 );
180 }
181 false
182}
183
184pub fn has_batch_support(chain_id: u64) -> bool {
186 if let Some(chain) = Chain::from(chain_id).named() {
187 return !chain.is_arbitrum();
188 }
189 true
190}
191
192pub trait LoadConfig {
200 fn figment(&self) -> Figment;
202
203 fn load_config(&self) -> Result<Config, ExtractConfigError> {
205 self.load_config_no_warnings().inspect(emit_warnings)
206 }
207
208 fn load_config_no_warnings(&self) -> Result<Config, ExtractConfigError> {
210 self.load_config_unsanitized_no_warnings().map(Config::sanitized)
211 }
212
213 fn load_config_unsanitized(&self) -> Result<Config, ExtractConfigError> {
215 self.load_config_unsanitized_no_warnings().inspect(emit_warnings)
216 }
217
218 fn load_config_unsanitized_no_warnings(&self) -> Result<Config, ExtractConfigError> {
220 Config::from_provider(self.figment())
221 }
222
223 fn load_config_and_evm_opts(&self) -> Result<(Config, EvmOpts)> {
225 self.load_config_and_evm_opts_no_warnings().inspect(|(config, _)| emit_warnings(config))
226 }
227
228 fn load_config_and_evm_opts_no_warnings(&self) -> Result<(Config, EvmOpts)> {
230 let figment = self.figment();
231
232 let mut evm_opts = figment.extract::<EvmOpts>().map_err(ExtractConfigError::new)?;
233 let config = Config::from_provider(figment)?.sanitized();
234
235 if let Some(fork_url) = config.get_rpc_url() {
237 trace!(target: "forge::config", ?fork_url, "Update EvmOpts fork url");
238 evm_opts.fork_url = Some(fork_url?.into_owned());
239 }
240
241 Ok((config, evm_opts))
242 }
243}
244
245impl<T> LoadConfig for T
246where
247 for<'a> Figment: From<&'a T>,
248{
249 fn figment(&self) -> Figment {
250 self.into()
251 }
252}
253
254fn emit_warnings(config: &Config) {
255 for warning in &config.warnings {
256 let _ = sh_warn!("{warning}");
257 }
258}
259
260pub fn read_constructor_args_file(constructor_args_path: PathBuf) -> Result<Vec<String>> {
262 if !constructor_args_path.exists() {
263 eyre::bail!("Constructor args file \"{}\" not found", constructor_args_path.display());
264 }
265 let args = if constructor_args_path.extension() == Some(std::ffi::OsStr::new("json")) {
266 read_json_file(&constructor_args_path).wrap_err(format!(
267 "Constructor args file \"{}\" must encode a json array",
268 constructor_args_path.display(),
269 ))?
270 } else {
271 fs::read_to_string(constructor_args_path)?.split_whitespace().map(str::to_string).collect()
272 };
273 Ok(args)
274}
275
276#[derive(Debug)]
278pub struct TraceResult {
279 pub success: bool,
280 pub traces: Option<Traces>,
281 pub gas_used: u64,
282}
283
284impl TraceResult {
285 pub fn from_raw(raw: RawCallResult, trace_kind: TraceKind) -> Self {
287 let RawCallResult { gas_used, traces, reverted, .. } = raw;
288 Self { success: !reverted, traces: traces.map(|arena| vec![(trace_kind, arena)]), gas_used }
289 }
290}
291
292impl From<DeployResult> for TraceResult {
293 fn from(result: DeployResult) -> Self {
294 Self::from_raw(result.raw, TraceKind::Deployment)
295 }
296}
297
298impl TryFrom<Result<DeployResult, EvmError>> for TraceResult {
299 type Error = EvmError;
300
301 fn try_from(value: Result<DeployResult, EvmError>) -> Result<Self, Self::Error> {
302 match value {
303 Ok(result) => Ok(Self::from(result)),
304 Err(EvmError::Execution(err)) => Ok(Self::from_raw(err.raw, TraceKind::Deployment)),
305 Err(err) => Err(err),
306 }
307 }
308}
309
310impl From<RawCallResult> for TraceResult {
311 fn from(result: RawCallResult) -> Self {
312 Self::from_raw(result, TraceKind::Execution)
313 }
314}
315
316impl TryFrom<Result<RawCallResult>> for TraceResult {
317 type Error = EvmError;
318
319 fn try_from(value: Result<RawCallResult>) -> Result<Self, Self::Error> {
320 match value {
321 Ok(result) => Ok(Self::from(result)),
322 Err(err) => Err(EvmError::from(err)),
323 }
324 }
325}
326
327pub async fn print_traces(
328 result: &mut TraceResult,
329 decoder: &CallTraceDecoder,
330 verbose: bool,
331 state_changes: bool,
332 trace_depth: Option<usize>,
333) -> Result<()> {
334 let traces = result.traces.as_mut().expect("No traces found");
335
336 if !shell::is_json() {
337 sh_println!("Traces:")?;
338 }
339
340 for (_, arena) in traces {
341 decode_trace_arena(arena, decoder).await;
342
343 if let Some(trace_depth) = trace_depth {
344 prune_trace_depth(arena, trace_depth);
345 }
346
347 sh_println!("{}", render_trace_arena_inner(arena, verbose, state_changes))?;
348 }
349
350 if shell::is_json() {
351 return Ok(());
352 }
353
354 sh_println!()?;
355 if result.success {
356 sh_println!("{}", "Transaction successfully executed.".green())?;
357 } else {
358 sh_err!("Transaction failed.")?;
359 }
360 sh_println!("Gas used: {}", result.gas_used)?;
361
362 Ok(())
363}
364
365pub fn cache_local_signatures(output: &ProjectCompileOutput) -> Result<()> {
368 let Some(cache_dir) = Config::foundry_cache_dir() else {
369 eyre::bail!("Failed to get `cache_dir` to generate local signatures.");
370 };
371 let path = cache_dir.join("signatures");
372 let mut signatures = SignaturesCache::load(&path);
373 for (_, artifact) in output.artifacts() {
374 if let Some(abi) = &artifact.abi {
375 signatures.extend_from_abi(abi);
376 }
377
378 if let Some(method_identifiers) = &artifact.method_identifiers {
380 signatures.extend(method_identifiers.iter().filter_map(|(signature, selector)| {
381 Some((SelectorKind::Function(selector.parse().ok()?), signature.clone()))
382 }));
383 }
384 }
385 signatures.save(&path);
386 Ok(())
387}