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