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:
2026-05-05 03:22:14 -05:00
parent 948ef419ac
commit 975743f754
43 changed files with 3732 additions and 702 deletions

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);