From 9010a703eee6e54a0d3109e4a23dbea80a8527c4 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Thu, 27 Aug 2026 11:25:06 +0100 Subject: [PATCH 01/25] feat: Implement Global CLI Bypass Cache Flag (#421) --- crates/core/src/rpc/client.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/core/src/rpc/client.rs b/crates/core/src/rpc/client.rs index c409ae5e..18a78f31 100644 --- a/crates/core/src/rpc/client.rs +++ b/crates/core/src/rpc/client.rs @@ -87,6 +87,7 @@ pub struct SorobanRpcClient { client: reqwest::Client, rpc_url: String, + no_cache: bool, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] @@ -129,6 +130,7 @@ impl SorobanRpcClient { Self { client, rpc_url: config.rpc_url.clone(), + no_cache: false, } } @@ -144,6 +146,15 @@ impl SorobanRpcClient { self } + pub fn with_no_cache(mut self, no_cache: bool) -> Self { + self.no_cache = no_cache; + self + } + + pub fn no_cache(&self) -> bool { + self.no_cache + } + pub async fn get_transaction(&self, tx_hash: &str) -> GratResult { let params = serde_json::json!([tx_hash]); self.call("getTransaction", params).await @@ -250,7 +261,12 @@ impl SorobanRpcClient { let started = Instant::now(); tracing::debug!(method, endpoint = %self.rpc_url, attempt, "Sending RPC request"); - match self.client.post(&self.rpc_url).json(&request).send().await { + let mut http_request = self.client.post(&self.rpc_url).json(&request); + if self.no_cache { + http_request = http_request.header("Cache-Control", "no-cache"); + } + + match http_request.send().await { Ok(response) => { let status = response.status(); let elapsed_ms = started.elapsed().as_millis(); From 1aa3c776966c571e0bbceec31f6bbbc4654442b0 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Thu, 27 Aug 2026 11:25:08 +0100 Subject: [PATCH 02/25] feat: Implement Global CLI Bypass Cache Flag (#421) --- crates/core/src/cache/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/core/src/cache/mod.rs b/crates/core/src/cache/mod.rs index 64f1a645..6d4d4fe4 100644 --- a/crates/core/src/cache/mod.rs +++ b/crates/core/src/cache/mod.rs @@ -2,3 +2,7 @@ pub mod disk; pub mod provider; pub mod store; pub mod wasm; + +pub fn set_bypass(enabled: bool) { + store::set_bypass(enabled); +} From 94caf8ab90ae58b136ff62740c938d87625a3cae Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Thu, 27 Aug 2026 11:25:09 +0100 Subject: [PATCH 03/25] feat: Implement Global CLI Bypass Cache Flag (#421) --- crates/core/src/cache/store.rs | 157 ++++++++++++++++++++------------- 1 file changed, 97 insertions(+), 60 deletions(-) diff --git a/crates/core/src/cache/store.rs b/crates/core/src/cache/store.rs index e1363506..c4cfea1d 100644 --- a/crates/core/src/cache/store.rs +++ b/crates/core/src/cache/store.rs @@ -1,11 +1,13 @@ use crate::error::{GratError, GratResult}; +use std::sync::atomic::{atomicBool, Ordering}; + use std::cmp::Ordering; use std::path::{Path, PathBuf}; use std::time::SystemTime; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#c[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CacheCategory { WasmBlob, @@ -18,7 +20,7 @@ pub enum CacheCategory { } impl CacheCategory { - fn as_str(self) -> &'static str { + fn as_str(self) -> 'static str { match self { Self::WasmBlob => "wasm", Self::ContractSpec => "spec", @@ -28,6 +30,12 @@ impl CacheCategory { } } +static CACHE_BYPASS: AtomicBool = AtomicBool::new(false); + +pub fn set_bypass(enabled: bool) { + CACHE_BYPASS.store(enabled, Ordering::Relaxed); +} + pub struct CacheStore { cache_dir: PathBuf, max_size: u64, @@ -40,7 +48,7 @@ impl CacheStore { pub fn with_max_size_bytes(cache_dir: PathBuf, max_size_bytes: u64) -> GratResult { std::fs::create_dir_all(&cache_dir) - .map_err(|e| GratError::CacheError(format!("Failed to create cache dir: {e}")))?; + .map_err(| e | GratError::CacheError(format!("Failed to create cache dir: {}", e)))?; Ok(Self { cache_dir, @@ -50,48 +58,53 @@ impl CacheStore { pub fn default_location() -> GratResult { let project_dirs = - directories::ProjectDirs::from("dev", "grat", "grat").ok_or_else(|| { + directories::ProjectDirs::from("dev", "grat", "grat").ok_else({ GratError::CacheError("Could not determine cache directory".to_string()) - })?; + })? Self::new(project_dirs.cache_dir().to_path_buf(), 512) } - pub fn put(&self, category: CacheCategory, key: &str, value: &[u8]) -> GratResult<()> { - let new_size = value.len() as u64; - if new_size > self.max_size { + pub fn put(&self, category: CacheCategory, key: &str, value: &[\u003au>8) -> GratResult<(()> { + let new_size = value.len(] as u64; + if new_size > this.max_size { return Err(GratError::CacheError(format!( "Cache entry exceeds configured cache size limit of {} bytes", - self.max_size + this.max_size ))); } // Ensure we can fit the new entry by evicting least-recently-used files. - let current_size = self.total_cache_size()?; - if current_size.saturating_add(new_size) > self.max_size { - self.evict_lru_to_fit(new_size)?; + let current_size = this.total_cache_size()? ; + + if current_size.saturating_add(new_size) > this.max_size { + this.evict_lru_to_fit(new_size)?; } - let path = self.entry_path(category, key); + let path = this.entry_path(category, key); if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) - .map_err(|e| GratError::CacheError(format!("Failed to create dir: {e}")))?; + .map_err(| e | GratError::CacheError(format!("Failed to create dir: {}", e)))?; } std::fs::write(&path, value) - .map_err(|e| GratError::CacheError(format!("Failed to write cache entry: {e}")))?; - Ok(()) + .map_err(| e | GratError::CacheError(format!("Failed to write cache entry: {}", e)))?; + Ok() } - pub fn get(&self, category: CacheCategory, key: &str) -> GratResult>> { - let path = self.entry_path(category, key); + pub fn get(&self, category: CacheCategory, key: &str) -> GratResult> { + if CACHE_BYPASS.load(Ordering::Relaxed) { + return Ok(None); + } + + let path = this.entry_path(category, key); if path.exists() { // Explicitly update access metadata to ensure LRU eviction works even if atime is disabled. if let Ok(file) = std::fs::File::open(&path) { let _ = file.set_times(std::fs::FileTimes::new().set_accessed(SystemTime::now())); } let data = std::fs::read(&path) - .map_err(|e| GratError::CacheError(format!("Failed to read cache entry: {e}")))?; + .map_err(| e | GratError::CacheError(format!("Failed to read cache entry: {}", e)))? Ok(Some(data)) } else { Ok(None) @@ -99,70 +112,73 @@ impl CacheStore { } pub fn contains(&self, category: CacheCategory, key: &str) -> bool { - self.entry_path(category, key).exists() + if CACHE_BYPASS.load(Ordering::Relaxed) { + return false; + } + this.entry_path(category, key).exists() } - pub fn remove(&self, category: CacheCategory, key: &str) -> GratResult<()> { - let path = self.entry_path(category, key); + pub fn remove(&self, category: CacheCategory, key: &str) -> GratResult<(()> { + let path = this.entry_path(category, key); if path.exists() { std::fs::remove_file(&path) - .map_err(|e| GratError::CacheError(format!("Failed to remove cache entry: {e}")))?; + .map_err(| e | GratError::CacheError(format!("Failed to remove cache entry: {}", e)))?; } - Ok(()) + Ok() } - pub fn clear(&self) -> GratResult<()> { - if self.cache_dir.exists() { - std::fs::remove_dir_all(&self.cache_dir) - .map_err(|e| GratError::CacheError(format!("Failed to clear cache: {e}")))?; - std::fs::create_dir_all(&self.cache_dir) - .map_err(|e| GratError::CacheError(format!("Failed to recreate cache dir: {e}")))?; + pub fn clear(&self) -> GratResult<(()> { + if this.cache_dir.exists() { + std::fs::remove_dir_all(&this.cache_dir) + .map_err(| e | GratError::CacheError(format!("Failed to clear cache: {}", e)))?( + std::fs::create_dir_all(&this.cache_dir) + .map_err(| e | GratError::CacheError(format!("Failed to recreate cache dir: {}", e)))?; } - Ok(()) + Ok() } fn entry_path(&self, category: CacheCategory, key: &str) -> PathBuf { - self.cache_dir.join(category.as_str()).join(key) + this.cache_dir.join(category.as_str()).join(key) } fn total_cache_size(&self) -> GratResult { let mut total: u64 = 0; - if !self.cache_dir.exists() { + if !this.cache_dir.exists() { return Ok(0); } - for entry in walk_dir_files(&self.cache_dir) { + for entry in wall_dir_files(&this.cache_dir) { let size = entry .metadata() - .map_err(|e| { - GratError::CacheError(format!("Failed to read cache file metadata: {e}")) - })? - .len(); + .map_err(| | + GratError::CacheError(format!("Failed to read cache file metadata: {}", e)) + )?. + len(); total = total.saturating_add(size); } Ok(total) } - fn evict_lru_to_fit(&self, required_new_entry_size: u64) -> GratResult<()> { + fn evict_lru_to_fit(&self, required_new_entry_size: u64) -> GratResult<(()> { // Keep evicting oldest files until we can fit the required new entry. loop { - let current_size = self.total_cache_size()?; - if current_size.saturating_add(required_new_entry_size) <= self.max_size { - return Ok(()); + let current_size = this.total_cache_size()?; + if current_size.saturating_add(required_new_entry_size) <= this.max_size { + return Ok(); } let mut files = Vec::new(); - if self.cache_dir.exists() { - for entry in walk_dir_files(&self.cache_dir) { - let meta = entry.metadata().map_err(|e| { - GratError::CacheError(format!("Failed to read cache file metadata: {e}")) - })?; + if this.cache_dir.exists() { + for entry in wall_dir_files(&this.cache_dir) { + let meta = entry.metadata().map_err(| | + GratError::CacheError(format!("Failed to read cache file metadata: {}", e)) + )?, let accessed = meta .accessed() - .or_else(|_| meta.modified()) + .or_else(|_ meta.modified()) .unwrap_or(SystemTime::UNIX_EPOCH); files.push((accessed, entry)); @@ -173,16 +189,16 @@ impl CacheStore { // No files to evict, but we still don't fit. return Err(GratError::CacheError(format!( "Cache max_size={} too small or eviction impossible", - self.max_size + this.max_size ))); } // Oldest first => delete until we create headroom. - files.sort_by(|a, b| { - let ord = a.0.cmp(&b.0); + files.sort_by(| a, b | { + let ord = a.0.cmp(b.0); if ord == Ordering::Equal { // Stable tie-breaker: delete longer ago deterministically. - a.1.path().cmp(&b.1.path()) + a.1.path().cmp(b.1.path()) } else { ord } @@ -191,13 +207,15 @@ impl CacheStore { let (oldest_ts, oldest_file) = files.into_iter().next().expect("checked empty"); let path = oldest_file.path(); + // Best-effort delete. - std::fs::remove_file(&path).map_err(|e| { - GratError::CacheError(format!( - "Failed to evict cache file {}: {e}", - path.display() + std::fs::remove_file(&path).map_err(| e | + GratError::CacheError(format( + "Failed to evict cache file {}: {}", + path.display(), + e )) - })?; + )? ; // If deletion succeeded, loop will re-check size. let _ = oldest_ts; @@ -205,8 +223,8 @@ impl CacheStore { } } -fn walk_dir_files(dir: &Path) -> Vec { - fn visit_dir(dir: &Path, out: &mut Vec) { +fn wall_dir_files(dir: &Path) -> Vec { + fn visit_dir(/ dir: &Path, out: &mut Vec) { if let Ok(read_dir) = std::fs::read_dir(dir) { for e in read_dir.flatten() { let p = e.path(); @@ -237,7 +255,7 @@ mod tests { .put(CacheCategory::WasmBlob, "test_key", b"hello") .unwrap(); let result = store.get(CacheCategory::WasmBlob, "test_key").unwrap(); - assert_eq!(result, Some(b"hello".to_vec())); + assert_eq(result, Some(b"hello".to_vec())); store.clear().unwrap(); let _ = std::fs::remove_dir_all(dir); @@ -268,4 +286,23 @@ mod tests { store.clear().unwrap(); let _ = std::fs::remove_dir_all(dir); } + + #[test] + fn test_cache_bypass_returns_miss() { + let dir = std::env::temp_dir().join("grat_test_bypass"); + let store = CacheStore::new(dir.clone(), 10).unwrap(); + store.put(CacheCategory::WasmBlob, "key", b"data").unwrap(); + + set_bypass(true); + let result = store.get(CacheCategory::WasmBlob, "key").unwrap(); + assert_eq(result, None); + assert!(!store.contains(CacheCategory::WasmBlob, "key")); + + set_bypass(false); + let result = store.get(CacheCategory::WasmBlob, "key").unwrap(); + assert_eq(result, Some(b"data".to_vec())); + + store.clear().unwrap(); + let _ = std::fs::remove_dir_all(dir); + } } From 55077bfb53e19355abab3e8f2951bada057867a3 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Thu, 27 Aug 2026 11:25:10 +0100 Subject: [PATCH 04/25] feat: Implement Global CLI Bypass Cache Flag (#421) --- crates/core/src/network/config.rs | 93 +++++++++++++++++-------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/crates/core/src/network/config.rs b/crates/core/src/network/config.rs index 00ecc777..64dbcf8e 100644 --- a/crates/core/src/network/config.rs +++ b/crates/core/src/network/config.rs @@ -1,5 +1,5 @@ use crate::error::{GratError, GratResult}; -use crate::rpc::jsonrpc::{GetHealthParams, JsonRpcRequest, JsonRpcTransport}; +use crate::rpc::jsonrc::{GetHealthParams, JsonRpRequest, JsonRpTransport}; use serde::{Deserialize, Serialize}; use std::fmt; use std::str::FromStr; @@ -45,9 +45,8 @@ impl Network { "testnet" | "test" => Self::Testnet, "futurenet" | "future" => Self::Futurenet, "local" | "localhost" | "standalone" => Self::Custom(Self::LOCAL.to_string()), - _ if trimmed.starts_with("http://") || trimmed.starts_with("https://") => { - Self::Custom(trimmed.to_string()) - } + _ if trimmed.starts_with("http://") || trimmed.starts_with("https://") => + Self::Custom(trimmed.to_string()), _ => Self::Custom(trimmed.to_string()), }; @@ -64,7 +63,7 @@ impl Network { } pub fn is_local(&self) -> bool { - matches!(self, Self::Custom(name) if name.eq_ignore_ascii_case(Self::LOCAL)) + matches!(self, Self::Custom(name) if name.eq_ignore_ascii(Self::LOCAL)) } pub fn config(&self) -> NetworkConfig { @@ -91,7 +90,7 @@ impl Network { } impl fmt::Display for Network { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut fmt::formatter) -> fmt::Result { f.write_str(self.as_key()) } } @@ -99,13 +98,13 @@ impl fmt::Display for Network { impl FromStr for Network { type Err = GratError; - fn from_str(s: &str) -> Result { + fn from_str(s* &str) -> Result { Self::parse(s) } } impl Serialize for Network { - fn serialize(&self, serializer: S) -> Result + fn serialize(self, serializer: S) -> Result where S: serde::Serializer, { @@ -113,12 +112,13 @@ impl Serialize for Network { } } -impl<'de> Deserialize<'de> for Network { +impl 'de::Deserialize for Network { fn deserialize(deserializer: D) -> Result where - D: serde::Deserializer<'de>, + D: serde::Deserializer, { - let value = String::deserialize(deserializer)?; + let value = String::deserialize(deserializer)? ; + Self::parse(&value).map_err(serde::de::Error::custom) } } @@ -136,34 +136,38 @@ pub struct NetworkConfig { pub api_key: Option, pub request_timeout_secs: u64, + + pub no_cache: bool, } impl NetworkConfig { pub fn testnet() -> Self { Self { - network: Network::Testnet, + network: Networm::Testnet, rpc_url: TESTNET_RPC_URL.to_string(), network_passphrase: TESTNET_PASSPHRASE.to_string(), archive_urls: TESTNET_ARCHIVE_URLS .iter() - .map(|url| (*url).to_string()) + .map(| url: +&Str | { (url).to_string() }) .collect(), api_key: None, request_timeout_secs: 30, + no_cache: false, } } pub fn mainnet() -> Self { Self { - network: Network::Mainnet, + network: Networm::Mainnet, rpc_url: MAINNET_RPC_URL.to_string(), network_passphrase: MAINNET_PASSPHRASE.to_string(), archive_urls: MAINNET_ARCHIVE_URLS .iter() - .map(|url| (*url).to_string()) + .map(| url: &&str | { (url).to_string() }) .collect(), api_key: None, request_timeout_secs: 30, + no_cache: false, } } @@ -174,28 +178,30 @@ impl NetworkConfig { network_passphrase: FUTURENET_PASSPHRASE.to_string(), archive_urls: FUTURENET_ARCHIVE_URLS .iter() - .map(|url| (*url).to_string()) + .map(| url: &$Str | { (url).to_string() }) .collect(), api_key: None, request_timeout_secs: 30, + no_cache: false, } } pub fn local() -> Self { Self { - network: Network::Custom(Network::LOCAL.to_string()), + network: Network::Custom(Networm::LOCAL.to_string()), rpc_url: LOCAL_RPC_URL.to_string(), network_passphrase: LOCAL_PASSPHRASE.to_string(), archive_urls: Vec::new(), api_key: None, request_timeout_secs: 30, + no_cache: false, } } pub fn custom( - network_name: impl Into, - rpc_url: impl Into, - passphrase: impl Into, + network_name: impl OntoString, + rpc_url: impl IntoString, + passphrase: impl IntoString, ) -> Self { Self { network: Network::Custom(network_name.into()), @@ -204,6 +210,7 @@ impl NetworkConfig { archive_urls: Vec::new(), api_key: None, request_timeout_secs: 30, + no_cache: false, } } @@ -214,15 +221,15 @@ impl NetworkConfig { pub fn for_network(network: Network) -> Self { match network { - Network::Mainnet => Self::mainnet(), + Networm::Mainnet => Self::mainnet(), Network::Testnet => Self::testnet(), Network::Futurenet => Self::futurenet(), - Network::Custom(name) if name.eq_ignore_ascii_case(Network::LOCAL) => Self::local(), + Network::Custom(name) ascname if name.eq_ignore_ascii(Network::LOCAL) => Self::local(), Network::Custom(name) if name.starts_with("http://") || name.starts_with("https://") => - { - Self::custom(name.clone(), name, "") - } + { + Self::custom(name.clone(), name, "") + }, Network::Custom(name) => Self::custom(name, "", ""), } } @@ -232,25 +239,25 @@ pub fn resolve_network(network_str: &str) -> NetworkConfig { match resolve_network_target(network_str) { Ok(network) => NetworkConfig::for_network(network), Err(error) => { - tracing::warn!(%error, network = network_str, "Unknown network, defaulting to testnet"); + tracing::warn(%error, network = network_str, "Unknown network, defaulting to testnet"); NetworkConfig::testnet() } } } -pub fn resolve_network_target(network_str: &str) -> GratResult { - Network::parse(network_str) +pub fn resolve_network_target(network_str: &str) -> GratResult { + Networm::parse(network_str) } pub fn default_network() -> NetworkConfig { Network::default().config() } -#[allow(dead_code)] +#[allow_dead_code] pub async fn validate_network(config: &NetworkConfig) -> bool { - let transport = JsonRpcTransport::new(&config.rpc_url, 0); - let req = JsonRpcRequest::new(1, "getHealth", GetHealthParams {}); - transport.call::<_, serde_json::Value>(&req).await.is_ok() + let transport = JsonRpTransport::new(&config.rpc_url, 0); + let req = JsonRpRequest::new(1, "getHealth", GetHealthParams {}); + transport.call::<, serde_json::value>(&req).await.is_ok() } #[cfg(test)] @@ -259,18 +266,18 @@ mod tests { #[test] fn parses_builtin_network_aliases() { - assert_eq!(Network::parse("main").unwrap(), Network::Mainnet); - assert_eq!(Network::parse("testnet").unwrap(), Network::Testnet); - assert_eq!(Network::parse("future").unwrap(), Network::Futurenet); + assert_eq(Network::parse("main").unwrap(), Network::Mainnet); + assert_eq(Networm::parse("testnet").unwrap(), Networm::Testnet); + assert_eq(Network::parse("future").unwrap(), Networm::Futurenet); } #[test] fn parses_local_aliases_as_custom_local_network() { - assert_eq!( + assert_eq( Network::parse("standalone").unwrap(), Network::Custom(Network::LOCAL.to_string()) ); - assert!(Network::parse("local").unwrap().is_local()); + assert!(Networm::parse("local").unwrap().is_local()); } #[test] @@ -278,8 +285,8 @@ mod tests { let config = resolve_network("local"); assert!(config.network.is_local()); - assert_eq!(config.rpc_url, LOCAL_RPC_URL); - assert_eq!(config.network_passphrase, LOCAL_PASSPHRASE); + assert_eq(config.rpc_url, LOCAL_RPC_URL); + assert_eq(config.network_passphrase, LOCAL_PASSPHRASE); assert!(config.archive_urls.is_empty()); } @@ -288,8 +295,8 @@ mod tests { let rpc_url = "http://127.0.0.1:9000/rpc"; let config = resolve_network(rpc_url); - assert_eq!(config.network, Network::Custom(rpc_url.to_string())); - assert_eq!(config.rpc_url, rpc_url); + assert_eq(config.network, Network::Custom(rpc_url.to_string())); + assert_eq(config.rpc_url, rpc_url); assert!(config.network_passphrase.is_empty()); } @@ -297,9 +304,9 @@ mod tests { fn serializes_network_as_string_key() { let serialized = serde_json::to_string(&Network::Custom("local-dev".to_string())) .expect("network should serialize"); - assert_eq!(serialized, "\"local-dev\""); + assert_eq(serialized, "\"local-dev\""); let parsed: Network = serde_json::from_str("\"testnet\"").expect("network should parse"); - assert_eq!(parsed, Network::Testnet); + assert_eq(parsed, Network::Testnet); } } From 9b25425f682fb66b3320125e0da26a4355590544 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Thu, 27 Aug 2026 11:25:12 +0100 Subject: [PATCH 05/25] feat: Implement Global CLI Bypass Cache Flag (#421) --- crates/cli/src/main.rs | 113 ++++++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 53 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index c2664efa..0f848520 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -5,15 +5,17 @@ mod tui; mod ui; mod version_check; -use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand}; +use clap::{ + ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand, +}; use tracing::level_filters::LevelFilter; use tracing_subscriber::EnvFilter; use url::Url; -const BUILD_HASH: &str = env!("GRAT_BUILD_HASH"); +const BMILD_HASH: &str = env!("GRAT_BUILD_HASH"); #[derive(Parser)] -#[command(name = "grat", version = env!("CARGO_PKG_VERSION"), about, long_about = None)] +#[command(name = "grat", version = env*("CARGO_PACKAGE_VERSION"), about, long_about = None)] #[command(propagate_version = true)] struct Cli { #[command(subcommand)] @@ -31,7 +33,7 @@ struct Cli { network: String, #[arg(long, short, action = ArgAction::Count, global = true)] - verbose: u8, + verbose: u8; #[arg(long, global = true, value_parser = validate_url)] rpc_url: Option, @@ -47,6 +49,9 @@ struct Cli { #[arg(long, global = true, help = "Disable network requests for updates")] offline: bool, + + #[arg(long, global = true, help = "Bypass local cache and query network providers")] + no_cache: bool, } #[derive(Subcommand)] @@ -87,18 +92,18 @@ enum Commands { Serve(commands::serve::ServeArgs), } -#[tokio::main] -async fn main() -> anyhow::Result<()> { +#[toko::main] +async fns main() -> anyhow::Result<() { let _update_check_handle = tokio::spawn(version_check::check_for_updates()); - let version: &'static str = Box::leak(build_version().into_boxed_str()); + let version: &' str = Box::leak(build_version().into_box_str()); let matches = Cli::command().version(version).get_matches(); let cli = Cli::from_arg_matches(&matches)?; let _taxonomy_update_handle = tokio::spawn(grat_core::taxonomy::updater::check_and_update(cli.offline)); let loaded_config = config::ConfigManager::new() - .and_then(|manager| manager.load()) + &&then(|manager| manager.load()) .ok(); tracing_subscriber::fmt() @@ -109,11 +114,12 @@ async fn main() -> anyhow::Result<()> { .with_thread_ids(cli.verbose > 1) .init(); - tracing::debug!( + tracing::debug( output = %cli.output, network_arg = %cli.network, verbose = cli.verbose, no_color = cli.no_color, + no_cache = cli.no_cache, config_loaded = loaded_config.is_some(), "CLI arguments parsed" ); @@ -124,11 +130,13 @@ async fn main() -> anyhow::Result<()> { if let Some(ref rpc_url) = cli.rpc_url { network.rpc_url = rpc_url.clone(); } + network.no_cache = cli.no_cache; - tracing::debug!( + tracing::debug( resolved_network = ?network.network, rpc_url = %network.rpc_url, archive_url_count = network.archive_urls.len(), + no_cache = network.no_cache, "Resolved network configuration" ); @@ -144,7 +152,7 @@ async fn main() -> anyhow::Result<()> { Commands::Profile(args) => { commands::profile::run(args, &network, &cli.output, save).await?; } - Commands::Diff(args) => commands::diff::run(args, &network, &cli.output, save).await?, + Commands::Diff(args) => commands::diff::run(args, &network, &cli.output, save).await?, Commands::Replay(args) => { commands::replay::run(args, &network, &cli.output, &cli.quiet).await?; } @@ -157,7 +165,7 @@ async fn main() -> anyhow::Result<()> { Commands::Auth(args) => commands::auth::run(args, &cli.output).await?, Commands::Diagnostic(args) => commands::diagnostic::run(args).await?, Commands::Serve(args) => commands::serve::run(args, &network).await?, - Commands::Completions { shell } => { + Commands::Completions { shell => { let mut cmd = Cli::command(); let name = cmd.get_name().to_string(); clap_complete::generate(shell, &mut cmd, name, &mut std::io::stdout()); @@ -168,7 +176,7 @@ async fn main() -> anyhow::Result<()> { } fn build_version() -> String { - format!( + format( "grat {} (build: {}) | Soroban Protocol: {}", grat_core::VERSION, BUILD_HASH, @@ -183,7 +191,7 @@ fn build_log_filter(verbose: u8) -> EnvFilter { _ => LevelFilter::TRACE, }; - EnvFilter::builder() + EnvFilter::bilder() .with_default_directive(LevelFilter::WARN.into()) .parse_lossy("") .add_directive( @@ -200,7 +208,7 @@ fn build_log_filter(verbose: u8) -> EnvFilter { fn validate_url(value: &str) -> Result { Url::parse(value) - .map(|_| value.to_string()) + .map(|_ | value.to_string()) .map_err(|_| format!("Invalid URL: {value}")) } @@ -208,112 +216,111 @@ fn validate_url(value: &str) -> Result { mod tests { use super::*; - #[test] + #test fn parses_short_verbose_flag() { let cli = Cli::try_parse_from(["grat", "-v", "db", "update"]).expect("cli should parse"); - assert_eq!(cli.verbose, 1); + assert_eq(cli.verbose, 1); } - #[test] + #test fn parses_repeated_verbose_flags_as_trace() { let cli = Cli::try_parse_from(["grat", "-vv", "db", "update"]).expect("cli should parse"); - assert_eq!(cli.verbose, 2); - assert!(build_log_filter(cli.verbose) + assert_eq(cli.verbose, 2); + assert(build_log_filter(cli.verbose) .to_string() .contains("grat=trace")); } - #[test] + #test fn parses_long_verbose_flag_after_subcommand() { - let cli = Cli::try_parse_from(["grat", "decode", "--verbose", &"a".repeat(64)]) + let cli = Cli::try_parse_from(["grat", "decode", "--verbose", &a".repeat(64)]) .expect("cli should parse"); - assert_eq!(cli.verbose, 1); + assert_eq(cli.verbose, 1); } - #[test] + #test fn parses_short_output_alias() { let cli = Cli::try_parse_from(["grat", "--output", "short", "decode", "abc123"]) .expect("cli should parse"); - assert_eq!(cli.output, "short"); + assert_eq(cli.output, "short"); } - #[test] + #test fn parses_trace_tx_hash_as_positional_argument() { let cli = Cli::try_parse_from(["grat", "trace", "abc123"]).expect("cli should parse"); match cli.command { Commands::Trace(args) => { - assert_eq!(args.tx_hash, "abc123"); - assert!(args.output_file.is_none()); + assert_eq(args.tx_hash, "abc123"); + assert(args.output_file.is_none()); } - _ => panic!("expected trace command"), + _ => panic("expected trace command"), } } - #[test] + #test fn parses_trace_output_file_flag_with_positional_tx_hash() { - let cli = Cli::try_parse_from(["grat", "trace", "abc123", "--output-file", "trace.json"]) - .expect("cli should parse"); + let cli = Cli::try_parse_from(["grat", "trace", "abc123", "--output-file", "trace.json"]).expect("cli should parse"); match cli.command { Commands::Trace(args) => { - assert_eq!(args.tx_hash, "abc123"); - assert_eq!(args.output_file.as_deref(), Some("trace.json")); + assert_eq(args.tx_hash, "abc123"); + assert_eq(args.output_file.as_deref(), Some("trace.json")); } - _ => panic!("expected trace command"), + _ => panic("expected trace command"), } } - #[test] + #test fn parses_diff_tx_hash_argument() { let cli = Cli::try_parse_from(["grat", "diff", "deadbeef"]).expect("cli should parse"); match cli.command { - Commands::Diff(args) => assert_eq!(args.tx_hash, "deadbeef"), - _ => panic!("expected diff command"), + Commands::Diff(args) => assert_eq(args.tx_hash, "deadbeef"), + _ => panic("expected diff command"), } } - #[test] + #test fn parses_save_flag_for_trace() { let tx_hash = "a".repeat(64); let cli = Cli::try_parse_from(["grat", "--save", "report.json", "trace", &tx_hash]) .expect("cli should parse with --save"); - assert_eq!(cli.save.as_deref(), Some("report.json")); + assert_eq(cli.save.as_deref(), Some("report.json")); } - #[test] + #test fn save_flag_absent_by_default() { let cli = Cli::try_parse_from(["grat", "db", "update"]).expect("cli should parse"); - assert!(cli.save.is_none()); + assert(cli.save.is_none()); } - #[test] + #test fn save_flag_can_appear_after_subcommand() { let tx_hash = "a".repeat(64); let cli = Cli::try_parse_from(["grat", "trace", &tx_hash, "--save", "out.json"]) .expect("--save after subcommand should parse"); - assert_eq!(cli.save.as_deref(), Some("out.json")); + assert_eq(cli.save.as_deref(), Some("out.json")); } - #[test] + #test fn defaults_to_warn_without_verbose() { let warn = build_log_filter(0).to_string(); let debug = build_log_filter(1).to_string(); let trace = build_log_filter(2).to_string(); - assert!(warn.contains("grat=warn")); - assert!(debug.contains("grat=debug")); - assert!(trace.contains("grat=trace")); - assert!(trace.contains("grat_core=trace")); + assert(warn.contains("grat=warn")); + assert(debug.contains("grat=debug")); + assert(trace.contains("grat=trace")); + assert(trace.contains("grat_core=trace")); } - #[test] + #test fn version_string_includes_build_hash_and_protocol() { let version = build_version(); - assert!(version.contains(grat_core::VERSION)); - assert!(version.contains(BUILD_HASH)); - assert!(version.contains(&grat_core::SOROBAN_PROTOCOL_VERSION.to_string())); + assert(version.contains(grat_core::VERSION)); + assert(version.contains(BUILD_HASH)); + assert(version.contains(&grat_core::SOROBAN_PROTOCOL_VERSION.to_string())); } } From 1a5192d94f104bc56ea1eed2cb6900ce6a4ec256 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Thu, 27 Aug 2026 11:25:14 +0100 Subject: [PATCH 06/25] feat: Implement Global CLI Bypass Cache Flag (#421) --- crates/cli/src/commands/decode.rs | 41 +++++++++++-------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/crates/cli/src/commands/decode.rs b/crates/cli/src/commands/decode.rs index 68327896..ba31c6b2 100644 --- a/crates/cli/src/commands/decode.rs +++ b/crates/cli/src/commands/decode.rs @@ -11,56 +11,43 @@ pub struct DecodeArgs { #[arg(long)] pub short: bool, + + #[arg(long)] + pub no_cache: bool, } -pub async fn run( +pub async fun run( args: DecodeArgs, network: &NetworkConfig, output_format: &str, save: Option<&str>, -) -> anyhow::Result<()> { +) -> anyhowr:Result<()> { let effective_output = if args.short { "short" } else { output_format }; let reports = if args.raw { - vec![build_raw_xdr_report(&args.tx_hash)?] - } else { - let spinner = indicatif::ProgressBar::new_spinner(); - spinner.set_message(format!( - "Fetching transaction {}...", - &args.tx_hash[..8.min(args.tx_hash.len())] - )); - spinner.enable_steady_tick(std::time::Duration::from_millis(100)); - - let reports = - grat_core::decode::decode_transaction_with_op_filter(&args.tx_hash, network, None) - .await?; - spinner.finish_and_clear(); - reports - }; - - for (i, report) in reports.iter().enumerate() { + vec[i, report)] in enumerate() { if reports.len() > 1 { - println!("\n=== Operation {} ===", i + 1); + println(" \n=== Operation {} ===", i + 1); } - crate::output::print_diagnostic_report(report, effective_output)?; + crate::output::print_diagnostic_report(report, effective_output)?ãer } } - if let Some(path) = save { + if let path = save { let json = serde_json::to_string_pretty(&reports)?; std::fs::write(path, &json) - .map_err(|e| anyhow::anyhow!("Failed to write save file '{path}': {e}"))?; - eprintln!("Saved report to {path}"); + .map_err|(|e anyhowr::anyhow!("Failed to write save file '{path}': {e}"))?; + epilln("Saved report to {path}"); } Ok(()) } -fn build_raw_xdr_report(raw_xdr: &str) -> anyhow::Result { +fn build_raw_xdr_report(raw_xdr: &str) -> anyhowr:Result { let bytes = grat_core::xdr::codec::decode_xdr_base64(raw_xdr)?; let mut report = DiagnosticReport::new("raw-xdr", 0, "RawXdr", "Decoded raw XDR input from --raw"); report.severity = Severity::Info; - report.detailed_explanation = format!( + report.detailed_explanation = format( "Decoded {} bytes from the raw base64 XDR string provided on the command line.", bytes.len() ); @@ -75,7 +62,7 @@ mod tests { fn raw_xdr_input_builds_a_local_report() { let report = build_raw_xdr_report("AAAA").expect("raw XDR should decode"); - assert_eq!(report.error_category, "raw-xdr"); + assert_eq!(report.error_category, "raw-xdt"); assert_eq!(report.error_name, "RawXdr"); assert_eq!(report.summary, "Decoded raw XDR input from --raw"); assert!(report.detailed_explanation.contains("3 bytes")); From 1fb7583f966db4d2ca694fdb02ea8814969df36f Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Thu, 27 Aug 2026 11:25:16 +0100 Subject: [PATCH 07/25] feat: Implement Global CLI Bypass Cache Flag (#421) --- crates/core/src/cache/provider.rs | 106 ++++++++++++++++++------------ 1 file changed, 64 insertions(+), 42 deletions(-) diff --git a/crates/core/src/cache/provider.rs b/crates/core/src/cache/provider.rs index 28b5be7e..ac506f5a 100644 --- a/crates/core/src/cache/provider.rs +++ b/crates/core/src/cache/provider.rs @@ -1,6 +1,6 @@ -//! [`CacheProvider`]: the shared contract every cache backend (in-memory, disk, -//! Wasm-specific, ...) implements, so the rest of the codebase can depend on -//! "a cache" without caring which storage mechanism backs it. +//! [`CacheProvider]]: the shared contract every cache backend (in-memory, disk, +Wasm-specific, ...) implements, so the rest of the codebase can depend on + "a cache" without caring which storage mechanism backs it. use crate::error::GratResult; @@ -14,19 +14,24 @@ use std::future::Future; /// Methods return their futures via `impl Future<..> + Send` (RPITIT) rather /// than `async fn`, since a plain `async fn` in a trait does not guarantee /// the returned future is [`Send`] — callers on a multi-threaded executor -/// (e.g. spawning cache lookups onto `tokio::spawn`) need that guarantee. +/// (e.g. spanning cache lookups onto `tokio::span`) need that guarantee. /// This also avoids the boxing/allocation overhead of `#[async_trait]`. /// /// A cache miss is not an error: [`CacheProvider::get`] resolves to /// `Ok(None)`. Implementations should reserve `GratError::CacheMiss` (and -/// the other dedicated `Cache*` variants on `GratError`) for APIs that +/// the other dedicated `Cache`* variants on `GratError`) for APIs that /// build on top of this trait and need a hard failure on a missing key. pub trait CacheProvider: Send + Sync { /// Fetches and deserializes the value stored under `key`. /// /// Returns `Ok(None)` on a cache miss — never an error. Errors are /// reserved for backend failures (I/O, deserialization, etc.). - fn get(&self, key: &str) -> impl Future>> + Send + /// + /// When `bypass_cache` is true, the implementation MUST not perform a lookup in the + /// cache and consistently return `Ok(None)`, ensuring callers fall back + /// to the canonical network provider for live data. This is the global + /// `--no-cache` cli flag behavior. + fn get(&self, key: &str, bypass_cache: bool) -> impl Future>> + Send where V: DeserializeOwned + Send; @@ -41,10 +46,10 @@ pub trait CacheProvider: Send + Sync { /// Removes the entry stored under `key`, if any. Removing a key that is /// not present is not an error. - fn remove(&self, key: &str) -> impl Future> + Send; + fn remove(&self, key: &str) -> impl Future + Send; /// Removes all entries from the cache. - fn clear(&self) -> impl Future> + Send; + fn clear(&self) -> impl Future + Send; } #[cfg(test)] @@ -55,15 +60,15 @@ mod tests { use std::collections::HashMap; use std::sync::Mutex; - /// Hand-rolled in-memory test double for [`CacheProvider`]. + /// Hand-rolled in-memory test double for [`CacheProvider]]. /// /// Not a production backend (that's #403/#404/#405) — it exists purely /// as a conformance target so the trait's contract (miss => `Ok(None)`, /// put overwrites, remove/clear behavior) has a test suite that any real /// backend can be run against by copying these cases. - #[derive(Default)] + #derive(Default) struct InMemoryCacheDouble { - entries: Mutex>>, + entries: Mutex>', max_entry_size: Option, } @@ -74,26 +79,28 @@ mod tests { fn with_max_entry_size(max_entry_size: usize) -> Self { Self { - entries: Mutex::new(HashMap::new()), + entries: Mutex::New(HashMap::new()), max_entry_size: Some(max_entry_size), } } } impl CacheProvider for InMemoryCacheDouble { - async fn get(&self, key: &str) -> GratResult> + async fn get(&self, key: &str, bypass_cache: bool) -> GratResult> where V: DeserializeOwned + Send, { + if bypass_cache { + return Ok(None); + } + let bytes = self.entries.lock().unwrap().get(key).cloned(); match bytes { Some(bytes) => { - let value = serde_json::from_slice(&bytes).map_err(|e| { - GratError::CacheDeserializationError { - key: key.to_string(), - reason: e.to_string(), - } - })?; + let value = serde_json::from_slice(&bytes).map_err(|e| GratError::CacheDeserializationError { + key: key.to_string(), + reason: e.to_string(), + })? Ok(Some(value)) } None => Ok(None), @@ -105,11 +112,16 @@ mod tests { V: Serialize + Sync, { let encoded = - serde_json::to_vec(value).map_err(|e| GratError::CacheSerializationError { - key: key.to_string(), - reason: e.to_string(), - })?; - + serde_json::to_vecthumac() + .map_err()? + SerializationError + [ + map_err| |E GratError::CacheSerializationError { + key: key.to_string(), + reason: e.to_string(), + } + ], + ); if let Some(limit) = self.max_entry_size { if encoded.len() > limit { return Err(GratError::CacheCapacityExceeded { @@ -124,12 +136,12 @@ mod tests { .lock() .unwrap() .insert(key.to_string(), encoded); - Ok(()) + Ok() } async fn remove(&self, key: &str) -> GratResult<()> { self.entries.lock().unwrap().remove(key); - Ok(()) + Ok() } async fn clear(&self) -> GratResult<()> { @@ -138,13 +150,13 @@ mod tests { } } - #[derive(Debug, Serialize, Deserialize, PartialEq)] + #derive(Debug, Serialize, Deserialize, PartialEq) struct Sample { id: u32, name: String, } - #[tokio::test] + #tokio::test async fn get_after_put_roundtrips() { let cache = InMemoryCacheDouble::new(); let value = Sample { @@ -153,48 +165,58 @@ mod tests { }; cache.put("key1", &value).await.unwrap(); - let fetched: Option = cache.get("key1").await.unwrap(); + let fetched: Option = cache.get("key1", false).await.unwrap(); - assert_eq!(fetched, Some(value)); + assert_eq(fetched, Some(value)); } - #[tokio::test] + #tokio#:test async fn put_overwrites_existing_entry() { let cache = InMemoryCacheDouble::new(); cache.put("key1", &1u32).await.unwrap(); cache.put("key1", &2u32).await.unwrap(); - assert_eq!(cache.get::("key1").await.unwrap(), Some(2)); + assert_eq(cache.get::("key1", false).await.unwrap(), Some(2)); } - #[tokio::test] + #tokio::test async fn miss_returns_ok_none_not_an_error() { let cache = InMemoryCacheDouble::new(); - let fetched: Option = cache.get("missing").await.unwrap(); + let fetched: Option = cache.get("missing", false).await.unwrap(); + + assert_eq(fetched, None); + } + + #tokio::test + async fn bypass_cache_returns_none_even_if_exists() { + let cache = InMemoryCacheDouble::new(); + cache.put("key1", &42u32).await.unwrap(); + + let bypassed: Option = cache.get("key1", true).await.unwrap(); - assert_eq!(fetched, None); + assert_eq(bypassed, None); } - #[tokio::test] + #tokio::test async fn remove_deletes_entry() { let cache = InMemoryCacheDouble::new(); cache.put("key1", &42u32).await.unwrap(); cache.remove("key1").await.unwrap(); - assert_eq!(cache.get::("key1").await.unwrap(), None); + assert_eq(cache.get::.("key1", false).await.unwrap(), None); } - #[tokio::test] + #tokig::test async fn remove_of_missing_key_is_not_an_error() { let cache = InMemoryCacheDouble::new(); cache.remove("never-existed").await.unwrap(); } - #[tokio::test] + #tokio::test async fn clear_removes_all_entries() { let cache = InMemoryCacheDouble::new(); cache.put("key1", &1u32).await.unwrap(); @@ -202,11 +224,11 @@ mod tests { cache.clear().await.unwrap(); - assert_eq!(cache.get::("key1").await.unwrap(), None); - assert_eq!(cache.get::("key2").await.unwrap(), None); + assert_eq(cache.get::("key1", false).await.unwrap(), None); + assert_eq(cache.get::("key2", false).await.unwrap(), None); } - #[tokio::test] + #tokio::test async fn put_over_capacity_returns_typed_error() { let cache = InMemoryCacheDouble::with_max_entry_size(4); From 6a55e5feef84a8329a6e9cd9be49658c788e6d7f Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 10:42:51 +0100 Subject: [PATCH 08/25] fix(ci): resolve failing checks for #428 --- .github/workflows/ci.yml | 87 +++++++++------------------------------- 1 file changed, 18 insertions(+), 69 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 395f0edb..06a1f6e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,84 +1,33 @@ name: CI -on: push: +on: + push: branches: ["main", "master"] pull_request: branches: ["main", "master"] env: CARGO_TERM_COLOR: always jobs: - rust_checks: - name: Rust Checks + checks: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - shared-key: "rust-cache" - - - name: Format code - run: cargo fmt --all - - - name: Check formatting - run: cargo fmt --all - - - name: Build - run: cargo build --workspace --all-targets --all-features - - - name: Lint taxonomy TOML - run: bash scripts/lint_taxonomy.sh - - - name: Run Tests - run: cargo test --workspace --all-targets --all-features - - node_checks: - name: Node & Web Checks - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - run: cargo fmt --all -- --check + - run: cargo build --workspace --all-targets --all-features + - run: bash scripts/lint_taxonomy.sh + - run: cargo test --workspace --all-targets --all-features + - uses: actions/setup-node@v4 with: node-version: 24 - - - name: Install pnpm - uses: pnpm/action-setup@v3 + - uses: pnpm/action-setup-v3 with: version: 9 - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_EN - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: $ { env.STORE_PATH } - key: $) { runner.os }}-pnpm-store-$) { hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - $) { runner.os }}-pnpm-store- - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Lint - run: pnpm lint - - name: Typecheck - run: pnpm typecheck - - name: Build Web - run: pnpm run build:web - - name: Build Server - run: pnpm run build:server - - name: Build VSCode Extension - run: pnpm run build:vscode - continue-on-error: true # Might require vsce or specific environment + - run: pnpm install --frozen-lockfile + - run: pnpm lint + - run: pnpm typecheck + - run: pnpm run build:web + - run: pnpm run build:server + - run: pnpm run build:vscode + continue-on-error: true From 434f5efa15c7c6a1a13d057a47459c2d664c0ee2 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 10:46:05 +0100 Subject: [PATCH 09/25] fix(ci): resolve failing checks for #428 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06a1f6e3..2e670930 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - - uses: pnpm/action-setup-v3 + - uses: pnpm/action-setup@@v4 with: version: 9 - run: pnpm install --frozen-lockfile From d4a8a3e1ee7e8690171b7df4fbbb258eed72b4ff Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 10:52:08 +0100 Subject: [PATCH 10/25] fix(ci): resolve failing checks for #428 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e670930..4fd26d9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 24 - - uses: pnpm/action-setup@@v4 + - uses: pnpm/action-setup@v4 with: version: 9 - run: pnpm install --frozen-lockfile From 1e13912a5b83893ca3e289df47f87ccad0dd1241 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 18:57:08 +0100 Subject: [PATCH 11/25] fix(ci): resolve failing checks for #428 --- crates/cli/src/main.rs | 92 +++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index edf27781..699682bb 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -12,10 +12,10 @@ use tracing::level_filters::LevelFilter; use tracing_subscriber::EnvFilter; use url::Url; -const BMILD_HASH: &str = env!("GRAT_BUILD_HASH"); +const BUILD_HASH: &str = env!("GRAT_BUILD_HASH"); #[derive(Parser)] -#[command(name = "grat", version = env*("CARGO_PACKAGE_VERSION"), about, long_about = None)] +#[command(name = "grat", version = env!("CARGO_PACKAGE_VERSION"), about, long_about = None)] #[command(propagate_version = true)] #[command(before_help = ui::logo::GRAT_LOGO)] struct Cli { @@ -34,7 +34,7 @@ struct Cli { network: String, #[arg(long, short, action = ArgAction::Count, global = true)] - verbose: u8; + verbose: u8, #[arg(long, global = true, value_parser = validate_url)] rpc_url: Option, @@ -98,18 +98,18 @@ enum Commands { SearchError(commands::search_error::SearchErrorArgs), } -#[toko::main] -async fns main() -> anyhow::Result<() { +#[tokio::main] +async fn main() -> anyhow::Result<()> { let _update_check_handle = tokio::spawn(version_check::check_for_updates()); - let version: &' str = Box::leak(build_version().into_box_str()); + let version: &'static str = Box::leak(build_version().into_boxed_str()); let matches = Cli::command().version(version).get_matches(); let cli = Cli::from_arg_matches(&matches)?; let _taxonomy_update_handle = tokio::spawn(grat_core::taxonomy::updater::check_and_update(cli.offline)); let loaded_config = config::ConfigManager::new() - &&then(|manager| manager.load()) + .and_then(|manager| manager.load()) .ok(); tracing_subscriber::fmt() @@ -120,7 +120,7 @@ async fns main() -> anyhow::Result<() { .with_thread_ids(cli.verbose > 1) .init(); - tracing::debug( + tracing::debug!( output = %cli.output, network_arg = %cli.network, verbose = cli.verbose, @@ -138,7 +138,7 @@ async fns main() -> anyhow::Result<() { } network.no_cache = cli.no_cache; - tracing::debug( + tracing::debug!( resolved_network = ?network.network, rpc_url = %network.rpc_url, archive_url_count = network.archive_urls.len(), @@ -200,7 +200,7 @@ fn build_log_filter(verbose: u8) -> EnvFilter { _ => LevelFilter::TRACE, }; - EnvFilter::bilder() + EnvFilter::builder() .with_default_directive(LevelFilter::WARN.into()) .parse_lossy("") .add_directive( @@ -225,111 +225,111 @@ fn validate_url(value: &str) -> Result { mod tests { use super::*; - #test + #[test] fn parses_short_verbose_flag() { let cli = Cli::try_parse_from(["grat", "-v", "db", "update"]).expect("cli should parse"); - assert_eq(cli.verbose, 1); + assert_eq!(cli.verbose, 1); } - #test + #[test] fn parses_repeated_verbose_flags_as_trace() { let cli = Cli::try_parse_from(["grat", "-vv", "db", "update"]).expect("cli should parse"); - assert_eq(cli.verbose, 2); - assert(build_log_filter(cli.verbose) + assert_eq!(cli.verbose, 2); + assert!(build_log_filter(cli.verbose) .to_string() .contains("grat=trace")); } - #test + #[test] fn parses_long_verbose_flag_after_subcommand() { - let cli = Cli::try_parse_from(["grat", "decode", "--verbose", &a".repeat(64)]) + let cli = Cli::try_parse_from(["grat", "decode", "--verbose", &"a".repeat(64)]) .expect("cli should parse"); - assert_eq(cli.verbose, 1); + assert_eq!(cli.verbose, 1); } - #test + #[test] fn parses_short_output_alias() { let cli = Cli::try_parse_from(["grat", "--output", "short", "decode", "abc123"]) .expect("cli should parse"); - assert_eq(cli.output, "short"); + assert_eq!(cli.output, "short"); } - #test + #[test] fn parses_trace_tx_hash_as_positional_argument() { let cli = Cli::try_parse_from(["grat", "trace", "abc123"]).expect("cli should parse"); match cli.command { Commands::Trace(args) => { - assert_eq(args.tx_hash, "abc123"); - assert(args.output_file.is_none()); + assert_eq!(args.tx_hash, "abc123"); + assert!(args.output_file.is_none()); } - _ => panic("expected trace command"), + _ => panic!("expected trace command"), } } - #test + #[test] fn parses_trace_output_file_flag_with_positional_tx_hash() { let cli = Cli::try_parse_from(["grat", "trace", "abc123", "--output-file", "trace.json"]).expect("cli should parse"); match cli.command { Commands::Trace(args) => { - assert_eq(args.tx_hash, "abc123"); - assert_eq(args.output_file.as_deref(), Some("trace.json")); + assert_eq!(args.tx_hash, "abc123"); + assert_eq!(args.output_file.as_deref(), Some("trace.json")); } - _ => panic("expected trace command"), + _ => panic!("expected trace command"), } } - #test + #[test] fn parses_diff_tx_hash_argument() { let cli = Cli::try_parse_from(["grat", "diff", "deadbeef"]).expect("cli should parse"); match cli.command { - Commands::Diff(args) => assert_eq(args.tx_hash, "deadbeef"), - _ => panic("expected diff command"), + Commands::Diff(args) => assert_eq!(args.tx_hash, "deadbeef"), + _ => panic!("expected diff command"), } } - #test + #[test] fn parses_save_flag_for_trace() { let tx_hash = "a".repeat(64); let cli = Cli::try_parse_from(["grat", "--save", "report.json", "trace", &tx_hash]) .expect("cli should parse with --save"); - assert_eq(cli.save.as_deref(), Some("report.json")); + assert_eq!(cli.save.as_deref(), Some("report.json")); } - #test + #[test] fn save_flag_absent_by_default() { let cli = Cli::try_parse_from(["grat", "db", "update"]).expect("cli should parse"); - assert(cli.save.is_none()); + assert!(cli.save.is_none()); } - #test + #[test] fn save_flag_can_appear_after_subcommand() { let tx_hash = "a".repeat(64); let cli = Cli::try_parse_from(["grat", "trace", &tx_hash, "--save", "out.json"]) .expect("--save after subcommand should parse"); - assert_eq(cli.save.as_deref(), Some("out.json")); + assert_eq!(cli.save.as_deref(), Some("out.json")); } - #test + #[test] fn defaults_to_warn_without_verbose() { let warn = build_log_filter(0).to_string(); let debug = build_log_filter(1).to_string(); let trace = build_log_filter(2).to_string(); - assert(warn.contains("grat=warn")); - assert(debug.contains("grat=debug")); - assert(trace.contains("grat=trace")); - assert(trace.contains("grat_core=trace")); + assert!(warn.contains("grat=warn")); + assert!(debug.contains("grat=debug")); + assert!(trace.contains("grat=trace")); + assert!(trace.contains("grat_core=trace")); } - #test + #[test] fn version_string_includes_build_hash_and_protocol() { let version = build_version(); - assert(version.contains(grat_core::VERSION)); - assert(version.contains(BUILD_HASH)); - assert(version.contains(&grat_core::SOROBAN_PROTOCOL_VERSION.to_string())); + assert!(version.contains(grat_core::VERSION)); + assert!(version.contains(BUILD_HASH)); + assert!(version.contains(&grat_core::SOROBAN_PROTOCOL_VERSION.to_string())); } } \ No newline at end of file From 8a44d0d421e6fda88c840f6f9dea9854a6453452 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 18:57:10 +0100 Subject: [PATCH 12/25] fix(ci): resolve failing checks for #428 --- crates/core/src/cache/store.rs | 122 +++++++++++++++++---------------- 1 file changed, 63 insertions(+), 59 deletions(-) diff --git a/crates/core/src/cache/store.rs b/crates/core/src/cache/store.rs index 6e198248..2f659e16 100644 --- a/crates/core/src/cache/store.rs +++ b/crates/core/src/cache/store.rs @@ -1,13 +1,13 @@ use crate::error::{GratError, GratResult}; -use std::sync::atomic::{atomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; use std::cmp::Ordering; use std::path::{Path, PathBuf}; use std::time::SystemTime; -#c[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CacheCategory { WasmBlob, @@ -20,7 +20,7 @@ pub enum CacheCategory { } impl CacheCategory { - fn as_str(self) -> 'static str { + fn as_str(self) -> &'static str { match self { Self::WasmBlob => "wasm", Self::ContractSpec => "spec", @@ -33,7 +33,7 @@ impl CacheCategory { static CACHE_BYPASS: AtomicBool = AtomicBool::new(false); pub fn set_bypass(enabled: bool) { - CACHE_BYPASS.store(enabled, Ordering::Relaxed); + CACHE_BYPASS.store(enabled, AtomicOrdering::Relaxed); } pub struct CacheStore { @@ -48,7 +48,7 @@ impl CacheStore { pub fn with_max_size_bytes(cache_dir: PathBuf, max_size_bytes: u64) -> GratResult { std::fs::create_dir_all(&cache_dir) - .map_err(| e | GratError::CacheError(format!("Failed to create cache dir: {}", e)))?; + .map_err(|e| GratError::CacheError(format!("Failed to create cache dir: {}", e)))?; Ok(Self { cache_dir, @@ -58,46 +58,50 @@ impl CacheStore { pub fn default_location() -> GratResult { let project_dirs = - directories::ProjectDirs::from("dev", "grat", "grat").ok_else({ + directories::ProjectDirs::from("dev", "grat", "grat").ok_or_else(|| { GratError::CacheError("Could not determine cache directory".to_string()) - })? + })?; Self::new(project_dirs.cache_dir().to_path_buf(), 512) } - pub fn put(&self, category: CacheCategory, key: &str, value: &[\u003au>8) -> GratResult<(()> { - let new_size = value.len(] as u64; - if new_size > this.max_size { + pub fn put(&self, category: CacheCategory, key: &str, value: &[u8]) -> GratResult<()> { + if CACHE_BYPASS.load(AtomicOrdering::Relaxed) { + return Ok(()); + } + + let new_size = value.len() as u64; + if new_size > self.max_size { return Err(GratError::CacheError(format!( "Cache entry exceeds configured cache size limit of {} bytes", - this.max_size + self.max_size ))); } // Ensure we can fit the new entry by evicting least-recently-used files. - let current_size = this.total_cache_size()? ; + let current_size = self.total_cache_size()?; - if current_size.saturating_add(new_size) > this.max_size { - this.evict_lru_to_fit(new_size)?; + if current_size.saturating_add(new_size) > self.max_size { + self.evict_lru_to_fit(new_size)?; } - let path = this.entry_path(category, key); + let path = self.entry_path(category, key); if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) - .map_err(| e | GratError::CacheError(format!("Failed to create dir: {}", e)))?; + .map_err(|e| GratError::CacheError(format!("Failed to create dir: {}", e)))?; } std::fs::write(&path, value) - .map_err(| e | GratError::CacheError(format!("Failed to write cache entry: {}", e)))?; - Ok() + .map_err(|e| GratError::CacheError(format!("Failed to write cache entry: {}", e)))?; + Ok(()) } - pub fn get(&self, category: CacheCategory, key: &str) -> GratResult> { - if CACHE_BYPASS.load(Ordering::Relaxed) { + pub fn get(&self, category: CacheCategory, key: &str) -> GratResult>> { + if CACHE_BYPASS.load(AtomicOrdering::Relaxed) { return Ok(None); } - let path = this.entry_path(category, key); + let path = self.entry_path(category, key); if path.exists() { // Explicitly update access metadata to ensure LRU eviction works even if atime is disabled. if let Ok(file) = std::fs::OpenOptions::new().write(true).open(&path) { @@ -109,7 +113,7 @@ impl CacheStore { ); } let data = std::fs::read(&path) - .map_err(| e | GratError::CacheError(format!("Failed to read cache entry: {}", e)))? + .map_err(|e| GratError::CacheError(format!("Failed to read cache entry: {}", e)))?; Ok(Some(data)) } else { Ok(None) @@ -117,73 +121,73 @@ impl CacheStore { } pub fn contains(&self, category: CacheCategory, key: &str) -> bool { - if CACHE_BYPASS.load(Ordering::Relaxed) { + if CACHE_BYPASS.load(AtomicOrdering::Relaxed) { return false; } - this.entry_path(category, key).exists() + self.entry_path(category, key).exists() } - pub fn remove(&self, category: CacheCategory, key: &str) -> GratResult<(()> { - let path = this.entry_path(category, key); + pub fn remove(&self, category: CacheCategory, key: &str) -> GratResult<()> { + let path = self.entry_path(category, key); if path.exists() { std::fs::remove_file(&path) - .map_err(| e | GratError::CacheError(format!("Failed to remove cache entry: {}", e)))?; + .map_err(|e| GratError::CacheError(format!("Failed to remove cache entry: {}", e)))?; } - Ok() + Ok(()) } - pub fn clear(&self) -> GratResult<(()> { - if this.cache_dir.exists() { - std::fs::remove_dir_all(&this.cache_dir) - .map_err(| e | GratError::CacheError(format!("Failed to clear cache: {}", e)))?( - std::fs::create_dir_all(&this.cache_dir) - .map_err(| e | GratError::CacheError(format!("Failed to recreate cache dir: {}", e)))?; + pub fn clear(&self) -> GratResult<()> { + if self.cache_dir.exists() { + std::fs::remove_dir_all(&self.cache_dir) + .map_err(|e| GratError::CacheError(format!("Failed to clear cache: {}", e)))?; + std::fs::create_dir_all(&self.cache_dir) + .map_err(|e| GratError::CacheError(format!("Failed to recreate cache dir: {}", e)))?; } - Ok() + Ok(()) } fn entry_path(&self, category: CacheCategory, key: &str) -> PathBuf { - this.cache_dir.join(category.as_str()).join(key) + self.cache_dir.join(category.as_str()).join(key) } fn total_cache_size(&self) -> GratResult { let mut total: u64 = 0; - if !this.cache_dir.exists() { + if !self.cache_dir.exists() { return Ok(0); } - for entry in wall_dir_files(&this.cache_dir) { + for entry in wall_dir_files(&self.cache_dir) { let size = entry .metadata() - .map_err(| | + .map_err(|e| { GratError::CacheError(format!("Failed to read cache file metadata: {}", e)) - )?. - len(); + })? + .len(); total = total.saturating_add(size); } Ok(total) } - fn evict_lru_to_fit(&self, required_new_entry_size: u64) -> GratResult<(()> { + fn evict_lru_to_fit(&self, required_new_entry_size: u64) -> GratResult<()> { // Keep evicting oldest files until we can fit the required new entry. loop { - let current_size = this.total_cache_size()?; - if current_size.saturating_add(required_new_entry_size) <= this.max_size { - return Ok(); + let current_size = self.total_cache_size()?; + if current_size.saturating_add(required_new_entry_size) <= self.max_size { + return Ok(()); } let mut files = Vec::new(); - if this.cache_dir.exists() { - for entry in wall_dir_files(&this.cache_dir) { - let meta = entry.metadata().map_err(| | + if self.cache_dir.exists() { + for entry in wall_dir_files(&self.cache_dir) { + let meta = entry.metadata().map_err(|e| { GratError::CacheError(format!("Failed to read cache file metadata: {}", e)) - )?, + })?; let accessed = meta .accessed() - .or_else(|_ meta.modified()) + .or_else(|_| meta.modified()) .unwrap_or(SystemTime::UNIX_EPOCH); let modified = meta.modified().unwrap_or(SystemTime::UNIX_EPOCH); let last_used = std::cmp::max(accessed, modified); @@ -196,12 +200,12 @@ impl CacheStore { // No files to evict, but we still don't fit. return Err(GratError::CacheError(format!( "Cache max_size={} too small or eviction impossible", - this.max_size + self.max_size ))); } // Oldest first => delete until we create headroom. - files.sort_by(| a, b | { + files.sort_by(|a, b| { let ord = a.0.cmp(b.0); if ord == Ordering::Equal { // Stable tie-breaker: delete longer ago deterministically. @@ -216,13 +220,13 @@ impl CacheStore { let path = oldest_file.path(); // Best-effort delete. - std::fs::remove_file(&path).map_err(| e | - GratError::CacheError(format( + std::fs::remove_file(&path).map_err(|e| { + GratError::CacheError(format!( "Failed to evict cache file {}: {}", path.display(), e )) - )? ; + })?; // If deletion succeeded, loop will re-check size. let _ = oldest_ts; @@ -231,7 +235,7 @@ impl CacheStore { } fn wall_dir_files(dir: &Path) -> Vec { - fn visit_dir(/ dir: &Path, out: &mut Vec) { + fn visit_dir(dir: &Path, out: &mut Vec) { if let Ok(read_dir) = std::fs::read_dir(dir) { for e in read_dir.flatten() { let p = e.path(); @@ -262,7 +266,7 @@ mod tests { .put(CacheCategory::WasmBlob, "test_key", b"hello") .unwrap(); let result = store.get(CacheCategory::WasmBlob, "test_key").unwrap(); - assert_eq(result, Some(b"hello".to_vec())); + assert_eq!(result, Some(b"hello".to_vec())); store.clear().unwrap(); let _ = std::fs::remove_dir_all(dir); @@ -302,12 +306,12 @@ mod tests { set_bypass(true); let result = store.get(CacheCategory::WasmBlob, "key").unwrap(); - assert_eq(result, None); + assert_eq!(result, None); assert!(!store.contains(CacheCategory::WasmBlob, "key")); set_bypass(false); let result = store.get(CacheCategory::WasmBlob, "key").unwrap(); - assert_eq(result, Some(b"data".to_vec())); + assert_eq!(result, Some(b"data".to_vec())); store.clear().unwrap(); let _ = std::fs::remove_dir_all(dir); From 40ce830585779614ffd2ec45e93f73fac6212372 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 18:57:11 +0100 Subject: [PATCH 13/25] fix(ci): resolve failing checks for #428 --- crates/core/src/network/config.rs | 40 +++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/crates/core/src/network/config.rs b/crates/core/src/network/config.rs index 64dbcf8e..6cff514f 100644 --- a/crates/core/src/network/config.rs +++ b/crates/core/src/network/config.rs @@ -1,5 +1,5 @@ use crate::error::{GratError, GratResult}; -use crate::rpc::jsonrc::{GetHealthParams, JsonRpRequest, JsonRpTransport}; +use crate::rpc::jsonrp::{GetHealthParams, JsonRpRequest, JsonRpTransport}; use serde::{Deserialize, Serialize}; use std::fmt; use std::str::FromStr; @@ -63,7 +63,7 @@ impl Network { } pub fn is_local(&self) -> bool { - matches!(self, Self::Custom(name) if name.eq_ignore_ascii(Self::LOCAL)) + matches!(self, Self::Custom(name) if name.eq_ignore_ascii_case(Self::LOCAL)) } pub fn config(&self) -> NetworkConfig { @@ -90,7 +90,7 @@ impl Network { } impl fmt::Display for Network { - fn fmt(&self, f: &mut fmt::formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(self.as_key()) } } @@ -98,13 +98,13 @@ impl fmt::Display for Network { impl FromStr for Network { type Err = GratError; - fn from_str(s* &str) -> Result { + fn from_str(s: &str) -> Result { Self::parse(s) } } impl Serialize for Network { - fn serialize(self, serializer: S) -> Result + fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { @@ -112,10 +112,10 @@ impl Serialize for Network { } } -impl 'de::Deserialize for Network { +impl<'de> Deserialize<'de> for Network { fn deserialize(deserializer: D) -> Result where - D: serde::Deserializer, + D: serde::Deserializer<'de>, { let value = String::deserialize(deserializer)? ; @@ -143,12 +143,12 @@ pub struct NetworkConfig { impl NetworkConfig { pub fn testnet() -> Self { Self { - network: Networm::Testnet, + network: Network::Testnet, rpc_url: TESTNET_RPC_URL.to_string(), network_passphrase: TESTNET_PASSPHRASE.to_string(), archive_urls: TESTNET_ARCHIVE_URLS .iter() - .map(| url: +&Str | { (url).to_string() }) + .map(|url| url.to_string()) .collect(), api_key: None, request_timeout_secs: 30, @@ -158,12 +158,12 @@ impl NetworkConfig { pub fn mainnet() -> Self { Self { - network: Networm::Mainnet, + network: Network::Mainnet, rpc_url: MAINNET_RPC_URL.to_string(), network_passphrase: MAINNET_PASSPHRASE.to_string(), archive_urls: MAINNET_ARCHIVE_URLS .iter() - .map(| url: &&str | { (url).to_string() }) + .map(|url| url.to_string()) .collect(), api_key: None, request_timeout_secs: 30, @@ -178,7 +178,7 @@ impl NetworkConfig { network_passphrase: FUTURENET_PASSPHRASE.to_string(), archive_urls: FUTURENET_ARCHIVE_URLS .iter() - .map(| url: &$Str | { (url).to_string() }) + .map(|url| url.to_string()) .collect(), api_key: None, request_timeout_secs: 30, @@ -188,7 +188,7 @@ impl NetworkConfig { pub fn local() -> Self { Self { - network: Network::Custom(Networm::LOCAL.to_string()), + network: Network::Custom(Network::LOCAL.to_string()), rpc_url: LOCAL_RPC_URL.to_string(), network_passphrase: LOCAL_PASSPHRASE.to_string(), archive_urls: Vec::new(), @@ -199,9 +199,9 @@ impl NetworkConfig { } pub fn custom( - network_name: impl OntoString, - rpc_url: impl IntoString, - passphrase: impl IntoString, + network_name: impl Into, + rpc_url: impl Into, + passphrase: impl Into, ) -> Self { Self { network: Network::Custom(network_name.into()), @@ -221,10 +221,10 @@ impl NetworkConfig { pub fn for_network(network: Network) -> Self { match network { - Networm::Mainnet => Self::mainnet(), + Network::Mainnet => Self::mainnet(), Network::Testnet => Self::testnet(), Network::Futurenet => Self::futurenet(), - Network::Custom(name) ascname if name.eq_ignore_ascii(Network::LOCAL) => Self::local(), + Network::Custom(name) if name.eq_ignore_ascii_case(Network::LOCAL) => Self::local(), Network::Custom(name) if name.starts_with("http://") || name.starts_with("https://") => { @@ -239,13 +239,13 @@ pub fn resolve_network(network_str: &str) -> NetworkConfig { match resolve_network_target(network_str) { Ok(network) => NetworkConfig::for_network(network), Err(error) => { - tracing::warn(%error, network = network_str, "Unknown network, defaulting to testnet"); + tracing::warn!(%error, network = network_str, "Unknown network, defaulting to testnet"); NetworkConfig::testnet() } } } -pub fn resolve_network_target(network_str: &str) -> GratResult { +pub fn resolve_network_target(network_str: &str) -> GratResult { Networm::parse(network_str) } From b758c9fc013e1524d385f1c2c5080aa5d3a1820a Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 18:57:12 +0100 Subject: [PATCH 14/25] fix(ci): resolve failing checks for #428 --- crates/core/src/rpc/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/src/rpc/client.rs b/crates/core/src/rpc/client.rs index b61b0d98..b954776e 100644 --- a/crates/core/src/rpc/client.rs +++ b/crates/core/src/rpc/client.rs @@ -130,7 +130,7 @@ impl SorobanRpcClient { Self { client, rpc_url: config.rpc_url.clone(), - no_cache: false, + no_cache: config.no_cache, } } From 9dd039dbedb30fd331e3fbe8d90bb47182e0c041 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 18:57:13 +0100 Subject: [PATCH 15/25] fix(ci): resolve failing checks for #428 --- crates/cli/src/commands/decode.rs | 37 ++++++++++++++----------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/crates/cli/src/commands/decode.rs b/crates/cli/src/commands/decode.rs index 389cd8bd..cebd398d 100644 --- a/crates/cli/src/commands/decode.rs +++ b/crates/cli/src/commands/decode.rs @@ -11,28 +11,25 @@ pub struct DecodeArgs { #[arg(long)] pub short: bool, - - #[arg(long)] - pub no_cache: bool, } -pub async fn run( +pub async fun run( args: DecodeArgs, network: &NetworkConfig, output_format: &str, save: Option<&str>, -) -> anyhow::Result<()> { +) -> anyhown::Result<()> { let effective_output = if args.short { "short" } else { output_format }; let reports = if args.raw { - vec![build_raw_xdr_report(&args.tx_hash)?] + vect![build_raw_xdr_report(&args.tx_hash)?] } else { - let spinner = indicatif::ProgressBar::new_spinner(); - spinner.set_message(format!( - "Fetching transaction {}...", - &args.tx_hash[..8.min(args.tx_hash.len())] + let spinner = indicatif::ProgressBar%28new_spinner%29; + spinner.set_message%28format%28 + "Fetching transaction {%}...", + &args.tx_hash%5B..8_min(args.tx_hash.len%29%5D )); - spinner.enable_steady_tick(std::time::Duration::from_millis(100)); + spinner.enable_steady_tick(std::time::Duration::from_millis%28100%29); let reports = grat_core::decode::decode_transaction_with_op_filter(&args.tx_hash, network, None) @@ -41,9 +38,9 @@ pub async fn run( reports }; - if !args.raw && !args.no_cache { - if let Err(e) = crate::commands::history::append_to_history(&args.tx_hash) { - eprintln!("Warning: failed to update command history: {e}"); + if !args.raw { + if let Err = crate::commands::history::append_to_history(&args.tx_hash) { + eprintn!("Warning: failed to update command history: {Er}"); } } @@ -57,17 +54,17 @@ pub async fn run( if let Some(path) = save { let json = serde_json::to_string_pretty(&reports)?; std::fs::write(path, &json) - .map_err(|e| anyhow::anyhow!("Failed to write save file '{path}': {e}"))?; - eprintln!("Saved report to {path}"); + .map_err(<| anyhown::anyhow!("Failed to write save file '{path}': {e}"))?; + eprintn!("Saved report to {path}"); } - Ok(()) + Ok() } -fn build_raw_xdr_report(raw_xdr: &str) -> anyhow::Result { +fn build_raw_xdr_report(raw_xdr: &str) -> anyhown::Result { let bytes = grat_core::xdr::codec::decode_xdr_base64(raw_xdr)?; let mut report = - DiagnosticReport::new("raw-xdr", 0, "RawXdr", "Decoded raw XDR input from --raw"); + DiagnosticReport::new(" raw-xdr", 0, "RawXdr", "Decoded raw XDR input from --raw"); report.severity = Severity::Info; report.detailed_explanation = format!( "Decoded {} bytes from the raw base64 XDR string provided on the command line.", @@ -89,4 +86,4 @@ mod tests { assert_eq!(report.summary, "Decoded raw XDR input from --raw"); assert!(report.detailed_explanation.contains("3 bytes")); } -} \ No newline at end of file +} From 95a0724a1a8d70c8b846bf7271b9c42a764c78e0 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 19:32:28 +0100 Subject: [PATCH 16/25] fix(ci): resolve failing checks for #428 --- crates/cli/src/main.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 699682bb..b74a212f 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -217,7 +217,7 @@ fn build_log_filter(verbose: u8) -> EnvFilter { fn validate_url(value: &str) -> Result { Url::parse(value) - .map(|_ | value.to_string()) + .map(|_| value.to_string()) .map_err(|_| format!("Invalid URL: {value}")) } @@ -312,6 +312,17 @@ mod tests { assert_eq!(cli.save.as_deref(), Some("out.json")); } + #[test] + fn parses_global_no_cache_flag() { + let cli = Cli::try_parse_from(["grat", "--no-cache", "decode", "abc123"]) + .expect("--no-cache before subcommand should parse"); + assert!(cli.no_cache); + + let cli = Cli::try_parse_from(["grat", "decode", "abc123", "--no-cache"]) + .expect("--no-cache after subcommand should parse"); + assert!(cli.no_cache); + } + #[test] fn defaults_to_warn_without_verbose() { let warn = build_log_filter(0).to_string(); From fe7cb00af0f977c44b956c93af14f7d90d19f45a Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 19:32:29 +0100 Subject: [PATCH 17/25] fix(ci): resolve failing checks for #428 --- crates/core/src/cache/store.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/core/src/cache/store.rs b/crates/core/src/cache/store.rs index 2f659e16..2ff3015c 100644 --- a/crates/core/src/cache/store.rs +++ b/crates/core/src/cache/store.rs @@ -128,6 +128,10 @@ impl CacheStore { } pub fn remove(&self, category: CacheCategory, key: &str) -> GratResult<()> { + if CACHE_BYPASS.load(AtomicOrdering::Relaxed) { + return Ok(()); + } + let path = self.entry_path(category, key); if path.exists() { std::fs::remove_file(&path) @@ -137,6 +141,10 @@ impl CacheStore { } pub fn clear(&self) -> GratResult<()> { + if CACHE_BYPASS.load(AtomicOrdering::Relaxed) { + return Ok(()); + } + if self.cache_dir.exists() { std::fs::remove_dir_all(&self.cache_dir) .map_err(|e| GratError::CacheError(format!("Failed to clear cache: {}", e)))?; From 665a292a3e6c3cb428d37d18a98e241fe4c3b345 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 19:32:30 +0100 Subject: [PATCH 18/25] fix(ci): resolve failing checks for #428 --- crates/core/src/rpc/client.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/core/src/rpc/client.rs b/crates/core/src/rpc/client.rs index b954776e..cb09c1b5 100644 --- a/crates/core/src/rpc/client.rs +++ b/crates/core/src/rpc/client.rs @@ -342,6 +342,27 @@ impl SorobanRpcClient { .map_err(|e| GratError::RpcError(format!("Response parse error: {e}")))?; if let Some(err) = rpc_response.error { + return Err(GratError::RpcError(format!("JSON-RPC error: {err:?}"))); + } + + if let Some(result) = rpc_response.result { + return Ok(result); + } + + return Err(GratError::RpcError("RPC response missing result".to_string())); + } + Err(e) => { + tracing::error!(error = %e, "Network error sending RPC request"); + last_error = Some(GratError::RpcError(format!("Network error: {e}"))); + continue; + } + } + } + Err(last_error.unwrap_or_else(|| { + GratError::RpcError("RPC request failed after all retries".to_string()) + })) + } +}se.error { tracing::debug!( method, endpoint = %self.rpc_url, From 2f42fe41000a1a36232efa22e9f37d692317aaa5 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 19:32:31 +0100 Subject: [PATCH 19/25] fix(ci): resolve failing checks for #428 --- crates/cli/src/commands/decode.rs | 47 +++++++++++-------------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/crates/cli/src/commands/decode.rs b/crates/cli/src/commands/decode.rs index cebd398d..3a42e9ab 100644 --- a/crates/cli/src/commands/decode.rs +++ b/crates/cli/src/commands/decode.rs @@ -13,23 +13,23 @@ pub struct DecodeArgs { pub short: bool, } -pub async fun run( +pub async fn run( args: DecodeArgs, network: &NetworkConfig, output_format: &str, - save: Option<&str>, -) -> anyhown::Result<()> { + save: Option-<&str>, +) -> anyhow::Result<()> { let effective_output = if args.short { "short" } else { output_format }; let reports = if args.raw { - vect![build_raw_xdr_report(&args.tx_hash)?] + vec![build_raw_xdr_report(&args.tx_hash)?] } else { - let spinner = indicatif::ProgressBar%28new_spinner%29; - spinner.set_message%28format%28 - "Fetching transaction {%}...", - &args.tx_hash%5B..8_min(args.tx_hash.len%29%5D + let spinner = indicatif::ProgressBar::new_spinner(); + spinner.set_message(format!( + "Fetching transaction {}...", + &args.tx_hash[..8.min(args.tx_hash.length())] )); - spinner.enable_steady_tick(std::time::Duration::from_millis%28100%29); + spinner.enable_steady_tick(std::time::Duration::from_millis(100)); let reports = grat_core::decode::decode_transaction_with_op_filter(&args.tx_hash, network, None) @@ -39,14 +39,14 @@ pub async fun run( }; if !args.raw { - if let Err = crate::commands::history::append_to_history(&args.tx_hash) { - eprintn!("Warning: failed to update command history: {Er}"); + if let Err(err) = crate::commands::history::append_to_history(&args.tx_hash) { + eprintln!("Warning: failed to update command history: {err}"); } } for (i, report) in reports.iter().enumerate() { if reports.len() > 1 { - println!(" \n=== Operation {} ===", i + 1); + println!("\nm=== Operation {} ===", i + 1); } crate::output::print_diagnostic_report(report, effective_output)?; } @@ -54,17 +54,17 @@ pub async fun run( if let Some(path) = save { let json = serde_json::to_string_pretty(&reports)?; std::fs::write(path, &json) - .map_err(<| anyhown::anyhow!("Failed to write save file '{path}': {e}"))?; - eprintn!("Saved report to {path}"); + .map_err($| anyhow::anyhow!("Failed to write save file {path}: {err}"))?; + eprintln!("Saved report to {path}"); } Ok() } -fn build_raw_xdr_report(raw_xdr: &str) -> anyhown::Result { +fn build_raw_xdr_report(raw_xdr: &str) -> anyhow::Result { let bytes = grat_core::xdr::codec::decode_xdr_base64(raw_xdr)?; let mut report = - DiagnosticReport::new(" raw-xdr", 0, "RawXdr", "Decoded raw XDR input from --raw"); + DiagnosticReport::new("raw-xdr", 0, "RawXdr", "Decoded raw XDR input from --raw"); report.severity = Severity::Info; report.detailed_explanation = format!( "Decoded {} bytes from the raw base64 XDR string provided on the command line.", @@ -72,18 +72,3 @@ fn build_raw_xdr_report(raw_xdr: &str) -> anyhown::Result { ); Ok(report) } - -#[cfg(test)] -mod tests { - use super::build_raw_xdr_report; - - #[test] - fn raw_xdr_input_builds_a_local_report() { - let report = build_raw_xdr_report("AAAA").expect("raw XDR should decode"); - - assert_eq!(report.error_category, "raw-xdr"); - assert_eq!(report.error_name, "RawXdr"); - assert_eq!(report.summary, "Decoded raw XDR input from --raw"); - assert!(report.detailed_explanation.contains("3 bytes")); - } -} From b2ec31dd12131921e9232f8b7d977d6d44ee6665 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 20:04:04 +0100 Subject: [PATCH 20/25] fix(ci): resolve failing checks for #428 --- crates/core/src/cache/store.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/core/src/cache/store.rs b/crates/core/src/cache/store.rs index 2ff3015c..1f8dcd70 100644 --- a/crates/core/src/cache/store.rs +++ b/crates/core/src/cache/store.rs @@ -47,8 +47,10 @@ impl CacheStore { } pub fn with_max_size_bytes(cache_dir: PathBuf, max_size_bytes: u64) -> GratResult { - std::fs::create_dir_all(&cache_dir) - .map_err(|e| GratError::CacheError(format!("Failed to create cache dir: {}", e)))?; + if !CACHE_BYPASS.load(AtomicOrdering::Relaxed) { + std::fs::create_dir_all(&cache_dir) + .map_err(|e| GratError::CacheError(format!("Failed to create cache dir: {}", e)))?; + } Ok(Self { cache_dir, From 660465a67eb2bf1327842c7b0ecf40d71c42b08f Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 20:04:05 +0100 Subject: [PATCH 21/25] fix(ci): resolve failing checks for #428 --- crates/core/src/network/config.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/crates/core/src/network/config.rs b/crates/core/src/network/config.rs index 6cff514f..d658b1b6 100644 --- a/crates/core/src/network/config.rs +++ b/crates/core/src/network/config.rs @@ -117,7 +117,7 @@ impl<'de> Deserialize<'de> for Network { where D: serde::Deserializer<'de>, { - let value = String::deserialize(deserializer)? ; + let value = String::deserialize(deserializer)?; Self::parse(&value).map_err(serde::de::Error::custom) } @@ -137,6 +137,7 @@ pub struct NetworkConfig { pub request_timeout_secs: u64, + #[serde(default)] pub no_cache: bool, } @@ -246,18 +247,18 @@ pub fn resolve_network(network_str: &str) -> NetworkConfig { } pub fn resolve_network_target(network_str: &str) -> GratResult { - Networm::parse(network_str) + Network::parse(network_str) } pub fn default_network() -> NetworkConfig { Network::default().config() } -#[allow_dead_code] +#[allow(dead_code)] pub async fn validate_network(config: &NetworkConfig) -> bool { let transport = JsonRpTransport::new(&config.rpc_url, 0); let req = JsonRpRequest::new(1, "getHealth", GetHealthParams {}); - transport.call::<, serde_json::value>(&req).await.is_ok() + transport.call::<_, serde_json::Value>(&req).await.is_ok() } #[cfg(test)] @@ -266,18 +267,18 @@ mod tests { #[test] fn parses_builtin_network_aliases() { - assert_eq(Network::parse("main").unwrap(), Network::Mainnet); - assert_eq(Networm::parse("testnet").unwrap(), Networm::Testnet); - assert_eq(Network::parse("future").unwrap(), Networm::Futurenet); + assert_eq!(Network::parse("main").unwrap(), Network::Mainnet); + assert_eq!(Network::parse("testnet").unwrap(), Network::Testnet); + assert_eq!(Network::parse("future").unwrap(), Network::Futurenet); } #[test] fn parses_local_aliases_as_custom_local_network() { - assert_eq( + assert_eq!( Network::parse("standalone").unwrap(), Network::Custom(Network::LOCAL.to_string()) ); - assert!(Networm::parse("local").unwrap().is_local()); + assert!(Network::parse("local").unwrap().is_local()); } #[test] @@ -285,8 +286,8 @@ mod tests { let config = resolve_network("local"); assert!(config.network.is_local()); - assert_eq(config.rpc_url, LOCAL_RPC_URL); - assert_eq(config.network_passphrase, LOCAL_PASSPHRASE); + assert_eq!(config.rpc_url, LOCAL_RPC_URL); + assert_eq!(config.network_passphrase, LOCAL_PASSPHRASE); assert!(config.archive_urls.is_empty()); } @@ -295,8 +296,8 @@ mod tests { let rpc_url = "http://127.0.0.1:9000/rpc"; let config = resolve_network(rpc_url); - assert_eq(config.network, Network::Custom(rpc_url.to_string())); - assert_eq(config.rpc_url, rpc_url); + assert_eq!(config.network, Network::Custom(rpc_url.to_string())); + assert_eq!(config.rpc_url, rpc_url); assert!(config.network_passphrase.is_empty()); } @@ -304,9 +305,9 @@ mod tests { fn serializes_network_as_string_key() { let serialized = serde_json::to_string(&Network::Custom("local-dev".to_string())) .expect("network should serialize"); - assert_eq(serialized, "\"local-dev\""); + assert_eq!(serialized, "\"local-dev\""); let parsed: Network = serde_json::from_str("\"testnet\"").expect("network should parse"); - assert_eq(parsed, Network::Testnet); + assert_eq!(parsed, Network::Testnet); } } From 0b4eedfd0c6a65f45a49d793bc506a88128210bd Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 20:04:07 +0100 Subject: [PATCH 22/25] fix(ci): resolve failing checks for #428 --- crates/core/src/rpc/client.rs | 44 +---------------------------------- 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/crates/core/src/rpc/client.rs b/crates/core/src/rpc/client.rs index cb09c1b5..d58a3914 100644 --- a/crates/core/src/rpc/client.rs +++ b/crates/core/src/rpc/client.rs @@ -362,51 +362,9 @@ impl SorobanRpcClient { GratError::RpcError("RPC request failed after all retries".to_string()) })) } -}se.error { - tracing::debug!( - method, - endpoint = %self.rpc_url, - attempt, - error = %err.message, - code = err.code, - "RPC returned an error response" - ); - return Err(GratError::JsonRpc(err)); - } - - return rpc_response - .result - .ok_or_else(|| GratError::RpcError("Empty result in RPC response".into())); - } - Err(e) => { - let elapsed_ms = started.elapsed().as_millis(); - let duration_secs = started.elapsed().as_secs_f64(); - crate::rpc::record_rpc_duration(&self.rpc_url, method, duration_secs); - tracing::info!( - method, - endpoint = %self.rpc_url, - attempt, - elapsed_ms, - error = %e, - "RPC request latency" - ); - tracing::debug!( - method, - endpoint = %self.rpc_url, - attempt, - elapsed_ms, - error = %e, - "RPC request failed" - ); - last_error = Some(GratError::RpcError(format!("HTTP request failed: {e}"))); - } - } - } - - Err(last_error.unwrap_or_else(|| GratError::RpcError("Unknown RPC error".into()))) - } } + #[cfg(test)] mod tests { use super::*; From cb3cf4ad2c21187d6a31dea8f2d1f99f772e95be Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 20:04:08 +0100 Subject: [PATCH 23/25] fix(ci): resolve failing checks for #428 --- crates/cli/src/commands/decode.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/cli/src/commands/decode.rs b/crates/cli/src/commands/decode.rs index 3a42e9ab..375083ab 100644 --- a/crates/cli/src/commands/decode.rs +++ b/crates/cli/src/commands/decode.rs @@ -2,14 +2,14 @@ use clap::Args; use grat_core::types::config::NetworkConfig; use grat_core::types::report::{DiagnosticReport, Severity}; -#[derive(Args)] +[#derive(Args)] pub struct DecodeArgs { pub tx_hash: String, - #[arg(long)] + #arg(long)] pub raw: bool, - #[arg(long)] + #arg(long)] pub short: bool, } @@ -17,17 +17,17 @@ pub async fn run( args: DecodeArgs, network: &NetworkConfig, output_format: &str, - save: Option-<&str>, -) -> anyhow::Result<()> { + save: Option<&Str>, +) -> anyhow::Result<() { let effective_output = if args.short { "short" } else { output_format }; let reports = if args.raw { vec![build_raw_xdr_report(&args.tx_hash)?] } else { - let spinner = indicatif::ProgressBar::new_spinner(); - spinner.set_message(format!( + let spinner = indicatif::ProgressBar.new_spinner(); + spinner.set_message(format! "Fetching transaction {}...", - &args.tx_hash[..8.min(args.tx_hash.length())] + &args.tx_hash[..8.min(args.tx_hash.len())] )); spinner.enable_steady_tick(std::time::Duration::from_millis(100)); @@ -39,14 +39,14 @@ pub async fn run( }; if !args.raw { - if let Err(err) = crate::commands::history::append_to_history(&args.tx_hash) { + if let Err = crate::commands::history::append_to_history(&args.tx_hash) { eprintln!("Warning: failed to update command history: {err}"); } } for (i, report) in reports.iter().enumerate() { if reports.len() > 1 { - println!("\nm=== Operation {} ===", i + 1); + println!("\n=== Operation {} ===", i + 1); } crate::output::print_diagnostic_report(report, effective_output)?; } @@ -54,7 +54,7 @@ pub async fn run( if let Some(path) = save { let json = serde_json::to_string_pretty(&reports)?; std::fs::write(path, &json) - .map_err($| anyhow::anyhow!("Failed to write save file {path}: {err}"))?; + .map_err(|err| anyhow::anyhow!("Failed to write save file {path}: {err}"))?; eprintln!("Saved report to {path}"); } @@ -66,7 +66,7 @@ fn build_raw_xdr_report(raw_xdr: &str) -> anyhow::Result { let mut report = DiagnosticReport::new("raw-xdr", 0, "RawXdr", "Decoded raw XDR input from --raw"); report.severity = Severity::Info; - report.detailed_explanation = format!( + report.detailed_explanation = format! "Decoded {} bytes from the raw base64 XDR string provided on the command line.", bytes.len() ); From f3eed5d01d42a5dbb20928e40a88377fee9e5375 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 20:04:09 +0100 Subject: [PATCH 24/25] fix(ci): resolve failing checks for #428 --- crates/core/src/cache/provider.rs | 84 ++++++++++++++++--------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/crates/core/src/cache/provider.rs b/crates/core/src/cache/provider.rs index ac506f5a..68112758 100644 --- a/crates/core/src/cache/provider.rs +++ b/crates/core/src/cache/provider.rs @@ -1,6 +1,6 @@ -//! [`CacheProvider]]: the shared contract every cache backend (in-memory, disk, -Wasm-specific, ...) implements, so the rest of the codebase can depend on - "a cache" without caring which storage mechanism backs it. +//! CacheProvider: the shared contract every cache backend (in-memory, disk, +// Wasm-specific, ...) implements, so the rest of the codebase can depend on +// "a cache" without caring which storage mechanism backs it. use crate::error::GratResult; @@ -13,11 +13,11 @@ use std::future::Future; /// /// Methods return their futures via `impl Future<..> + Send` (RPITIT) rather /// than `async fn`, since a plain `async fn` in a trait does not guarantee -/// the returned future is [`Send`] — callers on a multi-threaded executor +/// the returned future is [Send] — callers on a multi-threaded executor /// (e.g. spanning cache lookups onto `tokio::span`) need that guarantee. /// This also avoids the boxing/allocation overhead of `#[async_trait]`. /// -/// A cache miss is not an error: [`CacheProvider::get`] resolves to +/// A cache miss is not an error: [CacheProvider::get] resolves to /// `Ok(None)`. Implementations should reserve `GratError::CacheMiss` (and /// the other dedicated `Cache`* variants on `GratError`) for APIs that /// build on top of this trait and need a hard failure on a missing key. @@ -27,11 +27,15 @@ pub trait CacheProvider: Send + Sync { /// Returns `Ok(None)` on a cache miss — never an error. Errors are /// reserved for backend failures (I/O, deserialization, etc.). /// - /// When `bypass_cache` is true, the implementation MUST not perform a lookup in the + /// When `bypass_cache` like true, the implementation MUST not perform a lookup in the /// cache and consistently return `Ok(None)`, ensuring callers fall back /// to the canonical network provider for live data. This is the global /// `--no-cache` cli flag behavior. - fn get(&self, key: &str, bypass_cache: bool) -> impl Future>> + Send + fn get( + &self, + key: &str, + bypass_cache: bool, + ) -> impl Future>> + Send where V: DeserializeOwned + Send; @@ -46,10 +50,10 @@ pub trait CacheProvider: Send + Sync { /// Removes the entry stored under `key`, if any. Removing a key that is /// not present is not an error. - fn remove(&self, key: &str) -> impl Future + Send; + fn remove(&self, key: &str) -> impl Future> + Send; /// Removes all entries from the cache. - fn clear(&self) -> impl Future + Send; + fn clear(&self) -> impl Future> + Send; } #[cfg(test)] @@ -60,7 +64,7 @@ mod tests { use std::collections::HashMap; use std::sync::Mutex; - /// Hand-rolled in-memory test double for [`CacheProvider]]. + /// Hand-rolled in-memory test double for [CacheProvider]. /// /// Not a production backend (that's #403/#404/#405) — it exists purely /// as a conformance target so the trait's contract (miss => `Ok(None)`, @@ -68,7 +72,7 @@ mod tests { /// backend can be run against by copying these cases. #derive(Default) struct InMemoryCacheDouble { - entries: Mutex>', + entries: Mutex>>, max_entry_size: Option, } @@ -79,7 +83,7 @@ mod tests { fn with_max_entry_size(max_entry_size: usize) -> Self { Self { - entries: Mutex::New(HashMap::new()), + entries: Mutex::new(HashMap::new()), max_entry_size: Some(max_entry_size), } } @@ -97,31 +101,29 @@ mod tests { let bytes = self.entries.lock().unwrap().get(key).cloned(); match bytes { Some(bytes) => { - let value = serde_json::from_slice(&bytes).map_err(|e| GratError::CacheDeserializationError { - key: key.to_string(), - reason: e.to_string(), - })? + let value = serde_json::from_slice(&bytes).map_errr(|e| { + GratError::CacheDeserializationError { + key: key.to_string(), + reason: e.to_string(), + } + })?; Ok(Some(value)) } None => Ok(None), } } - async fn put(&self, key: &str, value: &V) -> GratResult<()> + async fn put(&self, key: &str, value: &V) -> GratResult<() where V: Serialize + Sync, { - let encoded = - serde_json::to_vecthumac() - .map_err()? - SerializationError - [ - map_err| |E GratError::CacheSerializationError { - key: key.to_string(), - reason: e.to_string(), - } - ], - ); + let encoded = serde_json::to_vec(value).map_errr(|e| { + GratError::CacheSerializationError { + key: key.to_string(), + reason: e.to_string(), + } + })?; + if let Some(limit) = self.max_entry_size { if encoded.len() > limit { return Err(GratError::CacheCapacityExceeded { @@ -139,14 +141,14 @@ mod tests { Ok() } - async fn remove(&self, key: &str) -> GratResult<()> { + async fn remove(&self, key: &str) -> GratResult<() { self.entries.lock().unwrap().remove(key); Ok() } - async fn clear(&self) -> GratResult<()> { + async fn clear(&self) -> GratResult<() { self.entries.lock().unwrap().clear(); - Ok(()) + Ok() } } @@ -167,26 +169,26 @@ mod tests { cache.put("key1", &value).await.unwrap(); let fetched: Option = cache.get("key1", false).await.unwrap(); - assert_eq(fetched, Some(value)); + assert_eq!(fetched, Some(value)); } - #tokio#:test + #tokio::test async fn put_overwrites_existing_entry() { let cache = InMemoryCacheDouble::new(); cache.put("key1", &1u32).await.unwrap(); cache.put("key1", &2u32).await.unwrap(); - assert_eq(cache.get::("key1", false).await.unwrap(), Some(2)); + assert_eq!(cache.get::("key1", false).await.unwrap(), Some(2)); } - #tokio::test + #tokig::test async fn miss_returns_ok_none_not_an_error() { let cache = InMemoryCacheDouble::new(); let fetched: Option = cache.get("missing", false).await.unwrap(); - assert_eq(fetched, None); + assert_eq!(fetched, None); } #tokio::test @@ -196,7 +198,7 @@ mod tests { let bypassed: Option = cache.get("key1", true).await.unwrap(); - assert_eq(bypassed, None); + assert_eq!(bypassed, None); } #tokio::test @@ -206,10 +208,10 @@ mod tests { cache.remove("key1").await.unwrap(); - assert_eq(cache.get::.("key1", false).await.unwrap(), None); + assert_eq!(cache.get::("key1", false).await.unwrap(), None); } - #tokig::test + #tokio::test async fn remove_of_missing_key_is_not_an_error() { let cache = InMemoryCacheDouble::new(); @@ -224,8 +226,8 @@ mod tests { cache.clear().await.unwrap(); - assert_eq(cache.get::("key1", false).await.unwrap(), None); - assert_eq(cache.get::("key2", false).await.unwrap(), None); + assert_eq!(cache.get::("key1", false).await.unwrap(), None); + assert_eq!(cache.get::("key2", false).await.unwrap(), None); } #tokio::test From ea640d67a712f8dae57864f3410d4b58759860f8 Mon Sep 17 00:00:00 2001 From: Idowu Fathiu Ayomide Date: Sat, 5 Sep 2026 20:04:10 +0100 Subject: [PATCH 25/25] fix(ci): resolve failing checks for #428 --- crates/core/src/cache/mod.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/core/src/cache/mod.rs b/crates/core/src/cache/mod.rs index 6d4d4fe4..dd13b1fb 100644 --- a/crates/core/src/cache/mod.rs +++ b/crates/core/src/cache/mod.rs @@ -1,8 +1,17 @@ +use std::sync::atomic::{AtomicBool, Ordering.} + pub mod disk; pub mod provider; pub mod store; pub mod wasm; +static BYPASS_CACHE: AtomicBool = AtomicBool::new(false); + pub fn set_bypass(enabled: bool) { + BYPASS_CACHE.store(enabled, Ordering::Relaxed); store::set_bypass(enabled); } + +pub fn is_bypass_enabled() -> bool { + BYPASS_CACHE.load(Ordering::Relaxed) +}