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:
dan_s
2026-02-28 15:06:35 -06:00
parent f5378a55ed
commit 4b815fc9d1
42 changed files with 1113 additions and 687 deletions

View File

@@ -16,6 +16,8 @@
#include "ui/material/typography.h"
#include "ui/material/draw_helpers.h"
#include "ui/schema/ui_schema.h"
#include "ui/theme.h"
#include "ui/effects/imgui_acrylic.h"
#include "util/platform.h"
#include "util/secure_vault.h"
#include "util/perf_log.h"
@@ -694,21 +696,18 @@ void App::renderLockScreen() {
void App::renderEncryptWalletDialog() {
if (!show_encrypt_dialog_ && !show_change_passphrase_) return;
using namespace ui::material;
// Encrypt wallet dialog — multi-phase: passphrase → encrypting → PIN setup
if (show_encrypt_dialog_) {
const char* dlgTitle = (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup)
? "Quick-Unlock PIN##EncDlg" : "Encrypt Wallet##EncDlg";
? "Quick-Unlock PIN" : "Encrypt Wallet";
// Prevent closing via X button while encrypting
bool canClose = (encrypt_dialog_phase_ != EncryptDialogPhase::Encrypting);
bool* pOpen = canClose ? &show_encrypt_dialog_ : nullptr;
ImGui::SetNextWindowSize(ImVec2(460, 0), ImGuiCond_FirstUseEver);
ImGuiWindowFlags dlgFlags = ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_AlwaysAutoResize;
if (ImGui::Begin(dlgTitle, pOpen, dlgFlags)) {
if (BeginOverlayDialog(dlgTitle, pOpen, 460.0f, 0.94f)) {
// ---- Phase 1: Passphrase entry ----
if (encrypt_dialog_phase_ == EncryptDialogPhase::PassphraseEntry) {
@@ -911,8 +910,8 @@ void App::renderEncryptWalletDialog() {
show_encrypt_dialog_ = false;
}
}
EndOverlayDialog();
}
ImGui::End();
// Clean up saved passphrase if dialog was closed via X button
if (!show_encrypt_dialog_ && !enc_dlg_saved_passphrase_.empty()) {
@@ -924,9 +923,8 @@ void App::renderEncryptWalletDialog() {
// Change passphrase dialog
if (show_change_passphrase_) {
ImGui::SetNextWindowSize(ImVec2(440, 320), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Change Passphrase##ChgDlg", &show_change_passphrase_,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking)) {
if (BeginOverlayDialog("Change Passphrase", &show_change_passphrase_, 440.0f, 0.94f)) {
ImGui::Text("Current Passphrase:");
ImGui::PushItemWidth(-1);
ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_),
@@ -959,8 +957,8 @@ void App::renderEncryptWalletDialog() {
std::string(change_new_pass_buf_));
}
ImGui::EndDisabled();
EndOverlayDialog();
}
ImGui::End();
}
}
@@ -978,16 +976,10 @@ void App::renderDecryptWalletDialog() {
if (!show_decrypt_dialog_) return;
using namespace ui::material;
const char* title = "Remove Wallet Encryption##DecryptDlg";
bool canClose = (decrypt_phase_ != 1); // don't close while working
bool* pOpen = canClose ? &show_decrypt_dialog_ : nullptr;
ImGui::SetNextWindowSize(ImVec2(480, 0), ImGuiCond_FirstUseEver);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_AlwaysAutoResize;
if (ImGui::Begin(title, pOpen, flags)) {
if (BeginOverlayDialog("Remove Wallet Encryption", pOpen, 480.0f, 0.94f)) {
// ---- Phase 0: Passphrase entry ----
if (decrypt_phase_ == 0) {
@@ -1026,6 +1018,7 @@ void App::renderDecryptWalletDialog() {
std::string passphrase(decrypt_pass_buf_);
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
decrypt_phase_ = 1;
decrypt_step_ = 0;
decrypt_in_progress_ = true;
decrypt_status_ = "Unlocking wallet...";
@@ -1044,129 +1037,163 @@ void App::renderDecryptWalletDialog() {
};
}
// Step 2: Export wallet to temp file
std::string exportFile = "obsidian_decrypt_export_" +
std::to_string(std::time(nullptr));
// Update step on main thread
return [this]() {
decrypt_step_ = 1;
decrypt_status_ = "Exporting wallet keys...";
// Continue with step 2
worker_->post([this]() -> rpc::RPCWorker::MainCb {
std::string dataDir = util::Platform::getDragonXDataDir();
std::string exportFile = "obsidiandecryptexport" +
std::to_string(std::time(nullptr));
std::string exportPath = dataDir + exportFile;
// Update status on main thread
// (we can't easily do mid-flow updates from worker,
// so we just proceed — the UI shows "working")
try {
rpc_->call("z_exportwallet", {exportFile});
} catch (const std::exception& e) {
std::string err = e.what();
return [this, err]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Export failed: " + err;
decrypt_phase_ = 3;
};
}
// Step 3: Stop daemon
try {
rpc_->call("stop");
} catch (...) {
// stop often throws because connection drops
}
// Wait for daemon to fully stop
std::this_thread::sleep_for(std::chrono::seconds(3));
// Step 4: Rename encrypted wallet.dat → wallet.dat.encrypted.bak
std::string dataDir = util::Platform::getDragonXDataDir();
std::string walletPath = dataDir + "wallet.dat";
std::string backupPath = dataDir + "wallet.dat.encrypted.bak";
std::error_code ec;
if (std::filesystem::exists(walletPath, ec)) {
// Remove old backup if exists
std::filesystem::remove(backupPath, ec);
std::filesystem::rename(walletPath, backupPath, ec);
if (ec) {
std::string err = ec.message();
return [this, err]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Failed to rename wallet.dat: " + err;
decrypt_phase_ = 3;
};
}
}
// Step 5: Restart daemon (creates fresh unencrypted wallet)
return [this, exportFile]() {
decrypt_status_ = "Restarting daemon...";
auto restartAndImport = [this, exportFile]() {
// Give daemon time to stop fully
std::this_thread::sleep_for(std::chrono::seconds(2));
if (isUsingEmbeddedDaemon()) {
stopEmbeddedDaemon();
std::this_thread::sleep_for(std::chrono::seconds(1));
startEmbeddedDaemon();
}
// Wait for daemon to become available
int maxWait = 60; // seconds
bool daemonUp = false;
for (int i = 0; i < maxWait; i++) {
std::this_thread::sleep_for(std::chrono::seconds(1));
try {
rpc_->call("getinfo");
daemonUp = true;
break;
} catch (...) {}
}
if (!daemonUp) {
// Schedule error on main thread — can't directly update
// but we'll let the import attempt fail
}
// Step 6: Import wallet (includes rescan)
try {
rpc_->call("z_importwallet", {exportFile});
rpc_->call("z_exportwallet", {exportFile});
} catch (const std::exception& e) {
std::string err = e.what();
// Post result back to main thread via worker
if (worker_) {
worker_->post([this, err]() -> rpc::RPCWorker::MainCb {
return [this, err]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Import failed: " + err +
"\nYour encrypted wallet backup is at wallet.dat.encrypted.bak";
decrypt_phase_ = 3;
};
});
}
return;
return [this, err]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Export failed: " + err;
decrypt_phase_ = 3;
};
}
// Success — post to main thread
if (worker_) {
worker_->post([this]() -> rpc::RPCWorker::MainCb {
return [this]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Wallet decrypted successfully!";
decrypt_phase_ = 2;
return [this, exportPath]() {
decrypt_step_ = 2;
decrypt_status_ = "Stopping daemon...";
// Continue with step 3
worker_->post([this, exportPath]() -> rpc::RPCWorker::MainCb {
try {
rpc_->call("stop");
} catch (...) {
// stop often throws because connection drops
}
// Clean up PIN vault since encryption is gone
if (vault_ && vault_->hasVault()) {
vault_->removeVault();
}
if (settings_ && settings_->getPinEnabled()) {
settings_->setPinEnabled(false);
settings_->save();
}
// Wait for daemon to fully stop
std::this_thread::sleep_for(std::chrono::seconds(3));
refreshWalletEncryptionState();
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
return [this, exportPath]() {
decrypt_step_ = 3;
decrypt_status_ = "Backing up encrypted wallet...";
// Continue with step 4 (rename)
worker_->post([this, exportPath]() -> rpc::RPCWorker::MainCb {
std::string dataDir = util::Platform::getDragonXDataDir();
std::string walletPath = dataDir + "wallet.dat";
std::string backupPath = dataDir + "wallet.dat.encrypted.bak";
std::error_code ec;
if (std::filesystem::exists(walletPath, ec)) {
std::filesystem::remove(backupPath, ec);
std::filesystem::rename(walletPath, backupPath, ec);
if (ec) {
std::string err = ec.message();
return [this, err]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Failed to rename wallet.dat: " + err;
decrypt_phase_ = 3;
};
}
}
return [this, exportPath]() {
decrypt_step_ = 4;
decrypt_status_ = "Restarting daemon...";
auto restartAndImport = [this, exportPath]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
if (isUsingEmbeddedDaemon()) {
stopEmbeddedDaemon();
std::this_thread::sleep_for(std::chrono::seconds(1));
startEmbeddedDaemon();
}
// Wait for daemon to become available
int maxWait = 60;
bool daemonUp = false;
for (int i = 0; i < maxWait; i++) {
std::this_thread::sleep_for(std::chrono::seconds(1));
try {
rpc_->call("getinfo");
daemonUp = true;
break;
} catch (...) {}
}
if (!daemonUp) {
if (worker_) {
worker_->post([this]() -> rpc::RPCWorker::MainCb {
return [this]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Daemon failed to restart";
decrypt_phase_ = 3;
};
});
}
return;
}
// Update step on main thread
if (worker_) {
worker_->post([this]() -> rpc::RPCWorker::MainCb {
return [this]() {
decrypt_step_ = 5;
decrypt_status_ = "Importing keys (this may take a while)...";
};
});
}
// Step 6: Import wallet (use full path)
try {
rpc_->call("z_importwallet", {exportPath});
} catch (const std::exception& e) {
std::string err = e.what();
if (worker_) {
worker_->post([this, err]() -> rpc::RPCWorker::MainCb {
return [this, err]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Import failed: " + err +
"\nYour encrypted wallet backup is at wallet.dat.encrypted.bak";
decrypt_phase_ = 3;
};
});
}
return;
}
// Success
if (worker_) {
worker_->post([this]() -> rpc::RPCWorker::MainCb {
return [this]() {
decrypt_in_progress_ = false;
decrypt_status_ = "Wallet decrypted successfully!";
decrypt_phase_ = 2;
if (vault_ && vault_->hasVault()) {
vault_->removeVault();
}
if (settings_ && settings_->getPinEnabled()) {
settings_->setPinEnabled(false);
settings_->save();
}
refreshWalletEncryptionState();
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
};
});
}
};
std::thread(restartAndImport).detach();
};
});
};
});
}
};
std::thread(restartAndImport).detach();
};
});
};
});
}
@@ -1181,7 +1208,45 @@ void App::renderDecryptWalletDialog() {
// ---- Phase 1: Working ----
} else if (decrypt_phase_ == 1) {
ImGui::Text("%s", decrypt_status_.empty() ? "Working..." : decrypt_status_.c_str());
// Step checklist
const char* stepLabels[] = {
"Unlocking wallet",
"Exporting wallet keys",
"Stopping daemon",
"Backing up encrypted wallet",
"Restarting daemon",
"Importing keys (rescan)"
};
const int numSteps = 6;
ImGui::Spacing();
for (int i = 0; i < numSteps; i++) {
ImGui::PushFont(Type().iconMed());
if (i < decrypt_step_) {
// Completed
ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.4f, 1.0f), ICON_MD_CHECK_CIRCLE);
} else if (i == decrypt_step_) {
// In progress - animate
float alpha = 0.5f + 0.5f * sinf((float)ImGui::GetTime() * 4.0f);
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, alpha), ICON_MD_PENDING);
} else {
// Not started
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 0.5f), ICON_MD_RADIO_BUTTON_UNCHECKED);
}
ImGui::PopFont();
ImGui::SameLine();
if (i == decrypt_step_) {
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.6f, 1.0f), "%s...", stepLabels[i]);
} else if (i < decrypt_step_) {
ImGui::TextColored(ImVec4(0.6f, 0.8f, 0.6f, 1.0f), "%s", stepLabels[i]);
} else {
ImGui::TextDisabled("%s", stepLabels[i]);
}
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Indeterminate progress bar
@@ -1242,6 +1307,7 @@ void App::renderDecryptWalletDialog() {
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button("Try Again", ImVec2(btnW, 40))) {
decrypt_phase_ = 0;
decrypt_step_ = 0;
decrypt_status_.clear();
}
ImGui::SameLine();
@@ -1249,8 +1315,8 @@ void App::renderDecryptWalletDialog() {
show_decrypt_dialog_ = false;
}
}
EndOverlayDialog();
}
ImGui::End();
}
// ===========================================================================
@@ -1258,11 +1324,12 @@ void App::renderDecryptWalletDialog() {
// ===========================================================================
void App::renderPinDialogs() {
using namespace ui::material;
// ---- Set PIN dialog ----
if (show_pin_setup_) {
ImGui::SetNextWindowSize(ImVec2(420, 340), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Set PIN##PinSetupDlg", &show_pin_setup_,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking)) {
if (BeginOverlayDialog("Set PIN", &show_pin_setup_, 420.0f, 0.94f)) {
ImGui::TextWrapped(
"Set a 4-8 digit PIN for quick wallet unlock. "
"Your wallet passphrase will be encrypted with this PIN "
@@ -1352,15 +1419,14 @@ void App::renderPinDialogs() {
}
}
ImGui::EndDisabled();
EndOverlayDialog();
}
ImGui::End();
}
// ---- Change PIN dialog ----
if (show_pin_change_) {
ImGui::SetNextWindowSize(ImVec2(420, 300), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Change PIN##PinChangeDlg", &show_pin_change_,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking)) {
if (BeginOverlayDialog("Change PIN", &show_pin_change_, 420.0f, 0.94f)) {
ImGui::TextWrapped("Change your unlock PIN. You need your current PIN and a new PIN.");
ImGui::Spacing();
ImGui::Separator();
@@ -1426,15 +1492,14 @@ void App::renderPinDialogs() {
}
}
ImGui::EndDisabled();
EndOverlayDialog();
}
ImGui::End();
}
// ---- Remove PIN dialog ----
if (show_pin_remove_) {
ImGui::SetNextWindowSize(ImVec2(400, 220), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Remove PIN##PinRemoveDlg", &show_pin_remove_,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking)) {
if (BeginOverlayDialog("Remove PIN", &show_pin_remove_, 400.0f, 0.94f)) {
ImGui::TextWrapped(
"Enter your current PIN to confirm removal. "
"You will need to use your full passphrase to unlock.");
@@ -1490,8 +1555,8 @@ void App::renderPinDialogs() {
}
}
ImGui::EndDisabled();
EndOverlayDialog();
}
ImGui::End();
}
}