feat(lite): M4 — send/shield/import/export/seed via controller + bridge

Add the spend & backup surface to LiteWalletController, with the real SDXL
backend contracts verified against the Rust source:

- send / shield: ASYNC (detached broadcast thread + takeBroadcastResult() slot,
  mirroring the sync thread's shared-lifetime pattern, since sapling proving can
  take seconds), plus synchronous *Blocking cores for tests. send uses the
  JSON-array form ([{address,amount,memo}]) because litelib_execute passes the
  whole args string as ONE argument (no whitespace split) — the space-separated
  CLI form would never parse. send/shield report failure via {"error":..} in the
  body (NOT an "Error:" prefix), so the result is derived from the parsed JSON.
- importKey: auto-detects transparent WIF (U/5/K/L -> timport) vs shielded key
  (-> import); takes the key by value and securely wipes it before returning.
- exportPrivateKeys / exportSeed: synchronous local reads returning SECRET
  material (flagged: no logging; caller wipes after the user saves the backup).
- broadcast thread is detached in the dtor (captures shared bridge + flag + slot,
  never `this`), so it is safe to outlive the controller.

Tests: testLiteWalletControllerM4 drives send (success / no-recipients /
{"error":..} / async-slot delivery / pre-open rejection), shield, export, seed,
and import (shielded + WIF + pre-open). Fake backend returns the real command
shapes + a g_liteFakeSendFails error toggle.

GUI wiring (send_tab button, backup/import UI) is deferred like the M3 UI hop
(GUI-unverifiable here). Plan doc updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 12:06:19 -05:00
parent 4b9d6f7db5
commit 6a4e98b7ed
5 changed files with 480 additions and 0 deletions

View File

@@ -34,6 +34,7 @@ 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 void resetLiteFakeCounters()
{
@@ -102,6 +103,24 @@ inline char* liteFakeExecute(const char* command, const char* args)
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\"}");
}
// Default for any other/unknown command.
return liteFakeDup("{\"version\":\"sdxl-fake\"}");