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:
@@ -104,7 +104,7 @@ static void DrawTxIcon(ImDrawList* dl, const std::string& type,
|
||||
} else if (type == "receive") {
|
||||
icon = ICON_MD_CALL_RECEIVED;
|
||||
} else if (type == "shield") {
|
||||
icon = ICON_MD_SHIELD;
|
||||
icon = ICON_MD_CALL_MADE;
|
||||
} else {
|
||||
icon = ICON_MD_CONSTRUCTION;
|
||||
}
|
||||
@@ -147,6 +147,8 @@ void RenderTransactionsTab(App* app)
|
||||
ImU32 greenCol = Success();
|
||||
ImU32 redCol = Error();
|
||||
ImU32 goldCol = Warning();
|
||||
std::string txLoadingText = app->transactionRefreshProgressText();
|
||||
bool txLoading = !txLoadingText.empty();
|
||||
|
||||
// Expanded row index for inline detail
|
||||
static int s_expanded_row = -1;
|
||||
@@ -319,6 +321,18 @@ void RenderTransactionsTab(App* app)
|
||||
ExportTransactionsDialog::show();
|
||||
}
|
||||
|
||||
if (txLoading) {
|
||||
ImGui::SameLine(0, filterGap);
|
||||
ImGui::PushFont(Type().iconSmall());
|
||||
float pulse = 0.55f + 0.45f * std::sin((float)ImGui::GetTime() * 3.0f);
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, pulse), ICON_MD_HOURGLASS_EMPTY);
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine(0, Layout::spacingXs());
|
||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||
const char* dotStr[] = {"", ".", "..", "..."};
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", txLoadingText.c_str(), dotStr[dots]);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm() + Layout::spacingXs()));
|
||||
|
||||
// ================================================================
|
||||
@@ -415,8 +429,8 @@ void RenderTransactionsTab(App* app)
|
||||
for (size_t i = 0; i < display_txns.size(); i++) {
|
||||
const auto& dtx = display_txns[i];
|
||||
if (type_filter != 0) {
|
||||
if (type_filter == 1 && dtx.display_type != "send") continue;
|
||||
if (type_filter == 2 && dtx.display_type != "receive" && dtx.display_type != "shield") continue;
|
||||
if (type_filter == 1 && dtx.display_type != "send" && dtx.display_type != "shield") continue;
|
||||
if (type_filter == 2 && dtx.display_type != "receive") continue;
|
||||
if (type_filter == 3 && dtx.display_type != "generate" && dtx.display_type != "immature" && dtx.display_type != "mined") continue;
|
||||
}
|
||||
if (!search_str.empty()) {
|
||||
@@ -567,7 +581,14 @@ void RenderTransactionsTab(App* app)
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
|
||||
} else if (state.transactions.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions"));
|
||||
if (txLoading) {
|
||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||
const char* dotStr[] = {"", ".", "..", "..."};
|
||||
snprintf(buf, sizeof(buf), "%s%s", txLoadingText.c_str(), dotStr[dots]);
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
||||
} else {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions"));
|
||||
}
|
||||
} else if (filtered_indices.empty()) {
|
||||
ImGui::Dummy(ImVec2(0, 20));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_matching"));
|
||||
@@ -596,10 +617,11 @@ void RenderTransactionsTab(App* app)
|
||||
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
|
||||
|
||||
// Determine type info
|
||||
bool shieldedDisplay = tx.display_type == "shield";
|
||||
ImU32 iconCol;
|
||||
const char* typeStr;
|
||||
if (tx.display_type == "shield") {
|
||||
iconCol = Primary(); typeStr = TR("shielded_type");
|
||||
if (shieldedDisplay) {
|
||||
iconCol = redCol; typeStr = TR("sent_type");
|
||||
} else if (tx.display_type == "receive") {
|
||||
iconCol = greenCol; typeStr = TR("recv_type");
|
||||
} else if (tx.display_type == "send") {
|
||||
@@ -627,7 +649,8 @@ void RenderTransactionsTab(App* app)
|
||||
float cy = rowPos.y + Layout::spacingMd();
|
||||
|
||||
// Icon
|
||||
DrawTxIcon(dl, tx.display_type, cx + rowIconSz, cy + body2->LegacySize * 0.5f, rowIconSz, iconCol);
|
||||
DrawTxIcon(dl, shieldedDisplay ? "send" : tx.display_type,
|
||||
cx + rowIconSz, cy + body2->LegacySize * 0.5f, rowIconSz, iconCol);
|
||||
|
||||
// Type label
|
||||
float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm();
|
||||
@@ -687,16 +710,35 @@ void RenderTransactionsTab(App* app)
|
||||
}
|
||||
// Position status badge in the middle-right area
|
||||
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
|
||||
float statusX = amtX - sSz.x - Layout::spacingXxl();
|
||||
const char* shieldedStr = TR("shielded_type");
|
||||
ImVec2 shieldSz = shieldedDisplay
|
||||
? capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, shieldedStr)
|
||||
: ImVec2(0, 0);
|
||||
float shieldPillW = shieldSz.x + Layout::spacingSm() * 2.0f;
|
||||
float stackW = shieldedDisplay ? std::max(sSz.x, shieldPillW) : sSz.x;
|
||||
float statusX = amtX - stackW - Layout::spacingXxl();
|
||||
float minStatusX = cx + innerW * 0.25f; // don't overlap address
|
||||
if (statusX < minStatusX) statusX = minStatusX;
|
||||
float statusTextX = statusX + (stackW - sSz.x) * 0.5f;
|
||||
if (shieldedDisplay) {
|
||||
float shieldX = statusX + (stackW - shieldSz.x) * 0.5f;
|
||||
ImU32 shieldCol = Primary();
|
||||
ImU32 shieldBg = (shieldCol & 0x00FFFFFFu) | (static_cast<ImU32>(30) << 24);
|
||||
ImVec2 shieldPillMin(shieldX - Layout::spacingSm(), cy - 1.0f);
|
||||
ImVec2 shieldPillMax(shieldX + shieldSz.x + Layout::spacingSm(),
|
||||
shieldPillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||
dl->AddRectFilled(shieldPillMin, shieldPillMax, shieldBg,
|
||||
schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(shieldX, cy), shieldCol, shieldedStr);
|
||||
}
|
||||
// Background pill
|
||||
ImU32 pillBg = (statusCol & 0x00FFFFFFu) | (static_cast<ImU32>(30) << 24);
|
||||
ImVec2 pillMin(statusX - Layout::spacingSm(), cy + body2->LegacySize + 1);
|
||||
ImVec2 pillMax(statusX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||
ImVec2 pillMin(statusTextX - Layout::spacingSm(), cy + body2->LegacySize + 1);
|
||||
ImVec2 pillMax(statusTextX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||
dl->AddRectFilled(pillMin, pillMax, pillBg, schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(statusX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
|
||||
ImVec2(statusTextX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
|
||||
}
|
||||
|
||||
// Click to expand/collapse + invisible button for interaction
|
||||
|
||||
Reference in New Issue
Block a user