feat(lite): real backend integration — controller, M0-M2a wiring, smoke tool, tests

- LiteWalletController (src/wallet/lite_wallet_controller.*): App-owned; runs real
  create/open/restore via the linked SDXL bridge with allowBridgeCalls=true; wipes
  seed/passphrase with sodium_memzero; persists on a ready wallet. M2a:
  applyLiteRefreshModelToWalletState maps a parsed refresh bundle into WalletState
  (zatoshi->DRGX, z/t split, tx typing + confirmations, sync progress).
- App wiring: liteWallet() accessor + init() construction when supportsLiteBackend();
  persist -> settings save.
- settings_page: "Validate" reroutes to the controller for real execution (validation-
  only fallback otherwise); wipes UI secret buffers after submit.
- chain name default -> "main" with load-time migration of legacy "DRAGONX"
  (settings.cpp), preventing the backend "Unknown chain" panic.
- M0: build.sh --lite-backend flag; lite_smoke real-backend tool + CMake targets;
  tests/fake_lite_backend.h deterministic harness.
- Tests (test_phase4): injectable-fake bridge, controller lifecycle, chain-name
  migration, refresh->WalletState mapping; plus the lite test-suite churn-cleanup rewrite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 21:15:44 -05:00
parent 863d015628
commit 5586f334a4
12 changed files with 3597 additions and 121 deletions

View File

@@ -11,6 +11,8 @@
#include "rpc/rpc_worker.h"
#include "rpc/connection.h"
#include "config/settings.h"
#include "wallet/lite_wallet_controller.h"
#include "wallet/lite_wallet_server_selection_adapter.h"
#include "daemon/daemon_controller.h"
#include "daemon/embedded_daemon.h"
#include "daemon/lifecycle_adapters.h"
@@ -164,6 +166,16 @@ bool App::init()
settings_->clearUpgradeSave();
}
// Lite builds with a linked SDXL backend own a lite wallet controller that drives
// real create/open/restore through the bridge. Full-node and unlinked-lite builds
// leave lite_wallet_ null (the UI falls back to validation-only).
if (supportsLiteBackend()) {
lite_wallet_ = wallet::LiteWalletController::createLinked(
walletCapabilities(),
wallet::liteConnectionSettingsFromAppSettings(*settings_));
lite_wallet_->setPersistCallback([this]() { settings_->save(); });
}
// Apply verbose logging preference from saved settings
util::Logger::instance().setVerbose(settings_->getVerboseLogging());
@@ -1126,10 +1138,7 @@ void App::render()
// Tabs that need balance/transaction data (show overlay):
// Overview, Send, Receive, History
// ---------------------------------------------------------------
bool pageNeedsWalletData = (current_page_ == ui::NavPage::Overview ||
current_page_ == ui::NavPage::Send ||
current_page_ == ui::NavPage::Receive ||
current_page_ == ui::NavPage::History);
bool pageNeedsWalletData = wallet::uiSurfaceNeedsWalletData(ui::NavPageSurface(current_page_));
bool daemonReady = state_.connected && !state_.warming_up;
// Don't show lock screen while pool mining — xmrig runs independently
@@ -1282,11 +1291,7 @@ void App::render()
}
}
};
// Fade content area window draw list (fills, text, borders)
fadeVerts(caDL, caVtxStart, caDL->VtxBuffer.Size);
// Fade ForegroundDrawList panel effects (rainbow border, edge trace,
// Apply to foreground panel effects (rainbow borders, edge trace,
// shimmer, specular glare) — but NOT viewport-wide effects (embers,
// overlay) which were added after fgVtxEnd.
fadeVerts(fgDL, fgVtxStart, fgVtxEnd);
@@ -2099,7 +2104,12 @@ void App::setCurrentTab(int tab) {
bool App::startEmbeddedDaemon()
{
if (!use_embedded_daemon_) {
if (!supportsEmbeddedDaemon()) {
DEBUG_LOGF("Embedded daemon support unavailable in this build, not starting\n");
return false;
}
if (!isUsingEmbeddedDaemon()) {
DEBUG_LOGF("Embedded daemon disabled, not starting\n");
return false;
}
@@ -2309,6 +2319,11 @@ bool App::isEmbeddedDaemonRunning() const
void App::rescanBlockchain()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
return;
}
auto decision = daemon::DaemonController::evaluateLifecycleOperation(
daemon::DaemonController::LifecycleOperation::Rescan,
isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning());
@@ -2343,6 +2358,11 @@ void App::rescanBlockchain()
void App::deleteBlockchainData()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
return;
}
auto decision = daemon::DaemonController::evaluateLifecycleOperation(
daemon::DaemonController::LifecycleOperation::DeleteBlockchainData,
isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning());
@@ -2366,6 +2386,10 @@ void App::deleteBlockchainData()
bool App::stopDaemonForBootstrap()
{
if (!supportsFullNodeLifecycleActions()) {
return false;
}
auto decision = daemon::DaemonController::evaluateLifecycleOperation(
daemon::DaemonController::LifecycleOperation::BootstrapStop,
isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning());
@@ -3152,9 +3176,14 @@ void App::maybeFinishTransactionSendProgress()
}
void App::restartDaemon()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
return;
}
auto decision = daemon::DaemonController::evaluateLifecycleOperation(
daemon::DaemonController::LifecycleOperation::ManualRestart,
use_embedded_daemon_, daemon_controller_ != nullptr, isEmbeddedDaemonRunning(), daemon_restarting_.load());
isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning(), daemon_restarting_.load());
if (!decision.allowed) return;
daemon_restarting_ = true;

