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>
96 lines
5.0 KiB
C++
96 lines
5.0 KiB
C++
#pragma once
|
|
|
|
// DragonX Wallet - HushChat service: turns harvested memo metadata into decrypted, threaded
|
|
// messages. Owns the long-lived chat identity keypair (a secret) + the in-memory store.
|
|
// Move-disabled (the secret stays pinned); wipes the secret on destruction/clear.
|
|
// Not thread-safe — drive from the main thread (where refresh results are applied).
|
|
|
|
#include "chat_crypto.h" // ChatKeyPair
|
|
#include "chat_protocol.h" // HushChatTransactionMetadata
|
|
#include "chat_outgoing.h" // OutgoingChatMemos, ChatComposeStatus
|
|
#include "chat_store.h"
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
namespace dragonx::chat {
|
|
|
|
class ChatDatabase; // optional persistent backing (Phase 2); set via setPersistence
|
|
|
|
class ChatService {
|
|
public:
|
|
ChatService() = default;
|
|
~ChatService();
|
|
ChatService(const ChatService&) = delete;
|
|
ChatService& operator=(const ChatService&) = delete;
|
|
ChatService(ChatService&&) = delete;
|
|
ChatService& operator=(ChatService&&) = delete;
|
|
|
|
// Provision (or replace) the chat identity. Copies the keypair — the caller should wipe
|
|
// its own copy afterwards (see chat_identity.h ownership note).
|
|
void setIdentity(const ChatKeyPair& keys);
|
|
bool hasIdentity() const { return has_identity_; }
|
|
void clearIdentity(); // wipes the held secret key
|
|
|
|
// Decrypt/record each metadata entry (a Message is decrypted; a ContactRequest carries its
|
|
// plaintext through) and thread it into the store. Each message is stamped with its own
|
|
// transaction time via `txTimestamps` (keyed by txid), falling back to `fallbackTimestamp`
|
|
// when the txid isn't present. Newly-added messages are also persisted (if a database is
|
|
// attached). Returns the number of NEW messages added; 0 with no identity. Undecryptable
|
|
// Messages are dropped silently (no logging of memo/plaintext).
|
|
// When `newIncomingCids` is non-null it is filled with the conversation ids of the genuinely-new
|
|
// incoming (non-request) messages appended this call — a reliable "a new message arrived here"
|
|
// signal for notifications that doesn't depend on any timestamp/seen-watermark comparison.
|
|
int ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
|
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
|
|
std::int64_t fallbackTimestamp = 0,
|
|
std::vector<std::string>* newIncomingCids = nullptr);
|
|
|
|
// Attach a persistent backing store (Phase 2). Not owned. Pass nullptr to detach. New messages
|
|
// from ingest() are written through; loadFromDatabase() rehydrates the in-memory store from it.
|
|
void setPersistence(ChatDatabase* db) { db_ = db; }
|
|
// Load previously-persisted messages (already decrypted at ingest, re-encrypted at rest under
|
|
// the seed-derived key) into the in-memory store. No-op without an unlocked database.
|
|
void loadFromDatabase();
|
|
|
|
// --- Outgoing (compose) ---
|
|
// My chat public key (hex), or "" without an identity — goes in an outgoing header's "p".
|
|
std::string identityPublicKeyHex() const;
|
|
// Construct the outgoing memos for an ENCRYPTED message, using the held identity to encrypt.
|
|
ChatComposeStatus composeMessage(const std::string& myReplyZaddr,
|
|
const std::string& peerPublicKeyHex,
|
|
const std::string& peerZaddr,
|
|
const std::string& conversationId,
|
|
const std::string& plaintext,
|
|
OutgoingChatMemos& out) const;
|
|
// Construct the outgoing memos for a plaintext contact request (no peer key needed yet).
|
|
ChatComposeStatus composeContactRequest(const std::string& myReplyZaddr,
|
|
const std::string& peerZaddr,
|
|
const std::string& conversationId,
|
|
const std::string& requestText,
|
|
OutgoingChatMemos& out) const;
|
|
// Echo a locally-composed outgoing message into the store (and DB). Returns true if new. (We
|
|
// never harvest our own sent memos — they land on the peer's address — so this echo is the
|
|
// only local record of what we sent.)
|
|
bool recordOutgoing(const ChatMessage& message);
|
|
|
|
// Two-phase echo for delivery tracking: record + persist immediately as Sending (survives an app
|
|
// quit), then resolveOutgoing() upserts the final status once the broadcast completes. A stray
|
|
// persisted Sending (crash mid-broadcast) loads back as Sent.
|
|
bool recordOutgoingPending(const ChatMessage& message);
|
|
void resolveOutgoing(const std::string& txid, ChatDelivery delivery);
|
|
|
|
const ChatStore& store() const { return store_; }
|
|
ChatStore& store() { return store_; }
|
|
|
|
private:
|
|
ChatKeyPair identity_{};
|
|
bool has_identity_ = false;
|
|
ChatStore store_;
|
|
ChatDatabase* db_ = nullptr; // optional; not owned
|
|
};
|
|
|
|
} // namespace dragonx::chat
|