feat(migrate): daemon-update prompt + migrate-to-seed UX polish + i18n

Fixes the Windows migrate-to-seed failure (the old bundled daemon lacked
z_exportmnemonic) and rounds out the flow:

- seed_wallet_creator: actionable "daemon too old" message instead of the
  raw "Method not found" RPC error.
- Startup daemon-update prompt: if the wallet bundles a newer node than the
  installed one, offer to update it (once per version, keyed on a new
  daemon_update_prompted_size setting; never silently clobbers a custom
  node). "Install bundled" now also works when attached to an external /
  leftover daemon (RPC-stops it, then swaps + restarts).
- Migrate-to-seed Intro pre-flight: detect an already-mnemonic wallet
  (offer backup instead), an old daemon, or a locked/disconnected wallet
  before offering to create anything.
- No-funds path: a zero-balance wallet can adopt the new seed wallet
  directly (behind an acknowledgement checkbox) instead of a dead sweep.
- Loading spinners on the Working/Sweeping/Adopting steps; sweep coverage
  for the new intro / no-funds surfaces.
- Translations for all 18 new strings across 8 languages (+ rebuilt CJK
  subset font).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 22:16:47 -05:00
parent 48c79567d7
commit 82e61da62f
17 changed files with 418 additions and 22 deletions

View File

