Refactor app services and stabilize refresh/UI flows
- Add refresh scheduler and network refresh service boundaries for typed refresh results, ordered RPC collectors, applicators, and price parsing. - Add daemon lifecycle and wallet security workflow helpers while preserving App-owned command RPC, decrypt, cancellation, and UI handoff behavior. - Split balance, console, mining, amount formatting, and async task logic into focused modules with expanded Phase 4 test coverage. - Fix market price loading by triggering price refresh immediately, avoiding queue-pressure drops, tracking loading/error state, and adding translations. - Polish send, explorer, peers, settings, theme/schema, and related tab UI. - Replace checked-in generated language headers with build-generated resources. - Document the cleanup audit, UI static-state guidance, and architecture updates.
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
#include "rpc/rpc_client.h"
|
||||
#include "rpc/rpc_worker.h"
|
||||
#include "rpc/connection.h"
|
||||
#include "services/wallet_security_workflow_executor.h"
|
||||
#include "config/settings.h"
|
||||
#include "daemon/embedded_daemon.h"
|
||||
#include "ui/notifications.h"
|
||||
@@ -27,13 +28,196 @@
|
||||
|
||||
#include "imgui.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
namespace dragonx {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace {
|
||||
|
||||
class WalletSecurityRpcAdapter : public services::WalletSecurityController::RpcGateway {
|
||||
public:
|
||||
explicit WalletSecurityRpcAdapter(rpc::RPCClient* rpc) : rpc_(rpc) {}
|
||||
|
||||
bool encryptWallet(const std::string& passphrase, std::string& error) override {
|
||||
return callWithError([&] { rpc_->call("encryptwallet", {passphrase}); }, error);
|
||||
}
|
||||
|
||||
bool unlockWallet(const std::string& passphrase, int timeoutSeconds, std::string& error) override {
|
||||
return callWithError([&] { rpc_->call("walletpassphrase", {passphrase, timeoutSeconds}); }, error);
|
||||
}
|
||||
|
||||
bool exportWallet(const std::string& fileName, long timeoutSeconds, std::string& error) override {
|
||||
return callWithError([&] { rpc_->call("z_exportwallet", {fileName}, timeoutSeconds); }, error);
|
||||
}
|
||||
|
||||
bool importWallet(const std::string& filePath, long timeoutSeconds, std::string& error) override {
|
||||
return callWithError([&] { rpc_->call("z_importwallet", {filePath}, timeoutSeconds); }, error);
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename Fn>
|
||||
bool callWithError(Fn&& fn, std::string& error) {
|
||||
if (!rpc_) {
|
||||
error = "RPC client unavailable";
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
fn();
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
error = e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
rpc::RPCClient* rpc_ = nullptr;
|
||||
};
|
||||
|
||||
class WalletSecurityVaultAdapter : public services::WalletSecurityController::VaultGateway {
|
||||
public:
|
||||
explicit WalletSecurityVaultAdapter(util::SecureVault* vault) : vault_(vault) {}
|
||||
|
||||
bool storePin(const std::string& pin, const std::string& passphrase) override {
|
||||
return vault_ && vault_->store(pin, passphrase);
|
||||
}
|
||||
|
||||
private:
|
||||
util::SecureVault* vault_ = nullptr;
|
||||
};
|
||||
|
||||
class WalletSecurityDecryptRpcAdapter : public services::WalletSecurityWorkflowExecutor::RpcGateway {
|
||||
public:
|
||||
using StopFn = std::function<bool(rpc::RPCClient&, const char*)>;
|
||||
|
||||
WalletSecurityDecryptRpcAdapter(rpc::RPCClient* rpc, StopFn stopFn)
|
||||
: rpc_(rpc), stopFn_(std::move(stopFn)) {}
|
||||
|
||||
bool unlockWallet(const std::string& passphrase, int timeoutSeconds, std::string& error) override {
|
||||
return callWithError([&] { rpc_->call("walletpassphrase", {passphrase, timeoutSeconds}); }, error);
|
||||
}
|
||||
|
||||
bool exportWallet(const std::string& fileName, long timeoutSeconds, std::string& error) override {
|
||||
return callWithError([&] { rpc_->call("z_exportwallet", {fileName}, timeoutSeconds); }, error);
|
||||
}
|
||||
|
||||
bool requestDaemonStop(std::string& error) override {
|
||||
if (!rpc_) {
|
||||
error = "RPC client unavailable";
|
||||
return false;
|
||||
}
|
||||
bool ok = stopFn_ ? stopFn_(*rpc_, "Decrypt export daemon stop") : false;
|
||||
if (!ok) error = "Stop RPC failed";
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool probeDaemon(std::string& error) override {
|
||||
return callWithError([&] { rpc_->call("getinfo"); }, error);
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename Fn>
|
||||
bool callWithError(Fn&& fn, std::string& error) {
|
||||
if (!rpc_) {
|
||||
error = "RPC client unavailable";
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
fn();
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
error = e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
rpc::RPCClient* rpc_ = nullptr;
|
||||
StopFn stopFn_;
|
||||
};
|
||||
|
||||
class WalletSecurityImportRpcAdapter : public services::WalletSecurityWorkflowExecutor::ImportGateway {
|
||||
public:
|
||||
WalletSecurityImportRpcAdapter(rpc::RPCClient* fallbackRpc, rpc::ConnectionConfig config)
|
||||
: fallbackRpc_(fallbackRpc), config_(std::move(config)) {}
|
||||
|
||||
bool importWallet(const std::string& exportPath, long timeoutSeconds, std::string& error) override {
|
||||
auto importRpc = std::make_unique<rpc::RPCClient>();
|
||||
bool importRpcOk = importRpc->connect(config_.host, config_.port,
|
||||
config_.rpcuser, config_.rpcpassword,
|
||||
config_.use_tls);
|
||||
if (!importRpcOk) importRpc.reset();
|
||||
|
||||
auto* rpcForImport = importRpc ? importRpc.get() : fallbackRpc_;
|
||||
if (!rpcForImport) {
|
||||
error = "RPC client unavailable";
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
rpcForImport->call("z_importwallet", {exportPath}, timeoutSeconds);
|
||||
if (importRpc) importRpc->disconnect();
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
if (importRpc) importRpc->disconnect();
|
||||
error = e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
rpc::RPCClient* fallbackRpc_ = nullptr;
|
||||
rpc::ConnectionConfig config_;
|
||||
};
|
||||
|
||||
class WalletSecurityFileAdapter : public services::WalletSecurityWorkflowExecutor::FileGateway {
|
||||
public:
|
||||
std::string dataDir() override { return util::Platform::getDragonXDataDir(); }
|
||||
|
||||
bool backupEncryptedWallet(const services::WalletSecurityWorkflowExecutor::WalletFilePlan& filePlan,
|
||||
std::string& error) override {
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(filePlan.walletPath, ec)) return true;
|
||||
|
||||
std::filesystem::remove(filePlan.backupPath, ec);
|
||||
ec.clear();
|
||||
std::filesystem::rename(filePlan.walletPath, filePlan.backupPath, ec);
|
||||
if (ec) {
|
||||
error = ec.message();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class WalletSecurityDaemonAdapter : public services::WalletSecurityWorkflowExecutor::DaemonGateway {
|
||||
public:
|
||||
WalletSecurityDaemonAdapter(App& app, const util::AsyncTaskManager::Token& token)
|
||||
: app_(app), token_(token) {}
|
||||
|
||||
bool isUsingEmbeddedDaemon() const override { return app_.isUsingEmbeddedDaemon(); }
|
||||
void stopEmbeddedDaemon() override { app_.stopEmbeddedDaemon(); }
|
||||
bool startEmbeddedDaemon() override { return app_.startEmbeddedDaemon(); }
|
||||
bool cancelled() const override { return token_.cancelled(); }
|
||||
bool shuttingDown() const override { return app_.isShuttingDown(); }
|
||||
void sleepForMs(int milliseconds) override {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(std::max(0, milliseconds)));
|
||||
}
|
||||
|
||||
private:
|
||||
App& app_;
|
||||
const util::AsyncTaskManager::Token& token_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
// Wallet encryption helpers
|
||||
@@ -45,9 +229,11 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||
encrypt_status_ = "Encrypting wallet...";
|
||||
|
||||
if (worker_) {
|
||||
worker_->post([this, passphrase]() -> rpc::RPCWorker::MainCb {
|
||||
try {
|
||||
auto result = rpc_->call("encryptwallet", {passphrase});
|
||||
worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb {
|
||||
WalletSecurityRpcAdapter rpcAdapter(rpc_.get());
|
||||
auto result = wallet_security_.runDeferredEncryption(
|
||||
{std::move(passphrase), {}}, rpcAdapter, nullptr);
|
||||
if (result.encrypted) {
|
||||
return [this]() {
|
||||
encrypt_in_progress_ = false;
|
||||
encrypt_status_ = "Wallet encrypted. Restarting daemon...";
|
||||
@@ -78,22 +264,22 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||
connection_status_ = TR("restarting_after_encryption");
|
||||
// Give daemon a moment to shut down, then restart
|
||||
// (do this off the main thread to avoid stalling the UI)
|
||||
std::thread([this]() {
|
||||
for (int i = 0; i < 20 && !shutting_down_; ++i)
|
||||
async_tasks_.submit("encrypt-daemon-restart", [this](const util::AsyncTaskManager::Token& token) {
|
||||
for (int i = 0; i < 20 && !token.cancelled() && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (shutting_down_) return;
|
||||
if (token.cancelled() || shutting_down_) return;
|
||||
stopEmbeddedDaemon();
|
||||
if (shutting_down_) return;
|
||||
if (token.cancelled() || shutting_down_) return;
|
||||
startEmbeddedDaemon();
|
||||
// tryConnect will be called by the update loop
|
||||
}).detach();
|
||||
});
|
||||
} else {
|
||||
ui::Notifications::instance().warning(
|
||||
"Please restart your daemon for encryption to take effect.");
|
||||
}
|
||||
};
|
||||
} catch (const std::exception& e) {
|
||||
std::string err = e.what();
|
||||
} else {
|
||||
std::string err = result.error;
|
||||
return [this, err]() {
|
||||
encrypt_in_progress_ = false;
|
||||
encrypt_status_ = "Encryption failed: " + err;
|
||||
@@ -118,15 +304,11 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
|
||||
// Called every frame from render() until the task completes.
|
||||
// ---------------------------------------------------------------------------
|
||||
void App::processDeferredEncryption() {
|
||||
if (!deferred_encrypt_pending_) return;
|
||||
if (!wallet_security_.hasDeferredEncryption()) return;
|
||||
|
||||
// Phase 1: wait for daemon connection
|
||||
if (!state_.connected || !rpc_ || !rpc_->isConnected()) {
|
||||
// Throttle connection attempts to every 3 seconds
|
||||
static double s_lastAttempt = -10.0;
|
||||
double now = ImGui::GetTime();
|
||||
if (now - s_lastAttempt >= 3.0) {
|
||||
s_lastAttempt = now;
|
||||
if (wallet_security_.shouldAttemptDeferredConnect(ImGui::GetTime())) {
|
||||
if (!connection_in_progress_) {
|
||||
// Just try to connect — tryConnect is now async
|
||||
tryConnect();
|
||||
@@ -140,31 +322,29 @@ void App::processDeferredEncryption() {
|
||||
|
||||
// Phase 2: connected — launch encryption
|
||||
if (!encrypt_in_progress_) {
|
||||
std::string passphrase = deferred_encrypt_passphrase_;
|
||||
std::string pin = deferred_encrypt_pin_;
|
||||
auto deferredEncryption = wallet_security_.deferredEncryption();
|
||||
std::string passphrase = std::move(deferredEncryption.passphrase);
|
||||
std::string pin = std::move(deferredEncryption.pin);
|
||||
|
||||
encrypt_in_progress_ = true;
|
||||
encrypt_status_ = "Encrypting wallet...";
|
||||
|
||||
if (worker_) {
|
||||
worker_->post([this, passphrase, pin]() -> rpc::RPCWorker::MainCb {
|
||||
try {
|
||||
rpc_->call("encryptwallet", {passphrase});
|
||||
worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb {
|
||||
WalletSecurityRpcAdapter rpcAdapter(rpc_.get());
|
||||
WalletSecurityVaultAdapter vaultAdapter(vault_.get());
|
||||
auto result = wallet_security_.runDeferredEncryption(
|
||||
std::move(request), rpcAdapter, vault_ ? &vaultAdapter : nullptr);
|
||||
|
||||
// Store PIN vault on the worker thread (Argon2id is expensive)
|
||||
bool pinStored = false;
|
||||
if (!pin.empty() && vault_) {
|
||||
pinStored = vault_->store(pin, passphrase);
|
||||
}
|
||||
|
||||
return [this, pinStored, pin]() {
|
||||
if (result.encrypted) {
|
||||
return [this, result]() {
|
||||
encrypt_in_progress_ = false;
|
||||
encrypt_status_.clear();
|
||||
DEBUG_LOGF("[App] Wallet encrypted (deferred)\n");
|
||||
|
||||
// Finalize PIN settings on main thread
|
||||
if (!pin.empty()) {
|
||||
if (pinStored) {
|
||||
if (result.pinProvided) {
|
||||
if (result.pinStored) {
|
||||
settings_->setPinEnabled(true);
|
||||
settings_->save();
|
||||
ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f);
|
||||
@@ -176,59 +356,32 @@ void App::processDeferredEncryption() {
|
||||
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f);
|
||||
}
|
||||
|
||||
// Securely clear deferred state
|
||||
if (!deferred_encrypt_passphrase_.empty()) {
|
||||
util::SecureVault::secureZero(
|
||||
&deferred_encrypt_passphrase_[0],
|
||||
deferred_encrypt_passphrase_.size());
|
||||
deferred_encrypt_passphrase_.clear();
|
||||
}
|
||||
if (!deferred_encrypt_pin_.empty()) {
|
||||
util::SecureVault::secureZero(
|
||||
&deferred_encrypt_pin_[0],
|
||||
deferred_encrypt_pin_.size());
|
||||
deferred_encrypt_pin_.clear();
|
||||
}
|
||||
deferred_encrypt_pending_ = false;
|
||||
wallet_security_.clearDeferredEncryption();
|
||||
|
||||
// Restart daemon (it shuts itself down after encryptwallet)
|
||||
if (isUsingEmbeddedDaemon()) {
|
||||
std::thread([this]() {
|
||||
for (int i = 0; i < 20 && !shutting_down_; ++i)
|
||||
async_tasks_.submit("deferred-encrypt-daemon-restart", [this](const util::AsyncTaskManager::Token& token) {
|
||||
for (int i = 0; i < 20 && !token.cancelled() && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (shutting_down_) return;
|
||||
if (token.cancelled() || shutting_down_) return;
|
||||
stopEmbeddedDaemon();
|
||||
if (shutting_down_) return;
|
||||
if (token.cancelled() || shutting_down_) return;
|
||||
startEmbeddedDaemon();
|
||||
// tryConnect will be called by the update loop
|
||||
}).detach();
|
||||
});
|
||||
} else {
|
||||
ui::Notifications::instance().warning(
|
||||
"Please restart your daemon for encryption to take effect.");
|
||||
}
|
||||
};
|
||||
} catch (const std::exception& e) {
|
||||
std::string err = e.what();
|
||||
} else {
|
||||
std::string err = result.error;
|
||||
return [this, err]() {
|
||||
encrypt_in_progress_ = false;
|
||||
encrypt_status_ = "Encryption failed: " + err;
|
||||
deferred_encrypt_pending_ = false;
|
||||
DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str());
|
||||
ui::Notifications::instance().error("Encryption failed: " + err);
|
||||
|
||||
// Clean up sensitive data on failure
|
||||
if (!deferred_encrypt_passphrase_.empty()) {
|
||||
util::SecureVault::secureZero(
|
||||
&deferred_encrypt_passphrase_[0],
|
||||
deferred_encrypt_passphrase_.size());
|
||||
deferred_encrypt_passphrase_.clear();
|
||||
}
|
||||
if (!deferred_encrypt_pin_.empty()) {
|
||||
util::SecureVault::secureZero(
|
||||
&deferred_encrypt_pin_[0],
|
||||
deferred_encrypt_pin_.size());
|
||||
deferred_encrypt_pin_.clear();
|
||||
}
|
||||
wallet_security_.clearDeferredEncryption();
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -244,14 +397,9 @@ void App::unlockWallet(const std::string& passphrase, int timeout) {
|
||||
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
|
||||
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
|
||||
w->post([this, r, passphrase, timeout]() -> rpc::RPCWorker::MainCb {
|
||||
bool ok = false;
|
||||
std::string err_msg;
|
||||
try {
|
||||
r->call("walletpassphrase", {passphrase, timeout});
|
||||
ok = true;
|
||||
} catch (const std::exception& e) {
|
||||
err_msg = e.what();
|
||||
}
|
||||
WalletSecurityRpcAdapter rpcAdapter(r);
|
||||
bool ok = rpcAdapter.unlockWallet(passphrase, timeout, err_msg);
|
||||
|
||||
return [this, ok, err_msg, timeout]() {
|
||||
lock_unlock_in_progress_ = false;
|
||||
@@ -1146,14 +1294,18 @@ void App::renderEncryptWalletDialog() {
|
||||
void App::renderDecryptWalletDialog() {
|
||||
if (!show_decrypt_dialog_) return;
|
||||
using namespace ui::material;
|
||||
using DecryptPhase = services::WalletSecurityWorkflow::DecryptPhase;
|
||||
using DecryptStep = services::WalletSecurityWorkflow::DecryptStep;
|
||||
|
||||
bool canClose = (decrypt_phase_ != 1); // don't close while working
|
||||
auto decryptState = wallet_security_workflow_.snapshot();
|
||||
|
||||
bool canClose = wallet_security_workflow_.canClose();
|
||||
bool* pOpen = canClose ? &show_decrypt_dialog_ : nullptr;
|
||||
|
||||
if (BeginOverlayDialog("Remove Wallet Encryption", pOpen, 480.0f, 0.94f)) {
|
||||
|
||||
// ---- Phase 0: Passphrase entry ----
|
||||
if (decrypt_phase_ == 0) {
|
||||
if (decryptState.phase == DecryptPhase::PassphraseEntry) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.7f, 0.3f, 1));
|
||||
ImGui::TextWrapped(ICON_MD_WARNING
|
||||
" This will remove encryption from your wallet. "
|
||||
@@ -1176,143 +1328,110 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
if (!decrypt_status_.empty()) {
|
||||
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", decrypt_status_.c_str());
|
||||
if (!decryptState.status.empty()) {
|
||||
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", decryptState.status.c_str());
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
bool valid = strlen(decrypt_pass_buf_) >= 1;
|
||||
|
||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
ImGui::BeginDisabled(!valid || decrypt_in_progress_);
|
||||
ImGui::BeginDisabled(!valid || decryptState.inProgress);
|
||||
if (ImGui::Button("Remove Encryption", ImVec2(btnW, 40)) || (enterPressed && valid)) {
|
||||
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...";
|
||||
decrypt_overall_start_time_ = std::chrono::steady_clock::now();
|
||||
decrypt_step_start_time_ = decrypt_overall_start_time_;
|
||||
wallet_security_workflow_.start(std::chrono::steady_clock::now());
|
||||
|
||||
// Run entire decrypt flow on worker thread
|
||||
if (worker_) {
|
||||
worker_->post([this, passphrase]() -> rpc::RPCWorker::MainCb {
|
||||
// Step 1: Unlock wallet
|
||||
try {
|
||||
rpc_->call("walletpassphrase", {passphrase, 600});
|
||||
} catch (const std::exception& e) {
|
||||
std::string err = e.what();
|
||||
return [this, err]() {
|
||||
decrypt_in_progress_ = false;
|
||||
decrypt_status_ = "Incorrect passphrase";
|
||||
decrypt_phase_ = 0; // back to entry
|
||||
WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
|
||||
[this](rpc::RPCClient& client, const char* context) {
|
||||
return sendStopCommandSafely(client, context);
|
||||
});
|
||||
auto unlock = services::WalletSecurityWorkflowExecutor::unlockWallet(passphrase, decryptRpc);
|
||||
if (!unlock.ok) {
|
||||
return [this]() {
|
||||
wallet_security_workflow_.failEntry("Incorrect passphrase");
|
||||
};
|
||||
}
|
||||
|
||||
// Update step on main thread
|
||||
return [this]() {
|
||||
decrypt_step_ = 1;
|
||||
decrypt_step_start_time_ = std::chrono::steady_clock::now();
|
||||
decrypt_status_ = "Exporting wallet keys...";
|
||||
wallet_security_workflow_.advanceTo(DecryptStep::ExportKeys,
|
||||
services::WalletSecurityWorkflow::stepStatus(DecryptStep::ExportKeys),
|
||||
std::chrono::steady_clock::now());
|
||||
|
||||
// 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;
|
||||
|
||||
try {
|
||||
rpc_->call("z_exportwallet", {exportFile}, 300L);
|
||||
} catch (const std::exception& e) {
|
||||
std::string err = e.what();
|
||||
WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
|
||||
[this](rpc::RPCClient& client, const char* context) {
|
||||
return sendStopCommandSafely(client, context);
|
||||
});
|
||||
WalletSecurityFileAdapter files;
|
||||
auto exportOutcome = services::WalletSecurityWorkflowExecutor::exportWallet(
|
||||
decryptRpc, files, static_cast<std::uint64_t>(std::time(nullptr)));
|
||||
if (!exportOutcome.ok) {
|
||||
std::string err = exportOutcome.error;
|
||||
return [this, err]() {
|
||||
decrypt_in_progress_ = false;
|
||||
decrypt_status_ = "Export failed: " + err;
|
||||
decrypt_phase_ = 3;
|
||||
wallet_security_workflow_.fail(err);
|
||||
};
|
||||
}
|
||||
|
||||
return [this, exportPath]() {
|
||||
decrypt_step_ = 2;
|
||||
decrypt_step_start_time_ = std::chrono::steady_clock::now();
|
||||
decrypt_status_ = "Stopping daemon...";
|
||||
auto filePlan = exportOutcome.filePlan;
|
||||
|
||||
return [this, filePlan]() {
|
||||
wallet_security_workflow_.advanceTo(DecryptStep::StopDaemon,
|
||||
services::WalletSecurityWorkflow::stepStatus(DecryptStep::StopDaemon),
|
||||
std::chrono::steady_clock::now());
|
||||
|
||||
// Continue with step 3
|
||||
worker_->post([this, exportPath]() -> rpc::RPCWorker::MainCb {
|
||||
try {
|
||||
rpc_->call("stop");
|
||||
} catch (...) {
|
||||
// stop often throws because connection drops
|
||||
}
|
||||
worker_->post([this, filePlan]() -> rpc::RPCWorker::MainCb {
|
||||
WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
|
||||
[this](rpc::RPCClient& client, const char* context) {
|
||||
return sendStopCommandSafely(client, context);
|
||||
});
|
||||
services::WalletSecurityWorkflowExecutor::stopDaemon(decryptRpc);
|
||||
|
||||
// Wait for daemon to fully stop
|
||||
for (int i = 0; i < 30 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
return [this, exportPath]() {
|
||||
decrypt_step_ = 3;
|
||||
decrypt_step_start_time_ = std::chrono::steady_clock::now();
|
||||
decrypt_status_ = "Backing up encrypted wallet...";
|
||||
return [this, filePlan]() {
|
||||
wallet_security_workflow_.advanceTo(DecryptStep::BackupWallet,
|
||||
services::WalletSecurityWorkflow::stepStatus(DecryptStep::BackupWallet),
|
||||
std::chrono::steady_clock::now());
|
||||
|
||||
// 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;
|
||||
};
|
||||
}
|
||||
worker_->post([this, filePlan]() -> rpc::RPCWorker::MainCb {
|
||||
WalletSecurityFileAdapter files;
|
||||
auto backup = services::WalletSecurityWorkflowExecutor::backupEncryptedWallet(files, filePlan);
|
||||
if (!backup.ok) {
|
||||
std::string err = backup.error;
|
||||
return [this, err]() {
|
||||
wallet_security_workflow_.fail(err);
|
||||
};
|
||||
}
|
||||
|
||||
return [this, exportPath]() {
|
||||
decrypt_step_ = 4;
|
||||
decrypt_step_start_time_ = std::chrono::steady_clock::now();
|
||||
decrypt_status_ = "Restarting daemon...";
|
||||
return [this, exportPath = filePlan.exportPath]() {
|
||||
wallet_security_workflow_.advanceTo(DecryptStep::RestartDaemon,
|
||||
services::WalletSecurityWorkflow::stepStatus(DecryptStep::RestartDaemon),
|
||||
std::chrono::steady_clock::now());
|
||||
|
||||
auto restartAndImport = [this, exportPath]() {
|
||||
for (int i = 0; i < 20 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (shutting_down_) return;
|
||||
|
||||
if (isUsingEmbeddedDaemon()) {
|
||||
stopEmbeddedDaemon();
|
||||
if (shutting_down_) return;
|
||||
for (int i = 0; i < 10 && !shutting_down_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (shutting_down_) return;
|
||||
startEmbeddedDaemon();
|
||||
}
|
||||
|
||||
// Wait for daemon to become available
|
||||
int maxWait = 60;
|
||||
bool daemonUp = false;
|
||||
for (int i = 0; i < maxWait && !shutting_down_; i++) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
try {
|
||||
rpc_->call("getinfo");
|
||||
daemonUp = true;
|
||||
break;
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
if (!daemonUp) {
|
||||
auto restartAndImport = [this, exportPath](const util::AsyncTaskManager::Token& token) {
|
||||
WalletSecurityDaemonAdapter daemonAdapter(*this, token);
|
||||
WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
|
||||
[this](rpc::RPCClient& client, const char* context) {
|
||||
return sendStopCommandSafely(client, context);
|
||||
});
|
||||
auto restart = services::WalletSecurityWorkflowExecutor::restartDaemonAndWait(
|
||||
daemonAdapter, decryptRpc);
|
||||
if (!restart.ok) {
|
||||
if (restart.error.empty()) return;
|
||||
if (worker_) {
|
||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||
return [this]() {
|
||||
decrypt_in_progress_ = false;
|
||||
decrypt_status_ = "Daemon failed to restart";
|
||||
decrypt_phase_ = 3;
|
||||
worker_->post([this, err = restart.error]() -> rpc::RPCWorker::MainCb {
|
||||
return [this, err]() {
|
||||
wallet_security_workflow_.fail(err);
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1324,9 +1443,8 @@ void App::renderDecryptWalletDialog() {
|
||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||
return [this]() {
|
||||
// Close the decrypt dialog — user can use the wallet now
|
||||
decrypt_in_progress_ = false;
|
||||
wallet_security_workflow_.closeDialogForImport();
|
||||
show_decrypt_dialog_ = false;
|
||||
decrypt_import_active_ = true;
|
||||
|
||||
// Mark rescanning so status bar picks it up immediately
|
||||
state_.sync.rescanning = true;
|
||||
@@ -1334,13 +1452,15 @@ void App::renderDecryptWalletDialog() {
|
||||
|
||||
// Clear encryption state early — vault/PIN removed now,
|
||||
// wallet file is already unencrypted
|
||||
if (vault_ && vault_->hasVault()) {
|
||||
vault_->removeVault();
|
||||
}
|
||||
if (settings_ && settings_->getPinEnabled()) {
|
||||
settings_->setPinEnabled(false);
|
||||
settings_->save();
|
||||
}
|
||||
services::WalletSecurityWorkflowExecutor::cleanupVaultAndPin([this]() {
|
||||
if (vault_ && vault_->hasVault()) {
|
||||
vault_->removeVault();
|
||||
}
|
||||
if (settings_ && settings_->getPinEnabled()) {
|
||||
settings_->setPinEnabled(false);
|
||||
settings_->save();
|
||||
}
|
||||
});
|
||||
|
||||
ui::Notifications::instance().info(
|
||||
"Importing keys & rescanning blockchain — wallet is usable while this runs",
|
||||
@@ -1349,32 +1469,17 @@ void App::renderDecryptWalletDialog() {
|
||||
});
|
||||
}
|
||||
|
||||
// Step 6: Import wallet in background (use full path)
|
||||
// Use a SEPARATE RPC client so the main rpc_'s
|
||||
// curl_mutex isn't held for the entire import duration.
|
||||
// Blocking rpc_ prevents refreshData/refreshPeerInfo
|
||||
// from running, which leaves the UI with no peers.
|
||||
auto importRpc = std::make_unique<rpc::RPCClient>();
|
||||
bool importRpcOk = importRpc->connect(
|
||||
saved_config_.host, saved_config_.port,
|
||||
saved_config_.rpcuser, saved_config_.rpcpassword);
|
||||
if (!importRpcOk) {
|
||||
// Fall back to main client if temp connect fails
|
||||
importRpc.reset();
|
||||
}
|
||||
auto* rpcForImport = importRpc ? importRpc.get() : rpc_.get();
|
||||
|
||||
// Use 20-minute timeout — import + rescan can be very slow
|
||||
try {
|
||||
rpcForImport->call("z_importwallet", {exportPath}, 1200L);
|
||||
} catch (const std::exception& e) {
|
||||
std::string err = e.what();
|
||||
WalletSecurityImportRpcAdapter importAdapter(rpc_.get(), saved_config_);
|
||||
auto importResult = services::WalletSecurityWorkflowExecutor::importWallet(
|
||||
importAdapter, exportPath);
|
||||
if (!importResult.ok) {
|
||||
std::string err = importResult.error;
|
||||
if (worker_) {
|
||||
worker_->post([this, err]() -> rpc::RPCWorker::MainCb {
|
||||
return [this, err]() {
|
||||
decrypt_import_active_ = false;
|
||||
wallet_security_workflow_.finishImport();
|
||||
ui::Notifications::instance().error(
|
||||
"Key import failed: " + err +
|
||||
err +
|
||||
"\nEncrypted backup: wallet.dat.encrypted.bak",
|
||||
12.0f);
|
||||
};
|
||||
@@ -1383,18 +1488,12 @@ void App::renderDecryptWalletDialog() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disconnect the temporary RPC client
|
||||
if (importRpc) {
|
||||
importRpc->disconnect();
|
||||
importRpc.reset();
|
||||
}
|
||||
|
||||
// Success — force full state refresh so peers,
|
||||
// balances, and addresses are fetched immediately.
|
||||
if (worker_) {
|
||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||
return [this]() {
|
||||
decrypt_import_active_ = false;
|
||||
wallet_security_workflow_.finishImport();
|
||||
|
||||
// Force address + peer refresh
|
||||
addresses_dirty_ = true;
|
||||
@@ -1414,7 +1513,7 @@ void App::renderDecryptWalletDialog() {
|
||||
}
|
||||
};
|
||||
|
||||
std::thread(restartAndImport).detach();
|
||||
async_tasks_.submit("decrypt-restart-import", restartAndImport);
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -1434,7 +1533,7 @@ void App::renderDecryptWalletDialog() {
|
||||
}
|
||||
|
||||
// ---- Phase 1: Working ----
|
||||
} else if (decrypt_phase_ == 1) {
|
||||
} else if (decryptState.phase == DecryptPhase::Working) {
|
||||
// Step checklist
|
||||
const char* stepLabels[] = {
|
||||
"Unlocking wallet",
|
||||
@@ -1448,17 +1547,18 @@ void App::renderDecryptWalletDialog() {
|
||||
// Compute elapsed times
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto stepElapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
now - decrypt_step_start_time_).count();
|
||||
now - decryptState.stepStarted).count();
|
||||
auto totalElapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
now - decrypt_overall_start_time_).count();
|
||||
now - decryptState.overallStarted).count();
|
||||
|
||||
ImGui::Spacing();
|
||||
for (int i = 0; i < numSteps; i++) {
|
||||
ImGui::PushFont(Type().iconMed());
|
||||
if (i < decrypt_step_) {
|
||||
if (services::WalletSecurityWorkflow::stepIsComplete(decryptState.step,
|
||||
services::WalletSecurityWorkflow::stepFromIndex(i))) {
|
||||
// Completed
|
||||
ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.4f, 1.0f), ICON_MD_CHECK_CIRCLE);
|
||||
} else if (i == decrypt_step_) {
|
||||
} else if (i == services::WalletSecurityWorkflow::stepIndex(decryptState.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);
|
||||
@@ -1469,7 +1569,7 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGui::PopFont();
|
||||
ImGui::SameLine();
|
||||
|
||||
if (i == decrypt_step_) {
|
||||
if (i == services::WalletSecurityWorkflow::stepIndex(decryptState.step)) {
|
||||
// Show step label with elapsed time
|
||||
int mins = (int)(stepElapsed / 60);
|
||||
int secs = (int)(stepElapsed % 60);
|
||||
@@ -1480,7 +1580,8 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.6f, 1.0f),
|
||||
"%s... (%ds)", stepLabels[i], secs);
|
||||
}
|
||||
} else if (i < decrypt_step_) {
|
||||
} else if (services::WalletSecurityWorkflow::stepIsComplete(decryptState.step,
|
||||
services::WalletSecurityWorkflow::stepFromIndex(i))) {
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.8f, 0.6f, 1.0f), "%s", stepLabels[i]);
|
||||
} else {
|
||||
ImGui::TextDisabled("%s", stepLabels[i]);
|
||||
@@ -1515,7 +1616,7 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGui::Spacing();
|
||||
|
||||
// Step-specific hints
|
||||
if (decrypt_step_ == 4) {
|
||||
if (decryptState.step == DecryptStep::RestartDaemon) {
|
||||
ImGui::TextWrapped("Waiting for the daemon to finish starting up...");
|
||||
} else {
|
||||
ImGui::TextWrapped("Please wait. The daemon is exporting keys, restarting, "
|
||||
@@ -1531,7 +1632,7 @@ void App::renderDecryptWalletDialog() {
|
||||
}
|
||||
|
||||
// ---- Phase 2: Success ----
|
||||
} else if (decrypt_phase_ == 2) {
|
||||
} else if (decryptState.phase == DecryptPhase::Success) {
|
||||
ImGui::PushFont(Type().iconLarge());
|
||||
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), ICON_MD_CHECK_CIRCLE);
|
||||
ImGui::PopFont();
|
||||
@@ -1549,7 +1650,7 @@ void App::renderDecryptWalletDialog() {
|
||||
}
|
||||
|
||||
// ---- Phase 3: Error ----
|
||||
} else if (decrypt_phase_ == 3) {
|
||||
} else if (decryptState.phase == DecryptPhase::Error) {
|
||||
ImGui::PushFont(Type().iconLarge());
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), ICON_MD_ERROR);
|
||||
ImGui::PopFont();
|
||||
@@ -1557,14 +1658,12 @@ void App::renderDecryptWalletDialog() {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Decryption failed");
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", decrypt_status_.c_str());
|
||||
ImGui::TextWrapped("%s", decryptState.status.c_str());
|
||||
|
||||
ImGui::Spacing();
|
||||
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();
|
||||
wallet_security_workflow_.reset();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Close", ImVec2(btnW, 40))) {
|
||||
|
||||
Reference in New Issue
Block a user