Skip to content

Commit e4b52e2

Browse files
committed
refactor: extract shared get_artifact_source
1 parent 37e0199 commit e4b52e2

1 file changed

Lines changed: 77 additions & 174 deletions

File tree

  • crates/cheatcodes/src

crates/cheatcodes/src/fs.rs

Lines changed: 77 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ use crate::{
55
Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*, inspector::exec_create,
66
};
77
use alloy_dyn_abi::DynSolType;
8-
use alloy_json_abi::{ContractObject, JsonAbi};
8+
use alloy_json_abi::ContractObject;
99
use alloy_network::{Network, ReceiptResponse};
1010
use alloy_primitives::{Bytes, FixedBytes, U256, hex, map::Entry};
1111
use alloy_sol_types::SolValue;
1212
use dialoguer::{Input, Password};
1313
use forge_script_sequence::{BroadcastReader, TransactionWithMetadata};
14-
use foundry_common::fs;
14+
use foundry_common::{contracts::ContractData, fs};
1515
use foundry_config::fs_permissions::FsAccessKind;
1616
use foundry_evm_core::evm::FoundryEvmNetwork;
1717
use revm::{
@@ -313,9 +313,20 @@ impl Cheatcode for getDeployedCodeCall {
313313
impl Cheatcode for getSelectorsCall {
314314
fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
315315
let Self { artifactPath: path } = self;
316-
let abi = get_artifact_abi(state, path)?;
317-
let selectors: Vec<FixedBytes<4>> =
318-
abi.functions().map(|func| func.selector().into()).collect();
316+
let selectors: Vec<FixedBytes<4>> = match get_artifact_source(state, path)? {
317+
ArtifactSource::InMemory(data) => data.abi.functions().map(|f| f.selector()).collect(),
318+
ArtifactSource::Disk(path) => {
319+
let data = read_artifact_file(state, &path)?;
320+
// Parse as raw JSON rather than `ContractObject` so we can still read selectors
321+
// from artifacts with unlinked bytecode (which `ContractObject` rejects).
322+
let json: serde_json::Value = serde_json::from_str(&data)?;
323+
let abi =
324+
json.get("abi").ok_or_else(|| fmt_err!("no `abi` field in artifact JSON"))?;
325+
let abi: alloy_json_abi::JsonAbi =
326+
serde_json::from_value(abi.clone()).map_err(|e| fmt_err!("{e}"))?;
327+
abi.functions().map(|f| f.selector()).collect()
328+
}
329+
};
319330
Ok(selectors.abi_encode())
320331
}
321332
}
@@ -457,175 +468,31 @@ fn deploy_code<FEN: FoundryEvmNetwork>(
457468
Ok(address.abi_encode())
458469
}
459470

460-
/// Returns the bytecode from a JSON artifact file.
471+
/// Resolved location of an artifact referenced by a cheatcode path argument.
472+
enum ArtifactSource<'a> {
473+
/// The artifact was matched in the in-memory `available_artifacts` list.
474+
InMemory(&'a ContractData),
475+
/// The artifact must be read from the given path on disk.
476+
Disk(PathBuf),
477+
}
478+
479+
/// Resolves a cheatcode artifact reference to its source.
461480
///
462-
/// Can parse following input formats:
481+
/// Can parse the following input formats:
463482
/// - `path/to/artifact.json`
464483
/// - `path/to/contract.sol`
465484
/// - `path/to/contract.sol:ContractName`
466485
/// - `path/to/contract.sol:ContractName:0.8.23`
467486
/// - `path/to/contract.sol:0.8.23`
468487
/// - `ContractName`
469488
/// - `ContractName:0.8.23`
470-
///
471-
/// This function is safe to use with contracts that have library dependencies.
472-
/// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will
473-
/// reject artifacts with unlinked library placeholders.
474-
fn get_artifact_code<FEN: FoundryEvmNetwork>(
475-
state: &Cheatcodes<FEN>,
476-
path: &str,
477-
deployed: bool,
478-
) -> Result<Bytes> {
479-
let path = if path.ends_with(".json") {
480-
PathBuf::from(path)
481-
} else {
482-
let mut parts = path.split(':');
483-
484-
let mut file = None;
485-
let mut contract_name = None;
486-
let mut version = None;
487-
488-
let path_or_name = parts.next().unwrap();
489-
if path_or_name.contains('.') {
490-
file = Some(PathBuf::from(path_or_name));
491-
if let Some(name_or_version) = parts.next() {
492-
if name_or_version.contains('.') {
493-
version = Some(name_or_version);
494-
} else {
495-
contract_name = Some(name_or_version);
496-
version = parts.next();
497-
}
498-
}
499-
} else {
500-
contract_name = Some(path_or_name);
501-
version = parts.next();
502-
}
503-
504-
let version = if let Some(version) = version {
505-
Some(Version::parse(version).map_err(|e| fmt_err!("failed parsing version: {e}"))?)
506-
} else {
507-
None
508-
};
509-
510-
// Use available artifacts list if present
511-
if let Some(artifacts) = &state.config.available_artifacts {
512-
let filtered = artifacts
513-
.iter()
514-
.filter(|(id, _)| {
515-
// name might be in the form of "Counter.0.8.23"
516-
let id_name = id.name.split('.').next().unwrap();
517-
518-
if let Some(path) = &file
519-
&& !id.source.ends_with(path)
520-
{
521-
return false;
522-
}
523-
if let Some(name) = contract_name
524-
&& id_name != name
525-
{
526-
return false;
527-
}
528-
if let Some(ref version) = version
529-
&& (id.version.minor != version.minor
530-
|| id.version.major != version.major
531-
|| id.version.patch != version.patch)
532-
{
533-
return false;
534-
}
535-
true
536-
})
537-
.collect::<Vec<_>>();
538-
539-
let artifact = match &filtered[..] {
540-
[] => None,
541-
[artifact] => Some(Ok(*artifact)),
542-
filtered => {
543-
let mut filtered = filtered.to_vec();
544-
// If we know the current script/test contract solc version, try to filter by it
545-
Some(
546-
state
547-
.config
548-
.running_artifact
549-
.as_ref()
550-
.and_then(|running| {
551-
// Firstly filter by version
552-
filtered.retain(|(id, _)| id.version == running.version);
553-
554-
// Return artifact if only one matched
555-
if filtered.len() == 1 {
556-
return Some(filtered[0]);
557-
}
558-
559-
// Try filtering by profile as well
560-
filtered.retain(|(id, _)| id.profile == running.profile);
561-
562-
(filtered.len() == 1).then(|| filtered[0])
563-
})
564-
.ok_or_else(|| fmt_err!("multiple matching artifacts found")),
565-
)
566-
}
567-
};
568-
569-
if let Some(artifact) = artifact {
570-
let artifact = artifact?;
571-
let maybe_bytecode = if deployed {
572-
artifact.1.deployed_bytecode().cloned()
573-
} else {
574-
artifact.1.bytecode().cloned()
575-
};
576-
577-
return maybe_bytecode.ok_or_else(|| {
578-
fmt_err!("no bytecode for contract; is it abstract or unlinked?")
579-
});
580-
}
581-
}
582-
583-
// Fallback: construct path manually when no artifacts list or no match found
584-
let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name)
585-
{
586-
(Some(file), Some(contract_name)) => {
587-
PathBuf::from(format!("{file}/{contract_name}.json"))
588-
}
589-
(None, Some(contract_name)) => {
590-
PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
591-
}
592-
(Some(file), None) => {
593-
let name = file.replace(".sol", "");
594-
PathBuf::from(format!("{file}/{name}.json"))
595-
}
596-
_ => bail!("invalid artifact path"),
597-
};
598-
599-
state.config.paths.artifacts.join(path_in_artifacts)
600-
};
601-
602-
let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
603-
let data = fs::read_to_string(path).map_err(|e| {
604-
if state.config.available_artifacts.is_some() {
605-
fmt_err!("no matching artifact found")
606-
} else {
607-
e.into()
608-
}
609-
})?;
610-
let artifact = serde_json::from_str::<ContractObject>(&data)?;
611-
let maybe_bytecode = if deployed { artifact.deployed_bytecode } else { artifact.bytecode };
612-
maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
613-
}
614-
615-
/// Returns the ABI of a matching artifact from the given path.
616-
///
617-
/// See [`get_artifact_code`] for the supported path formats.
618-
fn get_artifact_abi<FEN: FoundryEvmNetwork>(
619-
state: &Cheatcodes<FEN>,
489+
fn get_artifact_source<'a, FEN: FoundryEvmNetwork>(
490+
state: &'a Cheatcodes<FEN>,
620491
path: &str,
621-
) -> Result<JsonAbi> {
492+
) -> Result<ArtifactSource<'a>> {
622493
if path.ends_with(".json") {
623-
// Read JSON artifact directly from disk.
624494
let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
625-
let data = fs::read_to_string(path)?;
626-
let json: serde_json::Value = serde_json::from_str(&data)?;
627-
let abi = json.get("abi").ok_or_else(|| fmt_err!("no `abi` field in artifact JSON"))?;
628-
return serde_json::from_value(abi.clone()).map_err(|e| fmt_err!("{e}"));
495+
return Ok(ArtifactSource::Disk(path));
629496
}
630497

