forge_script_sequence/
reader.rs

1use crate::{ScriptSequence, TransactionWithMetadata};
2use alloy_network::AnyTransactionReceipt;
3use eyre::{bail, Result};
4use foundry_common::fs;
5use revm_inspectors::tracing::types::CallKind;
6use std::path::{Component, Path, PathBuf};
7
8/// This type reads broadcast files in the
9/// `project_root/broadcast/{contract_name}.s.sol/{chain_id}/` directory.
10///
11/// It consists methods that filter and search for transactions in the broadcast files that match a
12/// `transactionType` if provided.
13///
14/// Note:
15///
16/// It only returns transactions for which there exists a corresponding receipt in the broadcast.
17#[derive(Debug, Clone)]
18pub struct BroadcastReader {
19    contract_name: String,
20    chain_id: u64,
21    tx_type: Vec<CallKind>,
22    broadcast_path: PathBuf,
23}
24
25impl BroadcastReader {
26    /// Create a new `BroadcastReader` instance.
27    pub fn new(contract_name: String, chain_id: u64, broadcast_path: &Path) -> Result<Self> {
28        if !broadcast_path.exists() && !broadcast_path.is_dir() {
29            bail!("broadcast dir does not exist");
30        }
31
32        Ok(Self {
33            contract_name,
34            chain_id,
35            tx_type: Default::default(),
36            broadcast_path: broadcast_path.to_path_buf(),
37        })
38    }
39
40    /// Set the transaction type to filter by.
41    pub fn with_tx_type(mut self, tx_type: CallKind) -> Self {
42        self.tx_type.push(tx_type);
43        self
44    }
45
46    /// Read all broadcast files in the broadcast directory.
47    ///
48    /// Example structure:
49    ///
50    /// project-root/broadcast/{script_name}.s.sol/{chain_id}/*.json
51    /// project-root/broadcast/multi/{multichain_script_name}.s.sol-{timestamp}/deploy.json
52    pub fn read(&self) -> eyre::Result<Vec<ScriptSequence>> {
53        // 1. Recursively read all .json files in the broadcast directory
54        let mut broadcasts = vec![];
55        for entry in walkdir::WalkDir::new(&self.broadcast_path).into_iter() {
56            let entry = entry?;
57            let path = entry.path();
58
59            if path.is_file() && path.extension().is_some_and(|ext| ext == "json") {
60                // Ignore -latest to avoid duplicating broadcast entries
61                if path.components().any(|c| c.as_os_str().to_string_lossy().contains("-latest")) {
62                    continue;
63                }
64
65                // Detect Multichain broadcasts using "multi" in the path
66                if path.components().any(|c| c == Component::Normal("multi".as_ref())) {
67                    // Parse as MultiScriptSequence
68
69                    let broadcast = fs::read_json_file::<serde_json::Value>(path)?;
70                    let multichain_deployments = broadcast
71                        .get("deployments")
72                        .and_then(|deployments| {
73                            serde_json::from_value::<Vec<ScriptSequence>>(deployments.clone()).ok()
74                        })
75                        .unwrap_or_default();
76
77                    broadcasts.extend(multichain_deployments);
78                    continue;
79                }
80
81                let broadcast = fs::read_json_file::<ScriptSequence>(path)?;
82                broadcasts.push(broadcast);
83            }
84        }
85
86        let broadcasts = self.filter_and_sort(broadcasts);
87
88        Ok(broadcasts)
89    }
90
91    /// Attempts read the latest broadcast file in the broadcast directory.
92    ///
93    /// This may be the `run-latest.json` file or the broadcast file with the latest timestamp.
94    pub fn read_latest(&self) -> eyre::Result<ScriptSequence> {
95        let broadcasts = self.read()?;
96
97        // Find the broadcast with the latest timestamp
98        let target = broadcasts
99            .into_iter()
100            .max_by_key(|broadcast| broadcast.timestamp)
101            .ok_or_else(|| eyre::eyre!("No broadcasts found"))?;
102
103        Ok(target)
104    }
105
106    /// Applies the filters and sorts the broadcasts by descending timestamp.
107    pub fn filter_and_sort(&self, broadcasts: Vec<ScriptSequence>) -> Vec<ScriptSequence> {
108        // Apply the filters
109        let mut seqs = broadcasts
110            .into_iter()
111            .filter(|broadcast| {
112                if broadcast.chain != self.chain_id {
113                    return false;
114                }
115
116                broadcast.transactions.iter().any(move |tx| {
117                    let name_filter =
118                        tx.contract_name.clone().is_some_and(|cn| cn == self.contract_name);
119
120                    let type_filter = self.tx_type.is_empty() || self.tx_type.contains(&tx.opcode);
121
122                    name_filter && type_filter
123                })
124            })
125            .collect::<Vec<_>>();
126
127        // Sort by descending timestamp
128        seqs.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
129
130        seqs
131    }
132
133    /// Search for transactions in the broadcast that match the specified `contractName` and
134    /// `txType`.
135    ///
136    /// It cross-checks the transactions with their corresponding receipts in the broadcast and
137    /// returns the result.
138    ///
139    /// Transactions that don't have a corresponding receipt are ignored.
140    ///
141    /// Sorts the transactions by descending block number.
142    pub fn into_tx_receipts(
143        &self,
144        broadcast: ScriptSequence,
145    ) -> Vec<(TransactionWithMetadata, AnyTransactionReceipt)> {
146        let transactions = broadcast.transactions.clone();
147
148        let txs = transactions
149            .into_iter()
150            .filter(|tx| {
151                let name_filter =
152                    tx.contract_name.clone().is_some_and(|cn| cn == self.contract_name);
153
154                let type_filter = self.tx_type.is_empty() || self.tx_type.contains(&tx.opcode);
155
156                name_filter && type_filter
157            })
158            .collect::<Vec<_>>();
159
160        let mut targets = Vec::new();
161        for tx in txs.into_iter() {
162            let maybe_receipt = broadcast
163                .receipts
164                .iter()
165                .find(|receipt| tx.hash.is_some_and(|hash| hash == receipt.transaction_hash));
166
167            if let Some(receipt) = maybe_receipt {
168                targets.push((tx, receipt.clone()));
169            }
170        }
171
172        // Sort by descending block number
173        targets.sort_by(|a, b| b.1.block_number.cmp(&a.1.block_number));
174
175        targets
176    }
177}