@@ -467,6 +467,11 @@ bool App::init()
// Idempotent; a missing file is fine (returns true). Purely local — no daemon/RPC dependency.
address_book_.load();
// If this wallet build bundles a newer daemon than the one already installed, offer to update it
// (once per version). Fixes the trap where a wallet update carrying a newer node silently kept
// the old binary — e.g. an old daemon lacking z_exportmnemonic blocking migrate-to-seed.
maybeOfferDaemonUpdate();
DEBUG_LOGF("Initialization complete\n");
return true;
}
@@ -1931,6 +1936,10 @@ void App::render()
renderSeedMigrationDialog();
}
if (show_daemon_update_prompt_ && !capture_mode_) {
renderDaemonUpdatePrompt();
}
// Security overlay dialogs (encrypt, decrypt, PIN) are rendered AFTER ImGui::End()
// to ensure they appear on top of all other content
@@ -2963,6 +2972,52 @@ void App::renderBackupDialog()
ui::material::EndOverlayDialog();
}
void App::maybeOfferDaemonUpdate()
{
if (!supportsFullNodeLifecycleActions()) return; // lite / no embedded-daemon build
if (capture_mode_ || !settings_) return; // never during a UI sweep
auto inst = resources::getInstalledDaemonInfo();
auto bun = resources::getBundledDaemonInfo();
// Only when a daemon is ALREADY installed and its size differs from the bundled one — an absent
// daemon is extracted normally, and a same-size match needs nothing. Fire once per bundled
// version (never re-nag a user running a custom node): key on the bundled size last offered.
if (!inst.exists || !bun.available || inst.size == bun.size) return;
if ((unsigned long long)settings_->getDaemonUpdatePromptedSize() == bun.size) return;
daemon_update_bundled_size_ = bun.size;
show_daemon_update_prompt_ = true;
}
void App::renderDaemonUpdatePrompt()
{
if (!ui::material::BeginOverlayDialog(TR("daemon_update_title"), &show_daemon_update_prompt_, 520.0f, 0.94f))
return;
const float dp = ui::Layout::dpiScale();
ui::material::Type().text(ui::material::TypeStyle::H6, TR("daemon_update_title"));
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
ImGui::TextWrapped("%s", TR("daemon_update_body"));
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
ImGui::TextWrapped("%s", TR("daemon_update_safe"));
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton(TR("daemon_update_now"), ImVec2(170 * dp, 0))) {
show_daemon_update_prompt_ = false;
reinstallBundledDaemon(); // stops the node (managed or external), overwrites, restarts
}
ImGui::SameLine();
if (ui::material::StyledButton(TR("daemon_update_keep"), ImVec2(150 * dp, 0))) {
show_daemon_update_prompt_ = false;
}
ui::material::EndOverlayDialog();
// Any close path (Update / Keep / Esc / X): record the bundled size so we don't prompt again for
// this wallet version — a user keeping a custom daemon is asked at most once per version.
if (!show_daemon_update_prompt_ && settings_) {
settings_->setDaemonUpdatePromptedSize((long long)daemon_update_bundled_size_);
settings_->save();
}
}
void App::renderSeedBackupDialog()
{
// Wipe the revealed phrase and reset the dialog's state.
@@ -3101,22 +3156,72 @@ void App::renderSeedMigrationDialog()
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
switch (seed_migration_step_) {
case SeedMigrationStep::Intro:
ImGui::TextWrapped("%s", TR("mig_intro"));
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton(TR("mig_create"), ImVec2(180 * dp, 0)))
beginCreateSeedWallet();
ImGui::SameLine();
if (ui::material::StyledButton(TR("cancel"), ImVec2(120 * dp, 0)))
close();
case SeedMigrationStep::Intro: {
// Pre-flight needs a connected, unlocked wallet — gate on that, then branch on what the
// probe found so we never offer a pointless (already-seeded) or impossible (old daemon) run.
if (!isConnected()) {
ImGui::TextWrapped("%s", TR("mig_not_connected"));
ImGui::Spacing(); ImGui::Separator();
if (ui::material::StyledButton(TR("close"), ImVec2(120 * dp, 0))) close();
break;
}
if (state_.isLocked()) {
ImGui::TextWrapped("%s", TR("mig_unlock_to_migrate"));
ImGui::Spacing(); ImGui::Separator();
if (ui::material::StyledButton(TR("close"), ImVec2(120 * dp, 0))) close();
break;
}
if (!seed_migration_precheck_started_ && !capture_mode_) beginSeedMigrationPrecheck();
switch (seed_migration_precheck_) {
case SeedMigrationPrecheck::Pending:
ImGui::TextColored(kMedium, "%s%s", TR("mig_precheck_checking"), ui::material::LoadingDots());
break;
case SeedMigrationPrecheck::AlreadyMnemonic:
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 0.8f, 0.3f, 1.0f));
ImGui::TextWrapped("%s", TR("mig_already_mnemonic"));
ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Separator();
if (ui::material::StyledButton(TR("mig_backup_instead"), ImVec2(200 * dp, 0))) {
close();
showSeedBackupDialog();
}
ImGui::SameLine();
if (ui::material::StyledButton(TR("close"), ImVec2(120 * dp, 0))) close();
break;
case SeedMigrationPrecheck::DaemonTooOld:
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error());
ImGui::TextWrapped("%s", TR("mig_daemon_too_old"));
ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Separator();
if (ui::material::StyledButton(TR("close"), ImVec2(120 * dp, 0))) close();
break;
case SeedMigrationPrecheck::CheckFailed:
ImGui::TextWrapped("%s", TR("mig_check_failed"));
ImGui::Spacing(); ImGui::Separator();
if (ui::material::StyledButton(TR("mig_recheck"), ImVec2(120 * dp, 0)))
seed_migration_precheck_started_ = false; // re-probe next frame
ImGui::SameLine();
if (ui::material::StyledButton(TR("close"), ImVec2(120 * dp, 0))) close();
break;
case SeedMigrationPrecheck::Legacy:
default:
ImGui::TextWrapped("%s", TR("mig_intro"));
ImGui::Spacing(); ImGui::Separator();
if (ui::material::StyledButton(TR("mig_create"), ImVec2(180 * dp, 0)))
beginCreateSeedWallet();
ImGui::SameLine();
if (ui::material::StyledButton(TR("cancel"), ImVec2(120 * dp, 0)))
close();
break;
}
break;
}
case SeedMigrationStep::Working:
ImGui::TextWrapped("%s", TR("mig_working"));
ImGui::Spacing();
if (!seed_migration_status_.empty())
ImGui::TextColored(kMedium, "%s", seed_migration_status_.c_str());
ImGui::TextColored(kMedium, "%s%s", seed_migration_status_.c_str(), ui::material::LoadingDots());
break;
case SeedMigrationStep::ShowSeed: {
@@ -3153,6 +3258,8 @@ void App::renderSeedMigrationDialog()
if (ui::material::StyledButton(TR("mig_continue_sweep"), ImVec2(170 * dp, 0))) {
wipeSeed(); // the sweep/adopt steps only use the address, never the seed itself
seed_migration_step_ = SeedMigrationStep::Sweep;
seed_migration_balance_loaded_ = false;
seed_migration_nofunds_confirmed_ = false;
refreshSeedMigrationBalance();
}
if (!seed_migration_backed_up_) ImGui::EndDisabled();
@@ -3177,6 +3284,37 @@ void App::renderSeedMigrationDialog()
}
case SeedMigrationStep::Sweep: {
// Wait for the balance to load before deciding funds vs no-funds (0.0 is also the initial
// value, so gate on the loaded flag to avoid flashing "no funds" while it's still fetching).
if (!seed_migration_balance_loaded_) {
ImGui::TextColored(kMedium, "%s%s", TR("mig_checking_balance"), ui::material::LoadingDots());
ImGui::Spacing(); ImGui::Separator();
if (ui::material::StyledButton(TR("mig_later"), ImVec2(100 * dp, 0))) close();
break;
}
if (seed_migration_balance_ <= DRAGONX_DEFAULT_FEE) {
// Nothing to sweep — adopt the fresh seed wallet directly. Its wallet.dat replaces the
// current (empty) one; adopt still moves the legacy wallet aside to a timestamped backup.
ImGui::TextWrapped("%s", TR("mig_no_funds"));
ImGui::Spacing();
ImGui::TextWrapped("%s%s", TR("mig_to"), seed_migration_dest_.c_str());
ImGui::Spacing();
// Adopt swaps wallet.dat — gate it behind an explicit acknowledgement even though no
// funds are at risk (the legacy wallet is still moved aside to a backup on adopt).
ImGui::Checkbox(TR("mig_nofunds_confirm"), &seed_migration_nofunds_confirmed_);
ImGui::Spacing(); ImGui::Separator();
if (!seed_migration_nofunds_confirmed_) ImGui::BeginDisabled();
if (ui::material::StyledButton(TR("mig_adopt_now"), ImVec2(190 * dp, 0)))
beginAdoptSeedWallet();
if (!seed_migration_nofunds_confirmed_) ImGui::EndDisabled();
ImGui::SameLine();
if (ui::material::StyledButton(TR("refresh"), ImVec2(110 * dp, 0)))
refreshSeedMigrationBalance();
ImGui::SameLine();
if (ui::material::StyledButton(TR("mig_later"), ImVec2(100 * dp, 0)))
close();
break;
}
ImGui::TextWrapped("%s", TR("mig_sweep_intro"));
ImGui::Spacing();
char balbuf[96]; snprintf(balbuf, sizeof(balbuf), TR("mig_balance"), seed_migration_balance_);
@@ -3209,8 +3347,7 @@ void App::renderSeedMigrationDialog()
case SeedMigrationStep::Sweeping:
ImGui::TextWrapped("%s", TR("mig_sweeping"));
ImGui::Spacing();
if (!seed_migration_status_.empty())
ImGui::TextColored(kMedium, "%s", seed_migration_status_.c_str());
ImGui::TextColored(kMedium, "%s%s", seed_migration_status_.c_str(), ui::material::LoadingDots());
break;
case SeedMigrationStep::Confirming: {
@@ -3254,8 +3391,11 @@ void App::renderSeedMigrationDialog()
case SeedMigrationStep::Adopting:
ImGui::TextWrapped("%s", TR("mig_adopting"));
ImGui::Spacing();
if (!seed_migration_status_.empty())
ImGui::TextColored(kMedium, "%s", seed_migration_status_.c_str());
ImGui::TextColored(kMedium, "%s%s", seed_migration_status_.c_str(), ui::material::LoadingDots());
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
ImGui::TextWrapped("%s", TR("mig_adopting_note"));
ImGui::PopStyleColor();
break;
case SeedMigrationStep::Done:
@@ -3717,8 +3857,12 @@ void App::reinstallBundledDaemon()
ui::Notifications::instance().warning("This build has no bundled daemon to install");
return;
}
if (!daemon_controller_ || !isUsingEmbeddedDaemon()) {
ui::Notifications::instance().warning("Reinstalling the daemon requires the embedded daemon");
// Require embedded-daemon *support*, but NOT an active daemon_controller_. When the wallet is
// attached to an external or leftover dragonxd (controller null, so stopEmbeddedDaemon() no-ops)
// we can still RPC-stop that node, overwrite the binaries, and bring up our own managed daemon —
// which is exactly the state that previously blocked "Install bundled" with a cryptic warning.
if (!supportsEmbeddedDaemon()) {
ui::Notifications::instance().warning("This build has no embedded daemon to install");
return;
}
@@ -3733,7 +3877,13 @@ void App::reinstallBundledDaemon()
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
AppDaemonLifecycleRuntime runtime(*this);
daemon::AsyncLifecycleTaskContext ctx(token, shutting_down_);
// Stop the daemon so its binary is no longer locked (Windows) / in use (ETXTBSY on Linux).
// Stop the running node so its binary is free. stopDaemonWithPolicy() covers a wallet-managed
// daemon; if we're instead attached to an external/leftover dragonxd (daemon_controller_ is
// null, so stopEmbeddedDaemon() returns immediately), send an explicit RPC stop so that node
// exits too — otherwise it keeps the RPC port and the fresh daemon can't start.
if (!daemon_controller_ && rpc_ && rpc_->isConnected()) {
sendStopCommandSafely(*rpc_, "reinstall-daemon");
}
runtime.stopDaemonWithPolicy();
// Wait (bounded, ~12s) for the process to exit and release the binary before overwriting.
for (int i = 0; i < 120 && daemon::EmbeddedDaemon::isRpcPortInUse()

View File

@@ -699,6 +699,19 @@ private:
enum class SeedMigrationStep { Intro, Working, ShowSeed, Sweep, Sweeping, Confirming, Adopting, Done, Error };
bool show_seed_migration_ = false;
SeedMigrationStep seed_migration_step_ = SeedMigrationStep::Intro;
// Intro pre-flight: probe the current wallet before offering to create a seed wallet, so we can
// skip a pointless migration (AlreadyMnemonic → offer backup) or explain why it can't run
// (DaemonTooOld). Set from beginSeedMigrationPrecheck()'s async callback (main thread).
enum class SeedMigrationPrecheck { Pending, Legacy, AlreadyMnemonic, DaemonTooOld, CheckFailed };
SeedMigrationPrecheck seed_migration_precheck_ = SeedMigrationPrecheck::Pending;
bool seed_migration_precheck_started_ = false;
bool seed_migration_balance_loaded_ = false; // Sweep step: distinguishes "0 funds" from "not loaded yet"
bool seed_migration_nofunds_confirmed_ = false; // "replace my wallet" gate for the no-funds adopt
// "A newer node is bundled — update?" startup prompt (see maybeOfferDaemonUpdate).
bool show_daemon_update_prompt_ = false;
unsigned long long daemon_update_bundled_size_ = 0; // bundled daemon size the prompt offers
bool seed_migration_in_flight_ = false; // main-thread guard while the bg create task runs
std::string seed_migration_seed_; // SECRET — the revealed phrase, wiped on close
std::string seed_migration_dest_; // new shielded z-address (Phase 2 sweep target)
@@ -1018,6 +1031,9 @@ private:
void renderBackupDialog();
void renderSeedBackupDialog(); // full-node "Back up seed phrase" modal (z_exportmnemonic)
void renderSeedMigrationDialog(); // "Migrate to a seed wallet" guided modal (Phase 1: create)
void beginSeedMigrationPrecheck(); // Intro: probe whether the wallet is legacy / already-seeded
void maybeOfferDaemonUpdate(); // at startup, flag the prompt if a newer daemon is bundled
void renderDaemonUpdatePrompt(); // "a newer node is bundled — update the installed daemon?"
void renderFirstRunWizard();
void renderLockScreen();
void renderEncryptWalletDialog();

View File

@@ -2879,13 +2879,43 @@ void App::showSeedMigrationDialog()
seed_migration_step_ = SeedMigrationStep::Confirming;
} else {
seed_migration_step_ = SeedMigrationStep::Sweep;
seed_migration_balance_loaded_ = false;
seed_migration_nofunds_confirmed_ = false;
refreshSeedMigrationBalance();
}
} else {
seed_migration_step_ = SeedMigrationStep::Intro;
// Fresh start: the Intro step will pre-flight the wallet (legacy vs already-seeded vs old
// daemon) before offering to create anything.
seed_migration_precheck_ = SeedMigrationPrecheck::Pending;
seed_migration_precheck_started_ = false;
}
}
// Intro pre-flight: probe z_exportmnemonic on the CURRENT wallet so we can branch the Intro:
// ok → the wallet is already mnemonic-backed (offer backup, not a pointless migration)
// "not derived" → a legacy wallet (proceed with create)
// "Method not found" → the daemon is too old to create a seed wallet (explain + bail)
// The probe needs an unlocked, connected wallet; the Intro gates on that before calling this.
void App::beginSeedMigrationPrecheck()
{
seed_migration_precheck_started_ = true;
seed_migration_precheck_ = SeedMigrationPrecheck::Pending;
exportSeedPhrase([this](bool ok, bool noMnemonic, const std::string& /*phrase*/,
const std::string& error) {
// exportSeedPhrase wipes the phrase after this callback — we only need the classification.
if (ok)
seed_migration_precheck_ = SeedMigrationPrecheck::AlreadyMnemonic;
else if (noMnemonic)
seed_migration_precheck_ = SeedMigrationPrecheck::Legacy;
else if (error.find("Method not found") != std::string::npos ||
error.find("-32601") != std::string::npos)
seed_migration_precheck_ = SeedMigrationPrecheck::DaemonTooOld;
else
seed_migration_precheck_ = SeedMigrationPrecheck::CheckFailed; // transient — allow re-check
});
}
void App::refreshSeedMigrationBalance()
{
if (!rpc_ || !worker_ || !state_.connected) return;
@@ -2898,7 +2928,7 @@ void App::refreshSeedMigrationBalance()
total = std::stod(res["total"].get<std::string>());
ok = true;
} catch (...) {}
return [this, total, ok]() { if (ok) seed_migration_balance_ = total; };
return [this, total, ok]() { if (ok) { seed_migration_balance_ = total; seed_migration_balance_loaded_ = true; } };
});
}