631498
let mut parts = path.split(':');
@@ -661,6 +528,7 @@ fn get_artifact_abi<FEN: FoundryEvmNetwork>(
661528
let filtered = artifacts
662529
.iter()
663530
.filter(|(id, _)| {
531+
// name might be in the form of "Counter.0.8.23"
664532
let id_name = id.name.split('.').next().unwrap();
665533

666534
if let Some(path) = &file
@@ -689,17 +557,24 @@ fn get_artifact_abi<FEN: FoundryEvmNetwork>(
689557
[artifact] => Some(Ok(*artifact)),
690558
filtered => {
691559
let mut filtered = filtered.to_vec();
560+
// If we know the current script/test contract solc version, try to filter by it.
692561
Some(
693562
state
694563
.config
695564
.running_artifact
696565
.as_ref()
697566
.and_then(|running| {
567+
// Firstly filter by version.
698568
filtered.retain(|(id, _)| id.version == running.version);
569+
570+
// Return artifact if only one matched.
699571
if filtered.len() == 1 {
700572
return Some(filtered[0]);
701573
}
574+
575+
// Try filtering by profile as well.
702576
filtered.retain(|(id, _)| id.profile == running.profile);
577+
703578
(filtered.len() == 1).then(|| filtered[0])
704579
})
705580
.ok_or_else(|| fmt_err!("multiple matching artifacts found")),
@@ -708,16 +583,13 @@ fn get_artifact_abi<FEN: FoundryEvmNetwork>(
708583
};
709584

710585
if let Some(artifact) = artifact {
711-
let artifact = artifact?;
712-
return Ok(artifact.1.abi.clone());
586+
return Ok(ArtifactSource::InMemory(artifact?.1));
713587
}
714588
}
715589

716-
// Fallback: construct path manually and read JSON from disk.
590+
// Fallback: construct path manually when no artifacts list or no match found.
717591
let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name) {
718-
(Some(file), Some(contract_name)) => {
719-
PathBuf::from(format!("{file}/{contract_name}.json"))
720-
}
592+
(Some(file), Some(contract_name)) => PathBuf::from(format!("{file}/{contract_name}.json")),
721593
(None, Some(contract_name)) => {
722594
PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
723595
}
@@ -730,16 +602,47 @@ fn get_artifact_abi<FEN: FoundryEvmNetwork>(
730602

731603
let path = state.config.paths.artifacts.join(path_in_artifacts);
732604
let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
733-
let data = fs::read_to_string(path).map_err(|e| {
605+
Ok(ArtifactSource::Disk(path))
606+
}
607+
608+
/// Reads an artifact JSON file, mapping I/O errors to a helpful message when the
609+
/// lookup fell through the in-memory artifacts list.
610+
fn read_artifact_file<FEN: FoundryEvmNetwork>(
611+
state: &Cheatcodes<FEN>,
612+
path: &Path,
613+
) -> Result<String> {
614+
fs::read_to_string(path).map_err(|e| {
734615
if state.config.available_artifacts.is_some() {
735616
fmt_err!("no matching artifact found")
736617
} else {
737618
e.into()
738619
}
739-
})?;
740-
let json: serde_json::Value = serde_json::from_str(&data)?;
741-
let abi = json.get("abi").ok_or_else(|| fmt_err!("no `abi` field in artifact JSON"))?;
742-
serde_json::from_value(abi.clone()).map_err(|e| fmt_err!("{e}"))
620+
})
621+
}
622+
623+
/// Returns the bytecode from a JSON artifact file.
624+
///
625+
/// See [`get_artifact_source`] for the supported path formats.
626+
///
627+
/// This function is safe to use with contracts that have library dependencies.
628+
/// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will
629+
/// reject artifacts with unlinked library placeholders.
630+
fn get_artifact_code<FEN: FoundryEvmNetwork>(
631+
state: &Cheatcodes<FEN>,
632+
path: &str,
633+
deployed: bool,
634+
) -> Result<Bytes> {
635+
let maybe_bytecode = match get_artifact_source(state, path)? {
636+
ArtifactSource::InMemory(data) => {
637+
if deployed { data.deployed_bytecode() } else { data.bytecode() }.cloned()
638+
}
639+
ArtifactSource::Disk(path) => {
640+
let data = read_artifact_file(state, &path)?;
641+
let artifact = serde_json::from_str::<ContractObject>(&data)?;
642+
if deployed { artifact.deployed_bytecode } else { artifact.bytecode }
643+
}
644+
};
645+
maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
743646
}
744647

745648
impl Cheatcode for ffiCall {

0 commit comments

Comments
 (0)