feat(lite): M2b-1/2 — shared-bridge refactor + sync/refresh into WalletState

Shared-bridge refactor (litelib is a global singleton; every LiteClientBridge calls
litelib_shutdown() on destruction, so services must not each own one):
- LiteWalletLifecycleService, LiteWalletGateway, LiteSyncService now take a non-owning
  LiteClientBridge*; LiteWalletController owns the single bridge and passes &bridge_.

Sync + controller refresh:
- LiteSyncService::startSync executes the real "sync" command (was a stub).
- LiteWalletController: startSync() (auto-fires when a wallet becomes ready) and
  refreshWalletState(WalletState&) — polls syncstatus, runs gateway.refresh(), maps the
  bundle, applies balances/addresses/transactions/sync into WalletState.

Tests:
- fake_lite_backend.h returns command-shaped JSON (per tests/fixtures/lite/result_parsers.json).
- testLiteWalletControllerRefreshPopulatesState drives the full path against the fake.
- Surfaced + worked around a real integration issue: parseLiteInfoResponse requires
  latest_block_height and the gateway aborts the whole refresh on the first command's
  parse failure (fragile vs partial backend responses; hardening tracked for M2b-3).

Verified: ctest green; lite+backend, full-node, lite-no-backend apps + lite_smoke build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 22:24:18 -05:00
parent 5586f334a4
commit 012341b1a4
11 changed files with 159 additions and 33 deletions

View File

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

View File

@@ -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() ? "<redacted>" : "<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,

View File

@@ -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_;
};

View File

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

View File

@@ -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<void()> persist_;
bool walletOpen_ = false;
bool syncStarted_ = false;
WalletBackendStatus status_;
};

View File

@@ -135,11 +135,11 @@ LiteWalletRefreshBundle assembleLiteWalletRefreshBundle(const std::vector<LiteWa
LiteWalletGateway::LiteWalletGateway(WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings,
LiteClientBridge bridge,
LiteClientBridge* bridge,
LiteWalletGatewayOptions options)
: capabilities_(capabilities),
connectionSettings_(std::move(connectionSettings)),
bridge_(std::move(bridge)),
bridge_(bridge),
options_(options)
{
}
@@ -148,7 +148,7 @@ LiteWalletGatewayAvailability LiteWalletGateway::availability() const
{
if (!isLiteBuild(capabilities_)) return LiteWalletGatewayAvailability::UnsupportedBuild;
if (!supportsLiteBackend(capabilities_)) return LiteWalletGatewayAvailability::BackendUnavailable;
if (!bridge_.available()) return LiteWalletGatewayAvailability::BridgeUnavailable;
if (!bridge_ || !bridge_->available()) 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() ? "<redacted>" : "<empty>";
if (!bridgeCall.ok) {
result.status = makeGatewayStatus(WalletBackendState::Error, "lite wallet gateway bridge call failed");

View File

@@ -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_;
};

View File

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

View File

@@ -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_;
};

View File

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

View File

@@ -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<int>(state.z_addresses.size()), 1);
EXPECT_EQ(static_cast<int>(state.t_addresses.size()), 1);
EXPECT_EQ(static_cast<int>(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();