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