// 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 #include #include 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(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()}); } } 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(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(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