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)
319 lines
9.6 KiB
C++
319 lines
9.6 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#pragma once
|
|
|
|
#include "../material/material.h"
|
|
#include "../../embedded/IconsMaterialDesign.h"
|
|
#include "imgui.h"
|
|
#include <string>
|
|
#include <vector>
|
|
#include <functional>
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
namespace screens {
|
|
|
|
using namespace material;
|
|
|
|
// ============================================================================
|
|
// Receive Screen
|
|
// ============================================================================
|
|
// Display receiving addresses with QR codes
|
|
|
|
/**
|
|
* @brief Wallet address for receiving
|
|
*/
|
|
struct WalletAddress {
|
|
std::string address;
|
|
std::string label;
|
|
std::string type; // "shielded" or "transparent"
|
|
double balance;
|
|
bool isDefault;
|
|
};
|
|
|
|
/**
|
|
* @brief Receive screen with address cards
|
|
*/
|
|
class ReceiveScreen {
|
|
public:
|
|
void setAddresses(const std::vector<WalletAddress>& addresses) {
|
|
m_addresses = addresses;
|
|
}
|
|
|
|
void setOnCopyAddress(std::function<void(const std::string&)> callback) {
|
|
m_onCopyAddress = callback;
|
|
}
|
|
|
|
void setOnNewAddress(std::function<void(bool shielded)> callback) {
|
|
m_onNewAddress = callback;
|
|
}
|
|
|
|
void setOnShowQR(std::function<void(const std::string&)> callback) {
|
|
m_onShowQR = callback;
|
|
}
|
|
|
|
void setOnEditLabel(std::function<void(const std::string&, const std::string&)> callback) {
|
|
m_onEditLabel = callback;
|
|
}
|
|
|
|
void render();
|
|
|
|
private:
|
|
void renderAddressCard(const WalletAddress& addr, int index);
|
|
void renderNewAddressButton();
|
|
void renderQRCodePopup();
|
|
|
|
std::vector<WalletAddress> m_addresses;
|
|
std::function<void(const std::string&)> m_onCopyAddress;
|
|
std::function<void(bool shielded)> m_onNewAddress;
|
|
std::function<void(const std::string&)> m_onShowQR;
|
|
std::function<void(const std::string&, const std::string&)> m_onEditLabel;
|
|
|
|
std::string m_selectedQRAddress;
|
|
bool m_showQRPopup = false;
|
|
int m_selectedTab = 0; // 0 = shielded, 1 = transparent
|
|
};
|
|
|
|
// ============================================================================
|
|
// Implementation
|
|
// ============================================================================
|
|
|
|
inline void ReceiveScreen::render() {
|
|
// Title
|
|
Typography::instance().text(TypeStyle::H5, "Receive DRGX");
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
|
|
// Tab selection
|
|
TabSpec tabSpec;
|
|
tabSpec.variant = TabVariant::Fixed;
|
|
|
|
if (BeginTabs("receive_tabs", tabSpec)) {
|
|
if (Tab(ICON_MD_SHIELD " Shielded", m_selectedTab == 0)) {
|
|
m_selectedTab = 0;
|
|
}
|
|
if (Tab(ICON_MD_DESCRIPTION " Transparent", m_selectedTab == 1)) {
|
|
m_selectedTab = 1;
|
|
}
|
|
EndTabs();
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
|
|
// Filter addresses by type
|
|
std::string filterType = m_selectedTab == 0 ? "shielded" : "transparent";
|
|
|
|
// Display filtered addresses
|
|
int visibleCount = 0;
|
|
for (size_t i = 0; i < m_addresses.size(); i++) {
|
|
if (m_addresses[i].type == filterType) {
|
|
renderAddressCard(m_addresses[i], static_cast<int>(i));
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
visibleCount++;
|
|
}
|
|
}
|
|
|
|
// Empty state
|
|
if (visibleCount == 0) {
|
|
CardSpec emptySpec;
|
|
emptySpec.elevation = 1;
|
|
emptySpec.padding = spacing::dp(4);
|
|
|
|
if (BeginCard(emptySpec)) {
|
|
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
|
|
// Center text
|
|
const char* emptyText = m_selectedTab == 0 ?
|
|
"No shielded addresses yet" : "No transparent addresses yet";
|
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(emptyText);
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - textSize.x) * 0.5f);
|
|
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), emptyText);
|
|
|
|
EndCard();
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
}
|
|
|
|
// New address button
|
|
renderNewAddressButton();
|
|
|
|
// QR popup
|
|
if (m_showQRPopup) {
|
|
renderQRCodePopup();
|
|
}
|
|
}
|
|
|
|
inline void ReceiveScreen::renderAddressCard(const WalletAddress& addr, int index) {
|
|
CardSpec cardSpec;
|
|
cardSpec.elevation = addr.isDefault ? 3 : 1;
|
|
cardSpec.padding = spacing::dp(2);
|
|
|
|
char cardId[32];
|
|
snprintf(cardId, sizeof(cardId), "addr_card_%d", index);
|
|
ImGui::PushID(cardId);
|
|
|
|
if (BeginCard(cardSpec)) {
|
|
// Header row: Label + Default badge
|
|
ImGui::BeginGroup();
|
|
{
|
|
if (!addr.label.empty()) {
|
|
Typography::instance().text(TypeStyle::Subtitle1, addr.label.c_str());
|
|
} else {
|
|
Typography::instance().textColored(TypeStyle::Subtitle1, OnSurfaceMedium(), "Unnamed Address");
|
|
}
|
|
|
|
if (addr.isDefault) {
|
|
ImGui::SameLine();
|
|
|
|
ChipSpec chipSpec;
|
|
chipSpec.variant = ChipVariant::Filled;
|
|
chipSpec.color = Primary();
|
|
|
|
Chip("DEFAULT", chipSpec);
|
|
}
|
|
}
|
|
ImGui::EndGroup();
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
|
|
// Address (truncated with copy)
|
|
ImGui::BeginGroup();
|
|
{
|
|
// Show truncated address
|
|
std::string displayAddr = addr.address;
|
|
if (displayAddr.length() > 40) {
|
|
displayAddr = displayAddr.substr(0, 20) + "..." +
|
|
displayAddr.substr(displayAddr.length() - 16);
|
|
}
|
|
|
|
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(),
|
|
displayAddr.c_str());
|
|
}
|
|
ImGui::EndGroup();
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
|
|
// Balance
|
|
if (addr.balance > 0) {
|
|
char balanceStr[64];
|
|
snprintf(balanceStr, sizeof(balanceStr), "Balance: %.8f DRGX", addr.balance);
|
|
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), balanceStr);
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
}
|
|
|
|
// Action buttons
|
|
ImGui::BeginGroup();
|
|
{
|
|
if (OutlinedButton(ICON_MD_CONTENT_COPY " COPY")) {
|
|
if (m_onCopyAddress) m_onCopyAddress(addr.address);
|
|
}
|
|
|
|
ImGui::SameLine(0, spacing::dp(1));
|
|
|
|
if (OutlinedButton(ICON_MD_QR_CODE " QR")) {
|
|
m_selectedQRAddress = addr.address;
|
|
m_showQRPopup = true;
|
|
}
|
|
|
|
ImGui::SameLine(0, spacing::dp(1));
|
|
|
|
if (TextButton(ICON_MD_EDIT " EDIT")) {
|
|
// Would trigger edit label dialog
|
|
if (m_onEditLabel) m_onEditLabel(addr.address, addr.label);
|
|
}
|
|
}
|
|
ImGui::EndGroup();
|
|
|
|
EndCard();
|
|
}
|
|
|
|
ImGui::PopID();
|
|
}
|
|
|
|
inline void ReceiveScreen::renderNewAddressButton() {
|
|
// Floating action button style
|
|
float buttonWidth = 200.0f;
|
|
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - buttonWidth) * 0.5f);
|
|
|
|
if (ContainedButton("+ NEW ADDRESS")) {
|
|
if (m_onNewAddress) {
|
|
m_onNewAddress(m_selectedTab == 0); // true for shielded
|
|
}
|
|
}
|
|
}
|
|
|
|
inline void ReceiveScreen::renderQRCodePopup() {
|
|
DialogSpec dialogSpec;
|
|
dialogSpec.title = "QR Code";
|
|
dialogSpec.maxWidth = 400.0f;
|
|
|
|
DialogResult result = BeginDialog("qr_popup", dialogSpec);
|
|
|
|
if (result.isOpen) {
|
|
// QR code placeholder (actual QR generation would be separate)
|
|
float qrSize = 250.0f;
|
|
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - qrSize) * 0.5f);
|
|
|
|
// QR placeholder box
|
|
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
pos,
|
|
ImVec2(pos.x + qrSize, pos.y + qrSize),
|
|
ImGui::GetColorU32(Surface())
|
|
);
|
|
ImGui::GetWindowDrawList()->AddRect(
|
|
pos,
|
|
ImVec2(pos.x + qrSize, pos.y + qrSize),
|
|
ImGui::GetColorU32(Divider()),
|
|
4.0f
|
|
);
|
|
|
|
// Center text in QR box
|
|
const char* qrText = "QR CODE";
|
|
ImVec2 textSize = ImGui::CalcTextSize(qrText);
|
|
ImGui::SetCursorScreenPos(ImVec2(
|
|
pos.x + (qrSize - textSize.x) * 0.5f,
|
|
pos.y + (qrSize - textSize.y) * 0.5f
|
|
));
|
|
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), qrText);
|
|
|
|
ImGui::Dummy(ImVec2(qrSize, qrSize + spacing::dp(2)));
|
|
|
|
// Address below QR
|
|
ImGui::TextWrapped("%s", m_selectedQRAddress.c_str());
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
|
|
// Actions
|
|
float btnWidth = 100.0f;
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - btnWidth * 2 - spacing::dp(2)) * 0.5f);
|
|
|
|
if (OutlinedButton("COPY")) {
|
|
if (m_onCopyAddress) m_onCopyAddress(m_selectedQRAddress);
|
|
}
|
|
|
|
ImGui::SameLine(0, spacing::dp(2));
|
|
|
|
if (ContainedButton("CLOSE")) {
|
|
m_showQRPopup = false;
|
|
}
|
|
|
|
EndDialog();
|
|
}
|
|
|
|
if (result.dismissed) {
|
|
m_showQRPopup = false;
|
|
}
|
|
}
|
|
|
|
} // namespace screens
|
|
} // namespace ui
|
|
} // namespace dragonx
|