feat(wallet): confirm before stopping an adopted node to switch wallets
When switching wallets, the node the app would restart may be one this session ADOPTED — a dragonxd already running at launch (left up by "keep node running", or started by the user). Rather than stopping it silently (or the old dead-code "stop any external dragonxd first" refusal), show a "Stop the running node?" confirmation first; switching then stops it (RPC stop + wait for the port to fully free) and relaunches on the selected wallet. - switchToWallet(walletFile, stopDaemonConfirmed=false): when the node is adopted (externalDaemonDetected) and not yet confirmed, defer to the dialog and return. - renderSwitchStopDaemonDialog(): BlurFloat overlay (house style) with a warning header; confirm re-enters switchToWallet(w, true); cancel/X aborts, node keeps running. Owned nodes (started this session) still restart silently. - i18n (EN + 8 languages, additive) + CJK subset rebuild; modal-switch-stopnode sweep surface for visual review. Gate/re-entrancy adversarially verified (no state leak; owned switches ungated; prompt reappears on a reverted switch; dead-daemon-before-confirm handled). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -1294,6 +1294,10 @@
|
|||||||
"sweep_to": "Gefegt an:",
|
"sweep_to": "Gefegt an:",
|
||||||
"sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)",
|
"sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)",
|
||||||
"sweep_tx": "Transaktion:",
|
"sweep_tx": "Transaktion:",
|
||||||
|
"switch_stopnode_body": "Beim Wallet-Wechsel wird der Knoten mit der ausgewählten Wallet neu gestartet. Der laufende Knoten wird gestoppt und mit der neuen Wallet neu gestartet – wenn Sie ihn absichtlich laufen ließen, startet er automatisch wieder.",
|
||||||
|
"switch_stopnode_confirm": "Knoten stoppen & wechseln",
|
||||||
|
"switch_stopnode_title": "Laufenden Knoten stoppen?",
|
||||||
|
"switch_stopnode_warn": "Es läuft bereits ein Knoten, den diese Wallet nicht gestartet hat.",
|
||||||
"syncing": "Synchronisiere...",
|
"syncing": "Synchronisiere...",
|
||||||
"t_address": "T-Adresse",
|
"t_address": "T-Adresse",
|
||||||
"t_addresses": "T-Adressen",
|
"t_addresses": "T-Adressen",
|
||||||
|
|||||||
@@ -1294,6 +1294,10 @@
|
|||||||
"sweep_to": "Barrido a:",
|
"sweep_to": "Barrido a:",
|
||||||
"sweep_toggle": "Barrer a mi monedero (no conservar la clave)",
|
"sweep_toggle": "Barrer a mi monedero (no conservar la clave)",
|
||||||
"sweep_tx": "Transacción:",
|
"sweep_tx": "Transacción:",
|
||||||
|
"switch_stopnode_body": "Cambiar de cartera reinicia el nodo con la cartera seleccionada. El nodo en ejecución se detendrá y se reiniciará con la nueva cartera; si lo dejaste en marcha a propósito, volverá a iniciarse automáticamente.",
|
||||||
|
"switch_stopnode_confirm": "Detener nodo y cambiar",
|
||||||
|
"switch_stopnode_title": "¿Detener el nodo en ejecución?",
|
||||||
|
"switch_stopnode_warn": "Ya hay un nodo en ejecución que esta cartera no inició.",
|
||||||
"syncing": "Sincronizando...",
|
"syncing": "Sincronizando...",
|
||||||
"t_address": "Dirección T",
|
"t_address": "Dirección T",
|
||||||
"t_addresses": "Direcciones T",
|
"t_addresses": "Direcciones T",
|
||||||
|
|||||||
@@ -1294,6 +1294,10 @@
|
|||||||
"sweep_to": "Balayé vers :",
|
"sweep_to": "Balayé vers :",
|
||||||
"sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)",
|
"sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)",
|
||||||
"sweep_tx": "Transaction :",
|
"sweep_tx": "Transaction :",
|
||||||
|
"switch_stopnode_body": "Changer de portefeuille redémarre le nœud sur le portefeuille sélectionné. Le nœud en cours sera arrêté puis relancé sur le nouveau portefeuille — si vous l'avez laissé tourner exprès, il redémarre automatiquement.",
|
||||||
|
"switch_stopnode_confirm": "Arrêter le nœud et changer",
|
||||||
|
"switch_stopnode_title": "Arrêter le nœud en cours d'exécution ?",
|
||||||
|
"switch_stopnode_warn": "Un nœud que ce portefeuille n'a pas démarré est déjà en cours d'exécution.",
|
||||||
"syncing": "Synchronisation...",
|
"syncing": "Synchronisation...",
|
||||||
"t_address": "Adresse T",
|
"t_address": "Adresse T",
|
||||||
"t_addresses": "Adresses T",
|
"t_addresses": "Adresses T",
|
||||||
|
|||||||
@@ -1294,6 +1294,10 @@
|
|||||||
"sweep_to": "集約先:",
|
"sweep_to": "集約先:",
|
||||||
"sweep_toggle": "ウォレットに集約(鍵は保持しない)",
|
"sweep_toggle": "ウォレットに集約(鍵は保持しない)",
|
||||||
"sweep_tx": "取引:",
|
"sweep_tx": "取引:",
|
||||||
|
"switch_stopnode_body": "ウォレットを切り替えると、選択したウォレットでノードが再起動します。実行中のノードは停止され、新しいウォレットで再起動されます。意図的に起動したままにしていた場合は、自動的に復帰します。",
|
||||||
|
"switch_stopnode_confirm": "ノードを停止して切り替え",
|
||||||
|
"switch_stopnode_title": "実行中のノードを停止しますか?",
|
||||||
|
"switch_stopnode_warn": "このウォレットが起動していないノードがすでに実行中です。",
|
||||||
"syncing": "同期中...",
|
"syncing": "同期中...",
|
||||||
"t_address": "Tアドレス",
|
"t_address": "Tアドレス",
|
||||||
"t_addresses": "Tアドレス",
|
"t_addresses": "Tアドレス",
|
||||||
|
|||||||
@@ -1294,6 +1294,10 @@
|
|||||||
"sweep_to": "쓸어담은 주소:",
|
"sweep_to": "쓸어담은 주소:",
|
||||||
"sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)",
|
"sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)",
|
||||||
"sweep_tx": "거래:",
|
"sweep_tx": "거래:",
|
||||||
|
"switch_stopnode_body": "지갑을 전환하면 선택한 지갑으로 노드가 다시 시작됩니다. 실행 중인 노드가 중지되고 새 지갑으로 다시 시작됩니다. 일부러 계속 실행해 두었다면 자동으로 다시 켜집니다.",
|
||||||
|
"switch_stopnode_confirm": "노드 중지 후 전환",
|
||||||
|
"switch_stopnode_title": "실행 중인 노드를 중지할까요?",
|
||||||
|
"switch_stopnode_warn": "이 지갑이 시작하지 않은 노드가 이미 실행 중입니다.",
|
||||||
"syncing": "동기화 중...",
|
"syncing": "동기화 중...",
|
||||||
"t_address": "T 주소",
|
"t_address": "T 주소",
|
||||||
"t_addresses": "T 주소",
|
"t_addresses": "T 주소",
|
||||||
|
|||||||
@@ -1294,6 +1294,10 @@
|
|||||||
"sweep_to": "Varrido para:",
|
"sweep_to": "Varrido para:",
|
||||||
"sweep_toggle": "Varrer para minha carteira (não manter a chave)",
|
"sweep_toggle": "Varrer para minha carteira (não manter a chave)",
|
||||||
"sweep_tx": "Transação:",
|
"sweep_tx": "Transação:",
|
||||||
|
"switch_stopnode_body": "Trocar de carteira reinicia o nó com a carteira selecionada. O nó em execução será parado e reiniciado com a nova carteira — se você o deixou em execução de propósito, ele volta automaticamente.",
|
||||||
|
"switch_stopnode_confirm": "Parar nó e trocar",
|
||||||
|
"switch_stopnode_title": "Parar o nó em execução?",
|
||||||
|
"switch_stopnode_warn": "Já há um nó em execução que esta carteira não iniciou.",
|
||||||
"syncing": "Sincronizando...",
|
"syncing": "Sincronizando...",
|
||||||
"t_address": "Endereço T",
|
"t_address": "Endereço T",
|
||||||
"t_addresses": "Endereços T",
|
"t_addresses": "Endereços T",
|
||||||
|
|||||||
@@ -1294,6 +1294,10 @@
|
|||||||
"sweep_to": "Переведено на:",
|
"sweep_to": "Переведено на:",
|
||||||
"sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)",
|
"sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)",
|
||||||
"sweep_tx": "Транзакция:",
|
"sweep_tx": "Транзакция:",
|
||||||
|
"switch_stopnode_body": "Смена кошелька перезапускает узел с выбранным кошельком. Запущенный узел будет остановлен и перезапущен с новым кошельком — если вы намеренно оставили его работать, он запустится снова автоматически.",
|
||||||
|
"switch_stopnode_confirm": "Остановить узел и переключить",
|
||||||
|
"switch_stopnode_title": "Остановить запущенный узел?",
|
||||||
|
"switch_stopnode_warn": "Уже запущен узел, который не был запущен этим кошельком.",
|
||||||
"syncing": "Синхронизация...",
|
"syncing": "Синхронизация...",
|
||||||
"t_address": "T-адрес",
|
"t_address": "T-адрес",
|
||||||
"t_addresses": "T-адреса",
|
"t_addresses": "T-адреса",
|
||||||
|
|||||||
@@ -1294,6 +1294,10 @@
|
|||||||
"sweep_to": "归集到:",
|
"sweep_to": "归集到:",
|
||||||
"sweep_toggle": "归集到我的钱包(不保留密钥)",
|
"sweep_toggle": "归集到我的钱包(不保留密钥)",
|
||||||
"sweep_tx": "交易:",
|
"sweep_tx": "交易:",
|
||||||
|
"switch_stopnode_body": "切换钱包会以所选钱包重启节点。正在运行的节点将被停止并以新钱包重新启动——如果你是特意让它保持运行的,它会自动重新启动。",
|
||||||
|
"switch_stopnode_confirm": "停止节点并切换",
|
||||||
|
"switch_stopnode_title": "停止正在运行的节点?",
|
||||||
|
"switch_stopnode_warn": "已有一个并非由此钱包启动的节点正在运行。",
|
||||||
"syncing": "同步中...",
|
"syncing": "同步中...",
|
||||||
"t_address": "T 地址",
|
"t_address": "T 地址",
|
||||||
"t_addresses": "T 地址",
|
"t_addresses": "T 地址",
|
||||||
|
|||||||
37
src/app.cpp
37
src/app.cpp
@@ -2034,6 +2034,7 @@ void App::render()
|
|||||||
renderEncryptWalletDialog();
|
renderEncryptWalletDialog();
|
||||||
renderDecryptWalletDialog();
|
renderDecryptWalletDialog();
|
||||||
renderPinDialogs();
|
renderPinDialogs();
|
||||||
|
renderSwitchStopDaemonDialog();
|
||||||
|
|
||||||
// Render notifications (toast messages)
|
// Render notifications (toast messages)
|
||||||
ui::Notifications::instance().render();
|
ui::Notifications::instance().render();
|
||||||
@@ -3860,6 +3861,42 @@ void App::renderAntivirusHelpDialog()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::renderSwitchStopDaemonDialog()
|
||||||
|
{
|
||||||
|
if (!show_switch_stop_daemon_confirm_) return;
|
||||||
|
|
||||||
|
ui::material::OverlayDialogSpec ov;
|
||||||
|
ov.title = TR("switch_stopnode_title");
|
||||||
|
ov.p_open = &show_switch_stop_daemon_confirm_; // the X / backdrop click acts as Cancel
|
||||||
|
ov.style = ui::material::OverlayStyle::BlurFloat;
|
||||||
|
ov.cardWidth = 520.0f; ov.idSuffix = "switchstopnode";
|
||||||
|
if (!ui::material::BeginOverlayDialog(ov)) {
|
||||||
|
// Closed via X/backdrop — treat as cancel: drop the pending target.
|
||||||
|
if (!show_switch_stop_daemon_confirm_) pending_switch_wallet_file_.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float dp = ui::Layout::dpiScale();
|
||||||
|
|
||||||
|
ui::material::DialogWarningHeader(TR("switch_stopnode_warn"));
|
||||||
|
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
|
||||||
|
ImGui::TextWrapped("%s", TR("switch_stopnode_body"));
|
||||||
|
ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd()));
|
||||||
|
|
||||||
|
if (ui::material::TactileButton(TR("switch_stopnode_confirm"), ImVec2(200.0f * dp, 0))) {
|
||||||
|
const std::string w = pending_switch_wallet_file_;
|
||||||
|
pending_switch_wallet_file_.clear();
|
||||||
|
show_switch_stop_daemon_confirm_ = false;
|
||||||
|
switchToWallet(w, /*stopDaemonConfirmed=*/true); // now proceeds to stop + wait + restart
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ui::material::TactileButton(TR("cancel"), ImVec2(110.0f * dp, 0))) {
|
||||||
|
pending_switch_wallet_file_.clear();
|
||||||
|
show_switch_stop_daemon_confirm_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui::material::EndOverlayDialog();
|
||||||
|
}
|
||||||
|
|
||||||
void App::refreshNow()
|
void App::refreshNow()
|
||||||
{
|
{
|
||||||
// Trigger immediate refresh on all categories
|
// Trigger immediate refresh on all categories
|
||||||
|
|||||||
@@ -488,7 +488,9 @@ public:
|
|||||||
// Switch the active wallet: persist the new -wallet=<name>, stop the node, restart on it
|
// Switch the active wallet: persist the new -wallet=<name>, stop the node, restart on it
|
||||||
// (rescan only if it was never synced in this datadir). Per-wallet data follows automatically
|
// (rescan only if it was never synced in this datadir). Per-wallet data follows automatically
|
||||||
// via the identity-scoped caches (P1). No-op if that wallet is already active.
|
// via the identity-scoped caches (P1). No-op if that wallet is already active.
|
||||||
void switchToWallet(const std::string& walletFile);
|
// stopDaemonConfirmed: skip the "stop the running node?" confirmation (set true when re-entered from
|
||||||
|
// that dialog). The confirmation only appears when the node to restart was ADOPTED (externally running).
|
||||||
|
void switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed = false);
|
||||||
// Main-thread continuation: if a wallet switch's daemon failed to start, revert active_wallet_file.
|
// Main-thread continuation: if a wallet switch's daemon failed to start, revert active_wallet_file.
|
||||||
void processWalletSwitchRevert();
|
void processWalletSwitchRevert();
|
||||||
|
|
||||||
@@ -762,6 +764,10 @@ private:
|
|||||||
// loop flags a revert so a broken wallet still can't stick.
|
// loop flags a revert so a broken wallet still can't stick.
|
||||||
std::atomic<bool> wallet_switch_pending_confirm_{false};
|
std::atomic<bool> wallet_switch_pending_confirm_{false};
|
||||||
std::string wallet_switch_prev_file_;
|
std::string wallet_switch_prev_file_;
|
||||||
|
// Confirm-before-stop for a switch that would stop an ADOPTED (externally-running) node. switchToWallet
|
||||||
|
// sets these and defers to renderSwitchStopDaemonDialog; confirming re-calls switchToWallet(w, true).
|
||||||
|
bool show_switch_stop_daemon_confirm_ = false;
|
||||||
|
std::string pending_switch_wallet_file_;
|
||||||
|
|
||||||
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
|
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
|
||||||
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
|
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
|
||||||
@@ -1163,6 +1169,7 @@ private:
|
|||||||
void renderDecryptWalletDialog();
|
void renderDecryptWalletDialog();
|
||||||
void renderPinDialogs();
|
void renderPinDialogs();
|
||||||
void renderAntivirusHelpDialog();
|
void renderAntivirusHelpDialog();
|
||||||
|
void renderSwitchStopDaemonDialog(); // confirm before stopping an adopted node to switch wallets
|
||||||
void processDeferredEncryption();
|
void processDeferredEncryption();
|
||||||
|
|
||||||
// Private methods - connection
|
// Private methods - connection
|
||||||
|
|||||||
@@ -1023,7 +1023,7 @@ void App::updateWalletIndexForActiveWallet(bool markOpened)
|
|||||||
if (wallet_index_.upsert(e)) wallet_index_.save();
|
if (wallet_index_.upsert(e)) wallet_index_.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::switchToWallet(const std::string& walletFile)
|
void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed)
|
||||||
{
|
{
|
||||||
if (!supportsFullNodeLifecycleActions()) {
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
|
||||||
@@ -1056,6 +1056,15 @@ void App::switchToWallet(const std::string& walletFile)
|
|||||||
ui::Notifications::instance().warning("Finish or cancel the pending send before switching wallets.");
|
ui::Notifications::instance().warning("Finish or cancel the pending send before switching wallets.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The node we'd restart may be one this session ADOPTED — a dragonxd already running when the wallet
|
||||||
|
// launched (left running by "keep node running", or started by the user). Switching must stop+restart
|
||||||
|
// it on the new wallet, so confirm first rather than killing a node the user may have kept up on
|
||||||
|
// purpose. (An owned node we started this session restarts silently, as before.)
|
||||||
|
if (!stopDaemonConfirmed && daemon_controller_ && daemon_controller_->externalDaemonDetected()) {
|
||||||
|
pending_switch_wallet_file_ = walletFile;
|
||||||
|
show_switch_stop_daemon_confirm_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Rescan only if this wallet was never synced in this datadir (freshly imported/new). One we've
|
// Rescan only if this wallet was never synced in this datadir (freshly imported/new). One we've
|
||||||
// loaded here before just catches up from its recorded block on start — fast.
|
// loaded here before just catches up from its recorded block on start — fast.
|
||||||
|
|||||||
@@ -376,6 +376,9 @@ void App::buildSweepCatalog()
|
|||||||
add("modal-antivirus", ui::NavPage::Mining,
|
add("modal-antivirus", ui::NavPage::Mining,
|
||||||
[](App& a) { a.pending_antivirus_dialog_ = true; },
|
[](App& a) { a.pending_antivirus_dialog_ = true; },
|
||||||
[](App& a) { a.pending_antivirus_dialog_ = false; });
|
[](App& a) { a.pending_antivirus_dialog_ = false; });
|
||||||
|
add("modal-switch-stopnode", ui::NavPage::Settings,
|
||||||
|
[](App& a) { a.pending_switch_wallet_file_ = "wallet-savings.dat"; a.show_switch_stop_daemon_confirm_ = true; },
|
||||||
|
[](App& a) { a.show_switch_stop_daemon_confirm_ = false; a.pending_switch_wallet_file_.clear(); });
|
||||||
// Wave-2 fund/secret dialogs (setup never fires the async RPC — no button is clicked).
|
// Wave-2 fund/secret dialogs (setup never fires the async RPC — no button is clicked).
|
||||||
add("modal-shield", ui::NavPage::Send,
|
add("modal-shield", ui::NavPage::Send,
|
||||||
[](App&) { ui::ShieldDialog::showShieldCoinbase(); },
|
[](App&) { ui::ShieldDialog::showShieldCoinbase(); },
|
||||||
|
|||||||
@@ -1039,7 +1039,13 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["confirm_transaction"] = "Confirm Transaction";
|
strings_["confirm_transaction"] = "Confirm Transaction";
|
||||||
strings_["confirm_and_send"] = "Confirm & Send";
|
strings_["confirm_and_send"] = "Confirm & Send";
|
||||||
strings_["cancel"] = "Cancel";
|
strings_["cancel"] = "Cancel";
|
||||||
|
|
||||||
|
// Wallet-switch "stop the running node?" confirmation
|
||||||
|
strings_["switch_stopnode_title"] = "Stop the running node?";
|
||||||
|
strings_["switch_stopnode_warn"] = "A node is already running that this wallet didn't start.";
|
||||||
|
strings_["switch_stopnode_body"] = "Switching wallets restarts the node on the wallet you selected. The running node will be stopped and relaunched on the new wallet — if you kept it running on purpose, it comes back up automatically.";
|
||||||
|
strings_["switch_stopnode_confirm"] = "Stop node & switch";
|
||||||
|
|
||||||
// Receive Tab
|
// Receive Tab
|
||||||
strings_["receiving_addresses"] = "Your Receiving Addresses";
|
strings_["receiving_addresses"] = "Your Receiving Addresses";
|
||||||
strings_["new_z_shielded"] = "New z-Address (Shielded)";
|
strings_["new_z_shielded"] = "New z-Address (Shielded)";
|
||||||
|
|||||||
Reference in New Issue
Block a user