Commit Graph

3 Commits

Author SHA1 Message Date
4471f54842 feat(chat): stamp sender compose-time in message header (clock-clamped)
Carry the sender's compose time as an optional "ts" (Unix seconds) in the
plaintext header JSON that rides outside the AEAD, and prefer it as the
displayed message time so both ends show the same send time regardless of when
the tx confirms. Parse "ts" leniently. On ingest, clamp: reject a "ts"
implausibly in the future vs the receive/block time (1h skew tolerated) so a
wrong/ahead peer clock can't pin messages to the bottom of a thread; a past
compose time is fine (the note buffer may broadcast a queued message later, and
a confirmed tx's block time is always >= compose time). Tests cover the
round-trip and the future-clock clamp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:59:46 -05:00
e191680782 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>
2026-07-06 16:12:35 -05:00
4db609fb52 feat(chat): composer + contact requests — outgoing construction (Phase 4)
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>
2026-07-06 15:30:41 -05:00