Skip to content

Commit

Permalink
feat(forge, cast): add cast --with_local_artifacts/`forge selectors…
Browse files Browse the repository at this point in the history
… cache` to trace with local artifacts (#7359)

* add RunArgs generate_local_signatures to enable trace with local contracts functions and events

* make generate_local_signatures as a helper function

* rename generate_local_signatures to cache_local_signatures
merge project signatures with exists cached local signatures instead of
just override them

* extract duplicate method for CachedSignatures

* fix cache load path

* fix for lint

* fix fot lint

* remove unnecessary `let` binding

* fix for format check

* fix for clippy check

* fix for clippy check

* Move cache in forge selectors, use local artifacts for cast run and send traces

* Add test

* Review changes:
- compile without quiet, fix test
- merge local sources with etherscan

* Update crates/evm/traces/src/debug/sources.rs

Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>

---------

Co-authored-by: grandizzy <grandizzy.the.egg@gmail.com>
Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com>
Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
  • Loading branch information
4 people authored Nov 22, 2024
1 parent 8b7d5df commit 398ef4a
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 52 deletions.
16 changes: 15 additions & 1 deletion crates/cast/bin/cmd/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ pub struct CallArgs {

#[command(flatten)]
eth: EthereumOpts,

/// Use current project artifacts for trace decoding.
#[arg(long, visible_alias = "la")]
pub with_local_artifacts: bool,
}

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -127,6 +131,7 @@ impl CallArgs {
decode_internal,
labels,
data,
with_local_artifacts,
..
} = self;

Expand Down Expand Up @@ -195,7 +200,16 @@ impl CallArgs {
),
};

handle_traces(trace, &config, chain, labels, debug, decode_internal, false).await?;
handle_traces(
trace,
&config,
chain,
labels,
with_local_artifacts,
debug,
decode_internal,
)
.await?;

