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 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 remove_contract(
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::Moonbase
173 | NamedChain::Moonbeam
174 | NamedChain::MoonbeamDev
175 | NamedChain::Moonriver
176 | NamedChain::Metis
177 );
178 }
179 false
180}
181
182pub fn has_batch_support(chain_id: u64) -> bool {
184 if let Some(chain) = Chain::from(chain_id).named() {
185 return !chain.is_arbitrum();
186 }
187 true
188}
189
190pub trait LoadConfig {
198 fn figment(&self) -> Figment;
200
201 fn load_config(&self) -> Result<Config, ExtractConfigError> {
203 self.load_config_no_warnings().inspect(emit_warnings)
204 }
205
206 fn load_config_no_warnings(&self) -> Result<Config, ExtractConfigError> {
208 self.load_config_unsanitized_no_warnings().map(Config::sanitized)
209 }
210
211 fn load_config_unsanitized(&self) -> Result<Config, ExtractConfigError> {
213 self.load_config_unsanitized_no_warnings().inspect(emit_warnings)
214 }
215
216 fn load_config_unsanitized_no_warnings(&self) -> Result<Config, ExtractConfigError> {
218 Config::from_provider(self.figment())
219 }
220
221 fn load_config_and_evm_opts(&self) -> Result<(Config, EvmOpts)> {
223 self.load_config_and_evm_opts_no_warnings().inspect(|(config, _)| emit_warnings(config))
224 }
225
226 fn load_config_and_evm_opts_no_warnings(&self) -> Result<(Config, EvmOpts)> {
228 let figment = self.figment();
229
230 let mut evm_opts = figment.extract::<EvmOpts>().map_err(ExtractConfigError::new)?;
231 let config = Config::from_provider(figment)?.sanitized();
232
233 if let Some(fork_url) = config.get_rpc_url() {
235 trace!(target: "forge::config", ?fork_url, "Update EvmOpts fork url");
236 evm_opts.fork_url = Some(fork_url?.into_owned());
237 }
238
239 Ok((config, evm_opts))
240 }
241}
242
243impl<T> LoadConfig for T
244where
245 for<'a> Figment: From<&'a T>,
246{
247 fn figment(&self) -> Figment {
248 self.into()
249 }
250}
251
252fn emit_warnings(config: &Config) {
253 for warning in &config.warnings {
254 let _ = sh_warn!("{warning}");
255 }
256}
257
258pub fn read_constructor_args_file(constructor_args_path: PathBuf) -> Result<Vec<String>> {
260 if !constructor_args_path.exists() {
261 eyre::bail!("Constructor args file \"{}\" not found", constructor_args_path.display());
262 }
263 let args = if constructor_args_path.extension() == Some(std::ffi::OsStr::new("json")) {
264 read_json_file(&constructor_args_path).wrap_err(format!(
265 "Constructor args file \"{}\" must encode a json array",
266 constructor_args_path.display(),
267 ))?
268 } else {
269 fs::read_to_string(constructor_args_path)?.split_whitespace().map(str::to_string).collect()
270 };
271 Ok(args)
272}
273
274#[derive(Debug)]
276pub struct TraceResult {
277 pub success: bool,
278 pub traces: Option<Traces>,
279 pub gas_used: u64,
280}
281
282impl TraceResult {
283 pub fn from_raw(raw: RawCallResult, trace_kind: TraceKind) -> Self {
285 let RawCallResult { gas_used, traces, reverted, .. } = raw;
286 Self { success: !reverted, traces: traces.map(|arena| vec![(trace_kind, arena)]), gas_used }
287 }
288}
289
290impl From<DeployResult> for TraceResult {
291 fn from(result: DeployResult) -> Self {
292 Self::from_raw(result.raw, TraceKind::Deployment)
293 }
294}
295
296impl TryFrom<Result<DeployResult, EvmError>> for TraceResult {
297 type Error = EvmError;
298
299 fn try_from(value: Result<DeployResult, EvmError>) -> Result<Self, Self::Error> {
300 match value {
301 Ok(result) => Ok(Self::from(result)),
302 Err(EvmError::Execution(err)) => Ok(Self::from_raw(err.raw, TraceKind::Deployment)),
303 Err(err) => Err(err),
304 }
305 }
306}
307
308impl From<RawCallResult> for TraceResult {
309 fn from(result: RawCallResult) -> Self {
310 Self::from_raw(result, TraceKind::Execution)
311 }
312}
313
314impl TryFrom<Result<RawCallResult>> for TraceResult {
315 type Error = EvmError;
316
317 fn try_from(value: Result<RawCallResult>) -> Result<Self, Self::Error> {
318 match value {
319 Ok(result) => Ok(Self::from(result)),
320 Err(err) => Err(EvmError::from(err)),
321 }
322 }
323}
324
325pub async fn print_traces(
326 result: &mut TraceResult,
327 decoder: &CallTraceDecoder,
328 verbose: bool,
329 state_changes: bool,
330) -> Result<()> {
331 let traces = result.traces.as_mut().expect("No traces found");
332
333 if !shell::is_json() {
334 sh_println!("Traces:")?;
335 }
336
337 for (_, arena) in traces {
338 decode_trace_arena(arena, decoder).await;
339 sh_println!("{}", render_trace_arena_inner(arena, verbose, state_changes))?;
340 }
341
342 if shell::is_json() {
343 return Ok(());
344 }
345
346 sh_println!()?;
347 if result.success {
348 sh_println!("{}", "Transaction successfully executed.".green())?;
349 } else {
350 sh_err!("Transaction failed.")?;
351 }
352 sh_println!("Gas used: {}", result.gas_used)?;
353
354 Ok(())
355}
356
357pub fn cache_local_signatures(output: &ProjectCompileOutput) -> Result<()> {
360 let Some(cache_dir) = Config::foundry_cache_dir() else {
361 eyre::bail!("Failed to get `cache_dir` to generate local signatures.");
362 };
363 let path = cache_dir.join("signatures");
364 let mut signatures = SignaturesCache::load(&path);
365 for (_, artifact) in output.artifacts() {
366 if let Some(abi) = &artifact.abi {
367 signatures.extend_from_abi(abi);
368 }
369
370 if let Some(method_identifiers) = &artifact.method_identifiers {
372 signatures.extend(method_identifiers.iter().filter_map(|(signature, selector)| {
373 Some((SelectorKind::Function(selector.parse().ok()?), signature.clone()))
374 }));
375 }
376 }
377 signatures.save(&path);
378 Ok(())
379}