forge_script_sequence/
reader.rs1use 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#[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 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 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 pub fn read<N: Network>(&self) -> eyre::Result<Vec<ScriptSequence<N>>>
60 where
61 N::TxEnvelope: for<'d> Deserialize<'d>,
62 {
63 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 if path.components().any(|c| c.as_os_str().to_string_lossy().contains("-latest")) {
72 continue;
73 }
74
75 if path.components().any(|c| c == Component::Normal("multi".as_ref())) {
77 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 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 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 pub fn filter_and_sort<N: Network>(
122 &self,
123 broadcasts: Vec<ScriptSequence<N>>,
124 ) -> Vec<ScriptSequence<N>> {
125 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 seqs.sort_by_key(|s| std::cmp::Reverse(s.timestamp));
139
140 seqs
141 }
142
143 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 targets.sort_by_key(|t| std::cmp::Reverse(t.1.block_number()));
171
172 targets
173 }
174}