Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/cli/src/commands/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub async fn run(args: DbArgs, output_format: &str) -> anyhow::Result<()> {
match args.command {
DbCommands::Update => update_taxonomy_database(output_format).await?,
DbCommands::Stats => {
let db = grat_core::taxonomy::loader::TaxonomyDatabase::load_embedded()?;
let db = grat_core::taxonomy::loader::TaxonomyDatabase::load_latest()?;
if matches!(
crate::output::OutputFormat::parse(output_format),
crate::output::OutputFormat::Json
Expand Down Expand Up @@ -63,7 +63,7 @@ async fn update_taxonomy_database(output_format: &str) -> Result<()> {
crate::output::OutputFormat::Json
) {
tokio::time::sleep(Duration::from_secs(1)).await;
let db = grat_core::taxonomy::loader::TaxonomyDatabase::load_embedded()
let db = grat_core::taxonomy::loader::TaxonomyDatabase::load_latest()
.context("Failed to load updated taxonomy database")?;
let payload = serde_json::json!({
"status": "ok",
Expand Down Expand Up @@ -100,7 +100,7 @@ async fn update_taxonomy_database(output_format: &str) -> Result<()> {

spinner.finish_with_message("✅ Taxonomy database updated successfully!");

let db = grat_core::taxonomy::loader::TaxonomyDatabase::load_embedded()
let db = grat_core::taxonomy::loader::TaxonomyDatabase::load_latest()
.context("Failed to load updated taxonomy database")?;
println!("📊 Database now contains {} error definitions", db.len());

Expand Down
6 changes: 6 additions & 0 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ struct Cli {

#[arg(long, global = true)]
no_color: bool,

#[arg(long, global = true, help = "Disable network requests for updates")]
offline: bool,
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -88,6 +91,9 @@ async fn main() -> anyhow::Result<()> {
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()
.and_then(|manager| manager.load())
.ok();
Expand Down
4 changes: 2 additions & 2 deletions crates/core/src/decode/chain_analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ impl ChainAnalyzer {
// Detect failure
// ----------------------------------------------------------------
if is_failure(event, &topics, &v0.data) {
return Some(self.build_chain(&stack, event));
return Some(Self::build_chain(&stack, event));
}
}

Expand All @@ -251,7 +251,7 @@ impl ChainAnalyzer {

/// Assemble a [`CallChain`] from the current stack snapshot and the event
/// that triggered the failure detection.
fn build_chain(&self, stack: &[StackFrame], failing_event: &DiagnosticEvent) -> CallChain {
fn build_chain(stack: &[StackFrame], failing_event: &DiagnosticEvent) -> CallChain {
if stack.is_empty() {
// Failure occurred before any fn_call was seen - synthesize a
// single-frame chain from the event's own contract_id.
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/decode/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::taxonomy::loader::TaxonomyDatabase;
use crate::types::report::{DiagnosticReport, RootCause, Severity, SuggestedFix};

pub fn build_report(error: &ClassifiedError) -> GratResult<DiagnosticReport> {
let db = TaxonomyDatabase::load_embedded()?;
let db = TaxonomyDatabase::load_latest()?;

if let Some(entry) = db.lookup(&error.category, error.error_code) {
let report = DiagnosticReport {
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/replay/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub struct SandboxResult {
/// tracing is never cut short by an artificial memory limit.
const MEM_BYTES_CEILING: u64 = 500 * 1024 * 1024;

#[allow(clippy::too_many_lines)]
pub async fn execute_with_tracing(state: &LedgerState, tx_hash: &str) -> GratResult<SandboxResult> {
let inv = &state.invocation;

Expand Down
1 change: 1 addition & 0 deletions crates/core/src/replay/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ fn parse_protocol_version(value: Option<&serde_json::Value>) -> u32 {
.unwrap_or(0) as u32
}

#[allow(clippy::too_many_lines)]
async fn reconstruct_hot_path(
ledger_sequence: u32,
protocol_version: u32,
Expand Down
31 changes: 31 additions & 0 deletions crates/core/src/taxonomy/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,37 @@ pub struct TaxonomyDatabase {
}

impl TaxonomyDatabase {
pub fn load_latest() -> GratResult<Self> {
if let Some(db_path) = crate::taxonomy::updater::db_file_path() {
if db_path.exists() {
if db_path.is_dir() {
if let Ok(db) = Self::load_from_dir(&db_path) {
return Ok(db);
}
} else if let Ok(content) = std::fs::read_to_string(&db_path) {
if let Ok(schema) = TaxonomyParser::parse(&content) {
let mut db = Self {
entries: HashMap::new(),
all_entries: Vec::new(),
};
for entry in schema.errors {
db.entries
.insert((entry.category.clone(), entry.code), entry.clone());
db.all_entries.push(entry);
}
tracing::info!(
"Loaded {} taxonomy entries from downloaded file",
db.entries.len()
);
return Ok(db);
}
}
}
}

Self::load_embedded()
}

pub fn load_embedded() -> GratResult<Self> {
let mut db = Self {
entries: HashMap::new(),
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/taxonomy/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod linter;
pub mod loader;
pub mod schema;
pub mod updater;
117 changes: 117 additions & 0 deletions crates/core/src/taxonomy/updater.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use crate::error::GratResult;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::time::{Duration, SystemTime};

const TAXONOMY_URL: &str = "https://raw.githubusercontent.com/grat-soroban/grat/main/taxonomy/enhanced_error_taxonomy.toml";

#[derive(Serialize, Deserialize)]
struct UpdateCache {
last_check: SystemTime,
version: Option<String>,
}

pub fn cache_file_path() -> Option<PathBuf> {
directories::ProjectDirs::from("", "", "grat")
.map(|proj_dirs| proj_dirs.cache_dir().join("taxonomy_update.json"))
}

pub fn db_file_path() -> Option<PathBuf> {
directories::ProjectDirs::from("", "", "grat")
.map(|proj_dirs| proj_dirs.data_dir().join("taxonomy").join("database.toml"))
}

pub async fn check_and_update(offline: bool) -> GratResult<()> {
if offline {
tracing::debug!("Offline mode enabled, skipping taxonomy update check");
return Ok(());
}

let Some(cache_path) = cache_file_path() else {
return Ok(());
};

if let Ok(content) = fs::read_to_string(&cache_path) {
if let Ok(cache) = serde_json::from_str::<UpdateCache>(&content) {
if let Ok(elapsed) = cache.last_check.elapsed() {
if elapsed.as_secs() < 24 * 60 * 60 {
tracing::debug!("Taxonomy update checked recently, skipping");
return Ok(());
}
}
}
}

let client = match reqwest::Client::builder()
.user_agent("grat-taxonomy-updater")
.timeout(Duration::from_secs(10))
.build()
{
Ok(c) => c,
Err(e) => {
tracing::warn!("Failed to build reqwest client for taxonomy update: {e}");
return Ok(());
}
};

tracing::debug!("Checking for latest taxonomy version");

let response = match client.get(TAXONOMY_URL).send().await {
Ok(r) => r,
Err(e) => {
tracing::warn!("Failed to download taxonomy database: {e}");
return Ok(());
}
};

if !response.status().is_success() {
tracing::warn!(
"Failed to download taxonomy database: HTTP {}",
response.status()
);
return Ok(());
}

let content = match response.text().await {
Ok(t) => t,
Err(e) => {
tracing::warn!("Failed to read taxonomy database response: {e}");
return Ok(());
}
};

let Some(db_path) = db_file_path() else {
return Ok(());
};

if let Some(parent) = db_path.parent() {
let _ = fs::create_dir_all(parent);
}

// Atomically replace the database file
let temp_path = db_path.with_extension("tmp");
if fs::write(&temp_path, &content).is_ok() {
if fs::rename(&temp_path, &db_path).is_err() {
tracing::warn!("Failed to replace taxonomy database atomically");
let _ = fs::remove_file(temp_path);
} else {
tracing::info!("Taxonomy database updated successfully");
}
}

if let Some(parent) = cache_path.parent() {
let _ = fs::create_dir_all(parent);
}

let new_cache = UpdateCache {
last_check: SystemTime::now(),
version: None, // We don't have versioning for raw file yet, just timestamp cache
};

if let Ok(serialized) = serde_json::to_string(&new_cache) {
let _ = fs::write(&cache_path, serialized);
}

Ok(())
}
Loading