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

@@ -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