fix: FAQ CJK glyphs, outgoing-tx history, lite encryption, migrate & sync safety
Bug-report fixes plus adversarial-audit follow-ups. Bumps full 2.0.1->2.0.2 and lite 1.1.0->1.1.1. i18n / fonts: - Rebuild the NotoSansCJK subset with the glyphs the FAQ back-fill introduced (什/么/门/做); Chinese FAQ titles rendered as "??" on zh (and ja/ko). Transaction history (enumeration gaps): - Surface outgoing shielded z->z sends via z_listsentbyaddress (listtransactions and z_listreceivedbyaddress never report them). - Surface z->t deshields by parsing z_listsentbyaddress transparentSends (which z_viewtransaction does not expose); dedupe t->t against listtransactions. - Key send-row dedup on address so equal-value multi-output sends aren't dropped. Lite wallet (key safety): - The async create/restore/open path the UI uses now applies the passphrase: encrypt a new/restored wallet, unlock an existing one. It previously discarded the passphrase, storing the seed/keys in PLAINTEXT (create/restore) or leaving an encrypted wallet locked (open). Migrate-to-seed (fund safety): - Persist a sweep-submitted marker at broadcast so a resumed migration can't treat a ~0 balance (an unconfirmed in-flight sweep) as an empty wallet and adopt (swap wallet.dat) before the sweep confirms; best-effort locate routes to the confirm gate. Sync / threading: - Gate isSynced() on a peer-derived tip (tip_known) so a peerless node isn't reported synced (Send against a stale balance); add a "no peers" status banner. - Guard daemon_status_ with a mutex (monitor-thread write vs UI-thread read). Tests: extends tests/test_phase4.cpp across all of the above; suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwycPDQSfEKtrTS2uC4JeT
This commit is contained in:
@@ -1628,6 +1628,7 @@ void testNetworkRefreshRpcCollectors()
|
||||
{"time", 50}, {"memoStr", "shielded memo"}}
|
||||
}));
|
||||
transactionRpc.addResponse("z_listreceivedbyaddress", json::array());
|
||||
transactionRpc.addResponse("z_listsentbyaddress", json::array());
|
||||
transactionRpc.addResponse("z_viewtransaction", json{
|
||||
{"spends", json::array({json{{"address", "zs-from"}}})},
|
||||
{"outputs", json::array({
|
||||
@@ -1639,12 +1640,13 @@ void testNetworkRefreshRpcCollectors()
|
||||
auto transactionResult = Refresh::collectTransactionRefreshResult(transactionRpc, snapshot, 321, 4);
|
||||
EXPECT_TRUE(transactionRpc.methodNames() == std::vector<std::string>({
|
||||
"listtransactions", "z_listreceivedbyaddress", "z_listreceivedbyaddress",
|
||||
"z_viewtransaction", "gettransaction"
|
||||
"z_listsentbyaddress", "z_viewtransaction", "gettransaction"
|
||||
}));
|
||||
EXPECT_EQ(transactionRpc.calls[1].params, json::array({"zs-one", 0}));
|
||||
EXPECT_EQ(transactionRpc.calls[2].params, json::array({"zs-two", 0}));
|
||||
EXPECT_EQ(transactionRpc.calls[3].params, json::array({"pending-send"}));
|
||||
EXPECT_EQ(transactionRpc.calls[3].params, json::array({"zs-one", 0}));
|
||||
EXPECT_EQ(transactionRpc.calls[4].params, json::array({"pending-send"}));
|
||||
EXPECT_EQ(transactionRpc.calls[5].params, json::array({"pending-send"}));
|
||||
EXPECT_EQ(transactionResult.blockHeight, 321);
|
||||
EXPECT_EQ(transactionResult.newViewTxEntries.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(transactionResult.newViewTxEntries.count("pending-send"), static_cast<size_t>(1));
|
||||
@@ -1657,6 +1659,129 @@ void testNetworkRefreshRpcCollectors()
|
||||
EXPECT_EQ(transactionResult.transactions.front().timestamp, static_cast<int64_t>(500));
|
||||
EXPECT_EQ(transactionResult.transactions[1].txid, std::string("transparent-a"));
|
||||
|
||||
// Outgoing shielded (z->z) send discovery. A pure z->z send has valueBalance == 0, so it is
|
||||
// absent from listtransactions AND (being a send, not a receive) from z_listreceivedbyaddress —
|
||||
// it is surfaced only via the whole-wallet z_listsentbyaddress sweep, whose txids are folded
|
||||
// into the z_viewtransaction enrichment to build the send row. Regression test for the
|
||||
// "outgoing transactions missing from History" bug.
|
||||
{
|
||||
Refresh::TransactionRefreshSnapshot zzSnapshot;
|
||||
zzSnapshot.shieldedAddresses = {"zs-mine"};
|
||||
MockRefreshRpc zzRpc;
|
||||
zzRpc.addResponse("listtransactions", json::array()); // no transparent movement
|
||||
zzRpc.addResponse("z_listreceivedbyaddress", json::array()); // it's a send, not a receive
|
||||
zzRpc.addResponse("z_listsentbyaddress", json::array({
|
||||
json{{"txid", "zz-send"}} // discovered only here
|
||||
}));
|
||||
zzRpc.addResponse("z_viewtransaction", json{
|
||||
{"spends", json::array({json{{"address", "zs-mine"}}})},
|
||||
{"outputs", json::array({
|
||||
json{{"outgoing", true}, {"address", "zs-recipient"}, {"value", 1.25},
|
||||
{"memoStr", "z2z memo"}}
|
||||
})}
|
||||
});
|
||||
zzRpc.addResponse("gettransaction", json{{"time", 800}, {"confirmations", 3}});
|
||||
auto zzResult = Refresh::collectTransactionRefreshResult(zzRpc, zzSnapshot, 400, 4);
|
||||
EXPECT_TRUE(zzRpc.methodNames() == std::vector<std::string>({
|
||||
"listtransactions", "z_listreceivedbyaddress", "z_listsentbyaddress",
|
||||
"z_viewtransaction", "gettransaction"
|
||||
}));
|
||||
EXPECT_EQ(zzResult.transactions.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(zzResult.transactions[0].txid, std::string("zz-send"));
|
||||
EXPECT_EQ(zzResult.transactions[0].type, std::string("send"));
|
||||
EXPECT_NEAR(zzResult.transactions[0].amount, -1.25, 0.00000001);
|
||||
EXPECT_EQ(zzResult.transactions[0].address, std::string("zs-recipient"));
|
||||
EXPECT_EQ(zzResult.transactions[0].memo, std::string("z2z memo"));
|
||||
EXPECT_EQ(zzResult.transactions[0].timestamp, static_cast<int64_t>(800));
|
||||
EXPECT_EQ(zzResult.newViewTxEntries.count("zz-send"), static_cast<size_t>(1));
|
||||
|
||||
// Guard: with no shielded addresses the sweep is skipped entirely (transparent sends are
|
||||
// already covered by listtransactions), so no z_listsentbyaddress call is made.
|
||||
Refresh::TransactionRefreshSnapshot noZSnapshot;
|
||||
MockRefreshRpc noZRpc;
|
||||
noZRpc.addResponse("listtransactions", json::array());
|
||||
auto noZResult = Refresh::collectTransactionRefreshResult(noZRpc, noZSnapshot, 401, 4);
|
||||
EXPECT_TRUE(noZRpc.methodNames() == std::vector<std::string>({"listtransactions"}));
|
||||
EXPECT_EQ(noZResult.transactions.size(), static_cast<size_t>(0));
|
||||
}
|
||||
|
||||
// z->t deshield: the external transparent recipient is reported ONLY in z_listsentbyaddress's
|
||||
// sends.transparentSends (z_viewtransaction sees only the shielded change note, isOutgoing=false;
|
||||
// listtransactions omits the t-vout because inputs are shielded). The row must be synthesized
|
||||
// directly from that payload — a direct sibling of the z->z gap.
|
||||
{
|
||||
Refresh::TransactionRefreshSnapshot ztSnapshot;
|
||||
ztSnapshot.shieldedAddresses = {"zs-mine"};
|
||||
MockRefreshRpc ztRpc;
|
||||
ztRpc.addResponse("listtransactions", json::array()); // z-originated: no t-input row
|
||||
ztRpc.addResponse("z_listreceivedbyaddress", json::array());
|
||||
ztRpc.addResponse("z_listsentbyaddress", json::array({
|
||||
json{{"txid", "zt-send"}, {"confirmations", 5}, {"blocktime", 900},
|
||||
{"sends", json{
|
||||
{"transparentSends", json::array({
|
||||
json{{"address", "R-recipient"}, {"amount", -2.5}, {"vout", 0}}
|
||||
})},
|
||||
{"saplingSends", json::array()}
|
||||
}}}
|
||||
}));
|
||||
// z->t viewtransaction: only a change note back to self -> no outgoing outputs.
|
||||
ztRpc.addResponse("z_viewtransaction", json{{"spends", json::array()}, {"outputs", json::array()}});
|
||||
auto ztResult = Refresh::collectTransactionRefreshResult(ztRpc, ztSnapshot, 500, 4);
|
||||
EXPECT_EQ(ztResult.transactions.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(ztResult.transactions[0].txid, std::string("zt-send"));
|
||||
EXPECT_EQ(ztResult.transactions[0].type, std::string("send"));
|
||||
EXPECT_NEAR(ztResult.transactions[0].amount, -2.5, 0.00000001);
|
||||
EXPECT_EQ(ztResult.transactions[0].address, std::string("R-recipient"));
|
||||
EXPECT_EQ(ztResult.transactions[0].confirmations, 5);
|
||||
}
|
||||
|
||||
// t->t send is reported by BOTH listtransactions AND z_listsentbyaddress's transparentSends —
|
||||
// it must appear ONCE (deduped by txid+address+amount), not doubled.
|
||||
{
|
||||
Refresh::TransactionRefreshSnapshot ttSnapshot;
|
||||
ttSnapshot.shieldedAddresses = {"zs-mine"};
|
||||
MockRefreshRpc ttRpc;
|
||||
ttRpc.addResponse("listtransactions", json::array({
|
||||
json{{"txid", "tt-send"}, {"category", "send"}, {"amount", -1.0},
|
||||
{"time", 800}, {"confirmations", 3}, {"address", "R-dest"}}
|
||||
}));
|
||||
ttRpc.addResponse("z_listreceivedbyaddress", json::array());
|
||||
ttRpc.addResponse("z_listsentbyaddress", json::array({
|
||||
json{{"txid", "tt-send"}, {"confirmations", 3}, {"blocktime", 800},
|
||||
{"sends", json{{"transparentSends", json::array({
|
||||
json{{"address", "R-dest"}, {"amount", -1.0}, {"vout", 0}}
|
||||
})}}}}
|
||||
}));
|
||||
ttRpc.addResponse("z_viewtransaction", json{{"spends", json::array()}, {"outputs", json::array()}});
|
||||
auto ttResult = Refresh::collectTransactionRefreshResult(ttRpc, ttSnapshot, 500, 4);
|
||||
int ttSends = 0;
|
||||
for (const auto& t : ttResult.transactions)
|
||||
if (t.txid == std::string("tt-send") && t.type == std::string("send")) ++ttSends;
|
||||
EXPECT_EQ(ttSends, 1); // not double-counted
|
||||
}
|
||||
|
||||
// Multi-output send: two outgoing outputs of EQUAL value to DIFFERENT recipients in one tx must
|
||||
// both render (address-keyed dedup) — an amount-only dedup dropped the second.
|
||||
{
|
||||
Refresh::TransactionRefreshSnapshot multiSnapshot;
|
||||
multiSnapshot.sendTxids = {"multi-send"};
|
||||
MockRefreshRpc multiRpc;
|
||||
multiRpc.addResponse("listtransactions", json::array());
|
||||
multiRpc.addResponse("z_viewtransaction", json{
|
||||
{"spends", json::array({json{{"address", "zs-from"}}})},
|
||||
{"outputs", json::array({
|
||||
json{{"outgoing", true}, {"address", "zs-a"}, {"value", 0.5}, {"memoStr", "a"}},
|
||||
json{{"outgoing", true}, {"address", "zs-b"}, {"value", 0.5}, {"memoStr", "b"}}
|
||||
})}
|
||||
});
|
||||
multiRpc.addResponse("gettransaction", json{{"time", 700}, {"confirmations", 2}});
|
||||
auto multiResult = Refresh::collectTransactionRefreshResult(multiRpc, multiSnapshot, 500, 4);
|
||||
int multiSends = 0;
|
||||
for (const auto& t : multiResult.transactions)
|
||||
if (t.txid == std::string("multi-send") && t.type == std::string("send")) ++multiSends;
|
||||
EXPECT_EQ(multiSends, 2); // both equal-value outputs kept
|
||||
}
|
||||
|
||||
Refresh::TransactionRefreshSnapshot cachedOnlySnapshot;
|
||||
auto cachedOnlyEntry = cachedEntry;
|
||||
cachedOnlyEntry.timestamp = 450;
|
||||
@@ -1802,6 +1927,7 @@ void testNetworkRefreshRpcCollectors()
|
||||
strictRpc.addResponse("listtransactions", json::array());
|
||||
strictRpc.addResponse("z_listreceivedbyaddress", json::array());
|
||||
strictRpc.addResponse("z_listreceivedbyaddress", json::array());
|
||||
strictRpc.addResponse("z_listsentbyaddress", json::array());
|
||||
auto strictResult = Refresh::collectTransactionRefreshResult(strictRpc, strictSnapshot, 102, 4);
|
||||
EXPECT_EQ(strictResult.shieldedAddressesScanned, static_cast<size_t>(2)); // both re-scanned
|
||||
|
||||
@@ -1813,10 +1939,12 @@ void testNetworkRefreshRpcCollectors()
|
||||
MockRefreshRpc tolerantRpc;
|
||||
tolerantRpc.addResponse("listtransactions", json::array());
|
||||
// No z_listreceivedbyaddress responses: the addresses must be skipped (else call() throws).
|
||||
// The shielded scan still completes, so the whole-wallet z_listsentbyaddress sweep runs once.
|
||||
tolerantRpc.addResponse("z_listsentbyaddress", json::array());
|
||||
auto tolerantResult = Refresh::collectTransactionRefreshResult(tolerantRpc, tolerantSnapshot, 102, 4);
|
||||
EXPECT_EQ(tolerantResult.shieldedAddressesScanned, static_cast<size_t>(0)); // both skipped
|
||||
EXPECT_TRUE(tolerantResult.shieldedScanComplete);
|
||||
EXPECT_TRUE(tolerantRpc.methodNames() == std::vector<std::string>({"listtransactions"}));
|
||||
EXPECT_TRUE(tolerantRpc.methodNames() == std::vector<std::string>({"listtransactions", "z_listsentbyaddress"}));
|
||||
}
|
||||
|
||||
Refresh::TransactionRefreshSnapshot recentSnapshot;
|
||||
@@ -1902,10 +2030,11 @@ void testNetworkRefreshRpcCollectors()
|
||||
MockRefreshRpc finalShieldedRpc;
|
||||
finalShieldedRpc.addResponse("listtransactions", json::array());
|
||||
finalShieldedRpc.addResponse("z_listreceivedbyaddress", json::array());
|
||||
finalShieldedRpc.addResponse("z_listsentbyaddress", json::array());
|
||||
auto finalShielded = Refresh::collectTransactionRefreshResult(
|
||||
finalShieldedRpc, partialShieldedSnapshot, 400, 0);
|
||||
EXPECT_TRUE(finalShieldedRpc.methodNames() == std::vector<std::string>({
|
||||
"listtransactions", "z_listreceivedbyaddress"
|
||||
"listtransactions", "z_listreceivedbyaddress", "z_listsentbyaddress"
|
||||
}));
|
||||
EXPECT_EQ(finalShieldedRpc.calls[1].params, json::array({"zs-two", 0}));
|
||||
EXPECT_TRUE(finalShielded.shieldedScanComplete);
|
||||
@@ -1922,9 +2051,10 @@ void testNetworkRefreshRpcCollectors()
|
||||
});
|
||||
MockRefreshRpc cachedShieldedRpc;
|
||||
cachedShieldedRpc.addResponse("listtransactions", json::array());
|
||||
cachedShieldedRpc.addResponse("z_listsentbyaddress", json::array());
|
||||
auto cachedShielded = Refresh::collectTransactionRefreshResult(
|
||||
cachedShieldedRpc, cachedShieldedSnapshot, 500, 0);
|
||||
EXPECT_TRUE(cachedShieldedRpc.methodNames() == std::vector<std::string>({"listtransactions"}));
|
||||
EXPECT_TRUE(cachedShieldedRpc.methodNames() == std::vector<std::string>({"listtransactions", "z_listsentbyaddress"}));
|
||||
EXPECT_TRUE(cachedShielded.shieldedScanComplete);
|
||||
EXPECT_EQ(cachedShielded.shieldedAddressesScanned, static_cast<size_t>(0));
|
||||
EXPECT_EQ(cachedShielded.transactions.size(), static_cast<size_t>(1));
|
||||
@@ -1970,6 +2100,7 @@ void testNetworkRefreshRpcCollectors()
|
||||
json{{"txid", "shielded-mined"}, {"amount", 4.0}, {"confirmations", 102},
|
||||
{"time", 210}, {"memoStr", "pool"}}
|
||||
}));
|
||||
miningTxRpc.addResponse("z_listsentbyaddress", json::array());
|
||||
auto miningTxResult = Refresh::collectTransactionRefreshResult(miningTxRpc, miningSnapshot, 328, 0);
|
||||
EXPECT_EQ(miningTxResult.transactions.size(), static_cast<size_t>(2));
|
||||
EXPECT_EQ(miningTxResult.transactions[0].type, std::string("mined"));
|
||||
@@ -2005,6 +2136,25 @@ void testNetworkRefreshResultModels()
|
||||
EXPECT_EQ(state.longestchain, 110);
|
||||
EXPECT_EQ(state.notarized, 90);
|
||||
EXPECT_EQ(state.last_balance_update, static_cast<int64_t>(1234));
|
||||
EXPECT_TRUE(state.sync.tip_known); // longestchain>0 -> peer-derived tip is known
|
||||
EXPECT_FALSE(state.sync.isSynced()); // still catching up (blocks 100 < tip 110)
|
||||
|
||||
// Peerless node: the daemon reports longestchain == 0. Even at the local best height (blocks ==
|
||||
// headers) it must NOT be reported synced (the tip is unknown) — otherwise Send would be offered
|
||||
// against a stale balance on a node that has silently lost all peers. (audit: peerless-synced)
|
||||
{
|
||||
dragonx::WalletState peerless;
|
||||
auto pc = Refresh::parseCoreRefreshResult(
|
||||
json{{"private", "5.00000000"}, {"transparent", "0.00000000"}, {"total", "5.00000000"}},
|
||||
json{{"private", "5.00000000"}, {"transparent", "0.00000000"}, {"total", "5.00000000"}},
|
||||
true,
|
||||
json{{"blocks", 200}, {"headers", 200}, {"bestblockhash", "peerless-200"},
|
||||
{"verificationprogress", 1.0}, {"longestchain", 0}, {"notarized", 0}},
|
||||
true);
|
||||
Refresh::applyCoreRefreshResult(peerless, pc, 1235);
|
||||
EXPECT_FALSE(peerless.sync.tip_known);
|
||||
EXPECT_FALSE(peerless.sync.isSynced()); // peerless -> not synced despite blocks == headers
|
||||
}
|
||||
|
||||
auto connectionInfo = Refresh::parseConnectionInfoResult(
|
||||
json{{"version", 120000}, {"protocolversion", 170002}, {"p2pport", 8233},
|
||||
@@ -2638,6 +2788,28 @@ void testNodeStatusBanner()
|
||||
NodeBannerInputs in; in.connected = true;
|
||||
EXPECT_TRUE(!evaluateNodeStatusBanner(in).show);
|
||||
}
|
||||
// Connected but no peer-derived tip (0 peers) once chain info is in → amber "no peers" banner.
|
||||
{
|
||||
NodeBannerInputs in;
|
||||
in.connected = true; in.tip_known = false; in.blocks = 3262059;
|
||||
in.connection_status = "Peers: 0";
|
||||
NodeBannerState s = evaluateNodeStatusBanner(in);
|
||||
EXPECT_TRUE(s.show);
|
||||
EXPECT_TRUE(s.severity == NodeBannerSeverity::Warning);
|
||||
EXPECT_TRUE(s.reason == NodeBannerReason::NoPeers);
|
||||
EXPECT_TRUE(s.action == NodeBannerAction::None);
|
||||
EXPECT_EQ(s.detail, std::string("Peers: 0"));
|
||||
}
|
||||
// Connected, tip unknown, but no chain info yet (blocks==0) → no banner (avoid startup flicker).
|
||||
{
|
||||
NodeBannerInputs in; in.connected = true; in.tip_known = false; in.blocks = 0;
|
||||
EXPECT_TRUE(!evaluateNodeStatusBanner(in).show);
|
||||
}
|
||||
// Connected with a known tip → no banner.
|
||||
{
|
||||
NodeBannerInputs in; in.connected = true; in.tip_known = true; in.blocks = 100;
|
||||
EXPECT_TRUE(!evaluateNodeStatusBanner(in).show);
|
||||
}
|
||||
// Expected startup phases own the screen (loading/warmup overlay) → no banner.
|
||||
{
|
||||
NodeBannerInputs in; in.warming_up = true;
|
||||
@@ -2792,6 +2964,12 @@ void testSeedMigrationResume()
|
||||
// No txid and no opid → the Sweep gate (whether or not connected).
|
||||
EXPECT_TRUE(decideSeedMigrationResume(true, true, "", "", true) == MigrationResume::SweepGate);
|
||||
EXPECT_TRUE(decideSeedMigrationResume(true, true, "", "", false) == MigrationResume::SweepGate);
|
||||
|
||||
// fund-safety: the Sweep step may only short-circuit a ~0 balance straight to adopt when NO sweep
|
||||
// was ever submitted (a genuinely empty wallet). Once a sweep has been broadcast, a ~0 balance is
|
||||
// ambiguous (the tx may be unconfirmed in the mempool), so direct adopt must be blocked.
|
||||
EXPECT_TRUE(dragonx::sweepGateMayAdoptEmptyWallet(/*sweepSubmitted=*/false)); // empty wallet -> ok
|
||||
EXPECT_FALSE(dragonx::sweepGateMayAdoptEmptyWallet(/*sweepSubmitted=*/true)); // in-flight -> blocked
|
||||
}
|
||||
|
||||
void testLoggerFileSink()
|
||||
@@ -5301,6 +5479,92 @@ void testLiteWalletControllerAsyncLifecycleFailover()
|
||||
dragonx::test::g_liteFakeWarmupServerSubstr.clear();
|
||||
}
|
||||
|
||||
// C1/M1: the ASYNC lifecycle path (the one the Settings UI actually calls) must apply the
|
||||
// passphrase — encrypt a freshly created/restored wallet, unlock an existing encrypted one.
|
||||
// Previously the async path discarded the passphrase (encrypt/unlock lived only in the sync
|
||||
// wrappers), so a UI-created wallet stored its seed in PLAINTEXT and an encrypted wallet opened
|
||||
// but stayed locked.
|
||||
void testLiteWalletControllerAsyncAppliesPassphrase()
|
||||
{
|
||||
using namespace dragonx::wallet;
|
||||
const auto liteCaps = makeWalletCapabilities(WalletBuildKind::Lite, false, true);
|
||||
LiteConnectionSettings conn;
|
||||
conn.chainName = "main";
|
||||
conn.servers = { LiteServerEndpoint{"https://good.example", "Good", true} };
|
||||
conn.selectionMode = LiteServerSelectionMode::Sticky;
|
||||
conn.stickyServerUrl = "https://good.example";
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
// C1: async create with a passphrase encrypts (and locks) the new wallet; same passphrase unlocks.
|
||||
{
|
||||
dragonx::test::resetLiteFakeCounters();
|
||||
dragonx::test::g_liteFakeWalletExists = false;
|
||||
dragonx::test::g_liteFakeEncrypted = false;
|
||||
dragonx::test::g_liteFakeLocked = false;
|
||||
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());
|
||||
const auto s = controller.encryptionStatus();
|
||||
EXPECT_TRUE(s.encrypted); // async create ENCRYPTED the wallet (C1 fixed)
|
||||
EXPECT_TRUE(s.locked); // encrypt locks immediately
|
||||
EXPECT_TRUE(controller.unlockWallet("hunter2"));
|
||||
EXPECT_FALSE(controller.encryptionStatus().locked);
|
||||
}
|
||||
|
||||
// C1: async restore with a passphrase likewise encrypts the restored wallet.
|
||||
{
|
||||
dragonx::test::resetLiteFakeCounters();
|
||||
dragonx::test::g_liteFakeWalletExists = false;
|
||||
dragonx::test::g_liteFakeEncrypted = false;
|
||||
dragonx::test::g_liteFakeLocked = false;
|
||||
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;
|
||||
req.passphrase = "hunter2";
|
||||
EXPECT_TRUE(controller.beginRestoreWalletAsync(req));
|
||||
drain(controller);
|
||||
EXPECT_TRUE(controller.walletOpen());
|
||||
EXPECT_TRUE(controller.encryptionStatus().encrypted); // C1 fixed for restore
|
||||
}
|
||||
|
||||
// M1: async open of an already-encrypted+locked wallet unlocks it with the supplied passphrase.
|
||||
{
|
||||
dragonx::test::resetLiteFakeCounters();
|
||||
dragonx::test::g_liteFakeWalletExists = true;
|
||||
dragonx::test::g_liteFakeEncrypted = true; // existing wallet is encrypted...
|
||||
dragonx::test::g_liteFakeLocked = true; // ...and locked at open time
|
||||
LiteWalletController controller(liteCaps, conn,
|
||||
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||
LiteWalletOpenRequest req;
|
||||
req.passphrase = "hunter2";
|
||||
EXPECT_TRUE(controller.beginOpenWalletAsync(req));
|
||||
drain(controller);
|
||||
EXPECT_TRUE(controller.walletOpen());
|
||||
const auto s = controller.encryptionStatus();
|
||||
EXPECT_TRUE(s.encrypted);
|
||||
EXPECT_FALSE(s.locked); // async open UNLOCKED it (M1 fixed)
|
||||
}
|
||||
|
||||
dragonx::test::g_liteFakeWalletExists = false;
|
||||
dragonx::test::g_liteFakeEncrypted = false;
|
||||
dragonx::test::g_liteFakeLocked = 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.
|
||||
@@ -7500,6 +7764,7 @@ int main()
|
||||
testLiteWalletControllerM5Persistence();
|
||||
testLiteWalletControllerEncryption();
|
||||
testLiteWalletControllerCreateEncryptsWithPassphrase();
|
||||
testLiteWalletControllerAsyncAppliesPassphrase();
|
||||
testLiteChainNameMigration();
|
||||
testLiteRefreshModelAppliesToWalletState();
|
||||
testLiteSendShowsRecipientFromOutgoing();
|
||||
|
||||
Reference in New Issue
Block a user