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    let file = file.map(|file| {
576        let cwd = state
577            .config
578            .running_artifact
579            .as_ref()
580            .and_then(|artifact| artifact.source.parent())
581            .unwrap_or(&state.config.paths.root);
582        let relative_cwd = cwd.strip_prefix(&state.config.paths.root).unwrap_or(cwd);
583        let has_matching_remapping = state.config.paths.remappings.iter().any(|remapping| {
584            remapping.context.as_ref().is_none_or(|context| relative_cwd.starts_with(context))
585                && file.strip_prefix(&remapping.name).is_ok()
586        });
587
588        if has_matching_remapping {
589            state.config.paths.resolve_library_import(cwd, &file).map_or(file, |resolved| {
590                resolved.strip_prefix(&state.config.paths.root).unwrap_or(&resolved).to_path_buf()
591            })
592        } else {
593            file
594        }
595    });
596
597    // Use available artifacts list if present
598    if let Some(artifacts) = &state.config.available_artifacts {
599        let ambiguous_file_profile =
600            file.is_some() && version.is_none() && profile.is_none() && contract_name.is_some();
601        let filter_artifacts = |treat_ambiguous_as_profile: bool| -> Vec<_> {
602            artifacts
603                .iter()
604                .filter(|(id, _)| {
605                    // name might be in the form of "Counter.0.8.23"
606                    let id_name = id.name.split('.').next().unwrap();
607
608                    if let Some(path) = &file
609                        && !id.source.ends_with(path)
610                    {
611                        return false;
612                    }
613                    if let Some(ref version) = version
614                        && (id.version.minor != version.minor
615                            || id.version.major != version.major
616                            || id.version.patch != version.patch)
617                    {
618                        return false;
619                    }
620                    if let Some(profile) = profile
621                        && id.profile != profile
622                    {
623                        return false;
624                    }
625                    if let Some(name) = contract_name {
626                        if treat_ambiguous_as_profile && ambiguous_file_profile {
627                            return id.profile == name;
628                        }
629
630                        return id_name == name;
631                    }
632
633                    true
634                })
635                .collect()
636        };
637
638        let mut filtered = filter_artifacts(false);
639        if filtered.is_empty() && ambiguous_file_profile {
640            filtered = filter_artifacts(true);
641        }
642
643        let artifact = match &filtered[..] {
644            [] => None,
645            [artifact] => Some(Ok(*artifact)),
646            filtered => {
647                let mut filtered = filtered.to_vec();
648                // If we know the current script/test contract solc version, try to filter by it
649                Some(
650                    state
651                        .config
652                        .running_artifact
653                        .as_ref()
654                        .and_then(|running| {
655                            // Only filter by running version if user did NOT specify a version
656                            if version.is_none() {
657                                filtered.retain(|(id, _)| id.version == running.version);
658
659                                // Return artifact if only one matched
660                                if filtered.len() == 1 {
661                                    return Some(filtered[0]);
662                                }
663                            }
664
665                            // Only filter by running profile if user did NOT specify a profile
666                            if profile.is_none() {
667                                filtered.retain(|(id, _)| id.profile == running.profile);
668
669                                return (filtered.len() == 1).then(|| filtered[0]);
670                            }
671
672                            None
673                        })
674                        .ok_or_else(|| fmt_err!("multiple matching artifacts found")),
675                )
676            }
677        };
678
679        if let Some(artifact) = artifact {
680            return Ok(ArtifactSource::InMemory(artifact?.1));
681        }
682    }
683
684    // Fallback: construct path manually when no artifacts list or no match found
685    let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name) {
686        (Some(file), Some(contract_name)) => PathBuf::from(format!("{file}/{contract_name}.json")),
687        (None, Some(contract_name)) => {
688            PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
689        }
690        (Some(file), None) => {
691            let name = file.replace(".sol", "");
692            PathBuf::from(format!("{file}/{name}.json"))
693        }
694        _ => bail!("invalid artifact path"),
695    };
696
697    let path = state.config.paths.artifacts.join(path_in_artifacts);
698    let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
699    Ok(ArtifactSource::Disk(path))
700}
701
702/// Reads an artifact JSON file, mapping I/O errors to a helpful message when the
703/// lookup fell through the in-memory artifacts list.
704fn read_artifact_file<FEN: FoundryEvmNetwork>(
705    state: &Cheatcodes<FEN>,
706    path: &Path,
707) -> Result<String> {
708    fs::read_to_string(path).map_err(|e| {
709        if state.config.available_artifacts.is_some() {
710            fmt_err!("no matching artifact found")
711        } else {
712            e.into()
713        }
714    })
715}
716
717/// Returns the bytecode from a JSON artifact file.
718///
719/// See [`get_artifact_source`] for the supported path formats.
720///
721/// This function is safe to use with contracts that have library dependencies.
722/// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will
723/// reject artifacts with unlinked library placeholders.
724fn get_artifact_code<FEN: FoundryEvmNetwork>(
725    state: &Cheatcodes<FEN>,
726    path: &str,
727    deployed: bool,
728) -> Result<Bytes> {
729    let maybe_bytecode = match get_artifact_source(state, path)? {
730        ArtifactSource::InMemory(data) => {
731            if deployed { data.deployed_bytecode() } else { data.bytecode() }.cloned()
732        }
733        ArtifactSource::Disk(path) => {
734            let data = read_artifact_file(state, &path)?;
735            let artifact = serde_json::from_str::<ContractObject>(&data)?;
736            if deployed { artifact.deployed_bytecode } else { artifact.bytecode }
737        }
738    };
739    maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
740}
741
742impl Cheatcode for ffiCall {
743    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
744        let Self { commandInput: input } = self;
745
746        let output = ffi(state, input)?;
747
748        // Check the exit code of the command.
749        if output.exitCode != 0 {
750            // If the command failed, return an error with the exit code and stderr.
751            return Err(fmt_err!(
752                "ffi command {:?} exited with code {}. stderr: {}",
753                input,
754                output.exitCode,
755                String::from_utf8_lossy(&output.stderr)
756            ));
757        }
758
759        // If the command succeeded but still wrote to stderr, log it as a warning.
760        if !output.stderr.is_empty() {
761            let stderr = String::from_utf8_lossy(&output.stderr);
762            warn!(target: "cheatcodes", ?input, ?stderr, "ffi command wrote to stderr");
763        }
764
765        // We already hex-decoded the stdout in the `ffi` helper function.
766        Ok(output.stdout.abi_encode())
767    }
768}
769
770impl Cheatcode for tryFfiCall {
771    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
772        let Self { commandInput: input } = self;
773        ffi(state, input).map(|res| res.abi_encode())
774    }
775}
776
777impl Cheatcode for promptCall {
778    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
779        let Self { promptText: text } = self;
780        prompt(state, text, prompt_input).map(|res| res.abi_encode())
781    }
782}
783
784impl Cheatcode for promptSecretCall {
785    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
786        let Self { promptText: text } = self;
787        prompt(state, text, prompt_password).map(|res| res.abi_encode())
788    }
789}
790
791impl Cheatcode for promptSecretUintCall {
792    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
793        let Self { promptText: text } = self;
794        parse(&prompt(state, text, prompt_password)?, &DynSolType::Uint(256))
795    }
796}
797
798impl Cheatcode for promptAddressCall {
799    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
800        let Self { promptText: text } = self;
801        parse(&prompt(state, text, prompt_input)?, &DynSolType::Address)
802    }
803}
804
805impl Cheatcode for promptUintCall {
806    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
807        let Self { promptText: text } = self;
808        parse(&prompt(state, text, prompt_input)?, &DynSolType::Uint(256))
809    }
810}
811
812pub(super) fn write_file<FEN: FoundryEvmNetwork>(
813    state: &Cheatcodes<FEN>,
814    path: &Path,
815    contents: &[u8],
816) -> Result {
817    let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
818    // write access to foundry.toml is not allowed
819    state.config.ensure_not_foundry_toml(&path)?;
820
821    if state.fs_commit {
822        fs::locked_write(path, contents)?;
823    }
824
825    Ok(Default::default())
826}
827
828fn read_dir<FEN: FoundryEvmNetwork>(
829    state: &Cheatcodes<FEN>,
830    path: &Path,
831    max_depth: u64,
832    follow_links: bool,
833) -> Result {
834    let root = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
835    let paths: Vec<DirEntry> = WalkDir::new(root)
836        .min_depth(1)
837        .max_depth(max_depth.try_into().unwrap_or(usize::MAX))
838        .follow_links(follow_links)
839        .contents_first(false)
840        .same_file_system(true)
841        .sort_by_file_name()
842        .into_iter()
843        .map(|entry| match entry {
844            Ok(entry) => DirEntry {
845                errorMessage: String::new(),
846                path: entry.path().display().to_string(),
847                depth: entry.depth() as u64,
848                isDir: entry.file_type().is_dir(),
849                isSymlink: entry.path_is_symlink(),
850            },
851            Err(e) => DirEntry {
852                errorMessage: e.to_string(),
853                path: e.path().map(|p| p.display().to_string()).unwrap_or_default(),
854                depth: e.depth() as u64,
855                isDir: false,
856                isSymlink: false,
857            },
858        })
859        .collect();
860    Ok(paths.abi_encode())
861}
862
863fn ffi<FEN: FoundryEvmNetwork>(state: &Cheatcodes<FEN>, input: &[String]) -> Result<FfiResult> {
864    ensure!(
865        state.config.ffi,
866        "FFI is disabled; add the `--ffi` flag to allow tests to call external commands"
867    );
868    ensure!(!input.is_empty() && !input[0].is_empty(), "can't execute empty command");
869    let mut cmd = Command::new(&input[0]);
870    cmd.args(&input[1..]);
871
872    debug!(target: "cheatcodes", ?cmd, "invoking ffi");
873
874    let output = cmd
875        .current_dir(&state.config.root)
876        .output()
877        .map_err(|err| fmt_err!("failed to execute command {cmd:?}: {err}"))?;
878
879    // The stdout might be encoded on valid hex, or it might just be a string,
880    // so we need to determine which it is to avoid improperly encoding later.
881    let trimmed_stdout = String::from_utf8(output.stdout)?;
882    let trimmed_stdout = trimmed_stdout.trim();
883    let encoded_stdout = if let Ok(hex) = hex::decode(trimmed_stdout) {
884        hex
885    } else {
886        trimmed_stdout.as_bytes().to_vec()
887    };
888    Ok(FfiResult {
889        exitCode: output.status.code().unwrap_or(69),
890        stdout: encoded_stdout.into(),
891        stderr: output.stderr.into(),
892    })
893}
894
895fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> {
896    Input::new().allow_empty(true).with_prompt(prompt_text).interact_text()
897}
898
899fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> {
900    Password::new().with_prompt(prompt_text).interact()
901}
902
903fn prompt<FEN: FoundryEvmNetwork>(
904    state: &Cheatcodes<FEN>,
905    prompt_text: &str,
906    input: fn(&str) -> Result<String, dialoguer::Error>,
907) -> Result<String> {
908    let text_clone = prompt_text.to_string();
909    let timeout = state.config.prompt_timeout;
910    let (tx, rx) = mpsc::channel();
911
912    thread::spawn(move || {
913        let _ = tx.send(input(&text_clone));
914    });
915
916    match rx.recv_timeout(timeout) {
917        Ok(res) => res.map_err(|err| {
918            let _ = sh_println!();
919            err.to_string().into()
920        }),
921        Err(_) => {
922            let _ = sh_eprintln!();
923            Err("Prompt timed out".into())
924        }
925    }
926}
927
928impl Cheatcode for getBroadcastCall {
929    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
930        let Self { contractName, chainId, txType } = self;
931
932        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
933            contractName,
934            *chainId,
935            &state.config.broadcast,
936            vec![map_broadcast_tx_type(*txType)],
937        )?;
938
939        Ok(latest_broadcast.abi_encode())
940    }
941}
942
943impl Cheatcode for getBroadcasts_0Call {
944    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
945        let Self { contractName, chainId, txType } = self;
946
947        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
948            .with_tx_type(map_broadcast_tx_type(*txType));
949
950        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
951
952        let summaries = broadcasts
953            .into_iter()
954            .flat_map(|broadcast| {
955                let results = reader.into_tx_receipts(broadcast);
956                parse_broadcast_results(results)
957            })
958            .collect::<Vec<_>>();
959
960        Ok(summaries.abi_encode())
961    }
962}
963
964impl Cheatcode for getBroadcasts_1Call {
965    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
966        let Self { contractName, chainId } = self;
967
968        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?;
969
970        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
971
972        let summaries = broadcasts
973            .into_iter()
974            .flat_map(|broadcast| {
975                let results = reader.into_tx_receipts(broadcast);
976                parse_broadcast_results(results)
977            })
978            .collect::<Vec<_>>();
979
980        Ok(summaries.abi_encode())
981    }
982}
983
984impl Cheatcode for getDeployment_0Call {
985    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
986        let Self { contractName } = self;
987        let chain_id = ccx.ecx.cfg().chain_id();
988
989        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
990            contractName,
991            chain_id,
992            &ccx.state.config.broadcast,
993            vec![CallKind::Create, CallKind::Create2],
994        )?;
995
996        Ok(latest_broadcast.contractAddress.abi_encode())
997    }
998}
999
1000impl Cheatcode for getDeployment_1Call {
1001    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1002        let Self { contractName, chainId } = self;
1003
1004        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
1005            contractName,
1006            *chainId,
1007            &state.config.broadcast,
1008            vec![CallKind::Create, CallKind::Create2],
1009        )?;
1010
1011        Ok(latest_broadcast.contractAddress.abi_encode())
1012    }
1013}
1014
1015impl Cheatcode for getDeploymentsCall {
1016    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1017        let Self { contractName, chainId } = self;
1018
1019        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
1020            .with_tx_type(CallKind::Create)
1021            .with_tx_type(CallKind::Create2);
1022
1023        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
1024
1025        let summaries = broadcasts
1026            .into_iter()
1027            .flat_map(|broadcast| {
1028                let results = reader.into_tx_receipts(broadcast);
1029                parse_broadcast_results(results)
1030            })
1031            .collect::<Vec<_>>();
1032
1033        let deployed_addresses =
1034            summaries.into_iter().map(|summary| summary.contractAddress).collect::<Vec<_>>();
1035
1036        Ok(deployed_addresses.abi_encode())
1037    }
1038}
1039
1040fn map_broadcast_tx_type(tx_type: BroadcastTxType) -> CallKind {
1041    match tx_type {
1042        BroadcastTxType::Call => CallKind::Call,
1043        BroadcastTxType::Create => CallKind::Create,
1044        BroadcastTxType::Create2 => CallKind::Create2,
1045        _ => unreachable!("invalid tx type"),
1046    }
1047}
1048
1049fn parse_broadcast_results<N: Network>(
1050    results: Vec<(TransactionWithMetadata<N>, N::ReceiptResponse)>,
1051) -> Vec<BroadcastTxSummary> {
1052    results
1053        .into_iter()
1054        .map(|(tx, receipt)| BroadcastTxSummary {
1055            txHash: receipt.transaction_hash(),
1056            blockNumber: receipt.block_number().unwrap_or_default(),
1057            txType: match tx.call_kind {
1058                CallKind::Call => BroadcastTxType::Call,
1059                CallKind::Create => BroadcastTxType::Create,
1060                CallKind::Create2 => BroadcastTxType::Create2,
1061                _ => unreachable!("invalid tx type"),
1062            },
1063            contractAddress: tx.contract_address.unwrap_or_default(),
1064            success: receipt.status(),
1065        })
1066        .collect()
1067}
1068
1069fn latest_broadcast<N: Network>(
1070    contract_name: &String,
1071    chain_id: u64,
1072    broadcast_path: &Path,
1073    filters: Vec<CallKind>,
1074) -> Result<BroadcastTxSummary>
1075where
1076    N::TxEnvelope: for<'d> serde::Deserialize<'d>,
1077{
1078    let mut reader = BroadcastReader::new(contract_name.clone(), chain_id, broadcast_path)?;
1079
1080    for filter in filters {
1081        reader = reader.with_tx_type(filter);
1082    }
1083
1084    let broadcast = reader.read_latest::<N>()?;
1085
1086    let results = reader.into_tx_receipts(broadcast);
1087
1088    let summaries = parse_broadcast_results(results);
1089
1090    summaries
1091        .first()
1092        .ok_or_else(|| fmt_err!("no deployment found for {contract_name} on chain {chain_id}"))
1093        .cloned()
1094}
1095
1096#[cfg(test)]
1097mod tests {
1098    use super::*;
1099    use crate::CheatsConfig;
1100    use alloy_primitives::{address, b256};
1101    use foundry_common::ContractsByArtifact;
1102    use foundry_compilers::{
1103        ArtifactId,
1104        artifacts::{
1105            BytecodeObject, CompactBytecode, CompactContractBytecode, remappings::Remapping,
1106        },
1107    };
1108    use foundry_evm_core::evm::TempoEvmNetwork;
1109    use std::{env, fs as stdfs, str::FromStr, sync::Arc};
1110    use tempfile::TempDir;
1111
1112    fn cheats() -> Cheatcodes {
1113        let config = CheatsConfig {
1114            ffi: true,
1115            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1116            ..Default::default()
1117        };
1118        Cheatcodes::new(Arc::new(config))
1119    }
1120
1121    #[test]
1122    fn test_ffi_hex() {
1123        let msg = b"gm";
1124        let cheats = cheats();
1125        let args = ["echo".to_string(), hex::encode(msg)];
1126        let output = ffi(&cheats, &args).unwrap();
1127        assert_eq!(output.stdout, Bytes::from(msg));
1128    }
1129
1130    #[test]
1131    fn test_ffi_string() {
1132        let msg = "gm";
1133        let cheats = cheats();
1134        let args = ["echo".to_string(), msg.to_string()];
1135        let output = ffi(&cheats, &args).unwrap();
1136        assert_eq!(output.stdout, Bytes::from(msg.as_bytes()));
1137    }
1138
1139    #[test]
1140    fn test_ffi_fails_on_error_code() {
1141        let mut cheats = cheats();
1142
1143        // Use a command that is guaranteed to fail with a non-zero exit code on any platform.
1144        #[cfg(unix)]
1145        let args = vec!["false".to_string()];
1146        #[cfg(windows)]
1147        let args = vec!["cmd".to_string(), "/c".to_string(), "exit 1".to_string()];
1148
1149        let result = Cheatcode::apply(&ffiCall { commandInput: args }, &mut cheats);
1150
1151        // Assert that the cheatcode returned an error.
1152        assert!(result.is_err(), "Expected ffi cheatcode to fail, but it succeeded");
1153
1154        // Assert that the error message contains the expected information.
1155        let err_msg = result.unwrap_err().to_string();
1156        assert!(
1157            err_msg.contains("exited with code 1"),
1158            "Error message did not contain exit code: {err_msg}"
1159        );
1160    }
1161
1162    #[test]
1163    fn test_artifact_parsing() {
1164        let s = include_str!("../../evm/test-data/solc-obj.json");
1165        let artifact: ContractObject = serde_json::from_str(s).unwrap();
1166        assert!(artifact.bytecode.is_some());
1167
1168        let artifact: ContractObject = serde_json::from_str(s).unwrap();
1169        assert!(artifact.deployed_bytecode.is_some());
1170    }
1171
1172    #[test]
1173    fn test_alloy_json_abi_rejects_unlinked_bytecode() {
1174        let artifact_json = r#"{
1175            "abi": [],
1176            "bytecode": "0x73__$987e73aeca5e61ce83e4cb0814d87beda9$__63baf2f868"
1177        }"#;
1178
1179        let result: Result<ContractObject, _> = serde_json::from_str(artifact_json);
1180        assert!(result.is_err(), "should reject unlinked bytecode with placeholders");
1181        let err = result.unwrap_err().to_string();
1182        assert!(err.contains("expected bytecode, found unlinked bytecode with placeholder"));
1183    }
1184
1185    #[test]
1186    fn test_parse_artifact_path_file_only() {
1187        let parsed = super::parse_artifact_path("path/to/Contract.sol").unwrap();
1188        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1189        assert_eq!(parsed.contract_name, None);
1190        assert_eq!(parsed.version, None);
1191        assert_eq!(parsed.profile, None);
1192    }
1193
1194    #[test]
1195    fn test_parse_artifact_path_file_and_contract() {
1196        let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract").unwrap();
1197        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1198        assert_eq!(parsed.contract_name, Some("MyContract"));
1199        assert_eq!(parsed.version, None);
1200        assert_eq!(parsed.profile, None);
1201    }
1202
1203    #[test]
1204    fn test_parse_artifact_path_file_contract_version() {
1205        let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract:0.8.23").unwrap();
1206        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1207        assert_eq!(parsed.contract_name, Some("MyContract"));
1208        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1209        assert_eq!(parsed.profile, None);
1210    }
1211
1212    #[test]
1213    fn test_parse_artifact_path_file_contract_profile() {
1214        let parsed =
1215            super::parse_artifact_path("path/to/Contract.sol:MyContract:optimized").unwrap();
1216        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1217        assert_eq!(parsed.contract_name, Some("MyContract"));
1218        assert_eq!(parsed.version, None);
1219        assert_eq!(parsed.profile, Some("optimized"));
1220    }
1221
1222    #[test]
1223    fn test_parse_artifact_path_file_and_version() {
1224        let parsed = super::parse_artifact_path("path/to/Contract.sol:0.8.18").unwrap();
1225        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1226        assert_eq!(parsed.contract_name, None);
1227        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 18)));
1228        assert_eq!(parsed.profile, None);
1229    }
1230
1231    #[test]
1232    fn test_parse_artifact_path_file_and_profile() {
1233        // The parser keeps the two-part file form ambiguous. Artifact lookup can resolve this
1234        // segment as a profile when no contract name matches.
1235        let parsed = super::parse_artifact_path("Contract.sol:paris").unwrap();
1236        assert_eq!(parsed.file, Some(PathBuf::from("Contract.sol")));
1237        assert_eq!(parsed.contract_name, Some("paris"));
1238        assert_eq!(parsed.version, None);
1239        assert_eq!(parsed.profile, None);
1240    }
1241
1242    fn test_artifact(
1243        source: &str,
1244        name: &str,
1245        profile: &str,
1246        bytecode: Bytes,
1247    ) -> (ArtifactId, CompactContractBytecode) {
1248        (
1249            ArtifactId {
1250                path: PathBuf::from(format!("{source}/{name}.json")),
1251                name: name.to_owned(),
1252                source: PathBuf::from(source),
1253                version: Version::new(0, 8, 30),
1254                build_id: String::new(),
1255                profile: profile.to_owned(),
1256            },
1257            CompactContractBytecode {
1258                abi: Some(Default::default()),
1259                bytecode: Some(CompactBytecode {
1260                    object: BytecodeObject::Bytecode(bytecode),
1261                    source_map: None,
1262                    link_references: Default::default(),
1263                }),
1264                deployed_bytecode: None,
1265            },
1266        )
1267    }
1268
1269    #[test]
1270    fn test_get_artifact_code_resolves_file_profile_ambiguity() {
1271        let default_bytecode = Bytes::from_static(&[0x60, 0x01]);
1272        let paris_bytecode = Bytes::from_static(&[0x60, 0x02]);
1273        let source = "src/GetCodeProfile.t.sol";
1274        let artifacts = ContractsByArtifact::new([
1275            test_artifact(source, "GetCodeProfile", "default", default_bytecode),
1276            test_artifact(source, "GetCodeProfile", "paris", paris_bytecode.clone()),
1277        ]);
1278        let config = CheatsConfig {
1279            available_artifacts: Some(artifacts),
1280            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1281            ..Default::default()
1282        };
1283        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1284
1285        let bytecode =
1286            super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1287
1288        assert_eq!(bytecode, paris_bytecode);
1289    }
1290
1291    #[test]
1292    fn test_get_artifact_code_prefers_contract_name_over_file_profile_ambiguity() {
1293        let profile_bytecode = Bytes::from_static(&[0x60, 0x02]);
1294        let contract_bytecode = Bytes::from_static(&[0x60, 0x03]);
1295        let source = "src/GetCodeProfile.t.sol";
1296        let artifacts = ContractsByArtifact::new([
1297            test_artifact(source, "GetCodeProfile", "paris", profile_bytecode),
1298            test_artifact(source, "paris", "default", contract_bytecode.clone()),
1299        ]);
1300        let config = CheatsConfig {
1301            available_artifacts: Some(artifacts),
1302            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1303            ..Default::default()
1304        };
1305        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1306
1307        let bytecode =
1308            super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1309
1310        assert_eq!(bytecode, contract_bytecode);
1311    }
1312
1313    #[test]
1314    fn test_get_artifact_code_resolves_remapping() {
1315        let bytecode = Bytes::from_static(&[0x60, 0x01]);
1316        let artifacts = ContractsByArtifact::new([test_artifact(
1317            "src/Something.sol",
1318            "Something",
1319            "default",
1320            bytecode.clone(),
1321        )]);
1322        let root = PathBuf::from(&env!("CARGO_MANIFEST_DIR"));
1323        let paths = foundry_compilers::ProjectPathsConfig::builder()
1324            .remapping(Remapping::from_str("@example/=src/").unwrap())
1325            .build_with_root(&root);
1326        let config = CheatsConfig {
1327            available_artifacts: Some(artifacts),
1328            root,
1329            paths,
1330            ..Default::default()
1331        };
1332        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1333
1334        let resolved =
1335            super::get_artifact_code(&cheats, "@example/Something.sol:Something", false).unwrap();
1336
1337        assert_eq!(resolved, bytecode);
1338    }
1339
1340    #[test]
1341    fn test_get_artifact_code_preserves_project_path_on_library_collision() {
1342        let root_bytecode = Bytes::from_static(&[0x60, 0x01]);
1343        let library_bytecode = Bytes::from_static(&[0x60, 0x02]);
1344        let artifacts = ContractsByArtifact::new([
1345            test_artifact("src/Thing.sol", "RootThing", "default", root_bytecode.clone()),
1346            test_artifact("lib/src/Thing.sol", "LibThing", "default", library_bytecode),
1347        ]);
1348        let temp = TempDir::new().unwrap();
1349        let library = temp.path().join("lib");
1350        stdfs::create_dir_all(library.join("src")).unwrap();
1351        stdfs::write(library.join("src/Thing.sol"), "").unwrap();
1352        let paths = foundry_compilers::ProjectPathsConfig::builder()
1353            .remappings([])
1354            .lib(library)
1355            .build_with_root(temp.path());
1356        let config = CheatsConfig {
1357            available_artifacts: Some(artifacts),
1358            root: temp.path().to_path_buf(),
1359            paths,
1360            ..Default::default()
1361        };
1362        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1363
1364        let resolved = super::get_artifact_code(&cheats, "src/Thing.sol:RootThing", false).unwrap();
1365
1366        assert_eq!(resolved, root_bytecode);
1367    }
1368
1369    #[test]
1370    fn test_parse_artifact_path_contract_only() {
1371        let parsed = super::parse_artifact_path("MyContract").unwrap();
1372        assert_eq!(parsed.file, None);
1373        assert_eq!(parsed.contract_name, Some("MyContract"));
1374        assert_eq!(parsed.version, None);
1375        assert_eq!(parsed.profile, None);
1376    }
1377
1378    #[test]
1379    fn test_parse_artifact_path_contract_and_version() {
1380        let parsed = super::parse_artifact_path("MyContract:0.8.23").unwrap();
1381        assert_eq!(parsed.file, None);
1382        assert_eq!(parsed.contract_name, Some("MyContract"));
1383        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1384        assert_eq!(parsed.profile, None);
1385    }
1386
1387    #[test]
1388    fn test_parse_artifact_path_contract_and_profile() {
1389        let parsed = super::parse_artifact_path("MyContract:optimized").unwrap();
1390        assert_eq!(parsed.file, None);
1391        assert_eq!(parsed.contract_name, Some("MyContract"));
1392        assert_eq!(parsed.version, None);
1393        assert_eq!(parsed.profile, Some("optimized"));
1394    }
1395
1396    #[test]
1397    fn test_parse_artifact_path_profile_names() {
1398        // Test various profile name patterns
1399        for profile in ["v1", "v2", "paris", "optimized", "default", "prod", "dev"] {
1400            let path = format!("MyContract:{profile}");
1401            let parsed = super::parse_artifact_path(&path).unwrap();
1402            assert_eq!(parsed.contract_name, Some("MyContract"));
1403            assert_eq!(parsed.profile, Some(profile));
1404            assert_eq!(parsed.version, None);
1405        }
1406    }
1407
1408    #[test]
1409    fn test_parse_artifact_path_invalid_version() {
1410        // Invalid semver should be treated as profile
1411        let parsed = super::parse_artifact_path("MyContract:invalid").unwrap();
1412        assert_eq!(parsed.contract_name, Some("MyContract"));
1413        assert_eq!(parsed.profile, Some("invalid"));
1414        assert_eq!(parsed.version, None);
1415    }
1416
1417    fn unique_temp_dir(prefix: &str) -> PathBuf {
1418        env::temp_dir().join(format!(
1419            "foundry-cheatcodes-{prefix}-{}",
1420            SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()
1421        ))
1422    }
1423
1424    #[test]
1425    fn test_latest_broadcast_reads_tempo_sequences() {
1426        let root = unique_temp_dir("tempo-broadcast");
1427        let broadcast_path = root.join("broadcast");
1428        let sequence_dir = broadcast_path.join("Counter.s.sol").join("31337");
1429        stdfs::create_dir_all(&sequence_dir).unwrap();
1430
1431        let tx_hash = "0x04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f";
1432        let block_hash = "0x860f788b251ece768e63b0d3906d156f652d843848b71c7fe81faacd49139d66";
1433        let from = "0xa70ab0448e66cd77995bfbba5c5b64b41a85f3fd";
1434        let contract_address = "0x20c0000000000000000000000000000000000000";
1435        let zero_bloom = format!("0x{}", "0".repeat(512));
1436
1437        let sequence = serde_json::json!({
1438            "transactions": [{
1439                "hash": tx_hash,
1440                "transactionType": "CREATE",
1441                "contractName": "Counter",
1442                "contractAddress": contract_address,
1443                "function": serde_json::Value::Null,
1444                "arguments": serde_json::Value::Null,
1445                "transaction": {
1446                    "type": "0x76",
1447                    "from": from,
1448                    "to": serde_json::Value::Null,
1449                    "data": "0x",
1450                    "value": "0x0",
1451                    "gas": "0x5208",
1452                    "nonce": "0x0",
1453                    "accessList": [],
1454                    "calls": [],
1455                    "nonceKey": "0x0",
1456                    "feePayerSignature": serde_json::Value::Null,
1457                    "validBefore": serde_json::Value::Null,
1458                    "validAfter": serde_json::Value::Null,
1459                    "keyAuthorization": serde_json::Value::Null,
1460                    "aaAuthorizationList": []
1461                },
1462                "additionalContracts": [],
1463                "isFixedGasLimit": false
1464            }],
1465            "receipts": [{
1466                "type": "0x76",
1467                "status": "0x1",
1468                "cumulativeGasUsed": "0x5208",
1469                "logs": [],
1470                "logsBloom": zero_bloom,
1471                "transactionHash": tx_hash,
1472                "transactionIndex": "0x0",
1473                "blockHash": block_hash,
1474                "blockNumber": "0x7",
1475                "gasUsed": "0x5208",
1476                "effectiveGasPrice": "0x1",
1477                "from": from,
1478                "to": serde_json::Value::Null,
1479                "contractAddress": contract_address,
1480                "feePayer": from
1481            }],
1482            "libraries": [],
1483            "pending": [],
1484            "returns": {},
1485            "timestamp": 1,
1486            "chain": 31337,
1487            "commit": serde_json::Value::Null
1488        });
1489
1490        fs::write_json_file(&sequence_dir.join("run-1.json"), &sequence).unwrap();
1491
1492        let latest = latest_broadcast::<<TempoEvmNetwork as FoundryEvmNetwork>::Network>(
1493            &"Counter".to_owned(),
1494            31337,
1495            &broadcast_path,
1496            vec![CallKind::Create],
1497        )
1498        .unwrap();
1499
1500        assert_eq!(
1501            latest.txHash,
1502            b256!("04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f")
1503        );
1504        assert_eq!(latest.blockNumber, 7);
1505        assert!(matches!(latest.txType, BroadcastTxType::Create));
1506        assert_eq!(latest.contractAddress, address!("20c0000000000000000000000000000000000000"));
1507        assert!(latest.success);
1508
1509        stdfs::remove_dir_all(root).unwrap();
1510    }
1511}