feat: blockchain rescan via daemon restart + status bar progress

- Fix z_importwallet to use full path instead of filename only
- Add rescanBlockchain() method that restarts daemon with -rescan flag
- Track rescan progress via daemon output parsing and getrescaninfo RPC
- Display rescan progress in status bar with animated indicator when starting
- Improve dark theme card contrast: lighter surface-variant, tinted borders, stronger rim-light
This commit is contained in:
2026-02-28 15:06:35 -06:00
parent f5378a55ed
commit 1a5c4e8744
42 changed files with 1113 additions and 687 deletions

View File

@@ -108,6 +108,7 @@ static bool sp_stop_external_daemon = false;
static std::set<std::string> sp_debug_categories;
static bool sp_debug_cats_dirty = false; // true when changed but daemon not yet restarted
static bool sp_debug_expanded = false; // collapsible card state
static bool sp_confirm_clear_ztx = false; // confirmation dialog for clearing z-tx history
// (APPEARANCE card now uses ChannelsSplit like all other cards)
@@ -969,46 +970,41 @@ void RenderSettingsPage(App* app) {
ImGui::Indent(pad);
float contentW = availWidth - pad * 2;
bool wideBtns = availWidth >= S.drawElement("components.settings-page", "compact-breakpoint").size;
// Content-aware button sizing: uniform per-row width based on widest label
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(130.0f);
float btnSpacing = Layout::spacingMd();
float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f);
auto rowBtnW = [&](std::initializer_list<const char*> labels) -> float {
float maxTextW = 0;
for (auto* l : labels) maxTextW = std::max(maxTextW, ImGui::CalcTextSize(l).x);
return std::max(minBtnW, maxTextW + btnPad * 2);
};
// Calculate button width that fits available space
// 6 buttons total: on wide screens 3+3, on narrow screens 2+2+2
int btnsPerRow = (contentW >= 600.0f) ? 3 : 2;
float bw = (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow;
// Clamp to reasonable size
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(100.0f);
bw = std::max(minBtnW, bw);
// Row 1 — Tools & Actions
{
float bw = rowBtnW({"Address Book...", "Validate Address...", "Request Payment...", "Shield Mining...", "Merge to Address...", "Clear Z-Tx History"});
if (TactileButton("Address Book...", ImVec2(bw, 0), S.resolveFont("button")))
AddressBookDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Manage saved addresses for quick sending");
ImGui::SameLine(0, Layout::spacingMd());
ImGui::SameLine(0, btnSpacing);
if (TactileButton("Validate Address...", ImVec2(bw, 0), S.resolveFont("button")))
ValidateAddressDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Check if a DragonX address is valid");
ImGui::SameLine(0, Layout::spacingMd());
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
if (TactileButton("Request Payment...", ImVec2(bw, 0), S.resolveFont("button")))
RequestPaymentDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Generate a payment request with QR code");
if (wideBtns) ImGui::SameLine(0, Layout::spacingMd()); else ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (btnsPerRow >= 3) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } else { ImGui::SameLine(0, btnSpacing); }
if (TactileButton("Shield Mining...", ImVec2(bw, 0), S.resolveFont("button")))
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Move transparent mining rewards to a shielded address");
ImGui::SameLine(0, Layout::spacingMd());
ImGui::SameLine(0, btnSpacing);
if (TactileButton("Merge to Address...", ImVec2(bw, 0), S.resolveFont("button")))
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Consolidate multiple UTXOs into one address");
ImGui::SameLine(0, Layout::spacingMd());
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
if (TactileButton("Clear Z-Tx History", ImVec2(bw, 0), S.resolveFont("button"))) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file))
Notifications::instance().success("Z-transaction history cleared");
else
Notifications::instance().info("No history file found");
sp_confirm_clear_ztx = true;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Delete locally cached z-transaction history");
}
@@ -1348,24 +1344,7 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Verify the RPC connection to the daemon");
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton("Rescan Blockchain", ImVec2(nodeBtnW, 0), btnFont)) {
if (app->rpc() && app->rpc()->isConnected() && app->worker()) {
Notifications::instance().info("Starting blockchain rescan...");
app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
try {
rpc->call("rescanblockchain", {0});
return []() {
Notifications::instance().success("Blockchain rescan started");
};
} catch (const std::exception& e) {
std::string err = e.what();
return [err]() {
Notifications::instance().error("Failed to start rescan: " + err);
};
}
});
} else {
Notifications::instance().warning("Not connected to daemon");
}
app->rescanBlockchain();
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Rescan the blockchain for missing transactions");
ImGui::EndDisabled();
@@ -1839,6 +1818,48 @@ void RenderSettingsPage(App* app) {
ImGui::EndChild(); // ##SettingsPageScroll
// Confirmation dialog for clearing z-tx history
if (sp_confirm_clear_ztx) {
if (BeginOverlayDialog("Confirm Clear Z-Tx History", &sp_confirm_clear_ztx, 480.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Warning");
ImGui::Spacing();
ImGui::TextWrapped(
"Clearing z-transaction history may cause your shielded balance to show as 0 "
"until a wallet rescan is performed.");
ImGui::Spacing();
ImGui::TextWrapped(
"If this happens, you will need to re-import your z-address private keys with "
"rescan enabled to recover your balance.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button("Cancel", ImVec2(btnW, 40))) {
sp_confirm_clear_ztx = false;
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
if (ImGui::Button("Clear Anyway", ImVec2(btnW, 40))) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared");
} else {
Notifications::instance().info("No history file found");
}
sp_confirm_clear_ztx = false;
}
ImGui::PopStyleColor(2);
EndOverlayDialog();
}
}
}
} // namespace ui