From 2bb446e9387b61d6fed1c157a7330b07c610b52e Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:02:56 +0530 Subject: [PATCH] feat(`cheatcodes`): access broadcast artifacts (#9107) * refac(`script`): extract script sequence and related types to new crate * replace MultiChainSequence in script crate * replace TransactionWithMetadata and AdditionalContract * replace ScriptSequence * replace all underlying ScriptSequence and related types * doc nits * add `ScriptTransactionBuilder` * remove `TxWithMetadata` * mv verify fns and use `ScriptSequence` directly * clippy * feat(`cheatcodes`): vm.getDeployment * cargo cheats * getBroadcast by txType * get all broadcast txs * nits * fix * feat: getBroadcasts by txType * nit * fix * mv `BroadcastReader` to script-sequence * fix: search all broadcast files and then apply filters * fix: ignore run-latest to avoid duplicating entries * nit * sort by descending block number * tests * feat(`CheatsConfig`): add `broadcast` dir path * feat: read multichain sequences * nit * minify json * use walkdir * fix * fix path * feat: getDeployment cheatcodes * feat: read broadcasts with multiple tx types * test: getDeployment * nit * fmt * fix * nit * cli test * nit * remove solidity test * nit --- Cargo.lock | 2 + crates/cheatcodes/Cargo.toml | 1 + crates/cheatcodes/assets/cheatcodes.json | 169 +++++++++++++++++++ crates/cheatcodes/spec/src/lib.rs | 2 + crates/cheatcodes/spec/src/vm.rs | 63 +++++++ crates/cheatcodes/src/config.rs | 4 + crates/cheatcodes/src/fs.rs | 169 +++++++++++++++++++ crates/forge/tests/cli/test_cmd.rs | 202 +++++++++++++++++++++++ crates/script-sequence/Cargo.toml | 1 + crates/script-sequence/src/lib.rs | 2 + crates/script-sequence/src/reader.rs | 179 ++++++++++++++++++++ testdata/cheats/Vm.sol | 8 + 12 files changed, 802 insertions(+) create mode 100644 crates/script-sequence/src/reader.rs diff --git a/Cargo.lock b/Cargo.lock index 014b458f9642..de6f11ea9b21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3514,6 +3514,7 @@ dependencies = [ "serde", "serde_json", "tracing", + "walkdir", ] [[package]] @@ -3613,6 +3614,7 @@ dependencies = [ "dialoguer", "ecdsa", "eyre", + "forge-script-sequence", "foundry-cheatcodes-spec", "foundry-common", "foundry-compilers", diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index adce79b211bd..00d73ec4d41a 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -29,6 +29,7 @@ foundry-config.workspace = true foundry-evm-core.workspace = true foundry-evm-traces.workspace = true foundry-wallets.workspace = true +forge-script-sequence.workspace = true alloy-dyn-abi.workspace = true alloy-json-abi.workspace = true diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 4dbdd29b5da7..79761ae5a0ed 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -125,6 +125,24 @@ "description": "Unknown `forge` execution context." } ] + }, + { + "name": "BroadcastTxType", + "description": "The transaction type (`txType`) of the broadcast.", + "variants": [ + { + "name": "Call", + "description": "Represents a CALL broadcast tx." + }, + { + "name": "Create", + "description": "Represents a CREATE broadcast tx." + }, + { + "name": "Create2", + "description": "Represents a CREATE2 broadcast tx." + } + ] } ], "structs": [ @@ -524,6 +542,37 @@ "description": "The contract address where the opcode is running" } ] + }, + { + "name": "BroadcastTxSummary", + "description": "Represents a transaction's broadcast details.", + "fields": [ + { + "name": "txHash", + "ty": "bytes32", + "description": "The hash of the transaction that was broadcasted" + }, + { + "name": "txType", + "ty": "BroadcastTxType", + "description": "Represent the type of transaction among CALL, CREATE, CREATE2" + }, + { + "name": "contractAddress", + "ty": "address", + "description": "The address of the contract that was called or created.\n This is address of the contract that is created if the txType is CREATE or CREATE2." + }, + { + "name": "blockNumber", + "ty": "uint64", + "description": "The block number the transaction landed in." + }, + { + "name": "success", + "ty": "bool", + "description": "Status of the transaction, retrieved from the transaction receipt." + } + ] } ], "cheatcodes": [ @@ -5251,6 +5300,66 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "getBroadcast", + "description": "Returns the most recent broadcast for the given contract on `chainId` matching `txType`.\nFor example:\nThe most recent deployment can be fetched by passing `txType` as `CREATE` or `CREATE2`.\nThe most recent call can be fetched by passing `txType` as `CALL`.", + "declaration": "function getBroadcast(string memory contractName, uint64 chainId, BroadcastTxType txType) external returns (BroadcastTxSummary memory);", + "visibility": "external", + "mutability": "", + "signature": "getBroadcast(string,uint64,uint8)", + "selector": "0x3dc90cb3", + "selectorBytes": [ + 61, + 201, + 12, + 179 + ] + }, + "group": "filesystem", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getBroadcasts_0", + "description": "Returns all broadcasts for the given contract on `chainId` with the specified `txType`.\nSorted such that the most recent broadcast is the first element, and the oldest is the last. i.e descending order of BroadcastTxSummary.blockNumber.", + "declaration": "function getBroadcasts(string memory contractName, uint64 chainId, BroadcastTxType txType) external returns (BroadcastTxSummary[] memory);", + "visibility": "external", + "mutability": "", + "signature": "getBroadcasts(string,uint64,uint8)", + "selector": "0xf7afe919", + "selectorBytes": [ + 247, + 175, + 233, + 25 + ] + }, + "group": "filesystem", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getBroadcasts_1", + "description": "Returns all broadcasts for the given contract on `chainId`.\nSorted such that the most recent broadcast is the first element, and the oldest is the last. i.e descending order of BroadcastTxSummary.blockNumber.", + "declaration": "function getBroadcasts(string memory contractName, uint64 chainId) external returns (BroadcastTxSummary[] memory);", + "visibility": "external", + "mutability": "", + "signature": "getBroadcasts(string,uint64)", + "selector": "0xf2fa4a26", + "selectorBytes": [ + 242, + 250, + 74, + 38 + ] + }, + "group": "filesystem", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "getCode", @@ -5291,6 +5400,66 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "getDeployment_0", + "description": "Returns the most recent deployment for the current `chainId`.", + "declaration": "function getDeployment(string memory contractName) external returns (address deployedAddress);", + "visibility": "external", + "mutability": "", + "signature": "getDeployment(string)", + "selector": "0xa8091d97", + "selectorBytes": [ + 168, + 9, + 29, + 151 + ] + }, + "group": "filesystem", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getDeployment_1", + "description": "Returns the most recent deployment for the given contract on `chainId`", + "declaration": "function getDeployment(string memory contractName, uint64 chainId) external returns (address deployedAddress);", + "visibility": "external", + "mutability": "", + "signature": "getDeployment(string,uint64)", + "selector": "0x0debd5d6", + "selectorBytes": [ + 13, + 235, + 213, + 214 + ] + }, + "group": "filesystem", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getDeployments", + "description": "Returns all deployments for the given contract on `chainId`\nSorted in descending order of deployment time i.e descending order of BroadcastTxSummary.blockNumber.\nThe most recent deployment is the first element, and the oldest is the last.", + "declaration": "function getDeployments(string memory contractName, uint64 chainId) external returns (address[] memory deployedAddresses);", + "visibility": "external", + "mutability": "", + "signature": "getDeployments(string,uint64)", + "selector": "0x74e133dd", + "selectorBytes": [ + 116, + 225, + 51, + 221 + ] + }, + "group": "filesystem", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "getFoundryVersion", diff --git a/crates/cheatcodes/spec/src/lib.rs b/crates/cheatcodes/spec/src/lib.rs index 662853e9e811..eae2ae0ee9c2 100644 --- a/crates/cheatcodes/spec/src/lib.rs +++ b/crates/cheatcodes/spec/src/lib.rs @@ -86,11 +86,13 @@ impl Cheatcodes<'static> { Vm::StorageAccess::STRUCT.clone(), Vm::Gas::STRUCT.clone(), Vm::DebugStep::STRUCT.clone(), + Vm::BroadcastTxSummary::STRUCT.clone(), ]), enums: Cow::Owned(vec![ Vm::CallerMode::ENUM.clone(), Vm::AccountAccessKind::ENUM.clone(), Vm::ForgeContext::ENUM.clone(), + Vm::BroadcastTxType::ENUM.clone(), ]), errors: Vm::VM_ERRORS.iter().copied().cloned().collect(), events: Cow::Borrowed(&[]), diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 21d41b373e01..ce0bc08b0577 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -283,6 +283,31 @@ interface Vm { address contractAddr; } + /// The transaction type (`txType`) of the broadcast. + enum BroadcastTxType { + /// Represents a CALL broadcast tx. + Call, + /// Represents a CREATE broadcast tx. + Create, + /// Represents a CREATE2 broadcast tx. + Create2 + } + + /// Represents a transaction's broadcast details. + struct BroadcastTxSummary { + /// The hash of the transaction that was broadcasted + bytes32 txHash; + /// Represent the type of transaction among CALL, CREATE, CREATE2 + BroadcastTxType txType; + /// The address of the contract that was called or created. + /// This is address of the contract that is created if the txType is CREATE or CREATE2. + address contractAddress; + /// The block number the transaction landed in. + uint64 blockNumber; + /// Status of the transaction, retrieved from the transaction receipt. + bool success; + } + // ======== EVM ======== /// Gets the address for a given private key. @@ -1670,6 +1695,44 @@ interface Vm { #[cheatcode(group = Filesystem)] function getDeployedCode(string calldata artifactPath) external view returns (bytes memory runtimeBytecode); + /// Returns the most recent broadcast for the given contract on `chainId` matching `txType`. + /// + /// For example: + /// + /// The most recent deployment can be fetched by passing `txType` as `CREATE` or `CREATE2`. + /// + /// The most recent call can be fetched by passing `txType` as `CALL`. + #[cheatcode(group = Filesystem)] + function getBroadcast(string memory contractName, uint64 chainId, BroadcastTxType txType) external returns (BroadcastTxSummary memory); + + /// Returns all broadcasts for the given contract on `chainId` with the specified `txType`. + /// + /// Sorted such that the most recent broadcast is the first element, and the oldest is the last. i.e descending order of BroadcastTxSummary.blockNumber. + #[cheatcode(group = Filesystem)] + function getBroadcasts(string memory contractName, uint64 chainId, BroadcastTxType txType) external returns (BroadcastTxSummary[] memory); + + /// Returns all broadcasts for the given contract on `chainId`. + /// + /// Sorted such that the most recent broadcast is the first element, and the oldest is the last. i.e descending order of BroadcastTxSummary.blockNumber. + #[cheatcode(group = Filesystem)] + function getBroadcasts(string memory contractName, uint64 chainId) external returns (BroadcastTxSummary[] memory); + + /// Returns the most recent deployment for the current `chainId`. + #[cheatcode(group = Filesystem)] + function getDeployment(string memory contractName) external returns (address deployedAddress); + + /// Returns the most recent deployment for the given contract on `chainId` + #[cheatcode(group = Filesystem)] + function getDeployment(string memory contractName, uint64 chainId) external returns (address deployedAddress); + + /// Returns all deployments for the given contract on `chainId` + /// + /// Sorted in descending order of deployment time i.e descending order of BroadcastTxSummary.blockNumber. + /// + /// The most recent deployment is the first element, and the oldest is the last. + #[cheatcode(group = Filesystem)] + function getDeployments(string memory contractName, uint64 chainId) external returns (address[] memory deployedAddresses); + // -------- Foreign Function Interface -------- /// Performs a foreign function call via the terminal. diff --git a/crates/cheatcodes/src/config.rs b/crates/cheatcodes/src/config.rs index cfd6e9452df6..fa1ec6039cde 100644 --- a/crates/cheatcodes/src/config.rs +++ b/crates/cheatcodes/src/config.rs @@ -37,6 +37,8 @@ pub struct CheatsConfig { pub fs_permissions: FsPermissions, /// Project root pub root: PathBuf, + /// Absolute Path to broadcast dir i.e project_root/broadcast + pub broadcast: PathBuf, /// Paths (directories) where file reading/writing is allowed pub allowed_paths: Vec, /// How the evm was configured by the user @@ -87,6 +89,7 @@ impl CheatsConfig { paths: config.project_paths(), fs_permissions: config.fs_permissions.clone().joined(config.root.as_ref()), root: config.root.0.clone(), + broadcast: config.root.0.clone().join(&config.broadcast), allowed_paths, evm_opts, labels: config.labels.clone(), @@ -216,6 +219,7 @@ impl Default for CheatsConfig { paths: ProjectPathsConfig::builder().build_with_root("./"), fs_permissions: Default::default(), root: Default::default(), + broadcast: Default::default(), allowed_paths: vec![], evm_opts: Default::default(), labels: Default::default(), diff --git a/crates/cheatcodes/src/fs.rs b/crates/cheatcodes/src/fs.rs index c8c512b6f15e..04af101eb753 100644 --- a/crates/cheatcodes/src/fs.rs +++ b/crates/cheatcodes/src/fs.rs @@ -5,11 +5,15 @@ use crate::{Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::* use alloy_dyn_abi::DynSolType; use alloy_json_abi::ContractObject; use alloy_primitives::{hex, map::Entry, Bytes, U256}; +use alloy_provider::network::ReceiptResponse; +use alloy_rpc_types::AnyTransactionReceipt; use alloy_sol_types::SolValue; use dialoguer::{Input, Password}; +use forge_script_sequence::{BroadcastReader, TransactionWithMetadata}; use foundry_common::fs; use foundry_config::fs_permissions::FsAccessKind; use revm::interpreter::CreateInputs; +use revm_inspectors::tracing::types::CallKind; use semver::Version; use std::{ io::{BufRead, BufReader, Write}, @@ -626,6 +630,171 @@ fn prompt( } } +impl Cheatcode for getBroadcastCall { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { contractName, chainId, txType } = self; + + let latest_broadcast = latest_broadcast( + contractName, + *chainId, + &state.config.broadcast, + vec![map_broadcast_tx_type(*txType)], + )?; + + Ok(latest_broadcast.abi_encode()) + } +} + +impl Cheatcode for getBroadcasts_0Call { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { contractName, chainId, txType } = self; + + let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)? + .with_tx_type(map_broadcast_tx_type(*txType)); + + let broadcasts = reader.read()?; + + let summaries = broadcasts + .into_iter() + .flat_map(|broadcast| { + let results = reader.into_tx_receipts(broadcast); + parse_broadcast_results(results) + }) + .collect::>(); + + Ok(summaries.abi_encode()) + } +} + +impl Cheatcode for getBroadcasts_1Call { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { contractName, chainId } = self; + + let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?; + + let broadcasts = reader.read()?; + + let summaries = broadcasts + .into_iter() + .flat_map(|broadcast| { + let results = reader.into_tx_receipts(broadcast); + parse_broadcast_results(results) + }) + .collect::>(); + + Ok(summaries.abi_encode()) + } +} + +impl Cheatcode for getDeployment_0Call { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { contractName } = self; + let chain_id = ccx.ecx.env.cfg.chain_id; + + let latest_broadcast = latest_broadcast( + contractName, + chain_id, + &ccx.state.config.broadcast, + vec![CallKind::Create, CallKind::Create2], + )?; + + Ok(latest_broadcast.contractAddress.abi_encode()) + } +} + +impl Cheatcode for getDeployment_1Call { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { contractName, chainId } = self; + + let latest_broadcast = latest_broadcast( + contractName, + *chainId, + &state.config.broadcast, + vec![CallKind::Create, CallKind::Create2], + )?; + + Ok(latest_broadcast.contractAddress.abi_encode()) + } +} + +impl Cheatcode for getDeploymentsCall { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { contractName, chainId } = self; + + let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)? + .with_tx_type(CallKind::Create) + .with_tx_type(CallKind::Create2); + + let broadcasts = reader.read()?; + + let summaries = broadcasts + .into_iter() + .flat_map(|broadcast| { + let results = reader.into_tx_receipts(broadcast); + parse_broadcast_results(results) + }) + .collect::>(); + + let deployed_addresses = + summaries.into_iter().map(|summary| summary.contractAddress).collect::>(); + + Ok(deployed_addresses.abi_encode()) + } +} + +fn map_broadcast_tx_type(tx_type: BroadcastTxType) -> CallKind { + match tx_type { + BroadcastTxType::Call => CallKind::Call, + BroadcastTxType::Create => CallKind::Create, + BroadcastTxType::Create2 => CallKind::Create2, + _ => unreachable!("invalid tx type"), + } +} + +fn parse_broadcast_results( + results: Vec<(TransactionWithMetadata, AnyTransactionReceipt)>, +) -> Vec { + results + .into_iter() + .map(|(tx, receipt)| BroadcastTxSummary { + txHash: receipt.transaction_hash, + blockNumber: receipt.block_number.unwrap_or_default(), + txType: match tx.opcode { + CallKind::Call => BroadcastTxType::Call, + CallKind::Create => BroadcastTxType::Create, + CallKind::Create2 => BroadcastTxType::Create2, + _ => unreachable!("invalid tx type"), + }, + contractAddress: tx.contract_address.unwrap_or_default(), + success: receipt.status(), + }) + .collect() +} + +fn latest_broadcast( + contract_name: &String, + chain_id: u64, + broadcast_path: &Path, + filters: Vec, +) -> Result { + let mut reader = BroadcastReader::new(contract_name.clone(), chain_id, broadcast_path)?; + + for filter in filters { + reader = reader.with_tx_type(filter); + } + + let broadcast = reader.read_latest()?; + + let results = reader.into_tx_receipts(broadcast); + + let summaries = parse_broadcast_results(results); + + summaries + .first() + .ok_or_else(|| fmt_err!("no deployment found for {contract_name} on chain {chain_id}")) + .cloned() +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/forge/tests/cli/test_cmd.rs b/crates/forge/tests/cli/test_cmd.rs index d76a6124f7bd..8ce502a652b9 100644 --- a/crates/forge/tests/cli/test_cmd.rs +++ b/crates/forge/tests/cli/test_cmd.rs @@ -1,6 +1,7 @@ //! Contains various tests for `forge test`. use alloy_primitives::U256; +use anvil::{spawn, NodeConfig}; use foundry_config::{Config, FuzzConfig}; use foundry_test_utils::{ rpc, str, @@ -2390,3 +2391,204 @@ contract Dummy { assert!(dump_path.exists()); }); + +forgetest_async!(can_get_broadcast_txs, |prj, cmd| { + foundry_test_utils::util::initialize(prj.root()); + + let (_api, handle) = spawn(NodeConfig::test().silent()).await; + + prj.insert_vm(); + prj.insert_ds_test(); + prj.insert_console(); + + prj.add_source( + "Counter.sol", + r#" + contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} + "#, + ) + .unwrap(); + + prj.add_script( + "DeployCounter", + r#" + import "forge-std/Script.sol"; + import "src/Counter.sol"; + + contract DeployCounter is Script { + function run() public { + vm.startBroadcast(); + + Counter counter = new Counter(); + + counter.increment(); + + counter.setNumber(10); + + vm.stopBroadcast(); + } + } + "#, + ) + .unwrap(); + + prj.add_script( + "DeployCounterWithCreate2", + r#" + import "forge-std/Script.sol"; + import "src/Counter.sol"; + + contract DeployCounterWithCreate2 is Script { + function run() public { + vm.startBroadcast(); + + bytes32 salt = bytes32(uint256(1337)); + Counter counter = new Counter{salt: salt}(); + + counter.increment(); + + counter.setNumber(20); + + vm.stopBroadcast(); + } + } + "#, + ) + .unwrap(); + + let test = r#" + import {Vm} from "../src/Vm.sol"; + import {DSTest} from "../src/test.sol"; + import {console} from "../src/console.sol"; + + contract GetBroadcastTest is DSTest { + + Vm constant vm = Vm(HEVM_ADDRESS); + + function test_getLatestBroacast() external { + // Gets the latest create + Vm.BroadcastTxSummary memory broadcast = vm.getBroadcast( + "Counter", + 31337, + Vm.BroadcastTxType.Create + ); + + console.log("latest create"); + console.log(broadcast.blockNumber); + + assertEq(broadcast.blockNumber, 1); + + // Gets the latest create2 + Vm.BroadcastTxSummary memory broadcast2 = vm.getBroadcast( + "Counter", + 31337, + Vm.BroadcastTxType.Create2 + ); + + console.log("latest create2"); + console.log(broadcast2.blockNumber); + assertEq(broadcast2.blockNumber, 4); + + // Gets the latest call + Vm.BroadcastTxSummary memory broadcast3 = vm.getBroadcast( + "Counter", + 31337, + Vm.BroadcastTxType.Call + ); + + console.log("latest call"); + assertEq(broadcast3.blockNumber, 6); + } + + function test_getBroadcasts() public { + // Gets all calls + Vm.BroadcastTxSummary[] memory broadcasts = vm.getBroadcasts( + "Counter", + 31337, + Vm.BroadcastTxType.Call + ); + + assertEq(broadcasts.length, 4); + } + + function test_getAllBroadcasts() public { + // Gets all broadcasts + Vm.BroadcastTxSummary[] memory broadcasts2 = vm.getBroadcasts( + "Counter", + 31337 + ); + + assertEq(broadcasts2.length, 6); + } + + function test_getLatestDeployment() public { + address deployedAddress = vm.getDeployment( + "Counter", + 31337 + ); + + assertEq(deployedAddress, address(0x030D07c16e2c0a77f74ab16f3C8F10ACeF89FF81)); + } + + function test_getDeployments() public { + address[] memory deployments = vm.getDeployments( + "Counter", + 31337 + ); + + assertEq(deployments.length, 2); + assertEq(deployments[0], address(0x030D07c16e2c0a77f74ab16f3C8F10ACeF89FF81)); // Create2 address - latest deployment + assertEq(deployments[1], address(0x5FbDB2315678afecb367f032d93F642f64180aa3)); // Create address - oldest deployment + } + +} + "#; + + prj.add_test("GetBroadcast", test).unwrap(); + + let sender = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; + + cmd.args([ + "script", + "DeployCounter", + "--rpc-url", + &handle.http_endpoint(), + "--sender", + sender, + "--unlocked", + "--broadcast", + "--slow", + ]) + .assert_success(); + + cmd.forge_fuse() + .args([ + "script", + "DeployCounterWithCreate2", + "--rpc-url", + &handle.http_endpoint(), + "--sender", + sender, + "--unlocked", + "--broadcast", + "--slow", + ]) + .assert_success(); + + let broadcast_path = prj.root().join("broadcast"); + + // Check if the broadcast folder exists + assert!(broadcast_path.exists() && broadcast_path.is_dir()); + + cmd.forge_fuse().args(["test", "--mc", "GetBroadcastTest", "-vvv"]).assert_success(); +}); diff --git a/crates/script-sequence/Cargo.toml b/crates/script-sequence/Cargo.toml index e128fe37b5ef..13326e684cac 100644 --- a/crates/script-sequence/Cargo.toml +++ b/crates/script-sequence/Cargo.toml @@ -22,6 +22,7 @@ serde.workspace = true eyre.workspace = true serde_json.workspace = true tracing.workspace = true +walkdir.workspace = true revm-inspectors.workspace = true diff --git a/crates/script-sequence/src/lib.rs b/crates/script-sequence/src/lib.rs index 11970e9478be..929f44a724b8 100644 --- a/crates/script-sequence/src/lib.rs +++ b/crates/script-sequence/src/lib.rs @@ -3,8 +3,10 @@ #[macro_use] extern crate foundry_common; +pub mod reader; pub mod sequence; pub mod transaction; +pub use reader::*; pub use sequence::*; pub use transaction::*; diff --git a/crates/script-sequence/src/reader.rs b/crates/script-sequence/src/reader.rs new file mode 100644 index 000000000000..c4627dec09ec --- /dev/null +++ b/crates/script-sequence/src/reader.rs @@ -0,0 +1,179 @@ +use crate::{ScriptSequence, TransactionWithMetadata}; +use alloy_rpc_types::AnyTransactionReceipt; +use eyre::{bail, Result}; +use foundry_common::fs; +use revm_inspectors::tracing::types::CallKind; +use std::path::{Component, Path, PathBuf}; + +/// This type reads broadcast files in the +/// `project_root/broadcast/{contract_name}.s.sol/{chain_id}/` directory. +/// +/// It consists methods that filter and search for transactions in the broadcast files that match a +/// `transactionType` if provided. +/// +/// Note: +/// +/// It only returns transactions for which there exists a corresponding receipt in the broadcast. +#[derive(Debug, Clone)] +pub struct BroadcastReader { + contract_name: String, + chain_id: u64, + tx_type: Vec, + broadcast_path: PathBuf, +} + +impl BroadcastReader { + /// Create a new `BroadcastReader` instance. + pub fn new(contract_name: String, chain_id: u64, broadcast_path: &Path) -> Result { + if !broadcast_path.exists() && !broadcast_path.is_dir() { + bail!("broadcast dir does not exist"); + } + + Ok(Self { + contract_name, + chain_id, + tx_type: Default::default(), + broadcast_path: broadcast_path.to_path_buf(), + }) + } + + /// Set the transaction type to filter by. + pub fn with_tx_type(mut self, tx_type: CallKind) -> Self { + self.tx_type.push(tx_type); + self + } + + /// Read all broadcast files in the broadcast directory. + /// + /// Example structure: + /// + /// project-root/broadcast/{script_name}.s.sol/{chain_id}/*.json + /// project-root/broadcast/multi/{multichain_script_name}.s.sol-{timestamp}/deploy.json + pub fn read(&self) -> eyre::Result> { + // 1. Recursively read all .json files in the broadcast directory + let mut broadcasts = vec![]; + for entry in walkdir::WalkDir::new(&self.broadcast_path).into_iter() { + let entry = entry?; + let path = entry.path(); + + if path.is_file() && path.extension().is_some_and(|ext| ext == "json") { + // Ignore -latest to avoid duplicating broadcast entries + if path.components().any(|c| c.as_os_str().to_string_lossy().contains("-latest")) { + continue; + } + + // Detect Multichain broadcasts using "multi" in the path + if path.components().any(|c| c == Component::Normal("multi".as_ref())) { + // Parse as MultiScriptSequence + + let broadcast = fs::read_json_file::(path)?; + let multichain_deployments = broadcast + .get("deployments") + .and_then(|deployments| { + serde_json::from_value::>(deployments.clone()).ok() + }) + .unwrap_or_default(); + + broadcasts.extend(multichain_deployments); + continue; + } + + let broadcast = fs::read_json_file::(path)?; + broadcasts.push(broadcast); + } + } + + let broadcasts = self.filter_and_sort(broadcasts); + + Ok(broadcasts) + } + + /// Attempts read the latest broadcast file in the broadcast directory. + /// + /// This may be the `run-latest.json` file or the broadcast file with the latest timestamp. + pub fn read_latest(&self) -> eyre::Result { + let broadcasts = self.read()?; + + // Find the broadcast with the latest timestamp + let target = broadcasts + .into_iter() + .max_by_key(|broadcast| broadcast.timestamp) + .ok_or_else(|| eyre::eyre!("No broadcasts found"))?; + + Ok(target) + } + + /// Applies the filters and sorts the broadcasts by descending timestamp. + pub fn filter_and_sort(&self, broadcasts: Vec) -> Vec { + // Apply the filters + let mut seqs = broadcasts + .into_iter() + .filter(|broadcast| { + if broadcast.chain != self.chain_id { + return false; + } + + broadcast.transactions.iter().any(move |tx| { + let name_filter = + tx.contract_name.clone().is_some_and(|cn| cn == self.contract_name); + + let type_filter = self.tx_type.is_empty() || + self.tx_type.iter().any(|kind| *kind == tx.opcode); + + name_filter && type_filter + }) + }) + .collect::>(); + + // Sort by descending timestamp + seqs.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); + + seqs + } + + /// Search for transactions in the broadcast that match the specified `contractName` and + /// `txType`. + /// + /// It cross-checks the transactions with their corresponding receipts in the broadcast and + /// returns the result. + /// + /// Transactions that don't have a corresponding receipt are ignored. + /// + /// Sorts the transactions by descending block number. + pub fn into_tx_receipts( + &self, + broadcast: ScriptSequence, + ) -> Vec<(TransactionWithMetadata, AnyTransactionReceipt)> { + let transactions = broadcast.transactions.clone(); + + let txs = transactions + .into_iter() + .filter(|tx| { + let name_filter = + tx.contract_name.clone().is_some_and(|cn| cn == self.contract_name); + + let type_filter = + self.tx_type.is_empty() || self.tx_type.iter().any(|kind| *kind == tx.opcode); + + name_filter && type_filter + }) + .collect::>(); + + let mut targets = Vec::new(); + for tx in txs.into_iter() { + let maybe_receipt = broadcast + .receipts + .iter() + .find(|receipt| tx.hash.is_some_and(|hash| hash == receipt.transaction_hash)); + + if let Some(receipt) = maybe_receipt { + targets.push((tx, receipt.clone())); + } + } + + // Sort by descending block number + targets.sort_by(|a, b| b.1.block_number.cmp(&a.1.block_number)); + + targets + } +} diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 2d4030d30e1a..b33df8f2dc18 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -9,6 +9,7 @@ interface Vm { enum CallerMode { None, Broadcast, RecurrentBroadcast, Prank, RecurrentPrank } enum AccountAccessKind { Call, DelegateCall, CallCode, StaticCall, Create, SelfDestruct, Resume, Balance, Extcodesize, Extcodehash, Extcodecopy } enum ForgeContext { TestGroup, Test, Coverage, Snapshot, ScriptGroup, ScriptDryRun, ScriptBroadcast, ScriptResume, Unknown } + enum BroadcastTxType { Call, Create, Create2 } struct Log { bytes32[] topics; bytes data; address emitter; } struct Rpc { string key; string url; } struct EthGetLogs { address emitter; bytes32[] topics; bytes data; bytes32 blockHash; uint64 blockNumber; bytes32 transactionHash; uint64 transactionIndex; uint256 logIndex; bool removed; } @@ -21,6 +22,7 @@ interface Vm { struct StorageAccess { address account; bytes32 slot; bool isWrite; bytes32 previousValue; bytes32 newValue; bool reverted; } struct Gas { uint64 gasLimit; uint64 gasTotalUsed; uint64 gasMemoryUsed; int64 gasRefunded; uint64 gasRemaining; } struct DebugStep { uint256[] stack; bytes memoryInput; uint8 opcode; uint64 depth; bool isOutOfGas; address contractAddr; } + struct BroadcastTxSummary { bytes32 txHash; BroadcastTxType txType; address contractAddress; uint64 blockNumber; bool success; } function _expectCheatcodeRevert() external; function _expectCheatcodeRevert(bytes4 revertData) external; function _expectCheatcodeRevert(bytes calldata revertData) external; @@ -257,8 +259,14 @@ interface Vm { function getBlobhashes() external view returns (bytes32[] memory hashes); function getBlockNumber() external view returns (uint256 height); function getBlockTimestamp() external view returns (uint256 timestamp); + function getBroadcast(string memory contractName, uint64 chainId, BroadcastTxType txType) external returns (BroadcastTxSummary memory); + function getBroadcasts(string memory contractName, uint64 chainId, BroadcastTxType txType) external returns (BroadcastTxSummary[] memory); + function getBroadcasts(string memory contractName, uint64 chainId) external returns (BroadcastTxSummary[] memory); function getCode(string calldata artifactPath) external view returns (bytes memory creationBytecode); function getDeployedCode(string calldata artifactPath) external view returns (bytes memory runtimeBytecode); + function getDeployment(string memory contractName) external returns (address deployedAddress); + function getDeployment(string memory contractName, uint64 chainId) external returns (address deployedAddress); + function getDeployments(string memory contractName, uint64 chainId) external returns (address[] memory deployedAddresses); function getFoundryVersion() external view returns (string memory version); function getLabel(address account) external view returns (string memory currentLabel); function getMappingKeyAndParentOf(address target, bytes32 elementSlot) external returns (bool found, bytes32 key, bytes32 parent);