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