View File

@@ -234,11 +234,26 @@ void App::buildSweepCatalog()
a.seed_migration_seed_.clear(); a.seed_migration_status_.clear();
});
};
mig("modal-migrate-intro", SeedMigrationStep::Intro, nullptr);
// Intro pre-flight branches: pre-set the probe result so the sweep captures each variant
// (the probe itself is guarded off during capture_mode_).
mig("modal-migrate-intro", SeedMigrationStep::Intro, [](App& a) {
a.seed_migration_precheck_ = SeedMigrationPrecheck::Legacy;
a.seed_migration_precheck_started_ = true;
});
mig("modal-migrate-intro-seeded", SeedMigrationStep::Intro, [](App& a) {
a.seed_migration_precheck_ = SeedMigrationPrecheck::AlreadyMnemonic;
a.seed_migration_precheck_started_ = true;
});
mig("modal-migrate-intro-oldnode", SeedMigrationStep::Intro, [](App& a) {
a.seed_migration_precheck_ = SeedMigrationPrecheck::DaemonTooOld;
a.seed_migration_precheck_started_ = true;
});
mig("modal-migrate-showseed", SeedMigrationStep::ShowSeed,
[](App& a) { a.seed_migration_seed_ = kDemoMnemonic; a.seed_migration_backed_up_ = false; });
mig("modal-migrate-sweep", SeedMigrationStep::Sweep,
[](App& a) { a.seed_migration_balance_ = 15.75000000; });
[](App& a) { a.seed_migration_balance_ = 15.75000000; a.seed_migration_balance_loaded_ = true; });
mig("modal-migrate-nofunds", SeedMigrationStep::Sweep,
[](App& a) { a.seed_migration_balance_ = 0.0; a.seed_migration_balance_loaded_ = true; });
mig("modal-migrate-confirming", SeedMigrationStep::Confirming, [](App& a) {
a.seed_migration_sweep_confs_ = 1; a.seed_migration_sweep_txid_ = kDemoTxid;
a.seed_migration_legacy_remaining_ = 0.0;

View File

@@ -200,6 +200,7 @@ bool Settings::load(const std::string& path)
}
loadScalar(j, "wizard_completed", wizard_completed_);
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
loadScalar(j, "daemon_update_prompted_size", daemon_update_prompted_size_);
loadScalar(j, "seed_migration_pending", seed_migration_pending_);
loadScalar(j, "seed_migration_dest", seed_migration_dest_);
loadScalar(j, "seed_migration_temp_dir", seed_migration_temp_dir_);
@@ -438,6 +439,7 @@ bool Settings::save(const std::string& path)
}
j["wizard_completed"] = wizard_completed_;
j["seed_backup_reminded"] = seed_backup_reminded_;
j["daemon_update_prompted_size"] = daemon_update_prompted_size_;
j["seed_migration_pending"] = seed_migration_pending_;
j["seed_migration_dest"] = seed_migration_dest_;
j["seed_migration_temp_dir"] = seed_migration_temp_dir_;

