feat(wallet): persist history and surface pending sends
Add an encrypted SQLite transaction history cache with cached tip metadata and per-address shielded scan progress so startup and full refreshes avoid re-scanning every z-address while still invalidating on wallet/address/rescan changes. Improve wallet history loading by paging transparent transactions, preserving cached shielded and sent rows, keeping recent/unconfirmed activity visible, and classifying mining-address receives. Show z_sendmany opid sends immediately in History and Overview, pin pending rows through refreshes, and apply optimistic address/balance debits until opids resolve. Add timestamped RPC console tracing by source/method without logging params or results, reduce redundant refresh/RPC calls, and cache Explorer recent block summaries in SQLite. Expand focused tests for transaction cache encryption, scan-progress persistence/invalidation, history preservation, operation-status parsing, pending send visibility, and Explorer/RPC refresh behavior.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
#include "daemon/daemon_controller.h"
|
||||
#include "data/transaction_history_cache.h"
|
||||
#include "daemon/lifecycle_adapters.h"
|
||||
#include "data/wallet_state.h"
|
||||
#include "rpc/connection.h"
|
||||
@@ -8,6 +9,7 @@
|
||||
#include "services/wallet_security_controller.h"
|
||||
#include "services/wallet_security_workflow.h"
|
||||
#include "services/wallet_security_workflow_executor.h"
|
||||
#include "ui/explorer/explorer_block_cache.h"
|
||||
#include "ui/windows/balance_address_list.h"
|
||||
#include "ui/windows/balance_recent_tx.h"
|
||||
#include "ui/windows/console_input_model.h"
|
||||
@@ -485,13 +487,23 @@ void testRefreshScheduler()
|
||||
scheduler.markDue(Timer::Peers);
|
||||
EXPECT_TRUE(scheduler.consumeDue(Timer::Peers));
|
||||
|
||||
scheduler.applyPage(dragonx::ui::NavPage::Console);
|
||||
EXPECT_NEAR(scheduler.intervals().core, 10.0, 0.0001);
|
||||
EXPECT_NEAR(scheduler.intervals().transactions, 30.0, 0.0001);
|
||||
EXPECT_NEAR(scheduler.intervals().addresses, 30.0, 0.0001);
|
||||
EXPECT_NEAR(scheduler.intervals().peers, 0.0, 0.0001);
|
||||
|
||||
EXPECT_FALSE(scheduler.isDue(Timer::Price));
|
||||
scheduler.markDue(Timer::Price);
|
||||
EXPECT_TRUE(scheduler.consumeDue(Timer::Price));
|
||||
|
||||
EXPECT_FALSE(scheduler.shouldRefreshTransactions(100, 100, false, false));
|
||||
EXPECT_FALSE(scheduler.shouldRefreshTransactions(100, 100, false));
|
||||
EXPECT_TRUE(scheduler.shouldRefreshTransactions(-1, 100, false));
|
||||
EXPECT_TRUE(scheduler.shouldRefreshTransactions(99, 100, false));
|
||||
EXPECT_TRUE(scheduler.shouldRefreshTransactions(100, 100, true));
|
||||
scheduler.tick(RefreshScheduler::kTxMaxAge);
|
||||
EXPECT_TRUE(scheduler.shouldRefreshTransactions(100, 100, false, false));
|
||||
EXPECT_FALSE(scheduler.shouldRefreshTransactions(100, 100, false));
|
||||
EXPECT_TRUE(scheduler.isDue(Timer::TxAge));
|
||||
}
|
||||
|
||||
void testNetworkRefreshService()
|
||||
@@ -808,6 +820,15 @@ void testNetworkRefreshRpcCollectors()
|
||||
EXPECT_TRUE(prefetch.encryption.encrypted);
|
||||
EXPECT_EQ(prefetch.encryption.unlockedUntil, static_cast<int64_t>(99));
|
||||
|
||||
MockRefreshRpc connectionReuseRpc;
|
||||
connectionReuseRpc.addResponse("getwalletinfo", json{{"unlocked_until", 0}});
|
||||
auto reusedInfo = Refresh::parseConnectionInfoResult(json{{"version", 120002}, {"blocks", 241}});
|
||||
auto reusedConnection = Refresh::collectConnectionInitResult(connectionReuseRpc, reusedInfo);
|
||||
EXPECT_TRUE(connectionReuseRpc.methodNames() == std::vector<std::string>({"getwalletinfo"}));
|
||||
EXPECT_TRUE(reusedConnection.info.ok);
|
||||
EXPECT_EQ(*reusedConnection.info.daemonVersion, 120002);
|
||||
EXPECT_TRUE(reusedConnection.encryption.ok);
|
||||
|
||||
MockRefreshRpc coreRpc;
|
||||
coreRpc.addResponse("z_gettotalbalance", json{
|
||||
{"private", "3.00000000"},
|
||||
@@ -817,6 +838,7 @@ void testNetworkRefreshRpcCollectors()
|
||||
coreRpc.addResponse("getblockchaininfo", json{
|
||||
{"blocks", 150},
|
||||
{"headers", 155},
|
||||
{"bestblockhash", "core-best-150"},
|
||||
{"verificationprogress", 0.80},
|
||||
{"longestchain", 160},
|
||||
{"notarized", 145}
|
||||
@@ -831,6 +853,7 @@ void testNetworkRefreshRpcCollectors()
|
||||
EXPECT_TRUE(core.blockchainOk);
|
||||
EXPECT_NEAR(*core.totalBalance, 4.25, 0.00000001);
|
||||
EXPECT_EQ(*core.blocks, 150);
|
||||
EXPECT_EQ(*core.bestBlockHash, std::string("core-best-150"));
|
||||
EXPECT_EQ(*core.longestChain, 160);
|
||||
|
||||
MockRefreshRpc coreFallbackRpc;
|
||||
@@ -918,6 +941,14 @@ void testNetworkRefreshRpcCollectors()
|
||||
EXPECT_TRUE(miningPartial.miningOk);
|
||||
EXPECT_FALSE(*miningPartial.generate);
|
||||
|
||||
MockRefreshRpc miningSlowOnlyRpc;
|
||||
miningSlowOnlyRpc.addResponse("getmininginfo", json{{"generate", false}, {"networkhashps", 222.0}});
|
||||
auto miningSlowOnly = Refresh::collectMiningRefreshResult(miningSlowOnlyRpc, 7.0, true, false);
|
||||
EXPECT_TRUE(miningSlowOnlyRpc.methodNames() == std::vector<std::string>({"getmininginfo"}));
|
||||
EXPECT_FALSE(miningSlowOnly.localHashrate.has_value());
|
||||
EXPECT_TRUE(miningSlowOnly.miningOk);
|
||||
EXPECT_NEAR(*miningSlowOnly.networkHashrate, 222.0, 0.00000001);
|
||||
|
||||
MockRefreshRpc addressRpc;
|
||||
addressRpc.addResponse("z_listaddresses", json::array({"zs-one", "zs-two"}));
|
||||
addressRpc.addResponse("z_validateaddress", json{{"ismine", true}});
|
||||
@@ -961,6 +992,23 @@ void testNetworkRefreshRpcCollectors()
|
||||
EXPECT_TRUE(fallbackAddresses.shieldedAddresses[0].has_spending_key);
|
||||
EXPECT_NEAR(fallbackAddresses.shieldedAddresses[0].balance, 4.75, 0.00000001);
|
||||
|
||||
dragonx::WalletState cachedAddressState;
|
||||
cachedAddressState.z_addresses.push_back({"zs-cached", 0.0, "shielded", false});
|
||||
auto addressSnapshot = Refresh::buildAddressRefreshSnapshot(cachedAddressState);
|
||||
MockRefreshRpc cachedAddressRpc;
|
||||
cachedAddressRpc.addResponse("z_listaddresses", json::array({"zs-cached", "zs-new"}));
|
||||
cachedAddressRpc.addResponse("z_validateaddress", json{{"ismine", true}});
|
||||
cachedAddressRpc.addResponse("z_listunspent", json::array());
|
||||
cachedAddressRpc.addResponse("getaddressesbyaccount", json::array());
|
||||
cachedAddressRpc.addResponse("listunspent", json::array());
|
||||
auto cachedAddresses = Refresh::collectAddressRefreshResult(cachedAddressRpc, addressSnapshot);
|
||||
EXPECT_TRUE(cachedAddressRpc.methodNames() == std::vector<std::string>({
|
||||
"z_listaddresses", "z_validateaddress", "z_listunspent",
|
||||
"getaddressesbyaccount", "listunspent"
|
||||
}));
|
||||
EXPECT_FALSE(cachedAddresses.shieldedAddresses[0].has_spending_key);
|
||||
EXPECT_TRUE(cachedAddresses.shieldedAddresses[1].has_spending_key);
|
||||
|
||||
Refresh::TransactionRefreshSnapshot snapshot;
|
||||
snapshot.shieldedAddresses = {"zs-one", "zs-two"};
|
||||
snapshot.sendTxids = {"cached-send", "pending-send"};
|
||||
@@ -1075,6 +1123,227 @@ void testNetworkRefreshRpcCollectors()
|
||||
EXPECT_EQ(placeholderResult.transactions[0].confirmations, 0);
|
||||
EXPECT_EQ(placeholderResult.transactions[0].address, std::string("zs-placeholder-dest"));
|
||||
EXPECT_EQ(placeholderResult.newViewTxEntries.count("placeholder-send"), static_cast<size_t>(0));
|
||||
|
||||
Refresh::TransactionRefreshSnapshot missingAddressesSnapshot;
|
||||
missingAddressesSnapshot.previousTransactions.push_back(dragonx::TransactionInfo{
|
||||
"shielded-fallback", "receive", 1.25, 150, 2, "zs-one", "", "memo"
|
||||
});
|
||||
MockRefreshRpc missingAddressesRpc;
|
||||
missingAddressesRpc.addResponse("listtransactions", json::array());
|
||||
auto missingAddressesResult = Refresh::collectTransactionRefreshResult(
|
||||
missingAddressesRpc, missingAddressesSnapshot, 325, 4);
|
||||
EXPECT_TRUE(missingAddressesRpc.methodNames() == std::vector<std::string>({"listtransactions"}));
|
||||
EXPECT_EQ(missingAddressesResult.transactions.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(missingAddressesResult.transactions[0].txid, std::string("shielded-fallback"));
|
||||
EXPECT_EQ(missingAddressesResult.transactions[0].type, std::string("receive"));
|
||||
|
||||
Refresh::TransactionRefreshSnapshot pendingOpidSnapshot;
|
||||
pendingOpidSnapshot.pendingOpids = {"opid-visible-send"};
|
||||
pendingOpidSnapshot.previousTransactions.push_back(dragonx::TransactionInfo{
|
||||
"opid-visible-send", "send", -4.25, 170, 0, "R-destination", "R-source", ""
|
||||
});
|
||||
MockRefreshRpc pendingOpidRpc;
|
||||
pendingOpidRpc.addResponse("listtransactions", json::array());
|
||||
auto pendingOpidResult = Refresh::collectTransactionRefreshResult(
|
||||
pendingOpidRpc, pendingOpidSnapshot, 326, 4);
|
||||
EXPECT_TRUE(pendingOpidRpc.methodNames() == std::vector<std::string>({"listtransactions"}));
|
||||
EXPECT_EQ(pendingOpidResult.transactions.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(pendingOpidResult.transactions[0].txid, std::string("opid-visible-send"));
|
||||
EXPECT_EQ(pendingOpidResult.transactions[0].type, std::string("send"));
|
||||
EXPECT_NEAR(pendingOpidResult.transactions[0].amount, -4.25, 0.00000001);
|
||||
|
||||
Refresh::TransactionRefreshSnapshot partialFailureSnapshot;
|
||||
partialFailureSnapshot.shieldedAddresses = {"zs-one"};
|
||||
partialFailureSnapshot.previousTransactions.push_back(dragonx::TransactionInfo{
|
||||
"old-receive", "receive", 2.50, 140, 8, "zs-one", "", "old memo"
|
||||
});
|
||||
MockRefreshRpc partialFailureRpc;
|
||||
partialFailureRpc.addResponse("listtransactions", json::array({
|
||||
json{{"txid", "transparent-b"}, {"category", "receive"}, {"amount", 0.75},
|
||||
{"time", 160}, {"confirmations", 2}, {"address", "R-two"}}
|
||||
}));
|
||||
partialFailureRpc.addFailure("z_listreceivedbyaddress", "temporary receive failure");
|
||||
auto partialFailureResult = Refresh::collectTransactionRefreshResult(
|
||||
partialFailureRpc, partialFailureSnapshot, 326, 4);
|
||||
EXPECT_EQ(partialFailureResult.transactions.size(), static_cast<size_t>(2));
|
||||
EXPECT_EQ(partialFailureResult.transactions[0].txid, std::string("transparent-b"));
|
||||
EXPECT_EQ(partialFailureResult.transactions[1].txid, std::string("old-receive"));
|
||||
|
||||
Refresh::TransactionRefreshSnapshot pagedSnapshot;
|
||||
MockRefreshRpc pagedRpc;
|
||||
json firstPage = json::array();
|
||||
for (int i = 0; i < 1000; ++i) {
|
||||
firstPage.push_back(json{{"txid", "paged-" + std::to_string(i)}, {"category", "receive"},
|
||||
{"amount", 0.01}, {"time", i}, {"confirmations", 10},
|
||||
{"address", "R-page"}});
|
||||
}
|
||||
pagedRpc.addResponse("listtransactions", firstPage);
|
||||
pagedRpc.addResponse("listtransactions", json::array({
|
||||
json{{"txid", "paged-1000"}, {"category", "receive"}, {"amount", 0.02},
|
||||
{"time", 2000}, {"confirmations", 11}, {"address", "R-page"}}
|
||||
}));
|
||||
auto pagedResult = Refresh::collectTransactionRefreshResult(pagedRpc, pagedSnapshot, 327, 0);
|
||||
EXPECT_TRUE(pagedRpc.methodNames() == std::vector<std::string>({"listtransactions", "listtransactions"}));
|
||||
EXPECT_EQ(pagedRpc.calls[0].params, json::array({"", 1000, 0}));
|
||||
EXPECT_EQ(pagedRpc.calls[1].params, json::array({"", 1000, 1000}));
|
||||
EXPECT_EQ(pagedResult.transactions.size(), static_cast<size_t>(1001));
|
||||
EXPECT_EQ(pagedResult.transactions[0].txid, std::string("paged-1000"));
|
||||
|
||||
Refresh::TransactionRefreshSnapshot recentSnapshot;
|
||||
dragonx::TransactionInfo previousShielded;
|
||||
previousShielded.txid = "shielded-old";
|
||||
previousShielded.type = "receive";
|
||||
previousShielded.address = "zs-one";
|
||||
previousShielded.amount = 3.0;
|
||||
previousShielded.timestamp = 10;
|
||||
dragonx::TransactionInfo previousTransparent;
|
||||
previousTransparent.txid = "recent-one";
|
||||
previousTransparent.type = "receive";
|
||||
previousTransparent.address = "R-one";
|
||||
previousTransparent.amount = 1.0;
|
||||
previousTransparent.timestamp = 20;
|
||||
recentSnapshot.previousTransactions = {previousShielded, previousTransparent};
|
||||
MockRefreshRpc recentRpc;
|
||||
recentRpc.addResponse("listtransactions", json::array({
|
||||
json{{"txid", "recent-one"}, {"category", "receive"}, {"address", "R-one"}, {"amount", 2.0}, {"time", 30}, {"confirmations", 0}},
|
||||
json{{"txid", "recent-two"}, {"category", "send"}, {"address", "R-two"}, {"amount", -0.5}, {"time", 40}, {"confirmations", 0}}
|
||||
}));
|
||||
auto recent = Refresh::collectRecentTransactionRefreshResult(recentRpc, recentSnapshot, 123);
|
||||
EXPECT_TRUE(recentRpc.methodNames() == std::vector<std::string>({"listtransactions"}));
|
||||
EXPECT_EQ(recentRpc.calls[0].params, json::array({"", 100, 0}));
|
||||
EXPECT_EQ(recent.blockHeight, 123);
|
||||
EXPECT_EQ(recent.transactions.size(), static_cast<size_t>(3));
|
||||
EXPECT_EQ(recent.transactions[0].txid, std::string("recent-two"));
|
||||
EXPECT_EQ(recent.transactions[1].txid, std::string("recent-one"));
|
||||
EXPECT_NEAR(recent.transactions[1].amount, 2.0, 0.00000001);
|
||||
EXPECT_EQ(recent.transactions[2].txid, std::string("shielded-old"));
|
||||
|
||||
Refresh::TransactionRefreshSnapshot recentShieldedProbeSnapshot;
|
||||
recentShieldedProbeSnapshot.shieldedAddresses = {"zs-probe-a", "zs-probe-b"};
|
||||
recentShieldedProbeSnapshot.shieldedScanStartIndex = 1;
|
||||
recentShieldedProbeSnapshot.maxShieldedReceiveScans = 1;
|
||||
recentShieldedProbeSnapshot.shieldedScanHeights = {{"zs-probe-a", 600}, {"zs-probe-b", 600}};
|
||||
MockRefreshRpc recentShieldedProbeRpc;
|
||||
recentShieldedProbeRpc.addResponse("listtransactions", json::array());
|
||||
recentShieldedProbeRpc.addResponse("z_listreceivedbyaddress", json::array({
|
||||
json{{"txid", "same-tip-shielded"}, {"amount", 1.75}, {"confirmations", 0}, {"time", 170}}
|
||||
}));
|
||||
auto recentShieldedProbe = Refresh::collectRecentTransactionRefreshResult(
|
||||
recentShieldedProbeRpc, recentShieldedProbeSnapshot, 600);
|
||||
EXPECT_TRUE(recentShieldedProbeRpc.methodNames() == std::vector<std::string>({
|
||||
"listtransactions", "z_listreceivedbyaddress"
|
||||
}));
|
||||
EXPECT_EQ(recentShieldedProbeRpc.calls[1].params, json::array({"zs-probe-b", 0}));
|
||||
EXPECT_EQ(recentShieldedProbe.nextShieldedScanStartIndex, static_cast<size_t>(0));
|
||||
EXPECT_EQ(recentShieldedProbe.shieldedAddressesScanned, static_cast<size_t>(1));
|
||||
EXPECT_EQ(recentShieldedProbe.shieldedScanHeights.at("zs-probe-b"), 600);
|
||||
EXPECT_EQ(recentShieldedProbe.transactions.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(recentShieldedProbe.transactions[0].txid, std::string("same-tip-shielded"));
|
||||
EXPECT_EQ(recentShieldedProbe.transactions[0].confirmations, 0);
|
||||
|
||||
Refresh::TransactionRefreshSnapshot partialShieldedSnapshot;
|
||||
partialShieldedSnapshot.shieldedAddresses = {"zs-zero", "zs-one", "zs-two"};
|
||||
partialShieldedSnapshot.maxShieldedReceiveScans = 2;
|
||||
partialShieldedSnapshot.previousTransactions.push_back(dragonx::TransactionInfo{
|
||||
"old-zs-two", "receive", 1.0, 90, 10, "zs-two", "", "memo"
|
||||
});
|
||||
MockRefreshRpc partialShieldedRpc;
|
||||
partialShieldedRpc.addResponse("listtransactions", json::array());
|
||||
partialShieldedRpc.addResponse("z_listreceivedbyaddress", json::array({
|
||||
json{{"txid", "new-zs-zero"}, {"amount", 0.5}, {"confirmations", 1}, {"time", 100}}
|
||||
}));
|
||||
partialShieldedRpc.addResponse("z_listreceivedbyaddress", json::array());
|
||||
auto partialShielded = Refresh::collectTransactionRefreshResult(
|
||||
partialShieldedRpc, partialShieldedSnapshot, 400, 0);
|
||||
EXPECT_TRUE(partialShieldedRpc.methodNames() == std::vector<std::string>({
|
||||
"listtransactions", "z_listreceivedbyaddress", "z_listreceivedbyaddress"
|
||||
}));
|
||||
EXPECT_EQ(partialShieldedRpc.calls[1].params, json::array({"zs-zero", 0}));
|
||||
EXPECT_EQ(partialShieldedRpc.calls[2].params, json::array({"zs-one", 0}));
|
||||
EXPECT_FALSE(partialShielded.shieldedScanComplete);
|
||||
EXPECT_EQ(partialShielded.nextShieldedScanStartIndex, static_cast<size_t>(2));
|
||||
EXPECT_EQ(partialShielded.shieldedAddressesScanned, static_cast<size_t>(2));
|
||||
EXPECT_EQ(partialShielded.transactions.size(), static_cast<size_t>(2));
|
||||
EXPECT_EQ(partialShielded.transactions[0].txid, std::string("new-zs-zero"));
|
||||
EXPECT_EQ(partialShielded.transactions[1].txid, std::string("old-zs-two"));
|
||||
|
||||
partialShieldedSnapshot.shieldedScanStartIndex = partialShielded.nextShieldedScanStartIndex;
|
||||
partialShieldedSnapshot.shieldedScanHeights = partialShielded.shieldedScanHeights;
|
||||
MockRefreshRpc finalShieldedRpc;
|
||||
finalShieldedRpc.addResponse("listtransactions", json::array());
|
||||
finalShieldedRpc.addResponse("z_listreceivedbyaddress", json::array());
|
||||
auto finalShielded = Refresh::collectTransactionRefreshResult(
|
||||
finalShieldedRpc, partialShieldedSnapshot, 400, 0);
|
||||
EXPECT_TRUE(finalShieldedRpc.methodNames() == std::vector<std::string>({
|
||||
"listtransactions", "z_listreceivedbyaddress"
|
||||
}));
|
||||
EXPECT_EQ(finalShieldedRpc.calls[1].params, json::array({"zs-two", 0}));
|
||||
EXPECT_TRUE(finalShielded.shieldedScanComplete);
|
||||
EXPECT_EQ(finalShielded.nextShieldedScanStartIndex, static_cast<size_t>(0));
|
||||
EXPECT_EQ(finalShielded.shieldedScanHeights.at("zs-zero"), 400);
|
||||
EXPECT_EQ(finalShielded.shieldedScanHeights.at("zs-one"), 400);
|
||||
EXPECT_EQ(finalShielded.shieldedScanHeights.at("zs-two"), 400);
|
||||
|
||||
Refresh::TransactionRefreshSnapshot cachedShieldedSnapshot;
|
||||
cachedShieldedSnapshot.shieldedAddresses = {"zs-cached"};
|
||||
cachedShieldedSnapshot.shieldedScanHeights = {{"zs-cached", 500}};
|
||||
cachedShieldedSnapshot.previousTransactions.push_back(dragonx::TransactionInfo{
|
||||
"cached-shielded", "receive", 1.5, 120, 20, "zs-cached", "", "cached memo"
|
||||
});
|
||||
MockRefreshRpc cachedShieldedRpc;
|
||||
cachedShieldedRpc.addResponse("listtransactions", json::array());
|
||||
auto cachedShielded = Refresh::collectTransactionRefreshResult(
|
||||
cachedShieldedRpc, cachedShieldedSnapshot, 500, 0);
|
||||
EXPECT_TRUE(cachedShieldedRpc.methodNames() == std::vector<std::string>({"listtransactions"}));
|
||||
EXPECT_TRUE(cachedShielded.shieldedScanComplete);
|
||||
EXPECT_EQ(cachedShielded.shieldedAddressesScanned, static_cast<size_t>(0));
|
||||
EXPECT_EQ(cachedShielded.transactions.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(cachedShielded.transactions[0].txid, std::string("cached-shielded"));
|
||||
|
||||
Refresh::TransactionRefreshSnapshot staleProgressSnapshot;
|
||||
staleProgressSnapshot.shieldedAddresses = {"zs-current", "zs-stale", "zs-missing"};
|
||||
staleProgressSnapshot.shieldedScanHeights = {{"zs-current", 500}, {"zs-stale", 499}};
|
||||
staleProgressSnapshot.maxShieldedReceiveScans = 1;
|
||||
staleProgressSnapshot.previousTransactions.push_back(dragonx::TransactionInfo{
|
||||
"current-shielded", "receive", 0.75, 110, 12, "zs-current", "", ""
|
||||
});
|
||||
MockRefreshRpc staleProgressRpc;
|
||||
staleProgressRpc.addResponse("listtransactions", json::array());
|
||||
staleProgressRpc.addResponse("z_listreceivedbyaddress", json::array({
|
||||
json{{"txid", "stale-shielded"}, {"amount", 2.0}, {"confirmations", 1}, {"time", 130}}
|
||||
}));
|
||||
auto staleProgress = Refresh::collectTransactionRefreshResult(
|
||||
staleProgressRpc, staleProgressSnapshot, 500, 0);
|
||||
EXPECT_TRUE(staleProgressRpc.methodNames() == std::vector<std::string>({
|
||||
"listtransactions", "z_listreceivedbyaddress"
|
||||
}));
|
||||
EXPECT_EQ(staleProgressRpc.calls[1].params, json::array({"zs-stale", 0}));
|
||||
EXPECT_FALSE(staleProgress.shieldedScanComplete);
|
||||
EXPECT_EQ(staleProgress.nextShieldedScanStartIndex, static_cast<size_t>(2));
|
||||
EXPECT_EQ(staleProgress.shieldedAddressesScanned, static_cast<size_t>(1));
|
||||
EXPECT_EQ(staleProgress.shieldedScanHeights.at("zs-current"), 500);
|
||||
EXPECT_EQ(staleProgress.shieldedScanHeights.at("zs-stale"), 500);
|
||||
EXPECT_TRUE(staleProgress.shieldedScanHeights.find("zs-missing") == staleProgress.shieldedScanHeights.end());
|
||||
EXPECT_EQ(staleProgress.transactions.size(), static_cast<size_t>(2));
|
||||
EXPECT_EQ(staleProgress.transactions[0].txid, std::string("stale-shielded"));
|
||||
EXPECT_EQ(staleProgress.transactions[1].txid, std::string("current-shielded"));
|
||||
|
||||
Refresh::TransactionRefreshSnapshot miningSnapshot;
|
||||
miningSnapshot.shieldedAddresses = {"zs-mine"};
|
||||
miningSnapshot.miningAddresses = {"R-mine", "zs-mine"};
|
||||
MockRefreshRpc miningTxRpc;
|
||||
miningTxRpc.addResponse("listtransactions", json::array({
|
||||
json{{"txid", "transparent-mined"}, {"category", "receive"}, {"amount", 3.0},
|
||||
{"time", 220}, {"confirmations", 101}, {"address", "R-mine"}}
|
||||
}));
|
||||
miningTxRpc.addResponse("z_listreceivedbyaddress", json::array({
|
||||
json{{"txid", "shielded-mined"}, {"amount", 4.0}, {"confirmations", 102},
|
||||
{"time", 210}, {"memoStr", "pool"}}
|
||||
}));
|
||||
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"));
|
||||
EXPECT_EQ(miningTxResult.transactions[1].type, std::string("mined"));
|
||||
}
|
||||
|
||||
void testNetworkRefreshResultModels()
|
||||
@@ -1087,7 +1356,7 @@ void testNetworkRefreshResultModels()
|
||||
auto core = Refresh::parseCoreRefreshResult(
|
||||
json{{"private", "1.25000000"}, {"transparent", "0.50000000"}, {"total", "1.75000000"}},
|
||||
true,
|
||||
json{{"blocks", 100}, {"headers", 105}, {"verificationprogress", 0.75},
|
||||
json{{"blocks", 100}, {"headers", 105}, {"bestblockhash", "apply-best-100"}, {"verificationprogress", 0.75},
|
||||
{"longestchain", 110}, {"notarized", 90}},
|
||||
true);
|
||||
Refresh::applyCoreRefreshResult(state, core, 1234);
|
||||
@@ -1096,6 +1365,7 @@ void testNetworkRefreshResultModels()
|
||||
EXPECT_NEAR(state.total_balance, 1.75, 0.00000001);
|
||||
EXPECT_EQ(state.sync.blocks, 100);
|
||||
EXPECT_EQ(state.sync.headers, 105);
|
||||
EXPECT_EQ(state.sync.best_blockhash, std::string("apply-best-100"));
|
||||
EXPECT_TRUE(state.sync.syncing);
|
||||
EXPECT_EQ(state.longestchain, 110);
|
||||
EXPECT_EQ(state.notarized, 90);
|
||||
@@ -1117,7 +1387,6 @@ void testNetworkRefreshResultModels()
|
||||
EXPECT_TRUE(state.encrypted);
|
||||
EXPECT_TRUE(state.locked);
|
||||
EXPECT_TRUE(state.encryption_state_known);
|
||||
|
||||
auto unencrypted = Refresh::parseWalletEncryptionResult(json::object());
|
||||
Refresh::applyWalletEncryptionResult(state, unencrypted);
|
||||
EXPECT_FALSE(state.encrypted);
|
||||
@@ -1297,6 +1566,36 @@ void testNetworkRefreshResultModels()
|
||||
EXPECT_EQ(confirmedBlock, 222);
|
||||
}
|
||||
|
||||
void testOperationStatusPollParsing()
|
||||
{
|
||||
using Refresh = dragonx::services::NetworkRefreshService;
|
||||
using nlohmann::json;
|
||||
|
||||
auto parsed = Refresh::parseOperationStatusPoll(json::array({
|
||||
json{{"id", "op-success"}, {"status", "success"}, {"result", json{{"txid", "tx-success"}}}},
|
||||
json{{"id", "op-failed"}, {"status", "failed"}, {"error", json{{"message", "bad memo"}}}},
|
||||
json{{"id", "op-running"}, {"status", "executing"}}
|
||||
}), {"op-success", "op-failed", "op-running", "op-stale"});
|
||||
|
||||
EXPECT_TRUE(parsed.anySuccess);
|
||||
EXPECT_EQ(parsed.doneOpids.size(), static_cast<size_t>(2));
|
||||
EXPECT_EQ(parsed.doneOpids[0], std::string("op-success"));
|
||||
EXPECT_EQ(parsed.doneOpids[1], std::string("op-failed"));
|
||||
EXPECT_EQ(parsed.successTxids.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(parsed.successTxids[0], std::string("tx-success"));
|
||||
EXPECT_EQ(parsed.successTxidsByOpid.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(parsed.successTxidsByOpid.at("op-success"), std::string("tx-success"));
|
||||
EXPECT_EQ(parsed.failureMessages.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(parsed.failureMessages[0], std::string("bad memo"));
|
||||
EXPECT_EQ(parsed.staleOpids.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(parsed.staleOpids[0], std::string("op-stale"));
|
||||
|
||||
auto malformed = Refresh::parseOperationStatusPoll(json{{"status", "success"}}, {"op-keep"});
|
||||
EXPECT_FALSE(malformed.anySuccess);
|
||||
EXPECT_TRUE(malformed.doneOpids.empty());
|
||||
EXPECT_TRUE(malformed.staleOpids.empty());
|
||||
}
|
||||
|
||||
void testWalletSecurityController()
|
||||
{
|
||||
using dragonx::services::WalletSecurityController;
|
||||
@@ -1677,9 +1976,13 @@ void testRendererHelpers()
|
||||
EXPECT_FALSE(dragonx::ui::IsPoolMiningActive(true, false, true));
|
||||
EXPECT_TRUE(dragonx::ui::IsPoolMiningActive(false, false, true));
|
||||
|
||||
dragonx::ui::ConsoleOutputFilter filter{"error", false, false, 10, 20};
|
||||
dragonx::ui::ConsoleOutputFilter filter{"error", false, false, false, 10, 20, 30};
|
||||
EXPECT_TRUE(dragonx::ui::consoleLinePassesFilter("RPC Error", 20, filter));
|
||||
EXPECT_FALSE(dragonx::ui::consoleLinePassesFilter("daemon line", 10, filter));
|
||||
filter.text.clear();
|
||||
EXPECT_FALSE(dragonx::ui::consoleLinePassesFilter("[rpc] History -> listtransactions", 30, filter));
|
||||
filter.rpcTraceEnabled = true;
|
||||
EXPECT_TRUE(dragonx::ui::consoleLinePassesFilter("[rpc] History -> listtransactions", 30, filter));
|
||||
filter.daemonMessagesEnabled = true;
|
||||
filter.errorsOnly = true;
|
||||
filter.text.clear();
|
||||
@@ -1838,9 +2141,9 @@ void testBalanceAddressListModel()
|
||||
addresses.push_back(tFav);
|
||||
|
||||
std::vector<dragonx::ui::AddressListInput> inputs = {
|
||||
{&addresses[0], true, false, false, "Main Vault", "", -1},
|
||||
{&addresses[1], false, false, false, "Empty", "", -1},
|
||||
{&addresses[2], false, false, true, "Favorite", "", -1}
|
||||
{&addresses[0], true, false, false, false, "Main Vault", "", -1},
|
||||
{&addresses[1], false, false, false, false, "Empty", "", -1},
|
||||
{&addresses[2], false, false, true, false, "Favorite", "", -1}
|
||||
};
|
||||
|
||||
auto rows = dragonx::ui::BuildAddressListRows(inputs, "", true, false);
|
||||
@@ -1869,6 +2172,255 @@ void testBalanceAddressListModel()
|
||||
EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(0.0, 2.0), std::string(""));
|
||||
}
|
||||
|
||||
void testExplorerBlockCache()
|
||||
{
|
||||
using dragonx::ui::ExplorerBlockCache;
|
||||
using dragonx::ui::ExplorerBlockSummary;
|
||||
using nlohmann::json;
|
||||
|
||||
fs::path dir = makeTempDir();
|
||||
fs::path databasePath = dir / "explorer_blocks.sqlite";
|
||||
fs::path legacyPath = dir / "explorer_blocks_cache.json";
|
||||
|
||||
json legacy = {
|
||||
{"version", 1},
|
||||
{"tip_height", 10},
|
||||
{"tip_hash", "hash-10"},
|
||||
{"blocks", json::array({
|
||||
json{{"height", 10}, {"hash", "hash-10"}, {"tx_count", 3}, {"size", 1000}, {"time", 5000}, {"difficulty", 1.25}},
|
||||
json{{"height", 9}, {"hash", "hash-9"}, {"tx_count", 2}, {"size", 900}, {"time", 4900}, {"difficulty", 1.20}}
|
||||
})}
|
||||
};
|
||||
{
|
||||
std::ofstream file(legacyPath);
|
||||
file << legacy.dump(2);
|
||||
}
|
||||
|
||||
ExplorerBlockCache cache(databasePath.string(), legacyPath.string());
|
||||
EXPECT_TRUE(cache.ensureOpen());
|
||||
EXPECT_EQ(cache.cachedBlockCount(), 2);
|
||||
|
||||
auto range = cache.loadRange(9, 10);
|
||||
EXPECT_EQ(range.size(), static_cast<size_t>(2));
|
||||
EXPECT_EQ(range[10].hash, std::string("hash-10"));
|
||||
EXPECT_EQ(range[9].tx_count, 2);
|
||||
|
||||
auto sameTipValidation = cache.prepareValidation(10, "hash-10");
|
||||
EXPECT_FALSE(sameTipValidation.needed);
|
||||
|
||||
auto advancedValidation = cache.prepareValidation(12, "hash-12");
|
||||
EXPECT_TRUE(advancedValidation.needed);
|
||||
EXPECT_EQ(advancedValidation.height, 10);
|
||||
EXPECT_EQ(advancedValidation.expectedHash, std::string("hash-10"));
|
||||
cache.applySavedTipValidation(advancedValidation, "hash-10", 12, "hash-12");
|
||||
|
||||
ExplorerBlockSummary block12;
|
||||
block12.height = 12;
|
||||
block12.hash = "hash-12";
|
||||
block12.tx_count = 5;
|
||||
block12.size = 1200;
|
||||
block12.time = 5200;
|
||||
block12.difficulty = 1.40;
|
||||
EXPECT_TRUE(cache.storeBlock(block12));
|
||||
EXPECT_EQ(cache.loadRange(12, 12)[12].tx_count, 5);
|
||||
|
||||
{
|
||||
ExplorerBlockCache reopened(databasePath.string(), legacyPath.string());
|
||||
EXPECT_TRUE(reopened.ensureOpen());
|
||||
EXPECT_EQ(reopened.cachedBlockCount(), 3);
|
||||
auto noRepeatValidation = reopened.prepareValidation(12, "hash-12");
|
||||
EXPECT_FALSE(noRepeatValidation.needed);
|
||||
}
|
||||
|
||||
cache.prepareValidation(12, "different-tip");
|
||||
EXPECT_EQ(cache.cachedBlockCount(), 0);
|
||||
|
||||
{
|
||||
ExplorerBlockCache reopened(databasePath.string(), legacyPath.string());
|
||||
EXPECT_TRUE(reopened.ensureOpen());
|
||||
EXPECT_EQ(reopened.cachedBlockCount(), 0);
|
||||
}
|
||||
|
||||
fs::remove_all(dir);
|
||||
}
|
||||
|
||||
void testTransactionHistoryCache()
|
||||
{
|
||||
using dragonx::TransactionInfo;
|
||||
using dragonx::data::TransactionHistoryCache;
|
||||
|
||||
std::string identityA = TransactionHistoryCache::walletIdentityFromAddresses(
|
||||
{"zs-beta", "zs-alpha"}, {"R-one"});
|
||||
std::string identityB = TransactionHistoryCache::walletIdentityFromAddresses(
|
||||
{"zs-alpha", "zs-beta"}, {"R-one"});
|
||||
std::string identityWithSameText = TransactionHistoryCache::walletIdentityFromAddresses(
|
||||
{"same-address-text"}, {"same-address-text"});
|
||||
EXPECT_EQ(identityA, identityB);
|
||||
EXPECT_TRUE(identityA.find("protocol=") == std::string::npos);
|
||||
EXPECT_TRUE(identityA.find("p2p_port=") == std::string::npos);
|
||||
EXPECT_TRUE(identityA.find("z:zs-alpha") != std::string::npos);
|
||||
EXPECT_TRUE(identityA.find("t:R-one") != std::string::npos);
|
||||
EXPECT_TRUE(identityWithSameText.find("z:same-address-text") != std::string::npos);
|
||||
EXPECT_TRUE(identityWithSameText.find("t:same-address-text") != std::string::npos);
|
||||
|
||||
fs::path dir = makeTempDir();
|
||||
fs::path databasePath = dir / "transaction_history.sqlite";
|
||||
std::string walletIdentity = "mainnet|R-alpha|zs-beta";
|
||||
std::string passphrase = "correct horse battery staple";
|
||||
|
||||
std::vector<TransactionInfo> transactions;
|
||||
transactions.push_back(TransactionInfo{
|
||||
"tx-sensitive-send", "send", -1.25, 1700000100, 12,
|
||||
"zs-destination-sensitive", "R-source-sensitive", "private memo"
|
||||
});
|
||||
transactions.push_back(TransactionInfo{
|
||||
"tx-mined", "mined", 3.5, 1700000000, 104,
|
||||
"R-mining-address", "", ""
|
||||
});
|
||||
std::unordered_map<std::string, int> shieldedScanHeights{
|
||||
{"zs-beta", 120},
|
||||
{"zs-archive", 118}
|
||||
};
|
||||
|
||||
{
|
||||
TransactionHistoryCache cache(databasePath.string());
|
||||
EXPECT_TRUE(cache.ensureOpen());
|
||||
EXPECT_TRUE(cache.unlockWithPassphrase(walletIdentity, passphrase));
|
||||
EXPECT_TRUE(cache.replace(walletIdentity, 120, "tip-120", transactions, 1700000200, shieldedScanHeights));
|
||||
EXPECT_EQ(cache.snapshotCount(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
std::ifstream database(databasePath, std::ios::binary);
|
||||
std::string bytes((std::istreambuf_iterator<char>(database)), std::istreambuf_iterator<char>());
|
||||
EXPECT_TRUE(bytes.find("tx-sensitive-send") == std::string::npos);
|
||||
EXPECT_TRUE(bytes.find("zs-destination-sensitive") == std::string::npos);
|
||||
EXPECT_TRUE(bytes.find("zs-archive") == std::string::npos);
|
||||
EXPECT_TRUE(bytes.find("private memo") == std::string::npos);
|
||||
}
|
||||
|
||||
{
|
||||
TransactionHistoryCache wrongKey(databasePath.string());
|
||||
EXPECT_TRUE(wrongKey.unlockWithPassphrase(walletIdentity, "wrong passphrase"));
|
||||
auto wrongLoad = wrongKey.load(walletIdentity, 120, "tip-120");
|
||||
EXPECT_FALSE(wrongLoad.loaded);
|
||||
}
|
||||
|
||||
{
|
||||
TransactionHistoryCache reopened(databasePath.string());
|
||||
EXPECT_TRUE(reopened.unlockWithPassphrase(walletIdentity, passphrase));
|
||||
auto loaded = reopened.load(walletIdentity, 120, "tip-120");
|
||||
EXPECT_TRUE(loaded.loaded);
|
||||
EXPECT_FALSE(loaded.invalidated);
|
||||
EXPECT_EQ(loaded.tipHeight, 120);
|
||||
EXPECT_EQ(loaded.tipHash, std::string("tip-120"));
|
||||
EXPECT_EQ(loaded.updatedAt, static_cast<std::time_t>(1700000200));
|
||||
EXPECT_EQ(loaded.transactions.size(), static_cast<size_t>(2));
|
||||
EXPECT_EQ(loaded.transactions[0].txid, std::string("tx-sensitive-send"));
|
||||
EXPECT_EQ(loaded.transactions[0].memo, std::string("private memo"));
|
||||
EXPECT_EQ(loaded.transactions[1].type, std::string("mined"));
|
||||
EXPECT_EQ(loaded.shieldedScanHeights.size(), static_cast<size_t>(2));
|
||||
EXPECT_EQ(loaded.shieldedScanHeights.at("zs-beta"), 120);
|
||||
EXPECT_EQ(loaded.shieldedScanHeights.at("zs-archive"), 118);
|
||||
}
|
||||
|
||||
{
|
||||
TransactionHistoryCache cache(databasePath.string());
|
||||
EXPECT_TRUE(cache.unlockWithPassphrase(walletIdentity, passphrase));
|
||||
EXPECT_TRUE(cache.replace(walletIdentity, 120, "tip-120", transactions, 1700000250));
|
||||
auto loaded = cache.load(walletIdentity, 120, "tip-120");
|
||||
EXPECT_TRUE(loaded.loaded);
|
||||
EXPECT_EQ(loaded.shieldedScanHeights.size(), static_cast<size_t>(0));
|
||||
}
|
||||
|
||||
{
|
||||
TransactionHistoryCache staleTip(databasePath.string());
|
||||
EXPECT_TRUE(staleTip.unlockWithPassphrase(walletIdentity, passphrase));
|
||||
auto invalidated = staleTip.load(walletIdentity, 119, "tip-119");
|
||||
EXPECT_FALSE(invalidated.loaded);
|
||||
EXPECT_TRUE(invalidated.invalidated);
|
||||
EXPECT_EQ(staleTip.snapshotCount(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
TransactionHistoryCache cache(databasePath.string());
|
||||
EXPECT_TRUE(cache.unlockWithPassphrase(walletIdentity, passphrase));
|
||||
EXPECT_TRUE(cache.replace(walletIdentity, 120, "tip-120", transactions, 1700000300));
|
||||
auto invalidated = cache.load(walletIdentity, 120, "different-tip");
|
||||
EXPECT_FALSE(invalidated.loaded);
|
||||
EXPECT_TRUE(invalidated.invalidated);
|
||||
EXPECT_EQ(cache.snapshotCount(), 0);
|
||||
}
|
||||
|
||||
fs::remove_all(dir);
|
||||
}
|
||||
|
||||
void testTransactionHistoryCacheRefreshApply()
|
||||
{
|
||||
using Refresh = dragonx::services::NetworkRefreshService;
|
||||
using dragonx::TransactionInfo;
|
||||
using dragonx::data::TransactionHistoryCache;
|
||||
|
||||
fs::path dir = makeTempDir();
|
||||
fs::path databasePath = dir / "transaction_history_refresh.sqlite";
|
||||
std::string walletIdentity = "mainnet|R-refresh-cache|zs-refresh-cache";
|
||||
std::string passphrase = "refresh cache passphrase";
|
||||
|
||||
dragonx::WalletState state;
|
||||
state.sync.blocks = 333;
|
||||
state.sync.best_blockhash = "tip-333";
|
||||
|
||||
Refresh::TransactionRefreshResult refreshResult;
|
||||
refreshResult.blockHeight = 333;
|
||||
TransactionInfo refreshedTx;
|
||||
refreshedTx.txid = "rpc-refreshed-send";
|
||||
refreshedTx.type = "send";
|
||||
refreshedTx.amount = -2.75;
|
||||
refreshedTx.timestamp = 1700000400;
|
||||
refreshedTx.confirmations = 14;
|
||||
refreshedTx.address = "zs-rpc-destination";
|
||||
refreshedTx.from_address = "R-refresh-cache";
|
||||
refreshedTx.memo = "rpc refreshed memo";
|
||||
refreshResult.transactions = {refreshedTx};
|
||||
|
||||
Refresh::TransactionViewCache viewCache;
|
||||
std::unordered_set<std::string> sendTxids;
|
||||
std::vector<TransactionInfo> confirmedCache;
|
||||
std::unordered_set<std::string> confirmedIds;
|
||||
int confirmedBlock = -1;
|
||||
int lastTxBlock = -1;
|
||||
Refresh::TransactionCacheUpdate cacheUpdate{
|
||||
viewCache,
|
||||
sendTxids,
|
||||
confirmedCache,
|
||||
confirmedIds,
|
||||
confirmedBlock,
|
||||
lastTxBlock
|
||||
};
|
||||
Refresh::applyTransactionRefreshResult(state, cacheUpdate, std::move(refreshResult), 1700000500);
|
||||
|
||||
TransactionHistoryCache cache(databasePath.string());
|
||||
EXPECT_TRUE(cache.unlockWithPassphrase(walletIdentity, passphrase));
|
||||
EXPECT_TRUE(cache.replace(walletIdentity,
|
||||
state.sync.blocks,
|
||||
state.sync.best_blockhash,
|
||||
state.transactions,
|
||||
state.last_tx_update));
|
||||
|
||||
TransactionHistoryCache reopened(databasePath.string());
|
||||
EXPECT_TRUE(reopened.unlockWithPassphrase(walletIdentity, passphrase));
|
||||
auto loaded = reopened.load(walletIdentity, 333, "tip-333");
|
||||
EXPECT_TRUE(loaded.loaded);
|
||||
EXPECT_EQ(loaded.transactions.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(loaded.transactions[0].txid, std::string("rpc-refreshed-send"));
|
||||
EXPECT_EQ(loaded.transactions[0].memo, std::string("rpc refreshed memo"));
|
||||
EXPECT_EQ(loaded.updatedAt, static_cast<std::time_t>(1700000500));
|
||||
EXPECT_EQ(lastTxBlock, 333);
|
||||
EXPECT_EQ(confirmedIds.count("rpc-refreshed-send"), static_cast<size_t>(1));
|
||||
|
||||
fs::remove_all(dir);
|
||||
}
|
||||
|
||||
void testGeneratedResourceBehavior()
|
||||
{
|
||||
const auto* themes = dragonx::resources::getEmbeddedThemes();
|
||||
@@ -1893,6 +2445,7 @@ int main()
|
||||
testNetworkRefreshSnapshotHelpers();
|
||||
testNetworkRefreshRpcCollectors();
|
||||
testNetworkRefreshResultModels();
|
||||
testOperationStatusPollParsing();
|
||||
testWalletSecurityController();
|
||||
testWalletSecurityWorkflow();
|
||||
testWalletSecurityWorkflowExecutor();
|
||||
@@ -1903,6 +2456,9 @@ int main()
|
||||
testConsoleInputModel();
|
||||
testMiningBenchmarkModel();
|
||||
testBalanceAddressListModel();
|
||||
testExplorerBlockCache();
|
||||
testTransactionHistoryCache();
|
||||
testTransactionHistoryCacheRefreshApply();
|
||||
testGeneratedResourceBehavior();
|
||||
|
||||
if (g_failures != 0) {
|
||||
|
||||
Reference in New Issue
Block a user