feat(lite): runtime kill-switch + staged-rollout gate (M5b)
Adds a fail-open, local-only gate that decides whether the lite wallet may run,
so a post-release issue can disable it and rollout can be staged — without any
phone-home (privacy posture: no runtime network fetch; the per-install rollout
bucket is a hashed, never-transmitted local id).
- wallet/lite_rollout_policy.{h,cpp}: a pure decision core. Order — emergency env
kill-switch (absolute) -> local override -> manifest gates (global enable /
version floor-ceiling / blocklist / staged-rollout permille) -> fail-open allow.
Plus a JSON manifest loader (missing/invalid -> fail-open) and FNV-1a bucketing.
- Threads the decision through LiteWalletController -> LiteWalletLifecycleService:
new availability() reason RolloutDisabled blocks create/open/restore and surfaces
the gate's user-facing message via the lifecycle status.
- App::rebuildLiteWallet() resolves it from: DRAGONX_LITE_KILL_SWITCH (env), the
lite_rollout setting (auto/force_on/force_off), and a locally-cached manifest at
<config-dir>/lite_rollout.json. install id generated once via libsodium.
- Settings: persist lite_rollout override + the install id.
A signed remote fetcher can populate the manifest cache later without touching the
policy. Unit-tested (version compare, bucketing, override/env precedence, manifest
gates, staged rollout, loader fail-open, controller integration) and runtime-verified
on Linux (env kill-switch, manifest disable, control sync). Both variants build;
full suite passes; hygiene clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
61
src/app.cpp
61
src/app.cpp
@@ -13,7 +13,9 @@
|
||||
#include "config/settings.h"
|
||||
#include "wallet/lite_wallet_controller.h"
|
||||
#include "wallet/lite_wallet_server_selection_adapter.h"
|
||||
#include "wallet/lite_rollout_policy.h"
|
||||
|
||||
#include <cstdlib> // std::getenv for the lite kill-switch env var
|
||||
#include <sodium.h> // sodium_memzero for the lite unlock-passphrase buffer
|
||||
#include "daemon/daemon_controller.h"
|
||||
#include "daemon/embedded_daemon.h"
|
||||
@@ -370,6 +372,62 @@ void App::preFrame()
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Resolve the lite-wallet rollout / kill-switch decision (see wallet/lite_rollout_policy.h).
|
||||
// Local-only: reads the override + a stable per-install id from settings, an emergency env var,
|
||||
// and a locally-cached manifest file (NO network fetch). Fail-open: a missing/invalid manifest
|
||||
// leaves the wallet enabled.
|
||||
wallet::LiteRolloutDecision resolveLiteRolloutDecision(config::Settings& settings)
|
||||
{
|
||||
using namespace dragonx::wallet;
|
||||
LiteRolloutInputs inputs;
|
||||
inputs.appVersion = DRAGONX_VERSION;
|
||||
|
||||
// Emergency kill-switch env var: any value other than empty/"0"/"false" disables.
|
||||
if (const char* env = std::getenv("DRAGONX_LITE_KILL_SWITCH")) {
|
||||
const std::string v = env;
|
||||
inputs.killSwitchEnv = !v.empty() && v != "0" && v != "false";
|
||||
}
|
||||
|
||||
inputs.override = liteRolloutOverrideFromString(settings.getLiteRolloutOverride());
|
||||
|
||||
// Stable per-install bucket source — generated once, persisted, never transmitted, no PII.
|
||||
std::string installId = settings.getLiteInstallId();
|
||||
if (installId.empty()) {
|
||||
if (sodium_init() >= 0) {
|
||||
unsigned char buf[16];
|
||||
randombytes_buf(buf, sizeof(buf));
|
||||
static const char kHex[] = "0123456789abcdef";
|
||||
installId.reserve(sizeof(buf) * 2);
|
||||
for (unsigned char c : buf) {
|
||||
installId.push_back(kHex[c >> 4]);
|
||||
installId.push_back(kHex[c & 0x0F]);
|
||||
}
|
||||
}
|
||||
settings.setLiteInstallId(installId);
|
||||
settings.save();
|
||||
}
|
||||
inputs.installBucket = liteRolloutBucketFromInstallId(installId);
|
||||
|
||||
// Local manifest cache next to settings.json — no network fetch (a signed remote fetcher can
|
||||
// populate this later). Absent/unreadable -> fail-open.
|
||||
try {
|
||||
const std::filesystem::path cfgDir =
|
||||
std::filesystem::path(config::Settings::getDefaultPath()).parent_path();
|
||||
inputs.manifest = loadLiteRolloutManifestFromFile((cfgDir / "lite_rollout.json").string());
|
||||
} catch (...) {
|
||||
// leave manifest absent -> fail-open
|
||||
}
|
||||
|
||||
const LiteRolloutDecision decision = evaluateLiteRollout(inputs);
|
||||
if (!decision.allowed) {
|
||||
DEBUG_LOGF("[lite-rollout] lite wallet gated OFF: %s — %s\n",
|
||||
liteRolloutStatusName(decision.status), decision.message.c_str());
|
||||
}
|
||||
return decision;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void App::rebuildLiteWallet()
|
||||
{
|
||||
if (!supportsLiteBackend() || !settings_) return;
|
||||
@@ -381,7 +439,8 @@ void App::rebuildLiteWallet()
|
||||
|
||||
lite_wallet_ = wallet::LiteWalletController::createLinked(
|
||||
walletCapabilities(),
|
||||
wallet::liteConnectionSettingsFromAppSettings(*settings_));
|
||||
wallet::liteConnectionSettingsFromAppSettings(*settings_),
|
||||
resolveLiteRolloutDecision(*settings_));
|
||||
lite_wallet_->setPersistCallback([this]() { settings_->save(); });
|
||||
}
|
||||
|
||||
|
||||
@@ -222,6 +222,13 @@ bool Settings::load(const std::string& path)
|
||||
lite_servers_.push_back(preference);
|
||||
}
|
||||
}
|
||||
if (lite.contains("rollout_override") && lite["rollout_override"].is_string()) {
|
||||
const auto v = lite["rollout_override"].get<std::string>();
|
||||
lite_rollout_override_ = (v == "force_on" || v == "force_off") ? v : "auto";
|
||||
}
|
||||
if (lite.contains("install_id") && lite["install_id"].is_string()) {
|
||||
lite_install_id_ = lite["install_id"].get<std::string>();
|
||||
}
|
||||
}
|
||||
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
|
||||
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
|
||||
@@ -355,6 +362,8 @@ bool Settings::save(const std::string& path)
|
||||
entry["enabled"] = server.enabled;
|
||||
lite["servers"].push_back(entry);
|
||||
}
|
||||
lite["rollout_override"] = lite_rollout_override_;
|
||||
lite["install_id"] = lite_install_id_;
|
||||
j["lite_wallet"] = lite;
|
||||
}
|
||||
j["verbose_logging"] = verbose_logging_;
|
||||
|
||||
@@ -244,6 +244,15 @@ public:
|
||||
const std::vector<LiteServerPreference>& getLiteServers() const { return lite_servers_; }
|
||||
void setLiteServers(const std::vector<LiteServerPreference>& servers) { lite_servers_ = servers; }
|
||||
|
||||
// Lite wallet rollout / kill-switch (see wallet/lite_rollout_policy.h).
|
||||
// Override: "auto" (honor rollout manifest), "force_on", or "force_off".
|
||||
std::string getLiteRolloutOverride() const { return lite_rollout_override_; }
|
||||
void setLiteRolloutOverride(const std::string& v) { lite_rollout_override_ = v; }
|
||||
// Stable, locally-generated install id used only to derive the staged-rollout bucket.
|
||||
// Never transmitted; carries no PII. Generated on first use if empty.
|
||||
std::string getLiteInstallId() const { return lite_install_id_; }
|
||||
void setLiteInstallId(const std::string& v) { lite_install_id_ = v; }
|
||||
|
||||
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
|
||||
bool getVerboseLogging() const { return verbose_logging_; }
|
||||
void setVerboseLogging(bool v) { verbose_logging_ = v; }
|
||||
@@ -392,6 +401,8 @@ private:
|
||||
std::string lite_chain_name_ = "main"; // SDXL backend chain id; must be main/test/regtest
|
||||
std::size_t lite_random_selection_seed_ = 0;
|
||||
bool lite_persist_selected_server_ = true;
|
||||
std::string lite_rollout_override_ = "auto"; // auto|force_on|force_off
|
||||
std::string lite_install_id_; // random local-only id; rollout-bucket source
|
||||
std::vector<LiteServerPreference> lite_servers_ = {
|
||||
{"https://lite.dragonx.is", "DragonX Lite", true},
|
||||
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
|
||||
|
||||
181
src/wallet/lite_rollout_policy.cpp
Normal file
181
src/wallet/lite_rollout_policy.cpp
Normal file
@@ -0,0 +1,181 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "lite_rollout_policy.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace dragonx {
|
||||
namespace wallet {
|
||||
|
||||
namespace {
|
||||
using json = nlohmann::json;
|
||||
|
||||
// Numeric dotted core of a version, dropping any "-prerelease" / "+build" suffix.
|
||||
std::vector<long long> parseVersionCore(const std::string& v)
|
||||
{
|
||||
const std::string core = v.substr(0, v.find_first_of("-+"));
|
||||
std::vector<long long> parts;
|
||||
std::stringstream ss(core);
|
||||
std::string token;
|
||||
while (std::getline(ss, token, '.')) {
|
||||
if (token.empty()) { parts.push_back(0); continue; }
|
||||
try { parts.push_back(std::stoll(token)); }
|
||||
catch (...) { parts.push_back(0); }
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
const char* defaultMessageFor(LiteRolloutStatus s)
|
||||
{
|
||||
switch (s) {
|
||||
case LiteRolloutStatus::DisabledByKillSwitchEnv:
|
||||
return "The lite wallet is disabled by an emergency kill-switch on this machine.";
|
||||
case LiteRolloutStatus::DisabledByLocalOverride:
|
||||
return "The lite wallet is turned off in this app's settings.";
|
||||
case LiteRolloutStatus::DisabledByManifest:
|
||||
return "The lite wallet is currently disabled by the rollout policy.";
|
||||
case LiteRolloutStatus::DisabledUnsupportedVersion:
|
||||
return "This app version is not supported by the lite wallet; please update.";
|
||||
case LiteRolloutStatus::DisabledBlockedVersion:
|
||||
return "This app version has a known issue and the lite wallet is disabled; please update.";
|
||||
case LiteRolloutStatus::DisabledByStagedRollout:
|
||||
return "The lite wallet is rolling out gradually and is not yet enabled for this install.";
|
||||
case LiteRolloutStatus::Allowed:
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
LiteRolloutDecision disabledDecision(LiteRolloutStatus s, const std::string& manifestMessage)
|
||||
{
|
||||
LiteRolloutDecision d;
|
||||
d.allowed = false;
|
||||
d.status = s;
|
||||
d.message = !manifestMessage.empty() ? manifestMessage : defaultMessageFor(s);
|
||||
return d;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int compareLiteVersions(const std::string& a, const std::string& b)
|
||||
{
|
||||
const auto pa = parseVersionCore(a);
|
||||
const auto pb = parseVersionCore(b);
|
||||
const std::size_t n = std::max(pa.size(), pb.size());
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
const long long va = i < pa.size() ? pa[i] : 0;
|
||||
const long long vb = i < pb.size() ? pb[i] : 0;
|
||||
if (va < vb) return -1;
|
||||
if (va > vb) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int liteRolloutBucketFromInstallId(const std::string& installId)
|
||||
{
|
||||
if (installId.empty()) return 0;
|
||||
// FNV-1a (64-bit) → 0..999. Stable across runs for a given id.
|
||||
std::uint64_t h = 1469598103934665603ULL;
|
||||
for (unsigned char c : installId) {
|
||||
h ^= static_cast<std::uint64_t>(c);
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
return static_cast<int>(h % 1000ULL);
|
||||
}
|
||||
|
||||
LiteRolloutOverride liteRolloutOverrideFromString(const std::string& value)
|
||||
{
|
||||
if (value == "force_on") return LiteRolloutOverride::ForceOn;
|
||||
if (value == "force_off") return LiteRolloutOverride::ForceOff;
|
||||
return LiteRolloutOverride::Auto;
|
||||
}
|
||||
|
||||
const char* liteRolloutOverrideToString(LiteRolloutOverride o)
|
||||
{
|
||||
switch (o) {
|
||||
case LiteRolloutOverride::ForceOn: return "force_on";
|
||||
case LiteRolloutOverride::ForceOff: return "force_off";
|
||||
case LiteRolloutOverride::Auto: default: return "auto";
|
||||
}
|
||||
}
|
||||
|
||||
const char* liteRolloutStatusName(LiteRolloutStatus s)
|
||||
{
|
||||
switch (s) {
|
||||
case LiteRolloutStatus::Allowed: return "Allowed";
|
||||
case LiteRolloutStatus::DisabledByKillSwitchEnv: return "DisabledByKillSwitchEnv";
|
||||
case LiteRolloutStatus::DisabledByLocalOverride: return "DisabledByLocalOverride";
|
||||
case LiteRolloutStatus::DisabledByManifest: return "DisabledByManifest";
|
||||
case LiteRolloutStatus::DisabledUnsupportedVersion: return "DisabledUnsupportedVersion";
|
||||
case LiteRolloutStatus::DisabledBlockedVersion: return "DisabledBlockedVersion";
|
||||
case LiteRolloutStatus::DisabledByStagedRollout: return "DisabledByStagedRollout";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
LiteRolloutDecision evaluateLiteRollout(const LiteRolloutInputs& in)
|
||||
{
|
||||
// 1. Emergency env kill-switch is absolute — not even ForceOn bypasses it.
|
||||
if (in.killSwitchEnv)
|
||||
return disabledDecision(LiteRolloutStatus::DisabledByKillSwitchEnv, {});
|
||||
|
||||
// 2/3. Local override.
|
||||
if (in.override == LiteRolloutOverride::ForceOff)
|
||||
return disabledDecision(LiteRolloutStatus::DisabledByLocalOverride, {});
|
||||
if (in.override == LiteRolloutOverride::ForceOn)
|
||||
return LiteRolloutDecision{true, LiteRolloutStatus::Allowed, {}};
|
||||
|
||||
// 4. Manifest gates — fail-open: only enforced when a manifest is present AND valid.
|
||||
const LiteRolloutManifest& m = in.manifest;
|
||||
if (m.present && m.valid) {
|
||||
if (!m.globalEnabled)
|
||||
return disabledDecision(LiteRolloutStatus::DisabledByManifest, m.message);
|
||||
if (!m.minVersion.empty() && compareLiteVersions(in.appVersion, m.minVersion) < 0)
|
||||
return disabledDecision(LiteRolloutStatus::DisabledUnsupportedVersion, m.message);
|
||||
if (!m.maxVersion.empty() && compareLiteVersions(in.appVersion, m.maxVersion) > 0)
|
||||
return disabledDecision(LiteRolloutStatus::DisabledUnsupportedVersion, m.message);
|
||||
for (const auto& bad : m.blockedVersions)
|
||||
if (compareLiteVersions(in.appVersion, bad) == 0)
|
||||
return disabledDecision(LiteRolloutStatus::DisabledBlockedVersion, m.message);
|
||||
if (in.installBucket >= m.rolloutPermille)
|
||||
return disabledDecision(LiteRolloutStatus::DisabledByStagedRollout, m.message);
|
||||
}
|
||||
|
||||
// 5. Fail-open default.
|
||||
return LiteRolloutDecision{true, LiteRolloutStatus::Allowed, {}};
|
||||
}
|
||||
|
||||
LiteRolloutManifest loadLiteRolloutManifestFromFile(const std::string& path)
|
||||
{
|
||||
LiteRolloutManifest m; // present=false, valid=false (fail-open)
|
||||
std::ifstream in(path);
|
||||
if (!in.is_open()) return m; // no file -> no manifest
|
||||
m.present = true;
|
||||
try {
|
||||
json j;
|
||||
in >> j;
|
||||
if (j.contains("global_enabled")) m.globalEnabled = j["global_enabled"].get<bool>();
|
||||
if (j.contains("min_version")) m.minVersion = j["min_version"].get<std::string>();
|
||||
if (j.contains("max_version")) m.maxVersion = j["max_version"].get<std::string>();
|
||||
if (j.contains("blocked_versions"))
|
||||
m.blockedVersions = j["blocked_versions"].get<std::vector<std::string>>();
|
||||
if (j.contains("rollout_permille")) {
|
||||
const int p = j["rollout_permille"].get<int>();
|
||||
m.rolloutPermille = std::max(0, std::min(1000, p));
|
||||
}
|
||||
if (j.contains("message")) m.message = j["message"].get<std::string>();
|
||||
m.valid = true;
|
||||
} catch (...) {
|
||||
m.valid = false; // parse error -> fail-open
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace wallet
|
||||
} // namespace dragonx
|
||||
85
src/wallet/lite_rollout_policy.h
Normal file
85
src/wallet/lite_rollout_policy.h
Normal file
@@ -0,0 +1,85 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Lite-wallet rollout / kill-switch policy.
|
||||
//
|
||||
// A pure decision core that gates whether the lite wallet may run, given a local override, an
|
||||
// emergency env kill-switch, an optional rollout manifest, the running app version, and a stable
|
||||
// per-install rollout bucket. Privacy posture (this is a privacy coin): the manifest is read from a
|
||||
// LOCAL cache file only — there is NO runtime network fetch (a signed remote fetcher can populate
|
||||
// that cache later without touching this policy), it is consulted FAIL-OPEN (absent/invalid →
|
||||
// allowed, so a missing file never bricks a working install), and the install bucket is derived
|
||||
// from a locally-generated random id that is never transmitted and carries no PII.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx::wallet {
|
||||
|
||||
// User/support local override (persisted in settings as "lite_rollout").
|
||||
enum class LiteRolloutOverride {
|
||||
Auto, // honor the manifest / staged rollout
|
||||
ForceOn, // always enable; bypasses the manifest + staged rollout, but NOT the env kill-switch
|
||||
ForceOff // always disable
|
||||
};
|
||||
|
||||
enum class LiteRolloutStatus {
|
||||
Allowed,
|
||||
DisabledByKillSwitchEnv, // DRAGONX_LITE_KILL_SWITCH=1 (emergency, absolute)
|
||||
DisabledByLocalOverride, // lite_rollout=force_off
|
||||
DisabledByManifest, // manifest global_enabled=false
|
||||
DisabledUnsupportedVersion, // version below min / above max supported
|
||||
DisabledBlockedVersion, // version explicitly blocked
|
||||
DisabledByStagedRollout // install bucket above the rollout threshold
|
||||
};
|
||||
|
||||
// Rollout manifest. Loaded from a local cache file; present=false means no manifest (fail-open).
|
||||
struct LiteRolloutManifest {
|
||||
bool present = false; // a manifest file was found
|
||||
bool valid = false; // it parsed and validated
|
||||
bool globalEnabled = true; // master enable switch
|
||||
std::string minVersion; // inclusive floor (empty = none)
|
||||
std::string maxVersion; // inclusive ceiling (empty = none)
|
||||
std::vector<std::string> blockedVersions;
|
||||
int rolloutPermille = 1000; // 0..1000; install allowed when bucket < permille (1000 = 100%)
|
||||
std::string message; // user-facing reason shown when disabled
|
||||
};
|
||||
|
||||
struct LiteRolloutInputs {
|
||||
LiteRolloutOverride override = LiteRolloutOverride::Auto;
|
||||
bool killSwitchEnv = false;
|
||||
LiteRolloutManifest manifest;
|
||||
std::string appVersion;
|
||||
int installBucket = 0; // 0..999, stable per install
|
||||
};
|
||||
|
||||
struct LiteRolloutDecision {
|
||||
bool allowed = true;
|
||||
LiteRolloutStatus status = LiteRolloutStatus::Allowed;
|
||||
std::string message; // user-facing (empty when allowed)
|
||||
};
|
||||
|
||||
// The pure decision. No I/O. Evaluation order: env kill-switch (absolute) → local override →
|
||||
// manifest gates (global / version / blocked / staged rollout) → fail-open allow.
|
||||
LiteRolloutDecision evaluateLiteRollout(const LiteRolloutInputs& inputs);
|
||||
|
||||
// Stable bucket 0..999 from an install id (FNV-1a). Deterministic; empty id -> 0.
|
||||
int liteRolloutBucketFromInstallId(const std::string& installId);
|
||||
|
||||
// Compare dotted version cores, ignoring any "-suffix"/"+build". Returns -1 / 0 / 1.
|
||||
int compareLiteVersions(const std::string& a, const std::string& b);
|
||||
|
||||
// Override <-> string ("auto"/"force_on"/"force_off"); unknown parses to Auto.
|
||||
LiteRolloutOverride liteRolloutOverrideFromString(const std::string& value);
|
||||
const char* liteRolloutOverrideToString(LiteRolloutOverride override);
|
||||
|
||||
const char* liteRolloutStatusName(LiteRolloutStatus status);
|
||||
|
||||
// Load + validate a manifest from a JSON file. Missing file -> {present=false}; parse error ->
|
||||
// {present=true, valid=false}. Both are fail-open at evaluation time.
|
||||
LiteRolloutManifest loadLiteRolloutManifestFromFile(const std::string& path);
|
||||
|
||||
} // namespace dragonx::wallet
|
||||
@@ -196,7 +196,8 @@ LiteWalletController::LiteWalletController(WalletCapabilities capabilities,
|
||||
: bridge_(std::make_shared<LiteClientBridge>(std::move(bridge))),
|
||||
chainName_(connectionSettings.chainName),
|
||||
lifecycle_(capabilities, connectionSettings, bridge_.get(),
|
||||
LiteWalletLifecycleOptions{options.allowBridgeCalls}),
|
||||
LiteWalletLifecycleOptions{options.allowBridgeCalls,
|
||||
options.rolloutBlocked, options.rolloutMessage}),
|
||||
gateway_(capabilities, connectionSettings, bridge_.get(),
|
||||
LiteWalletGatewayOptions{options.allowBridgeCalls}),
|
||||
sync_(capabilities, connectionSettings, bridge_.get(),
|
||||
@@ -226,13 +227,14 @@ LiteWalletController::~LiteWalletController()
|
||||
|
||||
std::unique_ptr<LiteWalletController> LiteWalletController::createLinked(
|
||||
WalletCapabilities capabilities,
|
||||
LiteConnectionSettings connectionSettings)
|
||||
LiteConnectionSettings connectionSettings,
|
||||
LiteRolloutDecision rollout)
|
||||
{
|
||||
return std::make_unique<LiteWalletController>(
|
||||
capabilities,
|
||||
std::move(connectionSettings),
|
||||
LiteClientBridge::linkedSdxl(),
|
||||
LiteWalletControllerOptions{true});
|
||||
LiteWalletControllerOptions{true, !rollout.allowed, rollout.message});
|
||||
}
|
||||
|
||||
void LiteWalletController::onLifecycleResult(const LiteWalletLifecycleResult& result)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "lite_connection_service.h"
|
||||
#include "lite_wallet_lifecycle_service.h"
|
||||
#include "lite_wallet_gateway.h"
|
||||
#include "lite_rollout_policy.h"
|
||||
#include "lite_sync_service.h"
|
||||
#include "lite_wallet_state_mapper.h"
|
||||
#include "wallet_backend.h"
|
||||
@@ -53,6 +54,8 @@ void applyLiteRefreshModelToWalletState(const LiteWalletAppRefreshModel& model,
|
||||
|
||||
struct LiteWalletControllerOptions {
|
||||
bool allowBridgeCalls = true;
|
||||
bool rolloutBlocked = false; // runtime kill-switch / staged-rollout gate is blocking
|
||||
std::string rolloutMessage; // user-facing reason when rolloutBlocked
|
||||
};
|
||||
|
||||
struct LiteNewAddressResult {
|
||||
@@ -133,10 +136,13 @@ public:
|
||||
LiteWalletController(const LiteWalletController&) = delete;
|
||||
LiteWalletController& operator=(const LiteWalletController&) = delete;
|
||||
|
||||
// Production factory: links the SDXL backend compiled into this build.
|
||||
// Production factory: links the SDXL backend compiled into this build. The optional rollout
|
||||
// decision (runtime kill-switch / staged rollout) gates lifecycle execution: when not allowed,
|
||||
// availability() reports RolloutDisabled and create/open/restore are blocked with its message.
|
||||
static std::unique_ptr<LiteWalletController> createLinked(
|
||||
WalletCapabilities capabilities,
|
||||
LiteConnectionSettings connectionSettings);
|
||||
LiteConnectionSettings connectionSettings,
|
||||
LiteRolloutDecision rollout = LiteRolloutDecision{});
|
||||
|
||||
// Invoked after a wallet becomes ready, so the owner can persist settings.
|
||||
void setPersistCallback(std::function<void()> callback) { persist_ = std::move(callback); }
|
||||
|
||||
@@ -91,6 +91,8 @@ const char* liteWalletLifecycleAvailabilityName(LiteWalletLifecycleAvailability
|
||||
return "BridgeCallsDisabled";
|
||||
case LiteWalletLifecycleAvailability::NoUsableServer:
|
||||
return "NoUsableServer";
|
||||
case LiteWalletLifecycleAvailability::RolloutDisabled:
|
||||
return "RolloutDisabled";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
@@ -131,6 +133,9 @@ LiteWalletLifecycleAvailability LiteWalletLifecycleService::availability() const
|
||||
if (!isLiteBuild(capabilities_)) return LiteWalletLifecycleAvailability::UnsupportedBuild;
|
||||
if (!supportsLiteBackend(capabilities_)) return LiteWalletLifecycleAvailability::BackendUnavailable;
|
||||
if (!bridge_ || !bridge_->available()) return LiteWalletLifecycleAvailability::BridgeUnavailable;
|
||||
// Runtime kill-switch / staged-rollout gate: a structural readiness check, applied before
|
||||
// server selection so a gated-off wallet reports the rollout reason rather than a server error.
|
||||
if (options_.rolloutBlocked) return LiteWalletLifecycleAvailability::RolloutDisabled;
|
||||
if (!selectLiteServer(connectionSettings_).ok) return LiteWalletLifecycleAvailability::NoUsableServer;
|
||||
if (!options_.allowBridgeCalls) return LiteWalletLifecycleAvailability::BridgeCallsDisabled;
|
||||
return LiteWalletLifecycleAvailability::Ready;
|
||||
@@ -142,6 +147,9 @@ WalletBackendStatus LiteWalletLifecycleService::status() const
|
||||
if (currentAvailability == LiteWalletLifecycleAvailability::NoUsableServer) {
|
||||
return statusFor(currentAvailability, selectLiteServer(connectionSettings_).error);
|
||||
}
|
||||
if (currentAvailability == LiteWalletLifecycleAvailability::RolloutDisabled) {
|
||||
return statusFor(currentAvailability, options_.rolloutMessage);
|
||||
}
|
||||
return statusFor(currentAvailability);
|
||||
}
|
||||
|
||||
@@ -310,6 +318,14 @@ WalletBackendStatus LiteWalletLifecycleService::statusFor(
|
||||
{},
|
||||
0.0
|
||||
};
|
||||
case LiteWalletLifecycleAvailability::RolloutDisabled:
|
||||
return WalletBackendStatus{
|
||||
WalletBackendState::Unavailable,
|
||||
detail.empty() ? "the lite wallet is disabled by the rollout policy" : detail,
|
||||
{},
|
||||
{},
|
||||
0.0
|
||||
};
|
||||
}
|
||||
|
||||
return WalletBackendStatus{WalletBackendState::Unavailable, "unknown lite wallet lifecycle state", {}, {}, 0.0};
|
||||
|
||||
@@ -28,7 +28,8 @@ enum class LiteWalletLifecycleAvailability {
|
||||
BackendUnavailable,
|
||||
BridgeUnavailable,
|
||||
BridgeCallsDisabled,
|
||||
NoUsableServer
|
||||
NoUsableServer,
|
||||
RolloutDisabled // runtime kill-switch / staged-rollout gate (see lite_rollout_policy.h)
|
||||
};
|
||||
|
||||
enum class LitePrivateDataKind {
|
||||
@@ -93,6 +94,8 @@ struct LiteWalletLifecycleResult {
|
||||
|
||||
struct LiteWalletLifecycleOptions {
|
||||
bool allowBridgeCalls = false;
|
||||
bool rolloutBlocked = false; // runtime kill-switch / staged-rollout gate is blocking
|
||||
std::string rolloutMessage; // user-facing reason when rolloutBlocked
|
||||
};
|
||||
|
||||
const char* liteWalletLifecycleOperationName(LiteWalletLifecycleOperation operation);
|
||||
|
||||
Reference in New Issue
Block a user