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:
@@ -229,6 +229,13 @@ void App::onDisconnected(const std::string& reason)
|
||||
state_.clear();
|
||||
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
|
||||
if (fast_worker_) {
|
||||
fast_worker_->stop();
|
||||
@@ -255,11 +262,19 @@ void App::refreshData()
|
||||
const bool doEncrypt = !encryption_state_prefetched_;
|
||||
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 bool doTransactions = (last_tx_block_height_ < 0
|
||||
|| 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)
|
||||
std::vector<std::string> txZAddrs;
|
||||
@@ -270,8 +285,15 @@ void App::refreshData()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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) {
|
||||
if (tx.confirmations > 6 && tx.timestamp != 0) {
|
||||
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
|
||||
// on a single thread with no inter-task queue overhead.
|
||||
worker_->post([this, doAddresses, doPeers, doEncrypt, doTransactions,
|
||||
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
|
||||
// ================================================================
|
||||
@@ -376,6 +413,7 @@ void App::refreshData()
|
||||
// Phase 3: Transactions (only when new blocks)
|
||||
// ================================================================
|
||||
std::vector<TransactionInfo> txns;
|
||||
std::unordered_map<std::string, ViewTxCacheEntry> newViewTxEntries;
|
||||
bool txOk = false;
|
||||
if (doTransactions) {
|
||||
txOk = true;
|
||||
@@ -434,65 +472,103 @@ void App::refreshData()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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 {
|
||||
json vtx = rpc_->call("z_viewtransaction", json::array({txid}));
|
||||
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()) {
|
||||
for (const auto& output : vtx["outputs"]) {
|
||||
bool outgoing = false;
|
||||
if (output.contains("outgoing"))
|
||||
outgoing = output["outgoing"].get<bool>();
|
||||
if (!outgoing) continue;
|
||||
std::string destAddr;
|
||||
ViewTxCacheEntry::Output out;
|
||||
if (output.contains("address"))
|
||||
destAddr = output["address"].get<std::string>();
|
||||
double value = 0.0;
|
||||
out.address = output["address"].get<std::string>();
|
||||
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;
|
||||
out.value = output["value"].get<double>();
|
||||
if (output.contains("memoStr"))
|
||||
info.memo = output["memoStr"].get<std::string>();
|
||||
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);
|
||||
out.memo = output["memoStr"].get<std::string>();
|
||||
entry.outgoing_outputs.push_back(std::move(out));
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
(void)e; // z_viewtransaction may not be available for all txids
|
||||
}
|
||||
@@ -568,6 +644,7 @@ void App::refreshData()
|
||||
return [this, totalBal, blockInfo, balOk, blockOk,
|
||||
zAddrs = std::move(zAddrs), tAddrs = std::move(tAddrs), addrOk,
|
||||
txns = std::move(txns), txOk, currentBlocks,
|
||||
newViewTxEntries = std::move(newViewTxEntries),
|
||||
peers = std::move(peers), bannedPeers = std::move(bannedPeers), peerOk,
|
||||
walletInfo, encryptOk]() {
|
||||
// --- Balance ---
|
||||
@@ -641,6 +718,24 @@ void App::refreshData()
|
||||
state_.transactions = std::move(txns);
|
||||
state_.last_tx_update = std::time(nullptr);
|
||||
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 ---
|
||||
@@ -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()
|
||||
{
|
||||
// Use the dedicated fast-lane worker + connection so mining polls
|
||||
@@ -1106,14 +1026,19 @@ void App::refreshMiningInfo()
|
||||
|
||||
void App::refreshPeerInfo()
|
||||
{
|
||||
if (!worker_ || !rpc_) return;
|
||||
|
||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||
if (!rpc_) return;
|
||||
|
||||
// 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<BannedPeer> bannedPeers;
|
||||
|
||||
try {
|
||||
json result = rpc_->call("getpeerinfo");
|
||||
json result = r->call("getpeerinfo");
|
||||
for (const auto& peer : result) {
|
||||
PeerInfo info;
|
||||
if (peer.contains("id")) info.id = peer["id"].get<int>();
|
||||
@@ -1139,7 +1064,7 @@ void App::refreshPeerInfo()
|
||||
}
|
||||
|
||||
try {
|
||||
json result = rpc_->call("listbanned");
|
||||
json result = r->call("listbanned");
|
||||
for (const auto& ban : result) {
|
||||
BannedPeer info;
|
||||
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()) {
|
||||
DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n");
|
||||
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) {
|
||||
// A send changes address balances — refresh on next cycle
|
||||
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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user