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)
935 lines
38 KiB
Plaintext
935 lines
38 KiB
Plaintext
// 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
|