fix(lite): parse real syncstatus shapes (idle vs in-progress)
The real backend returns syncstatus as idle {"syncing":"false"} (string) or in-progress
{"syncing":"true","synced_blocks":N,"total_blocks":M} (commands.rs:83-87), but
parseLiteSyncStatusResponse hard-required the block fields and failed whenever the wallet
wasn't actively syncing — so sync/progress never updated in the real app.
- Read "syncing" as a string; require synced_blocks/total_blocks only when syncing=true;
idle => complete, synced/total 0.
- fake_lite_backend syncstatus now uses the real "syncing":"true" shape.
- testLiteSyncStatusParserRealShapes covers idle, in-progress, and missing-counts-while-syncing.
- Verified against the live backend via lite_smoke --refresh (syncstatus parse_ok=1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -97,7 +97,7 @@ Each milestone is independently demoable and gated by a fake-backend test. Order
|
||||
> - ✅ **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 (done + tested).** `LiteWalletController` owns a background worker (`std::thread`) that, once a wallet is ready, refreshes every ~4s and publishes a copyable `LiteWalletAppRefreshModel` under a mutex; `App::update()` calls `takeRefreshedModel()` and applies it into `state_` on the main thread (WalletState is non-copyable, so the model crosses the thread boundary, not the state). Worker auto-starts on lifecycle-ready and is stopped+joined in the controller destructor. `status_` is written only on the main thread to avoid races; `walletOpen_`/`syncStarted_` are atomic. `testLiteWalletControllerWorkerProducesModel()` opens a wallet and asserts the worker publishes a populated model (stable across repeated runs). Builds clean in all configs.
|
||||
> **Real-backend refresh smoke (2026-06-04): ran `lite_smoke --create --refresh` against the live backend — found two real bugs** the fake/fixture couldn't (smoke now links `lite_result_parsers` and runs each command's real output through the parser):
|
||||
> 1. **OPEN — `syncstatus` parser mismatch.** `parseLiteSyncStatusResponse` hard-requires `synced_blocks`/`total_blocks`, but the real backend (per `commands.rs:83-87`) returns **idle = `{"syncing":"false"}`** (string!) and only **in-progress = `{"syncing":"true","synced_blocks":N,"total_blocks":M}`**. So syncstatus fails to parse whenever the wallet isn't actively syncing → sync/progress never updates in the real app. Fix: read `syncing` as a string, make the block fields optional. (info/balance/addresses parsers verified OK against real output.)
|
||||
> 1. **FIXED — `syncstatus` parser mismatch.** `parseLiteSyncStatusResponse` hard-required `synced_blocks`/`total_blocks`, but the real backend (per `commands.rs:83-87`) returns **idle = `{"syncing":"false"}`** (string!) and only **in-progress = `{"syncing":"true","synced_blocks":N,"total_blocks":M}`**. The parser now reads `syncing` as a string and treats the block fields as in-progress-only (idle → complete, synced/total 0). Covered by `testLiteSyncStatusParserRealShapes()` and **verified against the live backend** (`syncstatus parse_ok=1`). (info/balance/addresses parsers also verified OK against real output.)
|
||||
> 2. **OPEN — first data query blocks on a full chain sync.** `execute("balance"/"list")` on a fresh wallet triggers a synchronous multi-million-block sync (observed "Syncing 1.76M/2.99M…"). On the M2b-3 worker thread that means the controller's destructor `join()` would hang at app shutdown. Needs: a cancel/timeout path for in-flight refresh (e.g., don't block shutdown on the worker), and likely gating data fetches until sync has progressed. **This is the main blocker for a usable real lite wallet** and should lead M2 polish / M3.
|
||||
>
|
||||
> - ⏳ **Remaining for M2 polish:** fix the syncstatus parser (above), address the blocking-sync/worker-shutdown issue (above), per-address balances (notes-correlation; currently aggregate-only), and harden the gateway's abort-on-first-failure (skip-and-continue per command).
|
||||
|
||||
@@ -557,11 +557,27 @@ LiteSyncStatusParseResult parseLiteSyncStatusResponse(const json& value)
|
||||
{
|
||||
auto result = makeResult<LiteSyncStatusParseResult>(LiteResultCommand::SyncStatus);
|
||||
if (!requireObject(value, "$", result)) return result;
|
||||
if (!readRequiredUnsignedField(value, "synced_blocks", result.syncStatus.syncedBlocks, result)) return result;
|
||||
if (!readRequiredUnsignedField(value, "total_blocks", result.syncStatus.totalBlocks, result)) return result;
|
||||
if (result.syncStatus.totalBlocks > 0) {
|
||||
result.syncStatus.progress = std::min(1.0, static_cast<double>(result.syncStatus.syncedBlocks) / static_cast<double>(result.syncStatus.totalBlocks));
|
||||
result.syncStatus.complete = result.syncStatus.syncedBlocks >= result.syncStatus.totalBlocks;
|
||||
|
||||
// The backend reports "syncing" as a STRING ("true"/"false"). synced_blocks/total_blocks
|
||||
// are present only while actively syncing; an idle wallet returns just {"syncing":"false"}.
|
||||
std::string syncing = "false";
|
||||
if (!readOptionalStringField(value, "syncing", syncing, result)) return result;
|
||||
const bool isSyncing = (syncing == "true" || syncing == "1");
|
||||
|
||||
if (isSyncing) {
|
||||
if (!readRequiredUnsignedField(value, "synced_blocks", result.syncStatus.syncedBlocks, result)) return result;
|
||||
if (!readRequiredUnsignedField(value, "total_blocks", result.syncStatus.totalBlocks, result)) return result;
|
||||
result.syncStatus.complete = false; // actively syncing
|
||||
result.syncStatus.progress = result.syncStatus.totalBlocks > 0
|
||||
? std::min(1.0, static_cast<double>(result.syncStatus.syncedBlocks) /
|
||||
static_cast<double>(result.syncStatus.totalBlocks))
|
||||
: 0.0;
|
||||
} else {
|
||||
// Not actively syncing: the backend reports no block counts. Treat as idle/caught-up.
|
||||
result.syncStatus.syncedBlocks = 0;
|
||||
result.syncStatus.totalBlocks = 0;
|
||||
result.syncStatus.complete = true;
|
||||
result.syncStatus.progress = 1.0;
|
||||
}
|
||||
succeed(result);
|
||||
return result;
|
||||
|
||||
@@ -65,8 +65,8 @@ inline char* liteFakeExecute(const char* command, const char*)
|
||||
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, "syncstatus") == 0) // real backend shape: "syncing" is a string
|
||||
return liteFakeDup("{\"syncing\":\"true\",\"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}");
|
||||
|
||||
@@ -4651,6 +4651,36 @@ void testLiteWalletControllerRefreshPopulatesState()
|
||||
EXPECT_FALSE(fresh.refreshWalletState(empty));
|
||||
}
|
||||
|
||||
// syncstatus parser must accept both real backend shapes: idle {"syncing":"false"} and
|
||||
// in-progress {"syncing":"true","synced_blocks":N,"total_blocks":M} ("syncing" is a string).
|
||||
void testLiteSyncStatusParserRealShapes()
|
||||
{
|
||||
using namespace dragonx::wallet;
|
||||
|
||||
// Idle: no block counts, syncing is the string "false".
|
||||
{
|
||||
const auto p = parseLiteSyncStatusResponse(std::string("{\"syncing\":\"false\"}"));
|
||||
EXPECT_TRUE(p.ok);
|
||||
EXPECT_EQ(static_cast<long long>(p.syncStatus.syncedBlocks), 0LL);
|
||||
EXPECT_TRUE(p.syncStatus.complete);
|
||||
}
|
||||
// In-progress: block counts present.
|
||||
{
|
||||
const auto p = parseLiteSyncStatusResponse(
|
||||
std::string("{\"syncing\":\"true\",\"synced_blocks\":900,\"total_blocks\":1000}"));
|
||||
EXPECT_TRUE(p.ok);
|
||||
EXPECT_EQ(static_cast<long long>(p.syncStatus.syncedBlocks), 900LL);
|
||||
EXPECT_EQ(static_cast<long long>(p.syncStatus.totalBlocks), 1000LL);
|
||||
EXPECT_FALSE(p.syncStatus.complete);
|
||||
EXPECT_NEAR(p.syncStatus.progress, 0.9, 1e-9);
|
||||
}
|
||||
// While syncing, missing block counts is still an error.
|
||||
{
|
||||
const auto p = parseLiteSyncStatusResponse(std::string("{\"syncing\":\"true\"}"));
|
||||
EXPECT_FALSE(p.ok);
|
||||
}
|
||||
}
|
||||
|
||||
// M2b-3: opening a wallet auto-starts the background worker, which produces a refresh model
|
||||
// the main thread can pick up via takeRefreshedModel() and apply to WalletState.
|
||||
void testLiteWalletControllerWorkerProducesModel()
|
||||
@@ -4719,6 +4749,7 @@ int main()
|
||||
testLiteWalletControllerLifecycle();
|
||||
testLiteChainNameMigration();
|
||||
testLiteRefreshModelAppliesToWalletState();
|
||||
testLiteSyncStatusParserRealShapes();
|
||||
testLiteWalletControllerRefreshPopulatesState();
|
||||
testLiteWalletControllerWorkerProducesModel();
|
||||
testLiteBridgeRuntimeShutdownIsIdempotent();
|
||||
|
||||
Reference in New Issue
Block a user