diff --git a/crates/script/src/progress.rs b/crates/script/src/progress.rs index 314c4d4983f3a..58d2270a8231e 100644 --- a/crates/script/src/progress.rs +++ b/crates/script/src/progress.rs @@ -1,13 +1,15 @@ use crate::receipts::{PendingReceiptError, TxStatus, check_tx_status, format_receipt}; use alloy_chains::Chain; use alloy_primitives::{ - B256, + Address, B256, map::{B256HashMap, HashMap}, }; use eyre::Result; use forge_script_sequence::ScriptSequence; +use forge_verify::provider::VerificationProviderType; use foundry_cli::utils::init_progress; use foundry_common::{provider::RetryProvider, shell}; +use foundry_compilers::artifacts::EvmVersion; use futures::StreamExt; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use parking_lot::RwLock; @@ -285,3 +287,144 @@ Add `--resume` to your command to try and continue broadcasting the transactions Ok(()) } } + +/// State of [ProgressBar]s displayed for contract verification. +/// Shows progress of all contracts being verified. +/// For each contract an individual progress bar is displayed. +/// When a contract verification completes, their progress is removed and result summary is +/// displayed. +#[derive(Debug)] +pub struct VerificationProgressState { + /// Main [MultiProgress] instance showing progress for all verifications. + multi: MultiProgress, + /// Progress bar counting completed / remaining verifications. + overall_progress: ProgressBar, + /// Individual contract verification progress. + contracts_progress: HashMap, + /// Contract details for display. + contract_details: HashMap, +} + +impl VerificationProgressState { + /// Creates overall verification progress state. + pub fn new(contracts_len: usize) -> Self { + let multi = MultiProgress::new(); + let overall_progress = multi.add(ProgressBar::new(contracts_len as u64)); + overall_progress.set_style( + indicatif::ProgressStyle::with_template("{bar:40.cyan/blue} {pos:>7}/{len:7} {msg}") + .unwrap() + .progress_chars("##-"), + ); + overall_progress.set_message("verifications completed"); + Self { + multi, + overall_progress, + contracts_progress: HashMap::default(), + contract_details: HashMap::default(), + } + } + + /// Creates new contract verification progress and add it to overall progress. + /// Displays contract details inline. + #[allow(clippy::too_many_arguments)] + pub fn start_contract_progress( + &mut self, + address: Address, + contract_name: Option<&str>, + chain: Chain, + evm_version: Option<&EvmVersion>, + compiler_version: Option<&str>, + constructor_args: Option<&str>, + verifier: &VerificationProviderType, + ) { + let contract_progress = self.multi.add(ProgressBar::new_spinner()); + contract_progress.set_style( + indicatif::ProgressStyle::with_template("{spinner} {wide_msg:.bold.dim}") + .unwrap() + .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "), + ); + + let display_name = contract_name + .map(|n| format!("{n} ({address})")) + .unwrap_or_else(|| format!("{address}")); + + // Build details string + let mut details = format!("{display_name}\n"); + details.push_str(&format!(" - chain: {chain}\n")); + if let Some(evm) = evm_version { + details.push_str(&format!(" - EVM version: {evm}\n")); + } + if let Some(compiler) = compiler_version { + details.push_str(&format!(" - Compiler version: {compiler}\n")); + } + if let Some(args) = constructor_args + && !args.is_empty() + { + details.push_str(&format!(" - Constructor args: {args}\n")); + } + details.push_str(&format!(" - verifier: {verifier}")); + + // Store details for later use + self.contract_details.insert(address, details.clone()); + + contract_progress.set_message(details); + contract_progress.enable_steady_tick(Duration::from_millis(100)); + self.contracts_progress.insert(address, contract_progress); + } + + /// Updates contract verification progress message inline (as last entry). + /// This can be used to show intermediate progress messages like "Submitted contract for + /// verification" or "Verification Job ID: ..." during the verification process. + #[allow(dead_code)] + pub fn update_contract_progress(&mut self, address: Address, message: &str) { + if let Some(contract_progress) = self.contracts_progress.get(&address) { + if let Some(details) = self.contract_details.get(&address) { + let updated_message = format!("{details}\n - {message}"); + contract_progress.set_message(updated_message); + } else { + contract_progress.set_message(message.to_string()); + } + } + } + + /// Prints contract verification result summary and removes it from overall progress. + /// Updates progress message inline instead of using suspend to avoid progress bar flakiness. + pub fn end_contract_progress(&mut self, address: Address, success: bool, error: Option<&str>) { + if let Some(contract_progress) = self.contracts_progress.remove(&address) { + // Update message inline as last entry + let status = if success { "✓ Verified".green() } else { "✗ Failed".red() }; + let final_message = if let Some(details) = self.contract_details.remove(&address) { + if let Some(err) = error { + format!("{details}\n - {status}: {err}") + } else { + format!("{details}\n - {status}") + } + } else if let Some(err) = error { + format!("{address}\n - {status}: {err}") + } else { + format!("{address}\n - {status}") + }; + contract_progress.set_message(final_message); + contract_progress.finish_and_clear(); + // Increment overall progress bar to reflect completed verification. + self.overall_progress.inc(1); + } + } + + /// Removes overall verification progress. + pub fn clear(&mut self) { + self.multi.clear().unwrap(); + } +} + +/// Cloneable wrapper around [VerificationProgressState]. +#[derive(Debug, Clone)] +pub struct VerificationProgress { + pub inner: Arc>, +} + +impl VerificationProgress { + pub fn new(contracts_len: usize) -> Self { + Self { inner: Arc::new(RwLock::new(VerificationProgressState::new(contracts_len))) } + } +} diff --git a/crates/script/src/verify.rs b/crates/script/src/verify.rs index ceec637e66661..f9b3235294564 100644 --- a/crates/script/src/verify.rs +++ b/crates/script/src/verify.rs @@ -1,6 +1,7 @@ use crate::{ ScriptArgs, ScriptConfig, build::LinkedBuildData, + progress::VerificationProgress, sequence::{ScriptSequenceKind, get_commit_hash}, }; use alloy_primitives::{Address, hex}; @@ -11,6 +12,7 @@ use foundry_cli::opts::{EtherscanOpts, ProjectPathOpts}; use foundry_common::ContractsByArtifact; use foundry_compilers::{Project, artifacts::EvmVersion, info::ContractInfo}; use foundry_config::{Chain, Config}; +use futures::future::join_all; use semver::Version; /// State after we have broadcasted the script. @@ -102,6 +104,7 @@ impl VerifyBundle { /// Given a `VerifyBundle` and contract details, it tries to generate a valid `VerifyArgs` to /// use against the `contract_address`. + /// Returns both the VerifyArgs and the contract name if found. pub fn get_verify_args( &self, contract_address: Address, @@ -109,7 +112,7 @@ impl VerifyBundle { data: &[u8], libraries: &[String], evm_version: EvmVersion, - ) -> Option { + ) -> Option<(VerifyArgs, String)> { for (artifact, contract) in self.known_contracts.iter() { let Some(bytecode) = contract.bytecode() else { continue }; // If it's a CREATE2, the tx.data comes with a 32-byte salt in the beginning @@ -140,6 +143,7 @@ impl VerifyBundle { artifact.version.patch, ); + let contract_name = contract.name.clone(); let verify = VerifyArgs { address: contract_address, contract: Some(contract), @@ -168,7 +172,7 @@ impl VerifyBundle { creation_transaction_hash: None, }; - return Some(verify); + return Some((verify, contract_name)); } } None @@ -190,12 +194,15 @@ async fn verify_contracts( { trace!(target: "script", "prepare future verifications"); - let mut future_verifications = Vec::with_capacity(sequence.receipts.len()); + // Store verification info: (address, contract_name, contract_details, future) + let mut verification_tasks = Vec::new(); let mut unverifiable_contracts = vec![]; // Make sure the receipts have the right order first. sequence.sort_receipts(); + let chain: Chain = sequence.chain.into(); + for (receipt, tx) in sequence.receipts.iter_mut().zip(sequence.transactions.iter()) { // create2 hash offset let mut offset = 0; @@ -214,7 +221,20 @@ async fn verify_contracts( &sequence.libraries, config.evm_version, ) { - Some(verify) => future_verifications.push(verify.run()), + Some((verify_args, contract_name)) => { + // Extract details before moving verify_args + let evm_version = verify_args.evm_version; + let compiler_version = verify_args.compiler_version.clone(); + let constructor_args = verify_args.constructor_args.clone(); + let verifier_type = verify_args.verifier.verifier.clone(); + let future = verify_args.run(); + verification_tasks.push(( + address, + Some(contract_name), + (evm_version, compiler_version, constructor_args, verifier_type), + future, + )); + } None => unverifiable_contracts.push(address), }; } @@ -228,30 +248,106 @@ async fn verify_contracts( &sequence.libraries, config.evm_version, ) { - Some(verify) => future_verifications.push(verify.run()), + Some((verify_args, contract_name)) => { + // Extract details before moving verify_args + let evm_version = verify_args.evm_version; + let compiler_version = verify_args.compiler_version.clone(); + let constructor_args = verify_args.constructor_args.clone(); + let verifier_type = verify_args.verifier.verifier.clone(); + let future = verify_args.run(); + verification_tasks.push(( + *address, + Some(contract_name), + (evm_version, compiler_version, constructor_args, verifier_type), + future, + )); + } None => unverifiable_contracts.push(*address), }; } } - trace!(target: "script", "collected {} verification jobs and {} unverifiable contracts", future_verifications.len(), unverifiable_contracts.len()); + trace!(target: "script", "collected {} verification jobs and {} unverifiable contracts", verification_tasks.len(), unverifiable_contracts.len()); check_unverified(sequence, unverifiable_contracts, verify); - let num_verifications = future_verifications.len(); - let mut num_of_successful_verifications = 0; + let num_verifications = verification_tasks.len(); + if num_verifications == 0 { + return Ok(()); + } + + // Create progress tracker + let progress = VerificationProgress::new(num_verifications); + let progress_ref = &progress; + + // Start progress for all contracts with details + for ( + address, + contract_name, + (evm_version, compiler_version, constructor_args, verifier_type), + _, + ) in &verification_tasks + { + progress_ref.inner.write().start_contract_progress( + *address, + contract_name.as_deref(), + chain, + evm_version.as_ref(), + compiler_version.as_deref(), + constructor_args.as_deref(), + verifier_type, + ); + } + + // Wrap each verification future to track progress + let futures_with_progress: Vec<_> = verification_tasks + .into_iter() + .map(|(address, _contract_name, _contract_details, future)| { + let progress = progress_ref.clone(); + async move { + let result = future.await; + let (success, error) = match &result { + Ok(_) => (true, None), + Err(err) => (false, Some(err.to_string())), + }; + progress.inner.write().end_contract_progress( + address, + success, + error.as_deref(), + ); + result + } + }) + .collect(); + sh_println!("##\nStart verification for ({num_verifications}) contracts")?; - for verification in future_verifications { - match verification.await { + let results = join_all(futures_with_progress).await; + + // Collect all errors and successes + let mut num_of_successful_verifications = 0; + let mut errors = Vec::new(); + + for result in results { + match result { Ok(_) => { num_of_successful_verifications += 1; } Err(err) => { - sh_err!("Failed to verify contract: {err:#}")?; + errors.push(err); } } } + // Clear progress display + progress.inner.write().clear(); + + // Report results + if !errors.is_empty() { + for err in &errors { + sh_err!("Failed to verify contract: {err:#}")?; + } + } + if num_of_successful_verifications < num_verifications { return Err(eyre!( "Not all ({num_of_successful_verifications} / {num_verifications}) contracts were verified!"