#include "wallet/lite_wallet_gateway.h" #include #include namespace dragonx::wallet { namespace { std::string trimGatewayCopy(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); } WalletBackendStatus makeGatewayStatus(WalletBackendState state, std::string message) { WalletBackendStatus status; status.state = state; status.message = std::move(message); return status; } WalletBackendStatus refreshStatusForRequest(const LiteWalletRefreshRequest& request) { if (request.haveSyncStatus) return walletStatusFromLiteSyncStatus(request.syncStatus); return makeGatewayStatus( WalletBackendState::Disconnected, "lite wallet refresh bundle assembled; runtime wallet state is unchanged"); } LiteWalletGatewayCommandResult parsedCommandResult(const LiteWalletGatewayPlan& plan) { LiteWalletGatewayCommandResult result; result.ok = true; result.attempted = true; result.bridgeAccepted = true; result.plan = plan; result.parserError = LiteResultParserError::None; result.status = makeGatewayStatus( WalletBackendState::Disconnected, "lite gateway response parsed into intermediate refresh model"); result.bridgeResponseRedacted = ""; return result; } } // namespace const char* liteWalletGatewayCommandName(LiteWalletGatewayCommand command) { switch (command) { case LiteWalletGatewayCommand::Info: return "info"; case LiteWalletGatewayCommand::Height: return "height"; case LiteWalletGatewayCommand::Balance: return "balance"; case LiteWalletGatewayCommand::Addresses: return "addresses"; case LiteWalletGatewayCommand::Notes: return "notes"; case LiteWalletGatewayCommand::List: return "list"; } return "unknown"; } const char* liteWalletGatewayAvailabilityName(LiteWalletGatewayAvailability availability) { switch (availability) { case LiteWalletGatewayAvailability::Ready: return "Ready"; case LiteWalletGatewayAvailability::UnsupportedBuild: return "UnsupportedBuild"; case LiteWalletGatewayAvailability::BackendUnavailable: return "BackendUnavailable"; case LiteWalletGatewayAvailability::BridgeUnavailable: return "BridgeUnavailable"; case LiteWalletGatewayAvailability::BridgeCallsDisabled: return "BridgeCallsDisabled"; case LiteWalletGatewayAvailability::NoUsableServer: return "NoUsableServer"; } return "Unknown"; } std::vector liteWalletRefreshCommands(const LiteWalletRefreshRequest& request) { std::vector commands; if (request.includeInfo) commands.push_back(LiteWalletGatewayCommand::Info); if (request.includeHeight) commands.push_back(LiteWalletGatewayCommand::Height); if (request.includeBalance) commands.push_back(LiteWalletGatewayCommand::Balance); if (request.includeAddresses) commands.push_back(LiteWalletGatewayCommand::Addresses); if (request.includeNotes) commands.push_back(LiteWalletGatewayCommand::Notes); if (request.includeTransactions) commands.push_back(LiteWalletGatewayCommand::List); return commands; } LiteWalletRefreshBundle assembleLiteWalletRefreshBundle(const std::vector& results, const LiteWalletRefreshRequest& request) { LiteWalletRefreshBundle bundle; if (request.haveSyncStatus) { bundle.hasSyncStatus = true; bundle.syncStatus = request.syncStatus; } for (const auto& result : results) { if (!result.ok) continue; ++bundle.successfulCommandCount; switch (result.plan.command) { case LiteWalletGatewayCommand::Info: bundle.hasInfo = true; bundle.info = result.info; break; case LiteWalletGatewayCommand::Height: bundle.hasHeight = true; bundle.height = result.height; break; case LiteWalletGatewayCommand::Balance: bundle.hasBalance = true; bundle.balance = result.balance; break; case LiteWalletGatewayCommand::Addresses: bundle.hasAddresses = true; bundle.addresses = result.addresses; break; case LiteWalletGatewayCommand::Notes: bundle.hasNotes = true; bundle.notes = result.notes; break; case LiteWalletGatewayCommand::List: bundle.hasTransactions = true; bundle.transactions = result.transactions; break; } } const auto expectedCommands = liteWalletRefreshCommands(request); bundle.complete = bundle.successfulCommandCount == expectedCommands.size(); return bundle; } LiteWalletGateway::LiteWalletGateway(WalletCapabilities capabilities, LiteConnectionSettings connectionSettings, LiteClientBridge* bridge, LiteWalletGatewayOptions options) : capabilities_(capabilities), connectionSettings_(std::move(connectionSettings)), bridge_(bridge), options_(options) { } LiteWalletGatewayAvailability LiteWalletGateway::availability() const { if (!isLiteBuild(capabilities_)) return LiteWalletGatewayAvailability::UnsupportedBuild; if (!supportsLiteBackend(capabilities_)) return LiteWalletGatewayAvailability::BackendUnavailable; if (!bridge_ || !bridge_->available()) return LiteWalletGatewayAvailability::BridgeUnavailable; if (!selectLiteServer(connectionSettings_).ok) return LiteWalletGatewayAvailability::NoUsableServer; if (!options_.allowBridgeCalls) return LiteWalletGatewayAvailability::BridgeCallsDisabled; return LiteWalletGatewayAvailability::Ready; } WalletBackendStatus LiteWalletGateway::status() const { const auto currentAvailability = availability(); if (currentAvailability == LiteWalletGatewayAvailability::NoUsableServer) { return statusFor(currentAvailability, selectLiteServer(connectionSettings_).error); } return statusFor(currentAvailability); } LiteWalletGatewayPlan LiteWalletGateway::planCommand(const LiteWalletGatewayRequest& request) const { LiteWalletGatewayPlan plan; plan.command = request.command; plan.commandName = liteWalletGatewayCommandName(request.command); plan.bridgeExecutionAllowed = options_.allowBridgeCalls; auto selection = selectServerForRequest(request.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; } LiteWalletRefreshPlan LiteWalletGateway::planRefresh(const LiteWalletRefreshRequest& request) const { LiteWalletRefreshPlan plan; const auto commands = liteWalletRefreshCommands(request); if (commands.empty()) { plan.error = "lite refresh request has no commands"; return plan; } for (const auto command : commands) { const auto commandPlan = planCommand(LiteWalletGatewayRequest{command, request.serverUrl}); if (!commandPlan.ok) { plan.error = commandPlan.error; return plan; } plan.commands.push_back(commandPlan); } plan.ok = true; return plan; } LiteWalletGatewayCommandResult LiteWalletGateway::fetchCommand(const LiteWalletGatewayRequest& request) { const auto plan = planCommand(request); if (!plan.ok) return blockedCommandResult(plan, makeGatewayStatus(WalletBackendState::Error, plan.error)); if (availability() != LiteWalletGatewayAvailability::Ready) return blockedCommandResult(plan, status()); return executePlannedCommand(plan); } LiteWalletRefreshResult LiteWalletGateway::refresh(const LiteWalletRefreshRequest& request) { LiteWalletRefreshResult result; result.plan = planRefresh(request); if (!result.plan.ok) { result.status = makeGatewayStatus(WalletBackendState::Error, result.plan.error); result.error = result.status.message; return result; } if (availability() != LiteWalletGatewayAvailability::Ready) { result.status = status(); result.error = result.status.message; return result; } result.attempted = true; // Run every planned command. A single command's bridge or parse failure must NOT abort the // whole refresh — partial data beats none, and real-backend response shapes can drift per // command. assembleLiteWalletRefreshBundle() ignores the failed results. for (const auto& commandPlan : result.plan.commands) { result.commandResults.push_back(executePlannedCommand(commandPlan)); } result.bundle = assembleLiteWalletRefreshBundle(result.commandResults, request); // Succeed if anything usable came back; some commands may have been skipped. (bundle.complete // still reflects whether *all* commands succeeded, for callers that care.) result.ok = result.bundle.successfulCommandCount > 0 || result.bundle.hasSyncStatus; if (!result.ok) { for (const auto& commandResult : result.commandResults) { if (!commandResult.ok) { result.status = commandResult.status; result.error = commandResult.error; break; } } if (result.error.empty()) { result.status = makeGatewayStatus(WalletBackendState::Error, "lite refresh produced no usable data"); result.error = result.status.message; } return result; } result.status = refreshStatusForRequest(request); return result; } LiteServerSelectionResult LiteWalletGateway::selectServerForRequest(const std::string& serverUrl) const { const auto overrideUrl = trimGatewayCopy(serverUrl); if (!overrideUrl.empty()) { if (!isLiteServerUrlUsable(overrideUrl)) { return LiteServerSelectionResult{false, {}, 0, false, "lite gateway server URL is not usable"}; } return LiteServerSelectionResult{ true, LiteServerEndpoint{overrideUrl, "Request", true}, 0, true, {} }; } return selectLiteServer(connectionSettings_); } WalletBackendStatus LiteWalletGateway::statusFor(LiteWalletGatewayAvailability availability, const std::string& detail) const { switch (availability) { case LiteWalletGatewayAvailability::Ready: return makeGatewayStatus( WalletBackendState::Disconnected, detail.empty() ? "lite wallet gateway scaffold ready; refresh execution is fake-test only" : detail); case LiteWalletGatewayAvailability::UnsupportedBuild: return makeGatewayStatus( WalletBackendState::Unavailable, "lite wallet gateway is unsupported in full-node builds"); case LiteWalletGatewayAvailability::BackendUnavailable: return makeGatewayStatus( WalletBackendState::Unavailable, "lite backend is not linked"); case LiteWalletGatewayAvailability::BridgeUnavailable: return makeGatewayStatus( WalletBackendState::Unavailable, detail.empty() ? (bridge_ ? bridge_->unavailableReason() : "lite bridge is unavailable") : detail); case LiteWalletGatewayAvailability::BridgeCallsDisabled: return makeGatewayStatus( WalletBackendState::Unavailable, "lite wallet gateway bridge calls are disabled"); case LiteWalletGatewayAvailability::NoUsableServer: return makeGatewayStatus( WalletBackendState::Error, detail.empty() ? "no usable lite servers are configured" : detail); } return makeGatewayStatus(WalletBackendState::Unavailable, "unknown lite wallet gateway state"); } LiteWalletGatewayCommandResult LiteWalletGateway::blockedCommandResult( const LiteWalletGatewayPlan& plan, const WalletBackendStatus& blockedStatus) const { LiteWalletGatewayCommandResult result; result.plan = plan; result.status = blockedStatus; result.error = blockedStatus.message; return result; } LiteWalletGatewayCommandResult LiteWalletGateway::executePlannedCommand(const LiteWalletGatewayPlan& plan) { LiteWalletGatewayCommandResult result; result.plan = plan; result.attempted = true; const auto bridgeCall = bridge_->execute(plan.commandName, plan.args); result.bridgeResponseRedacted = bridgeCall.ok || !bridgeCall.error.empty() ? "" : ""; if (!bridgeCall.ok) { result.status = makeGatewayStatus(WalletBackendState::Error, "lite wallet gateway bridge call failed"); result.error = result.status.message; return result; } return parseBridgeResponse(plan, bridgeCall.value); } LiteWalletGatewayCommandResult LiteWalletGateway::parseBridgeResponse( const LiteWalletGatewayPlan& plan, const std::string& response) const { auto result = parsedCommandResult(plan); switch (plan.command) { case LiteWalletGatewayCommand::Info: { const auto parsed = parseLiteInfoResponse(response); if (!parsed.ok) { result.ok = false; result.parserError = parsed.error; break; } result.info = parsed.info; return result; } case LiteWalletGatewayCommand::Height: { const auto parsed = parseLiteHeightResponse(response); if (!parsed.ok) { result.ok = false; result.parserError = parsed.error; break; } result.height = parsed.height; return result; } case LiteWalletGatewayCommand::Balance: { const auto parsed = parseLiteBalanceResponse(response); if (!parsed.ok) { result.ok = false; result.parserError = parsed.error; break; } result.balance = parsed.balance; return result; } case LiteWalletGatewayCommand::Addresses: { const auto parsed = parseLiteAddressesResponse(response); if (!parsed.ok) { result.ok = false; result.parserError = parsed.error; break; } result.addresses = parsed.addresses; return result; } case LiteWalletGatewayCommand::Notes: { const auto parsed = parseLiteNotesResponse(response); if (!parsed.ok) { result.ok = false; result.parserError = parsed.error; break; } result.notes = parsed.notes; return result; } case LiteWalletGatewayCommand::List: { const auto parsed = parseLiteTransactionsResponse(response); if (!parsed.ok) { result.ok = false; result.parserError = parsed.error; break; } result.transactions = parsed.transactions; return result; } } result.bridgeAccepted = true; result.status = makeGatewayStatus(WalletBackendState::Error, "lite wallet gateway response could not be parsed"); result.error = result.status.message; result.bridgeResponseRedacted = ""; return result; } } // namespace dragonx::wallet