diff --git a/src/app.cpp b/src/app.cpp index d10db7e..0c2e32f 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1935,10 +1935,12 @@ void App::renderLiteFirstRunPrompt() static std::vector> 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 ───────────────────── diff --git a/src/wallet/lite_wallet_controller.cpp b/src/wallet/lite_wallet_controller.cpp index f154ffe..bde44af 100644 --- a/src/wallet/lite_wallet_controller.cpp +++ b/src/wallet/lite_wallet_controller.cpp @@ -320,15 +320,19 @@ std::vector LiteWalletController::failoverServerUrls() const return urls; } -bool LiteWalletController::beginOpenExisting() +bool LiteWalletController::beginOpenExisting() { return beginAsyncLifecycle(/*create=*/false); } +bool LiteWalletController::beginCreateWallet() { return beginAsyncLifecycle(/*create=*/true); } + +bool LiteWalletController::beginAsyncLifecycle(bool create) { if (walletOpen_.load() || openRunning_->load()) return false; + const char* verb = create ? "Create" : "Open"; if (lifecycle_.availability() != LiteWalletLifecycleAvailability::Ready) { const std::string& reason = lifecycle_.status().message; status_ = WalletBackendStatus{WalletBackendState::Error, reason.empty() ? "lite wallet is not available" : reason, {}, {}, 0.0}; lastOpenError_ = status_.message; - liteLog("Open blocked: " + lastOpenError_); + liteLog(std::string(verb) + " blocked: " + lastOpenError_); return false; } auto servers = failoverServerUrls(); @@ -336,7 +340,7 @@ bool LiteWalletController::beginOpenExisting() status_ = WalletBackendStatus{WalletBackendState::Error, "no usable lite servers are configured", {}, {}, 0.0}; lastOpenError_ = status_.message; - liteLog("Open blocked: " + lastOpenError_); + liteLog(std::string(verb) + " blocked: " + lastOpenError_); return false; } @@ -346,24 +350,31 @@ bool LiteWalletController::beginOpenExisting() openResult_->reset(); } openRunning_->store(true); - status_ = WalletBackendStatus{WalletBackendState::Connecting, "opening wallet", {}, {}, 0.0}; - liteLog("Opening wallet — trying " + std::to_string(servers.size()) + " server(s)"); + status_ = WalletBackendStatus{WalletBackendState::Connecting, + create ? "creating wallet" : "opening wallet", {}, {}, 0.0}; + liteLog(std::string(create ? "Creating wallet — trying " : "Opening wallet — trying ") + + std::to_string(servers.size()) + " server(s)"); // Capture only shared refs + value copies (never `this`) so the thread can safely outlive us. auto bridge = bridge_; auto running = openRunning_; auto resultMutex = openResultMutex_; auto resultSlot = openResult_; - openThread_ = std::thread([bridge, servers, running, resultMutex, resultSlot]() { + openThread_ = std::thread([bridge, servers, running, resultMutex, resultSlot, create]() { OpenOutcome outcome; std::string preferredError; // error from the FIRST (preferred) server — the actionable one for (const auto& url : servers) { if (!bridge) break; liteLog(" connecting to " + url + " ..."); - // initialize_existing loads the wallet file but contacts the server to start the - // light client; ok && non-empty value == ready (mirrors the lifecycle's success test). - const auto call = bridge->initializeExisting(/*dangerous=*/false, url); - if (call.ok && !call.value.empty()) { + // create: initialize_new generates a NEW seed+wallet, but contacts the server FIRST, + // so an unreachable server fails before writing any file — safe to try the next. + // open: initialize_existing just loads the file. ok && non-empty value == ready. + auto call = create ? bridge->initializeNew(/*dangerous=*/false, url) + : bridge->initializeExisting(/*dangerous=*/false, url); + const bool ready = call.ok && !call.value.empty(); + // create's response IS the secret seed — wipe our copy; it's read back via exportSeed(). + if (create) secureWipeLiteSecret(call.value); + if (ready) { liteLog(" " + url + ": connected"); outcome.ok = true; outcome.serverUrl = url; @@ -405,7 +416,7 @@ void LiteWalletController::pumpAsyncOpen() lastOpenError_.clear(); lastOpenWarming_ = false; status_ = WalletBackendStatus{WalletBackendState::Ready, "wallet open", {}, {}, 0.0}; - liteLog("Wallet opened via " + outcome.serverUrl); + liteLog("Wallet ready via " + outcome.serverUrl); if (persist_) persist_(); startSync(); // begin background sync on the backend startWorker(); // begin periodic refresh -> WalletState diff --git a/src/wallet/lite_wallet_controller.h b/src/wallet/lite_wallet_controller.h index 7ed8c9a..21dff30 100644 --- a/src/wallet/lite_wallet_controller.h +++ b/src/wallet/lite_wallet_controller.h @@ -168,8 +168,14 @@ public: // already open, an open is already in progress, no wallet exists, or the rollout gate blocks // lite. On all-servers-fail, status()/lastOpenError() report the reason. bool beginOpenExisting(); - // Finalize a completed async open on the calling (main) thread: flips walletOpen()/status and - // starts sync, or records the failure. Cheap no-op when nothing is pending. Call each tick. + // Asynchronously CREATE a new wallet (new seed) with the same server failover as open: + // initialize_new contacts the server before writing any file, so an unreachable server fails + // cleanly and the next is tried. On success the seed lives in the wallet — read it back with + // exportSeed(). Non-blocking; finalized by pumpAsyncOpen(). Returns false if a wallet is open, + // an open/create is in progress, the rollout gate blocks lite, or no usable server exists. + bool beginCreateWallet(); + // Finalize a completed async open/create on the calling (main) thread: flips walletOpen()/status + // and starts sync, or records the failure. Cheap no-op when nothing is pending. Call each tick. void pumpAsyncOpen(); bool openInProgress() const { return openRunning_ && openRunning_->load(); } const std::string& lastOpenError() const { return lastOpenError_; } @@ -294,6 +300,8 @@ private: // the detached thread captures only shared_ptrs + value copies, never `this`, so it can // safely outlive the controller). pumpAsyncOpen() finalizes the result on the main thread. std::vector failoverServerUrls() const; + // Shared async open/create-with-failover core (create=true uses initialize_new). + bool beginAsyncLifecycle(bool create); struct OpenOutcome { bool ok = false; bool warming = false; std::string serverUrl; std::string error; }; std::thread openThread_; std::shared_ptr> openRunning_ = std::make_shared>(false); diff --git a/tests/fake_lite_backend.h b/tests/fake_lite_backend.h index f496ae9..d893aea 100644 --- a/tests/fake_lite_backend.h +++ b/tests/fake_lite_backend.h @@ -64,8 +64,20 @@ inline bool liteFakeWalletExists(const char*) { return g_liteFakeWalletExists; } // Match the real litelib_* return shapes so tests exercise the production walletReady path: // create/restore return a seed object ({"seed":..,"birthday":..}); open returns the bare // string "OK" (NOT JSON) — see litelib_initialize_existing. -inline char* liteFakeInitNew(bool, const char*) +inline char* liteFakeInitNew(bool, const char* server) { + // Honor the dead/warmup substrings so create-with-failover can be exercised (mirrors how the + // real initialize_new contacts the server first and fails before writing a wallet). + if (server) { + const std::string s(server); + if (!g_liteFakeWarmupServerSubstr.empty() && + s.find(g_liteFakeWarmupServerSubstr) != std::string::npos) + return liteFakeDup("Error: grpc-message: \"error requesting block: -28: " + "Activating best chain...\""); + if (!g_liteFakeDeadServerSubstr.empty() && + s.find(g_liteFakeDeadServerSubstr) != std::string::npos) + return liteFakeDup("Error: could not connect to server"); + } return liteFakeDup("{\"seed\":\"fake seed phrase words\",\"birthday\":0}"); } inline char* liteFakeInitFromPhrase(bool, const char*, const char*, diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index b708e25..223b8b8 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -3578,7 +3578,7 @@ void testLiteWalletControllerOpenFailover() bool sawConnecting = false, sawOpened = false; for (const auto& l : log) { if (l.find("connecting to") != std::string::npos) sawConnecting = true; - if (l.find("Wallet opened via") != std::string::npos) sawOpened = true; + if (l.find("Wallet ready via") != std::string::npos) sawOpened = true; } EXPECT_TRUE(sawConnecting); EXPECT_TRUE(sawOpened); @@ -3616,6 +3616,23 @@ void testLiteWalletControllerOpenFailover() EXPECT_TRUE(controller.lastOpenWasWarmup()); } + // Create also fails over: preferred dead, fallback good -> a new wallet is created via the + // fallback (no existing wallet beforehand). + { + dragonx::test::resetLiteFakeCounters(); + dragonx::test::g_liteFakeWalletExists = false; // creating fresh + dragonx::test::g_liteFakeDeadServerSubstr = "dead.example"; + LiteWalletController controller(liteCaps, conn, + LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi())); + EXPECT_FALSE(controller.walletOpen()); + EXPECT_TRUE(controller.beginCreateWallet()); + for (int i = 0; i < 400 && controller.openInProgress(); ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + controller.pumpAsyncOpen(); + EXPECT_TRUE(controller.walletOpen()); + EXPECT_TRUE(controller.lastOpenError().empty()); + } + dragonx::test::g_liteFakeWalletExists = false; dragonx::test::g_liteFakeDeadServerSubstr.clear(); dragonx::test::g_liteFakeWarmupServerSubstr.clear();