diff --git a/Cargo.lock b/Cargo.lock index 331f1b62b285e..8421b916266ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -559,6 +559,21 @@ dependencies = [ "alloy-serde", ] +[[package]] +name = "alloy-rpc-types-beacon" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46a3740113636ec4643b2740e3e9ea4192820f56b056177ceb0569d711b2af" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rpc-types-engine", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.16", +] + [[package]] name = "alloy-rpc-types-debug" version = "1.0.36" @@ -2538,6 +2553,7 @@ dependencies = [ "alloy-provider", "alloy-rlp", "alloy-rpc-types", + "alloy-rpc-types-beacon", "alloy-serde", "alloy-signer", "alloy-signer-local", diff --git a/Cargo.toml b/Cargo.toml index c721aab48706e..f3fe79f9f434a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -233,6 +233,7 @@ alloy-provider = { version = "1.0.36", default-features = false } alloy-pubsub = { version = "1.0.36", default-features = false } alloy-rpc-client = { version = "1.0.36", default-features = false } alloy-rpc-types = { version = "1.0.36", default-features = true } +alloy-rpc-types-beacon = { version = "1.0.36", default-features = true } alloy-serde = { version = "1.0.36", default-features = false } alloy-signer = { version = "1.0.36", default-features = false } alloy-signer-aws = { version = "1.0.36", default-features = false } diff --git a/crates/cast/Cargo.toml b/crates/cast/Cargo.toml index d28143821fc76..8146b8d80e669 100644 --- a/crates/cast/Cargo.toml +++ b/crates/cast/Cargo.toml @@ -49,6 +49,7 @@ alloy-provider = { workspace = true, features = [ ] } alloy-rlp.workspace = true alloy-rpc-types = { workspace = true, features = ["eth", "trace"] } +alloy-rpc-types-beacon.workspace = true alloy-serde.workspace = true alloy-signer-local = { workspace = true, features = ["mnemonic", "keystore"] } alloy-signer.workspace = true diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index 7e2cdce248733..21e1a1f18e37f 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -273,6 +273,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { CastSubcommand::ConstructorArgs(cmd) => cmd.run().await?, CastSubcommand::Artifact(cmd) => cmd.run().await?, CastSubcommand::Bind(cmd) => cmd.run().await?, + CastSubcommand::B2EPayload(cmd) => cmd.run().await?, CastSubcommand::PrettyCalldata { calldata, offline } => { let calldata = stdin::unwrap_line(calldata)?; sh_println!("{}", pretty_calldata(&calldata, offline).await?)?; diff --git a/crates/cast/src/cmd/b2e_payload.rs b/crates/cast/src/cmd/b2e_payload.rs new file mode 100644 index 0000000000000..7e3fc9a05d63f --- /dev/null +++ b/crates/cast/src/cmd/b2e_payload.rs @@ -0,0 +1,110 @@ +//! Command Line handler to convert Beacon block's execution payload to Execution format. + +use std::path::PathBuf; + +use alloy_rpc_types_beacon::payload::BeaconBlockData; +use clap::{Parser, builder::ValueParser}; +use eyre::{Result, eyre}; +use foundry_common::{fs, sh_print}; + +/// CLI arguments for `cast b2e-payload`, convert Beacon block's execution payload to Execution +/// format. +#[derive(Parser)] +pub struct B2EPayloadArgs { + /// Input data, it can be either a file path to JSON file or raw JSON string containing the + /// beacon block + #[arg(value_name = "INPUT", value_parser=ValueParser::new(parse_input_source), help = "File path to JSON file or raw JSON string containing the beacon block")] + pub input: InputSource, +} + +impl B2EPayloadArgs { + pub async fn run(self) -> Result<()> { + let beacon_block_json = match self.input { + InputSource::Json(json) => json, + InputSource::File(path) => fs::read_to_string(&path) + .map_err(|e| eyre!("Failed to read JSON file '{}': {}", path.display(), e))?, + }; + + let beacon_block_data: BeaconBlockData = serde_json::from_str(&beacon_block_json) + .map_err(|e| eyre!("Failed to parse beacon block JSON: {}", e))?; + + let execution_payload = beacon_block_data.execution_payload(); + + // Output raw execution payload + let output = serde_json::to_string(&execution_payload) + .map_err(|e| eyre!("Failed to serialize execution payload: {}", e))?; + sh_print!("{}", output)?; + + Ok(()) + } +} + +/// Represents the different input sources for beacon block data +#[derive(Debug, Clone)] +pub enum InputSource { + /// Path to a JSON file containing beacon block data + File(PathBuf), + /// Raw JSON string containing beacon block data + Json(String), +} + +fn parse_input_source(s: &str) -> Result { + // Try parsing as JSON first + if serde_json::from_str::(s).is_ok() { + return Ok(InputSource::Json(s.to_string())); + } + + // Otherwise treat as file path + Ok(InputSource::File(PathBuf::from(s))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_input_source_json_object() { + let json_input = r#"{"execution_payload": {"block_hash": "0x123"}}"#; + let result = parse_input_source(json_input).unwrap(); + + match result { + InputSource::Json(json) => assert_eq!(json, json_input), + InputSource::File(_) => panic!("Expected JSON input, got File"), + } + } + + #[test] + fn test_parse_input_source_json_array() { + let json_input = r#"[{"block": "data"}]"#; + let result = parse_input_source(json_input).unwrap(); + + match result { + InputSource::Json(json) => assert_eq!(json, json_input), + InputSource::File(_) => panic!("Expected JSON input, got File"), + } + } + + #[test] + fn test_parse_input_source_file_path() { + let file_path = + "block-12225729-6ceadbf2a6adbbd64cbec33fdebbc582f25171cd30ac43f641cbe76ac7313ddf.json"; + let result = parse_input_source(file_path).unwrap(); + + match result { + InputSource::File(path) => assert_eq!(path, PathBuf::from(file_path)), + InputSource::Json(_) => panic!("Expected File input, got JSON"), + } + } + + #[test] + fn test_parse_input_source_malformed_but_not_json() { + let malformed = "not-json-{"; + let result = parse_input_source(malformed).unwrap(); + + // Should be treated as file path since it's not valid JSON + match result { + InputSource::File(path) => assert_eq!(path, PathBuf::from(malformed)), + InputSource::Json(_) => panic!("Expected File input, got File"), + } + } +} diff --git a/crates/cast/src/cmd/mod.rs b/crates/cast/src/cmd/mod.rs index 482cb77b4e343..366155fa4038f 100644 --- a/crates/cast/src/cmd/mod.rs +++ b/crates/cast/src/cmd/mod.rs @@ -7,6 +7,7 @@ pub mod access_list; pub mod artifact; +pub mod b2e_payload; pub mod bind; pub mod call; pub mod constructor_args; diff --git a/crates/cast/src/opts.rs b/crates/cast/src/opts.rs index 96661e20938b3..8b7a86c169d76 100644 --- a/crates/cast/src/opts.rs +++ b/crates/cast/src/opts.rs @@ -1,9 +1,10 @@ use crate::cmd::{ - access_list::AccessListArgs, artifact::ArtifactArgs, bind::BindArgs, call::CallArgs, - constructor_args::ConstructorArgsArgs, create2::Create2Args, creation_code::CreationCodeArgs, - da_estimate::DAEstimateArgs, estimate::EstimateArgs, find_block::FindBlockArgs, - interface::InterfaceArgs, logs::LogsArgs, mktx::MakeTxArgs, rpc::RpcArgs, run::RunArgs, - send::SendTxArgs, storage::StorageArgs, txpool::TxPoolSubcommands, wallet::WalletSubcommands, + access_list::AccessListArgs, artifact::ArtifactArgs, b2e_payload::B2EPayloadArgs, + bind::BindArgs, call::CallArgs, constructor_args::ConstructorArgsArgs, create2::Create2Args, + creation_code::CreationCodeArgs, da_estimate::DAEstimateArgs, estimate::EstimateArgs, + find_block::FindBlockArgs, interface::InterfaceArgs, logs::LogsArgs, mktx::MakeTxArgs, + rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs, txpool::TxPoolSubcommands, + wallet::WalletSubcommands, }; use alloy_ens::NameOrAddress; use alloy_primitives::{Address, B256, Selector, U256}; @@ -1054,6 +1055,10 @@ pub enum CastSubcommand { #[command(visible_alias = "bi")] Bind(BindArgs), + /// Convert Beacon payload to execution payload. + #[command(visible_alias = "b2e")] + B2EPayload(B2EPayloadArgs), + /// Get the selector for a function. #[command(visible_alias = "si")] Sig {