Files
ObsidianDragon/src/wallet/lite_client_bridge.h
T
DanSandClaude Opus 4.8 ef14518089 perf(litebe): change-gate the idle lite refresh worker
The background 2 s refresh worker re-queried and re-copied the whole
wallet every cycle (a fresh TLS refresh plus a multi-megabyte JSON
build, copies and DOM parse), even when nothing had changed, so CPU and
the network never went idle.

The worker now skips the full refresh and the re-publish on a cycle that
is provably unchanged, while never letting the UI show stale confirmed
balance or transactions:

- The lite bridge keeps a monotonic mutation epoch, bumped after any
  command not on a read-only allow-list, so a local change made on any
  thread (send, shield, import, new address, encrypt/unlock/lock,
  console) is detected at one choke point.
- Each cycle the worker runs one cheap sync probe (one connection, the
  mandatory block ingest) and a local height read, and refreshes only
  when the scanned height advanced, the mutation epoch moved, the
  encryption state changed, a sync is in progress, or a ~10 s heartbeat
  elapsed; otherwise it skips. Direct/synchronous callers are not gated.
- A failed probe backs off instead of running a second sync, so a dead
  server is not retried back-to-back.

Confirmed funds can never be stale beyond one cycle (every confirmed
change advances the height or bumps the epoch); only unconfirmed mempool
rows can lag up to the heartbeat, and the spend path re-queries the
backend directly rather than reading the gated model. Average idle cost
drops from three data connections per cycle to about one.

Tests: a bridge mutation-epoch test and a worker change-gate test
(skip-when-unchanged; refresh on height advance, local mutation and
encryption change) against the fake backend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-14 12:00:26 -05:00

95 lines
3.9 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <atomic>
#include <cstdint>
namespace dragonx {
namespace wallet {
struct LiteBridgeStringResult {
bool ok = false;
std::string value;
std::string error;
};
struct LiteClientBridgeApi {
using WalletExistsFn = bool (*)(const char* chainName);
using InitializeNewFn = char* (*)(bool dangerous, const char* server);
using InitializeNewFromPhraseFn = char* (*)(bool dangerous,
const char* server,
const char* seed,
unsigned long long birthday,
unsigned long long account,
bool overwrite);
using InitializeExistingFn = char* (*)(bool dangerous, const char* server);
using ExecuteFn = char* (*)(const char* command, const char* args);
using FreeStringFn = void (*)(char* value);
using CheckServerOnlineFn = bool (*)(const char* server);
using ShutdownFn = void (*)();
WalletExistsFn walletExists = nullptr;
InitializeNewFn initializeNew = nullptr;
InitializeNewFromPhraseFn initializeNewFromPhrase = nullptr;
InitializeExistingFn initializeExisting = nullptr;
ExecuteFn execute = nullptr;
FreeStringFn freeString = nullptr;
CheckServerOnlineFn checkServerOnline = nullptr;
ShutdownFn shutdown = nullptr;
};
class LiteClientBridge {
public:
static LiteClientBridge unavailable(std::string reason);
static LiteClientBridge fromApi(LiteClientBridgeApi api);
static LiteClientBridge linkedSdxl();
LiteClientBridge(const LiteClientBridge&) = delete;
LiteClientBridge& operator=(const LiteClientBridge&) = delete;
LiteClientBridge(LiteClientBridge&& other) noexcept;
LiteClientBridge& operator=(LiteClientBridge&& other) noexcept;
~LiteClientBridge();
bool available() const;
const std::string& unavailableReason() const { return unavailableReason_; }
bool walletExists(const std::string& chainName) const;
bool checkServerOnline(const std::string& server) const;
LiteBridgeStringResult initializeNew(bool dangerous, const std::string& server);
LiteBridgeStringResult initializeNewFromPhrase(bool dangerous,
const std::string& server,
const std::string& seed,
unsigned long long birthday,
unsigned long long account,
bool overwrite);
LiteBridgeStringResult initializeExisting(bool dangerous, const std::string& server);
LiteBridgeStringResult execute(const std::string& command, const std::string& args);
void shutdown();
// Monotonic counter bumped AFTER every execute() of a command that could change wallet-visible
// data (anything not on the read-only allow-list). The refresh worker samples this to detect
// local mutations made on ANY thread without hooking each controller method. Compare-only (never
// cleared): the worker stores a baseline and refreshes whenever it differs, so a mutation can
// never be "consumed"/lost in a race.
std::uint64_t mutationEpoch() const { return mutationEpoch_.load(std::memory_order_acquire); }
private:
LiteClientBridge(LiteClientBridgeApi api, std::string unavailableReason);
LiteBridgeStringResult unavailableResult() const;
LiteBridgeStringResult takeOwnedString(char* rawValue) const;
static bool commandIsReadOnly(const std::string& command);
LiteClientBridgeApi api_;
std::string unavailableReason_;
bool shutdownCalled_ = false;
std::atomic<std::uint64_t> mutationEpoch_{0};
};
} // namespace wallet
} // namespace dragonx