From 82e61da62fea51c117ac43281f9f0562abdb2c12 Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 7 Jul 2026 22:16:47 -0500 Subject: [PATCH] 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) --- res/fonts/NotoSansCJK-Subset.ttf | Bin 543876 -> 555060 bytes res/lang/de.json | 18 +++ res/lang/es.json | 18 +++ res/lang/fr.json | 18 +++ res/lang/ja.json | 18 +++ res/lang/ko.json | 18 +++ res/lang/pt.json | 18 +++ res/lang/ru.json | 18 +++ res/lang/zh.json | 18 +++ src/app.cpp | 186 ++++++++++++++++++++++++++--- src/app.h | 16 +++ src/app_network.cpp | 32 ++++- src/app_sweep.cpp | 19 ++- src/config/settings.cpp | 2 + src/config/settings.h | 6 + src/daemon/seed_wallet_creator.cpp | 14 ++- src/util/i18n.cpp | 21 ++++ 17 files changed, 418 insertions(+), 22 deletions(-) diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index e264caf022a919ce65df189a0d14fb71fbb1b971..7b393b039b7907dc80ed31f1b9c0d129ad1a3343 100644 GIT binary patch delta 31931 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 XZ0iAm@-!^kPMb8<&l}bg>WlvZ4pr$k delta 21550 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 NÓ & SEGURANÇA -> Instalar incluído, ou Verificar atualizações) e tente novamente.", "mig_discard": "Descartar migração", "mig_done": "Migração concluída. Sua carteira agora é protegida pela sua frase de recuperação.", "mig_done_btn": "Concluído", "mig_done_detail": "O daemon está reescaneando para exibir seus fundos — isso pode levar alguns minutos. Sua carteira anterior foi salva como um arquivo .bak na pasta de dados.", "mig_intro": "Isso cria uma carteira totalmente nova, protegida por uma frase de recuperação de 24 palavras, em um nó isolado, para que você possa mover seus fundos para ela. Sua carteira atual NÃO é alterada e NENHUM fundo é movido nesta etapa — primeiro você fará o backup da nova frase de recuperação e, em seguida, varrerá seus fundos para ela como uma etapa separada e confirmada.", "mig_later": "Depois", + "mig_no_funds": "Sua carteira não tem fundos para migrar. Você pode adotar a nova carteira semente diretamente — ela substitui sua carteira atual (vazia) pela carteira protegida por frase de recuperação. Sua carteira antiga ainda é movida para um backup com data e hora, por precaução.", + "mig_nofunds_confirm": "Entendo que isso substitui minha carteira atual pela nova carteira semente", + "mig_not_connected": "Conecte-se ao seu nó primeiro e depois reabra esta janela para migrar.", + "mig_precheck_checking": "Verificando sua carteira", "mig_receive_addr": "Endereço de recebimento da nova carteira:", + "mig_recheck": "Verificar novamente", "mig_remainder": "Alguns fundos ainda estão na sua carteira antiga (entradas demais para uma única transação) — varra o restante antes de trocar.", "mig_remaining": "Restante na carteira antiga: %.8f DRGX", "mig_seed_warning": "Anote estas 24 palavras em ordem e guarde-as offline. Elas são o único backup da sua nova carteira — se você as perder, os fundos que migrar estarão perdidos para sempre.", @@ -411,6 +428,7 @@ "mig_to": "Para: ", "mig_txid": "txid: ", "mig_unlock_first": "Desbloqueie sua carteira primeiro para gastar a partir dela.", + "mig_unlock_to_migrate": "Desbloqueie sua carteira primeiro e depois reabra esta janela para migrar.", "mig_waiting_mine": "Aguardando a transação de varredura ser minerada…", "mig_working": "Criando sua nova carteira semente em um nó isolado — isso pode levar um minuto. Sua carteira principal continua em execução.", "mine_when_idle": "Minerar quando ocioso", diff --git a/res/lang/ru.json b/res/lang/ru.json index 3492d93..8009552 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -227,6 +227,11 @@ "copy_uri": "Копировать URI", "current_price": "Текущая цена", "custom_fees": "Пользовательские комиссии", + "daemon_update_body": "В эту сборку кошелька встроена более новая версия узла DragonX, чем установленная сейчас. При обновлении установленный dragonxd (а также dragonx-cli/dragonx-tx) заменяется, после чего узел останавливается и перезапускается, чтобы новая версия вступила в силу. Рекомендуется — более новый узел может добавить возможности (например, поддержку сид-фразы), которых нет в старом.", + "daemon_update_keep": "Оставить текущий", + "daemon_update_now": "Обновить сейчас", + "daemon_update_safe": "Ваш кошелёк, ключи и данные блокчейна не затрагиваются — заменяются только программные файлы демона. Если вы намеренно используете собственный узел, выберите «Оставить текущий».", + "daemon_update_title": "Обновить демон узла?", "daemon_version": "Демон", "dark": "Тёмная", "date": "Дата", @@ -384,21 +389,33 @@ "merge_started": "Операция объединения начата", "merge_title": "Объединить на адрес", "mig_adopt": "Сделать это моим кошельком", + "mig_adopt_now": "Принять сид-кошелёк", "mig_adopting": "Установка вашего нового кошелька и пересканирование…", + "mig_adopting_note": "Узел перезапускается на вашем новом кошельке и выполнит пересканирование цепочки — это может занять несколько минут. Оставьте кошелёк работающим; он подключится автоматически.", + "mig_already_mnemonic": "У вашего кошелька уже есть сид-фраза из 24 слов — переносить нечего. Просто сделайте её резервную копию и храните слова в безопасном месте.", "mig_backed_up": "Я записал свою сид-фразу", + "mig_backup_instead": "Сделать резервную копию сид-фразы", "mig_balance": "Баланс текущего кошелька: %.8f DRGX (за вычетом небольшой комиссии)", + "mig_check_failed": "Не удалось проверить ваш кошелёк. Убедитесь, что он разблокирован и подключён, затем выполните повторную проверку.", + "mig_checking_balance": "Проверка вашего баланса", "mig_confirming": "Ожидание подтверждения перевода в сети перед переключением кошельков — это гарантирует, что ваши средства действительно перемещены.", "mig_confs": "Подтверждения перевода: %d", "mig_continue_sweep": "Перейти к переводу", "mig_copy_seed": "Копировать сид-фразу", "mig_create": "Создать сид-кошелёк", + "mig_daemon_too_old": "Ваш узел DragonX слишком стар, чтобы создать сид-кошелёк — в нём нет необходимой для этого поддержки мнемоники. Обновите узел (Настройки → Узел и безопасность → Установить встроенный или Проверить обновления), затем повторите попытку.", "mig_discard": "Отменить миграцию", "mig_done": "Миграция завершена. Ваш кошелёк теперь защищён вашей сид-фразой.", "mig_done_btn": "Готово", "mig_done_detail": "Демон выполняет пересканирование, чтобы показать ваши средства — это может занять несколько минут. Ваш предыдущий кошелёк сохранён как .bak в папке с данными.", "mig_intro": "Это создаёт совершенно новый кошелёк, защищённый сид-фразой из 24 слов, в изолированном узле, чтобы вы могли перевести в него свои средства. Ваш текущий кошелёк НЕ затрагивается, и на этом шаге НИКАКИЕ средства не перемещаются — сначала вы сделаете резервную копию новой сид-фразы, а затем отдельным, подтверждённым шагом переведёте в неё свои средства.", "mig_later": "Позже", + "mig_no_funds": "В вашем кошельке нет средств для переноса. Вы можете сразу принять новый сид-кошелёк — он заменит ваш текущий (пустой) кошелёк на кошелёк с сид-фразой. Ваш старый кошелёк на всякий случай всё равно будет сохранён в резервную копию с меткой времени.", + "mig_nofunds_confirm": "Я понимаю, что это заменит мой текущий кошелёк новым сид-кошельком", + "mig_not_connected": "Сначала подключитесь к своему узлу, затем снова откройте это окно для миграции.", + "mig_precheck_checking": "Проверка вашего кошелька", "mig_receive_addr": "Адрес получения нового кошелька:", + "mig_recheck": "Проверить снова", "mig_remainder": "Часть средств всё ещё находится в вашем старом кошельке (слишком много входов для одной транзакции) — переведите остаток перед переключением.", "mig_remaining": "Осталось в старом кошельке: %.8f DRGX", "mig_seed_warning": "Запишите эти 24 слова по порядку и храните их офлайн. Это единственная резервная копия вашего нового кошелька — если вы их потеряете, средства, которые вы перенесёте, будут утрачены навсегда.", @@ -411,6 +428,7 @@ "mig_to": "Кому: ", "mig_txid": "txid: ", "mig_unlock_first": "Сначала разблокируйте кошелёк, чтобы тратить с него.", + "mig_unlock_to_migrate": "Сначала разблокируйте кошелёк, затем снова откройте это окно для миграции.", "mig_waiting_mine": "Ожидание включения транзакции перевода в блок…", "mig_working": "Создание вашего нового сид-кошелька в изолированном узле — это может занять минуту. Ваш основной кошелёк продолжает работать.", "mine_when_idle": "Майнить в простое", diff --git a/res/lang/zh.json b/res/lang/zh.json index 0e562f9..03b03b4 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -227,6 +227,11 @@ "copy_uri": "复制 URI", "current_price": "当前价格", "custom_fees": "自定义手续费", + "daemon_update_body": "此钱包版本内置的 DragonX 节点比当前已安装的更新。更新会替换已安装的 dragonxd(以及 dragonx-cli/dragonx-tx),然后停止并重启节点以使新版本生效。推荐更新——更新的节点可能增加旧版本所缺少的功能(例如助记词支持)。", + "daemon_update_keep": "保持当前版本", + "daemon_update_now": "立即更新", + "daemon_update_safe": "您的钱包、密钥和区块链数据不会被改动——仅替换守护进程的程序文件。如果您是有意运行自定义节点,请选择“保持当前版本”。", + "daemon_update_title": "更新节点守护进程?", "daemon_version": "守护进程", "dark": "深色", "date": "日期", @@ -384,21 +389,33 @@ "merge_started": "合并操作已开始", "merge_title": "合并到地址", "mig_adopt": "将其设为我的钱包", + "mig_adopt_now": "采用助记词钱包", "mig_adopting": "正在安装您的新钱包并重新扫描…", + "mig_adopting_note": "节点正在您的新钱包上重启,并将重新扫描区块链——这可能需要几分钟。请让钱包保持运行;它会自动重新连接。", + "mig_already_mnemonic": "您的钱包已经有 24 词助记词——无需迁移。只需备份它并将这些词妥善保存在安全的地方即可。", "mig_backed_up": "我已抄写下我的助记词", + "mig_backup_instead": "备份助记词", "mig_balance": "当前钱包余额:%.8f DRGX(扣除少量手续费)", + "mig_check_failed": "无法检查您的钱包。请确保它已解锁并已连接,然后重新检查。", + "mig_checking_balance": "正在检查您的余额", "mig_confirming": "正在等待此次扫入在链上确认,然后再切换钱包——这可确保您的资金确实已经转移。", "mig_confs": "扫入确认数:%d", "mig_continue_sweep": "继续扫入", "mig_copy_seed": "复制助记词", "mig_create": "创建助记词钱包", + "mig_daemon_too_old": "您的 DragonX 节点版本太旧,无法创建助记词钱包——它缺少此功能所需的助记词支持。请更新节点(设置 → 节点与安全 → 安装内置版本,或检查更新),然后重试。", "mig_discard": "放弃迁移", "mig_done": "迁移完成。您的钱包现在由您的助记词支持。", "mig_done_btn": "完成", "mig_done_detail": "守护进程正在重新扫描以显示您的资金——这可能需要几分钟。您之前的钱包已作为 .bak 保存在数据文件夹中。", "mig_intro": "这会在一个隔离的节点中创建一个由 24 词助记词支持的全新钱包,以便您将资金转入其中。您当前的钱包不会受到影响,本步骤也不会转移任何资金——您将先备份新助记词,然后再作为单独的、经确认的步骤把资金扫入其中。", "mig_later": "稍后", + "mig_no_funds": "您的钱包没有可迁移的资金。您可以直接采用新的助记词钱包——它会用由助记词支持的钱包替换您当前的(空)钱包。以防万一,您的旧钱包仍会移到一个带时间戳的备份中。", + "mig_nofunds_confirm": "我明白这会用新的助记词钱包替换我当前的钱包", + "mig_not_connected": "请先连接到您的节点,然后重新打开此界面进行迁移。", + "mig_precheck_checking": "正在检查您的钱包", "mig_receive_addr": "新钱包接收地址:", + "mig_recheck": "重新检查", "mig_remainder": "您的旧钱包中仍有部分资金(一笔交易的输入过多)——请在切换前扫入剩余部分。", "mig_remaining": "旧钱包中剩余:%.8f DRGX", "mig_seed_warning": "请按顺序将这 24 个词抄写下来并离线保存。它们是您新钱包唯一的备份——一旦丢失,您迁移的资金将永久无法恢复。", @@ -411,6 +428,7 @@ "mig_to": "至:", "mig_txid": "txid: ", "mig_unlock_first": "请先解锁您的钱包才能从中花费。", + "mig_unlock_to_migrate": "请先解锁您的钱包,然后重新打开此界面进行迁移。", "mig_waiting_mine": "正在等待扫入交易被打包…", "mig_working": "正在隔离的节点中创建您的新助记词钱包——这可能需要一分钟。您的主钱包将继续运行。", "mine_when_idle": "空闲时挖矿", diff --git a/src/app.cpp b/src/app.cpp index 9562636..cc146c3 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -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() diff --git a/src/app.h b/src/app.h index 0cd3ab3..0894941 100644 --- a/src/app.h +++ b/src/app.h @@ -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(); diff --git a/src/app_network.cpp b/src/app_network.cpp index 192e9bb..0bc8a35 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -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()); 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; } }; }); } diff --git a/src/app_sweep.cpp b/src/app_sweep.cpp index e1ab22b..13758fc 100644 --- a/src/app_sweep.cpp +++ b/src/app_sweep.cpp @@ -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; diff --git a/src/config/settings.cpp b/src/config/settings.cpp index 488b5f4..2245cc6 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -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_; diff --git a/src/config/settings.h b/src/config/settings.h index f4002cc..57c1848 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -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 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_; diff --git a/src/daemon/seed_wallet_creator.cpp b/src/daemon/seed_wallet_creator.cpp index 2a4fa61..bcad883 100644 --- a/src/daemon/seed_wallet_creator.cpp +++ b/src/daemon/seed_wallet_creator.cpp @@ -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). diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 7b726ed..b4fd442 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -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";