feat(lite): lite wallet foundation (inherited working-tree state)
Preserve the previously-uncommitted lite wallet implementation and related dev WIP under version control: - src/wallet/ lite services: client bridge, bridge runtime, connection, lifecycle, sync, gateway, result parsers, state mapper, artifact contract/resolver, refresh services, UI adapters, wallet_backend/capabilities. (Includes two small M1 fixes: lifecycle walletReady now parses the response; default chain name -> "main".) - src/chat/ chat protocol; tests/fixtures/ (lite + hushchat); tools/hushchat_fixture_check.cpp; scripts/build-lite-backend-artifact.sh. - Pre-existing modified app_network/security/wizard, network_refresh_service, sidebar, mining_tab, bootstrap dialog, and version headers captured as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
371
src/wallet/lite_sync_service.cpp
Normal file
371
src/wallet/lite_sync_service.cpp
Normal file
@@ -0,0 +1,371 @@
|
||||
#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_(std::move(bridge)),
|
||||
options_(options)
|
||||
{
|
||||
}
|
||||
|
||||
LiteSyncAvailability LiteSyncService::availability() const
|
||||
{
|
||||
if (!isLiteBuild(capabilities_)) return LiteSyncAvailability::UnsupportedBuild;
|
||||
if (!supportsLiteBackend(capabilities_)) return LiteSyncAvailability::BackendUnavailable;
|
||||
if (!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,
|
||||
false,
|
||||
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());
|
||||
}
|
||||
|
||||
return blockedStartResult(
|
||||
plan,
|
||||
makeSyncScaffoldStatus(WalletBackendState::Unavailable, "lite sync start execution is not implemented"));
|
||||
}
|
||||
|
||||
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_.unavailableReason() : 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
|
||||
Reference in New Issue
Block a user