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

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