// 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 #include #include #include #include namespace dragonx { namespace test { // Owned-string accounting (atomic: a detached sync thread may touch these concurrently). inline std::atomic g_liteFakeAlloc{0}; // owned strings handed to the bridge inline std::atomic 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 g_liteFakeSyncBlock{false}; // when true, the "sync" command blocks inline std::atomic g_liteFakeBadBalance{false}; // when true, "balance" returns invalid JSON inline std::atomic g_liteFakeSendFails{false}; // when true, "send"/"shield" return {"error":..} inline std::atomic g_liteFakeSaveCount{0}; // counts "save" commands (persistence checks) inline std::atomic g_liteFakeEncrypted{false}; // wallet-encryption state machine (encrypt/...) inline std::atomic 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(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