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