feat(wallet): persist history and surface pending sends

Add an encrypted SQLite transaction history cache with cached tip metadata and
per-address shielded scan progress so startup and full refreshes avoid
re-scanning every z-address while still invalidating on wallet/address/rescan
changes.

Improve wallet history loading by paging transparent transactions, preserving
cached shielded and sent rows, keeping recent/unconfirmed activity visible, and
classifying mining-address receives. Show z_sendmany opid sends immediately in
History and Overview, pin pending rows through refreshes, and apply optimistic
address/balance debits until opids resolve.

Add timestamped RPC console tracing by source/method without logging params or
results, reduce redundant refresh/RPC calls, and cache Explorer recent block
summaries in SQLite.

Expand focused tests for transaction cache encryption, scan-progress
persistence/invalidation, history preservation, operation-status parsing,
pending send visibility, and Explorer/RPC refresh behavior.
This commit is contained in:
dan_s
2026-05-05 03:22:14 -05:00
parent 973c390df5
commit 229373e937
43 changed files with 3732 additions and 702 deletions

View File

@@ -11,6 +11,7 @@
#include "../../app.h"
#include "../../util/i18n.h"
#include "../material/draw_helpers.h"
#include "../notifications.h"
#include "../theme.h"
#include "imgui.h"
@@ -171,11 +172,6 @@ public:
ImGui::PopStyleColor();
}
ImGui::Spacing();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Buttons
const char* cancelLabel = TR("cancel");
const char* confirmLabel = TR("confirm_transfer");
@@ -192,6 +188,20 @@ public:
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, sendingLabel).x);
float confirmW = std::max(confirmMinW, confirmTextW + buttonPadW);
float totalW = cancelW + confirmW + Layout::spacingMd();
float footerH = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y * 3.0f + 1.0f;
ImGuiViewport* vp = ImGui::GetMainViewport();
float cardBottomY = vp->Pos.y + vp->Size.y * 0.85f;
float footerTopY = cardBottomY - 24.0f * dp - footerH;
float currentY = ImGui::GetCursorScreenPos().y;
if (currentY < footerTopY) {
ImGui::Dummy(ImVec2(0, footerTopY - currentY));
} else {
ImGui::Spacing();
}
ImGui::Separator();
ImGui::Spacing();
float rowStartX = ImGui::GetCursorPosX();
float contentW = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f));
@@ -209,11 +219,14 @@ public:
[](bool ok, const std::string& result) {
s_sending = false;
s_success = ok;
if (ok)
s_resultMsg = result; // opid
else
s_resultMsg = result; // error message
s_resultMsg = result;
if (ok) {
Notifications::instance().success(TR("transfer_sent_desc"));
} else {
Notifications::instance().error(result.empty() ? TR("transfer_failed") : result);
}
});
s_open = false;
}
ImGui::EndDisabled();

View File

