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

@@ -33,6 +33,7 @@
#include "ui/windows/network_tab.h"
#include "ui/windows/console_command_executor.h"
#include "ui/windows/explorer_tab.h"
#include "ui/windows/seed_display.h"
#include "ui/windows/market_tab.h"
#include "ui/windows/settings_window.h"
#include "ui/windows/about_dialog.h"
@@ -715,6 +716,9 @@ void App::update()
// it can act; compiled away when the feature is off (constexpr gate inside).
maybeProvisionChatIdentity();
// One-time reminder to back up the wallet's seed phrase (mnemonic wallets only).
maybeRemindSeedBackup();
// Auto-lock check (only when connected + encrypted + unlocked)
if (state_.connected && state_.isUnlocked()) {
checkAutoLock();
@@ -1893,6 +1897,10 @@ void App::render()
renderBackupDialog();
}
if (show_seed_backup_) {
renderSeedBackupDialog();
}
// Security overlay dialogs (encrypt, decrypt, PIN) are rendered AFTER ImGui::End()
// to ensure they appear on top of all other content
@@ -2921,7 +2929,103 @@ void App::renderBackupDialog()
backup_status_.clear();
s_backup_confirm_overwrite = false;
}
ui::material::EndOverlayDialog();
}
void App::renderSeedBackupDialog()
{
// Wipe the revealed phrase and reset the dialog's state.
auto closeAndWipe = [this]() {
if (!seed_backup_phrase_.empty())
sodium_memzero(&seed_backup_phrase_[0], seed_backup_phrase_.size());
seed_backup_phrase_.clear();
seed_backup_status_.clear();
seed_backup_fetch_started_ = false;
seed_backup_loading_ = false;
seed_backup_no_mnemonic_ = false;
show_seed_backup_ = false;
};
// Kick off the async z_exportmnemonic fetch once, and only while unlocked.
if (!seed_backup_fetch_started_ && !state_.isLocked()) {
seed_backup_fetch_started_ = true;
seed_backup_loading_ = true;
seed_backup_no_mnemonic_ = false;
seed_backup_status_.clear();
exportSeedPhrase([this](bool ok, bool noMnemonic, const std::string& phrase,
const std::string& error) {
if (!show_seed_backup_) return; // dialog closed while the fetch was in flight
seed_backup_loading_ = false;
if (ok) {
seed_backup_phrase_ = phrase; // working copy — wiped on close
} else if (noMnemonic) {
seed_backup_no_mnemonic_ = true;
} else {
seed_backup_status_ = error.empty() ? std::string(TR("seed_backup_load_failed")) : error;
}
});
}
if (!ui::material::BeginOverlayDialog(TR("seed_backup_title"), &show_seed_backup_, 540.0f, 0.94f)) {
// Dismissed (Esc / outside click / X) — scrub the revealed secret.
if (seed_backup_fetch_started_) closeAndWipe();
return;
}
ui::material::Type().text(ui::material::TypeStyle::H6, TR("seed_backup_title"));
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
if (state_.isLocked()) {
ImGui::TextWrapped("%s", TR("seed_backup_locked"));
} else if (seed_backup_loading_) {
ImGui::TextWrapped("%s", TR("seed_backup_loading"));
} else if (seed_backup_no_mnemonic_) {
ImGui::TextWrapped("%s", TR("seed_backup_none"));
} else if (!seed_backup_phrase_.empty()) {
ImGui::TextWrapped("%s", TR("seed_backup_intro"));
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error());
ImGui::TextWrapped("%s", TR("seed_backup_warning"));
ImGui::PopStyleColor();
ImGui::Spacing();
ui::RenderSeedWordGrid(ui::SplitSeedWords(seed_backup_phrase_));
ImGui::Spacing();
if (!seed_backup_status_.empty()) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", seed_backup_status_.c_str());
ImGui::Spacing();
}
ImGui::Separator();
if (ui::material::StyledButton(TR("seed_backup_copy"), ImVec2(120, 0))) {
copySecretToClipboard(seed_backup_phrase_);
}
ImGui::SameLine();
if (ui::material::StyledButton(TR("seed_backup_save"), ImVec2(150, 0))) {
std::string path = util::Platform::getConfigDir() + "/dragonx-seed-backup.txt";
std::string content = seed_backup_phrase_ + "\n";
const bool ok = util::Platform::writeFileAtomically(path, content,
/*restrictPermissions=*/true);
sodium_memzero(&content[0], content.size());
seed_backup_status_ = (ok ? std::string(TR("seed_backup_saved"))
: std::string(TR("seed_backup_save_failed"))) + path;
}
ImGui::SameLine();
if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120, 0))) {
closeAndWipe();
ui::material::EndOverlayDialog();
return;
}
ui::material::EndOverlayDialog();
return;
} else if (!seed_backup_status_.empty()) {
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", seed_backup_status_.c_str());
}
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120, 0))) {
closeAndWipe();
}
ui::material::EndOverlayDialog();
}