diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index bab7ff391..d8ed89558 100644 --- a/cmd/soroban-cli/src/commands/mod.rs +++ b/cmd/soroban-cli/src/commands/mod.rs @@ -111,7 +111,7 @@ impl Root { Cmd::Events(events) => events.run().await?, Cmd::Xdr(xdr) => xdr.run()?, Cmd::Network(network) => network.run().await?, - Cmd::Snapshot(snapshot) => snapshot.run().await?, + Cmd::Snapshot(snapshot) => snapshot.run(&self.global_args).await?, Cmd::Version(version) => version.run(), Cmd::Keys(id) => id.run().await?, Cmd::Tx(tx) => tx.run(&self.global_args).await?, diff --git a/cmd/soroban-cli/src/commands/snapshot/create.rs b/cmd/soroban-cli/src/commands/snapshot/create.rs index ea17785ae..ee0f5f520 100644 --- a/cmd/soroban-cli/src/commands/snapshot/create.rs +++ b/cmd/soroban-cli/src/commands/snapshot/create.rs @@ -9,7 +9,7 @@ use sha2::{Digest, Sha256}; use soroban_ledger_snapshot::LedgerSnapshot; use std::{ collections::HashSet, - fs::{self}, + fs, io::{self}, path::PathBuf, str::FromStr, @@ -26,8 +26,9 @@ use stellar_xdr::curr::{ use tokio::fs::OpenOptions; use crate::{ - commands::{config::data, HEADING_RPC}, + commands::{config::data, global, HEADING_RPC}, config::{self, locator, network::passphrase}, + print, utils::{get_name_from_stellar_asset_contract_storage, parsing::parse_asset}, }; @@ -140,18 +141,20 @@ const CHECKPOINT_FREQUENCY: u32 = 64; impl Cmd { #[allow(clippy::too_many_lines)] - pub async fn run(&self) -> Result<(), Error> { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let print = print::Print::new(global_args.quiet); let start = Instant::now(); let archive_url = self.archive_url()?; - let history = get_history(&archive_url, self.ledger).await?; + let history = get_history(&print, &archive_url, self.ledger).await?; let ledger = history.current_ledger; let network_passphrase = &history.network_passphrase; let network_id = Sha256::digest(network_passphrase); - println!("ℹ️ Ledger: {ledger}"); - println!("ℹ️ Network Passphrase: {network_passphrase}"); - println!("ℹ️ Network ID: {}", hex::encode(network_id)); + + print.infoln(format!("Ledger: {ledger}")); + print.infoln(format!("Network Passphrase: {network_passphrase}")); + print.infoln(format!("Network id: {}", hex::encode(network_id))); // Prepare a flat list of buckets to read. They'll be ordered by their // level so that they can iterated higher level to lower level. @@ -164,7 +167,7 @@ impl Cmd { // Pre-cache the buckets. for (i, bucket) in buckets.iter().enumerate() { - cache_bucket(&archive_url, i, bucket).await?; + cache_bucket(&print, &archive_url, i, bucket).await?; } // The snapshot is what will be written to file at the end. Fields will @@ -215,29 +218,35 @@ impl Cmd { wasm_hashes: self.wasm_hashes.iter().cloned().collect(), }; let mut next = SearchInputs::default(); + loop { if current.is_empty() { break; } - println!( - "ℹ️ Searching for {} accounts, {} contracts, {} wasms", + + print.infoln(format!( + "Searching for {} accounts, {} contracts, {} wasms", current.account_ids.len(), current.contract_ids.len(), current.wasm_hashes.len(), - ); + )); + for (i, bucket) in buckets.iter().enumerate() { // Defined where the bucket will be read from, either from cache on // disk, or streamed from the archive. - let cache_path = cache_bucket(&archive_url, i, bucket).await?; + let cache_path = cache_bucket(&print, &archive_url, i, bucket).await?; let file = std::fs::OpenOptions::new() .read(true) .open(&cache_path) .map_err(Error::ReadOpeningCachedBucket)?; - print!("🔎 Searching bucket {i} {bucket}"); + + let message = format!("Searching bucket {i} {bucket}"); + print.search(format!("{message}…")); + if let Ok(metadata) = file.metadata() { - print!(" ({})", ByteSize(metadata.len())); + print.clear_line(); + print.searchln(format!("{message} ({})", ByteSize(metadata.len()))); } - println!(); // Stream the bucket entries from the bucket, identifying // entries that match the filters, and including only the @@ -288,10 +297,10 @@ impl Cmd { }) => { if !current.wasm_hashes.contains(hash) { next.wasm_hashes.insert(hash.clone()); - println!( - "ℹ️ Adding wasm {} to search", + print.infoln(format!( + "Adding wasm {} to search", hex::encode(hash) - ); + )); } } ScVal::ContractInstance(ScContractInstance { @@ -312,9 +321,9 @@ impl Cmd { Some(a12.issuer.clone()) } } { - println!( - "ℹ️ Adding asset issuer {issuer} to search" - ); + print.infoln(format!( + "Adding asset issuer {issuer} to search" + )); next.account_ids.insert(issuer); } } @@ -332,7 +341,7 @@ impl Cmd { count_saved += 1; } if count_saved > 0 { - println!("ℹ️ Found {count_saved} entries"); + print.infoln(format!("Found {count_saved} entries")); } } current = next; @@ -343,14 +352,14 @@ impl Cmd { snapshot .write_file(&self.out) .map_err(Error::WriteLedgerSnapshot)?; - println!( - "💾 Saved {} entries to {:?}", + print.saveln(format!( + "Saved {} entries to {:?}", snapshot.ledger_entries.len(), self.out - ); + )); let duration = Duration::from_secs(start.elapsed().as_secs()); - println!("✅ Completed in {}", format_duration(duration)); + print.checkln(format!("Completed in {}", format_duration(duration))); Ok(()) } @@ -380,7 +389,11 @@ impl Cmd { } } -async fn get_history(archive_url: &Uri, ledger: Option) -> Result { +async fn get_history( + print: &print::Print, + archive_url: &Uri, + ledger: Option, +) -> Result { let archive_url = archive_url.to_string(); let archive_url = archive_url.strip_suffix('/').unwrap_or(&archive_url); let history_url = if let Some(ledger) = ledger { @@ -394,34 +407,44 @@ async fn get_history(archive_url: &Uri, ledger: Option) -> Result(https) - .get(history_url) + .get(history_url.clone()) .await .map_err(Error::DownloadingHistory)?; + if !response.status().is_success() { // Check ledger is a checkpoint ledger and available in archives. if let Some(ledger) = ledger { let ledger_offset = (ledger + 1) % CHECKPOINT_FREQUENCY; + if ledger_offset != 0 { - println!( - "ℹ️ Ledger {ledger} may not be a checkpoint ledger, try {} or {}", + print.println(""); + print.errorln(format!( + "Ledger {ledger} may not be a checkpoint ledger, try {} or {}", ledger - ledger_offset, ledger + (CHECKPOINT_FREQUENCY - ledger_offset), - ); + )); } } return Err(Error::DownloadingHistoryGotStatusCode(response.status())); } + let body = hyper::body::to_bytes(response.into_body()) .await .map_err(Error::ReadHistoryHttpStream)?; + + print.clear_line(); + print.globeln(format!("Downloaded history {}", &history_url)); + serde_json::from_slice::(&body).map_err(Error::JsonDecodingHistory) } async fn cache_bucket( + print: &print::Print, archive_url: &Uri, bucket_index: usize, bucket: &str, @@ -434,7 +457,9 @@ async fn cache_bucket( let bucket_2 = &bucket[4..=5]; let bucket_url = format!("{archive_url}/bucket/{bucket_0}/{bucket_1}/{bucket_2}/bucket-{bucket}.xdr.gz"); - print!("🪣 Downloading bucket {bucket_index} {bucket}"); + + print.globe(format!("Downloading bucket {bucket_index} {bucket}…")); + let bucket_url = Uri::from_str(&bucket_url).map_err(Error::ParsingBucketUrl)?; let https = hyper_tls::HttpsConnector::new(); let response = hyper::Client::builder() @@ -442,18 +467,26 @@ async fn cache_bucket( .get(bucket_url) .await .map_err(Error::GettingBucket)?; + if !response.status().is_success() { - println!(); + print.println(""); return Err(Error::GettingBucketGotStatusCode(response.status())); } + if let Some(val) = response.headers().get("Content-Length") { if let Ok(str) = val.to_str() { if let Ok(len) = str.parse::() { - print!(" ({})", ByteSize(len)); + print.clear_line(); + print.globe(format!( + "Downloaded bucket {bucket_index} {bucket} ({})", + ByteSize(len) + )); } } } - println!(); + + print.println(""); + let read = response .into_body() .map(|result| result.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))) diff --git a/cmd/soroban-cli/src/commands/snapshot/mod.rs b/cmd/soroban-cli/src/commands/snapshot/mod.rs index 344b320d2..79d73009c 100644 --- a/cmd/soroban-cli/src/commands/snapshot/mod.rs +++ b/cmd/soroban-cli/src/commands/snapshot/mod.rs @@ -1,5 +1,7 @@ use clap::Parser; +use super::global; + pub mod create; /// Create and operate on ledger snapshots. @@ -15,9 +17,9 @@ pub enum Error { } impl Cmd { - pub async fn run(&self) -> Result<(), Error> { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { match self { - Cmd::Create(cmd) => cmd.run().await?, + Cmd::Create(cmd) => cmd.run(global_args).await?, }; Ok(()) } diff --git a/cmd/soroban-cli/src/print.rs b/cmd/soroban-cli/src/print.rs index 2a163af87..f04567663 100644 --- a/cmd/soroban-cli/src/print.rs +++ b/cmd/soroban-cli/src/print.rs @@ -16,6 +16,26 @@ impl Print { Print { quiet } } + pub fn print(&self, message: T) { + if !self.quiet { + print!("{message}"); + } + } + + pub fn println(&self, message: T) { + if !self.quiet { + println!("{message}"); + } + } + + pub fn clear_line(&self) { + if cfg!(windows) { + print!("\r"); + } else { + print!("\r\x1b[2K"); + } + } + /// # Errors /// /// Might return an error