1use crate::receipts::{PendingReceiptError, TxStatus, check_tx_status, format_receipt};
2use alloy_chains::Chain;
3use alloy_network::{Network, ReceiptResponse};
4use alloy_primitives::{
5 B256,
6 map::{B256HashMap, HashMap},
7};
8use alloy_provider::RootProvider;
9use eyre::Result;
10use forge_script_sequence::ScriptSequence;
11use foundry_cli::utils::init_progress;
12use foundry_common::shell;
13use futures::StreamExt;
14use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
15use parking_lot::RwLock;
16use std::{fmt::Write, sync::Arc, time::Duration};
17use yansi::Paint;
18
19#[derive(Debug)]
21pub struct SequenceProgressState {
22 top_spinner: ProgressBar,
24 txs: ProgressBar,
26 receipts: ProgressBar,
28 tx_spinners: B256HashMap<ProgressBar>,
30 multi: MultiProgress,
32}
33
34impl SequenceProgressState {
35 pub fn new<N: Network>(
36 sequence_idx: usize,
37 sequence: &ScriptSequence<N>,
38 multi: MultiProgress,
39 ) -> Self {
40 let mut state = if shell::is_quiet() || shell::is_json() {
41 let top_spinner = ProgressBar::hidden();
42 let txs = ProgressBar::hidden();
43 let receipts = ProgressBar::hidden();
44
45 Self { top_spinner, txs, receipts, tx_spinners: Default::default(), multi }
46 } else {
47 let mut template = "{spinner:.green}".to_string();
48 write!(template, " Sequence #{} on {}", sequence_idx + 1, Chain::from(sequence.chain))
49 .unwrap();
50 template.push_str("{msg}");
51
52 let top_spinner = ProgressBar::new_spinner().with_style(
53 ProgressStyle::with_template(&template).unwrap().tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈✅"),
54 );
55 let top_spinner = multi.add(top_spinner);
56
57 let txs = multi.insert_after(
58 &top_spinner,
59 init_progress(sequence.transactions.len() as u64, "txes").with_prefix(" "),
60 );
61
62 let receipts = multi.insert_after(
63 &txs,
64 init_progress(sequence.transactions.len() as u64, "receipts").with_prefix(" "),
65 );
66
67 top_spinner.enable_steady_tick(Duration::from_millis(100));
68 txs.enable_steady_tick(Duration::from_millis(1000));
69 receipts.enable_steady_tick(Duration::from_millis(1000));
70
71 txs.set_position(sequence.receipts.len() as u64);
72 receipts.set_position(sequence.receipts.len() as u64);
73
74 Self { top_spinner, txs, receipts, tx_spinners: Default::default(), multi }
75 };
76
77 for tx_hash in &sequence.pending {
78 state.tx_sent(*tx_hash);
79 }
80
81 state
82 }
83
84 pub fn tx_sent(&mut self, tx_hash: B256) {
87 if self.tx_spinners.len() < 10 {
89 let spinner = if shell::is_quiet() || shell::is_json() {
90 ProgressBar::hidden()
91 } else {
92 let spinner = ProgressBar::new_spinner()
93 .with_style(
94 ProgressStyle::with_template(" {spinner:.green} {msg}")
95 .unwrap()
96 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈"),
97 )
98 .with_message(format!("{} {}", "[Pending]".yellow(), tx_hash));
99
100 let spinner = self.multi.insert_before(&self.txs, spinner);
101 spinner.enable_steady_tick(Duration::from_millis(100));
102 spinner
103 };
104
105 self.tx_spinners.insert(tx_hash, spinner);
106 }
107 self.txs.inc(1);
108 }
109
110 pub fn finish_tx_spinner(&mut self, tx_hash: B256) {
112 if let Some(spinner) = self.tx_spinners.remove(&tx_hash) {
113 spinner.finish_and_clear();
114 }
115 self.receipts.inc(1);
116 }
117
118 pub fn finish_tx_spinner_with_msg(&mut self, tx_hash: B256, msg: &str) -> std::io::Result<()> {
120 self.finish_tx_spinner(tx_hash);
121
122 if !(shell::is_quiet() || shell::is_json()) {
123 self.multi.println(msg)?;
124 }
125
126 Ok(())
127 }
128
129 pub fn set_status(&mut self, status: &str) {
131 self.top_spinner.set_message(format!(" | {status}"));
132 }
133
134 pub fn finish(&self) {
137 self.top_spinner.finish();
138 self.txs.finish_and_clear();
139 self.receipts.finish_and_clear();
140 }
141}
142
143#[derive(Debug, Clone)]
145pub struct SequenceProgress {
146 pub inner: Arc<RwLock<SequenceProgressState>>,
147}
148
149impl SequenceProgress {
150 pub fn new<N: Network>(
151 sequence_idx: usize,
152 sequence: &ScriptSequence<N>,
153 multi: MultiProgress,
154 ) -> Self {
155 Self {
156 inner: Arc::new(RwLock::new(SequenceProgressState::new(sequence_idx, sequence, multi))),
157 }
158 }
159}
160
161#[derive(Debug, Clone, Default)]
163pub struct ScriptProgress {
164 state: Arc<RwLock<HashMap<usize, SequenceProgress>>>,
165 multi: MultiProgress,
166}
167
168impl ScriptProgress {
169 pub fn get_sequence_progress<N: Network>(
172 &self,
173 sequence_idx: usize,
174 sequence: &ScriptSequence<N>,
175 ) -> SequenceProgress {
176 if let Some(progress) = self.state.read().get(&sequence_idx) {
177 return progress.clone();
178 }
179 let progress = SequenceProgress::new(sequence_idx, sequence, self.multi.clone());
180 self.state.write().insert(sequence_idx, progress.clone());
181 progress
182 }
183
184 pub async fn wait_for_pending<N: Network>(
195 &self,
196 sequence_idx: usize,
197 deployment_sequence: &mut ScriptSequence<N>,
198 provider: &RootProvider<N>,
199 timeout: u64,
200 ) -> Result<()> {
201 if deployment_sequence.pending.is_empty() {
202 return Ok(());
203 }
204
205 let count = deployment_sequence.pending.len();
206 let seq_progress = self.get_sequence_progress(sequence_idx, deployment_sequence);
207
208 seq_progress.inner.write().set_status("Waiting for pending transactions");
209
210 trace!("Checking status of {count} pending transactions");
211
212 let futs = deployment_sequence
213 .pending
214 .clone()
215 .into_iter()
216 .map(|tx| check_tx_status(provider, tx, timeout));
217 let mut tasks = futures::stream::iter(futs).buffer_unordered(10);
218
219 let mut errors: Vec<String> = vec![];
220 let mut discarded_transactions = false;
221
222 while let Some((tx_hash, result)) = tasks.next().await {
223 match result {
224 Err(err) => {
225 if err.downcast_ref::<PendingReceiptError>().is_some() {
227 discarded_transactions = true;
230 deployment_sequence.remove_pending(tx_hash);
231 seq_progress
232 .inner
233 .write()
234 .finish_tx_spinner_with_msg(tx_hash, &err.to_string())?;
235 } else {
236 errors.push(format!(
237 "Failure on receiving a receipt for {tx_hash:?}:\n{err}"
238 ));
239 seq_progress.inner.write().finish_tx_spinner(tx_hash);
240 }
241 }
242 Ok(TxStatus::Dropped) => {
243 deployment_sequence.remove_pending(tx_hash);
245 discarded_transactions = true;
246
247 let msg = format!(
248 "Transaction {tx_hash:?} dropped from the mempool. It will be retried when using --resume."
249 );
250 seq_progress.inner.write().finish_tx_spinner_with_msg(tx_hash, &msg)?;
251 }
252 Ok(TxStatus::Success(receipt)) => {
253 trace!(tx_hash=?tx_hash, "received tx receipt");
254
255 let msg = format_receipt(
256 deployment_sequence.chain.into(),
257 &receipt,
258 Some(deployment_sequence),
259 );
260 seq_progress.inner.write().finish_tx_spinner_with_msg(tx_hash, &msg)?;
261
262 deployment_sequence.remove_pending(receipt.transaction_hash());
263 deployment_sequence.add_receipt(receipt);
264 }
265 Ok(TxStatus::Revert(receipt)) => {
266 warn!(tx_hash=?tx_hash, "Transaction Failure");
270 deployment_sequence.remove_pending(receipt.transaction_hash());
271
272 let msg = format_receipt(
273 deployment_sequence.chain.into(),
274 &receipt,
275 Some(deployment_sequence),
276 );
277 seq_progress.inner.write().finish_tx_spinner_with_msg(tx_hash, &msg)?;
278
279 errors.push(format!("Transaction Failure: {:?}", receipt.transaction_hash()));
280 }
281 }
282 }
283
284 if !errors.is_empty() {
286 let mut error_msg = errors.join("\n");
287
288 if !deployment_sequence.pending.is_empty() || discarded_transactions {
290 error_msg += r#"
291
292Add `--resume` to your command to try and continue broadcasting the transactions. This will attempt to resend transactions that were discarded by the RPC."#;
293 }
294
295 eyre::bail!(error_msg);
296 } else if discarded_transactions {
297 sh_warn!(
299 "Some transactions were discarded by the RPC node. Use `--resume` to retry these transactions."
300 )?;
301 }
302
303 Ok(())
304 }
305}