fix(wallet): retire tracked sends that only paid the wallet itself
A send stays tracked until z_viewtransaction shows an outgoing output. Shields, merges/consolidations and chat note splits pay only the wallet itself, so they never showed one. They stayed tracked for the whole session and forced a full history refresh past the scan throttle on every tab. - A tracked send whose view has no outgoing output is retired once it is mined (1+ confirmations) or conflicted. - Tracked sends are enriched first, and reaching the per-cycle view cap no longer ends the loop, so the cap can't starve them. - Backstop: a tracked send still unsettled 20 blocks after it was added (expired, dropped from the mempool) stops being tracked. - Tracked sends are cleared on disconnect: they belong to that connection's wallet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1094,8 +1094,21 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
if (!cachedView.outgoing_outputs.empty()) knownTxids.insert(txid);
|
||||
}
|
||||
|
||||
int viewTxCount = 0;
|
||||
// Tracked sends go first so they get the per-cycle z_viewtransaction budget ahead of routine
|
||||
// enrichment, and a spent budget only skips further RPCs: cached rows later in the order are still
|
||||
// appended (stopping the loop at the cap dropped them from History).
|
||||
std::vector<const std::string*> enrichOrder;
|
||||
enrichOrder.reserve(knownTxids.size());
|
||||
for (const auto& txid : knownTxids) {
|
||||
if (snapshot.sendTxids.count(txid)) enrichOrder.push_back(&txid);
|
||||
}
|
||||
for (const auto& txid : knownTxids) {
|
||||
if (!snapshot.sendTxids.count(txid)) enrichOrder.push_back(&txid);
|
||||
}
|
||||
|
||||
int viewTxCount = 0;
|
||||
for (const std::string* txidPtr : enrichOrder) {
|
||||
const std::string& txid = *txidPtr;
|
||||
bool trackedSend = snapshot.sendTxids.count(txid) > 0;
|
||||
|
||||
auto cached = snapshot.viewTxCache.find(txid);
|
||||
@@ -1112,7 +1125,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
|
||||
if (!trackedSend && snapshot.fullyEnrichedTxids.count(txid)) continue;
|
||||
|
||||
if (viewTxCount >= maxViewTransactionsPerCycle) break;
|
||||
if (viewTxCount >= maxViewTransactionsPerCycle) continue;
|
||||
++viewTxCount;
|
||||
|
||||
try {
|
||||
@@ -1163,6 +1176,24 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
|
||||
if (!trackedSend || !entry.outgoing_outputs.empty()) {
|
||||
result.newViewTxEntries[txid] = std::move(entry);
|
||||
} else {
|
||||
// A tracked op that only paid the wallet itself never shows an outgoing output, so the
|
||||
// check above kept it tracked (and forcing history refreshes past the scan throttle on
|
||||
// every tab) for the rest of the session. While it's unconfirmed the empty result stays
|
||||
// retryable; once it is mined, or conflicted, the view result is final: cache it and
|
||||
// stop tracking. Confirmations come from this txid's rows (listtransactions, or the
|
||||
// gettransaction-backed placeholder) and any raw metadata applied to the entry.
|
||||
bool mined = entry.confirmations >= 1;
|
||||
bool conflicted = entry.confirmations < 0;
|
||||
for (const auto& info : result.transactions) {
|
||||
if (info.txid != txid) continue;
|
||||
if (info.confirmations >= 1) mined = true;
|
||||
else if (info.confirmations < 0) conflicted = true;
|
||||
}
|
||||
if (mined || conflicted) {
|
||||
result.newViewTxEntries[txid] = std::move(entry);
|
||||
result.resolvedSendTxids.insert(txid);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
(void)e;
|
||||
@@ -1512,7 +1543,7 @@ void NetworkRefreshService::applyTransactionRefreshResult(WalletState& state,
|
||||
for (auto& [txid, entry] : result.newViewTxEntries) {
|
||||
bool hasOutgoingOutputs = !entry.outgoing_outputs.empty();
|
||||
cacheUpdate.viewTxCache[txid] = std::move(entry);
|
||||
if (hasOutgoingOutputs) cacheUpdate.sendTxids.erase(txid);
|
||||
if (hasOutgoingOutputs || result.resolvedSendTxids.count(txid)) cacheUpdate.sendTxids.erase(txid);
|
||||
}
|
||||
|
||||
cacheUpdate.confirmedTxCache.clear();
|
||||
|
||||
@@ -222,6 +222,10 @@ public:
|
||||
std::vector<chat::HushChatTransactionMetadata> hushChatMetadata;
|
||||
int blockHeight = -1;
|
||||
TransactionViewCache newViewTxEntries;
|
||||
// Tracked sends whose view result is final although it shows no outgoing output: the op only
|
||||
// paid the wallet itself (shield, merge/consolidate, auto-shield, chat note split, a transfer
|
||||
// between own addresses) and is now mined or conflicted. Applying the result stops tracking them.
|
||||
std::unordered_set<std::string> resolvedSendTxids;
|
||||
std::size_t nextShieldedScanStartIndex = 0;
|
||||
std::size_t shieldedAddressesScanned = 0;
|
||||
std::size_t shieldedAddressCount = 0;
|
||||
|
||||
Reference in New Issue
Block a user