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.**
|
> **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.
|
> - ✅ **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.
|
> - ℹ️ 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).
|
- 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`).
|
- 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.
|
- 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,
|
LiteSyncService::LiteSyncService(WalletCapabilities capabilities,
|
||||||
LiteConnectionSettings connectionSettings,
|
LiteConnectionSettings connectionSettings,
|
||||||
LiteClientBridge bridge,
|
LiteClientBridge* bridge,
|
||||||
LiteSyncServiceOptions options)
|
LiteSyncServiceOptions options)
|
||||||
: capabilities_(capabilities),
|
: capabilities_(capabilities),
|
||||||
connectionSettings_(std::move(connectionSettings)),
|
connectionSettings_(std::move(connectionSettings)),
|
||||||
bridge_(std::move(bridge)),
|
bridge_(bridge),
|
||||||
options_(options)
|
options_(options)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ LiteSyncAvailability LiteSyncService::availability() const
|
|||||||
{
|
{
|
||||||
if (!isLiteBuild(capabilities_)) return LiteSyncAvailability::UnsupportedBuild;
|
if (!isLiteBuild(capabilities_)) return LiteSyncAvailability::UnsupportedBuild;
|
||||||
if (!supportsLiteBackend(capabilities_)) return LiteSyncAvailability::BackendUnavailable;
|
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 (!selectLiteServer(connectionSettings_).ok) return LiteSyncAvailability::NoUsableServer;
|
||||||
if (!options_.allowSyncStatusBridgeCalls) return LiteSyncAvailability::BridgeCallsDisabled;
|
if (!options_.allowSyncStatusBridgeCalls) return LiteSyncAvailability::BridgeCallsDisabled;
|
||||||
return LiteSyncAvailability::Ready;
|
return LiteSyncAvailability::Ready;
|
||||||
@@ -199,7 +199,7 @@ LiteSyncPlan LiteSyncService::planStartSync(const LiteSyncStartRequest& request)
|
|||||||
return makePlan(
|
return makePlan(
|
||||||
LiteSyncOperation::StartSync,
|
LiteSyncOperation::StartSync,
|
||||||
request.serverUrl,
|
request.serverUrl,
|
||||||
false,
|
options_.allowSyncStatusBridgeCalls,
|
||||||
request.mode,
|
request.mode,
|
||||||
request.forceRescan,
|
request.forceRescan,
|
||||||
request.afterRestore);
|
request.afterRestore);
|
||||||
@@ -229,9 +229,21 @@ LiteSyncStartResult LiteSyncService::startSync(const LiteSyncStartRequest& reque
|
|||||||
return blockedStartResult(plan, status());
|
return blockedStartResult(plan, status());
|
||||||
}
|
}
|
||||||
|
|
||||||
return blockedStartResult(
|
LiteSyncStartResult result;
|
||||||
plan,
|
result.plan = plan;
|
||||||
makeSyncScaffoldStatus(WalletBackendState::Unavailable, "lite sync start execution is not implemented"));
|
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)
|
LiteSyncStatusResult LiteSyncService::pollSyncStatus(const LiteSyncStatusRequest& request)
|
||||||
@@ -245,7 +257,7 @@ LiteSyncStatusResult LiteSyncService::pollSyncStatus(const LiteSyncStatusRequest
|
|||||||
result.plan = plan;
|
result.plan = plan;
|
||||||
result.attempted = true;
|
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>";
|
result.bridgeResponseRedacted = bridgeCall.ok || !bridgeCall.error.empty() ? "<redacted>" : "<empty>";
|
||||||
if (!bridgeCall.ok) {
|
if (!bridgeCall.ok) {
|
||||||
result.status = makeSyncScaffoldStatus(WalletBackendState::Error, "lite syncstatus bridge call failed");
|
result.status = makeSyncScaffoldStatus(WalletBackendState::Error, "lite syncstatus bridge call failed");
|
||||||
@@ -334,7 +346,7 @@ WalletBackendStatus LiteSyncService::statusFor(LiteSyncAvailability availability
|
|||||||
case LiteSyncAvailability::BridgeUnavailable:
|
case LiteSyncAvailability::BridgeUnavailable:
|
||||||
return makeSyncScaffoldStatus(
|
return makeSyncScaffoldStatus(
|
||||||
WalletBackendState::Unavailable,
|
WalletBackendState::Unavailable,
|
||||||
detail.empty() ? bridge_.unavailableReason() : detail);
|
detail.empty() ? (bridge_ ? bridge_->unavailableReason() : "lite bridge is unavailable") : detail);
|
||||||
case LiteSyncAvailability::BridgeCallsDisabled:
|
case LiteSyncAvailability::BridgeCallsDisabled:
|
||||||
return makeSyncScaffoldStatus(
|
return makeSyncScaffoldStatus(
|
||||||
WalletBackendState::Unavailable,
|
WalletBackendState::Unavailable,
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class LiteSyncService {
|
|||||||
public:
|
public:
|
||||||
LiteSyncService(WalletCapabilities capabilities,
|
LiteSyncService(WalletCapabilities capabilities,
|
||||||
LiteConnectionSettings connectionSettings,
|
LiteConnectionSettings connectionSettings,
|
||||||
LiteClientBridge bridge,
|
LiteClientBridge* bridge,
|
||||||
LiteSyncServiceOptions options = {});
|
LiteSyncServiceOptions options = {});
|
||||||
|
|
||||||
const WalletCapabilities& capabilities() const { return capabilities_; }
|
const WalletCapabilities& capabilities() const { return capabilities_; }
|
||||||
@@ -155,7 +155,7 @@ private:
|
|||||||
|
|
||||||
WalletCapabilities capabilities_;
|
WalletCapabilities capabilities_;
|
||||||
LiteConnectionSettings connectionSettings_;
|
LiteConnectionSettings connectionSettings_;
|
||||||
LiteClientBridge bridge_;
|
LiteClientBridge* bridge_ = nullptr; // non-owning; owned by LiteWalletController
|
||||||
LiteSyncServiceOptions options_;
|
LiteSyncServiceOptions options_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -94,10 +94,13 @@ LiteWalletController::LiteWalletController(WalletCapabilities capabilities,
|
|||||||
LiteConnectionSettings connectionSettings,
|
LiteConnectionSettings connectionSettings,
|
||||||
LiteClientBridge bridge,
|
LiteClientBridge bridge,
|
||||||
LiteWalletControllerOptions options)
|
LiteWalletControllerOptions options)
|
||||||
: lifecycle_(capabilities,
|
: bridge_(std::move(bridge)),
|
||||||
std::move(connectionSettings),
|
lifecycle_(capabilities, connectionSettings, &bridge_,
|
||||||
std::move(bridge),
|
LiteWalletLifecycleOptions{options.allowBridgeCalls}),
|
||||||
LiteWalletLifecycleOptions{options.allowBridgeCalls})
|
gateway_(capabilities, connectionSettings, &bridge_,
|
||||||
|
LiteWalletGatewayOptions{options.allowBridgeCalls}),
|
||||||
|
sync_(capabilities, connectionSettings, &bridge_,
|
||||||
|
LiteSyncServiceOptions{options.allowBridgeCalls})
|
||||||
{
|
{
|
||||||
status_ = lifecycle_.status();
|
status_ = lifecycle_.status();
|
||||||
}
|
}
|
||||||
@@ -119,9 +122,46 @@ void LiteWalletController::onLifecycleResult(const LiteWalletLifecycleResult& re
|
|||||||
if (result.walletReady) {
|
if (result.walletReady) {
|
||||||
walletOpen_ = true;
|
walletOpen_ = true;
|
||||||
if (persist_) persist_();
|
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)
|
LiteWalletLifecycleResult LiteWalletController::createWallet(LiteWalletCreateRequest request)
|
||||||
{
|
{
|
||||||
auto result = lifecycle_.createWallet(request);
|
auto result = lifecycle_.createWallet(request);
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
#include "lite_client_bridge.h"
|
#include "lite_client_bridge.h"
|
||||||
#include "lite_connection_service.h"
|
#include "lite_connection_service.h"
|
||||||
#include "lite_wallet_lifecycle_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 "lite_wallet_state_mapper.h"
|
||||||
#include "wallet_backend.h"
|
#include "wallet_backend.h"
|
||||||
#include "wallet_capabilities.h"
|
#include "wallet_capabilities.h"
|
||||||
@@ -72,12 +74,26 @@ public:
|
|||||||
LiteWalletLifecycleResult openWallet(LiteWalletOpenRequest request);
|
LiteWalletLifecycleResult openWallet(LiteWalletOpenRequest request);
|
||||||
LiteWalletLifecycleResult restoreWallet(LiteWalletRestoreRequest 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:
|
private:
|
||||||
void onLifecycleResult(const LiteWalletLifecycleResult& result);
|
void onLifecycleResult(const LiteWalletLifecycleResult& result);
|
||||||
|
|
||||||
|
LiteClientBridge bridge_; // the single owned bridge; services below borrow &bridge_
|
||||||
LiteWalletLifecycleService lifecycle_;
|
LiteWalletLifecycleService lifecycle_;
|
||||||
|
LiteWalletGateway gateway_;
|
||||||
|
LiteSyncService sync_;
|
||||||
std::function<void()> persist_;
|
std::function<void()> persist_;
|
||||||
bool walletOpen_ = false;
|
bool walletOpen_ = false;
|
||||||
|
bool syncStarted_ = false;
|
||||||
WalletBackendStatus status_;
|
WalletBackendStatus status_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -135,11 +135,11 @@ LiteWalletRefreshBundle assembleLiteWalletRefreshBundle(const std::vector<LiteWa
|
|||||||
|
|
||||||
LiteWalletGateway::LiteWalletGateway(WalletCapabilities capabilities,
|
LiteWalletGateway::LiteWalletGateway(WalletCapabilities capabilities,
|
||||||
LiteConnectionSettings connectionSettings,
|
LiteConnectionSettings connectionSettings,
|
||||||
LiteClientBridge bridge,
|
LiteClientBridge* bridge,
|
||||||
LiteWalletGatewayOptions options)
|
LiteWalletGatewayOptions options)
|
||||||
: capabilities_(capabilities),
|
: capabilities_(capabilities),
|
||||||
connectionSettings_(std::move(connectionSettings)),
|
connectionSettings_(std::move(connectionSettings)),
|
||||||
bridge_(std::move(bridge)),
|
bridge_(bridge),
|
||||||
options_(options)
|
options_(options)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -148,7 +148,7 @@ LiteWalletGatewayAvailability LiteWalletGateway::availability() const
|
|||||||
{
|
{
|
||||||
if (!isLiteBuild(capabilities_)) return LiteWalletGatewayAvailability::UnsupportedBuild;
|
if (!isLiteBuild(capabilities_)) return LiteWalletGatewayAvailability::UnsupportedBuild;
|
||||||
if (!supportsLiteBackend(capabilities_)) return LiteWalletGatewayAvailability::BackendUnavailable;
|
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 (!selectLiteServer(connectionSettings_).ok) return LiteWalletGatewayAvailability::NoUsableServer;
|
||||||
if (!options_.allowBridgeCalls) return LiteWalletGatewayAvailability::BridgeCallsDisabled;
|
if (!options_.allowBridgeCalls) return LiteWalletGatewayAvailability::BridgeCallsDisabled;
|
||||||
return LiteWalletGatewayAvailability::Ready;
|
return LiteWalletGatewayAvailability::Ready;
|
||||||
@@ -288,7 +288,7 @@ WalletBackendStatus LiteWalletGateway::statusFor(LiteWalletGatewayAvailability a
|
|||||||
case LiteWalletGatewayAvailability::BridgeUnavailable:
|
case LiteWalletGatewayAvailability::BridgeUnavailable:
|
||||||
return makeGatewayStatus(
|
return makeGatewayStatus(
|
||||||
WalletBackendState::Unavailable,
|
WalletBackendState::Unavailable,
|
||||||
detail.empty() ? bridge_.unavailableReason() : detail);
|
detail.empty() ? (bridge_ ? bridge_->unavailableReason() : "lite bridge is unavailable") : detail);
|
||||||
case LiteWalletGatewayAvailability::BridgeCallsDisabled:
|
case LiteWalletGatewayAvailability::BridgeCallsDisabled:
|
||||||
return makeGatewayStatus(
|
return makeGatewayStatus(
|
||||||
WalletBackendState::Unavailable,
|
WalletBackendState::Unavailable,
|
||||||
@@ -319,7 +319,7 @@ LiteWalletGatewayCommandResult LiteWalletGateway::executePlannedCommand(const Li
|
|||||||
result.plan = plan;
|
result.plan = plan;
|
||||||
result.attempted = true;
|
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>";
|
result.bridgeResponseRedacted = bridgeCall.ok || !bridgeCall.error.empty() ? "<redacted>" : "<empty>";
|
||||||
if (!bridgeCall.ok) {
|
if (!bridgeCall.ok) {
|
||||||
result.status = makeGatewayStatus(WalletBackendState::Error, "lite wallet gateway bridge call failed");
|
result.status = makeGatewayStatus(WalletBackendState::Error, "lite wallet gateway bridge call failed");
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class LiteWalletGateway {
|
|||||||
public:
|
public:
|
||||||
LiteWalletGateway(WalletCapabilities capabilities,
|
LiteWalletGateway(WalletCapabilities capabilities,
|
||||||
LiteConnectionSettings connectionSettings,
|
LiteConnectionSettings connectionSettings,
|
||||||
LiteClientBridge bridge,
|
LiteClientBridge* bridge,
|
||||||
LiteWalletGatewayOptions options = {});
|
LiteWalletGatewayOptions options = {});
|
||||||
|
|
||||||
const WalletCapabilities& capabilities() const { return capabilities_; }
|
const WalletCapabilities& capabilities() const { return capabilities_; }
|
||||||
@@ -156,7 +156,7 @@ private:
|
|||||||
|
|
||||||
WalletCapabilities capabilities_;
|
WalletCapabilities capabilities_;
|
||||||
LiteConnectionSettings connectionSettings_;
|
LiteConnectionSettings connectionSettings_;
|
||||||
LiteClientBridge bridge_;
|
LiteClientBridge* bridge_ = nullptr; // non-owning; owned by LiteWalletController
|
||||||
LiteWalletGatewayOptions options_;
|
LiteWalletGatewayOptions options_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -119,11 +119,11 @@ std::string redactLitePrivateDataValue(const std::string& value)
|
|||||||
|
|
||||||
LiteWalletLifecycleService::LiteWalletLifecycleService(WalletCapabilities capabilities,
|
LiteWalletLifecycleService::LiteWalletLifecycleService(WalletCapabilities capabilities,
|
||||||
LiteConnectionSettings connectionSettings,
|
LiteConnectionSettings connectionSettings,
|
||||||
LiteClientBridge bridge,
|
LiteClientBridge* bridge,
|
||||||
LiteWalletLifecycleOptions options)
|
LiteWalletLifecycleOptions options)
|
||||||
: capabilities_(capabilities),
|
: capabilities_(capabilities),
|
||||||
connectionSettings_(std::move(connectionSettings)),
|
connectionSettings_(std::move(connectionSettings)),
|
||||||
bridge_(std::move(bridge)),
|
bridge_(bridge),
|
||||||
options_(options)
|
options_(options)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ LiteWalletLifecycleAvailability LiteWalletLifecycleService::availability() const
|
|||||||
{
|
{
|
||||||
if (!isLiteBuild(capabilities_)) return LiteWalletLifecycleAvailability::UnsupportedBuild;
|
if (!isLiteBuild(capabilities_)) return LiteWalletLifecycleAvailability::UnsupportedBuild;
|
||||||
if (!supportsLiteBackend(capabilities_)) return LiteWalletLifecycleAvailability::BackendUnavailable;
|
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 (!selectLiteServer(connectionSettings_).ok) return LiteWalletLifecycleAvailability::NoUsableServer;
|
||||||
if (!options_.allowBridgeCalls) return LiteWalletLifecycleAvailability::BridgeCallsDisabled;
|
if (!options_.allowBridgeCalls) return LiteWalletLifecycleAvailability::BridgeCallsDisabled;
|
||||||
return LiteWalletLifecycleAvailability::Ready;
|
return LiteWalletLifecycleAvailability::Ready;
|
||||||
@@ -291,7 +291,7 @@ WalletBackendStatus LiteWalletLifecycleService::statusFor(
|
|||||||
case LiteWalletLifecycleAvailability::BridgeUnavailable:
|
case LiteWalletLifecycleAvailability::BridgeUnavailable:
|
||||||
return WalletBackendStatus{
|
return WalletBackendStatus{
|
||||||
WalletBackendState::Unavailable,
|
WalletBackendState::Unavailable,
|
||||||
detail.empty() ? bridge_.unavailableReason() : detail,
|
detail.empty() ? (bridge_ ? bridge_->unavailableReason() : "lite bridge is unavailable") : detail,
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
0.0
|
0.0
|
||||||
@@ -321,7 +321,7 @@ LiteWalletLifecycleResult LiteWalletLifecycleService::executeCreate(
|
|||||||
const LiteWalletCreateRequest& request,
|
const LiteWalletCreateRequest& request,
|
||||||
const LiteWalletLifecyclePlan& plan)
|
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);
|
return bridgeResult(plan, lifecycleCompletedStatus(), bridgeCall);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ LiteWalletLifecycleResult LiteWalletLifecycleService::executeOpen(
|
|||||||
const LiteWalletOpenRequest& request,
|
const LiteWalletOpenRequest& request,
|
||||||
const LiteWalletLifecyclePlan& plan)
|
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);
|
return bridgeResult(plan, lifecycleCompletedStatus(), bridgeCall);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +337,7 @@ LiteWalletLifecycleResult LiteWalletLifecycleService::executeRestore(
|
|||||||
const LiteWalletRestoreRequest& request,
|
const LiteWalletRestoreRequest& request,
|
||||||
const LiteWalletLifecyclePlan& plan)
|
const LiteWalletLifecyclePlan& plan)
|
||||||
{
|
{
|
||||||
auto bridgeCall = bridge_.initializeNewFromPhrase(
|
auto bridgeCall = bridge_->initializeNewFromPhrase(
|
||||||
request.dangerous,
|
request.dangerous,
|
||||||
plan.server.url,
|
plan.server.url,
|
||||||
request.seedPhrase,
|
request.seedPhrase,
|
||||||
|
|||||||
@@ -102,9 +102,12 @@ std::string redactLitePrivateDataValue(const std::string& value);
|
|||||||
|
|
||||||
class LiteWalletLifecycleService {
|
class LiteWalletLifecycleService {
|
||||||
public:
|
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,
|
LiteWalletLifecycleService(WalletCapabilities capabilities,
|
||||||
LiteConnectionSettings connectionSettings,
|
LiteConnectionSettings connectionSettings,
|
||||||
LiteClientBridge bridge,
|
LiteClientBridge* bridge,
|
||||||
LiteWalletLifecycleOptions options = {});
|
LiteWalletLifecycleOptions options = {});
|
||||||
|
|
||||||
const LiteConnectionSettings& connectionSettings() const { return connectionSettings_; }
|
const LiteConnectionSettings& connectionSettings() const { return connectionSettings_; }
|
||||||
@@ -144,7 +147,7 @@ private:
|
|||||||
|
|
||||||
WalletCapabilities capabilities_;
|
WalletCapabilities capabilities_;
|
||||||
LiteConnectionSettings connectionSettings_;
|
LiteConnectionSettings connectionSettings_;
|
||||||
LiteClientBridge bridge_;
|
LiteClientBridge* bridge_ = nullptr; // non-owning; owned by LiteWalletController
|
||||||
LiteWalletLifecycleOptions options_;
|
LiteWalletLifecycleOptions options_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,28 @@ inline char* liteFakeExecute(const char* command, const char*)
|
|||||||
if (command && std::strcmp(command, "boom") == 0) {
|
if (command && std::strcmp(command, "boom") == 0) {
|
||||||
return liteFakeDup("Error: simulated lite backend failure");
|
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\"}");
|
return liteFakeDup("{\"version\":\"sdxl-fake\"}");
|
||||||
}
|
}
|
||||||
inline void liteFakeFree(char* v)
|
inline void liteFakeFree(char* v)
|
||||||
|
|||||||
@@ -4392,7 +4392,7 @@ void testLiteBackendInjectableFakeBridge()
|
|||||||
{
|
{
|
||||||
dragonx::test::resetLiteFakeCounters();
|
dragonx::test::resetLiteFakeCounters();
|
||||||
auto bridge = LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi());
|
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_TRUE(result.ok);
|
||||||
EXPECT_EQ(result.value, std::string("{\"version\":\"sdxl-fake\"}"));
|
EXPECT_EQ(result.value, std::string("{\"version\":\"sdxl-fake\"}"));
|
||||||
EXPECT_EQ(dragonx::test::g_liteFakeAlloc, 1L);
|
EXPECT_EQ(dragonx::test::g_liteFakeAlloc, 1L);
|
||||||
@@ -4620,6 +4620,36 @@ void testLiteRefreshModelAppliesToWalletState()
|
|||||||
EXPECT_FALSE(state.sync.syncing);
|
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
|
} // namespace
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
@@ -4656,6 +4686,7 @@ int main()
|
|||||||
testLiteWalletControllerLifecycle();
|
testLiteWalletControllerLifecycle();
|
||||||
testLiteChainNameMigration();
|
testLiteChainNameMigration();
|
||||||
testLiteRefreshModelAppliesToWalletState();
|
testLiteRefreshModelAppliesToWalletState();
|
||||||
|
testLiteWalletControllerRefreshPopulatesState();
|
||||||
testLiteBridgeRuntimeShutdownIsIdempotent();
|
testLiteBridgeRuntimeShutdownIsIdempotent();
|
||||||
testLiteBridgeRuntimeDestructorCallsShutdownOnce();
|
testLiteBridgeRuntimeDestructorCallsShutdownOnce();
|
||||||
testLiteBridgeRuntimeShutdownWaitsForOwnedStringRelease();
|
testLiteBridgeRuntimeShutdownWaitsForOwnedStringRelease();
|
||||||
|
|||||||
Reference in New Issue
Block a user