diff --git a/Cargo.lock b/Cargo.lock index b5415ff4d296f..d871af70ce744 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -546,6 +546,21 @@ dependencies = [ "alloy-serde", ] +[[package]] +name = "alloy-rpc-types-beacon" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3c0e6cc87a8be5582d08f929f96db25843f44cb636a0985a4a6bf02609c02f" +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.30" @@ -2542,6 +2557,8 @@ dependencies = [ "alloy-provider", "alloy-rlp", "alloy-rpc-types", + "alloy-rpc-types-beacon", + "alloy-rpc-types-engine", "alloy-serde", "alloy-signer", "alloy-signer-local", diff --git a/Cargo.toml b/Cargo.toml index a92537cdd3cad..07f8123a5423f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -224,6 +224,8 @@ alloy-provider = { version = "1.0.23", default-features = false } alloy-pubsub = { version = "1.0.23", default-features = false } alloy-rpc-client = { version = "1.0.23", default-features = false } alloy-rpc-types = { version = "1.0.23", default-features = true } +alloy-rpc-types-beacon = { version = "1.0.30", default-features = true } +alloy-rpc-types-engine = { version = "1.0.30", default-features = true } alloy-serde = { version = "1.0.23", default-features = false } alloy-signer = { version = "1.0.30", default-features = false } alloy-signer-aws = { version = "1.0.30", default-features = false } diff --git a/crates/cast/Cargo.toml b/crates/cast/Cargo.toml index de170151a9be2..17f2be8306895 100644 --- a/crates/cast/Cargo.toml +++ b/crates/cast/Cargo.toml @@ -47,6 +47,8 @@ alloy-provider = { workspace = true, features = [ ] } alloy-rlp.workspace = true alloy-rpc-types = { workspace = true, features = ["eth", "trace"] } +alloy-rpc-types-beacon.workspace = true +alloy-rpc-types-engine.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 28521d3a51159..3a0b06d4c07f3 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -264,6 +264,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..5581ba6831462 --- /dev/null +++ b/crates/cast/src/cmd/b2e_payload.rs @@ -0,0 +1,82 @@ +use alloy_rpc_types_beacon::payload::execution_payload_from_beacon_str; +use alloy_rpc_types_engine::ExecutionPayload; +use clap::Parser; +use eyre::{Result, eyre}; +use foundry_common::{fs, sh_print}; +use std::path::PathBuf; + +/// CLI arguments for `cast b2e-payload`, convert Beacon block's execution payload to Execution +/// JSON-RPC format. +#[derive(Parser)] +pub struct B2EPayloadArgs { + /// Input data provided through JSON file path. + #[arg( + long = "json-file", + value_name = "FILE", + help = "Path to the JSON file containing the beacon block" + )] + pub json_file: PathBuf, +} + +impl B2EPayloadArgs { + pub async fn run(self) -> Result<()> { + // Get input beacon block data + let beacon_block = fs::read_to_string(&self.json_file) + .map_err(|e| eyre!("Failed to read JSON file '{}': {}", self.json_file.display(), e))?; + + // Extract and convert execution payload + let execution_payload = Self::extract_and_convert_execution_payload(&beacon_block)?; + + let json_rpc_output = format_as_json_rpc(execution_payload)?; + sh_print!("{}", json_rpc_output)?; + + Ok(()) + } + + // Extracts the execution payload from a beacon block JSON string and converts it to + // `ExecutionPayload` It matches `beaconcha.in` json format + fn extract_and_convert_execution_payload(beacon_block: &str) -> Result { + let beacon_json: serde_json::Value = serde_json::from_str(beacon_block) + .map_err(|e| eyre!("Failed to parse beacon block JSON: {}", e))?; + + // early detection if the format is not correct + if beacon_json + .get("message") + .and_then(|m| m.get("body")) + .and_then(|b| b.get("execution_payload")) + .is_none() + { + return Err(eyre!("Invalid beacon block format: missing 'message' field")); + } + // Extract the "message.body.execution_payload" field from the beacon block JSON + // TODO: check if we extract from beacon api it works but not sure it will work with all API + // interfaces + let execution_payload_beacon_block = beacon_json + .get("message") + .and_then(|m| m.get("body")) + .and_then(|b| b.get("execution_payload")) + .ok_or_else(|| eyre!("Could not find execution_payload in beacon block"))?; + + let execution_payload_str = serde_json::to_string(execution_payload_beacon_block) + .map_err(|e| eyre!("Failed to serialize execution payload: {}", e))?; + + // Convert beacon block's execution payload to json rpc execution payload + let execution_payload = execution_payload_from_beacon_str(&execution_payload_str)?; + + Ok(execution_payload) + } +} + +// Helper to format the execution payload as JSON-RPC response +fn format_as_json_rpc(execution_payload: ExecutionPayload) -> Result { + // TODO: check if we used this format and this method engine version + let json_rpc_request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "engine_newPayloadV3", + "params": [execution_payload], + "id": 1 + }); + + serde_json::to_string_pretty(&json_rpc_request) + .map_err(|e| eyre!("Failed to serialize JSON-RPC response: {}", e)) +} 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 bb86b561def32..2b5576a133920 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}; @@ -1043,6 +1044,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 {