feat(wallet): give new full-node wallets a BIP39 seed phrase + backup UI

New full-node wallets now get a real 24-word recovery phrase, and users can
reveal/back it up from Settings.

- Pass -usemnemonic=1 to dragonxd. The daemon reads it only inside
  GenerateNewSeed() when a wallet has no seed yet, so it is inert on existing
  wallets (safe to pass unconditionally) and makes every freshly-created
  wallet mnemonic-backed — its phrase is then exportable via z_exportmnemonic
  and portable to SDXLite/ObsidianDragonLite. (Existing/legacy wallets are
  unaffected and keep using the chat identity's z_exportkey fallback.)
- App::exportSeedPhrase() wraps z_exportmnemonic on the RPC worker with
  sodium_memzero wiping on every path; it flags the daemon's "not derived
  from a mnemonic" error as a legacy wallet rather than a failure.
- New "Back up seed phrase" modal (Settings -> Backup & Data, full-node
  gated): reveals the phrase in a numbered word grid, copy (45s clipboard
  auto-clear) + save-to-file (0600), wipes the secret on every close path
  incl. dismiss-mid-fetch. Shared ui/windows/seed_display.h grid helper.
- One-time nudge (settings flag seed_backup_reminded) toasts mnemonic-wallet
  users to back up their phrase; legacy wallets are not nagged.
- 15 new strings translated into all 8 languages (additive, 957->972 keys);
  CJK subset font rebuilt for the new glyphs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 10:49:03 -05:00
parent 036199a011
commit 403e145b30
18 changed files with 394 additions and 2 deletions

View File

@@ -2863,6 +2863,69 @@ void App::importPrivateKey(const std::string& rawKey, std::function<void(bool, c
});
}
void App::exportSeedPhrase(std::function<void(bool, bool, const std::string&, const std::string&)> callback)
{
if (!state_.connected || !rpc_ || !worker_) {
if (callback) callback(false, false, std::string(), "Not connected to the daemon.");
return;
}
worker_->post([this, callback]() -> rpc::RPCWorker::MainCb {
std::string phrase; // SECRET — wiped after the callback runs
std::string err;
bool ok = false;
bool noMnemonic = false;
rpc::RPCClient::TraceScope trace("Settings / Export seed phrase");
try {
auto response = rpc_->call("z_exportmnemonic");
if (response.contains("mnemonic") && response["mnemonic"].is_string()) {
auto& m = response["mnemonic"].get_ref<std::string&>();
phrase = m;
if (!m.empty()) sodium_memzero(&m[0], m.size()); // scrub the json's own copy
ok = !phrase.empty();
} else {
err = "The daemon returned no seed phrase.";
}
} catch (const rpc::RpcError& e) {
err = e.what();
// The daemon errors with this specific message when the wallet's seed is not
// mnemonic-derived (a legacy wallet). Any other error (e.g. locked) stays generic.
if (err.find("not derived from a mnemonic") != std::string::npos)
noMnemonic = true;
} catch (const std::exception& e) {
err = e.what();
}
return [callback, ok, noMnemonic, phrase = std::move(phrase), err]() mutable {
if (callback) callback(ok, noMnemonic, phrase, err);
if (!phrase.empty()) sodium_memzero(&phrase[0], phrase.size());
};
});
}
// One-time nudge to back up the seed phrase on a mnemonic-backed full-node wallet. Fires at
// most one z_exportmnemonic probe per install; the phrase is not retained (the probe only
// learns whether a mnemonic exists). Legacy (non-mnemonic) wallets are not nagged.
void App::maybeRemindSeedBackup()
{
if (lite_wallet_) return; // lite has its own seed UX
if (!settings_ || settings_->getSeedBackupReminded()) return;
if (!state_.connected || !state_.encryption_state_known) return;
if (state_.isLocked()) return; // wait until unlocked to read it
if (seed_backup_reminder_in_flight_) return;
seed_backup_reminder_in_flight_ = true;
exportSeedPhrase([this](bool ok, bool noMnemonic, const std::string& /*phrase*/,
const std::string& /*error*/) {
seed_backup_reminder_in_flight_ = false;
// Only mark "reminded" once we have a definitive answer, so a transient failure retries
// next session rather than silently suppressing the reminder forever.
if (ok || noMnemonic) {
settings_->setSeedBackupReminded(true);
settings_->save();
}
if (ok)
ui::Notifications::instance().info(TR("seed_backup_reminder"), 12.0f);
});
}
void App::backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback)
{
if (!state_.connected || !rpc_) {