return Ok(());
}
Expand Down
8 changes: 6 additions & 2 deletions crates/cast/bin/cmd/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use foundry_cli::{
opts::{EtherscanOpts, RpcOpts},
utils::{handle_traces, init_progress, TraceResult},
};
use foundry_common::{is_known_system_sender, shell, SYSTEM_TRANSACTION_TYPE};
use foundry_common::{is_known_system_sender, SYSTEM_TRANSACTION_TYPE};
use foundry_compilers::artifacts::EvmVersion;
use foundry_config::{
figment::{
Expand Down Expand Up @@ -87,6 +87,10 @@ pub struct RunArgs {
/// Enables Alphanet features.
#[arg(long, alias = "odyssey")]
pub alphanet: bool,

/// Use current project artifacts for trace decoding.
#[arg(long, visible_alias = "la")]
pub with_local_artifacts: bool,
}

impl RunArgs {
Expand Down Expand Up @@ -251,9 +255,9 @@ impl RunArgs {
&config,
chain,
self.label,
self.with_local_artifacts,
self.debug,
self.decode_internal,
shell::verbosity() > 0,
)
.await?;

Expand Down
111 changes: 110 additions & 1 deletion crates/cast/tests/cli/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
//! Contains various tests for checking cast commands
use alloy_chains::NamedChain;
use alloy_network::TransactionResponse;
use alloy_primitives::{b256, B256};
use alloy_rpc_types::{BlockNumberOrTag, Index};
use anvil::{EthereumHardfork, NodeConfig};
use foundry_test_utils::{
casttest, file,
casttest, file, forgetest_async,
rpc::{
next_etherscan_api_key, next_http_rpc_endpoint, next_mainnet_etherscan_api_key,
next_rpc_endpoint, next_ws_rpc_endpoint,
Expand Down Expand Up @@ -1596,3 +1598,110 @@ casttest!(fetch_artifact_from_etherscan, |_prj, cmd| {
"#]]);
});

// tests cast can decode traces when using project artifacts
forgetest_async!(decode_traces_with_project_artifacts, |prj, cmd| {
let (api, handle) =
anvil::spawn(NodeConfig::test().with_disable_default_create2_deployer(true)).await;

foundry_test_utils::util::initialize(prj.root());
prj.add_source(
"LocalProjectContract",
r#"
contract LocalProjectContract {
event LocalProjectContractCreated(address owner);
constructor() {
emit LocalProjectContractCreated(msg.sender);
}
}
"#,
)
.unwrap();
prj.add_script(
"LocalProjectScript",
r#"
import "forge-std/Script.sol";
import {LocalProjectContract} from "../src/LocalProjectContract.sol";
contract LocalProjectScript is Script {
function run() public {
vm.startBroadcast();
new LocalProjectContract();
vm.stopBroadcast();
}
}
"#,
)
.unwrap();

cmd.args([
"script",
"--private-key",
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
"--rpc-url",
&handle.http_endpoint(),
"--broadcast",
"LocalProjectScript",
]);

cmd.assert_success();

let tx_hash = api
.transaction_by_block_number_and_index(BlockNumberOrTag::Latest, Index::from(0))
.await
.unwrap()
.unwrap()
.tx_hash();

// Assert cast with local artifacts from outside the project.
cmd.cast_fuse()
.args(["run", "--la", format!("{tx_hash}").as_str(), "--rpc-url", &handle.http_endpoint()])
.assert_success()
.stdout_eq(str![[r#"
Executing previous transactions from the block.
Compiling project to generate artifacts
Nothing to compile
"#]]);

// Run cast from project dir.
cmd.cast_fuse().set_current_dir(prj.root());

// Assert cast without local artifacts cannot decode traces.
cmd.cast_fuse()
.args(["run", format!("{tx_hash}").as_str(), "--rpc-url", &handle.http_endpoint()])
.assert_success()
.stdout_eq(str![[r#"
Executing previous transactions from the block.
Traces:
[13520] → new <unknown>@0x5FbDB2315678afecb367f032d93F642f64180aa3
├─ emit topic 0: 0xa7263295d3a687d750d1fd377b5df47de69d7db8decc745aaa4bbee44dc1688d
│ data: 0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
└─ ← [Return] 62 bytes of code
Transaction successfully executed.
[GAS]
"#]]);

// Assert cast with local artifacts can decode traces.
cmd.cast_fuse()
.args(["run", "--la", format!("{tx_hash}").as_str(), "--rpc-url", &handle.http_endpoint()])
.assert_success()
.stdout_eq(str![[r#"
Executing previous transactions from the block.
Compiling project to generate artifacts
No files changed, compilation skipped
Traces:
[13520] → new LocalProjectContract@0x5FbDB2315678afecb367f032d93F642f64180aa3
├─ emit LocalProjectContractCreated(owner: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266)
└─ ← [Return] 62 bytes of code
Transaction successfully executed.
[GAS]
"#]]);
});
102 changes: 69 additions & 33 deletions crates/cli/src/utils/cmd.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use alloy_json_abi::JsonAbi;
use alloy_primitives::Address;
use eyre::{Result, WrapErr};
use foundry_common::{fs, TestFunctionExt};
use foundry_common::{compile::ProjectCompiler, fs, shell, ContractsByArtifact, TestFunctionExt};
use foundry_compilers::{
artifacts::{CompactBytecode, Settings},
cache::{CacheEntry, CompilerCache},
Expand All @@ -14,9 +14,9 @@ use foundry_evm::{
executors::{DeployResult, EvmError, RawCallResult},
opts::EvmOpts,
traces::{
debug::DebugTraceIdentifier,
debug::{ContractSources, DebugTraceIdentifier},
decode_trace_arena,
identifier::{EtherscanIdentifier, SignaturesIdentifier},
identifier::{CachedSignatures, SignaturesIdentifier, TraceIdentifiers},
render_trace_arena_with_bytecodes, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind,
Traces,
},
Expand Down Expand Up @@ -383,10 +383,25 @@ pub async fn handle_traces(
config: &Config,
chain: Option<Chain>,
labels: Vec<String>,
with_local_artifacts: bool,
debug: bool,
decode_internal: bool,
verbose: bool,
) -> Result<()> {
let (known_contracts, mut sources) = if with_local_artifacts {
let _ = sh_println!("Compiling project to generate artifacts");
let project = config.project()?;
let compiler = ProjectCompiler::new();
let output = compiler.compile(&project)?;
(
Some(ContractsByArtifact::new(
output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
)),
ContractSources::from_project_output(&output, project.root(), None)?,
)
} else {
(None, ContractSources::default())
};

let labels = labels.iter().filter_map(|label_str| {
let mut iter = label_str.split(':');

Expand All @@ -398,45 +413,44 @@ pub async fn handle_traces(
None
});
let config_labels = config.labels.clone().into_iter();
let mut decoder = CallTraceDecoderBuilder::new()

let mut builder = CallTraceDecoderBuilder::new()
.with_labels(labels.chain(config_labels))
.with_signature_identifier(SignaturesIdentifier::new(
Config::foundry_cache_dir(),
config.offline,
)?)
.build();
)?);
let mut identifier = TraceIdentifiers::new().with_etherscan(config, chain)?;
if let Some(contracts) = &known_contracts {
builder = builder.with_known_contracts(contracts);
identifier = identifier.with_local(contracts);
}

let mut etherscan_identifier = EtherscanIdentifier::new(config, chain)?;
if let Some(etherscan_identifier) = &mut etherscan_identifier {
for (_, trace) in result.traces.as_deref_mut().unwrap_or_default() {
decoder.identify(trace, etherscan_identifier);
}
let mut decoder = builder.build();

for (_, trace) in result.traces.as_deref_mut().unwrap_or_default() {
decoder.identify(trace, &mut identifier);
}

if decode_internal {
let sources = if let Some(etherscan_identifier) = &etherscan_identifier {
etherscan_identifier.get_compiled_contracts().await?
} else {
Default::default()
};
if decode_internal || debug {
if let Some(ref etherscan_identifier) = identifier.etherscan {
sources.merge(etherscan_identifier.get_compiled_contracts().await?);
}

if debug {
let mut debugger = Debugger::builder()
.traces(result.traces.expect("missing traces"))
.decoder(&decoder)
.sources(sources)
.build();
debugger.try_run_tui()?;
return Ok(())
}

decoder.debug_identifier = Some(DebugTraceIdentifier::new(sources));
}

if debug {
let sources = if let Some(etherscan_identifier) = etherscan_identifier {
etherscan_identifier.get_compiled_contracts().await?
} else {
Default::default()
};
let mut debugger = Debugger::builder()
.traces(result.traces.expect("missing traces"))
.decoder(&decoder)
.sources(sources)
.build();
debugger.try_run_tui()?;
} else {
print_traces(&mut result, &decoder, verbose).await?;
}
print_traces(&mut result, &decoder, shell::verbosity() > 0).await?;

Ok(())
}
Expand Down Expand Up @@ -464,3 +478,25 @@ pub async fn print_traces(
sh_println!("Gas used: {}", result.gas_used)?;
Ok(())
}

/// Traverse the artifacts in the project to generate local signatures and merge them into the cache
/// file.
pub fn cache_local_signatures(output: &ProjectCompileOutput, cache_path: PathBuf) -> Result<()> {
let path = cache_path.join("signatures");
let mut cached_signatures = CachedSignatures::load(cache_path);
output.artifacts().for_each(|(_, artifact)| {
if let Some(abi) = &artifact.abi {
for func in abi.functions() {
cached_signatures.functions.insert(func.selector().to_string(), func.signature());
}
for event in abi.events() {
cached_signatures
.events
.insert(event.selector().to_string(), event.full_signature());
}
}
});

fs::write_json_file(&path, &cached_signatures)?;
Ok(())
}
8 changes: 8 additions & 0 deletions crates/evm/traces/src/debug/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,14 @@ impl ContractSources {
Ok(())
}

/// Merges given contract sources.
pub fn merge(&mut self, sources: Self) {
self.sources_by_id.extend(sources.sources_by_id);
for (name, artifacts) in sources.artifacts_by_name {
self.artifacts_by_name.entry(name).or_default().extend(artifacts);
}
}

/// Returns all sources for a contract by name.
pub fn get_sources(
&self,
Expand Down
2 changes: 1 addition & 1 deletion crates/evm/traces/src/identifier/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mod etherscan;
pub use etherscan::EtherscanIdentifier;

mod signatures;
pub use signatures::{SignaturesIdentifier, SingleSignaturesIdentifier};
pub use signatures::{CachedSignatures, SignaturesIdentifier, SingleSignaturesIdentifier};

/// An address identity
pub struct AddressIdentity<'a> {
Expand Down
Loading

0 comments on commit 398ef4a

Please sign in to comment.