ObsidianDragon - DragonX ImGui Wallet

Full-node GUI wallet for DragonX cryptocurrency.
Built with Dear ImGui, SDL3, and OpenGL3/DX11.

Features:
- Send/receive shielded and transparent transactions
- Autoshield with merged transaction display
- Built-in CPU mining (xmrig)
- Peer management and network monitoring
- Wallet encryption with PIN lock
- QR code generation for receive addresses
- Transaction history with pagination
- Console for direct RPC commands
- Cross-platform (Linux, Windows)
This commit is contained in:
2026-02-26 02:31:52 -06:00
commit 3aee55b49c
306 changed files with 177789 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "about_dialog.h"
#include "../../app.h"
#include "../../config/version.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
void RenderAboutDialog(App* app, bool* p_open)
{
(void)app;
auto& S = schema::UI();
auto win = S.window("dialogs.about");
auto linkBtn = S.button("dialogs.about", "link-button");
auto closeBtn = S.button("dialogs.about", "close-button");
auto versionLbl = S.label("dialogs.about", "version-label");
auto editionLbl = S.label("dialogs.about", "edition-label");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
// Use acrylic modal popup from current theme
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("About ObsidianDragon");
if (!effects::ImGuiAcrylic::BeginAcrylicPopupModal("About ObsidianDragon", p_open,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
effects::ImGuiAcrylic::EndAcrylicPopup();
return;
}
// Use Body2 font for all dialog text
ImGui::PushFont(Type().body2());
// Logo/Title area
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 0.69f, 0.31f, 1.0f)); // Green
ImGui::PushFont(Type().h4());
ImGui::Text("ObsidianDragon");
ImGui::PopFont();
ImGui::PopStyleColor();
ImGui::SameLine(ImGui::GetWindowWidth() - editionLbl.position);
ImGui::TextDisabled("ImGui Edition");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Version info
ImGui::Text("Version:");
ImGui::SameLine(versionLbl.position);
ImGui::Text("%s", DRAGONX_VERSION);
ImGui::Text("ImGui:");
ImGui::SameLine(versionLbl.position);
ImGui::Text("%s", IMGUI_VERSION);
ImGui::Text("Build Date:");
ImGui::SameLine(versionLbl.position);
ImGui::Text("%s %s", __DATE__, __TIME__);
#ifdef DRAGONX_DEBUG
ImGui::Text("Build Type:");
ImGui::SameLine(versionLbl.position);
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Debug");
#else
ImGui::Text("Build Type:");
ImGui::SameLine(versionLbl.position);
ImGui::Text("Release");
#endif
// Daemon info
if (app && app->isConnected()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Daemon:");
ImGui::SameLine(versionLbl.position);
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "Connected");
const auto& state = app->getWalletState();
ImGui::Text("Chain:");
ImGui::SameLine(versionLbl.position);
ImGui::Text("ObsidianDragon");
ImGui::Text("Block Height:");
ImGui::SameLine(versionLbl.position);
ImGui::Text("%d", state.sync.blocks);
ImGui::Text("Connections:");
ImGui::SameLine(versionLbl.position);
ImGui::Text("%zu peers", state.peers.size());
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Credits
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Credits");
ImGui::Spacing();
ImGui::BulletText("The Hush Developers");
ImGui::BulletText("ObsidianDragon Community");
ImGui::BulletText("Dear ImGui by Omar Cornut");
ImGui::BulletText("ImPlot by Evan Pezent");
ImGui::BulletText("SDL3 by Sam Lantinga");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// License
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "License");
ImGui::Spacing();
ImGui::TextWrapped(
"This software is released under the GNU General Public License v3 (GPLv3). "
"You are free to use, modify, and distribute this software under the terms of the license."
);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Links
if (material::StyledButton("Website", ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
#ifdef _WIN32
system("start https://dragonx.is");
#elif __APPLE__
system("open https://dragonx.is");
#else
system("xdg-open https://dragonx.is &");
#endif
}
ImGui::SameLine();
if (material::StyledButton("GitHub", ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
#ifdef _WIN32
system("start https://git.hush.is/dragonx/ObsidianDragon");
#elif __APPLE__
system("open https://git.hush.is/dragonx/ObsidianDragon");
#else
system("xdg-open https://git.hush.is/dragonx/ObsidianDragon &");
#endif
}
ImGui::SameLine();
if (material::StyledButton("Block Explorer", ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
#ifdef _WIN32
system("start https://explorer.dragonx.is");
#elif __APPLE__
system("open https://explorer.dragonx.is");
#else
system("xdg-open https://explorer.dragonx.is &");
#endif
}
ImGui::Spacing();
// Close button
float button_width = closeBtn.width;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) * 0.5f);
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
*p_open = false;
}
ImGui::PopFont(); // Body2
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,16 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
void RenderAboutDialog(App* app, bool* p_open);
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,316 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "address_book_dialog.h"
#include "../../app.h"
#include "../../data/address_book.h"
#include "../notifications.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "imgui.h"
#include <memory>
namespace dragonx {
namespace ui {
// Static member initialization
bool AddressBookDialog::s_open = false;
int AddressBookDialog::s_selected_index = -1;
bool AddressBookDialog::s_show_add_dialog = false;
bool AddressBookDialog::s_show_edit_dialog = false;
char AddressBookDialog::s_edit_label[128] = "";
char AddressBookDialog::s_edit_address[512] = "";
char AddressBookDialog::s_edit_notes[512] = "";
// Shared address book instance
static std::unique_ptr<data::AddressBook> s_address_book;
static data::AddressBook& getAddressBook() {
if (!s_address_book) {
s_address_book = std::make_unique<data::AddressBook>();
s_address_book->load();
}
return *s_address_book;
}
void AddressBookDialog::show()
{
s_open = true;
s_selected_index = -1;
s_show_add_dialog = false;
s_show_edit_dialog = false;
// Reload address book
getAddressBook().load();
}
bool AddressBookDialog::isOpen()
{
return s_open;
}
void AddressBookDialog::render(App* app)
{
(void)app; // May use for send-to feature later
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.address-book");
auto addrTable = S.table("dialogs.address-book", "address-table");
auto addrFrontLbl = S.label("dialogs.address-book", "address-front-label");
auto addrBackLbl = S.label("dialogs.address-book", "address-back-label");
auto addrInput = S.input("dialogs.address-book", "address-input");
auto notesInput = S.input("dialogs.address-book", "notes-input");
auto actionBtn = S.button("dialogs.address-book", "action-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Address Book");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Address Book", &s_open,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
auto& book = getAddressBook();
// Toolbar
if (material::StyledButton("Add New", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
s_show_add_dialog = true;
s_edit_label[0] = '\0';
s_edit_address[0] = '\0';
s_edit_notes[0] = '\0';
}
ImGui::SameLine();
bool has_selection = s_selected_index >= 0 && s_selected_index < static_cast<int>(book.size());
if (!has_selection) ImGui::BeginDisabled();
if (material::StyledButton("Edit", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
if (has_selection) {
const auto& entry = book.entries()[s_selected_index];
strncpy(s_edit_label, entry.label.c_str(), sizeof(s_edit_label) - 1);
strncpy(s_edit_address, entry.address.c_str(), sizeof(s_edit_address) - 1);
strncpy(s_edit_notes, entry.notes.c_str(), sizeof(s_edit_notes) - 1);
s_show_edit_dialog = true;
}
}
ImGui::SameLine();
if (material::StyledButton("Delete", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
if (has_selection) {
book.removeEntry(s_selected_index);
s_selected_index = -1;
Notifications::instance().success("Entry deleted");
}
}
ImGui::SameLine();
if (material::StyledButton("Copy Address", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
if (has_selection) {
ImGui::SetClipboardText(book.entries()[s_selected_index].address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
}
if (!has_selection) ImGui::EndDisabled();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Address list
if (ImGui::BeginTable("AddressBookTable", 3,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY,
ImVec2(0, addrTable.bottomReserve > 0 ? -addrTable.bottomReserve : -35)))
{
float labelColW = (addrTable.columns.count("label") && addrTable.columns.at("label").width > 0) ? addrTable.columns.at("label").width : 150;
float notesColW = (addrTable.columns.count("notes") && addrTable.columns.at("notes").width > 0) ? addrTable.columns.at("notes").width : 150;
ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, labelColW);
ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Notes", ImGuiTableColumnFlags_WidthFixed, notesColW);
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableHeadersRow();
if (book.empty()) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextDisabled("No saved addresses. Click 'Add New' to add one.");
} else {
for (size_t i = 0; i < book.size(); i++) {
const auto& entry = book.entries()[i];
ImGui::TableNextRow();
ImGui::PushID(static_cast<int>(i));
ImGui::TableNextColumn();
bool is_selected = (s_selected_index == static_cast<int>(i));
if (ImGui::Selectable(entry.label.c_str(), is_selected,
ImGuiSelectableFlags_SpanAllColumns)) {
s_selected_index = static_cast<int>(i);
}
// Double-click to edit
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
s_selected_index = static_cast<int>(i);
strncpy(s_edit_label, entry.label.c_str(), sizeof(s_edit_label) - 1);
strncpy(s_edit_address, entry.address.c_str(), sizeof(s_edit_address) - 1);
strncpy(s_edit_notes, entry.notes.c_str(), sizeof(s_edit_notes) - 1);
s_show_edit_dialog = true;
}
ImGui::TableNextColumn();
// Truncate long addresses
std::string addr_display = entry.address;
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0) ? addrTable.columns.at("address").truncate : 40;
if (addr_display.length() > static_cast<size_t>(addrTruncLen)) {
addr_display = addr_display.substr(0, addrFrontLbl.truncate) + "..." +
addr_display.substr(addr_display.length() - addrBackLbl.truncate);
}
ImGui::TextDisabled("%s", addr_display.c_str());
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", entry.address.c_str());
}
ImGui::TableNextColumn();
ImGui::TextDisabled("%s", entry.notes.c_str());
ImGui::PopID();
}
}
ImGui::EndTable();
}
// Status line
ImGui::TextDisabled("%zu addresses saved", book.size());
}
effects::ImGuiAcrylic::EndAcrylicPopup();
// Add dialog
if (s_show_add_dialog) {
ImGui::OpenPopup("Add Address");
}
// Re-use center from above (already defined at start of render)
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Add Address", &s_show_add_dialog, ImGuiWindowFlags_AlwaysAutoResize)) {
material::Type().text(material::TypeStyle::H6, "Add Address");
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Text("Label:");
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputText("##AddLabel", s_edit_label, sizeof(s_edit_label));
ImGui::Spacing();
ImGui::Text("Address:");
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputText("##AddAddress", s_edit_address, sizeof(s_edit_address));
ImGui::SameLine();
if (material::StyledButton("Paste##Add", ImVec2(0,0), S.resolveFont(actionBtn.font))) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
strncpy(s_edit_address, clipboard, sizeof(s_edit_address) - 1);
}
}
ImGui::Spacing();
ImGui::Text("Notes (optional):");
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputTextMultiline("##AddNotes", s_edit_notes, sizeof(s_edit_notes), ImVec2(addrInput.width, notesInput.height > 0 ? notesInput.height : 60));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
bool can_add = strlen(s_edit_label) > 0 && strlen(s_edit_address) > 0;
if (!can_add) ImGui::BeginDisabled();
if (material::StyledButton("Add", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
data::AddressBookEntry entry(s_edit_label, s_edit_address, s_edit_notes);
if (getAddressBook().addEntry(entry)) {
Notifications::instance().success("Address added to book");
s_show_add_dialog = false;
} else {
Notifications::instance().error("Address already exists in book");
}
}
if (!can_add) ImGui::EndDisabled();
ImGui::SameLine();
if (material::StyledButton("Cancel", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
s_show_add_dialog = false;
}
ImGui::EndPopup();
}
// Edit dialog
if (s_show_edit_dialog) {
ImGui::OpenPopup("Edit Address");
}
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Edit Address", &s_show_edit_dialog, ImGuiWindowFlags_AlwaysAutoResize)) {
material::Type().text(material::TypeStyle::H6, "Edit Address");
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Text("Label:");
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputText("##EditLabel", s_edit_label, sizeof(s_edit_label));
ImGui::Spacing();
ImGui::Text("Address:");
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputText("##EditAddress", s_edit_address, sizeof(s_edit_address));
ImGui::Spacing();
ImGui::Text("Notes (optional):");
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputTextMultiline("##EditNotes", s_edit_notes, sizeof(s_edit_notes), ImVec2(addrInput.width, notesInput.height > 0 ? notesInput.height : 60));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
bool can_save = strlen(s_edit_label) > 0 && strlen(s_edit_address) > 0;
if (!can_save) ImGui::BeginDisabled();
if (material::StyledButton("Save", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
data::AddressBookEntry entry(s_edit_label, s_edit_address, s_edit_notes);
if (getAddressBook().updateEntry(s_selected_index, entry)) {
Notifications::instance().success("Address updated");
s_show_edit_dialog = false;
} else {
Notifications::instance().error("Failed to update - address may be duplicate");
}
}
if (!can_save) ImGui::EndDisabled();
ImGui::SameLine();
if (material::StyledButton("Cancel", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
s_show_edit_dialog = false;
}
ImGui::EndPopup();
}
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,45 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
/**
* @brief Address book dialog for managing saved addresses
*/
class AddressBookDialog {
public:
/**
* @brief Show the address book dialog
*/
static void show();
/**
* @brief Render the dialog (call every frame)
* @param app Pointer to app instance
*/
static void render(App* app);
/**
* @brief Check if dialog is currently open
*/
static bool isOpen();
private:
static bool s_open;
static int s_selected_index;
static bool s_show_add_dialog;
static bool s_show_edit_dialog;
static char s_edit_label[128];
static char s_edit_address[512];
static char s_edit_notes[512];
};
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,199 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "backup_wallet_dialog.h"
#include "../../app.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "imgui.h"
#include <string>
#include <ctime>
#include <fstream>
#include <filesystem>
namespace dragonx {
namespace ui {
namespace fs = std::filesystem;
using json = nlohmann::json;
// Static state
static bool s_open = false;
static char s_destination[512] = "";
static std::string s_status;
static bool s_backing_up = false;
void BackupWalletDialog::show()
{
s_open = true;
s_status.clear();
s_backing_up = false;
// Generate default destination with timestamp
std::time_t now = std::time(nullptr);
char timebuf[32];
std::strftime(timebuf, sizeof(timebuf), "%Y%m%d_%H%M%S", std::localtime(&now));
// Default to home directory
std::string home = util::Platform::getHomeDir();
snprintf(s_destination, sizeof(s_destination), "%s/wallet_backup_%s.dat", home.c_str(), timebuf);
}
bool BackupWalletDialog::isOpen()
{
return s_open;
}
void BackupWalletDialog::render(App* app)
{
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.backup-wallet");
auto backupBtn = S.button("dialogs.backup-wallet", "backup-button");
auto closeBtn = S.button("dialogs.backup-wallet", "close-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Backup Wallet");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Backup Wallet", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
ImGui::TextWrapped(
"Create a backup of your wallet.dat file. This file contains all your "
"private keys and transaction history. Store the backup in a secure location."
);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (s_backing_up) {
ImGui::BeginDisabled();
}
// Destination path
ImGui::Text("Backup destination:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##Destination", s_destination, sizeof(s_destination));
ImGui::Spacing();
// Show wallet.dat location
std::string walletPath = util::Platform::getDataDir() + "/wallet.dat";
ImGui::TextDisabled("Source: %s", walletPath.c_str());
// Check if source exists
bool sourceExists = fs::exists(walletPath);
if (!sourceExists) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
ImGui::Text("Warning: wallet.dat not found at expected location");
ImGui::PopStyleColor();
}
if (s_backing_up) {
ImGui::EndDisabled();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Backup button - use RPC backupwallet
if (s_backing_up) {
ImGui::BeginDisabled();
}
if (material::StyledButton("Create Backup", ImVec2(backupBtn.width, 0), S.resolveFont(backupBtn.font))) {
if (strlen(s_destination) == 0) {
Notifications::instance().warning("Please enter a destination path");
} else if (!app->rpc() || !app->rpc()->isConnected()) {
Notifications::instance().error("Not connected to daemon");
} else {
s_backing_up = true;
s_status = "Creating backup...";
// Run backup on worker thread to avoid freezing UI
std::string dest(s_destination);
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), dest]() -> rpc::RPCWorker::MainCb {
bool success = false;
std::string statusMsg;
try {
rpc->call("backupwallet", json::array({dest}));
// Check if file was created
if (fs::exists(dest)) {
auto size = fs::file_size(dest);
char sizebuf[32];
if (size > 1024 * 1024) {
snprintf(sizebuf, sizeof(sizebuf), "%.2f MB", size / (1024.0 * 1024.0));
} else if (size > 1024) {
snprintf(sizebuf, sizeof(sizebuf), "%.2f KB", size / 1024.0);
} else {
snprintf(sizebuf, sizeof(sizebuf), "%zu bytes", size);
}
statusMsg = std::string("Backup created successfully (") + sizebuf + ")";
success = true;
} else {
statusMsg = "Backup may have failed - file not found";
}
} catch (const std::exception& e) {
statusMsg = std::string("Backup failed: ") + e.what();
}
return [success, statusMsg]() {
s_status = statusMsg;
s_backing_up = false;
if (success) {
Notifications::instance().success("Wallet backup created");
} else {
Notifications::instance().warning(statusMsg);
}
};
});
}
}
}
if (s_backing_up) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled("Backing up...");
}
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
// Status
if (!s_status.empty()) {
ImGui::Spacing();
ImGui::TextWrapped("%s", s_status.c_str());
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Tips
ImGui::TextDisabled("Tips:");
ImGui::BulletText("Store backups on external drives or cloud storage");
ImGui::BulletText("Create multiple backups in different locations");
ImGui::BulletText("Test restoring from backup periodically");
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,31 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
class App;
namespace ui {
/**
* @brief Dialog for backing up wallet.dat
*/
class BackupWalletDialog {
public:
// Show the dialog
static void show();
// Render the dialog (call each frame)
static void render(App* app);
// Check if dialog is open
static bool isOpen();
};
} // namespace ui
} // namespace dragonx

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <vector>
namespace dragonx {
class App;
namespace ui {
// A single balance layout entry read from ui.toml
struct BalanceLayoutEntry {
std::string id; // e.g. "classic", "donut", "minimal"
std::string name; // Display name, e.g. "Classic", "Donut Chart"
bool enabled = true; // Whether this layout appears in the settings dropdown
};
// Get the list of available balance layouts (parsed from ui.toml).
// On first call (or after RefreshBalanceLayoutConfig()), parses the
// "tabs.balance.layouts.options" array from the schema.
const std::vector<BalanceLayoutEntry>& GetBalanceLayouts();
// Get the default layout ID from ui.toml ("tabs.balance.layouts.default").
const std::string& GetDefaultBalanceLayout();
// Re-read layout config from schema (call after theme/skin hot-reload).
void RefreshBalanceLayoutConfig();
// Legacy int-to-string migration for old settings.json files.
// Maps BalanceLayout enum ordinals (0-8) to their string IDs.
std::string MigrateBalanceLayoutIndex(int index);
/**
* @brief Render the Balance tab
* Shows balance summary and address list.
* Dispatches to the selected layout from Settings.
*/
void RenderBalanceTab(App* app);
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,315 @@
// 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 "../../util/i18n.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.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 = "Error: " + 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 = "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");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Block Information");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Block Information", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
auto* rpc = app->rpc();
const auto& state = app->getWalletState();
// Height input
ImGui::Text("Block Height:");
ImGui::SetNextItemWidth(heightInput.width);
ImGui::InputInt("##Height", &s_height);
if (s_height < 1) s_height = 1;
ImGui::SameLine();
// Current block info
if (state.sync.blocks > 0) {
ImGui::TextDisabled("(Current: %d)", state.sync.blocks);
}
ImGui::SameLine();
// Fetch button
if (s_loading) {
ImGui::BeginDisabled();
}
if (material::StyledButton("Get Block Info", ImVec2(0,0), S.resolveFont(closeBtn.font))) {
if (rpc && rpc->isConnected()) {
s_loading = true;
s_error.clear();
s_has_data = false;
s_pending_app = app;
// Use getBlock(height) which uses UnifiedCallback
rpc->getBlock(s_height, handleBlockResponseUnified);
}
}
if (s_loading) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled("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("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()) {
ImGui::SetTooltip("Click to copy");
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(s_block_hash.c_str());
Notifications::instance().success("Block hash copied");
}
ImGui::Spacing();
// Timestamp
ImGui::Text("Timestamp:");
ImGui::SameLine(lbl.position);
if (s_block_time > 0) {
std::time_t t = static_cast<std::time_t>(s_block_time);
char time_buf[64];
std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
ImGui::Text("%s", time_buf);
} else {
ImGui::TextDisabled("Unknown");
}
// Confirmations
ImGui::Text("Confirmations:");
ImGui::SameLine(lbl.position);
ImGui::Text("%d", s_confirmations);
// Transaction count
ImGui::Text("Transactions:");
ImGui::SameLine(lbl.position);
ImGui::Text("%d", s_tx_count);
// Size
ImGui::Text("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("Difficulty:");
ImGui::SameLine(lbl.position);
ImGui::Text("%.4f", s_difficulty);
// Bits
ImGui::Text("Bits:");
ImGui::SameLine(lbl.position);
ImGui::Text("%s", s_bits.c_str());
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Merkle root
ImGui::Text("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("Previous Block:");
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 = prev_short.substr(0, hashFrontLbl.truncate) + "..." + prev_short.substr(prev_short.length() - hashBackLbl.truncate);
}
ImGui::Text("%s", prev_short.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Click to view previous block");
}
if (ImGui::IsItemClicked() && s_height > 1) {
s_height--;
s_has_data = false;
}
}
// Next block
if (!s_next_hash.empty()) {
ImGui::Text("Next Block:");
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 = next_short.substr(0, hashFrontLbl.truncate) + "..." + next_short.substr(next_short.length() - hashBackLbl.truncate);
}
ImGui::Text("%s", next_short.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Click to view next block");
}
if (ImGui::IsItemClicked()) {
s_height++;
s_has_data = false;
}
}
}
ImGui::Spacing();
ImGui::Spacing();
// Navigation buttons
if (s_has_data) {
if (s_height > 1) {
if (material::StyledButton("<< Previous", ImVec2(0,0), S.resolveFont(closeBtn.font))) {
s_height--;
s_has_data = false;
s_error.clear();
}
ImGui::SameLine();
}
if (!s_next_hash.empty()) {
if (material::StyledButton("Next >>", ImVec2(0,0), S.resolveFont(closeBtn.font))) {
s_height++;
s_has_data = false;
s_error.clear();
}
}
}
// Close button at bottom
ImGui::SetCursorPosY(ImGui::GetWindowHeight() - 40);
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,35 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
class App;
namespace ui {
/**
* @brief Dialog for viewing block information by height
*
* Allows entering a block height to view:
* - Block hash
* - Timestamp
* - Number of transactions
* - Size, bits, difficulty
* - Previous/next block hash
*/
class BlockInfoDialog {
public:
// Show the dialog, optionally at a specific height
static void show(int initialHeight = -1);
// Render the dialog (call each frame)
static void render(App* app);
};
} // namespace ui
} // namespace dragonx

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,148 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "../layout.h"
#include "../../daemon/embedded_daemon.h"
#include "../../daemon/xmrig_manager.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include <string>
#include <vector>
#include <deque>
#include <mutex>
namespace dragonx {
namespace ui {
/**
* @brief Console tab for daemon output and command input
*
* Shows dragonxd output and allows executing RPC commands.
*/
class ConsoleTab {
public:
ConsoleTab();
~ConsoleTab() = default;
/**
* @brief Render the console tab
* @param daemon Pointer to embedded daemon (may be null)
* @param rpc Pointer to RPC client for command execution
* @param xmrig Pointer to xmrig manager for pool mining output (may be null)
*/
void render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc::RPCWorker* worker, daemon::XmrigManager* xmrig = nullptr);
/**
* @brief Render the RPC Command Reference popup at top-level scope.
* Must be called outside any child window so the modal blocks all input.
*/
void renderCommandsPopupModal();
/**
* @brief Add a line to the console output
*/
void addLine(const std::string& line, ImU32 color = IM_COL32(200, 200, 200, 255));
/**
* @brief Clear console output
*/
void clear();
/**
* @brief Check if auto-scroll is enabled
*/
bool isAutoScrollEnabled() const { return auto_scroll_; }
// Scanline effect toggle (set from settings)
static bool s_scanline_enabled;
// Console output zoom factor (1.0 = default caption font size)
static float s_console_zoom;
// Show/hide daemon output messages
static bool s_daemon_messages_enabled;
// Show only error messages (filter toggle)
static bool s_errors_only_enabled;
/// Refresh console text colors for current theme (call after theme switch)
static void refreshColors();
// Colors — follow active theme (public for use by notification forwarding)
static ImU32 COLOR_COMMAND;
static ImU32 COLOR_RESULT;
static ImU32 COLOR_ERROR;
static ImU32 COLOR_DAEMON;
static ImU32 COLOR_INFO;
private:
struct ConsoleLine {
std::string text;
ImU32 color;
};
void executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc::RPCWorker* worker);
void addCommandResult(const std::string& cmd, const std::string& result, bool is_error = false);
void renderToolbar(daemon::EmbeddedDaemon* daemon);
void renderOutput();
void renderInput(rpc::RPCClient* rpc, rpc::RPCWorker* worker);
void renderCommandsPopup();
// Selection helpers
void handleSelection();
std::string getSelectedText() const;
void clearSelection();
// Convert screen position to line/column
struct TextPos {
int line = -1;
int col = 0;
};
TextPos screenToTextPos(ImVec2 screen_pos, float line_height) const;
bool isPosBeforeOrEqual(const TextPos& a, const TextPos& b) const;
TextPos selectionStart() const; // Returns the earlier of sel_anchor_ and sel_end_
TextPos selectionEnd() const; // Returns the later of sel_anchor_ and sel_end_
std::deque<ConsoleLine> lines_;
std::vector<std::string> command_history_;
int history_index_ = -1;
char input_buffer_[4096] = {0};
bool auto_scroll_ = true;
bool scroll_to_bottom_ = false;
int new_lines_since_scroll_ = 0; // new lines while scrolled up (for indicator)
size_t last_daemon_output_size_ = 0;
size_t last_xmrig_output_size_ = 0;
bool shown_startup_message_ = false;
daemon::EmbeddedDaemon::State last_daemon_state_ = daemon::EmbeddedDaemon::State::Stopped;
// Text selection state
bool is_selecting_ = false;
bool has_selection_ = false;
TextPos sel_anchor_; // Where the mouse was first pressed
TextPos sel_end_; // Where the mouse currently is / was released
float output_scroll_y_ = 0.0f; // Track scroll position for selection
ImVec2 output_origin_ = {0, 0}; // Top-left of output area
float output_line_height_ = 0.0f; // Text line height (for scanline alignment)
std::mutex lines_mutex_;
// Output filter
char filter_text_[128] = {0};
mutable std::vector<int> visible_indices_; // Cached for selection mapping
// Wrapped line height caching (for variable-height text wrapping)
mutable std::vector<float> wrapped_heights_; // Height of each visible line (accounts for wrapping)
mutable std::vector<float> cumulative_y_offsets_; // Cumulative Y offset for each visible line
mutable float total_wrapped_height_ = 0.0f; // Total height of all visible lines
mutable float cached_wrap_width_ = 0.0f; // Wrap width used for cached heights
// Commands popup
bool show_commands_popup_ = false;
};
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,258 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "export_all_keys_dialog.h"
#include "../../app.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../material/type.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <string>
#include <vector>
#include <fstream>
#include <ctime>
namespace dragonx {
namespace ui {
using json = nlohmann::json;
// Static state
static bool s_open = false;
static bool s_exporting = false;
static std::string s_status;
static std::string s_exported_keys;
static int s_total_addresses = 0;
static int s_exported_count = 0;
static bool s_include_z = true;
static bool s_include_t = true;
static char s_filename[256] = "";
void ExportAllKeysDialog::show()
{
s_open = true;
s_exporting = false;
s_status.clear();
s_exported_keys.clear();
s_total_addresses = 0;
s_exported_count = 0;
s_include_z = true;
s_include_t = true;
// Generate default filename with timestamp
std::time_t now = std::time(nullptr);
char timebuf[32];
std::strftime(timebuf, sizeof(timebuf), "%Y%m%d_%H%M%S", std::localtime(&now));
snprintf(s_filename, sizeof(s_filename), "dragonx_keys_%s.txt", timebuf);
}
bool ExportAllKeysDialog::isOpen()
{
return s_open;
}
void ExportAllKeysDialog::render(App* app)
{
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.export-all-keys");
auto exportBtn = S.button("dialogs.export-all-keys", "export-button");
auto closeBtn = S.button("dialogs.export-all-keys", "close-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Export All Private Keys");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Export All Private Keys", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
// Warning
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
ImGui::PushFont(material::Type().iconSmall());
ImGui::Text(ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f);
ImGui::TextWrapped("DANGER: This will export ALL private keys from your wallet! "
"Anyone with access to this file can steal your funds. "
"Store it securely and delete after use.");
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (s_exporting) {
ImGui::BeginDisabled();
}
// Options
ImGui::Text("Export options:");
ImGui::Checkbox("Include Z-addresses (shielded)", &s_include_z);
ImGui::Checkbox("Include T-addresses (transparent)", &s_include_t);
ImGui::Spacing();
// Filename
ImGui::Text("Output filename:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##Filename", s_filename, sizeof(s_filename));
ImGui::Spacing();
ImGui::TextDisabled("File will be saved in: ~/.config/ObsidianDragon/");
if (s_exporting) {
ImGui::EndDisabled();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Export button
if (s_exporting) {
ImGui::BeginDisabled();
}
if (material::StyledButton("Export Keys", ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
if (!s_include_z && !s_include_t) {
Notifications::instance().warning("Select at least one address type");
} else if (!app->rpc() || !app->rpc()->isConnected()) {
Notifications::instance().error("Not connected to daemon");
} else {
s_exporting = true;
s_exported_keys.clear();
s_exported_count = 0;
s_status = "Exporting keys...";
const auto& state = app->getWalletState();
// Count total addresses to export
s_total_addresses = 0;
if (s_include_z) s_total_addresses += static_cast<int>(state.z_addresses.size());
if (s_include_t) s_total_addresses += static_cast<int>(state.t_addresses.size());
if (s_total_addresses == 0) {
s_exporting = false;
s_status = "No addresses to export";
return;
}
// Collect addresses to export (copy for worker thread)
std::vector<std::string> z_addrs, t_addrs;
if (s_include_z) {
for (const auto& a : state.z_addresses) z_addrs.push_back(a.address);
}
if (s_include_t) {
for (const auto& a : state.t_addresses) t_addrs.push_back(a.address);
}
std::string filename(s_filename);
// Run all key exports on worker thread
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), z_addrs, t_addrs, filename]() -> rpc::RPCWorker::MainCb {
std::string keys;
int exported = 0;
int total = static_cast<int>(z_addrs.size() + t_addrs.size());
// Header
keys = "# DragonX Wallet - Private Keys Export\n";
keys += "# Generated: ";
std::time_t now = std::time(nullptr);
char timebuf[64];
std::strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S\n", std::localtime(&now));
keys += timebuf;
keys += "# KEEP THIS FILE SECURE!\n\n";
// Export Z-addresses
if (!z_addrs.empty()) {
keys += "# === Z-Addresses (Shielded) ===\n\n";
for (const auto& addr : z_addrs) {
try {
auto result = rpc->call("z_exportkey", {addr});
if (result.is_string()) {
keys += "# Address: " + addr + "\n";
keys += result.get<std::string>() + "\n\n";
}
} catch (...) {}
exported++;
}
}
// Export T-addresses
if (!t_addrs.empty()) {
keys += "# === T-Addresses (Transparent) ===\n\n";
for (const auto& addr : t_addrs) {
try {
auto result = rpc->call("dumpprivkey", {addr});
if (result.is_string()) {
keys += "# Address: " + addr + "\n";
keys += result.get<std::string>() + "\n\n";
}
} catch (...) {}
exported++;
}
}
// Save to file (still on worker thread)
std::string configDir = util::Platform::getConfigDir();
std::string filepath = configDir + "/" + filename;
bool writeOk = false;
{
std::ofstream file(filepath);
if (file.is_open()) {
file << keys;
file.close();
writeOk = true;
}
}
return [exported, total, filepath, writeOk]() {
s_exported_count = exported;
s_exporting = false;
if (writeOk) {
s_status = "Exported to: " + filepath;
Notifications::instance().success("Keys exported successfully");
} else {
s_status = "Failed to write file";
Notifications::instance().error("Failed to save key file");
}
};
});
}
}
}
if (s_exporting) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled("Exporting %d/%d...", s_exported_count, s_total_addresses);
}
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
// Status
if (!s_status.empty()) {
ImGui::Spacing();
ImGui::TextWrapped("%s", s_status.c_str());
}
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,33 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
class App;
namespace ui {
/**
* @brief Dialog for exporting all wallet keys to a file
*
* Exports all z-address and t-address private keys to a text file
*/
class ExportAllKeysDialog {
public:
// Show the dialog
static void show();
// Render the dialog (call each frame)
static void render(App* app);
// Check if dialog is open
static bool isOpen();
};
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,173 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "export_transactions_dialog.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "imgui.h"
#include <string>
#include <fstream>
#include <sstream>
#include <ctime>
#include <iomanip>
namespace dragonx {
namespace ui {
// Static state
static bool s_open = false;
static char s_filename[256] = "";
static std::string s_status;
// Helper to escape CSV field
static std::string escapeCSV(const std::string& field)
{
if (field.find(',') != std::string::npos ||
field.find('"') != std::string::npos ||
field.find('\n') != std::string::npos) {
// Escape quotes and wrap in quotes
std::string escaped;
escaped.reserve(field.size() + 4);
escaped += '"';
for (char c : field) {
if (c == '"') escaped += "\"\"";
else escaped += c;
}
escaped += '"';
return escaped;
}
return field;
}
void ExportTransactionsDialog::show()
{
s_open = true;
s_status.clear();
// Generate default filename with timestamp
std::time_t now = std::time(nullptr);
char timebuf[32];
std::strftime(timebuf, sizeof(timebuf), "%Y%m%d_%H%M%S", std::localtime(&now));
snprintf(s_filename, sizeof(s_filename), "dragonx_transactions_%s.csv", timebuf);
}
bool ExportTransactionsDialog::isOpen()
{
return s_open;
}
void ExportTransactionsDialog::render(App* app)
{
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.export-transactions");
auto exportBtn = S.button("dialogs.export-transactions", "export-button");
auto closeBtn = S.button("dialogs.export-transactions", "close-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Export Transactions to CSV");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Export Transactions to CSV", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
const auto& state = app->getWalletState();
ImGui::Text("Export %zu transactions to CSV file.", state.transactions.size());
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Filename
ImGui::Text("Output filename:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##Filename", s_filename, sizeof(s_filename));
ImGui::Spacing();
ImGui::TextDisabled("File will be saved in: ~/.config/ObsidianDragon/");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Export button
if (material::StyledButton("Export", ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
if (state.transactions.empty()) {
Notifications::instance().warning("No transactions to export");
} else {
std::string configDir = util::Platform::getConfigDir();
std::string filepath = configDir + "/" + s_filename;
std::ofstream file(filepath);
if (!file.is_open()) {
s_status = "Failed to create file";
Notifications::instance().error("Failed to create CSV file");
} else {
// Write CSV header
file << "Date,Type,Amount,Address,TXID,Confirmations,Memo\n";
// Write transactions
for (const auto& tx : state.transactions) {
// Date
std::time_t t = static_cast<std::time_t>(tx.timestamp);
char datebuf[32];
std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
file << datebuf << ",";
// Type
file << escapeCSV(tx.type) << ",";
// Amount
std::ostringstream amt;
amt << std::fixed << std::setprecision(8) << tx.amount;
file << amt.str() << ",";
// Address
file << escapeCSV(tx.address) << ",";
// TXID
file << escapeCSV(tx.txid) << ",";
// Confirmations
file << tx.confirmations << ",";
// Memo
file << escapeCSV(tx.memo) << "\n";
}
file.close();
s_status = "Exported " + std::to_string(state.transactions.size()) +
" transactions to: " + filepath;
Notifications::instance().success("Transactions exported successfully");
}
}
}
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
// Status
if (!s_status.empty()) {
ImGui::Spacing();
ImGui::TextWrapped("%s", s_status.c_str());
}
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,31 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
class App;
namespace ui {
/**
* @brief Dialog for exporting transactions to CSV
*/
class ExportTransactionsDialog {
public:
// Show the dialog
static void show();
// Render the dialog (call each frame)
static void render(App* app);
// Check if dialog is open
static bool isOpen();
};
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,291 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "import_key_dialog.h"
#include "../../app.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../material/type.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <string>
#include <vector>
#include <sstream>
namespace dragonx {
namespace ui {
using json = nlohmann::json;
// Static state
static bool s_open = false;
static char s_key_input[4096] = "";
static bool s_rescan = true;
static int s_rescan_height = 0; // 0 = full rescan
static bool s_importing = false;
static std::string s_status;
static int s_total_keys = 0;
static int s_imported_keys = 0;
static int s_failed_keys = 0;
// Helper to detect key type
static std::string detectKeyType(const std::string& key)
{
if (key.empty()) return "unknown";
// Z-address spending keys start with "secret-extended-key-" or "SK" prefix patterns
if (key.substr(0, 20) == "secret-extended-key-") {
return "z-spending";
}
// Legacy z-addr keys (SK prefix)
if (key.length() >= 2 && key[0] == 'S' && key[1] == 'K') {
return "z-spending";
}
// T-address private keys (WIF format) - start with 5, K, or L for Bitcoin-derived
// DragonX/HUSH uses different prefixes
if (key.length() >= 51 && key.length() <= 52) {
char first = key[0];
if (first == '5' || first == 'K' || first == 'L' || first == 'U') {
return "t-privkey";
}
}
return "unknown";
}
// Helper to split input into individual keys
static std::vector<std::string> splitKeys(const std::string& input)
{
std::vector<std::string> keys;
std::istringstream stream(input);
std::string line;
while (std::getline(stream, line)) {
// Trim whitespace
size_t start = line.find_first_not_of(" \t\r\n");
size_t end = line.find_last_not_of(" \t\r\n");
if (start != std::string::npos && end != std::string::npos) {
std::string key = line.substr(start, end - start + 1);
if (!key.empty() && key[0] != '#') { // Skip comments
keys.push_back(key);
}
}
}
return keys;
}
void ImportKeyDialog::show()
{
s_open = true;
s_key_input[0] = '\0';
s_rescan = true;
s_rescan_height = 0;
s_importing = false;
s_status.clear();
s_total_keys = 0;
s_imported_keys = 0;
s_failed_keys = 0;
}
bool ImportKeyDialog::isOpen()
{
return s_open;
}
void ImportKeyDialog::render(App* app)
{
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.import-key");
auto keyInput = S.input("dialogs.import-key", "key-input");
auto rescanInput = S.input("dialogs.import-key", "rescan-height-input");
auto importBtn = S.button("dialogs.import-key", "import-button");
auto closeBtn = S.button("dialogs.import-key", "close-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Import Private Key");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Import Private Key", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
// Warning
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.0f, 1.0f));
ImGui::PushFont(material::Type().iconSmall());
ImGui::Text(ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f);
ImGui::TextWrapped("Warning: Never share your private keys! "
"Importing keys from untrusted sources can compromise your wallet.");
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Key input
ImGui::Text("Private Key(s):");
ImGui::SameLine();
ImGui::TextDisabled("(?)");
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Enter one or more private keys, one per line.\n"
"Supports both z-address and t-address keys.\n"
"Lines starting with # are treated as comments.");
}
if (s_importing) {
ImGui::BeginDisabled();
}
ImGui::SetNextItemWidth(-1);
ImGui::InputTextMultiline("##KeyInput", s_key_input, sizeof(s_key_input),
ImVec2(-1, keyInput.height > 0 ? keyInput.height : 150), ImGuiInputTextFlags_AllowTabInput);
// Paste button
if (material::StyledButton("Paste from Clipboard", ImVec2(0,0), S.resolveFont(importBtn.font))) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
strncpy(s_key_input, clipboard, sizeof(s_key_input) - 1);
}
}
ImGui::SameLine();
if (material::StyledButton("Clear", ImVec2(0,0), S.resolveFont(importBtn.font))) {
s_key_input[0] = '\0';
}
ImGui::Spacing();
// Rescan options
ImGui::Checkbox("Rescan blockchain after import", &s_rescan);
if (s_rescan) {
ImGui::Indent();
ImGui::Text("Start height:");
ImGui::SameLine();
ImGui::SetNextItemWidth(rescanInput.width);
ImGui::InputInt("##RescanHeight", &s_rescan_height);
if (s_rescan_height < 0) s_rescan_height = 0;
ImGui::SameLine();
ImGui::TextDisabled("(0 = full rescan)");
ImGui::Unindent();
}
if (s_importing) {
ImGui::EndDisabled();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Import button
if (s_importing) {
ImGui::BeginDisabled();
}
if (material::StyledButton("Import Key(s)", ImVec2(importBtn.width, 0), S.resolveFont(importBtn.font))) {
auto keys = splitKeys(s_key_input);
if (keys.empty()) {
Notifications::instance().warning("No valid keys found in input");
} else if (!app->rpc() || !app->rpc()->isConnected()) {
Notifications::instance().error("Not connected to daemon");
} else {
s_importing = true;
s_total_keys = static_cast<int>(keys.size());
s_imported_keys = 0;
s_failed_keys = 0;
s_status = "Importing...";
// Import keys on worker thread to avoid freezing UI
bool rescan = s_rescan;
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), keys, rescan]() -> rpc::RPCWorker::MainCb {
int imported = 0;
int failed = 0;
for (const auto& key : keys) {
std::string keyType = detectKeyType(key);
try {
if (keyType == "z-spending") {
rpc->call("z_importkey", {key, rescan ? "yes" : "no"});
imported++;
} else if (keyType == "t-privkey") {
rpc->call("importprivkey", {key, "", rescan});
imported++;
} else {
failed++;
}
} catch (...) {
failed++;
}
}
return [imported, failed]() {
s_imported_keys = imported;
s_failed_keys = failed;
s_importing = false;
char buf[128];
snprintf(buf, sizeof(buf), "Import complete: %d success, %d failed",
imported, failed);
s_status = buf;
if (imported > 0) {
Notifications::instance().success("Keys imported successfully");
}
};
});
}
}
}
if (s_importing) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled("Importing %d/%d...", s_imported_keys + s_failed_keys, s_total_keys);
}
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
// Status
if (!s_status.empty()) {
ImGui::Spacing();
bool success = s_failed_keys == 0 && !s_importing;
ImGui::TextColored(
success ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) : ImVec4(1.0f, 0.8f, 0.0f, 1.0f),
"%s", s_status.c_str()
);
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Help text
ImGui::TextDisabled("Supported key formats:");
ImGui::BulletText("Z-address spending keys (secret-extended-key-...)");
ImGui::BulletText("T-address WIF private keys");
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,36 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
class App;
namespace ui {
/**
* @brief Dialog for importing private keys
*
* Supports importing:
* - Z-address private keys (z_importkey)
* - T-address private keys (importprivkey)
* - Batch import (multiple keys, one per line)
*/
class ImportKeyDialog {
public:
// Show the dialog
static void show();
// Render the dialog (call each frame)
static void render(App* app);
// Check if dialog is open
static bool isOpen();
};
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,247 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "key_export_dialog.h"
#include "../../app.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
// Static member initialization
bool KeyExportDialog::s_open = false;
bool KeyExportDialog::s_fetching = false;
bool KeyExportDialog::s_show_key = false;
KeyExportDialog::KeyType KeyExportDialog::s_key_type = KeyExportDialog::KeyType::Private;
std::string KeyExportDialog::s_address;
std::string KeyExportDialog::s_key;
std::string KeyExportDialog::s_error;
void KeyExportDialog::show(const std::string& address, KeyType type)
{
s_open = true;
s_fetching = false;
s_show_key = false;
s_key_type = type;
s_address = address;
s_key.clear();
s_error.clear();
}
bool KeyExportDialog::isOpen()
{
return s_open;
}
void KeyExportDialog::render(App* app)
{
if (!s_open) return;
const char* title = (s_key_type == KeyType::Private) ?
TR("export_private_key") : TR("export_viewing_key");
auto& S = schema::UI();
auto win = S.window("dialogs.key-export");
auto warningBox = S.drawElement("dialogs.key-export", "warning-box");
auto addrInput = S.input("dialogs.key-export", "address-input");
auto revealBtn = S.button("dialogs.key-export", "reveal-button");
auto keyDisplay = S.drawElement("dialogs.key-export", "key-display");
auto toggleBtn = S.button("dialogs.key-export", "toggle-button");
auto copyBtn = S.button("dialogs.key-export", "copy-button");
auto closeBtn = S.button("dialogs.key-export", "close-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImGui::OpenPopup(title);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
const auto& acrylicTheme = GetCurrentAcrylicTheme();
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal(title, &s_open,
ImGuiWindowFlags_NoResize, acrylicTheme.popup)) {
ImGui::Spacing();
// Warning section with colored background
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.2f, 0.2f, 0.3f));
ImGui::BeginChild("WarningBox", ImVec2(-1, warningBox.height > 0 ? warningBox.height : 80), true);
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), " WARNING!");
ImGui::Spacing();
if (s_key_type == KeyType::Private) {
ImGui::TextWrapped(" Keep this key SECRET! Anyone with this key can spend your "
"funds. Never share it online or with untrusted parties.");
} else {
ImGui::TextWrapped(" This viewing key allows others to see your incoming transactions "
"and balance, but NOT spend your funds. Share only with trusted parties.");
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Address display
ImGui::Text("Address:");
// Determine if it's a z-address (longer) or t-address
bool is_zaddr = s_address.length() > 50;
if (is_zaddr) {
// Use multiline for z-addresses
char addr_buf[512];
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
addr_buf[sizeof(addr_buf) - 1] = '\0';
ImGui::InputTextMultiline("##Address", addr_buf, sizeof(addr_buf),
ImVec2(-1, addrInput.height > 0 ? addrInput.height : 60), ImGuiInputTextFlags_ReadOnly);
} else {
char addr_buf[128];
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
addr_buf[sizeof(addr_buf) - 1] = '\0';
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
}
ImGui::Spacing();
// Key display section
const char* key_label = (s_key_type == KeyType::Private) ? "Private Key:" : "Viewing Key:";
ImGui::Text("%s", key_label);
if (s_fetching) {
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Fetching key from wallet...");
} else if (!s_error.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Error: %s", s_error.c_str());
} else if (s_key.empty()) {
// Show button to fetch key
if (material::StyledButton("Reveal Key", ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
s_fetching = true;
// Check if z-address or t-address
bool is_zaddress = (s_address.length() > 50 || s_address[0] == 'z');
if (s_key_type == KeyType::Private) {
// Export private key
std::string addr = s_address;
std::string method = is_zaddress ? "z_exportkey" : "dumpprivkey";
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), addr, method]() -> rpc::RPCWorker::MainCb {
std::string key;
std::string error;
try {
auto result = rpc->call(method, {addr});
key = result.get<std::string>();
} catch (const std::exception& e) {
error = e.what();
}
return [key, error]() {
if (error.empty()) {
s_key = key;
s_show_key = false; // Don't show by default
} else {
s_error = error;
}
s_fetching = false;
};
});
}
} else {
// Export viewing key (only for z-addresses)
if (is_zaddress) {
std::string addr = s_address;
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), addr]() -> rpc::RPCWorker::MainCb {
std::string key;
std::string error;
try {
auto result = rpc->call("z_exportviewingkey", {addr});
key = result.get<std::string>();
} catch (const std::exception& e) {
error = e.what();
}
return [key, error]() {
if (error.empty()) {
s_key = key;
s_show_key = true; // Viewing keys are less sensitive
} else {
s_error = error;
}
s_fetching = false;
};
});
}
} else {
s_error = "Viewing keys are only available for shielded (z) addresses";
s_fetching = false;
}
}
}
ImGui::SameLine();
ImGui::TextDisabled("Click to retrieve the key from your wallet");
} else {
// Key has been fetched - display it
if (s_show_key) {
// Show the actual key
char key_buf[1024];
strncpy(key_buf, s_key.c_str(), sizeof(key_buf) - 1);
key_buf[sizeof(key_buf) - 1] = '\0';
ImGui::InputTextMultiline("##Key", key_buf, sizeof(key_buf),
ImVec2(-1, keyDisplay.height > 0 ? keyDisplay.height : 80), ImGuiInputTextFlags_ReadOnly);
} else {
// Show masked
std::string masked(s_key.length(), '*');
char masked_buf[1024];
strncpy(masked_buf, masked.c_str(), sizeof(masked_buf) - 1);
masked_buf[sizeof(masked_buf) - 1] = '\0';
ImGui::InputTextMultiline("##Key", masked_buf, sizeof(masked_buf),
ImVec2(-1, keyDisplay.height > 0 ? keyDisplay.height : 80), ImGuiInputTextFlags_ReadOnly);
}
// Show/Hide and Copy buttons
ImGui::Spacing();
if (material::StyledButton(s_show_key ? "Hide" : "Show", ImVec2(toggleBtn.width, 0), S.resolveFont(toggleBtn.font))) {
s_show_key = !s_show_key;
}
ImGui::SameLine();
if (material::StyledButton(TR("copy_to_clipboard"), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
ImGui::SetClipboardText(s_key.c_str());
// Could add a notification here
}
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Close button
float button_width = closeBtn.width;
float avail_width = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail_width - button_width) / 2.0f);
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
// Clear sensitive data
s_key.clear();
s_show_key = false;
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,57 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
class App;
namespace ui {
/**
* @brief Dialog for exporting private/viewing keys
*
* Displays the private or viewing key for a given address with
* security warnings and copy functionality.
*/
class KeyExportDialog {
public:
enum class KeyType {
Private,
Viewing
};
/**
* @brief Show the key export dialog for an address
* @param address The address to export key for
* @param type The type of key to export
*/
static void show(const std::string& address, KeyType type);
/**
* @brief Render the dialog (call every frame)
* @param app Pointer to app instance for RPC calls
*/
static void render(App* app);
/**
* @brief Check if dialog is currently open
*/
static bool isOpen();
private:
static bool s_open;
static bool s_fetching;
static bool s_show_key;
static KeyType s_key_type;
static std::string s_address;
static std::string s_key;
static std::string s_error;
};
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,20 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "main_window.h"
#include "../../app.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
void RenderMainWindow(App* app)
{
// Main window rendering is handled by App::render()
// This file is for any shared window utilities
(void)app;
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,19 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
/**
* @brief Render the main window content
* Called from App::render(), manages the overall layout
*/
void RenderMainWindow(App* app);
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,819 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "market_tab.h"
#include "../../app.h"
#include "../../config/version.h"
#include "../../data/wallet_state.h"
#include "../../config/settings.h"
#include "../../data/exchange_info.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../material/typography.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../../util/platform.h"
#include "../layout.h"
#include "imgui.h"
#include <algorithm>
#include <cmath>
#include <ctime>
namespace dragonx {
namespace ui {
using namespace material;
// ---- Market tab persistent state ----
static std::vector<double> s_price_history;
static std::vector<double> s_time_history;
static bool s_history_initialized = false;
static double s_last_refresh_time = 0.0;
// Exchange / pair selection
static int s_exchange_idx = 0;
static int s_pair_idx = 0;
static float s_pair_scroll = 0.0f;
static float s_pair_scroll_target = 0.0f;
static bool s_pair_dragging = false;
static float s_pair_drag_start_x = 0.0f;
static float s_pair_drag_start_scroll = 0.0f;
static bool s_market_state_loaded = false;
// Helper: load selected exchange/pair from settings
static void LoadMarketState(config::Settings* settings)
{
if (s_market_state_loaded || !settings) return;
s_market_state_loaded = true;
const auto& registry = data::getExchangeRegistry();
std::string savedExchange = settings->getSelectedExchange();
std::string savedPair = settings->getSelectedPair();
for (int ei = 0; ei < (int)registry.size(); ei++) {
if (registry[ei].name == savedExchange) {
s_exchange_idx = ei;
for (int pi = 0; pi < (int)registry[ei].pairs.size(); pi++) {
if (registry[ei].pairs[pi].displayName == savedPair) {
s_pair_idx = pi;
break;
}
}
break;
}
}
}
// Helper: format compact currency
static std::string FormatCompactUSD(double val)
{
char buf[64];
if (val >= 1e9) snprintf(buf, sizeof(buf), "$%.2fB", val / 1e9);
else if (val >= 1e6) snprintf(buf, sizeof(buf), "$%.2fM", val / 1e6);
else if (val >= 1e3) snprintf(buf, sizeof(buf), "$%.2fK", val / 1e3);
else snprintf(buf, sizeof(buf), "$%.2f", val);
return std::string(buf);
}
// Helper: format price to sensible precision
static std::string FormatPrice(double price)
{
char buf[64];
if (price >= 0.01) snprintf(buf, sizeof(buf), "$%.4f", price);
else if (price >= 0.0001) snprintf(buf, sizeof(buf), "$%.6f", price);
else snprintf(buf, sizeof(buf), "$%.8f", price);
return std::string(buf);
}
void RenderMarketTab(App* app)
{
auto& S = schema::UI();
auto summaryPanel = S.table("tabs.market", "summary-panel");
auto btcPriceLbl = S.label("tabs.market", "btc-price-label");
auto change24hLbl = S.label("tabs.market", "change-24h-label");
auto volumeLbl = S.label("tabs.market", "volume-label");
auto volumeValLbl = S.label("tabs.market", "volume-value-label");
auto mktCapLbl = S.label("tabs.market", "market-cap-label");
auto mktCapValLbl = S.label("tabs.market", "market-cap-value-label");
auto chartElem = S.drawElement("tabs.market", "chart");
auto portfolioValLbl = S.label("tabs.market", "portfolio-value-label");
auto portfolioBtcLbl = S.label("tabs.market", "portfolio-btc-label");
const auto& state = app->getWalletState();
const auto& market = state.market;
// Load persisted exchange/pair on first frame
LoadMarketState(app->settings());
// Exchange registry
const auto& registry = data::getExchangeRegistry();
if (s_exchange_idx >= (int)registry.size()) s_exchange_idx = 0;
const auto& currentExchange = registry[s_exchange_idx];
if (s_pair_idx >= (int)currentExchange.pairs.size()) s_pair_idx = 0;
// Non-scrolling container — content resizes to fit available height
ImVec2 marketAvail = ImGui::GetContentRegionAvail();
ImGui::BeginChild("##MarketScroll", marketAvail, false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
// Responsive: scale factors per frame
float availWidth = ImGui::GetContentRegionAvail().x;
float hs = Layout::hScale(availWidth);
float vs = Layout::vScale(marketAvail.y);
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();
ImFont* h4 = Type().h4();
ImFont* body2 = Type().body2();
char buf[128];
// ================================================================
// Proportional section budget — all content fits without scrolling
// ================================================================
float mkSHdr = ovFont->LegacySize + Layout::spacingXs()
+ ImGui::GetStyle().ItemSpacing.y * 2.0f;
float mkGapOver = gap + ImGui::GetStyle().ItemSpacing.y;
float mkOverhead = 3.0f * (mkSHdr + mkGapOver) + 2.0f * mkGapOver;
float pairBarH = S.drawElement("tabs.market", "pair-bar-height").height;
float mkCardBudget = std::max(200.0f, marketAvail.y - mkOverhead);
Layout::SectionBudget mb(mkCardBudget);
float statsMarketBudH = mb.allocate(0.14f, S.drawElement("tabs.market", "stats-card-min-height").size);
float portfolioBudgetH = mb.allocate(0.18f, 50.0f);
// ================================================================
// PRICE HERO — Large glass card with price + change badge
// ================================================================
{
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float cardH = std::max(S.drawElement("tabs.market", "hero-card-min-height").size, S.drawElement("tabs.market", "hero-card-height").size * vs);
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
// Accent stripe — clipped to card rounded corners
{
float sw = S.drawElement("tabs.market", "accent-stripe-width").size;
dl->PushClipRect(cardMin, ImVec2(cardMin.x + sw, cardMax.y), true);
dl->AddRectFilled(cardMin, cardMax, WithAlpha(S.resolveColor("var(--accent-market)", Success()), 200),
glassSpec.rounding, ImDrawFlags_RoundCornersLeft);
dl->PopClipRect();
}
float cx = cardMin.x + Layout::spacingLg();
float cy = cardMin.y + Layout::spacingLg();
if (market.price_usd > 0) {
// Large price with text shadow
std::string priceStr = FormatPrice(market.price_usd);
ImU32 priceCol = Success();
DrawTextShadow(dl, h4, h4->LegacySize, ImVec2(cx, cy), priceCol, priceStr.c_str());
// BTC price beside it
float priceW = h4->CalcTextSizeA(h4->LegacySize, FLT_MAX, 0, priceStr.c_str()).x;
snprintf(buf, sizeof(buf), "%.10f BTC", market.price_btc);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx + priceW + Layout::spacingXl(), cy + (h4->LegacySize - capFont->LegacySize)),
OnSurfaceMedium(), buf);
// 24h change badge
float badgeY = cy + h4->LegacySize + 8;
ImU32 chgCol = market.change_24h >= 0 ? Success() : Error();
snprintf(buf, sizeof(buf), "%s%.2f%%", market.change_24h >= 0 ? "+" : "", market.change_24h);
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
float badgePad = Layout::spacingSm() + Layout::spacingXs();
ImVec2 bMin(cx, badgeY);
ImVec2 bMax(cx + txtSz.x + badgePad * 2, badgeY + txtSz.y + badgePad);
ImU32 badgeBg = market.change_24h >= 0 ? WithAlpha(Success(), 30) : WithAlpha(Error(), 30);
dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + badgePad, badgeY + badgePad * 0.5f), chgCol, buf);
// "24h" label after badge
dl->AddText(capFont, capFont->LegacySize,
ImVec2(bMax.x + 6, badgeY + badgePad * 0.5f), OnSurfaceDisabled(), "24h");
} else {
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx, cy + 10), OnSurfaceDisabled(), "Price data unavailable");
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
ImGui::Dummy(ImVec2(0, gap));
}
// ================================================================
// STATS — Three glass cards (Price | Volume | Market Cap)
// ================================================================
{
float cardW = (availWidth - 2 * gap) / 3.0f;
float cardH = std::min(StatCardHeight(vs), statsMarketBudH);
ImVec2 origin = ImGui::GetCursorScreenPos();
struct StatInfo { const char* label; std::string value; ImU32 col; ImU32 accent; };
StatInfo cards[3] = {
{"PRICE", market.price_usd > 0 ? FormatPrice(market.price_usd) : "N/A",
OnSurface(), WithAlpha(Success(), 200)},
{"24H VOLUME", FormatCompactUSD(market.volume_24h),
OnSurface(), WithAlpha(Secondary(), 200)},
{"MARKET CAP", FormatCompactUSD(market.market_cap),
OnSurface(), WithAlpha(Warning(), 200)},
};
for (int i = 0; i < 3; i++) {
float xOff = i * (cardW + gap);
ImVec2 cMin(origin.x + xOff, origin.y);
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
StatCardSpec sc;
sc.overline = cards[i].label;
sc.value = cards[i].value.c_str();
sc.valueCol = cards[i].col;
sc.accentCol = cards[i].accent;
sc.centered = true;
DrawStatCard(dl, cMin, cMax, sc, glassSpec);
}
ImGui::Dummy(ImVec2(availWidth, cardH));
ImGui::Dummy(ImVec2(0, gap));
}
// ================================================================
// PRICE CHART — Custom drawn inside glass panel (matches app design)
// ================================================================
{
// Initialize history with simulated data if not set
if (!s_history_initialized && market.price_usd > 0) {
s_price_history.clear();
s_time_history.clear();
double base = market.price_usd;
for (int i = 0; i < 24; i++) {
double variance = ((rand() % 1000) - 500) / 10000.0 * base;
s_price_history.push_back(base + variance);
s_time_history.push_back(static_cast<double>(i));
}
s_history_initialized = true;
}
// Chart height from schema
float chartH = std::max(60.0f, chartElem.height * vs);
ImVec2 chartMin = ImGui::GetCursorScreenPos();
ImVec2 chartMax(chartMin.x + availWidth, chartMin.y + chartH);
DrawGlassPanel(dl, chartMin, chartMax, glassSpec);
if (!s_price_history.empty() && s_price_history.size() >= 2) {
float chartPad = pad;
float labelPadLeft = std::max(S.drawElement("tabs.market", "chart-y-axis-min-padding").size, S.drawElement("tabs.market", "chart-y-axis-padding").size * hs);
float labelPadBottom = Layout::spacingXl();
float plotLeft = chartMin.x + labelPadLeft;
float plotRight = chartMax.x - chartPad;
float plotTop = chartMin.y + chartPad;
float plotBottom = chartMax.y - labelPadBottom;
float plotW = plotRight - plotLeft;
float plotH = plotBottom - plotTop;
// Compute Y range with padding
double yMin = *std::min_element(s_price_history.begin(), s_price_history.end());
double yMax = *std::max_element(s_price_history.begin(), s_price_history.end());
if (yMax <= yMin) { yMax = yMin + 1e-8; }
double yRange = yMax - yMin;
double yPadding = yRange * 0.12;
yMin -= yPadding;
yMax += yPadding;
// Horizontal grid lines (4 lines)
for (int g = 0; g <= 4; g++) {
float gy = plotTop + plotH * (float)g / 4.0f;
dl->AddLine(ImVec2(plotLeft, gy), ImVec2(plotRight, gy),
IM_COL32(255, 255, 255, 12), 1.0f);
double labelVal = yMax - (yMax - yMin) * (double)g / 4.0;
snprintf(buf, sizeof(buf), "$%.6f", labelVal);
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(plotLeft - labelSz.x - 6, gy - labelSz.y * 0.5f),
OnSurfaceDisabled(), buf);
}
// Build points
size_t n = s_price_history.size();
std::vector<ImVec2> points(n);
ImU32 lineCol = market.change_24h >= 0
? WithAlpha(Success(), 220) : WithAlpha(Error(), 220);
ImU32 fillCol = market.change_24h >= 0
? WithAlpha(Success(), 25) : WithAlpha(Error(), 25);
ImU32 dotCol = market.change_24h >= 0
? Success() : Error();
for (size_t i = 0; i < n; i++) {
float t = (n > 1) ? (float)i / (float)(n - 1) : 0.0f;
float x = plotLeft + t * plotW;
float y = plotBottom - (float)((s_price_history[i] - yMin) / (yMax - yMin)) * plotH;
points[i] = ImVec2(x, y);
}
// Fill under curve
for (size_t i = 0; i + 1 < n; i++) {
ImVec2 quad[4] = {
points[i],
points[i + 1],
ImVec2(points[i + 1].x, plotBottom),
ImVec2(points[i].x, plotBottom)
};
dl->AddConvexPolyFilled(quad, 4, fillCol);
}
// Line
dl->AddPolyline(points.data(), (int)points.size(), lineCol, ImDrawFlags_None, S.drawElement("tabs.market", "chart-line-thickness").size);
// Dots
float dotR = std::max(S.drawElement("tabs.market", "chart-dot-min-radius").size, S.drawElement("tabs.market", "chart-dot-radius").size * hs);
for (size_t i = 0; i < n; i++) {
dl->AddCircleFilled(points[i], dotR, dotCol);
}
// X-axis labels
const int xlabels[] = {0, 6, 12, 18, 23};
const char* xlabelText[] = {"24h", "18h", "12h", "6h", "Now"};
for (int xi = 0; xi < 5; xi++) {
int idx = xlabels[xi];
float t = (float)idx / (float)(n - 1);
float x = plotLeft + t * plotW;
ImVec2 lblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, xlabelText[xi]);
float lx = x - lblSz.x * 0.5f;
if (lx < plotLeft) lx = plotLeft;
if (lx + lblSz.x > plotRight) lx = plotRight - lblSz.x;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(lx, plotBottom + 2), OnSurfaceDisabled(), xlabelText[xi]);
}
// Hover crosshair + tooltip
ImVec2 mousePos = ImGui::GetIO().MousePos;
if (mousePos.x >= plotLeft && mousePos.x <= plotRight &&
mousePos.y >= plotTop && mousePos.y <= plotBottom + labelPadBottom)
{
float mx = mousePos.x - plotLeft;
float closest_t = mx / plotW;
int idx = (int)(closest_t * (n - 1) + 0.5f);
if (idx < 0) idx = 0;
if (idx >= (int)n) idx = (int)n - 1;
float px = points[idx].x;
float py = points[idx].y;
dl->AddLine(ImVec2(px, plotTop), ImVec2(px, plotBottom),
IM_COL32(255, 255, 255, 40), 1.0f);
dl->AddLine(ImVec2(plotLeft, py), ImVec2(plotRight, py),
IM_COL32(255, 255, 255, 40), 1.0f);
float hoverDotR = std::max(S.drawElement("tabs.market", "chart-hover-dot-min-radius").size, S.drawElement("tabs.market", "chart-hover-dot-radius").size * hs);
float hoverRingR = std::max(S.drawElement("tabs.market", "chart-hover-ring-min-radius").size, S.drawElement("tabs.market", "chart-hover-ring-radius").size * hs);
dl->AddCircleFilled(ImVec2(px, py), hoverDotR, dotCol);
dl->AddCircle(ImVec2(px, py), hoverRingR, IM_COL32(255, 255, 255, 80), 0, 1.5f);
snprintf(buf, sizeof(buf), "%dh ago: %s",
24 - idx, FormatPrice(s_price_history[idx]).c_str());
ImVec2 tipSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
float tipPad = Layout::spacingSm() + Layout::spacingXs();
float tipX = px + 10;
float tipY = py - tipSz.y - tipPad * 2 - 4;
if (tipX + tipSz.x + tipPad * 2 > plotRight)
tipX = px - tipSz.x - tipPad * 2 - 10;
if (tipY < plotTop) tipY = py + 10;
ImVec2 tipMin(tipX, tipY);
ImVec2 tipMax(tipX + tipSz.x + tipPad * 2, tipY + tipSz.y + tipPad * 2);
dl->AddRectFilled(tipMin, tipMax, IM_COL32(20, 20, 30, 230), 4.0f);
dl->AddRect(tipMin, tipMax, IM_COL32(255, 255, 255, 30), 4.0f, 0, 1.0f);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(tipX + tipPad, tipY + tipPad), dotCol, buf);
}
} else {
const char* msg = "No price history available";
ImVec2 ts = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, msg);
dl->AddText(sub1, sub1->LegacySize,
ImVec2(chartMin.x + (availWidth - ts.x) * 0.5f, chartMin.y + chartH * 0.45f),
OnSurfaceDisabled(), msg);
}
// --- Refresh button + timestamp pinned in chart top-right ---
{
float iconBtnSz = capFont->LegacySize + 8.0f;
float refreshX = chartMax.x - pad;
float refreshY = chartMin.y + pad * 0.5f;
// Draw refresh icon button
ImVec2 btnMin(refreshX - iconBtnSz, refreshY);
ImVec2 btnMax(refreshX, refreshY + iconBtnSz);
bool refreshHov = material::IsRectHovered(btnMin, btnMax);
if (refreshHov) {
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, 20), 4.0f);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
ImFont* iconSmall = material::Typography::instance().iconSmall();
ImVec2 iconSz = iconSmall->CalcTextSizeA(iconSmall->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH);
dl->AddText(iconSmall, iconSmall->LegacySize,
ImVec2(btnMin.x + (iconBtnSz - iconSz.x) * 0.5f, btnMin.y + (iconBtnSz - iconSz.y) * 0.5f),
refreshHov ? OnSurface() : OnSurfaceMedium(), ICON_MD_REFRESH);
ImGui::SetCursorScreenPos(btnMin);
if (ImGui::InvisibleButton("##RefreshMarket", ImVec2(iconBtnSz, iconBtnSz))) {
app->refreshMarketData();
s_history_initialized = false;
s_last_refresh_time = ImGui::GetTime();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Refresh price data");
// Timestamp text to the left of refresh button
if (s_last_refresh_time > 0.0) {
double elapsed = ImGui::GetTime() - s_last_refresh_time;
if (elapsed < 60.0)
snprintf(buf, sizeof(buf), "%.0fs ago", elapsed);
else if (elapsed < 3600.0)
snprintf(buf, sizeof(buf), "%.0fm ago", elapsed / 60.0);
else
snprintf(buf, sizeof(buf), "%.1fh ago", elapsed / 3600.0);
ImVec2 tsSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(btnMin.x - tsSz.x - 6, btnMin.y + (iconBtnSz - tsSz.y) * 0.5f),
OnSurfaceDisabled(), buf);
}
}
ImGui::SetCursorScreenPos(ImVec2(chartMin.x, chartMin.y + chartH));
ImGui::Dummy(ImVec2(availWidth, 0));
}
// ================================================================
// EXCHANGE SELECTOR — Combo dropdown + pair name + trade link
// ================================================================
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.market", "exchange-top-gap").size));
{
float comboW = S.drawElement("tabs.market", "exchange-combo-width").size;
ImGui::PushFont(body2);
ImGui::PushItemWidth(comboW);
if (ImGui::BeginCombo("##ExchangeCombo", currentExchange.name.c_str())) {
for (int i = 0; i < (int)registry.size(); i++) {
bool selected = (i == s_exchange_idx);
if (ImGui::Selectable(registry[i].name.c_str(), selected)) {
if (i != s_exchange_idx) {
s_exchange_idx = i;
s_pair_idx = 0;
s_pair_scroll = 0.0f;
s_pair_scroll_target = 0.0f;
s_history_initialized = false;
app->settings()->setSelectedExchange(registry[i].name);
if (!registry[i].pairs.empty())
app->settings()->setSelectedPair(registry[i].pairs[0].displayName);
app->settings()->save();
app->refreshMarketData();
}
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::PopItemWidth();
// Show current pair name beside combo
if (!currentExchange.pairs.empty()) {
ImGui::SameLine(0, Layout::spacingLg());
Type().textColored(TypeStyle::Subtitle1, OnSurface(),
currentExchange.pairs[s_pair_idx].displayName.c_str());
// "Open on exchange" button
ImGui::SameLine(0, Layout::spacingSm());
ImGui::PushFont(material::Typography::instance().iconSmall());
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 4));
snprintf(buf, sizeof(buf), ICON_MD_OPEN_IN_NEW "##TradeLink");
if (ImGui::Button(buf)) {
util::Platform::openUrl(currentExchange.pairs[s_pair_idx].tradeUrl);
}
ImGui::PopStyleVar();
ImGui::PopFont();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Open on %s", currentExchange.name.c_str());
}
// Attribution
ImGui::SameLine(0, Layout::spacingLg());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "Price data from CoinGecko API");
if (!market.last_updated.empty()) {
ImGui::SameLine(0, 12);
snprintf(buf, sizeof(buf), " \xc2\xb7 Updated %s", market.last_updated.c_str());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
}
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, gap));
}
// ================================================================
// PAIR BAR — Horizontally scrolling chip selector (always visible)
// ================================================================
{
float chipH = S.drawElement("tabs.market", "pair-chip-height").height;
float chipR = S.drawElement("tabs.market", "pair-chip-radius").radius;
float chipSpacing = S.drawElement("tabs.market", "pair-chip-spacing").size;
float fadeW = S.drawElement("tabs.market", "pair-bar-fade-width").size;
float arrowSz = S.drawElement("tabs.market", "pair-bar-arrow-size").size;
ImVec2 barOrigin = ImGui::GetCursorScreenPos();
float barW = availWidth;
float barH = pairBarH;
// Compute total content width of all chips
float totalChipW = 0.0f;
std::vector<float> chipWidths;
for (const auto& pair : currentExchange.pairs) {
float tw = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, pair.displayName.c_str()).x;
float cw = tw + Layout::spacingLg() * 2.0f;
chipWidths.push_back(cw);
totalChipW += cw + chipSpacing;
}
totalChipW -= chipSpacing; // no trailing spacing
float scrollableW = barW - arrowSz * 2.0f - Layout::spacingSm() * 2.0f;
float maxScroll = std::max(0.0f, totalChipW - scrollableW);
// Smooth scroll lerp
s_pair_scroll += (s_pair_scroll_target - s_pair_scroll) * 0.15f;
if (std::abs(s_pair_scroll - s_pair_scroll_target) < 0.5f)
s_pair_scroll = s_pair_scroll_target;
// Clamp
s_pair_scroll_target = std::clamp(s_pair_scroll_target, 0.0f, maxScroll);
s_pair_scroll = std::clamp(s_pair_scroll, 0.0f, maxScroll);
// Left arrow button
float arrowY = barOrigin.y + (barH - arrowSz) * 0.5f;
bool canScrollLeft = s_pair_scroll_target > 0.01f;
ImGui::SetCursorScreenPos(ImVec2(barOrigin.x, arrowY));
ImGui::BeginDisabled(!canScrollLeft);
ImGui::PushFont(material::Typography::instance().iconSmall());
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
if (ImGui::Button(ICON_MD_CHEVRON_LEFT "##PairLeft", ImVec2(arrowSz, arrowSz))) {
// Scroll left by average chip width
float avgChipW = totalChipW / currentExchange.pairs.size();
s_pair_scroll_target -= avgChipW + chipSpacing;
if (s_pair_scroll_target < 0) s_pair_scroll_target = 0;
}
ImGui::PopStyleVar();
ImGui::PopFont();
ImGui::EndDisabled();
// Chip area with clipping
float chipAreaLeft = barOrigin.x + arrowSz + Layout::spacingSm();
float chipAreaRight = barOrigin.x + barW - arrowSz - Layout::spacingSm();
float chipY = barOrigin.y + (barH - chipH) * 0.5f;
dl->PushClipRect(ImVec2(chipAreaLeft, barOrigin.y),
ImVec2(chipAreaRight, barOrigin.y + barH), true);
// Render chips
float cx = chipAreaLeft - s_pair_scroll;
bool anyClicked = false;
for (int i = 0; i < (int)currentExchange.pairs.size(); i++) {
float cw = chipWidths[i];
ImVec2 cMin(cx, chipY);
ImVec2 cMax(cx + cw, chipY + chipH);
bool selected = (i == s_pair_idx);
ImU32 chipBg = selected ? WithAlpha(Primary(), 200) : WithAlpha(OnSurface(), 20);
ImU32 chipBorder = selected ? Primary() : WithAlpha(OnSurface(), 40);
ImU32 chipText = selected ? IM_COL32(255, 255, 255, 255) : OnSurface();
dl->AddRectFilled(cMin, cMax, chipBg, chipR);
dl->AddRect(cMin, cMax, chipBorder, chipR, 0, 1.0f);
ImVec2 textSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0,
currentExchange.pairs[i].displayName.c_str());
float textX = cx + (cw - textSz.x) * 0.5f;
float textY = chipY + (chipH - textSz.y) * 0.5f;
dl->AddText(capFont, capFont->LegacySize, ImVec2(textX, textY), chipText,
currentExchange.pairs[i].displayName.c_str());
// Click detection via invisible button
ImGui::SetCursorScreenPos(cMin);
snprintf(buf, sizeof(buf), "##PairChip%d", i);
if (ImGui::InvisibleButton(buf, ImVec2(cw, chipH))) {
if (!s_pair_dragging || std::abs(ImGui::GetIO().MousePos.x - s_pair_drag_start_x) < 4.0f) {
s_pair_idx = i;
anyClicked = true;
app->settings()->setSelectedPair(currentExchange.pairs[i].displayName);
app->settings()->save();
s_history_initialized = false;
app->refreshMarketData();
}
}
cx += cw + chipSpacing;
}
dl->PopClipRect();
// Fade overlays on edges
ImU32 bgCol = IM_COL32(0, 0, 0, 0);
ImU32 surfaceCol = Surface();
if (s_pair_scroll > 1.0f) {
// Left fade
dl->AddRectFilledMultiColor(
ImVec2(chipAreaLeft, barOrigin.y), ImVec2(chipAreaLeft + fadeW, barOrigin.y + barH),
surfaceCol, bgCol, bgCol, surfaceCol);
}
if (s_pair_scroll < maxScroll - 1.0f) {
// Right fade
dl->AddRectFilledMultiColor(
ImVec2(chipAreaRight - fadeW, barOrigin.y), ImVec2(chipAreaRight, barOrigin.y + barH),
bgCol, surfaceCol, surfaceCol, bgCol);
}
// Right arrow button
bool canScrollRight = s_pair_scroll_target < maxScroll - 0.01f;
ImGui::SetCursorScreenPos(ImVec2(barOrigin.x + barW - arrowSz, arrowY));
ImGui::BeginDisabled(!canScrollRight);
ImGui::PushFont(material::Typography::instance().iconSmall());
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
if (ImGui::Button(ICON_MD_CHEVRON_RIGHT "##PairRight", ImVec2(arrowSz, arrowSz))) {
float avgChipW = totalChipW / currentExchange.pairs.size();
s_pair_scroll_target += avgChipW + chipSpacing;
if (s_pair_scroll_target > maxScroll) s_pair_scroll_target = maxScroll;
}
ImGui::PopStyleVar();
ImGui::PopFont();
ImGui::EndDisabled();
// Mouse wheel horizontal scroll when hovering pair bar
ImVec2 mPos = ImGui::GetIO().MousePos;
if (mPos.x >= barOrigin.x && mPos.x <= barOrigin.x + barW &&
mPos.y >= barOrigin.y && mPos.y <= barOrigin.y + barH) {
float wheel = ImGui::GetIO().MouseWheel;
if (wheel != 0.0f) {
float avgChipW = totalChipW / currentExchange.pairs.size();
s_pair_scroll_target -= wheel * (avgChipW + chipSpacing);
s_pair_scroll_target = std::clamp(s_pair_scroll_target, 0.0f, maxScroll);
}
}
// Mouse drag scrolling
if (ImGui::IsMouseClicked(0) && mPos.x >= chipAreaLeft && mPos.x <= chipAreaRight &&
mPos.y >= barOrigin.y && mPos.y <= barOrigin.y + barH) {
s_pair_dragging = true;
s_pair_drag_start_x = mPos.x;
s_pair_drag_start_scroll = s_pair_scroll_target;
}
if (s_pair_dragging) {
if (ImGui::IsMouseDown(0)) {
float dx = mPos.x - s_pair_drag_start_x;
s_pair_scroll_target = std::clamp(s_pair_drag_start_scroll - dx, 0.0f, maxScroll);
} else {
s_pair_dragging = false;
}
}
// Arrow key navigation
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow) && s_pair_idx > 0) {
s_pair_idx--;
app->settings()->setSelectedPair(currentExchange.pairs[s_pair_idx].displayName);
app->settings()->save();
s_history_initialized = false;
app->refreshMarketData();
anyClicked = true;
}
if (ImGui::IsKeyPressed(ImGuiKey_RightArrow) && s_pair_idx < (int)currentExchange.pairs.size() - 1) {
s_pair_idx++;
app->settings()->setSelectedPair(currentExchange.pairs[s_pair_idx].displayName);
app->settings()->save();
s_history_initialized = false;
app->refreshMarketData();
anyClicked = true;
}
}
// Auto-scroll to keep selected chip visible
if (anyClicked) {
float chipLeft = 0;
for (int i = 0; i < s_pair_idx; i++)
chipLeft += chipWidths[i] + chipSpacing;
float chipRight = chipLeft + chipWidths[s_pair_idx];
if (chipLeft < s_pair_scroll_target)
s_pair_scroll_target = chipLeft - chipSpacing;
if (chipRight > s_pair_scroll_target + scrollableW)
s_pair_scroll_target = chipRight - scrollableW + chipSpacing;
s_pair_scroll_target = std::clamp(s_pair_scroll_target, 0.0f, maxScroll);
}
// Advance cursor past the bar
ImGui::SetCursorScreenPos(ImVec2(barOrigin.x, barOrigin.y + barH));
ImGui::Dummy(ImVec2(availWidth, 0));
ImGui::Dummy(ImVec2(0, gap));
}
// ================================================================
// PORTFOLIO — Glass card with balance breakdown
// ================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PORTFOLIO");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
double total_balance = state.totalBalance;
double private_balance = state.privateBalance;
double transparent_balance = state.transparentBalance;
ImVec2 cardMin = ImGui::GetCursorScreenPos();
float cardH = std::max(50.0f, portfolioBudgetH);
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
// Accent stripe
{
float sw = S.drawElement("tabs.market", "accent-stripe-width").size;
dl->PushClipRect(cardMin, ImVec2(cardMin.x + sw, cardMax.y), true);
dl->AddRectFilled(cardMin, cardMax, WithAlpha(S.resolveColor("var(--accent-portfolio)", Secondary()), 200),
glassSpec.rounding, ImDrawFlags_RoundCornersLeft);
dl->PopClipRect();
}
float cx = cardMin.x + Layout::spacingLg();
float cy = cardMin.y + Layout::spacingLg();
if (market.price_usd > 0) {
double portfolio_usd = total_balance * market.price_usd;
if (portfolio_usd >= 1.0)
snprintf(buf, sizeof(buf), "$%.2f USD", portfolio_usd);
else
snprintf(buf, sizeof(buf), "$%.6f USD", portfolio_usd);
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx, cy), Success(), buf);
double portfolio_btc = total_balance * market.price_btc;
snprintf(buf, sizeof(buf), "%.10f BTC", portfolio_btc);
float valW = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf).x;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cardMax.x - valW - pad, cy + 2), OnSurfaceMedium(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), "No price data");
}
cy += sub1->LegacySize + 8;
snprintf(buf, sizeof(buf), "%.8f %s", total_balance, DRAGONX_TICKER);
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), OnSurface(), buf);
snprintf(buf, sizeof(buf), "Z: %.4f | T: %.4f", private_balance, transparent_balance);
float brkW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cardMax.x - brkW - pad, cy + 2), OnSurfaceDisabled(), buf);
cy += body2->LegacySize + 8;
if (total_balance > 0) {
float barW = availWidth - Layout::spacingXxl() * 1.5f;
float barH = std::max(S.drawElement("tabs.market", "ratio-bar-min-height").size, S.drawElement("tabs.market", "ratio-bar-height").size * vs);
float shieldedRatio = (float)(private_balance / total_balance);
if (shieldedRatio > 1.0f) shieldedRatio = 1.0f;
if (shieldedRatio < 0.0f) shieldedRatio = 0.0f;
float shieldedW = barW * shieldedRatio;
float transpW = barW - shieldedW;
ImVec2 barStart(cx, cy);
dl->AddRectFilled(barStart, ImVec2(barStart.x + barW, barStart.y + barH),
IM_COL32(255, 255, 255, 10), 3.0f);
if (shieldedW > 0.5f)
dl->AddRectFilled(barStart, ImVec2(barStart.x + shieldedW, barStart.y + barH),
WithAlpha(Success(), 200),
transpW > 0.5f ? ImDrawFlags_RoundCornersLeft : ImDrawFlags_RoundCornersAll, 3.0f);
if (transpW > 0.5f)
dl->AddRectFilled(ImVec2(barStart.x + shieldedW, barStart.y),
ImVec2(barStart.x + barW, barStart.y + barH),
WithAlpha(Warning(), 200),
shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f);
int pct = (int)(shieldedRatio * 100.0f + 0.5f);
snprintf(buf, sizeof(buf), "%d%% Shielded", pct);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx, cy + barH + 2), OnSurfaceDisabled(), buf);
}
float actualCardH = (total_balance > 0) ? std::max(60.0f, portfolioBudgetH) : cardH;
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + actualCardH));
ImGui::Dummy(ImVec2(availWidth, 0));
ImGui::Dummy(ImVec2(0, gap));
}
ImGui::EndChild(); // ##MarketScroll
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,19 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
/**
* @brief Render the Market tab
* Shows price information and charts
*/
void RenderMarketTab(App* app);
} // namespace ui
} // namespace dragonx

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
/**
* @brief Render the Mining tab
* Controls for CPU mining with RandomX
*/
void RenderMiningTab(App* app);
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,808 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "peers_tab.h"
#include "../../app.h"
#include "../../data/wallet_state.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.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 <string>
#include <algorithm>
#include <cmath>
#include <cstdio>
namespace dragonx {
namespace ui {
using namespace material;
// Track selected peer for ban action
static int s_selected_peer_idx = -1;
static int s_selected_banned_idx = -1;
// Helper: Extract IP without port
static std::string ExtractIP(const std::string& addr)
{
std::string ip = addr;
if (ip[0] == '[') {
auto pos = ip.rfind("]:");
if (pos != std::string::npos) ip = ip.substr(1, pos - 1);
} else {
auto pos = ip.rfind(':');
if (pos != std::string::npos) ip = ip.substr(0, pos);
}
return ip;
}
void RenderPeersTab(App* app)
{
auto& S = schema::UI();
auto peerTable = S.table("tabs.peers", "peer-table");
auto bannedTable = S.table("tabs.peers", "banned-table");
const auto& state = app->getWalletState();
// Scrollable child to contain all content within available space
ImVec2 peersAvail = ImGui::GetContentRegionAvail();
ImGui::BeginChild("##PeersScroll", peersAvail, false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
// Responsive: scale factors per frame
float availWidth = ImGui::GetContentRegionAvail().x;
float hs = Layout::hScale(availWidth);
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();
ImFont* body2 = Type().body2();
char buf[128];
// ================================================================
// BLOCKCHAIN & PEERS CARDS — Side by side
// ================================================================
float infoCardsH = 0;
{
const auto& mining = state.mining;
// Compute peer stats
int totalPeers = (int)state.peers.size();
int inboundCount = 0;
int outboundCount = 0;
double totalPing = 0;
int64_t totalBytesSent = 0, totalBytesRecv = 0;
int tlsCount = 0;
for (const auto& p : state.peers) {
if (p.inbound) inboundCount++;
else outboundCount++;
totalPing += p.pingtime;
totalBytesSent += p.bytessent;
totalBytesRecv += p.bytesrecv;
if (!p.tls_cipher.empty()) tlsCount++;
}
double avgPing = totalPeers > 0 ? (totalPing / totalPeers) * 1000.0 : 0;
// Format bytes helper
auto fmtBytes = [](int64_t bytes) -> std::string {
char b[32];
if (bytes >= 1073741824LL)
snprintf(b, sizeof(b), "%.1f GB", bytes / 1073741824.0);
else if (bytes >= 1048576LL)
snprintf(b, sizeof(b), "%.1f MB", bytes / 1048576.0);
else if (bytes >= 1024LL)
snprintf(b, sizeof(b), "%.0f KB", bytes / 1024.0);
else
snprintf(b, sizeof(b), "%lld B", (long long)bytes);
return b;
};
// Blockchain card: 5 rows, Peers card: 4 rows (2 cols per row)
float rowH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize;
float headerH = ovFont->LegacySize + Layout::spacingSm();
float dividerH = 1.0f * Layout::dpiScale();
// Use 5 rows for blockchain card (peers card will have empty space at bottom)
float cardInnerH = pad * 0.5f + headerH + rowH * 5 + (Layout::spacingSm() + dividerH) * 4 + pad * 0.5f;
infoCardsH = cardInnerH;
float cardW = (availWidth - gap) * 0.5f;
ImVec2 basePos = ImGui::GetCursorScreenPos();
float dp = Layout::dpiScale();
// ================================================================
// BLOCKCHAIN CARD (left)
// ================================================================
{
ImVec2 cardMin = basePos;
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + infoCardsH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
// Card header
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), "BLOCKCHAIN");
float colW = (cardW - pad * 2) / 2.0f;
float ry = cardMin.y + pad * 0.5f + headerH;
// Helper to draw a subtle horizontal divider
auto drawDivider = [&](float y) {
float rnd = glassSpec.rounding;
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, y),
ImVec2(cardMax.x - rnd * 0.5f, y),
WithAlpha(OnSurface(), 15), 1.0f * dp);
};
// Row 1: Blocks | Longest Chain
{
// Blocks
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Blocks");
int blocks = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
if (blocks > 0) {
snprintf(buf, sizeof(buf), "%d", blocks);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), "\xE2\x80\x94");
}
// Longest Chain
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Longest Chain");
if (state.longestchain > 0) {
snprintf(buf, sizeof(buf), "%d", state.longestchain);
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), chainCol, buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), "\xE2\x80\x94");
}
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
// Row 2: Hashrate | Difficulty
{
// Hashrate
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Hashrate");
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (mining.networkHashrate > 0) {
if (mining.networkHashrate >= 1e12)
snprintf(buf, sizeof(buf), "%.2f TH/s", mining.networkHashrate / 1e12);
else if (mining.networkHashrate >= 1e9)
snprintf(buf, sizeof(buf), "%.2f GH/s", mining.networkHashrate / 1e9);
else if (mining.networkHashrate >= 1e6)
snprintf(buf, sizeof(buf), "%.2f MH/s", mining.networkHashrate / 1e6);
else if (mining.networkHashrate >= 1e3)
snprintf(buf, sizeof(buf), "%.2f KH/s", mining.networkHashrate / 1e3);
else
snprintf(buf, sizeof(buf), "%.2f H/s", mining.networkHashrate);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), Success(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
// Difficulty
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Difficulty");
valY = ry + capFont->LegacySize + Layout::spacingXs();
if (mining.difficulty > 0) {
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
// Row 3: Notarized | Protocol
{
// Notarized
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Notarized");
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.notarized > 0) {
snprintf(buf, sizeof(buf), "%d", state.notarized);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
// Protocol
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Protocol");
valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.protocol_version > 0) {
snprintf(buf, sizeof(buf), "%d", state.protocol_version);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
// Row 4: Version | Memory
{
// Version
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Version");
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.daemon_version > 0) {
int major = state.daemon_version / 1000000;
int minor = (state.daemon_version / 10000) % 100;
int patch = (state.daemon_version / 100) % 100;
snprintf(buf, sizeof(buf), "%d.%d.%d", major, minor, patch);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
// Memory
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Memory");
valY = ry + capFont->LegacySize + Layout::spacingXs();
double memMb = state.mining.daemon_memory_mb;
if (memMb > 0) {
if (memMb >= 1024.0)
snprintf(buf, sizeof(buf), "%.1f GB", memMb / 1024.0);
else
snprintf(buf, sizeof(buf), "%.0f MB", memMb);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
// Row 5: Longest Chain | Best Block
{
// Longest Chain
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Longest");
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.longestchain > 0) {
snprintf(buf, sizeof(buf), "%d", state.longestchain);
// Color green if local matches longest, warning if behind
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), chainCol, buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
// Best Block (truncated hash)
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Best Block");
valY = ry + capFont->LegacySize + Layout::spacingXs();
if (!state.sync.best_blockhash.empty()) {
// Truncate hash to fit: first 6 + "..." + last 6
std::string hash = state.sync.best_blockhash;
std::string truncHash;
if (hash.length() > 15) {
truncHash = hash.substr(0, 6) + "..." + hash.substr(hash.length() - 6);
} else {
truncHash = hash;
}
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), truncHash.c_str());
// Click to copy full hash
ImVec2 hashSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, truncHash.c_str());
ImGui::SetCursorScreenPos(ImVec2(cx, valY));
ImGui::InvisibleButton("##BestBlockCopy", ImVec2(hashSz.x + Layout::spacingSm(), sub1->LegacySize + 2 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy: %s", hash.c_str());
dl->AddLine(ImVec2(cx, valY + sub1->LegacySize + 1 * dp),
ImVec2(cx + hashSz.x, valY + sub1->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(hash.c_str());
ui::Notifications::instance().info("Block hash copied");
}
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
}
}
// ================================================================
// PEERS CARD (right)
// ================================================================
{
ImVec2 cardMin(basePos.x + cardW + gap, basePos.y);
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + infoCardsH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
// Card header
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), "PEERS");
float colW = (cardW - pad * 2) / 2.0f;
float ry = cardMin.y + pad * 0.5f + headerH;
// Helper to draw a subtle horizontal divider
auto drawPeerDivider = [&](float y) {
float rnd = glassSpec.rounding;
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, y),
ImVec2(cardMax.x - rnd * 0.5f, y),
WithAlpha(OnSurface(), 15), 1.0f * dp);
};
// Row 1: Connected | In/Out
{
// Connected
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Connected");
snprintf(buf, sizeof(buf), "%d", totalPeers);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
// In / Out
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "In / Out");
snprintf(buf, sizeof(buf), "%d / %d", inboundCount, outboundCount);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawPeerDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
// Row 2: TLS | Avg Ping
{
// TLS
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "TLS");
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (totalPeers > 0) {
snprintf(buf, sizeof(buf), "%d / %d", tlsCount, totalPeers);
ImU32 tlsCol = (tlsCount == totalPeers) ? Success() : OnSurface();
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), tlsCol, buf);
if (tlsCount == totalPeers) {
ImFont* iconFont = Type().iconSmall();
ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf);
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4, valY), Success(), ICON_MD_CHECK);
}
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
// Avg Ping
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Avg Ping");
ImU32 pingCol;
if (avgPing < 100) pingCol = Success();
else if (avgPing < 500) pingCol = Warning();
else pingCol = Error();
snprintf(buf, sizeof(buf), "%.0f ms", avgPing);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), pingCol, buf);
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawPeerDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
// Row 3: Received | Sent
{
// Received
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Received");
std::string recvStr = fmtBytes(totalBytesRecv);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), recvStr.c_str());
// Sent
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Sent");
std::string sentStr = fmtBytes(totalBytesSent);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), sentStr.c_str());
}
ry += rowH + Layout::spacingSm() * 0.5f;
drawPeerDivider(ry);
ry += Layout::spacingSm() * 0.5f + dividerH;
// Row 4: P2P Port | Banned
{
// P2P Port
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "P2P Port");
float valY = ry + capFont->LegacySize + Layout::spacingXs();
if (state.p2p_port > 0) {
snprintf(buf, sizeof(buf), "%d", state.p2p_port);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
// Banned count
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Banned");
valY = ry + capFont->LegacySize + Layout::spacingXs();
size_t bannedCount = state.bannedPeers.size();
snprintf(buf, sizeof(buf), "%zu", bannedCount);
ImU32 bannedCol = (bannedCount > 0) ? Warning() : OnSurface();
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), bannedCol, buf); }
}
ImGui::Dummy(ImVec2(availWidth, infoCardsH));
ImGui::Dummy(ImVec2(0, gap));
}
// ================================================================
// Compute remaining space for peer list + footer
// ================================================================
float footerH = ImGui::GetFrameHeight() + Layout::spacingSm();
float toggleH = body2->LegacySize + Layout::spacingMd() * 2;
float remainForPeers = std::max(60.0f, peersAvail.y - (ImGui::GetCursorScreenPos().y - ImGui::GetWindowPos().y) - footerH - Layout::spacingSm());
float peerPanelHeight = remainForPeers - toggleH;
peerPanelHeight = std::max(S.drawElement("tabs.peers", "peer-panel-min-height").size, peerPanelHeight);
// ================================================================
// PEERS — Single glass card with Connected / Banned toggle
// ================================================================
static bool s_show_banned = false;
{
// Toggle header: "Connected (N)" / "Banned (N)"
float toggleY = ImGui::GetCursorScreenPos().y;
{
char connLabel[64], banLabel[64];
snprintf(connLabel, sizeof(connLabel), "Connected (%zu)", state.peers.size());
snprintf(banLabel, sizeof(banLabel), "Banned (%zu)", state.bannedPeers.size());
ImVec2 connSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, connLabel);
ImVec2 banSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, banLabel);
float tabGap = Layout::spacingXl();
float tabStartX = ImGui::GetCursorScreenPos().x + pad;
float dp = Layout::dpiScale();
// Connected tab
ImVec2 connPos(tabStartX, toggleY);
ImU32 connCol = s_show_banned ? OnSurfaceDisabled() : OnSurface();
dl->AddText(body2, body2->LegacySize, ImVec2(connPos.x, connPos.y + Layout::spacingMd() * 0.5f), connCol, connLabel);
if (!s_show_banned) {
float underY = connPos.y + Layout::spacingMd() * 0.5f + body2->LegacySize + 3.0f * dp;
dl->AddRectFilled(ImVec2(connPos.x, underY), ImVec2(connPos.x + connSz.x, underY + 2.0f * dp), Primary(), 1.0f * dp);
}
ImGui::SetCursorScreenPos(connPos);
if (ImGui::InvisibleButton("##tabConn", ImVec2(connSz.x, toggleH))) {
s_show_banned = false;
}
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
// Banned tab
float banX = tabStartX + connSz.x + tabGap;
ImVec2 banPos(banX, toggleY);
ImU32 banCol = s_show_banned ? OnSurface() : OnSurfaceDisabled();
dl->AddText(body2, body2->LegacySize, ImVec2(banPos.x, banPos.y + Layout::spacingMd() * 0.5f), banCol, banLabel);
if (s_show_banned) {
float underY = banPos.y + Layout::spacingMd() * 0.5f + body2->LegacySize + 3.0f * dp;
dl->AddRectFilled(ImVec2(banPos.x, underY), ImVec2(banPos.x + banSz.x, underY + 2.0f * dp), Primary(), 1.0f * dp);
}
ImGui::SetCursorScreenPos(banPos);
if (ImGui::InvisibleButton("##tabBan", ImVec2(banSz.x, toggleH))) {
s_show_banned = true;
}
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetWindowPos().x, toggleY + toggleH));
}
// Glass panel background
ImVec2 panelMin = ImGui::GetCursorScreenPos();
ImVec2 panelMax(panelMin.x + availWidth, panelMin.y + peerPanelHeight);
DrawGlassPanel(dl, panelMin, panelMax, glassSpec);
// Scroll-edge mask state
float listScrollY = 0.0f, listScrollMaxY = 0.0f;
int listParentVtx = dl->VtxBuffer.Size;
ImGui::BeginChild("##PeersList", ImVec2(availWidth, peerPanelHeight), false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
ApplySmoothScroll();
ImDrawList* listChildDL = ImGui::GetWindowDrawList();
int listChildVtx = listChildDL->VtxBuffer.Size;
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (!s_show_banned) {
// ---- Connected Peers ----
if (!app->isConnected()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " Not connected to daemon...");
} else if (state.peers.empty()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No connected peers");
} else {
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg();
float rowInset = Layout::spacingLg();
float innerW = ImGui::GetContentRegionAvail().x - rowInset * 2;
listScrollY = ImGui::GetScrollY();
listScrollMaxY = ImGui::GetScrollMaxY();
for (size_t i = 0; i < state.peers.size(); i++) {
const auto& peer = state.peers[i];
bool is_selected = (s_selected_peer_idx == static_cast<int>(i));
ImGui::PushID(static_cast<int>(i));
ImVec2 rawRowPos = ImGui::GetCursorScreenPos();
ImVec2 rowPos(rawRowPos.x + rowInset, rawRowPos.y);
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
if (is_selected) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "row-selection-rounding").size);
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size, rowEnd.y), Primary(), S.drawElement("tabs.peers", "row-accent-rounding").size);
}
bool hovered = material::IsRectHovered(rowPos, rowEnd);
if (hovered && !is_selected) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "row-selection-rounding").size);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
float cx = rowPos.x + pad;
float cy = rowPos.y + Layout::spacingSm();
double ping_ms = peer.pingtime * 1000.0;
ImU32 dotCol;
if (ping_ms < 100) dotCol = Success();
else if (ping_ms < 500) dotCol = Warning();
else dotCol = Error();
float pingDotR = S.drawElement("tabs.peers", "ping-dot-radius-base").size + S.drawElement("tabs.peers", "ping-dot-radius-scale").size * hs;
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ping-dot-x-offset").size, cy + body2->LegacySize * 0.5f), pingDotR, dotCol);
dl->AddText(body2, body2->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size, cy), OnSurface(), peer.addr.c_str());
{
const char* dirLabel = peer.inbound ? "In" : "Out";
ImU32 dirBg = peer.inbound ? WithAlpha(Success(), 30) : WithAlpha(Secondary(), 30);
ImU32 dirFg = peer.inbound ? WithAlpha(Success(), 200) : WithAlpha(Secondary(), 200);
ImVec2 dirSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, dirLabel);
float dirX = rowPos.x + innerW - dirSz.x - Layout::spacingXl();
ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size);
ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size);
dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size);
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2), dirFg, dirLabel);
}
{
char pingBuf[32];
snprintf(pingBuf, sizeof(pingBuf), "%.0fms", ping_ms);
ImVec2 pingSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, pingBuf);
float pingX = rowPos.x + innerW - pingSz.x - Layout::spacingXl() * 3;
dl->AddText(capFont, capFont->LegacySize, ImVec2(pingX, cy + 2), dotCol, pingBuf);
}
float cy2 = cy + body2->LegacySize + Layout::spacingXs();
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size, cy2),
OnSurfaceDisabled(), peer.subver.c_str());
float verW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, peer.subver.c_str()).x;
float tlsBadgeW = std::max(S.drawElement("tabs.peers", "tls-badge-min-width").size, S.drawElement("tabs.peers", "tls-badge-width").size * hs);
if (!peer.tls_cipher.empty()) {
ImU32 tlsBg = WithAlpha(Success(), 25);
ImU32 tlsFg = WithAlpha(Success(), 200);
ImVec2 tlsMin(cx + S.drawElement("tabs.peers", "address-x-offset").size + verW + Layout::spacingSm(), cy2);
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2);
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size);
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS");
} else {
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size + verW + Layout::spacingSm(), cy2),
WithAlpha(Error(), 140), "No TLS");
}
if (peer.banscore > 0) {
char banBuf[16];
snprintf(banBuf, sizeof(banBuf), "Ban: %d", peer.banscore);
ImU32 banCol = peer.banscore > 50 ? Error() : Warning();
ImVec2 banSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banBuf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(rowPos.x + innerW - banSz.x - Layout::spacingLg(), cy2), banCol, banBuf);
}
ImGui::InvisibleButton("##peerRow", ImVec2(innerW, rowH));
if (ImGui::IsItemClicked(0)) {
s_selected_peer_idx = static_cast<int>(i);
}
const auto& acrylicTheme = GetCurrentAcrylicTheme();
if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme.menu)) {
ImGui::Text("Peer: %s", peer.addr.c_str());
ImGui::Separator();
if (ImGui::MenuItem("Copy Address")) {
ImGui::SetClipboardText(peer.addr.c_str());
}
if (ImGui::MenuItem("Copy IP")) {
ImGui::SetClipboardText(ExtractIP(peer.addr).c_str());
}
ImGui::Separator();
if (ImGui::MenuItem("Ban Peer (24h)")) {
app->banPeer(ExtractIP(peer.addr), 86400);
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(8, 3));
if (ImGui::BeginTable("##PeerTT", 2, ImGuiTableFlags_SizingFixedFit)) {
auto TTRow = [](const char* label, const char* value) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextDisabled("%s", label);
ImGui::TableNextColumn();
ImGui::Text("%s", value);
};
char ttBuf[128];
snprintf(ttBuf, sizeof(ttBuf), "%d", peer.id);
TTRow("ID", ttBuf);
TTRow("Services", peer.services.c_str());
snprintf(ttBuf, sizeof(ttBuf), "%d", peer.startingheight);
TTRow("Start Height", ttBuf);
snprintf(ttBuf, sizeof(ttBuf), "%ld bytes", peer.bytessent);
TTRow("Sent", ttBuf);
snprintf(ttBuf, sizeof(ttBuf), "%ld bytes", peer.bytesrecv);
TTRow("Received", ttBuf);
snprintf(ttBuf, sizeof(ttBuf), "%d / %d", peer.synced_headers, peer.synced_blocks);
TTRow("Synced H/B", ttBuf);
if (!peer.tls_cipher.empty())
TTRow("TLS Cipher", peer.tls_cipher.c_str());
ImGui::EndTable();
}
ImGui::PopStyleVar();
ImGui::EndTooltip();
}
if (i < state.peers.size() - 1) {
ImVec2 divStart = ImGui::GetCursorScreenPos();
dl->AddLine(ImVec2(divStart.x + pad + 18, divStart.y),
ImVec2(divStart.x + innerW - pad, divStart.y),
IM_COL32(255, 255, 255, 15));
}
ImGui::PopID();
}
}
} else {
// ---- Banned Peers ----
if (!app->isConnected()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " Not connected to daemon...");
} else if (state.bannedPeers.empty()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No banned peers");
} else {
float rowH = capFont->LegacySize + S.drawElement("tabs.peers", "banned-row-height-padding").size;
float rowInsetB = pad;
float innerW = ImGui::GetContentRegionAvail().x - rowInsetB * 2;
listScrollY = ImGui::GetScrollY();
listScrollMaxY = ImGui::GetScrollMaxY();
for (size_t i = 0; i < state.bannedPeers.size(); i++) {
const auto& banned = state.bannedPeers[i];
bool is_selected = (s_selected_banned_idx == static_cast<int>(i));
ImGui::PushID(static_cast<int>(i));
ImVec2 rawRowPosB = ImGui::GetCursorScreenPos();
ImVec2 rowPos(rawRowPosB.x + rowInsetB, rawRowPosB.y);
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
if (is_selected) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "banned-row-rounding").size);
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size, rowEnd.y), WithAlpha(Error(), 200), S.drawElement("tabs.peers", "banned-accent-rounding").size);
}
if (material::IsRectHovered(rowPos, rowEnd) && !is_selected) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "banned-row-rounding").size);
}
float cx = rowPos.x + pad;
float cy = rowPos.y + Layout::spacingXs();
float banDotR = S.drawElement("tabs.peers", "ban-dot-radius-base").size + S.drawElement("tabs.peers", "ban-dot-radius-scale").size * hs;
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ban-dot-x-offset").size, cy + capFont->LegacySize * 0.4f), banDotR, WithAlpha(Error(), 200));
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "banned-address-x-offset").size, cy),
OnSurfaceDisabled(), banned.address.c_str());
std::string banUntil = banned.getBannedUntilString();
ImVec2 banSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banUntil.c_str());
dl->AddText(capFont, capFont->LegacySize,
ImVec2(rowPos.x + innerW - banSz.x - Layout::spacingXl() * 5, cy),
OnSurfaceDisabled(), banUntil.c_str());
float btnX = rowPos.x + innerW - Layout::spacingXl() * S.drawElement("tabs.peers", "unban-btn-right-offset-multiplier").size;
ImGui::SetCursorScreenPos(ImVec2(btnX, cy - 1));
if (TactileSmallButton("Unban", S.resolveFont("button"))) {
app->unbanPeer(banned.address);
}
ImGui::SetCursorScreenPos(rowPos);
ImGui::InvisibleButton("##bannedRow", ImVec2(innerW - S.drawElement("tabs.peers", "banned-row-btn-reserve").size, rowH));
if (ImGui::IsItemClicked(0)) {
s_selected_banned_idx = static_cast<int>(i);
}
const auto& acrylicTheme2 = GetCurrentAcrylicTheme();
if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme2.menu)) {
if (ImGui::MenuItem("Copy Address")) {
ImGui::SetClipboardText(banned.address.c_str());
}
if (ImGui::MenuItem("Unban")) {
app->unbanPeer(banned.address);
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowEnd.y));
if (i < state.bannedPeers.size() - 1) {
ImVec2 divStart = ImGui::GetCursorScreenPos();
dl->AddLine(ImVec2(divStart.x + pad + 8, divStart.y),
ImVec2(divStart.x + innerW - pad, divStart.y),
IM_COL32(255, 255, 255, 15));
}
ImGui::PopID();
}
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::EndChild();
// CSS-style clipping mask
{
float fadeFont = s_show_banned ? capFont->LegacySize : body2->LegacySize;
float fadeZone = std::min(fadeFont * 3.0f, peerPanelHeight * 0.18f);
ApplyScrollEdgeMask(dl, listParentVtx, listChildDL, listChildVtx,
panelMin.y, panelMax.y, fadeZone, listScrollY, listScrollMaxY);
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
// ================================================================
// Footer — Refresh + Clear Bans (material styled)
// ================================================================
{
ImGui::BeginDisabled(!app->isConnected());
if (TactileSmallButton("Refresh Peers", S.resolveFont("button"))) {
app->refreshPeerInfo();
}
if (s_show_banned && !state.bannedPeers.empty()) {
ImGui::SameLine();
if (TactileSmallButton("Clear All Bans", S.resolveFont("button"))) {
app->clearBans();
}
}
ImGui::EndDisabled();
}
ImGui::EndChild(); // ##PeersScroll
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,19 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
/**
* @brief Render the Peers tab
* Shows connected peers and network info
*/
void RenderPeersTab(App* app);
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,151 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "qr_popup_dialog.h"
#include "../../app.h"
#include "../widgets/qr_code.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
// Static member initialization
bool QRPopupDialog::s_open = false;
std::string QRPopupDialog::s_address;
std::string QRPopupDialog::s_label;
uintptr_t QRPopupDialog::s_qr_texture = 0;
int QRPopupDialog::s_qr_width = 0;
int QRPopupDialog::s_qr_height = 0;
void QRPopupDialog::show(const std::string& address, const std::string& label)
{
// Cleanup previous texture
if (s_qr_texture != 0) {
FreeQRTexture(s_qr_texture);
s_qr_texture = 0;
}
s_open = true;
s_address = address;
s_label = label;
// Generate new QR texture
s_qr_texture = GenerateQRTexture(address.c_str(), &s_qr_width, &s_qr_height);
}
bool QRPopupDialog::isOpen()
{
return s_open;
}
void QRPopupDialog::close()
{
s_open = false;
if (s_qr_texture != 0) {
FreeQRTexture(s_qr_texture);
s_qr_texture = 0;
}
}
void QRPopupDialog::render(App* app)
{
(void)app; // Unused for now
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.qr-popup");
auto qr = S.drawElement("dialogs.qr-popup", "qr-code");
auto addrInput = S.input("dialogs.qr-popup", "address-input");
auto actionBtn = S.button("dialogs.qr-popup", "action-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("QR Code");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("QR Code", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
// Label if present
if (!s_label.empty()) {
ImGui::TextWrapped("%s", s_label.c_str());
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
}
// Center the QR code
float qr_size = qr.size > 0 ? qr.size : 280;
float window_width = ImGui::GetWindowWidth();
float padding = (window_width - qr_size) / 2.0f;
ImGui::SetCursorPosX(padding);
// Render QR code
if (s_qr_texture != 0) {
ImGui::Image((ImTextureID)s_qr_texture, ImVec2(qr_size, qr_size));
} else {
// Fallback: show error
ImGui::BeginChild("QRPlaceholder", ImVec2(qr_size, qr_size), true);
ImGui::TextWrapped("Failed to generate QR code");
ImGui::EndChild();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Address display
ImGui::Text("Address:");
// Use multiline for z-addresses
if (s_address.length() > 50) {
char addr_buf[512];
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
addr_buf[sizeof(addr_buf) - 1] = '\0';
ImGui::InputTextMultiline("##QRAddress", addr_buf, sizeof(addr_buf),
ImVec2(-1, addrInput.height > 0 ? addrInput.height : 60), ImGuiInputTextFlags_ReadOnly);
} else {
char addr_buf[128];
strncpy(addr_buf, s_address.c_str(), sizeof(addr_buf) - 1);
addr_buf[sizeof(addr_buf) - 1] = '\0';
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##QRAddress", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
}
ImGui::Spacing();
// Buttons
float button_width = actionBtn.width;
float total_width = button_width * 2 + ImGui::GetStyle().ItemSpacing.x;
float start_x = (window_width - total_width) / 2.0f;
ImGui::SetCursorPosX(start_x);
if (material::StyledButton("Copy Address", ImVec2(button_width, 0), S.resolveFont(actionBtn.font))) {
ImGui::SetClipboardText(s_address.c_str());
}
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(actionBtn.font))) {
close();
}
}
effects::ImGuiAcrylic::EndAcrylicPopup();
// Handle window close button
if (!s_open && s_qr_texture != 0) {
FreeQRTexture(s_qr_texture);
s_qr_texture = 0;
}
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,54 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <cstdint>
namespace dragonx {
class App;
namespace ui {
/**
* @brief Dialog showing a large QR code for an address
*/
class QRPopupDialog {
public:
/**
* @brief Show the dialog for an address
* @param address The address to display as QR
* @param label Optional label for the address
*/
static void show(const std::string& address, const std::string& label = "");
/**
* @brief Render the dialog (call every frame)
* @param app Pointer to app instance
*/
static void render(App* app);
/**
* @brief Check if dialog is currently open
*/
static bool isOpen();
/**
* @brief Close the dialog and cleanup
*/
static void close();
private:
static bool s_open;
static std::string s_address;
static std::string s_label;
static uintptr_t s_qr_texture;
static int s_qr_width;
static int s_qr_height;
};
} // namespace ui
} // namespace dragonx

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,934 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Layout G: QR-Centered Hero
// - QR code dominates center as hero element
// - Address info wraps around the QR
// - Payment request section below QR
// - Horizontal address strip at bottom for fast switching
#include "receive_tab.h"
#include "send_tab.h"
#include "../../app.h"
#include "../../version.h"
#include "../../wallet_state.h"
#include "../../ui/widgets/qr_code.h"
#include "../sidebar.h"
#include "../layout.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../notifications.h"
#include "imgui.h"
#include <string>
#include <algorithm>
#include <cmath>
#include <map>
namespace dragonx {
namespace ui {
using namespace material;
// ============================================================================
// State
// ============================================================================
static int s_selected_address_idx = -1;
static double s_request_amount = 0.0;
static char s_request_memo[256] = "";
static std::string s_cached_qr_data;
static uintptr_t s_qr_texture = 0;
static bool s_payment_request_open = false;
// Track newly created addresses for NEW badge
static std::map<std::string, double> s_new_address_timestamps;
static size_t s_prev_address_count = 0;
// Address labels (in-memory until persistent config)
static std::map<std::string, std::string> s_address_labels;
static char s_label_edit_buf[64] = "";
// Address type filter
static int s_addr_type_filter = 0; // 0=All, 1=Z, 2=T
// ============================================================================
// Helpers
// ============================================================================
static std::string TruncateAddress(const std::string& addr, size_t maxLen = 35) {
if (addr.length() <= maxLen) return addr;
size_t halfLen = (maxLen - 3) / 2;
return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen);
}
static void OpenExplorerURL(const std::string& address) {
std::string url = "https://explorer.dragonx.com/address/" + address;
#ifdef _WIN32
std::string cmd = "start \"\" \"" + url + "\"";
#elif __APPLE__
std::string cmd = "open \"" + url + "\"";
#else
std::string cmd = "xdg-open \"" + url + "\"";
#endif
system(cmd.c_str());
}
// ============================================================================
// Sync banner
// ============================================================================
static void RenderSyncBanner(const WalletState& state) {
if (!state.sync.syncing || state.sync.isSynced()) return;
float syncPct = (state.sync.headers > 0)
? (float)state.sync.blocks / state.sync.headers * 100.0f : 0.0f;
char syncBuf[128];
snprintf(syncBuf, sizeof(syncBuf),
"Blockchain syncing (%.1f%%)... Balances may be inaccurate.", syncPct);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.4f, 0.0f, 0.15f));
ImGui::BeginChild("##SyncBannerRecv", ImVec2(ImGui::GetContentRegionAvail().x, 28),
false, ImGuiWindowFlags_NoScrollbar);
ImGui::SetCursorPos(ImVec2(Layout::spacingLg(), 6));
Type().textColored(TypeStyle::Caption, Warning(), syncBuf);
ImGui::EndChild();
ImGui::PopStyleColor();
}
// ============================================================================
// Track new addresses (detect creations)
// ============================================================================
static void TrackNewAddresses(const WalletState& state) {
if (state.addresses.size() > s_prev_address_count && s_prev_address_count > 0) {
for (const auto& a : state.addresses) {
if (s_new_address_timestamps.find(a.address) == s_new_address_timestamps.end()) {
s_new_address_timestamps[a.address] = ImGui::GetTime();
}
}
} else if (s_prev_address_count == 0) {
for (const auto& a : state.addresses) {
s_new_address_timestamps[a.address] = 0.0;
}
}
s_prev_address_count = state.addresses.size();
}
// ============================================================================
// Build sorted address groups
// ============================================================================
struct AddressGroups {
std::vector<int> shielded;
std::vector<int> transparent;
};
static AddressGroups BuildSortedAddressGroups(const WalletState& state) {
AddressGroups groups;
for (int i = 0; i < (int)state.addresses.size(); i++) {
if (state.addresses[i].type == "shielded")
groups.shielded.push_back(i);
else
groups.transparent.push_back(i);
}
std::sort(groups.shielded.begin(), groups.shielded.end(), [&](int a, int b) {
return state.addresses[a].balance > state.addresses[b].balance;
});
std::sort(groups.transparent.begin(), groups.transparent.end(), [&](int a, int b) {
return state.addresses[a].balance > state.addresses[b].balance;
});
return groups;
}
// ============================================================================
// QR Hero — the centerpiece of Layout G
// ============================================================================
static void RenderQRHero(App* app, ImDrawList* dl, const AddressInfo& addr,
float width, float qrSize,
const std::string& qr_data,
const GlassPanelSpec& glassSpec,
const WalletState& state,
ImFont* sub1, ImFont* /*body2*/, ImFont* capFont) {
char buf[128];
bool isZ = addr.type == "shielded";
ImU32 typeCol = isZ ? IM_COL32(77, 204, 77, 255) : IM_COL32(204, 170, 51, 255);
const char* typeBadge = isZ ? "Shielded" : "Transparent";
float qrPadding = Layout::spacingLg();
float totalQrSize = qrSize + qrPadding * 2;
float heroH = totalQrSize + 80.0f; // QR + info below
ImVec2 heroMin = ImGui::GetCursorScreenPos();
ImVec2 heroMax(heroMin.x + width, heroMin.y + heroH);
GlassPanelSpec heroGlass = glassSpec;
heroGlass.fillAlpha = 16;
heroGlass.borderAlpha = 35;
DrawGlassPanel(dl, heroMin, heroMax, heroGlass);
// --- Address info bar above QR ---
float infoBarH = 32.0f;
float cx = heroMin.x + Layout::spacingLg();
float cy = heroMin.y + Layout::spacingSm();
// Type badge circle + label
dl->AddCircleFilled(ImVec2(cx + 8, cy + 10), 8.0f, IM_COL32(255, 255, 255, 20));
const char* typeChar = isZ ? "Z" : "T";
ImVec2 tcSz = sub1->CalcTextSizeA(sub1->LegacySize, 100, 0, typeChar);
dl->AddText(sub1, sub1->LegacySize,
ImVec2(cx + 8 - tcSz.x * 0.5f, cy + 10 - tcSz.y * 0.5f),
typeCol, typeChar);
// Education tooltip on badge
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::InvisibleButton("##TypeBadgeHero", ImVec2(22, 22));
if (ImGui::IsItemHovered()) {
if (isZ) {
ImGui::SetTooltip(
"Shielded Address (Z)\n"
"- Full transaction privacy\n"
"- Encrypted sender, receiver, amount\n"
"- Supports encrypted memos\n"
"- Recommended for privacy");
} else {
ImGui::SetTooltip(
"Transparent Address (T)\n"
"- Publicly visible on blockchain\n"
"- Similar to Bitcoin addresses\n"
"- No memo support\n"
"- Use Z addresses for privacy");
}
}
// Type label text
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + 24, cy + 4), typeCol, typeBadge);
// Balance right-aligned
snprintf(buf, sizeof(buf), "%.8f %s", addr.balance, DRAGONX_TICKER);
ImVec2 balSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf);
float balX = heroMax.x - balSz.x - Layout::spacingLg();
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(balX, cy + 2), typeCol, buf);
// USD value
if (state.market.price_usd > 0 && addr.balance > 0) {
double usd = addr.balance * state.market.price_usd;
snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.2f", usd);
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(heroMax.x - usdSz.x - Layout::spacingLg(), cy + sub1->LegacySize + 4),
OnSurfaceDisabled(), buf);
}
// --- QR Code centered ---
float qrOffset = (width - totalQrSize) * 0.5f;
ImVec2 qrPanelMin(heroMin.x + qrOffset, heroMin.y + infoBarH + Layout::spacingSm());
ImVec2 qrPanelMax(qrPanelMin.x + totalQrSize, qrPanelMin.y + totalQrSize);
// Subtle inner panel for QR
GlassPanelSpec qrGlass;
qrGlass.rounding = glassSpec.rounding * 0.75f;
qrGlass.fillAlpha = 12;
qrGlass.borderAlpha = 25;
DrawGlassPanel(dl, qrPanelMin, qrPanelMax, qrGlass);
ImGui::SetCursorScreenPos(ImVec2(qrPanelMin.x + qrPadding, qrPanelMin.y + qrPadding));
if (s_qr_texture) {
RenderQRCode(s_qr_texture, qrSize);
} else {
ImGui::Dummy(ImVec2(qrSize, qrSize));
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - 50,
qrPanelMin.y + totalQrSize * 0.5f);
dl->AddText(capFont, capFont->LegacySize, textPos,
OnSurfaceDisabled(), "QR unavailable");
}
// Click QR to copy
ImGui::SetCursorScreenPos(qrPanelMin);
ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy %s",
s_request_amount > 0 ? "payment URI" : "address");
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(qr_data.c_str());
Notifications::instance().info(s_request_amount > 0
? "Payment URI copied to clipboard"
: "Address copied to clipboard");
}
// --- Address strip below QR ---
float addrStripY = qrPanelMax.y + Layout::spacingMd();
float addrStripX = heroMin.x + Layout::spacingLg();
float addrStripW = width - Layout::spacingXxl();
// Full address (word-wrapped)
ImVec2 fullAddrPos(addrStripX, addrStripY);
float wrapWidth = addrStripW;
ImVec2 addrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX,
wrapWidth, addr.address.c_str());
dl->AddText(capFont, capFont->LegacySize, fullAddrPos,
OnSurface(), addr.address.c_str(), nullptr, wrapWidth);
// Address click-to-copy overlay
ImGui::SetCursorScreenPos(fullAddrPos);
ImGui::InvisibleButton("##addrCopyHero", ImVec2(wrapWidth, addrSz.y));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy address");
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
// Action buttons row
float btnRowY = addrStripY + addrSz.y + Layout::spacingMd();
ImGui::SetCursorScreenPos(ImVec2(addrStripX, btnRowY));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
{
// Copy — primary (uses global glass style)
if (TactileSmallButton("Copy Address##hero", schema::UI().resolveFont("button"))) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
ImGui::SameLine();
// Explorer
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
if (TactileSmallButton("Explorer##hero", schema::UI().resolveFont("button"))) {
OpenExplorerURL(addr.address);
}
ImGui::PopStyleColor(3);
// Send From
if (addr.balance > 0) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()));
if (TactileSmallButton("Send \xe2\x86\x97##hero", schema::UI().resolveFont("button"))) {
SetSendFromAddress(addr.address);
app->setCurrentPage(NavPage::Send);
}
ImGui::PopStyleColor(3);
}
// Label editor (inline)
ImGui::SameLine(0, Layout::spacingXl());
auto lblIt = s_address_labels.find(addr.address);
std::string currentLabel = (lblIt != s_address_labels.end()) ? lblIt->second : "";
snprintf(s_label_edit_buf, sizeof(s_label_edit_buf), "%s", currentLabel.c_str());
ImGui::SetNextItemWidth(std::min(200.0f, addrStripW * 0.3f));
if (ImGui::InputTextWithHint("##LabelHero", "Add label...",
s_label_edit_buf, sizeof(s_label_edit_buf))) {
s_address_labels[addr.address] = std::string(s_label_edit_buf);
}
}
ImGui::PopStyleVar();
// Update hero height based on actual content
float actualBottom = btnRowY + 24;
heroH = actualBottom - heroMin.y + Layout::spacingMd();
heroMax.y = heroMin.y + heroH;
ImGui::SetCursorScreenPos(ImVec2(heroMin.x, heroMax.y));
ImGui::Dummy(ImVec2(width, 0));
}
// ============================================================================
// Payment request section (below QR hero)
// ============================================================================
static void RenderPaymentRequest(ImDrawList* dl, const AddressInfo& addr,
float innerW, const GlassPanelSpec& glassSpec,
const char* suffix) {
auto& S = schema::UI();
const float kLabelPos = S.label("tabs.receive", "label-column").position;
bool hasMemo = (addr.type == "shielded");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PAYMENT REQUEST");
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Compute card height
float prCardH = 16.0f + 24.0f + 8.0f + 12.0f;
if (hasMemo) prCardH += 24.0f;
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
ImFont* capF = Type().caption();
ImVec2 uriSz = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX,
innerW - 24, s_cached_qr_data.c_str());
prCardH += uriSz.y + 8.0f;
}
if (s_request_amount > 0) prCardH += 32.0f;
if (s_request_amount > 0 || s_request_memo[0]) prCardH += 4.0f;
ImVec2 prMin = ImGui::GetCursorScreenPos();
ImVec2 prMax(prMin.x + innerW, prMin.y + prCardH);
DrawGlassPanel(dl, prMin, prMax, glassSpec);
ImGui::SetCursorScreenPos(ImVec2(prMin.x + Layout::spacingLg(), prMin.y + Layout::spacingMd()));
ImGui::Dummy(ImVec2(0, 0));
ImGui::Text("Amount:");
ImGui::SameLine(kLabelPos);
ImGui::SetNextItemWidth(std::max(S.input("tabs.receive", "amount-input").width, innerW * 0.4f));
char amtId[32];
snprintf(amtId, sizeof(amtId), "##RequestAmount%s", suffix);
ImGui::InputDouble(amtId, &s_request_amount, 0.01, 1.0, "%.8f");
ImGui::SameLine();
ImGui::Text("%s", DRAGONX_TICKER);
if (hasMemo) {
ImGui::Text("Memo:");
ImGui::SameLine(kLabelPos);
ImGui::SetNextItemWidth(innerW - kLabelPos - Layout::spacingXxl());
char memoId[32];
snprintf(memoId, sizeof(memoId), "##RequestMemo%s", suffix);
ImGui::InputText(memoId, s_request_memo, sizeof(s_request_memo));
}
// Live URI preview
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
ImGui::Spacing();
ImFont* capF = Type().caption();
ImVec2 uriPos = ImGui::GetCursorScreenPos();
float uriWrapW = innerW - Layout::spacingXxl();
ImVec2 uriSz = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX,
uriWrapW, s_cached_qr_data.c_str());
dl->AddText(capF, capF->LegacySize, uriPos,
OnSurfaceDisabled(), s_cached_qr_data.c_str(), nullptr, uriWrapW);
ImGui::Dummy(ImVec2(uriWrapW, uriSz.y + Layout::spacingSm()));
}
ImGui::Spacing();
if (s_request_amount > 0) {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
char copyUriId[64];
snprintf(copyUriId, sizeof(copyUriId), "Copy Payment URI%s", suffix);
if (TactileButton(copyUriId, ImVec2(innerW - Layout::spacingXxl(), 0), S.resolveFont("button"))) {
ImGui::SetClipboardText(s_cached_qr_data.c_str());
Notifications::instance().info("Payment URI copied to clipboard");
}
ImGui::PopStyleVar();
// Share as text
char shareId[32];
snprintf(shareId, sizeof(shareId), "Share as Text%s", suffix);
if (TactileSmallButton(shareId, S.resolveFont("button"))) {
char shareBuf[1024];
snprintf(shareBuf, sizeof(shareBuf),
"Payment Request\nAmount: %.8f %s\nAddress: %s\nURI: %s",
s_request_amount, DRAGONX_TICKER,
addr.address.c_str(), s_cached_qr_data.c_str());
ImGui::SetClipboardText(shareBuf);
Notifications::instance().info("Payment request copied to clipboard");
}
}
if (s_request_amount > 0 || s_request_memo[0]) {
ImGui::SameLine();
char clearId[32];
snprintf(clearId, sizeof(clearId), "Clear%s", suffix);
if (TactileSmallButton(clearId, S.resolveFont("button"))) {
s_request_amount = 0.0;
s_request_memo[0] = '\0';
}
}
ImGui::SetCursorScreenPos(ImVec2(prMin.x, prMax.y));
ImGui::Dummy(ImVec2(innerW, 0));
}
// ============================================================================
// Recent received transactions for selected address
// ============================================================================
static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& addr,
const WalletState& state, float width,
ImFont* capFont) {
char buf[128];
int recvCount = 0;
for (const auto& tx : state.transactions) {
if (tx.address == addr.address && tx.type == "receive") recvCount++;
}
if (recvCount == 0) return;
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
snprintf(buf, sizeof(buf), "RECENT RECEIVED (%d)", std::min(recvCount, 3));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), buf);
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
int shown = 0;
for (const auto& tx : state.transactions) {
if (tx.address != addr.address || tx.type != "receive") continue;
if (shown >= 3) break;
ImVec2 rMin = ImGui::GetCursorScreenPos();
float rH = 22.0f;
ImVec2 rMax(rMin.x + width, rMin.y + rH);
GlassPanelSpec rsGlass;
rsGlass.rounding = Layout::glassRounding() * 0.5f;
rsGlass.fillAlpha = 8;
DrawGlassPanel(dl, rMin, rMax, rsGlass);
float rx = rMin.x + Layout::spacingMd();
float ry = rMin.y + (rH - capFont->LegacySize) * 0.5f;
// Arrow indicator
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx, ry),
Success(), "\xe2\x86\x90");
snprintf(buf, sizeof(buf), "+%.8f %s %s %s",
tx.amount, DRAGONX_TICKER,
tx.getTimeString().c_str(),
tx.confirmations < 1 ? "(unconfirmed)" : "");
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx + 16, ry),
tx.confirmations >= 1 ? Success() : Warning(), buf);
ImGui::Dummy(ImVec2(width, rH));
ImGui::Dummy(ImVec2(0, 2));
shown++;
}
}
// ============================================================================
// Horizontal Address Strip — bottom switching bar (Layout G signature)
// ============================================================================
static void RenderAddressStrip(App* app, ImDrawList* dl, const WalletState& state,
float width, float hs,
ImFont* /*sub1*/, ImFont* capFont) {
char buf[128];
// Header row with filter and + New button
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "YOUR ADDRESSES");
float btnW = std::max(70.0f, 85.0f * hs);
float comboW = std::max(48.0f, 58.0f * hs);
ImGui::SameLine(width - btnW - comboW - Layout::spacingMd());
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
const char* types[] = { "All", "Z", "T" };
ImGui::SetNextItemWidth(comboW);
ImGui::Combo("##AddrTypeStrip", &s_addr_type_filter, types, 3);
ImGui::SameLine();
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton("+ New##strip", ImVec2(btnW, 0), schema::UI().resolveFont("button"))) {
if (s_addr_type_filter != 2) {
app->createNewZAddress([](const std::string& addr) {
if (addr.empty())
Notifications::instance().error("Failed to create new shielded address");
else
Notifications::instance().success("New shielded address created");
});
} else {
app->createNewTAddress([](const std::string& addr) {
if (addr.empty())
Notifications::instance().error("Failed to create new transparent address");
else
Notifications::instance().success("New transparent address created");
});
}
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (!app->isConnected()) {
Type().textColored(TypeStyle::Caption, Warning(), "Waiting for connection...");
return;
}
if (state.addresses.empty()) {
// Loading skeleton
ImVec2 skelPos = ImGui::GetCursorScreenPos();
float alpha = (float)(0.3 + 0.15 * std::sin(ImGui::GetTime() * 2.0));
ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255));
for (int sk = 0; sk < 3; sk++) {
dl->AddRectFilled(
ImVec2(skelPos.x + sk * (130 + 8), skelPos.y),
ImVec2(skelPos.x + sk * (130 + 8) + 120, skelPos.y + 56),
skelCol, 6.0f);
}
ImGui::Dummy(ImVec2(width, 60));
return;
}
TrackNewAddresses(state);
AddressGroups groups = BuildSortedAddressGroups(state);
// Build filtered list
std::vector<int> filteredIdxs;
if (s_addr_type_filter != 2)
for (int idx : groups.shielded) filteredIdxs.push_back(idx);
if (s_addr_type_filter != 1)
for (int idx : groups.transparent) filteredIdxs.push_back(idx);
// Horizontal scrolling strip
float cardW = std::max(140.0f, std::min(200.0f, width * 0.22f));
float cardH = std::max(52.0f, 64.0f * hs);
float stripH = cardH + 8;
ImGui::BeginChild("##AddrStrip", ImVec2(width, stripH), false,
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoBackground);
ImDrawList* sdl = ImGui::GetWindowDrawList();
for (size_t fi = 0; fi < filteredIdxs.size(); fi++) {
int i = filteredIdxs[fi];
const auto& addr = state.addresses[i];
bool isCurrent = (i == s_selected_address_idx);
bool isZ = addr.type == "shielded";
ImU32 typeCol = isZ ? IM_COL32(77, 204, 77, 255) : IM_COL32(204, 170, 51, 255);
bool hasBalance = addr.balance > 0;
ImVec2 cardMin = ImGui::GetCursorScreenPos();
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
// Card background
GlassPanelSpec cardGlass;
cardGlass.rounding = Layout::glassRounding() * 0.75f;
cardGlass.fillAlpha = isCurrent ? 28 : 14;
cardGlass.borderAlpha = isCurrent ? 50 : 25;
DrawGlassPanel(sdl, cardMin, cardMax, cardGlass);
// Selected indicator — top accent bar
if (isCurrent) {
sdl->AddRectFilled(cardMin, ImVec2(cardMax.x, cardMin.y + 3), Primary(),
cardGlass.rounding);
}
float ix = cardMin.x + Layout::spacingMd();
float iy = cardMin.y + Layout::spacingSm() + (isCurrent ? 4 : 0);
// Type dot
sdl->AddCircleFilled(ImVec2(ix + 4, iy + 6), 3.5f, typeCol);
// Address label or truncated address
auto lblIt = s_address_labels.find(addr.address);
bool hasLabel = (lblIt != s_address_labels.end() && !lblIt->second.empty());
size_t addrTruncLen = static_cast<size_t>(std::max(8.0f, (cardW - 30) / 9.0f));
if (hasLabel) {
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, iy),
isCurrent ? PrimaryLight() : OnSurfaceMedium(),
lblIt->second.c_str());
std::string shortAddr = TruncateAddress(addr.address, std::max((size_t)6, addrTruncLen / 2));
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, iy + capFont->LegacySize + 2),
OnSurfaceDisabled(), shortAddr.c_str());
} else {
std::string dispAddr = TruncateAddress(addr.address, addrTruncLen);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, iy),
isCurrent ? OnSurface() : OnSurfaceDisabled(),
dispAddr.c_str());
}
// Balance
snprintf(buf, sizeof(buf), "%.4f %s", addr.balance, DRAGONX_TICKER);
ImVec2 balSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
float balY = cardMax.y - balSz.y - Layout::spacingSm();
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(ix + 14, balY),
hasBalance ? typeCol : OnSurfaceDisabled(), buf);
// NEW badge
double now = ImGui::GetTime();
auto newIt = s_new_address_timestamps.find(addr.address);
if (newIt != s_new_address_timestamps.end() && newIt->second > 0.0) {
double age = now - newIt->second;
if (age < 10.0) {
float alpha = (float)std::max(0.0, 1.0 - age / 10.0);
int a = (int)(alpha * 220);
ImVec2 badgePos(cardMax.x - 32, cardMin.y + 4);
sdl->AddRectFilled(badgePos, ImVec2(badgePos.x + 28, badgePos.y + 14),
IM_COL32(77, 204, 255, a / 4), 3.0f);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(badgePos.x + 4, badgePos.y + 1),
IM_COL32(77, 204, 255, a), "NEW");
}
}
// Click interaction
ImGui::SetCursorScreenPos(cardMin);
ImGui::PushID(i);
ImGui::InvisibleButton("##addrCard", ImVec2(cardW, cardH));
if (ImGui::IsItemHovered()) {
if (!isCurrent)
sdl->AddRectFilled(cardMin, cardMax, IM_COL32(255, 255, 255, 10),
cardGlass.rounding);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s\nBalance: %.8f %s%s\nDouble-click to copy | Right-click for options",
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
isCurrent ? " (selected)" : "");
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
s_selected_address_idx = i;
s_cached_qr_data.clear();
}
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
// Context menu
if (ImGui::BeginPopupContextItem("##addrStripCtx")) {
if (ImGui::MenuItem("Copy Address")) {
ImGui::SetClipboardText(addr.address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
if (ImGui::MenuItem("View on Explorer")) {
OpenExplorerURL(addr.address);
}
if (addr.balance > 0) {
if (ImGui::MenuItem("Send From This Address")) {
SetSendFromAddress(addr.address);
app->setCurrentPage(NavPage::Send);
}
}
ImGui::EndPopup();
}
ImGui::PopID();
ImGui::SameLine(0, Layout::spacingSm());
}
// Total balance at end of strip
{
double totalBal = 0;
for (const auto& a : state.addresses) totalBal += a.balance;
ImVec2 totPos = ImGui::GetCursorScreenPos();
float totCardW = std::max(100.0f, cardW * 0.6f);
ImVec2 totMax(totPos.x + totCardW, totPos.y + cardH);
GlassPanelSpec totGlass;
totGlass.rounding = Layout::glassRounding() * 0.75f;
totGlass.fillAlpha = 8;
totGlass.borderAlpha = 15;
DrawGlassPanel(sdl, totPos, totMax, totGlass);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(totPos.x + Layout::spacingMd(), totPos.y + Layout::spacingSm()),
OnSurfaceMedium(), "TOTAL");
snprintf(buf, sizeof(buf), "%.8f", totalBal);
ImVec2 totSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(totPos.x + Layout::spacingMd(),
totMax.y - totSz.y - Layout::spacingSm()),
OnSurface(), buf);
snprintf(buf, sizeof(buf), "%s", DRAGONX_TICKER);
sdl->AddText(capFont, capFont->LegacySize,
ImVec2(totPos.x + Layout::spacingMd(),
totMax.y - totSz.y - Layout::spacingSm() - capFont->LegacySize - 2),
OnSurfaceDisabled(), buf);
ImGui::Dummy(ImVec2(totCardW, cardH));
}
// Keyboard navigation
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
int next = s_selected_address_idx + 1;
if (next < (int)state.addresses.size()) {
s_selected_address_idx = next;
s_cached_qr_data.clear();
}
}
if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
int prev = s_selected_address_idx - 1;
if (prev >= 0) {
s_selected_address_idx = prev;
s_cached_qr_data.clear();
}
}
if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) {
if (s_selected_address_idx >= 0 && s_selected_address_idx < (int)state.addresses.size()) {
ImGui::SetClipboardText(state.addresses[s_selected_address_idx].address.c_str());
Notifications::instance().info("Address copied to clipboard");
}
}
}
ImGui::EndChild(); // ##AddrStrip
}
// ============================================================================
// MAIN: RenderReceiveTab — Layout G: QR-Centered Hero
// ============================================================================
void RenderReceiveTab(App* app)
{
const auto& state = app->getWalletState();
RenderSyncBanner(state);
ImVec2 recvAvail = ImGui::GetContentRegionAvail();
ImGui::BeginChild("##ReceiveScroll", recvAvail, false, ImGuiWindowFlags_NoBackground);
float hs = Layout::hScale(recvAvail.x);
float vScale = Layout::vScale(recvAvail.y);
float glassRound = Layout::glassRounding();
float availWidth = ImGui::GetContentRegionAvail().x;
float contentWidth = std::min(availWidth * 0.92f, 1200.0f * hs);
float offsetX = (availWidth - contentWidth) * 0.5f;
if (offsetX > 0) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offsetX);
float sectionGap = Layout::spacingXl() * vScale;
ImGui::BeginGroup();
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
glassSpec.rounding = glassRound;
ImFont* capFont = Type().caption();
ImFont* sub1 = Type().subtitle1();
ImFont* body2 = Type().body2();
// Auto-select first address
if (!state.addresses.empty() &&
(s_selected_address_idx < 0 ||
s_selected_address_idx >= (int)state.addresses.size())) {
s_selected_address_idx = 0;
}
const AddressInfo* selected = nullptr;
if (s_selected_address_idx >= 0 &&
s_selected_address_idx < (int)state.addresses.size()) {
selected = &state.addresses[s_selected_address_idx];
}
// Generate QR data
std::string qr_data;
if (selected) {
qr_data = selected->address;
if (s_request_amount > 0) {
qr_data = std::string("dragonx:") + selected->address +
"?amount=" + std::to_string(s_request_amount);
if (s_request_memo[0] && selected->type == "shielded") {
qr_data += "&memo=" + std::string(s_request_memo);
}
}
if (qr_data != s_cached_qr_data) {
if (s_qr_texture) {
FreeQRTexture(s_qr_texture);
s_qr_texture = 0;
}
int w, h;
s_qr_texture = GenerateQRTexture(qr_data.c_str(), &w, &h);
s_cached_qr_data = qr_data;
}
}
// ================================================================
// Not connected / empty state
// ================================================================
if (!app->isConnected()) {
ImVec2 emptyMin = ImGui::GetCursorScreenPos();
float emptyH = 120.0f;
ImVec2 emptyMax(emptyMin.x + contentWidth, emptyMin.y + emptyH);
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
dl->AddText(sub1, sub1->LegacySize,
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl()),
OnSurfaceDisabled(), "Waiting for daemon connection...");
dl->AddText(capFont, capFont->LegacySize,
ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl() + sub1->LegacySize + 8),
OnSurfaceDisabled(), "Your receiving addresses will appear here once connected.");
ImGui::Dummy(ImVec2(contentWidth, emptyH));
ImGui::EndGroup();
ImGui::EndChild();
return;
}
if (state.addresses.empty()) {
ImVec2 emptyMin = ImGui::GetCursorScreenPos();
float emptyH = 100.0f;
ImVec2 emptyMax(emptyMin.x + contentWidth, emptyMin.y + emptyH);
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
float alpha = (float)(0.3 + 0.15 * std::sin(ImGui::GetTime() * 2.0));
ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255));
dl->AddRectFilled(
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg()),
ImVec2(emptyMin.x + contentWidth * 0.6f, emptyMin.y + Layout::spacingLg() + 16),
skelCol, 4.0f);
dl->AddRectFilled(
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + 24),
ImVec2(emptyMin.x + contentWidth * 0.4f, emptyMin.y + Layout::spacingLg() + 36),
skelCol, 4.0f);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - 24),
OnSurfaceDisabled(), "Loading addresses...");
ImGui::Dummy(ImVec2(contentWidth, emptyH));
ImGui::EndGroup();
ImGui::EndChild();
return;
}
// ================================================================
// QR HERO — dominates center (Layout G signature)
// ================================================================
if (selected) {
// Calculate QR size based on available space
float maxQrForWidth = std::min(contentWidth * 0.6f, 400.0f);
float maxQrForHeight = std::min(recvAvail.y * 0.45f, 400.0f);
float qrSize = std::max(140.0f, std::min(maxQrForWidth, maxQrForHeight));
// Center the hero horizontally
float heroW = std::min(contentWidth, 700.0f * hs);
float heroOffsetX = (contentWidth - heroW) * 0.5f;
if (heroOffsetX > 4) {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + heroOffsetX);
}
RenderQRHero(app, dl, *selected, heroW, qrSize, qr_data,
glassSpec, state, sub1, body2, capFont);
ImGui::Dummy(ImVec2(0, sectionGap));
// ---- PAYMENT REQUEST (collapsible on narrow) ----
constexpr float kTwoColumnThreshold = 800.0f;
bool isNarrow = contentWidth < kTwoColumnThreshold;
if (isNarrow) {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1, 1, 1, 0.05f));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1, 1, 1, 0.08f));
ImGui::PushFont(Type().overline());
s_payment_request_open = ImGui::CollapsingHeader(
"PAYMENT REQUEST (OPTIONAL)",
s_payment_request_open ? ImGuiTreeNodeFlags_DefaultOpen : 0);
ImGui::PopFont();
ImGui::PopStyleColor(3);
if (s_payment_request_open) {
float prW = std::min(contentWidth, 600.0f * hs);
float prOffX = (contentWidth - prW) * 0.5f;
if (prOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + prOffX);
RenderPaymentRequest(dl, *selected, prW, glassSpec, "##hero");
}
} else {
float prW = std::min(contentWidth, 600.0f * hs);
float prOffX = (contentWidth - prW) * 0.5f;
if (prOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + prOffX);
RenderPaymentRequest(dl, *selected, prW, glassSpec, "##hero");
}
ImGui::Dummy(ImVec2(0, sectionGap));
// ---- RECENT RECEIVED ----
{
float rcvW = std::min(contentWidth, 600.0f * hs);
float rcvOffX = (contentWidth - rcvW) * 0.5f;
if (rcvOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rcvOffX);
RenderRecentReceived(dl, *selected, state, rcvW, capFont);
}
ImGui::Dummy(ImVec2(0, sectionGap));
}
// ================================================================
// ADDRESS STRIP — horizontal switching bar at bottom
// ================================================================
RenderAddressStrip(app, dl, state, contentWidth, hs, sub1, capFont);
ImGui::EndGroup();
ImGui::EndChild(); // ##ReceiveScroll
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,19 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
/**
* @brief Render the Receive tab
* Shows addresses for receiving funds with QR codes
*/
void RenderReceiveTab(App* app);
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,298 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "request_payment_dialog.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../widgets/qr_code.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "imgui.h"
#include <string>
#include <sstream>
#include <iomanip>
namespace dragonx {
namespace ui {
// Static state
static bool s_open = false;
static char s_address[512] = "";
static double s_amount = 0.0;
static char s_memo[512] = "";
static char s_label[128] = "";
static int s_selected_addr_idx = -1;
static std::string s_payment_uri;
static uintptr_t s_qr_texture = 0;
static int s_qr_width = 0;
static int s_qr_height = 0;
static bool s_uri_dirty = true;
// Helper to build payment URI
static std::string buildPaymentUri()
{
if (s_address[0] == '\0') return "";
std::ostringstream uri;
uri << "drgx:" << s_address;
bool hasParams = false;
auto addParam = [&](const char* key, const std::string& value) {
if (value.empty()) return;
uri << (hasParams ? "&" : "?") << key << "=" << value;
hasParams = true;
};
if (s_amount > 0) {
std::ostringstream amt;
amt << std::fixed << std::setprecision(8) << s_amount;
addParam("amount", amt.str());
}
if (s_label[0] != '\0') {
// URL encode label
std::string encoded;
for (char c : std::string(s_label)) {
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
encoded += c;
} else if (c == ' ') {
encoded += "%20";
} else {
char hex[4];
snprintf(hex, sizeof(hex), "%%%02X", (unsigned char)c);
encoded += hex;
}
}
addParam("label", encoded);
}
if (s_memo[0] != '\0') {
// URL encode memo
std::string encoded;
for (char c : std::string(s_memo)) {
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
encoded += c;
} else if (c == ' ') {
encoded += "%20";
} else {
char hex[4];
snprintf(hex, sizeof(hex), "%%%02X", (unsigned char)c);
encoded += hex;
}
}
addParam("memo", encoded);
}
return uri.str();
}
void RequestPaymentDialog::show(const std::string& address)
{
s_open = true;
s_amount = 0.0;
s_memo[0] = '\0';
s_label[0] = '\0';
s_selected_addr_idx = -1;
s_uri_dirty = true;
if (!address.empty()) {
strncpy(s_address, address.c_str(), sizeof(s_address) - 1);
} else {
s_address[0] = '\0';
}
// Free old QR texture
if (s_qr_texture != 0) {
FreeQRTexture(s_qr_texture);
s_qr_texture = 0;
}
}
void RequestPaymentDialog::render(App* app)
{
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.request-payment");
auto zAddrLbl = S.label("dialogs.request-payment", "z-addr-label");
auto zAddrFrontLbl = S.label("dialogs.request-payment", "z-addr-front-label");
auto zAddrBackLbl = S.label("dialogs.request-payment", "z-addr-back-label");
auto tAddrLbl = S.label("dialogs.request-payment", "t-addr-label");
auto tAddrFrontLbl = S.label("dialogs.request-payment", "t-addr-front-label");
auto tAddrBackLbl = S.label("dialogs.request-payment", "t-addr-back-label");
auto amountInput = S.input("dialogs.request-payment", "amount-input");
auto memoInput = S.input("dialogs.request-payment", "memo-input");
auto qr = S.drawElement("dialogs.request-payment", "qr-code");
auto actionBtn = S.button("dialogs.request-payment", "action-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Request Payment");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Request Payment", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
const auto& state = app->getWalletState();
ImGui::TextWrapped(
"Generate a payment request that others can scan or copy. "
"The QR code contains your address and optional amount/memo."
);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Address selection
ImGui::Text("Receive Address:");
std::string addr_display = s_address[0] ? s_address : "Select address...";
if (addr_display.length() > static_cast<size_t>(zAddrLbl.truncate)) {
addr_display = addr_display.substr(0, zAddrFrontLbl.truncate) + "..." + addr_display.substr(addr_display.length() - zAddrBackLbl.truncate);
}
ImGui::SetNextItemWidth(-1);
if (ImGui::BeginCombo("##Address", addr_display.c_str())) {
// Z-addresses
if (!state.z_addresses.empty()) {
ImGui::TextDisabled("-- Shielded Addresses --");
for (size_t i = 0; i < state.z_addresses.size(); i++) {
const auto& addr = state.z_addresses[i];
std::string label = addr.address;
if (label.length() > static_cast<size_t>(zAddrLbl.truncate)) {
label = label.substr(0, zAddrFrontLbl.truncate) + "..." + label.substr(label.length() - zAddrBackLbl.truncate);
}
if (ImGui::Selectable(label.c_str(), s_address == addr.address)) {
strncpy(s_address, addr.address.c_str(), sizeof(s_address) - 1);
s_uri_dirty = true;
}
}
}
// T-addresses
if (!state.t_addresses.empty()) {
ImGui::TextDisabled("-- Transparent Addresses --");
for (size_t i = 0; i < state.t_addresses.size(); i++) {
const auto& addr = state.t_addresses[i];
std::string label = addr.address;
if (label.length() > static_cast<size_t>(tAddrLbl.truncate)) {
label = label.substr(0, tAddrFrontLbl.truncate) + "..." + label.substr(label.length() - tAddrBackLbl.truncate);
}
if (ImGui::Selectable(label.c_str(), s_address == addr.address)) {
strncpy(s_address, addr.address.c_str(), sizeof(s_address) - 1);
s_uri_dirty = true;
}
}
}
ImGui::EndCombo();
}
ImGui::Spacing();
// Amount (optional)
ImGui::Text("Amount (optional):");
ImGui::SetNextItemWidth(amountInput.width);
if (ImGui::InputDouble("##Amount", &s_amount, 0.1, 1.0, "%.8f")) {
s_uri_dirty = true;
}
ImGui::SameLine();
ImGui::TextDisabled("DRGX");
ImGui::Spacing();
// Label (optional)
ImGui::Text("Label (optional):");
ImGui::SetNextItemWidth(-1);
if (ImGui::InputText("##Label", s_label, sizeof(s_label))) {
s_uri_dirty = true;
}
ImGui::Spacing();
// Memo (optional, only for z-addr)
bool is_zaddr = (s_address[0] == 'z');
if (is_zaddr) {
ImGui::Text("Memo (optional):");
ImGui::SetNextItemWidth(-1);
if (ImGui::InputTextMultiline("##Memo", s_memo, sizeof(s_memo), ImVec2(-1, memoInput.height > 0 ? memoInput.height : 60))) {
s_uri_dirty = true;
}
} else {
s_memo[0] = '\0'; // Clear memo for t-addr
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Build and display payment URI
if (s_uri_dirty && s_address[0] != '\0') {
s_payment_uri = buildPaymentUri();
// Generate new QR code
if (s_qr_texture != 0) {
FreeQRTexture(s_qr_texture);
}
s_qr_texture = GenerateQRTexture(s_payment_uri.c_str(), &s_qr_width, &s_qr_height);
s_uri_dirty = false;
}
// QR Code display
if (s_qr_texture != 0) {
RenderQRCode(s_qr_texture, qr.size > 0 ? qr.size : 200);
}
ImGui::Spacing();
// Payment URI display
if (!s_payment_uri.empty()) {
ImGui::Text("Payment URI:");
ImGui::SetNextItemWidth(-1);
// Use a selectable text area for the URI
char uri_buf[1024];
strncpy(uri_buf, s_payment_uri.c_str(), sizeof(uri_buf) - 1);
ImGui::InputText("##URI", uri_buf, sizeof(uri_buf), ImGuiInputTextFlags_ReadOnly);
ImGui::Spacing();
// Copy button
if (material::StyledButton("Copy URI", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
ImGui::SetClipboardText(s_payment_uri.c_str());
Notifications::instance().success("Payment URI copied to clipboard");
}
ImGui::SameLine();
if (material::StyledButton("Copy Address", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
ImGui::SetClipboardText(s_address);
Notifications::instance().success("Address copied to clipboard");
}
}
ImGui::Spacing();
// Close button
if (material::StyledButton("Close", ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
s_open = false;
}
}
effects::ImGuiAcrylic::EndAcrylicPopup();
// Cleanup on close
if (!s_open && s_qr_texture != 0) {
FreeQRTexture(s_qr_texture);
s_qr_texture = 0;
}
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,32 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
class App;
namespace ui {
/**
* @brief Dialog for generating payment requests with QR codes
*/
class RequestPaymentDialog {
public:
/**
* @brief Show the request payment dialog
* @param address Pre-fill with this address (optional)
*/
static void show(const std::string& address = "");
/**
* @brief Render the dialog (call each frame)
*/
static void render(App* app);
};
} // namespace ui
} // namespace dragonx

1556
src/ui/windows/send_tab.cpp Normal file

File diff suppressed because it is too large Load Diff

34
src/ui/windows/send_tab.h Normal file
View File

@@ -0,0 +1,34 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
class App;
namespace ui {
/**
* @brief Render the Send tab
* Form for sending transactions
*/
void RenderSendTab(App* app);
/**
* @brief Render the send-confirm modal popup.
* Must be called at the top-level window scope (after ImGui::End of the main
* window) so the modal dim layer blocks ALL input — sidebar, tabs, etc.
*/
void RenderSendConfirmPopup(App* app);
/**
* @brief Pre-fill the "from" address in the send tab
* @param address The address to send from
*/
void SetSendFromAddress(const std::string& address);
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,567 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "settings_window.h"
#include "../../app.h"
#include "../../config/version.h"
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../../rpc/rpc_client.h"
#include "../theme.h"
#include "../schema/ui_schema.h"
#include "../schema/skin_manager.h"
#include "../notifications.h"
#include "../effects/imgui_acrylic.h"
#include "../material/draw_helpers.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <vector>
#include <filesystem>
// Icon text for settings UI
#define ICON_CUSTOM_THEME ICON_MD_TUNE
#define ICON_REFRESH_THEMES ICON_MD_REFRESH
namespace dragonx {
namespace ui {
// Settings state - these get loaded from Settings on window open
static bool s_initialized = false;
static int s_language_index = 0;
static bool s_save_ztxs = true;
static bool s_allow_custom_fees = false;
static bool s_auto_shield = false;
static bool s_fetch_prices = true;
static bool s_use_tor = false;
static char s_rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST;
static char s_rpc_port[16] = DRAGONX_DEFAULT_RPC_PORT;
static char s_rpc_user[64] = "";
static char s_rpc_password[64] = "";
static char s_tx_explorer[256] = "https://explorer.dragonx.is/tx/";
static char s_addr_explorer[256] = "https://explorer.dragonx.is/address/";
// Acrylic settings
static bool s_acrylic_enabled = true;
static float s_blur_amount = 1.5f; // 0.0=Off, continuous blur multiplier
static float s_noise_opacity = 0.5f;
static bool s_reduced_transparency = false; // Accessibility option
static bool s_gradient_background = false; // Gradient background mode
// Saved skin ID for cancel/revert
static std::string s_saved_skin_id;
// Load current settings into UI state
static void loadSettingsToUI(config::Settings* settings) {
if (!settings) return;
s_saved_skin_id = settings->getSkinId();
s_save_ztxs = settings->getSaveZtxs();
s_allow_custom_fees = settings->getAllowCustomFees();
s_auto_shield = settings->getAutoShield();
s_fetch_prices = settings->getFetchPrices();
s_use_tor = settings->getUseTor();
strncpy(s_tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(s_tx_explorer) - 1);
strncpy(s_addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(s_addr_explorer) - 1);
// Set language index
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
std::string current_lang = settings->getLanguage();
if (current_lang.empty()) current_lang = "en";
s_language_index = 0;
int idx = 0;
for (const auto& lang : languages) {
if (lang.first == current_lang) {
s_language_index = idx;
break;
}
idx++;
}
s_initialized = true;
}
// Save UI state to settings
static void saveSettingsFromUI(config::Settings* settings) {
if (!settings) return;
settings->setTheme(settings->getSkinId()); // Theme now synced with skin
settings->setSaveZtxs(s_save_ztxs);
settings->setAllowCustomFees(s_allow_custom_fees);
settings->setAutoShield(s_auto_shield);
settings->setFetchPrices(s_fetch_prices);
settings->setUseTor(s_use_tor);
settings->setTxExplorerUrl(s_tx_explorer);
settings->setAddressExplorerUrl(s_addr_explorer);
// Save language
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
auto it = languages.begin();
std::advance(it, s_language_index);
if (it != languages.end()) {
settings->setLanguage(it->first);
}
// Save acrylic / visual effects settings
settings->setAcrylicEnabled(s_acrylic_enabled);
settings->setAcrylicQuality(s_blur_amount > 0.001f ? static_cast<int>(effects::AcrylicQuality::Low) : static_cast<int>(effects::AcrylicQuality::Off));
settings->setBlurMultiplier(s_blur_amount);
settings->setReducedTransparency(s_reduced_transparency);
settings->setNoiseOpacity(s_noise_opacity);
settings->setGradientBackground(s_gradient_background);
settings->save();
}
void RenderSettingsWindow(App* app, bool* p_open)
{
// Load settings on first open
if (!s_initialized && app->settings()) {
loadSettingsToUI(app->settings());
// Initialize acrylic settings from current state
s_acrylic_enabled = effects::ImGuiAcrylic::IsEnabled();
s_blur_amount = effects::ImGuiAcrylic::GetBlurMultiplier();
s_noise_opacity = effects::ImGuiAcrylic::GetNoiseOpacity();
s_reduced_transparency = effects::ImGuiAcrylic::GetReducedTransparency();
s_gradient_background = schema::SkinManager::instance().isGradientMode();
}
auto& S = schema::UI();
auto win = S.window("dialogs.settings");
auto lbl = S.label("dialogs.settings", "label");
auto cmb = S.combo("dialogs.settings", "combo");
auto connLbl = S.label("dialogs.settings", "connection-label");
auto portInput = S.input("dialogs.settings", "port-input");
auto walletBtn = S.button("dialogs.settings", "wallet-button");
auto saveBtn = S.button("dialogs.settings", "save-button");
auto cancelBtn = S.button("dialogs.settings", "cancel-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
// Use acrylic modal popup
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Settings");
if (!effects::ImGuiAcrylic::BeginAcrylicPopupModal("Settings", p_open, ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
effects::ImGuiAcrylic::EndAcrylicPopup();
return;
}
if (ImGui::BeginTabBar("SettingsTabs")) {
// General settings tab
if (ImGui::BeginTabItem("General")) {
ImGui::Spacing();
// Skin/theme selection
ImGui::Text("Theme:");
ImGui::SameLine(lbl.position);
// Active skin combo (populated from SkinManager)
auto& skinMgr = schema::SkinManager::instance();
const auto& skins = skinMgr.available();
// Find active skin for preview text
std::string active_preview = "DragonX";
bool active_is_custom = false;
for (const auto& skin : skins) {
if (skin.id == skinMgr.activeSkinId()) {
active_preview = skin.name;
active_is_custom = !skin.bundled;
break;
}
}
ImGui::SetNextItemWidth(cmb.width);
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
// Bundled themes header
ImGui::TextDisabled("Built-in");
ImGui::Separator();
for (size_t i = 0; i < skins.size(); i++) {
const auto& skin = skins[i];
if (!skin.bundled) continue;
bool is_selected = (skin.id == skinMgr.activeSkinId());
if (ImGui::Selectable(skin.name.c_str(), is_selected)) {
skinMgr.setActiveSkin(skin.id);
if (app->settings()) {
app->settings()->setSkinId(skin.id);
app->settings()->save();
}
}
if (is_selected) {
ImGui::SetItemDefaultFocus();
}
}
// Custom themes (if any)
bool has_custom = false;
for (const auto& skin : skins) {
if (!skin.bundled) { has_custom = true; break; }
}
if (has_custom) {
ImGui::Spacing();
ImGui::TextDisabled("Custom");
ImGui::Separator();
for (size_t i = 0; i < skins.size(); i++) {
const auto& skin = skins[i];
if (skin.bundled) continue;
bool is_selected = (skin.id == skinMgr.activeSkinId());
if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::BeginDisabled(true);
std::string label = skin.name + " (invalid)";
ImGui::Selectable(label.c_str(), false);
ImGui::EndDisabled();
ImGui::PopStyleColor();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip("%s", skin.validationError.c_str());
}
} else {
std::string label = skin.name;
if (!skin.author.empty()) {
label += " (" + skin.author + ")";
}
if (ImGui::Selectable(label.c_str(), is_selected)) {
skinMgr.setActiveSkin(skin.id);
if (app->settings()) {
app->settings()->setSkinId(skin.id);
app->settings()->save();
}
}
if (is_selected) {
ImGui::SetItemDefaultFocus();
}
}
}
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Hotkey: Ctrl+Left/Right to cycle themes");
// Show indicator if custom theme is active
if (active_is_custom) {
ImGui::SameLine();
ImGui::PushFont(material::Type().iconSmall());
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), ICON_CUSTOM_THEME);
ImGui::PopFont();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Custom theme active");
}
}
ImGui::SameLine();
ImGui::PushFont(material::Type().iconSmall());
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
skinMgr.refresh();
Notifications::instance().info("Theme list refreshed");
}
ImGui::PopFont();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Scan for new themes.\nPlace theme folders in:\n%s",
schema::SkinManager::getUserSkinsDirectory().c_str());
}
ImGui::Spacing();
// Language selection
ImGui::Text("Language:");
ImGui::SameLine(lbl.position);
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
// Build language display names array
std::vector<const char*> lang_names;
lang_names.reserve(languages.size());
for (const auto& lang : languages) {
lang_names.push_back(lang.second.c_str()); // Display name
}
ImGui::SetNextItemWidth(cmb.width);
if (ImGui::Combo("##Language", &s_language_index, lang_names.data(), static_cast<int>(lang_names.size()))) {
// Get locale code from index
auto it = languages.begin();
std::advance(it, s_language_index);
i18n.loadLanguage(it->first);
}
ImGui::TextDisabled(" Note: Some text requires restart to update");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Acrylic Effects settings
ImGui::Text("Visual Effects");
ImGui::Spacing();
ImGui::Text("Acrylic Level:");
ImGui::SameLine(lbl.position);
ImGui::SetNextItemWidth(cmb.width);
{
char blur_fmt[16];
if (s_blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
else
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", s_blur_amount * 25.0f);
if (ImGui::SliderFloat("##AcrylicBlur", &s_blur_amount, 0.0f, 4.0f, blur_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
if (s_blur_amount > 0.0f && s_blur_amount < 0.15f) s_blur_amount = 0.0f;
s_acrylic_enabled = (s_blur_amount > 0.001f);
effects::ImGuiAcrylic::ApplyBlurAmount(s_blur_amount);
}
}
ImGui::TextDisabled(" Blur amount (0%% = off, 100%% = maximum)");
ImGui::Spacing();
ImGui::Text("Noise Opacity:");
ImGui::SameLine(lbl.position);
ImGui::SetNextItemWidth(cmb.width);
{
char noise_fmt[16];
if (s_noise_opacity < 0.01f)
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
else
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", s_noise_opacity * 100.0f);
if (ImGui::SliderFloat("##NoiseOpacity", &s_noise_opacity, 0.0f, 1.0f, noise_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
effects::ImGuiAcrylic::SetNoiseOpacity(s_noise_opacity);
}
}
ImGui::TextDisabled(" Grain texture intensity (0%% = off, 100%% = maximum)");
ImGui::Spacing();
// Accessibility: Reduced transparency
if (ImGui::Checkbox("Reduce transparency", &s_reduced_transparency)) {
effects::ImGuiAcrylic::SetReducedTransparency(s_reduced_transparency);
}
ImGui::TextDisabled(" Use solid colors instead of blur effects (accessibility)");
ImGui::Spacing();
if (ImGui::Checkbox("Simple background", &s_gradient_background)) {
schema::SkinManager::instance().setGradientMode(s_gradient_background);
}
ImGui::TextDisabled(" Replace textured backgrounds with smooth gradients");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Privacy settings
ImGui::Text("Privacy");
ImGui::Spacing();
ImGui::Checkbox("Save shielded transaction history locally", &s_save_ztxs);
ImGui::TextDisabled(" Stores z-addr transactions in a local file for viewing");
ImGui::Spacing();
ImGui::Checkbox("Auto-shield transparent funds", &s_auto_shield);
ImGui::TextDisabled(" Automatically move transparent funds to shielded addresses");
ImGui::Spacing();
ImGui::Checkbox("Use Tor for network connections", &s_use_tor);
ImGui::TextDisabled(" Route all connections through Tor for enhanced privacy");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Other settings
ImGui::Text("Other");
ImGui::Spacing();
ImGui::Checkbox("Allow custom transaction fees", &s_allow_custom_fees);
ImGui::Checkbox("Fetch price data from CoinGecko", &s_fetch_prices);
ImGui::EndTabItem();
}
// Connection settings tab
if (ImGui::BeginTabItem("Connection")) {
ImGui::Spacing();
ImGui::Text("RPC Connection");
ImGui::TextDisabled("Configure connection to dragonxd daemon");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Host:");
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCHost", s_rpc_host, sizeof(s_rpc_host));
ImGui::Text("Port:");
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(portInput.width);
ImGui::InputText("##RPCPort", s_rpc_port, sizeof(s_rpc_port));
ImGui::Spacing();
ImGui::Text("Username:");
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCUser", s_rpc_user, sizeof(s_rpc_user));
ImGui::Text("Password:");
ImGui::SameLine(connLbl.position);
ImGui::SetNextItemWidth(cmb.width);
ImGui::InputText("##RPCPassword", s_rpc_password, sizeof(s_rpc_password),
ImGuiInputTextFlags_Password);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::TextDisabled("Note: Connection settings are usually auto-detected from DRAGONX.conf");
ImGui::Spacing();
if (material::StyledButton("Test Connection", ImVec2(0,0), S.resolveFont("button"))) {
if (app->rpc()) {
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
if (error.empty()) {
std::string version = result.value("version", "unknown");
std::string msg = "Connection successful!\ndragonxd version: " + version;
Notifications::instance().success(msg);
} else {
Notifications::instance().error("Connection failed: " + error);
}
});
} else {
Notifications::instance().error("RPC client not initialized");
}
}
ImGui::EndTabItem();
}
// Wallet tab
if (ImGui::BeginTabItem("Wallet")) {
ImGui::Spacing();
ImGui::Text("Wallet Maintenance");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (material::StyledButton("Rescan Blockchain", ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
if (app->rpc()) {
// Start rescan from block 0
app->rpc()->rescanBlockchain(0, [](const nlohmann::json& result, const std::string& error) {
if (error.empty()) {
int start = result.value("start_height", 0);
int end = result.value("stop_height", 0);
std::string msg = "Rescan started from block " + std::to_string(start) +
" to " + std::to_string(end);
Notifications::instance().success(msg);
} else {
Notifications::instance().error("Rescan failed: " + error);
}
});
} else {
Notifications::instance().error("RPC client not initialized");
}
}
ImGui::TextDisabled(" Rescan blockchain for missing transactions");
ImGui::Spacing();
if (material::StyledButton("Clear Saved Z-Transaction History", ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
// Clear z-transaction history file
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared");
} else {
Notifications::instance().info("No history file found");
}
}
ImGui::TextDisabled(" Delete locally stored shielded transaction data");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Wallet Info");
ImGui::Spacing();
// Get actual wallet size
std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat";
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
if (wallet_size > 0) {
std::string size_str = util::Platform::formatFileSize(wallet_size);
ImGui::Text("Wallet file size: %s", size_str.c_str());
} else {
ImGui::TextDisabled("Wallet file not found");
}
ImGui::Text("Wallet location: %s", wallet_path.c_str());
ImGui::EndTabItem();
}
// Explorer tab
if (ImGui::BeginTabItem("Explorer")) {
ImGui::Spacing();
ImGui::Text("Block Explorer URLs");
ImGui::TextDisabled("Configure external block explorer links");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Transaction URL:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##TxExplorer", s_tx_explorer, sizeof(s_tx_explorer));
ImGui::Text("Address URL:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##AddrExplorer", s_addr_explorer, sizeof(s_addr_explorer));
ImGui::Spacing();
ImGui::TextDisabled("URLs should include a trailing slash. The txid/address will be appended.");
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Save/Cancel buttons
if (material::StyledButton("Save", ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
saveSettingsFromUI(app->settings());
Notifications::instance().success("Settings saved");
*p_open = false;
}
ImGui::SameLine();
if (material::StyledButton("Cancel", ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
// Reload settings to revert changes
loadSettingsToUI(app->settings());
// Revert skin to what was active when settings opened
if (!s_saved_skin_id.empty()) {
schema::SkinManager::instance().setActiveSkin(s_saved_skin_id);
if (app->settings()) {
app->settings()->setSkinId(s_saved_skin_id);
app->settings()->save();
}
}
*p_open = false;
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,19 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
/**
* @brief Render the Settings window
* Modal dialog for application settings
*/
void RenderSettingsWindow(App* app, bool* p_open);
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,312 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "shield_dialog.h"
#include "../../app.h"
#include "../../config/version.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "imgui.h"
#include <vector>
#include <string>
namespace dragonx {
namespace ui {
// Static state
static bool s_open = false;
static ShieldDialog::Mode s_mode = ShieldDialog::Mode::ShieldCoinbase;
static char s_from_address[512] = "*";
static char s_to_address[512] = "";
static double s_fee = DRAGONX_DEFAULT_FEE;
static int s_utxo_limit = 50; // overridden by schema at runtime
static bool s_operation_pending = false;
static std::string s_operation_id;
static std::string s_status_message;
static int s_selected_zaddr_idx = -1;
void ShieldDialog::show(Mode mode)
{
s_mode = mode;
s_open = true;
s_operation_pending = false;
s_status_message.clear();
s_operation_id.clear();
if (mode == Mode::ShieldCoinbase) {
strncpy(s_from_address, "*", sizeof(s_from_address));
} else {
s_from_address[0] = '\0';
}
s_to_address[0] = '\0';
s_fee = DRAGONX_DEFAULT_FEE;
s_utxo_limit = (int)schema::UI().drawElement("business", "utxo-limit").size;
s_selected_zaddr_idx = -1;
}
void ShieldDialog::showShieldCoinbase(const std::string& fromAddress)
{
show(Mode::ShieldCoinbase);
strncpy(s_from_address, fromAddress.c_str(), sizeof(s_from_address) - 1);
}
void ShieldDialog::showMerge()
{
show(Mode::MergeToAddress);
}
void ShieldDialog::render(App* app)
{
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.shield");
auto addrLbl = S.label("dialogs.shield", "address-label");
auto addrFrontLbl = S.label("dialogs.shield", "address-front-label");
auto addrBackLbl = S.label("dialogs.shield", "address-back-label");
auto feeInput = S.input("dialogs.shield", "fee-input");
auto utxoInput = S.input("dialogs.shield", "utxo-limit-input");
auto shieldBtn = S.button("dialogs.shield", "shield-button");
auto cancelBtn = S.button("dialogs.shield", "cancel-button");
const char* title = (s_mode == Mode::ShieldCoinbase)
? "Shield Coinbase Rewards"
: "Merge to Address";
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup(title);
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal(title, &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
const auto& state = app->getWalletState();
// Description
if (s_mode == Mode::ShieldCoinbase) {
ImGui::TextWrapped(
"Shield your mining rewards by sending coinbase outputs from "
"transparent addresses to a shielded address. This improves "
"privacy by hiding your mining income."
);
} else {
ImGui::TextWrapped(
"Merge multiple UTXOs into a single shielded address. This can "
"help reduce wallet size and improve privacy."
);
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// From address (for shield coinbase)
if (s_mode == Mode::ShieldCoinbase) {
ImGui::Text("From Address:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##FromAddr", s_from_address, sizeof(s_from_address));
ImGui::TextDisabled("Use '*' to shield from all transparent addresses");
ImGui::Spacing();
}
// To address (z-address dropdown)
ImGui::Text("To Address (Shielded):");
// Get z-addresses for dropdown
std::string to_display = s_to_address[0] ? s_to_address : "Select z-address...";
if (to_display.length() > static_cast<size_t>(addrLbl.truncate)) {
to_display = to_display.substr(0, addrFrontLbl.truncate) + "..." + to_display.substr(to_display.length() - addrBackLbl.truncate);
}
ImGui::SetNextItemWidth(-1);
if (ImGui::BeginCombo("##ToAddr", to_display.c_str())) {
for (size_t i = 0; i < state.z_addresses.size(); i++) {
const auto& addr = state.z_addresses[i];
std::string label = addr.address;
if (label.length() > static_cast<size_t>(addrLbl.truncate)) {
label = label.substr(0, addrFrontLbl.truncate) + "..." + label.substr(label.length() - addrBackLbl.truncate);
}
bool selected = (s_selected_zaddr_idx == static_cast<int>(i));
if (ImGui::Selectable(label.c_str(), selected)) {
s_selected_zaddr_idx = static_cast<int>(i);
strncpy(s_to_address, addr.address.c_str(), sizeof(s_to_address) - 1);
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
ImGui::Spacing();
// Fee
ImGui::Text("Fee:");
ImGui::SetNextItemWidth(feeInput.width);
ImGui::InputDouble("##Fee", &s_fee, 0.0001, 0.001, "%.8f");
ImGui::SameLine();
ImGui::TextDisabled("DRGX");
ImGui::Spacing();
// UTXO limit
ImGui::Text("UTXO Limit:");
ImGui::SetNextItemWidth(utxoInput.width);
ImGui::InputInt("##Limit", &s_utxo_limit);
ImGui::SameLine();
ImGui::TextDisabled("Max UTXOs per operation");
if (s_utxo_limit < 1) s_utxo_limit = 1;
if (s_utxo_limit > 100) s_utxo_limit = 100;
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Status message
if (!s_status_message.empty()) {
if (s_operation_pending) {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", s_status_message.c_str());
} else {
ImGui::TextWrapped("%s", s_status_message.c_str());
}
ImGui::Spacing();
}
// Buttons
bool can_submit = !s_operation_pending && s_to_address[0] != '\0';
if (!can_submit) ImGui::BeginDisabled();
const char* btn_label = (s_mode == Mode::ShieldCoinbase) ? "Shield Funds" : "Merge Funds";
if (material::StyledButton(btn_label, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) {
s_operation_pending = true;
s_status_message = "Submitting operation...";
if (s_mode == Mode::ShieldCoinbase) {
std::string from(s_from_address), to(s_to_address);
double fee = s_fee;
int limit = s_utxo_limit;
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), from, to, fee, limit]() -> rpc::RPCWorker::MainCb {
nlohmann::json result;
std::string error;
try {
result = rpc->call("z_shieldcoinbase", {from, to, fee, limit});
} catch (const std::exception& e) {
error = e.what();
}
return [result, error]() {
s_operation_pending = false;
if (error.empty()) {
s_operation_id = result.value("opid", "");
s_status_message = "Operation submitted: " + s_operation_id;
Notifications::instance().success("Shield operation started");
} else {
s_status_message = "Error: " + error;
Notifications::instance().error("Shield failed: " + error);
}
};
});
}
} else {
std::vector<std::string> fromAddrs;
fromAddrs.push_back("ANY_TADDR");
std::string to(s_to_address);
double fee = s_fee;
int limit = s_utxo_limit;
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), fromAddrs, to, fee, limit]() -> rpc::RPCWorker::MainCb {
nlohmann::json addrs = nlohmann::json::array();
for (const auto& addr : fromAddrs) addrs.push_back(addr);
nlohmann::json result;
std::string error;
try {
result = rpc->call("z_mergetoaddress", {addrs, to, fee, 0, limit});
} catch (const std::exception& e) {
error = e.what();
}
return [result, error]() {
s_operation_pending = false;
if (error.empty()) {
s_operation_id = result.value("opid", "");
s_status_message = "Operation submitted: " + s_operation_id;
Notifications::instance().success("Merge operation started");
} else {
s_status_message = "Error: " + error;
Notifications::instance().error("Merge failed: " + error);
}
};
});
}
}
}
if (!can_submit) ImGui::EndDisabled();
ImGui::SameLine();
if (material::StyledButton("Cancel", ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
s_open = false;
}
// Show operation status if we have an opid
if (!s_operation_id.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Operation ID: %s", s_operation_id.c_str());
if (material::StyledButton("Check Status", ImVec2(0,0), S.resolveFont(shieldBtn.font))) {
std::string opid = s_operation_id;
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), opid]() -> rpc::RPCWorker::MainCb {
nlohmann::json result;
std::string error;
try {
nlohmann::json ids = nlohmann::json::array();
ids.push_back(opid);
result = rpc->call("z_getoperationstatus", {ids});
} catch (const std::exception& e) {
error = e.what();
}
return [result, error]() {
if (error.empty() && result.is_array() && !result.empty()) {
auto& op = result[0];
std::string status = op.value("status", "unknown");
if (status == "success") {
s_status_message = "Operation completed successfully!";
Notifications::instance().success("Shield/merge completed!");
} else if (status == "failed") {
std::string errMsg = op.value("error", nlohmann::json{}).value("message", "Unknown error");
s_status_message = "Operation failed: " + errMsg;
Notifications::instance().error("Operation failed: " + errMsg);
} else if (status == "executing") {
s_status_message = "Operation in progress...";
} else {
s_status_message = "Status: " + status;
}
} else if (!error.empty()) {
s_status_message = "Error checking status: " + error;
}
};
});
}
}
}
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,47 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
class App;
namespace ui {
/**
* @brief Dialog for shielding coinbase rewards and merging funds
*/
class ShieldDialog {
public:
enum class Mode {
ShieldCoinbase, // Shield mining rewards (t-addr coinbase -> z-addr)
MergeToAddress // Merge multiple inputs to single z-addr
};
/**
* @brief Show the shield dialog
* @param mode Operating mode
*/
static void show(Mode mode = Mode::ShieldCoinbase);
/**
* @brief Show shield coinbase dialog for specific address
*/
static void showShieldCoinbase(const std::string& fromAddress = "*");
/**
* @brief Show merge to address dialog
*/
static void showMerge();
/**
* @brief Render the dialog (call each frame)
*/
static void render(App* app);
};
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,208 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "transaction_details_dialog.h"
#include "../../app.h"
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "imgui.h"
#include <ctime>
namespace dragonx {
namespace ui {
// Static member initialization
bool TransactionDetailsDialog::s_open = false;
TransactionInfo TransactionDetailsDialog::s_transaction;
void TransactionDetailsDialog::show(const TransactionInfo& tx)
{
s_open = true;
s_transaction = tx;
}
bool TransactionDetailsDialog::isOpen()
{
return s_open;
}
void TransactionDetailsDialog::render(App* app)
{
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.transaction-details");
auto lbl = S.label("dialogs.transaction-details", "label");
auto confLbl = S.label("dialogs.transaction-details", "confirmations-label");
auto txidInput = S.input("dialogs.transaction-details", "txid-input");
auto copyBtn = S.button("dialogs.transaction-details", "copy-button");
auto addrInput = S.input("dialogs.transaction-details", "address-input");
auto memoInput = S.input("dialogs.transaction-details", "memo-input");
auto bottomBtn = S.button("dialogs.transaction-details", "bottom-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Transaction Details");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Transaction Details", &s_open,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
const auto& tx = s_transaction;
// Type indicator with color
ImVec4 type_color;
std::string type_display;
if (tx.type == "receive") {
type_color = ImVec4(0.3f, 0.8f, 0.3f, 1.0f);
type_display = "RECEIVED";
} else if (tx.type == "send") {
type_color = ImVec4(0.8f, 0.3f, 0.3f, 1.0f);
type_display = "SENT";
} else if (tx.type == "generate" || tx.type == "mined") {
type_color = ImVec4(0.3f, 0.6f, 0.9f, 1.0f);
type_display = "MINED";
} else if (tx.type == "immature") {
type_color = ImVec4(0.8f, 0.8f, 0.3f, 1.0f);
type_display = "IMMATURE";
} else {
type_color = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
type_display = tx.type;
}
ImGui::TextColored(type_color, "%s", type_display.c_str());
ImGui::SameLine(ImGui::GetWindowWidth() - confLbl.position);
// Confirmations badge
if (tx.confirmations == 0) {
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "Pending");
} else if (tx.confirmations < 10) {
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.3f, 1.0f), "%d confirmations", tx.confirmations);
} else {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%d confirmations", tx.confirmations);
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Amount (prominent display)
ImGui::Text("Amount:");
ImGui::SameLine(lbl.position);
if (tx.amount >= 0) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "+%.8f DRGX", tx.amount);
} else {
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%.8f DRGX", tx.amount);
}
// USD equivalent if price available
double price_usd = app->state().market.price_usd;
if (price_usd > 0) {
ImGui::SameLine();
ImGui::TextDisabled("(~$%.2f USD)", std::abs(tx.amount) * price_usd);
}
ImGui::Spacing();
// Date/Time
ImGui::Text("Date:");
ImGui::SameLine(lbl.position);
ImGui::Text("%s", tx.getTimeString().c_str());
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Transaction ID
ImGui::Text("Transaction ID:");
char txid_buf[128];
strncpy(txid_buf, tx.txid.c_str(), sizeof(txid_buf) - 1);
txid_buf[sizeof(txid_buf) - 1] = '\0';
ImGui::SetNextItemWidth(txidInput.width);
ImGui::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly);
ImGui::SameLine();
if (material::StyledButton("Copy##TxID", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
ImGui::SetClipboardText(tx.txid.c_str());
}
ImGui::Spacing();
// Address
if (!tx.address.empty()) {
ImGui::Text(tx.type == "send" ? "To Address:" : "From Address:");
// Use multiline for z-addresses
if (tx.address.length() > 50) {
char addr_buf[512];
strncpy(addr_buf, tx.address.c_str(), sizeof(addr_buf) - 1);
addr_buf[sizeof(addr_buf) - 1] = '\0';
ImGui::InputTextMultiline("##Address", addr_buf, sizeof(addr_buf),
ImVec2(addrInput.width, addrInput.height > 0 ? addrInput.height : 50), ImGuiInputTextFlags_ReadOnly);
} else {
char addr_buf[128];
strncpy(addr_buf, tx.address.c_str(), sizeof(addr_buf) - 1);
addr_buf[sizeof(addr_buf) - 1] = '\0';
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
}
ImGui::SameLine();
if (material::StyledButton("Copy##Addr", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
ImGui::SetClipboardText(tx.address.c_str());
}
}
ImGui::Spacing();
// Memo (if present)
if (!tx.memo.empty()) {
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Memo:");
char memo_buf[512];
strncpy(memo_buf, tx.memo.c_str(), sizeof(memo_buf) - 1);
memo_buf[sizeof(memo_buf) - 1] = '\0';
ImGui::InputTextMultiline("##Memo", memo_buf, sizeof(memo_buf),
ImVec2(-1, memoInput.height > 0 ? memoInput.height : 60), ImGuiInputTextFlags_ReadOnly);
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Buttons
float button_width = bottomBtn.width;
float total_width = button_width * 2 + ImGui::GetStyle().ItemSpacing.x;
float start_x = (ImGui::GetWindowWidth() - total_width) / 2.0f;
ImGui::SetCursorPosX(start_x);
if (material::StyledButton("View on Explorer", ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
// Platform-specific URL opening
#ifdef _WIN32
ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
#elif __APPLE__
std::string cmd = "open \"" + url + "\"";
system(cmd.c_str());
#else
std::string cmd = "xdg-open \"" + url + "\" &";
system(cmd.c_str());
#endif
}
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
s_open = false;
}
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,44 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include "../../data/wallet_state.h"
namespace dragonx {
class App;
namespace ui {
/**
* @brief Dialog showing full transaction details
*/
class TransactionDetailsDialog {
public:
/**
* @brief Show the dialog for a transaction
* @param tx The transaction to display
*/
static void show(const TransactionInfo& tx);
/**
* @brief Render the dialog (call every frame)
* @param app Pointer to app instance
*/
static void render(App* app);
/**
* @brief Check if dialog is currently open
*/
static bool isOpen();
private:
static bool s_open;
static TransactionInfo s_transaction;
};
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,858 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "transactions_tab.h"
#include "transaction_details_dialog.h"
#include "export_transactions_dialog.h"
#include "../../app.h"
#include "../../config/settings.h"
#include "../../config/version.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../layout.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <algorithm>
#include <cctype>
#include <ctime>
#include <cmath>
#include <unordered_map>
namespace dragonx {
namespace ui {
using namespace material;
// Helper to truncate strings
static std::string truncateString(const std::string& str, int maxLen = 16) {
if (str.length() <= static_cast<size_t>(maxLen)) return str;
int half = (maxLen - 3) / 2;
return str.substr(0, half) + "..." + str.substr(str.length() - half);
}
// Case-insensitive string search
static bool containsIgnoreCase(const std::string& str, const std::string& search) {
if (search.empty()) return true;
std::string str_lower = str;
std::string search_lower = search;
std::transform(str_lower.begin(), str_lower.end(), str_lower.begin(), ::tolower);
std::transform(search_lower.begin(), search_lower.end(), search_lower.begin(), ::tolower);
return str_lower.find(search_lower) != std::string::npos;
}
// A display-ready transaction that may be a merged autoshield pair.
// For non-merged entries, send_idx or recv_idx is -1.
struct DisplayTx {
std::string txid;
std::string display_type; // "send", "receive", "mined", "immature", "shield"
double amount = 0.0;
int64_t timestamp = 0;
int confirmations = 0;
std::string address; // primary display address
std::string from_address;
std::string memo;
int orig_idx = -1; // index into state.transactions (first/primary)
int send_idx = -1; // index of send leg (shield only)
int recv_idx = -1; // index of recv leg (shield only)
bool is_shield = false;
bool isConfirmed() const { return confirmations >= 1; }
std::string getTimeString() const;
};
std::string DisplayTx::getTimeString() const {
if (timestamp <= 0) return "Pending";
std::time_t t = static_cast<std::time_t>(timestamp);
char buf[64];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", std::localtime(&t));
return buf;
}
// Relative time string
static std::string timeAgo(int64_t timestamp) {
if (timestamp <= 0) return "";
int64_t now = (int64_t)std::time(nullptr);
int64_t diff = now - timestamp;
if (diff < 0) diff = 0;
if (diff < 60) return std::to_string(diff) + "s ago";
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
return std::to_string(diff / 86400) + "d ago";
}
// Draw a small transaction-type icon
static void DrawTxIcon(ImDrawList* dl, const std::string& type,
float cx, float cy, float /*s*/, ImU32 col)
{
using namespace material;
ImFont* iconFont = Type().iconSmall();
const char* icon;
if (type == "send") {
icon = ICON_MD_CALL_MADE;
} else if (type == "receive") {
icon = ICON_MD_CALL_RECEIVED;
} else if (type == "shield") {
icon = ICON_MD_SHIELD;
} else {
icon = ICON_MD_CONSTRUCTION;
}
ImVec2 sz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(cx - sz.x * 0.5f, cy - sz.y * 0.5f), col, icon);
}
void RenderTransactionsTab(App* app)
{
auto& S = schema::UISchema::instance();
const auto searchInput = S.input("tabs.transactions", "search-input");
const auto filterCombo = S.combo("tabs.transactions", "filter-combo");
const auto filterGapEl = S.drawElement("tabs.transactions", "filter-gap");
const auto txTable = S.table("tabs.transactions", "transaction-table");
const auto addrLabel = S.label("tabs.transactions", "address-label");
const auto& state = app->state();
// Responsive scale factors (recomputed every frame)
ImVec2 contentAvail = ImGui::GetContentRegionAvail();
const float hs = Layout::hScale(contentAvail.x);
const float vs = Layout::vScale(contentAvail.y);
const float glassRound = Layout::glassRounding();
const float innerPad = Layout::cardInnerPadding();
const float cGap = Layout::cardGap();
// Non-scrolling container — content resizes to fit available height
ImVec2 txAvail = ImGui::GetContentRegionAvail();
ImGui::BeginChild("##TxScroll", txAvail, false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
glassSpec.rounding = glassRound;
ImFont* ovFont = Type().overline();
ImFont* capFont = Type().caption();
ImFont* body2 = Type().body2();
char buf[128];
ImU32 greenCol = Success();
ImU32 redCol = Error();
ImU32 goldCol = Warning();
// Expanded row index for inline detail
static int s_expanded_row = -1;
// Pagination state
static int s_current_page = 0;
static int s_prev_filter_hash = 0; // detect filter changes to reset page
// ================================================================
// Summary Cards — Received | Sent | Mined
// ================================================================
{
int recvCount = 0, sendCount = 0, minedCount = 0;
double recvTotal = 0.0, sendTotal = 0.0, minedTotal = 0.0;
for (const auto& tx : state.transactions) {
if (tx.type == "receive") {
recvCount++;
recvTotal += std::abs(tx.amount);
} else if (tx.type == "send") {
sendCount++;
sendTotal += std::abs(tx.amount);
} else if (tx.type == "generate" || tx.type == "immature" || tx.type == "mined") {
minedCount++;
minedTotal += std::abs(tx.amount);
}
}
float availWidth = ImGui::GetContentRegionAvail().x;
float cardGap = cGap;
float cardW = (availWidth - 2 * cardGap) / 3.0f;
float cardH = Layout::cardHeight(70.0f, vs);
float iconSz = std::max(4.0f, schema::UI().drawElement("tabs.transactions", "summary-icon-size").size * hs);
ImVec2 origin = ImGui::GetCursorScreenPos();
// Clickable type filter (clicking a card sets the type filter)
static int type_filter = 0;
// --- Received card ---
{
ImVec2 cMin = origin;
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
DrawGlassPanel(dl, cMin, cMax, glassSpec);
float cx = cMin.x + innerPad;
float cy = cMin.y + Layout::spacingMd();
// Icon
DrawTxIcon(dl, "receive", cx + iconSz, cy + iconSz * 1.33f, iconSz, greenCol);
float labelX = cx + iconSz * 3.0f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), "RECEIVED");
cy += ovFont->LegacySize + Layout::spacingSm();
snprintf(buf, sizeof(buf), "%d txs", recvCount);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
cy += capFont->LegacySize + Layout::spacingXs();
snprintf(buf, sizeof(buf), "+%.4f %s", recvTotal, DRAGONX_TICKER);
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), greenCol, buf);
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 2) ? 0 : 2;
}
}
// --- Sent card ---
{
float xOff = cardW + cardGap;
ImVec2 cMin(origin.x + xOff, origin.y);
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
DrawGlassPanel(dl, cMin, cMax, glassSpec);
float cx = cMin.x + innerPad;
float cy = cMin.y + Layout::spacingMd();
DrawTxIcon(dl, "send", cx + iconSz, cy + iconSz * 1.33f, iconSz, redCol);
float labelX = cx + iconSz * 3.0f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), "SENT");
cy += ovFont->LegacySize + Layout::spacingSm();
snprintf(buf, sizeof(buf), "%d txs", sendCount);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
cy += capFont->LegacySize + Layout::spacingXs();
snprintf(buf, sizeof(buf), "-%.4f %s", sendTotal, DRAGONX_TICKER);
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), redCol, buf);
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 1) ? 0 : 1;
}
}
// --- Mined card ---
{
float xOff = 2 * (cardW + cardGap);
ImVec2 cMin(origin.x + xOff, origin.y);
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
DrawGlassPanel(dl, cMin, cMax, glassSpec);
float cx = cMin.x + innerPad;
float cy = cMin.y + Layout::spacingMd();
DrawTxIcon(dl, "mined", cx + iconSz, cy + iconSz * 1.33f, iconSz, goldCol);
float labelX = cx + iconSz * 3.0f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), "MINED");
cy += ovFont->LegacySize + Layout::spacingSm();
snprintf(buf, sizeof(buf), "%d txs", minedCount);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
cy += capFont->LegacySize + Layout::spacingXs();
snprintf(buf, sizeof(buf), "+%.4f %s", minedTotal, DRAGONX_TICKER);
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), goldCol, buf);
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 3) ? 0 : 3;
}
}
// Selected card accent
if (type_filter > 0) {
int idx_map[] = {-1, 1, 0, 2};
int idx = idx_map[type_filter];
float xOff = idx * (cardW + cardGap);
ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3);
ImVec2 acMax(origin.x + xOff + cardW, origin.y + cardH);
ImU32 acCol = (type_filter == 1) ? redCol : (type_filter == 2) ? greenCol : goldCol;
dl->AddRectFilled(acMin, acMax, acCol, 2.0f);
}
ImGui::Dummy(ImVec2(availWidth, cardH));
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// ================================================================
// Search & Filter bar
// ================================================================
static char search_filter[128] = "";
float searchMaxW = (searchInput.maxWidth >= 0) ? searchInput.maxWidth : 300.0f;
float searchRatio = (searchInput.widthRatio >= 0) ? searchInput.widthRatio : 0.30f;
float searchWidth = std::min(searchMaxW * hs, availWidth * searchRatio);
ImGui::SetNextItemWidth(searchWidth);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
ImGui::InputTextWithHint("##TxSearch", "Search...", search_filter, sizeof(search_filter));
ImGui::PopStyleVar();
float filterGap = std::max(8.0f, ((filterGapEl.size > 0) ? filterGapEl.size : 20.0f) * hs);
ImGui::SameLine(0, filterGap);
float comboW = std::max(80.0f, ((filterCombo.width > 0) ? filterCombo.width : 120.0f) * hs);
ImGui::SetNextItemWidth(comboW);
const char* types[] = { "All", "Sent", "Received", "Mined" };
ImGui::Combo("##TxType", &type_filter, types, IM_ARRAYSIZE(types));
ImGui::SameLine(0, filterGap);
if (TactileButton("Refresh", ImVec2(0, 0), S.resolveFont("button"))) {
app->refreshNow();
}
ImGui::SameLine(0, filterGap);
if (TactileButton("Export CSV", ImVec2(0, 0), S.resolveFont("button"))) {
ExportTransactionsDialog::show();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm() + Layout::spacingXs()));
// ================================================================
// Transaction list — DrawList-based rows in scrollable child
// ================================================================
int filtered_count = 0;
std::string search_str(search_filter);
// Build display list, merging autoshield send+receive pairs.
// Two entries sharing the same txid where one is "send" and
// one is "receive" to a z-address are combined into a single
// "shield" entry. We first index by txid, then build the list.
std::vector<DisplayTx> display_txns;
{
// Map txid -> indices in state.transactions
std::unordered_map<std::string, std::vector<size_t>> txid_map;
for (size_t i = 0; i < state.transactions.size(); i++) {
txid_map[state.transactions[i].txid].push_back(i);
}
std::vector<bool> consumed(state.transactions.size(), false);
for (size_t i = 0; i < state.transactions.size(); i++) {
if (consumed[i]) continue;
const auto& tx = state.transactions[i];
// Try to find an autoshield pair (same txid, send+receive to z-addr)
bool merged = false;
const auto& siblings = txid_map[tx.txid];
if (siblings.size() >= 2) {
int send_i = -1, recv_i = -1;
for (size_t si : siblings) {
if (consumed[si]) continue;
const auto& stx = state.transactions[si];
if (stx.type == "send" && send_i < 0) send_i = (int)si;
else if (stx.type == "receive" && recv_i < 0) recv_i = (int)si;
}
if (send_i >= 0 && recv_i >= 0) {
const auto& stx = state.transactions[send_i];
const auto& rtx = state.transactions[recv_i];
// Confirm receive goes to a z-address (shielded)
bool recv_is_shielded = !rtx.address.empty() && rtx.address[0] == 'z';
if (recv_is_shielded) {
DisplayTx dtx;
dtx.txid = tx.txid;
dtx.display_type = "shield";
dtx.is_shield = true;
dtx.amount = rtx.amount; // positive receive amount
dtx.timestamp = std::max(stx.timestamp, rtx.timestamp);
dtx.confirmations = std::min(stx.confirmations, rtx.confirmations);
dtx.address = rtx.address; // shielded destination
dtx.from_address = stx.address.empty() ? stx.from_address : stx.address;
dtx.memo = rtx.memo.empty() ? stx.memo : rtx.memo;
dtx.orig_idx = send_i;
dtx.send_idx = send_i;
dtx.recv_idx = recv_i;
consumed[send_i] = true;
consumed[recv_i] = true;
display_txns.push_back(std::move(dtx));
merged = true;
}
}
}
if (!merged) {
consumed[i] = true;
DisplayTx dtx;
dtx.txid = tx.txid;
dtx.display_type = tx.type;
dtx.amount = tx.amount;
dtx.timestamp = tx.timestamp;
dtx.confirmations = tx.confirmations;
dtx.address = tx.address;
dtx.from_address = tx.from_address;
dtx.memo = tx.memo;
dtx.orig_idx = (int)i;
display_txns.push_back(std::move(dtx));
}
}
// Sort by timestamp descending (same as raw list)
std::sort(display_txns.begin(), display_txns.end(),
[](const DisplayTx& a, const DisplayTx& b) {
return a.timestamp > b.timestamp;
});
}
// Apply type + search filters
std::vector<size_t> filtered_indices;
for (size_t i = 0; i < display_txns.size(); i++) {
const auto& dtx = display_txns[i];
if (type_filter != 0) {
if (type_filter == 1 && dtx.display_type != "send") continue;
if (type_filter == 2 && dtx.display_type != "receive" && dtx.display_type != "shield") continue;
if (type_filter == 3 && dtx.display_type != "generate" && dtx.display_type != "immature" && dtx.display_type != "mined") continue;
}
if (!search_str.empty()) {
if (!containsIgnoreCase(dtx.address, search_str) &&
!containsIgnoreCase(dtx.txid, search_str) &&
!containsIgnoreCase(dtx.memo, search_str) &&
!(dtx.is_shield && containsIgnoreCase(std::string("shielded"), search_str))) {
continue;
}
}
filtered_indices.push_back(i);
}
filtered_count = static_cast<int>(filtered_indices.size());
// Pagination — slice filtered results into pages
int perPage = std::max(10, (int)schema::UI().drawElement("tabs.transactions", "rows-per-page").sizeOr(50.0f));
int totalPages = std::max(1, (filtered_count + perPage - 1) / perPage);
// Reset page when filters change
int filterHash = type_filter * 1000003 + filtered_count * 31 + static_cast<int>(search_str.size());
if (filterHash != s_prev_filter_hash) {
s_current_page = 0;
s_prev_filter_hash = filterHash;
}
if (s_current_page >= totalPages) s_current_page = totalPages - 1;
if (s_current_page < 0) s_current_page = 0;
int pageStart = s_current_page * perPage;
int pageEnd = std::min(pageStart + perPage, filtered_count);
// ---- Heading line: "TRANSACTIONS" left, pagination right ----
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TRANSACTIONS");
if (totalPages > 1) {
float paginationH = ImGui::GetFrameHeight();
float btnW = paginationH;
float gap = Layout::spacingSm();
float pageNumW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, " 999 / 999 ").x + Layout::spacingLg();
float totalPagW = btnW * 4 + pageNumW + gap * 4;
// Right-align: position cursor so the group ends at the right edge
ImGui::SameLine();
float startX = ImGui::GetContentRegionMax().x - totalPagW;
ImGui::SetCursorPosX(startX);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.5f, 0.5f));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 30)));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 45)));
// First page
ImGui::BeginDisabled(s_current_page == 0);
ImGui::PushFont(Type().iconSmall());
if (ImGui::Button(ICON_MD_FIRST_PAGE "##txFirst", ImVec2(btnW, btnW))) {
s_current_page = 0;
s_expanded_row = -1;
}
ImGui::PopFont();
ImGui::EndDisabled();
ImGui::SameLine(0, gap);
// Previous page
ImGui::BeginDisabled(s_current_page == 0);
ImGui::PushFont(Type().iconSmall());
if (ImGui::Button(ICON_MD_CHEVRON_LEFT "##txPrev", ImVec2(btnW, btnW))) {
s_current_page--;
s_expanded_row = -1;
}
ImGui::PopFont();
ImGui::EndDisabled();
ImGui::SameLine(0, gap);
// Page indicator — render centered text over a fixed-width dummy
{
snprintf(buf, sizeof(buf), "%d / %d", s_current_page + 1, totalPages);
ImVec2 pageSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
ImVec2 regionPos = ImGui::GetCursorScreenPos();
// Reserve the fixed width without advancing to a new line
ImGui::InvisibleButton("##pageNum", ImVec2(pageNumW, paginationH));
// Draw the text centered within that reserved area
ImVec2 textPos(
regionPos.x + (pageNumW - pageSz.x) * 0.5f,
regionPos.y + (paginationH - pageSz.y) * 0.5f
);
ImGui::GetWindowDrawList()->AddText(capFont, capFont->LegacySize, textPos, OnSurfaceMedium(), buf);
}
ImGui::SameLine(0, gap);
// Next page
ImGui::BeginDisabled(s_current_page >= totalPages - 1);
ImGui::PushFont(Type().iconSmall());
if (ImGui::Button(ICON_MD_CHEVRON_RIGHT "##txNext", ImVec2(btnW, btnW))) {
s_current_page++;
s_expanded_row = -1;
}
ImGui::PopFont();
ImGui::EndDisabled();
ImGui::SameLine(0, gap);
// Last page
ImGui::BeginDisabled(s_current_page >= totalPages - 1);
ImGui::PushFont(Type().iconSmall());
if (ImGui::Button(ICON_MD_LAST_PAGE "##txLast", ImVec2(btnW, btnW))) {
s_current_page = totalPages - 1;
s_expanded_row = -1;
}
ImGui::PopFont();
ImGui::EndDisabled();
ImGui::PopStyleColor(3);
ImGui::PopStyleVar(2);
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Glass panel wrapping the list area — scale reserve with vScale
float scaledReserve = (txTable.bottomReserve > 0) ? txTable.bottomReserve : std::max(schema::UI().drawElement("tabs.transactions", "bottom-reserve-min").size, schema::UI().drawElement("tabs.transactions", "bottom-reserve-base").size * vs);
float listH = ImGui::GetContentRegionAvail().y - scaledReserve;
float minListH = std::max(schema::UI().drawElement("tabs.transactions", "list-min-height").size, schema::UI().drawElement("tabs.transactions", "list-base-height").size * vs);
if (listH < minListH) listH = minListH;
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
ImVec2 listPanelMax(listPanelMin.x + availWidth, listPanelMin.y + listH);
DrawGlassPanel(dl, listPanelMin, listPanelMax, glassSpec);
// Scroll state for clipping mask (captured inside child, used after EndChild)
float scrollY = 0.0f;
float scrollMaxY = 0.0f;
// Vertex start indices for CSS-style clipping mask (alpha fade at edges)
int vtxMaskStart = dl->VtxBuffer.Size;
ImGui::BeginChild("##TxList", ImVec2(availWidth, listH), false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
ApplySmoothScroll();
ImDrawList* childDL = ImGui::GetWindowDrawList();
int childVtxStart = childDL->VtxBuffer.Size;
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
{
if (!app->isConnected()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " Not connected to daemon...");
} else if (state.transactions.empty()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No transactions found");
} else if (filtered_indices.empty()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No matching transactions");
} else {
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
float innerW = ImGui::GetContentRegionAvail().x;
float rowIconSz = std::max(3.5f, schema::UI().drawElement("tabs.transactions", "row-icon-size").size * hs);
float rowPadLeft = Layout::spacingLg();
// Scroll state for gradient overlay fade (drawn after EndChild)
scrollY = ImGui::GetScrollY();
scrollMaxY = ImGui::GetScrollMaxY();
// Viewport culling bounds
float viewTop = scrollY;
float viewBot = scrollY + ImGui::GetWindowHeight();
// Render only the current page slice
for (int fi = pageStart; fi < pageEnd; fi++) {
size_t i = filtered_indices[fi];
const auto& tx = display_txns[i];
bool is_expanded = (s_expanded_row == static_cast<int>(i));
ImGui::PushID(static_cast<int>(i));
ImVec2 rowPos = ImGui::GetCursorScreenPos();
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
// Determine type info
ImU32 iconCol;
const char* typeStr;
if (tx.display_type == "shield") {
iconCol = Primary(); typeStr = "Shielded";
} else if (tx.display_type == "receive") {
iconCol = greenCol; typeStr = "Recv";
} else if (tx.display_type == "send") {
iconCol = redCol; typeStr = "Sent";
} else if (tx.display_type == "immature") {
iconCol = Warning(); typeStr = "Immature";
} else {
iconCol = goldCol; typeStr = "Mined";
}
// Expanded selection accent
if (is_expanded) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), schema::UI().drawElement("tabs.transactions", "row-hover-rounding").size);
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + schema::UI().drawElement("tabs.transactions", "row-accent-width").size, rowEnd.y), Primary(), schema::UI().drawElement("tabs.transactions", "accent-bar-rounding").size);
}
// Hover glow
bool hovered = material::IsRectHovered(rowPos, rowEnd);
if (hovered && !is_expanded) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), schema::UI().drawElement("tabs.transactions", "row-hover-rounding").size);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
float cx = rowPos.x + rowPadLeft;
float cy = rowPos.y + Layout::spacingMd();
// Icon
DrawTxIcon(dl, tx.display_type, cx + rowIconSz, cy + body2->LegacySize * 0.5f, rowIconSz, iconCol);
// Type label
float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm();
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), iconCol, typeStr);
// Time (next to type)
std::string ago = timeAgo(tx.timestamp);
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeStr).x;
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy),
OnSurfaceDisabled(), ago.c_str());
// Address (second line, left side)
std::string addr_display = truncateString(tx.address, (addrLabel.truncate > 0) ? addrLabel.truncate : 20);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceMedium(), addr_display.c_str());
// Amount (right-aligned, first line)
ImU32 amtCol = (tx.amount >= 0) ? greenCol : redCol;
if (tx.amount >= 0)
snprintf(buf, sizeof(buf), "+%.8f", tx.amount);
else
snprintf(buf, sizeof(buf), "%.8f", tx.amount);
ImVec2 amtSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, buf);
float amtX = rowPos.x + innerW - amtSz.x - Layout::spacingLg();
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(amtX, cy), amtCol, buf,
1.0f, 1.0f, IM_COL32(0, 0, 0, 120));
// USD equivalent (right-aligned, second line)
double priceUsd = state.market.price_usd;
if (priceUsd > 0.0) {
double usdVal = std::abs(tx.amount) * priceUsd;
if (usdVal >= 1.0)
snprintf(buf, sizeof(buf), "$%.2f", usdVal);
else if (usdVal >= 0.01)
snprintf(buf, sizeof(buf), "$%.4f", usdVal);
else
snprintf(buf, sizeof(buf), "$%.6f", usdVal);
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(rowPos.x + innerW - usdSz.x - Layout::spacingLg(), cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceDisabled(), buf);
}
// Status badge (centered area, second line)
{
const char* statusStr;
ImU32 statusCol;
if (tx.confirmations == 0) {
statusStr = "Pending"; statusCol = Warning();
} else if (tx.confirmations < 10) {
snprintf(buf, sizeof(buf), "%d conf", tx.confirmations);
statusStr = buf; statusCol = Warning();
} else if (tx.confirmations >= 100 && (tx.display_type == "generate" || tx.display_type == "mined")) {
statusStr = "Mature"; statusCol = greenCol;
} else {
statusStr = "Confirmed"; statusCol = WithAlpha(Success(), 140);
}
// Position status badge in the middle-right area
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
float statusX = amtX - sSz.x - Layout::spacingXxl();
float minStatusX = cx + innerW * 0.25f; // don't overlap address
if (statusX < minStatusX) statusX = minStatusX;
// Background pill
ImU32 pillBg = (statusCol & 0x00FFFFFFu) | (static_cast<ImU32>(30) << 24);
ImVec2 pillMin(statusX - Layout::spacingSm(), cy + body2->LegacySize + 1);
ImVec2 pillMax(statusX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs());
dl->AddRectFilled(pillMin, pillMax, pillBg, schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(statusX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
}
// Click to expand/collapse + invisible button for interaction
ImGui::InvisibleButton("##txRow", ImVec2(innerW, rowH));
if (ImGui::IsItemClicked(0)) {
s_expanded_row = is_expanded ? -1 : static_cast<int>(i);
}
// Tooltip
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s\n%s\n%s", tx.address.c_str(),
tx.txid.c_str(), tx.getTimeString().c_str());
}
// Context menu
const auto& acrylicTheme = GetCurrentAcrylicTheme();
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("TxContext", 0, acrylicTheme.menu)) {
if (ImGui::MenuItem("Copy Address") && !tx.address.empty()) {
ImGui::SetClipboardText(tx.address.c_str());
}
if (ImGui::MenuItem("Copy TxID")) {
ImGui::SetClipboardText(tx.txid.c_str());
}
ImGui::Separator();
if (ImGui::MenuItem("View on Explorer")) {
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
#ifdef _WIN32
ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
#elif __APPLE__
std::string cmd = "open \"" + url + "\"";
system(cmd.c_str());
#else
std::string cmd = "xdg-open \"" + url + "\" &";
system(cmd.c_str());
#endif
}
if (ImGui::MenuItem("View Details")) {
if (tx.orig_idx >= 0 && tx.orig_idx < (int)state.transactions.size())
TransactionDetailsDialog::show(state.transactions[tx.orig_idx]);
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
// ---- Inline detail expansion ----
if (is_expanded) {
ImVec2 detailPos = ImGui::GetCursorScreenPos();
// We'll draw the glass panel after measuring the content
float detailPad = Layout::spacingLg();
float detailW = innerW - detailPad * 2;
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + detailPad);
ImGui::BeginGroup();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + detailW);
// From address
if (!tx.from_address.empty()) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "FROM");
ImGui::TextWrapped("%s", tx.from_address.c_str());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
// To address
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(),
tx.display_type == "send" ? "TO" : (tx.display_type == "shield" ? "SHIELDED TO" : "ADDRESS"));
ImGui::TextWrapped("%s", tx.address.c_str());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// TxID (full, copyable)
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TRANSACTION ID");
ImGui::TextWrapped("%s", tx.txid.c_str());
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsItemClicked()) ImGui::SetClipboardText(tx.txid.c_str());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Memo
if (!tx.memo.empty()) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO");
ImGui::TextWrapped("%s", tx.memo.c_str());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
// Confirmations + time
snprintf(buf, sizeof(buf), "%d confirmations | %s",
tx.confirmations, tx.getTimeString().c_str());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Action buttons
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 30)));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.transactions", "detail-btn-rounding").size);
if (TactileSmallButton("Copy TxID##detail", S.resolveFont("button"))) {
ImGui::SetClipboardText(tx.txid.c_str());
}
ImGui::SameLine();
if (!tx.address.empty() && TactileSmallButton("Copy Address##detail", S.resolveFont("button"))) {
ImGui::SetClipboardText(tx.address.c_str());
}
ImGui::SameLine();
if (TactileSmallButton("Explorer##detail", S.resolveFont("button"))) {
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
#ifdef _WIN32
ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
#elif __APPLE__
std::string cmd2 = "open \"" + url + "\"";
system(cmd2.c_str());
#else
std::string cmd2 = "xdg-open \"" + url + "\" &";
system(cmd2.c_str());
#endif
}
ImGui::SameLine();
if (TactileSmallButton("Full Details##detail", S.resolveFont("button"))) {
if (tx.orig_idx >= 0 && tx.orig_idx < (int)state.transactions.size())
TransactionDetailsDialog::show(state.transactions[tx.orig_idx]);
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(2);
ImGui::PopTextWrapPos();
ImGui::EndGroup();
// Draw glass panel behind detail area
ImVec2 detailEnd = ImGui::GetCursorScreenPos();
float detailH = detailEnd.y - detailPos.y + Layout::spacingMd();
GlassPanelSpec detailGlass;
detailGlass.rounding = glassRound * 0.75f;
detailGlass.fillAlpha = 25;
DrawGlassPanel(dl, ImVec2(detailPos.x + Layout::spacingSm() + Layout::spacingXs(), detailPos.y),
ImVec2(detailPos.x + innerW - Layout::spacingSm() - Layout::spacingXs(), detailPos.y + detailH), detailGlass);
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
}
// Subtle divider between rows
if (fi < pageEnd - 1 && !is_expanded) {
ImVec2 divStart = ImGui::GetCursorScreenPos();
dl->AddLine(ImVec2(divStart.x + rowPadLeft + rowIconSz * 2.0f, divStart.y),
ImVec2(divStart.x + innerW - Layout::spacingLg(), divStart.y),
IM_COL32(255, 255, 255, 15));
}
ImGui::PopID();
}
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::EndChild();
// CSS-style clipping mask
{
float fadeZone = std::min(
(body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd()) * 1.2f,
listH * 0.18f);
ApplyScrollEdgeMask(dl, vtxMaskStart, childDL, childVtxStart,
listPanelMin.y, listPanelMax.y, fadeZone, scrollY, scrollMaxY);
}
// Status line with page info
snprintf(buf, sizeof(buf), "Showing %d\xe2\x80\x93%d of %d transactions (total: %zu)",
filtered_count > 0 ? pageStart + 1 : 0, pageEnd, filtered_count, state.transactions.size());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
}
ImGui::EndChild(); // ##TxScroll
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,19 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
/**
* @brief Render the Transactions tab
* Shows transaction history
*/
void RenderTransactionsTab(App* app);
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,224 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "validate_address_dialog.h"
#include "../../app.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
// Static member initialization
bool ValidateAddressDialog::s_open = false;
bool ValidateAddressDialog::s_validated = false;
bool ValidateAddressDialog::s_validating = false;
char ValidateAddressDialog::s_address_input[512] = "";
bool ValidateAddressDialog::s_is_valid = false;
bool ValidateAddressDialog::s_is_mine = false;
std::string ValidateAddressDialog::s_address_type;
std::string ValidateAddressDialog::s_error_message;
void ValidateAddressDialog::show()
{
s_open = true;
s_validated = false;
s_validating = false;
s_address_input[0] = '\0';
s_is_valid = false;
s_is_mine = false;
s_address_type.clear();
s_error_message.clear();
}
bool ValidateAddressDialog::isOpen()
{
return s_open;
}
void ValidateAddressDialog::render(App* app)
{
if (!s_open) return;
auto& S = schema::UI();
auto win = S.window("dialogs.validate-address");
auto valBtn = S.button("dialogs.validate-address", "validate-button");
auto pasteBtn = S.button("dialogs.validate-address", "paste-button");
auto lbl = S.label("dialogs.validate-address", "label");
auto closeBtn = S.button("dialogs.validate-address", "close-button");
ImGui::SetNextWindowSize(ImVec2(win.width, win.height), ImGuiCond_FirstUseEver);
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowFocus();
const auto& acrylicTheme = GetCurrentAcrylicTheme();
ImGui::OpenPopup("Validate Address");
if (effects::ImGuiAcrylic::BeginAcrylicPopupModal("Validate Address", &s_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
ImGui::TextWrapped("Enter a DragonX address to check if it's valid and whether it belongs to this wallet.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Address input
ImGui::Text("Address:");
ImGui::SetNextItemWidth(-1);
bool enter_pressed = ImGui::InputText("##ValidateAddr", s_address_input, sizeof(s_address_input),
ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::Spacing();
// Validate button
bool can_validate = strlen(s_address_input) > 0 && !s_validating && app->isConnected();
if (!can_validate) {
ImGui::BeginDisabled();
}
if (material::StyledButton("Validate", ImVec2(valBtn.width, 0), S.resolveFont(valBtn.font)) || (enter_pressed && can_validate)) {
s_validating = true;
s_validated = false;
s_error_message.clear();
std::string address(s_address_input);
// Determine if z-address or t-address
bool is_zaddr = !address.empty() && address[0] == 'z';
if (is_zaddr) {
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), address]() -> rpc::RPCWorker::MainCb {
bool valid = false, mine = false;
std::string error;
try {
auto result = rpc->call("validateaddress", {address});
valid = result.value("isvalid", false);
mine = result.value("ismine", false);
} catch (const std::exception& e) {
error = e.what();
}
return [valid, mine, error]() {
if (error.empty()) {
s_is_valid = valid;
s_is_mine = mine;
s_address_type = "Shielded (z-address)";
} else {
s_error_message = error;
s_is_valid = false;
}
s_validated = true;
s_validating = false;
};
});
}
} else {
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), address]() -> rpc::RPCWorker::MainCb {
bool valid = false, mine = false;
std::string error;
try {
auto result = rpc->call("validateaddress", {address});
valid = result.value("isvalid", false);
mine = result.value("ismine", false);
} catch (const std::exception& e) {
error = e.what();
}
return [valid, mine, error]() {
if (error.empty()) {
s_is_valid = valid;
s_is_mine = mine;
s_address_type = "Transparent (t-address)";
} else {
s_error_message = error;
s_is_valid = false;
}
s_validated = true;
s_validating = false;
};
});
}
}
}
if (!can_validate) {
ImGui::EndDisabled();
}
ImGui::SameLine();
if (material::StyledButton("Paste", ImVec2(pasteBtn.width, 0), S.resolveFont(pasteBtn.font))) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
strncpy(s_address_input, clipboard, sizeof(s_address_input) - 1);
s_address_input[sizeof(s_address_input) - 1] = '\0';
s_validated = false;
}
}
if (s_validating) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Validating...");
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Results
if (s_validated) {
ImGui::Text("Results:");
ImGui::Spacing();
if (!s_error_message.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Error: %s", s_error_message.c_str());
} else {
// Valid/Invalid indicator
ImGui::Text("Status:");
ImGui::SameLine(lbl.position);
if (s_is_valid) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "VALID");
} else {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "INVALID");
}
if (s_is_valid) {
// Address type
ImGui::Text("Type:");
ImGui::SameLine(lbl.position);
ImGui::Text("%s", s_address_type.c_str());
// Is mine?
ImGui::Text("Ownership:");
ImGui::SameLine(lbl.position);
if (s_is_mine) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "This wallet owns this address");
} else {
ImGui::TextDisabled("Not owned by this wallet");
}
}
}
} else if (!app->isConnected()) {
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Not connected to daemon");
}
ImGui::Spacing();
// Close button at bottom
float button_width = closeBtn.width;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) / 2.0f);
if (material::StyledButton("Close", ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;
}
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,50 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
class App;
namespace ui {
/**
* @brief Dialog for validating cryptocurrency addresses
*/
class ValidateAddressDialog {
public:
/**
* @brief Show the validate address dialog
*/
static void show();
/**
* @brief Render the dialog (call every frame)
* @param app Pointer to app instance for RPC calls
*/
static void render(App* app);
/**
* @brief Check if dialog is currently open
*/
static bool isOpen();
private:
static bool s_open;
static bool s_validated;
static bool s_validating;
static char s_address_input[512];
// Validation results
static bool s_is_valid;
static bool s_is_mine;
static std::string s_address_type;
static std::string s_error_message;
};
} // namespace ui
} // namespace dragonx