// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "lite_wallet_lifecycle_service.h" #include #include namespace dragonx { namespace wallet { namespace { std::string trimLifecycleCopy(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); } LiteRedactedPrivateData redactedField(LitePrivateDataKind kind, const std::string& value) { return LiteRedactedPrivateData{kind, !value.empty(), redactLitePrivateDataValue(value)}; } std::vector createPrivateData(const LiteWalletCreateRequest& request) { return {redactedField(LitePrivateDataKind::Passphrase, request.passphrase)}; } std::vector openPrivateData(const LiteWalletOpenRequest& request) { return { redactedField(LitePrivateDataKind::WalletPath, request.walletPath), redactedField(LitePrivateDataKind::Passphrase, request.passphrase) }; } std::vector restorePrivateData(const LiteWalletRestoreRequest& request) { return { redactedField(LitePrivateDataKind::SeedPhrase, request.seedPhrase), redactedField(LitePrivateDataKind::WalletPath, request.walletPath), redactedField(LitePrivateDataKind::Passphrase, request.passphrase) }; } WalletBackendStatus lifecycleCompletedStatus() { return WalletBackendStatus{ WalletBackendState::Disconnected, "lite wallet lifecycle bridge call completed; sync is not implemented", {}, {}, 0.0 }; } } // namespace const char* liteWalletLifecycleOperationName(LiteWalletLifecycleOperation operation) { switch (operation) { case LiteWalletLifecycleOperation::CreateNew: return "CreateNew"; case LiteWalletLifecycleOperation::OpenExisting: return "OpenExisting"; case LiteWalletLifecycleOperation::RestoreFromSeed: return "RestoreFromSeed"; } return "Unknown"; } const char* liteWalletLifecycleAvailabilityName(LiteWalletLifecycleAvailability availability) { switch (availability) { case LiteWalletLifecycleAvailability::Ready: return "Ready"; case LiteWalletLifecycleAvailability::UnsupportedBuild: return "UnsupportedBuild"; case LiteWalletLifecycleAvailability::BackendUnavailable: return "BackendUnavailable"; case LiteWalletLifecycleAvailability::BridgeUnavailable: return "BridgeUnavailable"; case LiteWalletLifecycleAvailability::BridgeCallsDisabled: return "BridgeCallsDisabled"; case LiteWalletLifecycleAvailability::NoUsableServer: return "NoUsableServer"; case LiteWalletLifecycleAvailability::RolloutDisabled: return "RolloutDisabled"; } return "Unknown"; } const char* litePrivateDataKindName(LitePrivateDataKind kind) { switch (kind) { case LitePrivateDataKind::SeedPhrase: return "SeedPhrase"; case LitePrivateDataKind::Passphrase: return "Passphrase"; case LitePrivateDataKind::WalletPath: return "WalletPath"; case LitePrivateDataKind::BridgeResponse: return "BridgeResponse"; } return "Unknown"; } std::string redactLitePrivateDataValue(const std::string& value) { return value.empty() ? "" : ""; } LiteWalletLifecycleService::LiteWalletLifecycleService(WalletCapabilities capabilities, LiteConnectionSettings connectionSettings, LiteClientBridge* bridge, LiteWalletLifecycleOptions options) : capabilities_(capabilities), connectionSettings_(std::move(connectionSettings)), bridge_(bridge), options_(options) { } LiteWalletLifecycleAvailability LiteWalletLifecycleService::availability() const { if (!isLiteBuild(capabilities_)) return LiteWalletLifecycleAvailability::UnsupportedBuild; if (!supportsLiteBackend(capabilities_)) return LiteWalletLifecycleAvailability::BackendUnavailable; if (!bridge_ || !bridge_->available()) return LiteWalletLifecycleAvailability::BridgeUnavailable; // Runtime kill-switch / staged-rollout gate: a structural readiness check, applied before // server selection so a gated-off wallet reports the rollout reason rather than a server error. if (options_.rolloutBlocked) return LiteWalletLifecycleAvailability::RolloutDisabled; if (!selectLiteServer(connectionSettings_).ok) return LiteWalletLifecycleAvailability::NoUsableServer; if (!options_.allowBridgeCalls) return LiteWalletLifecycleAvailability::BridgeCallsDisabled; return LiteWalletLifecycleAvailability::Ready; } WalletBackendStatus LiteWalletLifecycleService::status() const { const auto currentAvailability = availability(); if (currentAvailability == LiteWalletLifecycleAvailability::NoUsableServer) { return statusFor(currentAvailability, selectLiteServer(connectionSettings_).error); } if (currentAvailability == LiteWalletLifecycleAvailability::RolloutDisabled) { return statusFor(currentAvailability, options_.rolloutMessage); } return statusFor(currentAvailability); } LiteWalletLifecyclePlan LiteWalletLifecycleService::planCreateWallet( const LiteWalletCreateRequest& request) const { return makePlan(LiteWalletLifecycleOperation::CreateNew, request.serverUrl, createPrivateData(request)); } LiteWalletLifecyclePlan LiteWalletLifecycleService::planOpenWallet( const LiteWalletOpenRequest& request) const { return makePlan(LiteWalletLifecycleOperation::OpenExisting, request.serverUrl, openPrivateData(request)); } LiteWalletLifecyclePlan LiteWalletLifecycleService::planRestoreWallet( const LiteWalletRestoreRequest& request) const { const std::string validationError = trimLifecycleCopy(request.seedPhrase).empty() ? "restore seed phrase is required" : std::string(); return makePlan(LiteWalletLifecycleOperation::RestoreFromSeed, request.serverUrl, restorePrivateData(request), validationError); } LiteWalletLifecycleResult LiteWalletLifecycleService::createWallet( const LiteWalletCreateRequest& request) { const auto plan = planCreateWallet(request); if (!plan.ok) return blockedResult(plan, WalletBackendStatus{WalletBackendState::Error, plan.error, {}, {}, 0.0}); const auto currentStatus = status(); if (availability() != LiteWalletLifecycleAvailability::Ready) return blockedResult(plan, currentStatus); return executeCreate(request, plan); } LiteWalletLifecycleResult LiteWalletLifecycleService::openWallet( const LiteWalletOpenRequest& request) { const auto plan = planOpenWallet(request); if (!plan.ok) return blockedResult(plan, WalletBackendStatus{WalletBackendState::Error, plan.error, {}, {}, 0.0}); const auto currentStatus = status(); if (availability() != LiteWalletLifecycleAvailability::Ready) return blockedResult(plan, currentStatus); return executeOpen(request, plan); } LiteWalletLifecycleResult LiteWalletLifecycleService::restoreWallet( const LiteWalletRestoreRequest& request) { const auto plan = planRestoreWallet(request); if (!plan.ok) return blockedResult(plan, WalletBackendStatus{WalletBackendState::Error, plan.error, {}, {}, 0.0}); const auto currentStatus = status(); if (availability() != LiteWalletLifecycleAvailability::Ready) return blockedResult(plan, currentStatus); return executeRestore(request, plan); } LiteServerSelectionResult LiteWalletLifecycleService::selectServerForRequest( const std::string& serverUrl) const { const std::string overrideUrl = trimLifecycleCopy(serverUrl); if (!overrideUrl.empty()) { if (!isLiteServerUrlUsable(overrideUrl)) { return LiteServerSelectionResult{false, {}, 0, false, "lite lifecycle server URL is not usable"}; } return LiteServerSelectionResult{ true, LiteServerEndpoint{overrideUrl, "Request", true}, 0, true, {} }; } return selectLiteServer(connectionSettings_); } LiteWalletLifecyclePlan LiteWalletLifecycleService::makePlan( LiteWalletLifecycleOperation operation, const std::string& serverUrl, std::vector privateData, const std::string& validationError) const { LiteWalletLifecyclePlan plan; plan.operation = operation; plan.privateData = std::move(privateData); plan.bridgeExecutionAllowed = options_.allowBridgeCalls; if (!validationError.empty()) { plan.error = validationError; return plan; } 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 LiteWalletLifecycleService::statusFor( LiteWalletLifecycleAvailability availability, const std::string& detail) const { switch (availability) { case LiteWalletLifecycleAvailability::Ready: return WalletBackendStatus{ WalletBackendState::Disconnected, detail.empty() ? "lite wallet lifecycle scaffold ready; sync is not implemented" : detail, {}, {}, 0.0 }; case LiteWalletLifecycleAvailability::UnsupportedBuild: return WalletBackendStatus{ WalletBackendState::Unavailable, "lite wallet lifecycle is unsupported in full-node builds", {}, {}, 0.0 }; case LiteWalletLifecycleAvailability::BackendUnavailable: return WalletBackendStatus{ WalletBackendState::Unavailable, "lite backend is not linked", {}, {}, 0.0 }; case LiteWalletLifecycleAvailability::BridgeUnavailable: return WalletBackendStatus{ WalletBackendState::Unavailable, detail.empty() ? (bridge_ ? bridge_->unavailableReason() : "lite bridge is unavailable") : detail, {}, {}, 0.0 }; case LiteWalletLifecycleAvailability::BridgeCallsDisabled: return WalletBackendStatus{ WalletBackendState::Unavailable, "lite wallet lifecycle bridge calls are disabled", {}, {}, 0.0 }; case LiteWalletLifecycleAvailability::NoUsableServer: return WalletBackendStatus{ WalletBackendState::Error, detail.empty() ? "no usable lite servers are configured" : detail, {}, {}, 0.0 }; case LiteWalletLifecycleAvailability::RolloutDisabled: return WalletBackendStatus{ WalletBackendState::Unavailable, detail.empty() ? "the lite wallet is disabled by the rollout policy" : detail, {}, {}, 0.0 }; } return WalletBackendStatus{WalletBackendState::Unavailable, "unknown lite wallet lifecycle state", {}, {}, 0.0}; } LiteWalletLifecycleResult LiteWalletLifecycleService::executeCreate( const LiteWalletCreateRequest& request, const LiteWalletLifecyclePlan& plan) { auto bridgeCall = bridge_->initializeNew(request.dangerous, plan.server.url); return bridgeResult(plan, lifecycleCompletedStatus(), bridgeCall); } LiteWalletLifecycleResult LiteWalletLifecycleService::executeOpen( const LiteWalletOpenRequest& request, const LiteWalletLifecyclePlan& plan) { auto bridgeCall = bridge_->initializeExisting(request.dangerous, plan.server.url); return bridgeResult(plan, lifecycleCompletedStatus(), bridgeCall); } LiteWalletLifecycleResult LiteWalletLifecycleService::executeRestore( const LiteWalletRestoreRequest& request, const LiteWalletLifecyclePlan& plan) { auto bridgeCall = bridge_->initializeNewFromPhrase( request.dangerous, plan.server.url, request.seedPhrase, request.birthday, request.account, request.overwrite); return bridgeResult(plan, lifecycleCompletedStatus(), bridgeCall); } LiteWalletLifecycleResult LiteWalletLifecycleService::blockedResult( const LiteWalletLifecyclePlan& plan, const WalletBackendStatus& blockedStatus) const { LiteWalletLifecycleResult result; result.operation = plan.operation; result.plan = plan; result.status = blockedStatus; result.error = blockedStatus.message; return result; } LiteWalletLifecycleResult LiteWalletLifecycleService::bridgeResult( const LiteWalletLifecyclePlan& plan, const WalletBackendStatus& successStatus, const LiteBridgeStringResult& bridgeCall) const { LiteWalletLifecycleResult result; result.operation = plan.operation; result.plan = plan; result.attempted = true; result.bridgeResponseRedacted = redactLitePrivateDataValue(bridgeCall.ok ? bridgeCall.value : bridgeCall.error); if (!bridgeCall.ok) { result.status = WalletBackendStatus{ WalletBackendState::Error, "lite wallet lifecycle bridge call failed", {}, {}, 0.0 }; result.error = result.status.message; return result; } result.ok = true; result.bridgeAccepted = true; // The bridge already classifies "Error:"-prefixed responses (and null/empty returns) as // failures (ok=false), so reaching here means the backend genuinely succeeded. The success // payloads are NOT uniformly JSON: create/restore return a seed object // ({"seed":..,"birthday":..}), but open (litelib_initialize_existing) returns the bare // string "OK". A JSON-validity test would therefore wrongly mark a successful open as // not-ready, so treat any non-empty success response as a ready wallet. result.walletReady = !bridgeCall.value.empty(); result.status = successStatus; return result; } } // namespace wallet } // namespace dragonx