Skip to main content

forge_script/
progress.rs

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/// State of [ProgressBar]s displayed for the given [ScriptSequence].
20#[derive(Debug)]
21pub struct SequenceProgressState {
22    /// The top spinner with content of the format "Sequence #{id} on {network} | {status}""
23    top_spinner: ProgressBar,
24    /// Progress bar with the count of transactions.
25    txs: ProgressBar,
26    /// Progress var with the count of confirmed transactions.
27    receipts: ProgressBar,
28    /// Standalone spinners for pending transactions.
29    tx_spinners: B256HashMap<ProgressBar>,
30    /// Copy of the main [MultiProgress] instance.
31    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    /// Called when a new transaction is sent. Displays a spinner with a hash of the transaction and
85    /// advances the sent transactions progress bar.
86    pub fn tx_sent(&mut self, tx_hash: B256) {
87        // Avoid showing more than 10 spinners.
88        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    /// Removes the pending transaction spinner and advances confirmed transactions progress bar.
111    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    /// Same as finish_tx_spinner but also prints a message to stdout above all other progress bars.
119    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    /// Sets status for the current sequence progress.
130    pub fn set_status(&mut self, status: &str) {
131        self.top_spinner.set_message(format!(" | {status}"));
132    }
133
134    /// Hides transactions and receipts progress bar, leaving only top line with the latest set
135    /// status.
136    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/// Cloneable wrapper around [SequenceProgressState].
144#[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/// Container for multiple [SequenceProgress] instances keyed by sequence index.
162#[derive(Debug, Clone, Default)]
163pub struct ScriptProgress {
164    state: Arc<RwLock<HashMap<usize, SequenceProgress>>>,
165    multi: MultiProgress,
166}
167
168impl ScriptProgress {
169    /// Returns a [SequenceProgress] instance for the given sequence index. If it doesn't exist,
170    /// creates one.
171    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    /// Traverses a set of pending transactions and either finds receipts, or clears
185    /// them from the deployment sequence.
186    ///
187    /// For each `tx_hash`, we check if it has confirmed. If it has
188    /// confirmed, we push the receipt (if successful) or push an error (if
189    /// revert). If the transaction has not confirmed, but can be found in the
190    /// node's mempool, we wait for its receipt to be available. If the transaction
191    /// has not confirmed, and cannot be found in the mempool, we remove it from
192    /// the `deploy_sequence.pending` vector so that it will be rebroadcast in
193    /// later steps.
194    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        confirmations: u64,
201    ) -> Result<()> {
202        if deployment_sequence.pending.is_empty() {
203            return Ok(());
204        }
205
206        let count = deployment_sequence.pending.len();
207        let seq_progress = self.get_sequence_progress(sequence_idx, deployment_sequence);
208
209        seq_progress.inner.write().set_status("Waiting for pending transactions");
210
211        trace!("Checking status of {count} pending transactions");
212
213        let futs = deployment_sequence
214            .pending
215            .clone()
216            .into_iter()
217            .map(|tx| check_tx_status(provider, tx, timeout, confirmations));
218        let mut tasks = futures::stream::iter(futs).buffer_unordered(10);
219
220        let mut errors: Vec<String> = vec![];
221        let mut discarded_transactions = false;
222
223        while let Some((tx_hash, result)) = tasks.next().await {
224            match result {
225                Err(err) => {
226                    // Check if this is a retry error for pending receipts
227                    if err.downcast_ref::<PendingReceiptError>().is_some() {
228                        // We've already retried several times with sleep, but the receipt is still
229                        // pending
230                        discarded_transactions = true;
231                        deployment_sequence.remove_pending(tx_hash);
232                        seq_progress
233                            .inner
234                            .write()
235                            .finish_tx_spinner_with_msg(tx_hash, &err.to_string())?;
236                    } else {
237                        errors.push(format!(
238                            "Failure on receiving a receipt for {tx_hash:?}:\n{err}"
239                        ));
240                        seq_progress.inner.write().finish_tx_spinner(tx_hash);
241                    }
242                }
243                Ok(TxStatus::Dropped) => {
244                    // We want to remove it from pending so it will be re-broadcast.
245                    deployment_sequence.remove_pending(tx_hash);
246                    discarded_transactions = true;
247
248                    let msg = format!(
249                        "Transaction {tx_hash:?} dropped from the mempool. It will be retried when using --resume."
250                    );
251                    seq_progress.inner.write().finish_tx_spinner_with_msg(tx_hash, &msg)?;
252                }
253                Ok(TxStatus::Success(receipt)) => {
254                    trace!(tx_hash=?tx_hash, "received tx receipt");
255
256                    let msg = format_receipt(
257                        deployment_sequence.chain.into(),
258                        &receipt,
259                        Some(deployment_sequence),
260                    );
261                    seq_progress.inner.write().finish_tx_spinner_with_msg(tx_hash, &msg)?;
262
263                    deployment_sequence.remove_pending(receipt.transaction_hash());
264                    deployment_sequence.add_receipt(receipt);
265                }
266                Ok(TxStatus::Revert(receipt)) => {
267                    // consider:
268                    // if this is not removed from pending, then the script becomes
269                    // un-resumable. Is this desirable on reverts?
270                    warn!(tx_hash=?tx_hash, "Transaction Failure");
271                    deployment_sequence.remove_pending(receipt.transaction_hash());
272
273                    let msg = format_receipt(
274                        deployment_sequence.chain.into(),
275                        &receipt,
276                        Some(deployment_sequence),
277                    );
278                    seq_progress.inner.write().finish_tx_spinner_with_msg(tx_hash, &msg)?;
279
280                    errors.push(format!("Transaction Failure: {:?}", receipt.transaction_hash()));
281                }
282            }
283        }
284
285        // print any errors
286        if !errors.is_empty() {
287            let mut error_msg = errors.join("\n");
288
289            // Add information about using --resume if necessary
290            if !deployment_sequence.pending.is_empty() || discarded_transactions {
291                error_msg += r#"
292
293Add `--resume` to your command to try and continue broadcasting the transactions. This will attempt to resend transactions that were discarded by the RPC."#;
294            }
295
296            eyre::bail!(error_msg);
297        } else if discarded_transactions {
298            // If we have discarded transactions but no errors, still inform the user
299            sh_warn!(
300                "Some transactions were discarded by the RPC node. Use `--resume` to retry these transactions."
301            )?;
302        }
303
304        Ok(())
305    }
306}