Add the outgoing side of HushChat: compose messages and start conversations.
The wire construction is the byte-exact inverse of the receive parser and is
proven by a self-consistent round-trip (build → parse → decrypt).
- src/chat/chat_outgoing.{h,cpp} (pure): buildOutgoingMessage encrypts via
encryptOutgoing and buildOutgoingContactRequest carries plaintext; both emit
the header memo JSON ({h,v,z,cid,t,e,p}, which nlohmann serializes
alphabetically to match SilentDragonXLite) + the payload memo, validating the
512-byte memo limit, the 64-hex peer key, and the "no leading '{'" rule for
request text. My public key goes in header "p"; the peer's key is the
encryption recipient.
- ChatService: composeMessage/composeContactRequest (encrypt with the held
identity) + recordOutgoing (echo an Outgoing ChatMessage into the store + DB —
we never harvest our own sends, so this is the only local record).
- App: sendChatMessage(cid,text) sources the peer z-addr + public key from the
conversation, composes, and records a random-id echo; startChatConversation
(zaddr,text) mints a random cid + composes a plaintext contact request;
chatReplyZaddr() picks a spendable z-addr. broadcastChatMemos() is the Phase-5
transport seam (network delivery + real-SDXL interop verification land there).
- Chat tab: a message composer (shown once the peer's key is known, else a
"waiting for reply" hint) + a "New conversation" modal that sends a contact
request to a z-address.
- Tests: outgoing round-trip through the receive parser + decrypt, contact-request
passthrough, validation guards, and ChatService compose + recordOutgoing echo.
Adversarially reviewed (crypto/interop, secret hygiene, logic, UI) — 0 findings.
Gated by DRAGONX_ENABLE_CHAT (default OFF). Verified: Linux + Windows(mingw)
build with chat ON, ctest 100%, hygiene clean; caches restored to the OFF default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
4.1 KiB
C++
109 lines
4.1 KiB
C++
// DragonX Wallet - HushChat service (implementation).
|
|
|
|
#include "chat_service.h"
|
|
|
|
#include "chat_database.h"
|
|
#include "chat_identity.h" // chatIdentityPublicKeyHex
|
|
|
|
#include <utility>
|
|
|
|
namespace dragonx::chat {
|
|
|
|
ChatService::~ChatService() {
|
|
clearIdentity();
|
|
}
|
|
|
|
void ChatService::setIdentity(const ChatKeyPair& keys) {
|
|
identity_ = keys;
|
|
has_identity_ = true;
|
|
}
|
|
|
|
void ChatService::clearIdentity() {
|
|
wipeChatKeyPair(identity_);
|
|
has_identity_ = false;
|
|
}
|
|
|
|
int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
|
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
|
|
std::int64_t fallbackTimestamp) {
|
|
if (!has_identity_) return 0;
|
|
|
|
int added = 0;
|
|
for (const auto& meta : metadata) {
|
|
ChatMessage message;
|
|
message.direction = ChatDirection::Incoming;
|
|
message.txid = meta.txid;
|
|
message.conversation_id = meta.conversation_id;
|
|
message.peer_zaddr = meta.reply_zaddr;
|
|
message.peer_public_key_hex = meta.sender_public_key_hex;
|
|
const auto timeIt = txTimestamps.find(meta.txid);
|
|
message.timestamp = timeIt != txTimestamps.end() ? timeIt->second : fallbackTimestamp;
|
|
message.payload_position = meta.payload_position;
|
|
|
|
if (meta.type == HushChatHeaderType::ContactRequest) {
|
|
message.kind = ChatMessageKind::ContactRequest;
|
|
message.body = meta.payload_memo; // plaintext request text
|
|
} else {
|
|
message.kind = ChatMessageKind::Message;
|
|
std::string plaintext;
|
|
const ChatCryptoStatus status = decryptIncoming(
|
|
identity_, meta.sender_public_key_hex, meta.secretstream_header_hex,
|
|
meta.payload_memo, plaintext);
|
|
if (status != ChatCryptoStatus::Ok) continue; // drop undecryptable silently
|
|
message.body = std::move(plaintext);
|
|
}
|
|
|
|
// In-memory store dedups (txid+position); only persist the genuinely new ones. On the next
|
|
// session loadFromDatabase() repopulates the store, so re-scanning the chain re-ingests but
|
|
// the store dedup prevents a duplicate write.
|
|
if (store_.append(message)) {
|
|
if (db_) db_->append(message);
|
|
++added;
|
|
}
|
|
}
|
|
return added;
|
|
}
|
|
|
|
void ChatService::loadFromDatabase() {
|
|
if (!db_) return;
|
|
for (const auto& message : db_->load()) {
|
|
store_.append(message);
|
|
}
|
|
}
|
|
|
|
std::string ChatService::identityPublicKeyHex() const {
|
|
if (!has_identity_) return {};
|
|
return chatIdentityPublicKeyHex(identity_);
|
|
}
|
|
|
|
ChatComposeStatus ChatService::composeMessage(const std::string& myReplyZaddr,
|
|
const std::string& peerPublicKeyHex,
|
|
const std::string& peerZaddr,
|
|
const std::string& conversationId,
|
|
const std::string& plaintext,
|
|
OutgoingChatMemos& out) const {
|
|
if (!has_identity_) return ChatComposeStatus::MissingField;
|
|
return buildOutgoingMessage(identity_, chatIdentityPublicKeyHex(identity_), myReplyZaddr,
|
|
peerPublicKeyHex, peerZaddr, conversationId, plaintext, out);
|
|
}
|
|
|
|
ChatComposeStatus ChatService::composeContactRequest(const std::string& myReplyZaddr,
|
|
const std::string& peerZaddr,
|
|
const std::string& conversationId,
|
|
const std::string& requestText,
|
|
OutgoingChatMemos& out) const {
|
|
if (!has_identity_) return ChatComposeStatus::MissingField;
|
|
return buildOutgoingContactRequest(chatIdentityPublicKeyHex(identity_), myReplyZaddr,
|
|
peerZaddr, conversationId, requestText, out);
|
|
}
|
|
|
|
bool ChatService::recordOutgoing(const ChatMessage& message) {
|
|
if (store_.append(message)) {
|
|
if (db_) db_->append(message);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace dragonx::chat
|