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, FixedBytes, U256, hex, map::Entry};
11use alloy_sol_types::SolValue;
12use dialoguer::{Input, Password};
13use forge_script_sequence::{BroadcastReader, TransactionWithMetadata};
14use foundry_common::{contracts::ContractData, 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 getSelectorsCall {
384    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
385        let Self { artifactPath: path } = self;
386        let selectors: Vec<FixedBytes<4>> = match get_artifact_source(state, path)? {
387            ArtifactSource::InMemory(data) => data.abi.functions().map(|f| f.selector()).collect(),
388            ArtifactSource::Disk(path) => {
389                let data = read_artifact_file(state, &path)?;
390                // Parse as raw JSON rather than `ContractObject` so we can still read selectors
391                // from artifacts with unlinked bytecode (which `ContractObject` rejects).
392                let json: serde_json::Value = serde_json::from_str(&data)?;
393                let abi =
394                    json.get("abi").ok_or_else(|| fmt_err!("no `abi` field in artifact JSON"))?;
395                let abi: alloy_json_abi::JsonAbi =
396                    serde_json::from_value(abi.clone()).map_err(|e| fmt_err!("{e}"))?;
397                abi.functions().map(|f| f.selector()).collect()
398            }
399        };
400        Ok(selectors.abi_encode())
401    }
402}
403
404impl Cheatcode for deployCode_0Call {
405    fn apply_full<FEN: FoundryEvmNetwork>(
406        &self,
407        ccx: &mut CheatsCtxt<'_, '_, FEN>,
408        executor: &mut dyn CheatcodesExecutor<FEN>,
409    ) -> Result {
410        let Self { artifactPath: path } = self;
411        deploy_code(ccx, executor, path, None, None, None)
412    }
413}
414
415impl Cheatcode for deployCode_1Call {
416    fn apply_full<FEN: FoundryEvmNetwork>(
417        &self,
418        ccx: &mut CheatsCtxt<'_, '_, FEN>,
419        executor: &mut dyn CheatcodesExecutor<FEN>,
420    ) -> Result {
421        let Self { artifactPath: path, constructorArgs: args } = self;
422        deploy_code(ccx, executor, path, Some(args), None, None)
423    }
424}
425
426impl Cheatcode for deployCode_2Call {
427    fn apply_full<FEN: FoundryEvmNetwork>(
428        &self,
429        ccx: &mut CheatsCtxt<'_, '_, FEN>,
430        executor: &mut dyn CheatcodesExecutor<FEN>,
431    ) -> Result {
432        let Self { artifactPath: path, value } = self;
433        deploy_code(ccx, executor, path, None, Some(*value), None)
434    }
435}
436
437impl Cheatcode for deployCode_3Call {
438    fn apply_full<FEN: FoundryEvmNetwork>(
439        &self,
440        ccx: &mut CheatsCtxt<'_, '_, FEN>,
441        executor: &mut dyn CheatcodesExecutor<FEN>,
442    ) -> Result {
443        let Self { artifactPath: path, constructorArgs: args, value } = self;
444        deploy_code(ccx, executor, path, Some(args), Some(*value), None)
445    }
446}
447
448impl Cheatcode for deployCode_4Call {
449    fn apply_full<FEN: FoundryEvmNetwork>(
450        &self,
451        ccx: &mut CheatsCtxt<'_, '_, FEN>,
452        executor: &mut dyn CheatcodesExecutor<FEN>,
453    ) -> Result {
454        let Self { artifactPath: path, salt } = self;
455        deploy_code(ccx, executor, path, None, None, Some((*salt).into()))
456    }
457}
458
459impl Cheatcode for deployCode_5Call {
460    fn apply_full<FEN: FoundryEvmNetwork>(
461        &self,
462        ccx: &mut CheatsCtxt<'_, '_, FEN>,
463        executor: &mut dyn CheatcodesExecutor<FEN>,
464    ) -> Result {
465        let Self { artifactPath: path, constructorArgs: args, salt } = self;
466        deploy_code(ccx, executor, path, Some(args), None, Some((*salt).into()))
467    }
468}
469
470impl Cheatcode for deployCode_6Call {
471    fn apply_full<FEN: FoundryEvmNetwork>(
472        &self,
473        ccx: &mut CheatsCtxt<'_, '_, FEN>,
474        executor: &mut dyn CheatcodesExecutor<FEN>,
475    ) -> Result {
476        let Self { artifactPath: path, value, salt } = self;
477        deploy_code(ccx, executor, path, None, Some(*value), Some((*salt).into()))
478    }
479}
480
481impl Cheatcode for deployCode_7Call {
482    fn apply_full<FEN: FoundryEvmNetwork>(
483        &self,
484        ccx: &mut CheatsCtxt<'_, '_, FEN>,
485        executor: &mut dyn CheatcodesExecutor<FEN>,
486    ) -> Result {
487        let Self { artifactPath: path, constructorArgs: args, value, salt } = self;
488        deploy_code(ccx, executor, path, Some(args), Some(*value), Some((*salt).into()))
489    }
490}
491
492/// Helper function to deploy contract from artifact code.
493/// Uses CREATE2 scheme if salt specified.
494fn deploy_code<FEN: FoundryEvmNetwork>(
495    ccx: &mut CheatsCtxt<'_, '_, FEN>,
496    executor: &mut dyn CheatcodesExecutor<FEN>,
497    path: &str,
498    constructor_args: Option<&Bytes>,
499    value: Option<U256>,
500    salt: Option<U256>,
501) -> Result {
502    let mut bytecode = get_artifact_code(ccx.state, path, false)?.to_vec();
503
504    // If active broadcast then set flag to deploy from code.
505    if let Some(broadcast) = &mut ccx.state.broadcast {
506        broadcast.deploy_from_code = true;
507    }
508
509    if let Some(args) = constructor_args {
510        bytecode.extend_from_slice(args);
511    }
512
513    let scheme =
514        if let Some(salt) = salt { CreateScheme::Create2 { salt } } else { CreateScheme::Create };
515
516    // If prank active at current depth, then use it as caller for create input.
517    let caller =
518        ccx.state.get_prank(ccx.ecx.journal().depth()).map_or(ccx.caller, |prank| prank.new_caller);
519
520    let outcome = exec_create(
521        executor,
522        CreateInputs::new(
523            caller,
524            scheme,
525            value.unwrap_or(U256::ZERO),
526            bytecode.into(),
527            ccx.gas_limit,
528            0,
529        ),
530        ccx,
531    )?;
532
533    if !outcome.result.result.is_ok() {
534        return Err(crate::Error::from(outcome.result.output));
535    }
536
537    let address = outcome.address.ok_or_else(|| fmt_err!("contract creation failed"))?;
538
539    Ok(address.abi_encode())
540}
541
542/// Resolved location of an artifact referenced by a cheatcode path argument.
543enum ArtifactSource<'a> {
544    /// The artifact was matched in the in-memory `available_artifacts` list.
545    InMemory(&'a ContractData),
546    /// The artifact must be read from the given path on disk.
547    Disk(PathBuf),
548}
549
550/// Resolves a cheatcode artifact reference to its source.
551///
552/// Can parse the following input formats:
553/// - `path/to/artifact.json`
554/// - `path/to/contract.sol`
555/// - `path/to/contract.sol:ContractName`
556/// - `path/to/contract.sol:ContractName:0.8.23`
557/// - `path/to/contract.sol:ContractName:profile`
558/// - `path/to/contract.sol:0.8.23`
559/// - `path/to/contract.sol:profile`
560/// - `ContractName`
561/// - `ContractName:0.8.23`
562/// - `ContractName:profile`
563fn get_artifact_source<'a, FEN: FoundryEvmNetwork>(
564    state: &'a Cheatcodes<FEN>,
565    path: &str,
566) -> Result<ArtifactSource<'a>> {
567    if path.ends_with(".json") {
568        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
569        return Ok(ArtifactSource::Disk(path));
570    }
571
572    let parsed =
573        parse_artifact_path(path).map_err(|e| fmt_err!("failed to parse artifact path: {e}"))?;
574    let ParsedArtifactPath { file, contract_name, version, profile } = parsed;
575
576    // Use available artifacts list if present
577    if let Some(artifacts) = &state.config.available_artifacts {
578        let ambiguous_file_profile =
579            file.is_some() && version.is_none() && profile.is_none() && contract_name.is_some();
580        let filter_artifacts = |treat_ambiguous_as_profile: bool| -> Vec<_> {
581            artifacts
582                .iter()
583                .filter(|(id, _)| {
584                    // name might be in the form of "Counter.0.8.23"
585                    let id_name = id.name.split('.').next().unwrap();
586
587                    if let Some(path) = &file
588                        && !id.source.ends_with(path)
589                    {
590                        return false;
591                    }
592                    if let Some(ref version) = version
593                        && (id.version.minor != version.minor
594                            || id.version.major != version.major
595                            || id.version.patch != version.patch)
596                    {
597                        return false;
598                    }
599                    if let Some(profile) = profile
600                        && id.profile != profile
601                    {
602                        return false;
603                    }
604                    if let Some(name) = contract_name {
605                        if treat_ambiguous_as_profile && ambiguous_file_profile {
606                            return id.profile == name;
607                        }
608
609                        return id_name == name;
610                    }
611
612                    true
613                })
614                .collect()
615        };
616
617        let mut filtered = filter_artifacts(false);
618        if filtered.is_empty() && ambiguous_file_profile {
619            filtered = filter_artifacts(true);
620        }
621
622        let artifact = match &filtered[..] {
623            [] => None,
624            [artifact] => Some(Ok(*artifact)),
625            filtered => {
626                let mut filtered = filtered.to_vec();
627                // If we know the current script/test contract solc version, try to filter by it
628                Some(
629                    state
630                        .config
631                        .running_artifact
632                        .as_ref()
633                        .and_then(|running| {
634                            // Only filter by running version if user did NOT specify a version
635                            if version.is_none() {
636                                filtered.retain(|(id, _)| id.version == running.version);
637
638                                // Return artifact if only one matched
639                                if filtered.len() == 1 {
640                                    return Some(filtered[0]);
641                                }
642                            }
643
644                            // Only filter by running profile if user did NOT specify a profile
645                            if profile.is_none() {
646                                filtered.retain(|(id, _)| id.profile == running.profile);
647
648                                return (filtered.len() == 1).then(|| filtered[0]);
649                            }
650
651                            None
652                        })
653                        .ok_or_else(|| fmt_err!("multiple matching artifacts found")),
654                )
655            }
656        };
657
658        if let Some(artifact) = artifact {
659            return Ok(ArtifactSource::InMemory(artifact?.1));
660        }
661    }
662
663    // Fallback: construct path manually when no artifacts list or no match found
664    let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name) {
665        (Some(file), Some(contract_name)) => PathBuf::from(format!("{file}/{contract_name}.json")),
666        (None, Some(contract_name)) => {
667            PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
668        }
669        (Some(file), None) => {
670            let name = file.replace(".sol", "");
671            PathBuf::from(format!("{file}/{name}.json"))
672        }
673        _ => bail!("invalid artifact path"),
674    };
675
676    let path = state.config.paths.artifacts.join(path_in_artifacts);
677    let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
678    Ok(ArtifactSource::Disk(path))
679}
680
681/// Reads an artifact JSON file, mapping I/O errors to a helpful message when the
682/// lookup fell through the in-memory artifacts list.
683fn read_artifact_file<FEN: FoundryEvmNetwork>(
684    state: &Cheatcodes<FEN>,
685    path: &Path,
686) -> Result<String> {
687    fs::read_to_string(path).map_err(|e| {
688        if state.config.available_artifacts.is_some() {
689            fmt_err!("no matching artifact found")
690        } else {
691            e.into()
692        }
693    })
694}
695
696/// Returns the bytecode from a JSON artifact file.
697///
698/// See [`get_artifact_source`] for the supported path formats.
699///
700/// This function is safe to use with contracts that have library dependencies.
701/// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will
702/// reject artifacts with unlinked library placeholders.
703fn get_artifact_code<FEN: FoundryEvmNetwork>(
704    state: &Cheatcodes<FEN>,
705    path: &str,
706    deployed: bool,
707) -> Result<Bytes> {
708    let maybe_bytecode = match get_artifact_source(state, path)? {
709        ArtifactSource::InMemory(data) => {
710            if deployed { data.deployed_bytecode() } else { data.bytecode() }.cloned()
711        }
712        ArtifactSource::Disk(path) => {
713            let data = read_artifact_file(state, &path)?;
714            let artifact = serde_json::from_str::<ContractObject>(&data)?;
715            if deployed { artifact.deployed_bytecode } else { artifact.bytecode }
716        }
717    };
718    maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
719}
720
721impl Cheatcode for ffiCall {
722    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
723        let Self { commandInput: input } = self;
724
725        let output = ffi(state, input)?;
726
727        // Check the exit code of the command.
728        if output.exitCode != 0 {
729            // If the command failed, return an error with the exit code and stderr.
730            return Err(fmt_err!(
731                "ffi command {:?} exited with code {}. stderr: {}",
732                input,
733                output.exitCode,
734                String::from_utf8_lossy(&output.stderr)
735            ));
736        }
737
738        // If the command succeeded but still wrote to stderr, log it as a warning.
739        if !output.stderr.is_empty() {
740            let stderr = String::from_utf8_lossy(&output.stderr);
741            warn!(target: "cheatcodes", ?input, ?stderr, "ffi command wrote to stderr");
742        }
743
744        // We already hex-decoded the stdout in the `ffi` helper function.
745        Ok(output.stdout.abi_encode())
746    }
747}
748
749impl Cheatcode for tryFfiCall {
750    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
751        let Self { commandInput: input } = self;
752        ffi(state, input).map(|res| res.abi_encode())
753    }
754}
755
756impl Cheatcode for promptCall {
757    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
758        let Self { promptText: text } = self;
759        prompt(state, text, prompt_input).map(|res| res.abi_encode())
760    }
761}
762
763impl Cheatcode for promptSecretCall {
764    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
765        let Self { promptText: text } = self;
766        prompt(state, text, prompt_password).map(|res| res.abi_encode())
767    }
768}
769
770impl Cheatcode for promptSecretUintCall {
771    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
772        let Self { promptText: text } = self;
773        parse(&prompt(state, text, prompt_password)?, &DynSolType::Uint(256))
774    }
775}
776
777impl Cheatcode for promptAddressCall {
778    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
779        let Self { promptText: text } = self;
780        parse(&prompt(state, text, prompt_input)?, &DynSolType::Address)
781    }
782}
783
784impl Cheatcode for promptUintCall {
785    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
786        let Self { promptText: text } = self;
787        parse(&prompt(state, text, prompt_input)?, &DynSolType::Uint(256))
788    }
789}
790
791pub(super) fn write_file<FEN: FoundryEvmNetwork>(
792    state: &Cheatcodes<FEN>,
793    path: &Path,
794    contents: &[u8],
795) -> Result {
796    let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
797    // write access to foundry.toml is not allowed
798    state.config.ensure_not_foundry_toml(&path)?;
799
800    if state.fs_commit {
801        fs::locked_write(path, contents)?;
802    }
803
804    Ok(Default::default())
805}
806
807fn read_dir<FEN: FoundryEvmNetwork>(
808    state: &Cheatcodes<FEN>,
809    path: &Path,
810    max_depth: u64,
811    follow_links: bool,
812) -> Result {
813    let root = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
814    let paths: Vec<DirEntry> = WalkDir::new(root)
815        .min_depth(1)
816        .max_depth(max_depth.try_into().unwrap_or(usize::MAX))
817        .follow_links(follow_links)
818        .contents_first(false)
819        .same_file_system(true)
820        .sort_by_file_name()
821        .into_iter()
822        .map(|entry| match entry {
823            Ok(entry) => DirEntry {
824                errorMessage: String::new(),
825                path: entry.path().display().to_string(),
826                depth: entry.depth() as u64,
827                isDir: entry.file_type().is_dir(),
828                isSymlink: entry.path_is_symlink(),
829            },
830            Err(e) => DirEntry {
831                errorMessage: e.to_string(),
832                path: e.path().map(|p| p.display().to_string()).unwrap_or_default(),
833                depth: e.depth() as u64,
834                isDir: false,
835                isSymlink: false,
836            },
837        })
838        .collect();
839    Ok(paths.abi_encode())
840}
841
842fn ffi<FEN: FoundryEvmNetwork>(state: &Cheatcodes<FEN>, input: &[String]) -> Result<FfiResult> {
843    ensure!(
844        state.config.ffi,
845        "FFI is disabled; add the `--ffi` flag to allow tests to call external commands"
846    );
847    ensure!(!input.is_empty() && !input[0].is_empty(), "can't execute empty command");
848    let mut cmd = Command::new(&input[0]);
849    cmd.args(&input[1..]);
850
851    debug!(target: "cheatcodes", ?cmd, "invoking ffi");
852
853    let output = cmd
854        .current_dir(&state.config.root)
855        .output()
856        .map_err(|err| fmt_err!("failed to execute command {cmd:?}: {err}"))?;
857
858    // The stdout might be encoded on valid hex, or it might just be a string,
859    // so we need to determine which it is to avoid improperly encoding later.
860    let trimmed_stdout = String::from_utf8(output.stdout)?;
861    let trimmed_stdout = trimmed_stdout.trim();
862    let encoded_stdout = if let Ok(hex) = hex::decode(trimmed_stdout) {
863        hex
864    } else {
865        trimmed_stdout.as_bytes().to_vec()
866    };
867    Ok(FfiResult {
868        exitCode: output.status.code().unwrap_or(69),
869        stdout: encoded_stdout.into(),
870        stderr: output.stderr.into(),
871    })
872}
873
874fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> {
875    Input::new().allow_empty(true).with_prompt(prompt_text).interact_text()
876}
877
878fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> {
879    Password::new().with_prompt(prompt_text).interact()
880}
881
882fn prompt<FEN: FoundryEvmNetwork>(
883    state: &Cheatcodes<FEN>,
884    prompt_text: &str,
885    input: fn(&str) -> Result<String, dialoguer::Error>,
886) -> Result<String> {
887    let text_clone = prompt_text.to_string();
888    let timeout = state.config.prompt_timeout;
889    let (tx, rx) = mpsc::channel();
890
891    thread::spawn(move || {
892        let _ = tx.send(input(&text_clone));
893    });
894
895    match rx.recv_timeout(timeout) {
896        Ok(res) => res.map_err(|err| {
897            let _ = sh_println!();
898            err.to_string().into()
899        }),
900        Err(_) => {
901            let _ = sh_eprintln!();
902            Err("Prompt timed out".into())
903        }
904    }
905}
906
907impl Cheatcode for getBroadcastCall {
908    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
909        let Self { contractName, chainId, txType } = self;
910
911        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
912            contractName,
913            *chainId,
914            &state.config.broadcast,
915            vec![map_broadcast_tx_type(*txType)],
916        )?;
917
918        Ok(latest_broadcast.abi_encode())
919    }
920}
921
922impl Cheatcode for getBroadcasts_0Call {
923    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
924        let Self { contractName, chainId, txType } = self;
925
926        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
927            .with_tx_type(map_broadcast_tx_type(*txType));
928
929        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
930
931        let summaries = broadcasts
932            .into_iter()
933            .flat_map(|broadcast| {
934                let results = reader.into_tx_receipts(broadcast);
935                parse_broadcast_results(results)
936            })
937            .collect::<Vec<_>>();
938
939        Ok(summaries.abi_encode())
940    }
941}
942
943impl Cheatcode for getBroadcasts_1Call {
944    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
945        let Self { contractName, chainId } = self;
946
947        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?;
948
949        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
950
951        let summaries = broadcasts
952            .into_iter()
953            .flat_map(|broadcast| {
954                let results = reader.into_tx_receipts(broadcast);
955                parse_broadcast_results(results)
956            })
957            .collect::<Vec<_>>();
958
959        Ok(summaries.abi_encode())
960    }
961}
962
963impl Cheatcode for getDeployment_0Call {
964    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
965        let Self { contractName } = self;
966        let chain_id = ccx.ecx.cfg().chain_id();
967
968        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
969            contractName,
970            chain_id,
971            &ccx.state.config.broadcast,
972            vec![CallKind::Create, CallKind::Create2],
973        )?;
974
975        Ok(latest_broadcast.contractAddress.abi_encode())
976    }
977}
978
979impl Cheatcode for getDeployment_1Call {
980    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
981        let Self { contractName, chainId } = self;
982
983        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
984            contractName,
985            *chainId,
986            &state.config.broadcast,
987            vec![CallKind::Create, CallKind::Create2],
988        )?;
989
990        Ok(latest_broadcast.contractAddress.abi_encode())
991    }
992}
993
994impl Cheatcode for getDeploymentsCall {
995    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
996        let Self { contractName, chainId } = self;
997
998        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
999            .with_tx_type(CallKind::Create)
1000            .with_tx_type(CallKind::Create2);
1001
1002        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
1003
1004        let summaries = broadcasts
1005            .into_iter()
1006            .flat_map(|broadcast| {
1007                let results = reader.into_tx_receipts(broadcast);
1008                parse_broadcast_results(results)
1009            })
1010            .collect::<Vec<_>>();
1011
1012        let deployed_addresses =
1013            summaries.into_iter().map(|summary| summary.contractAddress).collect::<Vec<_>>();
1014
1015        Ok(deployed_addresses.abi_encode())
1016    }
1017}
1018
1019fn map_broadcast_tx_type(tx_type: BroadcastTxType) -> CallKind {
1020    match tx_type {
1021        BroadcastTxType::Call => CallKind::Call,
1022        BroadcastTxType::Create => CallKind::Create,
1023        BroadcastTxType::Create2 => CallKind::Create2,
1024        _ => unreachable!("invalid tx type"),
1025    }
1026}
1027
1028fn parse_broadcast_results<N: Network>(
1029    results: Vec<(TransactionWithMetadata<N>, N::ReceiptResponse)>,
1030) -> Vec<BroadcastTxSummary> {
1031    results
1032        .into_iter()
1033        .map(|(tx, receipt)| BroadcastTxSummary {
1034            txHash: receipt.transaction_hash(),
1035            blockNumber: receipt.block_number().unwrap_or_default(),
1036            txType: match tx.call_kind {
1037                CallKind::Call => BroadcastTxType::Call,
1038                CallKind::Create => BroadcastTxType::Create,
1039                CallKind::Create2 => BroadcastTxType::Create2,
1040                _ => unreachable!("invalid tx type"),
1041            },
1042            contractAddress: tx.contract_address.unwrap_or_default(),
1043            success: receipt.status(),
1044        })
1045        .collect()
1046}
1047
1048fn latest_broadcast<N: Network>(
1049    contract_name: &String,
1050    chain_id: u64,
1051    broadcast_path: &Path,
1052    filters: Vec<CallKind>,
1053) -> Result<BroadcastTxSummary>
1054where
1055    N::TxEnvelope: for<'d> serde::Deserialize<'d>,
1056{
1057    let mut reader = BroadcastReader::new(contract_name.clone(), chain_id, broadcast_path)?;
1058
1059    for filter in filters {
1060        reader = reader.with_tx_type(filter);
1061    }
1062
1063    let broadcast = reader.read_latest::<N>()?;
1064
1065    let results = reader.into_tx_receipts(broadcast);
1066
1067    let summaries = parse_broadcast_results(results);
1068
1069    summaries
1070        .first()
1071        .ok_or_else(|| fmt_err!("no deployment found for {contract_name} on chain {chain_id}"))
1072        .cloned()
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077    use super::*;
1078    use crate::CheatsConfig;
1079    use alloy_primitives::{address, b256};
1080    use foundry_common::ContractsByArtifact;
1081    use foundry_compilers::{
1082        ArtifactId,
1083        artifacts::{BytecodeObject, CompactBytecode, CompactContractBytecode},
1084    };
1085    use foundry_evm_core::evm::TempoEvmNetwork;
1086    use std::{env, fs as stdfs, sync::Arc};
1087
1088    fn cheats() -> Cheatcodes {
1089        let config = CheatsConfig {
1090            ffi: true,
1091            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1092            ..Default::default()
1093        };
1094        Cheatcodes::new(Arc::new(config))
1095    }
1096
1097    #[test]
1098    fn test_ffi_hex() {
1099        let msg = b"gm";
1100        let cheats = cheats();
1101        let args = ["echo".to_string(), hex::encode(msg)];
1102        let output = ffi(&cheats, &args).unwrap();
1103        assert_eq!(output.stdout, Bytes::from(msg));
1104    }
1105
1106    #[test]
1107    fn test_ffi_string() {
1108        let msg = "gm";
1109        let cheats = cheats();
1110        let args = ["echo".to_string(), msg.to_string()];
1111        let output = ffi(&cheats, &args).unwrap();
1112        assert_eq!(output.stdout, Bytes::from(msg.as_bytes()));
1113    }
1114
1115    #[test]
1116    fn test_ffi_fails_on_error_code() {
1117        let mut cheats = cheats();
1118
1119        // Use a command that is guaranteed to fail with a non-zero exit code on any platform.
1120        #[cfg(unix)]
1121        let args = vec!["false".to_string()];
1122        #[cfg(windows)]
1123        let args = vec!["cmd".to_string(), "/c".to_string(), "exit 1".to_string()];
1124
1125        let result = Cheatcode::apply(&ffiCall { commandInput: args }, &mut cheats);
1126
1127        // Assert that the cheatcode returned an error.
1128        assert!(result.is_err(), "Expected ffi cheatcode to fail, but it succeeded");
1129
1130        // Assert that the error message contains the expected information.
1131        let err_msg = result.unwrap_err().to_string();
1132        assert!(
1133            err_msg.contains("exited with code 1"),
1134            "Error message did not contain exit code: {err_msg}"
1135        );
1136    }
1137
1138    #[test]
1139    fn test_artifact_parsing() {
1140        let s = include_str!("../../evm/test-data/solc-obj.json");
1141        let artifact: ContractObject = serde_json::from_str(s).unwrap();
1142        assert!(artifact.bytecode.is_some());
1143
1144        let artifact: ContractObject = serde_json::from_str(s).unwrap();
1145        assert!(artifact.deployed_bytecode.is_some());
1146    }
1147
1148    #[test]
1149    fn test_alloy_json_abi_rejects_unlinked_bytecode() {
1150        let artifact_json = r#"{
1151            "abi": [],
1152            "bytecode": "0x73__$987e73aeca5e61ce83e4cb0814d87beda9$__63baf2f868"
1153        }"#;
1154
1155        let result: Result<ContractObject, _> = serde_json::from_str(artifact_json);
1156        assert!(result.is_err(), "should reject unlinked bytecode with placeholders");
1157        let err = result.unwrap_err().to_string();
1158        assert!(err.contains("expected bytecode, found unlinked bytecode with placeholder"));
1159    }
1160
1161    #[test]
1162    fn test_parse_artifact_path_file_only() {
1163        let parsed = super::parse_artifact_path("path/to/Contract.sol").unwrap();
1164        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1165        assert_eq!(parsed.contract_name, None);
1166        assert_eq!(parsed.version, None);
1167        assert_eq!(parsed.profile, None);
1168    }
1169
1170    #[test]
1171    fn test_parse_artifact_path_file_and_contract() {
1172        let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract").unwrap();
1173        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1174        assert_eq!(parsed.contract_name, Some("MyContract"));
1175        assert_eq!(parsed.version, None);
1176        assert_eq!(parsed.profile, None);
1177    }
1178
1179    #[test]
1180    fn test_parse_artifact_path_file_contract_version() {
1181        let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract:0.8.23").unwrap();
1182        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1183        assert_eq!(parsed.contract_name, Some("MyContract"));
1184        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1185        assert_eq!(parsed.profile, None);
1186    }
1187
1188    #[test]
1189    fn test_parse_artifact_path_file_contract_profile() {
1190        let parsed =
1191            super::parse_artifact_path("path/to/Contract.sol:MyContract:optimized").unwrap();
1192        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1193        assert_eq!(parsed.contract_name, Some("MyContract"));
1194        assert_eq!(parsed.version, None);
1195        assert_eq!(parsed.profile, Some("optimized"));
1196    }
1197
1198    #[test]
1199    fn test_parse_artifact_path_file_and_version() {
1200        let parsed = super::parse_artifact_path("path/to/Contract.sol:0.8.18").unwrap();
1201        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1202        assert_eq!(parsed.contract_name, None);
1203        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 18)));
1204        assert_eq!(parsed.profile, None);
1205    }
1206
1207    #[test]
1208    fn test_parse_artifact_path_file_and_profile() {
1209        // The parser keeps the two-part file form ambiguous. Artifact lookup can resolve this
1210        // segment as a profile when no contract name matches.
1211        let parsed = super::parse_artifact_path("Contract.sol:paris").unwrap();
1212        assert_eq!(parsed.file, Some(PathBuf::from("Contract.sol")));
1213        assert_eq!(parsed.contract_name, Some("paris"));
1214        assert_eq!(parsed.version, None);
1215        assert_eq!(parsed.profile, None);
1216    }
1217
1218    fn test_artifact(
1219        source: &str,
1220        name: &str,
1221        profile: &str,
1222        bytecode: Bytes,
1223    ) -> (ArtifactId, CompactContractBytecode) {
1224        (
1225            ArtifactId {
1226                path: PathBuf::from(format!("{source}/{name}.json")),
1227                name: name.to_owned(),
1228                source: PathBuf::from(source),
1229                version: Version::new(0, 8, 30),
1230                build_id: String::new(),
1231                profile: profile.to_owned(),
1232            },
1233            CompactContractBytecode {
1234                abi: Some(Default::default()),
1235                bytecode: Some(CompactBytecode {
1236                    object: BytecodeObject::Bytecode(bytecode),
1237                    source_map: None,
1238                    link_references: Default::default(),
1239                }),
1240                deployed_bytecode: None,
1241            },
1242        )
1243    }
1244
1245    #[test]
1246    fn test_get_artifact_code_resolves_file_profile_ambiguity() {
1247        let default_bytecode = Bytes::from_static(&[0x60, 0x01]);
1248        let paris_bytecode = Bytes::from_static(&[0x60, 0x02]);
1249        let source = "src/GetCodeProfile.t.sol";
1250        let artifacts = ContractsByArtifact::new([
1251            test_artifact(source, "GetCodeProfile", "default", default_bytecode),
1252            test_artifact(source, "GetCodeProfile", "paris", paris_bytecode.clone()),
1253        ]);
1254        let config = CheatsConfig {
1255            available_artifacts: Some(artifacts),
1256            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1257            ..Default::default()
1258        };
1259        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1260
1261        let bytecode =
1262            super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1263
1264        assert_eq!(bytecode, paris_bytecode);
1265    }
1266
1267    #[test]
1268    fn test_get_artifact_code_prefers_contract_name_over_file_profile_ambiguity() {
1269        let profile_bytecode = Bytes::from_static(&[0x60, 0x02]);
1270        let contract_bytecode = Bytes::from_static(&[0x60, 0x03]);
1271        let source = "src/GetCodeProfile.t.sol";
1272        let artifacts = ContractsByArtifact::new([
1273            test_artifact(source, "GetCodeProfile", "paris", profile_bytecode),
1274            test_artifact(source, "paris", "default", contract_bytecode.clone()),
1275        ]);
1276        let config = CheatsConfig {
1277            available_artifacts: Some(artifacts),
1278            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1279            ..Default::default()
1280        };
1281        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1282
1283        let bytecode =
1284            super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1285
1286        assert_eq!(bytecode, contract_bytecode);
1287    }
1288
1289    #[test]
1290    fn test_parse_artifact_path_contract_only() {
1291        let parsed = super::parse_artifact_path("MyContract").unwrap();
1292        assert_eq!(parsed.file, None);
1293        assert_eq!(parsed.contract_name, Some("MyContract"));
1294        assert_eq!(parsed.version, None);
1295        assert_eq!(parsed.profile, None);
1296    }
1297
1298    #[test]
1299    fn test_parse_artifact_path_contract_and_version() {
1300        let parsed = super::parse_artifact_path("MyContract:0.8.23").unwrap();
1301        assert_eq!(parsed.file, None);
1302        assert_eq!(parsed.contract_name, Some("MyContract"));
1303        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1304        assert_eq!(parsed.profile, None);
1305    }
1306
1307    #[test]
1308    fn test_parse_artifact_path_contract_and_profile() {
1309        let parsed = super::parse_artifact_path("MyContract:optimized").unwrap();
1310        assert_eq!(parsed.file, None);
1311        assert_eq!(parsed.contract_name, Some("MyContract"));
1312        assert_eq!(parsed.version, None);
1313        assert_eq!(parsed.profile, Some("optimized"));
1314    }
1315
1316    #[test]
1317    fn test_parse_artifact_path_profile_names() {
1318        // Test various profile name patterns
1319        for profile in ["v1", "v2", "paris", "optimized", "default", "prod", "dev"] {
1320            let path = format!("MyContract:{profile}");
1321            let parsed = super::parse_artifact_path(&path).unwrap();
1322            assert_eq!(parsed.contract_name, Some("MyContract"));
1323            assert_eq!(parsed.profile, Some(profile));
1324            assert_eq!(parsed.version, None);
1325        }
1326    }
1327
1328    #[test]
1329    fn test_parse_artifact_path_invalid_version() {
1330        // Invalid semver should be treated as profile
1331        let parsed = super::parse_artifact_path("MyContract:invalid").unwrap();
1332        assert_eq!(parsed.contract_name, Some("MyContract"));
1333        assert_eq!(parsed.profile, Some("invalid"));
1334        assert_eq!(parsed.version, None);
1335    }
1336
1337    fn unique_temp_dir(prefix: &str) -> PathBuf {
1338        env::temp_dir().join(format!(
1339            "foundry-cheatcodes-{prefix}-{}",
1340            SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()
1341        ))
1342    }
1343
1344    #[test]
1345    fn test_latest_broadcast_reads_tempo_sequences() {
1346        let root = unique_temp_dir("tempo-broadcast");
1347        let broadcast_path = root.join("broadcast");
1348        let sequence_dir = broadcast_path.join("Counter.s.sol").join("31337");
1349        stdfs::create_dir_all(&sequence_dir).unwrap();
1350
1351        let tx_hash = "0x04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f";
1352        let block_hash = "0x860f788b251ece768e63b0d3906d156f652d843848b71c7fe81faacd49139d66";
1353        let from = "0xa70ab0448e66cd77995bfbba5c5b64b41a85f3fd";
1354        let contract_address = "0x20c0000000000000000000000000000000000000";
1355        let zero_bloom = format!("0x{}", "0".repeat(512));
1356
1357        let sequence = serde_json::json!({
1358            "transactions": [{
1359                "hash": tx_hash,
1360                "transactionType": "CREATE",
1361                "contractName": "Counter",
1362                "contractAddress": contract_address,
1363                "function": serde_json::Value::Null,
1364                "arguments": serde_json::Value::Null,
1365                "transaction": {
1366                    "type": "0x76",
1367                    "from": from,
1368                    "to": serde_json::Value::Null,
1369                    "data": "0x",
1370                    "value": "0x0",
1371                    "gas": "0x5208",
1372                    "nonce": "0x0",
1373                    "accessList": [],
1374                    "calls": [],
1375                    "nonceKey": "0x0",
1376                    "feePayerSignature": serde_json::Value::Null,
1377                    "validBefore": serde_json::Value::Null,
1378                    "validAfter": serde_json::Value::Null,
1379                    "keyAuthorization": serde_json::Value::Null,
1380                    "aaAuthorizationList": []
1381                },
1382                "additionalContracts": [],
1383                "isFixedGasLimit": false
1384            }],
1385            "receipts": [{
1386                "type": "0x76",
1387                "status": "0x1",
1388                "cumulativeGasUsed": "0x5208",
1389                "logs": [],
1390                "logsBloom": zero_bloom,
1391                "transactionHash": tx_hash,
1392                "transactionIndex": "0x0",
1393                "blockHash": block_hash,
1394                "blockNumber": "0x7",
1395                "gasUsed": "0x5208",
1396                "effectiveGasPrice": "0x1",
1397                "from": from,
1398                "to": serde_json::Value::Null,
1399                "contractAddress": contract_address,
1400                "feePayer": from
1401            }],
1402            "libraries": [],
1403            "pending": [],
1404            "returns": {},
1405            "timestamp": 1,
1406            "chain": 31337,
1407            "commit": serde_json::Value::Null
1408        });
1409
1410        fs::write_json_file(&sequence_dir.join("run-1.json"), &sequence).unwrap();
1411
1412        let latest = latest_broadcast::<<TempoEvmNetwork as FoundryEvmNetwork>::Network>(
1413            &"Counter".to_owned(),
1414            31337,
1415            &broadcast_path,
1416            vec![CallKind::Create],
1417        )
1418        .unwrap();
1419
1420        assert_eq!(
1421            latest.txHash,
1422            b256!("04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f")
1423        );
1424        assert_eq!(latest.blockNumber, 7);
1425        assert!(matches!(latest.txType, BroadcastTxType::Create));
1426        assert_eq!(latest.contractAddress, address!("20c0000000000000000000000000000000000000"));
1427        assert!(latest.success);
1428
1429        stdfs::remove_dir_all(root).unwrap();
1430    }
1431}