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:
106
src/app.cpp
106
src/app.cpp
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user