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)
463 lines
14 KiB
C++
463 lines
14 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;
|
|
|
|
// ============================================================================
|
|
// Peers Screen
|
|
// ============================================================================
|
|
// Display connected peers with Material list
|
|
|
|
/**
|
|
* @brief Peer connection info
|
|
*/
|
|
struct PeerInfo {
|
|
int id;
|
|
std::string address;
|
|
std::string subversion;
|
|
int startingHeight;
|
|
int currentHeight;
|
|
int latency; // ms
|
|
int64_t bytesSent;
|
|
int64_t bytesReceived;
|
|
std::string connTime; // Connection duration
|
|
bool isInbound;
|
|
};
|
|
|
|
/**
|
|
* @brief Network statistics
|
|
*/
|
|
struct NetworkStats {
|
|
int totalConnections;
|
|
int inboundCount;
|
|
int outboundCount;
|
|
int64_t totalBytesSent;
|
|
int64_t totalBytesReceived;
|
|
int currentHeight;
|
|
std::string networkName;
|
|
};
|
|
|
|
/**
|
|
* @brief Peers screen with network info
|
|
*/
|
|
class PeersScreen {
|
|
public:
|
|
void setPeers(const std::vector<PeerInfo>& peers) { m_peers = peers; }
|
|
void setNetworkStats(const NetworkStats& stats) { m_networkStats = stats; }
|
|
|
|
void setOnDisconnectPeer(std::function<void(int peerId)> callback) {
|
|
m_onDisconnectPeer = callback;
|
|
}
|
|
void setOnAddNode(std::function<void(const std::string&)> callback) {
|
|
m_onAddNode = callback;
|
|
}
|
|
|
|
void render();
|
|
|
|
private:
|
|
void renderNetworkStats();
|
|
void renderPeersList();
|
|
void renderPeerDetails();
|
|
void renderAddNodeDialog();
|
|
|
|
std::vector<PeerInfo> m_peers;
|
|
NetworkStats m_networkStats;
|
|
|
|
std::function<void(int)> m_onDisconnectPeer;
|
|
std::function<void(const std::string&)> m_onAddNode;
|
|
|
|
int m_selectedPeerId = -1;
|
|
bool m_showDetails = false;
|
|
bool m_showAddNode = false;
|
|
char m_addNodeBuffer[256] = {0};
|
|
};
|
|
|
|
// ============================================================================
|
|
// Implementation
|
|
// ============================================================================
|
|
|
|
inline void PeersScreen::render() {
|
|
// Title
|
|
ImGui::BeginGroup();
|
|
{
|
|
Typography::instance().text(TypeStyle::H5, "Network Peers");
|
|
|
|
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 120);
|
|
|
|
if (OutlinedButton("+ ADD NODE")) {
|
|
m_showAddNode = true;
|
|
}
|
|
}
|
|
ImGui::EndGroup();
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
|
|
// Network stats cards
|
|
renderNetworkStats();
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
|
|
// Peers list
|
|
renderPeersList();
|
|
|
|
// Dialogs
|
|
if (m_showDetails) {
|
|
renderPeerDetails();
|
|
}
|
|
|
|
if (m_showAddNode) {
|
|
renderAddNodeDialog();
|
|
}
|
|
}
|
|
|
|
inline void PeersScreen::renderNetworkStats() {
|
|
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
float cardWidth = (availWidth - spacing::dp(2) * 3) / 4.0f;
|
|
|
|
// Connections card
|
|
ImGui::BeginGroup();
|
|
{
|
|
CardSpec cardSpec;
|
|
cardSpec.elevation = 1;
|
|
cardSpec.padding = spacing::dp(2);
|
|
cardSpec.width = cardWidth;
|
|
|
|
ImGui::PushID("conn_card");
|
|
if (BeginCard(cardSpec)) {
|
|
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "CONNECTIONS");
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
|
|
char connStr[32];
|
|
snprintf(connStr, sizeof(connStr), "%d", m_networkStats.totalConnections);
|
|
Typography::instance().text(TypeStyle::H4, connStr);
|
|
|
|
char detailStr[64];
|
|
snprintf(detailStr, sizeof(detailStr), ICON_MD_ARROW_DOWNWARD "%d in " ICON_MD_ARROW_UPWARD "%d out",
|
|
m_networkStats.inboundCount, m_networkStats.outboundCount);
|
|
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), detailStr);
|
|
|
|
EndCard();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndGroup();
|
|
|
|
ImGui::SameLine(0, spacing::dp(2));
|
|
|
|
// Block height card
|
|
ImGui::BeginGroup();
|
|
{
|
|
CardSpec cardSpec;
|
|
cardSpec.elevation = 1;
|
|
cardSpec.padding = spacing::dp(2);
|
|
cardSpec.width = cardWidth;
|
|
|
|
ImGui::PushID("height_card");
|
|
if (BeginCard(cardSpec)) {
|
|
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCK HEIGHT");
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
|
|
char heightStr[32];
|
|
snprintf(heightStr, sizeof(heightStr), "%d", m_networkStats.currentHeight);
|
|
Typography::instance().text(TypeStyle::H4, heightStr);
|
|
|
|
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(),
|
|
m_networkStats.networkName.c_str());
|
|
|
|
EndCard();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndGroup();
|
|
|
|
ImGui::SameLine(0, spacing::dp(2));
|
|
|
|
// Data sent card
|
|
ImGui::BeginGroup();
|
|
{
|
|
CardSpec cardSpec;
|
|
cardSpec.elevation = 1;
|
|
cardSpec.padding = spacing::dp(2);
|
|
cardSpec.width = cardWidth;
|
|
|
|
ImGui::PushID("sent_card");
|
|
if (BeginCard(cardSpec)) {
|
|
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "DATA SENT");
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
|
|
char dataStr[32];
|
|
double mb = m_networkStats.totalBytesSent / (1024.0 * 1024.0);
|
|
if (mb > 1024) {
|
|
snprintf(dataStr, sizeof(dataStr), "%.1f GB", mb / 1024.0);
|
|
} else {
|
|
snprintf(dataStr, sizeof(dataStr), "%.1f MB", mb);
|
|
}
|
|
Typography::instance().text(TypeStyle::H5, dataStr);
|
|
|
|
EndCard();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndGroup();
|
|
|
|
ImGui::SameLine(0, spacing::dp(2));
|
|
|
|
// Data received card
|
|
ImGui::BeginGroup();
|
|
{
|
|
CardSpec cardSpec;
|
|
cardSpec.elevation = 1;
|
|
cardSpec.padding = spacing::dp(2);
|
|
cardSpec.width = cardWidth;
|
|
|
|
ImGui::PushID("recv_card");
|
|
if (BeginCard(cardSpec)) {
|
|
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "DATA RECEIVED");
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
|
|
char dataStr[32];
|
|
double mb = m_networkStats.totalBytesReceived / (1024.0 * 1024.0);
|
|
if (mb > 1024) {
|
|
snprintf(dataStr, sizeof(dataStr), "%.1f GB", mb / 1024.0);
|
|
} else {
|
|
snprintf(dataStr, sizeof(dataStr), "%.1f MB", mb);
|
|
}
|
|
Typography::instance().text(TypeStyle::H5, dataStr);
|
|
|
|
EndCard();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndGroup();
|
|
}
|
|
|
|
inline void PeersScreen::renderPeersList() {
|
|
CardSpec cardSpec;
|
|
cardSpec.elevation = 1;
|
|
cardSpec.padding = 0;
|
|
|
|
if (BeginCard(cardSpec)) {
|
|
if (m_peers.empty()) {
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(4)));
|
|
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
|
|
const char* emptyText = "No peers connected";
|
|
ImVec2 textSize = ImGui::CalcTextSize(emptyText);
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - textSize.x) * 0.5f);
|
|
Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), emptyText);
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(4)));
|
|
} else {
|
|
BeginList("peers_list", false);
|
|
|
|
for (const auto& peer : m_peers) {
|
|
ListItemSpec itemSpec;
|
|
|
|
// Direction icon
|
|
itemSpec.leadingIcon = peer.isInbound ? ICON_MD_ARROW_DOWNWARD : ICON_MD_ARROW_UPWARD;
|
|
|
|
// Address as primary
|
|
itemSpec.primaryText = peer.address.c_str();
|
|
|
|
// Version + latency as secondary
|
|
char secondaryStr[128];
|
|
snprintf(secondaryStr, sizeof(secondaryStr), "%s • %dms • Height: %d",
|
|
peer.subversion.c_str(), peer.latency, peer.currentHeight);
|
|
itemSpec.secondaryText = secondaryStr;
|
|
|
|
itemSpec.trailingIcon = ICON_MD_INFO;
|
|
itemSpec.dividerBelow = true;
|
|
|
|
char peerId[16];
|
|
snprintf(peerId, sizeof(peerId), "peer_%d", peer.id);
|
|
ImGui::PushID(peerId);
|
|
|
|
if (ListItem(itemSpec)) {
|
|
m_selectedPeerId = peer.id;
|
|
m_showDetails = true;
|
|
}
|
|
|
|
ImGui::PopID();
|
|
}
|
|
|
|
EndList();
|
|
}
|
|
|
|
EndCard();
|
|
}
|
|
}
|
|
|
|
inline void PeersScreen::renderPeerDetails() {
|
|
// Find selected peer
|
|
const PeerInfo* selected = nullptr;
|
|
for (const auto& peer : m_peers) {
|
|
if (peer.id == m_selectedPeerId) {
|
|
selected = &peer;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!selected) {
|
|
m_showDetails = false;
|
|
return;
|
|
}
|
|
|
|
DialogSpec dialogSpec;
|
|
dialogSpec.title = "Peer Details";
|
|
dialogSpec.maxWidth = 450.0f;
|
|
|
|
DialogResult result = BeginDialog("peer_details", dialogSpec);
|
|
|
|
if (result.isOpen) {
|
|
const PeerInfo& peer = *selected;
|
|
|
|
// Direction badge
|
|
ChipSpec chipSpec;
|
|
chipSpec.variant = ChipVariant::Filled;
|
|
chipSpec.color = peer.isInbound ? colors::Blue500 : colors::Orange500;
|
|
Chip(peer.isInbound ? ICON_MD_ARROW_DOWNWARD " Inbound" : ICON_MD_ARROW_UPWARD " Outbound", chipSpec);
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
|
|
// Address
|
|
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "ADDRESS");
|
|
Typography::instance().text(TypeStyle::Body1, peer.address.c_str());
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
|
|
// Client
|
|
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "CLIENT");
|
|
Typography::instance().text(TypeStyle::Body2, peer.subversion.c_str());
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
|
|
// Height
|
|
char heightStr[64];
|
|
snprintf(heightStr, sizeof(heightStr), "%d (started at %d)",
|
|
peer.currentHeight, peer.startingHeight);
|
|
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCK HEIGHT");
|
|
Typography::instance().text(TypeStyle::Body2, heightStr);
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
|
|
// Latency
|
|
char latencyStr[32];
|
|
snprintf(latencyStr, sizeof(latencyStr), "%d ms", peer.latency);
|
|
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "LATENCY");
|
|
Typography::instance().text(TypeStyle::Body2, latencyStr);
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
|
|
// Connected time
|
|
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "CONNECTED");
|
|
Typography::instance().text(TypeStyle::Body2, peer.connTime.c_str());
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1)));
|
|
|
|
// Data transferred
|
|
char dataStr[64];
|
|
snprintf(dataStr, sizeof(dataStr), ICON_MD_ARROW_UPWARD " %.2f MB " ICON_MD_ARROW_DOWNWARD " %.2f MB",
|
|
peer.bytesSent / (1024.0 * 1024.0),
|
|
peer.bytesReceived / (1024.0 * 1024.0));
|
|
Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "DATA TRANSFERRED");
|
|
Typography::instance().text(TypeStyle::Body2, dataStr);
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
|
|
|
|
// Actions
|
|
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
|
|
// Disconnect button (danger)
|
|
ImGui::PushStyleColor(ImGuiCol_Button, colors::withAlpha(colors::Red500, 0.1f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::withAlpha(colors::Red500, 0.2f));
|
|
ImGui::PushStyleColor(ImGuiCol_Text, colors::Red500);
|
|
|
|
if (ImGui::Button("DISCONNECT", ImVec2(120, 36))) {
|
|
if (m_onDisconnectPeer) m_onDisconnectPeer(peer.id);
|
|
m_showDetails = false;
|
|
}
|
|
|
|
ImGui::PopStyleColor(3);
|
|
|
|
ImGui::SameLine(availWidth - 80);
|
|
|
|
if (ContainedButton("CLOSE")) {
|
|
m_showDetails = false;
|
|
}
|
|
|
|
EndDialog();
|
|
}
|
|
|
|
if (result.dismissed) {
|
|
m_showDetails = false;
|
|
}
|
|
}
|
|
|
|
inline void PeersScreen::renderAddNodeDialog() {
|
|
DialogSpec dialogSpec;
|
|
dialogSpec.title = "Add Node";
|
|
dialogSpec.maxWidth = 400.0f;
|
|
|
|
DialogResult result = BeginDialog("add_node", dialogSpec);
|
|
|
|
if (result.isOpen) {
|
|
Typography::instance().text(TypeStyle::Body1,
|
|
"Enter the IP address or hostname of a node to connect to:");
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(2)));
|
|
|
|
TextFieldSpec textSpec;
|
|
textSpec.label = "Node Address";
|
|
textSpec.placeholder = "192.168.1.1:18030 or node.example.com";
|
|
textSpec.variant = TextFieldVariant::Outlined;
|
|
textSpec.width = -1;
|
|
|
|
TextField("add_node_addr", m_addNodeBuffer, sizeof(m_addNodeBuffer), textSpec);
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(3)));
|
|
|
|
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availWidth - 200);
|
|
|
|
if (TextButton("CANCEL")) {
|
|
m_showAddNode = false;
|
|
memset(m_addNodeBuffer, 0, sizeof(m_addNodeBuffer));
|
|
}
|
|
|
|
ImGui::SameLine(0, spacing::dp(2));
|
|
|
|
bool canAdd = strlen(m_addNodeBuffer) > 0;
|
|
ImGui::BeginDisabled(!canAdd);
|
|
if (ContainedButton("ADD")) {
|
|
if (m_onAddNode) m_onAddNode(m_addNodeBuffer);
|
|
m_showAddNode = false;
|
|
memset(m_addNodeBuffer, 0, sizeof(m_addNodeBuffer));
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
EndDialog();
|
|
}
|
|
|
|
if (result.dismissed) {
|
|
m_showAddNode = false;
|
|
}
|
|
}
|
|
|
|
} // namespace screens
|
|
} // namespace ui
|
|
} // namespace dragonx
|