test(lite): smoke-check M4/M5 command shapes against the real backend

Add `lite_smoke --keys`: create a fresh wallet and exercise the M4/M5
spend/backup commands (new-address, export, seed, save) against the real linked
SDXL backend, verifying each response's JSON shape with nlohmann. SECRET-SAFE:
seed and private-key VALUES are never printed — only field presence/shape and
counts (no send/shield, which would broadcast).

Verified live (isolated HOME, throwaway wallet shredded after):
  new z      shape_ok=1            new t      shape_ok=1
  seed       has_seed=1 has_birthday=1 (REDACTED)
  export     is_array=1 count=4 has_private_key=1 (REDACTED)
  save       result_success=1

Confirms the controller's newAddress / exportSeed / exportPrivateKeys / save
parsing matches real backend output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 14:00:05 -05:00
parent db4778e6a7
commit 0c819afdd4

View File

@@ -7,18 +7,22 @@
// against a real lightwalletd server. The "real backend smoke test" the plan gates behind
// passing fake-backend tests.
//
// lite_smoke [server-url] [--create] [--refresh] [--full] [--restore-recent]
// lite_smoke [server-url] [--create] [--refresh] [--full] [--restore-recent] [--keys]
//
// Read-only by default. --create initializes a new wallet. --refresh runs the refresh
// commands through the parsers (shape check). --full also does a (slow) full sync first.
// --restore-recent restores a throwaway wallet with a birthday near the tip so the sync is
// minimal (seconds), then runs the data-shape checks — fast verification of balance/addresses/
// list shapes without a multi-minute full scan. Always run with an isolated HOME.
// list shapes without a multi-minute full scan. --keys creates a fresh wallet and checks the
// M4/M5 spend/backup command shapes (new-address / export / seed / save) — fast, no sync, and
// SECRET-SAFE (seed/key values are never printed). Always run with an isolated HOME.
#include "wallet/lite_client_bridge.h"
#include "wallet/lite_connection_service.h"
#include "wallet/lite_result_parsers.h"
#include <nlohmann/json.hpp>
#include <cstdio>
#include <string>
@@ -68,18 +72,73 @@ static void runDataShapeChecks(LiteClientBridge& bridge)
}
}
// Exercise the M4/M5 spend/backup command shapes against the real backend: new-address,
// export (private keys), seed, and save. SECRET-SAFE — seed/private-key VALUES are never
// printed; only their presence and JSON shape are reported. (No send/shield here: those would
// attempt a real broadcast.)
static void runLiteKeysShapeChecks(LiteClientBridge& bridge)
{
auto parse = [](const std::string& s) { return nlohmann::json::parse(s, nullptr, false); };
{
auto r = bridge.execute("new", "zs");
auto j = parse(r.value);
const bool shape = j.is_array() && !j.empty() && j[0].is_string() &&
j[0].get<std::string>().rfind("zs", 0) == 0;
std::printf("[lite-smoke] new z bridge_ok=%d shape_ok=%d (addr redacted)\n", r.ok, shape);
if (!shape) std::printf("[lite-smoke] new z RAW = %.80s\n", r.value.c_str());
}
{
auto r = bridge.execute("new", "R");
auto j = parse(r.value);
const bool shape = j.is_array() && !j.empty() && j[0].is_string() &&
j[0].get<std::string>().rfind("R", 0) == 0;
std::printf("[lite-smoke] new t bridge_ok=%d shape_ok=%d (addr redacted)\n", r.ok, shape);
if (!shape) std::printf("[lite-smoke] new t RAW = %.80s\n", r.value.c_str());
}
{
// seed: {"seed": "...", "birthday": N} — verify shape ONLY; never print the seed.
auto r = bridge.execute("seed", "");
auto j = parse(r.value);
const bool hasSeed = j.is_object() && j.contains("seed") && j["seed"].is_string() &&
!j["seed"].get<std::string>().empty();
const bool hasBday = j.is_object() && j.contains("birthday");
std::printf("[lite-smoke] seed bridge_ok=%d has_seed=%d has_birthday=%d (seed REDACTED)\n",
r.ok, hasSeed, hasBday);
if (!hasSeed) std::printf("[lite-smoke] seed shape unexpected (len=%zu)\n", r.value.size());
}
{
// export: [{"address","private_key","viewing_key"}, ...] — shape ONLY; never print keys.
auto r = bridge.execute("export", "");
auto j = parse(r.value);
const bool isArr = j.is_array();
const bool hasPk = isArr && !j.empty() && j[0].is_object() && j[0].contains("private_key");
std::printf("[lite-smoke] export bridge_ok=%d is_array=%d count=%zu has_private_key=%d (keys REDACTED)\n",
r.ok, isArr, isArr ? j.size() : 0, hasPk);
if (!isArr) std::printf("[lite-smoke] export shape unexpected (len=%zu)\n", r.value.size());
}
{
auto r = bridge.execute("save", "");
auto j = parse(r.value);
const bool ok = j.is_object() && j.value("result", std::string()) == "success";
std::printf("[lite-smoke] save bridge_ok=%d result_success=%d\n", r.ok, ok);
if (!ok) std::printf("[lite-smoke] save RAW = %.80s\n", r.value.c_str());
}
}
int main(int argc, char** argv)
{
std::setvbuf(stdout, nullptr, _IONBF, 0); // unbuffered so output survives a timeout kill
std::string server = "https://lite.dragonx.is";
bool doCreate = false, doRefresh = false, doFull = false, doRestoreRecent = false;
bool doCreate = false, doRefresh = false, doFull = false, doRestoreRecent = false, doKeys = false;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "--create") doCreate = true;
else if (arg == "--refresh") doRefresh = true;
else if (arg == "--full") doFull = true;
else if (arg == "--restore-recent") doRestoreRecent = true;
else if (arg == "--keys") doKeys = true;
else server = arg;
}
@@ -124,6 +183,24 @@ int main(int argc, char** argv)
return 0;
}
if (doKeys) {
// Fast M4/M5 path check: create a fresh wallet (no sync needed for local key ops) and
// exercise new-address / export / seed / save shapes. Run with an isolated HOME.
std::printf("[lite-smoke] initializeNew() for M4/M5 keys check...\n");
auto cr = bridge.initializeNew(false, server);
std::printf("[lite-smoke] initializeNew ok=%d\n", cr.ok);
if (!cr.ok) {
std::printf("[lite-smoke] error = %s\n", cr.error.c_str());
bridge.shutdown();
return 1;
}
std::printf("[lite-smoke] --- M4/M5 shape check (new/export/seed/save) ---\n");
runLiteKeysShapeChecks(bridge);
bridge.shutdown();
std::printf("[lite-smoke] done\n");
return 0;
}
if (doCreate) {
std::printf("[lite-smoke] initializeNew() ... (real network + writes wallet state)\n");
auto result = bridge.initializeNew(false, server);