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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user