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:
2026-06-08 11:42:47 -05:00
parent 320c659689
commit 6f9123f651
5 changed files with 315 additions and 15 deletions

View File

@@ -3638,6 +3638,81 @@ void testLiteWalletControllerOpenFailover()
dragonx::test::g_liteFakeWarmupServerSubstr.clear();
}
// Async FULL lifecycle (Settings-page create/open/restore WITH passphrase/restore params) also
// fails over: the request runs off the UI thread against the preferred server, then the other
// usable defaults, finalized by pumpLifecycleResult() on the main thread.
void testLiteWalletControllerAsyncLifecycleFailover()
{
using namespace dragonx::wallet;
const auto liteCaps = makeWalletCapabilities(WalletBuildKind::Lite, false, true);
LiteConnectionSettings conn;
conn.chainName = "main";
conn.servers = {
LiteServerEndpoint{"https://dead.example", "Dead", true},
LiteServerEndpoint{"https://good.example", "Good", true},
};
conn.selectionMode = LiteServerSelectionMode::Sticky;
conn.stickyServerUrl = "https://dead.example"; // preferred server is the dead one
const auto drain = [](LiteWalletController& c) {
for (int i = 0; i < 400 && c.lifecycleRequestInProgress(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(5));
c.pumpLifecycleResult();
};
// Async create with a passphrase: preferred dead, fallback good -> wallet created via fallback.
{
dragonx::test::resetLiteFakeCounters();
dragonx::test::g_liteFakeWalletExists = false;
dragonx::test::g_liteFakeDeadServerSubstr = "dead.example";
LiteWalletController controller(liteCaps, conn,
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
LiteWalletCreateRequest req;
req.passphrase = "hunter2";
EXPECT_TRUE(controller.beginCreateWalletAsync(req));
drain(controller);
EXPECT_TRUE(controller.walletOpen());
EXPECT_TRUE(controller.lastLifecycleResult().walletReady);
}
// Async restore from seed: preferred dead, fallback good -> wallet restored via fallback.
{
dragonx::test::resetLiteFakeCounters();
dragonx::test::g_liteFakeWalletExists = false;
dragonx::test::g_liteFakeDeadServerSubstr = "dead.example";
LiteWalletController controller(liteCaps, conn,
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
LiteWalletRestoreRequest req;
req.seedPhrase = "abandon abandon abandon abandon abandon abandon abandon abandon "
"abandon abandon abandon abandon abandon abandon abandon abandon "
"abandon abandon abandon abandon abandon abandon abandon art";
req.birthday = 0;
EXPECT_TRUE(controller.beginRestoreWalletAsync(req));
drain(controller);
EXPECT_TRUE(controller.walletOpen());
EXPECT_TRUE(controller.lastLifecycleResult().walletReady);
}
// All servers dead -> async lifecycle fails, wallet stays closed, reason surfaced.
{
dragonx::test::resetLiteFakeCounters();
dragonx::test::g_liteFakeWalletExists = false;
dragonx::test::g_liteFakeDeadServerSubstr = "example"; // both servers fail
LiteWalletController controller(liteCaps, conn,
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
EXPECT_TRUE(controller.beginCreateWalletAsync(LiteWalletCreateRequest{}));
drain(controller);
EXPECT_FALSE(controller.walletOpen());
EXPECT_FALSE(controller.lastLifecycleResult().walletReady);
EXPECT_TRUE(!controller.lastOpenError().empty());
}
dragonx::test::g_liteFakeWalletExists = false;
dragonx::test::g_liteFakeDeadServerSubstr.clear();
dragonx::test::g_liteFakeWarmupServerSubstr.clear();
}
// M2: a parsed lite refresh bundle maps through to the app's WalletState (the last hop
// the Balance/Receive/Transactions tabs read), with zatoshi->DRGX conversion, z/t address
// split, transaction typing, confirmations, and sync progress.
@@ -4520,6 +4595,7 @@ int main()
testLiteBackendInjectableFakeBridge();
testLiteWalletControllerLifecycle();
testLiteWalletControllerOpenFailover();
testLiteWalletControllerAsyncLifecycleFailover();
testLiteWalletControllerM4();
testLiteWalletControllerM5Persistence();
testLiteWalletControllerEncryption();