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

@@ -10,13 +10,76 @@
#include "../util/base64.h"
#include <curl/curl.h>
#include <atomic>
#include <cstdio>
#include <cstring>
#include <utility>
#include "../util/logger.h"
namespace dragonx {
namespace rpc {
namespace {
std::mutex g_trace_mutex;
RPCClient::TraceCallback g_trace_callback;
std::atomic_bool g_trace_enabled{false};
thread_local std::string g_trace_source;
void emitRpcTrace(const std::string& method)
{
if (!g_trace_enabled.load(std::memory_order_relaxed)) return;
RPCClient::TraceCallback callback;
{
std::lock_guard<std::mutex> lock(g_trace_mutex);
callback = g_trace_callback;
}
if (!callback) return;
std::string source = g_trace_source.empty() ? std::string("App") : g_trace_source;
callback(source, method);
}
} // namespace
RPCClient::TraceScope::TraceScope(std::string source)
: previous_(RPCClient::currentTraceSource())
{
RPCClient::setTraceSource(std::move(source));
}
RPCClient::TraceScope::~TraceScope()
{
RPCClient::setTraceSource(std::move(previous_));
}
void RPCClient::setTraceCallback(TraceCallback callback)
{
std::lock_guard<std::mutex> lock(g_trace_mutex);
g_trace_callback = std::move(callback);
}
void RPCClient::setTraceEnabled(bool enabled)
{
g_trace_enabled.store(enabled, std::memory_order_relaxed);
}
bool RPCClient::isTraceEnabled()
{
return g_trace_enabled.load(std::memory_order_relaxed);
}
std::string RPCClient::currentTraceSource()
{
return g_trace_source;
}
void RPCClient::setTraceSource(std::string source)
{
g_trace_source = std::move(source);
}
// Callback for libcurl to write response data
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
size_t totalSize = size * nmemb;
@@ -71,6 +134,7 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
host_ = host;
port_ = port;
last_connect_info_ = json();
// Create Basic auth header with proper base64 encoding
std::string credentials = user + ":" + password;
@@ -116,6 +180,7 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
warming_up_ = false;
warmup_status_.clear();
last_connect_error_.clear();
last_connect_info_ = result;
DEBUG_LOGF("Connected to dragonxd v%d\n", result["version"].get<int>());
return true;
}
@@ -146,15 +211,23 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
connected_ = false;
warming_up_ = false;
warmup_status_.clear();
last_connect_info_ = json();
return false;
}
json RPCClient::getLastConnectInfo() const
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
return last_connect_info_;
}
void RPCClient::disconnect()
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
connected_ = false;
warming_up_ = false;
warmup_status_.clear();
last_connect_info_ = json();
if (impl_->curl) {
curl_easy_cleanup(impl_->curl);
impl_->curl = nullptr;
@@ -182,6 +255,8 @@ json RPCClient::call(const std::string& method, const json& params)
throw std::runtime_error("Not connected");
}
emitRpcTrace(method);
json payload = makePayload(method, params);
std::string body = payload.dump();
std::string response_data;
@@ -235,6 +310,8 @@ json RPCClient::call(const std::string& method, const json& params, long timeout
throw std::runtime_error("Not connected");
}
emitRpcTrace(method);
// Temporarily override timeout
long prevTimeout = 30L;
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec);
@@ -294,6 +371,8 @@ std::string RPCClient::callRaw(const std::string& method, const json& params)
throw std::runtime_error("Not connected");
}
emitRpcTrace(method);
json payload = makePayload(method, params);
std::string body = payload.dump();
std::string response_data;