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:
@@ -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,
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user