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