Bundles the session's UI work (the touched files carry several of these changes together, so they are committed as one coherent UI batch): - FAQ: new RenderFaqDialog + data-driven faq_content, opened from a status-bar "?" (and the Windows title bar), styled like the Wallets modal with search, Wallet/Daemon tabs, and smooth scroll. - DPI/font-scale audit: multiply hand-drawn absolute geometry by Layout::dpiScale() across ~30 files so nothing renders native-size at HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x). - Mining: chart now fills the horizontal space; thread stepper +/- buttons match the input-box height; move the stratum-host toggle into Node & Security (v1.3.0+). - Settings: fix the auto-shield status text overlapping the grid. - Sidebar: drop the peer-count badge on the Network button. - i18n: wrap 193 hardcoded literals with TR() (keys/translations added in the preceding i18n commit), so the security/PIN/lock flow, seed-backup wizard, and witness-rebuild dialog localize. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
338 lines
12 KiB
C++
338 lines
12 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "block_info_dialog.h"
|
|
#include "../../app.h"
|
|
#include "../../rpc/rpc_client.h"
|
|
#include "../../rpc/rpc_worker.h"
|
|
#include "../../util/i18n.h"
|
|
#include "../../util/text_format.h"
|
|
#include "../notifications.h"
|
|
#include "../schema/ui_schema.h"
|
|
#include "../material/draw_helpers.h"
|
|
#include "imgui.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
#include <string>
|
|
#include <ctime>
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
|
|
using json = nlohmann::json;
|
|
|
|
// Static state
|
|
static bool s_open = false;
|
|
static int s_height = 0;
|
|
static bool s_loading = false;
|
|
static bool s_has_data = false;
|
|
static std::string s_error;
|
|
|
|
// Block data
|
|
static std::string s_block_hash;
|
|
static int64_t s_block_time = 0;
|
|
static int s_tx_count = 0;
|
|
static int s_block_size = 0;
|
|
static std::string s_bits;
|
|
static double s_difficulty = 0.0;
|
|
static std::string s_prev_hash;
|
|
static std::string s_next_hash;
|
|
static std::string s_merkle_root;
|
|
static int s_confirmations = 0;
|
|
|
|
// Pending RPC app pointer (for async callback)
|
|
static App* s_pending_app = nullptr;
|
|
|
|
void BlockInfoDialog::show(int initialHeight)
|
|
{
|
|
s_open = true;
|
|
s_height = initialHeight > 0 ? initialHeight : 1;
|
|
s_loading = false;
|
|
s_has_data = false;
|
|
s_error.clear();
|
|
}
|
|
|
|
// Callback to handle getblock response
|
|
static void handleBlockResponseUnified(const json& result, const std::string& error)
|
|
{
|
|
s_loading = false;
|
|
|
|
if (!error.empty()) {
|
|
s_error = std::string(TR("grpa_error_prefix")) + error;
|
|
return;
|
|
}
|
|
|
|
if (!result.is_null()) {
|
|
auto block = result;
|
|
|
|
s_block_hash = block.value("hash", "");
|
|
s_block_time = block.value("time", (int64_t)0);
|
|
s_confirmations = block.value("confirmations", 0);
|
|
s_block_size = block.value("size", 0);
|
|
s_bits = block.value("bits", "");
|
|
s_difficulty = block.value("difficulty", 0.0);
|
|
s_prev_hash = block.value("previousblockhash", "");
|
|
s_next_hash = block.value("nextblockhash", "");
|
|
s_merkle_root = block.value("merkleroot", "");
|
|
|
|
if (block.contains("tx") && block["tx"].is_array()) {
|
|
s_tx_count = static_cast<int>(block["tx"].size());
|
|
} else {
|
|
s_tx_count = 0;
|
|
}
|
|
|
|
s_has_data = true;
|
|
} else {
|
|
s_error = TR("grpa_invalid_response_from_daemon");
|
|
}
|
|
}
|
|
|
|
void BlockInfoDialog::render(App* app)
|
|
{
|
|
if (!s_open) return;
|
|
|
|
auto& S = schema::UI();
|
|
auto win = S.window("dialogs.block-info");
|
|
auto heightInput = S.input("dialogs.block-info", "height-input");
|
|
auto lbl = S.label("dialogs.block-info", "label");
|
|
auto hashLbl = S.label("dialogs.block-info", "hash-label");
|
|
auto hashFrontLbl = S.label("dialogs.block-info", "hash-front-label");
|
|
auto hashBackLbl = S.label("dialogs.block-info", "hash-back-label");
|
|
auto closeBtn = S.button("dialogs.block-info", "close-button");
|
|
|
|
material::OverlayDialogSpec ov;
|
|
ov.title = TR("block_info_title"); ov.p_open = &s_open;
|
|
ov.style = material::OverlayStyle::BlurFloat; // floating content on the blur, plain heading
|
|
ov.cardWidth = win.width; // keep the authored width
|
|
ov.cardBottomViewportRatio = 0.94f;
|
|
if (material::BeginOverlayDialog(ov)) {
|
|
auto* rpc = app->rpc();
|
|
const auto& state = app->getWalletState();
|
|
|
|
// Height input
|
|
ImGui::Text("%s", TR("block_height"));
|
|
ImGui::SetNextItemWidth(heightInput.width * Layout::dpiScale());
|
|
ImGui::InputInt("##Height", &s_height);
|
|
if (s_height < 1) s_height = 1;
|
|
// Clamp to the chain tip so navigation/typing can't request a height
|
|
// above the tip (which would only yield a raw RPC error).
|
|
if (state.sync.blocks > 0 && s_height > state.sync.blocks) {
|
|
s_height = state.sync.blocks;
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
|
|
// Current block info
|
|
if (state.sync.blocks > 0) {
|
|
ImGui::TextDisabled(TR("grpa_current_block_paren"), state.sync.blocks);
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
|
|
// Fetch button
|
|
if (s_loading) {
|
|
ImGui::BeginDisabled();
|
|
}
|
|
|
|
if (material::TactileButton(TR("block_get_info"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
|
if (rpc && rpc->isConnected() && app->worker()) {
|
|
s_loading = true;
|
|
s_error.clear();
|
|
s_has_data = false;
|
|
s_pending_app = app;
|
|
|
|
// Run the two chained RPCs (getblockhash → getblock) on the worker thread;
|
|
// doing them inline froze the UI for two round-trips. Guard the hash type.
|
|
int height = s_height;
|
|
app->worker()->post([rpc, height]() -> rpc::RPCWorker::MainCb {
|
|
json block;
|
|
std::string error;
|
|
try {
|
|
rpc::RPCClient::TraceScope trace("Explorer / Block info");
|
|
auto hashResult = rpc->call("getblockhash", {height});
|
|
if (!hashResult.is_string()) {
|
|
error = TR("grpa_unexpected_getblockhash_result");
|
|
} else {
|
|
block = rpc->call("getblock", {hashResult.get<std::string>()});
|
|
}
|
|
} catch (const std::exception& e) {
|
|
error = e.what();
|
|
}
|
|
return [block, error]() { handleBlockResponseUnified(block, error); };
|
|
});
|
|
}
|
|
}
|
|
|
|
if (s_loading) {
|
|
ImGui::EndDisabled();
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("%s", TR("loading"));
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
// Error display
|
|
if (!s_error.empty()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
|
|
ImGui::TextWrapped("%s", s_error.c_str());
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Block info display
|
|
if (s_has_data) {
|
|
// Block hash
|
|
ImGui::Text("%s", TR("block_hash"));
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.8f, 1.0f, 1.0f));
|
|
ImGui::TextWrapped("%s", s_block_hash.c_str());
|
|
ImGui::PopStyleColor();
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
material::Tooltip("%s", TR("click_to_copy"));
|
|
}
|
|
if (ImGui::IsItemClicked()) {
|
|
ImGui::SetClipboardText(s_block_hash.c_str());
|
|
Notifications::instance().success(TR("block_hash_copied"));
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
// Timestamp
|
|
ImGui::Text("%s", TR("block_timestamp"));
|
|
ImGui::SameLine(lbl.position);
|
|
if (s_block_time > 0) {
|
|
ImGui::Text("%s", dragonx::util::formatClockDateTime(s_block_time, /*withSeconds=*/true).c_str());
|
|
} else {
|
|
ImGui::TextDisabled("%s", TR("unknown"));
|
|
}
|
|
|
|
// Confirmations
|
|
ImGui::Text("%s", TR("confirmations"));
|
|
ImGui::SameLine(lbl.position);
|
|
ImGui::Text("%d", s_confirmations);
|
|
|
|
// Transaction count
|
|
ImGui::Text("%s", TR("block_transactions"));
|
|
ImGui::SameLine(lbl.position);
|
|
ImGui::Text("%d", s_tx_count);
|
|
|
|
// Size
|
|
ImGui::Text("%s", TR("block_size"));
|
|
ImGui::SameLine(lbl.position);
|
|
if (s_block_size > 1024 * 1024) {
|
|
ImGui::Text("%.2f MB", s_block_size / (1024.0 * 1024.0));
|
|
} else if (s_block_size > 1024) {
|
|
ImGui::Text("%.2f KB", s_block_size / 1024.0);
|
|
} else {
|
|
ImGui::Text("%d bytes", s_block_size);
|
|
}
|
|
|
|
// Difficulty
|
|
ImGui::Text("%s", TR("difficulty"));
|
|
ImGui::SameLine(lbl.position);
|
|
ImGui::Text("%.4f", s_difficulty);
|
|
|
|
// Bits
|
|
ImGui::Text("%s", TR("block_bits"));
|
|
ImGui::SameLine(lbl.position);
|
|
ImGui::Text("%s", s_bits.c_str());
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
// Merkle root
|
|
ImGui::Text("%s", TR("block_merkle_root"));
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 1.0f));
|
|
ImGui::TextWrapped("%s", s_merkle_root.c_str());
|
|
ImGui::PopStyleColor();
|
|
|
|
ImGui::Spacing();
|
|
|
|
// Previous block
|
|
if (!s_prev_hash.empty()) {
|
|
ImGui::Text("%s", TR("block_previous"));
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.8f, 1.0f, 1.0f));
|
|
|
|
// Truncate for display
|
|
std::string prev_short = s_prev_hash;
|
|
if (prev_short.length() > static_cast<size_t>(hashLbl.truncate)) {
|
|
prev_short = util::truncateMiddle(prev_short, hashFrontLbl.truncate, hashBackLbl.truncate);
|
|
}
|
|
ImGui::Text("%s", prev_short.c_str());
|
|
ImGui::PopStyleColor();
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
material::Tooltip("%s", TR("block_click_prev"));
|
|
}
|
|
if (ImGui::IsItemClicked() && s_height > 1) {
|
|
s_height--;
|
|
s_has_data = false;
|
|
}
|
|
}
|
|
|
|
// Next block
|
|
if (!s_next_hash.empty()) {
|
|
ImGui::Text("%s", TR("block_next"));
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.8f, 1.0f, 1.0f));
|
|
|
|
// Truncate for display
|
|
std::string next_short = s_next_hash;
|
|
if (next_short.length() > static_cast<size_t>(hashLbl.truncate)) {
|
|
next_short = util::truncateMiddle(next_short, hashFrontLbl.truncate, hashBackLbl.truncate);
|
|
}
|
|
ImGui::Text("%s", next_short.c_str());
|
|
ImGui::PopStyleColor();
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
material::Tooltip("%s", TR("block_click_next"));
|
|
}
|
|
if (ImGui::IsItemClicked() &&
|
|
(state.sync.blocks <= 0 || s_height < state.sync.blocks)) {
|
|
s_height++;
|
|
s_has_data = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Spacing();
|
|
|
|
// Navigation buttons
|
|
if (s_has_data) {
|
|
if (s_height > 1) {
|
|
if (material::TactileButton(TR("block_nav_prev"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
|
s_height--;
|
|
s_has_data = false;
|
|
s_error.clear();
|
|
}
|
|
ImGui::SameLine();
|
|
}
|
|
|
|
// Only offer "Next" when below the chain tip (the tip block has no
|
|
// nextblockhash, so this stays hidden there).
|
|
if (!s_next_hash.empty() &&
|
|
(state.sync.blocks <= 0 || s_height < state.sync.blocks)) {
|
|
if (material::TactileButton(TR("block_nav_next"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
|
|
s_height++;
|
|
s_has_data = false;
|
|
s_error.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Close button at bottom — centered via the shared footer helper (no separator, matching
|
|
// the prior hand-rolled placement).
|
|
material::BeginOverlayDialogFooter(closeBtn.width, /*drawSeparator=*/false);
|
|
if (material::TactileButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
|
|
s_open = false;
|
|
}
|
|
material::EndOverlayDialog();
|
|
}
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace dragonx
|