1use super::string::parse;
4use crate::{
5 Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*, inspector::exec_create,
6};
7use alloy_dyn_abi::DynSolType;
8use alloy_json_abi::ContractObject;
9use alloy_network::{Network, ReceiptResponse};
10use alloy_primitives::{Bytes, FixedBytes, U256, hex, map::Entry};
11use alloy_sol_types::SolValue;
12use dialoguer::{Input, Password};
13use forge_script_sequence::{BroadcastReader, TransactionWithMetadata};
14use foundry_common::{contracts::ContractData, fs};
15use foundry_config::fs_permissions::FsAccessKind;
16use foundry_evm_core::evm::FoundryEvmNetwork;
17use revm::{
18 context::{Cfg, ContextTr, CreateScheme, JournalTr},
19 interpreter::CreateInputs,
20};
21use revm_inspectors::tracing::types::CallKind;
22use semver::Version;
23use std::{
24 io::{BufRead, BufReader},
25 path::{Path, PathBuf},
26 process::Command,
27 sync::mpsc,
28 thread,
29 time::{SystemTime, UNIX_EPOCH},
30};
31use walkdir::WalkDir;
32
33#[derive(Debug, Default, PartialEq, Eq)]
35struct ParsedArtifactPath<'a> {
36 file: Option<PathBuf>,
37 contract_name: Option<&'a str>,
38 version: Option<Version>,
39 profile: Option<&'a str>,
40}
41
42fn parse_artifact_path(path: &str) -> std::result::Result<ParsedArtifactPath<'_>, String> {
55 let mut parts = path.split(':');
56
57 let mut file = None;
58 let mut contract_name = None;
59 let mut version = None;
60 let mut profile = None;
61
62 let path_or_name = parts.next().unwrap();
63 if path_or_name.contains('.') {
64 file = Some(PathBuf::from(path_or_name));
65 if let Some(name_or_version_or_profile) = parts.next() {
66 if name_or_version_or_profile.contains('.')
67 || Version::parse(name_or_version_or_profile).is_ok()
68 {
69 version = Some(name_or_version_or_profile);
70 } else {
71 contract_name = Some(name_or_version_or_profile);
72 if let Some(version_or_profile) = parts.next() {
73 if version_or_profile.contains('.')
74 || Version::parse(version_or_profile).is_ok()
75 {
76 version = Some(version_or_profile);
77 } else {
78 profile = Some(version_or_profile);
79 }
80 }
81 }
82 }
83 } else {
84 contract_name = Some(path_or_name);
85 if let Some(version_or_profile) = parts.next() {
86 if version_or_profile.contains('.') || Version::parse(version_or_profile).is_ok() {
87 version = Some(version_or_profile);
88 } else {
89 profile = Some(version_or_profile);
90 }
91 }
92 }
93
94 let version = if let Some(version) = version {
95 Some(Version::parse(version).map_err(|e| format!("failed parsing version: {e}"))?)
96 } else {
97 None
98 };
99
100 Ok(ParsedArtifactPath { file, contract_name, version, profile })
101}
102
103impl Cheatcode for existsCall {
104 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
105 let Self { path } = self;
106 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
107 Ok(path.exists().abi_encode())
108 }
109}
110
111impl Cheatcode for fsMetadataCall {
112 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
113 let Self { path } = self;
114 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
115
116 let metadata = path.metadata()?;
117
118 let [modified, accessed, created] =
120 [metadata.modified(), metadata.accessed(), metadata.created()].map(|time| {
121 time.unwrap_or(UNIX_EPOCH).duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
122 });
123
124 Ok(FsMetadata {
125 isDir: metadata.is_dir(),
126 isSymlink: metadata.is_symlink(),
127 length: U256::from(metadata.len()),
128 readOnly: metadata.permissions().readonly(),
129 modified: U256::from(modified),
130 accessed: U256::from(accessed),
131 created: U256::from(created),
132 }
133 .abi_encode())
134 }
135}
136
137impl Cheatcode for isDirCall {
138 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
139 let Self { path } = self;
140 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
141 Ok(path.is_dir().abi_encode())
142 }
143}
144
145impl Cheatcode for isFileCall {
146 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
147 let Self { path } = self;
148 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
149 Ok(path.is_file().abi_encode())
150 }
151}
152
153impl Cheatcode for projectRootCall {
154 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
155 let Self {} = self;
156 Ok(state.config.root.display().to_string().abi_encode())
157 }
158}
159
160impl Cheatcode for currentFilePathCall {
161 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
162 let Self {} = self;
163 let artifact = state
164 .config
165 .running_artifact
166 .as_ref()
167 .ok_or_else(|| fmt_err!("no running contract found"))?;
168 let relative = artifact.source.strip_prefix(&state.config.root).unwrap_or(&artifact.source);
169 Ok(relative.display().to_string().abi_encode())
170 }
171}
172
173impl Cheatcode for unixTimeCall {
174 fn apply<FEN: FoundryEvmNetwork>(&self, _state: &mut Cheatcodes<FEN>) -> Result {
175 let Self {} = self;
176 let difference = SystemTime::now()
177 .duration_since(UNIX_EPOCH)
178 .map_err(|e| fmt_err!("failed getting Unix timestamp: {e}"))?;
179 Ok(difference.as_millis().abi_encode())
180 }
181}
182
183impl Cheatcode for closeFileCall {
184 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
185 let Self { path } = self;
186 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
187
188 state.test_context.opened_read_files.remove(&path);
189
190 Ok(Default::default())
191 }
192}
193
194impl Cheatcode for copyFileCall {
195 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
196 let Self { from, to } = self;
197 let from = state.config.ensure_path_allowed(from, FsAccessKind::Read)?;
198 let to = state.config.ensure_path_allowed(to, FsAccessKind::Write)?;
199 state.config.ensure_not_foundry_toml(&to)?;
200
201 let n = fs::copy(from, to)?;
202 Ok(n.abi_encode())
203 }
204}
205
206impl Cheatcode for createDirCall {
207 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
208 let Self { path, recursive } = self;
209 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
210 if *recursive { fs::create_dir_all(path) } else { fs::create_dir(path) }?;
211 Ok(Default::default())
212 }
213}
214
215impl Cheatcode for readDir_0Call {
216 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
217 let Self { path } = self;
218 read_dir(state, path.as_ref(), 1, false)
219 }
220}
221
222impl Cheatcode for readDir_1Call {
223 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
224 let Self { path, maxDepth } = self;
225 read_dir(state, path.as_ref(), *maxDepth, false)
226 }
227}
228
229impl Cheatcode for readDir_2Call {
230 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
231 let Self { path, maxDepth, followLinks } = self;
232 read_dir(state, path.as_ref(), *maxDepth, *followLinks)
233 }
234}
235
236impl Cheatcode for readFileCall {
237 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
238 let Self { path } = self;
239 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
240 Ok(fs::locked_read_to_string(path)?.abi_encode())
241 }
242}
243
244impl Cheatcode for readFileBinaryCall {
245 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
246 let Self { path } = self;
247 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
248 Ok(fs::locked_read(path)?.abi_encode())
249 }
250}
251
252impl Cheatcode for readLineCall {
253 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
254 let Self { path } = self;
255 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
256
257 let reader = match state.test_context.opened_read_files.entry(path.clone()) {
259 Entry::Occupied(entry) => entry.into_mut(),
260 Entry::Vacant(entry) => entry.insert(BufReader::new(fs::open(path)?)),
261 };
262
263 let mut line: String = String::new();
264 reader.read_line(&mut line)?;
265
266 if line.ends_with('\n') {
268 line.pop();
269 if line.ends_with('\r') {
270 line.pop();
271 }
272 }
273
274 Ok(line.abi_encode())
275 }
276}
277
278impl Cheatcode for readLinkCall {
279 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
280 let Self { linkPath: path } = self;
281 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
282 let target = fs::read_link(path)?;
283 Ok(target.display().to_string().abi_encode())
284 }
285}
286
287impl Cheatcode for removeDirCall {
288 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
289 let Self { path, recursive } = self;
290 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
291 if *recursive { fs::remove_dir_all(path) } else { fs::remove_dir(path) }?;
292 Ok(Default::default())
293 }
294}
295
296impl Cheatcode for removeFileCall {
297 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
298 let Self { path } = self;
299 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
300 state.config.ensure_not_foundry_toml(&path)?;
301
302 state.test_context.opened_read_files.remove(&path);
304
305 if state.fs_commit {
306 fs::remove_file(&path)?;
307 }
308
309 Ok(Default::default())
310 }
311}
312
313impl Cheatcode for writeFileCall {
314 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
315 let Self { path, data } = self;
316 write_file(state, path.as_ref(), data.as_bytes())
317 }
318}
319
320impl Cheatcode for writeFileBinaryCall {
321 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
322 let Self { path, data } = self;
323 write_file(state, path.as_ref(), data)
324 }
325}
326
327impl Cheatcode for writeLineCall {
328 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
329 let Self { path, data: line } = self;
330 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
331 state.config.ensure_not_foundry_toml(&path)?;
332
333 if state.fs_commit {
334 fs::locked_write_line(path, line)?;
335 }
336
337 Ok(Default::default())
338 }
339}
340
341impl Cheatcode for getArtifactPathByCodeCall {
342 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
343 let Self { code } = self;
344 let (artifact_id, _) = state
345 .config
346 .available_artifacts
347 .as_ref()
348 .and_then(|artifacts| artifacts.find_by_creation_code(code))
349 .ok_or_else(|| fmt_err!("no matching artifact found"))?;
350
351 Ok(artifact_id.path.to_string_lossy().abi_encode())
352 }
353}
354
355impl Cheatcode for getArtifactPathByDeployedCodeCall {
356 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
357 let Self { deployedCode } = self;
358 let (artifact_id, _) = state
359 .config
360 .available_artifacts
361 .as_ref()
362 .and_then(|artifacts| artifacts.find_by_deployed_code(deployedCode))
363 .ok_or_else(|| fmt_err!("no matching artifact found"))?;
364
365 Ok(artifact_id.path.to_string_lossy().abi_encode())
366 }
367}
368
369impl Cheatcode for getCodeCall {
370 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
371 let Self { artifactPath: path } = self;
372 Ok(get_artifact_code(state, path, false)?.abi_encode())
373 }
374}
375
376impl Cheatcode for getDeployedCodeCall {
377 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
378 let Self { artifactPath: path } = self;
379 Ok(get_artifact_code(state, path, true)?.abi_encode())
380 }
381}
382
383impl Cheatcode for getSelectorsCall {
384 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
385 let Self { artifactPath: path } = self;
386 let selectors: Vec<FixedBytes<4>> = match get_artifact_source(state, path)? {
387 ArtifactSource::InMemory(data) => data.abi.functions().map(|f| f.selector()).collect(),
388 ArtifactSource::Disk(path) => {
389 let data = read_artifact_file(state, &path)?;
390 let json: serde_json::Value = serde_json::from_str(&data)?;
393 let abi =
394 json.get("abi").ok_or_else(|| fmt_err!("no `abi` field in artifact JSON"))?;
395 let abi: alloy_json_abi::JsonAbi =
396 serde_json::from_value(abi.clone()).map_err(|e| fmt_err!("{e}"))?;
397 abi.functions().map(|f| f.selector()).collect()
398 }
399 };
400 Ok(selectors.abi_encode())
401 }
402}
403
404impl Cheatcode for deployCode_0Call {
405 fn apply_full<FEN: FoundryEvmNetwork>(
406 &self,
407 ccx: &mut CheatsCtxt<'_, '_, FEN>,
408 executor: &mut dyn CheatcodesExecutor<FEN>,
409 ) -> Result {
410 let Self { artifactPath: path } = self;
411 deploy_code(ccx, executor, path, None, None, None)
412 }
413}
414
415impl Cheatcode for deployCode_1Call {
416 fn apply_full<FEN: FoundryEvmNetwork>(
417 &self,
418 ccx: &mut CheatsCtxt<'_, '_, FEN>,
419 executor: &mut dyn CheatcodesExecutor<FEN>,
420 ) -> Result {
421 let Self { artifactPath: path, constructorArgs: args } = self;
422 deploy_code(ccx, executor, path, Some(args), None, None)
423 }
424}
425
426impl Cheatcode for deployCode_2Call {
427 fn apply_full<FEN: FoundryEvmNetwork>(
428 &self,
429 ccx: &mut CheatsCtxt<'_, '_, FEN>,
430 executor: &mut dyn CheatcodesExecutor<FEN>,
431 ) -> Result {
432 let Self { artifactPath: path, value } = self;
433 deploy_code(ccx, executor, path, None, Some(*value), None)
434 }
435}
436
437impl Cheatcode for deployCode_3Call {
438 fn apply_full<FEN: FoundryEvmNetwork>(
439 &self,
440 ccx: &mut CheatsCtxt<'_, '_, FEN>,
441 executor: &mut dyn CheatcodesExecutor<FEN>,
442 ) -> Result {
443 let Self { artifactPath: path, constructorArgs: args, value } = self;
444 deploy_code(ccx, executor, path, Some(args), Some(*value), None)
445 }
446}
447
448impl Cheatcode for deployCode_4Call {
449 fn apply_full<FEN: FoundryEvmNetwork>(
450 &self,
451 ccx: &mut CheatsCtxt<'_, '_, FEN>,
452 executor: &mut dyn CheatcodesExecutor<FEN>,
453 ) -> Result {
454 let Self { artifactPath: path, salt } = self;
455 deploy_code(ccx, executor, path, None, None, Some((*salt).into()))
456 }
457}
458
459impl Cheatcode for deployCode_5Call {
460 fn apply_full<FEN: FoundryEvmNetwork>(
461 &self,
462 ccx: &mut CheatsCtxt<'_, '_, FEN>,
463 executor: &mut dyn CheatcodesExecutor<FEN>,
464 ) -> Result {
465 let Self { artifactPath: path, constructorArgs: args, salt } = self;
466 deploy_code(ccx, executor, path, Some(args), None, Some((*salt).into()))
467 }
468}
469
470impl Cheatcode for deployCode_6Call {
471 fn apply_full<FEN: FoundryEvmNetwork>(
472 &self,
473 ccx: &mut CheatsCtxt<'_, '_, FEN>,
474 executor: &mut dyn CheatcodesExecutor<FEN>,
475 ) -> Result {
476 let Self { artifactPath: path, value, salt } = self;
477 deploy_code(ccx, executor, path, None, Some(*value), Some((*salt).into()))
478 }
479}
480
481impl Cheatcode for deployCode_7Call {
482 fn apply_full<FEN: FoundryEvmNetwork>(
483 &self,
484 ccx: &mut CheatsCtxt<'_, '_, FEN>,
485 executor: &mut dyn CheatcodesExecutor<FEN>,
486 ) -> Result {
487 let Self { artifactPath: path, constructorArgs: args, value, salt } = self;
488 deploy_code(ccx, executor, path, Some(args), Some(*value), Some((*salt).into()))
489 }
490}
491
492fn deploy_code<FEN: FoundryEvmNetwork>(
495 ccx: &mut CheatsCtxt<'_, '_, FEN>,
496 executor: &mut dyn CheatcodesExecutor<FEN>,
497 path: &str,
498 constructor_args: Option<&Bytes>,
499 value: Option<U256>,
500 salt: Option<U256>,
501) -> Result {
502 let mut bytecode = get_artifact_code(ccx.state, path, false)?.to_vec();
503
504 if let Some(broadcast) = &mut ccx.state.broadcast {
506 broadcast.deploy_from_code = true;
507 }
508
509 if let Some(args) = constructor_args {
510 bytecode.extend_from_slice(args);
511 }
512
513 let scheme =
514 if let Some(salt) = salt { CreateScheme::Create2 { salt } } else { CreateScheme::Create };
515
516 let caller =
518 ccx.state.get_prank(ccx.ecx.journal().depth()).map_or(ccx.caller, |prank| prank.new_caller);
519
520 let outcome = exec_create(
521 executor,
522 CreateInputs::new(
523 caller,
524 scheme,
525 value.unwrap_or(U256::ZERO),
526 bytecode.into(),
527 ccx.gas_limit,
528 0,
529 ),
530 ccx,
531 )?;
532
533 if !outcome.result.result.is_ok() {
534 return Err(crate::Error::from(outcome.result.output));
535 }
536
537 let address = outcome.address.ok_or_else(|| fmt_err!("contract creation failed"))?;
538
539 Ok(address.abi_encode())
540}
541
542enum ArtifactSource<'a> {
544 InMemory(&'a ContractData),
546 Disk(PathBuf),
548}
549
550fn get_artifact_source<'a, FEN: FoundryEvmNetwork>(
564 state: &'a Cheatcodes<FEN>,
565 path: &str,
566) -> Result<ArtifactSource<'a>> {
567 if path.ends_with(".json") {
568 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
569 return Ok(ArtifactSource::Disk(path));
570 }
571
572 let parsed =
573 parse_artifact_path(path).map_err(|e| fmt_err!("failed to parse artifact path: {e}"))?;
574 let ParsedArtifactPath { file, contract_name, version, profile } = parsed;
575 let file = file.map(|file| {
576 let cwd = state
577 .config
578 .running_artifact
579 .as_ref()
580 .and_then(|artifact| artifact.source.parent())
581 .unwrap_or(&state.config.paths.root);
582 let relative_cwd = cwd.strip_prefix(&state.config.paths.root).unwrap_or(cwd);
583 let has_matching_remapping = state.config.paths.remappings.iter().any(|remapping| {
584 remapping.context.as_ref().is_none_or(|context| relative_cwd.starts_with(context))
585 && file.strip_prefix(&remapping.name).is_ok()
586 });
587
588 if has_matching_remapping {
589 state.config.paths.resolve_library_import(cwd, &file).map_or(file, |resolved| {
590 resolved.strip_prefix(&state.config.paths.root).unwrap_or(&resolved).to_path_buf()
591 })
592 } else {
593 file
594 }
595 });
596
597 if let Some(artifacts) = &state.config.available_artifacts {
599 let ambiguous_file_profile =
600 file.is_some() && version.is_none() && profile.is_none() && contract_name.is_some();
601 let filter_artifacts = |treat_ambiguous_as_profile: bool| -> Vec<_> {
602 artifacts
603 .iter()
604 .filter(|(id, _)| {
605 let id_name = id.name.split('.').next().unwrap();
607
608 if let Some(path) = &file
609 && !id.source.ends_with(path)
610 {
611 return false;
612 }
613 if let Some(ref version) = version
614 && (id.version.minor != version.minor
615 || id.version.major != version.major
616 || id.version.patch != version.patch)
617 {
618 return false;
619 }
620 if let Some(profile) = profile
621 && id.profile != profile
622 {
623 return false;
624 }
625 if let Some(name) = contract_name {
626 if treat_ambiguous_as_profile && ambiguous_file_profile {
627 return id.profile == name;
628 }
629
630 return id_name == name;
631 }
632
633 true
634 })
635 .collect()
636 };
637
638 let mut filtered = filter_artifacts(false);
639 if filtered.is_empty() && ambiguous_file_profile {
640 filtered = filter_artifacts(true);
641 }
642
643 let artifact = match &filtered[..] {
644 [] => None,
645 [artifact] => Some(Ok(*artifact)),
646 filtered => {
647 let mut filtered = filtered.to_vec();
648 Some(
650 state
651 .config
652 .running_artifact
653 .as_ref()
654 .and_then(|running| {
655 if version.is_none() {
657 filtered.retain(|(id, _)| id.version == running.version);
658
659 if filtered.len() == 1 {
661 return Some(filtered[0]);
662 }
663 }
664
665 if profile.is_none() {
667 filtered.retain(|(id, _)| id.profile == running.profile);
668
669 return (filtered.len() == 1).then(|| filtered[0]);
670 }
671
672 None
673 })
674 .ok_or_else(|| fmt_err!("multiple matching artifacts found")),
675 )
676 }
677 };
678
679 if let Some(artifact) = artifact {
680 return Ok(ArtifactSource::InMemory(artifact?.1));
681 }
682 }
683
684 let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name) {
686 (Some(file), Some(contract_name)) => PathBuf::from(format!("{file}/{contract_name}.json")),
687 (None, Some(contract_name)) => {
688 PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
689 }
690 (Some(file), None) => {
691 let name = file.replace(".sol", "");
692 PathBuf::from(format!("{file}/{name}.json"))
693 }
694 _ => bail!("invalid artifact path"),
695 };
696
697 let path = state.config.paths.artifacts.join(path_in_artifacts);
698 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
699 Ok(ArtifactSource::Disk(path))
700}
701
702fn read_artifact_file<FEN: FoundryEvmNetwork>(
705 state: &Cheatcodes<FEN>,
706 path: &Path,
707) -> Result<String> {
708 fs::read_to_string(path).map_err(|e| {
709 if state.config.available_artifacts.is_some() {
710 fmt_err!("no matching artifact found")
711 } else {
712 e.into()
713 }
714 })
715}
716
717fn get_artifact_code<FEN: FoundryEvmNetwork>(
725 state: &Cheatcodes<FEN>,
726 path: &str,
727 deployed: bool,
728) -> Result<Bytes> {
729 let maybe_bytecode = match get_artifact_source(state, path)? {
730 ArtifactSource::InMemory(data) => {
731 if deployed { data.deployed_bytecode() } else { data.bytecode() }.cloned()
732 }
733 ArtifactSource::Disk(path) => {
734 let data = read_artifact_file(state, &path)?;
735 let artifact = serde_json::from_str::<ContractObject>(&data)?;
736 if deployed { artifact.deployed_bytecode } else { artifact.bytecode }
737 }
738 };
739 maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
740}
741
742impl Cheatcode for ffiCall {
743 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
744 let Self { commandInput: input } = self;
745
746 let output = ffi(state, input)?;
747
748 if output.exitCode != 0 {
750 return Err(fmt_err!(
752 "ffi command {:?} exited with code {}. stderr: {}",
753 input,
754 output.exitCode,
755 String::from_utf8_lossy(&output.stderr)
756 ));
757 }
758
759 if !output.stderr.is_empty() {
761 let stderr = String::from_utf8_lossy(&output.stderr);
762 warn!(target: "cheatcodes", ?input, ?stderr, "ffi command wrote to stderr");
763 }
764
765 Ok(output.stdout.abi_encode())
767 }
768}
769
770impl Cheatcode for tryFfiCall {
771 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
772 let Self { commandInput: input } = self;
773 ffi(state, input).map(|res| res.abi_encode())
774 }
775}
776
777impl Cheatcode for promptCall {
778 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
779 let Self { promptText: text } = self;
780 prompt(state, text, prompt_input).map(|res| res.abi_encode())
781 }
782}
783
784impl Cheatcode for promptSecretCall {
785 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
786 let Self { promptText: text } = self;
787 prompt(state, text, prompt_password).map(|res| res.abi_encode())
788 }
789}
790
791impl Cheatcode for promptSecretUintCall {
792 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
793 let Self { promptText: text } = self;
794 parse(&prompt(state, text, prompt_password)?, &DynSolType::Uint(256))
795 }
796}
797
798impl Cheatcode for promptAddressCall {
799 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
800 let Self { promptText: text } = self;
801 parse(&prompt(state, text, prompt_input)?, &DynSolType::Address)
802 }
803}
804
805impl Cheatcode for promptUintCall {
806 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
807 let Self { promptText: text } = self;
808 parse(&prompt(state, text, prompt_input)?, &DynSolType::Uint(256))
809 }
810}
811
812pub(super) fn write_file<FEN: FoundryEvmNetwork>(
813 state: &Cheatcodes<FEN>,
814 path: &Path,
815 contents: &[u8],
816) -> Result {
817 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
818 state.config.ensure_not_foundry_toml(&path)?;
820
821 if state.fs_commit {
822 fs::locked_write(path, contents)?;
823 }
824
825 Ok(Default::default())
826}
827
828fn read_dir<FEN: FoundryEvmNetwork>(
829 state: &Cheatcodes<FEN>,
830 path: &Path,
831 max_depth: u64,
832 follow_links: bool,
833) -> Result {
834 let root = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
835 let paths: Vec<DirEntry> = WalkDir::new(root)
836 .min_depth(1)
837 .max_depth(max_depth.try_into().unwrap_or(usize::MAX))
838 .follow_links(follow_links)
839 .contents_first(false)
840 .same_file_system(true)
841 .sort_by_file_name()
842 .into_iter()
843 .map(|entry| match entry {
844 Ok(entry) => DirEntry {
845 errorMessage: String::new(),
846 path: entry.path().display().to_string(),
847 depth: entry.depth() as u64,
848 isDir: entry.file_type().is_dir(),
849 isSymlink: entry.path_is_symlink(),
850 },
851 Err(e) => DirEntry {
852 errorMessage: e.to_string(),
853 path: e.path().map(|p| p.display().to_string()).unwrap_or_default(),
854 depth: e.depth() as u64,
855 isDir: false,
856 isSymlink: false,
857 },
858 })
859 .collect();
860 Ok(paths.abi_encode())
861}
862
863fn ffi<FEN: FoundryEvmNetwork>(state: &Cheatcodes<FEN>, input: &[String]) -> Result<FfiResult> {
864 ensure!(
865 state.config.ffi,
866 "FFI is disabled; add the `--ffi` flag to allow tests to call external commands"
867 );
868 ensure!(!input.is_empty() && !input[0].is_empty(), "can't execute empty command");
869 let mut cmd = Command::new(&input[0]);
870 cmd.args(&input[1..]);
871
872 debug!(target: "cheatcodes", ?cmd, "invoking ffi");
873
874 let output = cmd
875 .current_dir(&state.config.root)
876 .output()
877 .map_err(|err| fmt_err!("failed to execute command {cmd:?}: {err}"))?;
878
879 let trimmed_stdout = String::from_utf8(output.stdout)?;
882 let trimmed_stdout = trimmed_stdout.trim();
883 let encoded_stdout = if let Ok(hex) = hex::decode(trimmed_stdout) {
884 hex
885 } else {
886 trimmed_stdout.as_bytes().to_vec()
887 };
888 Ok(FfiResult {
889 exitCode: output.status.code().unwrap_or(69),
890 stdout: encoded_stdout.into(),
891 stderr: output.stderr.into(),
892 })
893}
894
895fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> {
896 Input::new().allow_empty(true).with_prompt(prompt_text).interact_text()
897}
898
899fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> {
900 Password::new().with_prompt(prompt_text).interact()
901}
902
903fn prompt<FEN: FoundryEvmNetwork>(
904 state: &Cheatcodes<FEN>,
905 prompt_text: &str,
906 input: fn(&str) -> Result<String, dialoguer::Error>,
907) -> Result<String> {
908 let text_clone = prompt_text.to_string();
909 let timeout = state.config.prompt_timeout;
910 let (tx, rx) = mpsc::channel();
911
912 thread::spawn(move || {
913 let _ = tx.send(input(&text_clone));
914 });
915
916 match rx.recv_timeout(timeout) {
917 Ok(res) => res.map_err(|err| {
918 let _ = sh_println!();
919 err.to_string().into()
920 }),
921 Err(_) => {
922 let _ = sh_eprintln!();
923 Err("Prompt timed out".into())
924 }
925 }
926}
927
928impl Cheatcode for getBroadcastCall {
929 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
930 let Self { contractName, chainId, txType } = self;
931
932 let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
933 contractName,
934 *chainId,
935 &state.config.broadcast,
936 vec![map_broadcast_tx_type(*txType)],
937 )?;
938
939 Ok(latest_broadcast.abi_encode())
940 }
941}
942
943impl Cheatcode for getBroadcasts_0Call {
944 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
945 let Self { contractName, chainId, txType } = self;
946
947 let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
948 .with_tx_type(map_broadcast_tx_type(*txType));
949
950 let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
951
952 let summaries = broadcasts
953 .into_iter()
954 .flat_map(|broadcast| {
955 let results = reader.into_tx_receipts(broadcast);
956 parse_broadcast_results(results)
957 })
958 .collect::<Vec<_>>();
959
960 Ok(summaries.abi_encode())
961 }
962}
963
964impl Cheatcode for getBroadcasts_1Call {
965 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
966 let Self { contractName, chainId } = self;
967
968 let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?;
969
970 let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
971
972 let summaries = broadcasts
973 .into_iter()
974 .flat_map(|broadcast| {
975 let results = reader.into_tx_receipts(broadcast);
976 parse_broadcast_results(results)
977 })
978 .collect::<Vec<_>>();
979
980 Ok(summaries.abi_encode())
981 }
982}
983
984impl Cheatcode for getDeployment_0Call {
985 fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
986 let Self { contractName } = self;
987 let chain_id = ccx.ecx.cfg().chain_id();
988
989 let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
990 contractName,
991 chain_id,
992 &ccx.state.config.broadcast,
993 vec![CallKind::Create, CallKind::Create2],
994 )?;
995
996 Ok(latest_broadcast.contractAddress.abi_encode())
997 }
998}
999
1000impl Cheatcode for getDeployment_1Call {
1001 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1002 let Self { contractName, chainId } = self;
1003
1004 let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
1005 contractName,
1006 *chainId,
1007 &state.config.broadcast,
1008 vec![CallKind::Create, CallKind::Create2],
1009 )?;
1010
1011 Ok(latest_broadcast.contractAddress.abi_encode())
1012 }
1013}
1014
1015impl Cheatcode for getDeploymentsCall {
1016 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1017 let Self { contractName, chainId } = self;
1018
1019 let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
1020 .with_tx_type(CallKind::Create)
1021 .with_tx_type(CallKind::Create2);
1022
1023 let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
1024
1025 let summaries = broadcasts
1026 .into_iter()
1027 .flat_map(|broadcast| {
1028 let results = reader.into_tx_receipts(broadcast);
1029 parse_broadcast_results(results)
1030 })
1031 .collect::<Vec<_>>();
1032
1033 let deployed_addresses =
1034 summaries.into_iter().map(|summary| summary.contractAddress).collect::<Vec<_>>();
1035
1036 Ok(deployed_addresses.abi_encode())
1037 }
1038}
1039
1040fn map_broadcast_tx_type(tx_type: BroadcastTxType) -> CallKind {
1041 match tx_type {
1042 BroadcastTxType::Call => CallKind::Call,
1043 BroadcastTxType::Create => CallKind::Create,
1044 BroadcastTxType::Create2 => CallKind::Create2,
1045 _ => unreachable!("invalid tx type"),
1046 }
1047}
1048
1049fn parse_broadcast_results<N: Network>(
1050 results: Vec<(TransactionWithMetadata<N>, N::ReceiptResponse)>,
1051) -> Vec<BroadcastTxSummary> {
1052 results
1053 .into_iter()
1054 .map(|(tx, receipt)| BroadcastTxSummary {
1055 txHash: receipt.transaction_hash(),
1056 blockNumber: receipt.block_number().unwrap_or_default(),
1057 txType: match tx.call_kind {
1058 CallKind::Call => BroadcastTxType::Call,
1059 CallKind::Create => BroadcastTxType::Create,
1060 CallKind::Create2 => BroadcastTxType::Create2,
1061 _ => unreachable!("invalid tx type"),
1062 },
1063 contractAddress: tx.contract_address.unwrap_or_default(),
1064 success: receipt.status(),
1065 })
1066 .collect()
1067}
1068
1069fn latest_broadcast<N: Network>(
1070 contract_name: &String,
1071 chain_id: u64,
1072 broadcast_path: &Path,
1073 filters: Vec<CallKind>,
1074) -> Result<BroadcastTxSummary>
1075where
1076 N::TxEnvelope: for<'d> serde::Deserialize<'d>,
1077{
1078 let mut reader = BroadcastReader::new(contract_name.clone(), chain_id, broadcast_path)?;
1079
1080 for filter in filters {
1081 reader = reader.with_tx_type(filter);
1082 }
1083
1084 let broadcast = reader.read_latest::<N>()?;
1085
1086 let results = reader.into_tx_receipts(broadcast);
1087
1088 let summaries = parse_broadcast_results(results);
1089
1090 summaries
1091 .first()
1092 .ok_or_else(|| fmt_err!("no deployment found for {contract_name} on chain {chain_id}"))
1093 .cloned()
1094}
1095
1096#[cfg(test)]
1097mod tests {
1098 use super::*;
1099 use crate::CheatsConfig;
1100 use alloy_primitives::{address, b256};
1101 use foundry_common::ContractsByArtifact;
1102 use foundry_compilers::{
1103 ArtifactId,
1104 artifacts::{
1105 BytecodeObject, CompactBytecode, CompactContractBytecode, remappings::Remapping,
1106 },
1107 };
1108 use foundry_evm_core::evm::TempoEvmNetwork;
1109 use std::{env, fs as stdfs, str::FromStr, sync::Arc};
1110 use tempfile::TempDir;
1111
1112 fn cheats() -> Cheatcodes {
1113 let config = CheatsConfig {
1114 ffi: true,
1115 root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1116 ..Default::default()
1117 };
1118 Cheatcodes::new(Arc::new(config))
1119 }
1120
1121 #[test]
1122 fn test_ffi_hex() {
1123 let msg = b"gm";
1124 let cheats = cheats();
1125 let args = ["echo".to_string(), hex::encode(msg)];
1126 let output = ffi(&cheats, &args).unwrap();
1127 assert_eq!(output.stdout, Bytes::from(msg));
1128 }
1129
1130 #[test]
1131 fn test_ffi_string() {
1132 let msg = "gm";
1133 let cheats = cheats();
1134 let args = ["echo".to_string(), msg.to_string()];
1135 let output = ffi(&cheats, &args).unwrap();
1136 assert_eq!(output.stdout, Bytes::from(msg.as_bytes()));
1137 }
1138
1139 #[test]
1140 fn test_ffi_fails_on_error_code() {
1141 let mut cheats = cheats();
1142
1143 #[cfg(unix)]
1145 let args = vec!["false".to_string()];
1146 #[cfg(windows)]
1147 let args = vec!["cmd".to_string(), "/c".to_string(), "exit 1".to_string()];
1148
1149 let result = Cheatcode::apply(&ffiCall { commandInput: args }, &mut cheats);
1150
1151 assert!(result.is_err(), "Expected ffi cheatcode to fail, but it succeeded");
1153
1154 let err_msg = result.unwrap_err().to_string();
1156 assert!(
1157 err_msg.contains("exited with code 1"),
1158 "Error message did not contain exit code: {err_msg}"
1159 );
1160 }
1161
1162 #[test]
1163 fn test_artifact_parsing() {
1164 let s = include_str!("../../evm/test-data/solc-obj.json");
1165 let artifact: ContractObject = serde_json::from_str(s).unwrap();
1166 assert!(artifact.bytecode.is_some());
1167
1168 let artifact: ContractObject = serde_json::from_str(s).unwrap();
1169 assert!(artifact.deployed_bytecode.is_some());
1170 }
1171
1172 #[test]
1173 fn test_alloy_json_abi_rejects_unlinked_bytecode() {
1174 let artifact_json = r#"{
1175 "abi": [],
1176 "bytecode": "0x73__$987e73aeca5e61ce83e4cb0814d87beda9$__63baf2f868"
1177 }"#;
1178
1179 let result: Result<ContractObject, _> = serde_json::from_str(artifact_json);
1180 assert!(result.is_err(), "should reject unlinked bytecode with placeholders");
1181 let err = result.unwrap_err().to_string();
1182 assert!(err.contains("expected bytecode, found unlinked bytecode with placeholder"));
1183 }
1184
1185 #[test]
1186 fn test_parse_artifact_path_file_only() {
1187 let parsed = super::parse_artifact_path("path/to/Contract.sol").unwrap();
1188 assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1189 assert_eq!(parsed.contract_name, None);
1190 assert_eq!(parsed.version, None);
1191 assert_eq!(parsed.profile, None);
1192 }
1193
1194 #[test]
1195 fn test_parse_artifact_path_file_and_contract() {
1196 let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract").unwrap();
1197 assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1198 assert_eq!(parsed.contract_name, Some("MyContract"));
1199 assert_eq!(parsed.version, None);
1200 assert_eq!(parsed.profile, None);
1201 }
1202
1203 #[test]
1204 fn test_parse_artifact_path_file_contract_version() {
1205 let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract:0.8.23").unwrap();
1206 assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1207 assert_eq!(parsed.contract_name, Some("MyContract"));
1208 assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1209 assert_eq!(parsed.profile, None);
1210 }
1211
1212 #[test]
1213 fn test_parse_artifact_path_file_contract_profile() {
1214 let parsed =
1215 super::parse_artifact_path("path/to/Contract.sol:MyContract:optimized").unwrap();
1216 assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1217 assert_eq!(parsed.contract_name, Some("MyContract"));
1218 assert_eq!(parsed.version, None);
1219 assert_eq!(parsed.profile, Some("optimized"));
1220 }
1221
1222 #[test]
1223 fn test_parse_artifact_path_file_and_version() {
1224 let parsed = super::parse_artifact_path("path/to/Contract.sol:0.8.18").unwrap();
1225 assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1226 assert_eq!(parsed.contract_name, None);
1227 assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 18)));
1228 assert_eq!(parsed.profile, None);
1229 }
1230
1231 #[test]
1232 fn test_parse_artifact_path_file_and_profile() {
1233 let parsed = super::parse_artifact_path("Contract.sol:paris").unwrap();
1236 assert_eq!(parsed.file, Some(PathBuf::from("Contract.sol")));
1237 assert_eq!(parsed.contract_name, Some("paris"));
1238 assert_eq!(parsed.version, None);
1239 assert_eq!(parsed.profile, None);
1240 }
1241
1242 fn test_artifact(
1243 source: &str,
1244 name: &str,
1245 profile: &str,
1246 bytecode: Bytes,
1247 ) -> (ArtifactId, CompactContractBytecode) {
1248 (
1249 ArtifactId {
1250 path: PathBuf::from(format!("{source}/{name}.json")),
1251 name: name.to_owned(),
1252 source: PathBuf::from(source),
1253 version: Version::new(0, 8, 30),
1254 build_id: String::new(),
1255 profile: profile.to_owned(),
1256 },
1257 CompactContractBytecode {
1258 abi: Some(Default::default()),
1259 bytecode: Some(CompactBytecode {
1260 object: BytecodeObject::Bytecode(bytecode),
1261 source_map: None,
1262 link_references: Default::default(),
1263 }),
1264 deployed_bytecode: None,
1265 },
1266 )
1267 }
1268
1269 #[test]
1270 fn test_get_artifact_code_resolves_file_profile_ambiguity() {
1271 let default_bytecode = Bytes::from_static(&[0x60, 0x01]);
1272 let paris_bytecode = Bytes::from_static(&[0x60, 0x02]);
1273 let source = "src/GetCodeProfile.t.sol";
1274 let artifacts = ContractsByArtifact::new([
1275 test_artifact(source, "GetCodeProfile", "default", default_bytecode),
1276 test_artifact(source, "GetCodeProfile", "paris", paris_bytecode.clone()),
1277 ]);
1278 let config = CheatsConfig {
1279 available_artifacts: Some(artifacts),
1280 root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1281 ..Default::default()
1282 };
1283 let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1284
1285 let bytecode =
1286 super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1287
1288 assert_eq!(bytecode, paris_bytecode);
1289 }
1290
1291 #[test]
1292 fn test_get_artifact_code_prefers_contract_name_over_file_profile_ambiguity() {
1293 let profile_bytecode = Bytes::from_static(&[0x60, 0x02]);
1294 let contract_bytecode = Bytes::from_static(&[0x60, 0x03]);
1295 let source = "src/GetCodeProfile.t.sol";
1296 let artifacts = ContractsByArtifact::new([
1297 test_artifact(source, "GetCodeProfile", "paris", profile_bytecode),
1298 test_artifact(source, "paris", "default", contract_bytecode.clone()),
1299 ]);
1300 let config = CheatsConfig {
1301 available_artifacts: Some(artifacts),
1302 root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1303 ..Default::default()
1304 };
1305 let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1306
1307 let bytecode =
1308 super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1309
1310 assert_eq!(bytecode, contract_bytecode);
1311 }
1312
1313 #[test]
1314 fn test_get_artifact_code_resolves_remapping() {
1315 let bytecode = Bytes::from_static(&[0x60, 0x01]);
1316 let artifacts = ContractsByArtifact::new([test_artifact(
1317 "src/Something.sol",
1318 "Something",
1319 "default",
1320 bytecode.clone(),
1321 )]);
1322 let root = PathBuf::from(&env!("CARGO_MANIFEST_DIR"));
1323 let paths = foundry_compilers::ProjectPathsConfig::builder()
1324 .remapping(Remapping::from_str("@example/=src/").unwrap())
1325 .build_with_root(&root);
1326 let config = CheatsConfig {
1327 available_artifacts: Some(artifacts),
1328 root,
1329 paths,
1330 ..Default::default()
1331 };
1332 let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1333
1334 let resolved =
1335 super::get_artifact_code(&cheats, "@example/Something.sol:Something", false).unwrap();
1336
1337 assert_eq!(resolved, bytecode);
1338 }
1339
1340 #[test]
1341 fn test_get_artifact_code_preserves_project_path_on_library_collision() {
1342 let root_bytecode = Bytes::from_static(&[0x60, 0x01]);
1343 let library_bytecode = Bytes::from_static(&[0x60, 0x02]);
1344 let artifacts = ContractsByArtifact::new([
1345 test_artifact("src/Thing.sol", "RootThing", "default", root_bytecode.clone()),
1346 test_artifact("lib/src/Thing.sol", "LibThing", "default", library_bytecode),
1347 ]);
1348 let temp = TempDir::new().unwrap();
1349 let library = temp.path().join("lib");
1350 stdfs::create_dir_all(library.join("src")).unwrap();
1351 stdfs::write(library.join("src/Thing.sol"), "").unwrap();
1352 let paths = foundry_compilers::ProjectPathsConfig::builder()
1353 .remappings([])
1354 .lib(library)
1355 .build_with_root(temp.path());
1356 let config = CheatsConfig {
1357 available_artifacts: Some(artifacts),
1358 root: temp.path().to_path_buf(),
1359 paths,
1360 ..Default::default()
1361 };
1362 let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1363
1364 let resolved = super::get_artifact_code(&cheats, "src/Thing.sol:RootThing", false).unwrap();
1365
1366 assert_eq!(resolved, root_bytecode);
1367 }
1368
1369 #[test]
1370 fn test_parse_artifact_path_contract_only() {
1371 let parsed = super::parse_artifact_path("MyContract").unwrap();
1372 assert_eq!(parsed.file, None);
1373 assert_eq!(parsed.contract_name, Some("MyContract"));
1374 assert_eq!(parsed.version, None);
1375 assert_eq!(parsed.profile, None);
1376 }
1377
1378 #[test]
1379 fn test_parse_artifact_path_contract_and_version() {
1380 let parsed = super::parse_artifact_path("MyContract:0.8.23").unwrap();
1381 assert_eq!(parsed.file, None);
1382 assert_eq!(parsed.contract_name, Some("MyContract"));
1383 assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1384 assert_eq!(parsed.profile, None);
1385 }
1386
1387 #[test]
1388 fn test_parse_artifact_path_contract_and_profile() {
1389 let parsed = super::parse_artifact_path("MyContract:optimized").unwrap();
1390 assert_eq!(parsed.file, None);
1391 assert_eq!(parsed.contract_name, Some("MyContract"));
1392 assert_eq!(parsed.version, None);
1393 assert_eq!(parsed.profile, Some("optimized"));
1394 }
1395
1396 #[test]
1397 fn test_parse_artifact_path_profile_names() {
1398 for profile in ["v1", "v2", "paris", "optimized", "default", "prod", "dev"] {
1400 let path = format!("MyContract:{profile}");
1401 let parsed = super::parse_artifact_path(&path).unwrap();
1402 assert_eq!(parsed.contract_name, Some("MyContract"));
1403 assert_eq!(parsed.profile, Some(profile));
1404 assert_eq!(parsed.version, None);
1405 }
1406 }
1407
1408 #[test]
1409 fn test_parse_artifact_path_invalid_version() {
1410 let parsed = super::parse_artifact_path("MyContract:invalid").unwrap();
1412 assert_eq!(parsed.contract_name, Some("MyContract"));
1413 assert_eq!(parsed.profile, Some("invalid"));
1414 assert_eq!(parsed.version, None);
1415 }
1416
1417 fn unique_temp_dir(prefix: &str) -> PathBuf {
1418 env::temp_dir().join(format!(
1419 "foundry-cheatcodes-{prefix}-{}",
1420 SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()
1421 ))
1422 }
1423
1424 #[test]
1425 fn test_latest_broadcast_reads_tempo_sequences() {
1426 let root = unique_temp_dir("tempo-broadcast");
1427 let broadcast_path = root.join("broadcast");
1428 let sequence_dir = broadcast_path.join("Counter.s.sol").join("31337");
1429 stdfs::create_dir_all(&sequence_dir).unwrap();
1430
1431 let tx_hash = "0x04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f";
1432 let block_hash = "0x860f788b251ece768e63b0d3906d156f652d843848b71c7fe81faacd49139d66";
1433 let from = "0xa70ab0448e66cd77995bfbba5c5b64b41a85f3fd";
1434 let contract_address = "0x20c0000000000000000000000000000000000000";
1435 let zero_bloom = format!("0x{}", "0".repeat(512));
1436
1437 let sequence = serde_json::json!({
1438 "transactions": [{
1439 "hash": tx_hash,
1440 "transactionType": "CREATE",
1441 "contractName": "Counter",
1442 "contractAddress": contract_address,
1443 "function": serde_json::Value::Null,
1444 "arguments": serde_json::Value::Null,
1445 "transaction": {
1446 "type": "0x76",
1447 "from": from,
1448 "to": serde_json::Value::Null,
1449 "data": "0x",
1450 "value": "0x0",
1451 "gas": "0x5208",
1452 "nonce": "0x0",
1453 "accessList": [],
1454 "calls": [],
1455 "nonceKey": "0x0",
1456 "feePayerSignature": serde_json::Value::Null,
1457 "validBefore": serde_json::Value::Null,
1458 "validAfter": serde_json::Value::Null,
1459 "keyAuthorization": serde_json::Value::Null,
1460 "aaAuthorizationList": []
1461 },
1462 "additionalContracts": [],
1463 "isFixedGasLimit": false
1464 }],
1465 "receipts": [{
1466 "type": "0x76",
1467 "status": "0x1",
1468 "cumulativeGasUsed": "0x5208",
1469 "logs": [],
1470 "logsBloom": zero_bloom,
1471 "transactionHash": tx_hash,
1472 "transactionIndex": "0x0",
1473 "blockHash": block_hash,
1474 "blockNumber": "0x7",
1475 "gasUsed": "0x5208",
1476 "effectiveGasPrice": "0x1",
1477 "from": from,
1478 "to": serde_json::Value::Null,
1479 "contractAddress": contract_address,
1480 "feePayer": from
1481 }],
1482 "libraries": [],
1483 "pending": [],
1484 "returns": {},
1485 "timestamp": 1,
1486 "chain": 31337,
1487 "commit": serde_json::Value::Null
1488 });
1489
1490 fs::write_json_file(&sequence_dir.join("run-1.json"), &sequence).unwrap();
1491
1492 let latest = latest_broadcast::<<TempoEvmNetwork as FoundryEvmNetwork>::Network>(
1493 &"Counter".to_owned(),
1494 31337,
1495 &broadcast_path,
1496 vec![CallKind::Create],
1497 )
1498 .unwrap();
1499
1500 assert_eq!(
1501 latest.txHash,
1502 b256!("04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f")
1503 );
1504 assert_eq!(latest.blockNumber, 7);
1505 assert!(matches!(latest.txType, BroadcastTxType::Create));
1506 assert_eq!(latest.contractAddress, address!("20c0000000000000000000000000000000000000"));
1507 assert!(latest.success);
1508
1509 stdfs::remove_dir_all(root).unwrap();
1510 }
1511}