Files
ObsidianDragon/src/wallet/lite_wallet_lifecycle_service.cpp
T
DanSandClaude Opus 4.8 dcd09dc542 refactor(audit): batch 7 — RPC/settings/lite/effects mechanical dedups
Six behavior-preserving consolidations from the audit:

- RPCClient::call() overloads share a private performCall() (payload dump +
  curl_easy_perform + http code) and a static parseRpcResult() (error->RpcError
  extraction). The timeout overload restores the prior timeout in both the
  success and catch paths, exactly as before.
- The six UnifiedCallback methods delegate to one splitUnified() that builds
  the (Callback, ErrorCallback) pair once (null-check preserved).
- One liteTrimCopy() in lite_connection_service replaces 5 file-local
  trim-copy helpers across the lite slice.
- lite_wallet_controller's inline height JSON parse now routes through the
  tested parseLiteHeightResponse().
- theme_effects.cpp: unpackRGB()/scaledAlpha() replace 14 RGB-unpack + 5
  alpha-scale copy-paste blocks.
- settings.cpp load() uses loadScalar()/loadClamped() helpers for ~44 scalar
  fields. Besides removing boilerplate this HARDENS loading: ~44 fields that
  previously only checked contains() now also verify the JSON type, so a
  malformed value in settings.json falls back to the default instead of
  throwing/misreading. Custom cases (enum parses, legacy migrations, arrays)
  stay inline; save() is unchanged.

Full-node + Lite build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:56:54 -05:00

401 lines
14 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "lite_wallet_lifecycle_service.h"
#include <utility>
namespace dragonx {
namespace wallet {
namespace {
LiteRedactedPrivateData redactedField(LitePrivateDataKind kind, const std::string& value)
{
return LiteRedactedPrivateData{kind, !value.empty(), redactLitePrivateDataValue(value)};
}
std::vector<LiteRedactedPrivateData> createPrivateData(const LiteWalletCreateRequest& request)
{
return {redactedField(LitePrivateDataKind::Passphrase, request.passphrase)};
}
std::vector<LiteRedactedPrivateData> openPrivateData(const LiteWalletOpenRequest& request)
{
return {
redactedField(LitePrivateDataKind::WalletPath, request.walletPath),
redactedField(LitePrivateDataKind::Passphrase, request.passphrase)
};
}
std::vector<LiteRedactedPrivateData> 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() ? "<empty>" : "<redacted>";
}
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 = liteTrimCopy(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 = liteTrimCopy(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<LiteRedactedPrivateData> 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