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

@@ -281,6 +281,38 @@ void App::update()
fast_refresh_timer_ = 0.0f;
if (state_.connected && !state_.isLocked()) {
refreshMiningInfo();
// Poll getrescaninfo for rescan progress (if rescan flag is set)
if (state_.sync.rescanning && fast_worker_) {
fast_worker_->post([this]() -> rpc::RPCWorker::MainCb {
try {
auto info = rpc_->call("getrescaninfo");
bool rescanning = info.value("rescanning", false);
float progress = 0.0f;
if (info.contains("rescan_progress")) {
std::string progStr = info["rescan_progress"].get<std::string>();
try { progress = std::stof(progStr) * 100.0f; } catch (...) {}
}
return [this, rescanning, progress]() {
if (rescanning) {
state_.sync.rescanning = true;
if (progress > 0.0f) {
state_.sync.rescan_progress = progress / 100.0f;
}
} else if (state_.sync.rescanning) {
// Rescan just finished
ui::Notifications::instance().success("Blockchain rescan complete");
state_.sync.rescanning = false;
state_.sync.rescan_progress = 1.0f;
state_.sync.rescan_status.clear();
}
};
} catch (...) {
// RPC not available yet or failed
return [](){};
}
});
}
}
// Poll xmrig stats every ~2 seconds (use a simple toggle)
@@ -315,6 +347,100 @@ void App::update()
} else if (xmrig_manager_ && !xmrig_manager_->isRunning()) {
state_.pool_mining.xmrig_running = false;
}
// Check daemon output for rescan progress
if (embedded_daemon_ && embedded_daemon_->isRunning()) {
std::string newOutput = embedded_daemon_->getOutputSince(daemon_output_offset_);
if (!newOutput.empty()) {
// Look for rescan progress patterns in new output
// Hush patterns: "Still rescanning. At block X. Progress=Y" or "Rescanning..." with percentage
bool foundRescan = false;
float rescanPct = 0.0f;
// Search line by line for rescan info
size_t pos = 0;
while (pos < newOutput.size()) {
size_t eol = newOutput.find('\n', pos);
if (eol == std::string::npos) eol = newOutput.size();
std::string line = newOutput.substr(pos, eol - pos);
pos = eol + 1;
// Check for "Rescanning from height" (rescan starting)
if (line.find("Rescanning from height") != std::string::npos ||
line.find("Rescanning last") != std::string::npos) {
foundRescan = true;
state_.sync.rescan_status = line;
}
// Check for "Still rescanning" with progress
auto stillIdx = line.find("Still rescanning");
if (stillIdx != std::string::npos) {
foundRescan = true;
// Try to extract progress (Progress=0.XXXX)
auto progIdx = line.find("Progress=");
if (progIdx != std::string::npos) {
size_t numStart = progIdx + 9; // strlen("Progress=")
size_t numEnd = numStart;
while (numEnd < line.size() && (std::isdigit(line[numEnd]) || line[numEnd] == '.')) {
numEnd++;
}
if (numEnd > numStart) {
try {
rescanPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f;
} catch (...) {}
}
}
state_.sync.rescan_status = line;
}
// Check for "Rescanning..." with percentage (ShowProgress output)
auto rescIdx = line.find("Rescanning...");
if (rescIdx != std::string::npos) {
foundRescan = true;
// Try to extract percentage
auto pctIdx = line.find('%');
if (pctIdx != std::string::npos && pctIdx > 0) {
// Walk backwards to find the number
size_t numEnd = pctIdx;
size_t numStart = numEnd;
while (numStart > 0 && (std::isdigit(line[numStart - 1]) || line[numStart - 1] == '.')) {
numStart--;
}
if (numStart < numEnd) {
try {
rescanPct = std::stof(line.substr(numStart, numEnd - numStart));
} catch (...) {}
}
}
state_.sync.rescan_status = line;
}
// Check for "Done rescanning" (rescan complete)
if (line.find("Done rescanning") != std::string::npos ||
line.find("Rescan complete") != std::string::npos) {
if (state_.sync.rescanning) {
ui::Notifications::instance().success("Blockchain rescan complete");
}
state_.sync.rescanning = false;
state_.sync.rescan_progress = 1.0f;
state_.sync.rescan_status.clear();
}
}
if (foundRescan) {
state_.sync.rescanning = true;
if (rescanPct > 0.0f) {
state_.sync.rescan_progress = rescanPct / 100.0f;
}
}
}
} else if (!embedded_daemon_ || !embedded_daemon_->isRunning()) {
// Clear rescan state if daemon is not running (but preserve during restart)
if (state_.sync.rescanning && state_.sync.rescan_progress >= 0.99f) {
state_.sync.rescanning = false;
state_.sync.rescan_status.clear();
}
}
}
// Regular refresh every 5 seconds
@@ -965,12 +1091,8 @@ void App::render()
renderBackupDialog();
}
// Encrypt wallet / change passphrase dialogs (from Settings)
renderEncryptWalletDialog();
renderDecryptWalletDialog();
// PIN setup / change / remove dialogs (from Settings)
renderPinDialogs();
// Security overlay dialogs (encrypt, decrypt, PIN) are rendered AFTER ImGui::End()
// to ensure they appear on top of all other content
// Send confirm popup
ui::RenderSendConfirmPopup(this);
@@ -1020,6 +1142,11 @@ void App::render()
ImGui::ShowDemoWindow(&show_demo_window_);
}
// Security overlay dialogs (must render LAST to be on top of everything)
renderEncryptWalletDialog();
renderDecryptWalletDialog();
renderPinDialogs();
// Render notifications (toast messages)
ui::Notifications::instance().render();
}
@@ -1111,7 +1238,19 @@ void App::renderStatusBar()
ImGui::SameLine(0, sbSectionGap);
ImGui::TextDisabled("|");
ImGui::SameLine(0, sbSeparatorGap);
if (state_.sync.syncing) {
if (state_.sync.rescanning) {
// Show rescan progress (takes priority over sync)
// Use animated dots if progress is unknown (0%)
if (state_.sync.rescan_progress > 0.01f) {
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Rescanning %.0f%%",
state_.sync.rescan_progress * 100.0f);
} else {
// Animated "Rescanning..." with pulsing dots
int dots = (int)(ImGui::GetTime() * 2.0f) % 4;
const char* dotStr = (dots == 0) ? "." : (dots == 1) ? ".." : (dots == 2) ? "..." : "";
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Rescanning%s", dotStr);
}
} else if (state_.sync.syncing) {
int blocksLeft = state_.sync.headers - state_.sync.blocks;
if (blocksLeft < 0) blocksLeft = 0;
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left)",
@@ -1226,40 +1365,33 @@ void App::renderAboutDialog()
auto it = dlg.extraFloats.find(key);
return it != dlg.extraFloats.end() ? it->second : fb;
};
ImGui::OpenPopup("About ObsidianDragon");
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(dlgF("width", 400.0f), dlg.height > 0 ? dlg.height : 250.0f));
const auto& acrylicTheme = ui::GetCurrentAcrylicTheme();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(ui::Layout::spacingXl(), ui::Layout::spacingLg()));
if (ui::effects::ImGuiAcrylic::BeginAcrylicPopupModal("About ObsidianDragon", &show_about_, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
ui::material::Type().text(ui::material::TypeStyle::H6, DRAGONX_APP_NAME);
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::Text("Version: %s", DRAGONX_VERSION);
ImGui::Text("ImGui: %s", IMGUI_VERSION);
ImGui::Spacing();
ImGui::TextWrapped("A shielded cryptocurrency wallet for DragonX (DRGX), "
"built with Dear ImGui for a lightweight, portable experience.");
ImGui::Spacing();
ImGui::Text("Copyright 2024-2026 The Hush Developers");
ImGui::Text("Released under the GPLv3 License");
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont((int)dlgF("button-font", 1)))) {
show_about_ = false;
ImGui::CloseCurrentPopup();
}
ui::effects::ImGuiAcrylic::EndAcrylicPopup();
if (!ui::material::BeginOverlayDialog("About ObsidianDragon", &show_about_, dlgF("width", 400.0f), 0.94f)) {
return;
}
ImGui::PopStyleVar();
ui::material::Type().text(ui::material::TypeStyle::H6, DRAGONX_APP_NAME);
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::Text("Version: %s", DRAGONX_VERSION);
ImGui::Text("ImGui: %s", IMGUI_VERSION);
ImGui::Spacing();
ImGui::TextWrapped("A shielded cryptocurrency wallet for DragonX (DRGX), "
"built with Dear ImGui for a lightweight, portable experience.");
ImGui::Spacing();
ImGui::Text("Copyright 2024-2026 The Hush Developers");
ImGui::Text("Released under the GPLv3 License");
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont((int)dlgF("button-font", 1)))) {
show_about_ = false;
}
ui::material::EndOverlayDialog();
}
void App::renderImportKeyDialog()
@@ -1269,62 +1401,55 @@ void App::renderImportKeyDialog()
auto it = dlg.extraFloats.find(key);
return it != dlg.extraFloats.end() ? it->second : fb;
};
ImGui::OpenPopup("Import Private Key");
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(dlgF("width", 500.0f), dlg.height > 0 ? dlg.height : 200.0f));
const auto& acrylicTheme = ui::GetCurrentAcrylicTheme();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(ui::Layout::spacingXl(), ui::Layout::spacingLg()));
if (ui::effects::ImGuiAcrylic::BeginAcrylicPopupModal("Import Private Key", &show_import_key_, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
ui::material::Type().text(ui::material::TypeStyle::H6, "Import Private Key");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped("Enter a private key to import. The wallet will rescan the blockchain for transactions.");
ImGui::Spacing();
ImGui::Text("Private Key:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_));
ImGui::Spacing();
if (!import_status_.empty()) {
if (import_success_) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", import_status_.c_str());
} else {
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", import_status_.c_str());
}
}
ImGui::Spacing();
ImGui::Separator();
int btnFont = (int)dlgF("button-font", 1);
float btnW = dlgF("button-width", 120.0f);
if (ui::material::StyledButton("Import", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
std::string key(import_key_input_);
if (!key.empty()) {
importPrivateKey(key, [this](bool success, const std::string& msg) {
import_success_ = success;
import_status_ = msg;
if (success) {
memset(import_key_input_, 0, sizeof(import_key_input_));
}
});
}
}
ImGui::SameLine();
if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
show_import_key_ = false;
import_status_.clear();
memset(import_key_input_, 0, sizeof(import_key_input_));
ImGui::CloseCurrentPopup();
}
ui::effects::ImGuiAcrylic::EndAcrylicPopup();
if (!ui::material::BeginOverlayDialog("Import Private Key", &show_import_key_, dlgF("width", 500.0f), 0.94f)) {
return;
}
ImGui::PopStyleVar();
ui::material::Type().text(ui::material::TypeStyle::H6, "Import Private Key");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped("Enter a private key to import. The wallet will rescan the blockchain for transactions.");
ImGui::Spacing();
ImGui::Text("Private Key:");
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_));
ImGui::Spacing();
if (!import_status_.empty()) {
if (import_success_) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", import_status_.c_str());
} else {
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", import_status_.c_str());
}
}
ImGui::Spacing();
ImGui::Separator();
int btnFont = (int)dlgF("button-font", 1);
float btnW = dlgF("button-width", 120.0f);
if (ui::material::StyledButton("Import", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
std::string key(import_key_input_);
if (!key.empty()) {
importPrivateKey(key, [this](bool success, const std::string& msg) {
import_success_ = success;
import_status_ = msg;
if (success) {
memset(import_key_input_, 0, sizeof(import_key_input_));
}
});
}
}
ImGui::SameLine();
if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
show_import_key_ = false;
import_status_.clear();
memset(import_key_input_, 0, sizeof(import_key_input_));
}
ui::material::EndOverlayDialog();
}
void App::renderExportKeyDialog()
@@ -1334,78 +1459,71 @@ void App::renderExportKeyDialog()
auto it = dlg.extraFloats.find(key);
return it != dlg.extraFloats.end() ? it->second : fb;
};
ImGui::OpenPopup("Export Private Key");
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(dlgF("width", 600.0f), dlg.height > 0 ? dlg.height : 300.0f));
const auto& acrylicTheme = ui::GetCurrentAcrylicTheme();
int btnFont = (int)dlgF("button-font", 1);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(ui::Layout::spacingXl(), ui::Layout::spacingLg()));
if (ui::effects::ImGuiAcrylic::BeginAcrylicPopupModal("Export Private Key", &show_export_key_, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
ui::material::Type().text(ui::material::TypeStyle::H6, "Export Private Key");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f),
"WARNING: Anyone with this key can spend your coins!");
ImGui::Spacing();
// Address selector
ImGui::Text("Select Address:");
std::vector<std::string> all_addrs;
for (const auto& a : state_.t_addresses) all_addrs.push_back(a.address);
for (const auto& a : state_.z_addresses) all_addrs.push_back(a.address);
int addrFrontLen = (int)dlgF("addr-front-len", 20);
int addrBackLen = (int)dlgF("addr-back-len", 8);
if (ImGui::BeginCombo("##exportaddr", export_address_.empty() ? "Select address..." : export_address_.c_str())) {
for (const auto& addr : all_addrs) {
bool selected = (export_address_ == addr);
std::string display = addr.substr(0, addrFrontLen) + "..." + addr.substr(addr.length() - addrBackLen);
if (ImGui::Selectable(display.c_str(), selected)) {
export_address_ = addr;
export_result_.clear();
}
}
ImGui::EndCombo();
}
ImGui::SameLine();
if (ui::material::StyledButton("Export", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
if (!export_address_.empty()) {
exportPrivateKey(export_address_, [this](const std::string& key) {
export_result_ = key;
});
}
}
ImGui::Spacing();
if (!export_result_.empty()) {
ImGui::Text("Private Key:");
ImGui::InputTextMultiline("##exportresult", (char*)export_result_.c_str(),
export_result_.size() + 1, ImVec2(-1, dlgF("key-display-height", 60.0f)), ImGuiInputTextFlags_ReadOnly);
if (ui::material::StyledButton("Copy to Clipboard", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
ImGui::SetClipboardText(export_result_.c_str());
}
}
ImGui::Spacing();
ImGui::Separator();
int closeBtnFont = (int)dlgF("close-button-font", -1);
if (closeBtnFont < 0) closeBtnFont = btnFont;
if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont(closeBtnFont))) {
show_export_key_ = false;
export_result_.clear();
export_address_.clear();
ImGui::CloseCurrentPopup();
}
ui::effects::ImGuiAcrylic::EndAcrylicPopup();
if (!ui::material::BeginOverlayDialog("Export Private Key", &show_export_key_, dlgF("width", 600.0f), 0.94f)) {
return;
}
ImGui::PopStyleVar();
ui::material::Type().text(ui::material::TypeStyle::H6, "Export Private Key");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f),
"WARNING: Anyone with this key can spend your coins!");
ImGui::Spacing();
// Address selector
ImGui::Text("Select Address:");
std::vector<std::string> all_addrs;
for (const auto& a : state_.t_addresses) all_addrs.push_back(a.address);
for (const auto& a : state_.z_addresses) all_addrs.push_back(a.address);
int addrFrontLen = (int)dlgF("addr-front-len", 20);
int addrBackLen = (int)dlgF("addr-back-len", 8);
if (ImGui::BeginCombo("##exportaddr", export_address_.empty() ? "Select address..." : export_address_.c_str())) {
for (const auto& addr : all_addrs) {
bool selected = (export_address_ == addr);
std::string display = addr.substr(0, addrFrontLen) + "..." + addr.substr(addr.length() - addrBackLen);
if (ImGui::Selectable(display.c_str(), selected)) {
export_address_ = addr;
export_result_.clear();
}
}
ImGui::EndCombo();
}
ImGui::SameLine();
if (ui::material::StyledButton("Export", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
if (!export_address_.empty()) {
exportPrivateKey(export_address_, [this](const std::string& key) {
export_result_ = key;
});
}
}
ImGui::Spacing();
if (!export_result_.empty()) {
ImGui::Text("Private Key:");
ImGui::InputTextMultiline("##exportresult", (char*)export_result_.c_str(),
export_result_.size() + 1, ImVec2(-1, dlgF("key-display-height", 60.0f)), ImGuiInputTextFlags_ReadOnly);
if (ui::material::StyledButton("Copy to Clipboard", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
ImGui::SetClipboardText(export_result_.c_str());
}
}
ImGui::Spacing();
ImGui::Separator();
int closeBtnFont = (int)dlgF("close-button-font", -1);
if (closeBtnFont < 0) closeBtnFont = btnFont;
if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont(closeBtnFont))) {
show_export_key_ = false;
export_result_.clear();
export_address_.clear();
}
ui::material::EndOverlayDialog();
}
void App::renderBackupDialog()
@@ -1415,59 +1533,52 @@ void App::renderBackupDialog()
auto it = dlg.extraFloats.find(key);
return it != dlg.extraFloats.end() ? it->second : fb;
};
ImGui::OpenPopup("Backup Wallet");
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(dlgF("width", 500.0f), dlg.height > 0 ? dlg.height : 200.0f));
const auto& acrylicTheme = ui::GetCurrentAcrylicTheme();
int btnFont = (int)dlgF("button-font", 1);
float btnW = dlgF("button-width", 120.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(ui::Layout::spacingXl(), ui::Layout::spacingLg()));
if (ui::effects::ImGuiAcrylic::BeginAcrylicPopupModal("Backup Wallet", &show_backup_, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
ui::material::Type().text(ui::material::TypeStyle::H6, "Backup Wallet");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped("Export all private keys to a file. Keep this file secure!");
ImGui::Spacing();
ImGui::Text("Backup File Path:");
static char backup_path[512] = "dragonx-backup.txt";
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##backuppath", backup_path, sizeof(backup_path));
ImGui::Spacing();
if (!backup_status_.empty()) {
if (backup_success_) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", backup_status_.c_str());
} else {
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", backup_status_.c_str());
}
}
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton("Save Backup", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
std::string path(backup_path);
if (!path.empty()) {
backupWallet(path, [this](bool success, const std::string& msg) {
backup_success_ = success;
backup_status_ = msg;
});
}
}
ImGui::SameLine();
if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
show_backup_ = false;
backup_status_.clear();
ImGui::CloseCurrentPopup();
}
ui::effects::ImGuiAcrylic::EndAcrylicPopup();
if (!ui::material::BeginOverlayDialog("Backup Wallet", &show_backup_, dlgF("width", 500.0f), 0.94f)) {
return;
}
ImGui::PopStyleVar();
ui::material::Type().text(ui::material::TypeStyle::H6, "Backup Wallet");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped("Export all private keys to a file. Keep this file secure!");
ImGui::Spacing();
ImGui::Text("Backup File Path:");
static char backup_path[512] = "dragonx-backup.txt";
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##backuppath", backup_path, sizeof(backup_path));
ImGui::Spacing();
if (!backup_status_.empty()) {
if (backup_success_) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", backup_status_.c_str());
} else {
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", backup_status_.c_str());
}
}
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton("Save Backup", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
std::string path(backup_path);
if (!path.empty()) {
backupWallet(path, [this](bool success, const std::string& msg) {
backup_success_ = success;
backup_status_ = msg;
});
}
}
ImGui::SameLine();
if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
show_backup_ = false;
backup_status_.clear();
}
ui::material::EndOverlayDialog();
}
void App::renderAntivirusHelpDialog()
@@ -1475,57 +1586,46 @@ void App::renderAntivirusHelpDialog()
#ifdef _WIN32
if (!pending_antivirus_dialog_) return;
ImGui::OpenPopup("Windows Defender Blocked Miner");
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(560.0f, 360.0f));
const auto& acrylicTheme = ui::GetCurrentAcrylicTheme();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(ui::Layout::spacingXl(), ui::Layout::spacingLg()));
if (ui::effects::ImGuiAcrylic::BeginAcrylicPopupModal("Windows Defender Blocked Miner", &pending_antivirus_dialog_,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar, acrylicTheme.popup)) {
ui::material::Type().text(ui::material::TypeStyle::H6, "Windows Defender Blocked xmrig");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped(
"Mining software is often flagged as potentially unwanted. "
"Follow these steps to enable pool mining:");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 1: Add Exclusion");
ImGui::BulletText("Open Windows Security > Virus & threat protection");
ImGui::BulletText("Click Manage settings > Exclusions > Add or remove");
ImGui::BulletText("Add folder: %%APPDATA%%\\ObsidianDragon\\");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 2: Restore from Quarantine (if needed)");
ImGui::BulletText("Windows Security > Protection history");
ImGui::BulletText("Find xmrig.exe and click Restore");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 3: Restart wallet and try again");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
ImGui::Separator();
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
float btnW = 160.0f;
if (ui::material::StyledButton("Open Windows Security", ImVec2(btnW, 0))) {
// Open Windows Security app to the exclusions page
ShellExecuteA(NULL, "open", "windowsdefender://threat", NULL, NULL, SW_SHOWNORMAL);
}
ImGui::SameLine();
if (ui::material::StyledButton("Close", ImVec2(100.0f, 0))) {
pending_antivirus_dialog_ = false;
ImGui::CloseCurrentPopup();
}
ui::effects::ImGuiAcrylic::EndAcrylicPopup();
if (!ui::material::BeginOverlayDialog("Windows Defender Blocked Miner", &pending_antivirus_dialog_, 560.0f, 0.94f)) {
return;
}
ImGui::PopStyleVar();
ui::material::Type().text(ui::material::TypeStyle::H6, "Windows Defender Blocked xmrig");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped(
"Mining software is often flagged as potentially unwanted. "
"Follow these steps to enable pool mining:");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 1: Add Exclusion");
ImGui::BulletText("Open Windows Security > Virus & threat protection");
ImGui::BulletText("Click Manage settings > Exclusions > Add or remove");
ImGui::BulletText("Add folder: %%APPDATA%%\\ObsidianDragon\\");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 2: Restore from Quarantine (if needed)");
ImGui::BulletText("Windows Security > Protection history");
ImGui::BulletText("Find xmrig.exe and click Restore");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ui::material::Type().text(ui::material::TypeStyle::Subtitle2, "Step 3: Restart wallet and try again");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
ImGui::Separator();
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
float btnW = 160.0f;
if (ui::material::StyledButton("Open Windows Security", ImVec2(btnW, 0))) {
// Open Windows Security app to the exclusions page
ShellExecuteA(NULL, "open", "windowsdefender://threat", NULL, NULL, SW_SHOWNORMAL);
}
ImGui::SameLine();
if (ui::material::StyledButton("Close", ImVec2(100.0f, 0))) {
pending_antivirus_dialog_ = false;
}
ui::material::EndOverlayDialog();
#endif
}
@@ -1723,6 +1823,43 @@ bool App::isEmbeddedDaemonRunning() const
return embedded_daemon_ && embedded_daemon_->isRunning();
}
void App::rescanBlockchain()
{
if (!isUsingEmbeddedDaemon() || !embedded_daemon_) {
ui::Notifications::instance().warning(
"Rescan requires embedded daemon. Restart your daemon with -rescan manually.");
return;
}
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
ui::Notifications::instance().info("Restarting daemon with -rescan flag...");
// Initialize rescan state for status bar display
state_.sync.rescanning = true;
state_.sync.rescan_progress = 0.0f;
state_.sync.rescan_status = "Starting rescan...";
// Set rescan flag BEFORE stopping so it's ready when we restart
embedded_daemon_->setRescanOnNextStart(true);
DEBUG_LOGF("[App] Rescan flag set, rescanOnNextStart=%d\n", embedded_daemon_->rescanOnNextStart() ? 1 : 0);
// Stop daemon, then restart
std::thread([this]() {
DEBUG_LOGF("[App] Stopping daemon for rescan...\n");
stopEmbeddedDaemon();
// Wait for daemon to fully stop
DEBUG_LOGF("[App] Waiting for daemon to fully stop...\n");
std::this_thread::sleep_for(std::chrono::seconds(3));
// Reset output offset so we parse fresh output for rescan progress
daemon_output_offset_ = 0;
DEBUG_LOGF("[App] Starting daemon with rescan flag=%d\n", embedded_daemon_->rescanOnNextStart() ? 1 : 0);
startEmbeddedDaemon();
}).detach();
}
double App::getDaemonMemoryUsageMB() const
{
// If we have an embedded daemon with a tracked process handle, use it