feat(chat): 0-conf fast-scan so incoming messages show before a block
The receive harvest gates each z-address behind scannedAtTip — it only re-scans when a new block advances the tip — so incoming chat waited ~1 confirmation even though the daemon already exposes mempool notes (FindMySaplingNotes runs on mempool txs; z_listreceivedbyaddress(addr,0) returns them). Add App::fastScanChatMemos(): every transaction-refresh cycle, re-scan JUST the chat reply address (where peers send) at minconf=0, extract chat metadata, and ingest — so messages surface at mempool speed (a few seconds) instead of waiting for a block. Full-node only (lite has its own harvest). An in-flight guard avoids stacking RPCs; the store dedups on txid+position, so the confirmed harvest never double-inserts. Hidden conversations are deliberately skipped by the fast path — they don't get the mempool speed-up and still come back through the normal confirmed harvest (which un-hides on a new message). New non-muted messages toast off-tab as usual. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1168,6 +1168,9 @@ void App::update()
|
|||||||
} else if (walletDataPage && shouldRefreshRecentTransactions()) {
|
} else if (walletDataPage && shouldRefreshRecentTransactions()) {
|
||||||
refreshRecentTransactionData();
|
refreshRecentTransactionData();
|
||||||
}
|
}
|
||||||
|
// 0-conf chat: the normal harvest above only re-scans on a new block, so also do a light
|
||||||
|
// mempool scan of the chat address every cycle to surface messages before confirmation.
|
||||||
|
fastScanChatMemos();
|
||||||
}
|
}
|
||||||
if (network_refresh_.consumeDue(RefreshTimer::Addresses)) {
|
if (network_refresh_.consumeDue(RefreshTimer::Addresses)) {
|
||||||
if (walletDataPage || addresses_dirty_ || hasTransactionSendProgress()) {
|
if (walletDataPage || addresses_dirty_ || hasTransactionSendProgress()) {
|
||||||
|
|||||||
@@ -730,6 +730,11 @@ private:
|
|||||||
bool broadcastChatMemos(const chat::OutgoingChatMemos& memos, const std::string& echoLocalId); // true if submitted
|
bool broadcastChatMemos(const chat::OutgoingChatMemos& memos, const std::string& echoLocalId); // true if submitted
|
||||||
bool broadcastChatMemosLite(const chat::OutgoingChatMemos& memos); // lite two-recipient send
|
bool broadcastChatMemosLite(const chat::OutgoingChatMemos& memos); // lite two-recipient send
|
||||||
void ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model); // lite chat receive harvest
|
void ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model); // lite chat receive harvest
|
||||||
|
// Full-node 0-conf fast path: re-scan just the chat reply address at minconf=0 every refresh cycle
|
||||||
|
// so incoming messages surface at mempool speed (before a block). Hidden conversations are skipped
|
||||||
|
// (they still un-hide via the normal confirmed harvest). Self-gated; no-op without a chat identity.
|
||||||
|
void fastScanChatMemos();
|
||||||
|
bool chat_fast_scan_in_flight_ = false; // guard against overlapping fast-scan RPCs
|
||||||
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
|
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
|
||||||
bool lite_firstrun_dismissed_ = false;
|
bool lite_firstrun_dismissed_ = false;
|
||||||
// Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
|
// Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
|
||||||
|
|||||||
@@ -3165,6 +3165,72 @@ void App::ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::fastScanChatMemos()
|
||||||
|
{
|
||||||
|
// Full-node only (lite has its own harvest). Re-scan JUST the chat reply address at minconf=0 every
|
||||||
|
// cycle so peers' messages surface at mempool speed. The normal, block-tip-gated harvest still
|
||||||
|
// ingests everything on confirmation (the store dedups on txid+position, so no double-insert).
|
||||||
|
if (lite_wallet_) return;
|
||||||
|
if (!chat::hushChatFeatureEnabledAtBuild() || !chat_service_.hasIdentity()) return;
|
||||||
|
if (!state_.connected || !rpc_ || !worker_) return;
|
||||||
|
if (chat_fast_scan_in_flight_) return; // don't stack RPCs if a previous scan is still running
|
||||||
|
const std::string addr = chatReplyZaddr();
|
||||||
|
if (addr.empty()) return;
|
||||||
|
|
||||||
|
chat_fast_scan_in_flight_ = true;
|
||||||
|
worker_->post([this, addr]() -> rpc::RPCWorker::MainCb {
|
||||||
|
std::vector<chat::HushChatTransactionMetadata> metadata;
|
||||||
|
try {
|
||||||
|
rpc::RPCClient::TraceScope trace("HushChat / 0-conf fast scan");
|
||||||
|
nlohmann::json received = rpc_->call("z_listreceivedbyaddress", {addr, 0}); // 0 = include mempool
|
||||||
|
if (received.is_array()) {
|
||||||
|
std::unordered_map<std::string, chat::HushChatTransactionInput> byTxid;
|
||||||
|
std::size_t noteIndex = 0;
|
||||||
|
for (const auto& note : received) {
|
||||||
|
const std::size_t fallbackPos = noteIndex++;
|
||||||
|
if (!note.is_object()) continue;
|
||||||
|
const std::string txid = note.value("txid", std::string());
|
||||||
|
const std::string memo = note.value("memoStr", std::string());
|
||||||
|
if (txid.empty() || memo.empty()) continue;
|
||||||
|
std::size_t pos = fallbackPos;
|
||||||
|
for (const char* key : {"position", "outputIndex", "outindex"})
|
||||||
|
if (note.contains(key) && note[key].is_number_integer() && note[key].get<int>() >= 0) {
|
||||||
|
pos = static_cast<std::size_t>(note[key].get<int>());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
auto& in = byTxid[txid];
|
||||||
|
in.txid = txid;
|
||||||
|
in.outputs.push_back(chat::HushChatMemoOutput{pos, memo});
|
||||||
|
}
|
||||||
|
for (auto& entry : byTxid) {
|
||||||
|
auto extracted = chat::extractHushChatTransactionMetadata(entry.second);
|
||||||
|
for (auto& m : extracted.metadata) metadata.push_back(std::move(m));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception&) {}
|
||||||
|
return [this, metadata = std::move(metadata)]() mutable {
|
||||||
|
chat_fast_scan_in_flight_ = false;
|
||||||
|
if (metadata.empty()) return;
|
||||||
|
// Skip HIDDEN conversations — the mempool fast path deliberately doesn't surface them; they
|
||||||
|
// still come back through the normal confirmed harvest (which un-hides on a new message).
|
||||||
|
std::vector<chat::HushChatTransactionMetadata> visible;
|
||||||
|
visible.reserve(metadata.size());
|
||||||
|
for (auto& m : metadata)
|
||||||
|
if (!(settings_ && settings_->isChatHidden(m.conversation_id)))
|
||||||
|
visible.push_back(std::move(m));
|
||||||
|
if (visible.empty()) return;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::int64_t> noTimes; // mempool: no block time → ingest uses now
|
||||||
|
std::vector<std::string> newChatCids;
|
||||||
|
chat_service_.ingest(visible, noTimes, std::time(nullptr), &newChatCids);
|
||||||
|
if (current_page_ != ui::NavPage::Chat &&
|
||||||
|
std::any_of(newChatCids.begin(), newChatCids.end(),
|
||||||
|
[this](const std::string& cid){ return !(settings_ && settings_->isChatMuted(cid)); }))
|
||||||
|
ui::Notifications::instance().info(TR("chat_new_message_toast"));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
bool App::lockLiteWallet()
|
bool App::lockLiteWallet()
|
||||||
{
|
{
|
||||||
if (!lite_wallet_) return false;
|
if (!lite_wallet_) return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user