Files
ObsidianDragon/src/wallet/lite_wallet_controller.h
DanS 2daea67a1e feat(lite): M3 — new-address generation + sync-indicator confirmation
- LiteWalletController::newAddress(shielded) runs the backend "new" command ("zs"/"R" ->
  do_new_address), parses the ["addr"] response, and returns the new address; the next
  refresh lists it. Fast (local derivation), safe on the UI thread.
- fake_lite_backend returns ["zs1fakenew"]/["R1fakenew"] for "new" by args.
- testLiteWalletControllerNewAddress covers shielded/transparent + no-wallet error.

Also confirmed (no code needed): the sync-progress indicator already works for lite —
balance_tab reads state.sync.* which M2b-3 populates. Per-address balances landed in M2.

Remaining M3 is pure UI wiring (receive_tab button -> newAddress, loading/empty states),
which isn't verifiable without a GUI session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:36:00 -05:00

154 lines
6.6 KiB
C++

// 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_gateway.h"
#include "lite_sync_service.h"
#include "lite_wallet_state_mapper.h"
#include "wallet_backend.h"
#include "wallet_capabilities.h"
#include <atomic>
#include <condition_variable>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
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;
};
struct LiteNewAddressResult {
bool ok = false;
std::string address;
std::string error;
};
class LiteWalletController {
public:
LiteWalletController(WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings,
LiteClientBridge bridge,
LiteWalletControllerOptions options = LiteWalletControllerOptions{});
~LiteWalletController(); // stops + joins the background refresh worker
LiteWalletController(const LiteWalletController&) = delete;
LiteWalletController& operator=(const LiteWalletController&) = delete;
// 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);
bool syncStarted() const { return syncStarted_; }
bool syncComplete() const { return syncDone_ && syncDone_->load(); }
// Launch the backend sync on a detached background thread (NON-blocking; the backend's
// `sync` command runs a full, uninterruptible chain scan). Auto-invoked when a lifecycle
// op produces a ready wallet; safe to call once.
void startSync();
// Generate a new address (shielded if true, else transparent) via the backend. Fast (local
// key derivation), safe to call on the UI thread; the next refresh lists the new address.
LiteNewAddressResult newAddress(bool shielded);
// Poll sync status + fetch balance/addresses/transactions, and apply the result into the
// app's WalletState. Returns true if state was updated. Safe no-op when no wallet is open.
// Synchronous (blocks on the backend); used by tests and as the worker's unit of work.
bool refreshWalletState(dragonx::WalletState& state);
// Synchronous refresh that returns the mapped model (or nullopt). Pure w.r.t. shared
// state; safe to call from the background worker. nullopt when no wallet is open or the
// refresh produced nothing usable.
std::optional<LiteWalletAppRefreshModel> refreshModel();
// Main-thread handoff: if the background worker has produced a fresh model since the last
// call, move it into `out` and return true. Apply it with applyLiteRefreshModelToWalletState.
bool takeRefreshedModel(LiteWalletAppRefreshModel& out);
private:
void onLifecycleResult(const LiteWalletLifecycleResult& result);
void startWorker();
void stopWorker();
void workerLoop();
// The bridge is shared (not just owned) so the detached, uninterruptible sync thread can
// safely outlive the controller: it holds a ref, so the underlying bridge is destroyed
// (and litelib_shutdown called) only once BOTH the controller and a running sync release it.
std::shared_ptr<LiteClientBridge> bridge_;
LiteWalletLifecycleService lifecycle_;
LiteWalletGateway gateway_;
LiteSyncService sync_;
std::function<void()> persist_;
std::atomic<bool> walletOpen_{false};
std::atomic<bool> syncStarted_{false};
WalletBackendStatus status_; // written only on the main thread (lifecycle ops)
// Detached background sync (backend `sync` is a blocking, uninterruptible full scan).
std::thread syncThread_;
bool syncLaunched_ = false;
std::shared_ptr<std::atomic<bool>> syncDone_ = std::make_shared<std::atomic<bool>>(false);
// Joinable background refresh worker (fast iterations: syncstatus, plus data once synced).
std::thread worker_;
std::atomic<bool> running_{false};
std::mutex wakeMutex_;
std::condition_variable wakeCv_;
std::mutex modelMutex_;
std::optional<LiteWalletAppRefreshModel> pendingModel_; // guarded by modelMutex_
static constexpr int kRefreshIntervalMs = 2000;
};
} // namespace wallet
} // namespace dragonx