fix(resilience,perf): gate send on a known tip, surface dropped/stale txs, advance cached confs, memoize explorer
Backlog mediums (display/gating/caching only — no send/sweep/encrypt call or args changed): Resilience: - Gate Send/Review on a KNOWN peer-derived tip (SyncInfo::sendTipReady): a fresh reconnect or peerless node has tip_known=false, so its local height isn't a trustworthy network tip — sending is disabled with a clear tooltip until it is. Lite is exempt (its own sync gate). The z_sendmany call is untouched. - History now shows "Dropped" for a conflicted tx (confirmations < 0) and "Expired?" for one unconfirmed > 1h, instead of "Pending" forever (new TransactionInfo::isConflicted/isStalePending; the tab inlines the check on its DisplayTx fields). - A cached z_viewtransaction row's confirmations now advance by the blocks mined since it was sampled (observed_height), instead of freezing at the cached value; never goes backwards; pending (0) isn't extrapolated. Performance: - Explorer block list uses loadRangeCached() (memoized on the cache revision) instead of loadRange(), so it only hits SQLite after the cache changes, not every frame. Tests: testResilienceMediums (tip gate, confirmations-advance, dropped/stale predicates) + loadRangeCached memoization in testExplorerBlockCache (isolated on its own cache). Held: Overview/Balance per-frame list cache, peers clippers, off-thread tx-history cache load (larger refactors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -846,7 +846,8 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
||||
|
||||
std::map<int, BlockSummary> cachedBlocks;
|
||||
if (!pageHeights.empty()) {
|
||||
cachedBlocks = s_recent_block_cache.loadRange(pageHeights.back(), pageHeights.front());
|
||||
// Memoized on the cache revision: SQLite is only hit after the cache changed, not every frame.
|
||||
cachedBlocks = s_recent_block_cache.loadRangeCached(pageHeights.back(), pageHeights.front());
|
||||
}
|
||||
|
||||
// Fuzzy mode: a non-numeric, non-full-hash query filters the list to cached blocks whose hash
|
||||
@@ -890,7 +891,7 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
||||
}
|
||||
std::map<int, BlockSummary> nextCachedBlocks;
|
||||
if (!nextPageHeights.empty()) {
|
||||
nextCachedBlocks = s_recent_block_cache.loadRange(nextPageHeights.back(), nextPageHeights.front());
|
||||
nextCachedBlocks = s_recent_block_cache.loadRangeCached(nextPageHeights.back(), nextPageHeights.front());
|
||||
}
|
||||
for (int height : nextPageHeights) {
|
||||
if (nextCachedBlocks.find(height) == nextCachedBlocks.end()) {
|
||||
|
||||
@@ -917,7 +917,8 @@ void RenderSendConfirmPopup(App* app) {
|
||||
// since Review. Don't broadcast a now-invalid transaction.
|
||||
const auto& liveState = app->getWalletState();
|
||||
double liveAvail = GetAvailableBalance(app);
|
||||
if (!app->isConnected() || liveState.sync.syncing || s_amount <= 0 ||
|
||||
if (!app->isConnected() || liveState.sync.syncing ||
|
||||
!liveState.sync.sendTipReady(app->isLiteBuild()) || s_amount <= 0 ||
|
||||
!util::canAfford(s_amount, s_fee, liveAvail)) {
|
||||
s_tx_status = TR("send_cannot_send_now");
|
||||
s_status_success = false;
|
||||
@@ -1048,6 +1049,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
}
|
||||
bool can_send = app->isConnected() &&
|
||||
!state.sync.syncing &&
|
||||
state.sync.sendTipReady(app->isLiteBuild()) &&
|
||||
is_valid_address &&
|
||||
s_amount > 0 &&
|
||||
s_from_address[0] != '\0' &&
|
||||
@@ -1072,7 +1074,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
float btnH = std::max(schema::UI().drawElement("tabs.send", "action-btn-min-height").size, schema::UI().drawElement("tabs.send", "action-btn-height").size * vScale);
|
||||
|
||||
// Explain why Review/Send is greyed while syncing (the fields above are usable now).
|
||||
if (state.sync.syncing) {
|
||||
if (state.sync.syncing || (app->isConnected() && !state.sync.sendTipReady(app->isLiteBuild()))) {
|
||||
Type().textColored(TypeStyle::Caption, Warning(), TR("send_available_when_synced"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
}
|
||||
@@ -1101,6 +1103,8 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
material::Tooltip("%s", TR(app->isLiteBuild() ? "lite_no_wallet_short" : "send_tooltip_not_connected"));
|
||||
else if (state.sync.syncing)
|
||||
material::Tooltip("%s", TR("send_tooltip_syncing"));
|
||||
else if (!state.sync.sendTipReady(app->isLiteBuild()))
|
||||
material::Tooltip("%s", TR("send_tooltip_no_tip"));
|
||||
else if (s_from_address[0] == '\0')
|
||||
material::Tooltip("%s", TR("send_tooltip_select_source"));
|
||||
else if (is_mining_addr)
|
||||
|
||||
@@ -855,7 +855,12 @@ void RenderTransactionsTab(App* app)
|
||||
{
|
||||
const char* statusStr;
|
||||
ImU32 statusCol;
|
||||
if (tx.confirmations == 0) {
|
||||
const int64_t nowSec = static_cast<int64_t>(std::time(nullptr));
|
||||
if (tx.confirmations < 0) { // gettransaction < 0: conflicted / double-spent / reorged out
|
||||
statusStr = TR("tx_dropped"); statusCol = Error();
|
||||
} else if (tx.confirmations == 0 && tx.timestamp > 0 && nowSec - tx.timestamp > 3600) {
|
||||
statusStr = TR("tx_stale_pending"); statusCol = Warning();
|
||||
} else if (tx.confirmations == 0) {
|
||||
statusStr = TR("pending"); statusCol = Warning();
|
||||
} else if (tx.confirmations < 10) {
|
||||
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
|
||||
|
||||
Reference in New Issue
Block a user