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        submission_hashes: (&[B256], &[B256]),
202    ) -> Result<()> {
203        let (durable_hashes, replayable_hashes) = submission_hashes;
204        if deployment_sequence.pending.is_empty() {
205            return Ok(());
206        }
207
208        let count = deployment_sequence.pending.len();
209        let seq_progress = self.get_sequence_progress(sequence_idx, deployment_sequence);
210
211        seq_progress.inner.write().set_status("Waiting for pending transactions");
212
213        trace!("Checking status of {count} pending transactions");
214
215        let futs = deployment_sequence
216            .pending
217            .clone()
218            .into_iter()
219            .map(|tx| check_tx_status(provider, tx, timeout, confirmations));
220        let mut tasks = futures::stream::iter(futs).buffer_unordered(10);
221
222        let mut errors: Vec<String> = vec![];
223        let mut discarded_transactions = false;
224
225        while let Some((tx_hash, result)) = tasks.next().await {
226            match result {
227                Err(err) => {
228                    // Check if this is a retry error for pending receipts
229                    if err.downcast_ref::<PendingReceiptError>().is_some() {
230                        // We've already retried several times with sleep, but the receipt is still
231                        // pending
232                        if durable_hashes.contains(&tx_hash) {
233                            errors.push(format!(
234                                "Durable submission {tx_hash:?} is still pending; refusing to discard its recovery identity"
235                            ));
236                        } else {
237                            discarded_transactions = true;
238                            deployment_sequence.remove_pending(tx_hash);
239                        }
240                        seq_progress
241                            .inner
242                            .write()
243                            .finish_tx_spinner_with_msg(tx_hash, &err.to_string())?;
244                    } else {
245                        errors.push(format!(
246                            "Failure on receiving a receipt for {tx_hash:?}:\n{err}"
247                        ));
248                        seq_progress.inner.write().finish_tx_spinner(tx_hash);
249                    }
250                }
251                Ok(TxStatus::Dropped) => {
252                    if replayable_hashes.contains(&tx_hash) {
253                        deployment_sequence.remove_pending(tx_hash);
254                        discarded_transactions = true;
255                    } else if durable_hashes.contains(&tx_hash) {
256                        errors.push(format!(
257                            "Durable submission {tx_hash:?} is not currently visible; refusing to discard its recovery identity"
258                        ));
259                    } else {
260                        // We want to remove it from pending so it will be re-broadcast.
261                        deployment_sequence.remove_pending(tx_hash);
262                        discarded_transactions = true;
263                    }
264
265                    let msg = format!(
266                        "Transaction {tx_hash:?} is not currently visible to the RPC endpoint."
267                    );
268                    seq_progress.inner.write().finish_tx_spinner_with_msg(tx_hash, &msg)?;
269                }
270                Ok(TxStatus::Success(receipt)) => {
271                    trace!(tx_hash=?tx_hash, "received tx receipt");
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                    deployment_sequence.remove_pending(receipt.transaction_hash());
281                    deployment_sequence.add_receipt(receipt);
282                }
283                Ok(TxStatus::Revert(receipt)) => {
284                    // consider:
285                    // if this is not removed from pending, then the script becomes
286                    // un-resumable. Is this desirable on reverts?
287                    warn!(tx_hash=?tx_hash, "Transaction Failure");
288                    deployment_sequence.remove_pending(receipt.transaction_hash());
289
290                    let msg = format_receipt(
291                        deployment_sequence.chain.into(),
292                        &receipt,
293                        Some(deployment_sequence),
294                    );
295                    seq_progress.inner.write().finish_tx_spinner_with_msg(tx_hash, &msg)?;
296
297                    errors.push(format!("Transaction Failure: {:?}", receipt.transaction_hash()));
298                }
299            }
300        }
301
302        // print any errors
303        if !errors.is_empty() {
304            let mut error_msg = errors.join("\n");
305
306            // Add information about using --resume if necessary
307            if !deployment_sequence.pending.is_empty() || discarded_transactions {
308                error_msg += r#"
309
310Add `--resume` to your command to try and continue broadcasting the transactions. This will attempt to resend transactions that were discarded by the RPC."#;
311            }
312
313            eyre::bail!(error_msg);
314        } else if discarded_transactions {
315            // If we have discarded transactions but no errors, still inform the user
316            sh_warn!(
317                "Some transactions were discarded by the RPC node. Use `--resume` to retry these transactions."
318            )?;
319        }
320
321        Ok(())
322    }
323}