feat(lite): async + failover for Settings-page create/open/restore
The Settings page drove the controller's synchronous createWallet/openWallet/
restoreWallet, which blocks the UI thread on the (often flaky) lightwalletd and
gives up after the first server. Add a generic async lifecycle path that mirrors
the async-open failover but carries the full request (passphrase, restore seed/
birthday/account/overwrite):
- beginCreateWalletAsync / beginOpenWalletAsync / beginRestoreWalletAsync run
on a detached thread that builds its OWN local LiteWalletLifecycleService
from captured value copies + the shared bridge (never `this`, so it can
safely outlive the controller). Each request type's serverUrl override field
feeds the failover: try the preferred server, then every other usable
default; stop on the first ready wallet or a structural block; keep the
preferred server's error on total failure. The request's secrets are wiped
once the attempt finishes.
- pumpLifecycleResult() finalizes on the main thread (flip walletOpen, persist,
start sync) and caches the result for the UI; wired into App::update next to
pumpAsyncOpen(). beginAsyncLifecycle() now also yields to an in-flight
lifecycle request so the auto-open loop can't race it on the same bridge.
- settings_page kicks off the async op, disables the button while in flight,
and polls the cached result each frame for the status/summary.
Tests: testLiteWalletControllerAsyncLifecycleFailover covers async create (with
passphrase) and restore failing over preferred->fallback, plus all-servers-down.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,9 @@ struct SettingsPageState {
|
||||
bool lite_restore_overwrite = false;
|
||||
std::string lite_lifecycle_status;
|
||||
std::string lite_lifecycle_summary;
|
||||
// True while an async create/open/restore is in flight (Settings drives the controller's
|
||||
// async lifecycle path so a flaky server never freezes the UI; the result is polled each frame).
|
||||
bool lite_lifecycle_pending = false;
|
||||
// Backup & keys (only populated for an open lite wallet). lite_export_secret holds the
|
||||
// revealed seed/private-keys backup and is SECRET: securely wiped on hide / new export /
|
||||
// import. lite_import_key is the import input buffer (wiped right after submission).
|
||||
@@ -231,30 +234,34 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) {
|
||||
}
|
||||
}
|
||||
|
||||
// When a linked lite backend is present, execute the operation for real through the
|
||||
// App-owned controller. Otherwise fall back to the validation-only adapter.
|
||||
// When a linked lite backend is present, execute the operation for real through the App-owned
|
||||
// controller — ASYNCHRONOUSLY, with server failover, so an unreachable/flaky server never
|
||||
// freezes the UI. The request (with its secrets) is moved into the controller; the page polls
|
||||
// the outcome each frame in renderLiteLifecyclePending() below. (Secret wiping of the page char
|
||||
// buffers + the moved-from request copies is handled by liteSecretScrubber at function exit.)
|
||||
if (auto* lite = app->liteWallet()) {
|
||||
wallet::LiteWalletLifecycleResult result;
|
||||
bool started = false;
|
||||
switch (input.request.operation) {
|
||||
case wallet::LiteWalletLifecycleOperation::CreateNew:
|
||||
result = lite->createWallet(input.request.createRequest);
|
||||
started = lite->beginCreateWalletAsync(std::move(input.request.createRequest));
|
||||
break;
|
||||
case wallet::LiteWalletLifecycleOperation::OpenExisting:
|
||||
result = lite->openWallet(input.request.openRequest);
|
||||
started = lite->beginOpenWalletAsync(std::move(input.request.openRequest));
|
||||
break;
|
||||
case wallet::LiteWalletLifecycleOperation::RestoreFromSeed:
|
||||
result = lite->restoreWallet(input.request.restoreRequest);
|
||||
started = lite->beginRestoreWalletAsync(std::move(input.request.restoreRequest));
|
||||
break;
|
||||
}
|
||||
|
||||
// (Secret wiping is handled unconditionally by liteSecretScrubber at function exit.)
|
||||
s_settingsState.lite_lifecycle_summary = result.bridgeResponseRedacted;
|
||||
if (result.walletReady) {
|
||||
s_settingsState.lite_lifecycle_status = "Wallet ready";
|
||||
if (started) {
|
||||
s_settingsState.lite_lifecycle_pending = true;
|
||||
s_settingsState.lite_lifecycle_status = "Working…";
|
||||
s_settingsState.lite_lifecycle_summary.clear();
|
||||
} else {
|
||||
s_settingsState.lite_lifecycle_status = result.error.empty()
|
||||
? result.status.message
|
||||
: result.error;
|
||||
// Rejected before any thread launched (wallet already open, an attempt in flight, or no
|
||||
// usable server). The controller's status carries the reason.
|
||||
s_settingsState.lite_lifecycle_status = lite->status().message.empty()
|
||||
? "Could not start the operation"
|
||||
: lite->status().message;
|
||||
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
|
||||
}
|
||||
return;
|
||||
@@ -1600,10 +1607,36 @@ void RenderSettingsPage(App* app) {
|
||||
sizeof(s_settingsState.lite_lifecycle_passphrase),
|
||||
ImGuiInputTextFlags_Password);
|
||||
|
||||
// Poll a completed async create/open/restore (driven by the App-owned
|
||||
// controller; finalized on the main thread by App::update's
|
||||
// pumpLifecycleResult()). While in flight the button is disabled.
|
||||
if (s_settingsState.lite_lifecycle_pending) {
|
||||
if (auto* lite = app->liteWallet()) {
|
||||
if (!lite->lifecycleRequestInProgress()) {
|
||||
s_settingsState.lite_lifecycle_pending = false;
|
||||
const auto& result = lite->lastLifecycleResult();
|
||||
s_settingsState.lite_lifecycle_summary = result.bridgeResponseRedacted;
|
||||
if (result.walletReady) {
|
||||
s_settingsState.lite_lifecycle_status = "Wallet ready";
|
||||
} else {
|
||||
s_settingsState.lite_lifecycle_status = result.error.empty()
|
||||
? result.status.message
|
||||
: result.error;
|
||||
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s_settingsState.lite_lifecycle_pending = false; // backend went away
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||
const bool liteLifecycleBusy = s_settingsState.lite_lifecycle_pending;
|
||||
if (liteLifecycleBusy) ImGui::BeginDisabled();
|
||||
if (TactileButton("Validate##LiteLifecycleValidate", ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
evaluateLiteLifecycleRequestFromPageState(app);
|
||||
}
|
||||
if (liteLifecycleBusy) ImGui::EndDisabled();
|
||||
|
||||
if (!s_settingsState.lite_lifecycle_status.empty()) {
|
||||
ImGui::SameLine(0, Layout::spacingSm());
|
||||
|
||||
Reference in New Issue
Block a user