View File

@@ -19,6 +19,7 @@
#include "services/wallet_security_controller.h"
#include "services/wallet_security_workflow.h"
#include "util/async_task_manager.h"
#include "wallet/wallet_capabilities.h"
#include "ui/sidebar.h"
#include "ui/windows/console_tab.h"
#include "imgui.h"
@@ -32,6 +33,7 @@ namespace dragonx {
namespace config { class Settings; }
namespace daemon { class DaemonController; class EmbeddedDaemon; class XmrigManager; }
namespace util { class Bootstrap; class SecureVault; }
namespace wallet { class LiteWalletController; }
}
namespace dragonx {
@@ -130,8 +132,13 @@ public:
* @brief Whether we are in the shutdown phase
*/
bool isShuttingDown() const { return shutting_down_; }
bool isLiteBuild() const { return DRAGONX_LITE_BUILD != 0; }
bool supportsEmbeddedDaemon() const { return DRAGONX_ENABLE_EMBEDDED_DAEMON != 0; }
wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); }
bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); }
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
bool supportsSoloMining() const { return wallet::supportsSoloMining(walletCapabilities()); }
bool supportsPoolMining() const { return wallet::supportsPoolMining(walletCapabilities()); }
bool supportsLiteBackend() const { return wallet::supportsLiteBackend(walletCapabilities()); }
/**
* @brief Render the shutdown overlay (called instead of normal UI during shutdown)
@@ -148,6 +155,8 @@ public:
rpc::RPCClient* rpc() { return rpc_.get(); }
rpc::RPCWorker* worker() { return worker_.get(); }
config::Settings* settings() { return settings_.get(); }
// Lite wallet controller (non-null only in lite builds with a linked backend).
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
WalletState& state() { return state_; }
const WalletState& state() const { return state_; }
const WalletState& getWalletState() const { return state_; }
@@ -276,8 +285,8 @@ public:
bool startEmbeddedDaemon();
void stopEmbeddedDaemon();
bool isEmbeddedDaemonRunning() const;
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
void rescanBlockchain(); // restart daemon with -rescan flag
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
@@ -406,6 +415,7 @@ private:
rpc::ConnectionConfig saved_config_;
std::unique_ptr<config::Settings> settings_;
std::unique_ptr<wallet::LiteWalletController> lite_wallet_; // lite builds w/ linked backend
std::unique_ptr<daemon::DaemonController> daemon_controller_;
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
util::AsyncTaskManager async_tasks_;
@@ -441,7 +451,7 @@ private:
bool show_address_book_ = false;
// Embedded daemon state
bool use_embedded_daemon_ = (DRAGONX_ENABLE_EMBEDDED_DAEMON != 0);
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
std::string daemon_status_;
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)

View File

@@ -30,6 +30,33 @@ namespace config {
Settings::Settings() = default;
Settings::~Settings() = default;
namespace {
Settings::LiteServerSelectionPreferenceMode parseLiteServerSelectionPreferenceMode(
const json& value)
{
if (!value.is_string()) return Settings::LiteServerSelectionPreferenceMode::Sticky;
const std::string mode = value.get<std::string>();
if (mode == "random" || mode == "Random") {
return Settings::LiteServerSelectionPreferenceMode::Random;
}
return Settings::LiteServerSelectionPreferenceMode::Sticky;
}
const char* liteServerSelectionPreferenceModeName(
Settings::LiteServerSelectionPreferenceMode mode)
{
switch (mode) {
case Settings::LiteServerSelectionPreferenceMode::Sticky:
return "sticky";
case Settings::LiteServerSelectionPreferenceMode::Random:
return "random";
}
return "sticky";
}
} // namespace
std::string Settings::getDefaultPath()
{
#ifdef _WIN32
@@ -148,6 +175,54 @@ bool Settings::load(const std::string& path)
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get<bool>();
if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get<bool>();
if (j.contains("max_connections")) max_connections_ = j["max_connections"].get<int>();
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
const auto& lite = j["lite_wallet"];
if (lite.contains("server_selection_mode")) {
lite_server_selection_mode_ = parseLiteServerSelectionPreferenceMode(
lite["server_selection_mode"]);
}
if (lite.contains("sticky_server_url") && lite["sticky_server_url"].is_string()) {
lite_sticky_server_url_ = lite["sticky_server_url"].get<std::string>();
}
if (lite.contains("chain_name") && lite["chain_name"].is_string()) {
lite_chain_name_ = lite["chain_name"].get<std::string>();
}
// Migration: the SDXL backend only accepts main/test/regtest and hard-panics
// (process abort) on any other chain name. Older builds persisted the "DRAGONX"
// ticker here, which crashed the lite backend on launch. Rewrite any invalid
// value to "main" and flag a re-save so the corrected setting persists.
if (lite_chain_name_ != "main" && lite_chain_name_ != "test" &&
lite_chain_name_ != "regtest") {
lite_chain_name_ = "main";
needs_upgrade_save_ = true;
}
if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_unsigned()) {
lite_random_selection_seed_ = lite["random_selection_seed"].get<std::size_t>();
} else if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_integer()) {
const auto seed = lite["random_selection_seed"].get<long long>();
lite_random_selection_seed_ = seed > 0 ? static_cast<std::size_t>(seed) : 0;
}
if (lite.contains("persist_selected_server") && lite["persist_selected_server"].is_boolean()) {
lite_persist_selected_server_ = lite["persist_selected_server"].get<bool>();
}
if (lite.contains("servers") && lite["servers"].is_array()) {
lite_servers_.clear();
for (const auto& server : lite["servers"]) {
if (!server.is_object()) continue;
LiteServerPreference preference;
if (server.contains("url") && server["url"].is_string()) {
preference.url = server["url"].get<std::string>();
}
if (server.contains("label") && server["label"].is_string()) {
preference.label = server["label"].get<std::string>();
}
if (server.contains("enabled") && server["enabled"].is_boolean()) {
preference.enabled = server["enabled"].get<bool>();
}
lite_servers_.push_back(preference);
}
}
}
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
debug_categories_.clear();
@@ -265,6 +340,23 @@ bool Settings::save(const std::string& path)
j["keep_daemon_running"] = keep_daemon_running_;
j["stop_external_daemon"] = stop_external_daemon_;
j["max_connections"] = max_connections_;
{
json lite = json::object();
lite["server_selection_mode"] = liteServerSelectionPreferenceModeName(lite_server_selection_mode_);
lite["sticky_server_url"] = lite_sticky_server_url_;
lite["chain_name"] = lite_chain_name_;
lite["random_selection_seed"] = lite_random_selection_seed_;
lite["persist_selected_server"] = lite_persist_selected_server_;
lite["servers"] = json::array();
for (const auto& server : lite_servers_) {
json entry = json::object();
entry["url"] = server.url;
entry["label"] = server.label;
entry["enabled"] = server.enabled;
lite["servers"].push_back(entry);
}
j["lite_wallet"] = lite;
}
j["verbose_logging"] = verbose_logging_;
j["debug_categories"] = json::array();
for (const auto& cat : debug_categories_)

View File

@@ -5,6 +5,7 @@
#pragma once
#include <algorithm>
#include <cstddef>
#include <map>
#include <string>
#include <set>
@@ -54,6 +55,17 @@ public:
*/
static std::string getDefaultPath();
enum class LiteServerSelectionPreferenceMode {
Sticky,
Random
};
struct LiteServerPreference {
std::string url;
std::string label;
bool enabled = true;
};
// Theme
std::string getTheme() const { return theme_; }
void setTheme(const std::string& theme) { theme_ = theme; }
@@ -218,6 +230,20 @@ public:
int getMaxConnections() const { return max_connections_; }
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
// Lite wallet server selection
LiteServerSelectionPreferenceMode getLiteServerSelectionMode() const { return lite_server_selection_mode_; }
void setLiteServerSelectionMode(LiteServerSelectionPreferenceMode mode) { lite_server_selection_mode_ = mode; }
std::string getLiteStickyServerUrl() const { return lite_sticky_server_url_; }
void setLiteStickyServerUrl(const std::string& url) { lite_sticky_server_url_ = url; }
std::string getLiteChainName() const { return lite_chain_name_; }
void setLiteChainName(const std::string& chainName) { lite_chain_name_ = chainName; }
std::size_t getLiteRandomSelectionSeed() const { return lite_random_selection_seed_; }
void setLiteRandomSelectionSeed(std::size_t seed) { lite_random_selection_seed_ = seed; }
bool getLitePersistSelectedServer() const { return lite_persist_selected_server_; }
void setLitePersistSelectedServer(bool persist) { lite_persist_selected_server_ = persist; }
const std::vector<LiteServerPreference>& getLiteServers() const { return lite_servers_; }
void setLiteServers(const std::vector<LiteServerPreference>& servers) { lite_servers_ = servers; }
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
bool getVerboseLogging() const { return verbose_logging_; }
void setVerboseLogging(bool v) { verbose_logging_ = v; }
@@ -358,6 +384,23 @@ private:
bool keep_daemon_running_ = false;
bool stop_external_daemon_ = false;
int max_connections_ = 0; // 0 = daemon default
// Lite wallet server preferences. These are user/server settings only;
// wallet secrets, wallet files, and lifecycle state are never stored here.
LiteServerSelectionPreferenceMode lite_server_selection_mode_ = LiteServerSelectionPreferenceMode::Sticky;
std::string lite_sticky_server_url_ = "https://lite.dragonx.is";
std::string lite_chain_name_ = "main"; // SDXL backend chain id; must be main/test/regtest
std::size_t lite_random_selection_seed_ = 0;
bool lite_persist_selected_server_ = true;
std::vector<LiteServerPreference> lite_servers_ = {
{"https://lite.dragonx.is", "DragonX Lite", true},
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
{"https://lite2.dragonx.is", "DragonX Lite 2", true},
{"https://lite3.dragonx.is", "DragonX Lite 3", true},
{"https://lite4.dragonx.is", "DragonX Lite 4", true},
{"https://lite5.dragonx.is", "DragonX Lite 5", true}
};
bool verbose_logging_ = false;
std::set<std::string> debug_categories_;
bool theme_effects_enabled_ = true;

