feat(lite): M3 — new-address generation + sync-indicator confirmation
- LiteWalletController::newAddress(shielded) runs the backend "new" command ("zs"/"R" ->
do_new_address), parses the ["addr"] response, and returns the new address; the next
refresh lists it. Fast (local derivation), safe on the UI thread.
- fake_lite_backend returns ["zs1fakenew"]/["R1fakenew"] for "new" by args.
- testLiteWalletControllerNewAddress covers shielded/transparent + no-wallet error.
Also confirmed (no code needed): the sync-progress indicator already works for lite —
balance_tab reads state.sync.* which M2b-3 populates. Per-address balances landed in M2.
Remaining M3 is pure UI wiring (receive_tab button -> newAddress, loading/empty states),
which isn't verifiable without a GUI session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,12 @@ Each milestone is independently demoable and gated by a fake-backend test. Order
|
|||||||
- Sync status/progress indicator; loading/empty states; new shielded/transparent address generation via the gateway (`receive_tab`); capability-gated surfaces verified (`isUiSurfaceAvailable`, `settings_page.cpp:1494` lite/full branch).
|
- Sync status/progress indicator; loading/empty states; new shielded/transparent address generation via the gateway (`receive_tab`); capability-gated surfaces verified (`isUiSurfaceAvailable`, `settings_page.cpp:1494` lite/full branch).
|
||||||
- **Exit demo / test:** Full read-only experience — balances, address book, history, live sync progress — against fake then real backend.
|
- **Exit demo / test:** Full read-only experience — balances, address book, history, live sync progress — against fake then real backend.
|
||||||
|
|
||||||
|
> **Status (2026-06-05): testable logic done; pure-UI wiring remains (GUI-unverifiable here).**
|
||||||
|
> - ✅ **Sync progress indicator already works** — `balance_tab.cpp` reads `state.sync.{syncing,verification_progress,headers,isSynced()}`, which M2b-3 now populates from the lite backend. No code change needed.
|
||||||
|
> - ✅ **Per-address balances** — done in M2 (Receive/Balance show per-address amounts).
|
||||||
|
> - ✅ **New-address generation** — `LiteWalletController::newAddress(shielded)` runs the backend `new` command (`"zs"`/`"R"` → `do_new_address`), parses the `["addr"]` response, returns the address; the next refresh lists it. `testLiteWalletControllerNewAddress` covers it.
|
||||||
|
> - ⏳ **Remaining (pure UI, not verifiable without a GUI session):** wire the `receive_tab` "new address" button to `controller.newAddress()` (+ trigger a refresh); loading/empty states while syncing/unopened; final capability-gating pass. These compile-check only here — defer real verification to a `/run` GUI session.
|
||||||
|
|
||||||
### M4 — Send / import / export / shield
|
### M4 — Send / import / export / shield
|
||||||
**Goal:** A user can spend and back up.
|
**Goal:** A user can spend and back up.
|
||||||
- Wire `send_tab` (`:780-787` `sendTransaction`) to `litelib_execute` (`send`/`z_sendmany`) via the gateway, with fee + confirmation UI, result parsing, and tx-status polling that updates `WalletState`.
|
- Wire `send_tab` (`:780-787` `sendTransaction`) to `litelib_execute` (`send`/`z_sendmany`) via the gateway, with fee + confirmation UI, result parsing, and tx-status polling that updates `WalletState`.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
#include <sodium.h>
|
#include <sodium.h>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
@@ -202,6 +203,35 @@ std::optional<LiteWalletAppRefreshModel> LiteWalletController::refreshModel()
|
|||||||
return mapped.model;
|
return mapped.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LiteNewAddressResult LiteWalletController::newAddress(bool shielded)
|
||||||
|
{
|
||||||
|
LiteNewAddressResult out;
|
||||||
|
if (!walletOpen_.load() || !bridge_) {
|
||||||
|
out.error = "no wallet is open";
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
// Backend address-type tokens: "zs" (shielded) / "R" (transparent) (do_new_address).
|
||||||
|
const auto result = bridge_->execute("new", shielded ? "zs" : "R");
|
||||||
|
if (!result.ok) {
|
||||||
|
out.error = result.error.empty() ? "new address generation failed" : result.error;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
// Response is a JSON array with the new address, e.g. ["zs1..."].
|
||||||
|
try {
|
||||||
|
const auto parsed = nlohmann::json::parse(result.value);
|
||||||
|
if (parsed.is_array() && !parsed.empty() && parsed[0].is_string()) {
|
||||||
|
out.address = parsed[0].get<std::string>();
|
||||||
|
} else if (parsed.is_string()) {
|
||||||
|
out.address = parsed.get<std::string>();
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
// fall through to the error below
|
||||||
|
}
|
||||||
|
out.ok = !out.address.empty();
|
||||||
|
if (!out.ok) out.error = "could not parse new address response";
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
bool LiteWalletController::refreshWalletState(dragonx::WalletState& state)
|
bool LiteWalletController::refreshWalletState(dragonx::WalletState& state)
|
||||||
{
|
{
|
||||||
auto model = refreshModel();
|
auto model = refreshModel();
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ struct LiteWalletControllerOptions {
|
|||||||
bool allowBridgeCalls = true;
|
bool allowBridgeCalls = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct LiteNewAddressResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::string address;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
class LiteWalletController {
|
class LiteWalletController {
|
||||||
public:
|
public:
|
||||||
LiteWalletController(WalletCapabilities capabilities,
|
LiteWalletController(WalletCapabilities capabilities,
|
||||||
@@ -92,6 +98,10 @@ public:
|
|||||||
// op produces a ready wallet; safe to call once.
|
// op produces a ready wallet; safe to call once.
|
||||||
void startSync();
|
void startSync();
|
||||||
|
|
||||||
|
// Generate a new address (shielded if true, else transparent) via the backend. Fast (local
|
||||||
|
// key derivation), safe to call on the UI thread; the next refresh lists the new address.
|
||||||
|
LiteNewAddressResult newAddress(bool shielded);
|
||||||
|
|
||||||
// Poll sync status + fetch balance/addresses/transactions, and apply the result into the
|
// 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.
|
// 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.
|
// Synchronous (blocks on the backend); used by tests and as the worker's unit of work.
|
||||||
|
|||||||
@@ -58,8 +58,12 @@ inline char* liteFakeInitFromPhrase(bool, const char*, const char*,
|
|||||||
return liteFakeDup("{\"result\":\"restored\"}");
|
return liteFakeDup("{\"result\":\"restored\"}");
|
||||||
}
|
}
|
||||||
inline char* liteFakeInitExisting(bool, const char*) { return liteFakeDup("{\"result\":\"opened\"}"); }
|
inline char* liteFakeInitExisting(bool, const char*) { return liteFakeDup("{\"result\":\"opened\"}"); }
|
||||||
inline char* liteFakeExecute(const char* command, const char*)
|
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
|
// A command named "boom" yields an "Error:"-prefixed response (the bridge's
|
||||||
// looksLikeError contract maps that to ok=false).
|
// looksLikeError contract maps that to ok=false).
|
||||||
if (command && std::strcmp(command, "boom") == 0) {
|
if (command && std::strcmp(command, "boom") == 0) {
|
||||||
|
|||||||
@@ -4718,6 +4718,30 @@ void testLitePerAddressBalances()
|
|||||||
EXPECT_NEAR(state.t_addresses[0].balance, 0.5, 1e-9); // 50000000
|
EXPECT_NEAR(state.t_addresses[0].balance, 0.5, 1e-9); // 50000000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// M3: new-address generation via the controller (backend "new" zs/R) returns the address.
|
||||||
|
void testLiteWalletControllerNewAddress()
|
||||||
|
{
|
||||||
|
using namespace dragonx::wallet;
|
||||||
|
const auto caps = makeWalletCapabilities(WalletBuildKind::Lite, /*embeddedDaemon*/ false, /*liteBackendLinked*/ true);
|
||||||
|
const auto conn = defaultLiteConnectionSettings();
|
||||||
|
|
||||||
|
LiteWalletController controller(caps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||||
|
EXPECT_TRUE(controller.createWallet(LiteWalletCreateRequest{}).walletReady);
|
||||||
|
|
||||||
|
const auto z = controller.newAddress(/*shielded*/ true);
|
||||||
|
EXPECT_TRUE(z.ok);
|
||||||
|
EXPECT_TRUE(z.address.rfind("zs1", 0) == 0);
|
||||||
|
|
||||||
|
const auto t = controller.newAddress(/*shielded*/ false);
|
||||||
|
EXPECT_TRUE(t.ok);
|
||||||
|
EXPECT_TRUE(t.address.rfind("R1", 0) == 0);
|
||||||
|
|
||||||
|
// No wallet open -> error, no address.
|
||||||
|
LiteWalletController idle(caps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||||
|
const auto none = idle.newAddress(true);
|
||||||
|
EXPECT_FALSE(none.ok);
|
||||||
|
}
|
||||||
|
|
||||||
// Gateway hardening: one command's parse failure must not abort the whole refresh — the
|
// Gateway hardening: one command's parse failure must not abort the whole refresh — the
|
||||||
// other commands still populate the bundle (graceful degradation against real-shape drift).
|
// other commands still populate the bundle (graceful degradation against real-shape drift).
|
||||||
void testLiteWalletGatewayRefreshSkipsFailedCommand()
|
void testLiteWalletGatewayRefreshSkipsFailedCommand()
|
||||||
@@ -4841,6 +4865,7 @@ int main()
|
|||||||
testLiteChainNameMigration();
|
testLiteChainNameMigration();
|
||||||
testLiteRefreshModelAppliesToWalletState();
|
testLiteRefreshModelAppliesToWalletState();
|
||||||
testLitePerAddressBalances();
|
testLitePerAddressBalances();
|
||||||
|
testLiteWalletControllerNewAddress();
|
||||||
testLiteSyncStatusParserRealShapes();
|
testLiteSyncStatusParserRealShapes();
|
||||||
testLiteWalletControllerRefreshPopulatesState();
|
testLiteWalletControllerRefreshPopulatesState();
|
||||||
testLiteWalletGatewayRefreshSkipsFailedCommand();
|
testLiteWalletGatewayRefreshSkipsFailedCommand();
|
||||||
|
|||||||
Reference in New Issue
Block a user