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::{hex, map::Entry, Bytes, U256};
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::interpreter::CreateInputs;
16use revm_inspectors::tracing::types::CallKind;
17use semver::Version;
18use std::{
19    io::{BufRead, BufReader, Write},
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.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::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::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.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.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            let mut file = std::fs::OpenOptions::new().append(true).create(true).open(path)?;
247
248            writeln!(file, "{line}")?;
249        }
250
251        Ok(Default::default())
252    }
253}
254
255impl Cheatcode for getArtifactPathByCodeCall {
256    fn apply(&self, state: &mut Cheatcodes) -> Result {
257        let Self { code } = self;
258        let (artifact_id, _) = state
259            .config
260            .available_artifacts
261            .as_ref()
262            .and_then(|artifacts| artifacts.find_by_creation_code(code))
263            .ok_or_else(|| fmt_err!("no matching artifact found"))?;
264
265        Ok(artifact_id.path.to_string_lossy().abi_encode())
266    }
267}
268
269impl Cheatcode for getArtifactPathByDeployedCodeCall {
270    fn apply(&self, state: &mut Cheatcodes) -> Result {
271        let Self { deployedCode } = self;
272        let (artifact_id, _) = state
273            .config
274            .available_artifacts
275            .as_ref()
276            .and_then(|artifacts| artifacts.find_by_deployed_code(deployedCode))
277            .ok_or_else(|| fmt_err!("no matching artifact found"))?;
278
279        Ok(artifact_id.path.to_string_lossy().abi_encode())
280    }
281}
282
283impl Cheatcode for getCodeCall {
284    fn apply(&self, state: &mut Cheatcodes) -> Result {
285        let Self { artifactPath: path } = self;
286        Ok(get_artifact_code(state, path, false)?.abi_encode())
287    }
288}
289
290impl Cheatcode for getDeployedCodeCall {
291    fn apply(&self, state: &mut Cheatcodes) -> Result {
292        let Self { artifactPath: path } = self;
293        Ok(get_artifact_code(state, path, true)?.abi_encode())
294    }
295}
296
297impl Cheatcode for deployCode_0Call {
298    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
299        let Self { artifactPath: path } = self;
300        deploy_code(ccx, executor, path, None, None, None)
301    }
302}
303
304impl Cheatcode for deployCode_1Call {
305    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
306        let Self { artifactPath: path, constructorArgs: args } = self;
307        deploy_code(ccx, executor, path, Some(args), None, None)
308    }
309}
310
311impl Cheatcode for deployCode_2Call {
312    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
313        let Self { artifactPath: path, value } = self;
314        deploy_code(ccx, executor, path, None, Some(*value), None)
315    }
316}
317
318impl Cheatcode for deployCode_3Call {
319    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
320        let Self { artifactPath: path, constructorArgs: args, value } = self;
321        deploy_code(ccx, executor, path, Some(args), Some(*value), None)
322    }
323}
324
325impl Cheatcode for deployCode_4Call {
326    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
327        let Self { artifactPath: path, salt } = self;
328        deploy_code(ccx, executor, path, None, None, Some((*salt).into()))
329    }
330}
331
332impl Cheatcode for deployCode_5Call {
333    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
334        let Self { artifactPath: path, constructorArgs: args, salt } = self;
335        deploy_code(ccx, executor, path, Some(args), None, Some((*salt).into()))
336    }
337}
338
339impl Cheatcode for deployCode_6Call {
340    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
341        let Self { artifactPath: path, value, salt } = self;
342        deploy_code(ccx, executor, path, None, Some(*value), Some((*salt).into()))
343    }
344}
345
346impl Cheatcode for deployCode_7Call {
347    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
348        let Self { artifactPath: path, constructorArgs: args, value, salt } = self;
349        deploy_code(ccx, executor, path, Some(args), Some(*value), Some((*salt).into()))
350    }
351}
352
353/// Helper function to deploy contract from artifact code.
354/// Uses CREATE2 scheme if salt specified.
355fn deploy_code(
356    ccx: &mut CheatsCtxt,
357    executor: &mut dyn CheatcodesExecutor,
358    path: &str,
359    constructor_args: Option<&Bytes>,
360    value: Option<U256>,
361    salt: Option<U256>,
362) -> Result {
363    let mut bytecode = get_artifact_code(ccx.state, path, false)?.to_vec();
364    if let Some(args) = constructor_args {
365        bytecode.extend_from_slice(args);
366    }
367
368    let scheme = if let Some(salt) = salt {
369        revm::primitives::CreateScheme::Create2 { salt }
370    } else {
371        revm::primitives::CreateScheme::Create
372    };
373
374    let outcome = executor.exec_create(
375        CreateInputs {
376            caller: ccx.caller,
377            scheme,
378            value: value.unwrap_or(U256::ZERO),
379            init_code: bytecode.into(),
380            gas_limit: ccx.gas_limit,
381        },
382        ccx,
383    )?;
384
385    if !outcome.result.result.is_ok() {
386        return Err(crate::Error::from(outcome.result.output))
387    }
388
389    let address = outcome.address.ok_or_else(|| fmt_err!("contract creation failed"))?;
390
391    Ok(address.abi_encode())
392}
393
394/// Returns the path to the json artifact depending on the input
395///
396/// Can parse following input formats:
397/// - `path/to/artifact.json`
398/// - `path/to/contract.sol`
399/// - `path/to/contract.sol:ContractName`
400/// - `path/to/contract.sol:ContractName:0.8.23`
401/// - `path/to/contract.sol:0.8.23`
402/// - `ContractName`
403/// - `ContractName:0.8.23`
404fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result<Bytes> {
405    let path = if path.ends_with(".json") {
406        PathBuf::from(path)
407    } else {
408        let mut parts = path.split(':');
409
410        let mut file = None;
411        let mut contract_name = None;
412        let mut version = None;
413
414        let path_or_name = parts.next().unwrap();
415        if path_or_name.contains('.') {
416            file = Some(PathBuf::from(path_or_name));
417            if let Some(name_or_version) = parts.next() {
418                if name_or_version.contains('.') {
419                    version = Some(name_or_version);
420                } else {
421                    contract_name = Some(name_or_version);
422                    version = parts.next();
423                }
424            }
425        } else {
426            contract_name = Some(path_or_name);
427            version = parts.next();
428        }
429
430        let version = if let Some(version) = version {
431            Some(Version::parse(version).map_err(|e| fmt_err!("failed parsing version: {e}"))?)
432        } else {
433            None
434        };
435
436        // Use available artifacts list if present
437        if let Some(artifacts) = &state.config.available_artifacts {
438            let filtered = artifacts
439                .iter()
440                .filter(|(id, _)| {
441                    // name might be in the form of "Counter.0.8.23"
442                    let id_name = id.name.split('.').next().unwrap();
443
444                    if let Some(path) = &file {
445                        if !id.source.ends_with(path) {
446                            return false;
447                        }
448                    }
449                    if let Some(name) = contract_name {
450                        if id_name != name {
451                            return false;
452                        }
453                    }
454                    if let Some(ref version) = version {
455                        if id.version.minor != version.minor ||
456                            id.version.major != version.major ||
457                            id.version.patch != version.patch
458                        {
459                            return false;
460                        }
461                    }
462                    true
463                })
464                .collect::<Vec<_>>();
465
466            let artifact = match &filtered[..] {
467                [] => Err(fmt_err!("no matching artifact found")),
468                [artifact] => Ok(*artifact),
469                filtered => {
470                    let mut filtered = filtered.to_vec();
471                    // If we know the current script/test contract solc version, try to filter by it
472                    state
473                        .config
474                        .running_artifact
475                        .as_ref()
476                        .and_then(|running| {
477                            // Firstly filter by version
478                            filtered.retain(|(id, _)| id.version == running.version);
479
480                            // Return artifact if only one matched
481                            if filtered.len() == 1 {
482                                return Some(filtered[0])
483                            }
484
485                            // Try filtering by profile as well
486                            filtered.retain(|(id, _)| id.profile == running.profile);
487
488                            if filtered.len() == 1 {
489                                Some(filtered[0])
490                            } else {
491                                None
492                            }
493                        })
494                        .ok_or_else(|| fmt_err!("multiple matching artifacts found"))
495                }
496            }?;
497
498            let maybe_bytecode = if deployed {
499                artifact.1.deployed_bytecode().cloned()
500            } else {
501                artifact.1.bytecode().cloned()
502            };
503
504            return maybe_bytecode
505                .ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"));
506        } else {
507            let path_in_artifacts =
508                match (file.map(|f| f.to_string_lossy().to_string()), contract_name) {
509                    (Some(file), Some(contract_name)) => {
510                        PathBuf::from(format!("{file}/{contract_name}.json"))
511                    }
512                    (None, Some(contract_name)) => {
513                        PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
514                    }
515                    (Some(file), None) => {
516                        let name = file.replace(".sol", "");
517                        PathBuf::from(format!("{file}/{name}.json"))
518                    }
519                    _ => bail!("invalid artifact path"),
520                };
521
522            state.config.paths.artifacts.join(path_in_artifacts)
523        }
524    };
525
526    let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
527    let data = fs::read_to_string(path)?;
528    let artifact = serde_json::from_str::<ContractObject>(&data)?;
529    let maybe_bytecode = if deployed { artifact.deployed_bytecode } else { artifact.bytecode };
530    maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
531}
532
533impl Cheatcode for ffiCall {
534    fn apply(&self, state: &mut Cheatcodes) -> Result {
535        let Self { commandInput: input } = self;
536
537        let output = ffi(state, input)?;
538        // TODO: check exit code?
539        if !output.stderr.is_empty() {
540            let stderr = String::from_utf8_lossy(&output.stderr);
541            error!(target: "cheatcodes", ?input, ?stderr, "non-empty stderr");
542        }
543        // we already hex-decoded the stdout in `ffi`
544        Ok(output.stdout.abi_encode())
545    }
546}
547
548impl Cheatcode for tryFfiCall {
549    fn apply(&self, state: &mut Cheatcodes) -> Result {
550        let Self { commandInput: input } = self;
551        ffi(state, input).map(|res| res.abi_encode())
552    }
553}
554
555impl Cheatcode for promptCall {
556    fn apply(&self, state: &mut Cheatcodes) -> Result {
557        let Self { promptText: text } = self;
558        prompt(state, text, prompt_input).map(|res| res.abi_encode())
559    }
560}
561
562impl Cheatcode for promptSecretCall {
563    fn apply(&self, state: &mut Cheatcodes) -> Result {
564        let Self { promptText: text } = self;
565        prompt(state, text, prompt_password).map(|res| res.abi_encode())
566    }
567}
568
569impl Cheatcode for promptSecretUintCall {
570    fn apply(&self, state: &mut Cheatcodes) -> Result {
571        let Self { promptText: text } = self;
572        parse(&prompt(state, text, prompt_password)?, &DynSolType::Uint(256))
573    }
574}
575
576impl Cheatcode for promptAddressCall {
577    fn apply(&self, state: &mut Cheatcodes) -> Result {
578        let Self { promptText: text } = self;
579        parse(&prompt(state, text, prompt_input)?, &DynSolType::Address)
580    }
581}
582
583impl Cheatcode for promptUintCall {
584    fn apply(&self, state: &mut Cheatcodes) -> Result {
585        let Self { promptText: text } = self;
586        parse(&prompt(state, text, prompt_input)?, &DynSolType::Uint(256))
587    }
588}
589
590pub(super) fn write_file(state: &Cheatcodes, path: &Path, contents: &[u8]) -> Result {
591    let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
592    // write access to foundry.toml is not allowed
593    state.config.ensure_not_foundry_toml(&path)?;
594
595    if state.fs_commit {
596        fs::write(path, contents)?;
597    }
598
599    Ok(Default::default())
600}
601
602fn read_dir(state: &Cheatcodes, path: &Path, max_depth: u64, follow_links: bool) -> Result {
603    let root = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
604    let paths: Vec<DirEntry> = WalkDir::new(root)
605        .min_depth(1)
606        .max_depth(max_depth.try_into().unwrap_or(usize::MAX))
607        .follow_links(follow_links)
608        .contents_first(false)
609        .same_file_system(true)
610        .sort_by_file_name()
611        .into_iter()
612        .map(|entry| match entry {
613            Ok(entry) => DirEntry {
614                errorMessage: String::new(),
615                path: entry.path().display().to_string(),
616                depth: entry.depth() as u64,
617                isDir: entry.file_type().is_dir(),
618                isSymlink: entry.path_is_symlink(),
619            },
620            Err(e) => DirEntry {
621                errorMessage: e.to_string(),
622                path: e.path().map(|p| p.display().to_string()).unwrap_or_default(),
623                depth: e.depth() as u64,
624                isDir: false,
625                isSymlink: false,
626            },
627        })
628        .collect();
629    Ok(paths.abi_encode())
630}
631
632fn ffi(state: &Cheatcodes, input: &[String]) -> Result<FfiResult> {
633    ensure!(
634        state.config.ffi,
635        "FFI is disabled; add the `--ffi` flag to allow tests to call external commands"
636    );
637    ensure!(!input.is_empty() && !input[0].is_empty(), "can't execute empty command");
638    let mut cmd = Command::new(&input[0]);
639    cmd.args(&input[1..]);
640
641    debug!(target: "cheatcodes", ?cmd, "invoking ffi");
642
643    let output = cmd
644        .current_dir(&state.config.root)
645        .output()
646        .map_err(|err| fmt_err!("failed to execute command {cmd:?}: {err}"))?;
647
648    // The stdout might be encoded on valid hex, or it might just be a string,
649    // so we need to determine which it is to avoid improperly encoding later.
650    let trimmed_stdout = String::from_utf8(output.stdout)?;
651    let trimmed_stdout = trimmed_stdout.trim();
652    let encoded_stdout = if let Ok(hex) = hex::decode(trimmed_stdout) {
653        hex
654    } else {
655        trimmed_stdout.as_bytes().to_vec()
656    };
657    Ok(FfiResult {
658        exitCode: output.status.code().unwrap_or(69),
659        stdout: encoded_stdout.into(),
660        stderr: output.stderr.into(),
661    })
662}
663
664fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> {
665    Input::new().allow_empty(true).with_prompt(prompt_text).interact_text()
666}
667
668fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> {
669    Password::new().with_prompt(prompt_text).interact()
670}
671
672fn prompt(
673    state: &Cheatcodes,
674    prompt_text: &str,
675    input: fn(&str) -> Result<String, dialoguer::Error>,
676) -> Result<String> {
677    let text_clone = prompt_text.to_string();
678    let timeout = state.config.prompt_timeout;
679    let (tx, rx) = mpsc::channel();
680
681    thread::spawn(move || {
682        let _ = tx.send(input(&text_clone));
683    });
684
685    match rx.recv_timeout(timeout) {
686        Ok(res) => res.map_err(|err| {
687            let _ = sh_println!();
688            err.to_string().into()
689        }),
690        Err(_) => {
691            let _ = sh_eprintln!();
692            Err("Prompt timed out".into())
693        }
694    }
695}
696
697impl Cheatcode for getBroadcastCall {
698    fn apply(&self, state: &mut Cheatcodes) -> Result {
699        let Self { contractName, chainId, txType } = self;
700
701        let latest_broadcast = latest_broadcast(
702            contractName,
703            *chainId,
704            &state.config.broadcast,
705            vec![map_broadcast_tx_type(*txType)],
706        )?;
707
708        Ok(latest_broadcast.abi_encode())
709    }
710}
711
712impl Cheatcode for getBroadcasts_0Call {
713    fn apply(&self, state: &mut Cheatcodes) -> Result {
714        let Self { contractName, chainId, txType } = self;
715
716        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
717            .with_tx_type(map_broadcast_tx_type(*txType));
718
719        let broadcasts = reader.read()?;
720
721        let summaries = broadcasts
722            .into_iter()
723            .flat_map(|broadcast| {
724                let results = reader.into_tx_receipts(broadcast);
725                parse_broadcast_results(results)
726            })
727            .collect::<Vec<_>>();
728
729        Ok(summaries.abi_encode())
730    }
731}
732
733impl Cheatcode for getBroadcasts_1Call {
734    fn apply(&self, state: &mut Cheatcodes) -> Result {
735        let Self { contractName, chainId } = self;
736
737        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?;
738
739        let broadcasts = reader.read()?;
740
741        let summaries = broadcasts
742            .into_iter()
743            .flat_map(|broadcast| {
744                let results = reader.into_tx_receipts(broadcast);
745                parse_broadcast_results(results)
746            })
747            .collect::<Vec<_>>();
748
749        Ok(summaries.abi_encode())
750    }
751}
752
753impl Cheatcode for getDeployment_0Call {
754    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
755        let Self { contractName } = self;
756        let chain_id = ccx.ecx.env.cfg.chain_id;
757
758        let latest_broadcast = latest_broadcast(
759            contractName,
760            chain_id,
761            &ccx.state.config.broadcast,
762            vec![CallKind::Create, CallKind::Create2],
763        )?;
764
765        Ok(latest_broadcast.contractAddress.abi_encode())
766    }
767}
768
769impl Cheatcode for getDeployment_1Call {
770    fn apply(&self, state: &mut Cheatcodes) -> Result {
771        let Self { contractName, chainId } = self;
772
773        let latest_broadcast = latest_broadcast(
774            contractName,
775            *chainId,
776            &state.config.broadcast,
777            vec![CallKind::Create, CallKind::Create2],
778        )?;
779
780        Ok(latest_broadcast.contractAddress.abi_encode())
781    }
782}
783
784impl Cheatcode for getDeploymentsCall {
785    fn apply(&self, state: &mut Cheatcodes) -> Result {
786        let Self { contractName, chainId } = self;
787
788        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
789            .with_tx_type(CallKind::Create)
790            .with_tx_type(CallKind::Create2);
791
792        let broadcasts = reader.read()?;
793
794        let summaries = broadcasts
795            .into_iter()
796            .flat_map(|broadcast| {
797                let results = reader.into_tx_receipts(broadcast);
798                parse_broadcast_results(results)
799            })
800            .collect::<Vec<_>>();
801
802        let deployed_addresses =
803            summaries.into_iter().map(|summary| summary.contractAddress).collect::<Vec<_>>();
804
805        Ok(deployed_addresses.abi_encode())
806    }
807}
808
809fn map_broadcast_tx_type(tx_type: BroadcastTxType) -> CallKind {
810    match tx_type {
811        BroadcastTxType::Call => CallKind::Call,
812        BroadcastTxType::Create => CallKind::Create,
813        BroadcastTxType::Create2 => CallKind::Create2,
814        _ => unreachable!("invalid tx type"),
815    }
816}
817
818fn parse_broadcast_results(
819    results: Vec<(TransactionWithMetadata, AnyTransactionReceipt)>,
820) -> Vec<BroadcastTxSummary> {
821    results
822        .into_iter()
823        .map(|(tx, receipt)| BroadcastTxSummary {
824            txHash: receipt.transaction_hash,
825            blockNumber: receipt.block_number.unwrap_or_default(),
826            txType: match tx.opcode {
827                CallKind::Call => BroadcastTxType::Call,
828                CallKind::Create => BroadcastTxType::Create,
829                CallKind::Create2 => BroadcastTxType::Create2,
830                _ => unreachable!("invalid tx type"),
831            },
832            contractAddress: tx.contract_address.unwrap_or_default(),
833            success: receipt.status(),
834        })
835        .collect()
836}
837
838fn latest_broadcast(
839    contract_name: &String,
840    chain_id: u64,
841    broadcast_path: &Path,
842    filters: Vec<CallKind>,
843) -> Result<BroadcastTxSummary> {
844    let mut reader = BroadcastReader::new(contract_name.clone(), chain_id, broadcast_path)?;
845
846    for filter in filters {
847        reader = reader.with_tx_type(filter);
848    }
849
850    let broadcast = reader.read_latest()?;
851
852    let results = reader.into_tx_receipts(broadcast);
853
854    let summaries = parse_broadcast_results(results);
855
856    summaries
857        .first()
858        .ok_or_else(|| fmt_err!("no deployment found for {contract_name} on chain {chain_id}"))
859        .cloned()
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865    use crate::CheatsConfig;
866    use std::sync::Arc;
867
868    fn cheats() -> Cheatcodes {
869        let config = CheatsConfig {
870            ffi: true,
871            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
872            ..Default::default()
873        };
874        Cheatcodes::new(Arc::new(config))
875    }
876
877    #[test]
878    fn test_ffi_hex() {
879        let msg = b"gm";
880        let cheats = cheats();
881        let args = ["echo".to_string(), hex::encode(msg)];
882        let output = ffi(&cheats, &args).unwrap();
883        assert_eq!(output.stdout, Bytes::from(msg));
884    }
885
886    #[test]
887    fn test_ffi_string() {
888        let msg = "gm";
889        let cheats = cheats();
890        let args = ["echo".to_string(), msg.to_string()];
891        let output = ffi(&cheats, &args).unwrap();
892        assert_eq!(output.stdout, Bytes::from(msg.as_bytes()));
893    }
894
895    #[test]
896    fn test_artifact_parsing() {
897        let s = include_str!("../../evm/test-data/solc-obj.json");
898        let artifact: ContractObject = serde_json::from_str(s).unwrap();
899        assert!(artifact.bytecode.is_some());
900
901        let artifact: ContractObject = serde_json::from_str(s).unwrap();
902        assert!(artifact.deployed_bytecode.is_some());
903    }
904}