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:
409
src/wallet/lite_wallet_gateway.cpp
Normal file
409
src/wallet/lite_wallet_gateway.cpp
Normal 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
|
||||
Reference in New Issue
Block a user