1use crate::receipts::{
2 PendingReceiptError, TxStatus, check_tx_status, check_tx_status_once, format_receipt,
3};
4use alloy_chains::Chain;
5use alloy_network::{Network, ReceiptResponse};
6use alloy_primitives::{
7 B256,
8 map::{B256HashMap, HashMap},
9};
10use alloy_provider::RootProvider;
11use eyre::Result;
12use forge_script_sequence::ScriptSequence;
13use foundry_cli::utils::init_progress;
14use foundry_common::shell;
15use futures::StreamExt;
16use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
17use parking_lot::RwLock;
18use std::{fmt::Write, sync::Arc, time::Duration};
19use yansi::Paint;
20
21#[derive(Debug)]
23pub struct SequenceProgressState {
24 top_spinner: ProgressBar,
26 txs: ProgressBar,
28 receipts: ProgressBar,
30 tx_spinners: B256HashMap<ProgressBar>,
32 multi: MultiProgress,
34}
35
36impl SequenceProgressState {
37 pub fn new<N: Network>(
38 sequence_idx: usize,
39 sequence: &ScriptSequence<N>,
40 multi: MultiProgress,
41 ) -> Self {
42 let mut state = if shell::is_quiet() || shell::is_json() {
43 let top_spinner = ProgressBar::hidden();
44 let txs = ProgressBar::hidden();
45 let receipts = ProgressBar::hidden();
46
47 Self { top_spinner, txs, receipts, tx_spinners: Default::default(), multi }
48 } else {
49 let mut template = "{spinner:.green}".to_string();
50 write!(template, " Sequence #{} on {}", sequence_idx + 1, Chain::from(sequence.chain))
51 .unwrap();
52 template.push_str("{msg}");
53
54 let top_spinner = ProgressBar::new_spinner().with_style(
55 ProgressStyle::with_template(&template).unwrap().tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈✅"),
56 );
57 let top_spinner = multi.add(top_spinner);
58
59 let txs = multi.insert_after(
60 &top_spinner,
61 init_progress(sequence.transactions.len() as u64, "txes").with_prefix(" "),
62 );
63
64 let receipts = multi.insert_after(
65 &txs,
66 init_progress(sequence.transactions.len() as u64, "receipts").with_prefix(" "),
67 );
68
69 top_spinner.enable_steady_tick(Duration::from_millis(100));
70 txs.enable_steady_tick(Duration::from_millis(1000));
71 receipts.enable_steady_tick(Duration::from_millis(1000));
72
73 txs.set_position(sequence.receipts.len() as u64);
74 receipts.set_position(sequence.receipts.len() as u64);
75
76 Self { top_spinner, txs, receipts, tx_spinners: Default::default(), multi }
77 };
78
79 for tx_hash in &sequence.pending {
80 state.tx_sent(*tx_hash);
81 }
82
83 state
84 }
85
86 pub fn tx_sent(&mut self, tx_hash: B256) {
89 if self.tx_spinners.len() < 10 {
91 let spinner = if shell::is_quiet() || shell::is_json() {
92 ProgressBar::hidden()
93 } else {
94 let spinner = ProgressBar::new_spinner()
95 .with_style(
96 ProgressStyle::with_template(" {spinner:.green} {msg}")
97 .unwrap()
98 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈"),
99 )
100 .with_message(format!("{} {}", "[Pending]".yellow(), tx_hash));
101
102 let spinner = self.multi.insert_before(&self.txs, spinner);
103 spinner.enable_steady_tick(Duration::from_millis(100));
104 spinner
105 };
106
107 self.tx_spinners.insert(tx_hash, spinner);
108 }
109 self.txs.inc(1);
110 }
111
112 pub fn finish_tx_spinner(&mut self, tx_hash: B256) {
114 if let Some(spinner) = self.tx_spinners.remove(&tx_hash) {
115 spinner.finish_and_clear();
116 }
117 self.receipts.inc(1);
118 }
119
120 pub fn finish_tx_spinner_with_msg(&mut self, tx_hash: B256, msg: &str) -> std::io::Result<()> {
122 self.finish_tx_spinner(tx_hash);
123
124 if !(shell::is_quiet() || shell::is_json()) {
125 self.multi.println(msg)?;
126 }
127
128 Ok(())
129 }
130
131 pub fn set_status(&mut self, status: &str) {
133 self.top_spinner.set_message(format!(" | {status}"));
134 }
135
136 pub fn finish(&self) {
139 for spinner in self.tx_spinners.values() {
141 spinner.finish_and_clear();
142 }
143 self.top_spinner.finish();
144 self.txs.finish_and_clear();
145 self.receipts.finish_and_clear();
146 }
147}
148
149#[derive(Debug, Clone)]
151pub struct SequenceProgress {
152 pub inner: Arc<RwLock<SequenceProgressState>>,
153}
154
155impl SequenceProgress {
156 pub fn new<N: Network>(
157 sequence_idx: usize,
158 sequence: &ScriptSequence<N>,
159 multi: MultiProgress,
160 ) -> Self {
161 Self {
162 inner: Arc::new(RwLock::new(SequenceProgressState::new(sequence_idx, sequence, multi))),
163 }
164 }
165}
166
167#[derive(Debug, Clone, Default)]
169pub struct ScriptProgress {
170 state: Arc<RwLock<HashMap<usize, SequenceProgress>>>,
171 multi: MultiProgress,
172}
173
174impl ScriptProgress {
175 pub fn get_sequence_progress<N: Network>(
178 &self,
179 sequence_idx: usize,
180 sequence: &ScriptSequence<N>,
181 ) -> SequenceProgress {
182 if let Some(progress) = self.state.read().get(&sequence_idx) {
183 return progress.clone();
184 }
185 let progress = SequenceProgress::new(sequence_idx, sequence, self.multi.clone());
186 self.state.write().insert(sequence_idx, progress.clone());
187 progress
188 }
189
190 #[allow(clippy::too_many_arguments)]
203 pub async fn wait_for_pending<N: Network>(
204 &self,
205 sequence_idx: usize,
206 deployment_sequence: &mut ScriptSequence<N>,
207 provider: &RootProvider<N>,
208 timeout: u64,
209 confirmations: u64,
210 submission_hashes: (&[B256], &[B256]),
211 blocked_hashes: &[B256],
212 ) -> Result<()> {
213 let (durable_hashes, replayable_hashes) = submission_hashes;
214 if deployment_sequence.pending.is_empty() {
215 return Ok(());
216 }
217
218 let count = deployment_sequence.pending.len();
219 let seq_progress = self.get_sequence_progress(sequence_idx, deployment_sequence);
220
221 seq_progress.inner.write().set_status("Waiting for pending transactions");
222
223 trace!("Checking status of {count} pending transactions");
224
225 let mut pending = deployment_sequence.pending.clone();
227 pending.sort_by_cached_key(|hash| {
228 deployment_sequence.transactions.iter().position(|tx| tx.hash == Some(*hash))
229 });
230
231 let futs = pending
232 .iter()
233 .filter(|hash| !blocked_hashes.contains(hash))
234 .map(|&tx| check_tx_status(provider, tx, timeout, confirmations));
235 let mut tasks = Some(futures::stream::iter(futs).buffer_unordered(10));
236 let mut polled = Vec::with_capacity(count);
237 let mut unpolled = None;
238
239 let mut errors: Vec<String> = vec![];
240 let mut discarded_transactions = false;
241
242 loop {
243 let (tx_hash, result) = if let Some(tasks) = &mut tasks
244 && let Some((tx_hash, result)) = tasks.next().await
245 {
246 polled.push(tx_hash);
247 (tx_hash, result)
248 } else {
249 tasks = None;
251 let unpolled = unpolled.get_or_insert_with(|| {
252 pending
253 .iter()
254 .rev()
255 .filter(|hash| !polled.contains(hash))
256 .copied()
257 .collect::<Vec<_>>()
258 });
259 let Some(tx_hash) = unpolled.pop() else { break };
260 let Some(status) = check_tx_status_once(provider, tx_hash, confirmations).await
261 else {
262 continue;
263 };
264 (tx_hash, Ok(status))
265 };
266 match result {
267 Err(err) => {
268 if err.downcast_ref::<PendingReceiptError>().is_some() {
270 if durable_hashes.contains(&tx_hash) {
273 errors.push(format!(
274 "Durable submission {tx_hash:?} is still pending; refusing to discard its recovery identity"
275 ));
276 } else {
277 discarded_transactions = true;
278 deployment_sequence.remove_pending(tx_hash);
279 }
280 seq_progress
281 .inner
282 .write()
283 .finish_tx_spinner_with_msg(tx_hash, &err.to_string())?;
284 } else {
285 errors.push(format!(
286 "Failure on receiving a receipt for {tx_hash:?}:\n{err}"
287 ));
288 seq_progress.inner.write().finish_tx_spinner(tx_hash);
289 }
290 }
291 Ok(TxStatus::Dropped) => {
292 if replayable_hashes.contains(&tx_hash) {
293 deployment_sequence.remove_pending(tx_hash);
294 discarded_transactions = true;
295 } else if durable_hashes.contains(&tx_hash) {
296 errors.push(format!(
297 "Durable submission {tx_hash:?} is not currently visible; refusing to discard its recovery identity"
298 ));
299 } else {
300 deployment_sequence.remove_pending(tx_hash);
302 discarded_transactions = true;
303 }
304
305 let msg = format!(
306 "Transaction {tx_hash:?} is not currently visible to the RPC endpoint."
307 );
308 seq_progress.inner.write().finish_tx_spinner_with_msg(tx_hash, &msg)?;
309
310 tasks = None;
313 }
314 Ok(TxStatus::Success(receipt)) => {
315 trace!(tx_hash=?tx_hash, "received tx receipt");
316
317 let msg = format_receipt(
318 deployment_sequence.chain.into(),
319 &receipt,
320 Some(deployment_sequence),
321 );
322 seq_progress.inner.write().finish_tx_spinner_with_msg(tx_hash, &msg)?;
323
324 deployment_sequence.remove_pending(receipt.transaction_hash());
325 deployment_sequence.add_receipt(receipt);
326 }
327 Ok(TxStatus::Revert(receipt)) => {
328 warn!(tx_hash=?tx_hash, "Transaction Failure");
329 deployment_sequence.remove_pending(receipt.transaction_hash());
330 if !deployment_sequence.reverted.contains(&tx_hash) {
331 deployment_sequence.reverted.push(tx_hash);
332 }
333
334 let msg = format_receipt(
335 deployment_sequence.chain.into(),
336 &receipt,
337 Some(deployment_sequence),
338 );
339 seq_progress.inner.write().finish_tx_spinner_with_msg(tx_hash, &msg)?;
340
341 errors.push(format!("Transaction Failure: {:?}", receipt.transaction_hash()));
342 }
343 }
344 }
345
346 if !errors.is_empty() {
348 let mut error_msg = errors.join("\n");
349
350 if !deployment_sequence.pending.is_empty() || discarded_transactions {
352 error_msg += r#"
353
354Add `--resume` to your command to try and continue broadcasting the transactions. This will attempt to resend transactions that were discarded by the RPC."#;
355 }
356
357 eyre::bail!(error_msg);
358 } else if discarded_transactions {
359 sh_warn!(
361 "Some transactions were discarded by the RPC node. Use `--resume` to retry these transactions."
362 )?;
363 }
364
365 Ok(())
366 }
367}