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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user