Files
ObsidianDragon/tools/lite_smoke.cpp
DanS 950d7ace50 feat(lite): startup unlock prompt + real-backend encryption verification
Startup lock screen (soft): once the first refresh reveals the auto-opened wallet
is encrypted+locked, show the unlock modal on launch (reusing renderLiteUnlockPrompt,
one-shot per session). Soft by design — balances stay viewable via viewing keys
while locked, so the user may dismiss and browse read-only; only spending needs
the passphrase.

Real-backend verification: add `lite_smoke --encrypt` (create -> encryptionstatus
-> encrypt -> lock -> unlock, checking flags; passphrase never printed). Running it
against the real SDXL backend showed encrypt LOCKS immediately
(after encrypt: encrypted=1, locked=1) — the backend removes spending keys right
after encrypting. The controller already relays encryptionstatus faithfully (UI is
state-driven, so unaffected), but the fake modeled encrypt->unlocked; corrected the
fake (encrypt -> encrypted+locked) and the test sequence (encrypt -> unlock -> lock
-> decrypt) to match real behavior.

Builds clean, tests pass, hygiene clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 18:53:35 -05:00

279 lines
14 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Real-backend smoke harness for the lite wallet bridge. Links the actual SDXL litelib_*
// backend (via the same imported CMake target the app uses) and exercises LiteClientBridge
// 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] [--keys] [--encrypt]
//
// 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. --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>
using namespace dragonx::wallet;
// A standard valid 24-word BIP39 test mnemonic (no funds). Restored only into an isolated HOME.
static const char* kTestSeed =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon "
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon "
"abandon art";
// Push each refresh command's real output through its parser; print flags/counts, and the raw
// (truncated) response only on a parse failure to diagnose real-shape mismatches.
static void runDataShapeChecks(LiteClientBridge& bridge)
{
// Non-blocking commands first (read local wallet state); balance last (may need a sync).
{
auto r = bridge.execute("syncstatus", "");
auto p = parseLiteSyncStatusResponse(r.value);
std::printf("[lite-smoke] syncstatus parse_ok=%d complete=%d synced=%llu/%llu\n",
p.ok, p.syncStatus.complete,
(unsigned long long)p.syncStatus.syncedBlocks, (unsigned long long)p.syncStatus.totalBlocks);
if (!p.ok) std::printf("[lite-smoke] syncstatus RAW = %.150s\n", r.value.c_str());
}
{
auto r = bridge.execute("addresses", "");
auto p = parseLiteAddressesResponse(r.value);
std::printf("[lite-smoke] addresses bridge_ok=%d parse_ok=%d err=%s z=%zu t=%zu\n",
r.ok, p.ok, liteResultParserErrorName(p.error),
p.addresses.zAddresses.size(), p.addresses.tAddresses.size());
if (!p.ok) std::printf("[lite-smoke] addresses RAW = %.150s\n", r.value.c_str());
}
{
auto r = bridge.execute("list", "");
auto p = parseLiteTransactionsResponse(r.value);
std::printf("[lite-smoke] list bridge_ok=%d parse_ok=%d err=%s count=%zu\n",
r.ok, p.ok, liteResultParserErrorName(p.error), p.transactions.transactions.size());
if (!p.ok) std::printf("[lite-smoke] list RAW = %.150s\n", r.value.c_str());
}
std::printf("[lite-smoke] balance (may block until synced)...\n");
{
auto r = bridge.execute("balance", "");
auto p = parseLiteBalanceResponse(r.value);
std::printf("[lite-smoke] balance bridge_ok=%d parse_ok=%d err=%s\n",
r.ok, p.ok, liteResultParserErrorName(p.error));
if (!p.ok) std::printf("[lite-smoke] balance RAW = %.150s\n", r.value.c_str());
}
}
// 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());
}
}
// Exercise the encryption commands (encrypt/lock/unlock + encryptionstatus) against the real
// backend. SECRET-SAFE: the throwaway passphrase is never printed; only encrypted/locked flags.
static void runLiteEncryptionChecks(LiteClientBridge& bridge)
{
const char* kPass = "smoke-encryption-passphrase"; // throwaway; isolated HOME only
auto status = [&](const char* label) {
auto r = bridge.execute("encryptionstatus", "");
auto j = nlohmann::json::parse(r.value, nullptr, false);
const bool enc = j.is_object() && j.value("encrypted", false);
const bool lck = j.is_object() && j.value("locked", false);
std::printf("[lite-smoke] encstatus %-13s bridge_ok=%d encrypted=%d locked=%d\n",
label, r.ok, enc, lck);
};
status("(initial)");
std::printf("[lite-smoke] encrypt bridge_ok=%d\n", bridge.execute("encrypt", kPass).ok);
status("(after encrypt)");
std::printf("[lite-smoke] lock bridge_ok=%d\n", bridge.execute("lock", "").ok);
status("(after lock)");
std::printf("[lite-smoke] unlock bridge_ok=%d\n", bridge.execute("unlock", kPass).ok);
status("(after unlock)");
bridge.execute("save", "");
}
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, doKeys = false,
doEncrypt = 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 if (arg == "--encrypt") doEncrypt = true;
else server = arg;
}
std::printf("[lite-smoke] server = %s\n", server.c_str());
auto bridge = LiteClientBridge::linkedSdxl();
std::printf("[lite-smoke] available() = %s\n", bridge.available() ? "true" : "false");
if (!bridge.available()) {
std::printf("[lite-smoke] reason = %s\n", bridge.unavailableReason().c_str());
std::printf("[lite-smoke] FAIL: backend not linked\n");
return 2;
}
std::printf("[lite-smoke] walletExists(%s) = %s\n", kDragonXLiteChainName,
bridge.walletExists(kDragonXLiteChainName) ? "true" : "false");
std::printf("[lite-smoke] checkServerOnline() = %s\n", bridge.checkServerOnline(server) ? "true" : "false");
if (doRestoreRecent) {
// Bootstrap a client to learn the chain tip, then restore a throwaway wallet with a
// birthday near the tip so the scan is minimal (seconds), then check data shapes.
std::printf("[lite-smoke] bootstrap initializeNew() to learn tip...\n");
bridge.initializeNew(false, server);
unsigned long long tip = 0;
{
auto r = bridge.execute("info", "");
auto p = parseLiteInfoResponse(r.value);
tip = p.info.latestBlockHeight.has_value() ? (unsigned long long)*p.info.latestBlockHeight : 0;
std::printf("[lite-smoke] info parse_ok=%d tip=%llu\n", p.ok, tip);
}
const unsigned long long birthday = tip > 10 ? tip - 10 : tip;
std::printf("[lite-smoke] restore-from-phrase at birthday=%llu (minimal scan)...\n", birthday);
auto rr = bridge.initializeNewFromPhrase(false, server, kTestSeed, birthday, 0, /*overwrite*/ true);
std::printf("[lite-smoke] restore ok=%d\n", rr.ok);
if (!rr.ok) std::printf("[lite-smoke] restore err = %s\n", rr.error.c_str());
// NB: this backend downloads from a fixed checkpoint regardless of birthday, so an
// explicit full "sync" is slow. addresses/list/syncstatus read local state and don't
// need it; balance may block until synced.
std::printf("[lite-smoke] --- data shape check (no full sync) ---\n");
runDataShapeChecks(bridge);
bridge.shutdown();
std::printf("[lite-smoke] done\n");
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 (doEncrypt) {
// Encryption command check against the real backend (fast, no sync). Isolated HOME.
std::printf("[lite-smoke] initializeNew() for encryption 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] --- encryption check (encrypt/lock/unlock + status) ---\n");
runLiteEncryptionChecks(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);
std::printf("[lite-smoke] initializeNew ok = %s\n", result.ok ? "true" : "false");
if (result.ok) {
std::printf("[lite-smoke] wallet created; response len = %zu (seed redacted)\n", result.value.size());
} else {
std::printf("[lite-smoke] error = %s\n", result.error.c_str());
}
}
if (doRefresh) {
std::printf("[lite-smoke] --- refresh shape check (real backend output -> parsers) ---\n");
{
auto r = bridge.execute("info", "");
auto p = parseLiteInfoResponse(r.value);
std::printf("[lite-smoke] info bridge_ok=%d parse_ok=%d err=%s\n",
r.ok, p.ok, liteResultParserErrorName(p.error));
}
if (!doFull) {
bridge.shutdown();
std::printf("[lite-smoke] (skipping balance/addresses/list; pass --full or --restore-recent)\n");
return 0;
}
std::printf("[lite-smoke] running full sync (blocking; can take many minutes)...\n");
bridge.execute("sync", "");
runDataShapeChecks(bridge);
}
bridge.shutdown();
std::printf("[lite-smoke] done\n");
return 0;
}