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:
@@ -27,12 +27,14 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
struct WalletState; // data/wallet_state.h
|
||||
@@ -59,6 +61,51 @@ struct LiteNewAddressResult {
|
||||
std::string error;
|
||||
};
|
||||
|
||||
// A single send recipient. amountZatoshis is in zatoshis (1e-8 DRGX); memo is ignored by the
|
||||
// backend for transparent destination addresses.
|
||||
struct LiteSendRecipient {
|
||||
std::string address;
|
||||
std::uint64_t amountZatoshis = 0;
|
||||
std::string memo;
|
||||
};
|
||||
|
||||
struct LiteSendRequest {
|
||||
std::vector<LiteSendRecipient> recipients;
|
||||
};
|
||||
|
||||
// send and shield both broadcast a transaction and yield its txid (or an error). NOTE: the
|
||||
// backend reports failure via {"error":..} in the response body, NOT the bridge's "Error:"
|
||||
// prefix, so the result is derived by inspecting the parsed JSON.
|
||||
struct LiteBroadcastResult {
|
||||
bool ok = false;
|
||||
std::string txid;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
// importKey outcome. `detail` is the backend's (non-secret) confirmation payload.
|
||||
struct LiteImportResult {
|
||||
bool ok = false;
|
||||
std::string detail;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
// SECRET RESULT: holds exported per-address private keys ({address, private_key, viewing_key}).
|
||||
// Treat privateKeysJson as sensitive: never log it; wipe it after the user has saved the backup.
|
||||
struct LiteExportResult {
|
||||
bool ok = false;
|
||||
std::string privateKeysJson;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
// SECRET RESULT: holds the wallet seed phrase. Treat seedPhrase as sensitive: never log it;
|
||||
// wipe it after the user has saved the backup.
|
||||
struct LiteSeedResult {
|
||||
bool ok = false;
|
||||
std::string seedPhrase;
|
||||
std::uint64_t birthday = 0;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
class LiteWalletController {
|
||||
public:
|
||||
LiteWalletController(WalletCapabilities capabilities,
|
||||
@@ -102,6 +149,30 @@ public:
|
||||
// key derivation), safe to call on the UI thread; the next refresh lists the new address.
|
||||
LiteNewAddressResult newAddress(bool shielded);
|
||||
|
||||
// --- M4: spend & backup ---------------------------------------------------------------
|
||||
// Build & broadcast a transaction (send) or shield all transparent funds. Proving can take
|
||||
// seconds, so the public entry points are ASYNCHRONOUS: they run the blocking core on a
|
||||
// detached thread and deliver the result to a main-thread slot drained by
|
||||
// takeBroadcastResult(). Each returns false if a broadcast is already in flight or no wallet
|
||||
// is open. Only one broadcast runs at a time. After a successful broadcast the periodic
|
||||
// refresh surfaces the new (unconfirmed) transaction and tracks its confirmations.
|
||||
bool sendTransaction(const LiteSendRequest& request);
|
||||
bool shieldFunds(const std::string& optionalAddress = {});
|
||||
bool broadcastInProgress() const { return broadcastRunning_ && broadcastRunning_->load(); }
|
||||
bool takeBroadcastResult(LiteBroadcastResult& out);
|
||||
|
||||
// Synchronous cores for send/shield (block on the backend; safe off the UI thread). Used by
|
||||
// the async entry points above and directly by tests.
|
||||
LiteBroadcastResult sendTransactionBlocking(const LiteSendRequest& request);
|
||||
LiteBroadcastResult shieldFundsBlocking(const std::string& optionalAddress = {});
|
||||
|
||||
// Fast, local secret operations (synchronous). importKey takes the key BY VALUE and securely
|
||||
// wipes it before returning (transparent WIF vs shielded key is auto-detected). exportPrivate
|
||||
// Keys / exportSeed return SECRET material the caller must treat as sensitive and wipe.
|
||||
LiteImportResult importKey(std::string spendingOrViewingKey);
|
||||
LiteExportResult exportPrivateKeys(const std::string& optionalAddress = {});
|
||||
LiteSeedResult exportSeed();
|
||||
|
||||
// Poll sync status + fetch balance/addresses/transactions, and apply the result into the
|
||||
// app's WalletState. Returns true if state was updated. Safe no-op when no wallet is open.
|
||||
// Synchronous (blocks on the backend); used by tests and as the worker's unit of work.
|
||||
@@ -121,6 +192,9 @@ private:
|
||||
void startWorker();
|
||||
void stopWorker();
|
||||
void workerLoop();
|
||||
// Launch `op` (a self-contained broadcast that must NOT capture `this`) on the detached
|
||||
// broadcast thread. Returns false if a broadcast is already running.
|
||||
bool startBroadcast(std::function<LiteBroadcastResult()> op);
|
||||
|
||||
// The bridge is shared (not just owned) so the detached, uninterruptible sync thread can
|
||||
// safely outlive the controller: it holds a ref, so the underlying bridge is destroyed
|
||||
@@ -147,6 +221,16 @@ private:
|
||||
std::atomic<bool> syncLaunched_{false}; // startSync() guard (set on the main thread)
|
||||
std::shared_ptr<std::atomic<bool>> syncDone_ = std::make_shared<std::atomic<bool>>(false);
|
||||
|
||||
// Detached background broadcast (send/shield): proving can take seconds. Mirrors the sync
|
||||
// thread's shared-lifetime pattern (shared running flag + result slot captured by the
|
||||
// thread, never `this`) so the thread can safely outlive the controller.
|
||||
std::thread broadcastThread_;
|
||||
std::shared_ptr<std::atomic<bool>> broadcastRunning_ =
|
||||
std::make_shared<std::atomic<bool>>(false);
|
||||
std::shared_ptr<std::mutex> broadcastResultMutex_ = std::make_shared<std::mutex>();
|
||||
std::shared_ptr<std::optional<LiteBroadcastResult>> broadcastResult_ =
|
||||
std::make_shared<std::optional<LiteBroadcastResult>>();
|
||||
|
||||
// Joinable background refresh worker (fast iterations: syncstatus, plus data once synced).
|
||||
std::thread worker_;
|
||||
std::atomic<bool> running_{false};
|
||||
|
||||
Reference in New Issue
Block a user