View File

@@ -250,6 +250,11 @@ public:
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
// Bundled-daemon size we last prompted to install (see App::renderDaemonUpdatePrompt). Lets the
// "a newer node is bundled — update?" prompt fire once per wallet version, never re-nagging.
long long getDaemonUpdatePromptedSize() const { return daemon_update_prompted_size_; }
void setDaemonUpdatePromptedSize(long long v) { daemon_update_prompted_size_ = v; }
// Pending "migrate to a seed wallet" state (Phase 1 created the wallet; a later sweep/adopt
// step consumes it). dest = the new wallet's sweep-target z-address; tempDir = its datadir.
bool getSeedMigrationPending() const { return seed_migration_pending_; }
@@ -462,6 +467,7 @@ private:
std::map<std::string, AddressMeta> address_meta_;
bool wizard_completed_ = false;
bool seed_backup_reminded_ = false;
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt
bool seed_migration_pending_ = false;
std::string seed_migration_dest_;
std::string seed_migration_temp_dir_;

View File

@@ -117,7 +117,19 @@ SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
r.ok = !r.seedPhrase.empty() && !r.destAddress.empty();
if (!r.ok) r.error = "The isolated node returned an empty seed or address.";
} catch (const std::exception& e) {
r.error = std::string("Seed export failed: ") + e.what();
const std::string what = e.what();
// "Method not found" (JSON-RPC -32601) means this dragonxd predates mnemonic support —
// it has no z_exportmnemonic RPC (the older bundled binary). Migrate-to-seed can't work
// until the daemon is updated, so give an actionable message, not the raw RPC error.
if (what.find("Method not found") != std::string::npos ||
what.find("-32601") != std::string::npos) {
r.error = "This DragonX daemon is too old to create a seed wallet — it lacks mnemonic "
"support (the z_exportmnemonic RPC). Update to the latest DragonX daemon "
"(Settings -> NODE & SECURITY -> Check for updates, or Install bundled), then "
"try again.";
} else {
r.error = std::string("Seed export failed: ") + what;
}
}
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly).

