feat(lite): async wallet creation with server failover

Mirror the async-open path for wallet creation. beginOpenExisting() and
beginCreateWallet() now both delegate to beginAsyncLifecycle(bool create),
which runs the backend init on a detached thread and walks the failover
server list (preferred server first, then all usable defaults), reporting
the preferred server's error on total failure. The first-run wizard's
Create button drives this through a non-blocking "creating" poll state so
the UI no longer freezes while the backend contacts a (possibly flaky)
lightwalletd. The created seed response is securely wiped immediately and
read back via exportSeed for the reveal/verify steps.

Safe because litelib_initialize_new contacts the server before writing any
wallet file and LightClient::new errors if a wallet already exists, so a
failed candidate leaves no partial state.

Tests: fake backend's initialize_new now honors the dead/warmup server
substrings; testLiteWalletControllerOpenFailover gains a create-failover
case (preferred dead, fallback good -> walletOpen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 11:29:59 -05:00
parent 6ff1fda870
commit 320c659689
5 changed files with 102 additions and 37 deletions

View File

@@ -1935,10 +1935,12 @@ void App::renderLiteFirstRunPrompt()
static std::vector<std::pair<std::string, bool>> chips; // shuffled (word, consumed)
static int progress = 0; // # words confirmed in order
static double wrongFlashUntil = 0.0; // brief "not the next word" hint
static bool creating = false; // async create (with failover) in flight
// The welcome page is only relevant before any wallet exists; once create has started the
// reveal/verify (step>0), keep showing it through to completion even though the wallet is open.
if (step == 0 && (lite_wallet_->walletOpen() || lite_wallet_->walletExists())) return;
// The welcome page is only relevant before any wallet exists; once create has started
// (creating) or reached reveal/verify (step>0), keep showing it through to completion even
// though the wallet becomes open.
if (step == 0 && !creating && (lite_wallet_->walletOpen() || lite_wallet_->walletExists())) return;
if (!ImGui::IsPopupOpen("##LiteFirstRun")) ImGui::OpenPopup("##LiteFirstRun");
ImGuiViewport* vp = ImGui::GetMainViewport();
@@ -1951,6 +1953,7 @@ void App::renderLiteFirstRunPrompt()
chips.clear();
progress = 0;
step = 0;
creating = false;
lite_firstrun_dismissed_ = true;
ImGui::CloseCurrentPopup();
};
@@ -1980,37 +1983,51 @@ void App::renderLiteFirstRunPrompt()
ImGui::PopTextWrapPos();
ImGui::Spacing(); ImGui::Spacing();
if (ImGui::Button(TR("lite_welcome_create"), ImVec2(btnW, 0))) {
const auto result = lite_wallet_->createWallet(wallet::LiteWalletCreateRequest{});
if (result.walletReady) {
// Read the freshly generated seed back so we can show + verify it.
auto s = lite_wallet_->exportSeed();
if (creating) {
// Async create (with server failover) is in flight — driven to completion by
// App::update()'s pumpAsyncOpen(). Poll the controller for the outcome.
ImGui::TextUnformatted("Creating your wallet\xE2\x80\xA6");
if (lite_wallet_->walletOpen()) {
auto s = lite_wallet_->exportSeed(); // read the new seed back (local, fast)
if (s.ok && !s.seedPhrase.empty()) {
seed = s.seedPhrase;
birthday = s.birthday;
wallet::secureWipeLiteSecret(s.seedPhrase);
words = splitWords(seed);
creating = false;
step = 1;
} else {
// Created, but couldn't reveal the seed now — fall back to Settings backup.
ui::Notifications::instance().success(TR("lite_welcome_created"), 8.0f);
setCurrentPage(ui::NavPage::Settings);
finish();
}
} else {
ui::Notifications::instance().warning(TR("lite_welcome_create_failed"));
} else if (!lite_wallet_->openInProgress() &&
!lite_wallet_->lastOpenError().empty()) {
ui::Notifications::instance().warning(
std::string("Create failed: ") + lite_wallet_->lastOpenError());
creating = false; // back to the buttons so the user can retry
}
} else {
if (ImGui::Button(TR("lite_welcome_create"), ImVec2(btnW, 0))) {
// Async create with the same server failover as open (no UI freeze; a dead
// server falls through to the next). On success the wizard reveals the seed.
if (lite_wallet_->beginCreateWallet()) {
creating = true;
} else {
ui::Notifications::instance().warning(TR("lite_welcome_create_failed"));
}
}
ImGui::SameLine();
if (ImGui::Button(TR("lite_welcome_restore"), ImVec2(btnW, 0))) {
ui::Notifications::instance().info(TR("lite_welcome_restore_hint"), 8.0f);
setCurrentPage(ui::NavPage::Settings);
finish();
}
ImGui::Spacing();
if (ImGui::Button(TR("lite_welcome_later"),
ImVec2(btnW * 2 + ImGui::GetStyle().ItemSpacing.x, 0))) {
finish();
}
}
ImGui::SameLine();
if (ImGui::Button(TR("lite_welcome_restore"), ImVec2(btnW, 0))) {
ui::Notifications::instance().info(TR("lite_welcome_restore_hint"), 8.0f);
setCurrentPage(ui::NavPage::Settings);
finish();
}
ImGui::Spacing();
if (ImGui::Button(TR("lite_welcome_later"),
ImVec2(btnW * 2 + ImGui::GetStyle().ItemSpacing.x, 0))) {
finish();
}
} else if (step == 1) {
// ── Reveal the seed + birthday with backup warnings ─────────────────────