View File

@@ -6,6 +6,10 @@
#include "../../app.h"
#include "../../config/version.h"
#include "../../config/settings.h"
#include "../../wallet/lite_wallet_lifecycle_ui_adapter.h"
#include "../../wallet/lite_wallet_server_selection_adapter.h"
#include "../../wallet/lite_wallet_controller.h"
#include <sodium.h>
#include "../../util/logger.h"
#include "../windows/balance_tab.h"
#include "../windows/console_tab.h"
@@ -42,6 +46,8 @@
#include <filesystem>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstring>
namespace dragonx {
namespace ui {
@@ -99,6 +105,22 @@ struct SettingsPageState {
LowSpecSnapshot low_spec_snapshot;
bool keep_daemon_running = false;
bool stop_external_daemon = false;
int lite_server_mode = 0;
char lite_server_url[256] = "https://lite.dragonx.is";
int lite_random_seed = 0;
bool lite_persist_selected_server = true;
std::vector<wallet::LiteServerEndpoint> lite_servers;
std::string lite_server_status;
bool lite_lifecycle_expanded = false;
int lite_lifecycle_operation = 0;
char lite_wallet_path[256] = "";
char lite_lifecycle_passphrase[128] = "";
char lite_restore_seed[512] = "";
int lite_restore_birthday = 0;
int lite_restore_account = 0;
bool lite_restore_overwrite = false;
std::string lite_lifecycle_status;
std::string lite_lifecycle_summary;
bool mine_when_idle = false;
int mine_idle_delay = 120;
bool idle_thread_scaling = false;
@@ -117,6 +139,148 @@ struct SettingsPageState {
static SettingsPageState s_settingsState;
static void copyToSettingsBuffer(char* dest, std::size_t destSize, const std::string& value) {
if (!dest || destSize == 0) return;
std::strncpy(dest, value.c_str(), destSize - 1);
dest[destSize - 1] = '\0';
}
static wallet::LiteConnectionSettings liteConnectionSettingsFromPageState(config::Settings* settings) {
wallet::LiteConnectionSettings connectionSettings = settings
? wallet::liteConnectionSettingsFromAppSettings(*settings)
: wallet::defaultLiteConnectionSettings();
if (!s_settingsState.lite_servers.empty()) {
connectionSettings.servers = s_settingsState.lite_servers;
}
connectionSettings.selectionMode = s_settingsState.lite_server_mode == 1
? wallet::LiteServerSelectionMode::Random
: wallet::LiteServerSelectionMode::Sticky;
connectionSettings.stickyServerUrl = s_settingsState.lite_server_url;
connectionSettings.chainName = wallet::kDragonXLiteChainName;
connectionSettings.randomSelectionSeed = static_cast<std::size_t>(std::max(0, s_settingsState.lite_random_seed));
return connectionSettings;
}
static wallet::LiteWalletLifecycleOperation liteLifecycleOperationFromPageState() {
switch (s_settingsState.lite_lifecycle_operation) {
case 1: return wallet::LiteWalletLifecycleOperation::OpenExisting;
case 2: return wallet::LiteWalletLifecycleOperation::RestoreFromSeed;
default: return wallet::LiteWalletLifecycleOperation::CreateNew;
}
}
static void saveLiteServerSelectionFromPageState(App* app) {
if (!app || !app->settings()) return;
const auto connectionSettings = liteConnectionSettingsFromPageState(app->settings());
wallet::LiteWalletServerSelectionUiExecutionInput input;
input.capabilities = app->walletCapabilities();
input.intent.selectedServerIntentProvided = true;
input.intent.selectionMode = connectionSettings.selectionMode;
input.intent.selectedServerUrl = connectionSettings.stickyServerUrl;
input.intent.randomSelectionSeed = connectionSettings.randomSelectionSeed;
input.intent.chainName = connectionSettings.chainName;
input.intent.replaceServers = true;
input.intent.servers = connectionSettings.servers;
input.persistence.settingsLoaded = true;
input.persistence.havePersistedSelectionIntent = true;
input.persistence.persistSelectedServer = s_settingsState.lite_persist_selected_server;
input.persistence.persistenceOwnerReady = true;
input.persistence.writeSettings = true;
input.ui.selectedServerDisplayReady = true;
input.ui.lifecycleUiOwnerReady = true;
input.ui.operationConfirmed = true;
input.ui.privateDataRedactionReady = true;
input.ui.syncPlannerFeedReady = true;
input.requireLifecycleReadiness = false;
const auto result = wallet::executeLiteWalletServerSelectionUi(*app->settings(), input);
if (result.settingsWritten) {
s_settingsState.lite_server_status = "Saved";
} else if (!result.error.empty()) {
s_settingsState.lite_server_status = result.error;
Notifications::instance().warning(result.error);
}
}
static void evaluateLiteLifecycleRequestFromPageState(App* app) {
if (!app || !app->settings()) return;
wallet::LiteWalletLifecycleUiExecutionInput input;
input.capabilities = app->walletCapabilities();
input.settingsLoaded = true;
input.requirePersistedServerSelectionIntent = true;
input.ui.selectedServerDisplayReady = true;
input.ui.lifecycleUiOwnerReady = true;
input.ui.operationConfirmed = true;
input.ui.privateDataRedactionReady = true;
input.ui.syncPlannerFeedReady = true;
input.request.requestProvided = true;
input.request.operation = liteLifecycleOperationFromPageState();
switch (input.request.operation) {
case wallet::LiteWalletLifecycleOperation::CreateNew:
input.request.createRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
break;
case wallet::LiteWalletLifecycleOperation::OpenExisting:
input.request.openRequest.walletPath = s_settingsState.lite_wallet_path;
input.request.openRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
break;
case wallet::LiteWalletLifecycleOperation::RestoreFromSeed:
input.request.restoreRequest.walletPath = s_settingsState.lite_wallet_path;
input.request.restoreRequest.seedPhrase = s_settingsState.lite_restore_seed;
input.request.restoreRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
input.request.restoreRequest.birthday = static_cast<unsigned long long>(std::max(0, s_settingsState.lite_restore_birthday));
input.request.restoreRequest.account = static_cast<unsigned long long>(std::max(0, s_settingsState.lite_restore_account));
input.request.restoreRequest.overwrite = s_settingsState.lite_restore_overwrite;
break;
}
// When a linked lite backend is present, execute the operation for real through the
// App-owned controller. Otherwise fall back to the validation-only adapter.
if (auto* lite = app->liteWallet()) {
wallet::LiteWalletLifecycleResult result;
switch (input.request.operation) {
case wallet::LiteWalletLifecycleOperation::CreateNew:
result = lite->createWallet(input.request.createRequest);
break;
case wallet::LiteWalletLifecycleOperation::OpenExisting:
result = lite->openWallet(input.request.openRequest);
break;
case wallet::LiteWalletLifecycleOperation::RestoreFromSeed:
result = lite->restoreWallet(input.request.restoreRequest);
break;
}
// Secrets have been consumed; wipe the UI buffers (and the request copies) so they
// do not linger in memory after the attempt.
sodium_memzero(s_settingsState.lite_lifecycle_passphrase, sizeof(s_settingsState.lite_lifecycle_passphrase));
sodium_memzero(s_settingsState.lite_restore_seed, sizeof(s_settingsState.lite_restore_seed));
s_settingsState.lite_lifecycle_summary = result.bridgeResponseRedacted;
if (result.walletReady) {
s_settingsState.lite_lifecycle_status = "Wallet ready";
} else {
s_settingsState.lite_lifecycle_status = result.error.empty()
? result.status.message
: result.error;
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
}
return;
}
const auto result = wallet::executeLiteWalletLifecycleUiRequest(*app->settings(), input);
s_settingsState.lite_lifecycle_summary = result.requestSummaryRedacted;
if (result.ok) {
s_settingsState.lite_lifecycle_status = "Ready";
} else {
s_settingsState.lite_lifecycle_status = result.error.empty()
? wallet::liteWalletLifecycleUiExecutionStatusName(result.status)
: result.error;
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
}
}
// (APPEARANCE card now uses ChannelsSplit like all other cards)
static void loadSettingsPageState(config::Settings* settings) {
@@ -166,6 +330,17 @@ static void loadSettingsPageState(config::Settings* settings) {
Layout::setUserFontScale(s_settingsState.font_scale); // sync with Layout on load
s_settingsState.keep_daemon_running = settings->getKeepDaemonRunning();
s_settingsState.stop_external_daemon = settings->getStopExternalDaemon();
{
const auto liteSettings = wallet::liteConnectionSettingsFromAppSettings(*settings);
s_settingsState.lite_server_mode = liteSettings.selectionMode == wallet::LiteServerSelectionMode::Random ? 1 : 0;
copyToSettingsBuffer(s_settingsState.lite_server_url,
sizeof(s_settingsState.lite_server_url),
liteSettings.stickyServerUrl);
s_settingsState.lite_random_seed = static_cast<int>(liteSettings.randomSelectionSeed);
s_settingsState.lite_persist_selected_server = settings->getLitePersistSelectedServer();
s_settingsState.lite_servers = liteSettings.servers;
s_settingsState.lite_server_status.clear();
}
s_settingsState.mine_when_idle = settings->getMineWhenIdle();
s_settingsState.mine_idle_delay = settings->getMineIdleDelay();
s_settingsState.idle_thread_scaling = settings->getIdleThreadScaling();
@@ -220,6 +395,11 @@ static void saveSettingsPageState(config::Settings* settings) {
settings->setFontScale(s_settingsState.font_scale);
settings->setKeepDaemonRunning(s_settingsState.keep_daemon_running);
settings->setStopExternalDaemon(s_settingsState.stop_external_daemon);
{
auto liteSettings = liteConnectionSettingsFromPageState(settings);
wallet::applyLiteConnectionSettingsToAppSettings(*settings, liteSettings);
settings->setLitePersistSelectedServer(s_settingsState.lite_persist_selected_server);
}
settings->setMineWhenIdle(s_settingsState.mine_when_idle);
settings->setMineIdleDelay(s_settingsState.mine_idle_delay);
settings->setIdleThreadScaling(s_settingsState.idle_thread_scaling);
@@ -1073,7 +1253,7 @@ void RenderSettingsPage(App* app) {
// Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit
{
const bool showDaemonOptions = !app->isLiteBuild();
const bool showDaemonOptions = app->supportsFullNodeLifecycleActions();
float cbSpacing = Layout::spacingMd();
float fh = ImGui::GetFrameHeight();
float inner = ImGui::GetStyle().ItemInnerSpacing.x;
@@ -1221,6 +1401,7 @@ void RenderSettingsPage(App* app) {
TR("tt_backup"),
TR("tt_export_csv")
};
const bool showFullNodeLifecycleActions = app->supportsFullNodeLifecycleActions();
const char* wizLabel = TR("setup_wizard");
const char* bsLabel = TR("download_bootstrap");
float sp = Layout::spacingSm();
@@ -1230,9 +1411,10 @@ void RenderSettingsPage(App* app) {
float naturalW = 0;
for (int i = 0; i < 5; i++)
naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX;
float wizW = ImGui::CalcTextSize(wizLabel).x + btnPadX;
float bsW = ImGui::CalcTextSize(bsLabel).x + btnPadX;
float totalW = naturalW + wizW + bsW + sp * 7;
float wizW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(wizLabel).x + btnPadX : 0.0f;
float bsW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(bsLabel).x + btnPadX : 0.0f;
float totalW = naturalW + sp * 5;
if (showFullNodeLifecycleActions) totalW += wizW + bsW + sp * 2;
float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
if (scale < 1.0f) ImGui::SetWindowFontScale(scale);
@@ -1259,27 +1441,29 @@ void RenderSettingsPage(App* app) {
ExportTransactionsDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[4]);
// Right-align Setup Wizard + Download Bootstrap
float framePadX2 = ImGui::GetStyle().FramePadding.x * 2.0f;
float curX = ImGui::GetCursorScreenPos().x;
float wizBtnW = ImGui::CalcTextSize(wizLabel).x + framePadX2;
float bsBtnW = ImGui::CalcTextSize(bsLabel).x + framePadX2;
float rightEdge = cardMin.x + availWidth - pad;
float rightGroupW = bsBtnW + scaledSp + wizBtnW;
float groupX = rightEdge - rightGroupW;
if (groupX > curX) {
ImGui::SameLine(0, 0);
ImGui::SetCursorScreenPos(ImVec2(groupX, ImGui::GetCursorScreenPos().y));
} else {
if (showFullNodeLifecycleActions) {
// Right-align Setup Wizard + Download Bootstrap
float framePadX2 = ImGui::GetStyle().FramePadding.x * 2.0f;
float curX = ImGui::GetCursorScreenPos().x;
float wizBtnW = ImGui::CalcTextSize(wizLabel).x + framePadX2;
float bsBtnW = ImGui::CalcTextSize(bsLabel).x + framePadX2;
float rightEdge = cardMin.x + availWidth - pad;
float rightGroupW = bsBtnW + scaledSp + wizBtnW;
float groupX = rightEdge - rightGroupW;
if (groupX > curX) {
ImGui::SameLine(0, 0);
ImGui::SetCursorScreenPos(ImVec2(groupX, ImGui::GetCursorScreenPos().y));
} else {
ImGui::SameLine(0, scaledSp);
}
if (TactileButton(bsLabel, ImVec2(0, 0), btnFont))
BootstrapDownloadDialog::show(app);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_download_bootstrap"));
ImGui::SameLine(0, scaledSp);
if (TactileButton(wizLabel, ImVec2(0, 0), btnFont))
app->restartWizard();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_wizard"));
}
if (TactileButton(bsLabel, ImVec2(0, 0), btnFont))
BootstrapDownloadDialog::show(app);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_download_bootstrap"));
ImGui::SameLine(0, scaledSp);
if (TactileButton(wizLabel, ImVec2(0, 0), btnFont))
app->restartWizard();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_wizard"));
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
}
@@ -1342,6 +1526,189 @@ void RenderSettingsPage(App* app) {
if (!body2Info) body2Info = Type().body2();
ImGui::PushFont(body2Info);
if (app->isLiteBuild()) {
float liteLabelW = std::min(leftColW * 0.35f, 132.0f);
float liteInputW = std::max(80.0f, leftColW - liteLabelW - Layout::spacingSm());
const char* modeLabels[] = {"Sticky", "Random"};
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Mode");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
if (ImGui::BeginCombo("##LiteServerMode", modeLabels[s_settingsState.lite_server_mode == 1 ? 1 : 0])) {
for (int modeIndex = 0; modeIndex < 2; ++modeIndex) {
const bool selected = s_settingsState.lite_server_mode == modeIndex;
if (ImGui::Selectable(modeLabels[modeIndex], selected)) {
s_settingsState.lite_server_mode = modeIndex;
saveLiteServerSelectionFromPageState(app);
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Preset");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
std::string presetPreview = s_settingsState.lite_server_url[0] != '\0'
? std::string(s_settingsState.lite_server_url)
: std::string("Select");
for (const auto& server : s_settingsState.lite_servers) {
if (server.url == s_settingsState.lite_server_url && !server.label.empty()) {
presetPreview = server.label;
break;
}
}
ImGui::SetNextItemWidth(liteInputW);
if (ImGui::BeginCombo("##LiteServerPreset", presetPreview.c_str())) {
for (const auto& server : s_settingsState.lite_servers) {
if (!server.enabled) continue;
const std::string label = server.label.empty() ? server.url : server.label;
const bool selected = server.url == s_settingsState.lite_server_url;
if (ImGui::Selectable(label.c_str(), selected)) {
copyToSettingsBuffer(s_settingsState.lite_server_url,
sizeof(s_settingsState.lite_server_url),
server.url);
s_settingsState.lite_server_mode = 0;
saveLiteServerSelectionFromPageState(app);
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Server");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteServerUrl", s_settingsState.lite_server_url,
sizeof(s_settingsState.lite_server_url));
if (ImGui::IsItemDeactivatedAfterEdit()) {
s_settingsState.lite_server_mode = 0;
saveLiteServerSelectionFromPageState(app);
}
if (s_settingsState.lite_server_mode == 1) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Seed");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
if (ImGui::InputInt("##LiteRandomSeed", &s_settingsState.lite_random_seed)) {
if (s_settingsState.lite_random_seed < 0) s_settingsState.lite_random_seed = 0;
}
if (ImGui::IsItemDeactivatedAfterEdit()) saveLiteServerSelectionFromPageState(app);
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (ImGui::Checkbox("Persist selected server##LitePersistServer",
&s_settingsState.lite_persist_selected_server)) {
saveLiteServerSelectionFromPageState(app);
}
if (!s_settingsState.lite_server_status.empty()) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
s_settingsState.lite_server_status.c_str());
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (ImGui::Button("Lite wallet request##LiteLifecycleToggle", ImVec2(liteInputW, 0))) {
s_settingsState.lite_lifecycle_expanded = !s_settingsState.lite_lifecycle_expanded;
}
if (s_settingsState.lite_lifecycle_expanded) {
const char* lifecycleLabels[] = {"Create", "Open", "Restore"};
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Action");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
if (ImGui::BeginCombo("##LiteLifecycleOperation",
lifecycleLabels[std::max(0, std::min(2, s_settingsState.lite_lifecycle_operation))])) {
for (int operationIndex = 0; operationIndex < 3; ++operationIndex) {
const bool selected = s_settingsState.lite_lifecycle_operation == operationIndex;
if (ImGui::Selectable(lifecycleLabels[operationIndex], selected)) {
s_settingsState.lite_lifecycle_operation = operationIndex;
s_settingsState.lite_lifecycle_status.clear();
s_settingsState.lite_lifecycle_summary.clear();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (s_settingsState.lite_lifecycle_operation == 1 ||
s_settingsState.lite_lifecycle_operation == 2) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Wallet");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path,
sizeof(s_settingsState.lite_wallet_path));
}
if (s_settingsState.lite_lifecycle_operation == 2) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Seed");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteRestoreSeed", s_settingsState.lite_restore_seed,
sizeof(s_settingsState.lite_restore_seed),
ImGuiInputTextFlags_Password);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Birthday");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Account");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::Checkbox("Overwrite##LiteRestoreOverwrite",
&s_settingsState.lite_restore_overwrite);
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Passphrase");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteLifecyclePassphrase",
s_settingsState.lite_lifecycle_passphrase,
sizeof(s_settingsState.lite_lifecycle_passphrase),
ImGuiInputTextFlags_Password);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Validate##LiteLifecycleValidate", ImVec2(0, 0), S.resolveFont("button"))) {
evaluateLiteLifecycleRequestFromPageState(app);
}
if (!s_settingsState.lite_lifecycle_status.empty()) {
ImGui::SameLine(0, Layout::spacingSm());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
s_settingsState.lite_lifecycle_status.c_str());
}
if (!s_settingsState.lite_lifecycle_summary.empty()) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
s_settingsState.lite_lifecycle_summary.c_str());
}
}
} else {
float rpcLblW = std::max(
S.drawElement("components.settings-page", "rpc-label-min-width").size,
std::min(leftColW * 0.35f, S.drawElement("components.settings-page", "rpc-label-width").size * hs));
@@ -1476,7 +1843,7 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Node maintenance buttons (full-node build only)
if (!app->isLiteBuild()) {
if (app->supportsFullNodeLifecycleActions()) {
ImFont* btnFont = S.resolveFont("button");
float nodeBtnW;
{
@@ -1524,6 +1891,8 @@ void RenderSettingsPage(App* app) {
ImGui::EndDisabled();
}
}
ImGui::PopFont();
}
@@ -1950,7 +2319,7 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// "Restart daemon" button — only active when categories changed
if (s_settingsState.debug_cats_dirty) {
if (s_settingsState.debug_cats_dirty && app->supportsFullNodeLifecycleActions()) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 218, 0, 255));
ImFont* iconFont = Type().iconSmall();
if (iconFont) {
@@ -2085,7 +2454,8 @@ void RenderSettingsPage(App* app) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
if (ImGui::Button(TrId("delete_blockchain_confirm", "del_bc_btn").c_str(), ImVec2(btnW, 40))) {
app->deleteBlockchainData();
if (app->supportsFullNodeLifecycleActions())
app->deleteBlockchainData();
s_settingsState.confirm_delete_blockchain = false;
}
ImGui::PopStyleColor(2);

View File

@@ -0,0 +1,151 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "lite_wallet_controller.h"
#include "../data/wallet_state.h"
#include <utility>
#include <sodium.h>
namespace dragonx {
namespace wallet {
namespace {
constexpr double kZatoshisPerCoin = 100000000.0; // DRGX has 1e8 zatoshis per coin
}
void secureWipeLiteSecret(std::string& secret)
{
if (!secret.empty()) {
sodium_memzero(&secret[0], secret.size());
}
secret.clear();
}
void applyLiteRefreshModelToWalletState(const LiteWalletAppRefreshModel& model,
dragonx::WalletState& state)
{
if (model.hasBalance) {
state.privateBalance = static_cast<double>(model.balance.shieldedZatoshis) / kZatoshisPerCoin;
state.transparentBalance = static_cast<double>(model.balance.transparentZatoshis) / kZatoshisPerCoin;
state.totalBalance = static_cast<double>(model.balance.totalZatoshis) / kZatoshisPerCoin;
state.unconfirmedBalance = static_cast<double>(model.balance.unconfirmedZatoshis) / kZatoshisPerCoin;
}
if (model.hasAddresses) {
state.addresses.clear();
state.z_addresses.clear();
state.t_addresses.clear();
for (const auto& addr : model.addresses) {
AddressInfo info;
info.address = addr.address;
info.balance = 0.0; // lite address listing is aggregate-only; per-address balance is M2b (notes correlation)
info.type = (addr.kind == LiteWalletAppAddressKind::Shielded) ? "shielded" : "transparent";
info.has_spending_key = addr.spendabilityKnown ? addr.spendable : true;
if (addr.kind == LiteWalletAppAddressKind::Shielded) {
state.z_addresses.push_back(info);
} else {
state.t_addresses.push_back(info);
}
state.addresses.push_back(std::move(info));
}
}
if (model.hasTransactions) {
state.transactions.clear();
const int64_t chainHeight =
model.hasSyncStatus ? static_cast<int64_t>(model.sync.chainHeight) : 0;
for (const auto& record : model.transactions) {
TransactionInfo tx;
tx.txid = record.txid;
if (record.kind == LiteWalletAppTransactionKind::Send) {
tx.type = "send";
} else if (record.kind == LiteWalletAppTransactionKind::Receive) {
tx.type = "receive";
} else {
tx.type = record.signedAmountZatoshis < 0 ? "send" : "receive";
}
tx.amount = static_cast<double>(record.amountZatoshis) / kZatoshisPerCoin;
tx.timestamp = record.timestamp;
tx.address = record.address;
tx.memo = record.memo;
if (record.unconfirmed || !record.blockHeight.has_value() || chainHeight == 0) {
tx.confirmations = record.unconfirmed ? 0 : 1;
} else {
const int64_t confs = chainHeight - *record.blockHeight + 1;
tx.confirmations = confs > 0 ? static_cast<int>(confs) : 0;
}
state.transactions.push_back(std::move(tx));
}
}
if (model.hasSyncStatus) {
state.sync.blocks = static_cast<int>(model.sync.walletHeight);
state.sync.headers = static_cast<int>(model.sync.chainHeight);
state.sync.verification_progress = model.sync.progress;
state.sync.syncing = !model.sync.complete;
}
}
LiteWalletController::LiteWalletController(WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings,
LiteClientBridge bridge,
LiteWalletControllerOptions options)
: lifecycle_(capabilities,
std::move(connectionSettings),
std::move(bridge),
LiteWalletLifecycleOptions{options.allowBridgeCalls})
{
status_ = lifecycle_.status();
}
std::unique_ptr<LiteWalletController> LiteWalletController::createLinked(
WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings)
{
return std::make_unique<LiteWalletController>(
capabilities,
std::move(connectionSettings),
LiteClientBridge::linkedSdxl(),
LiteWalletControllerOptions{true});
}
void LiteWalletController::onLifecycleResult(const LiteWalletLifecycleResult& result)
{
status_ = result.status;
if (result.walletReady) {
walletOpen_ = true;
if (persist_) persist_();
}
}
LiteWalletLifecycleResult LiteWalletController::createWallet(LiteWalletCreateRequest request)
{
auto result = lifecycle_.createWallet(request);
secureWipeLiteSecret(request.passphrase);
onLifecycleResult(result);
return result;
}
LiteWalletLifecycleResult LiteWalletController::openWallet(LiteWalletOpenRequest request)
{
auto result = lifecycle_.openWallet(request);
secureWipeLiteSecret(request.passphrase);
onLifecycleResult(result);
return result;
}
LiteWalletLifecycleResult LiteWalletController::restoreWallet(LiteWalletRestoreRequest request)
{
auto result = lifecycle_.restoreWallet(request);
secureWipeLiteSecret(request.seedPhrase);
secureWipeLiteSecret(request.passphrase);
onLifecycleResult(result);
return result;
}
} // namespace wallet
} // namespace dragonx

View File

@@ -0,0 +1,85 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// App-owned controller that drives the lite wallet. It constructs and owns the
// lite services with bridge calls ENABLED (the services otherwise default to
// allowBridgeCalls=false and are never instantiated), executes real create/open/
// restore operations through the linked SDXL backend, securely wipes secrets after
// use, tracks wallet-open state, and invokes a persistence callback on success.
//
// Construction:
// - Production: LiteWalletController::createLinked(caps, connectionSettings)
// (uses LiteClientBridge::linkedSdxl(); requires DRAGONX_ENABLE_LITE_BACKEND).
// - Tests: construct directly with an injected bridge
// (e.g. LiteClientBridge::fromApi(makeFakeLiteApi())).
#pragma once
#include "lite_client_bridge.h"
#include "lite_connection_service.h"
#include "lite_wallet_lifecycle_service.h"
#include "lite_wallet_state_mapper.h"
#include "wallet_backend.h"
#include "wallet_capabilities.h"
#include <functional>
#include <memory>
#include <string>
namespace dragonx {
struct WalletState; // data/wallet_state.h
namespace wallet {
// Securely zero and clear a string holding secret material (seed/passphrase).
void secureWipeLiteSecret(std::string& secret);
// Apply a normalized lite refresh model onto the app's WalletState (the last hop the
// existing Balance/Receive/Transactions tabs read from). Converts zatoshis -> DRGX,
// splits addresses into shielded/transparent, and maps sync progress. Mutates in place
// (WalletState is non-copyable); only sections present in the model are touched.
void applyLiteRefreshModelToWalletState(const LiteWalletAppRefreshModel& model,
dragonx::WalletState& state);
struct LiteWalletControllerOptions {
bool allowBridgeCalls = true;
};
class LiteWalletController {
public:
LiteWalletController(WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings,
LiteClientBridge bridge,
LiteWalletControllerOptions options = LiteWalletControllerOptions{});
// Production factory: links the SDXL backend compiled into this build.
static std::unique_ptr<LiteWalletController> createLinked(
WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings);
// Invoked after a wallet becomes ready, so the owner can persist settings.
void setPersistCallback(std::function<void()> callback) { persist_ = std::move(callback); }
bool walletOpen() const { return walletOpen_; }
const WalletBackendStatus& status() const { return status_; }
LiteWalletLifecycleAvailability availability() const { return lifecycle_.availability(); }
// Execute a real lifecycle operation. The request is taken by value; its secret
// fields are securely wiped before returning. On a ready wallet, walletOpen()
// becomes true and the persist callback (if any) fires.
LiteWalletLifecycleResult createWallet(LiteWalletCreateRequest request);
LiteWalletLifecycleResult openWallet(LiteWalletOpenRequest request);
LiteWalletLifecycleResult restoreWallet(LiteWalletRestoreRequest request);
private:
void onLifecycleResult(const LiteWalletLifecycleResult& result);
LiteWalletLifecycleService lifecycle_;
std::function<void()> persist_;
bool walletOpen_ = false;
WalletBackendStatus status_;
};
} // namespace wallet
} // namespace dragonx