Explorer tab:
- New block explorer tab with search, chain stats, mempool info,
recent blocks table, block detail modal with tx expansion
- Sidebar nav entry, i18n strings, ui.toml layout values
Bootstrap fixes:
- Move wizard Done handler into render() — was dead code, preventing
startEmbeddedDaemon() and tryConnect() from firing post-wizard
- Stop deleting BDB database/ dir during cleanup — caused LSN mismatch
that salvaged wallet.dat into wallet.{timestamp}.bak
- Add banlist.dat, db.log, .lock to cleanup file list
- Fatal extraction failure for blocks/ and chainstate/ files
- Verification progress: split SHA-256 (0-50%) and MD5 (50-100%)
Theme system:
- Expand overlay merge to apply ALL sections (tabs, dialogs, components,
screens, flat sections), not just theme+backdrop+effects
- Add screens and security section parsing to UISchema
- Build-time theme expansion via expand_themes.py (CMake + build.sh)
Other:
- Version bump to 1.1.0
- WalletState::clear() resets all fields (sync, daemon info, etc.)
- Sidebar item-height 42 → 36
1048 lines
41 KiB
C++
1048 lines
41 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "explorer_tab.h"
|
|
#include "../../app.h"
|
|
#include "../../rpc/rpc_client.h"
|
|
#include "../../rpc/rpc_worker.h"
|
|
#include "../../data/wallet_state.h"
|
|
#include "../../util/i18n.h"
|
|
#include "../schema/ui_schema.h"
|
|
#include "../material/type.h"
|
|
#include "../material/draw_helpers.h"
|
|
#include "../material/colors.h"
|
|
#include "../layout.h"
|
|
#include "../notifications.h"
|
|
#include "../../embedded/IconsMaterialDesign.h"
|
|
#include "imgui.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <ctime>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <atomic>
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
|
|
using json = nlohmann::json;
|
|
using namespace material;
|
|
|
|
// ─── Static state ───────────────────────────────────────────────
|
|
|
|
// Search
|
|
static char s_search_buf[128] = {};
|
|
static bool s_search_loading = false;
|
|
static std::string s_search_error;
|
|
|
|
// Recent blocks cache
|
|
struct BlockSummary {
|
|
int height = 0;
|
|
std::string hash;
|
|
int tx_count = 0;
|
|
int size = 0;
|
|
int64_t time = 0;
|
|
double difficulty = 0.0;
|
|
};
|
|
static std::vector<BlockSummary> s_recent_blocks;
|
|
static int s_last_known_height = 0;
|
|
static std::atomic<int> s_pending_block_fetches{0};
|
|
|
|
// Block detail modal
|
|
static bool s_show_detail_modal = false;
|
|
static bool s_detail_loading = false;
|
|
static int s_detail_height = 0;
|
|
static std::string s_detail_hash;
|
|
static int64_t s_detail_time = 0;
|
|
static int s_detail_confirmations = 0;
|
|
static int s_detail_size = 0;
|
|
static double s_detail_difficulty = 0.0;
|
|
static std::string s_detail_bits;
|
|
static std::string s_detail_merkle;
|
|
static std::string s_detail_prev_hash;
|
|
static std::string s_detail_next_hash;
|
|
static std::vector<std::string> s_detail_txids;
|
|
|
|
// Expanded transaction detail
|
|
static int s_expanded_tx_idx = -1;
|
|
static bool s_tx_loading = false;
|
|
static json s_tx_detail;
|
|
|
|
// Mempool info
|
|
static int s_mempool_tx_count = 0;
|
|
static int64_t s_mempool_size = 0;
|
|
static bool s_mempool_loading = false;
|
|
|
|
// ─── Helpers ────────────────────────────────────────────────────
|
|
|
|
static const char* relativeTime(int64_t timestamp) {
|
|
static char buf[64];
|
|
int64_t now = std::time(nullptr);
|
|
int64_t diff = now - timestamp;
|
|
if (diff < 0) diff = 0;
|
|
if (diff < 60)
|
|
snprintf(buf, sizeof(buf), "%lld sec ago", (long long)diff);
|
|
else if (diff < 3600)
|
|
snprintf(buf, sizeof(buf), "%lld min ago", (long long)(diff / 60));
|
|
else if (diff < 86400)
|
|
snprintf(buf, sizeof(buf), "%lld hr ago", (long long)(diff / 3600));
|
|
else
|
|
snprintf(buf, sizeof(buf), "%lld days ago", (long long)(diff / 86400));
|
|
return buf;
|
|
}
|
|
|
|
static std::string truncateHash(const std::string& hash, int front, int back) {
|
|
if (hash.length() <= static_cast<size_t>(front + back + 3))
|
|
return hash;
|
|
return hash.substr(0, front) + "..." + hash.substr(hash.length() - back);
|
|
}
|
|
|
|
// Adaptive hash truncation based on available pixel width
|
|
static std::string truncateHashToFit(const std::string& hash, ImFont* font, float maxWidth) {
|
|
ImVec2 fullSz = font->CalcTextSizeA(font->LegacySize, 10000.0f, 0.0f, hash.c_str());
|
|
if (fullSz.x <= maxWidth) return hash;
|
|
// Binary search for the right truncation
|
|
int front = 6, back = 4;
|
|
for (int f = (int)hash.size() / 2; f >= 4; f -= 2) {
|
|
int b = std::max(4, f - 2);
|
|
std::string t = hash.substr(0, f) + "..." + hash.substr(hash.size() - b);
|
|
ImVec2 sz = font->CalcTextSizeA(font->LegacySize, 10000.0f, 0.0f, t.c_str());
|
|
if (sz.x <= maxWidth) { front = f; back = b; break; }
|
|
}
|
|
return truncateHash(hash, front, back);
|
|
}
|
|
|
|
static std::string formatSize(int bytes) {
|
|
char b[32];
|
|
if (bytes >= 1048576)
|
|
snprintf(b, sizeof(b), "%.2f MB", bytes / (1024.0 * 1024.0));
|
|
else if (bytes >= 1024)
|
|
snprintf(b, sizeof(b), "%.2f KB", bytes / 1024.0);
|
|
else
|
|
snprintf(b, sizeof(b), "%d B", bytes);
|
|
return b;
|
|
}
|
|
|
|
// Copy icon button — draws a small icon that copies text to clipboard on click
|
|
static void copyButton(const char* id, const std::string& text, float x, float y) {
|
|
ImFont* iconFont = Type().iconSmall();
|
|
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 100.0f, 0.0f, ICON_MD_CONTENT_COPY);
|
|
float btnSize = iconSz.x + Layout::spacingSm() * 2;
|
|
ImGui::SetCursorScreenPos(ImVec2(x, y));
|
|
ImGui::PushID(id);
|
|
ImGui::InvisibleButton("##copy", ImVec2(btnSize, iconSz.y + Layout::spacingXs()));
|
|
bool hovered = ImGui::IsItemHovered();
|
|
bool clicked = ImGui::IsItemClicked(0);
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
dl->AddText(iconFont, iconFont->LegacySize,
|
|
ImVec2(x + Layout::spacingSm(), y),
|
|
hovered ? Primary() : OnSurfaceMedium(), ICON_MD_CONTENT_COPY);
|
|
if (hovered) ImGui::SetTooltip("%s", TR("click_to_copy"));
|
|
if (clicked) {
|
|
ImGui::SetClipboardText(text.c_str());
|
|
Notifications::instance().success(TR("copied_to_clipboard"));
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
|
|
// Draw a label: value pair on one line inside a card
|
|
static void drawLabelValue(ImDrawList* dl, float x, float y, float labelW,
|
|
const char* label, const char* value,
|
|
ImFont* labelFont, ImFont* valueFont) {
|
|
dl->AddText(labelFont, labelFont->LegacySize, ImVec2(x, y), OnSurfaceMedium(), label);
|
|
dl->AddText(valueFont, valueFont->LegacySize, ImVec2(x + labelW, y), OnSurface(), value);
|
|
}
|
|
|
|
// ─── Async RPC Helpers ──────────────────────────────────────────
|
|
// All RPC work is dispatched to the RPCWorker background thread.
|
|
// Results are delivered back to the UI thread via MainCb lambdas.
|
|
|
|
static void applyBlockDetailResult(const json& result, const std::string& error) {
|
|
s_detail_loading = false;
|
|
s_search_loading = false;
|
|
|
|
if (!error.empty()) {
|
|
s_search_error = error;
|
|
s_show_detail_modal = false;
|
|
return;
|
|
}
|
|
|
|
if (result.is_null()) {
|
|
s_search_error = "Invalid response from daemon";
|
|
s_show_detail_modal = false;
|
|
return;
|
|
}
|
|
|
|
s_detail_height = result.value("height", 0);
|
|
s_detail_hash = result.value("hash", "");
|
|
s_detail_time = result.value("time", (int64_t)0);
|
|
s_detail_confirmations = result.value("confirmations", 0);
|
|
s_detail_size = result.value("size", 0);
|
|
s_detail_difficulty = result.value("difficulty", 0.0);
|
|
s_detail_bits = result.value("bits", "");
|
|
s_detail_merkle = result.value("merkleroot", "");
|
|
s_detail_prev_hash = result.value("previousblockhash", "");
|
|
s_detail_next_hash = result.value("nextblockhash", "");
|
|
|
|
s_detail_txids.clear();
|
|
if (result.contains("tx") && result["tx"].is_array()) {
|
|
for (const auto& tx : result["tx"]) {
|
|
if (tx.is_string()) s_detail_txids.push_back(tx.get<std::string>());
|
|
}
|
|
}
|
|
|
|
s_show_detail_modal = true;
|
|
s_expanded_tx_idx = -1;
|
|
s_search_error.clear();
|
|
}
|
|
|
|
static void fetchBlockDetail(App* app, int height) {
|
|
auto* worker = app->worker();
|
|
auto* rpc = app->rpc();
|
|
if (!worker || !rpc) return;
|
|
s_detail_loading = true;
|
|
s_search_loading = true;
|
|
s_search_error.clear();
|
|
worker->post([rpc, height]() -> rpc::RPCWorker::MainCb {
|
|
json result;
|
|
std::string error;
|
|
try {
|
|
auto hashResult = rpc->call("getblockhash", {height});
|
|
std::string hash = hashResult.get<std::string>();
|
|
result = rpc->call("getblock", {hash});
|
|
} catch (const std::exception& e) { error = e.what(); }
|
|
return [result, error]() {
|
|
applyBlockDetailResult(result, error);
|
|
};
|
|
});
|
|
}
|
|
|
|
static void fetchBlockDetailByHash(App* app, const std::string& hash) {
|
|
auto* worker = app->worker();
|
|
auto* rpc = app->rpc();
|
|
if (!worker || !rpc) return;
|
|
s_detail_loading = true;
|
|
s_search_loading = true;
|
|
s_search_error.clear();
|
|
worker->post([rpc, hash]() -> rpc::RPCWorker::MainCb {
|
|
json blockResult;
|
|
json txResult;
|
|
std::string blockErr;
|
|
bool gotBlock = false;
|
|
bool gotTx = false;
|
|
// Try as block hash first
|
|
try {
|
|
blockResult = rpc->call("getblock", {hash});
|
|
gotBlock = true;
|
|
} catch (...) {}
|
|
// If not a block hash, try as txid
|
|
if (!gotBlock) {
|
|
try {
|
|
txResult = rpc->call("getrawtransaction", {hash, 1});
|
|
gotTx = true;
|
|
} catch (...) {}
|
|
}
|
|
return [blockResult, txResult, gotBlock, gotTx]() {
|
|
if (gotBlock) {
|
|
applyBlockDetailResult(blockResult, "");
|
|
} else if (gotTx && !txResult.is_null()) {
|
|
s_detail_loading = false;
|
|
s_search_loading = false;
|
|
s_tx_detail = txResult;
|
|
s_tx_loading = false;
|
|
s_expanded_tx_idx = 0;
|
|
s_search_error.clear();
|
|
s_show_detail_modal = false;
|
|
} else {
|
|
s_detail_loading = false;
|
|
s_search_loading = false;
|
|
s_search_error = "No block or transaction found for this hash";
|
|
s_show_detail_modal = false;
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
static void fetchRecentBlocks(App* app, int currentHeight, int count = 10) {
|
|
auto* worker = app->worker();
|
|
auto* rpc = app->rpc();
|
|
if (!worker || !rpc || s_pending_block_fetches > 0) return;
|
|
|
|
s_recent_blocks.clear();
|
|
s_recent_blocks.resize(count);
|
|
s_pending_block_fetches = 1; // single batched fetch
|
|
|
|
worker->post([rpc, currentHeight, count]() -> rpc::RPCWorker::MainCb {
|
|
std::vector<BlockSummary> results(count);
|
|
for (int i = 0; i < count; i++) {
|
|
int h = currentHeight - i;
|
|
if (h < 1) continue;
|
|
try {
|
|
auto hashResult = rpc->call("getblockhash", {h});
|
|
auto hash = hashResult.get<std::string>();
|
|
auto result = rpc->call("getblock", {hash});
|
|
auto& bs = results[i];
|
|
bs.height = result.value("height", 0);
|
|
bs.hash = result.value("hash", "");
|
|
bs.time = result.value("time", (int64_t)0);
|
|
bs.size = result.value("size", 0);
|
|
bs.difficulty = result.value("difficulty", 0.0);
|
|
if (result.contains("tx") && result["tx"].is_array())
|
|
bs.tx_count = static_cast<int>(result["tx"].size());
|
|
} catch (...) {}
|
|
}
|
|
return [results]() {
|
|
s_recent_blocks = results;
|
|
s_pending_block_fetches = 0;
|
|
};
|
|
});
|
|
}
|
|
|
|
static void fetchMempoolInfo(App* app) {
|
|
auto* worker = app->worker();
|
|
auto* rpc = app->rpc();
|
|
if (!worker || !rpc || s_mempool_loading) return;
|
|
s_mempool_loading = true;
|
|
worker->post([rpc]() -> rpc::RPCWorker::MainCb {
|
|
int txCount = 0;
|
|
int64_t bytes = 0;
|
|
try {
|
|
auto result = rpc->call("getmempoolinfo", json::array());
|
|
txCount = result.value("size", 0);
|
|
bytes = result.value("bytes", (int64_t)0);
|
|
} catch (...) {}
|
|
return [txCount, bytes]() {
|
|
s_mempool_tx_count = txCount;
|
|
s_mempool_size = bytes;
|
|
s_mempool_loading = false;
|
|
};
|
|
});
|
|
}
|
|
|
|
static void fetchTxDetail(App* app, const std::string& txid) {
|
|
auto* worker = app->worker();
|
|
auto* rpc = app->rpc();
|
|
if (!worker || !rpc) return;
|
|
s_tx_loading = true;
|
|
s_tx_detail = json();
|
|
worker->post([rpc, txid]() -> rpc::RPCWorker::MainCb {
|
|
json result;
|
|
try { result = rpc->call("getrawtransaction", {txid, 1}); }
|
|
catch (...) {}
|
|
return [result]() {
|
|
s_tx_loading = false;
|
|
s_tx_detail = result;
|
|
};
|
|
});
|
|
}
|
|
|
|
static void performSearch(App* app, const std::string& query) {
|
|
if (query.empty()) return;
|
|
|
|
// Integer = block height
|
|
bool isNumeric = std::all_of(query.begin(), query.end(), ::isdigit);
|
|
if (isNumeric && !query.empty()) {
|
|
int height = std::stoi(query);
|
|
fetchBlockDetail(app, height);
|
|
return;
|
|
}
|
|
|
|
// 64-char hex = block hash or txid
|
|
bool isHex64 = query.size() == 64 &&
|
|
std::all_of(query.begin(), query.end(), [](char c) {
|
|
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
|
});
|
|
if (isHex64) {
|
|
fetchBlockDetailByHash(app, query);
|
|
return;
|
|
}
|
|
|
|
s_search_error = TR("explorer_invalid_query");
|
|
}
|
|
|
|
// ─── Render Sections ────────────────────────────────────────────
|
|
|
|
static void renderSearchBar(App* app, float availWidth) {
|
|
auto& S = schema::UI();
|
|
float pad = Layout::cardInnerPadding();
|
|
|
|
float inputW = std::min(
|
|
S.drawElement("tabs.explorer", "search-input-width").size,
|
|
availWidth * 0.65f);
|
|
float btnW = S.drawElement("tabs.explorer", "search-button-width").size;
|
|
float barH = S.drawElement("tabs.explorer", "search-bar-height").size;
|
|
|
|
// Clamp so search bar never overflows
|
|
float maxInputW = availWidth - btnW - pad * 3 - Type().iconMed()->LegacySize;
|
|
if (inputW > maxInputW) inputW = maxInputW;
|
|
if (inputW < 80.0f) inputW = 80.0f;
|
|
|
|
ImGui::Spacing();
|
|
|
|
// Icon
|
|
ImGui::PushFont(Type().iconMed());
|
|
float iconH = Type().iconMed()->LegacySize;
|
|
float cursorY = ImGui::GetCursorScreenPos().y;
|
|
// Vertically center icon with input
|
|
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x, cursorY + (barH - iconH) * 0.5f));
|
|
ImGui::TextUnformatted(ICON_MD_SEARCH);
|
|
ImGui::PopFont();
|
|
ImGui::SameLine();
|
|
|
|
// Input — match height to barH
|
|
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x, cursorY));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
|
|
ImVec2(ImGui::GetStyle().FramePadding.x, (barH - ImGui::GetFontSize()) * 0.5f));
|
|
ImGui::SetNextItemWidth(inputW);
|
|
bool enter = ImGui::InputText("##ExplorerSearch", s_search_buf, sizeof(s_search_buf),
|
|
ImGuiInputTextFlags_EnterReturnsTrue);
|
|
ImGui::PopStyleVar();
|
|
|
|
ImGui::SameLine();
|
|
|
|
// Search button — same height as input
|
|
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x, cursorY));
|
|
bool clicked = material::StyledButton(TR("explorer_search"),
|
|
ImVec2(btnW, barH));
|
|
|
|
if ((enter || clicked) && app->rpc() && app->rpc()->isConnected()) {
|
|
std::string query(s_search_buf);
|
|
while (!query.empty() && query.front() == ' ') query.erase(query.begin());
|
|
while (!query.empty() && query.back() == ' ') query.pop_back();
|
|
performSearch(app, query);
|
|
}
|
|
|
|
// Loading spinner
|
|
if (s_search_loading) {
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("%s", TR("loading"));
|
|
}
|
|
|
|
// Error
|
|
if (!s_search_error.empty()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
|
|
ImGui::TextWrapped("%s", s_search_error.c_str());
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
}
|
|
|
|
static void renderChainStats(App* app, float availWidth) {
|
|
const auto& state = app->getWalletState();
|
|
float pad = Layout::cardInnerPadding();
|
|
float gap = Layout::cardGap();
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
GlassPanelSpec glassSpec;
|
|
glassSpec.rounding = Layout::glassRounding();
|
|
|
|
ImFont* ovFont = Type().overline();
|
|
ImFont* capFont = Type().caption();
|
|
ImFont* sub1 = Type().subtitle1();
|
|
|
|
float cardW = (availWidth - gap) * 0.5f;
|
|
float rowH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize;
|
|
float headerH = ovFont->LegacySize + Layout::spacingSm();
|
|
float cardH = pad * 0.5f + headerH + rowH * 3 + Layout::spacingSm() * 2 + pad * 0.5f;
|
|
|
|
ImVec2 basePos = ImGui::GetCursorScreenPos();
|
|
char buf[128];
|
|
|
|
// ── Chain Card (left) ──
|
|
{
|
|
ImVec2 cardMin = basePos;
|
|
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
dl->AddText(ovFont, ovFont->LegacySize,
|
|
ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_chain_stats"));
|
|
|
|
float colW = (cardW - pad * 2) / 2.0f;
|
|
float ry = cardMin.y + pad * 0.5f + headerH;
|
|
|
|
// Row 1: Height | Difficulty
|
|
{
|
|
float cx = cardMin.x + pad;
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("explorer_block_height"));
|
|
snprintf(buf, sizeof(buf), "%d", state.sync.blocks);
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
|
|
|
cx = cardMin.x + pad + colW;
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("difficulty"));
|
|
snprintf(buf, sizeof(buf), "%.4f", state.mining.difficulty);
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
|
}
|
|
ry += rowH + Layout::spacingSm();
|
|
|
|
// Row 2: Hashrate | Notarized
|
|
{
|
|
float cx = cardMin.x + pad;
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_hashrate"));
|
|
double hr = state.mining.networkHashrate;
|
|
if (hr >= 1e9)
|
|
snprintf(buf, sizeof(buf), "%.2f GH/s", hr / 1e9);
|
|
else if (hr >= 1e6)
|
|
snprintf(buf, sizeof(buf), "%.2f MH/s", hr / 1e6);
|
|
else if (hr >= 1e3)
|
|
snprintf(buf, sizeof(buf), "%.2f KH/s", hr / 1e3);
|
|
else
|
|
snprintf(buf, sizeof(buf), "%.0f H/s", hr);
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
|
|
|
cx = cardMin.x + pad + colW;
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_notarized"));
|
|
snprintf(buf, sizeof(buf), "%d", state.notarized);
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
|
}
|
|
ry += rowH + Layout::spacingSm();
|
|
|
|
// Row 3: Best Block Hash — adaptive truncation to fit card
|
|
{
|
|
float cx = cardMin.x + pad;
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_best_block"));
|
|
float maxHashW = cardW - pad * 2;
|
|
std::string hashDisp = truncateHashToFit(state.sync.best_blockhash, sub1, maxHashW);
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), hashDisp.c_str());
|
|
}
|
|
}
|
|
|
|
// ── Mempool Card (right) ──
|
|
{
|
|
ImVec2 cardMin(basePos.x + cardW + gap, basePos.y);
|
|
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
dl->AddText(ovFont, ovFont->LegacySize,
|
|
ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_mempool"));
|
|
|
|
float ry = cardMin.y + pad * 0.5f + headerH;
|
|
|
|
// Row 1: Transactions
|
|
{
|
|
float cx = cardMin.x + pad;
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("explorer_mempool_txs"));
|
|
snprintf(buf, sizeof(buf), "%d", s_mempool_tx_count);
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
|
}
|
|
ry += rowH + Layout::spacingSm();
|
|
|
|
// Row 2: Size
|
|
{
|
|
float cx = cardMin.x + pad;
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("explorer_mempool_size"));
|
|
std::string sizeStr = formatSize(static_cast<int>(s_mempool_size));
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), sizeStr.c_str());
|
|
}
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(availWidth, cardH + Layout::spacingMd()));
|
|
}
|
|
|
|
static void renderRecentBlocks(App* app, float availWidth) {
|
|
auto& S = schema::UI();
|
|
float pad = Layout::cardInnerPadding();
|
|
float dp = Layout::dpiScale();
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
GlassPanelSpec glassSpec;
|
|
glassSpec.rounding = Layout::glassRounding();
|
|
|
|
ImFont* ovFont = Type().overline();
|
|
ImFont* capFont = Type().caption();
|
|
ImFont* body2 = Type().body2();
|
|
ImFont* sub1 = Type().subtitle1();
|
|
|
|
float rowH = S.drawElement("tabs.explorer", "row-height").size;
|
|
float rowRound = S.drawElement("tabs.explorer", "row-rounding").size;
|
|
float headerH = ovFont->LegacySize + Layout::spacingSm() + pad * 0.5f;
|
|
|
|
// Filter out empty entries
|
|
std::vector<const BlockSummary*> blocks;
|
|
for (const auto& bs : s_recent_blocks) {
|
|
if (bs.height > 0) blocks.push_back(&bs);
|
|
}
|
|
|
|
// Fixed card height — content scrolls inside
|
|
float maxRows = 10.0f;
|
|
float contentH = capFont->LegacySize + Layout::spacingXs() + rowH * maxRows;
|
|
float tableH = headerH + contentH + pad;
|
|
|
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + tableH);
|
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
|
|
// Header
|
|
dl->AddText(ovFont, ovFont->LegacySize,
|
|
ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_recent_blocks"));
|
|
|
|
// Responsive column layout — give height more room, use remaining for data
|
|
float innerW = availWidth - pad * 2;
|
|
float colHeight = pad;
|
|
float colTxs = colHeight + innerW * 0.14f;
|
|
float colSize = colHeight + innerW * 0.24f;
|
|
float colDiff = colHeight + innerW * 0.38f;
|
|
float colHash = colHeight + innerW * 0.56f;
|
|
float colTime = colHeight + innerW * 0.82f;
|
|
|
|
float hdrY = cardMin.y + pad * 0.5f + ovFont->LegacySize + Layout::spacingSm();
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colHeight, hdrY), OnSurfaceMedium(), TR("explorer_block_height"));
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colTxs, hdrY), OnSurfaceMedium(), TR("explorer_block_txs"));
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colSize, hdrY), OnSurfaceMedium(), TR("explorer_block_size"));
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colDiff, hdrY), OnSurfaceMedium(), TR("difficulty"));
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colHash, hdrY), OnSurfaceMedium(), TR("explorer_block_hash"));
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colTime, hdrY), OnSurfaceMedium(), TR("explorer_block_time"));
|
|
|
|
// Scrollable child region for rows
|
|
float rowAreaTop = hdrY + capFont->LegacySize + Layout::spacingXs();
|
|
float rowAreaH = cardMax.y - rowAreaTop - pad * 0.5f;
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, rowAreaTop));
|
|
|
|
int parentVtx = dl->VtxBuffer.Size;
|
|
ImGui::BeginChild("##BlockRows", ImVec2(availWidth, rowAreaH), false,
|
|
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
|
ApplySmoothScroll();
|
|
|
|
ImDrawList* childDL = ImGui::GetWindowDrawList();
|
|
int childVtx = childDL->VtxBuffer.Size;
|
|
float scrollY = ImGui::GetScrollY();
|
|
float scrollMaxY = ImGui::GetScrollMaxY();
|
|
|
|
char buf[128];
|
|
float rowInset = 2 * dp;
|
|
|
|
if (blocks.empty() && s_pending_block_fetches > 0) {
|
|
ImGui::SetCursorPosY(Layout::spacingMd());
|
|
ImGui::TextDisabled("%s", TR("loading"));
|
|
} else {
|
|
for (size_t i = 0; i < blocks.size(); i++) {
|
|
const auto* bs = blocks[i];
|
|
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
|
float rowW = ImGui::GetContentRegionAvail().x - rowInset * 2;
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(rowPos.x + rowInset, rowPos.y));
|
|
|
|
// InvisibleButton for proper interaction
|
|
ImGui::PushID(static_cast<int>(i));
|
|
ImGui::InvisibleButton("##blkRow", ImVec2(rowW, rowH));
|
|
bool hovered = ImGui::IsItemHovered();
|
|
bool clicked = ImGui::IsItemClicked(0);
|
|
ImGui::PopID();
|
|
|
|
ImVec2 rowMin(rowPos.x + rowInset, rowPos.y);
|
|
ImVec2 rowMax(rowMin.x + rowW, rowMin.y + rowH);
|
|
|
|
// Subtle alternating row background for readability
|
|
if (i % 2 == 0) {
|
|
childDL->AddRectFilled(rowMin, rowMax, WithAlpha(OnSurface(), 6), rowRound);
|
|
}
|
|
|
|
// Hover highlight — clear clickable feedback
|
|
if (hovered) {
|
|
childDL->AddRectFilled(rowMin, rowMax, WithAlpha(Primary(), 20), rowRound);
|
|
childDL->AddRect(rowMin, rowMax, WithAlpha(Primary(), 40), rowRound);
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
}
|
|
|
|
// Selected highlight
|
|
if (s_show_detail_modal && s_detail_height == bs->height) {
|
|
childDL->AddRectFilled(rowMin, rowMax, WithAlpha(Primary(), 25), rowRound);
|
|
}
|
|
|
|
// Click opens block detail modal
|
|
if (clicked && app->rpc() && app->rpc()->isConnected()) {
|
|
fetchBlockDetail(app, bs->height);
|
|
}
|
|
|
|
float textY = rowPos.y + (rowH - sub1->LegacySize) * 0.5f;
|
|
float textY2 = rowPos.y + (rowH - body2->LegacySize) * 0.5f;
|
|
|
|
// Height — emphasized with larger font and primary color
|
|
snprintf(buf, sizeof(buf), "#%d", bs->height);
|
|
childDL->AddText(sub1, sub1->LegacySize, ImVec2(cardMin.x + colHeight, textY),
|
|
hovered ? Primary() : schema::UI().resolveColor("var(--primary-light)"), buf);
|
|
|
|
// Tx count
|
|
snprintf(buf, sizeof(buf), "%d tx", bs->tx_count);
|
|
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colTxs, textY2), OnSurface(), buf);
|
|
|
|
// Size
|
|
std::string sizeStr = formatSize(bs->size);
|
|
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colSize, textY2), OnSurface(), sizeStr.c_str());
|
|
|
|
// Difficulty
|
|
snprintf(buf, sizeof(buf), "%.2f", bs->difficulty);
|
|
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colDiff, textY2), OnSurface(), buf);
|
|
|
|
// Hash — adaptive to available column width
|
|
float hashMaxW = (colTime - colHash) - Layout::spacingSm();
|
|
std::string hashDisp = truncateHashToFit(bs->hash, body2, hashMaxW);
|
|
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colHash, textY2),
|
|
OnSurfaceMedium(), hashDisp.c_str());
|
|
|
|
// Time
|
|
const char* timeStr = relativeTime(bs->time);
|
|
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colTime, textY2), OnSurfaceMedium(), timeStr);
|
|
}
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
|
|
float fadeZone = S.drawElement("tabs.explorer", "scroll-fade-zone").size;
|
|
ApplyScrollEdgeMask(dl, parentVtx, childDL, childVtx,
|
|
rowAreaTop, rowAreaTop + rowAreaH, fadeZone, scrollY, scrollMaxY);
|
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
|
ImGui::Dummy(ImVec2(availWidth, Layout::spacingMd()));
|
|
}
|
|
|
|
static void renderBlockDetailModal(App* app) {
|
|
if (!s_show_detail_modal && !s_detail_loading) return;
|
|
|
|
auto& S = schema::UI();
|
|
float pad = Layout::cardInnerPadding();
|
|
|
|
ImFont* ovFont = Type().overline();
|
|
ImFont* capFont = Type().caption();
|
|
ImFont* sub1 = Type().subtitle1();
|
|
ImFont* body2 = Type().body2();
|
|
|
|
float modalW = S.drawElement("tabs.explorer", "detail-modal-width").sizeOr(700.0f);
|
|
|
|
if (s_detail_loading && !s_show_detail_modal) {
|
|
// Show a loading modal while fetching
|
|
if (BeginOverlayDialog(TR("loading"), &s_detail_loading, 300.0f, 0.85f)) {
|
|
ImGui::Spacing();
|
|
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
|
const char* dotStr[] = {"", ".", "..", "..."};
|
|
char loadBuf[64];
|
|
snprintf(loadBuf, sizeof(loadBuf), "%s%s", TR("loading"), dotStr[dots]);
|
|
ImGui::TextDisabled("%s", loadBuf);
|
|
ImGui::Spacing();
|
|
EndOverlayDialog();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!BeginOverlayDialog(TR("explorer_block_detail"), &s_show_detail_modal, modalW, 0.90f))
|
|
return;
|
|
|
|
float contentW = ImGui::GetContentRegionAvail().x;
|
|
float hashMaxW = contentW - pad * 2 - Type().iconSmall()->LegacySize;
|
|
char buf[256];
|
|
|
|
// ── Header: "Block #123456" + nav buttons ──
|
|
{
|
|
ImGui::PushFont(sub1);
|
|
snprintf(buf, sizeof(buf), "%s #%d", TR("explorer_block_detail"), s_detail_height);
|
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Primary()), "%s", buf);
|
|
ImGui::PopFont();
|
|
|
|
// Nav buttons on same line
|
|
ImGui::SameLine(contentW - Layout::spacingXl() * 3);
|
|
|
|
// Prev
|
|
if (s_detail_height > 1) {
|
|
ImGui::PushFont(Type().iconMed());
|
|
ImGui::PushID("prevBlock");
|
|
if (ImGui::SmallButton(ICON_MD_CHEVRON_LEFT)) {
|
|
if (app->rpc() && app->rpc()->isConnected())
|
|
fetchBlockDetail(app, s_detail_height - 1);
|
|
}
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Previous block");
|
|
ImGui::PopID();
|
|
ImGui::PopFont();
|
|
ImGui::SameLine();
|
|
}
|
|
|
|
// Next
|
|
if (!s_detail_next_hash.empty()) {
|
|
ImGui::PushFont(Type().iconMed());
|
|
ImGui::PushID("nextBlock");
|
|
if (ImGui::SmallButton(ICON_MD_CHEVRON_RIGHT)) {
|
|
if (app->rpc() && app->rpc()->isConnected())
|
|
fetchBlockDetail(app, s_detail_height + 1);
|
|
}
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Next block");
|
|
ImGui::PopID();
|
|
ImGui::PopFont();
|
|
}
|
|
}
|
|
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
// Show loading state inside the modal when navigating between blocks
|
|
if (s_detail_loading) {
|
|
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
|
const char* dotStr[] = {"", ".", "..", "..."};
|
|
snprintf(buf, sizeof(buf), "%s%s", TR("loading"), dotStr[dots]);
|
|
ImGui::TextDisabled("%s", buf);
|
|
EndOverlayDialog();
|
|
return;
|
|
}
|
|
|
|
// ── Block Hash ──
|
|
{
|
|
ImGui::PushFont(capFont);
|
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("explorer_block_hash"));
|
|
ImGui::PopFont();
|
|
|
|
std::string hashDisp = truncateHashToFit(s_detail_hash, body2, hashMaxW);
|
|
ImGui::PushFont(body2);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor("var(--secondary-light)")));
|
|
ImGui::TextWrapped("%s", hashDisp.c_str());
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopFont();
|
|
|
|
ImGui::SameLine();
|
|
copyButton("cpHash", s_detail_hash,
|
|
ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y);
|
|
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip("%s", s_detail_hash.c_str());
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
// ── Info grid ──
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
float rowH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize;
|
|
float labelW = S.drawElement("tabs.explorer", "label-column").size;
|
|
{
|
|
ImVec2 gridPos = ImGui::GetCursorScreenPos();
|
|
float gx = gridPos.x;
|
|
float gy = gridPos.y;
|
|
float halfW = contentW * 0.5f;
|
|
|
|
// Row 1: Timestamp | Confirmations
|
|
drawLabelValue(dl, gx, gy, labelW, TR("block_timestamp"), "", capFont, sub1);
|
|
if (s_detail_time > 0) {
|
|
std::time_t t = static_cast<std::time_t>(s_detail_time);
|
|
char time_buf[64];
|
|
std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(gx + labelW, gy), OnSurface(), time_buf);
|
|
}
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(gx + halfW, gy), OnSurfaceMedium(), TR("confirmations"));
|
|
snprintf(buf, sizeof(buf), "%d", s_detail_confirmations);
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(gx + halfW + labelW * 0.7f, gy), OnSurface(), buf);
|
|
gy += rowH + Layout::spacingSm();
|
|
|
|
// Row 2: Size | Difficulty
|
|
drawLabelValue(dl, gx, gy, labelW, TR("block_size"),
|
|
formatSize(s_detail_size).c_str(), capFont, sub1);
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(gx + halfW, gy), OnSurfaceMedium(), TR("difficulty"));
|
|
snprintf(buf, sizeof(buf), "%.4f", s_detail_difficulty);
|
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(gx + halfW + labelW * 0.7f, gy), OnSurface(), buf);
|
|
gy += rowH + Layout::spacingSm();
|
|
|
|
// Row 3: Bits
|
|
drawLabelValue(dl, gx, gy, labelW, TR("block_bits"), s_detail_bits.c_str(), capFont, sub1);
|
|
gy += rowH + Layout::spacingSm();
|
|
|
|
// Row 4: Merkle Root
|
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(gx, gy), OnSurfaceMedium(), TR("explorer_block_merkle"));
|
|
gy += capFont->LegacySize + Layout::spacingXs();
|
|
{
|
|
float merkleMaxW = contentW - Type().iconSmall()->LegacySize - Layout::spacingMd();
|
|
std::string merkleTrunc = truncateHashToFit(s_detail_merkle, body2, merkleMaxW);
|
|
dl->AddText(body2, body2->LegacySize, ImVec2(gx, gy),
|
|
OnSurfaceMedium(), merkleTrunc.c_str());
|
|
}
|
|
gy += body2->LegacySize + Layout::spacingXs();
|
|
|
|
// Advance ImGui cursor past the drawlist content
|
|
ImGui::SetCursorScreenPos(ImVec2(gridPos.x, gy));
|
|
}
|
|
|
|
// Copy merkle button
|
|
copyButton("cpMerkle", s_detail_merkle,
|
|
ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x - Layout::spacingXl(),
|
|
ImGui::GetCursorScreenPos().y - body2->LegacySize - Layout::spacingSm());
|
|
|
|
ImGui::Spacing();
|
|
|
|
// ── Transactions ──
|
|
{
|
|
ImGui::PushFont(ovFont);
|
|
snprintf(buf, sizeof(buf), "%s (%d)", TR("explorer_block_txs"), (int)s_detail_txids.size());
|
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Primary()), "%s", buf);
|
|
ImGui::PopFont();
|
|
|
|
ImGui::Spacing();
|
|
|
|
float txRowH = S.drawElement("tabs.explorer", "tx-row-height").size;
|
|
ImU32 linkCol = schema::UI().resolveColor("var(--secondary-light)");
|
|
|
|
for (int i = 0; i < (int)s_detail_txids.size(); i++) {
|
|
const auto& txid = s_detail_txids[i];
|
|
bool isExpanded = (s_expanded_tx_idx == i);
|
|
|
|
ImGui::PushID(i);
|
|
|
|
// Expand/collapse icon + txid on one row via InvisibleButton
|
|
ImFont* iconFont = Type().iconSmall();
|
|
const char* expandIcon = isExpanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
|
|
|
|
ImVec2 rowStart = ImGui::GetCursorScreenPos();
|
|
float txContentW = ImGui::GetContentRegionAvail().x;
|
|
|
|
ImGui::InvisibleButton("##txRow", ImVec2(txContentW, txRowH));
|
|
bool rowHovered = ImGui::IsItemHovered();
|
|
bool rowClicked = ImGui::IsItemClicked(0);
|
|
bool rightClicked = ImGui::IsItemClicked(1);
|
|
|
|
ImDrawList* txDL = ImGui::GetWindowDrawList();
|
|
|
|
// Hover highlight
|
|
if (rowHovered) {
|
|
txDL->AddRectFilled(rowStart,
|
|
ImVec2(rowStart.x + txContentW, rowStart.y + txRowH),
|
|
WithAlpha(OnSurface(), 10),
|
|
S.drawElement("tabs.explorer", "row-rounding").size);
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
ImGui::SetTooltip("%s", txid.c_str());
|
|
}
|
|
|
|
// Draw icon + text
|
|
float iconY = rowStart.y + (txRowH - iconFont->LegacySize) * 0.5f;
|
|
txDL->AddText(iconFont, iconFont->LegacySize,
|
|
ImVec2(rowStart.x, iconY), OnSurfaceMedium(), expandIcon);
|
|
|
|
float txTextX = rowStart.x + iconFont->LegacySize + Layout::spacingXs();
|
|
float txMaxW = txContentW - iconFont->LegacySize - Layout::spacingXl() * 3;
|
|
std::string txTrunc = truncateHashToFit(txid, body2, txMaxW);
|
|
float textY = rowStart.y + (txRowH - body2->LegacySize) * 0.5f;
|
|
txDL->AddText(body2, body2->LegacySize, ImVec2(txTextX, textY), linkCol, txTrunc.c_str());
|
|
|
|
// Copy icon at end of row
|
|
float copyX = rowStart.x + txContentW - Layout::spacingXl();
|
|
if (rowHovered) {
|
|
float copyY = rowStart.y + (txRowH - iconFont->LegacySize) * 0.5f;
|
|
txDL->AddText(iconFont, iconFont->LegacySize,
|
|
ImVec2(copyX, copyY), OnSurfaceMedium(), ICON_MD_CONTENT_COPY);
|
|
}
|
|
|
|
if (rowClicked) {
|
|
if (isExpanded) {
|
|
s_expanded_tx_idx = -1;
|
|
} else {
|
|
s_expanded_tx_idx = i;
|
|
fetchTxDetail(app, txid);
|
|
}
|
|
}
|
|
if (rightClicked) {
|
|
ImGui::SetClipboardText(txid.c_str());
|
|
Notifications::instance().success(TR("copied_to_clipboard"));
|
|
}
|
|
|
|
// ── Expanded transaction detail ──
|
|
if (isExpanded) {
|
|
float indent = Layout::spacingXl();
|
|
if (s_tx_loading) {
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + indent);
|
|
ImGui::TextDisabled("%s", TR("loading"));
|
|
} else if (!s_tx_detail.is_null()) {
|
|
ImGui::Indent(indent);
|
|
|
|
// Full TxID with copy
|
|
ImGui::PushFont(capFont);
|
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "TxID:");
|
|
ImGui::PopFont();
|
|
ImGui::PushFont(body2);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(linkCol));
|
|
ImGui::TextWrapped("%s", txid.c_str());
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopFont();
|
|
|
|
// Size
|
|
if (s_tx_detail.contains("size")) {
|
|
ImGui::PushFont(capFont);
|
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()),
|
|
"%s: %s", TR("explorer_tx_size"),
|
|
formatSize(s_tx_detail.value("size", 0)).c_str());
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
// Outputs
|
|
if (s_tx_detail.contains("vout") && s_tx_detail["vout"].is_array()) {
|
|
ImGui::PushFont(capFont);
|
|
snprintf(buf, sizeof(buf), "%s (%d):", TR("explorer_tx_outputs"),
|
|
(int)s_tx_detail["vout"].size());
|
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", buf);
|
|
ImGui::PopFont();
|
|
|
|
ImGui::PushFont(body2);
|
|
for (const auto& vout : s_tx_detail["vout"]) {
|
|
double val = vout.value("value", 0.0);
|
|
int n = vout.value("n", 0);
|
|
std::string addr = "shielded";
|
|
if (vout.contains("scriptPubKey") && vout["scriptPubKey"].contains("addresses") &&
|
|
vout["scriptPubKey"]["addresses"].is_array() && !vout["scriptPubKey"]["addresses"].empty()) {
|
|
addr = vout["scriptPubKey"]["addresses"][0].get<std::string>();
|
|
}
|
|
float addrMaxW = ImGui::GetContentRegionAvail().x * 0.5f;
|
|
std::string addrDisp = truncateHashToFit(addr, body2, addrMaxW);
|
|
snprintf(buf, sizeof(buf), "[%d] %s %.8f DRGX", n, addrDisp.c_str(), val);
|
|
ImGui::TextUnformatted(buf);
|
|
}
|
|
ImGui::PopFont();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Unindent(indent);
|
|
}
|
|
}
|
|
|
|
ImGui::PopID();
|
|
}
|
|
|
|
if (s_detail_txids.size() > 100) {
|
|
snprintf(buf, sizeof(buf), "... showing first 100 of %d", (int)s_detail_txids.size());
|
|
ImGui::TextDisabled("%s", buf);
|
|
}
|
|
}
|
|
|
|
EndOverlayDialog();
|
|
}
|
|
|
|
// ─── Main Entry Point ──────────────────────────────────────────
|
|
|
|
void RenderExplorerTab(App* app)
|
|
{
|
|
const auto& state = app->getWalletState();
|
|
auto* rpc = app->rpc();
|
|
|
|
ImVec2 avail = ImGui::GetContentRegionAvail();
|
|
ImGui::BeginChild("##ExplorerScroll", avail, false,
|
|
ImGuiWindowFlags_NoBackground);
|
|
ApplySmoothScroll();
|
|
|
|
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
|
|
// Auto-refresh recent blocks when chain height changes
|
|
if (state.sync.blocks > 0 && state.sync.blocks != s_last_known_height) {
|
|
s_last_known_height = state.sync.blocks;
|
|
if (rpc && rpc->isConnected()) {
|
|
fetchRecentBlocks(app, state.sync.blocks);
|
|
fetchMempoolInfo(app);
|
|
}
|
|
}
|
|
|
|
renderSearchBar(app, availWidth);
|
|
renderChainStats(app, availWidth);
|
|
renderRecentBlocks(app, availWidth);
|
|
|
|
ImGui::EndChild();
|
|
|
|
// Block detail modal — rendered outside the scroll region so it's fullscreen
|
|
renderBlockDetailModal(app);
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace dragonx
|