Make composed messages actually reach the wire: broadcastChatMemos sends the header + payload as two 0-value memo outputs to the peer's z-address (header first, the lower memo position), on both variants. - chat_outgoing: chatSendOutputs(memos, utf8Prefix) — the pure, testable memo encoder. Full-node memos get a "utf8:" prefix (dragonxd rejects raw JSON and then UTF-8-encodes on-chain, byte-identical to SilentDragonXLite's Memo::from_str); the payload is NOT double-hex. Lite memos are raw UTF-8 (the backend does Memo::from_str directly). Header is always output 0. - App::broadcastChatMemos: full-node builds a two-recipient z_sendmany array (amount 0, from the spendable reply z-address, default fee) via submitZSendMany with markFeeGapRetry=true — deliberately, to suppress the fee-gap auto-retry, which rebuilds a single-recipient tx and would drop the payload output. Lite routes to broadcastChatMemosLite (two 0-value LiteSendRecipients, raw memos; the backend accepts duplicate addresses + 0-value for exactly this pattern). - Encoding + design established by a four-codebase mapping (wallet, daemon, SDXL, lite backend) and an adversarial review of the wiring (0 confirmed findings). - Tests: chatSendOutputs (utf8:-prefix + header-first for full-node, raw for lite) + on-chain round-trip (strip "utf8:" -> the harvest parser re-pairs and decrypts). Remaining as a LIVE test (cannot be proven from source): that dragonxd returns the memo under memoStr verbatim, that recipient-array order maps to note position, that a 0-value memo-only tx relays, and full SDXL<->DragonX interop. 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.6 KiB
C++
109 lines
4.6 KiB
C++
// DragonX Wallet - HushChat outgoing memo construction (implementation).
|
|
|
|
#include "chat_outgoing.h"
|
|
|
|
#include "chat_protocol.h" // kHushChat* constants
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
namespace dragonx::chat {
|
|
|
|
namespace {
|
|
|
|
// Serialize the HushChat header. nlohmann emits object keys in sorted (alphabetical) order —
|
|
// cid,e,h,p,t,v,z — which is exactly SilentDragonXLite's on-wire key order.
|
|
std::string buildHeaderMemo(const std::string& replyZaddr,
|
|
const std::string& conversationId,
|
|
const char* type,
|
|
const std::string& streamHeaderHex,
|
|
const std::string& publicKeyHex)
|
|
{
|
|
nlohmann::json header;
|
|
header["h"] = 1; // header number (>= 1)
|
|
header["v"] = kHushChatSupportedVersion; // 0
|
|
header["z"] = replyZaddr; // where the peer should reply (my address)
|
|
header["cid"] = conversationId;
|
|
header["t"] = type; // "Memo" or "Cont"
|
|
header["e"] = streamHeaderHex; // 48-hex secretstream header (Memo) / "" (Cont)
|
|
header["p"] = publicKeyHex; // my 64-hex crypto_kx public key
|
|
return header.dump();
|
|
}
|
|
|
|
bool present(const std::string& value) { return !value.empty(); }
|
|
|
|
} // namespace
|
|
|
|
std::array<ChatSendOutput, 2> chatSendOutputs(const OutgoingChatMemos& memos, bool utf8Prefix)
|
|
{
|
|
const std::string prefix = utf8Prefix ? "utf8:" : "";
|
|
return {{
|
|
{ memos.recipientZaddr, prefix + memos.headerMemo }, // header — the lower memo position
|
|
{ memos.recipientZaddr, prefix + memos.payloadMemo },
|
|
}};
|
|
}
|
|
|
|
ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine,
|
|
const std::string& myPublicKeyHex,
|
|
const std::string& myReplyZaddr,
|
|
const std::string& peerPublicKeyHex,
|
|
const std::string& peerZaddr,
|
|
const std::string& conversationId,
|
|
const std::string& plaintext,
|
|
OutgoingChatMemos& out)
|
|
{
|
|
if (plaintext.empty()) return ChatComposeStatus::EmptyBody;
|
|
if (!present(myPublicKeyHex) || !present(myReplyZaddr) || !present(peerZaddr) ||
|
|
!present(conversationId)) {
|
|
return ChatComposeStatus::MissingField;
|
|
}
|
|
if (peerPublicKeyHex.size() != kHushChatPublicKeyHexLength) return ChatComposeStatus::BadPeerKey;
|
|
|
|
std::string streamHeaderHex;
|
|
std::string ciphertextHex;
|
|
if (encryptOutgoing(mine, peerPublicKeyHex, plaintext, streamHeaderHex, ciphertextHex)
|
|
!= ChatCryptoStatus::Ok) {
|
|
return ChatComposeStatus::EncryptFailed;
|
|
}
|
|
|
|
OutgoingChatMemos memos;
|
|
memos.recipientZaddr = peerZaddr;
|
|
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex, myPublicKeyHex);
|
|
memos.payloadMemo = ciphertextHex;
|
|
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
|
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
|
return ChatComposeStatus::TooLong;
|
|
}
|
|
out = std::move(memos);
|
|
return ChatComposeStatus::Ok;
|
|
}
|
|
|
|
ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
|
const std::string& myReplyZaddr,
|
|
const std::string& peerZaddr,
|
|
const std::string& conversationId,
|
|
const std::string& requestText,
|
|
OutgoingChatMemos& out)
|
|
{
|
|
if (requestText.empty()) return ChatComposeStatus::EmptyBody;
|
|
// The receive parser treats any memo starting with '{' as a header, so a request payload must
|
|
// not start with one (see isContactPayloadCandidate in chat_protocol.cpp).
|
|
if (requestText.front() == '{') return ChatComposeStatus::BadRequestText;
|
|
if (!present(myPublicKeyHex) || !present(myReplyZaddr) || !present(peerZaddr) ||
|
|
!present(conversationId)) {
|
|
return ChatComposeStatus::MissingField;
|
|
}
|
|
|
|
OutgoingChatMemos memos;
|
|
memos.recipientZaddr = peerZaddr;
|
|
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Cont", "", myPublicKeyHex);
|
|
memos.payloadMemo = requestText;
|
|
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
|
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
|
return ChatComposeStatus::TooLong;
|
|
}
|
|
out = std::move(memos);
|
|
return ChatComposeStatus::Ok;
|
|
}
|
|
|
|
} // namespace dragonx::chat
|