feat: RPC caching, background decrypt import, fast-lane peers, mining fix
RPC client: - Add call() overload with per-call timeout parameter - z_exportwallet uses 300s, z_importwallet uses 1200s timeout Decrypt wallet (app_security.cpp, app.cpp): - Show per-step and overall elapsed timers during decrypt flow - Reduce dialog to 5 steps; close before key import begins - Run z_importwallet on detached background thread - Add pulsing "Importing keys..." status bar indicator - Report success/failure via notifications instead of dialog RPC caching (app_network.cpp, app.h): - Cache z_viewtransaction results in viewtx_cache_ across refresh cycles - Skip RPC calls for already-cached txids (biggest perf win) - Build confirmed_tx_cache_ for deeply-confirmed transactions - Clear all caches on disconnect - Remove unused refreshTransactions() dead code Peers (app_network.cpp, peers_tab.cpp): - Route refreshPeerInfo() through fast_worker_ to avoid head-of-line blocking - Replace footer "Refresh Peers" button with ICON_MD_REFRESH in toggle header - Refresh button triggers both peer list and full blockchain data refresh Mining (mining_tab.cpp): - Allow pool mining toggle when blockchain is not synced - Pool mining only needs xmrig, not local daemon sync
This commit is contained in:
79
src/app.cpp
79
src/app.cpp
@@ -68,6 +68,7 @@
|
|||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include <algorithm>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -273,6 +274,8 @@ void App::update()
|
|||||||
refresh_timer_ += io.DeltaTime;
|
refresh_timer_ += io.DeltaTime;
|
||||||
price_timer_ += io.DeltaTime;
|
price_timer_ += io.DeltaTime;
|
||||||
fast_refresh_timer_ += io.DeltaTime;
|
fast_refresh_timer_ += io.DeltaTime;
|
||||||
|
tx_age_timer_ += io.DeltaTime;
|
||||||
|
opid_poll_timer_ += io.DeltaTime;
|
||||||
|
|
||||||
// Fast refresh (mining stats + daemon memory) every second
|
// Fast refresh (mining stats + daemon memory) every second
|
||||||
// Skip when wallet is locked — no need to poll, and queued tasks
|
// Skip when wallet is locked — no need to poll, and queued tasks
|
||||||
@@ -443,6 +446,64 @@ void App::update()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Poll pending z_sendmany operations for completion
|
||||||
|
if (opid_poll_timer_ >= OPID_POLL_INTERVAL && !pending_opids_.empty()
|
||||||
|
&& state_.connected && fast_worker_) {
|
||||||
|
opid_poll_timer_ = 0.0f;
|
||||||
|
auto opids = pending_opids_; // copy for worker thread
|
||||||
|
fast_worker_->post([this, opids]() -> rpc::RPCWorker::MainCb {
|
||||||
|
auto* rpc = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
||||||
|
if (!rpc) return [](){};
|
||||||
|
json ids = json::array();
|
||||||
|
for (const auto& id : opids) ids.push_back(id);
|
||||||
|
json result;
|
||||||
|
try {
|
||||||
|
result = rpc->call("z_getoperationstatus", {ids});
|
||||||
|
} catch (...) {
|
||||||
|
return [](){};
|
||||||
|
}
|
||||||
|
// Collect completed/failed opids
|
||||||
|
std::vector<std::string> done;
|
||||||
|
bool anySuccess = false;
|
||||||
|
for (const auto& op : result) {
|
||||||
|
std::string status = op.value("status", "");
|
||||||
|
std::string opid = op.value("id", "");
|
||||||
|
if (status == "success") {
|
||||||
|
done.push_back(opid);
|
||||||
|
anySuccess = true;
|
||||||
|
} else if (status == "failed") {
|
||||||
|
done.push_back(opid);
|
||||||
|
std::string msg = "Transaction failed";
|
||||||
|
if (op.contains("error") && op["error"].contains("message"))
|
||||||
|
msg = op["error"]["message"].get<std::string>();
|
||||||
|
// Capture for main thread
|
||||||
|
return [this, done, msg]() {
|
||||||
|
ui::Notifications::instance().error(msg);
|
||||||
|
for (const auto& id : done) {
|
||||||
|
pending_opids_.erase(
|
||||||
|
std::remove(pending_opids_.begin(), pending_opids_.end(), id),
|
||||||
|
pending_opids_.end());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [this, done, anySuccess]() {
|
||||||
|
for (const auto& id : done) {
|
||||||
|
pending_opids_.erase(
|
||||||
|
std::remove(pending_opids_.begin(), pending_opids_.end(), id),
|
||||||
|
pending_opids_.end());
|
||||||
|
}
|
||||||
|
if (anySuccess) {
|
||||||
|
// Transaction confirmed by daemon — force immediate data refresh
|
||||||
|
transactions_dirty_ = true;
|
||||||
|
addresses_dirty_ = true;
|
||||||
|
last_tx_block_height_ = -1;
|
||||||
|
refresh_timer_ = REFRESH_INTERVAL;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Regular refresh every 5 seconds
|
// Regular refresh every 5 seconds
|
||||||
// Skip when wallet is locked — same reason as above.
|
// Skip when wallet is locked — same reason as above.
|
||||||
if (refresh_timer_ >= REFRESH_INTERVAL) {
|
if (refresh_timer_ >= REFRESH_INTERVAL) {
|
||||||
@@ -1288,6 +1349,21 @@ void App::renderStatusBar()
|
|||||||
state_.mining.localHashrate);
|
state_.mining.localHashrate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decrypt-import background task indicator
|
||||||
|
if (decrypt_import_active_) {
|
||||||
|
ImGui::SameLine(0, sbSectionGap);
|
||||||
|
ImGui::TextDisabled("|");
|
||||||
|
ImGui::SameLine(0, sbSeparatorGap);
|
||||||
|
ImGui::PushFont(ui::material::Type().iconSmall());
|
||||||
|
float pulse = 0.6f + 0.4f * sinf((float)ImGui::GetTime() * 3.0f);
|
||||||
|
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, pulse), ICON_MD_LOCK_OPEN);
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SameLine(0, sbIconTextGap);
|
||||||
|
int dots = (int)(ImGui::GetTime() * 2.0f) % 4;
|
||||||
|
const char* dotStr = (dots == 0) ? "." : (dots == 1) ? ".." : (dots == 2) ? "..." : "";
|
||||||
|
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Importing keys%s", dotStr);
|
||||||
|
}
|
||||||
|
|
||||||
// Right side: connection status message (if any) + version always at far right
|
// Right side: connection status message (if any) + version always at far right
|
||||||
float rightStart = ImGui::GetWindowWidth() - sbRightContentOff;
|
float rightStart = ImGui::GetWindowWidth() - sbRightContentOff;
|
||||||
if (!connection_status_.empty() && connection_status_ != "Connected") {
|
if (!connection_status_.empty() && connection_status_ != "Connected") {
|
||||||
@@ -1632,6 +1708,9 @@ void App::renderAntivirusHelpDialog()
|
|||||||
void App::refreshNow()
|
void App::refreshNow()
|
||||||
{
|
{
|
||||||
refresh_timer_ = REFRESH_INTERVAL; // Trigger immediate refresh
|
refresh_timer_ = REFRESH_INTERVAL; // Trigger immediate refresh
|
||||||
|
transactions_dirty_ = true; // Force transaction list update
|
||||||
|
addresses_dirty_ = true; // Force address/balance update
|
||||||
|
last_tx_block_height_ = -1; // Reset tx cache
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::handlePaymentURI(const std::string& uri)
|
void App::handlePaymentURI(const std::string& uri)
|
||||||
|
|||||||
35
src/app.h
35
src/app.h
@@ -10,6 +10,8 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
#include "data/wallet_state.h"
|
#include "data/wallet_state.h"
|
||||||
#include "rpc/connection.h"
|
#include "rpc/connection.h"
|
||||||
#include "ui/sidebar.h"
|
#include "ui/sidebar.h"
|
||||||
@@ -443,11 +445,40 @@ private:
|
|||||||
|
|
||||||
// P4: Incremental transaction cache
|
// P4: Incremental transaction cache
|
||||||
int last_tx_block_height_ = -1; // block height at last full tx fetch
|
int last_tx_block_height_ = -1; // block height at last full tx fetch
|
||||||
|
float tx_age_timer_ = 0.0f; // seconds since last tx fetch
|
||||||
|
static constexpr float TX_MAX_AGE = 15.0f; // force tx refresh every N seconds even without new blocks
|
||||||
|
static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh
|
||||||
|
|
||||||
|
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for
|
||||||
|
// txids we've already enriched. Keyed by txid.
|
||||||
|
struct ViewTxCacheEntry {
|
||||||
|
std::string from_address; // first spend address
|
||||||
|
struct Output {
|
||||||
|
std::string address;
|
||||||
|
double value = 0.0;
|
||||||
|
std::string memo;
|
||||||
|
};
|
||||||
|
std::vector<Output> outgoing_outputs;
|
||||||
|
};
|
||||||
|
std::unordered_map<std::string, ViewTxCacheEntry> viewtx_cache_;
|
||||||
|
|
||||||
|
// P4c: Confirmed transaction cache — deeply-confirmed txns (>= 10 confs)
|
||||||
|
// are accumulated here and reused across refresh cycles. Only
|
||||||
|
// recent/unconfirmed txns are re-fetched from the daemon each time.
|
||||||
|
std::vector<TransactionInfo> confirmed_tx_cache_;
|
||||||
|
std::unordered_set<std::string> confirmed_tx_ids_; // fast lookup
|
||||||
|
int confirmed_cache_block_ = -1; // block height when cache was last built
|
||||||
|
|
||||||
// Dirty flags for demand-driven refresh
|
// Dirty flags for demand-driven refresh
|
||||||
bool addresses_dirty_ = true; // true → refreshAddresses() will run
|
bool addresses_dirty_ = true; // true → refreshAddresses() will run
|
||||||
|
bool transactions_dirty_ = false; // true → force tx refresh regardless of block height
|
||||||
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
|
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
|
||||||
|
|
||||||
|
// Pending z_sendmany operation tracking
|
||||||
|
std::vector<std::string> pending_opids_; // opids to poll for completion
|
||||||
|
float opid_poll_timer_ = 0.0f;
|
||||||
|
static constexpr float OPID_POLL_INTERVAL = 2.0f;
|
||||||
|
|
||||||
// First-run wizard state
|
// First-run wizard state
|
||||||
WizardPhase wizard_phase_ = WizardPhase::None;
|
WizardPhase wizard_phase_ = WizardPhase::None;
|
||||||
std::unique_ptr<util::Bootstrap> bootstrap_;
|
std::unique_ptr<util::Bootstrap> bootstrap_;
|
||||||
@@ -512,6 +543,9 @@ private:
|
|||||||
char decrypt_pass_buf_[256] = {};
|
char decrypt_pass_buf_[256] = {};
|
||||||
std::string decrypt_status_;
|
std::string decrypt_status_;
|
||||||
bool decrypt_in_progress_ = false;
|
bool decrypt_in_progress_ = false;
|
||||||
|
std::chrono::steady_clock::time_point decrypt_step_start_time_{};
|
||||||
|
std::chrono::steady_clock::time_point decrypt_overall_start_time_{};
|
||||||
|
std::atomic<bool> decrypt_import_active_{false}; // background z_importwallet running
|
||||||
|
|
||||||
// Wizard PIN setup state
|
// Wizard PIN setup state
|
||||||
char wizard_pin_buf_[16] = {};
|
char wizard_pin_buf_[16] = {};
|
||||||
@@ -544,7 +578,6 @@ private:
|
|||||||
void refreshData();
|
void refreshData();
|
||||||
void refreshBalance();
|
void refreshBalance();
|
||||||
void refreshAddresses();
|
void refreshAddresses();
|
||||||
void refreshTransactions();
|
|
||||||
void refreshPrice();
|
void refreshPrice();
|
||||||
void refreshWalletEncryptionState();
|
void refreshWalletEncryptionState();
|
||||||
void checkAutoLock();
|
void checkAutoLock();
|
||||||
|
|||||||
@@ -229,6 +229,13 @@ void App::onDisconnected(const std::string& reason)
|
|||||||
state_.clear();
|
state_.clear();
|
||||||
connection_status_ = reason;
|
connection_status_ = reason;
|
||||||
|
|
||||||
|
// Clear RPC result caches
|
||||||
|
viewtx_cache_.clear();
|
||||||
|
confirmed_tx_cache_.clear();
|
||||||
|
confirmed_tx_ids_.clear();
|
||||||
|
confirmed_cache_block_ = -1;
|
||||||
|
last_tx_block_height_ = -1;
|
||||||
|
|
||||||
// Tear down the fast-lane connection
|
// Tear down the fast-lane connection
|
||||||
if (fast_worker_) {
|
if (fast_worker_) {
|
||||||
fast_worker_->stop();
|
fast_worker_->stop();
|
||||||
@@ -255,11 +262,19 @@ void App::refreshData()
|
|||||||
const bool doEncrypt = !encryption_state_prefetched_;
|
const bool doEncrypt = !encryption_state_prefetched_;
|
||||||
if (encryption_state_prefetched_) encryption_state_prefetched_ = false;
|
if (encryption_state_prefetched_) encryption_state_prefetched_ = false;
|
||||||
|
|
||||||
// P4a: Skip transactions if no new blocks since last full fetch
|
// P4a: Refresh transactions when new blocks arrive, when explicitly
|
||||||
|
// dirtied (e.g. after a send), or when the max-age timer expires
|
||||||
|
// (catches mempool changes for incoming unconfirmed transactions).
|
||||||
const int currentBlocks = state_.sync.blocks;
|
const int currentBlocks = state_.sync.blocks;
|
||||||
const bool doTransactions = (last_tx_block_height_ < 0
|
const bool doTransactions = (last_tx_block_height_ < 0
|
||||||
|| currentBlocks != last_tx_block_height_
|
|| currentBlocks != last_tx_block_height_
|
||||||
|| state_.transactions.empty());
|
|| state_.transactions.empty()
|
||||||
|
|| transactions_dirty_
|
||||||
|
|| tx_age_timer_ >= TX_MAX_AGE);
|
||||||
|
if (doTransactions) {
|
||||||
|
transactions_dirty_ = false;
|
||||||
|
tx_age_timer_ = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
// Snapshot z-addresses for transaction fetch (needed on worker thread)
|
// Snapshot z-addresses for transaction fetch (needed on worker thread)
|
||||||
std::vector<std::string> txZAddrs;
|
std::vector<std::string> txZAddrs;
|
||||||
@@ -270,8 +285,15 @@ void App::refreshData()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// P4b: Collect txids that are fully enriched (skip re-enrichment)
|
// P4b: Collect txids that are fully enriched (skip re-enrichment)
|
||||||
std::set<std::string> fullyEnriched;
|
// Include all txids already in the viewtx cache — these never need
|
||||||
|
// another z_viewtransaction RPC call.
|
||||||
|
std::unordered_set<std::string> fullyEnriched;
|
||||||
if (doTransactions) {
|
if (doTransactions) {
|
||||||
|
// Txids we already have z_viewtransaction results for
|
||||||
|
for (const auto& [txid, _] : viewtx_cache_) {
|
||||||
|
fullyEnriched.insert(txid);
|
||||||
|
}
|
||||||
|
// Txids that are deeply confirmed and timestamped
|
||||||
for (const auto& tx : state_.transactions) {
|
for (const auto& tx : state_.transactions) {
|
||||||
if (tx.confirmations > 6 && tx.timestamp != 0) {
|
if (tx.confirmations > 6 && tx.timestamp != 0) {
|
||||||
fullyEnriched.insert(tx.txid);
|
fullyEnriched.insert(tx.txid);
|
||||||
@@ -279,11 +301,26 @@ void App::refreshData()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P4c: Snapshot the confirmed transaction cache for the worker thread.
|
||||||
|
// These are deeply-confirmed txns we can reuse without re-fetching.
|
||||||
|
std::vector<TransactionInfo> cachedConfirmedTxns;
|
||||||
|
std::unordered_set<std::string> cachedConfirmedIds;
|
||||||
|
if (doTransactions && !confirmed_tx_cache_.empty()) {
|
||||||
|
cachedConfirmedTxns = confirmed_tx_cache_;
|
||||||
|
cachedConfirmedIds = confirmed_tx_ids_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot viewtx cache for the worker thread
|
||||||
|
auto viewtxCacheSnap = viewtx_cache_;
|
||||||
|
|
||||||
// Single consolidated worker task — all RPC calls happen back-to-back
|
// Single consolidated worker task — all RPC calls happen back-to-back
|
||||||
// on a single thread with no inter-task queue overhead.
|
// on a single thread with no inter-task queue overhead.
|
||||||
worker_->post([this, doAddresses, doPeers, doEncrypt, doTransactions,
|
worker_->post([this, doAddresses, doPeers, doEncrypt, doTransactions,
|
||||||
currentBlocks, txZAddrs = std::move(txZAddrs),
|
currentBlocks, txZAddrs = std::move(txZAddrs),
|
||||||
fullyEnriched = std::move(fullyEnriched)]() -> rpc::RPCWorker::MainCb {
|
fullyEnriched = std::move(fullyEnriched),
|
||||||
|
cachedConfirmedTxns = std::move(cachedConfirmedTxns),
|
||||||
|
cachedConfirmedIds = std::move(cachedConfirmedIds),
|
||||||
|
viewtxCacheSnap = std::move(viewtxCacheSnap)]() -> rpc::RPCWorker::MainCb {
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// Phase 1: Balance + blockchain info
|
// Phase 1: Balance + blockchain info
|
||||||
// ================================================================
|
// ================================================================
|
||||||
@@ -376,6 +413,7 @@ void App::refreshData()
|
|||||||
// Phase 3: Transactions (only when new blocks)
|
// Phase 3: Transactions (only when new blocks)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
std::vector<TransactionInfo> txns;
|
std::vector<TransactionInfo> txns;
|
||||||
|
std::unordered_map<std::string, ViewTxCacheEntry> newViewTxEntries;
|
||||||
bool txOk = false;
|
bool txOk = false;
|
||||||
if (doTransactions) {
|
if (doTransactions) {
|
||||||
txOk = true;
|
txOk = true;
|
||||||
@@ -434,65 +472,103 @@ void App::refreshData()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3c: detect shielded sends via z_viewtransaction
|
// Phase 3c: detect shielded sends via z_viewtransaction
|
||||||
|
// Check the in-memory viewtx cache first; only make RPC calls
|
||||||
|
// for txids we haven't seen before.
|
||||||
|
int viewTxCount = 0;
|
||||||
|
|
||||||
|
auto applyViewTxEntry = [&](const std::string& txid,
|
||||||
|
const ViewTxCacheEntry& entry) {
|
||||||
|
for (const auto& out : entry.outgoing_outputs) {
|
||||||
|
bool alreadyTracked = false;
|
||||||
|
for (const auto& existing : txns) {
|
||||||
|
if (existing.txid == txid && existing.type == "send"
|
||||||
|
&& std::abs(existing.amount + out.value) < 0.00000001) {
|
||||||
|
alreadyTracked = true; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alreadyTracked) continue;
|
||||||
|
TransactionInfo info;
|
||||||
|
info.txid = txid;
|
||||||
|
info.type = "send";
|
||||||
|
info.address = out.address;
|
||||||
|
info.amount = -out.value;
|
||||||
|
info.memo = out.memo;
|
||||||
|
info.from_address = entry.from_address;
|
||||||
|
// Copy confirmations/timestamp from an existing entry for this txid
|
||||||
|
for (const auto& existing : txns) {
|
||||||
|
if (existing.txid == txid) {
|
||||||
|
info.confirmations = existing.confirmations;
|
||||||
|
info.timestamp = existing.timestamp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
txns.push_back(info);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for (const std::string& txid : knownTxids) {
|
for (const std::string& txid : knownTxids) {
|
||||||
if (fullyEnriched.count(txid)) continue;
|
if (fullyEnriched.count(txid)) continue;
|
||||||
|
|
||||||
|
// Check viewtx cache first — avoid RPC call
|
||||||
|
auto cit = viewtxCacheSnap.find(txid);
|
||||||
|
if (cit != viewtxCacheSnap.end()) {
|
||||||
|
applyViewTxEntry(txid, cit->second);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewTxCount >= MAX_VIEWTX_PER_CYCLE) break;
|
||||||
|
++viewTxCount;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
json vtx = rpc_->call("z_viewtransaction", json::array({txid}));
|
json vtx = rpc_->call("z_viewtransaction", json::array({txid}));
|
||||||
if (vtx.is_null() || !vtx.is_object()) continue;
|
if (vtx.is_null() || !vtx.is_object()) continue;
|
||||||
|
|
||||||
|
// Build cache entry from RPC result
|
||||||
|
ViewTxCacheEntry entry;
|
||||||
|
if (vtx.contains("spends") && vtx["spends"].is_array()) {
|
||||||
|
for (const auto& spend : vtx["spends"]) {
|
||||||
|
if (spend.contains("address")) {
|
||||||
|
entry.from_address = spend["address"].get<std::string>();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (vtx.contains("outputs") && vtx["outputs"].is_array()) {
|
if (vtx.contains("outputs") && vtx["outputs"].is_array()) {
|
||||||
for (const auto& output : vtx["outputs"]) {
|
for (const auto& output : vtx["outputs"]) {
|
||||||
bool outgoing = false;
|
bool outgoing = false;
|
||||||
if (output.contains("outgoing"))
|
if (output.contains("outgoing"))
|
||||||
outgoing = output["outgoing"].get<bool>();
|
outgoing = output["outgoing"].get<bool>();
|
||||||
if (!outgoing) continue;
|
if (!outgoing) continue;
|
||||||
std::string destAddr;
|
ViewTxCacheEntry::Output out;
|
||||||
if (output.contains("address"))
|
if (output.contains("address"))
|
||||||
destAddr = output["address"].get<std::string>();
|
out.address = output["address"].get<std::string>();
|
||||||
double value = 0.0;
|
|
||||||
if (output.contains("value"))
|
if (output.contains("value"))
|
||||||
value = output["value"].get<double>();
|
out.value = output["value"].get<double>();
|
||||||
bool alreadyTracked = false;
|
|
||||||
for (const auto& existing : txns) {
|
|
||||||
if (existing.txid == txid && existing.type == "send"
|
|
||||||
&& std::abs(existing.amount + value) < 0.00000001) {
|
|
||||||
alreadyTracked = true; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (alreadyTracked) continue;
|
|
||||||
TransactionInfo info;
|
|
||||||
info.txid = txid;
|
|
||||||
info.type = "send";
|
|
||||||
info.address = destAddr;
|
|
||||||
info.amount = -value;
|
|
||||||
if (output.contains("memoStr"))
|
if (output.contains("memoStr"))
|
||||||
info.memo = output["memoStr"].get<std::string>();
|
out.memo = output["memoStr"].get<std::string>();
|
||||||
for (const auto& existing : txns) {
|
entry.outgoing_outputs.push_back(std::move(out));
|
||||||
if (existing.txid == txid) {
|
|
||||||
info.confirmations = existing.confirmations;
|
|
||||||
info.timestamp = existing.timestamp;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (info.timestamp == 0) {
|
|
||||||
try {
|
|
||||||
json rawtx = rpc_->call("gettransaction", json::array({txid}));
|
|
||||||
if (!rawtx.is_null() && rawtx.contains("time"))
|
|
||||||
info.timestamp = rawtx["time"].get<int64_t>();
|
|
||||||
if (!rawtx.is_null() && rawtx.contains("confirmations"))
|
|
||||||
info.confirmations = rawtx["confirmations"].get<int>();
|
|
||||||
} catch (...) {}
|
|
||||||
}
|
|
||||||
if (vtx.contains("spends") && vtx["spends"].is_array()) {
|
|
||||||
for (const auto& spend : vtx["spends"]) {
|
|
||||||
if (spend.contains("address")) {
|
|
||||||
info.from_address = spend["address"].get<std::string>();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
txns.push_back(info);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply to current txns list
|
||||||
|
applyViewTxEntry(txid, entry);
|
||||||
|
|
||||||
|
// Fetch timestamp if missing for any new txns we just added
|
||||||
|
for (auto& info : txns) {
|
||||||
|
if (info.txid == txid && info.timestamp == 0) {
|
||||||
|
try {
|
||||||
|
json rawtx = rpc_->call("gettransaction", json::array({txid}));
|
||||||
|
if (!rawtx.is_null() && rawtx.contains("time"))
|
||||||
|
info.timestamp = rawtx["time"].get<int64_t>();
|
||||||
|
if (!rawtx.is_null() && rawtx.contains("confirmations"))
|
||||||
|
info.confirmations = rawtx["confirmations"].get<int>();
|
||||||
|
} catch (...) {}
|
||||||
|
break; // only need to fetch once per txid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store for cache update on main thread
|
||||||
|
newViewTxEntries[txid] = std::move(entry);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
(void)e; // z_viewtransaction may not be available for all txids
|
(void)e; // z_viewtransaction may not be available for all txids
|
||||||
}
|
}
|
||||||
@@ -568,6 +644,7 @@ void App::refreshData()
|
|||||||
return [this, totalBal, blockInfo, balOk, blockOk,
|
return [this, totalBal, blockInfo, balOk, blockOk,
|
||||||
zAddrs = std::move(zAddrs), tAddrs = std::move(tAddrs), addrOk,
|
zAddrs = std::move(zAddrs), tAddrs = std::move(tAddrs), addrOk,
|
||||||
txns = std::move(txns), txOk, currentBlocks,
|
txns = std::move(txns), txOk, currentBlocks,
|
||||||
|
newViewTxEntries = std::move(newViewTxEntries),
|
||||||
peers = std::move(peers), bannedPeers = std::move(bannedPeers), peerOk,
|
peers = std::move(peers), bannedPeers = std::move(bannedPeers), peerOk,
|
||||||
walletInfo, encryptOk]() {
|
walletInfo, encryptOk]() {
|
||||||
// --- Balance ---
|
// --- Balance ---
|
||||||
@@ -641,6 +718,24 @@ void App::refreshData()
|
|||||||
state_.transactions = std::move(txns);
|
state_.transactions = std::move(txns);
|
||||||
state_.last_tx_update = std::time(nullptr);
|
state_.last_tx_update = std::time(nullptr);
|
||||||
last_tx_block_height_ = currentBlocks;
|
last_tx_block_height_ = currentBlocks;
|
||||||
|
|
||||||
|
// Merge new z_viewtransaction results into the persistent cache
|
||||||
|
for (auto& [txid, entry] : newViewTxEntries) {
|
||||||
|
viewtx_cache_[txid] = std::move(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild confirmed transaction cache: txns with >= 10
|
||||||
|
// confirmations are stable and won't change, so we keep
|
||||||
|
// them for future cycles.
|
||||||
|
confirmed_tx_cache_.clear();
|
||||||
|
confirmed_tx_ids_.clear();
|
||||||
|
for (const auto& tx : state_.transactions) {
|
||||||
|
if (tx.confirmations >= 10 && tx.timestamp != 0) {
|
||||||
|
confirmed_tx_ids_.insert(tx.txid);
|
||||||
|
confirmed_tx_cache_.push_back(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
confirmed_cache_block_ = currentBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Peers ---
|
// --- Peers ---
|
||||||
@@ -849,181 +944,6 @@ void App::refreshAddresses()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::refreshTransactions()
|
|
||||||
{
|
|
||||||
if (!worker_ || !rpc_) return;
|
|
||||||
|
|
||||||
// P4a: Skip if no new blocks since last full fetch
|
|
||||||
int currentBlocks = state_.sync.blocks;
|
|
||||||
bool fullRefresh = (last_tx_block_height_ < 0 || currentBlocks != last_tx_block_height_
|
|
||||||
|| state_.transactions.empty());
|
|
||||||
if (!fullRefresh) return;
|
|
||||||
|
|
||||||
// Capture the z-addresses list for the worker thread
|
|
||||||
std::vector<std::string> zAddrs;
|
|
||||||
for (const auto& za : state_.z_addresses) {
|
|
||||||
if (!za.address.empty()) zAddrs.push_back(za.address);
|
|
||||||
}
|
|
||||||
|
|
||||||
worker_->post([this, zAddrs = std::move(zAddrs), currentBlocks]() -> rpc::RPCWorker::MainCb {
|
|
||||||
// --- Worker thread: all blocking RPC calls happen here ---
|
|
||||||
std::vector<TransactionInfo> txns;
|
|
||||||
std::set<std::string> knownTxids;
|
|
||||||
|
|
||||||
// P4b: Collect txids that are fully enriched (skip re-enrichment)
|
|
||||||
std::set<std::string> fullyEnriched;
|
|
||||||
for (const auto& tx : state_.transactions) {
|
|
||||||
if (tx.confirmations > 6 && tx.timestamp != 0) {
|
|
||||||
fullyEnriched.insert(tx.txid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Phase 1: transparent transactions from listtransactions ----
|
|
||||||
try {
|
|
||||||
json result = rpc_->call("listtransactions", json::array({"", 9999}));
|
|
||||||
for (const auto& tx : result) {
|
|
||||||
TransactionInfo info;
|
|
||||||
if (tx.contains("txid")) info.txid = tx["txid"].get<std::string>();
|
|
||||||
if (tx.contains("category")) info.type = tx["category"].get<std::string>();
|
|
||||||
if (tx.contains("amount")) info.amount = tx["amount"].get<double>();
|
|
||||||
if (tx.contains("time")) info.timestamp = tx["time"].get<int64_t>();
|
|
||||||
if (tx.contains("confirmations")) info.confirmations = tx["confirmations"].get<int>();
|
|
||||||
if (tx.contains("address")) info.address = tx["address"].get<std::string>();
|
|
||||||
knownTxids.insert(info.txid);
|
|
||||||
txns.push_back(info);
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
DEBUG_LOGF("listtransactions error: %s\n", e.what());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Phase 2: shielded receives via z_listreceivedbyaddress ----
|
|
||||||
for (const auto& addr : zAddrs) {
|
|
||||||
try {
|
|
||||||
json zresult = rpc_->call("z_listreceivedbyaddress", json::array({addr, 0}));
|
|
||||||
if (zresult.is_null() || !zresult.is_array()) continue;
|
|
||||||
|
|
||||||
for (const auto& note : zresult) {
|
|
||||||
std::string txid;
|
|
||||||
if (note.contains("txid")) txid = note["txid"].get<std::string>();
|
|
||||||
if (txid.empty()) continue;
|
|
||||||
if (note.contains("change") && note["change"].get<bool>()) continue;
|
|
||||||
|
|
||||||
bool dominated = false;
|
|
||||||
for (const auto& existing : txns) {
|
|
||||||
if (existing.txid == txid && existing.type == "receive") {
|
|
||||||
dominated = true; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dominated) continue;
|
|
||||||
|
|
||||||
TransactionInfo info;
|
|
||||||
info.txid = txid;
|
|
||||||
info.type = "receive";
|
|
||||||
info.address = addr;
|
|
||||||
if (note.contains("amount")) info.amount = note["amount"].get<double>();
|
|
||||||
if (note.contains("confirmations")) info.confirmations = note["confirmations"].get<int>();
|
|
||||||
if (note.contains("time")) info.timestamp = note["time"].get<int64_t>();
|
|
||||||
if (note.contains("memoStr")) info.memo = note["memoStr"].get<std::string>();
|
|
||||||
knownTxids.insert(txid);
|
|
||||||
txns.push_back(info);
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
DEBUG_LOGF("z_listreceivedbyaddress error for %s: %s\n",
|
|
||||||
addr.substr(0, 12).c_str(), e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Phase 3: detect shielded sends via z_viewtransaction ----
|
|
||||||
// P4d: Only check new/unconfirmed txids
|
|
||||||
for (const std::string& txid : knownTxids) {
|
|
||||||
if (fullyEnriched.count(txid)) continue; // P4b: skip already-enriched
|
|
||||||
|
|
||||||
try {
|
|
||||||
json vtx = rpc_->call("z_viewtransaction", json::array({txid}));
|
|
||||||
if (vtx.is_null() || !vtx.is_object()) continue;
|
|
||||||
|
|
||||||
if (vtx.contains("outputs") && vtx["outputs"].is_array()) {
|
|
||||||
for (const auto& output : vtx["outputs"]) {
|
|
||||||
bool outgoing = false;
|
|
||||||
if (output.contains("outgoing"))
|
|
||||||
outgoing = output["outgoing"].get<bool>();
|
|
||||||
if (!outgoing) continue;
|
|
||||||
|
|
||||||
std::string destAddr;
|
|
||||||
if (output.contains("address"))
|
|
||||||
destAddr = output["address"].get<std::string>();
|
|
||||||
double value = 0.0;
|
|
||||||
if (output.contains("value"))
|
|
||||||
value = output["value"].get<double>();
|
|
||||||
|
|
||||||
bool alreadyTracked = false;
|
|
||||||
for (const auto& existing : txns) {
|
|
||||||
if (existing.txid == txid && existing.type == "send"
|
|
||||||
&& std::abs(existing.amount + value) < 0.00000001) {
|
|
||||||
alreadyTracked = true; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (alreadyTracked) continue;
|
|
||||||
|
|
||||||
TransactionInfo info;
|
|
||||||
info.txid = txid;
|
|
||||||
info.type = "send";
|
|
||||||
info.address = destAddr;
|
|
||||||
info.amount = -value;
|
|
||||||
if (output.contains("memoStr"))
|
|
||||||
info.memo = output["memoStr"].get<std::string>();
|
|
||||||
|
|
||||||
// Get confirmations/time from existing entry
|
|
||||||
for (const auto& existing : txns) {
|
|
||||||
if (existing.txid == txid) {
|
|
||||||
info.confirmations = existing.confirmations;
|
|
||||||
info.timestamp = existing.timestamp;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.timestamp == 0) {
|
|
||||||
try {
|
|
||||||
json rawtx = rpc_->call("gettransaction", json::array({txid}));
|
|
||||||
if (!rawtx.is_null() && rawtx.contains("time"))
|
|
||||||
info.timestamp = rawtx["time"].get<int64_t>();
|
|
||||||
if (!rawtx.is_null() && rawtx.contains("confirmations"))
|
|
||||||
info.confirmations = rawtx["confirmations"].get<int>();
|
|
||||||
} catch (...) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vtx.contains("spends") && vtx["spends"].is_array()) {
|
|
||||||
for (const auto& spend : vtx["spends"]) {
|
|
||||||
if (spend.contains("address")) {
|
|
||||||
info.from_address = spend["address"].get<std::string>();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
txns.push_back(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
// z_viewtransaction may not be available for all txids
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by timestamp descending
|
|
||||||
std::sort(txns.begin(), txns.end(),
|
|
||||||
[](const TransactionInfo& a, const TransactionInfo& b) {
|
|
||||||
return a.timestamp > b.timestamp;
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Main thread: apply results ---
|
|
||||||
return [this, txns = std::move(txns), currentBlocks]() {
|
|
||||||
state_.transactions = std::move(txns);
|
|
||||||
state_.last_tx_update = std::time(nullptr);
|
|
||||||
last_tx_block_height_ = currentBlocks; // P4a: track last-fetched height
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void App::refreshMiningInfo()
|
void App::refreshMiningInfo()
|
||||||
{
|
{
|
||||||
// Use the dedicated fast-lane worker + connection so mining polls
|
// Use the dedicated fast-lane worker + connection so mining polls
|
||||||
@@ -1106,14 +1026,19 @@ void App::refreshMiningInfo()
|
|||||||
|
|
||||||
void App::refreshPeerInfo()
|
void App::refreshPeerInfo()
|
||||||
{
|
{
|
||||||
if (!worker_ || !rpc_) return;
|
if (!rpc_) return;
|
||||||
|
|
||||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
// Use fast-lane worker to bypass head-of-line blocking behind refreshData.
|
||||||
|
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
||||||
|
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
||||||
|
if (!w) return;
|
||||||
|
|
||||||
|
w->post([this, r]() -> rpc::RPCWorker::MainCb {
|
||||||
std::vector<PeerInfo> peers;
|
std::vector<PeerInfo> peers;
|
||||||
std::vector<BannedPeer> bannedPeers;
|
std::vector<BannedPeer> bannedPeers;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
json result = rpc_->call("getpeerinfo");
|
json result = r->call("getpeerinfo");
|
||||||
for (const auto& peer : result) {
|
for (const auto& peer : result) {
|
||||||
PeerInfo info;
|
PeerInfo info;
|
||||||
if (peer.contains("id")) info.id = peer["id"].get<int>();
|
if (peer.contains("id")) info.id = peer["id"].get<int>();
|
||||||
@@ -1139,7 +1064,7 @@ void App::refreshPeerInfo()
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
json result = rpc_->call("listbanned");
|
json result = r->call("listbanned");
|
||||||
for (const auto& ban : result) {
|
for (const auto& ban : result) {
|
||||||
BannedPeer info;
|
BannedPeer info;
|
||||||
if (ban.contains("address")) info.address = ban["address"].get<std::string>();
|
if (ban.contains("address")) info.address = ban["address"].get<std::string>();
|
||||||
@@ -1340,6 +1265,12 @@ void App::startPoolMining(int threads)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: use pool worker address from settings (available even before
|
||||||
|
// the daemon is connected or the blockchain is synced).
|
||||||
|
if (cfg.wallet_address.empty() && !cfg.worker_name.empty()) {
|
||||||
|
cfg.wallet_address = cfg.worker_name;
|
||||||
|
}
|
||||||
|
|
||||||
if (cfg.wallet_address.empty()) {
|
if (cfg.wallet_address.empty()) {
|
||||||
DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n");
|
DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n");
|
||||||
ui::Notifications::instance().error("No wallet address available for pool mining");
|
ui::Notifications::instance().error("No wallet address available for pool mining");
|
||||||
@@ -1660,6 +1591,13 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
// A send changes address balances — refresh on next cycle
|
// A send changes address balances — refresh on next cycle
|
||||||
addresses_dirty_ = true;
|
addresses_dirty_ = true;
|
||||||
|
// Force transaction list refresh so the sent tx appears immediately
|
||||||
|
transactions_dirty_ = true;
|
||||||
|
last_tx_block_height_ = -1;
|
||||||
|
// Track the opid so we can poll for completion
|
||||||
|
if (!result_str.empty()) {
|
||||||
|
pending_opids_.push_back(result_str);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (callback) callback(ok, result_str);
|
if (callback) callback(ok, result_str);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1021,6 +1021,8 @@ void App::renderDecryptWalletDialog() {
|
|||||||
decrypt_step_ = 0;
|
decrypt_step_ = 0;
|
||||||
decrypt_in_progress_ = true;
|
decrypt_in_progress_ = true;
|
||||||
decrypt_status_ = "Unlocking wallet...";
|
decrypt_status_ = "Unlocking wallet...";
|
||||||
|
decrypt_overall_start_time_ = std::chrono::steady_clock::now();
|
||||||
|
decrypt_step_start_time_ = decrypt_overall_start_time_;
|
||||||
|
|
||||||
// Run entire decrypt flow on worker thread
|
// Run entire decrypt flow on worker thread
|
||||||
if (worker_) {
|
if (worker_) {
|
||||||
@@ -1040,6 +1042,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
// Update step on main thread
|
// Update step on main thread
|
||||||
return [this]() {
|
return [this]() {
|
||||||
decrypt_step_ = 1;
|
decrypt_step_ = 1;
|
||||||
|
decrypt_step_start_time_ = std::chrono::steady_clock::now();
|
||||||
decrypt_status_ = "Exporting wallet keys...";
|
decrypt_status_ = "Exporting wallet keys...";
|
||||||
|
|
||||||
// Continue with step 2
|
// Continue with step 2
|
||||||
@@ -1050,7 +1053,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
std::string exportPath = dataDir + exportFile;
|
std::string exportPath = dataDir + exportFile;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
rpc_->call("z_exportwallet", {exportFile});
|
rpc_->call("z_exportwallet", {exportFile}, 300L);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::string err = e.what();
|
std::string err = e.what();
|
||||||
return [this, err]() {
|
return [this, err]() {
|
||||||
@@ -1062,6 +1065,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
|
|
||||||
return [this, exportPath]() {
|
return [this, exportPath]() {
|
||||||
decrypt_step_ = 2;
|
decrypt_step_ = 2;
|
||||||
|
decrypt_step_start_time_ = std::chrono::steady_clock::now();
|
||||||
decrypt_status_ = "Stopping daemon...";
|
decrypt_status_ = "Stopping daemon...";
|
||||||
|
|
||||||
// Continue with step 3
|
// Continue with step 3
|
||||||
@@ -1077,6 +1081,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
|
|
||||||
return [this, exportPath]() {
|
return [this, exportPath]() {
|
||||||
decrypt_step_ = 3;
|
decrypt_step_ = 3;
|
||||||
|
decrypt_step_start_time_ = std::chrono::steady_clock::now();
|
||||||
decrypt_status_ = "Backing up encrypted wallet...";
|
decrypt_status_ = "Backing up encrypted wallet...";
|
||||||
|
|
||||||
// Continue with step 4 (rename)
|
// Continue with step 4 (rename)
|
||||||
@@ -1100,6 +1105,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
|
|
||||||
return [this, exportPath]() {
|
return [this, exportPath]() {
|
||||||
decrypt_step_ = 4;
|
decrypt_step_ = 4;
|
||||||
|
decrypt_step_start_time_ = std::chrono::steady_clock::now();
|
||||||
decrypt_status_ = "Restarting daemon...";
|
decrypt_status_ = "Restarting daemon...";
|
||||||
|
|
||||||
auto restartAndImport = [this, exportPath]() {
|
auto restartAndImport = [this, exportPath]() {
|
||||||
@@ -1136,28 +1142,50 @@ void App::renderDecryptWalletDialog() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update step on main thread
|
// Update step on main thread — close dialog, import in background
|
||||||
if (worker_) {
|
if (worker_) {
|
||||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||||
return [this]() {
|
return [this]() {
|
||||||
decrypt_step_ = 5;
|
// Close the decrypt dialog — user can use the wallet now
|
||||||
decrypt_status_ = "Importing keys (this may take a while)...";
|
decrypt_in_progress_ = false;
|
||||||
|
show_decrypt_dialog_ = false;
|
||||||
|
decrypt_import_active_ = true;
|
||||||
|
|
||||||
|
// Mark rescanning so status bar picks it up immediately
|
||||||
|
state_.sync.rescanning = true;
|
||||||
|
state_.sync.rescan_progress = 0.0f;
|
||||||
|
|
||||||
|
// Clear encryption state early — vault/PIN removed now,
|
||||||
|
// wallet file is already unencrypted
|
||||||
|
if (vault_ && vault_->hasVault()) {
|
||||||
|
vault_->removeVault();
|
||||||
|
}
|
||||||
|
if (settings_ && settings_->getPinEnabled()) {
|
||||||
|
settings_->setPinEnabled(false);
|
||||||
|
settings_->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui::Notifications::instance().info(
|
||||||
|
"Importing keys & rescanning blockchain — wallet is usable while this runs",
|
||||||
|
8.0f);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6: Import wallet (use full path)
|
// Step 6: Import wallet in background (use full path)
|
||||||
|
// Use 20-minute timeout — import + rescan can be very slow
|
||||||
try {
|
try {
|
||||||
rpc_->call("z_importwallet", {exportPath});
|
rpc_->call("z_importwallet", {exportPath}, 1200L);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::string err = e.what();
|
std::string err = e.what();
|
||||||
if (worker_) {
|
if (worker_) {
|
||||||
worker_->post([this, err]() -> rpc::RPCWorker::MainCb {
|
worker_->post([this, err]() -> rpc::RPCWorker::MainCb {
|
||||||
return [this, err]() {
|
return [this, err]() {
|
||||||
decrypt_in_progress_ = false;
|
decrypt_import_active_ = false;
|
||||||
decrypt_status_ = "Import failed: " + err +
|
ui::Notifications::instance().error(
|
||||||
"\nYour encrypted wallet backup is at wallet.dat.encrypted.bak";
|
"Key import failed: " + err +
|
||||||
decrypt_phase_ = 3;
|
"\nEncrypted backup: wallet.dat.encrypted.bak",
|
||||||
|
12.0f);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1168,19 +1196,12 @@ void App::renderDecryptWalletDialog() {
|
|||||||
if (worker_) {
|
if (worker_) {
|
||||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||||
return [this]() {
|
return [this]() {
|
||||||
decrypt_in_progress_ = false;
|
decrypt_import_active_ = false;
|
||||||
decrypt_status_ = "Wallet decrypted successfully!";
|
|
||||||
decrypt_phase_ = 2;
|
|
||||||
|
|
||||||
if (vault_ && vault_->hasVault()) {
|
|
||||||
vault_->removeVault();
|
|
||||||
}
|
|
||||||
if (settings_ && settings_->getPinEnabled()) {
|
|
||||||
settings_->setPinEnabled(false);
|
|
||||||
settings_->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshWalletEncryptionState();
|
refreshWalletEncryptionState();
|
||||||
|
ui::Notifications::instance().success(
|
||||||
|
"Wallet decrypted successfully! All keys imported.",
|
||||||
|
8.0f);
|
||||||
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
|
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1214,10 +1235,16 @@ void App::renderDecryptWalletDialog() {
|
|||||||
"Exporting wallet keys",
|
"Exporting wallet keys",
|
||||||
"Stopping daemon",
|
"Stopping daemon",
|
||||||
"Backing up encrypted wallet",
|
"Backing up encrypted wallet",
|
||||||
"Restarting daemon",
|
"Restarting daemon"
|
||||||
"Importing keys (rescan)"
|
|
||||||
};
|
};
|
||||||
const int numSteps = 6;
|
const int numSteps = 5;
|
||||||
|
|
||||||
|
// Compute elapsed times
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
auto stepElapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
||||||
|
now - decrypt_step_start_time_).count();
|
||||||
|
auto totalElapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
||||||
|
now - decrypt_overall_start_time_).count();
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
for (int i = 0; i < numSteps; i++) {
|
for (int i = 0; i < numSteps; i++) {
|
||||||
@@ -1237,7 +1264,16 @@ void App::renderDecryptWalletDialog() {
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
if (i == decrypt_step_) {
|
if (i == decrypt_step_) {
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.6f, 1.0f), "%s...", stepLabels[i]);
|
// Show step label with elapsed time
|
||||||
|
int mins = (int)(stepElapsed / 60);
|
||||||
|
int secs = (int)(stepElapsed % 60);
|
||||||
|
if (mins > 0) {
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.6f, 1.0f),
|
||||||
|
"%s... (%dm %02ds)", stepLabels[i], mins, secs);
|
||||||
|
} else {
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.6f, 1.0f),
|
||||||
|
"%s... (%ds)", stepLabels[i], secs);
|
||||||
|
}
|
||||||
} else if (i < decrypt_step_) {
|
} else if (i < decrypt_step_) {
|
||||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 0.6f, 1.0f), "%s", stepLabels[i]);
|
ImGui::TextColored(ImVec4(0.6f, 0.8f, 0.6f, 1.0f), "%s", stepLabels[i]);
|
||||||
} else {
|
} else {
|
||||||
@@ -1271,8 +1307,22 @@ void App::renderDecryptWalletDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("Please wait. The daemon is exporting keys, restarting, "
|
|
||||||
"and re-importing. This may take several minutes.");
|
// Step-specific hints
|
||||||
|
if (decrypt_step_ == 4) {
|
||||||
|
ImGui::TextWrapped("Waiting for the daemon to finish starting up...");
|
||||||
|
} else {
|
||||||
|
ImGui::TextWrapped("Please wait. The daemon is exporting keys, restarting, "
|
||||||
|
"and re-importing. This may take several minutes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total elapsed
|
||||||
|
{
|
||||||
|
int tMins = (int)(totalElapsed / 60);
|
||||||
|
int tSecs = (int)(totalElapsed % 60);
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextDisabled("Total elapsed: %dm %02ds", tMins, tSecs);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Phase 2: Success ----
|
// ---- Phase 2: Success ----
|
||||||
} else if (decrypt_phase_ == 2) {
|
} else if (decrypt_phase_ == 2) {
|
||||||
|
|||||||
@@ -182,6 +182,65 @@ json RPCClient::call(const std::string& method, const json& params)
|
|||||||
return response["result"];
|
return response["result"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json RPCClient::call(const std::string& method, const json& params, long timeoutSec)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||||
|
if (!impl_->curl) {
|
||||||
|
throw std::runtime_error("Not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporarily override timeout
|
||||||
|
long prevTimeout = 30L;
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Unlock before calling to avoid recursive lock issues — but we already hold it,
|
||||||
|
// and call() also locks with recursive_mutex, so just delegate to the body directly.
|
||||||
|
json payload = makePayload(method, params);
|
||||||
|
std::string body = payload.dump();
|
||||||
|
std::string response_data;
|
||||||
|
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDS, body.c_str());
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDSIZE, (long)body.size());
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_WRITEDATA, &response_data);
|
||||||
|
|
||||||
|
CURLcode res = curl_easy_perform(impl_->curl);
|
||||||
|
|
||||||
|
// Restore original timeout
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
||||||
|
|
||||||
|
if (res != CURLE_OK) {
|
||||||
|
throw std::runtime_error("RPC request failed: " + std::string(curl_easy_strerror(res)));
|
||||||
|
}
|
||||||
|
|
||||||
|
long http_code = 0;
|
||||||
|
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||||
|
|
||||||
|
if (http_code != 200) {
|
||||||
|
try {
|
||||||
|
json response = json::parse(response_data);
|
||||||
|
if (response.contains("error") && !response["error"].is_null()) {
|
||||||
|
std::string err_msg = response["error"]["message"].get<std::string>();
|
||||||
|
throw std::runtime_error(err_msg);
|
||||||
|
}
|
||||||
|
} catch (const json::exception&) {}
|
||||||
|
throw std::runtime_error("RPC error: HTTP " + std::to_string(http_code));
|
||||||
|
}
|
||||||
|
|
||||||
|
json response = json::parse(response_data);
|
||||||
|
if (response.contains("error") && !response["error"].is_null()) {
|
||||||
|
std::string err_msg = response["error"]["message"].get<std::string>();
|
||||||
|
throw std::runtime_error("RPC error: " + err_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response["result"];
|
||||||
|
} catch (...) {
|
||||||
|
// Ensure timeout is always restored
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::string RPCClient::callRaw(const std::string& method, const json& params)
|
std::string RPCClient::callRaw(const std::string& method, const json& params)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||||
|
|||||||
@@ -61,6 +61,15 @@ public:
|
|||||||
*/
|
*/
|
||||||
json call(const std::string& method, const json& params = json::array());
|
json call(const std::string& method, const json& params = json::array());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Make a raw RPC call with a custom timeout
|
||||||
|
* @param method RPC method name
|
||||||
|
* @param params Method parameters
|
||||||
|
* @param timeoutSec Timeout in seconds (0 = no timeout)
|
||||||
|
* @return JSON response or throws on error
|
||||||
|
*/
|
||||||
|
json call(const std::string& method, const json& params, long timeoutSec);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Make a raw RPC call and return the result field as a string
|
* @brief Make a raw RPC call and return the result field as a string
|
||||||
* @param method RPC method name
|
* @param method RPC method name
|
||||||
|
|||||||
@@ -608,7 +608,11 @@ void RenderMiningTab(App* app)
|
|||||||
bool isSyncing = state.sync.syncing;
|
bool isSyncing = state.sync.syncing;
|
||||||
bool poolBlockedBySolo = s_pool_mode && mining.generate && !state.pool_mining.xmrig_running;
|
bool poolBlockedBySolo = s_pool_mode && mining.generate && !state.pool_mining.xmrig_running;
|
||||||
bool isToggling = app->isMiningToggleInProgress();
|
bool isToggling = app->isMiningToggleInProgress();
|
||||||
bool disabled = !app->isConnected() || isToggling || isSyncing || poolBlockedBySolo;
|
// Pool mining connects to an external pool via xmrig — it does not
|
||||||
|
// need the local blockchain synced or even the daemon connected.
|
||||||
|
bool disabled = s_pool_mode
|
||||||
|
? (isToggling || poolBlockedBySolo)
|
||||||
|
: (!app->isConnected() || isToggling || isSyncing);
|
||||||
|
|
||||||
// Glass panel background with state-dependent tint
|
// Glass panel background with state-dependent tint
|
||||||
GlassPanelSpec btnGlass;
|
GlassPanelSpec btnGlass;
|
||||||
@@ -733,7 +737,7 @@ void RenderMiningTab(App* app)
|
|||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
if (isToggling)
|
if (isToggling)
|
||||||
ImGui::SetTooltip(isMiningActive ? "Stopping miner..." : "Starting miner...");
|
ImGui::SetTooltip(isMiningActive ? "Stopping miner..." : "Starting miner...");
|
||||||
else if (isSyncing)
|
else if (isSyncing && !s_pool_mode)
|
||||||
ImGui::SetTooltip("Syncing blockchain... (%.1f%%)", state.sync.verification_progress * 100.0);
|
ImGui::SetTooltip("Syncing blockchain... (%.1f%%)", state.sync.verification_progress * 100.0);
|
||||||
else if (poolBlockedBySolo)
|
else if (poolBlockedBySolo)
|
||||||
ImGui::SetTooltip("Stop solo mining before starting pool mining");
|
ImGui::SetTooltip("Stop solo mining before starting pool mining");
|
||||||
|
|||||||
@@ -505,6 +505,32 @@ void RenderPeersTab(App* app)
|
|||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
|
||||||
|
// Refresh button — top-right of the toggle header line
|
||||||
|
{
|
||||||
|
ImFont* iconFont = Type().iconMed();
|
||||||
|
float iconSz = iconFont->LegacySize;
|
||||||
|
float btnPad = Layout::spacingSm();
|
||||||
|
float btnX = ImGui::GetWindowPos().x + availWidth - iconSz - btnPad;
|
||||||
|
float btnY = toggleY + (toggleH - iconSz) * 0.5f;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
|
||||||
|
ImGui::PushID("##peersRefresh");
|
||||||
|
if (ImGui::InvisibleButton("##btn", ImVec2(iconSz + btnPad, iconSz + btnPad))) {
|
||||||
|
app->refreshPeerInfo();
|
||||||
|
app->refreshNow();
|
||||||
|
}
|
||||||
|
bool hovered = ImGui::IsItemHovered();
|
||||||
|
if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImU32 iconCol = hovered ? OnSurface() : OnSurfaceMedium();
|
||||||
|
// Centre icon within the invisible button
|
||||||
|
float drawX = btnX + (iconSz + btnPad - iconSz) * 0.5f;
|
||||||
|
float drawY = btnY + (iconSz + btnPad - iconSz) * 0.5f;
|
||||||
|
dl->AddText(iconFont, iconSz, ImVec2(drawX, drawY), iconCol, ICON_MD_REFRESH);
|
||||||
|
if (hovered) {
|
||||||
|
ImGui::SetTooltip("Refresh peers & blockchain");
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetWindowPos().x, toggleY + toggleH));
|
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetWindowPos().x, toggleY + toggleH));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,22 +808,13 @@ void RenderPeersTab(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// Footer — Refresh + Clear Bans (material styled)
|
// Footer — Clear Bans (material styled)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
{
|
if (s_show_banned && !state.bannedPeers.empty()) {
|
||||||
ImGui::BeginDisabled(!app->isConnected());
|
ImGui::BeginDisabled(!app->isConnected());
|
||||||
|
if (TactileSmallButton("Clear All Bans", S.resolveFont("button"))) {
|
||||||
if (TactileSmallButton("Refresh Peers", S.resolveFont("button"))) {
|
app->clearBans();
|
||||||
app->refreshPeerInfo();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s_show_banned && !state.bannedPeers.empty()) {
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (TactileSmallButton("Clear All Bans", S.resolveFont("button"))) {
|
|
||||||
app->clearBans();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user