Skip to content

Commit 398ef4a

Browse files
byteshijinngrandizzyklkvr
authored
feat(forge, cast): add cast --with_local_artifacts/forge selectors 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 <[email protected]> --------- Co-authored-by: grandizzy <[email protected]> Co-authored-by: grandizzy <[email protected]> Co-authored-by: Arsenii Kulikov <[email protected]>
1 parent 8b7d5df commit 398ef4a

File tree

8 files changed

+256
-52
lines changed

8 files changed

+256
-52
lines changed

crates/cast/bin/cmd/call.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ pub struct CallArgs {
8181

8282
#[command(flatten)]
8383
eth: EthereumOpts,
84+
85+
/// Use current project artifacts for trace decoding.
86+
#[arg(long, visible_alias = "la")]
87+
pub with_local_artifacts: bool,
8488
}
8589

8690
#[derive(Debug, Parser)]
@@ -127,6 +131,7 @@ impl CallArgs {
127131
decode_internal,
128132
labels,
129133
data,
134+
with_local_artifacts,
130135
..
131136
} = self;
132137

@@ -195,7 +200,16 @@ impl CallArgs {
195200
),
196201
};
197202

198-
handle_traces(trace, &config, chain, labels, debug, decode_internal, false).await?;
203+
handle_traces(
204+
trace,
205+
&config,
206+
chain,
207+
labels,
208+
with_local_artifacts,
209+
debug,
210+
decode_internal,
211+
)
212+
.await?;
199213

200214
return Ok(());
201215
}

