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