forge_script_sequence/
reader.rs1use crate::{ScriptSequence, TransactionWithMetadata};
2use alloy_network::AnyTransactionReceipt;
3use eyre::{Result, bail};
4use foundry_common::fs;
5use revm_inspectors::tracing::types::CallKind;
6use std::path::{Component, Path, PathBuf};
7
8#[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 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 pub fn with_tx_type(mut self, tx_type: CallKind) -> Self {
42 self.tx_type.push(tx_type);
43 self
44 }
45
46 pub fn read(&self) -> eyre::Result<Vec<ScriptSequence>> {
53 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 if path.components().any(|c| c.as_os_str().to_string_lossy().contains("-latest")) {
62 continue;
63 }
64
65 if path.components().any(|c| c == Component::Normal("multi".as_ref())) {
67 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 pub fn read_latest(&self) -> eyre::Result<ScriptSequence> {
95 let broadcasts = self.read()?;
96
97 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 pub fn filter_and_sort(&self, broadcasts: Vec<ScriptSequence>) -> Vec<ScriptSequence> {
108 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.as_ref().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 seqs.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
129
130 seqs
131 }
132
133 pub fn into_tx_receipts(
143 &self,
144 broadcast: ScriptSequence,
145 ) -> Vec<(TransactionWithMetadata, AnyTransactionReceipt)> {
146 let ScriptSequence { transactions, receipts, .. } = broadcast;
147
148 let mut targets = Vec::new();
149 for tx in transactions.into_iter().filter(|tx| {
150 let name_filter = tx.contract_name.as_ref().is_some_and(|cn| *cn == self.contract_name);
151
152 let type_filter = self.tx_type.is_empty() || self.tx_type.contains(&tx.opcode);
153
154 name_filter && type_filter
155 }) {
156 let maybe_receipt = receipts
157 .iter()
158 .find(|receipt| tx.hash.is_some_and(|hash| hash == receipt.transaction_hash));
159
160 if let Some(receipt) = maybe_receipt {
161 targets.push((tx, receipt.clone()));
162 }
163 }
164
165 targets.sort_by(|a, b| b.1.block_number.cmp(&a.1.block_number));
167
168 targets
169 }
170}