Re-audited this session's lite-wallet changes (originally written at medium
effort) and fixed the genuine issues found:
- walletReady (open path): litelib_initialize_existing returns the bare string
"OK", which is NOT valid JSON, so the previous `json::accept(value)` check
marked a *successful* open as not-ready. Key off a non-empty success response
instead (the bridge already maps "Error:"/null to failure). Drops the now
unused nlohmann include.
- sync progress: while the detached sync thread is still running, syncDone_ is
authoritative — don't surface the backend's transient idle syncstatus
({"syncing":"false"} -> parser progress=1.0/complete=true) as a misleading
100%/done. Force complete=false and zero the bogus 1.0 in the progress model.
- per-address balance: also exclude `pending` outputs (notes/utxos from an
unconfirmed received tx) so per-address figures match confirmed/available.
- secret wiping: the settings page left the page-local request copies
(input.request.*Request.{passphrase,seedPhrase}) unwiped, and the
validation-only fallback path wiped nothing. Replace the single-path memzero
with an RAII scrubber that wipes both the UI char buffers and the request
string copies on every return path.
- concurrency: document that concurrent bridge->execute() is intentionally
unguarded — litelib serializes wallet access internally via
Arc<RwLock<LightWallet>>, so a C++ mutex is unnecessary and would defeat the
sync/syncstatus concurrency the design relies on. syncLaunched_ -> atomic.
Tests: fake backend now returns the real init shapes (seed object for
create/restore, bare "OK" for open) and a new open-path case guards the
walletReady regression. Removed an unreliable alloc==freed leak assert from the
thread-bearing controller test (kept in the thread-free bridge test). Also fixed
a stray CMake indent and removed ~220MB of untracked build/debug scratch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
135 lines
5.9 KiB
C++
135 lines
5.9 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 void resetLiteFakeCounters()
|
|
{
|
|
g_liteFakeAlloc = 0;
|
|
g_liteFakeFreed = 0;
|
|
g_liteFakeShutdownCalled = false;
|
|
}
|
|
|
|
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}");
|
|
}
|
|
inline char* liteFakeInitExisting(bool, const char*) { 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}");
|
|
}
|
|
// 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
|