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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user