diff --git a/src/services/network_refresh_service.cpp b/src/services/network_refresh_service.cpp index 24d537d..2421cb6 100644 --- a/src/services/network_refresh_service.cpp +++ b/src/services/network_refresh_service.cpp @@ -48,6 +48,76 @@ void applyBalancesFromUnspent(std::vector& addresses, const json& u } } +bool hasTransactionType(const std::vector& transactions, + const std::string& txid, + const std::string& type) +{ + for (const auto& transaction : transactions) { + if (transaction.txid == txid && transaction.type == type) return true; + } + return false; +} + +bool tryFetchRawTransaction(NetworkRefreshService::RefreshRpcGateway& rpc, + const std::string& txid, + json& rawTransaction) +{ + try { + rawTransaction = rpc.call("gettransaction", json::array({txid})); + return !rawTransaction.is_null(); + } catch (...) { + return false; + } +} + +void applyRawTransactionMetadata(TransactionInfo& info, + const json& rawTransaction, + bool includeSendDetails) +{ + if (rawTransaction.is_null()) return; + + if (auto value = readOptional(rawTransaction, "time")) info.timestamp = *value; + else if (auto value = readOptional(rawTransaction, "timereceived")) info.timestamp = *value; + if (auto value = readOptional(rawTransaction, "confirmations")) info.confirmations = *value; + + if (!includeSendDetails) return; + + if (auto value = readOptional(rawTransaction, "amount")) info.amount = *value; + + if (!rawTransaction.contains("details") || !rawTransaction["details"].is_array()) return; + for (const auto& detail : rawTransaction["details"]) { + auto category = readOptional(detail, "category"); + if (!category || *category != "send") continue; + + if (auto value = readOptional(detail, "amount")) info.amount = *value; + if (auto value = readOptional(detail, "address")) info.address = *value; + break; + } +} + +void applyRawTransactionMetadata(NetworkRefreshService::TransactionViewCacheEntry& entry, + const json& rawTransaction) +{ + if (rawTransaction.is_null()) return; + + if (auto value = readOptional(rawTransaction, "time")) entry.timestamp = *value; + else if (auto value = readOptional(rawTransaction, "timereceived")) entry.timestamp = *value; + if (auto value = readOptional(rawTransaction, "confirmations")) entry.confirmations = *value; +} + +void appendTrackedSendPlaceholder(std::vector& transactions, + const std::string& txid, + const json* rawTransaction) +{ + if (hasTransactionType(transactions, txid, "send")) return; + + TransactionInfo info; + info.txid = txid; + info.type = "send"; + if (rawTransaction) applyRawTransactionMetadata(info, *rawTransaction, true); + transactions.push_back(std::move(info)); +} + } // namespace NetworkRefreshService::ConnectionInfoResult NetworkRefreshService::parseConnectionInfoResult(const json& info) @@ -577,13 +647,19 @@ void NetworkRefreshService::appendViewTransactionOutputs(std::vector 0; auto cached = snapshot.viewTxCache.find(txid); if (cached != snapshot.viewTxCache.end()) { - appendViewTransactionOutputs(result.transactions, txid, cached->second); - continue; + if (!trackedSend || !cached->second.outgoing_outputs.empty()) { + appendViewTransactionOutputs(result.transactions, txid, cached->second); + continue; + } } + if (!trackedSend && snapshot.fullyEnrichedTxids.count(txid)) continue; + if (viewTxCount >= maxViewTransactionsPerCycle) break; ++viewTxCount; try { json viewTransaction = rpc.call("z_viewtransaction", json::array({txid})); - if (viewTransaction.is_null() || !viewTransaction.is_object()) continue; + if (viewTransaction.is_null() || !viewTransaction.is_object()) { + if (trackedSend) { + json rawTransaction; + bool hasRawTransaction = tryFetchRawTransaction(rpc, txid, rawTransaction); + appendTrackedSendPlaceholder(result.transactions, + txid, + hasRawTransaction ? &rawTransaction : nullptr); + } + continue; + } auto entry = parseViewTransactionCacheEntry(viewTransaction); appendViewTransactionOutputs(result.transactions, txid, entry); - for (auto& info : result.transactions) { - if (info.txid != txid || info.timestamp != 0) continue; - - try { - json rawTransaction = rpc.call("gettransaction", json::array({txid})); - if (!rawTransaction.is_null()) { - if (auto value = readOptional(rawTransaction, "time")) info.timestamp = *value; - if (auto value = readOptional(rawTransaction, "confirmations")) info.confirmations = *value; + json rawTransaction; + bool hasRawTransaction = false; + bool needsRawTransaction = false; + for (const auto& info : result.transactions) { + if (info.txid == txid && info.timestamp == 0) { + needsRawTransaction = true; + break; + } + } + if (needsRawTransaction) { + hasRawTransaction = tryFetchRawTransaction(rpc, txid, rawTransaction); + if (hasRawTransaction) { + applyRawTransactionMetadata(entry, rawTransaction); + for (auto& info : result.transactions) { + if (info.txid == txid && info.timestamp == 0) { + applyRawTransactionMetadata(info, rawTransaction, false); + } } - } catch (...) {} - break; + } } - result.newViewTxEntries[txid] = std::move(entry); + if (trackedSend && !hasTransactionType(result.transactions, txid, "send")) { + if (!hasRawTransaction) hasRawTransaction = tryFetchRawTransaction(rpc, txid, rawTransaction); + appendTrackedSendPlaceholder(result.transactions, + txid, + hasRawTransaction ? &rawTransaction : nullptr); + } + + if (!trackedSend || !entry.outgoing_outputs.empty()) { + result.newViewTxEntries[txid] = std::move(entry); + } } catch (const std::exception& e) { (void)e; + if (trackedSend) { + json rawTransaction; + bool hasRawTransaction = tryFetchRawTransaction(rpc, txid, rawTransaction); + appendTrackedSendPlaceholder(result.transactions, + txid, + hasRawTransaction ? &rawTransaction : nullptr); + } } } @@ -817,8 +934,9 @@ void NetworkRefreshService::applyTransactionRefreshResult(WalletState& state, cacheUpdate.lastTxBlockHeight = result.blockHeight; for (auto& [txid, entry] : result.newViewTxEntries) { + bool hasOutgoingOutputs = !entry.outgoing_outputs.empty(); cacheUpdate.viewTxCache[txid] = std::move(entry); - cacheUpdate.sendTxids.erase(txid); + if (hasOutgoingOutputs) cacheUpdate.sendTxids.erase(txid); } cacheUpdate.confirmedTxCache.clear(); diff --git a/src/services/network_refresh_service.h b/src/services/network_refresh_service.h index b3303f2..62cead8 100644 --- a/src/services/network_refresh_service.h +++ b/src/services/network_refresh_service.h @@ -148,6 +148,8 @@ public: struct TransactionViewCacheEntry { std::string from_address; + std::int64_t timestamp = 0; + int confirmations = 0; struct Output { std::string address; double value = 0.0; diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index f9fd41e..8723e1d 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -1000,12 +1000,81 @@ void testNetworkRefreshRpcCollectors() EXPECT_EQ(transactionResult.blockHeight, 321); EXPECT_EQ(transactionResult.newViewTxEntries.size(), static_cast(1)); EXPECT_EQ(transactionResult.newViewTxEntries.count("pending-send"), static_cast(1)); + EXPECT_EQ(transactionResult.newViewTxEntries.at("pending-send").timestamp, static_cast(500)); + EXPECT_EQ(transactionResult.newViewTxEntries.at("pending-send").confirmations, 1); EXPECT_EQ(transactionResult.transactions.size(), static_cast(4)); EXPECT_EQ(transactionResult.transactions.front().txid, std::string("pending-send")); EXPECT_EQ(transactionResult.transactions.front().type, std::string("send")); EXPECT_NEAR(transactionResult.transactions.front().amount, -0.40, 0.00000001); EXPECT_EQ(transactionResult.transactions.front().timestamp, static_cast(500)); EXPECT_EQ(transactionResult.transactions[1].txid, std::string("transparent-a")); + + Refresh::TransactionRefreshSnapshot cachedOnlySnapshot; + auto cachedOnlyEntry = cachedEntry; + cachedOnlyEntry.timestamp = 450; + cachedOnlyEntry.confirmations = 6; + cachedOnlySnapshot.viewTxCache["cached-only-send"] = cachedOnlyEntry; + cachedOnlySnapshot.fullyEnrichedTxids = {"cached-only-send"}; + MockRefreshRpc cachedOnlyRpc; + cachedOnlyRpc.addResponse("listtransactions", json::array()); + auto cachedOnlyResult = Refresh::collectTransactionRefreshResult(cachedOnlyRpc, cachedOnlySnapshot, 322, 4); + EXPECT_TRUE(cachedOnlyRpc.methodNames() == std::vector({"listtransactions"})); + EXPECT_EQ(cachedOnlyResult.transactions.size(), static_cast(1)); + EXPECT_EQ(cachedOnlyResult.transactions[0].txid, std::string("cached-only-send")); + EXPECT_EQ(cachedOnlyResult.transactions[0].type, std::string("send")); + EXPECT_NEAR(cachedOnlyResult.transactions[0].amount, -0.20, 0.00000001); + EXPECT_EQ(cachedOnlyResult.transactions[0].timestamp, static_cast(450)); + EXPECT_EQ(cachedOnlyResult.transactions[0].confirmations, 6); + + Refresh::TransactionRefreshSnapshot retrySnapshot; + retrySnapshot.sendTxids = {"retry-send"}; + retrySnapshot.viewTxCache["retry-send"] = Refresh::TransactionViewCacheEntry{}; + retrySnapshot.fullyEnrichedTxids = {"retry-send"}; + MockRefreshRpc retryRpc; + retryRpc.addResponse("listtransactions", json::array()); + retryRpc.addResponse("z_viewtransaction", json{ + {"spends", json::array({json{{"address", "zs-retry-from"}}})}, + {"outputs", json::array({ + json{{"outgoing", true}, {"address", "zs-retry-dest"}, {"value", 0.55}} + })} + }); + retryRpc.addResponse("gettransaction", json{{"time", 650}, {"confirmations", 2}}); + auto retryResult = Refresh::collectTransactionRefreshResult(retryRpc, retrySnapshot, 323, 4); + EXPECT_TRUE(retryRpc.methodNames() == std::vector({ + "listtransactions", "z_viewtransaction", "gettransaction" + })); + EXPECT_EQ(retryResult.transactions.size(), static_cast(1)); + EXPECT_EQ(retryResult.transactions[0].txid, std::string("retry-send")); + EXPECT_NEAR(retryResult.transactions[0].amount, -0.55, 0.00000001); + EXPECT_EQ(retryResult.newViewTxEntries.count("retry-send"), static_cast(1)); + EXPECT_EQ(retryResult.newViewTxEntries.at("retry-send").timestamp, static_cast(650)); + EXPECT_EQ(retryResult.newViewTxEntries.at("retry-send").confirmations, 2); + + Refresh::TransactionRefreshSnapshot placeholderSnapshot; + placeholderSnapshot.sendTxids = {"placeholder-send"}; + MockRefreshRpc placeholderRpc; + placeholderRpc.addResponse("listtransactions", json::array()); + placeholderRpc.addResponse("z_viewtransaction", json{{"spends", json::array()}, {"outputs", json::array()}}); + placeholderRpc.addResponse("gettransaction", json{ + {"time", 700}, + {"confirmations", 0}, + {"amount", -0.33}, + {"details", json::array({ + json{{"category", "send"}, {"address", "zs-placeholder-dest"}, {"amount", -0.33}} + })} + }); + auto placeholderResult = Refresh::collectTransactionRefreshResult(placeholderRpc, placeholderSnapshot, 324, 4); + EXPECT_TRUE(placeholderRpc.methodNames() == std::vector({ + "listtransactions", "z_viewtransaction", "gettransaction" + })); + EXPECT_EQ(placeholderResult.transactions.size(), static_cast(1)); + EXPECT_EQ(placeholderResult.transactions[0].txid, std::string("placeholder-send")); + EXPECT_EQ(placeholderResult.transactions[0].type, std::string("send")); + EXPECT_NEAR(placeholderResult.transactions[0].amount, -0.33, 0.00000001); + EXPECT_EQ(placeholderResult.transactions[0].timestamp, static_cast(700)); + 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(0)); } void testNetworkRefreshResultModels() @@ -1198,9 +1267,10 @@ void testNetworkRefreshResultModels() pendingTx.timestamp = 1001; transactionResult.transactions = {pendingTx, confirmedTx}; transactionResult.newViewTxEntries["shielded-send"] = viewEntry; + transactionResult.newViewTxEntries["pending-empty"] = Refresh::TransactionViewCacheEntry{}; Refresh::TransactionViewCache viewCache; - std::unordered_set sendTxids{"shielded-send", "pending"}; + std::unordered_set sendTxids{"shielded-send", "pending", "pending-empty"}; std::vector confirmedCache; std::unordered_set confirmedIds; int confirmedBlock = -1; @@ -1217,9 +1287,10 @@ void testNetworkRefreshResultModels() EXPECT_EQ(state.transactions.size(), static_cast(2)); EXPECT_EQ(state.last_tx_update, static_cast(4567)); EXPECT_EQ(lastTxBlock, 222); - EXPECT_EQ(viewCache.size(), static_cast(1)); + EXPECT_EQ(viewCache.size(), static_cast(2)); EXPECT_EQ(sendTxids.count("shielded-send"), static_cast(0)); EXPECT_EQ(sendTxids.count("pending"), static_cast(1)); + EXPECT_EQ(sendTxids.count("pending-empty"), static_cast(1)); EXPECT_EQ(confirmedCache.size(), static_cast(1)); EXPECT_EQ(confirmedCache[0].txid, std::string("confirmed")); EXPECT_EQ(confirmedIds.count("confirmed"), static_cast(1));