diff --git a/docs/lite-wallet-implementation-plan-v2-2026-06-04.md b/docs/lite-wallet-implementation-plan-v2-2026-06-04.md index 4c472af..89d5c81 100644 --- a/docs/lite-wallet-implementation-plan-v2-2026-06-04.md +++ b/docs/lite-wallet-implementation-plan-v2-2026-06-04.md @@ -93,7 +93,9 @@ Each milestone is independently demoable and gated by a fake-backend test. Order > **Status (2026-06-04): data pipeline landed; live wiring (M2b) remains.** > - ✅ **Last hop implemented + tested** — `applyLiteRefreshModelToWalletState(model, WalletState&)` in `lite_wallet_controller.{h,cpp}`: zatoshi→DRGX balances, z/t address split, transaction typing + confirmations (`chainHeight - blockHeight + 1`), sync progress. Mutates `WalletState` in place (it's non-copyable). `testLiteRefreshModelAppliesToWalletState()` drives a bundle through the existing `mapLiteWalletRefreshBundle` → apply → asserts the populated `WalletState`. `ctest` green. > - ℹ️ The fetch/parse/assemble pipeline already exists and works: `LiteWalletGateway::refresh()` → `LiteWalletRefreshBundle` → `mapLiteWalletRefreshBundle()` → `LiteWalletAppRefreshModel`. M2 just needed the final `→ WalletState` hop (above) plus live wiring. -> - ⏳ **M2b (remaining) — live wiring.** Blocked on a design decision surfaced by the M1 smoke run: `litelib` is a **global singleton** and every `LiteClientBridge` calls `litelib_shutdown()` (stops the live client) on destruction, so the controller cannot own a second owning bridge for the gateway/sync. **Decision: refactor the lite services (`LiteWalletLifecycleService`, `LiteWalletGateway`, `LiteConnectionService`, `LiteSyncService`) to take a non-owning bridge (`LiteClientBridge*`/shared handle); the controller owns the one bridge.** Then: implement `LiteSyncService::startSync` (replace the "not implemented" stub) + a background worker polling `syncstatus` and running `gateway.refresh()` (mirror `NetworkRefreshService`/`RefreshScheduler`: enqueue → worker → apply on main thread), apply into `App` `WalletState`, and hook into `App::update()`. Note: `litelib_execute` is already panic-safe (`catch_unwind`), so the polling workhorse won't abort the app. Per-address balances need notes-correlation (currently aggregate-only). +> - ✅ **M2b-1 — shared-bridge refactor (done).** `litelib` is a global singleton and every `LiteClientBridge` calls `litelib_shutdown()` on destruction, so services must not each own one. `LiteWalletLifecycleService`, `LiteWalletGateway`, and `LiteSyncService` now take a **non-owning `LiteClientBridge*`**; `LiteWalletController` owns the single bridge and passes `&bridge_`. Builds clean in all configs; existing tests stay green. +> - ✅ **M2b-2 — sync + controller refresh (done + tested).** `LiteSyncService::startSync` now executes the `sync` command (was a stub). `LiteWalletController` gained `startSync()` (auto-invoked when a wallet becomes ready) and `refreshWalletState(WalletState&)` which polls `syncstatus`, runs `gateway.refresh()`, maps the bundle, and applies it into `WalletState`. `testLiteWalletControllerRefreshPopulatesState()` drives the full path against the real-shape fake (balances/addresses/transactions/sync populated; no-op when no wallet open). The fake harness now returns command-shaped JSON per `tests/fixtures/lite/result_parsers.json`. (Surfaced a real bug: `info` requires `latest_block_height`, and the gateway aborts the whole refresh on the first command's parse failure — fixed in the fake; worth noting the gateway's abort-on-first-failure is fragile against partial backend responses.) +> - ⏳ **M2b-3 — threaded App hook (remaining).** Drive `refreshWalletState` from a controller-owned background worker (mirror `NetworkRefreshService`/`RefreshScheduler`): worker produces a copyable `LiteWalletAppRefreshModel`; `App::update()` applies it into `state_` on the main thread (WalletState is non-copyable, so pass the model, not the state, across threads). `litelib_execute` is `catch_unwind`-safe so the worker won't abort the app. Also: per-address balances (notes-correlation) and a real-backend refresh smoke (the real backend's JSON shapes may differ from the hand-built fixture). - Implement `LiteSyncService::startSync` (replace the "not implemented" stub) + a background worker polling `syncstatus`, mirroring `NetworkRefreshService`/`RefreshScheduler` (enqueue → worker → apply on main thread). - Drive `LiteWalletGateway` refresh (info/height/balance/addresses/notes/list/transactions) through `lite_result_parsers` → `lite_wallet_state_mapper` → `App` `WalletState` (`privateBalance`, `transparentBalance`, `addresses`, `transactions`, `sync`). - Hook the controller into `App::update()`'s refresh dispatch alongside (not inside) the full-node path. diff --git a/src/wallet/lite_sync_service.cpp b/src/wallet/lite_sync_service.cpp index 4448cd3..2a78a5d 100644 --- a/src/wallet/lite_sync_service.cpp +++ b/src/wallet/lite_sync_service.cpp @@ -166,11 +166,11 @@ LiteSyncRecoveryDecision evaluateLiteSyncRecovery(const LiteSyncRecoveryInput& i LiteSyncService::LiteSyncService(WalletCapabilities capabilities, LiteConnectionSettings connectionSettings, - LiteClientBridge bridge, + LiteClientBridge* bridge, LiteSyncServiceOptions options) : capabilities_(capabilities), connectionSettings_(std::move(connectionSettings)), - bridge_(std::move(bridge)), + bridge_(bridge), options_(options) { } @@ -179,7 +179,7 @@ LiteSyncAvailability LiteSyncService::availability() const { if (!isLiteBuild(capabilities_)) return LiteSyncAvailability::UnsupportedBuild; if (!supportsLiteBackend(capabilities_)) return LiteSyncAvailability::BackendUnavailable; - if (!bridge_.available()) return LiteSyncAvailability::BridgeUnavailable; + if (!bridge_ || !bridge_->available()) return LiteSyncAvailability::BridgeUnavailable; if (!selectLiteServer(connectionSettings_).ok) return LiteSyncAvailability::NoUsableServer; if (!options_.allowSyncStatusBridgeCalls) return LiteSyncAvailability::BridgeCallsDisabled; return LiteSyncAvailability::Ready; @@ -199,7 +199,7 @@ LiteSyncPlan LiteSyncService::planStartSync(const LiteSyncStartRequest& request) return makePlan( LiteSyncOperation::StartSync, request.serverUrl, - false, + options_.allowSyncStatusBridgeCalls, request.mode, request.forceRescan, request.afterRestore); @@ -229,9 +229,21 @@ LiteSyncStartResult LiteSyncService::startSync(const LiteSyncStartRequest& reque return blockedStartResult(plan, status()); } - return blockedStartResult( - plan, - makeSyncScaffoldStatus(WalletBackendState::Unavailable, "lite sync start execution is not implemented")); + LiteSyncStartResult result; + result.plan = plan; + result.attempted = true; + + const auto bridgeCall = bridge_->execute(plan.command, plan.args); + if (!bridgeCall.ok) { + result.status = makeSyncScaffoldStatus(WalletBackendState::Error, "lite sync start bridge call failed"); + result.error = result.status.message; + return result; + } + + result.ok = true; + result.syncStarted = true; + result.status = makeSyncScaffoldStatus(WalletBackendState::Syncing, "lite sync started"); + return result; } LiteSyncStatusResult LiteSyncService::pollSyncStatus(const LiteSyncStatusRequest& request) @@ -245,7 +257,7 @@ LiteSyncStatusResult LiteSyncService::pollSyncStatus(const LiteSyncStatusRequest result.plan = plan; result.attempted = true; - const auto bridgeCall = bridge_.execute(plan.command, plan.args); + const auto bridgeCall = bridge_->execute(plan.command, plan.args); result.bridgeResponseRedacted = bridgeCall.ok || !bridgeCall.error.empty() ? "" : ""; if (!bridgeCall.ok) { result.status = makeSyncScaffoldStatus(WalletBackendState::Error, "lite syncstatus bridge call failed"); @@ -334,7 +346,7 @@ WalletBackendStatus LiteSyncService::statusFor(LiteSyncAvailability availability case LiteSyncAvailability::BridgeUnavailable: return makeSyncScaffoldStatus( WalletBackendState::Unavailable, - detail.empty() ? bridge_.unavailableReason() : detail); + detail.empty() ? (bridge_ ? bridge_->unavailableReason() : "lite bridge is unavailable") : detail); case LiteSyncAvailability::BridgeCallsDisabled: return makeSyncScaffoldStatus( WalletBackendState::Unavailable, diff --git a/src/wallet/lite_sync_service.h b/src/wallet/lite_sync_service.h index 0438b68..c2865d0 100644 --- a/src/wallet/lite_sync_service.h +++ b/src/wallet/lite_sync_service.h @@ -123,7 +123,7 @@ class LiteSyncService { public: LiteSyncService(WalletCapabilities capabilities, LiteConnectionSettings connectionSettings, - LiteClientBridge bridge, + LiteClientBridge* bridge, LiteSyncServiceOptions options = {}); const WalletCapabilities& capabilities() const { return capabilities_; } @@ -155,7 +155,7 @@ private: WalletCapabilities capabilities_; LiteConnectionSettings connectionSettings_; - LiteClientBridge bridge_; + LiteClientBridge* bridge_ = nullptr; // non-owning; owned by LiteWalletController LiteSyncServiceOptions options_; }; diff --git a/src/wallet/lite_wallet_controller.cpp b/src/wallet/lite_wallet_controller.cpp index f9a4d41..c94ff4c 100644 --- a/src/wallet/lite_wallet_controller.cpp +++ b/src/wallet/lite_wallet_controller.cpp @@ -94,10 +94,13 @@ LiteWalletController::LiteWalletController(WalletCapabilities capabilities, LiteConnectionSettings connectionSettings, LiteClientBridge bridge, LiteWalletControllerOptions options) - : lifecycle_(capabilities, - std::move(connectionSettings), - std::move(bridge), - LiteWalletLifecycleOptions{options.allowBridgeCalls}) + : bridge_(std::move(bridge)), + lifecycle_(capabilities, connectionSettings, &bridge_, + LiteWalletLifecycleOptions{options.allowBridgeCalls}), + gateway_(capabilities, connectionSettings, &bridge_, + LiteWalletGatewayOptions{options.allowBridgeCalls}), + sync_(capabilities, connectionSettings, &bridge_, + LiteSyncServiceOptions{options.allowBridgeCalls}) { status_ = lifecycle_.status(); } @@ -119,9 +122,46 @@ void LiteWalletController::onLifecycleResult(const LiteWalletLifecycleResult& re if (result.walletReady) { walletOpen_ = true; if (persist_) persist_(); + startSync(); // begin background sync now that a wallet is ready } } +LiteSyncStartResult LiteWalletController::startSync() +{ + auto result = sync_.startSync(LiteSyncStartRequest{}); + if (result.syncStarted) syncStarted_ = true; + return result; +} + +bool LiteWalletController::refreshWalletState(dragonx::WalletState& state) +{ + if (!walletOpen_) return false; + + // Poll sync status first so the refresh bundle (and the mapped sync model) carries it. + LiteWalletRefreshRequest request; + const auto syncResult = sync_.pollSyncStatus(LiteSyncStatusRequest{}); + if (syncResult.ok) { + request.haveSyncStatus = true; + request.syncStatus = syncResult.syncStatus; + } + + const auto refreshResult = gateway_.refresh(request); + if (refreshResult.bundle.successfulCommandCount == 0 && !request.haveSyncStatus) { + status_ = refreshResult.status; + return false; + } + + const auto mapped = mapLiteWalletRefreshResult(refreshResult); + if (!mapped.ok) { + status_ = refreshResult.status; + return false; + } + + applyLiteRefreshModelToWalletState(mapped.model, state); + status_ = refreshResult.status; + return true; +} + LiteWalletLifecycleResult LiteWalletController::createWallet(LiteWalletCreateRequest request) { auto result = lifecycle_.createWallet(request); diff --git a/src/wallet/lite_wallet_controller.h b/src/wallet/lite_wallet_controller.h index 7137770..f7cd605 100644 --- a/src/wallet/lite_wallet_controller.h +++ b/src/wallet/lite_wallet_controller.h @@ -19,6 +19,8 @@ #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" @@ -72,12 +74,26 @@ public: LiteWalletLifecycleResult openWallet(LiteWalletOpenRequest request); LiteWalletLifecycleResult restoreWallet(LiteWalletRestoreRequest request); + bool syncStarted() const { return syncStarted_; } + + // Begin background sync on the backend (idempotent enough to call once a wallet is ready; + // also invoked automatically when a lifecycle op produces a ready wallet). + LiteSyncStartResult 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. + bool refreshWalletState(dragonx::WalletState& state); + private: void onLifecycleResult(const LiteWalletLifecycleResult& result); + LiteClientBridge bridge_; // the single owned bridge; services below borrow &bridge_ LiteWalletLifecycleService lifecycle_; + LiteWalletGateway gateway_; + LiteSyncService sync_; std::function persist_; bool walletOpen_ = false; + bool syncStarted_ = false; WalletBackendStatus status_; }; diff --git a/src/wallet/lite_wallet_gateway.cpp b/src/wallet/lite_wallet_gateway.cpp index 361ef9e..ae330ec 100644 --- a/src/wallet/lite_wallet_gateway.cpp +++ b/src/wallet/lite_wallet_gateway.cpp @@ -135,11 +135,11 @@ LiteWalletRefreshBundle assembleLiteWalletRefreshBundle(const std::vectoravailable()) return LiteWalletGatewayAvailability::BridgeUnavailable; if (!selectLiteServer(connectionSettings_).ok) return LiteWalletGatewayAvailability::NoUsableServer; if (!options_.allowBridgeCalls) return LiteWalletGatewayAvailability::BridgeCallsDisabled; return LiteWalletGatewayAvailability::Ready; @@ -288,7 +288,7 @@ WalletBackendStatus LiteWalletGateway::statusFor(LiteWalletGatewayAvailability a case LiteWalletGatewayAvailability::BridgeUnavailable: return makeGatewayStatus( WalletBackendState::Unavailable, - detail.empty() ? bridge_.unavailableReason() : detail); + detail.empty() ? (bridge_ ? bridge_->unavailableReason() : "lite bridge is unavailable") : detail); case LiteWalletGatewayAvailability::BridgeCallsDisabled: return makeGatewayStatus( WalletBackendState::Unavailable, @@ -319,7 +319,7 @@ LiteWalletGatewayCommandResult LiteWalletGateway::executePlannedCommand(const Li result.plan = plan; result.attempted = true; - const auto bridgeCall = bridge_.execute(plan.commandName, plan.args); + const auto bridgeCall = bridge_->execute(plan.commandName, plan.args); result.bridgeResponseRedacted = bridgeCall.ok || !bridgeCall.error.empty() ? "" : ""; if (!bridgeCall.ok) { result.status = makeGatewayStatus(WalletBackendState::Error, "lite wallet gateway bridge call failed"); diff --git a/src/wallet/lite_wallet_gateway.h b/src/wallet/lite_wallet_gateway.h index 38ff88c..cb493d7 100644 --- a/src/wallet/lite_wallet_gateway.h +++ b/src/wallet/lite_wallet_gateway.h @@ -129,7 +129,7 @@ class LiteWalletGateway { public: LiteWalletGateway(WalletCapabilities capabilities, LiteConnectionSettings connectionSettings, - LiteClientBridge bridge, + LiteClientBridge* bridge, LiteWalletGatewayOptions options = {}); const WalletCapabilities& capabilities() const { return capabilities_; } @@ -156,7 +156,7 @@ private: WalletCapabilities capabilities_; LiteConnectionSettings connectionSettings_; - LiteClientBridge bridge_; + LiteClientBridge* bridge_ = nullptr; // non-owning; owned by LiteWalletController LiteWalletGatewayOptions options_; }; diff --git a/src/wallet/lite_wallet_lifecycle_service.cpp b/src/wallet/lite_wallet_lifecycle_service.cpp index 7519979..009f0c9 100644 --- a/src/wallet/lite_wallet_lifecycle_service.cpp +++ b/src/wallet/lite_wallet_lifecycle_service.cpp @@ -119,11 +119,11 @@ std::string redactLitePrivateDataValue(const std::string& value) LiteWalletLifecycleService::LiteWalletLifecycleService(WalletCapabilities capabilities, LiteConnectionSettings connectionSettings, - LiteClientBridge bridge, + LiteClientBridge* bridge, LiteWalletLifecycleOptions options) : capabilities_(capabilities), connectionSettings_(std::move(connectionSettings)), - bridge_(std::move(bridge)), + bridge_(bridge), options_(options) { } @@ -132,7 +132,7 @@ LiteWalletLifecycleAvailability LiteWalletLifecycleService::availability() const { if (!isLiteBuild(capabilities_)) return LiteWalletLifecycleAvailability::UnsupportedBuild; if (!supportsLiteBackend(capabilities_)) return LiteWalletLifecycleAvailability::BackendUnavailable; - if (!bridge_.available()) return LiteWalletLifecycleAvailability::BridgeUnavailable; + if (!bridge_ || !bridge_->available()) return LiteWalletLifecycleAvailability::BridgeUnavailable; if (!selectLiteServer(connectionSettings_).ok) return LiteWalletLifecycleAvailability::NoUsableServer; if (!options_.allowBridgeCalls) return LiteWalletLifecycleAvailability::BridgeCallsDisabled; return LiteWalletLifecycleAvailability::Ready; @@ -291,7 +291,7 @@ WalletBackendStatus LiteWalletLifecycleService::statusFor( case LiteWalletLifecycleAvailability::BridgeUnavailable: return WalletBackendStatus{ WalletBackendState::Unavailable, - detail.empty() ? bridge_.unavailableReason() : detail, + detail.empty() ? (bridge_ ? bridge_->unavailableReason() : "lite bridge is unavailable") : detail, {}, {}, 0.0 @@ -321,7 +321,7 @@ LiteWalletLifecycleResult LiteWalletLifecycleService::executeCreate( const LiteWalletCreateRequest& request, const LiteWalletLifecyclePlan& plan) { - auto bridgeCall = bridge_.initializeNew(request.dangerous, plan.server.url); + auto bridgeCall = bridge_->initializeNew(request.dangerous, plan.server.url); return bridgeResult(plan, lifecycleCompletedStatus(), bridgeCall); } @@ -329,7 +329,7 @@ LiteWalletLifecycleResult LiteWalletLifecycleService::executeOpen( const LiteWalletOpenRequest& request, const LiteWalletLifecyclePlan& plan) { - auto bridgeCall = bridge_.initializeExisting(request.dangerous, plan.server.url); + auto bridgeCall = bridge_->initializeExisting(request.dangerous, plan.server.url); return bridgeResult(plan, lifecycleCompletedStatus(), bridgeCall); } @@ -337,7 +337,7 @@ LiteWalletLifecycleResult LiteWalletLifecycleService::executeRestore( const LiteWalletRestoreRequest& request, const LiteWalletLifecyclePlan& plan) { - auto bridgeCall = bridge_.initializeNewFromPhrase( + auto bridgeCall = bridge_->initializeNewFromPhrase( request.dangerous, plan.server.url, request.seedPhrase, diff --git a/src/wallet/lite_wallet_lifecycle_service.h b/src/wallet/lite_wallet_lifecycle_service.h index b7dfaa1..39c0b5e 100644 --- a/src/wallet/lite_wallet_lifecycle_service.h +++ b/src/wallet/lite_wallet_lifecycle_service.h @@ -102,9 +102,12 @@ std::string redactLitePrivateDataValue(const std::string& value); class LiteWalletLifecycleService { public: + // bridge is NON-OWNING (the controller owns the single shared LiteClientBridge); + // it must outlive this service. The lite backend is a global singleton and every + // LiteClientBridge shuts it down on destruction, so services must not own one. LiteWalletLifecycleService(WalletCapabilities capabilities, LiteConnectionSettings connectionSettings, - LiteClientBridge bridge, + LiteClientBridge* bridge, LiteWalletLifecycleOptions options = {}); const LiteConnectionSettings& connectionSettings() const { return connectionSettings_; } @@ -144,7 +147,7 @@ private: WalletCapabilities capabilities_; LiteConnectionSettings connectionSettings_; - LiteClientBridge bridge_; + LiteClientBridge* bridge_ = nullptr; // non-owning; owned by LiteWalletController LiteWalletLifecycleOptions options_; }; diff --git a/tests/fake_lite_backend.h b/tests/fake_lite_backend.h index c03c870..2263909 100644 --- a/tests/fake_lite_backend.h +++ b/tests/fake_lite_backend.h @@ -60,6 +60,28 @@ inline char* liteFakeExecute(const char* command, const char*) if (command && std::strcmp(command, "boom") == 0) { return liteFakeDup("Error: simulated lite backend failure"); } + // Command-appropriate canned responses matching the litelib JSON shapes (see + // tests/fixtures/lite/result_parsers.json), so the gateway/sync refresh path parses. + if (command) { + const char* c = command; + if (std::strcmp(c, "sync") == 0) return liteFakeDup("{\"result\":\"success\"}"); + if (std::strcmp(c, "syncstatus") == 0) + return liteFakeDup("{\"synced_blocks\":1000,\"total_blocks\":1000}"); + if (std::strcmp(c, "balance") == 0) + return liteFakeDup("{\"tbalance\":100000000,\"zbalance\":200000000,\"unconfirmed\":50000000," + "\"verified_zbalance\":180000000,\"spendable_zbalance\":170000000}"); + if (std::strcmp(c, "addresses") == 0) + return liteFakeDup("{\"z_addresses\":[\"zs1fakeaddr\"],\"t_addresses\":[\"R1fakeaddr\"]}"); + if (std::strcmp(c, "notes") == 0) + return liteFakeDup("{\"unspent_notes\":[],\"utxos\":[],\"pending_notes\":[],\"pending_utxos\":[]}"); + if (std::strcmp(c, "list") == 0) + return liteFakeDup("[{\"txid\":\"faketx\",\"datetime\":1700000000,\"block_height\":990," + "\"unconfirmed\":false,\"address\":\"zs1fakeaddr\",\"amount\":150000000,\"memo\":\"\"}]"); + if (std::strcmp(c, "height") == 0) return liteFakeDup("{\"height\":1000}"); + if (std::strcmp(c, "info") == 0) + return liteFakeDup("{\"chain_name\":\"main\",\"version\":\"sdxl-fake\",\"latest_block_height\":1000}"); + } + // Default for any other/unknown command. return liteFakeDup("{\"version\":\"sdxl-fake\"}"); } inline void liteFakeFree(char* v) diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 1487a65..709f36b 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -4392,7 +4392,7 @@ void testLiteBackendInjectableFakeBridge() { dragonx::test::resetLiteFakeCounters(); auto bridge = LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()); - const auto result = bridge.execute("info", ""); + const auto result = bridge.execute("ping", ""); // unknown command -> default fake response EXPECT_TRUE(result.ok); EXPECT_EQ(result.value, std::string("{\"version\":\"sdxl-fake\"}")); EXPECT_EQ(dragonx::test::g_liteFakeAlloc, 1L); @@ -4620,6 +4620,36 @@ void testLiteRefreshModelAppliesToWalletState() EXPECT_FALSE(state.sync.syncing); } +// M2b: the controller, after a wallet is ready, auto-starts sync and refreshWalletState() +// pulls balance/addresses/transactions/syncstatus through the shared bridge into WalletState. +void testLiteWalletControllerRefreshPopulatesState() +{ + using namespace dragonx::wallet; + const auto caps = makeWalletCapabilities(WalletBuildKind::Lite, /*embeddedDaemon*/ false, /*liteBackendLinked*/ true); + const auto conn = defaultLiteConnectionSettings(); + + LiteWalletController controller(caps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi())); + EXPECT_TRUE(controller.createWallet(LiteWalletCreateRequest{}).walletReady); + EXPECT_TRUE(controller.walletOpen()); + EXPECT_TRUE(controller.syncStarted()); // auto-started when the wallet became ready + + dragonx::WalletState state; + EXPECT_TRUE(controller.refreshWalletState(state)); + EXPECT_NEAR(state.privateBalance, 2.0, 1e-9); + EXPECT_NEAR(state.transparentBalance, 1.0, 1e-9); + EXPECT_EQ(static_cast(state.z_addresses.size()), 1); + EXPECT_EQ(static_cast(state.t_addresses.size()), 1); + EXPECT_EQ(static_cast(state.transactions.size()), 1); + EXPECT_EQ(state.transactions[0].type, std::string("receive")); + EXPECT_NEAR(state.transactions[0].amount, 1.5, 1e-9); + EXPECT_EQ(state.sync.headers, 1000); + + // No wallet open -> refresh is a safe no-op + LiteWalletController fresh(caps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi())); + dragonx::WalletState empty; + EXPECT_FALSE(fresh.refreshWalletState(empty)); +} + } // namespace int main() @@ -4656,6 +4686,7 @@ int main() testLiteWalletControllerLifecycle(); testLiteChainNameMigration(); testLiteRefreshModelAppliesToWalletState(); + testLiteWalletControllerRefreshPopulatesState(); testLiteBridgeRuntimeShutdownIsIdempotent(); testLiteBridgeRuntimeDestructorCallsShutdownOnce(); testLiteBridgeRuntimeShutdownWaitsForOwnedStringRelease();