Files
ObsidianDragon/src/wallet/lite_sync_service.cpp
DanS 012341b1a4 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>
2026-06-04 22:24:18 -05:00

383 lines
14 KiB
C++

#include "wallet/lite_sync_service.h"
#include <algorithm>
#include <cctype>
#include <limits>
#include <utility>
namespace dragonx::wallet {
namespace {
std::string trimSyncCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
std::uint64_t effectiveStagnantThreshold(std::uint64_t threshold)
{
return threshold == 0 ? 1 : threshold;
}
WalletBackendStatus makeSyncScaffoldStatus(WalletBackendState state,
std::string message,
const LiteSyncStatusResponse* syncStatus = nullptr)
{
WalletBackendStatus status;
status.state = state;
status.message = std::move(message);
if (syncStatus) {
status.walletHeight = static_cast<int>(std::min<std::uint64_t>(syncStatus->syncedBlocks, static_cast<std::uint64_t>(std::numeric_limits<int>::max())));
status.chainHeight = static_cast<int>(std::min<std::uint64_t>(syncStatus->totalBlocks, static_cast<std::uint64_t>(std::numeric_limits<int>::max())));
status.syncProgress = syncStatus->progress;
}
return status;
}
} // namespace
const char* liteSyncOperationName(LiteSyncOperation operation)
{
switch (operation) {
case LiteSyncOperation::StartSync: return "StartSync";
case LiteSyncOperation::PollSyncStatus: return "PollSyncStatus";
}
return "Unknown";
}
const char* liteSyncStartModeName(LiteSyncStartMode mode)
{
switch (mode) {
case LiteSyncStartMode::Startup: return "Startup";
case LiteSyncStartMode::Restore: return "Restore";
case LiteSyncStartMode::Rescan: return "Rescan";
case LiteSyncStartMode::Recovery: return "Recovery";
}
return "Unknown";
}
const char* liteSyncAvailabilityName(LiteSyncAvailability availability)
{
switch (availability) {
case LiteSyncAvailability::Ready: return "Ready";
case LiteSyncAvailability::UnsupportedBuild: return "UnsupportedBuild";
case LiteSyncAvailability::BackendUnavailable: return "BackendUnavailable";
case LiteSyncAvailability::BridgeUnavailable: return "BridgeUnavailable";
case LiteSyncAvailability::BridgeCallsDisabled: return "BridgeCallsDisabled";
case LiteSyncAvailability::NoUsableServer: return "NoUsableServer";
}
return "Unknown";
}
const char* liteSyncRecoveryDecisionKindName(LiteSyncRecoveryDecisionKind kind)
{
switch (kind) {
case LiteSyncRecoveryDecisionKind::KeepPolling: return "KeepPolling";
case LiteSyncRecoveryDecisionKind::SyncComplete: return "SyncComplete";
case LiteSyncRecoveryDecisionKind::Stuck: return "Stuck";
case LiteSyncRecoveryDecisionKind::ReorgDetected: return "ReorgDetected";
case LiteSyncRecoveryDecisionKind::InvalidStatus: return "InvalidStatus";
}
return "Unknown";
}
WalletBackendStatus walletStatusFromLiteSyncStatus(const LiteSyncStatusResponse& syncStatus)
{
if (syncStatus.complete) {
return makeSyncScaffoldStatus(
WalletBackendState::Ready,
"lite syncstatus reports sync complete",
&syncStatus);
}
return makeSyncScaffoldStatus(
WalletBackendState::Syncing,
"lite syncstatus reports sync in progress",
&syncStatus);
}
LiteSyncRecoveryDecision evaluateLiteSyncRecovery(const LiteSyncRecoveryInput& input)
{
if (input.totalBlocks > 0 && input.syncedBlocks > input.totalBlocks) {
return LiteSyncRecoveryDecision{
LiteSyncRecoveryDecisionKind::InvalidStatus,
false,
false,
false,
false,
true,
"synced block count is greater than total block count"
};
}
if (input.havePreviousSyncedBlocks && input.syncedBlocks < input.previousSyncedBlocks) {
return LiteSyncRecoveryDecision{
LiteSyncRecoveryDecisionKind::ReorgDetected,
false,
true,
true,
true,
false,
"synced block count moved backwards; model clear/rescan recovery"
};
}
if (input.totalBlocks > 0 && input.syncedBlocks >= input.totalBlocks) {
return LiteSyncRecoveryDecision{
LiteSyncRecoveryDecisionKind::SyncComplete,
false,
false,
false,
false,
false,
"syncstatus reports all blocks synced"
};
}
const auto threshold = effectiveStagnantThreshold(input.stagnantPollThreshold);
if (input.havePreviousSyncedBlocks &&
input.syncedBlocks == input.previousSyncedBlocks &&
input.stagnantPollCount >= threshold) {
return LiteSyncRecoveryDecision{
LiteSyncRecoveryDecisionKind::Stuck,
false,
false,
false,
true,
false,
"synced block count has not advanced past the configured threshold"
};
}
return LiteSyncRecoveryDecision{
LiteSyncRecoveryDecisionKind::KeepPolling,
true,
false,
false,
false,
false,
"syncstatus is usable; keep polling"
};
}
LiteSyncService::LiteSyncService(WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings,
LiteClientBridge* bridge,
LiteSyncServiceOptions options)
: capabilities_(capabilities),
connectionSettings_(std::move(connectionSettings)),
bridge_(bridge),
options_(options)
{
}
LiteSyncAvailability LiteSyncService::availability() const
{
if (!isLiteBuild(capabilities_)) return LiteSyncAvailability::UnsupportedBuild;
if (!supportsLiteBackend(capabilities_)) return LiteSyncAvailability::BackendUnavailable;
if (!bridge_ || !bridge_->available()) return LiteSyncAvailability::BridgeUnavailable;
if (!selectLiteServer(connectionSettings_).ok) return LiteSyncAvailability::NoUsableServer;
if (!options_.allowSyncStatusBridgeCalls) return LiteSyncAvailability::BridgeCallsDisabled;
return LiteSyncAvailability::Ready;
}
WalletBackendStatus LiteSyncService::status() const
{
const auto currentAvailability = availability();
if (currentAvailability == LiteSyncAvailability::NoUsableServer) {
return statusFor(currentAvailability, selectLiteServer(connectionSettings_).error);
}
return statusFor(currentAvailability);
}
LiteSyncPlan LiteSyncService::planStartSync(const LiteSyncStartRequest& request) const
{
return makePlan(
LiteSyncOperation::StartSync,
request.serverUrl,
options_.allowSyncStatusBridgeCalls,
request.mode,
request.forceRescan,
request.afterRestore);
}
LiteSyncPlan LiteSyncService::planSyncStatus(const LiteSyncStatusRequest& request) const
{
return makePlan(
LiteSyncOperation::PollSyncStatus,
request.serverUrl,
options_.allowSyncStatusBridgeCalls,
LiteSyncStartMode::Startup,
false,
false);
}
LiteSyncStartResult LiteSyncService::startSync(const LiteSyncStartRequest& request)
{
const auto plan = planStartSync(request);
if (!plan.ok) return blockedStartResult(plan, makeSyncScaffoldStatus(WalletBackendState::Error, plan.error));
const auto currentAvailability = availability();
if (currentAvailability == LiteSyncAvailability::UnsupportedBuild ||
currentAvailability == LiteSyncAvailability::BackendUnavailable ||
currentAvailability == LiteSyncAvailability::BridgeUnavailable ||
currentAvailability == LiteSyncAvailability::NoUsableServer) {
return blockedStartResult(plan, status());
}
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)
{
const auto plan = planSyncStatus(request);
if (!plan.ok) return blockedStatusResult(plan, makeSyncScaffoldStatus(WalletBackendState::Error, plan.error));
if (availability() != LiteSyncAvailability::Ready) return blockedStatusResult(plan, status());
LiteSyncStatusResult result;
result.plan = plan;
result.attempted = true;
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");
result.error = result.status.message;
return result;
}
result.bridgeAccepted = true;
const auto parsed = parseLiteSyncStatusResponse(bridgeCall.value);
if (!parsed.ok) {
result.parserError = parsed.error;
result.status = makeSyncScaffoldStatus(WalletBackendState::Error, "lite syncstatus response could not be parsed");
result.error = result.status.message;
return result;
}
result.ok = true;
result.parserError = LiteResultParserError::None;
result.syncStatus = parsed.syncStatus;
result.status = walletStatusFromLiteSyncStatus(result.syncStatus);
return result;
}
LiteServerSelectionResult LiteSyncService::selectServerForRequest(const std::string& serverUrl) const
{
const auto overrideUrl = trimSyncCopy(serverUrl);
if (!overrideUrl.empty()) {
if (!isLiteServerUrlUsable(overrideUrl)) {
return LiteServerSelectionResult{false, {}, 0, false, "lite sync server URL is not usable"};
}
return LiteServerSelectionResult{
true,
LiteServerEndpoint{overrideUrl, "Request", true},
0,
true,
{}
};
}
return selectLiteServer(connectionSettings_);
}
LiteSyncPlan LiteSyncService::makePlan(LiteSyncOperation operation,
const std::string& serverUrl,
bool bridgeExecutionAllowed,
LiteSyncStartMode startMode,
bool forceRescan,
bool afterRestore) const
{
LiteSyncPlan plan;
plan.operation = operation;
plan.startMode = startMode;
plan.bridgeExecutionAllowed = bridgeExecutionAllowed;
plan.forceRescan = forceRescan;
plan.afterRestore = afterRestore;
plan.command = operation == LiteSyncOperation::StartSync ? "sync" : "syncstatus";
auto selection = selectServerForRequest(serverUrl);
if (!selection.ok) {
plan.error = selection.error;
return plan;
}
plan.ok = true;
plan.server = selection.server;
plan.serverIndex = selection.serverIndex;
plan.customServer = selection.customServer;
return plan;
}
WalletBackendStatus LiteSyncService::statusFor(LiteSyncAvailability availability,
const std::string& detail) const
{
switch (availability) {
case LiteSyncAvailability::Ready:
return makeSyncScaffoldStatus(
WalletBackendState::Disconnected,
detail.empty() ? "lite sync scaffold ready; sync is not started" : detail);
case LiteSyncAvailability::UnsupportedBuild:
return makeSyncScaffoldStatus(
WalletBackendState::Unavailable,
"lite sync service is unsupported in full-node builds");
case LiteSyncAvailability::BackendUnavailable:
return makeSyncScaffoldStatus(
WalletBackendState::Unavailable,
"lite backend is not linked");
case LiteSyncAvailability::BridgeUnavailable:
return makeSyncScaffoldStatus(
WalletBackendState::Unavailable,
detail.empty() ? (bridge_ ? bridge_->unavailableReason() : "lite bridge is unavailable") : detail);
case LiteSyncAvailability::BridgeCallsDisabled:
return makeSyncScaffoldStatus(
WalletBackendState::Unavailable,
"lite syncstatus bridge calls are disabled");
case LiteSyncAvailability::NoUsableServer:
return makeSyncScaffoldStatus(
WalletBackendState::Error,
detail.empty() ? "no usable lite servers are configured" : detail);
}
return makeSyncScaffoldStatus(WalletBackendState::Unavailable, "unknown lite sync state");
}
LiteSyncStartResult LiteSyncService::blockedStartResult(const LiteSyncPlan& plan,
const WalletBackendStatus& blockedStatus) const
{
LiteSyncStartResult result;
result.plan = plan;
result.status = blockedStatus;
result.error = blockedStatus.message;
return result;
}
LiteSyncStatusResult LiteSyncService::blockedStatusResult(const LiteSyncPlan& plan,
const WalletBackendStatus& blockedStatus) const
{
LiteSyncStatusResult result;
result.plan = plan;
result.status = blockedStatus;
result.error = blockedStatus.message;
return result;
}
} // namespace dragonx::wallet