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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user