Transactions that carried a chat message (sent or received, messages + contact
requests) now show a "Message" badge in the History tab, and a new "Chat" option
in the type-filter combo shows only those transactions.
Detection reuses what the chat store already tracks: ChatStore gains an inline
chatTxids() returning the set of on-chain txids that carried a chat message; the
tab builds it once per frame (cheap — O(chat messages)) and tests txid membership
for the badge and the filter. Empty when chat is disabled or the wallet has no
chat identity. Both variants populate the chat store before this tab renders, so
the same code serves full-node and lite with no separate handling.
The "Message" badge reuses the stacked top-pill slot (chat txs are send/receive,
not the autoshield "shield" type, so it and the "Shielded" badge are mutually
exclusive; chat takes precedence) in a distinct Secondary() colour. Guards the
summary-card accent idx_map so the new filter value (4) can't index it OOB.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Chat sends move 0 value, so the network fee is structurally load-bearing (it's
the only thing that forces a real shielded input; 0-value + 0-fee builds a
degenerate, unrelayable tx). Three gaps addressed:
1. Fee floor — broadcastChatMemos now uses max(getDefaultFee(), kChatMinFeeDrgx),
so a 0 / too-low global default-fee setting can't silently break chat.
2. Real delivery status — the echo was marked Sent on SUBMIT regardless of the
on-chain outcome (the z_sendmany callback was empty), so failures were
invisible and the Retry affordance never fired for async failures. Add a third
ChatDelivery::Sending state (appended so persisted 0=Sent stays valid); record
the echo in-memory as Sending, and resolve it to Sent/Failed from the
z_sendmany completion callback — persisting only the final status (so a restart
never shows a stuck spinner; a stray persisted Sending loads as Sent). A subtle
"sending…" label shows while in flight.
3. Pay-from-funded + pre-check — z_sendmany spends from one z-address, and the
identity reply address may be unfunded while funds sit elsewhere. chatPayFromZaddr
picks a spendable z-address that can cover the fee (preferring the identity
address); the memo still advertises the identity address as reply-to, so paying
from a different note is transport-transparent. If nothing can cover the fee, a
clear "need a small shielded balance" toast replaces the cryptic failure.
Full node only for the callback path; lite resolves optimistically on queue.
8-language strings + CJK subset (+1 glyph 賄).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap the in-memory-only chat store for durable sqlite persistence with real
per-transaction timestamps, encrypted at rest under a key derived from the
wallet's own seed (no passphrase, works on encrypted and unencrypted wallets).
- ChatDatabase (src/chat/chat_database.{h,cpp}): sqlite store mirroring
data::TransactionHistoryCache. unlockWithSecret(seed) derives a 32-byte AEAD
storage key and a wallet-partition tag via domain-separated keyed BLAKE2b
(generichash) contexts. Every record — bodies, peer z-addrs, threading,
timestamps — is crypto_aead_xchacha20poly1305_ietf-encrypted with a random
nonce and the wallet tag as associated data; even the dedup key is a keyed
hash of txid+position, so nothing about your conversations is in cleartext on
disk. Rows are partitioned per-wallet; a different seed sees nothing. Messages
are decrypted once at ingest then re-encrypted under the storage key, so
load() needs only the storage key, not the chat identity.
- ChatService: ingest() now stamps each message with its own transaction time
(txid->time map + fallback) and writes new (store-deduped) messages through to
the database; loadFromDatabase() rehydrates the in-memory read model on unlock.
- App: unlock the chat DB with the same seed in provisionChatIdentityFromSecret
and load prior history; lock the DB + clear the decrypted in-memory store on
relock and on lite-controller rebuild.
Adversarial review (4 confirmed findings, all fixed): don't provision if the
wallet locks mid-fetch (re-check isLocked at completion); wipe the serialized
plaintext temporary in append(); trim the seed into a separate fully-wiped
buffer (no residue past a shrunk size()); scrub the mnemonic copy in the RPC
json result.
Tests: ChatDatabase round-trip (persist/reload, field + order fidelity), dedup,
per-wallet isolation, lock inertness, and ChatService write-through + reload
without an identity. 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>
Phase 1 Steps 4-6 (the receive pipeline, minus the App/sync wiring which
lands next).
- chat_message: the in-memory ChatMessage model (direction, kind, txid, cid,
peer zaddr/pubkey, body, timestamp, payload_position). No libsodium.
- chat_store: threads messages by conversation_id and dedups by
(txid, payload_position) so re-scanning the chain never double-inserts.
- chat_service: owns the long-lived chat identity keypair (move-disabled,
wiped on destruction/clear) and the store. ingest() decrypts each Message
(drops undecryptable ones silently — no plaintext/memo logging), passes a
ContactRequest's plaintext through, and threads the result. No-op without an
identity.
Tests: a metadata batch decrypts into a threaded conversation; re-ingest
dedups; a contact request carries through; a wrong identity decrypts nothing;
no-identity ingest is a no-op.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>