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:
151
src/wallet/lite_wallet_controller.cpp
Normal file
151
src/wallet/lite_wallet_controller.cpp
Normal 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
|
||||
85
src/wallet/lite_wallet_controller.h
Normal file
85
src/wallet/lite_wallet_controller.h
Normal 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
|
||||
Reference in New Issue
Block a user