@@ -120,6 +120,7 @@ void BackupWalletDialog::render(App* app)
bool success = false;
std::string statusMsg;
try {
rpc::RPCClient::TraceScope trace("Settings / Backup wallet");
rpc->call("backupwallet", json::array({dest}));
// Check if file was created
if (fs::exists(dest)) {

View File

@@ -42,7 +42,7 @@ std::vector<AddressListRow> BuildAddressListRows(const std::vector<AddressListIn
if (!AddressListMatchesFilter(input, filter)) continue;
if (input.hidden && !showHidden) continue;
if (hideZeroBalances && input.info->balance < 1e-9 && !input.hidden && !input.favorite) continue;
rows.push_back({input.info, input.isZ, input.hidden, input.favorite,
rows.push_back({input.info, input.isZ, input.hidden, input.favorite, input.mining,
input.label, input.icon, input.sortOrder});
}

View File

@@ -13,6 +13,7 @@ struct AddressListInput {
bool isZ = false;
bool hidden = false;
bool favorite = false;
bool mining = false;
std::string label;
std::string icon;
int sortOrder = -1;
@@ -23,6 +24,7 @@ struct AddressListRow {
bool isZ = false;
bool hidden = false;
bool favorite = false;
bool mining = false;
std::string label;
std::string icon;
int sortOrder = -1;

View File

@@ -652,6 +652,7 @@ static void RenderBalanceClassic(App* app)
bool isZ;
bool hidden;
bool favorite;
bool mining;
};
std::vector<AddrRow> rows;
rows.reserve(state.z_addresses.size() + state.t_addresses.size());
@@ -665,7 +666,7 @@ static void RenderBalanceClassic(App* app)
bool isFav = app->isAddressFavorite(a.address);
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav)
continue;
rows.push_back({&a, true, isHidden, isFav});
rows.push_back({&a, true, isHidden, isFav, app->isMiningAddress(a.address)});
}
for (const auto& a : state.t_addresses) {
std::string filter(addr_search);
@@ -677,7 +678,7 @@ static void RenderBalanceClassic(App* app)
bool isFav = app->isAddressFavorite(a.address);
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav)
continue;
rows.push_back({&a, false, isHidden, isFav});
rows.push_back({&a, false, isHidden, isFav, app->isMiningAddress(a.address)});
}
// Sort: favorites first, then Z addresses, then by balance descending
@@ -951,8 +952,9 @@ static void RenderBalanceClassic(App* app)
const char* typeLabel = row.isZ ? "Shielded" : "Transparent";
const char* hiddenTag = row.hidden ? " (hidden)" : "";
const char* viewOnlyTag = (!addr.has_spending_key) ? " (view-only)" : "";
const char* miningTag = row.mining ? TR("mining_tag") : "";
char typeBuf[64];
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, viewOnlyTag);
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s%s", typeLabel, hiddenTag, viewOnlyTag, miningTag);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
// Label (if present, next to type)
@@ -1033,6 +1035,9 @@ static void RenderBalanceClassic(App* app)
app->setCurrentPage(NavPage::Send);
}
ImGui::Separator();
if (ImGui::MenuItem(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
app->setMiningAddress(addr.address, !row.mining);
}
if (ImGui::MenuItem(TR("export_private_key"))) {
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
}
@@ -1111,18 +1116,19 @@ static void RenderBalanceClassic(App* app)
}
ImGui::Spacing();
const auto& txs = state.transactions;
int maxTx = kRecentTxCount;
int count = (int)txs.size();
if (count > maxTx) count = maxTx;
ImFont* capFont = Type().caption();
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
ImGui::BeginChild("##BalanceClassicRecentRows", ImVec2(0, listH), false,
ImGuiWindowFlags_NoBackground);
const auto& txs = state.transactions;
int count = (int)txs.size();
if (count == 0) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
"No transactions yet");
} else {
ImDrawList* dl = ImGui::GetWindowDrawList();
ImFont* capFont = Type().caption();
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size, S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
for (int i = 0; i < count; i++) {
@@ -1176,6 +1182,7 @@ static void RenderBalanceClassic(App* app)
ImGui::Dummy(ImVec2(0, rowH));
}
}
ImGui::EndChild();
}
}
@@ -1297,7 +1304,8 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
std::string addrLabel = app->getAddressLabel(a.address);
bool isHidden = app->isAddressHidden(a.address);
bool isFav = app->isAddressFavorite(a.address);
rowInputs.push_back({&a, isZ, isHidden, isFav,
bool isMining = app->isMiningAddress(a.address);
rowInputs.push_back({&a, isZ, isHidden, isFav, isMining,
addrLabel, app->getAddressIcon(a.address),
app->getAddressSortOrder(a.address)});
}
@@ -1646,8 +1654,9 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
{
const char* typeLabel = row.isZ ? TR("shielded") : TR("transparent");
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
const char* miningTag = row.mining ? TR("mining_tag") : "";
char typeBuf[64];
snprintf(typeBuf, sizeof(typeBuf), "%s%s", typeLabel, hiddenTag);
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
// User label next to type
@@ -1761,6 +1770,9 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
if (ImGui::MenuItem(TR("set_label"))) {
AddressLabelDialog::show(app, addr.address, row.isZ);
}
if (ImGui::MenuItem(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
app->setMiningAddress(addr.address, !row.mining);
}
if (ImGui::MenuItem(TR("export_private_key")))
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
if (row.isZ) {
@@ -1869,10 +1881,13 @@ static void RenderSharedRecentTx(App* app, float recentH, float availW, float hs
ImGui::Spacing();
float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
int maxTx = std::clamp((int)(recentH / scaledRowH), 2, 5);
float availableListH = ImGui::GetContentRegionAvail().y;
float listH = std::max({scaledRowH, recentH, availableListH});
ImGui::BeginChild("##BalanceSharedRecentRows", ImVec2(0, listH), false,
ImGuiWindowFlags_NoBackground);
const auto& txs = state.transactions;
int count = (int)txs.size();
if (count > maxTx) count = maxTx;
if (count == 0) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No transactions yet");
@@ -1923,6 +1938,7 @@ static void RenderSharedRecentTx(App* app, float recentH, float availW, float hs
ImGui::Dummy(ImVec2(0, rowH));
}
}
ImGui::EndChild();
}
// Render sync progress bar (used by multiple layouts)

View File

@@ -20,6 +20,7 @@ bool consoleLinePassesFilter(const std::string& lineText,
const ConsoleOutputFilter& filter)
{
if (!filter.daemonMessagesEnabled && lineColor == filter.daemonColor) return false;
if (!filter.rpcTraceEnabled && lineColor == filter.rpcTraceColor) return false;
if (filter.errorsOnly && lineColor != filter.errorColor) return false;
if (!filter.text.empty()) {
std::string needle = lowerCopy(filter.text);

View File

@@ -11,8 +11,10 @@ struct ConsoleOutputFilter {
std::string text;
bool daemonMessagesEnabled = true;
bool errorsOnly = false;
bool rpcTraceEnabled = false;
ImU32 daemonColor = 0;
ImU32 errorColor = 0;
ImU32 rpcTraceColor = 0;
};
bool consoleLinePassesFilter(const std::string& lineText,

View File

@@ -27,6 +27,8 @@
#include <sstream>
#include <algorithm>
#include <cmath>
#include <mutex>
#include <ctime>
#include <unordered_set>
namespace dragonx {
@@ -38,10 +40,36 @@ ImU32 ConsoleTab::COLOR_RESULT = IM_COL32(200, 200, 200, 255);
ImU32 ConsoleTab::COLOR_ERROR = IM_COL32(246, 71, 64, 255);
ImU32 ConsoleTab::COLOR_DAEMON = IM_COL32(160, 160, 160, 180);
ImU32 ConsoleTab::COLOR_INFO = IM_COL32(191, 209, 229, 255);
ImU32 ConsoleTab::COLOR_RPC = IM_COL32(120, 180, 255, 210);
bool ConsoleTab::s_scanline_enabled = true;
float ConsoleTab::s_console_zoom = 1.0f;
bool ConsoleTab::s_daemon_messages_enabled = true;
bool ConsoleTab::s_errors_only_enabled = false;
bool ConsoleTab::s_rpc_trace_enabled = false;
namespace {
std::mutex s_rpc_trace_console_mutex;
ConsoleTab* s_rpc_trace_console = nullptr;
std::string rpcTraceTimestamp()
{
std::time_t now = std::time(nullptr);
std::tm localTime{};
static std::mutex timeMutex;
{
std::lock_guard<std::mutex> lock(timeMutex);
if (const std::tm* current = std::localtime(&now)) {
localTime = *current;
}
}
char buffer[16];
std::strftime(buffer, sizeof(buffer), "%H:%M:%S", &localTime);
return buffer;
}
} // namespace
void ConsoleTab::refreshColors()
{
@@ -55,18 +83,21 @@ void ConsoleTab::refreshColors()
auto err = S.drawElement("console", "color-error");
auto dmn = S.drawElement("console", "color-daemon");
auto inf = S.drawElement("console", "color-info");
auto rpc = S.drawElement("console", "color-rpc");
ImU32 defCmd = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
ImU32 defRes = dark ? IM_COL32(200, 200, 200, 255) : IM_COL32(50, 50, 50, 255);
ImU32 defErr = dark ? IM_COL32(246, 71, 64, 255) : IM_COL32(198, 40, 40, 255);
ImU32 defDmn = dark ? IM_COL32(160, 160, 160, 180) : IM_COL32(90, 90, 90, 200);
ImU32 defInf = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
ImU32 defRpc = dark ? IM_COL32(120, 180, 255, 210) : IM_COL32(25, 118, 210, 220);
COLOR_COMMAND = !cmd.color.empty() ? S.resolveColor(cmd.color, defCmd) : defCmd;
COLOR_RESULT = !res.color.empty() ? S.resolveColor(res.color, defRes) : defRes;
COLOR_ERROR = !err.color.empty() ? S.resolveColor(err.color, defErr) : defErr;
COLOR_DAEMON = !dmn.color.empty() ? S.resolveColor(dmn.color, defDmn) : defDmn;
COLOR_INFO = !inf.color.empty() ? S.resolveColor(inf.color, defInf) : defInf;
COLOR_RPC = !rpc.color.empty() ? S.resolveColor(rpc.color, defRpc) : defRpc;
} else {
// No schema — use hardcoded defaults per theme
COLOR_COMMAND = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
@@ -74,11 +105,26 @@ void ConsoleTab::refreshColors()
COLOR_ERROR = dark ? IM_COL32(246, 71, 64, 255) : IM_COL32(198, 40, 40, 255);
COLOR_DAEMON = dark ? IM_COL32(160, 160, 160, 180) : IM_COL32(90, 90, 90, 200);
COLOR_INFO = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
COLOR_RPC = dark ? IM_COL32(120, 180, 255, 210) : IM_COL32(25, 118, 210, 220);
}
}
ConsoleTab::ConsoleTab()
{
{
std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
s_rpc_trace_console = this;
}
rpc::RPCClient::setTraceCallback([](const std::string& source, const std::string& method) {
ConsoleTab* console = nullptr;
{
std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
console = s_rpc_trace_console;
}
if (console) console->addRpcTraceLine(source, method);
});
rpc::RPCClient::setTraceEnabled(s_rpc_trace_enabled);
// Load console colors from ui.toml schema (uses current theme)
refreshColors();
@@ -88,6 +134,14 @@ ConsoleTab::ConsoleTab()
addLine("", COLOR_RESULT);
}
ConsoleTab::~ConsoleTab()
{
rpc::RPCClient::setTraceEnabled(false);
rpc::RPCClient::setTraceCallback(nullptr);
std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
if (s_rpc_trace_console == this) s_rpc_trace_console = nullptr;
}
void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc::RPCWorker* worker, daemon::XmrigManager* xmrig)
{
using namespace material;
@@ -100,7 +154,7 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc
// Save old colors to remap existing lines
ImU32 oldCmd = COLOR_COMMAND, oldRes = COLOR_RESULT;
ImU32 oldErr = COLOR_ERROR, oldDmn = COLOR_DAEMON;
ImU32 oldInf = COLOR_INFO;
ImU32 oldInf = COLOR_INFO, oldRpc = COLOR_RPC;
refreshColors();
// Remap stored line colors from old to new
{
@@ -111,6 +165,7 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc
else if (line.color == oldErr) line.color = COLOR_ERROR;
else if (line.color == oldDmn) line.color = COLOR_DAEMON;
else if (line.color == oldInf) line.color = COLOR_INFO;
else if (line.color == oldRpc) line.color = COLOR_RPC;
}
}
s_lastDark = nowDark;
@@ -282,81 +337,46 @@ void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc
// CRT scanline effect over output area — aligned to text lines
if (s_scanline_enabled) {
float panelH = outPanelMax.y - outPanelMin.y;
// --- Text-aligned horizontal scanlines ---
// Stride matches the actual text line height so each band sits between lines.
float textLineH = output_line_height_;
if (textLineH <= 1.0f) textLineH = Type().caption()->LegacySize * s_console_zoom + 2.0f; // fallback
float bandH = schema::UI().drawElement("tabs.console", "scanline-gap").sizeOr(2.0f);
int lineAlpha = (int)schema::UI().drawElement("tabs.console", "scanline-line-alpha").sizeOr(18.0f);
if (textLineH <= 1.0f) textLineH = Type().caption()->LegacySize * s_console_zoom + 2.0f;
// Glow fringe parameters (soft gradient above/below each band)
float glowSpread = schema::UI().drawElement("tabs.console", "scanline-glow-spread").sizeOr(0.0f);
float glowIntensity = schema::UI().drawElement("tabs.console", "scanline-glow-intensity").sizeOr(0.6f);
int glowRGB = (int)schema::UI().drawElement("tabs.console", "scanline-glow-color").sizeOr(255.0f);
bool drawGlow = glowSpread > 0.0f && glowIntensity > 0.0f && lineAlpha > 0;
int glowAlpha = drawGlow ? std::min(255, (int)(lineAlpha * glowIntensity)) : 0;
ImU32 glowPeak = IM_COL32(glowRGB, glowRGB, glowRGB, glowAlpha);
ImU32 glowClear = IM_COL32(glowRGB, glowRGB, glowRGB, 0);
if (textLineH >= 1.0f && lineAlpha > 0) {
ImU32 lineCol = IM_COL32(255, 255, 255, lineAlpha);
float stride = textLineH; // one text line per scanline period
// Align with text: account for inner padding and scroll position
float padY = Layout::spacingSm();
float scrollFrac = std::fmod(consoleScrollY, stride);
float startY = outPanelMin.y + padY - scrollFrac;
// Ensure first band starts above the visible area
while (startY > outPanelMin.y) startY -= stride;
for (float y = startY; y < outPanelMax.y; y += stride) {
// Place the dark band at the bottom edge of each text line period
float bandTop = y + stride - bandH;
float bandBot = y + stride;
float yTop = std::max(bandTop, outPanelMin.y);
float yBot = std::min(bandBot, outPanelMax.y);
int lightAlpha = std::clamp((int)schema::UI().drawElement("tabs.console", "scanline-line-alpha").sizeOr(10.0f), 0, 255);
int darkAlpha = std::clamp(lightAlpha + 10, 0, 255);
if (textLineH >= 1.0f && (lightAlpha > 0 || darkAlpha > 0)) {
ImU32 lightCol = IM_COL32(255, 255, 255, lightAlpha);
ImU32 darkCol = IM_COL32(0, 0, 0, darkAlpha);
for (const auto& row : scanline_rows_) {
float yTop = std::max(row.yTop, outPanelMin.y);
float yBot = std::min(row.yBot, outPanelMax.y);
if (yTop < yBot) {
// Glow fringes (gradient tapers away from band)
if (drawGlow) {
// Above fringe: transparent at top, glowPeak at bottom
float gTop = std::max(yTop - glowSpread, outPanelMin.y);
if (gTop < yTop) {
dlOut->AddRectFilledMultiColor(
ImVec2(outPanelMin.x, gTop), ImVec2(outPanelMax.x, yTop),
glowClear, glowClear, glowPeak, glowPeak);
}
// Below fringe: glowPeak at top, transparent at bottom
float gBot = std::min(yBot + glowSpread, outPanelMax.y);
if (yBot < gBot) {
dlOut->AddRectFilledMultiColor(
ImVec2(outPanelMin.x, yBot), ImVec2(outPanelMax.x, gBot),
glowPeak, glowPeak, glowClear, glowClear);
}
}
// Opaque scanline band (drawn on top of glow)
dlOut->AddRectFilled(ImVec2(outPanelMin.x, yTop), ImVec2(outPanelMax.x, yBot), lineCol);
dlOut->AddRectFilled(ImVec2(outPanelMin.x, yTop), ImVec2(outPanelMax.x, yBot),
(row.rowIndex % 2 == 0) ? lightCol : darkCol);
}
}
}
// --- Animated sweep band (brighter moving highlight) ---
float panelH = outPanelMax.y - outPanelMin.y;
float scanSpeed = schema::UI().drawElement("tabs.console", "scanline-speed").sizeOr(40.0f);
float scanH = schema::UI().drawElement("tabs.console", "scanline-height").sizeOr(30.0f);
int scanAlpha = (int)schema::UI().drawElement("tabs.console", "scanline-alpha").sizeOr(12.0f);
float t = (float)std::fmod(ImGui::GetTime() * scanSpeed, (double)(panelH + scanH));
float scanY = outPanelMin.y + t - scanH;
float yTop = std::max(scanY, outPanelMin.y);
float yBot = std::min(scanY + scanH, outPanelMax.y);
if (yTop < yBot) {
float mid = (yTop + yBot) * 0.5f;
ImU32 clear = IM_COL32(255, 255, 255, 0);
ImU32 peak = IM_COL32(255, 255, 255, scanAlpha);
dlOut->AddRectFilledMultiColor(
ImVec2(outPanelMin.x, yTop), ImVec2(outPanelMax.x, mid),
clear, clear, peak, peak);
dlOut->AddRectFilledMultiColor(
ImVec2(outPanelMin.x, mid), ImVec2(outPanelMax.x, yBot),
peak, peak, clear, clear);
float rawScanH = schema::UI().drawElement("tabs.console", "scanline-height").sizeOr(textLineH * 2.0f);
int scanAlpha = std::clamp((int)schema::UI().drawElement("tabs.console", "scanline-alpha").sizeOr(8.0f), 0, 255);
if (panelH > 1.0f && textLineH >= 1.0f && scanSpeed > 0.0f && scanAlpha > 0) {
float scanLines = std::max(1.0f, std::round(rawScanH / textLineH));
float scanH = scanLines * textLineH;
float t = (float)std::fmod(ImGui::GetTime() * scanSpeed, (double)(panelH + scanH));
float scanY = outPanelMin.y + t - scanH;
float yTop = std::max(scanY, outPanelMin.y);
float yBot = std::min(scanY + scanH, outPanelMax.y);
if (yTop < yBot) {
float mid = (yTop + yBot) * 0.5f;
ImU32 clear = IM_COL32(255, 255, 255, 0);
ImU32 peak = IM_COL32(255, 255, 255, scanAlpha);
dlOut->AddRectFilledMultiColor(
ImVec2(outPanelMin.x, yTop), ImVec2(outPanelMax.x, mid),
clear, clear, peak, peak);
dlOut->AddRectFilledMultiColor(
ImVec2(outPanelMin.x, mid), ImVec2(outPanelMax.x, yBot),
peak, peak, clear, clear);
}
}
}
@@ -492,6 +512,25 @@ void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", TR("console_show_errors_only"));
}
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
// App RPC trace toggle — captures method/source only, never results or params
{
static bool s_prev_rpc_trace_enabled = false;
if (ImGui::Checkbox(TR("console_rpc_trace"), &s_rpc_trace_enabled)) {
rpc::RPCClient::setTraceEnabled(s_rpc_trace_enabled);
}
if (s_prev_rpc_trace_enabled != s_rpc_trace_enabled && auto_scroll_) {
scroll_to_bottom_ = true;
}
s_prev_rpc_trace_enabled = s_rpc_trace_enabled;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", TR("console_show_rpc_trace"));
}
ImGui::SameLine();
ImGui::Spacing();
@@ -607,12 +646,15 @@ void ConsoleTab::renderOutput()
output_line_height_ = line_height; // store for scanline alignment
output_origin_ = ImGui::GetCursorScreenPos();
output_scroll_y_ = ImGui::GetScrollY();
scanline_rows_.clear();
// Build filtered line index list BEFORE mouse handling (so screenToTextPos works)
ConsoleOutputFilter outputFilter{filter_text_, s_daemon_messages_enabled,
s_errors_only_enabled, COLOR_DAEMON, COLOR_ERROR};
s_errors_only_enabled, s_rpc_trace_enabled,
COLOR_DAEMON, COLOR_ERROR, COLOR_RPC};
bool has_text_filter = !outputFilter.text.empty();
bool has_filter = has_text_filter || !outputFilter.daemonMessagesEnabled || outputFilter.errorsOnly;
bool has_filter = has_text_filter || !outputFilter.daemonMessagesEnabled ||
!outputFilter.rpcTraceEnabled || outputFilter.errorsOnly;
visible_indices_.clear();
if (has_filter) {
for (int i = 0; i < static_cast<int>(lines_.size()); i++) {
@@ -826,6 +868,11 @@ void ConsoleTab::renderOutput()
float rowY = lineOrigin.y + seg.yOffset;
const char* segStart = line.text.c_str() + seg.byteStart;
const char* segEnd = line.text.c_str() + seg.byteEnd;
if (s_scanline_enabled && line_height > 0.0f) {
int rowIndex = static_cast<int>(std::floor((cumulative_y_offsets_[vi] + seg.yOffset) / line_height + 0.5f));
scanline_rows_.push_back({rowY, rowY + seg.height, rowIndex});
}
// Selection highlight for this sub-row
if (lineSelected && selByteStart < seg.byteEnd && selByteEnd > seg.byteStart) {
@@ -1469,6 +1516,7 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
std::string result_str;
bool is_error = false;
try {
rpc::RPCClient::TraceScope trace("Console tab / User command");
result_str = rpc->callRaw(method, params);
} catch (const std::exception& e) {
result_str = e.what();
@@ -1499,6 +1547,7 @@ void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc
} else {
// Fallback: synchronous execution if no worker available
try {
rpc::RPCClient::TraceScope trace("Console tab / User command");
std::string result_str = rpc->callRaw(method, params);
for (const auto& resultLine : FormatConsoleRpcResultLines(result_str, false)) {
addLine(resultLine.text, COLOR_RESULT);
@@ -1545,6 +1594,11 @@ void ConsoleTab::addLine(const std::string& line, ImU32 color)
scroll_to_bottom_ = auto_scroll_;
}
void ConsoleTab::addRpcTraceLine(const std::string& source, const std::string& method)
{
addLine("[rpc] [" + rpcTraceTimestamp() + "] [" + source + "] " + method, COLOR_RPC);
}
void ConsoleTab::addCommandResult(const std::string& cmd, const std::string& result, bool is_error)
{
addLine("> " + cmd, COLOR_COMMAND);

View File

@@ -26,7 +26,7 @@ namespace ui {
class ConsoleTab {
public:
ConsoleTab();
~ConsoleTab() = default;
~ConsoleTab();
/**
* @brief Render the console tab
@@ -46,6 +46,7 @@ public:
* @brief Add a line to the console output
*/
void addLine(const std::string& line, ImU32 color = IM_COL32(200, 200, 200, 255));
void addRpcTraceLine(const std::string& source, const std::string& method);
/**
* @brief Clear console output
@@ -69,6 +70,9 @@ public:
// Show only error messages (filter toggle)
static bool s_errors_only_enabled;
// Show app RPC calls made through RPCClient (method/source only)
static bool s_rpc_trace_enabled;
/// Refresh console text colors for current theme (call after theme switch)
static void refreshColors();
@@ -78,6 +82,7 @@ public:
static ImU32 COLOR_ERROR;
static ImU32 COLOR_DAEMON;
static ImU32 COLOR_INFO;
static ImU32 COLOR_RPC;
private:
struct ConsoleLine {
@@ -129,6 +134,13 @@ private:
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)
struct ScanlineRow {
float yTop = 0.0f;
float yBot = 0.0f;
int rowIndex = 0;
};
std::vector<ScanlineRow> scanline_rows_;
std::mutex lines_mutex_;

View File

@@ -8,6 +8,7 @@
#include "../../rpc/rpc_worker.h"
#include "../../data/wallet_state.h"
#include "../../util/i18n.h"
#include "../explorer/explorer_block_cache.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
@@ -25,6 +26,8 @@
#include <algorithm>
#include <cctype>
#include <atomic>
#include <map>
#include <set>
namespace dragonx {
namespace ui {
@@ -39,18 +42,13 @@ static char s_search_buf[128] = {};
static bool s_search_loading = false;
static std::string s_search_error;
// Recent blocks cache
struct BlockSummary {
int height = 0;
std::string hash;
int tx_count = 0;
int size = 0;
int64_t time = 0;
double difficulty = 0.0;
};
static std::vector<BlockSummary> s_recent_blocks;
static int s_last_known_height = 0;
using BlockSummary = ExplorerBlockSummary;
static ExplorerBlockCache s_recent_block_cache;
static std::set<int> s_pending_block_heights;
static int s_recent_page = 0;
static int s_recent_max_page = 0;
static std::atomic<int> s_pending_block_fetches{0};
static bool s_recent_disk_cache_validation_pending = false;
// Block detail modal
static bool s_show_detail_modal = false;
@@ -116,6 +114,34 @@ static std::string truncateHashToFit(const std::string& hash, ImFont* font, floa
return truncateHash(hash, front, back);
}
static void validateRecentBlockCache(App* app, const WalletState& state) {
if (s_recent_disk_cache_validation_pending) return;
auto validation = s_recent_block_cache.prepareValidation(state.sync.blocks, state.sync.best_blockhash);
if (!validation.needed) return;
auto* worker = app ? app->worker() : nullptr;
auto* rpc = app ? app->rpc() : nullptr;
if (!worker || !rpc || !rpc->isConnected()) return;
int currentHeight = state.sync.blocks;
std::string currentHash = state.sync.best_blockhash;
s_recent_disk_cache_validation_pending = true;
worker->post([rpc, validation, currentHeight, currentHash = std::move(currentHash)]() mutable -> rpc::RPCWorker::MainCb {
std::string actualHash;
try {
rpc::RPCClient::TraceScope trace("Explorer tab / Cache validation");
actualHash = rpc->call("getblockhash", {validation.height}).get<std::string>();
} catch (...) {}
return [validation, currentHeight, currentHash = std::move(currentHash),
actualHash = std::move(actualHash)]() mutable {
s_recent_disk_cache_validation_pending = false;
s_recent_block_cache.applySavedTipValidation(validation, actualHash, currentHeight, currentHash);
};
});
}
static std::string formatSize(int bytes) {
char b[32];
if (bytes >= 1048576)
@@ -127,6 +153,21 @@ static std::string formatSize(int bytes) {
return b;
}
static std::string formatHashrate(double hashrate) {
char b[32];
if (hashrate >= 1e12)
snprintf(b, sizeof(b), "%.2f TH/s", hashrate / 1e12);
else if (hashrate >= 1e9)
snprintf(b, sizeof(b), "%.2f GH/s", hashrate / 1e9);
else if (hashrate >= 1e6)
snprintf(b, sizeof(b), "%.2f MH/s", hashrate / 1e6);
else if (hashrate >= 1e3)
snprintf(b, sizeof(b), "%.2f KH/s", hashrate / 1e3);
else
snprintf(b, sizeof(b), "%.0f H/s", hashrate);
return b;
}
// Copy icon button — draws a small icon that copies text to clipboard on click
static void copyButton(const char* id, const std::string& text, float x, float y) {
ImFont* iconFont = Type().iconSmall();
@@ -211,6 +252,7 @@ static void fetchBlockDetail(App* app, int height) {
json result;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Explorer tab / Block detail");
auto hashResult = rpc->call("getblockhash", {height});
std::string hash = hashResult.get<std::string>();
result = rpc->call("getblock", {hash});
@@ -236,12 +278,14 @@ static void fetchBlockDetailByHash(App* app, const std::string& hash) {
bool gotTx = false;
// Try as block hash first
try {
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
blockResult = rpc->call("getblock", {hash});
gotBlock = true;
} catch (...) {}
// If not a block hash, try as txid
if (!gotBlock) {
try {
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
txResult = rpc->call("getrawtransaction", {hash, 1});
gotTx = true;
} catch (...) {}
@@ -267,43 +311,39 @@ static void fetchBlockDetailByHash(App* app, const std::string& hash) {
});
}
static bool fetchRecentBlocks(App* app, int currentHeight, int count = 10) {
static bool fetchRecentBlock(App* app, int height) {
auto* worker = app->worker();
auto* rpc = app->rpc();
if (!worker || !rpc || s_pending_block_fetches > 0) return false;
if (height < 1 || !worker || !rpc) return false;
if (s_pending_block_heights.find(height) != s_pending_block_heights.end()) return false;
if (s_recent_blocks.empty()) s_recent_blocks.resize(count);
s_pending_block_fetches = 1; // single batched fetch
s_pending_block_heights.insert(height);
s_pending_block_fetches.fetch_add(1);
worker->post([rpc, currentHeight, count]() -> rpc::RPCWorker::MainCb {
std::vector<BlockSummary> results(count);
for (int i = 0; i < count; i++) {
int h = currentHeight - i;
if (h < 1) continue;
try {
auto hashResult = rpc->call("getblockhash", {h});
auto hash = hashResult.get<std::string>();
auto result = rpc->call("getblock", {hash});
auto& bs = results[i];
bs.height = result.value("height", 0);
bs.hash = result.value("hash", "");
bs.time = result.value("time", (int64_t)0);
bs.size = result.value("size", 0);
bs.difficulty = result.value("difficulty", 0.0);
if (result.contains("tx") && result["tx"].is_array())
bs.tx_count = static_cast<int>(result["tx"].size());
} catch (...) {}
}
return [results = std::move(results)]() mutable {
bool gotAny = false;
for (const auto& block : results) {
if (block.height > 0) {
gotAny = true;
break;
}
worker->post([rpc, height]() -> rpc::RPCWorker::MainCb {
BlockSummary block;
try {
rpc::RPCClient::TraceScope trace("Explorer tab / Recent blocks");
auto hashResult = rpc->call("getblockhash", {height});
auto hash = hashResult.get<std::string>();
auto result = rpc->call("getblock", {hash});
block.height = result.value("height", 0);
block.hash = result.value("hash", "");
block.time = result.value("time", (int64_t)0);
block.size = result.value("size", 0);
block.difficulty = result.value("difficulty", 0.0);
if (result.contains("tx") && result["tx"].is_array())
block.tx_count = static_cast<int>(result["tx"].size());
} catch (...) {}
return [height, block = std::move(block)]() mutable {
if (block.height > 0) {
s_recent_block_cache.storeBlock(block);
}
s_pending_block_heights.erase(height);
if (s_pending_block_fetches.load() > 0) {
s_pending_block_fetches.fetch_sub(1);
}
if (gotAny) s_recent_blocks = std::move(results);
s_pending_block_fetches = 0;
};
});
return true;
@@ -318,6 +358,7 @@ static void fetchMempoolInfo(App* app) {
int txCount = 0;
int64_t bytes = 0;
try {
rpc::RPCClient::TraceScope trace("Explorer tab / Mempool summary");
auto result = rpc->call("getmempoolinfo", json::array());
txCount = result.value("size", 0);
bytes = result.value("bytes", (int64_t)0);
@@ -338,7 +379,10 @@ static void fetchTxDetail(App* app, const std::string& txid) {
s_tx_detail = json();
worker->post([rpc, txid]() -> rpc::RPCWorker::MainCb {
json result;
try { result = rpc->call("getrawtransaction", {txid, 1}); }
try {
rpc::RPCClient::TraceScope trace("Explorer tab / Transaction detail");
result = rpc->call("getrawtransaction", {txid, 1});
}
catch (...) {}
return [result]() {
s_tx_loading = false;
@@ -377,6 +421,16 @@ static void renderSearchBar(App* app, float availWidth) {
auto& S = schema::UI();
float pad = Layout::cardInnerPadding();
ImFont* capFont = Type().caption();
float navBtnSz = std::max(24.0f * Layout::dpiScale(), capFont->LegacySize + 8.0f * Layout::dpiScale());
if (s_recent_page > s_recent_max_page) s_recent_page = s_recent_max_page;
if (s_recent_page < 0) s_recent_page = 0;
char pageBuf[32];
snprintf(pageBuf, sizeof(pageBuf), "%d / %d", s_recent_page + 1, s_recent_max_page + 1);
float pageW = capFont->CalcTextSizeA(capFont->LegacySize, 1000.0f, 0.0f, pageBuf).x;
float navGap = Layout::spacingXs();
float navW = navBtnSz * 2.0f + pageW + navGap * 2.0f;
float inputW = std::min(
S.drawElement("tabs.explorer", "search-input-width").size,
availWidth * 0.65f);
@@ -384,11 +438,12 @@ static void renderSearchBar(App* app, float availWidth) {
float barH = S.drawElement("tabs.explorer", "search-bar-height").size;
// Clamp so search bar never overflows
float maxInputW = availWidth - btnW - pad * 3 - Type().iconMed()->LegacySize;
float maxInputW = availWidth - btnW - navW - pad * 4 - Type().iconMed()->LegacySize;
if (inputW > maxInputW) inputW = maxInputW;
if (inputW < 80.0f) inputW = 80.0f;
ImGui::Spacing();
float rowStartX = ImGui::GetCursorScreenPos().x;
// Icon
ImGui::PushFont(Type().iconMed());
@@ -429,6 +484,32 @@ static void renderSearchBar(App* app, float availWidth) {
ImGui::TextDisabled("%s", TR("loading"));
}
float navX = rowStartX + availWidth - navW;
float navY = cursorY + std::max(0.0f, (barH - navBtnSz) * 0.5f);
ImGui::SetCursorScreenPos(ImVec2(navX, navY));
ImGui::BeginDisabled(s_recent_page <= 0);
ImGui::PushID("RecentBlocksSearchPrev");
if (TactileButton(ICON_MD_CHEVRON_LEFT, ImVec2(navBtnSz, navBtnSz), Type().iconSmall())) {
--s_recent_page;
}
ImGui::PopID();
ImGui::EndDisabled();
ImGui::SameLine(0, navGap);
ImVec2 pagePos = ImGui::GetCursorScreenPos();
ImGui::GetWindowDrawList()->AddText(capFont, capFont->LegacySize,
ImVec2(pagePos.x, pagePos.y + (navBtnSz - capFont->LegacySize) * 0.5f), OnSurfaceMedium(), pageBuf);
ImGui::Dummy(ImVec2(pageW, navBtnSz));
ImGui::SameLine(0, navGap);
ImGui::BeginDisabled(s_recent_page >= s_recent_max_page);
ImGui::PushID("RecentBlocksSearchNext");
if (TactileButton(ICON_MD_CHEVRON_RIGHT, ImVec2(navBtnSz, navBtnSz), Type().iconSmall())) {
++s_recent_page;
}
ImGui::PopID();
ImGui::EndDisabled();
ImGui::SetCursorScreenPos(ImVec2(rowStartX, cursorY + barH));
// Error
if (!s_search_error.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
@@ -443,6 +524,7 @@ static void renderChainStats(App* app, float availWidth) {
const auto& state = app->getWalletState();
float pad = Layout::cardInnerPadding();
float gap = Layout::cardGap();
float dp = Layout::dpiScale();
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
@@ -451,109 +533,185 @@ static void renderChainStats(App* app, float availWidth) {
ImFont* ovFont = Type().overline();
ImFont* capFont = Type().caption();
ImFont* sub1 = Type().subtitle1();
ImFont* heroFont = Type().h2();
ImFont* iconFont = Type().iconSmall();
float cardW = (availWidth - gap) * 0.5f;
float rowH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize;
float headerH = ovFont->LegacySize + Layout::spacingSm();
float cardH = pad * 0.5f + headerH + rowH * 3 + Layout::spacingSm() * 2 + pad * 0.5f;
float heroLineH = capFont->LegacySize + Layout::spacingXs() + heroFont->LegacySize;
float hashLineH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize;
float cardH = std::max(148.0f * dp,
pad * 1.5f + headerH + heroLineH + hashLineH + Layout::spacingLg() * 2.0f);
bool stacked = availWidth < 760.0f * dp;
float chainW = stacked ? availWidth : (availWidth - gap) * 0.60f;
float metricsW = stacked ? availWidth : availWidth - chainW - gap;
ImVec2 basePos = ImGui::GetCursorScreenPos();
char buf[128];
// ── Chain Card (left) ──
{
ImVec2 cardMin = basePos;
auto drawStatusPill = [&](const ImVec2& cardMin, float cardW) {
bool connected = app->rpc() && app->rpc()->isConnected();
ImU32 pillCol = connected ? Success() : Error();
std::string statusText;
if (!connected) {
statusText = TR("not_connected");
} else if (state.sync.syncing && state.sync.headers > 0) {
snprintf(buf, sizeof(buf), "%s %.1f%%", TR("syncing"), state.sync.verification_progress * 100.0);
statusText = buf;
pillCol = Warning();
} else {
statusText = TR("connected");
}
float maxTextW = cardW * 0.34f;
statusText = truncateHashToFit(statusText, capFont, maxTextW);
ImVec2 textSz = capFont->CalcTextSizeA(capFont->LegacySize, 1000.0f, 0.0f, statusText.c_str());
float pillPadX = Layout::spacingSm();
float pillH = capFont->LegacySize + Layout::spacingXs() * 2.0f;
float pillW = textSz.x + pillPadX * 2.0f;
ImVec2 pillMin(cardMin.x + cardW - pad - pillW, cardMin.y + pad * 0.5f);
ImVec2 pillMax(pillMin.x + pillW, pillMin.y + pillH);
dl->AddRectFilled(pillMin, pillMax, WithAlpha(pillCol, 32), pillH * 0.5f);
dl->AddRect(pillMin, pillMax, WithAlpha(pillCol, 110), pillH * 0.5f, 0, 1.0f * dp);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(pillMin.x + pillPadX, pillMin.y + Layout::spacingXs()), pillCol, statusText.c_str());
};
auto drawChainTip = [&](const ImVec2& cardMin, float cardW) {
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
dl->AddText(ovFont, ovFont->LegacySize,
ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_chain_stats"));
drawStatusPill(cardMin, cardW);
float colW = (cardW - pad * 2) / 2.0f;
float ry = cardMin.y + pad * 0.5f + headerH;
float labelY = cardMin.y + pad * 0.5f + headerH + Layout::spacingLg();
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cardMin.x + pad, labelY), OnSurfaceMedium(), TR("explorer_block_height"));
// Row 1: Height | Difficulty
{
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("explorer_block_height"));
snprintf(buf, sizeof(buf), "%d", state.sync.blocks);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
snprintf(buf, sizeof(buf), "%d", state.sync.blocks);
float valueY = labelY + capFont->LegacySize + Layout::spacingXs();
DrawTextShadow(dl, heroFont, heroFont->LegacySize,
ImVec2(cardMin.x + pad, valueY), OnSurface(), buf);
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("difficulty"));
snprintf(buf, sizeof(buf), "%.4f", state.mining.difficulty);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
if (state.sync.syncing && state.sync.headers > 0) {
float progress = std::clamp(static_cast<float>(state.sync.verification_progress), 0.0f, 1.0f);
float barY = valueY + heroFont->LegacySize + Layout::spacingMd();
float barH = std::max(3.0f * dp, 3.0f);
float barW = cardW - pad * 2.0f;
dl->AddRectFilled(ImVec2(cardMin.x + pad, barY),
ImVec2(cardMin.x + pad + barW, barY + barH), WithAlpha(OnSurface(), 18), barH * 0.5f);
dl->AddRectFilled(ImVec2(cardMin.x + pad, barY),
ImVec2(cardMin.x + pad + barW * progress, barY + barH), WithAlpha(Warning(), 180), barH * 0.5f);
}
ry += rowH + Layout::spacingSm();
// Row 2: Hashrate | Notarized
{
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_hashrate"));
double hr = state.mining.networkHashrate;
if (hr >= 1e9)
snprintf(buf, sizeof(buf), "%.2f GH/s", hr / 1e9);
else if (hr >= 1e6)
snprintf(buf, sizeof(buf), "%.2f MH/s", hr / 1e6);
else if (hr >= 1e3)
snprintf(buf, sizeof(buf), "%.2f KH/s", hr / 1e3);
else
snprintf(buf, sizeof(buf), "%.0f H/s", hr);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
float hashLabelY = cardMax.y - pad - hashLineH;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cardMin.x + pad, hashLabelY), OnSurfaceMedium(), TR("peers_best_block"));
cx = cardMin.x + pad + colW;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_notarized"));
snprintf(buf, sizeof(buf), "%d", state.notarized);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
float hashY = hashLabelY + capFont->LegacySize + Layout::spacingXs();
float copyW = iconFont->LegacySize + Layout::spacingSm() * 2.0f;
float hashMaxW = cardW - pad * 2.0f - (state.sync.best_blockhash.empty() ? 0.0f : copyW + Layout::spacingSm());
std::string hashDisp = state.sync.best_blockhash.empty()
? std::string("--")
: truncateHashToFit(state.sync.best_blockhash, sub1, hashMaxW);
dl->AddText(sub1, sub1->LegacySize,
ImVec2(cardMin.x + pad, hashY), OnSurface(), hashDisp.c_str());
if (!state.sync.best_blockhash.empty()) {
ImVec2 savedCursor = ImGui::GetCursorScreenPos();
copyButton("ChainTipBestHash", state.sync.best_blockhash,
cardMax.x - pad - copyW, hashY);
ImGui::SetCursorScreenPos(savedCursor);
}
ry += rowH + Layout::spacingSm();
};
// Row 3: Best Block Hash — adaptive truncation to fit card
{
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_best_block"));
float maxHashW = cardW - pad * 2;
std::string hashDisp = truncateHashToFit(state.sync.best_blockhash, sub1, maxHashW);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), hashDisp.c_str());
}
}
// ── Mempool Card (right) ──
{
ImVec2 cardMin(basePos.x + cardW + gap, basePos.y);
auto drawMetricGrid = [&](const ImVec2& cardMin, float cardW) {
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
dl->AddText(ovFont, ovFont->LegacySize,
ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_mempool"));
struct MetricSpec {
const char* icon;
const char* label;
std::string value;
std::string detail;
ImU32 accent;
};
float ry = cardMin.y + pad * 0.5f + headerH;
snprintf(buf, sizeof(buf), "%.4f", state.mining.difficulty);
std::string difficulty = buf;
snprintf(buf, sizeof(buf), "%d", state.notarized);
std::string notarized = buf;
snprintf(buf, sizeof(buf), "%d", s_mempool_tx_count);
std::string mempoolTxs = s_mempool_loading ? std::string("...") : std::string(buf);
// Row 1: Transactions
{
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("explorer_mempool_txs"));
snprintf(buf, sizeof(buf), "%d", s_mempool_tx_count);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
MetricSpec metrics[4] = {
{ ICON_MD_SPEED, TR("difficulty"), difficulty, "", Warning() },
{ ICON_MD_SHOW_CHART, TR("peers_hashrate"), formatHashrate(state.mining.networkHashrate), "", Success() },
{ ICON_MD_CONFIRMATION_NUMBER, TR("peers_notarized"), notarized, "", Primary() },
{ ICON_MD_STORAGE, TR("explorer_mempool"), mempoolTxs, s_mempool_loading ? std::string("") : formatSize(static_cast<int>(s_mempool_size)), Secondary() },
};
float gridX = cardMin.x + pad;
float gridY = cardMin.y + pad;
float gridW = cardW - pad * 2.0f;
float gridH = cardH - pad * 2.0f;
float cellW = gridW * 0.5f;
float cellH = gridH * 0.5f;
ImU32 dividerCol = WithAlpha(OnSurface(), 22);
dl->AddLine(ImVec2(gridX + cellW, gridY), ImVec2(gridX + cellW, gridY + gridH), dividerCol, 1.0f * dp);
dl->AddLine(ImVec2(gridX, gridY + cellH), ImVec2(gridX + gridW, gridY + cellH), dividerCol, 1.0f * dp);
for (int i = 0; i < 4; ++i) {
int col = i % 2;
int row = i / 2;
ImVec2 cellMin(gridX + col * cellW, gridY + row * cellH);
ImVec2 cellMax(cellMin.x + cellW, cellMin.y + cellH);
float inset = Layout::spacingSm();
float textX = cellMin.x + inset;
float textY = cellMin.y + inset;
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(cellMax.x - inset - iconFont->LegacySize, textY),
WithAlpha(metrics[i].accent, 180), metrics[i].icon);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(textX, textY), OnSurfaceMedium(), metrics[i].label);
float valueY = textY + capFont->LegacySize + Layout::spacingXs();
float valueMaxW = cellW - inset * 2.0f;
std::string value = truncateHashToFit(metrics[i].value, sub1, valueMaxW);
dl->AddText(sub1, sub1->LegacySize,
ImVec2(textX, valueY), OnSurface(), value.c_str());
if (!metrics[i].detail.empty()) {
std::string detail = truncateHashToFit(metrics[i].detail, capFont, valueMaxW);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(textX, valueY + sub1->LegacySize + Layout::spacingXs()),
OnSurfaceDisabled(), detail.c_str());
}
}
ry += rowH + Layout::spacingSm();
};
// Row 2: Size
{
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("explorer_mempool_size"));
std::string sizeStr = formatSize(static_cast<int>(s_mempool_size));
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), sizeStr.c_str());
}
drawChainTip(basePos, chainW);
if (stacked) {
drawMetricGrid(ImVec2(basePos.x, basePos.y + cardH + gap), metricsW);
ImGui::Dummy(ImVec2(availWidth, cardH * 2.0f + gap + Layout::spacingMd()));
} else {
drawMetricGrid(ImVec2(basePos.x + chainW + gap, basePos.y), metricsW);
ImGui::Dummy(ImVec2(availWidth, cardH + Layout::spacingMd()));
}
ImGui::Dummy(ImVec2(availWidth, cardH + Layout::spacingMd()));
}
static void renderRecentBlocks(App* app, float availWidth) {
auto& S = schema::UI();
float pad = Layout::cardInnerPadding();
float dp = Layout::dpiScale();
const auto& state = app->getWalletState();
validateRecentBlockCache(app, state);
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
@@ -564,19 +722,13 @@ static void renderRecentBlocks(App* app, float availWidth) {
ImFont* body2 = Type().body2();
ImFont* sub1 = Type().subtitle1();
float rowH = S.drawElement("tabs.explorer", "row-height").size;
float baseRowH = S.drawElement("tabs.explorer", "row-height").size;
float rowRound = S.drawElement("tabs.explorer", "row-rounding").size;
float headerH = ovFont->LegacySize + Layout::spacingSm() + pad * 0.5f;
// Filter out empty entries
std::vector<const BlockSummary*> blocks;
for (const auto& bs : s_recent_blocks) {
if (bs.height > 0) blocks.push_back(&bs);
}
// Stretch card to fill the remaining tab height; rows scroll inside.
float maxRows = 10.0f;
float contentH = capFont->LegacySize + Layout::spacingXs() + rowH * maxRows;
float contentH = capFont->LegacySize + Layout::spacingXs() + baseRowH * maxRows;
float minTableH = headerH + contentH + pad;
float remainingH = ImGui::GetContentRegionAvail().y;
float tableH = std::max(minTableH, remainingH - Layout::spacingSm());
@@ -585,10 +737,84 @@ static void renderRecentBlocks(App* app, float availWidth) {
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + tableH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
// Header
dl->AddText(ovFont, ovFont->LegacySize,
ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_recent_blocks"));
float hdrY = cardMin.y + pad * 0.5f + ovFont->LegacySize + Layout::spacingSm();
float rowAreaTop = hdrY + capFont->LegacySize + Layout::spacingXs();
float rowAreaH = cardMax.y - rowAreaTop - pad * 0.5f;
int rowsPerPage = std::max(1, (int)std::ceil(rowAreaH / std::max(1.0f, baseRowH)));
float rowH = baseRowH;
int maxPage = (state.sync.blocks > 0) ? (state.sync.blocks - 1) / rowsPerPage : 0;
s_recent_max_page = maxPage;
if (s_recent_page > maxPage) s_recent_page = maxPage;
if (s_recent_page < 0) s_recent_page = 0;
int startHeight = state.sync.blocks - s_recent_page * rowsPerPage;
std::vector<int> pageHeights;
pageHeights.reserve(rowsPerPage);
for (int i = 0; i < rowsPerPage; ++i) {
int height = startHeight - i;
if (height < 1) break;
pageHeights.push_back(height);
}
std::map<int, BlockSummary> cachedBlocks;
if (!pageHeights.empty()) {
cachedBlocks = s_recent_block_cache.loadRange(pageHeights.back(), pageHeights.front());
}
bool canFetchBlocks = state.sync.blocks > 0 &&
!s_show_detail_modal && !s_detail_loading && !s_tx_loading;
if (canFetchBlocks) {
bool currentPageComplete = true;
bool scheduledFetch = false;
auto* rpc = app->rpc();
if (rpc && rpc->isConnected()) {
for (int height : pageHeights) {
if (cachedBlocks.find(height) == cachedBlocks.end()) {
currentPageComplete = false;
scheduledFetch = fetchRecentBlock(app, height) || scheduledFetch;
}
}
if (currentPageComplete && s_recent_page < maxPage) {
std::vector<int> nextPageHeights;
nextPageHeights.reserve(rowsPerPage);
int nextStartHeight = state.sync.blocks - (s_recent_page + 1) * rowsPerPage;
for (int i = 0; i < rowsPerPage; ++i) {
int height = nextStartHeight - i;
if (height < 1) break;
nextPageHeights.push_back(height);
}
std::map<int, BlockSummary> nextCachedBlocks;
if (!nextPageHeights.empty()) {
nextCachedBlocks = s_recent_block_cache.loadRange(nextPageHeights.back(), nextPageHeights.front());
}
for (int height : nextPageHeights) {
if (nextCachedBlocks.find(height) == nextCachedBlocks.end()) {
scheduledFetch = fetchRecentBlock(app, height) || scheduledFetch;
}
}
}
}
if (scheduledFetch) {
fetchMempoolInfo(app);
}
}
struct RecentBlockRow {
int height = 0;
const BlockSummary* block = nullptr;
};
std::vector<RecentBlockRow> blocks;
blocks.reserve(pageHeights.size());
for (int height : pageHeights) {
auto it = cachedBlocks.find(height);
blocks.push_back({height,
it != cachedBlocks.end() ? &it->second : nullptr});
}
// Responsive column layout — give height more room, use remaining for data
float innerW = availWidth - pad * 2;
float colHeight = pad;
@@ -598,7 +824,6 @@ static void renderRecentBlocks(App* app, float availWidth) {
float colHash = colHeight + innerW * 0.56f;
float colTime = colHeight + innerW * 0.82f;
float hdrY = cardMin.y + pad * 0.5f + ovFont->LegacySize + Layout::spacingSm();
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colHeight, hdrY), OnSurfaceMedium(), TR("explorer_block_height"));
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colTxs, hdrY), OnSurfaceMedium(), TR("explorer_block_txs"));
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colSize, hdrY), OnSurfaceMedium(), TR("explorer_block_size"));
@@ -607,13 +832,11 @@ static void renderRecentBlocks(App* app, float availWidth) {
dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colTime, hdrY), OnSurfaceMedium(), TR("explorer_block_time"));
// Scrollable child region for rows
float rowAreaTop = hdrY + capFont->LegacySize + Layout::spacingXs();
float rowAreaH = cardMax.y - rowAreaTop - pad * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, rowAreaTop));
int parentVtx = dl->VtxBuffer.Size;
ImGui::BeginChild("##BlockRows", ImVec2(availWidth, rowAreaH), false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
ApplySmoothScroll();
ImDrawList* childDL = ImGui::GetWindowDrawList();
@@ -624,12 +847,14 @@ static void renderRecentBlocks(App* app, float availWidth) {
char buf[128];
float rowInset = 2 * dp;
if (blocks.empty() && s_pending_block_fetches > 0) {
if (blocks.empty()) {
ImGui::SetCursorPosY(Layout::spacingMd());
ImGui::TextDisabled("%s", TR("loading"));
} else {
for (size_t i = 0; i < blocks.size(); i++) {
const auto* bs = blocks[i];
const auto& row = blocks[i];
const auto* bs = row.block;
int blockHeight = bs ? bs->height : row.height;
ImVec2 rowPos = ImGui::GetCursorScreenPos();
float rowW = ImGui::GetContentRegionAvail().x - rowInset * 2;
@@ -651,19 +876,19 @@ static void renderRecentBlocks(App* app, float availWidth) {
}
// Hover highlight — clear clickable feedback
if (hovered) {
if (hovered && bs) {
childDL->AddRectFilled(rowMin, rowMax, WithAlpha(Primary(), 20), rowRound);
childDL->AddRect(rowMin, rowMax, WithAlpha(Primary(), 40), rowRound);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
// Selected highlight
if (s_show_detail_modal && s_detail_height == bs->height) {
if (bs && s_show_detail_modal && s_detail_height == bs->height) {
childDL->AddRectFilled(rowMin, rowMax, WithAlpha(Primary(), 25), rowRound);
}
// Click opens block detail modal
if (clicked && app->rpc() && app->rpc()->isConnected()) {
if (bs && clicked && app->rpc() && app->rpc()->isConnected()) {
fetchBlockDetail(app, bs->height);
}
@@ -671,30 +896,32 @@ static void renderRecentBlocks(App* app, float availWidth) {
float textY2 = rowPos.y + (rowH - body2->LegacySize) * 0.5f;
// Height — emphasized with larger font and primary color
snprintf(buf, sizeof(buf), "#%d", bs->height);
snprintf(buf, sizeof(buf), "#%d", blockHeight);
childDL->AddText(sub1, sub1->LegacySize, ImVec2(cardMin.x + colHeight, textY),
hovered ? Primary() : schema::UI().resolveColor("var(--primary-light)"), buf);
(hovered && bs) ? Primary() : schema::UI().resolveColor("var(--primary-light)"), buf);
// Tx count
snprintf(buf, sizeof(buf), "%d tx", bs->tx_count);
if (bs) snprintf(buf, sizeof(buf), "%d tx", bs->tx_count);
else snprintf(buf, sizeof(buf), "...");
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colTxs, textY2), OnSurface(), buf);
// Size
std::string sizeStr = formatSize(bs->size);
std::string sizeStr = bs ? formatSize(bs->size) : "...";
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colSize, textY2), OnSurface(), sizeStr.c_str());
// Difficulty
snprintf(buf, sizeof(buf), "%.2f", bs->difficulty);
if (bs) snprintf(buf, sizeof(buf), "%.2f", bs->difficulty);
else snprintf(buf, sizeof(buf), "...");
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colDiff, textY2), OnSurface(), buf);
// Hash — adaptive to available column width
float hashMaxW = (colTime - colHash) - Layout::spacingSm();
std::string hashDisp = truncateHashToFit(bs->hash, body2, hashMaxW);
std::string hashDisp = bs ? truncateHashToFit(bs->hash, body2, hashMaxW) : "...";
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colHash, textY2),
OnSurfaceMedium(), hashDisp.c_str());
// Time
const char* timeStr = relativeTime(bs->time);
const char* timeStr = bs ? relativeTime(bs->time) : "...";
childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colTime, textY2), OnSurfaceMedium(), timeStr);
}
}
@@ -1023,30 +1250,14 @@ static void renderBlockDetailModal(App* app) {
void RenderExplorerTab(App* app)
{
const auto& state = app->getWalletState();
auto* rpc = app->rpc();
ImVec2 avail = ImGui::GetContentRegionAvail();
ImGui::BeginChild("##ExplorerScroll", avail, false,
ImGuiWindowFlags_NoBackground);
ApplySmoothScroll();
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
float availWidth = ImGui::GetContentRegionAvail().x;
// Auto-refresh recent blocks when chain height changes, but avoid
// starting expensive block fetches while the user is viewing details.
if (state.sync.blocks > 0 && state.sync.blocks != s_last_known_height &&
!s_show_detail_modal && !s_detail_loading && !s_tx_loading) {
if (rpc && rpc->isConnected()) {
if (fetchRecentBlocks(app, state.sync.blocks)) {
s_last_known_height = state.sync.blocks;
fetchMempoolInfo(app);
}
}
}
renderSearchBar(app, availWidth);
renderChainStats(app, availWidth);
renderSearchBar(app, availWidth);
renderRecentBlocks(app, availWidth);
ImGui::EndChild();

View File

@@ -170,6 +170,7 @@ void ExportAllKeysDialog::render(App* app)
keys += "# === Z-Addresses (Shielded) ===\n\n";
for (const auto& addr : z_addrs) {
try {
rpc::RPCClient::TraceScope trace("Settings / Export all keys");
auto result = rpc->call("z_exportkey", {addr});
if (result.is_string()) {
keys += "# Address: " + addr + "\n";
@@ -185,6 +186,7 @@ void ExportAllKeysDialog::render(App* app)
keys += "# === T-Addresses (Transparent) ===\n\n";
for (const auto& addr : t_addrs) {
try {
rpc::RPCClient::TraceScope trace("Settings / Export all keys");
auto result = rpc->call("dumpprivkey", {addr});
if (result.is_string()) {
keys += "# Address: " + addr + "\n";

View File

@@ -309,7 +309,7 @@ void ImportKeyDialog::render(App* app)
bool rescan = s_rescan;
int rescanHeight = s_rescan_height;
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), keys, rescan, rescanHeight]() -> rpc::RPCWorker::MainCb {
app->worker()->post([app, rpc = app->rpc(), keys, rescan, rescanHeight]() -> rpc::RPCWorker::MainCb {
int imported = 0;
int failed = 0;
@@ -317,6 +317,7 @@ void ImportKeyDialog::render(App* app)
std::string keyType = detectKeyType(key);
try {
rpc::RPCClient::TraceScope trace("Settings / Import private keys");
if (keyType == "z-spending") {
// z_importkey "key" "yes"|"no" startheight
if (rescan && rescanHeight > 0) {
@@ -348,13 +349,14 @@ void ImportKeyDialog::render(App* app)
// single rescanblockchain from that height now.
if (rescan && rescanHeight > 0 && imported > 0) {
try {
rpc::RPCClient::TraceScope trace("Settings / Import private keys");
rpc->call("rescanblockchain", {rescanHeight});
} catch (...) {
// rescan failure is non-fatal; user can retry
}
}
return [imported, failed]() {
return [app, imported, failed]() {
s_imported_keys = imported;
s_failed_keys = failed;
s_importing = false;
@@ -363,6 +365,8 @@ void ImportKeyDialog::render(App* app)
imported, failed);
s_status = buf;
if (imported > 0) {
app->invalidateAddressValidationCache();
app->refreshNow();
Notifications::instance().success(TR("import_key_success"), 5.0f);
}
};

View File

@@ -127,6 +127,7 @@ void KeyExportDialog::render(App* app)
std::string key;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Settings / Export key");
auto result = rpc->call(method, {addr});
key = result.get<std::string>();
} catch (const std::exception& e) {
@@ -152,6 +153,7 @@ void KeyExportDialog::render(App* app)
std::string key;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Settings / Export viewing key");
auto result = rpc->call("z_exportviewingkey", {addr});
key = result.get<std::string>();
} catch (const std::exception& e) {

View File

@@ -162,15 +162,6 @@ void RenderMarketTab(App* app)
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();
@@ -797,15 +788,6 @@ void RenderMarketTab(App* app)
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();

View File

@@ -347,6 +347,7 @@ void RenderPeersTab(App* app)
// Click to copy full hash
ImVec2 hashSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, truncHash.c_str());
ImVec2 savedCursor = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(ImVec2(cx, valY));
ImGui::InvisibleButton("##BestBlockCopy", ImVec2(hashSz.x + Layout::spacingSm(), sub1->LegacySize + 2 * dp));
if (ImGui::IsItemHovered()) {
@@ -360,6 +361,7 @@ void RenderPeersTab(App* app)
ImGui::SetClipboardText(hash.c_str());
ui::Notifications::instance().info(TR("peers_hash_copied"));
}
ImGui::SetCursorScreenPos(savedCursor);
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
}
@@ -482,6 +484,7 @@ void RenderPeersTab(App* app)
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), bannedCol, buf); }
}
ImGui::SetCursorScreenPos(basePos);
ImGui::Dummy(ImVec2(availWidth, infoCardsH));
ImGui::Dummy(ImVec2(0, gap));
}

View File

@@ -9,7 +9,6 @@
// - Recent received at bottom
#include "receive_tab.h"
#include "send_tab.h"
#include "../../app.h"
#include "../../config/settings.h"
#include "../../util/i18n.h"
@@ -360,145 +359,99 @@ static void DrawRecvIcon(ImDrawList* dl, float cx, float cy, float s, ImU32 col)
// ============================================================================
// Recent received transactions — styled to match transactions list
// ============================================================================
static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */,
static void RenderRecentReceived(const AddressInfo& /* addr */,
const WalletState& state, float width,
ImFont* capFont, App* app) {
auto& S = schema::UI();
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_received"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float hs = Layout::hScale(width);
float glassRound = Layout::glassRounding();
ImFont* body2 = Type().body2();
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
float iconSz = std::max(schema::UI().drawElement("tabs.receive", "recent-icon-min-size").size, schema::UI().drawElement("tabs.receive", "recent-icon-size").size * hs);
float vs = Layout::vScale(std::max(1.0f, ImGui::GetContentRegionAvail().y));
float dp = Layout::dpiScale();
float rowH = std::max(18.0f * dp, S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f) * vs);
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
ImU32 recvCol = Success();
ImU32 greenCol = WithAlpha(Success(), (int)schema::UI().drawElement("tabs.receive", "recent-green-alpha").size);
float rowPadLeft = Layout::spacingLg();
// Collect matching transactions
std::vector<const TransactionInfo*> recvs;
for (const auto& tx : state.transactions) {
if (tx.type != "receive") continue;
if (tx.type != "receive" && tx.type != "mined") continue;
recvs.push_back(&tx);
if (recvs.size() >= (size_t)schema::UI().drawElement("tabs.receive", "max-recent-receives").size) break;
}
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
ImGui::BeginChild("##RecentReceivedRows", ImVec2(width, listH), false,
ImGuiWindowFlags_NoBackground);
ImDrawList* rowDL = ImGui::GetWindowDrawList();
char buf[64];
if (recvs.empty()) {
ImGui::SetCursorPosY(Layout::spacingMd());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_recent_receives"));
ImGui::EndChild();
return;
}
// Outer glass panel wrapping all rows
float itemSpacingY = ImGui::GetStyle().ItemSpacing.y;
float listH = rowH * (float)recvs.size() + itemSpacingY * (float)(recvs.size() - 1);
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
ImVec2 listPanelMax(listPanelMin.x + width, listPanelMin.y + listH);
GlassPanelSpec glassSpec;
glassSpec.rounding = glassRound;
DrawGlassPanel(dl, listPanelMin, listPanelMax, glassSpec);
// Clip draw commands to panel bounds to prevent overflow
dl->PushClipRect(listPanelMin, listPanelMax, true);
char buf[64];
for (size_t ri = 0; ri < recvs.size(); ri++) {
const auto& tx = *recvs[ri];
ImVec2 rowPos = ImGui::GetCursorScreenPos();
ImVec2 rowEnd(rowPos.x + width, rowPos.y + rowH);
float rowY = rowPos.y + rowH * 0.5f;
// Hover glow
bool hovered = material::IsRectHovered(rowPos, rowEnd);
if (hovered) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.receive", "row-hover-alpha").size), schema::UI().drawElement("tabs.receive", "row-hover-rounding").size);
// Icon
DrawRecvIcon(rowDL, rowPos.x + Layout::spacingMd(), rowY, iconSz * 0.5f, recvCol);
// Type label (first line)
float txX = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
const char* typeText = tx.type == "mined" ? TR("mined_type") : TR("received_label");
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(txX, rowPos.y + 2.0f * dp), OnSurfaceMedium(), typeText);
// Address (second line)
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
std::string addrDisplay = TruncateAddress(tx.address,
(size_t)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());
// Amount (right-aligned, first line)
snprintf(buf, sizeof(buf), "+%.4f %s", std::abs(tx.amount), DRAGONX_TICKER);
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, buf);
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size,
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(amtX, rowPos.y + 2.0f * dp), recvCol, buf);
// Time ago
std::string ago = recvTimeAgo(tx.timestamp);
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str());
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f),
rowPos.y + 2.0f * dp),
OnSurfaceDisabled(), ago.c_str());
// Clickable row — hover highlight + navigate to History
float rowW = ImGui::GetContentRegionAvail().x;
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
if (material::IsRectHovered(rowPos, rowEnd)) {
rowDL->AddRectFilled(rowPos, rowEnd,
IM_COL32(255, 255, 255, 15),
S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) {
app->setCurrentPage(ui::NavPage::History);
}
}
float cx = rowPos.x + rowPadLeft;
float cy = rowPos.y + Layout::spacingMd();
// Icon
DrawRecvIcon(dl, cx + iconSz, cy + body2->LegacySize * 0.5f, iconSz, recvCol);
// Type label (first line)
float labelX = cx + iconSz * 2.0f + Layout::spacingSm();
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), recvCol, TR("received_label"));
// Time (next to type)
std::string ago = recvTimeAgo(tx.timestamp);
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("received_label")).x;
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy),
OnSurfaceDisabled(), ago.c_str());
// Address (second line)
std::string addr_display = TruncateAddress(tx.address, (int)schema::UI().drawElement("tabs.receive", "recent-addr-trunc-len").size);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceMedium(), addr_display.c_str());
// Amount (right-aligned, first line)
snprintf(buf, sizeof(buf), "+%.8f", tx.amount);
ImVec2 amtSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, buf);
float amtX = rowPos.x + width - amtSz.x - Layout::spacingLg();
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(amtX, cy), recvCol, buf,
schema::UI().drawElement("tabs.receive", "text-shadow-offset-x").size, schema::UI().drawElement("tabs.receive", "text-shadow-offset-y").size, IM_COL32(0, 0, 0, (int)schema::UI().drawElement("tabs.receive", "text-shadow-alpha").size));
// USD equivalent (right-aligned, second line)
double priceUsd = state.market.price_usd;
if (priceUsd > 0.0) {
double usdVal = 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 + width - usdSz.x - Layout::spacingLg(), cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceDisabled(), buf);
}
// Status badge
{
const char* statusStr;
ImU32 statusCol;
if (tx.confirmations == 0) {
statusStr = TR("pending"); statusCol = Warning();
} else if (tx.confirmations < (int)schema::UI().drawElement("tabs.receive", "confirmed-threshold").size) {
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
statusStr = buf; statusCol = Warning();
} else {
statusStr = TR("confirmed"); statusCol = greenCol;
}
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
float statusX = amtX - sSz.x - Layout::spacingXxl();
float minStatusX = cx + width * schema::UI().drawElement("tabs.receive", "status-min-x-ratio").size;
if (statusX < minStatusX) statusX = minStatusX;
ImU32 pillBg = (statusCol & 0x00FFFFFFu) | (static_cast<ImU32>((int)schema::UI().drawElement("tabs.receive", "status-pill-bg-alpha").size) << 24);
ImVec2 pillMin(statusX - Layout::spacingSm(), cy + body2->LegacySize + (int)schema::UI().drawElement("tabs.receive", "status-pill-y-offset").size);
ImVec2 pillMax(statusX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs());
dl->AddRectFilled(pillMin, pillMax, pillBg, schema::UI().drawElement("tabs.receive", "status-pill-rounding").size);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(statusX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
}
ImGui::Dummy(ImVec2(0, rowH));
// Subtle divider between rows
if (ri < recvs.size() - 1) {
ImVec2 divStart = ImGui::GetCursorScreenPos();
dl->AddLine(ImVec2(divStart.x + rowPadLeft + iconSz * 2.0f, divStart.y),
ImVec2(divStart.x + width - Layout::spacingLg(), divStart.y),
IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.receive", "row-divider-alpha").size));
}
}
dl->PopClipRect();
ImGui::EndChild();
}
// ============================================================================
@@ -843,23 +796,6 @@ void RenderReceiveTab(App* app)
ImGui::Dummy(ImVec2(addrColW, capFont->LegacySize + Layout::spacingSm()));
}
// Clear button
{
bool hasData = (s_request_amount > 0 || s_request_memo[0]);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "clear-btn-hover-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(hasData ? OnSurfaceMedium() : OnSurfaceDisabled()));
ImGui::BeginDisabled(!hasData);
if (TactileSmallButton(TrId("clear_request", "recv").c_str(), S.resolveFont("button"))) {
s_request_amount = 0.0;
s_request_usd_amount = 0.0;
s_request_memo[0] = '\0';
}
ImGui::EndDisabled();
ImGui::PopStyleColor(3);
}
}
float leftBottom = ImGui::GetCursorScreenPos().y;
@@ -869,9 +805,14 @@ void RenderReceiveTab(App* app)
float rx = qrColX;
float ry = sectionTop.y;
float maxQrSize = std::min(qrAvailW - Layout::spacingMd() * 2, S.drawElement("tabs.receive", "qr-max-size").size);
float qrSize = std::max(S.drawElement("tabs.receive", "qr-min-size").size, maxQrSize);
float qrPadding = Layout::spacingMd();
float qrWidthBound = std::max(0.0f, qrAvailW - qrPadding * 2.0f);
float qrHeightBound = std::max(0.0f, leftBottom - sectionTop.y - qrPadding * 2.0f);
float qrMaxSize = qrHeightBound > 0.0f
? std::min(qrWidthBound, qrHeightBound)
: qrWidthBound;
float qrMinSize = std::min(S.drawElement("tabs.receive", "qr-min-size").size, qrWidthBound);
float qrSize = std::max(qrMinSize, qrMaxSize);
float totalQrSize = qrSize + qrPadding * 2;
float qrOffsetX = std::max(0.0f, (qrAvailW - totalQrSize) * 0.5f);
ImVec2 qrPanelMin(rx + qrOffsetX, ry);
@@ -917,6 +858,18 @@ void RenderReceiveTab(App* app)
}
// Divider before action buttons
{
float actionBtnH = std::max(S.drawElement("tabs.receive", "action-btn-min-height").size,
S.drawElement("tabs.receive", "action-btn-height").size * vScale);
float footerH = innerGap + actionBtnH + pad;
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
float targetCardH = Layout::mainCardTargetH(formW, vScale);
float footerTopH = targetCardH - footerH;
if (currentCardH < footerTopH) {
ImGui::Dummy(ImVec2(0, footerTopH - currentCardH));
}
}
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
{
ImVec2 divPos = ImGui::GetCursorScreenPos();
@@ -964,6 +917,25 @@ void RenderReceiveTab(App* app)
ImGui::PopStyleColor(3);
}
{
bool hasData = (s_request_amount > 0 || s_request_memo[0]);
if (!firstBtn) ImGui::SameLine(0, btnGap);
firstBtn = false;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "clear-btn-hover-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(hasData ? OnSurfaceMedium() : OnSurfaceDisabled()));
ImGui::BeginDisabled(!hasData);
if (TactileButton(TrId("clear_request", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
s_request_amount = 0.0;
s_request_usd_amount = 0.0;
s_request_memo[0] = '\0';
}
ImGui::EndDisabled();
ImGui::PopStyleColor(3);
}
if (!firstBtn) ImGui::SameLine(0, btnGap);
firstBtn = false;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
@@ -978,20 +950,6 @@ void RenderReceiveTab(App* app)
ImGui::PopStyleVar(); // FrameBorderSize
ImGui::PopStyleColor(4);
if (selected.balance > 0) {
if (!firstBtn) ImGui::SameLine(0, btnGap);
firstBtn = false;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "btn-hover-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()));
if (TactileButton(TrId("send", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
SetSendFromAddress(selected.address);
app->setCurrentPage(NavPage::Send);
}
ImGui::PopStyleColor(3);
}
}
// Bottom padding
@@ -1020,7 +978,7 @@ void RenderReceiveTab(App* app)
// ================================================================
// RECENT RECEIVED
// ================================================================
RenderRecentReceived(dl, selected, state, formW, capFont, app);
RenderRecentReceived(selected, state, formW, capFont, app);
ImGui::EndGroup();
float measuredH = ImGui::GetCursorPosY() - contentStartY;

View File

@@ -971,8 +971,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
// ============================================================================
// Recent Sends section — styled to match transactions list
// ============================================================================
static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
float width, ImFont* capFont, App* app) {
static void RenderRecentSends(const WalletState& state, float width, ImFont* capFont, App* app) {
auto& S = schema::UI();
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_recent_sends"));
@@ -980,139 +979,89 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
ImVec2 avail = ImGui::GetContentRegionAvail();
float hs = Layout::hScale(avail.x);
float glassRound = Layout::glassRounding();
ImFont* body2 = Type().body2();
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
float iconSz = std::max(schema::UI().drawElement("tabs.send", "recent-icon-min-size").size, schema::UI().drawElement("tabs.send", "recent-icon-size").size * hs);
float vs = Layout::vScale(std::max(1.0f, avail.y));
float dp = Layout::dpiScale();
float rowH = std::max(18.0f * dp, S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f) * vs);
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
ImU32 sendCol = Error();
ImU32 greenCol = WithAlpha(Success(), (int)S.drawElement("tabs.send", "recent-green-alpha").size);
float rowPadLeft = Layout::spacingLg();
// Collect matching transactions
std::vector<const TransactionInfo*> sends;
for (const auto& tx : state.transactions) {
if (tx.type != "send") continue;
if (tx.type != "send" && tx.type != "shield") continue;
sends.push_back(&tx);
if (sends.size() >= (size_t)S.drawElement("tabs.send", "max-recent-sends").size) break;
}
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
ImGui::BeginChild("##RecentSendRows", ImVec2(width, listH), false,
ImGuiWindowFlags_NoBackground);
ImDrawList* rowDL = ImGui::GetWindowDrawList();
char buf[64];
if (sends.empty()) {
ImGui::SetCursorPosY(Layout::spacingMd());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("send_no_recent"));
ImGui::EndChild();
return;
}
// Outer glass panel wrapping all rows
float itemSpacingY = ImGui::GetStyle().ItemSpacing.y;
float listH = rowH * (float)sends.size() + itemSpacingY * (float)(sends.size() - 1);
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
float listW = width;
ImVec2 listPanelMax(listPanelMin.x + listW, listPanelMin.y + listH);
GlassPanelSpec glassSpec;
glassSpec.rounding = glassRound;
DrawGlassPanel(dl, listPanelMin, listPanelMax, glassSpec);
// Clip draw commands to panel bounds to prevent overflow
dl->PushClipRect(listPanelMin, listPanelMax, true);
char buf[64];
for (size_t si = 0; si < sends.size(); si++) {
const auto& tx = *sends[si];
ImVec2 rowPos = ImGui::GetCursorScreenPos();
float innerW = listW;
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
float rowY = rowPos.y + rowH * 0.5f;
// Hover glow
bool hovered = material::IsRectHovered(rowPos, rowEnd);
if (hovered) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "row-hover-alpha").size), schema::UI().drawElement("tabs.send", "row-hover-rounding").size);
// Icon
DrawTxIcon(rowDL, "send", rowPos.x + Layout::spacingMd(), rowY, iconSz, sendCol);
// Type label (first line)
float txX = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(txX, rowPos.y + 2.0f * dp), OnSurfaceMedium(), TR("sent_type"));
// Address (second line)
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
std::string addrDisplay = TruncateAddress(tx.address,
(size_t)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());
// Amount (right-aligned, first line)
snprintf(buf, sizeof(buf), "-%.4f %s", std::abs(tx.amount), DRAGONX_TICKER);
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, buf);
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size,
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(amtX, rowPos.y + 2.0f * dp), sendCol, buf);
// Time ago
std::string ago = timeAgo(tx.timestamp);
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str());
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f),
rowPos.y + 2.0f * dp),
OnSurfaceDisabled(), ago.c_str());
// Clickable row — hover highlight + navigate to History
float rowW = ImGui::GetContentRegionAvail().x;
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
if (material::IsRectHovered(rowPos, rowEnd)) {
rowDL->AddRectFilled(rowPos, rowEnd,
IM_COL32(255, 255, 255, 15),
S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) {
app->setCurrentPage(ui::NavPage::History);
}
}
float cx = rowPos.x + rowPadLeft;
float cy = rowPos.y + Layout::spacingMd();
// Icon
DrawTxIcon(dl, tx.type, cx + iconSz, cy + body2->LegacySize * 0.5f, iconSz, sendCol);
// Type label (first line)
float labelX = cx + iconSz * 2.0f + Layout::spacingSm();
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), sendCol, TR("sent_upper"));
// Time (next to type)
std::string ago = timeAgo(tx.timestamp);
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("sent_upper")).x;
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy),
OnSurfaceDisabled(), ago.c_str());
// Address (second line)
std::string addr_display = TruncateAddress(tx.address, (int)S.drawElement("tabs.send", "recent-addr-trunc-len").size);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceMedium(), addr_display.c_str());
// Amount (right-aligned, first line)
snprintf(buf, sizeof(buf), "-%.8f", std::abs(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), sendCol, buf,
S.drawElement("tabs.send", "text-shadow-offset-x").size, S.drawElement("tabs.send", "text-shadow-offset-y").size, IM_COL32(0, 0, 0, (int)S.drawElement("tabs.send", "text-shadow-alpha").size));
// 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
{
const char* statusStr;
ImU32 statusCol;
if (tx.confirmations == 0) {
statusStr = TR("pending"); statusCol = Warning();
} else if (tx.confirmations < (int)S.drawElement("tabs.send", "confirmed-threshold").size) {
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
statusStr = buf; statusCol = Warning();
} else {
statusStr = TR("confirmed"); statusCol = greenCol;
}
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
float statusX = amtX - sSz.x - Layout::spacingXxl();
float minStatusX = cx + innerW * S.drawElement("tabs.send", "status-min-x-ratio").size;
if (statusX < minStatusX) statusX = minStatusX;
ImU32 pillBg = (statusCol & 0x00FFFFFFu) | (static_cast<ImU32>((int)S.drawElement("tabs.send", "status-pill-bg-alpha").size) << 24);
ImVec2 pillMin(statusX - Layout::spacingSm(), cy + body2->LegacySize + (int)S.drawElement("tabs.send", "status-pill-y-offset").size);
ImVec2 pillMax(statusX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs());
dl->AddRectFilled(pillMin, pillMax, pillBg, schema::UI().drawElement("tabs.send", "status-pill-rounding").size);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(statusX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
}
ImGui::Dummy(ImVec2(0, rowH));
// Subtle divider between rows
if (si < sends.size() - 1) {
ImVec2 divStart = ImGui::GetCursorScreenPos();
dl->AddLine(ImVec2(divStart.x + rowPadLeft + iconSz * 2.0f, divStart.y),
ImVec2(divStart.x + innerW - Layout::spacingLg(), divStart.y),
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "row-divider-alpha").size));
}
}
dl->PopClipRect();
ImGui::EndChild();
}
// ============================================================================
@@ -1456,6 +1405,18 @@ void RenderSendTab(App* app)
}
// Divider before action buttons
{
float actionBtnH = std::max(S.drawElement("tabs.send", "action-btn-min-height").size,
S.drawElement("tabs.send", "action-btn-height").size * vScale);
float footerH = innerGap + actionBtnH + pad * vScale;
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
float targetCardH = Layout::mainCardTargetH(formW, vScale);
float footerTopH = targetCardH - footerH;
if (currentCardH < footerTopH) {
ImGui::Dummy(ImVec2(0, footerTopH - currentCardH));
}
}
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
{
ImVec2 divPos = ImGui::GetCursorScreenPos();
@@ -1507,7 +1468,7 @@ void RenderSendTab(App* app)
}
// ---- RECENT SENDS ----
RenderRecentSends(dl, state, formW, capFont, app);
RenderRecentSends(state, formW, capFont, app);
ImGui::EndGroup();
ImGui::EndDisabled(); // sendSyncing guard