View File

@@ -255,6 +255,22 @@ void I18n::loadBuiltinEnglish()
strings_["mig_title"] = "Migrate to a seed wallet";
strings_["mig_intro"] = "This creates a brand-new wallet backed by a 24-word seed phrase, in an isolated node, so you can move your funds into it. Your current wallet is NOT touched and NO funds move in this step — you'll back up the new seed first, then sweep your funds into it as a separate, confirmed step.";
strings_["mig_create"] = "Create seed wallet";
// Intro pre-flight (checking the current wallet before offering to migrate)
strings_["mig_precheck_checking"] = "Checking your wallet";
strings_["mig_already_mnemonic"] = "Your wallet already has a 24-word seed phrase — there's nothing to migrate. Just back it up and keep the words somewhere safe.";
strings_["mig_backup_instead"] = "Back up seed phrase";
strings_["mig_daemon_too_old"] = "Your DragonX node is too old to create a seed wallet — it lacks the mnemonic support this needs. Update the node (Settings → NODE & SECURITY → Install bundled, or Check for updates), then try again.";
strings_["mig_not_connected"] = "Connect to your node first, then reopen this to migrate.";
strings_["mig_unlock_to_migrate"] = "Unlock your wallet first, then reopen this to migrate.";
strings_["mig_check_failed"] = "Couldn't check your wallet. Make sure it's unlocked and connected, then re-check.";
strings_["mig_recheck"] = "Re-check";
// Sweep step: balance still loading / no funds to move
strings_["mig_checking_balance"] = "Checking your balance";
strings_["mig_no_funds"] = "Your wallet has no funds to migrate. You can adopt the new seed wallet directly — it replaces your current (empty) wallet with the seed-backed one. Your old wallet is still moved aside to a timestamped backup, just in case.";
strings_["mig_adopt_now"] = "Adopt seed wallet";
strings_["mig_nofunds_confirm"] = "I understand this replaces my current wallet with the new seed wallet";
// Adopting step note (shown under the spinner while the node restarts + rescans)
strings_["mig_adopting_note"] = "The node is restarting on your new wallet and will rescan the chain — this can take several minutes. Leave the wallet running; it reconnects automatically.";
strings_["mig_working"] = "Creating your new seed wallet in an isolated node — this can take a minute. Your main wallet keeps running.";
strings_["mig_seed_warning"] = "Write these 24 words down in order and store them offline. They are the only backup of your new wallet — if you lose them, the funds you migrate are gone forever.";
strings_["mig_receive_addr"] = "New wallet receive address:";
@@ -521,6 +537,11 @@ void I18n::loadBuiltinEnglish()
strings_["confirm_reinstall_daemon_title"] = "Install Bundled Daemon";
strings_["confirm_reinstall_daemon_msg"] = "This stops the daemon, overwrites the installed dragonxd (and dragonx-cli/dragonx-tx) with the versions bundled in this wallet build, then restarts the node. Use this to recover or update the node binary.";
strings_["confirm_reinstall_daemon_safe"] = "Your wallet, keys and blockchain data are not touched — only the daemon program files are replaced.";
strings_["daemon_update_title"] = "Update the node daemon?";
strings_["daemon_update_body"] = "This wallet build bundles a newer DragonX node than the one currently installed. Updating replaces the installed dragonxd (and dragonx-cli/dragonx-tx), then stops and restarts the node so the new version takes effect. Recommended — a newer node can add features (e.g. seed-phrase support) the old one lacks.";
strings_["daemon_update_safe"] = "Your wallet, keys and blockchain data are not touched — only the daemon program files are replaced. If you deliberately run a custom node, choose Keep current.";
strings_["daemon_update_now"] = "Update now";
strings_["daemon_update_keep"] = "Keep current";
strings_["confirm_restart_daemon_title"] = "Restart Daemon";
strings_["confirm_restart_daemon_msg"] = "This stops and restarts the daemon to apply the changed options. The wallet will briefly disconnect and reconnect.";
strings_["lite_maintenance"] = "Maintenance";