feat(chat): provision seed-derived identity + wire the receive sink

Complete the Phase-1 App integration for HushChat, now that the dragonx
`hd-transparent-keys` daemon exposes a BIP39 mnemonic (z_exportmnemonic)
that is byte-compatible with SilentDragonXLite. Both variants derive the
chat identity from the wallet's OWN seed phrase, so it is portable across
full-node and lite (same words -> same KDF input -> same identity) and
recoverable from the single wallet backup.

- App owns a dragonx::chat::ChatService; maybeProvisionChatIdentity() runs
  each update() tick and, once the wallet seed is reachable and unlocked,
  derives the identity via deriveChatIdentityFromSecret and sets it on the
  service. Full-node fetches z_exportmnemonic off the UI thread via the RPC
  worker; lite reads it synchronously through LiteWalletController::exportSeed.
  Secrets are wiped on every path.
- Wire the previously-dead TransactionRefreshResult.hushChatMetadata sink:
  both the full and recent transaction-refresh completions now ChatService
  ::ingest the harvested memos (before the result is moved) so incoming
  messages are decrypted and threaded. The store dedups across both paths.
- Robust provisioning gates: skip + re-arm on relock (isLocked, mirrored for
  both variants), don't fetch before the encryption state is known, and treat
  a non-mnemonic wallet (RpcError) as identity-unavailable rather than
  retrying every tick. rebuildLiteWallet re-arms so a server switch / re-open
  re-derives from the newly opened wallet.

Gated by DRAGONX_ENABLE_CHAT (default OFF) via the constexpr
hushChatFeatureEnabledAtBuild() gate, so the wiring folds away in shipping
builds. 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:
2026-07-06 03:33:00 -05:00
parent ba03de938e
commit 1738468f8c
3 changed files with 137 additions and 0 deletions

View File

@@ -602,6 +602,13 @@ void App::rebuildLiteWallet(bool force)
// controller but never reopen, leaving a permanent "disconnected" state.
lite_autoopen_done_ = false;
lite_open_error_.clear();
// A rebuilt controller may back a different wallet (server switch / re-open); drop any chat
// identity and re-arm provisioning so it re-derives from the newly opened wallet's seed.
chat_service_.clearIdentity();
chat_identity_provisioned_ = false;
chat_identity_fetch_in_flight_ = false;
chat_identity_unavailable_ = false;
}
void App::update()
@@ -696,6 +703,11 @@ void App::update()
}
async_tasks_.reapCompleted();
// HushChat: once the wallet seed is reachable+unlocked, derive & set the chat identity so the
// transaction-refresh harvest can decrypt incoming memos. Cheap early-outs keep it idle until
// it can act; compiled away when the feature is off (constexpr gate inside).
maybeProvisionChatIdentity();
// Auto-lock check (only when connected + encrypted + unlocked)
if (state_.connected && state_.isUnlocked()) {
checkAutoLock();