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    ) -> 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                    // Check if this is a retry error for pending receipts
226                    if err.downcast_ref::<PendingReceiptError>().is_some() {
227                        // We've already retried several times with sleep, but the receipt is still
228                        // pending
229                        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                    // We want to remove it from pending so it will be re-broadcast.
244                    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                    // consider:
267                    // if this is not removed from pending, then the script becomes
268                    // un-resumable. Is this desirable on reverts?
269                    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        // print any errors
285        if !errors.is_empty() {
286            let mut error_msg = errors.join("\n");
287
288            // Add information about using --resume if necessary
289            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            // If we have discarded transactions but no errors, still inform the user
298            sh_warn!(
299                "Some transactions were discarded by the RPC node. Use `--resume` to retry these transactions."
300            )?;
301        }
302
303        Ok(())
304    }
305}