#include "explorer_block_cache.h" #include "../../util/logger.h" #include "../../util/platform.h" #include #include #include #include #include #include namespace fs = std::filesystem; using json = nlohmann::json; namespace dragonx { namespace ui { namespace { constexpr int kCacheSchemaVersion = 1; struct Statement { sqlite3_stmt* handle = nullptr; Statement(sqlite3* db, const char* sql) { if (sqlite3_prepare_v2(db, sql, -1, &handle, nullptr) != SQLITE_OK) { handle = nullptr; } } ~Statement() { if (handle) sqlite3_finalize(handle); } Statement(const Statement&) = delete; Statement& operator=(const Statement&) = delete; }; bool bindText(sqlite3_stmt* statement, int index, const std::string& value) { return sqlite3_bind_text(statement, index, value.c_str(), -1, SQLITE_TRANSIENT) == SQLITE_OK; } bool blockSummaryFromJson(const json& source, ExplorerBlockSummary& block) { if (!source.is_object()) return false; try { block.height = source.value("height", 0); block.hash = source.value("hash", std::string()); block.tx_count = source.value("tx_count", 0); block.size = source.value("size", 0); block.time = source.value("time", static_cast(0)); block.difficulty = source.value("difficulty", 0.0); } catch (...) { return false; } return block.height > 0 && !block.hash.empty(); } } // namespace ExplorerBlockCache::ExplorerBlockCache() : ExplorerBlockCache(defaultDatabasePath(), defaultLegacyJsonPath()) { } ExplorerBlockCache::ExplorerBlockCache(std::string databasePath, std::string legacyJsonPath) : database_path_(std::move(databasePath)), legacy_json_path_(std::move(legacyJsonPath)) { } ExplorerBlockCache::~ExplorerBlockCache() { close(); } std::string ExplorerBlockCache::defaultDatabasePath() { return (fs::path(util::Platform::getConfigDir()) / "explorer_blocks.sqlite").string(); } std::string ExplorerBlockCache::defaultLegacyJsonPath() { return (fs::path(util::Platform::getConfigDir()) / "explorer_blocks_cache.json").string(); } bool ExplorerBlockCache::ensureOpen() { if (db_) return true; try { fs::path path(database_path_); if (!path.parent_path().empty()) fs::create_directories(path.parent_path()); } catch (const std::exception& e) { DEBUG_LOGF("Failed to create explorer cache directory: %s\n", e.what()); return false; } sqlite3* openedDb = nullptr; if (sqlite3_open(database_path_.c_str(), &openedDb) != SQLITE_OK) { DEBUG_LOGF("Failed to open explorer block cache: %s\n", openedDb ? sqlite3_errmsg(openedDb) : "unknown error"); if (openedDb) sqlite3_close(openedDb); return false; } db_ = openedDb; sqlite3_busy_timeout(db_, 2000); exec("PRAGMA journal_mode=WAL"); exec("PRAGMA synchronous=NORMAL"); if (!createSchema()) { close(); return false; } migrateLegacyJsonIfNeeded(); return true; } std::map ExplorerBlockCache::loadRange(int minHeight, int maxHeight) { std::map blocks; if (minHeight > maxHeight) std::swap(minHeight, maxHeight); if (minHeight < 1 || maxHeight < 1 || !ensureOpen()) return blocks; Statement statement(db_, "SELECT height, hash, tx_count, size, time, difficulty " "FROM explorer_blocks WHERE height BETWEEN ? AND ? ORDER BY height DESC"); if (!statement.handle) return blocks; sqlite3_bind_int(statement.handle, 1, minHeight); sqlite3_bind_int(statement.handle, 2, maxHeight); while (sqlite3_step(statement.handle) == SQLITE_ROW) { ExplorerBlockSummary block; block.height = sqlite3_column_int(statement.handle, 0); const unsigned char* hashText = sqlite3_column_text(statement.handle, 1); if (hashText) block.hash = reinterpret_cast(hashText); block.tx_count = sqlite3_column_int(statement.handle, 2); block.size = sqlite3_column_int(statement.handle, 3); block.time = static_cast(sqlite3_column_int64(statement.handle, 4)); block.difficulty = sqlite3_column_double(statement.handle, 5); if (block.height > 0 && !block.hash.empty()) { blocks[block.height] = std::move(block); } } return blocks; } std::vector ExplorerBlockCache::searchBlocks(const std::string& query, int limit) { std::vector results; if (query.empty() || limit < 1 || !ensureOpen()) return results; // Escape LIKE wildcards in the user input so '%' / '_' are matched literally. std::string escaped; escaped.reserve(query.size()); for (char c : query) { if (c == '%' || c == '_' || c == '\\') escaped.push_back('\\'); escaped.push_back(c); } std::string pattern = "%" + escaped + "%"; Statement statement(db_, "SELECT height, hash, tx_count, size, time, difficulty " "FROM explorer_blocks " "WHERE CAST(height AS TEXT) LIKE ?1 ESCAPE '\\' OR hash LIKE ?1 ESCAPE '\\' " "ORDER BY height DESC LIMIT ?2"); if (!statement.handle) return results; sqlite3_bind_text(statement.handle, 1, pattern.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(statement.handle, 2, limit); while (sqlite3_step(statement.handle) == SQLITE_ROW) { ExplorerBlockSummary block; block.height = sqlite3_column_int(statement.handle, 0); const unsigned char* hashText = sqlite3_column_text(statement.handle, 1); if (hashText) block.hash = reinterpret_cast(hashText); block.tx_count = sqlite3_column_int(statement.handle, 2); block.size = sqlite3_column_int(statement.handle, 3); block.time = static_cast(sqlite3_column_int64(statement.handle, 4)); block.difficulty = sqlite3_column_double(statement.handle, 5); if (block.height > 0 && !block.hash.empty()) results.push_back(std::move(block)); } return results; } bool ExplorerBlockCache::storeBlock(const ExplorerBlockSummary& block) { if (block.height < 1 || block.hash.empty() || !ensureOpen()) return false; Statement statement(db_, "INSERT OR REPLACE INTO explorer_blocks " "(height, hash, tx_count, size, time, difficulty) VALUES (?, ?, ?, ?, ?, ?)"); if (!statement.handle) return false; sqlite3_bind_int(statement.handle, 1, block.height); if (!bindText(statement.handle, 2, block.hash)) return false; sqlite3_bind_int(statement.handle, 3, block.tx_count); sqlite3_bind_int(statement.handle, 4, block.size); sqlite3_bind_int64(statement.handle, 5, static_cast(block.time)); sqlite3_bind_double(statement.handle, 6, block.difficulty); return sqlite3_step(statement.handle) == SQLITE_DONE; } int ExplorerBlockCache::cachedBlockCount() { if (!ensureOpen()) return 0; Statement statement(db_, "SELECT COUNT(*) FROM explorer_blocks"); if (!statement.handle || sqlite3_step(statement.handle) != SQLITE_ROW) return 0; return sqlite3_column_int(statement.handle, 0); } void ExplorerBlockCache::clearBlocks() { if (!ensureOpen()) return; exec("DELETE FROM explorer_blocks"); } ExplorerBlockCache::SavedTipValidation ExplorerBlockCache::prepareValidation( int currentHeight, const std::string& currentBestHash) { SavedTipValidation validation; if (currentHeight <= 0 || !ensureOpen()) return validation; int savedHeight = getMetaInt("tip_height", 0); std::string savedHash = getMetaValue("tip_hash"); if (savedHeight <= 0 || savedHash.empty()) { if (!currentBestHash.empty()) updateTip(currentHeight, currentBestHash); return validation; } if (currentHeight < savedHeight) { clearBlocks(); if (!currentBestHash.empty()) updateTip(currentHeight, currentBestHash); else updateTip(0, std::string()); return validation; } if (currentHeight == savedHeight) { if (currentBestHash.empty()) return validation; if (currentBestHash != savedHash) clearBlocks(); updateTip(currentHeight, currentBestHash); return validation; } if (currentBestHash.empty()) return validation; validation.needed = true; validation.height = savedHeight; validation.expectedHash = savedHash; return validation; } void ExplorerBlockCache::applySavedTipValidation(const SavedTipValidation& validation, const std::string& actualHash, int currentHeight, const std::string& currentBestHash) { if (!validation.needed || !ensureOpen()) return; if (actualHash.empty()) return; if (actualHash != validation.expectedHash) { clearBlocks(); } if (currentHeight > 0 && !currentBestHash.empty()) { updateTip(currentHeight, currentBestHash); } } void ExplorerBlockCache::updateTip(int height, const std::string& hash) { if (!ensureOpen()) return; setMetaValue("tip_height", std::to_string(std::max(0, height))); setMetaValue("tip_hash", hash); } bool ExplorerBlockCache::exec(const char* sql) { if (!db_) return false; char* error = nullptr; int result = sqlite3_exec(db_, sql, nullptr, nullptr, &error); if (result != SQLITE_OK) { DEBUG_LOGF("Explorer block cache SQL error: %s\n", error ? error : sqlite3_errmsg(db_)); if (error) sqlite3_free(error); return false; } return true; } std::string ExplorerBlockCache::getMetaValue(const std::string& key) { if (!ensureOpen()) return {}; Statement statement(db_, "SELECT value FROM explorer_cache_meta WHERE key = ?"); if (!statement.handle) return {}; if (!bindText(statement.handle, 1, key)) return {}; if (sqlite3_step(statement.handle) != SQLITE_ROW) return {}; const unsigned char* valueText = sqlite3_column_text(statement.handle, 0); return valueText ? reinterpret_cast(valueText) : std::string(); } int ExplorerBlockCache::getMetaInt(const std::string& key, int fallback) { std::string value = getMetaValue(key); if (value.empty()) return fallback; try { return std::stoi(value); } catch (...) { return fallback; } } void ExplorerBlockCache::setMetaValue(const std::string& key, const std::string& value) { if (!ensureOpen()) return; Statement statement(db_, "INSERT OR REPLACE INTO explorer_cache_meta (key, value) VALUES (?, ?)"); if (!statement.handle) return; if (!bindText(statement.handle, 1, key)) return; if (!bindText(statement.handle, 2, value)) return; sqlite3_step(statement.handle); } bool ExplorerBlockCache::createSchema() { return exec("CREATE TABLE IF NOT EXISTS explorer_blocks (" "height INTEGER PRIMARY KEY," "hash TEXT NOT NULL," "tx_count INTEGER NOT NULL," "size INTEGER NOT NULL," "time INTEGER NOT NULL," "difficulty REAL NOT NULL)") && exec("CREATE TABLE IF NOT EXISTS explorer_cache_meta (" "key TEXT PRIMARY KEY," "value TEXT NOT NULL)") && exec("INSERT OR IGNORE INTO explorer_cache_meta (key, value) VALUES " "('schema_version', '1')"); } void ExplorerBlockCache::migrateLegacyJsonIfNeeded() { if (!db_ || getMetaValue("json_migrated") == "1") return; bool migrated = false; try { if (!legacy_json_path_.empty() && fs::exists(legacy_json_path_)) { std::ifstream file(legacy_json_path_); json cache; file >> cache; if (cache.is_object() && cache.value("version", 0) == kCacheSchemaVersion) { exec("BEGIN IMMEDIATE TRANSACTION"); if (cache.contains("blocks") && cache["blocks"].is_array()) { for (const auto& entry : cache["blocks"]) { ExplorerBlockSummary block; if (blockSummaryFromJson(entry, block)) { storeBlock(block); } } } exec("COMMIT"); int tipHeight = cache.value("tip_height", 0); std::string tipHash = cache.value("tip_hash", std::string()); if (tipHeight > 0 && !tipHash.empty()) { updateTip(tipHeight, tipHash); } } } migrated = true; } catch (const std::exception& e) { exec("ROLLBACK"); DEBUG_LOGF("Failed to migrate explorer JSON cache: %s\n", e.what()); migrated = true; } if (migrated) setMetaValue("json_migrated", "1"); } void ExplorerBlockCache::close() { if (!db_) return; sqlite3_close(db_); db_ = nullptr; } } // namespace ui } // namespace dragonx