feat(chat): harvest chat memos on the lite variant (lite receive)

The full-node receive harvest works off z_viewtransaction; the lite wallet's
transactions come from the backend instead, so lite chat receive was unwired.

App::ingestLiteChatMemos runs after each lite refresh: the backend lists one
entry per received (non-change) note — same txid, distinct position + decoded
UTF-8 memo — so a chat tx yields two entries (header + payload). Group the
Receive-kind notes by txid, feed them through the SAME extractHushChatTransaction
Metadata parser (now order-tolerant, so the daemon's output shuffle is a non-issue),
and thread the result into ChatService. The store dedups (txid+position), so
re-listing every refresh is harmless.

This closes the lite variant's chat loop end-to-end: identity (exportSeed), DB
unlock/load, receive (here), UI, and send (broadcastChatMemosLite). Gated by
DRAGONX_ENABLE_CHAT (default OFF). Verified: Linux + Windows build with chat ON,
ctest 100%, hygiene clean; caches restored to OFF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 17:05:20 -05:00
parent 2e6188a36d
commit 008cc82ee5
3 changed files with 35 additions and 1 deletions

View File

@@ -38,6 +38,7 @@
#include <cctype>
#include "config/settings.h"
#include "wallet/lite_wallet_controller.h" // lite send/new-address routing
#include "wallet/lite_wallet_state_mapper.h" // LiteWalletAppRefreshModel for lite chat harvest
#include "daemon/daemon_controller.h"
#include "daemon/embedded_daemon.h"
#include "daemon/xmrig_manager.h"
@@ -2630,6 +2631,35 @@ void App::startChatConversation(const std::string& peerZaddr, const std::string&
ui::Notifications::instance().success("Contact request queued.");
}
// HushChat (lite variant): the full-node harvest works off z_viewtransaction, but the lite wallet's
// transactions come from the backend. The backend lists one entry per received (non-change) note —
// same txid, distinct position + decoded-UTF-8 memo — so a chat tx yields two entries (header +
// payload). Group the received notes by txid, run them through the SAME parser, and thread the
// result. The store dedups (txid+position), so re-listing every refresh is harmless.
void App::ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model)
{
if (!chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
std::unordered_map<std::string, chat::HushChatTransactionInput> byTxid;
std::unordered_map<std::string, std::int64_t> txTimestamps;
for (const auto& tx : model.transactions) {
if (tx.kind != wallet::LiteWalletAppTransactionKind::Receive) continue; // only incoming carry chat
if (tx.memo.empty() || !tx.position) continue;
auto& input = byTxid[tx.txid];
input.txid = tx.txid;
input.outputs.push_back({static_cast<std::size_t>(*tx.position), tx.memo});
txTimestamps[tx.txid] = tx.timestamp;
}
if (byTxid.empty()) return;
std::vector<chat::HushChatTransactionMetadata> metadata;
for (auto& entry : byTxid) {
auto extracted = chat::extractHushChatTransactionMetadata(entry.second, true);
for (auto& meta : extracted.metadata) metadata.push_back(std::move(meta));
}
if (!metadata.empty()) chat_service_.ingest(metadata, txTimestamps, std::time(nullptr));
}
void App::exportAllKeys(std::function<void(const std::string&, int, int)> callback)
{
if (!state_.connected || !rpc_) {