foundry_cheatcodes/
fs.rs

1//! Implementations of [`Filesystem`](spec::Group::Filesystem) cheatcodes.
2
3use super::string::parse;
4use crate::{Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*};
5use alloy_dyn_abi::DynSolType;
6use alloy_json_abi::ContractObject;
7use alloy_network::AnyTransactionReceipt;
8use alloy_primitives::{Bytes, U256, hex, map::Entry};
9use alloy_provider::network::ReceiptResponse;
10use alloy_sol_types::SolValue;
11use dialoguer::{Input, Password};
12use forge_script_sequence::{BroadcastReader, TransactionWithMetadata};
13use foundry_common::fs;
14use foundry_config::fs_permissions::FsAccessKind;
15use revm::{
16    context::{CreateScheme, JournalTr},
17    interpreter::CreateInputs,
18};
19use revm_inspectors::tracing::types::CallKind;
20use semver::Version;
21use std::{
22    io::{BufRead, BufReader},
23    path::{Path, PathBuf},
24    process::Command,
25    sync::mpsc,
26    thread,
27    time::{SystemTime, UNIX_EPOCH},
28};
29use walkdir::WalkDir;
30
31impl Cheatcode for existsCall {
32    fn apply(&self, state: &mut Cheatcodes) -> Result {
33        let Self { path } = self;
34        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
35        Ok(path.exists().abi_encode())
36    }
37}
38
39impl Cheatcode for fsMetadataCall {
40    fn apply(&self, state: &mut Cheatcodes) -> Result {
41        let Self { path } = self;
42        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
43
44        let metadata = path.metadata()?;
45
46        // These fields not available on all platforms; default to 0
47        let [modified, accessed, created] =
48            [metadata.modified(), metadata.accessed(), metadata.created()].map(|time| {
49                time.unwrap_or(UNIX_EPOCH).duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
50            });
51
52        Ok(FsMetadata {
53            isDir: metadata.is_dir(),
54            isSymlink: metadata.is_symlink(),
55            length: U256::from(metadata.len()),
56            readOnly: metadata.permissions().readonly(),
57            modified: U256::from(modified),
58            accessed: U256::from(accessed),
59            created: U256::from(created),
60        }
61        .abi_encode())
62    }
63}
64
65impl Cheatcode for isDirCall {
66    fn apply(&self, state: &mut Cheatcodes) -> Result {
67        let Self { path } = self;
68        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
69        Ok(path.is_dir().abi_encode())
70    }
71}
72
73impl Cheatcode for isFileCall {
74    fn apply(&self, state: &mut Cheatcodes) -> Result {
75        let Self { path } = self;
76        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
77        Ok(path.is_file().abi_encode())
78    }
79}
80
81impl Cheatcode for projectRootCall {
82    fn apply(&self, state: &mut Cheatcodes) -> Result {
83        let Self {} = self;
84        Ok(state.config.root.display().to_string().abi_encode())
85    }
86}
87
88impl Cheatcode for unixTimeCall {
89    fn apply(&self, _state: &mut Cheatcodes) -> Result {
90        let Self {} = self;
91        let difference = SystemTime::now()
92            .duration_since(UNIX_EPOCH)
93            .map_err(|e| fmt_err!("failed getting Unix timestamp: {e}"))?;
94        Ok(difference.as_millis().abi_encode())
95    }
96}
97
98impl Cheatcode for closeFileCall {
99    fn apply(&self, state: &mut Cheatcodes) -> Result {
100        let Self { path } = self;
101        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
102
103        state.test_context.opened_read_files.remove(&path);
104
105        Ok(Default::default())
106    }
107}
108
109impl Cheatcode for copyFileCall {
110    fn apply(&self, state: &mut Cheatcodes) -> Result {
111        let Self { from, to } = self;
112        let from = state.config.ensure_path_allowed(from, FsAccessKind::Read)?;
113        let to = state.config.ensure_path_allowed(to, FsAccessKind::Write)?;
114        state.config.ensure_not_foundry_toml(&to)?;
115
116        let n = fs::copy(from, to)?;
117        Ok(n.abi_encode())
118    }
119}
120
121impl Cheatcode for createDirCall {
122    fn apply(&self, state: &mut Cheatcodes) -> Result {
123        let Self { path, recursive } = self;
124        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
125        if *recursive { fs::create_dir_all(path) } else { fs::create_dir(path) }?;
126        Ok(Default::default())
127    }
128}
129
130impl Cheatcode for readDir_0Call {
131    fn apply(&self, state: &mut Cheatcodes) -> Result {
132        let Self { path } = self;
133        read_dir(state, path.as_ref(), 1, false)
134    }
135}
136
137impl Cheatcode for readDir_1Call {
138    fn apply(&self, state: &mut Cheatcodes) -> Result {
139        let Self { path, maxDepth } = self;
140        read_dir(state, path.as_ref(), *maxDepth, false)
141    }
142}
143
144impl Cheatcode for readDir_2Call {
145    fn apply(&self, state: &mut Cheatcodes) -> Result {
146        let Self { path, maxDepth, followLinks } = self;
147        read_dir(state, path.as_ref(), *maxDepth, *followLinks)
148    }
149}
150
151impl Cheatcode for readFileCall {
152    fn apply(&self, state: &mut Cheatcodes) -> Result {
153        let Self { path } = self;
154        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
155        Ok(fs::locked_read_to_string(path)?.abi_encode())
156    }
157}
158
159impl Cheatcode for readFileBinaryCall {
160    fn apply(&self, state: &mut Cheatcodes) -> Result {
161        let Self { path } = self;
162        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
163        Ok(fs::locked_read(path)?.abi_encode())
164    }
165}
166
167impl Cheatcode for readLineCall {
168    fn apply(&self, state: &mut Cheatcodes) -> Result {
169        let Self { path } = self;
170        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
171
172        // Get reader for previously opened file to continue reading OR initialize new reader
173        let reader = match state.test_context.opened_read_files.entry(path.clone()) {
174            Entry::Occupied(entry) => entry.into_mut(),
175            Entry::Vacant(entry) => entry.insert(BufReader::new(fs::open(path)?)),
176        };
177
178        let mut line: String = String::new();
179        reader.read_line(&mut line)?;
180
181        // Remove trailing newline character, preserving others for cases where it may be important
182        if line.ends_with('\n') {
183            line.pop();
184            if line.ends_with('\r') {
185                line.pop();
186            }
187        }
188
189        Ok(line.abi_encode())
190    }
191}
192
193impl Cheatcode for readLinkCall {
194    fn apply(&self, state: &mut Cheatcodes) -> Result {
195        let Self { linkPath: path } = self;
196        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
197        let target = fs::read_link(path)?;
198        Ok(target.display().to_string().abi_encode())
199    }
200}
201
202impl Cheatcode for removeDirCall {
203    fn apply(&self, state: &mut Cheatcodes) -> Result {
204        let Self { path, recursive } = self;
205        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
206        if *recursive { fs::remove_dir_all(path) } else { fs::remove_dir(path) }?;
207        Ok(Default::default())
208    }
209}
210
211impl Cheatcode for removeFileCall {
212    fn apply(&self, state: &mut Cheatcodes) -> Result {
213        let Self { path } = self;
214        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
215        state.config.ensure_not_foundry_toml(&path)?;
216
217        // also remove from the set if opened previously
218        state.test_context.opened_read_files.remove(&path);
219
220        if state.fs_commit {
221            fs::remove_file(&path)?;
222        }
223
224        Ok(Default::default())
225    }
226}
227
228impl Cheatcode for writeFileCall {
229    fn apply(&self, state: &mut Cheatcodes) -> Result {
230        let Self { path, data } = self;
231        write_file(state, path.as_ref(), data.as_bytes())
232    }
233}
234
235impl Cheatcode for writeFileBinaryCall {
236    fn apply(&self, state: &mut Cheatcodes) -> Result {
237        let Self { path, data } = self;
238        write_file(state, path.as_ref(), data)
239    }
240}
241
242impl Cheatcode for writeLineCall {
243    fn apply(&self, state: &mut Cheatcodes) -> Result {
244        let Self { path, data: line } = self;
245        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
246        state.config.ensure_not_foundry_toml(&path)?;
247
248        if state.fs_commit {
249            fs::locked_write_line(path, line)?;
250        }
251
252        Ok(Default::default())
253    }
254}
255
256impl Cheatcode for getArtifactPathByCodeCall {
257    fn apply(&self, state: &mut Cheatcodes) -> Result {
258        let Self { code } = self;
259        let (artifact_id, _) = state
260            .config
261            .available_artifacts
262            .as_ref()
263            .and_then(|artifacts| artifacts.find_by_creation_code(code))
264            .ok_or_else(|| fmt_err!("no matching artifact found"))?;
265
266        Ok(artifact_id.path.to_string_lossy().abi_encode())
267    }
268}
269
270impl Cheatcode for getArtifactPathByDeployedCodeCall {
271    fn apply(&self, state: &mut Cheatcodes) -> Result {
272        let Self { deployedCode } = self;
273        let (artifact_id, _) = state
274            .config
275            .available_artifacts
276            .as_ref()
277            .and_then(|artifacts| artifacts.find_by_deployed_code(deployedCode))
278            .ok_or_else(|| fmt_err!("no matching artifact found"))?;
279
280        Ok(artifact_id.path.to_string_lossy().abi_encode())
281    }
282}
283
284impl Cheatcode for getCodeCall {
285    fn apply(&self, state: &mut Cheatcodes) -> Result {
286        let Self { artifactPath: path } = self;
287        Ok(get_artifact_code(state, path, false)?.abi_encode())
288    }
289}
290
291impl Cheatcode for getDeployedCodeCall {
292    fn apply(&self, state: &mut Cheatcodes) -> Result {
293        let Self { artifactPath: path } = self;
294        Ok(get_artifact_code(state, path, true)?.abi_encode())
295    }
296}
297
298impl Cheatcode for deployCode_0Call {
299    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
300        let Self { artifactPath: path } = self;
301        deploy_code(ccx, executor, path, None, None, None)
302    }
303}
304
305impl Cheatcode for deployCode_1Call {
306    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
307        let Self { artifactPath: path, constructorArgs: args } = self;
308        deploy_code(ccx, executor, path, Some(args), None, None)
309    }
310}
311
312impl Cheatcode for deployCode_2Call {
313    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
314        let Self { artifactPath: path, value } = self;
315        deploy_code(ccx, executor, path, None, Some(*value), None)
316    }
317}
318
319impl Cheatcode for deployCode_3Call {
320    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
321        let Self { artifactPath: path, constructorArgs: args, value } = self;
322        deploy_code(ccx, executor, path, Some(args), Some(*value), None)
323    }
324}
325
326impl Cheatcode for deployCode_4Call {
327    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
328        let Self { artifactPath: path, salt } = self;
329        deploy_code(ccx, executor, path, None, None, Some((*salt).into()))
330    }
331}
332
333impl Cheatcode for deployCode_5Call {
334    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
335        let Self { artifactPath: path, constructorArgs: args, salt } = self;
336        deploy_code(ccx, executor, path, Some(args), None, Some((*salt).into()))
337    }
338}
339
340impl Cheatcode for deployCode_6Call {
341    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
342        let Self { artifactPath: path, value, salt } = self;
343        deploy_code(ccx, executor, path, None, Some(*value), Some((*salt).into()))
344    }
345}
346
347impl Cheatcode for deployCode_7Call {
348    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
349        let Self { artifactPath: path, constructorArgs: args, value, salt } = self;
350        deploy_code(ccx, executor, path, Some(args), Some(*value), Some((*salt).into()))
351    }
352}
353
354/// Helper function to deploy contract from artifact code.
355/// Uses CREATE2 scheme if salt specified.
356fn deploy_code(
357    ccx: &mut CheatsCtxt,
358    executor: &mut dyn CheatcodesExecutor,
359    path: &str,
360    constructor_args: Option<&Bytes>,
361    value: Option<U256>,
362    salt: Option<U256>,
363) -> Result {
364    let mut bytecode = get_artifact_code(ccx.state, path, false)?.to_vec();
365
366    // If active broadcast then set flag to deploy from code.
367    if let Some(broadcast) = &mut ccx.state.broadcast {
368        broadcast.deploy_from_code = true;
369    }
370
371    if let Some(args) = constructor_args {
372        bytecode.extend_from_slice(args);
373    }
374
375    let scheme =
376        if let Some(salt) = salt { CreateScheme::Create2 { salt } } else { CreateScheme::Create };
377
378    // If prank active at current depth, then use it as caller for create input.
379    let caller = ccx
380        .state
381        .get_prank(ccx.ecx.journaled_state.depth())
382        .map_or(ccx.caller, |prank| prank.new_caller);
383
384    let outcome = executor.exec_create(
385        CreateInputs::new(
386            caller,
387            scheme,
388            value.unwrap_or(U256::ZERO),
389            bytecode.into(),
390            ccx.gas_limit,
391        ),
392        ccx,
393    )?;
394
395    if !outcome.result.result.is_ok() {
396        return Err(crate::Error::from(outcome.result.output));
397    }
398
399    let address = outcome.address.ok_or_else(|| fmt_err!("contract creation failed"))?;
400
401    Ok(address.abi_encode())
402}
403
404/// Returns the bytecode from a JSON artifact file.
405///
406/// Can parse following input formats:
407/// - `path/to/artifact.json`
408/// - `path/to/contract.sol`
409/// - `path/to/contract.sol:ContractName`
410/// - `path/to/contract.sol:ContractName:0.8.23`
411/// - `path/to/contract.sol:0.8.23`
412/// - `ContractName`
413/// - `ContractName:0.8.23`
414///
415/// This function is safe to use with contracts that have library dependencies.
416/// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will
417/// reject artifacts with unlinked library placeholders.
418fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result<Bytes> {
419    let path = if path.ends_with(".json") {
420        PathBuf::from(path)
421    } else {
422        let mut parts = path.split(':');
423
424        let mut file = None;
425        let mut contract_name = None;
426        let mut version = None;
427
428        let path_or_name = parts.next().unwrap();
429        if path_or_name.contains('.') {
430            file = Some(PathBuf::from(path_or_name));
431            if let Some(name_or_version) = parts.next() {
432                if name_or_version.contains('.') {
433                    version = Some(name_or_version);
434                } else {
435                    contract_name = Some(name_or_version);
436                    version = parts.next();
437                }
438            }
439        } else {
440            contract_name = Some(path_or_name);
441            version = parts.next();
442        }
443
444        let version = if let Some(version) = version {
445            Some(Version::parse(version).map_err(|e| fmt_err!("failed parsing version: {e}"))?)
446        } else {
447            None
448        };
449
450        // Use available artifacts list if present
451        if let Some(artifacts) = &state.config.available_artifacts {
452            let filtered = artifacts
453                .iter()
454                .filter(|(id, _)| {
455                    // name might be in the form of "Counter.0.8.23"
456                    let id_name = id.name.split('.').next().unwrap();
457
458                    if let Some(path) = &file
459                        && !id.source.ends_with(path)
460                    {
461                        return false;
462                    }
463                    if let Some(name) = contract_name
464                        && id_name != name
465                    {
466                        return false;
467                    }
468                    if let Some(ref version) = version
469                        && (id.version.minor != version.minor
470                            || id.version.major != version.major
471                            || id.version.patch != version.patch)
472                    {
473                        return false;
474                    }
475                    true
476                })
477                .collect::<Vec<_>>();
478
479            let artifact = match &filtered[..] {
480                [] => None,
481                [artifact] => Some(Ok(*artifact)),
482                filtered => {
483                    let mut filtered = filtered.to_vec();
484                    // If we know the current script/test contract solc version, try to filter by it
485                    Some(
486                        state
487                            .config
488                            .running_artifact
489                            .as_ref()
490                            .and_then(|running| {
491                                // Firstly filter by version
492                                filtered.retain(|(id, _)| id.version == running.version);
493
494                                // Return artifact if only one matched
495                                if filtered.len() == 1 {
496                                    return Some(filtered[0]);
497                                }
498
499                                // Try filtering by profile as well
500                                filtered.retain(|(id, _)| id.profile == running.profile);
501
502                                if filtered.len() == 1 { Some(filtered[0]) } else { None }
503                            })
504                            .ok_or_else(|| fmt_err!("multiple matching artifacts found")),
505                    )
506                }
507            };
508
509            if let Some(artifact) = artifact {
510                let artifact = artifact?;
511                let maybe_bytecode = if deployed {
512                    artifact.1.deployed_bytecode().cloned()
513                } else {
514                    artifact.1.bytecode().cloned()
515                };
516
517                return maybe_bytecode.ok_or_else(|| {
518                    fmt_err!("no bytecode for contract; is it abstract or unlinked?")
519                });
520            }
521        }
522
523        // Fallback: construct path manually when no artifacts list or no match found
524        let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name)
525        {
526            (Some(file), Some(contract_name)) => {
527                PathBuf::from(format!("{file}/{contract_name}.json"))
528            }
529            (None, Some(contract_name)) => {
530                PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
531            }
532            (Some(file), None) => {
533                let name = file.replace(".sol", "");
534                PathBuf::from(format!("{file}/{name}.json"))
535            }
536            _ => bail!("invalid artifact path"),
537        };
538
539        state.config.paths.artifacts.join(path_in_artifacts)
540    };
541
542    let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
543    let data = fs::read_to_string(path).map_err(|e| {
544        if state.config.available_artifacts.is_some() {
545            fmt_err!("no matching artifact found")
546        } else {
547            e.into()
548        }
549    })?;
550    let artifact = serde_json::from_str::<ContractObject>(&data)?;
551    let maybe_bytecode = if deployed { artifact.deployed_bytecode } else { artifact.bytecode };
552    maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
553}
554
555impl Cheatcode for ffiCall {
556    fn apply(&self, state: &mut Cheatcodes) -> Result {
557        let Self { commandInput: input } = self;
558
559        let output = ffi(state, input)?;
560
561        // Check the exit code of the command.
562        if output.exitCode != 0 {
563            // If the command failed, return an error with the exit code and stderr.
564            return Err(fmt_err!(
565                "ffi command {:?} exited with code {}. stderr: {}",
566                input,
567                output.exitCode,
568                String::from_utf8_lossy(&output.stderr)
569            ));
570        }
571
572        // If the command succeeded but still wrote to stderr, log it as a warning.
573        if !output.stderr.is_empty() {
574            let stderr = String::from_utf8_lossy(&output.stderr);
575            warn!(target: "cheatcodes", ?input, ?stderr, "ffi command wrote to stderr");
576        }
577
578        // We already hex-decoded the stdout in the `ffi` helper function.
579        Ok(output.stdout.abi_encode())
580    }
581}
582
583impl Cheatcode for tryFfiCall {
584    fn apply(&self, state: &mut Cheatcodes) -> Result {
585        let Self { commandInput: input } = self;
586        ffi(state, input).map(|res| res.abi_encode())
587    }
588}
589
590impl Cheatcode for promptCall {
591    fn apply(&self, state: &mut Cheatcodes) -> Result {
592        let Self { promptText: text } = self;
593        prompt(state, text, prompt_input).map(|res| res.abi_encode())
594    }
595}
596
597impl Cheatcode for promptSecretCall {
598    fn apply(&self, state: &mut Cheatcodes) -> Result {
599        let Self { promptText: text } = self;
600        prompt(state, text, prompt_password).map(|res| res.abi_encode())
601    }
602}
603
604impl Cheatcode for promptSecretUintCall {
605    fn apply(&self, state: &mut Cheatcodes) -> Result {
606        let Self { promptText: text } = self;
607        parse(&prompt(state, text, prompt_password)?, &DynSolType::Uint(256))
608    }
609}
610
611impl Cheatcode for promptAddressCall {
612    fn apply(&self, state: &mut Cheatcodes) -> Result {
613        let Self { promptText: text } = self;
614        parse(&prompt(state, text, prompt_input)?, &DynSolType::Address)
615    }
616}
617
618impl Cheatcode for promptUintCall {
619    fn apply(&self, state: &mut Cheatcodes) -> Result {
620        let Self { promptText: text } = self;
621        parse(&prompt(state, text, prompt_input)?, &DynSolType::Uint(256))
622    }
623}
624
625pub(super) fn write_file(state: &Cheatcodes, path: &Path, contents: &[u8]) -> Result {
626    let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
627    // write access to foundry.toml is not allowed
628    state.config.ensure_not_foundry_toml(&path)?;
629
630    if state.fs_commit {
631        fs::locked_write(path, contents)?;
632    }
633
634    Ok(Default::default())
635}
636
637fn read_dir(state: &Cheatcodes, path: &Path, max_depth: u64, follow_links: bool) -> Result {
638    let root = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
639    let paths: Vec<DirEntry> = WalkDir::new(root)
640        .min_depth(1)
641        .max_depth(max_depth.try_into().unwrap_or(usize::MAX))
642        .follow_links(follow_links)
643        .contents_first(false)
644        .same_file_system(true)
645        .sort_by_file_name()
646        .into_iter()
647        .map(|entry| match entry {
648            Ok(entry) => DirEntry {
649                errorMessage: String::new(),
650                path: entry.path().display().to_string(),
651                depth: entry.depth() as u64,
652                isDir: entry.file_type().is_dir(),
653                isSymlink: entry.path_is_symlink(),
654            },
655            Err(e) => DirEntry {
656                errorMessage: e.to_string(),
657                path: e.path().map(|p| p.display().to_string()).unwrap_or_default(),
658                depth: e.depth() as u64,
659                isDir: false,
660                isSymlink: false,
661            },
662        })
663        .collect();
664    Ok(paths.abi_encode())
665}
666
667fn ffi(state: &Cheatcodes, input: &[String]) -> Result<FfiResult> {
668    ensure!(
669        state.config.ffi,
670        "FFI is disabled; add the `--ffi` flag to allow tests to call external commands"
671    );
672    ensure!(!input.is_empty() && !input[0].is_empty(), "can't execute empty command");
673    let mut cmd = Command::new(&input[0]);
674    cmd.args(&input[1..]);
675
676    debug!(target: "cheatcodes", ?cmd, "invoking ffi");
677
678    let output = cmd
679        .current_dir(&state.config.root)
680        .output()
681        .map_err(|err| fmt_err!("failed to execute command {cmd:?}: {err}"))?;
682
683    // The stdout might be encoded on valid hex, or it might just be a string,
684    // so we need to determine which it is to avoid improperly encoding later.
685    let trimmed_stdout = String::from_utf8(output.stdout)?;
686    let trimmed_stdout = trimmed_stdout.trim();
687    let encoded_stdout = if let Ok(hex) = hex::decode(trimmed_stdout) {
688        hex
689    } else {
690        trimmed_stdout.as_bytes().to_vec()
691    };
692    Ok(FfiResult {
693        exitCode: output.status.code().unwrap_or(69),
694        stdout: encoded_stdout.into(),
695        stderr: output.stderr.into(),
696    })
697}
698
699fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> {
700    Input::new().allow_empty(true).with_prompt(prompt_text).interact_text()
701}
702
703fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> {
704    Password::new().with_prompt(prompt_text).interact()
705}
706
707fn prompt(
708    state: &Cheatcodes,
709    prompt_text: &str,
710    input: fn(&str) -> Result<String, dialoguer::Error>,
711) -> Result<String> {
712    let text_clone = prompt_text.to_string();
713    let timeout = state.config.prompt_timeout;
714    let (tx, rx) = mpsc::channel();
715
716    thread::spawn(move || {
717        let _ = tx.send(input(&text_clone));
718    });
719
720    match rx.recv_timeout(timeout) {
721        Ok(res) => res.map_err(|err| {
722            let _ = sh_println!();
723            err.to_string().into()
724        }),
725        Err(_) => {
726            let _ = sh_eprintln!();
727            Err("Prompt timed out".into())
728        }
729    }
730}
731
732impl Cheatcode for getBroadcastCall {
733    fn apply(&self, state: &mut Cheatcodes) -> Result {
734        let Self { contractName, chainId, txType } = self;
735
736        let latest_broadcast = latest_broadcast(
737            contractName,
738            *chainId,
739            &state.config.broadcast,
740            vec![map_broadcast_tx_type(*txType)],
741        )?;
742
743        Ok(latest_broadcast.abi_encode())
744    }
745}
746
747impl Cheatcode for getBroadcasts_0Call {
748    fn apply(&self, state: &mut Cheatcodes) -> Result {
749        let Self { contractName, chainId, txType } = self;
750
751        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
752            .with_tx_type(map_broadcast_tx_type(*txType));
753
754        let broadcasts = reader.read()?;
755
756        let summaries = broadcasts
757            .into_iter()
758            .flat_map(|broadcast| {
759                let results = reader.into_tx_receipts(broadcast);
760                parse_broadcast_results(results)
761            })
762            .collect::<Vec<_>>();
763
764        Ok(summaries.abi_encode())
765    }
766}
767
768impl Cheatcode for getBroadcasts_1Call {
769    fn apply(&self, state: &mut Cheatcodes) -> Result {
770        let Self { contractName, chainId } = self;
771
772        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?;
773
774        let broadcasts = reader.read()?;
775
776        let summaries = broadcasts
777            .into_iter()
778            .flat_map(|broadcast| {
779                let results = reader.into_tx_receipts(broadcast);
780                parse_broadcast_results(results)
781            })
782            .collect::<Vec<_>>();
783
784        Ok(summaries.abi_encode())
785    }
786}
787
788impl Cheatcode for getDeployment_0Call {
789    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
790        let Self { contractName } = self;
791        let chain_id = ccx.ecx.cfg.chain_id;
792
793        let latest_broadcast = latest_broadcast(
794            contractName,
795            chain_id,
796            &ccx.state.config.broadcast,
797            vec![CallKind::Create, CallKind::Create2],
798        )?;
799
800        Ok(latest_broadcast.contractAddress.abi_encode())
801    }
802}
803
804impl Cheatcode for getDeployment_1Call {
805    fn apply(&self, state: &mut Cheatcodes) -> Result {
806        let Self { contractName, chainId } = self;
807
808        let latest_broadcast = latest_broadcast(
809            contractName,
810            *chainId,
811            &state.config.broadcast,
812            vec![CallKind::Create, CallKind::Create2],
813        )?;
814
815        Ok(latest_broadcast.contractAddress.abi_encode())
816    }
817}
818
819impl Cheatcode for getDeploymentsCall {
820    fn apply(&self, state: &mut Cheatcodes) -> Result {
821        let Self { contractName, chainId } = self;
822
823        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
824            .with_tx_type(CallKind::Create)
825            .with_tx_type(CallKind::Create2);
826
827        let broadcasts = reader.read()?;
828
829        let summaries = broadcasts
830            .into_iter()
831            .flat_map(|broadcast| {
832                let results = reader.into_tx_receipts(broadcast);
833                parse_broadcast_results(results)
834            })
835            .collect::<Vec<_>>();
836
837        let deployed_addresses =
838            summaries.into_iter().map(|summary| summary.contractAddress).collect::<Vec<_>>();
839
840        Ok(deployed_addresses.abi_encode())
841    }
842}
843
844fn map_broadcast_tx_type(tx_type: BroadcastTxType) -> CallKind {
845    match tx_type {
846        BroadcastTxType::Call => CallKind::Call,
847        BroadcastTxType::Create => CallKind::Create,
848        BroadcastTxType::Create2 => CallKind::Create2,
849        _ => unreachable!("invalid tx type"),
850    }
851}
852
853fn parse_broadcast_results(
854    results: Vec<(TransactionWithMetadata, AnyTransactionReceipt)>,
855) -> Vec<BroadcastTxSummary> {
856    results
857        .into_iter()
858        .map(|(tx, receipt)| BroadcastTxSummary {
859            txHash: receipt.transaction_hash,
860            blockNumber: receipt.block_number.unwrap_or_default(),
861            txType: match tx.opcode {
862                CallKind::Call => BroadcastTxType::Call,
863                CallKind::Create => BroadcastTxType::Create,
864                CallKind::Create2 => BroadcastTxType::Create2,
865                _ => unreachable!("invalid tx type"),
866            },
867            contractAddress: tx.contract_address.unwrap_or_default(),
868            success: receipt.status(),
869        })
870        .collect()
871}
872
873fn latest_broadcast(
874    contract_name: &String,
875    chain_id: u64,
876    broadcast_path: &Path,
877    filters: Vec<CallKind>,
878) -> Result<BroadcastTxSummary> {
879    let mut reader = BroadcastReader::new(contract_name.clone(), chain_id, broadcast_path)?;
880
881    for filter in filters {
882        reader = reader.with_tx_type(filter);
883    }
884
885    let broadcast = reader.read_latest()?;
886
887    let results = reader.into_tx_receipts(broadcast);
888
889    let summaries = parse_broadcast_results(results);
890
891    summaries
892        .first()
893        .ok_or_else(|| fmt_err!("no deployment found for {contract_name} on chain {chain_id}"))
894        .cloned()
895}
896
897#[cfg(test)]
898mod tests {
899    use super::*;
900    use crate::CheatsConfig;
901    use std::sync::Arc;
902
903    fn cheats() -> Cheatcodes {
904        let config = CheatsConfig {
905            ffi: true,
906            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
907            ..Default::default()
908        };
909        Cheatcodes::new(Arc::new(config))
910    }
911
912    #[test]
913    fn test_ffi_hex() {
914        let msg = b"gm";
915        let cheats = cheats();
916        let args = ["echo".to_string(), hex::encode(msg)];
917        let output = ffi(&cheats, &args).unwrap();
918        assert_eq!(output.stdout, Bytes::from(msg));
919    }
920
921    #[test]
922    fn test_ffi_string() {
923        let msg = "gm";
924        let cheats = cheats();
925        let args = ["echo".to_string(), msg.to_string()];
926        let output = ffi(&cheats, &args).unwrap();
927        assert_eq!(output.stdout, Bytes::from(msg.as_bytes()));
928    }
929
930    #[test]
931    fn test_ffi_fails_on_error_code() {
932        let mut cheats = cheats();
933
934        // Use a command that is guaranteed to fail with a non-zero exit code on any platform.
935        #[cfg(unix)]
936        let args = vec!["false".to_string()];
937        #[cfg(windows)]
938        let args = vec!["cmd".to_string(), "/c".to_string(), "exit 1".to_string()];
939
940        let result = ffiCall { commandInput: args }.apply(&mut cheats);
941
942        // Assert that the cheatcode returned an error.
943        assert!(result.is_err(), "Expected ffi cheatcode to fail, but it succeeded");
944
945        // Assert that the error message contains the expected information.
946        let err_msg = result.unwrap_err().to_string();
947        assert!(
948            err_msg.contains("exited with code 1"),
949            "Error message did not contain exit code: {err_msg}"
950        );
951    }
952
953    #[test]
954    fn test_artifact_parsing() {
955        let s = include_str!("../../evm/test-data/solc-obj.json");
956        let artifact: ContractObject = serde_json::from_str(s).unwrap();
957        assert!(artifact.bytecode.is_some());
958
959        let artifact: ContractObject = serde_json::from_str(s).unwrap();
960        assert!(artifact.deployed_bytecode.is_some());
961    }
962
963    #[test]
964    fn test_alloy_json_abi_rejects_unlinked_bytecode() {
965        let artifact_json = r#"{
966            "abi": [],
967            "bytecode": "0x73__$987e73aeca5e61ce83e4cb0814d87beda9$__63baf2f868"
968        }"#;
969
970        let result: Result<ContractObject, _> = serde_json::from_str(artifact_json);
971        assert!(result.is_err(), "should reject unlinked bytecode with placeholders");
972        let err = result.unwrap_err().to_string();
973        assert!(err.contains("expected bytecode, found unlinked bytecode with placeholder"));
974    }
975}