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