fix(chat): address adversarial review of the send-path change

Four confirmed findings from the review of ef247c9:

1. Persistence regression — the deferred-persist echo (in-memory Sending, written
   only when the async callback resolved) meant a message broadcast on-chain but
   whose callback hadn't fired yet was LOST from history if the app quit/crashed
   in that window. Persist the echo immediately as Sending and UPSERT the final
   status on resolve (new ChatDatabase::upsert with ON CONFLICT DO UPDATE, since
   append is INSERT-OR-IGNORE). A stray persisted Sending still loads as Sent.

2. Fee ceiling — dragonxd REJECTS a 0-value tx whose fee exceeds the default
   miners fee (0.0001), and max(getDefaultFee(), 0.0001) can only raise it, so a
   default_fee > 0.0001 broke every chat send. Pin chat to exactly kChatMinFeeDrgx,
   dropping getDefaultFee() from this path (chat always moves 0 value).

3. Lifetime — the resolve callback had no generation guard, so a wallet lock (which
   doesn't disconnect) between submit and callback could resolve against a cleared
   store. Capture chat_session_generation_ and bail on mismatch (both the full-node
   callback and the lite optimistic resolve), matching the identity-fetch pattern.

4. Retry misdirect — Retry on a failed CONTACT REQUEST called sendChatMessage,
   which (no peer key yet) just showed "waiting for reply". Route it to
   sendContactRequestForCid() (refactored out of startChatConversation) so it
   re-sends the request into the SAME conversation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-16 20:42:59 -05:00
parent ef247c95ff
commit 76093fe82d
7 changed files with 77 additions and 12 deletions

View File

@@ -513,7 +513,14 @@ void RenderChatTab(App* app)
ImGui::TextUnformatted(TR("chat_send_failed"));
ImGui::PopStyleColor(); ImGui::PopFont();
ImGui::SameLine();
if (ImGui::SmallButton(TR("chat_retry"))) { app->sendChatMessage(sel->cid, m.body); s_scroll_to_cid = sel->cid; }
if (ImGui::SmallButton(TR("chat_retry"))) {
// A failed contact request must be re-sent as a request (it has no peer key
// yet) into the SAME conversation — not routed through sendChatMessage,
// which would just say "waiting for reply".
if (request) app->sendContactRequestForCid(sel->cid, m.peer_zaddr, m.body);
else app->sendChatMessage(sel->cid, m.body);
s_scroll_to_cid = sel->cid;
}
}
// In-flight send → subtle right-aligned "sending…"; the op callback resolves it.
if (sending) {