feat(lite): M5a — wallet persistence after sync/send/shield
Verified against the SDXL Rust source that the backend auto-saves only on new-address / import / rescan; it does NOT save after sync, send, or shield, and litelib_shutdown merely sets a flag. So without intervention a first sync (~30 min) and any sent transaction are lost on restart. The controller now triggers the backend `save` at exactly the right points: - after the detached `sync` completes — and BEFORE syncDone_ is set, so a syncComplete() observer always sees a fully persisted wallet; - after a successful send / shield (the doSend/doShield cores; skipped on failure so a failed broadcast doesn't write); - a guarded best-effort flush in the destructor, only when syncDone_ and no broadcast is in flight, so shutdown never blocks on the wallet lock held by an uninterruptible scan or in-progress proving; - plus a public saveWallet() for explicit/periodic saves. Wallet-file crash recovery (.dat / .dat.bak rotation) is already handled inside the backend. Tests: testLiteWalletControllerM5Persistence proves saves fire after sync/send/shield and explicit saveWallet(), and do NOT fire on a failed send or with no wallet open (fake gains a save counter). Plan doc updated; M5b (packaging/CI/signing/rollout) remains. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ inline bool g_liteFakeShutdownCalled = false;
|
||||
inline std::atomic<bool> g_liteFakeSyncBlock{false}; // when true, the "sync" command blocks
|
||||
inline std::atomic<bool> g_liteFakeBadBalance{false}; // when true, "balance" returns invalid JSON
|
||||
inline std::atomic<bool> g_liteFakeSendFails{false}; // when true, "send"/"shield" return {"error":..}
|
||||
inline std::atomic<long> g_liteFakeSaveCount{0}; // counts "save" commands (persistence checks)
|
||||
|
||||
inline void resetLiteFakeCounters()
|
||||
{
|
||||
@@ -121,6 +122,10 @@ inline char* liteFakeExecute(const char* command, const char* args)
|
||||
// import (shielded key) / timport (transparent WIF): do_import_* pretty-print JSON.
|
||||
if (std::strcmp(c, "import") == 0 || std::strcmp(c, "timport") == 0)
|
||||
return liteFakeDup("{\"result\":\"success\",\"address\":\"zs1imported\"}");
|
||||
if (std::strcmp(c, "save") == 0) {
|
||||
++g_liteFakeSaveCount;
|
||||
return liteFakeDup("{\"result\":\"success\"}");
|
||||
}
|
||||
}
|
||||
// Default for any other/unknown command.
|
||||
return liteFakeDup("{\"version\":\"sdxl-fake\"}");
|
||||
|
||||
@@ -4677,6 +4677,87 @@ void testLiteWalletControllerM4()
|
||||
}
|
||||
}
|
||||
|
||||
// M5: persistence. The backend auto-saves on new-address/import but NOT after sync/send/shield,
|
||||
// so the controller must trigger `save` at those points (otherwise a ~30-min scan and sent txs
|
||||
// are lost on restart). Drives the fake's save counter to prove saves happen exactly when needed.
|
||||
void testLiteWalletControllerM5Persistence()
|
||||
{
|
||||
using namespace dragonx::wallet;
|
||||
const auto liteCaps = makeWalletCapabilities(WalletBuildKind::Lite, false, true);
|
||||
const LiteConnectionSettings conn = defaultLiteConnectionSettings();
|
||||
|
||||
auto openController = [&]() {
|
||||
auto c = std::make_unique<LiteWalletController>(
|
||||
liteCaps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||
LiteWalletCreateRequest req;
|
||||
req.passphrase = "hunter2";
|
||||
(void)c->createWallet(req);
|
||||
return c;
|
||||
};
|
||||
// Open and wait until the background sync (and its persist) has completed, so the save count
|
||||
// is stable before a sub-test captures its baseline.
|
||||
auto openSynced = [&]() {
|
||||
auto c = openController();
|
||||
for (int i = 0; i < 400 && !c->syncComplete(); ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
return c;
|
||||
};
|
||||
|
||||
// sync persists: completing a sync saves the freshly-scanned wallet.
|
||||
{
|
||||
dragonx::test::g_liteFakeSyncBlock = false;
|
||||
dragonx::test::g_liteFakeSaveCount = 0;
|
||||
auto c = openSynced();
|
||||
EXPECT_TRUE(c->syncComplete());
|
||||
EXPECT_TRUE(dragonx::test::g_liteFakeSaveCount.load() >= 1);
|
||||
}
|
||||
|
||||
// send persists the new transaction (backend does not auto-save sends)
|
||||
{
|
||||
dragonx::test::g_liteFakeSendFails = false;
|
||||
auto c = openSynced();
|
||||
const long before = dragonx::test::g_liteFakeSaveCount.load();
|
||||
LiteSendRequest req;
|
||||
req.recipients.push_back({"zs1dest", 1ULL, ""});
|
||||
EXPECT_TRUE(c->sendTransactionBlocking(req).ok);
|
||||
EXPECT_EQ(dragonx::test::g_liteFakeSaveCount.load(), before + 1);
|
||||
}
|
||||
|
||||
// a FAILED send does NOT save
|
||||
{
|
||||
dragonx::test::g_liteFakeSendFails = true;
|
||||
auto c = openSynced();
|
||||
const long before = dragonx::test::g_liteFakeSaveCount.load();
|
||||
LiteSendRequest req;
|
||||
req.recipients.push_back({"zs1dest", 1ULL, ""});
|
||||
EXPECT_FALSE(c->sendTransactionBlocking(req).ok);
|
||||
EXPECT_EQ(dragonx::test::g_liteFakeSaveCount.load(), before);
|
||||
dragonx::test::g_liteFakeSendFails = false;
|
||||
}
|
||||
|
||||
// shield persists
|
||||
{
|
||||
auto c = openSynced();
|
||||
const long before = dragonx::test::g_liteFakeSaveCount.load();
|
||||
EXPECT_TRUE(c->shieldFundsBlocking().ok);
|
||||
EXPECT_EQ(dragonx::test::g_liteFakeSaveCount.load(), before + 1);
|
||||
}
|
||||
|
||||
// explicit saveWallet() persists; with no wallet open it is a no-op
|
||||
{
|
||||
auto c = openSynced();
|
||||
const long before = dragonx::test::g_liteFakeSaveCount.load();
|
||||
EXPECT_TRUE(c->saveWallet());
|
||||
EXPECT_EQ(dragonx::test::g_liteFakeSaveCount.load(), before + 1);
|
||||
}
|
||||
{
|
||||
LiteWalletController c(liteCaps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||
const long before = dragonx::test::g_liteFakeSaveCount.load();
|
||||
EXPECT_FALSE(c.saveWallet());
|
||||
EXPECT_EQ(dragonx::test::g_liteFakeSaveCount.load(), before);
|
||||
}
|
||||
}
|
||||
|
||||
// Migration: a saved lite chain_name outside {main,test,regtest} (e.g. the legacy
|
||||
// "DRAGONX" ticker) is rewritten to "main" on load, since the backend hard-panics
|
||||
// on unknown chains. Valid values are preserved.
|
||||
@@ -5010,6 +5091,7 @@ int main()
|
||||
testLiteBackendInjectableFakeBridge();
|
||||
testLiteWalletControllerLifecycle();
|
||||
testLiteWalletControllerM4();
|
||||
testLiteWalletControllerM5Persistence();
|
||||
testLiteChainNameMigration();
|
||||
testLiteRefreshModelAppliesToWalletState();
|
||||
testLitePerAddressBalances();
|
||||
|
||||
Reference in New Issue
Block a user