Skip to main content

forge_script_sequence/
reader.rs

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