The backend `sync` command is a blocking, uninterruptible full chain scan (do_sync(true); does not honor the shutdown flag), and balance/list block until synced. Previously startSync() ran on the main thread (would freeze wallet creation) and the worker could block, making the destructor join() hang at shutdown. Redesign: - bridge is now std::shared_ptr<LiteClientBridge>, shared with a detached sync thread so detaching is safe and litelib_shutdown isn't called while a running sync still holds the bridge; the controller's own ref prevents premature shutdown during normal operation. - startSync() launches the blocking `sync` on a detached thread (non-blocking; never joined). - refreshModel() gates on syncDone_: while syncing it publishes syncstatus progress only; once synced it does the full balance/addresses/list refresh (now fast). - destructor joins only the fast poll worker and detaches the sync thread -> no hang. - syncComplete() accessor added. Tests (deterministic, via a blocking-sync fake; counters made atomic for the detached thread): testLiteWalletControllerShutdownDoesNotHangDuringSync (destructor returns <1.5s with sync blocked); refresh/worker tests wait for syncComplete()/a balance-bearing model. Stable across repeated runs; lite+backend and full-node apps build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
144 lines
6.3 KiB
C++
144 lines
6.3 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;
|
|
};
|
|
|
|
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();
|
|
|
|
// 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
|