Files
ObsidianDragon/src/wallet/lite_wallet_gateway.cpp
DanS 268eba6321 fix(lite): gateway refresh degrades gracefully on a failed command
LiteWalletGateway::refresh() aborted the entire refresh on the first command whose bridge
call or parse failed — which turned a single real-backend shape mismatch (e.g. syncstatus)
into a total, empty-everything refresh. Since the balance/addresses/list real shapes are
still unverified and we've already hit shape drift twice, make refresh resilient:

- Run every planned command; assembleLiteWalletRefreshBundle already skips failed results.
- result.ok = any usable data came back (bundle.complete still reflects all-succeeded).
- One command's failure now degrades gracefully — the other sections still populate.

testLiteWalletGatewayRefreshSkipsFailedCommand (fake balance returns invalid JSON) asserts
the refresh still succeeds with addresses/transactions/info populated and balance skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 06:57:10 -05:00

418 lines
16 KiB
C++

#include "wallet/lite_wallet_gateway.h"
#include <cctype>
#include <utility>
namespace dragonx::wallet {
namespace {
std::string trimGatewayCopy(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);
}
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 = "<redacted>";
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<LiteWalletGatewayCommand> liteWalletRefreshCommands(const LiteWalletRefreshRequest& request)
{
std::vector<LiteWalletGatewayCommand> 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<LiteWalletGatewayCommandResult>& 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() ? "<redacted>" : "<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 = "<redacted>";
return result;
}
} // namespace dragonx::wallet