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:
@@ -652,6 +652,7 @@ static void RenderBalanceClassic(App* app)
|
||||
bool isZ;
|
||||
bool hidden;
|
||||
bool favorite;
|
||||
bool mining;
|
||||
};
|
||||
std::vector<AddrRow> rows;
|
||||
rows.reserve(state.z_addresses.size() + state.t_addresses.size());
|
||||
@@ -665,7 +666,7 @@ static void RenderBalanceClassic(App* app)
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav)
|
||||
continue;
|
||||
rows.push_back({&a, true, isHidden, isFav});
|
||||
rows.push_back({&a, true, isHidden, isFav, app->isMiningAddress(a.address)});
|
||||
}
|
||||
for (const auto& a : state.t_addresses) {
|
||||
std::string filter(addr_search);
|
||||
@@ -677,7 +678,7 @@ static void RenderBalanceClassic(App* app)
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav)
|
||||
continue;
|
||||
rows.push_back({&a, false, isHidden, isFav});
|
||||
rows.push_back({&a, false, isHidden, isFav, app->isMiningAddress(a.address)});
|
||||
}
|
||||
|
||||
// Sort: favorites first, then Z addresses, then by balance descending
|
||||
@@ -951,8 +952,9 @@ static void RenderBalanceClassic(App* app)
|
||||
const char* typeLabel = row.isZ ? "Shielded" : "Transparent";
|
||||
const char* hiddenTag = row.hidden ? " (hidden)" : "";
|
||||
const char* viewOnlyTag = (!addr.has_spending_key) ? " (view-only)" : "";
|
||||
const char* miningTag = row.mining ? TR("mining_tag") : "";
|
||||
char typeBuf[64];
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, viewOnlyTag);
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s%s", typeLabel, hiddenTag, viewOnlyTag, miningTag);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
||||
|
||||
// Label (if present, next to type)
|
||||
@@ -1033,6 +1035,9 @@ static void RenderBalanceClassic(App* app)
|
||||
app->setCurrentPage(NavPage::Send);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
|
||||
app->setMiningAddress(addr.address, !row.mining);
|
||||
}
|
||||
if (ImGui::MenuItem(TR("export_private_key"))) {
|
||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
|
||||
}
|
||||
@@ -1111,18 +1116,19 @@ static void RenderBalanceClassic(App* app)
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
const auto& txs = state.transactions;
|
||||
int maxTx = kRecentTxCount;
|
||||
int count = (int)txs.size();
|
||||
if (count > maxTx) count = maxTx;
|
||||
ImFont* capFont = Type().caption();
|
||||
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
|
||||
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
|
||||
ImGui::BeginChild("##BalanceClassicRecentRows", ImVec2(0, listH), false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
|
||||
const auto& txs = state.transactions;
|
||||
int count = (int)txs.size();
|
||||
if (count == 0) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||
"No transactions yet");
|
||||
} else {
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImFont* capFont = Type().caption();
|
||||
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * 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);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
@@ -1176,6 +1182,7 @@ static void RenderBalanceClassic(App* app)
|
||||
ImGui::Dummy(ImVec2(0, rowH));
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1297,7 +1304,8 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
std::string addrLabel = app->getAddressLabel(a.address);
|
||||
bool isHidden = app->isAddressHidden(a.address);
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
rowInputs.push_back({&a, isZ, isHidden, isFav,
|
||||
bool isMining = app->isMiningAddress(a.address);
|
||||
rowInputs.push_back({&a, isZ, isHidden, isFav, isMining,
|
||||
addrLabel, app->getAddressIcon(a.address),
|
||||
app->getAddressSortOrder(a.address)});
|
||||
}
|
||||
@@ -1646,8 +1654,9 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
{
|
||||
const char* typeLabel = row.isZ ? TR("shielded") : TR("transparent");
|
||||
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
|
||||
const char* miningTag = row.mining ? TR("mining_tag") : "";
|
||||
char typeBuf[64];
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s", typeLabel, hiddenTag);
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
||||
|
||||
// User label next to type
|
||||
@@ -1761,6 +1770,9 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
if (ImGui::MenuItem(TR("set_label"))) {
|
||||
AddressLabelDialog::show(app, addr.address, row.isZ);
|
||||
}
|
||||
if (ImGui::MenuItem(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
|
||||
app->setMiningAddress(addr.address, !row.mining);
|
||||
}
|
||||
if (ImGui::MenuItem(TR("export_private_key")))
|
||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
|
||||
if (row.isZ) {
|
||||
@@ -1869,10 +1881,13 @@ static void RenderSharedRecentTx(App* app, float recentH, float availW, float hs
|
||||
ImGui::Spacing();
|
||||
|
||||
float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
|
||||
int maxTx = std::clamp((int)(recentH / scaledRowH), 2, 5);
|
||||
float availableListH = ImGui::GetContentRegionAvail().y;
|
||||
float listH = std::max({scaledRowH, recentH, availableListH});
|
||||
ImGui::BeginChild("##BalanceSharedRecentRows", ImVec2(0, listH), false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
|
||||
const auto& txs = state.transactions;
|
||||
int count = (int)txs.size();
|
||||
if (count > maxTx) count = maxTx;
|
||||
|
||||
if (count == 0) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No transactions yet");
|
||||
@@ -1923,6 +1938,7 @@ static void RenderSharedRecentTx(App* app, float recentH, float availW, float hs
|
||||
ImGui::Dummy(ImVec2(0, rowH));
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
// Render sync progress bar (used by multiple layouts)
|
||||
|
||||
Reference in New Issue
Block a user