#include "wallet/lite_sync_service.h" #include #include #include #include namespace dragonx::wallet { namespace { std::string trimSyncCopy(const std::string& value) { auto begin = value.begin(); while (begin != value.end() && std::isspace(static_cast(*begin))) ++begin; auto end = value.end(); while (end != begin && std::isspace(static_cast(*(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(std::min(syncStatus->syncedBlocks, static_cast(std::numeric_limits::max()))); status.chainHeight = static_cast(std::min(syncStatus->totalBlocks, static_cast(std::numeric_limits::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() ? "" : ""; 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