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:
@@ -971,8 +971,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
// ============================================================================
|
||||
// Recent Sends section — styled to match transactions list
|
||||
// ============================================================================
|
||||
static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
|
||||
float width, ImFont* capFont, App* app) {
|
||||
static void RenderRecentSends(const WalletState& state, float width, ImFont* capFont, App* app) {
|
||||
auto& S = schema::UI();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_recent_sends"));
|
||||
@@ -980,139 +979,89 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
|
||||
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
float hs = Layout::hScale(avail.x);
|
||||
float glassRound = Layout::glassRounding();
|
||||
|
||||
ImFont* body2 = Type().body2();
|
||||
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
|
||||
float iconSz = std::max(schema::UI().drawElement("tabs.send", "recent-icon-min-size").size, schema::UI().drawElement("tabs.send", "recent-icon-size").size * hs);
|
||||
float vs = Layout::vScale(std::max(1.0f, avail.y));
|
||||
float dp = Layout::dpiScale();
|
||||
float rowH = std::max(18.0f * dp, S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f) * vs);
|
||||
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
|
||||
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
|
||||
ImU32 sendCol = Error();
|
||||
ImU32 greenCol = WithAlpha(Success(), (int)S.drawElement("tabs.send", "recent-green-alpha").size);
|
||||
float rowPadLeft = Layout::spacingLg();
|
||||
|
||||
// Collect matching transactions
|
||||
std::vector<const TransactionInfo*> sends;
|
||||
for (const auto& tx : state.transactions) {
|
||||
if (tx.type != "send") continue;
|
||||
if (tx.type != "send" && tx.type != "shield") continue;
|
||||
sends.push_back(&tx);
|
||||
if (sends.size() >= (size_t)S.drawElement("tabs.send", "max-recent-sends").size) break;
|
||||
}
|
||||
|
||||
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
|
||||
|
||||
ImGui::BeginChild("##RecentSendRows", ImVec2(width, listH), false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
ImDrawList* rowDL = ImGui::GetWindowDrawList();
|
||||
|
||||
char buf[64];
|
||||
if (sends.empty()) {
|
||||
ImGui::SetCursorPosY(Layout::spacingMd());
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("send_no_recent"));
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
// Outer glass panel wrapping all rows
|
||||
float itemSpacingY = ImGui::GetStyle().ItemSpacing.y;
|
||||
float listH = rowH * (float)sends.size() + itemSpacingY * (float)(sends.size() - 1);
|
||||
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
|
||||
float listW = width;
|
||||
ImVec2 listPanelMax(listPanelMin.x + listW, listPanelMin.y + listH);
|
||||
GlassPanelSpec glassSpec;
|
||||
glassSpec.rounding = glassRound;
|
||||
DrawGlassPanel(dl, listPanelMin, listPanelMax, glassSpec);
|
||||
|
||||
// Clip draw commands to panel bounds to prevent overflow
|
||||
dl->PushClipRect(listPanelMin, listPanelMax, true);
|
||||
|
||||
char buf[64];
|
||||
for (size_t si = 0; si < sends.size(); si++) {
|
||||
const auto& tx = *sends[si];
|
||||
|
||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
||||
float innerW = listW;
|
||||
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
|
||||
float rowY = rowPos.y + rowH * 0.5f;
|
||||
|
||||
// Hover glow
|
||||
bool hovered = material::IsRectHovered(rowPos, rowEnd);
|
||||
if (hovered) {
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "row-hover-alpha").size), schema::UI().drawElement("tabs.send", "row-hover-rounding").size);
|
||||
// Icon
|
||||
DrawTxIcon(rowDL, "send", rowPos.x + Layout::spacingMd(), rowY, iconSz, sendCol);
|
||||
|
||||
// Type label (first line)
|
||||
float txX = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(txX, rowPos.y + 2.0f * dp), OnSurfaceMedium(), TR("sent_type"));
|
||||
|
||||
// Address (second line)
|
||||
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||
std::string addrDisplay = TruncateAddress(tx.address,
|
||||
(size_t)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());
|
||||
|
||||
// Amount (right-aligned, first line)
|
||||
snprintf(buf, sizeof(buf), "-%.4f %s", std::abs(tx.amount), DRAGONX_TICKER);
|
||||
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, buf);
|
||||
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
|
||||
float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size,
|
||||
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(amtX, rowPos.y + 2.0f * dp), sendCol, buf);
|
||||
|
||||
// Time ago
|
||||
std::string ago = timeAgo(tx.timestamp);
|
||||
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str());
|
||||
rowDL->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f),
|
||||
rowPos.y + 2.0f * dp),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
|
||||
// Clickable row — hover highlight + navigate to History
|
||||
float rowW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
|
||||
if (material::IsRectHovered(rowPos, rowEnd)) {
|
||||
rowDL->AddRectFilled(rowPos, rowEnd,
|
||||
IM_COL32(255, 255, 255, 15),
|
||||
S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0)) {
|
||||
app->setCurrentPage(ui::NavPage::History);
|
||||
}
|
||||
}
|
||||
|
||||
float cx = rowPos.x + rowPadLeft;
|
||||
float cy = rowPos.y + Layout::spacingMd();
|
||||
|
||||
// Icon
|
||||
DrawTxIcon(dl, tx.type, cx + iconSz, cy + body2->LegacySize * 0.5f, iconSz, sendCol);
|
||||
|
||||
// Type label (first line)
|
||||
float labelX = cx + iconSz * 2.0f + Layout::spacingSm();
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), sendCol, TR("sent_upper"));
|
||||
|
||||
// Time (next to type)
|
||||
std::string ago = timeAgo(tx.timestamp);
|
||||
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("sent_upper")).x;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy),
|
||||
OnSurfaceDisabled(), ago.c_str());
|
||||
|
||||
// Address (second line)
|
||||
std::string addr_display = TruncateAddress(tx.address, (int)S.drawElement("tabs.send", "recent-addr-trunc-len").size);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceMedium(), addr_display.c_str());
|
||||
|
||||
// Amount (right-aligned, first line)
|
||||
snprintf(buf, sizeof(buf), "-%.8f", std::abs(tx.amount));
|
||||
ImVec2 amtSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, buf);
|
||||
float amtX = rowPos.x + innerW - amtSz.x - Layout::spacingLg();
|
||||
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(amtX, cy), sendCol, buf,
|
||||
S.drawElement("tabs.send", "text-shadow-offset-x").size, S.drawElement("tabs.send", "text-shadow-offset-y").size, IM_COL32(0, 0, 0, (int)S.drawElement("tabs.send", "text-shadow-alpha").size));
|
||||
|
||||
// USD equivalent (right-aligned, second line)
|
||||
double priceUsd = state.market.price_usd;
|
||||
if (priceUsd > 0.0) {
|
||||
double usdVal = std::abs(tx.amount) * priceUsd;
|
||||
if (usdVal >= 1.0)
|
||||
snprintf(buf, sizeof(buf), "$%.2f", usdVal);
|
||||
else if (usdVal >= 0.01)
|
||||
snprintf(buf, sizeof(buf), "$%.4f", usdVal);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "$%.6f", usdVal);
|
||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rowPos.x + innerW - usdSz.x - Layout::spacingLg(), cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
// Status badge
|
||||
{
|
||||
const char* statusStr;
|
||||
ImU32 statusCol;
|
||||
if (tx.confirmations == 0) {
|
||||
statusStr = TR("pending"); statusCol = Warning();
|
||||
} else if (tx.confirmations < (int)S.drawElement("tabs.send", "confirmed-threshold").size) {
|
||||
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
|
||||
statusStr = buf; statusCol = Warning();
|
||||
} else {
|
||||
statusStr = TR("confirmed"); statusCol = greenCol;
|
||||
}
|
||||
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
|
||||
float statusX = amtX - sSz.x - Layout::spacingXxl();
|
||||
float minStatusX = cx + innerW * S.drawElement("tabs.send", "status-min-x-ratio").size;
|
||||
if (statusX < minStatusX) statusX = minStatusX;
|
||||
ImU32 pillBg = (statusCol & 0x00FFFFFFu) | (static_cast<ImU32>((int)S.drawElement("tabs.send", "status-pill-bg-alpha").size) << 24);
|
||||
ImVec2 pillMin(statusX - Layout::spacingSm(), cy + body2->LegacySize + (int)S.drawElement("tabs.send", "status-pill-y-offset").size);
|
||||
ImVec2 pillMax(statusX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs());
|
||||
dl->AddRectFilled(pillMin, pillMax, pillBg, schema::UI().drawElement("tabs.send", "status-pill-rounding").size);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(statusX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, rowH));
|
||||
|
||||
// Subtle divider between rows
|
||||
if (si < sends.size() - 1) {
|
||||
ImVec2 divStart = ImGui::GetCursorScreenPos();
|
||||
dl->AddLine(ImVec2(divStart.x + rowPadLeft + iconSz * 2.0f, divStart.y),
|
||||
ImVec2(divStart.x + innerW - Layout::spacingLg(), divStart.y),
|
||||
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "row-divider-alpha").size));
|
||||
}
|
||||
}
|
||||
|
||||
dl->PopClipRect();
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1456,6 +1405,18 @@ void RenderSendTab(App* app)
|
||||
}
|
||||
|
||||
// Divider before action buttons
|
||||
{
|
||||
float actionBtnH = std::max(S.drawElement("tabs.send", "action-btn-min-height").size,
|
||||
S.drawElement("tabs.send", "action-btn-height").size * vScale);
|
||||
float footerH = innerGap + actionBtnH + pad * vScale;
|
||||
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
|
||||
float targetCardH = Layout::mainCardTargetH(formW, vScale);
|
||||
float footerTopH = targetCardH - footerH;
|
||||
if (currentCardH < footerTopH) {
|
||||
ImGui::Dummy(ImVec2(0, footerTopH - currentCardH));
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
|
||||
{
|
||||
ImVec2 divPos = ImGui::GetCursorScreenPos();
|
||||
@@ -1507,7 +1468,7 @@ void RenderSendTab(App* app)
|
||||
}
|
||||
|
||||
// ---- RECENT SENDS ----
|
||||
RenderRecentSends(dl, state, formW, capFont, app);
|
||||
RenderRecentSends(state, formW, capFont, app);
|
||||
|
||||
ImGui::EndGroup();
|
||||
ImGui::EndDisabled(); // sendSyncing guard
|
||||
|
||||
Reference in New Issue
Block a user