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:
@@ -320,15 +320,19 @@ std::vector<std::string> 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
|
||||
|
||||
Reference in New Issue
Block a user