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:
2026-06-04 21:15:28 -05:00
parent a78a13edf3
commit 863d015628
69 changed files with 39458 additions and 85 deletions

View File

@@ -0,0 +1,409 @@
#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_(std::move(bridge)),
options_(options)
{
}
LiteWalletGatewayAvailability LiteWalletGateway::availability() const
{
if (!isLiteBuild(capabilities_)) return LiteWalletGatewayAvailability::UnsupportedBuild;
if (!supportsLiteBackend(capabilities_)) return LiteWalletGatewayAvailability::BackendUnavailable;
if (!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;
for (const auto& commandPlan : result.plan.commands) {
auto commandResult = executePlannedCommand(commandPlan);
if (!commandResult.ok) {
result.status = commandResult.status;
result.error = commandResult.error;
result.commandResults.push_back(std::move(commandResult));
return result;
}
result.commandResults.push_back(std::move(commandResult));
}
result.bundle = assembleLiteWalletRefreshBundle(result.commandResults, request);
result.ok = result.bundle.complete;
result.status = refreshStatusForRequest(request);
if (!result.ok) {
result.status = makeGatewayStatus(WalletBackendState::Error, "lite refresh bundle is incomplete");
result.error = result.status.message;
}
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_.unavailableReason() : 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