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#[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
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 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 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 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}