Opening an existing lite wallet ran synchronously on the UI thread and used a
single server, so a dead/unreachable lightwalletd server froze startup for the
connect timeout and then stranded the wallet ("disconnected" spinner) — and the
DragonX lite servers are flaky (often several down at once).
Add LiteWalletController::beginOpenExisting() / pumpAsyncOpen(): the open runs on
a background thread (mirroring the sync/broadcast shared-lifetime pattern — it
captures only shared_ptrs + value copies, never `this`), trying the preferred
server first and then every other usable default until one succeeds. The main
thread finalizes the result (flips walletOpen, starts sync) or records the reason.
The rollout gate is still checked up-front on the main thread.
App: auto-open now calls beginOpenExisting() and pumps it each tick, retrying on
a 20s interval so a transient outage self-heals once a server returns; a failed
open surfaces its reason (notification + Network tab) instead of a silent spinner.
Tested: a fake bridge that fails specific servers exercises both
preferred-dead -> fallback-opens and all-dead -> fails-with-reason. Built clean
for full-node, lite, and Windows cross-compile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
198 lines
9.5 KiB
C++
198 lines
9.5 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
//
|
|
// Deterministic in-process fake SDXL backend for lite-wallet tests.
|
|
//
|
|
// Provides a LiteClientBridgeApi whose function pointers return canned JSON and
|
|
// track owned-string allocation/free counts, so tests can drive the lite bridge
|
|
// (and, from M1 on, the lite services) without a real litelib_* library, network,
|
|
// or the DRAGONX_ENABLE_LITE_BACKEND build flag. Build a bridge with:
|
|
//
|
|
// auto bridge = dragonx::wallet::LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi());
|
|
//
|
|
// Invariant for leak/double-free checks: after a test, g_liteFakeAlloc == g_liteFakeFreed.
|
|
|
|
#pragma once
|
|
|
|
#include "wallet/lite_client_bridge.h"
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <thread>
|
|
|
|
namespace dragonx {
|
|
namespace test {
|
|
|
|
// Owned-string accounting (atomic: a detached sync thread may touch these concurrently).
|
|
inline std::atomic<long> g_liteFakeAlloc{0}; // owned strings handed to the bridge
|
|
inline std::atomic<long> g_liteFakeFreed{0}; // owned strings released via freeString
|
|
inline bool g_liteFakeWalletExists = true;
|
|
inline bool g_liteFakeServerOnline = true;
|
|
inline bool g_liteFakeShutdownCalled = false;
|
|
inline std::atomic<bool> g_liteFakeSyncBlock{false}; // when true, the "sync" command blocks
|
|
inline std::atomic<bool> g_liteFakeBadBalance{false}; // when true, "balance" returns invalid JSON
|
|
inline std::atomic<bool> g_liteFakeSendFails{false}; // when true, "send"/"shield" return {"error":..}
|
|
inline std::atomic<long> g_liteFakeSaveCount{0}; // counts "save" commands (persistence checks)
|
|
inline std::atomic<bool> g_liteFakeEncrypted{false}; // wallet-encryption state machine (encrypt/...)
|
|
inline std::atomic<bool> g_liteFakeLocked{false}; // spending-keys-locked state
|
|
// initialize_existing fails for any server URL containing this substring (open-failover tests).
|
|
inline std::string g_liteFakeDeadServerSubstr;
|
|
|
|
inline void resetLiteFakeCounters()
|
|
{
|
|
g_liteFakeAlloc = 0;
|
|
g_liteFakeFreed = 0;
|
|
g_liteFakeShutdownCalled = false;
|
|
g_liteFakeDeadServerSubstr.clear();
|
|
}
|
|
|
|
inline char* liteFakeDup(const char* s)
|
|
{
|
|
char* p = static_cast<char*>(std::malloc(std::strlen(s) + 1));
|
|
std::strcpy(p, s);
|
|
++g_liteFakeAlloc;
|
|
return p;
|
|
}
|
|
|
|
inline bool liteFakeWalletExists(const char*) { return g_liteFakeWalletExists; }
|
|
// Match the real litelib_* return shapes so tests exercise the production walletReady path:
|
|
// create/restore return a seed object ({"seed":..,"birthday":..}); open returns the bare
|
|
// string "OK" (NOT JSON) — see litelib_initialize_existing.
|
|
inline char* liteFakeInitNew(bool, const char*)
|
|
{
|
|
return liteFakeDup("{\"seed\":\"fake seed phrase words\",\"birthday\":0}");
|
|
}
|
|
inline char* liteFakeInitFromPhrase(bool, const char*, const char*,
|
|
unsigned long long, unsigned long long, bool)
|
|
{
|
|
return liteFakeDup("{\"seed\":\"fake seed phrase words\",\"birthday\":0}");
|
|
}
|
|
// initialize_existing fails (server unreachable) for any server URL containing
|
|
// g_liteFakeDeadServerSubstr, so tests can exercise the controller's open-with-failover.
|
|
inline char* liteFakeInitExisting(bool, const char* server)
|
|
{
|
|
if (!g_liteFakeDeadServerSubstr.empty() && server &&
|
|
std::string(server).find(g_liteFakeDeadServerSubstr) != std::string::npos) {
|
|
return liteFakeDup("Error: could not connect to server"); // bridge maps to ok=false
|
|
}
|
|
return liteFakeDup("OK");
|
|
}
|
|
inline char* liteFakeExecute(const char* command, const char* args)
|
|
{
|
|
// new-address generation returns a JSON array with the new address (type from args: zs/R).
|
|
if (command && std::strcmp(command, "new") == 0) {
|
|
return liteFakeDup(args && std::strcmp(args, "R") == 0 ? "[\"R1fakenew\"]" : "[\"zs1fakenew\"]");
|
|
}
|
|
// A command named "boom" yields an "Error:"-prefixed response (the bridge's
|
|
// looksLikeError contract maps that to ok=false).
|
|
if (command && std::strcmp(command, "boom") == 0) {
|
|
return liteFakeDup("Error: simulated lite backend failure");
|
|
}
|
|
// Command-appropriate canned responses matching the litelib JSON shapes (see
|
|
// tests/fixtures/lite/result_parsers.json), so the gateway/sync refresh path parses.
|
|
if (command) {
|
|
const char* c = command;
|
|
if (std::strcmp(c, "sync") == 0) {
|
|
// Simulate the real backend's blocking full sync when requested, so tests can
|
|
// verify shutdown doesn't hang on an in-flight sync.
|
|
while (g_liteFakeSyncBlock.load()) std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
return liteFakeDup("{\"result\":\"success\"}");
|
|
}
|
|
if (std::strcmp(c, "syncstatus") == 0) // real backend shape: "syncing" is a string
|
|
return liteFakeDup("{\"syncing\":\"true\",\"synced_blocks\":1000,\"total_blocks\":1000}");
|
|
if (std::strcmp(c, "balance") == 0 && g_liteFakeBadBalance.load())
|
|
return liteFakeDup("not-json"); // simulate a shape/parse failure for one command
|
|
if (std::strcmp(c, "balance") == 0)
|
|
return liteFakeDup("{\"tbalance\":100000000,\"zbalance\":200000000,\"unconfirmed\":50000000,"
|
|
"\"verified_zbalance\":180000000,\"spendable_zbalance\":170000000}");
|
|
if (std::strcmp(c, "addresses") == 0)
|
|
return liteFakeDup("{\"z_addresses\":[\"zs1fakeaddr\"],\"t_addresses\":[\"R1fakeaddr\"]}");
|
|
if (std::strcmp(c, "notes") == 0)
|
|
return liteFakeDup("{\"unspent_notes\":[],\"utxos\":[],\"pending_notes\":[],\"pending_utxos\":[]}");
|
|
if (std::strcmp(c, "list") == 0)
|
|
return liteFakeDup("[{\"txid\":\"faketx\",\"datetime\":1700000000,\"block_height\":990,"
|
|
"\"unconfirmed\":false,\"address\":\"zs1fakeaddr\",\"amount\":150000000,\"memo\":\"\"}]");
|
|
if (std::strcmp(c, "height") == 0) return liteFakeDup("{\"height\":1000}");
|
|
if (std::strcmp(c, "info") == 0)
|
|
return liteFakeDup("{\"chain_name\":\"main\",\"version\":\"sdxl-fake\",\"latest_block_height\":1000}");
|
|
// M4 spend/backup commands. send/shield report failure via {"error":..} (NOT an
|
|
// "Error:" prefix), matching the real backend's object!{ "error" => e } shape.
|
|
if (std::strcmp(c, "send") == 0) {
|
|
if (g_liteFakeSendFails.load()) return liteFakeDup("{\n \"error\": \"insufficient funds\"\n}");
|
|
return liteFakeDup("{\n \"txid\": \"faketxid123\"\n}");
|
|
}
|
|
if (std::strcmp(c, "shield") == 0) {
|
|
if (g_liteFakeSendFails.load()) return liteFakeDup("{\n \"error\": \"nothing to shield\"\n}");
|
|
return liteFakeDup("{\n \"txid\": \"fakeshieldtxid\"\n}");
|
|
}
|
|
if (std::strcmp(c, "export") == 0)
|
|
return liteFakeDup("[{\"address\":\"zs1fakeaddr\",\"private_key\":\"SECRET-ZKEY\","
|
|
"\"viewing_key\":\"zxviewsFAKE\"}]");
|
|
if (std::strcmp(c, "seed") == 0)
|
|
return liteFakeDup("{\"seed\":\"fake seed phrase words\",\"birthday\":0}");
|
|
// import (shielded key) / timport (transparent WIF): do_import_* pretty-print JSON.
|
|
if (std::strcmp(c, "import") == 0 || std::strcmp(c, "timport") == 0)
|
|
return liteFakeDup("{\"result\":\"success\",\"address\":\"zs1imported\"}");
|
|
if (std::strcmp(c, "save") == 0) {
|
|
++g_liteFakeSaveCount;
|
|
return liteFakeDup("{\"result\":\"success\"}");
|
|
}
|
|
// Encryption state machine. encrypt -> encrypted AND locked (matches the real backend,
|
|
// which removes keys from memory right after encrypting — verified via lite_smoke --encrypt);
|
|
// unlock/lock toggle locked; decrypt clears it; encryptionstatus reports {encrypted,locked}.
|
|
if (std::strcmp(c, "encrypt") == 0) {
|
|
g_liteFakeEncrypted = true; g_liteFakeLocked = true;
|
|
return liteFakeDup("{\"result\":\"success\"}");
|
|
}
|
|
if (std::strcmp(c, "unlock") == 0) {
|
|
g_liteFakeLocked = false;
|
|
return liteFakeDup("{\"result\":\"success\"}");
|
|
}
|
|
if (std::strcmp(c, "lock") == 0) {
|
|
g_liteFakeLocked = true;
|
|
return liteFakeDup("{\"result\":\"success\"}");
|
|
}
|
|
if (std::strcmp(c, "decrypt") == 0) {
|
|
g_liteFakeEncrypted = false; g_liteFakeLocked = false;
|
|
return liteFakeDup("{\"result\":\"success\"}");
|
|
}
|
|
if (std::strcmp(c, "encryptionstatus") == 0) {
|
|
return liteFakeDup(g_liteFakeEncrypted.load()
|
|
? (g_liteFakeLocked.load() ? "{\"encrypted\":true,\"locked\":true}"
|
|
: "{\"encrypted\":true,\"locked\":false}")
|
|
: "{\"encrypted\":false,\"locked\":false}");
|
|
}
|
|
}
|
|
// Default for any other/unknown command.
|
|
return liteFakeDup("{\"version\":\"sdxl-fake\"}");
|
|
}
|
|
inline void liteFakeFree(char* v)
|
|
{
|
|
if (v) {
|
|
++g_liteFakeFreed;
|
|
std::free(v);
|
|
}
|
|
}
|
|
inline bool liteFakeServerOnline(const char*) { return g_liteFakeServerOnline; }
|
|
inline void liteFakeShutdown() { g_liteFakeShutdownCalled = true; }
|
|
|
|
inline dragonx::wallet::LiteClientBridgeApi makeFakeLiteApi()
|
|
{
|
|
dragonx::wallet::LiteClientBridgeApi api;
|
|
api.walletExists = &liteFakeWalletExists;
|
|
api.initializeNew = &liteFakeInitNew;
|
|
api.initializeNewFromPhrase = &liteFakeInitFromPhrase;
|
|
api.initializeExisting = &liteFakeInitExisting;
|
|
api.execute = &liteFakeExecute;
|
|
api.freeString = &liteFakeFree;
|
|
api.checkServerOnline = &liteFakeServerOnline;
|
|
api.shutdown = &liteFakeShutdown;
|
|
return api;
|
|
}
|
|
|
|
} // namespace test
|
|
} // namespace dragonx
|