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:
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user