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

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