Skip to content
145 changes: 144 additions & 1 deletion crates/script/src/progress.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Address, ProgressBar>,
/// Contract details for display.
contract_details: HashMap<Address, String>,
}

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<RwLock<VerificationProgressState>>,
}

impl VerificationProgress {
pub fn new(contracts_len: usize) -> Self {
Self { inner: Arc::new(RwLock::new(VerificationProgressState::new(contracts_len))) }
}
}
118 changes: 107 additions & 11 deletions crates/script/src/verify.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
ScriptArgs, ScriptConfig,
build::LinkedBuildData,
progress::VerificationProgress,
sequence::{ScriptSequenceKind, get_commit_hash},
};
use alloy_primitives::{Address, hex};
Expand All @@ -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.
Expand Down Expand Up @@ -102,14 +104,15 @@ 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,
create2_offset: usize,
data: &[u8],
libraries: &[String],
evm_version: EvmVersion,
) -> Option<VerifyArgs> {
) -> 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
Expand Down Expand Up @@ -140,6 +143,7 @@ impl VerifyBundle {
artifact.version.patch,
);

let contract_name = contract.name.clone();
let verify = VerifyArgs {
address: contract_address,
contract: Some(contract),
Expand Down Expand Up @@ -168,7 +172,7 @@ impl VerifyBundle {
creation_transaction_hash: None,
};

return Some(verify);
return Some((verify, contract_name));
}
}
None
Expand All @@ -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;
Expand All @@ -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),
};
}
Expand All @@ -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!"
Expand Down
Loading