View File

@@ -429,6 +429,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (material::StyledButton(TR("test_connection"), ImVec2(0,0), S.resolveFont("button"))) {
if (app->rpc()) {
rpc::RPCClient::TraceScope trace("Settings / Test connection");
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
if (error.empty()) {
std::string version = result.value("version", "unknown");

View File

@@ -185,6 +185,7 @@ void ShieldDialog::render(App* app)
nlohmann::json result;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Send tab / Shield coinbase");
result = rpc->call("z_shieldcoinbase", {from, to, fee, limit});
} catch (const std::exception& e) {
error = e.what();
@@ -215,6 +216,7 @@ void ShieldDialog::render(App* app)
nlohmann::json result;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Send tab / Merge funds");
result = rpc->call("z_mergetoaddress", {addrs, to, fee, 0, limit});
} catch (const std::exception& e) {
error = e.what();
@@ -258,6 +260,7 @@ void ShieldDialog::render(App* app)
nlohmann::json result;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Send tab / Shield operation status");
nlohmann::json ids = nlohmann::json::array();
ids.push_back(opid);
result = rpc->call("z_getoperationstatus", {ids});

View File

@@ -104,7 +104,7 @@ static void DrawTxIcon(ImDrawList* dl, const std::string& type,
} else if (type == "receive") {
icon = ICON_MD_CALL_RECEIVED;
} else if (type == "shield") {
icon = ICON_MD_SHIELD;
icon = ICON_MD_CALL_MADE;
} else {
icon = ICON_MD_CONSTRUCTION;
}
@@ -147,6 +147,8 @@ void RenderTransactionsTab(App* app)
ImU32 greenCol = Success();
ImU32 redCol = Error();
ImU32 goldCol = Warning();
std::string txLoadingText = app->transactionRefreshProgressText();
bool txLoading = !txLoadingText.empty();
// Expanded row index for inline detail
static int s_expanded_row = -1;
@@ -319,6 +321,18 @@ void RenderTransactionsTab(App* app)
ExportTransactionsDialog::show();
}
if (txLoading) {
ImGui::SameLine(0, filterGap);
ImGui::PushFont(Type().iconSmall());
float pulse = 0.55f + 0.45f * std::sin((float)ImGui::GetTime() * 3.0f);
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, pulse), ICON_MD_HOURGLASS_EMPTY);
ImGui::PopFont();
ImGui::SameLine(0, Layout::spacingXs());
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", txLoadingText.c_str(), dotStr[dots]);
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm() + Layout::spacingXs()));
// ================================================================
@@ -415,8 +429,8 @@ void RenderTransactionsTab(App* app)
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 == 1 && dtx.display_type != "send" && dtx.display_type != "shield") continue;
if (type_filter == 2 && dtx.display_type != "receive") continue;
if (type_filter == 3 && dtx.display_type != "generate" && dtx.display_type != "immature" && dtx.display_type != "mined") continue;
}
if (!search_str.empty()) {
@@ -567,7 +581,14 @@ void RenderTransactionsTab(App* app)
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
} else if (state.transactions.empty()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions"));
if (txLoading) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
snprintf(buf, sizeof(buf), "%s%s", txLoadingText.c_str(), dotStr[dots]);
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
} else {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions"));
}
} else if (filtered_indices.empty()) {
ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_matching"));
@@ -596,10 +617,11 @@ void RenderTransactionsTab(App* app)
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
// Determine type info
bool shieldedDisplay = tx.display_type == "shield";
ImU32 iconCol;
const char* typeStr;
if (tx.display_type == "shield") {
iconCol = Primary(); typeStr = TR("shielded_type");
if (shieldedDisplay) {
iconCol = redCol; typeStr = TR("sent_type");
} else if (tx.display_type == "receive") {
iconCol = greenCol; typeStr = TR("recv_type");
} else if (tx.display_type == "send") {
@@ -627,7 +649,8 @@ void RenderTransactionsTab(App* app)
float cy = rowPos.y + Layout::spacingMd();
// Icon
DrawTxIcon(dl, tx.display_type, cx + rowIconSz, cy + body2->LegacySize * 0.5f, rowIconSz, iconCol);
DrawTxIcon(dl, shieldedDisplay ? "send" : tx.display_type,
cx + rowIconSz, cy + body2->LegacySize * 0.5f, rowIconSz, iconCol);
// Type label
float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm();
@@ -687,16 +710,35 @@ void RenderTransactionsTab(App* app)
}
// 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();
const char* shieldedStr = TR("shielded_type");
ImVec2 shieldSz = shieldedDisplay
? capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, shieldedStr)
: ImVec2(0, 0);
float shieldPillW = shieldSz.x + Layout::spacingSm() * 2.0f;
float stackW = shieldedDisplay ? std::max(sSz.x, shieldPillW) : sSz.x;
float statusX = amtX - stackW - Layout::spacingXxl();
float minStatusX = cx + innerW * 0.25f; // don't overlap address
if (statusX < minStatusX) statusX = minStatusX;
float statusTextX = statusX + (stackW - sSz.x) * 0.5f;
if (shieldedDisplay) {
float shieldX = statusX + (stackW - shieldSz.x) * 0.5f;
ImU32 shieldCol = Primary();
ImU32 shieldBg = (shieldCol & 0x00FFFFFFu) | (static_cast<ImU32>(30) << 24);
ImVec2 shieldPillMin(shieldX - Layout::spacingSm(), cy - 1.0f);
ImVec2 shieldPillMax(shieldX + shieldSz.x + Layout::spacingSm(),
shieldPillMin.y + capFont->LegacySize + Layout::spacingXs());
dl->AddRectFilled(shieldPillMin, shieldPillMax, shieldBg,
schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(shieldX, cy), shieldCol, shieldedStr);
}
// 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());
ImVec2 pillMin(statusTextX - Layout::spacingSm(), cy + body2->LegacySize + 1);
ImVec2 pillMax(statusTextX + 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);
ImVec2(statusTextX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
}
// Click to expand/collapse + invisible button for interaction

View File

@@ -91,6 +91,7 @@ void ValidateAddressDialog::render(App* app)
bool valid = false, mine = false;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Receive tab / Validate address");
auto result = rpc->call("validateaddress", {address});
valid = result.value("isvalid", false);
mine = result.value("ismine", false);
@@ -117,6 +118,7 @@ void ValidateAddressDialog::render(App* app)
bool valid = false, mine = false;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Receive tab / Validate address");
auto result = rpc->call("validateaddress", {address});
valid = result.value("isvalid", false);
mine = result.value("ismine", false);