feat(lite): M2b-1/2 — shared-bridge refactor + sync/refresh into WalletState
Shared-bridge refactor (litelib is a global singleton; every LiteClientBridge calls litelib_shutdown() on destruction, so services must not each own one): - LiteWalletLifecycleService, LiteWalletGateway, LiteSyncService now take a non-owning LiteClientBridge*; LiteWalletController owns the single bridge and passes &bridge_. Sync + controller refresh: - LiteSyncService::startSync executes the real "sync" command (was a stub). - LiteWalletController: startSync() (auto-fires when a wallet becomes ready) and refreshWalletState(WalletState&) — polls syncstatus, runs gateway.refresh(), maps the bundle, applies balances/addresses/transactions/sync into WalletState. Tests: - fake_lite_backend.h returns command-shaped JSON (per tests/fixtures/lite/result_parsers.json). - testLiteWalletControllerRefreshPopulatesState drives the full path against the fake. - Surfaced + worked around a real integration issue: parseLiteInfoResponse requires latest_block_height and the gateway aborts the whole refresh on the first command's parse failure (fragile vs partial backend responses; hardening tracked for M2b-3). Verified: ctest green; lite+backend, full-node, lite-no-backend apps + lite_smoke build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -93,7 +93,9 @@ Each milestone is independently demoable and gated by a fake-backend test. Order
|
||||
> **Status (2026-06-04): data pipeline landed; live wiring (M2b) remains.**
|
||||
> - ✅ **Last hop implemented + tested** — `applyLiteRefreshModelToWalletState(model, WalletState&)` in `lite_wallet_controller.{h,cpp}`: zatoshi→DRGX balances, z/t address split, transaction typing + confirmations (`chainHeight - blockHeight + 1`), sync progress. Mutates `WalletState` in place (it's non-copyable). `testLiteRefreshModelAppliesToWalletState()` drives a bundle through the existing `mapLiteWalletRefreshBundle` → apply → asserts the populated `WalletState`. `ctest` green.
|
||||
> - ℹ️ The fetch/parse/assemble pipeline already exists and works: `LiteWalletGateway::refresh()` → `LiteWalletRefreshBundle` → `mapLiteWalletRefreshBundle()` → `LiteWalletAppRefreshModel`. M2 just needed the final `→ WalletState` hop (above) plus live wiring.
|
||||
> - ⏳ **M2b (remaining) — live wiring.** Blocked on a design decision surfaced by the M1 smoke run: `litelib` is a **global singleton** and every `LiteClientBridge` calls `litelib_shutdown()` (stops the live client) on destruction, so the controller cannot own a second owning bridge for the gateway/sync. **Decision: refactor the lite services (`LiteWalletLifecycleService`, `LiteWalletGateway`, `LiteConnectionService`, `LiteSyncService`) to take a non-owning bridge (`LiteClientBridge*`/shared handle); the controller owns the one bridge.** Then: implement `LiteSyncService::startSync` (replace the "not implemented" stub) + a background worker polling `syncstatus` and running `gateway.refresh()` (mirror `NetworkRefreshService`/`RefreshScheduler`: enqueue → worker → apply on main thread), apply into `App` `WalletState`, and hook into `App::update()`. Note: `litelib_execute` is already panic-safe (`catch_unwind`), so the polling workhorse won't abort the app. Per-address balances need notes-correlation (currently aggregate-only).
|
||||
> - ✅ **M2b-1 — shared-bridge refactor (done).** `litelib` is a global singleton and every `LiteClientBridge` calls `litelib_shutdown()` on destruction, so services must not each own one. `LiteWalletLifecycleService`, `LiteWalletGateway`, and `LiteSyncService` now take a **non-owning `LiteClientBridge*`**; `LiteWalletController` owns the single bridge and passes `&bridge_`. Builds clean in all configs; existing tests stay green.
|
||||
> - ✅ **M2b-2 — sync + controller refresh (done + tested).** `LiteSyncService::startSync` now executes the `sync` command (was a stub). `LiteWalletController` gained `startSync()` (auto-invoked when a wallet becomes ready) and `refreshWalletState(WalletState&)` which polls `syncstatus`, runs `gateway.refresh()`, maps the bundle, and applies it into `WalletState`. `testLiteWalletControllerRefreshPopulatesState()` drives the full path against the real-shape fake (balances/addresses/transactions/sync populated; no-op when no wallet open). The fake harness now returns command-shaped JSON per `tests/fixtures/lite/result_parsers.json`. (Surfaced a real bug: `info` requires `latest_block_height`, and the gateway aborts the whole refresh on the first command's parse failure — fixed in the fake; worth noting the gateway's abort-on-first-failure is fragile against partial backend responses.)
|
||||
> - ⏳ **M2b-3 — threaded App hook (remaining).** Drive `refreshWalletState` from a controller-owned background worker (mirror `NetworkRefreshService`/`RefreshScheduler`): worker produces a copyable `LiteWalletAppRefreshModel`; `App::update()` applies it into `state_` on the main thread (WalletState is non-copyable, so pass the model, not the state, across threads). `litelib_execute` is `catch_unwind`-safe so the worker won't abort the app. Also: per-address balances (notes-correlation) and a real-backend refresh smoke (the real backend's JSON shapes may differ from the hand-built fixture).
|
||||
- Implement `LiteSyncService::startSync` (replace the "not implemented" stub) + a background worker polling `syncstatus`, mirroring `NetworkRefreshService`/`RefreshScheduler` (enqueue → worker → apply on main thread).
|
||||
- Drive `LiteWalletGateway` refresh (info/height/balance/addresses/notes/list/transactions) through `lite_result_parsers` → `lite_wallet_state_mapper` → `App` `WalletState` (`privateBalance`, `transparentBalance`, `addresses`, `transactions`, `sync`).
|
||||
- Hook the controller into `App::update()`'s refresh dispatch alongside (not inside) the full-node path.
|
||||
|
||||
@@ -166,11 +166,11 @@ LiteSyncRecoveryDecision evaluateLiteSyncRecovery(const LiteSyncRecoveryInput& i
|
||||
|
||||
LiteSyncService::LiteSyncService(WalletCapabilities capabilities,
|
||||
LiteConnectionSettings connectionSettings,
|
||||
LiteClientBridge bridge,
|
||||
LiteClientBridge* bridge,
|
||||
LiteSyncServiceOptions options)
|
||||
: capabilities_(capabilities),
|
||||
connectionSettings_(std::move(connectionSettings)),
|
||||
bridge_(std::move(bridge)),
|
||||
bridge_(bridge),
|
||||
options_(options)
|
||||
{
|
||||
}
|
||||
@@ -179,7 +179,7 @@ LiteSyncAvailability LiteSyncService::availability() const
|
||||
{
|
||||
if (!isLiteBuild(capabilities_)) return LiteSyncAvailability::UnsupportedBuild;
|
||||
if (!supportsLiteBackend(capabilities_)) return LiteSyncAvailability::BackendUnavailable;
|
||||
if (!bridge_.available()) return LiteSyncAvailability::BridgeUnavailable;
|
||||
if (!bridge_ || !bridge_->available()) return LiteSyncAvailability::BridgeUnavailable;
|
||||
if (!selectLiteServer(connectionSettings_).ok) return LiteSyncAvailability::NoUsableServer;
|
||||
if (!options_.allowSyncStatusBridgeCalls) return LiteSyncAvailability::BridgeCallsDisabled;
|
||||
return LiteSyncAvailability::Ready;
|
||||
@@ -199,7 +199,7 @@ LiteSyncPlan LiteSyncService::planStartSync(const LiteSyncStartRequest& request)
|
||||
return makePlan(
|
||||
LiteSyncOperation::StartSync,
|
||||
request.serverUrl,
|
||||
false,
|
||||
options_.allowSyncStatusBridgeCalls,
|
||||
request.mode,
|
||||
request.forceRescan,
|
||||
request.afterRestore);
|
||||
@@ -229,9 +229,21 @@ LiteSyncStartResult LiteSyncService::startSync(const LiteSyncStartRequest& reque
|
||||
return blockedStartResult(plan, status());
|
||||
}
|
||||
|
||||
return blockedStartResult(
|
||||
plan,
|
||||
makeSyncScaffoldStatus(WalletBackendState::Unavailable, "lite sync start execution is not implemented"));
|
||||
LiteSyncStartResult result;
|
||||
result.plan = plan;
|
||||
result.attempted = true;
|
||||
|
||||
const auto bridgeCall = bridge_->execute(plan.command, plan.args);
|
||||
if (!bridgeCall.ok) {
|
||||
result.status = makeSyncScaffoldStatus(WalletBackendState::Error, "lite sync start bridge call failed");
|
||||
result.error = result.status.message;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.ok = true;
|
||||
result.syncStarted = true;
|
||||
result.status = makeSyncScaffoldStatus(WalletBackendState::Syncing, "lite sync started");
|
||||
return result;
|
||||
}
|
||||
|
||||
LiteSyncStatusResult LiteSyncService::pollSyncStatus(const LiteSyncStatusRequest& request)
|
||||
@@ -245,7 +257,7 @@ LiteSyncStatusResult LiteSyncService::pollSyncStatus(const LiteSyncStatusRequest
|
||||
result.plan = plan;
|
||||
result.attempted = true;
|
||||
|
||||
const auto bridgeCall = bridge_.execute(plan.command, plan.args);
|
||||
const auto bridgeCall = bridge_->execute(plan.command, plan.args);
|
||||
result.bridgeResponseRedacted = bridgeCall.ok || !bridgeCall.error.empty() ? "<redacted>" : "<empty>";
|
||||
if (!bridgeCall.ok) {
|
||||
result.status = makeSyncScaffoldStatus(WalletBackendState::Error, "lite syncstatus bridge call failed");
|
||||
@@ -334,7 +346,7 @@ WalletBackendStatus LiteSyncService::statusFor(LiteSyncAvailability availability
|
||||
case LiteSyncAvailability::BridgeUnavailable:
|
||||
return makeSyncScaffoldStatus(
|
||||
WalletBackendState::Unavailable,
|
||||
detail.empty() ? bridge_.unavailableReason() : detail);
|
||||
detail.empty() ? (bridge_ ? bridge_->unavailableReason() : "lite bridge is unavailable") : detail);
|
||||
case LiteSyncAvailability::BridgeCallsDisabled:
|
||||
return makeSyncScaffoldStatus(
|
||||
WalletBackendState::Unavailable,
|
||||
|
||||
@@ -123,7 +123,7 @@ class LiteSyncService {
|
||||
public:
|
||||
LiteSyncService(WalletCapabilities capabilities,
|
||||
LiteConnectionSettings connectionSettings,
|
||||
LiteClientBridge bridge,
|
||||
LiteClientBridge* bridge,
|
||||
LiteSyncServiceOptions options = {});
|
||||
|
||||
const WalletCapabilities& capabilities() const { return capabilities_; }
|
||||
@@ -155,7 +155,7 @@ private:
|
||||
|
||||
WalletCapabilities capabilities_;
|
||||
LiteConnectionSettings connectionSettings_;
|
||||
LiteClientBridge bridge_;
|
||||
LiteClientBridge* bridge_ = nullptr; // non-owning; owned by LiteWalletController
|
||||
LiteSyncServiceOptions options_;
|
||||
};
|
||||
|
||||
|
||||
@@ -94,10 +94,13 @@ LiteWalletController::LiteWalletController(WalletCapabilities capabilities,
|
||||
LiteConnectionSettings connectionSettings,
|
||||
LiteClientBridge bridge,
|
||||
LiteWalletControllerOptions options)
|
||||
: lifecycle_(capabilities,
|
||||
std::move(connectionSettings),
|
||||
std::move(bridge),
|
||||
LiteWalletLifecycleOptions{options.allowBridgeCalls})
|
||||
: bridge_(std::move(bridge)),
|
||||
lifecycle_(capabilities, connectionSettings, &bridge_,
|
||||
LiteWalletLifecycleOptions{options.allowBridgeCalls}),
|
||||
gateway_(capabilities, connectionSettings, &bridge_,
|
||||
LiteWalletGatewayOptions{options.allowBridgeCalls}),
|
||||
sync_(capabilities, connectionSettings, &bridge_,
|
||||
LiteSyncServiceOptions{options.allowBridgeCalls})
|
||||
{
|
||||
status_ = lifecycle_.status();
|
||||
}
|
||||
@@ -119,9 +122,46 @@ void LiteWalletController::onLifecycleResult(const LiteWalletLifecycleResult& re
|
||||
if (result.walletReady) {
|
||||
walletOpen_ = true;
|
||||
if (persist_) persist_();
|
||||
startSync(); // begin background sync now that a wallet is ready
|
||||
}
|
||||
}
|
||||
|
||||
LiteSyncStartResult LiteWalletController::startSync()
|
||||
{
|
||||
auto result = sync_.startSync(LiteSyncStartRequest{});
|
||||
if (result.syncStarted) syncStarted_ = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool LiteWalletController::refreshWalletState(dragonx::WalletState& state)
|
||||
{
|
||||
if (!walletOpen_) return false;
|
||||
|
||||
// Poll sync status first so the refresh bundle (and the mapped sync model) carries it.
|
||||
LiteWalletRefreshRequest request;
|
||||
const auto syncResult = sync_.pollSyncStatus(LiteSyncStatusRequest{});
|
||||
if (syncResult.ok) {
|
||||
request.haveSyncStatus = true;
|
||||
request.syncStatus = syncResult.syncStatus;
|
||||
}
|
||||
|
||||
const auto refreshResult = gateway_.refresh(request);
|
||||
if (refreshResult.bundle.successfulCommandCount == 0 && !request.haveSyncStatus) {
|
||||
status_ = refreshResult.status;
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto mapped = mapLiteWalletRefreshResult(refreshResult);
|
||||
if (!mapped.ok) {
|
||||
status_ = refreshResult.status;
|
||||
return false;
|
||||
}
|
||||
|
||||
applyLiteRefreshModelToWalletState(mapped.model, state);
|
||||
status_ = refreshResult.status;
|
||||
return true;
|
||||
}
|
||||
|
||||
LiteWalletLifecycleResult LiteWalletController::createWallet(LiteWalletCreateRequest request)
|
||||
{
|
||||
auto result = lifecycle_.createWallet(request);
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#include "lite_client_bridge.h"
|
||||
#include "lite_connection_service.h"
|
||||
#include "lite_wallet_lifecycle_service.h"
|
||||
#include "lite_wallet_gateway.h"
|
||||
#include "lite_sync_service.h"
|
||||
#include "lite_wallet_state_mapper.h"
|
||||
#include "wallet_backend.h"
|
||||
#include "wallet_capabilities.h"
|
||||
@@ -72,12 +74,26 @@ public:
|
||||
LiteWalletLifecycleResult openWallet(LiteWalletOpenRequest request);
|
||||
LiteWalletLifecycleResult restoreWallet(LiteWalletRestoreRequest request);
|
||||
|
||||
bool syncStarted() const { return syncStarted_; }
|
||||
|
||||
// Begin background sync on the backend (idempotent enough to call once a wallet is ready;
|
||||
// also invoked automatically when a lifecycle op produces a ready wallet).
|
||||
LiteSyncStartResult startSync();
|
||||
|
||||
// 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.
|
||||
bool refreshWalletState(dragonx::WalletState& state);
|
||||
|
||||
private:
|
||||
void onLifecycleResult(const LiteWalletLifecycleResult& result);
|
||||
|
||||
LiteClientBridge bridge_; // the single owned bridge; services below borrow &bridge_
|
||||
LiteWalletLifecycleService lifecycle_;
|
||||
LiteWalletGateway gateway_;
|
||||
LiteSyncService sync_;
|
||||
std::function<void()> persist_;
|
||||
bool walletOpen_ = false;
|
||||
bool syncStarted_ = false;
|
||||
WalletBackendStatus status_;
|
||||
};
|
||||
|
||||
|
||||
@@ -135,11 +135,11 @@ LiteWalletRefreshBundle assembleLiteWalletRefreshBundle(const std::vector<LiteWa
|
||||
|
||||
LiteWalletGateway::LiteWalletGateway(WalletCapabilities capabilities,
|
||||
LiteConnectionSettings connectionSettings,
|
||||
LiteClientBridge bridge,
|
||||
LiteClientBridge* bridge,
|
||||
LiteWalletGatewayOptions options)
|
||||
: capabilities_(capabilities),
|
||||
connectionSettings_(std::move(connectionSettings)),
|
||||
bridge_(std::move(bridge)),
|
||||
bridge_(bridge),
|
||||
options_(options)
|
||||
{
|
||||
}
|
||||
@@ -148,7 +148,7 @@ LiteWalletGatewayAvailability LiteWalletGateway::availability() const
|
||||
{
|
||||
if (!isLiteBuild(capabilities_)) return LiteWalletGatewayAvailability::UnsupportedBuild;
|
||||
if (!supportsLiteBackend(capabilities_)) return LiteWalletGatewayAvailability::BackendUnavailable;
|
||||
if (!bridge_.available()) return LiteWalletGatewayAvailability::BridgeUnavailable;
|
||||
if (!bridge_ || !bridge_->available()) return LiteWalletGatewayAvailability::BridgeUnavailable;
|
||||
if (!selectLiteServer(connectionSettings_).ok) return LiteWalletGatewayAvailability::NoUsableServer;
|
||||
if (!options_.allowBridgeCalls) return LiteWalletGatewayAvailability::BridgeCallsDisabled;
|
||||
return LiteWalletGatewayAvailability::Ready;
|
||||
@@ -288,7 +288,7 @@ WalletBackendStatus LiteWalletGateway::statusFor(LiteWalletGatewayAvailability a
|
||||
case LiteWalletGatewayAvailability::BridgeUnavailable:
|
||||
return makeGatewayStatus(
|
||||
WalletBackendState::Unavailable,
|
||||
detail.empty() ? bridge_.unavailableReason() : detail);
|
||||
detail.empty() ? (bridge_ ? bridge_->unavailableReason() : "lite bridge is unavailable") : detail);
|
||||
case LiteWalletGatewayAvailability::BridgeCallsDisabled:
|
||||
return makeGatewayStatus(
|
||||
WalletBackendState::Unavailable,
|
||||
@@ -319,7 +319,7 @@ LiteWalletGatewayCommandResult LiteWalletGateway::executePlannedCommand(const Li
|
||||
result.plan = plan;
|
||||
result.attempted = true;
|
||||
|
||||
const auto bridgeCall = bridge_.execute(plan.commandName, plan.args);
|
||||
const auto bridgeCall = bridge_->execute(plan.commandName, plan.args);
|
||||
result.bridgeResponseRedacted = bridgeCall.ok || !bridgeCall.error.empty() ? "<redacted>" : "<empty>";
|
||||
if (!bridgeCall.ok) {
|
||||
result.status = makeGatewayStatus(WalletBackendState::Error, "lite wallet gateway bridge call failed");
|
||||
|
||||
@@ -129,7 +129,7 @@ class LiteWalletGateway {
|
||||
public:
|
||||
LiteWalletGateway(WalletCapabilities capabilities,
|
||||
LiteConnectionSettings connectionSettings,
|
||||
LiteClientBridge bridge,
|
||||
LiteClientBridge* bridge,
|
||||
LiteWalletGatewayOptions options = {});
|
||||
|
||||
const WalletCapabilities& capabilities() const { return capabilities_; }
|
||||
@@ -156,7 +156,7 @@ private:
|
||||
|
||||
WalletCapabilities capabilities_;
|
||||
LiteConnectionSettings connectionSettings_;
|
||||
LiteClientBridge bridge_;
|
||||
LiteClientBridge* bridge_ = nullptr; // non-owning; owned by LiteWalletController
|
||||
LiteWalletGatewayOptions options_;
|
||||
};
|
||||
|
||||
|
||||
@@ -119,11 +119,11 @@ std::string redactLitePrivateDataValue(const std::string& value)
|
||||
|
||||
LiteWalletLifecycleService::LiteWalletLifecycleService(WalletCapabilities capabilities,
|
||||
LiteConnectionSettings connectionSettings,
|
||||
LiteClientBridge bridge,
|
||||
LiteClientBridge* bridge,
|
||||
LiteWalletLifecycleOptions options)
|
||||
: capabilities_(capabilities),
|
||||
connectionSettings_(std::move(connectionSettings)),
|
||||
bridge_(std::move(bridge)),
|
||||
bridge_(bridge),
|
||||
options_(options)
|
||||
{
|
||||
}
|
||||
@@ -132,7 +132,7 @@ LiteWalletLifecycleAvailability LiteWalletLifecycleService::availability() const
|
||||
{
|
||||
if (!isLiteBuild(capabilities_)) return LiteWalletLifecycleAvailability::UnsupportedBuild;
|
||||
if (!supportsLiteBackend(capabilities_)) return LiteWalletLifecycleAvailability::BackendUnavailable;
|
||||
if (!bridge_.available()) return LiteWalletLifecycleAvailability::BridgeUnavailable;
|
||||
if (!bridge_ || !bridge_->available()) return LiteWalletLifecycleAvailability::BridgeUnavailable;
|
||||
if (!selectLiteServer(connectionSettings_).ok) return LiteWalletLifecycleAvailability::NoUsableServer;
|
||||
if (!options_.allowBridgeCalls) return LiteWalletLifecycleAvailability::BridgeCallsDisabled;
|
||||
return LiteWalletLifecycleAvailability::Ready;
|
||||
@@ -291,7 +291,7 @@ WalletBackendStatus LiteWalletLifecycleService::statusFor(
|
||||
case LiteWalletLifecycleAvailability::BridgeUnavailable:
|
||||
return WalletBackendStatus{
|
||||
WalletBackendState::Unavailable,
|
||||
detail.empty() ? bridge_.unavailableReason() : detail,
|
||||
detail.empty() ? (bridge_ ? bridge_->unavailableReason() : "lite bridge is unavailable") : detail,
|
||||
{},
|
||||
{},
|
||||
0.0
|
||||
@@ -321,7 +321,7 @@ LiteWalletLifecycleResult LiteWalletLifecycleService::executeCreate(
|
||||
const LiteWalletCreateRequest& request,
|
||||
const LiteWalletLifecyclePlan& plan)
|
||||
{
|
||||
auto bridgeCall = bridge_.initializeNew(request.dangerous, plan.server.url);
|
||||
auto bridgeCall = bridge_->initializeNew(request.dangerous, plan.server.url);
|
||||
return bridgeResult(plan, lifecycleCompletedStatus(), bridgeCall);
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@ LiteWalletLifecycleResult LiteWalletLifecycleService::executeOpen(
|
||||
const LiteWalletOpenRequest& request,
|
||||
const LiteWalletLifecyclePlan& plan)
|
||||
{
|
||||
auto bridgeCall = bridge_.initializeExisting(request.dangerous, plan.server.url);
|
||||
auto bridgeCall = bridge_->initializeExisting(request.dangerous, plan.server.url);
|
||||
return bridgeResult(plan, lifecycleCompletedStatus(), bridgeCall);
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ LiteWalletLifecycleResult LiteWalletLifecycleService::executeRestore(
|
||||
const LiteWalletRestoreRequest& request,
|
||||
const LiteWalletLifecyclePlan& plan)
|
||||
{
|
||||
auto bridgeCall = bridge_.initializeNewFromPhrase(
|
||||
auto bridgeCall = bridge_->initializeNewFromPhrase(
|
||||
request.dangerous,
|
||||
plan.server.url,
|
||||
request.seedPhrase,
|
||||
|
||||
@@ -102,9 +102,12 @@ std::string redactLitePrivateDataValue(const std::string& value);
|
||||
|
||||
class LiteWalletLifecycleService {
|
||||
public:
|
||||
// bridge is NON-OWNING (the controller owns the single shared LiteClientBridge);
|
||||
// it must outlive this service. The lite backend is a global singleton and every
|
||||
// LiteClientBridge shuts it down on destruction, so services must not own one.
|
||||
LiteWalletLifecycleService(WalletCapabilities capabilities,
|
||||
LiteConnectionSettings connectionSettings,
|
||||
LiteClientBridge bridge,
|
||||
LiteClientBridge* bridge,
|
||||
LiteWalletLifecycleOptions options = {});
|
||||
|
||||
const LiteConnectionSettings& connectionSettings() const { return connectionSettings_; }
|
||||
@@ -144,7 +147,7 @@ private:
|
||||
|
||||
WalletCapabilities capabilities_;
|
||||
LiteConnectionSettings connectionSettings_;
|
||||
LiteClientBridge bridge_;
|
||||
LiteClientBridge* bridge_ = nullptr; // non-owning; owned by LiteWalletController
|
||||
LiteWalletLifecycleOptions options_;
|
||||
};
|
||||
|
||||
|
||||
@@ -60,6 +60,28 @@ inline char* liteFakeExecute(const char* command, const char*)
|
||||
if (command && std::strcmp(command, "boom") == 0) {
|
||||
return liteFakeDup("Error: simulated lite backend failure");
|
||||
}
|
||||
// Command-appropriate canned responses matching the litelib JSON shapes (see
|
||||
// tests/fixtures/lite/result_parsers.json), so the gateway/sync refresh path parses.
|
||||
if (command) {
|
||||
const char* c = command;
|
||||
if (std::strcmp(c, "sync") == 0) return liteFakeDup("{\"result\":\"success\"}");
|
||||
if (std::strcmp(c, "syncstatus") == 0)
|
||||
return liteFakeDup("{\"synced_blocks\":1000,\"total_blocks\":1000}");
|
||||
if (std::strcmp(c, "balance") == 0)
|
||||
return liteFakeDup("{\"tbalance\":100000000,\"zbalance\":200000000,\"unconfirmed\":50000000,"
|
||||
"\"verified_zbalance\":180000000,\"spendable_zbalance\":170000000}");
|
||||
if (std::strcmp(c, "addresses") == 0)
|
||||
return liteFakeDup("{\"z_addresses\":[\"zs1fakeaddr\"],\"t_addresses\":[\"R1fakeaddr\"]}");
|
||||
if (std::strcmp(c, "notes") == 0)
|
||||
return liteFakeDup("{\"unspent_notes\":[],\"utxos\":[],\"pending_notes\":[],\"pending_utxos\":[]}");
|
||||
if (std::strcmp(c, "list") == 0)
|
||||
return liteFakeDup("[{\"txid\":\"faketx\",\"datetime\":1700000000,\"block_height\":990,"
|
||||
"\"unconfirmed\":false,\"address\":\"zs1fakeaddr\",\"amount\":150000000,\"memo\":\"\"}]");
|
||||
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}");
|
||||
}
|
||||
// Default for any other/unknown command.
|
||||
return liteFakeDup("{\"version\":\"sdxl-fake\"}");
|
||||
}
|
||||
inline void liteFakeFree(char* v)
|
||||
|
||||
@@ -4392,7 +4392,7 @@ void testLiteBackendInjectableFakeBridge()
|
||||
{
|
||||
dragonx::test::resetLiteFakeCounters();
|
||||
auto bridge = LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi());
|
||||
const auto result = bridge.execute("info", "");
|
||||
const auto result = bridge.execute("ping", ""); // unknown command -> default fake response
|
||||
EXPECT_TRUE(result.ok);
|
||||
EXPECT_EQ(result.value, std::string("{\"version\":\"sdxl-fake\"}"));
|
||||
EXPECT_EQ(dragonx::test::g_liteFakeAlloc, 1L);
|
||||
@@ -4620,6 +4620,36 @@ void testLiteRefreshModelAppliesToWalletState()
|
||||
EXPECT_FALSE(state.sync.syncing);
|
||||
}
|
||||
|
||||
// M2b: the controller, after a wallet is ready, auto-starts sync and refreshWalletState()
|
||||
// pulls balance/addresses/transactions/syncstatus through the shared bridge into WalletState.
|
||||
void testLiteWalletControllerRefreshPopulatesState()
|
||||
{
|
||||
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);
|
||||
EXPECT_TRUE(controller.walletOpen());
|
||||
EXPECT_TRUE(controller.syncStarted()); // auto-started when the wallet became ready
|
||||
|
||||
dragonx::WalletState state;
|
||||
EXPECT_TRUE(controller.refreshWalletState(state));
|
||||
EXPECT_NEAR(state.privateBalance, 2.0, 1e-9);
|
||||
EXPECT_NEAR(state.transparentBalance, 1.0, 1e-9);
|
||||
EXPECT_EQ(static_cast<int>(state.z_addresses.size()), 1);
|
||||
EXPECT_EQ(static_cast<int>(state.t_addresses.size()), 1);
|
||||
EXPECT_EQ(static_cast<int>(state.transactions.size()), 1);
|
||||
EXPECT_EQ(state.transactions[0].type, std::string("receive"));
|
||||
EXPECT_NEAR(state.transactions[0].amount, 1.5, 1e-9);
|
||||
EXPECT_EQ(state.sync.headers, 1000);
|
||||
|
||||
// No wallet open -> refresh is a safe no-op
|
||||
LiteWalletController fresh(caps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||
dragonx::WalletState empty;
|
||||
EXPECT_FALSE(fresh.refreshWalletState(empty));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -4656,6 +4686,7 @@ int main()
|
||||
testLiteWalletControllerLifecycle();
|
||||
testLiteChainNameMigration();
|
||||
testLiteRefreshModelAppliesToWalletState();
|
||||
testLiteWalletControllerRefreshPopulatesState();
|
||||
testLiteBridgeRuntimeShutdownIsIdempotent();
|
||||
testLiteBridgeRuntimeDestructorCallsShutdownOnce();
|
||||
testLiteBridgeRuntimeShutdownWaitsForOwnedStringRelease();
|
||||
|
||||
Reference in New Issue
Block a user