feat(chat): enable HushChat by default + legacy (non-mnemonic) wallet support
Flip DRAGONX_ENABLE_CHAT to default ON (a fresh configure now builds chat in), and make full-node chat work for every wallet + any daemon. Legacy compat: chat identity is derived from the wallet's mnemonic (z_exportmnemonic) for a portable, SDXLite-compatible identity — but a legacy random-seed wallet has no mnemonic, and released daemons don't have that RPC at all. Provisioning now falls back to a stable z-address's spending key (z_exportkey) in those cases: a functional, wallet-local identity that works on any daemon. Since a chat identity is local (peers learn your public key from your message headers, not your derivation), interop is unaffected; only cross-client portability needs the mnemonic. The spending key is an in-memory KDF input over a key the wallet already holds, wiped after use — no new exposure. Stability: the chosen chat z-address (the reply-to in headers AND the legacy identity source) is now persisted in settings (chat_reply_zaddr), so the identity + reply address don't shift when new addresses are generated. chatReplyZaddr() picks the smallest spendable z-addr once and reuses it. CLAUDE.md updated to reflect the default flip. Verified: Linux + Windows build with chat ON, fresh-configure default confirmed ON, ctest 100%, hygiene clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,7 +55,7 @@ There is no per-test filtering — it is one binary that runs every assertion. T
|
||||
|
||||
> ⚠️ **Do not regrow the `_plan`/`_batch` churn.** This directory previously held ~160 dead `lite_wallet_*_plan` / `*_batch*_receipt_custody_acceptance_confirmation_archive_handoff_*` files (filenames up to 250 chars) — auto-generated scaffolding that never reached the shipping binary. They were deleted. When extending lite-wallet behavior, **edit the named service/bridge/runtime files in place**; never add another "promotion/receipt/custody/handoff/stewardship" wrapper layer. `scripts/check-source-hygiene.sh` (wired as a `.git/hooks/pre-commit` hook) blocks >80-char filenames and chained churn-token names — run it in CI too.
|
||||
|
||||
**Chat** (`src/chat/chat_protocol.cpp`): experimental HushChat protocol, compiled in only when `DRAGONX_ENABLE_CHAT=ON`.
|
||||
**Chat** (`src/chat/*`): the HushChat protocol port (Contacts/Chat tabs, seed-derived identity, secretstream crypto, seed-encrypted sqlite store, two-variant send/receive transport). Runtime behavior is gated by `DRAGONX_ENABLE_CHAT`, now **default ON** (the sources always compile; the flag folds the feature away at runtime via `hushChatFeatureEnabledAtBuild()`). Full-node chat derives its identity from the wallet's mnemonic (`z_exportmnemonic`, portable/SDXLite-compatible) or, for legacy/non-mnemonic wallets, a stable z-address spending key (`z_exportkey`).
|
||||
|
||||
## Build variants & feature gating
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON)
|
||||
option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON)
|
||||
option(DRAGONX_BUILD_LITE "Build ObsidianDragonLite variant without full-node features" OFF)
|
||||
option(DRAGONX_ENABLE_LITE_BACKEND "Enable real lite wallet backend integration" OFF)
|
||||
option(DRAGONX_ENABLE_CHAT "Enable experimental HushChat protocol/UI integration" OFF)
|
||||
option(DRAGONX_ENABLE_CHAT "Enable the HushChat protocol/UI integration" ON)
|
||||
set(DRAGONX_LITE_BACKEND_LIBRARY "" CACHE FILEPATH "Path to a prebuilt SDXL-compatible lite backend library")
|
||||
set(DRAGONX_LITE_BACKEND_INCLUDE_DIR "" CACHE PATH "Optional include directory for SDXL-compatible lite backend headers")
|
||||
set(DRAGONX_LITE_BACKEND_EXTRA_LIBS "" CACHE STRING "Additional libraries needed by the SDXL-compatible lite backend")
|
||||
|
||||
@@ -600,7 +600,7 @@ private:
|
||||
// feature is off, already provisioned, in flight, or unavailable.
|
||||
void maybeProvisionChatIdentity();
|
||||
void provisionChatIdentityFromSecret(std::string secret);
|
||||
std::string chatReplyZaddr() const; // a stable wallet z-addr for outgoing headers
|
||||
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr for chat
|
||||
std::string generateChatLocalId(const char* prefix, int numBytes) const; // unique echo id / cid
|
||||
void broadcastChatMemos(const chat::OutgoingChatMemos& memos); // full-node z_sendmany / lite
|
||||
void broadcastChatMemosLite(const chat::OutgoingChatMemos& memos); // lite two-recipient send
|
||||
|
||||
@@ -2428,55 +2428,91 @@ void App::maybeProvisionChatIdentity()
|
||||
provisionChatIdentityFromSecret(seed.seedPhrase); // copies internally
|
||||
wallet::secureWipeLiteSecret(seed.seedPhrase);
|
||||
} else if (supportsFullNodeLifecycleActions()) {
|
||||
// Full-node: z_exportmnemonic blocks on synchronous curl — fetch it off the UI thread and
|
||||
// provision on the main thread. Requires a mnemonic wallet (created/restored from a phrase);
|
||||
// a legacy random-seed wallet is rejected by the daemon (RpcError) → identity unavailable.
|
||||
// Full-node: fetch the identity secret off the UI thread, provision on the main thread.
|
||||
// Prefer the wallet's mnemonic (z_exportmnemonic) for a portable, SDXLite-compatible identity;
|
||||
// if the wallet has no mnemonic (a legacy random-seed wallet, or a daemon without that RPC),
|
||||
// fall back to a stable z-address's spending key (z_exportkey) — a functional, wallet-local
|
||||
// identity that works on any daemon. Both secrets are wiped after deriving.
|
||||
if (!state_.connected || !rpc_ || !worker_) return;
|
||||
if (!state_.encryption_state_known) return; // don't fetch before we know the lock state
|
||||
const std::string fallbackZaddr = chatReplyZaddr(); // main thread: stable identity source
|
||||
chat_identity_fetch_in_flight_ = true;
|
||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||
std::string mnemonic;
|
||||
bool definitiveFail = false; // daemon rejected (non-mnemonic wallet) — do not retry
|
||||
bool transientFail = false; // connection blip — allow a later retry
|
||||
worker_->post([this, fallbackZaddr]() -> rpc::RPCWorker::MainCb {
|
||||
std::string secret; // the mnemonic (portable) OR a spending key (legacy fallback)
|
||||
bool transientFail = false; // connection blip — allow a later retry
|
||||
bool unavailable = false; // no usable secret — stop retrying this session
|
||||
rpc::RPCClient::TraceScope trace("HushChat / identity");
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("HushChat / identity");
|
||||
auto response = rpc_->call("z_exportmnemonic");
|
||||
if (response.contains("mnemonic") && response["mnemonic"].is_string()) {
|
||||
// Scrub the json's own copy of the seed after taking ours (the rest of the RPC
|
||||
// response chain is unmanaged — a fuller fix belongs in the RPC layer).
|
||||
auto& phrase = response["mnemonic"].get_ref<std::string&>();
|
||||
mnemonic = phrase;
|
||||
secret = phrase;
|
||||
if (!phrase.empty()) sodium_memzero(&phrase[0], phrase.size());
|
||||
}
|
||||
} catch (const rpc::RpcError&) {
|
||||
definitiveFail = true;
|
||||
// No mnemonic → derive from a spending key instead (legacy wallet / old daemon).
|
||||
if (fallbackZaddr.empty()) {
|
||||
unavailable = true;
|
||||
} else {
|
||||
try {
|
||||
secret = rpc_->call("z_exportkey", {fallbackZaddr}).get<std::string>();
|
||||
} catch (const rpc::RpcError&) {
|
||||
unavailable = true; // no spending key either (view-only?) — give up
|
||||
} catch (const std::exception&) {
|
||||
transientFail = true;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
transientFail = true;
|
||||
}
|
||||
return [this, mnemonic = std::move(mnemonic), definitiveFail, transientFail]() mutable {
|
||||
return [this, secret = std::move(secret), transientFail, unavailable]() mutable {
|
||||
chat_identity_fetch_in_flight_ = false;
|
||||
if (definitiveFail) chat_identity_unavailable_ = true;
|
||||
if (unavailable) chat_identity_unavailable_ = true;
|
||||
// Provision only on success AND while still unlocked: a lock (e.g. auto-lock) can
|
||||
// land during the blocking fetch, and provisioning then would unlock the chat DB +
|
||||
// load decrypted history while the wallet is locked. Leaving the flag cleared here
|
||||
// re-arms a fresh fetch on the next unlock.
|
||||
if (!definitiveFail && !transientFail && !mnemonic.empty() && !state_.isLocked()) {
|
||||
provisionChatIdentityFromSecret(mnemonic); // copies internally
|
||||
if (!transientFail && !unavailable && !secret.empty() && !state_.isLocked()) {
|
||||
provisionChatIdentityFromSecret(secret); // copies internally
|
||||
}
|
||||
if (!mnemonic.empty()) sodium_memzero(&mnemonic[0], mnemonic.size());
|
||||
if (!secret.empty()) sodium_memzero(&secret[0], secret.size());
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// A stable wallet z-address for outgoing chat headers (the "z" reply field + the send-from address).
|
||||
// Prefers a spendable one so replies land somewhere we control and fees can be paid.
|
||||
std::string App::chatReplyZaddr() const
|
||||
// A STABLE wallet z-address for chat: the "z" reply field + the send-from address, and (for
|
||||
// legacy/non-mnemonic wallets) the source of the seed-derived chat identity. Persisted on first use
|
||||
// so the identity + reply address don't shift when new addresses are generated. Prefers a spendable
|
||||
// one so replies land somewhere we control and fees can be paid.
|
||||
std::string App::chatReplyZaddr()
|
||||
{
|
||||
// Reuse the previously-chosen address as long as we still own it spendably.
|
||||
if (settings_) {
|
||||
const std::string saved = settings_->getChatReplyZaddr();
|
||||
if (!saved.empty()) {
|
||||
for (const auto& addr : state_.z_addresses)
|
||||
if (addr.address == saved && addr.has_spending_key) return saved;
|
||||
}
|
||||
}
|
||||
// Otherwise pick the lexicographically-smallest spendable z-address (deterministic) and persist.
|
||||
std::string best;
|
||||
for (const auto& addr : state_.z_addresses)
|
||||
if (addr.has_spending_key && !addr.address.empty()) return addr.address;
|
||||
if (!state_.z_addresses.empty()) return state_.z_addresses.front().address;
|
||||
return {};
|
||||
if (addr.has_spending_key && !addr.address.empty() && (best.empty() || addr.address < best))
|
||||
best = addr.address;
|
||||
if (!best.empty()) {
|
||||
if (settings_ && settings_->getChatReplyZaddr() != best) {
|
||||
settings_->setChatReplyZaddr(best);
|
||||
settings_->save();
|
||||
}
|
||||
return best;
|
||||
}
|
||||
// No spendable z-address yet — fall back to the smallest we have (don't persist a view-only one).
|
||||
for (const auto& addr : state_.z_addresses)
|
||||
if (!addr.address.empty() && (best.empty() || addr.address < best)) best = addr.address;
|
||||
return best;
|
||||
}
|
||||
|
||||
// A unique opaque id (hex), used for the local echo id (we never harvest our own sends) and for
|
||||
|
||||
@@ -146,6 +146,7 @@ bool Settings::load(const std::string& path)
|
||||
loadScalar(j, "address_explorer_url", address_explorer_url_);
|
||||
loadScalar(j, "language", language_);
|
||||
loadScalar(j, "skin_id", skin_id_);
|
||||
loadScalar(j, "chat_reply_zaddr", chat_reply_zaddr_);
|
||||
loadScalar(j, "acrylic_enabled", acrylic_enabled_);
|
||||
loadScalar(j, "acrylic_quality", acrylic_quality_);
|
||||
loadScalar(j, "blur_multiplier", blur_multiplier_);
|
||||
@@ -401,6 +402,7 @@ bool Settings::save(const std::string& path)
|
||||
j["address_explorer_url"] = address_explorer_url_;
|
||||
j["language"] = language_;
|
||||
j["skin_id"] = skin_id_;
|
||||
j["chat_reply_zaddr"] = chat_reply_zaddr_;
|
||||
j["acrylic_enabled"] = acrylic_enabled_;
|
||||
j["acrylic_quality"] = acrylic_quality_;
|
||||
j["blur_multiplier"] = blur_multiplier_;
|
||||
|
||||
@@ -108,6 +108,12 @@ public:
|
||||
std::string getSkinId() const { return skin_id_; }
|
||||
void setSkinId(const std::string& id) { skin_id_ = id; }
|
||||
|
||||
// Stable z-address chosen for HushChat: the reply-to address in outgoing headers, and (for
|
||||
// legacy/non-mnemonic wallets) the source of the seed-derived chat identity. Persisted so the
|
||||
// identity + reply address don't shift when new addresses are generated.
|
||||
std::string getChatReplyZaddr() const { return chat_reply_zaddr_; }
|
||||
void setChatReplyZaddr(const std::string& z) { chat_reply_zaddr_ = z; }
|
||||
|
||||
// Privacy
|
||||
bool getSaveZtxs() const { return save_ztxs_; }
|
||||
void setSaveZtxs(bool save) { save_ztxs_ = save; }
|
||||
@@ -410,6 +416,7 @@ private:
|
||||
// Settings values
|
||||
std::string theme_ = "dragonx";
|
||||
std::string skin_id_ = "dragonx";
|
||||
std::string chat_reply_zaddr_;
|
||||
bool save_ztxs_ = true;
|
||||
bool auto_shield_ = true;
|
||||
bool use_tor_ = false;
|
||||
|
||||
Reference in New Issue
Block a user