crates/cast/bin/cmd/run.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use foundry_cli::{
1010
opts::{EtherscanOpts, RpcOpts},
1111
utils::{handle_traces, init_progress, TraceResult},
1212
};
13-
use foundry_common::{is_known_system_sender, shell, SYSTEM_TRANSACTION_TYPE};
13+
use foundry_common::{is_known_system_sender, SYSTEM_TRANSACTION_TYPE};
1414
use foundry_compilers::artifacts::EvmVersion;
1515
use foundry_config::{
1616
figment::{
@@ -87,6 +87,10 @@ pub struct RunArgs {
8787
/// Enables Alphanet features.
8888
#[arg(long, alias = "odyssey")]
8989
pub alphanet: bool,
90+
91+
/// Use current project artifacts for trace decoding.
92+
#[arg(long, visible_alias = "la")]
93+
pub with_local_artifacts: bool,
9094
}
9195

9296
impl RunArgs {
@@ -251,9 +255,9 @@ impl RunArgs {
251255
&config,
252256
chain,
253257
self.label,
258+
self.with_local_artifacts,
254259
self.debug,
255260
self.decode_internal,
256-
shell::verbosity() > 0,
257261
)
258262
.await?;
259263

crates/cast/tests/cli/main.rs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
//! Contains various tests for checking cast commands
22
33
use alloy_chains::NamedChain;
4+
use alloy_network::TransactionResponse;
45
use alloy_primitives::{b256, B256};
6+
use alloy_rpc_types::{BlockNumberOrTag, Index};
57
use anvil::{EthereumHardfork, NodeConfig};
68
use foundry_test_utils::{
7-
casttest, file,
9+
casttest, file, forgetest_async,
810
rpc::{
911
next_etherscan_api_key, next_http_rpc_endpoint, next_mainnet_etherscan_api_key,
1012
next_rpc_endpoint, next_ws_rpc_endpoint,
@@ -1596,3 +1598,110 @@ casttest!(fetch_artifact_from_etherscan, |_prj, cmd| {
15961598
15971599
"#]]);
15981600
});
1601+
1602+
// tests cast can decode traces when using project artifacts
1603+
forgetest_async!(decode_traces_with_project_artifacts, |prj, cmd| {
1604+
let (api, handle) =
1605+
anvil::spawn(NodeConfig::test().with_disable_default_create2_deployer(true)).await;
1606+
1607+
foundry_test_utils::util::initialize(prj.root());
1608+
prj.add_source(
1609+
"LocalProjectContract",
1610+
r#"
1611+
contract LocalProjectContract {
1612+
event LocalProjectContractCreated(address owner);
1613+
1614+
constructor() {
1615+
emit LocalProjectContractCreated(msg.sender);
1616+
}
1617+
}
1618+
"#,
1619+
)
1620+
.unwrap();
1621+
prj.add_script(
1622+
"LocalProjectScript",
1623+
r#"
1624+
import "forge-std/Script.sol";
1625+
import {LocalProjectContract} from "../src/LocalProjectContract.sol";
1626+
1627+
contract LocalProjectScript is Script {
1628+
function run() public {
1629+
vm.startBroadcast();
1630+
new LocalProjectContract();
1631+
vm.stopBroadcast();
1632+
}
1633+
}
1634+
"#,
1635+
)
1636+
.unwrap();
1637+
1638+
cmd.args([
1639+
"script",
1640+
"--private-key",
1641+
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
1642+
"--rpc-url",
1643+
&handle.http_endpoint(),
1644+
"--broadcast",
1645+
"LocalProjectScript",
1646+
]);
1647+
1648+
cmd.assert_success();
1649+
1650+
let tx_hash = api
1651+
.transaction_by_block_number_and_index(BlockNumberOrTag::Latest, Index::from(0))
1652+
.await
1653+
.unwrap()
1654+
.unwrap()
1655+
.tx_hash();
1656+
1657+
// Assert cast with local artifacts from outside the project.
1658+
cmd.cast_fuse()
1659+
.args(["run", "--la", format!("{tx_hash}").as_str(), "--rpc-url", &handle.http_endpoint()])
1660+
.assert_success()
1661+
.stdout_eq(str![[r#"
1662+
Executing previous transactions from the block.
1663+
Compiling project to generate artifacts
1664+
Nothing to compile
1665+
1666+
"#]]);
1667+
1668+
// Run cast from project dir.
1669+
cmd.cast_fuse().set_current_dir(prj.root());
1670+
1671+
// Assert cast without local artifacts cannot decode traces.
1672+
cmd.cast_fuse()
1673+
.args(["run", format!("{tx_hash}").as_str(), "--rpc-url", &handle.http_endpoint()])
1674+
.assert_success()
1675+
.stdout_eq(str![[r#"
1676+
Executing previous transactions from the block.
1677+
Traces:
1678+
[13520] → new <unknown>@0x5FbDB2315678afecb367f032d93F642f64180aa3
1679+
├─ emit topic 0: 0xa7263295d3a687d750d1fd377b5df47de69d7db8decc745aaa4bbee44dc1688d
1680+
│ data: 0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
1681+
└─ ← [Return] 62 bytes of code
1682+
1683+
1684+
Transaction successfully executed.
1685+
[GAS]
1686+
1687+
"#]]);
1688+
1689+
// Assert cast with local artifacts can decode traces.
1690+
cmd.cast_fuse()
1691+
.args(["run", "--la", format!("{tx_hash}").as_str(), "--rpc-url", &handle.http_endpoint()])
1692+
.assert_success()
1693+
.stdout_eq(str![[r#"
1694+
Executing previous transactions from the block.
1695+
Compiling project to generate artifacts
1696+
No files changed, compilation skipped
1697+
Traces:
1698+
[13520] → new LocalProjectContract@0x5FbDB2315678afecb367f032d93F642f64180aa3
1699+
├─ emit LocalProjectContractCreated(owner: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266)
1700+
└─ ← [Return] 62 bytes of code
1701+
1702+
1703+
Transaction successfully executed.
1704+
[GAS]
1705+
1706+
"#]]);
1707+
});

crates/cli/src/utils/cmd.rs

Lines changed: 69 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use alloy_json_abi::JsonAbi;
22
use alloy_primitives::Address;
33
use eyre::{Result, WrapErr};
4-
use foundry_common::{fs, TestFunctionExt};
4+
use foundry_common::{compile::ProjectCompiler, fs, shell, ContractsByArtifact, TestFunctionExt};
55
use foundry_compilers::{
66
artifacts::{CompactBytecode, Settings},
77
cache::{CacheEntry, CompilerCache},
@@ -14,9 +14,9 @@ use foundry_evm::{
1414
executors::{DeployResult, EvmError, RawCallResult},
1515
opts::EvmOpts,
1616
traces::{
17-
debug::DebugTraceIdentifier,
17+
debug::{ContractSources, DebugTraceIdentifier},
1818
decode_trace_arena,
19-
identifier::{EtherscanIdentifier, SignaturesIdentifier},
19+
identifier::{CachedSignatures, SignaturesIdentifier, TraceIdentifiers},
2020
render_trace_arena_with_bytecodes, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind,
2121
Traces,
2222
},
@@ -383,10 +383,25 @@ pub async fn handle_traces(
383383
config: &Config,
384384
chain: Option<Chain>,
385385
labels: Vec<String>,
386+
with_local_artifacts: bool,
386387
debug: bool,
387388
decode_internal: bool,
388-
verbose: bool,
389389
) -> Result<()> {
390+
let (known_contracts, mut sources) = if with_local_artifacts {
391+
let _ = sh_println!("Compiling project to generate artifacts");
392+
let project = config.project()?;
393+
let compiler = ProjectCompiler::new();
394+
let output = compiler.compile(&project)?;
395+
(
396+
Some(ContractsByArtifact::new(
397+
output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
398+
)),
399+
ContractSources::from_project_output(&output, project.root(), None)?,
400+
)
401+
} else {
402+
(None, ContractSources::default())
403+
};
404+
390405
let labels = labels.iter().filter_map(|label_str| {
391406
let mut iter = label_str.split(':');
392407

@@ -398,45 +413,44 @@ pub async fn handle_traces(
398413
None
399414
});
400415
let config_labels = config.labels.clone().into_iter();
401-
let mut decoder = CallTraceDecoderBuilder::new()
416+
417+
let mut builder = CallTraceDecoderBuilder::new()
402418
.with_labels(labels.chain(config_labels))
403419
.with_signature_identifier(SignaturesIdentifier::new(
404420
Config::foundry_cache_dir(),
405421
config.offline,
406-
)?)
407-
.build();
422+
)?);
423+
let mut identifier = TraceIdentifiers::new().with_etherscan(config, chain)?;
424+
if let Some(contracts) = &known_contracts {
425+
builder = builder.with_known_contracts(contracts);
426+
identifier = identifier.with_local(contracts);
427+
}
408428

409-
let mut etherscan_identifier = EtherscanIdentifier::new(config, chain)?;
410-
if let Some(etherscan_identifier) = &mut etherscan_identifier {
411-
for (_, trace) in result.traces.as_deref_mut().unwrap_or_default() {
412-
decoder.identify(trace, etherscan_identifier);
413-
}
429+
let mut decoder = builder.build();
430+
431+
for (_, trace) in result.traces.as_deref_mut().unwrap_or_default() {
432+
decoder.identify(trace, &mut identifier);
414433
}
415434

416-
if decode_internal {
417-
let sources = if let Some(etherscan_identifier) = &etherscan_identifier {
418-
etherscan_identifier.get_compiled_contracts().await?
419-
} else {
420-
Default::default()
421-
};
435+
if decode_internal || debug {
436+
if let Some(ref etherscan_identifier) = identifier.etherscan {
437+
sources.merge(etherscan_identifier.get_compiled_contracts().await?);
438+
}
439+
440+
if debug {
441+
let mut debugger = Debugger::builder()
442+
.traces(result.traces.expect("missing traces"))
443+
.decoder(&decoder)
444+
.sources(sources)
445+
.build();
446+
debugger.try_run_tui()?;
447+
return Ok(())
448+
}
449+
422450
decoder.debug_identifier = Some(DebugTraceIdentifier::new(sources));
423451
}
424452

425-
if debug {
426-
let sources = if let Some(etherscan_identifier) = etherscan_identifier {
427-
etherscan_identifier.get_compiled_contracts().await?
428-
} else {
429-
Default::default()
430-
};
431-
let mut debugger = Debugger::builder()
432-
.traces(result.traces.expect("missing traces"))
433-
.decoder(&decoder)
434-
.sources(sources)
435-
.build();
436-
debugger.try_run_tui()?;
437-
} else {
438-
print_traces(&mut result, &decoder, verbose).await?;
439-
}
453+
print_traces(&mut result, &decoder, shell::verbosity() > 0).await?;
440454

441455
Ok(())
442456
}
@@ -464,3 +478,25 @@ pub async fn print_traces(
464478
sh_println!("Gas used: {}", result.gas_used)?;
465479
Ok(())
466480
}
481+
482+
/// Traverse the artifacts in the project to generate local signatures and merge them into the cache
483+
/// file.
484+
pub fn cache_local_signatures(output: &ProjectCompileOutput, cache_path: PathBuf) -> Result<()> {
485+
let path = cache_path.join("signatures");
486+
let mut cached_signatures = CachedSignatures::load(cache_path);
487+
output.artifacts().for_each(|(_, artifact)| {
488+
if let Some(abi) = &artifact.abi {
489+
for func in abi.functions() {
490+
cached_signatures.functions.insert(func.selector().to_string(), func.signature());
491+
}
492+
for event in abi.events() {
493+
cached_signatures
494+
.events
495+
.insert(event.selector().to_string(), event.full_signature());
496+
}
497+
}
498+
});
499+
500+
fs::write_json_file(&path, &cached_signatures)?;
501+
Ok(())
502+
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,14 @@ impl ContractSources {
212212
Ok(())
213213
}
214214

215+
/// Merges given contract sources.
216+
pub fn merge(&mut self, sources: Self) {
217+
self.sources_by_id.extend(sources.sources_by_id);
218+
for (name, artifacts) in sources.artifacts_by_name {
219+
self.artifacts_by_name.entry(name).or_default().extend(artifacts);
220+
}
221+
}
222+
215223
/// Returns all sources for a contract by name.
216224
pub fn get_sources(
217225
&self,

crates/evm/traces/src/identifier/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ mod etherscan;
1212
pub use etherscan::EtherscanIdentifier;
1313

1414
mod signatures;
15-
pub use signatures::{SignaturesIdentifier, SingleSignaturesIdentifier};
15+
pub use signatures::{CachedSignatures, SignaturesIdentifier, SingleSignaturesIdentifier};
1616

1717
/// An address identity
1818
pub struct AddressIdentity<'a> {

0 commit comments

Comments
 (0)