forge_script/
progress.rs

1use crate::receipts::{check_tx_status, format_receipt, TxStatus};
2use alloy_chains::Chain;
3use alloy_primitives::{
4    map::{B256HashMap, HashMap},
5    B256,
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/// State of [ProgressBar]s displayed for the given [ScriptSequence].
18#[derive(Debug)]
19pub struct SequenceProgressState {
20    /// The top spinner with content of the format "Sequence #{id} on {network} | {status}""
21    top_spinner: ProgressBar,
22    /// Progress bar with the count of transactions.
23    txs: ProgressBar,
24    /// Progress var with the count of confirmed transactions.
25    receipts: ProgressBar,
26    /// Standalone spinners for pending transactions.
27    tx_spinners: B256HashMap<ProgressBar>,
28    /// Copy of the main [MultiProgress] instance.
29    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    /// Called when a new transaction is sent. Displays a spinner with a hash of the transaction and
79    /// advances the sent transactions progress bar.
80    pub fn tx_sent(&mut self, tx_hash: B256) {
81        // Avoid showing more than 10 spinners.
82        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    /// Removes the pending transaction spinner and advances confirmed transactions progress bar.
105    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    /// Same as finish_tx_spinner but also prints a message to stdout above all other progress bars.
113    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    /// Sets status for the current sequence progress.
124    pub fn set_status(&mut self, status: &str) {
125        self.top_spinner.set_message(format!(" | {status}"));
126    }
127
128    /// Hides transactions and receipts progress bar, leaving only top line with the latest set
129    /// status.
130    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/// Clonable wrapper around [SequenceProgressState].
138#[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/// Container for multiple [SequenceProgress] instances keyed by sequence index.
152#[derive(Debug, Clone, Default)]
153pub struct ScriptProgress {
154    state: Arc<RwLock<HashMap<usize, SequenceProgress>>>,
155    multi: MultiProgress,
156}
157
158impl ScriptProgress {
159    /// Returns a [SequenceProgress] instance for the given sequence index. If it doesn't exist,
160    /// creates one.
161    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    /// Traverses a set of pendings and either finds receipts, or clears them from
175    /// the deployment sequence.
176    ///
177    /// For each `tx_hash`, we check if it has confirmed. If it has
178    /// confirmed, we push the receipt (if successful) or push an error (if
179    /// revert). If the transaction has not confirmed, but can be found in the
180    /// node's mempool, we wait for its receipt to be available. If the transaction
181    /// has not confirmed, and cannot be found in the mempool, we remove it from
182    /// the `deploy_sequence.pending` vector so that it will be rebroadcast in
183    /// later steps.
184    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
211        while let Some((tx_hash, result)) = tasks.next().await {
212            match result {
213                Err(err) => {
214                    errors.push(format!("Failure on receiving a receipt for {tx_hash:?}:\n{err}"));
215
216                    seq_progress.inner.write().finish_tx_spinner(tx_hash);
217                }
218                Ok(TxStatus::Dropped) => {
219                    // We want to remove it from pending so it will be re-broadcast.
220                    deployment_sequence.remove_pending(tx_hash);
221                    errors.push(format!("Transaction dropped from the mempool: {tx_hash:?}"));
222
223                    seq_progress.inner.write().finish_tx_spinner(tx_hash);
224                }
225                Ok(TxStatus::Success(receipt)) => {
226                    trace!(tx_hash=?tx_hash, "received tx receipt");
227
228                    let msg = format_receipt(deployment_sequence.chain.into(), &receipt);
229                    seq_progress.inner.write().finish_tx_spinner_with_msg(tx_hash, &msg)?;
230
231                    deployment_sequence.remove_pending(receipt.transaction_hash);
232                    deployment_sequence.add_receipt(receipt);
233                }
234                Ok(TxStatus::Revert(receipt)) => {
235                    // consider:
236                    // if this is not removed from pending, then the script becomes
237                    // un-resumable. Is this desirable on reverts?
238                    warn!(tx_hash=?tx_hash, "Transaction Failure");
239                    deployment_sequence.remove_pending(receipt.transaction_hash);
240
241                    let msg = format_receipt(deployment_sequence.chain.into(), &receipt);
242                    seq_progress.inner.write().finish_tx_spinner_with_msg(tx_hash, &msg)?;
243
244                    errors.push(format!("Transaction Failure: {:?}", receipt.transaction_hash));
245                }
246            }
247        }
248
249        // print any errors
250        if !errors.is_empty() {
251            let mut error_msg = errors.join("\n");
252            if !deployment_sequence.pending.is_empty() {
253                error_msg += "\n\n Add `--resume` to your command to try and continue broadcasting
254        the transactions."
255            }
256            eyre::bail!(error_msg);
257        }
258
259        Ok(())
260    }
261}