feat(chat): wire the two-variant send transport (Phase 5)
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>
This commit is contained in:
@@ -2491,13 +2491,68 @@ std::string App::generateChatLocalId(const char* prefix, int numBytes) const
|
||||
return id;
|
||||
}
|
||||
|
||||
// Phase-5 seam: broadcast the header + payload as two 0-value memo outputs to memos.recipientZaddr
|
||||
// (full-node z_sendmany via submitZSendMany with a two-recipient array; lite via the controller).
|
||||
// Not wired yet — the composed message is recorded locally so the UI reflects it; over-the-wire
|
||||
// delivery + real-SDXL interop verification land in the next phase.
|
||||
// Broadcast the header + payload as two 0-value memo outputs to memos.recipientZaddr (header first,
|
||||
// the lower memo position). Routes to the lite controller in lite builds, else full-node z_sendmany.
|
||||
//
|
||||
// LIVE-VERIFY (cannot be proven from source): that dragonxd returns the memo under `memoStr` verbatim
|
||||
// on receive, that recipient-array order maps to note position (the receive pairing needs the header
|
||||
// at a lower position), that a 0-value memo-only tx relays, and full SDXL<->DragonX interop.
|
||||
void App::broadcastChatMemos(const chat::OutgoingChatMemos& memos)
|
||||
{
|
||||
(void)memos;
|
||||
if (memos.recipientZaddr.empty()) return;
|
||||
|
||||
if (lite_wallet_) {
|
||||
broadcastChatMemosLite(memos);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state_.connected || !rpc_ || !worker_) {
|
||||
ui::Notifications::instance().error("Not connected — chat message not sent.");
|
||||
return;
|
||||
}
|
||||
const std::string from = chatReplyZaddr(); // spendable + advertised as the reply-to address
|
||||
if (from.empty()) {
|
||||
ui::Notifications::instance().error("No spendable z-address to send chat from.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Full-node: each memo needs the daemon's "utf8:" prefix (raw JSON/hex is otherwise rejected).
|
||||
const auto outputs = chat::chatSendOutputs(memos, /*utf8Prefix=*/true);
|
||||
nlohmann::json recipients = nlohmann::json::array();
|
||||
for (const auto& out : outputs) {
|
||||
nlohmann::json recipient;
|
||||
recipient["address"] = out.address;
|
||||
recipient["amount"] = util::formatAmountFixed(0.0); // 0-value memo output
|
||||
recipient["memo"] = out.memo;
|
||||
recipients.push_back(std::move(recipient));
|
||||
}
|
||||
|
||||
const double fee = settings_ ? settings_->getDefaultFee() : 0.0001;
|
||||
// markFeeGapRetry=true is deliberate: it suppresses the fee-gap auto-retry, which rebuilds a
|
||||
// SINGLE-recipient tx from the scalar to/amount/memo and would drop the second (payload) output.
|
||||
submitZSendMany(from, memos.recipientZaddr, 0.0, fee, /*memo*/"", recipients,
|
||||
"HushChat / broadcast", /*markFeeGapRetry*/ true, /*callback*/{});
|
||||
}
|
||||
|
||||
// Lite variant: two 0-value recipients to the same z-address, RAW memos (the backend does
|
||||
// Memo::from_str directly — no "utf8:" prefix). The backend accepts duplicate addresses + 0-value
|
||||
// outputs for exactly this HushChat pattern. Fire-and-forget: the result surfaces via the lite
|
||||
// broadcast log; the message is already echoed locally.
|
||||
void App::broadcastChatMemosLite(const chat::OutgoingChatMemos& memos)
|
||||
{
|
||||
if (!lite_wallet_) return;
|
||||
const auto outputs = chat::chatSendOutputs(memos, /*utf8Prefix=*/false);
|
||||
wallet::LiteSendRequest req;
|
||||
for (const auto& out : outputs) {
|
||||
wallet::LiteSendRecipient recipient;
|
||||
recipient.address = out.address;
|
||||
recipient.amountZatoshis = 0;
|
||||
recipient.memo = out.memo;
|
||||
req.recipients.push_back(std::move(recipient));
|
||||
}
|
||||
if (!lite_wallet_->sendTransaction(req)) {
|
||||
ui::Notifications::instance().error("A send is already in progress, or no wallet is open.");
|
||||
}
|
||||
}
|
||||
|
||||
void App::sendChatMessage(const std::string& conversationId, const std::string& text)
|
||||
|
||||
Reference in New Issue
Block a user