Skip to main content

foundry_cheatcodes/
fs.rs

1//! Implementations of [`Filesystem`](spec::Group::Filesystem) cheatcodes.
2
3use super::string::parse;
4use crate::{
5    Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*, inspector::exec_create,
6};
7use alloy_dyn_abi::DynSolType;
8use alloy_json_abi::ContractObject;
9use alloy_network::{Network, ReceiptResponse};
10use alloy_primitives::{Bytes, U256, hex, map::Entry};
11use alloy_sol_types::SolValue;
12use dialoguer::{Input, Password};
13use forge_script_sequence::{BroadcastReader, TransactionWithMetadata};
14use foundry_common::fs;
15use foundry_config::fs_permissions::FsAccessKind;
16use foundry_evm_core::evm::FoundryEvmNetwork;
17use revm::{
18    context::{Cfg, ContextTr, CreateScheme, JournalTr},
19    interpreter::CreateInputs,
20};
21use revm_inspectors::tracing::types::CallKind;
22use semver::Version;
23use std::{
24    io::{BufRead, BufReader},
25    path::{Path, PathBuf},
26    process::Command,
27    sync::mpsc,
28    thread,
29    time::{SystemTime, UNIX_EPOCH},
30};
31use walkdir::WalkDir;
32
33/// Parsed artifact path components.
34#[derive(Debug, Default, PartialEq, Eq)]
35struct ParsedArtifactPath<'a> {
36    file: Option<PathBuf>,
37    contract_name: Option<&'a str>,
38    version: Option<Version>,
39    profile: Option<&'a str>,
40}
41
42/// Parses an artifact path string into its components.
43///
44/// Supports the following formats:
45/// - `path/to/contract.sol`
46/// - `path/to/contract.sol:ContractName`
47/// - `path/to/contract.sol:ContractName:0.8.23`
48/// - `path/to/contract.sol:ContractName:profile`
49/// - `path/to/contract.sol:0.8.23`
50/// - `path/to/contract.sol:profile`
51/// - `ContractName`
52/// - `ContractName:0.8.23`
53/// - `ContractName:profile`
54fn parse_artifact_path(path: &str) -> std::result::Result<ParsedArtifactPath<'_>, String> {
55    let mut parts = path.split(':');
56
57    let mut file = None;
58    let mut contract_name = None;
59    let mut version = None;
60    let mut profile = None;
61
62    let path_or_name = parts.next().unwrap();
63    if path_or_name.contains('.') {
64        file = Some(PathBuf::from(path_or_name));
65        if let Some(name_or_version_or_profile) = parts.next() {
66            if name_or_version_or_profile.contains('.')
67                || Version::parse(name_or_version_or_profile).is_ok()
68            {
69                version = Some(name_or_version_or_profile);
70            } else {
71                contract_name = Some(name_or_version_or_profile);
72                if let Some(version_or_profile) = parts.next() {
73                    if version_or_profile.contains('.')
74                        || Version::parse(version_or_profile).is_ok()
75                    {
76                        version = Some(version_or_profile);
77                    } else {
78                        profile = Some(version_or_profile);
79                    }
80                }
81            }
82        }
83    } else {
84        contract_name = Some(path_or_name);
85        if let Some(version_or_profile) = parts.next() {
86            if version_or_profile.contains('.') || Version::parse(version_or_profile).is_ok() {
87                version = Some(version_or_profile);
88            } else {
89                profile = Some(version_or_profile);
90            }
91        }
92    }
93
94    let version = if let Some(version) = version {
95        Some(Version::parse(version).map_err(|e| format!("failed parsing version: {e}"))?)
96    } else {
97        None
98    };
99
100    Ok(ParsedArtifactPath { file, contract_name, version, profile })
101}
102
103impl Cheatcode for existsCall {
104    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
105        let Self { path } = self;
106        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
107        Ok(path.exists().abi_encode())
108    }
109}
110
111impl Cheatcode for fsMetadataCall {
112    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
113        let Self { path } = self;
114        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
115
116        let metadata = path.metadata()?;
117
118        // These fields not available on all platforms; default to 0
119        let [modified, accessed, created] =
120            [metadata.modified(), metadata.accessed(), metadata.created()].map(|time| {
121                time.unwrap_or(UNIX_EPOCH).duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
122            });
123
124        Ok(FsMetadata {
125            isDir: metadata.is_dir(),
126            isSymlink: metadata.is_symlink(),
127            length: U256::from(metadata.len()),
128            readOnly: metadata.permissions().readonly(),
129            modified: U256::from(modified),
130            accessed: U256::from(accessed),
131            created: U256::from(created),
132        }
133        .abi_encode())
134    }
135}
136
137impl Cheatcode for isDirCall {
138    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
139        let Self { path } = self;
140        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
141        Ok(path.is_dir().abi_encode())
142    }
143}
144
145impl Cheatcode for isFileCall {
146    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
147        let Self { path } = self;
148        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
149        Ok(path.is_file().abi_encode())
150    }
151}
152
153impl Cheatcode for projectRootCall {
154    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
155        let Self {} = self;
156        Ok(state.config.root.display().to_string().abi_encode())
157    }
158}
159
160impl Cheatcode for currentFilePathCall {
161    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
162        let Self {} = self;
163        let artifact = state
164            .config
165            .running_artifact
166            .as_ref()
167            .ok_or_else(|| fmt_err!("no running contract found"))?;
168        let relative = artifact.source.strip_prefix(&state.config.root).unwrap_or(&artifact.source);
169        Ok(relative.display().to_string().abi_encode())
170    }
171}
172
173impl Cheatcode for unixTimeCall {
174    fn apply<FEN: FoundryEvmNetwork>(&self, _state: &mut Cheatcodes<FEN>) -> Result {
175        let Self {} = self;
176        let difference = SystemTime::now()
177            .duration_since(UNIX_EPOCH)
178            .map_err(|e| fmt_err!("failed getting Unix timestamp: {e}"))?;
179        Ok(difference.as_millis().abi_encode())
180    }
181}
182
183impl Cheatcode for closeFileCall {
184    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
185        let Self { path } = self;
186        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
187
188        state.test_context.opened_read_files.remove(&path);
189
190        Ok(Default::default())
191    }
192}
193
194impl Cheatcode for copyFileCall {
195    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
196        let Self { from, to } = self;
197        let from = state.config.ensure_path_allowed(from, FsAccessKind::Read)?;
198        let to = state.config.ensure_path_allowed(to, FsAccessKind::Write)?;
199        state.config.ensure_not_foundry_toml(&to)?;
200
201        let n = fs::copy(from, to)?;
202        Ok(n.abi_encode())
203    }
204}
205
206impl Cheatcode for createDirCall {
207    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
208        let Self { path, recursive } = self;
209        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
210        if *recursive { fs::create_dir_all(path) } else { fs::create_dir(path) }?;
211        Ok(Default::default())
212    }
213}
214
215impl Cheatcode for readDir_0Call {
216    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
217        let Self { path } = self;
218        read_dir(state, path.as_ref(), 1, false)
219    }
220}
221
222impl Cheatcode for readDir_1Call {
223    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
224        let Self { path, maxDepth } = self;
225        read_dir(state, path.as_ref(), *maxDepth, false)
226    }
227}
228
229impl Cheatcode for readDir_2Call {
230    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
231        let Self { path, maxDepth, followLinks } = self;
232        read_dir(state, path.as_ref(), *maxDepth, *followLinks)
233    }
234}
235
236impl Cheatcode for readFileCall {
237    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
238        let Self { path } = self;
239        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
240        Ok(fs::locked_read_to_string(path)?.abi_encode())
241    }
242}
243
244impl Cheatcode for readFileBinaryCall {
245    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
246        let Self { path } = self;
247        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
248        Ok(fs::locked_read(path)?.abi_encode())
249    }
250}
251
252impl Cheatcode for readLineCall {
253    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
254        let Self { path } = self;
255        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
256
257        // Get reader for previously opened file to continue reading OR initialize new reader
258        let reader = match state.test_context.opened_read_files.entry(path.clone()) {
259            Entry::Occupied(entry) => entry.into_mut(),
260            Entry::Vacant(entry) => entry.insert(BufReader::new(fs::open(path)?)),
261        };
262
263        let mut line: String = String::new();
264        reader.read_line(&mut line)?;
265
266        // Remove trailing newline character, preserving others for cases where it may be important
267        if line.ends_with('\n') {
268            line.pop();
269            if line.ends_with('\r') {
270                line.pop();
271            }
272        }
273
274        Ok(line.abi_encode())
275    }
276}
277
278impl Cheatcode for readLinkCall {
279    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
280        let Self { linkPath: path } = self;
281        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
282        let target = fs::read_link(path)?;
283        Ok(target.display().to_string().abi_encode())
284    }
285}
286
287impl Cheatcode for removeDirCall {
288    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
289        let Self { path, recursive } = self;
290        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
291        if *recursive { fs::remove_dir_all(path) } else { fs::remove_dir(path) }?;
292        Ok(Default::default())
293    }
294}
295
296impl Cheatcode for removeFileCall {
297    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
298        let Self { path } = self;
299        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
300        state.config.ensure_not_foundry_toml(&path)?;
301
302        // also remove from the set if opened previously
303        state.test_context.opened_read_files.remove(&path);
304
305        if state.fs_commit {
306            fs::remove_file(&path)?;
307        }
308
309        Ok(Default::default())
310    }
311}
312
313impl Cheatcode for writeFileCall {
314    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
315        let Self { path, data } = self;
316        write_file(state, path.as_ref(), data.as_bytes())
317    }
318}
319
320impl Cheatcode for writeFileBinaryCall {
321    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
322        let Self { path, data } = self;
323        write_file(state, path.as_ref(), data)
324    }
325}
326
327impl Cheatcode for writeLineCall {
328    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
329        let Self { path, data: line } = self;
330        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
331        state.config.ensure_not_foundry_toml(&path)?;
332
333        if state.fs_commit {
334            fs::locked_write_line(path, line)?;
335        }
336
337        Ok(Default::default())
338    }
339}
340
341impl Cheatcode for getArtifactPathByCodeCall {
342    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
343        let Self { code } = self;
344        let (artifact_id, _) = state
345            .config
346            .available_artifacts
347            .as_ref()
348            .and_then(|artifacts| artifacts.find_by_creation_code(code))
349            .ok_or_else(|| fmt_err!("no matching artifact found"))?;
350
351        Ok(artifact_id.path.to_string_lossy().abi_encode())
352    }
353}
354
355impl Cheatcode for getArtifactPathByDeployedCodeCall {
356    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
357        let Self { deployedCode } = self;
358        let (artifact_id, _) = state
359            .config
360            .available_artifacts
361            .as_ref()
362            .and_then(|artifacts| artifacts.find_by_deployed_code(deployedCode))
363            .ok_or_else(|| fmt_err!("no matching artifact found"))?;
364
365        Ok(artifact_id.path.to_string_lossy().abi_encode())
366    }
367}
368
369impl Cheatcode for getCodeCall {
370    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
371        let Self { artifactPath: path } = self;
372        Ok(get_artifact_code(state, path, false)?.abi_encode())
373    }
374}
375
376impl Cheatcode for getDeployedCodeCall {
377    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
378        let Self { artifactPath: path } = self;
379        Ok(get_artifact_code(state, path, true)?.abi_encode())
380    }
381}
382
383impl Cheatcode for deployCode_0Call {
384    fn apply_full<FEN: FoundryEvmNetwork>(
385        &self,
386        ccx: &mut CheatsCtxt<'_, '_, FEN>,
387        executor: &mut dyn CheatcodesExecutor<FEN>,
388    ) -> Result {
389        let Self { artifactPath: path } = self;
390        deploy_code(ccx, executor, path, None, None, None)
391    }
392}
393
394impl Cheatcode for deployCode_1Call {
395    fn apply_full<FEN: FoundryEvmNetwork>(
396        &self,
397        ccx: &mut CheatsCtxt<'_, '_, FEN>,
398        executor: &mut dyn CheatcodesExecutor<FEN>,
399    ) -> Result {
400        let Self { artifactPath: path, constructorArgs: args } = self;
401        deploy_code(ccx, executor, path, Some(args), None, None)
402    }
403}
404
405impl Cheatcode for deployCode_2Call {
406    fn apply_full<FEN: FoundryEvmNetwork>(
407        &self,
408        ccx: &mut CheatsCtxt<'_, '_, FEN>,
409        executor: &mut dyn CheatcodesExecutor<FEN>,
410    ) -> Result {
411        let Self { artifactPath: path, value } = self;
412        deploy_code(ccx, executor, path, None, Some(*value), None)
413    }
414}
415
416impl Cheatcode for deployCode_3Call {
417    fn apply_full<FEN: FoundryEvmNetwork>(
418        &self,
419        ccx: &mut CheatsCtxt<'_, '_, FEN>,
420        executor: &mut dyn CheatcodesExecutor<FEN>,
421    ) -> Result {
422        let Self { artifactPath: path, constructorArgs: args, value } = self;
423        deploy_code(ccx, executor, path, Some(args), Some(*value), None)
424    }
425}
426
427impl Cheatcode for deployCode_4Call {
428    fn apply_full<FEN: FoundryEvmNetwork>(
429        &self,
430        ccx: &mut CheatsCtxt<'_, '_, FEN>,
431        executor: &mut dyn CheatcodesExecutor<FEN>,
432    ) -> Result {
433        let Self { artifactPath: path, salt } = self;
434        deploy_code(ccx, executor, path, None, None, Some((*salt).into()))
435    }
436}
437
438impl Cheatcode for deployCode_5Call {
439    fn apply_full<FEN: FoundryEvmNetwork>(
440        &self,
441        ccx: &mut CheatsCtxt<'_, '_, FEN>,
442        executor: &mut dyn CheatcodesExecutor<FEN>,
443    ) -> Result {
444        let Self { artifactPath: path, constructorArgs: args, salt } = self;
445        deploy_code(ccx, executor, path, Some(args), None, Some((*salt).into()))
446    }
447}
448
449impl Cheatcode for deployCode_6Call {
450    fn apply_full<FEN: FoundryEvmNetwork>(
451        &self,
452        ccx: &mut CheatsCtxt<'_, '_, FEN>,
453        executor: &mut dyn CheatcodesExecutor<FEN>,
454    ) -> Result {
455        let Self { artifactPath: path, value, salt } = self;
456        deploy_code(ccx, executor, path, None, Some(*value), Some((*salt).into()))
457    }
458}
459
460impl Cheatcode for deployCode_7Call {
461    fn apply_full<FEN: FoundryEvmNetwork>(
462        &self,
463        ccx: &mut CheatsCtxt<'_, '_, FEN>,
464        executor: &mut dyn CheatcodesExecutor<FEN>,
465    ) -> Result {
466        let Self { artifactPath: path, constructorArgs: args, value, salt } = self;
467        deploy_code(ccx, executor, path, Some(args), Some(*value), Some((*salt).into()))
468    }
469}
470
471/// Helper function to deploy contract from artifact code.
472/// Uses CREATE2 scheme if salt specified.
473fn deploy_code<FEN: FoundryEvmNetwork>(
474    ccx: &mut CheatsCtxt<'_, '_, FEN>,
475    executor: &mut dyn CheatcodesExecutor<FEN>,
476    path: &str,
477    constructor_args: Option<&Bytes>,
478    value: Option<U256>,
479    salt: Option<U256>,
480) -> Result {
481    let mut bytecode = get_artifact_code(ccx.state, path, false)?.to_vec();
482
483    // If active broadcast then set flag to deploy from code.
484    if let Some(broadcast) = &mut ccx.state.broadcast {
485        broadcast.deploy_from_code = true;
486    }
487
488    if let Some(args) = constructor_args {
489        bytecode.extend_from_slice(args);
490    }
491
492    let scheme =
493        if let Some(salt) = salt { CreateScheme::Create2 { salt } } else { CreateScheme::Create };
494
495    // If prank active at current depth, then use it as caller for create input.
496    let caller =
497        ccx.state.get_prank(ccx.ecx.journal().depth()).map_or(ccx.caller, |prank| prank.new_caller);
498
499    let outcome = exec_create(
500        executor,
501        CreateInputs::new(
502            caller,
503            scheme,
504            value.unwrap_or(U256::ZERO),
505            bytecode.into(),
506            ccx.gas_limit,
507            0,
508        ),
509        ccx,
510    )?;
511
512    if !outcome.result.result.is_ok() {
513        return Err(crate::Error::from(outcome.result.output));
514    }
515
516    let address = outcome.address.ok_or_else(|| fmt_err!("contract creation failed"))?;
517
518    Ok(address.abi_encode())
519}
520
521/// Returns the bytecode from a JSON artifact file.
522///
523/// Can parse following input formats:
524/// - `path/to/artifact.json`
525/// - `path/to/contract.sol`
526/// - `path/to/contract.sol:ContractName`
527/// - `path/to/contract.sol:ContractName:0.8.23`
528/// - `path/to/contract.sol:ContractName:profile`
529/// - `path/to/contract.sol:0.8.23`
530/// - `path/to/contract.sol:profile`
531/// - `ContractName`
532/// - `ContractName:0.8.23`
533/// - `ContractName:profile`
534///
535/// This function is safe to use with contracts that have library dependencies.
536/// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will
537/// reject artifacts with unlinked library placeholders.
538fn get_artifact_code<FEN: FoundryEvmNetwork>(
539    state: &Cheatcodes<FEN>,
540    path: &str,
541    deployed: bool,
542) -> Result<Bytes> {
543    let path = if path.ends_with(".json") {
544        PathBuf::from(path)
545    } else {
546        let parsed = parse_artifact_path(path)
547            .map_err(|e| fmt_err!("failed to parse artifact path: {e}"))?;
548        let ParsedArtifactPath { file, contract_name, version, profile } = parsed;
549
550        // Use available artifacts list if present
551        if let Some(artifacts) = &state.config.available_artifacts {
552            let ambiguous_file_profile =
553                file.is_some() && version.is_none() && profile.is_none() && contract_name.is_some();
554            let filter_artifacts = |treat_ambiguous_as_profile: bool| -> Vec<_> {
555                artifacts
556                    .iter()
557                    .filter(|(id, _)| {
558                        // name might be in the form of "Counter.0.8.23"
559                        let id_name = id.name.split('.').next().unwrap();
560
561                        if let Some(path) = &file
562                            && !id.source.ends_with(path)
563                        {
564                            return false;
565                        }
566                        if let Some(ref version) = version
567                            && (id.version.minor != version.minor
568                                || id.version.major != version.major
569                                || id.version.patch != version.patch)
570                        {
571                            return false;
572                        }
573                        if let Some(profile) = profile
574                            && id.profile != profile
575                        {
576                            return false;
577                        }
578                        if let Some(name) = contract_name {
579                            if treat_ambiguous_as_profile && ambiguous_file_profile {
580                                return id.profile == name;
581                            }
582
583                            return id_name == name;
584                        }
585
586                        true
587                    })
588                    .collect()
589            };
590
591            let mut filtered = filter_artifacts(false);
592            if filtered.is_empty() && ambiguous_file_profile {
593                filtered = filter_artifacts(true);
594            }
595
596            let artifact = match &filtered[..] {
597                [] => None,
598                [artifact] => Some(Ok(*artifact)),
599                filtered => {
600                    let mut filtered = filtered.to_vec();
601                    // If we know the current script/test contract solc version, try to filter by it
602                    Some(
603                        state
604                            .config
605                            .running_artifact
606                            .as_ref()
607                            .and_then(|running| {
608                                // Only filter by running version if user did NOT specify a version
609                                if version.is_none() {
610                                    filtered.retain(|(id, _)| id.version == running.version);
611
612                                    // Return artifact if only one matched
613                                    if filtered.len() == 1 {
614                                        return Some(filtered[0]);
615                                    }
616                                }
617
618                                // Only filter by running profile if user did NOT specify a profile
619                                if profile.is_none() {
620                                    filtered.retain(|(id, _)| id.profile == running.profile);
621
622                                    return (filtered.len() == 1).then(|| filtered[0]);
623                                }
624
625                                None
626                            })
627                            .ok_or_else(|| fmt_err!("multiple matching artifacts found")),
628                    )
629                }
630            };
631
632            if let Some(artifact) = artifact {
633                let artifact = artifact?;
634                let maybe_bytecode = if deployed {
635                    artifact.1.deployed_bytecode().cloned()
636                } else {
637                    artifact.1.bytecode().cloned()
638                };
639
640                return maybe_bytecode.ok_or_else(|| {
641                    fmt_err!("no bytecode for contract; is it abstract or unlinked?")
642                });
643            }
644        }
645
646        // Fallback: construct path manually when no artifacts list or no match found
647        let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name)
648        {
649            (Some(file), Some(contract_name)) => {
650                PathBuf::from(format!("{file}/{contract_name}.json"))
651            }
652            (None, Some(contract_name)) => {
653                PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
654            }
655            (Some(file), None) => {
656                let name = file.replace(".sol", "");
657                PathBuf::from(format!("{file}/{name}.json"))
658            }
659            _ => bail!("invalid artifact path"),
660        };
661
662        state.config.paths.artifacts.join(path_in_artifacts)
663    };
664
665    let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
666    let data = fs::read_to_string(path).map_err(|e| {
667        if state.config.available_artifacts.is_some() {
668            fmt_err!("no matching artifact found")
669        } else {
670            e.into()
671        }
672    })?;
673    let artifact = serde_json::from_str::<ContractObject>(&data)?;
674    let maybe_bytecode = if deployed { artifact.deployed_bytecode } else { artifact.bytecode };
675    maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
676}
677
678impl Cheatcode for ffiCall {
679    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
680        let Self { commandInput: input } = self;
681
682        let output = ffi(state, input)?;
683
684        // Check the exit code of the command.
685        if output.exitCode != 0 {
686            // If the command failed, return an error with the exit code and stderr.
687            return Err(fmt_err!(
688                "ffi command {:?} exited with code {}. stderr: {}",
689                input,
690                output.exitCode,
691                String::from_utf8_lossy(&output.stderr)
692            ));
693        }
694
695        // If the command succeeded but still wrote to stderr, log it as a warning.
696        if !output.stderr.is_empty() {
697            let stderr = String::from_utf8_lossy(&output.stderr);
698            warn!(target: "cheatcodes", ?input, ?stderr, "ffi command wrote to stderr");
699        }
700
701        // We already hex-decoded the stdout in the `ffi` helper function.
702        Ok(output.stdout.abi_encode())
703    }
704}
705
706impl Cheatcode for tryFfiCall {
707    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
708        let Self { commandInput: input } = self;
709        ffi(state, input).map(|res| res.abi_encode())
710    }
711}
712
713impl Cheatcode for promptCall {
714    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
715        let Self { promptText: text } = self;
716        prompt(state, text, prompt_input).map(|res| res.abi_encode())
717    }
718}
719
720impl Cheatcode for promptSecretCall {
721    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
722        let Self { promptText: text } = self;
723        prompt(state, text, prompt_password).map(|res| res.abi_encode())
724    }
725}
726
727impl Cheatcode for promptSecretUintCall {
728    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
729        let Self { promptText: text } = self;
730        parse(&prompt(state, text, prompt_password)?, &DynSolType::Uint(256))
731    }
732}
733
734impl Cheatcode for promptAddressCall {
735    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
736        let Self { promptText: text } = self;
737        parse(&prompt(state, text, prompt_input)?, &DynSolType::Address)
738    }
739}
740
741impl Cheatcode for promptUintCall {
742    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
743        let Self { promptText: text } = self;
744        parse(&prompt(state, text, prompt_input)?, &DynSolType::Uint(256))
745    }
746}
747
748pub(super) fn write_file<FEN: FoundryEvmNetwork>(
749    state: &Cheatcodes<FEN>,
750    path: &Path,
751    contents: &[u8],
752) -> Result {
753    let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
754    // write access to foundry.toml is not allowed
755    state.config.ensure_not_foundry_toml(&path)?;
756
757    if state.fs_commit {
758        fs::locked_write(path, contents)?;
759    }
760
761    Ok(Default::default())
762}
763
764fn read_dir<FEN: FoundryEvmNetwork>(
765    state: &Cheatcodes<FEN>,
766    path: &Path,
767    max_depth: u64,
768    follow_links: bool,
769) -> Result {
770    let root = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
771    let paths: Vec<DirEntry> = WalkDir::new(root)
772        .min_depth(1)
773        .max_depth(max_depth.try_into().unwrap_or(usize::MAX))
774        .follow_links(follow_links)
775        .contents_first(false)
776        .same_file_system(true)
777        .sort_by_file_name()
778        .into_iter()
779        .map(|entry| match entry {
780            Ok(entry) => DirEntry {
781                errorMessage: String::new(),
782                path: entry.path().display().to_string(),
783                depth: entry.depth() as u64,
784                isDir: entry.file_type().is_dir(),
785                isSymlink: entry.path_is_symlink(),
786            },
787            Err(e) => DirEntry {
788                errorMessage: e.to_string(),
789                path: e.path().map(|p| p.display().to_string()).unwrap_or_default(),
790                depth: e.depth() as u64,
791                isDir: false,
792                isSymlink: false,
793            },
794        })
795        .collect();
796    Ok(paths.abi_encode())
797}
798
799fn ffi<FEN: FoundryEvmNetwork>(state: &Cheatcodes<FEN>, input: &[String]) -> Result<FfiResult> {
800    ensure!(
801        state.config.ffi,
802        "FFI is disabled; add the `--ffi` flag to allow tests to call external commands"
803    );
804    ensure!(!input.is_empty() && !input[0].is_empty(), "can't execute empty command");
805    let mut cmd = Command::new(&input[0]);
806    cmd.args(&input[1..]);
807
808    debug!(target: "cheatcodes", ?cmd, "invoking ffi");
809
810    let output = cmd
811        .current_dir(&state.config.root)
812        .output()
813        .map_err(|err| fmt_err!("failed to execute command {cmd:?}: {err}"))?;
814
815    // The stdout might be encoded on valid hex, or it might just be a string,
816    // so we need to determine which it is to avoid improperly encoding later.
817    let trimmed_stdout = String::from_utf8(output.stdout)?;
818    let trimmed_stdout = trimmed_stdout.trim();
819    let encoded_stdout = if let Ok(hex) = hex::decode(trimmed_stdout) {
820        hex
821    } else {
822        trimmed_stdout.as_bytes().to_vec()
823    };
824    Ok(FfiResult {
825        exitCode: output.status.code().unwrap_or(69),
826        stdout: encoded_stdout.into(),
827        stderr: output.stderr.into(),
828    })
829}
830
831fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> {
832    Input::new().allow_empty(true).with_prompt(prompt_text).interact_text()
833}
834
835fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> {
836    Password::new().with_prompt(prompt_text).interact()
837}
838
839fn prompt<FEN: FoundryEvmNetwork>(
840    state: &Cheatcodes<FEN>,
841    prompt_text: &str,
842    input: fn(&str) -> Result<String, dialoguer::Error>,
843) -> Result<String> {
844    let text_clone = prompt_text.to_string();
845    let timeout = state.config.prompt_timeout;
846    let (tx, rx) = mpsc::channel();
847
848    thread::spawn(move || {
849        let _ = tx.send(input(&text_clone));
850    });
851
852    match rx.recv_timeout(timeout) {
853        Ok(res) => res.map_err(|err| {
854            let _ = sh_println!();
855            err.to_string().into()
856        }),
857        Err(_) => {
858            let _ = sh_eprintln!();
859            Err("Prompt timed out".into())
860        }
861    }
862}
863
864impl Cheatcode for getBroadcastCall {
865    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
866        let Self { contractName, chainId, txType } = self;
867
868        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
869            contractName,
870            *chainId,
871            &state.config.broadcast,
872            vec![map_broadcast_tx_type(*txType)],
873        )?;
874
875        Ok(latest_broadcast.abi_encode())
876    }
877}
878
879impl Cheatcode for getBroadcasts_0Call {
880    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
881        let Self { contractName, chainId, txType } = self;
882
883        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
884            .with_tx_type(map_broadcast_tx_type(*txType));
885
886        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
887
888        let summaries = broadcasts
889            .into_iter()
890            .flat_map(|broadcast| {
891                let results = reader.into_tx_receipts(broadcast);
892                parse_broadcast_results(results)
893            })
894            .collect::<Vec<_>>();
895
896        Ok(summaries.abi_encode())
897    }
898}
899
900impl Cheatcode for getBroadcasts_1Call {
901    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
902        let Self { contractName, chainId } = self;
903
904        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?;
905
906        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
907
908        let summaries = broadcasts
909            .into_iter()
910            .flat_map(|broadcast| {
911                let results = reader.into_tx_receipts(broadcast);
912                parse_broadcast_results(results)
913            })
914            .collect::<Vec<_>>();
915
916        Ok(summaries.abi_encode())
917    }
918}
919
920impl Cheatcode for getDeployment_0Call {
921    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
922        let Self { contractName } = self;
923        let chain_id = ccx.ecx.cfg().chain_id();
924
925        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
926            contractName,
927            chain_id,
928            &ccx.state.config.broadcast,
929            vec![CallKind::Create, CallKind::Create2],
930        )?;
931
932        Ok(latest_broadcast.contractAddress.abi_encode())
933    }
934}
935
936impl Cheatcode for getDeployment_1Call {
937    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
938        let Self { contractName, chainId } = self;
939
940        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
941            contractName,
942            *chainId,
943            &state.config.broadcast,
944            vec![CallKind::Create, CallKind::Create2],
945        )?;
946
947        Ok(latest_broadcast.contractAddress.abi_encode())
948    }
949}
950
951impl Cheatcode for getDeploymentsCall {
952    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
953        let Self { contractName, chainId } = self;
954
955        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
956            .with_tx_type(CallKind::Create)
957            .with_tx_type(CallKind::Create2);
958
959        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
960
961        let summaries = broadcasts
962            .into_iter()
963            .flat_map(|broadcast| {
964                let results = reader.into_tx_receipts(broadcast);
965                parse_broadcast_results(results)
966            })
967            .collect::<Vec<_>>();
968
969        let deployed_addresses =
970            summaries.into_iter().map(|summary| summary.contractAddress).collect::<Vec<_>>();
971
972        Ok(deployed_addresses.abi_encode())
973    }
974}
975
976fn map_broadcast_tx_type(tx_type: BroadcastTxType) -> CallKind {
977    match tx_type {
978        BroadcastTxType::Call => CallKind::Call,
979        BroadcastTxType::Create => CallKind::Create,
980        BroadcastTxType::Create2 => CallKind::Create2,
981        _ => unreachable!("invalid tx type"),
982    }
983}
984
985fn parse_broadcast_results<N: Network>(
986    results: Vec<(TransactionWithMetadata<N>, N::ReceiptResponse)>,
987) -> Vec<BroadcastTxSummary> {
988    results
989        .into_iter()
990        .map(|(tx, receipt)| BroadcastTxSummary {
991            txHash: receipt.transaction_hash(),
992            blockNumber: receipt.block_number().unwrap_or_default(),
993            txType: match tx.call_kind {
994                CallKind::Call => BroadcastTxType::Call,
995                CallKind::Create => BroadcastTxType::Create,
996                CallKind::Create2 => BroadcastTxType::Create2,
997                _ => unreachable!("invalid tx type"),
998            },
999            contractAddress: tx.contract_address.unwrap_or_default(),
1000            success: receipt.status(),
1001        })
1002        .collect()
1003}
1004
1005fn latest_broadcast<N: Network>(
1006    contract_name: &String,
1007    chain_id: u64,
1008    broadcast_path: &Path,
1009    filters: Vec<CallKind>,
1010) -> Result<BroadcastTxSummary>
1011where
1012    N::TxEnvelope: for<'d> serde::Deserialize<'d>,
1013{
1014    let mut reader = BroadcastReader::new(contract_name.clone(), chain_id, broadcast_path)?;
1015
1016    for filter in filters {
1017        reader = reader.with_tx_type(filter);
1018    }
1019
1020    let broadcast = reader.read_latest::<N>()?;
1021
1022    let results = reader.into_tx_receipts(broadcast);
1023
1024    let summaries = parse_broadcast_results(results);
1025
1026    summaries
1027        .first()
1028        .ok_or_else(|| fmt_err!("no deployment found for {contract_name} on chain {chain_id}"))
1029        .cloned()
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034    use super::*;
1035    use crate::CheatsConfig;
1036    use alloy_primitives::{address, b256};
1037    use foundry_common::ContractsByArtifact;
1038    use foundry_compilers::{
1039        ArtifactId,
1040        artifacts::{BytecodeObject, CompactBytecode, CompactContractBytecode},
1041    };
1042    use foundry_evm_core::evm::TempoEvmNetwork;
1043    use std::{env, fs as stdfs, sync::Arc};
1044
1045    fn cheats() -> Cheatcodes {
1046        let config = CheatsConfig {
1047            ffi: true,
1048            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1049            ..Default::default()
1050        };
1051        Cheatcodes::new(Arc::new(config))
1052    }
1053
1054    #[test]
1055    fn test_ffi_hex() {
1056        let msg = b"gm";
1057        let cheats = cheats();
1058        let args = ["echo".to_string(), hex::encode(msg)];
1059        let output = ffi(&cheats, &args).unwrap();
1060        assert_eq!(output.stdout, Bytes::from(msg));
1061    }
1062
1063    #[test]
1064    fn test_ffi_string() {
1065        let msg = "gm";
1066        let cheats = cheats();
1067        let args = ["echo".to_string(), msg.to_string()];
1068        let output = ffi(&cheats, &args).unwrap();
1069        assert_eq!(output.stdout, Bytes::from(msg.as_bytes()));
1070    }
1071
1072    #[test]
1073    fn test_ffi_fails_on_error_code() {
1074        let mut cheats = cheats();
1075
1076        // Use a command that is guaranteed to fail with a non-zero exit code on any platform.
1077        #[cfg(unix)]
1078        let args = vec!["false".to_string()];
1079        #[cfg(windows)]
1080        let args = vec!["cmd".to_string(), "/c".to_string(), "exit 1".to_string()];
1081
1082        let result = Cheatcode::apply(&ffiCall { commandInput: args }, &mut cheats);
1083
1084        // Assert that the cheatcode returned an error.
1085        assert!(result.is_err(), "Expected ffi cheatcode to fail, but it succeeded");
1086
1087        // Assert that the error message contains the expected information.
1088        let err_msg = result.unwrap_err().to_string();
1089        assert!(
1090            err_msg.contains("exited with code 1"),
1091            "Error message did not contain exit code: {err_msg}"
1092        );
1093    }
1094
1095    #[test]
1096    fn test_artifact_parsing() {
1097        let s = include_str!("../../evm/test-data/solc-obj.json");
1098        let artifact: ContractObject = serde_json::from_str(s).unwrap();
1099        assert!(artifact.bytecode.is_some());
1100
1101        let artifact: ContractObject = serde_json::from_str(s).unwrap();
1102        assert!(artifact.deployed_bytecode.is_some());
1103    }
1104
1105    #[test]
1106    fn test_alloy_json_abi_rejects_unlinked_bytecode() {
1107        let artifact_json = r#"{
1108            "abi": [],
1109            "bytecode": "0x73__$987e73aeca5e61ce83e4cb0814d87beda9$__63baf2f868"
1110        }"#;
1111
1112        let result: Result<ContractObject, _> = serde_json::from_str(artifact_json);
1113        assert!(result.is_err(), "should reject unlinked bytecode with placeholders");
1114        let err = result.unwrap_err().to_string();
1115        assert!(err.contains("expected bytecode, found unlinked bytecode with placeholder"));
1116    }
1117
1118    #[test]
1119    fn test_parse_artifact_path_file_only() {
1120        let parsed = super::parse_artifact_path("path/to/Contract.sol").unwrap();
1121        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1122        assert_eq!(parsed.contract_name, None);
1123        assert_eq!(parsed.version, None);
1124        assert_eq!(parsed.profile, None);
1125    }
1126
1127    #[test]
1128    fn test_parse_artifact_path_file_and_contract() {
1129        let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract").unwrap();
1130        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1131        assert_eq!(parsed.contract_name, Some("MyContract"));
1132        assert_eq!(parsed.version, None);
1133        assert_eq!(parsed.profile, None);
1134    }
1135
1136    #[test]
1137    fn test_parse_artifact_path_file_contract_version() {
1138        let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract:0.8.23").unwrap();
1139        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1140        assert_eq!(parsed.contract_name, Some("MyContract"));
1141        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1142        assert_eq!(parsed.profile, None);
1143    }
1144
1145    #[test]
1146    fn test_parse_artifact_path_file_contract_profile() {
1147        let parsed =
1148            super::parse_artifact_path("path/to/Contract.sol:MyContract:optimized").unwrap();
1149        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1150        assert_eq!(parsed.contract_name, Some("MyContract"));
1151        assert_eq!(parsed.version, None);
1152        assert_eq!(parsed.profile, Some("optimized"));
1153    }
1154
1155    #[test]
1156    fn test_parse_artifact_path_file_and_version() {
1157        let parsed = super::parse_artifact_path("path/to/Contract.sol:0.8.18").unwrap();
1158        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1159        assert_eq!(parsed.contract_name, None);
1160        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 18)));
1161        assert_eq!(parsed.profile, None);
1162    }
1163
1164    #[test]
1165    fn test_parse_artifact_path_file_and_profile() {
1166        // The parser keeps the two-part file form ambiguous. Artifact lookup can resolve this
1167        // segment as a profile when no contract name matches.
1168        let parsed = super::parse_artifact_path("Contract.sol:paris").unwrap();
1169        assert_eq!(parsed.file, Some(PathBuf::from("Contract.sol")));
1170        assert_eq!(parsed.contract_name, Some("paris"));
1171        assert_eq!(parsed.version, None);
1172        assert_eq!(parsed.profile, None);
1173    }
1174
1175    fn test_artifact(
1176        source: &str,
1177        name: &str,
1178        profile: &str,
1179        bytecode: Bytes,
1180    ) -> (ArtifactId, CompactContractBytecode) {
1181        (
1182            ArtifactId {
1183                path: PathBuf::from(format!("{source}/{name}.json")),
1184                name: name.to_owned(),
1185                source: PathBuf::from(source),
1186                version: Version::new(0, 8, 30),
1187                build_id: String::new(),
1188                profile: profile.to_owned(),
1189            },
1190            CompactContractBytecode {
1191                abi: Some(Default::default()),
1192                bytecode: Some(CompactBytecode {
1193                    object: BytecodeObject::Bytecode(bytecode),
1194                    source_map: None,
1195                    link_references: Default::default(),
1196                }),
1197                deployed_bytecode: None,
1198            },
1199        )
1200    }
1201
1202    #[test]
1203    fn test_get_artifact_code_resolves_file_profile_ambiguity() {
1204        let default_bytecode = Bytes::from_static(&[0x60, 0x01]);
1205        let paris_bytecode = Bytes::from_static(&[0x60, 0x02]);
1206        let source = "src/GetCodeProfile.t.sol";
1207        let artifacts = ContractsByArtifact::new([
1208            test_artifact(source, "GetCodeProfile", "default", default_bytecode),
1209            test_artifact(source, "GetCodeProfile", "paris", paris_bytecode.clone()),
1210        ]);
1211        let config = CheatsConfig {
1212            available_artifacts: Some(artifacts),
1213            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1214            ..Default::default()
1215        };
1216        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1217
1218        let bytecode =
1219            super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1220
1221        assert_eq!(bytecode, paris_bytecode);
1222    }
1223
1224    #[test]
1225    fn test_get_artifact_code_prefers_contract_name_over_file_profile_ambiguity() {
1226        let profile_bytecode = Bytes::from_static(&[0x60, 0x02]);
1227        let contract_bytecode = Bytes::from_static(&[0x60, 0x03]);
1228        let source = "src/GetCodeProfile.t.sol";
1229        let artifacts = ContractsByArtifact::new([
1230            test_artifact(source, "GetCodeProfile", "paris", profile_bytecode),
1231            test_artifact(source, "paris", "default", contract_bytecode.clone()),
1232        ]);
1233        let config = CheatsConfig {
1234            available_artifacts: Some(artifacts),
1235            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1236            ..Default::default()
1237        };
1238        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1239
1240        let bytecode =
1241            super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1242
1243        assert_eq!(bytecode, contract_bytecode);
1244    }
1245
1246    #[test]
1247    fn test_parse_artifact_path_contract_only() {
1248        let parsed = super::parse_artifact_path("MyContract").unwrap();
1249        assert_eq!(parsed.file, None);
1250        assert_eq!(parsed.contract_name, Some("MyContract"));
1251        assert_eq!(parsed.version, None);
1252        assert_eq!(parsed.profile, None);
1253    }
1254
1255    #[test]
1256    fn test_parse_artifact_path_contract_and_version() {
1257        let parsed = super::parse_artifact_path("MyContract:0.8.23").unwrap();
1258        assert_eq!(parsed.file, None);
1259        assert_eq!(parsed.contract_name, Some("MyContract"));
1260        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1261        assert_eq!(parsed.profile, None);
1262    }
1263
1264    #[test]
1265    fn test_parse_artifact_path_contract_and_profile() {
1266        let parsed = super::parse_artifact_path("MyContract:optimized").unwrap();
1267        assert_eq!(parsed.file, None);
1268        assert_eq!(parsed.contract_name, Some("MyContract"));
1269        assert_eq!(parsed.version, None);
1270        assert_eq!(parsed.profile, Some("optimized"));
1271    }
1272
1273    #[test]
1274    fn test_parse_artifact_path_profile_names() {
1275        // Test various profile name patterns
1276        for profile in ["v1", "v2", "paris", "optimized", "default", "prod", "dev"] {
1277            let path = format!("MyContract:{profile}");
1278            let parsed = super::parse_artifact_path(&path).unwrap();
1279            assert_eq!(parsed.contract_name, Some("MyContract"));
1280            assert_eq!(parsed.profile, Some(profile));
1281            assert_eq!(parsed.version, None);
1282        }
1283    }
1284
1285    #[test]
1286    fn test_parse_artifact_path_invalid_version() {
1287        // Invalid semver should be treated as profile
1288        let parsed = super::parse_artifact_path("MyContract:invalid").unwrap();
1289        assert_eq!(parsed.contract_name, Some("MyContract"));
1290        assert_eq!(parsed.profile, Some("invalid"));
1291        assert_eq!(parsed.version, None);
1292    }
1293
1294    fn unique_temp_dir(prefix: &str) -> PathBuf {
1295        env::temp_dir().join(format!(
1296            "foundry-cheatcodes-{prefix}-{}",
1297            SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()
1298        ))
1299    }
1300
1301    #[test]
1302    fn test_latest_broadcast_reads_tempo_sequences() {
1303        let root = unique_temp_dir("tempo-broadcast");
1304        let broadcast_path = root.join("broadcast");
1305        let sequence_dir = broadcast_path.join("Counter.s.sol").join("31337");
1306        stdfs::create_dir_all(&sequence_dir).unwrap();
1307
1308        let tx_hash = "0x04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f";
1309        let block_hash = "0x860f788b251ece768e63b0d3906d156f652d843848b71c7fe81faacd49139d66";
1310        let from = "0xa70ab0448e66cd77995bfbba5c5b64b41a85f3fd";
1311        let contract_address = "0x20c0000000000000000000000000000000000000";
1312        let zero_bloom = format!("0x{}", "0".repeat(512));
1313
1314        let sequence = serde_json::json!({
1315            "transactions": [{
1316                "hash": tx_hash,
1317                "transactionType": "CREATE",
1318                "contractName": "Counter",
1319                "contractAddress": contract_address,
1320                "function": serde_json::Value::Null,
1321                "arguments": serde_json::Value::Null,
1322                "transaction": {
1323                    "type": "0x76",
1324                    "from": from,
1325                    "to": serde_json::Value::Null,
1326                    "data": "0x",
1327                    "value": "0x0",
1328                    "gas": "0x5208",
1329                    "nonce": "0x0",
1330                    "accessList": [],
1331                    "calls": [],
1332                    "nonceKey": "0x0",
1333                    "feePayerSignature": serde_json::Value::Null,
1334                    "validBefore": serde_json::Value::Null,
1335                    "validAfter": serde_json::Value::Null,
1336                    "keyAuthorization": serde_json::Value::Null,
1337                    "aaAuthorizationList": []
1338                },
1339                "additionalContracts": [],
1340                "isFixedGasLimit": false
1341            }],
1342            "receipts": [{
1343                "type": "0x76",
1344                "status": "0x1",
1345                "cumulativeGasUsed": "0x5208",
1346                "logs": [],
1347                "logsBloom": zero_bloom,
1348                "transactionHash": tx_hash,
1349                "transactionIndex": "0x0",
1350                "blockHash": block_hash,
1351                "blockNumber": "0x7",
1352                "gasUsed": "0x5208",
1353                "effectiveGasPrice": "0x1",
1354                "from": from,
1355                "to": serde_json::Value::Null,
1356                "contractAddress": contract_address,
1357                "feePayer": from
1358            }],
1359            "libraries": [],
1360            "pending": [],
1361            "returns": {},
1362            "timestamp": 1,
1363            "chain": 31337,
1364            "commit": serde_json::Value::Null
1365        });
1366
1367        fs::write_json_file(&sequence_dir.join("run-1.json"), &sequence).unwrap();
1368
1369        let latest = latest_broadcast::<<TempoEvmNetwork as FoundryEvmNetwork>::Network>(
1370            &"Counter".to_owned(),
1371            31337,
1372            &broadcast_path,
1373            vec![CallKind::Create],
1374        )
1375        .unwrap();
1376
1377        assert_eq!(
1378            latest.txHash,
1379            b256!("04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f")
1380        );
1381        assert_eq!(latest.blockNumber, 7);
1382        assert!(matches!(latest.txType, BroadcastTxType::Create));
1383        assert_eq!(latest.contractAddress, address!("20c0000000000000000000000000000000000000"));
1384        assert!(latest.success);
1385
1386        stdfs::remove_dir_all(root).unwrap();
1387    }
1388}