feat(lite): wallet encryption controller layer (encrypt/unlock/lock/decrypt)
Wire the backend passphrase-encryption commands into LiteWalletController:
- encryptWallet / decryptWallet (take passphrase by value, securely wipe it,
save after), unlockWallet / lockWallet (bring spending keys into/out of
memory), and encryptionStatus() -> {encrypted, locked}. All return
failure-safe results; errors arrive as {"error":..} or "Error:" (handled).
- Fold encryptionstatus into refreshModel() (polled every cycle, available even
mid-sync since it reads local wallet state) and apply it in
applyLiteRefreshModelToWalletState, so WalletState.isEncrypted()/isLocked()
track the backend — which gates the existing locked/auto-lock UI.
Backend contracts verified against the SDXL source: encrypt/unlock/decrypt take
the passphrase as the single arg; lock takes none; encryptionstatus returns
{"encrypted","locked"}; ops return {"result":"success"} / {"error":..}.
Tests: testLiteWalletControllerEncryption drives encrypt -> lock -> unlock ->
decrypt via encryptionStatus(), checks empty-passphrase + closed-wallet rejection,
and that the status folds into WalletState. Fake models the state machine.
GUI wiring (encrypt in Settings, unlock prompt / lock action) is the follow-up;
the backend create flow remains unencrypted by default until encrypt is run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,8 @@ inline std::atomic<bool> g_liteFakeSyncBlock{false}; // when true, the "sync" c
|
||||
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 std::atomic<bool> g_liteFakeEncrypted{false}; // wallet-encryption state machine (encrypt/...)
|
||||
inline std::atomic<bool> g_liteFakeLocked{false}; // spending-keys-locked state
|
||||
|
||||
inline void resetLiteFakeCounters()
|
||||
{
|
||||
@@ -126,6 +128,30 @@ inline char* liteFakeExecute(const char* command, const char* args)
|
||||
++g_liteFakeSaveCount;
|
||||
return liteFakeDup("{\"result\":\"success\"}");
|
||||
}
|
||||
// Encryption state machine. encrypt -> encrypted (keys stay in memory this session);
|
||||
// lock/unlock toggle locked; decrypt clears it; encryptionstatus reports {encrypted,locked}.
|
||||
if (std::strcmp(c, "encrypt") == 0) {
|
||||
g_liteFakeEncrypted = true; g_liteFakeLocked = false;
|
||||
return liteFakeDup("{\"result\":\"success\"}");
|
||||
}
|
||||
if (std::strcmp(c, "unlock") == 0) {
|
||||
g_liteFakeLocked = false;
|
||||
return liteFakeDup("{\"result\":\"success\"}");
|
||||
}
|
||||
if (std::strcmp(c, "lock") == 0) {
|
||||
g_liteFakeLocked = true;
|
||||
return liteFakeDup("{\"result\":\"success\"}");
|
||||
}
|
||||
if (std::strcmp(c, "decrypt") == 0) {
|
||||
g_liteFakeEncrypted = false; g_liteFakeLocked = false;
|
||||
return liteFakeDup("{\"result\":\"success\"}");
|
||||
}
|
||||
if (std::strcmp(c, "encryptionstatus") == 0) {
|
||||
return liteFakeDup(g_liteFakeEncrypted.load()
|
||||
? (g_liteFakeLocked.load() ? "{\"encrypted\":true,\"locked\":true}"
|
||||
: "{\"encrypted\":true,\"locked\":false}")
|
||||
: "{\"encrypted\":false,\"locked\":false}");
|
||||
}
|
||||
}
|
||||
// Default for any other/unknown command.
|
||||
return liteFakeDup("{\"version\":\"sdxl-fake\"}");
|
||||
|
||||
@@ -4758,6 +4758,78 @@ void testLiteWalletControllerM5Persistence()
|
||||
}
|
||||
}
|
||||
|
||||
// Encryption: the controller drives encrypt/unlock/lock/decrypt + encryptionstatus through the
|
||||
// (injected fake) backend, wipes passphrases, and folds status into WalletState (encrypted/locked).
|
||||
void testLiteWalletControllerEncryption()
|
||||
{
|
||||
using namespace dragonx::wallet;
|
||||
const auto liteCaps = makeWalletCapabilities(WalletBuildKind::Lite, false, true);
|
||||
const LiteConnectionSettings conn = defaultLiteConnectionSettings();
|
||||
|
||||
auto open = [&]() {
|
||||
dragonx::test::g_liteFakeEncrypted = false;
|
||||
dragonx::test::g_liteFakeLocked = false;
|
||||
auto c = std::make_unique<LiteWalletController>(
|
||||
liteCaps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||
LiteWalletCreateRequest req;
|
||||
req.passphrase = "hunter2";
|
||||
(void)c->createWallet(req);
|
||||
return c;
|
||||
};
|
||||
|
||||
// encrypt -> lock -> unlock -> decrypt, observed via encryptionStatus()
|
||||
{
|
||||
auto c = open();
|
||||
const auto s0 = c->encryptionStatus();
|
||||
EXPECT_TRUE(s0.ok);
|
||||
EXPECT_FALSE(s0.encrypted);
|
||||
EXPECT_FALSE(s0.locked);
|
||||
|
||||
EXPECT_TRUE(c->encryptWallet("walletpass").ok);
|
||||
const auto s1 = c->encryptionStatus();
|
||||
EXPECT_TRUE(s1.encrypted);
|
||||
EXPECT_FALSE(s1.locked);
|
||||
|
||||
EXPECT_TRUE(c->lockWallet());
|
||||
const auto s2 = c->encryptionStatus();
|
||||
EXPECT_TRUE(s2.encrypted);
|
||||
EXPECT_TRUE(s2.locked);
|
||||
|
||||
EXPECT_TRUE(c->unlockWallet("walletpass"));
|
||||
const auto s3 = c->encryptionStatus();
|
||||
EXPECT_TRUE(s3.encrypted);
|
||||
EXPECT_FALSE(s3.locked);
|
||||
|
||||
EXPECT_TRUE(c->decryptWallet("walletpass").ok);
|
||||
const auto s4 = c->encryptionStatus();
|
||||
EXPECT_FALSE(s4.encrypted);
|
||||
EXPECT_FALSE(s4.locked);
|
||||
}
|
||||
|
||||
// empty passphrase rejected; operations on a closed wallet fail closed
|
||||
{
|
||||
auto c = open();
|
||||
EXPECT_FALSE(c->encryptWallet("").ok);
|
||||
|
||||
LiteWalletController closed(liteCaps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||
EXPECT_FALSE(closed.unlockWallet("x"));
|
||||
EXPECT_FALSE(closed.lockWallet());
|
||||
EXPECT_FALSE(closed.encryptionStatus().ok);
|
||||
}
|
||||
|
||||
// encryption status folds into WalletState (gates the locked UI)
|
||||
{
|
||||
LiteWalletAppRefreshModel model;
|
||||
model.hasEncryptionStatus = true;
|
||||
model.encrypted = true;
|
||||
model.locked = true;
|
||||
dragonx::WalletState state;
|
||||
applyLiteRefreshModelToWalletState(model, state);
|
||||
EXPECT_TRUE(state.isEncrypted());
|
||||
EXPECT_TRUE(state.isLocked());
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -5092,6 +5164,7 @@ int main()
|
||||
testLiteWalletControllerLifecycle();
|
||||
testLiteWalletControllerM4();
|
||||
testLiteWalletControllerM5Persistence();
|
||||
testLiteWalletControllerEncryption();
|
||||
testLiteChainNameMigration();
|
||||
testLiteRefreshModelAppliesToWalletState();
|
||||
testLitePerAddressBalances();
|
||||
|
||||
Reference in New Issue
Block a user