New address, Import key, Encrypt and Remove encryption called the
backend on the UI thread. Each ends in the backend's save, which waits on
the lock a scan holds for its whole duration and every refresh read holds
briefly, so during a scan the window froze until the scan ended. A create
or restore with a passphrase encrypted and saved just after its first
scan had started: for a restore, a freeze for the whole initial scan. A
server switch destroyed the old controller on the UI thread and joined
its refresh worker, which can sit for minutes in a read against a dead
server (each balance, notes or list read retries for ~62 s).
- The four ops run one at a time on a background lane and their results
are applied on the main thread; Settings shows "Working…" meanwhile.
Key and passphrase copies are wiped after the call, or when a queued
op is dropped.
- A create or restore encrypts and saves (an open unlocks) before the
first scan starts, so the save takes milliseconds and the scan runs on
the encrypted file.
- Import, Encrypt, Remove encryption and Receive's New address are
disabled while a scan runs ("Wait for blockchain to sync"); Redownload
blocks is disabled while an op is queued.
- A switch retires the old controller without blocking (queued ops
dropped, pending ones reported failed, its client shut down) and
destroys it on a background thread. It no longer saves on the way out:
the new client re-scans the blocks since the last save, as after a
restart.
- A switch waits for an op in flight, at most 15 s, so the op can't
finish and save on the old client while the new one owns the wallet
file. The Network tab already shows the new selection meanwhile.
- The refresh worker stops between backend commands, skips the remaining
server reads once one fails, and then retries after 4, 8, then 15 s
instead of every 2 s.
- Quit waits up to 2.5 s for an op in flight, e.g. Encrypt's save.
Encrypt and Remove encryption now report a failed save: the backend
returns it as a JSON error that the old check never saw, so the failure
was reported as success. The trimmed copy of a key typed into Import is
now wiped too, and stopping the refresh worker can no longer miss its
wakeup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
177 lines
6.4 KiB
C++
177 lines
6.4 KiB
C++
#pragma once
|
|
|
|
#include "lite_client_bridge.h"
|
|
#include "lite_connection_service.h"
|
|
#include "lite_result_parsers.h"
|
|
#include "lite_sync_service.h"
|
|
#include "wallet_backend.h"
|
|
#include "wallet_capabilities.h"
|
|
|
|
#include <cstddef>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace dragonx::wallet {
|
|
|
|
enum class LiteWalletGatewayCommand {
|
|
Info,
|
|
Height,
|
|
Balance,
|
|
Addresses,
|
|
Notes,
|
|
List,
|
|
};
|
|
|
|
enum class LiteWalletGatewayAvailability {
|
|
Ready,
|
|
UnsupportedBuild,
|
|
BackendUnavailable,
|
|
BridgeUnavailable,
|
|
BridgeCallsDisabled,
|
|
NoUsableServer,
|
|
};
|
|
|
|
struct LiteWalletGatewayRequest {
|
|
LiteWalletGatewayCommand command = LiteWalletGatewayCommand::Info;
|
|
std::string serverUrl;
|
|
};
|
|
|
|
struct LiteWalletGatewayPlan {
|
|
bool ok = false;
|
|
LiteWalletGatewayCommand command = LiteWalletGatewayCommand::Info;
|
|
LiteServerEndpoint server;
|
|
std::size_t serverIndex = 0;
|
|
bool customServer = false;
|
|
bool bridgeExecutionAllowed = false;
|
|
std::string commandName;
|
|
std::string args;
|
|
std::string error;
|
|
};
|
|
|
|
struct LiteWalletGatewayCommandResult {
|
|
bool ok = false;
|
|
bool attempted = false;
|
|
bool bridgeAccepted = false;
|
|
LiteWalletGatewayPlan plan;
|
|
LiteResultParserError parserError = LiteResultParserError::None;
|
|
bool backendError = false; // bridge call failed, or the reply was not JSON (the backend's plain-text sync error)
|
|
WalletBackendStatus status;
|
|
std::string bridgeResponseRedacted = "<empty>";
|
|
std::string error;
|
|
|
|
LiteInfoResponse info;
|
|
LiteHeightResponse height;
|
|
LiteBalanceResponse balance;
|
|
LiteAddressesResponse addresses;
|
|
LiteNotesResponse notes;
|
|
LiteTransactionsResponse transactions;
|
|
};
|
|
|
|
struct LiteWalletRefreshRequest {
|
|
std::string serverUrl;
|
|
bool includeInfo = true;
|
|
bool includeHeight = true;
|
|
bool includeBalance = true;
|
|
bool includeAddresses = true;
|
|
bool includeNotes = true;
|
|
bool includeTransactions = true;
|
|
bool haveSyncStatus = false;
|
|
LiteSyncStatusResponse syncStatus;
|
|
// Optional cooperative cancel, consulted before every backend command; returning false ends the refresh early
|
|
// (result.cancelled, model discarded by the caller).
|
|
std::function<bool()> keepGoing;
|
|
// Skip the remaining server-bound commands (info/balance/notes/list) once one returned a backend error: each
|
|
// `balance`/`notes`/`list` runs do_sync, which costs ~62 s of retry back-off against a refusing server.
|
|
bool stopSyncReadsOnFailure = false;
|
|
};
|
|
|
|
struct LiteWalletRefreshPlan {
|
|
bool ok = false;
|
|
std::vector<LiteWalletGatewayPlan> commands;
|
|
std::string error;
|
|
};
|
|
|
|
struct LiteWalletRefreshBundle {
|
|
bool hasInfo = false;
|
|
bool hasHeight = false;
|
|
bool hasBalance = false;
|
|
bool hasAddresses = false;
|
|
bool hasNotes = false;
|
|
bool hasTransactions = false;
|
|
bool hasSyncStatus = false;
|
|
bool complete = false;
|
|
std::size_t successfulCommandCount = 0;
|
|
|
|
LiteInfoResponse info;
|
|
LiteHeightResponse height;
|
|
LiteBalanceResponse balance;
|
|
LiteAddressesResponse addresses;
|
|
LiteNotesResponse notes;
|
|
LiteTransactionsResponse transactions;
|
|
LiteSyncStatusResponse syncStatus;
|
|
};
|
|
|
|
struct LiteWalletRefreshResult {
|
|
bool ok = false;
|
|
bool attempted = false;
|
|
bool cancelled = false; // keepGoing() returned false; partial results only
|
|
bool syncReadFailed = false; // a server-bound command returned a backend error this cycle
|
|
LiteWalletRefreshPlan plan;
|
|
std::vector<LiteWalletGatewayCommandResult> commandResults;
|
|
LiteWalletRefreshBundle bundle;
|
|
WalletBackendStatus status;
|
|
std::string error;
|
|
};
|
|
|
|
struct LiteWalletGatewayOptions {
|
|
bool allowBridgeCalls = false;
|
|
};
|
|
|
|
const char* liteWalletGatewayCommandName(LiteWalletGatewayCommand command);
|
|
// Pure helpers (unit-tested).
|
|
bool liteResponseIsBackendError(const std::string& response); // first non-space char is neither '{' nor '['
|
|
bool liteGatewayCommandReachesServer(LiteWalletGatewayCommand command); // Info/Balance/Notes/List; Height/Addresses are local
|
|
int liteRefreshDelayMs(int consecutiveFailures, int baseMs); // base, x2 per failure, capped at 15000 ms
|
|
const char* liteWalletGatewayAvailabilityName(LiteWalletGatewayAvailability availability);
|
|
|
|
std::vector<LiteWalletGatewayCommand> liteWalletRefreshCommands(const LiteWalletRefreshRequest& request);
|
|
LiteWalletRefreshBundle assembleLiteWalletRefreshBundle(const std::vector<LiteWalletGatewayCommandResult>& results,
|
|
const LiteWalletRefreshRequest& request);
|
|
|
|
class LiteWalletGateway {
|
|
public:
|
|
LiteWalletGateway(WalletCapabilities capabilities,
|
|
LiteConnectionSettings connectionSettings,
|
|
LiteClientBridge* bridge,
|
|
LiteWalletGatewayOptions options = {});
|
|
|
|
const WalletCapabilities& capabilities() const { return capabilities_; }
|
|
const LiteConnectionSettings& connectionSettings() const { return connectionSettings_; }
|
|
const LiteWalletGatewayOptions& options() const { return options_; }
|
|
|
|
LiteWalletGatewayAvailability availability() const;
|
|
WalletBackendStatus status() const;
|
|
|
|
LiteWalletGatewayPlan planCommand(const LiteWalletGatewayRequest& request) const;
|
|
LiteWalletRefreshPlan planRefresh(const LiteWalletRefreshRequest& request) const;
|
|
LiteWalletGatewayCommandResult fetchCommand(const LiteWalletGatewayRequest& request);
|
|
LiteWalletRefreshResult refresh(const LiteWalletRefreshRequest& request);
|
|
|
|
private:
|
|
LiteServerSelectionResult selectServerForRequest(const std::string& serverUrl) const;
|
|
WalletBackendStatus statusFor(LiteWalletGatewayAvailability availability,
|
|
const std::string& detail = {}) const;
|
|
LiteWalletGatewayCommandResult blockedCommandResult(const LiteWalletGatewayPlan& plan,
|
|
const WalletBackendStatus& blockedStatus) const;
|
|
LiteWalletGatewayCommandResult executePlannedCommand(const LiteWalletGatewayPlan& plan);
|
|
LiteWalletGatewayCommandResult parseBridgeResponse(const LiteWalletGatewayPlan& plan,
|
|
const std::string& response) const;
|
|
|
|
WalletCapabilities capabilities_;
|
|
LiteConnectionSettings connectionSettings_;
|
|
LiteClientBridge* bridge_ = nullptr; // non-owning; owned by LiteWalletController
|
|
LiteWalletGatewayOptions options_;
|
|
};
|
|
|
|
} // namespace dragonx::wallet
|