feat(wallet): wallet-files list + switching (P2)

Second phase of multi-wallet: list wallet files and switch the active one.

- Daemon plumbing: EmbeddedDaemon::setWalletFile -> start() passes -wallet=<name>
  (non-default only; skipped during the isolated seed-migration start), and
  DaemonController::syncSettings pushes active_wallet_file on each start.
- App::switchToWallet: persist the new active wallet, then stop + restart the
  node on -wallet=<name> (rescan only if it was never synced in this datadir).
  Reuses the seed-adopt restart coordination; WalletState clears on disconnect
  and the P1 identity-scoped caches re-key, so no previous-wallet data leaks.
  Guarded: full-node, embedded daemon, not mid-restart, no pending send.
- Wallets dialog (Settings -> Backup & Data -> "Wallets…"): a table of wallet
  files (datadir wallet*.dat + user-added folders' *.dat) with size (disk) and
  cached balance / address count / last-opened (wallet index), a current/Active
  badge, Open (switch), and Add folder. Out-of-datadir wallets show "import to
  open" (P3). Added as a sweep surface.
- Mining payout safety: non-destructive warning if the pool-mode worker looks
  like a DragonX address not in the current wallet (stale after a switch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 16:20:41 -05:00
parent 72bf59149d
commit e8a8ce68b2
11 changed files with 353 additions and 0 deletions

View File

@@ -54,6 +54,7 @@
#include "ui/windows/bootstrap_download_dialog.h" #include "ui/windows/bootstrap_download_dialog.h"
#include "ui/windows/xmrig_download_dialog.h" #include "ui/windows/xmrig_download_dialog.h"
#include "ui/windows/daemon_download_dialog.h" #include "ui/windows/daemon_download_dialog.h"
#include "ui/windows/wallets_dialog.h"
#include "ui/windows/console_tab.h" #include "ui/windows/console_tab.h"
#include "ui/pages/settings_page.h" #include "ui/pages/settings_page.h"
#include "ui/theme.h" #include "ui/theme.h"
@@ -1990,6 +1991,7 @@ void App::render()
ui::BootstrapDownloadDialog::render(); ui::BootstrapDownloadDialog::render();
ui::XmrigDownloadDialog::render(); ui::XmrigDownloadDialog::render();
ui::DaemonUpdateDialog::render(); ui::DaemonUpdateDialog::render();
ui::WalletsDialog::render();
// Windows Defender antivirus help dialog // Windows Defender antivirus help dialog
renderAntivirusHelpDialog(); renderAntivirusHelpDialog();

View File

@@ -202,6 +202,9 @@ public:
// per-wallet data (e.g. address-book contacts). Empty until addresses are known (pre-connect). // per-wallet data (e.g. address-book contacts). Empty until addresses are known (pre-connect).
std::string activeWalletIdentityHash() const; std::string activeWalletIdentityHash() const;
data::WalletIndex& walletIndex() { return wallet_index_; }
const data::WalletIndex& walletIndex() const { return wallet_index_; }
// Connection state (convenience wrappers) // Connection state (convenience wrappers)
bool isConnected() const { return state_.connected; } bool isConnected() const { return state_.connected; }
int getBlockHeight() const { return state_.sync.blocks; } int getBlockHeight() const { return state_.sync.blocks; }
@@ -447,6 +450,10 @@ public:
* Shows "Restarting daemon..." in the loading overlay while the daemon cycles. * Shows "Restarting daemon..." in the loading overlay while the daemon cycles.
*/ */
void restartDaemon(); void restartDaemon();
// 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
// via the identity-scoped caches (P1). No-op if that wallet is already active.
void switchToWallet(const std::string& walletFile);
// Wallet encryption helpers // Wallet encryption helpers
void encryptWalletWithPassphrase(const std::string& passphrase); void encryptWalletWithPassphrase(const std::string& passphrase);

View File

@@ -1007,6 +1007,58 @@ 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)
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
return;
}
if (walletFile.empty() || !settings_) return;
if (settings_->getActiveWalletFile() == walletFile) {
ui::Notifications::instance().info("That wallet is already open.");
return;
}
if (daemon_restarting_) {
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
return;
}
if (!isUsingEmbeddedDaemon()) {
ui::Notifications::instance().warning("Switching wallets needs the embedded daemon — stop any external dragonxd first.");
return;
}
if (hasTransactionSendProgress()) {
ui::Notifications::instance().warning("Finish or cancel the pending send before switching wallets.");
return;
}
// 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.
bool needRescan = true;
if (const auto* e = wallet_index_.find(walletFile)) needRescan = !e->syncedHere;
settings_->setActiveWalletFile(walletFile);
settings_->save();
// Same restart coordination as the seed-adopt flow: gate the main-loop reconnect, disconnect,
// then stop + restart on a background thread. syncSettings() re-reads active_wallet_file on
// start, so the daemon comes up on -wallet=<name>. WalletState clears on disconnect and the
// per-wallet caches re-key off the new identity (P1), so no previous-wallet data leaks through.
daemon_restarting_ = true;
if (rpc_ && rpc_->isConnected()) rpc_->disconnect();
onDisconnected("Switching wallet");
ui::Notifications::instance().info("Switching wallet — the node will restart…");
async_tasks_.submit("Switch wallet", [this, needRescan](const util::AsyncTaskManager::Token&) {
// Stop the node fully so it releases the datadir .lock + RPC port before we relaunch.
stopEmbeddedDaemon();
for (int i = 0; i < 60 && isEmbeddedDaemonRunning(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(500));
if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true);
if (!shutting_down_) startEmbeddedDaemon();
daemon_restarting_ = false; // re-arm reconnect once the new daemon has been launched
});
}
void App::wipePendingTransactionHistoryCachePassphrase() void App::wipePendingTransactionHistoryCachePassphrase()
{ {
if (!pending_transaction_history_cache_passphrase_.empty()) { if (!pending_transaction_history_cache_passphrase_.empty()) {

View File

@@ -17,6 +17,7 @@
#include "ui/notifications.h" #include "ui/notifications.h"
#include "ui/sidebar.h" #include "ui/sidebar.h"
#include "ui/windows/send_tab.h" #include "ui/windows/send_tab.h"
#include "ui/windows/wallets_dialog.h"
#include "util/platform.h" #include "util/platform.h"
#include "wallet/wallet_capabilities.h" #include "wallet/wallet_capabilities.h"
@@ -261,6 +262,30 @@ void App::buildSweepCatalog()
mig("modal-migrate-done", SeedMigrationStep::Done, nullptr); mig("modal-migrate-done", SeedMigrationStep::Done, nullptr);
mig("modal-migrate-error", SeedMigrationStep::Error, mig("modal-migrate-error", SeedMigrationStep::Error,
[](App& a) { a.seed_migration_status_ = "Example error: the daemon did not respond."; }); [](App& a) { a.seed_migration_status_ = "Example error: the daemon did not respond."; });
// Multi-wallet: the wallet-files list. Drop a couple of demo wallet files in the (throwaway)
// datadir + cache their metadata so the list renders populated.
add("modal-wallets", ui::NavPage::Settings,
[](App& a) {
std::error_code ec;
const std::string dd = util::Platform::getDragonXDataDir();
fs::create_directories(dd, ec);
if (!fs::exists(dd + "/wallet.dat", ec))
std::ofstream(dd + "/wallet.dat", std::ios::binary) << std::string(122880, '\0');
if (!fs::exists(dd + "/wallet-savings.dat", ec))
std::ofstream(dd + "/wallet-savings.dat", std::ios::binary) << std::string(65536, '\0');
data::WalletIndexEntry e1; e1.fileName = "wallet.dat"; e1.displayName = "wallet.dat";
e1.cachedBalance = 15.7526; e1.cachedAddressCount = 4;
e1.lastOpenedEpoch = 1720000000; e1.syncedHere = true;
data::WalletIndexEntry e2; e2.fileName = "wallet-savings.dat"; e2.displayName = "wallet-savings.dat";
e2.cachedBalance = 250.0; e2.cachedAddressCount = 2;
e2.lastOpenedEpoch = 1719400000; e2.syncedHere = true;
a.walletIndex().upsert(e1);
a.walletIndex().upsert(e2);
if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
ui::WalletsDialog::show(&a);
},
[](App&) { ui::WalletsDialog::hide(); });
} }
// First-run wizard — one per meaningful phase (full-node only; blocks all other UI while shown). // First-run wizard — one per meaningful phase (full-node only; blocks all other UI while shown).

View File

@@ -23,6 +23,7 @@ void DaemonController::syncSettings(const config::Settings* settings)
if (!settings) return; if (!settings) return;
daemon_->setDebugCategories(settings->getDebugCategories()); daemon_->setDebugCategories(settings->getDebugCategories());
daemon_->setMaxConnections(settings->getMaxConnections()); daemon_->setMaxConnections(settings->getMaxConnections());
daemon_->setWalletFile(settings->getActiveWalletFile());
} }
bool DaemonController::start(const config::Settings* settings) bool DaemonController::start(const config::Settings* settings)

View File

@@ -488,6 +488,14 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
args.push_back("-maxconnections=" + std::to_string(max_connections_)); args.push_back("-maxconnections=" + std::to_string(max_connections_));
} }
// Active wallet file (multi-wallet). The daemon loads <datadir>/<name>. Only pass it for a
// non-default name so the common case's command line is unchanged; skip during an isolated
// start (seed migration manages its own throwaway wallet).
if (!wallet_file_.empty() && wallet_file_ != "wallet.dat" && override_datadir_.empty()) {
DEBUG_LOGF("[INFO] Loading wallet file: %s\n", wallet_file_.c_str());
args.push_back("-wallet=" + wallet_file_);
}
// Add wallet-repair flag if requested (one-shot). -zapwallettxes=2 wipes all wallet tx/note // Add wallet-repair flag if requested (one-shot). -zapwallettxes=2 wipes all wallet tx/note
// records and rebuilds them from the chain; it implies -rescan, so don't also pass -rescan. // records and rebuilds them from the chain; it implies -rescan, so don't also pass -rescan.
if (zap_on_next_start_.exchange(false)) { if (zap_on_next_start_.exchange(false)) {

View File

@@ -183,6 +183,12 @@ public:
void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; } void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; }
bool rescanOnNextStart() const { return rescan_on_next_start_.load(); } bool rescanOnNextStart() const { return rescan_on_next_start_.load(); }
// Active wallet file (multi-wallet). Passed to the daemon as -wallet=<name> so it loads
// <datadir>/<name>; empty or "wallet.dat" keeps the daemon default (no arg). Must be a plain
// filename in the datadir — the daemon rejects paths.
void setWalletFile(const std::string& v) { wallet_file_ = v; }
std::string walletFile() const { return wallet_file_; }
/** /**
* @brief Request a wallet repair (-zapwallettxes=2) on the next daemon start. This deletes all * @brief Request a wallet repair (-zapwallettxes=2) on the next daemon start. This deletes all
* wallet transaction/note records and rebuilds them from the chain (keys are kept); the * wallet transaction/note records and rebuilds them from the chain (keys are kept); the
@@ -251,6 +257,7 @@ private:
std::atomic<bool> should_stop_{false}; std::atomic<bool> should_stop_{false};
std::set<std::string> debug_categories_; std::set<std::string> debug_categories_;
int max_connections_ = 0; // 0 = daemon default int max_connections_ = 0; // 0 = daemon default
std::string wallet_file_; // -wallet=<name> for the active wallet; empty/"wallet.dat" = default
std::atomic<int> crash_count_{0}; // consecutive crash counter std::atomic<int> crash_count_{0}; // consecutive crash counter
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
std::atomic<bool> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start std::atomic<bool> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start

View File

@@ -40,6 +40,7 @@
#include "../windows/export_transactions_dialog.h" #include "../windows/export_transactions_dialog.h"
#include "../windows/bootstrap_download_dialog.h" #include "../windows/bootstrap_download_dialog.h"
#include "../windows/daemon_download_dialog.h" #include "../windows/daemon_download_dialog.h"
#include "../windows/wallets_dialog.h"
#include "../../embedded/IconsMaterialDesign.h" #include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h" #include "imgui.h"
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
@@ -1340,6 +1341,11 @@ void RenderSettingsPage(App* app) {
if (TactileButton(TR("seed_migrate_button"), ImVec2(0, 0), btnFont)) if (TactileButton(TR("seed_migrate_button"), ImVec2(0, 0), btnFont))
app->showSeedMigrationDialog(); app->showSeedMigrationDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_migrate")); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_migrate"));
// Multi-wallet: list wallet files + switch the active one.
ImGui::SameLine(0, scaledSp);
if (TactileButton(TR("wallets_button"), ImVec2(0, 0), btnFont))
ui::WalletsDialog::show(app);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_wallets_button"));
} }
ImGui::SameLine(0, scaledSp); ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[4], ImVec2(0, 0), btnFont)) if (TactileButton(r1[4], ImVec2(0, 0), btnFont))

View File

@@ -277,6 +277,23 @@ static void RenderMiningTabContent(App* app)
RenderMiningModeToggle(app, state, mining, dl, capFont, ovFont, dp, hs, gap, availWidth, RenderMiningModeToggle(app, state, mining, dl, capFont, ovFont, dp, hs, gap, availWidth,
s_pool_mode, s_pool_url, s_pool_worker, s_pool_settings_dirty); s_pool_mode, s_pool_url, s_pool_worker, s_pool_settings_dirty);
// Payout safety (multi-wallet): in pool mode, warn if the worker looks like a DragonX address
// that isn't in the CURRENT wallet — e.g. left over from a wallet you switched away from, so
// rewards would land in the wrong wallet. Non-destructive: we only warn, never overwrite (it
// could be a deliberate external payout).
if (s_pool_mode) {
std::string w(s_pool_worker);
while (!w.empty() && (w.front()==' '||w.front()=='\t'||w.front()=='\n'||w.front()=='\r')) w.erase(w.begin());
while (!w.empty() && (w.back()==' '||w.back()=='\t'||w.back()=='\n'||w.back()=='\r')) w.pop_back();
const bool looksLikeAddr = w.size() > 10 && (w.rfind("zs", 0) == 0 || w[0] == 'R');
bool inWallet = false;
for (const auto& a : state.addresses) if (a.address == w) { inWallet = true; break; }
if (looksLikeAddr && !inWallet && !state.addresses.empty()) {
ImGui::Dummy(ImVec2(0, gap));
Type().textColored(TypeStyle::Caption, Warning(), TR("mining_payout_foreign"));
}
}
// ================================================================ // ================================================================
// CONTROLS — Glass card with CPU core grid (no heading) // CONTROLS — Glass card with CPU core grid (no heading)
// ================================================================ // ================================================================

View File

@@ -0,0 +1,207 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Wallet-files list + switcher (multi-wallet P2). Lists wallet files in the datadir (and any
// user-added folders) with cached metadata (size from disk; balance / address count / last-opened
// from the wallet index, since those can't be read off a wallet.dat without loading it). "Open"
// switches the active wallet via App::switchToWallet (daemon restart on -wallet=<name>).
#pragma once
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
#include "imgui.h"
#include "../../app.h"
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../../util/text_format.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../notifications.h"
#include "../layout.h"
#include "../material/colors.h"
#include "../material/draw_helpers.h"
#include "../material/type.h"
namespace dragonx {
namespace ui {
class WalletsDialog {
public:
static void show(App* app) {
s_open = true;
s_app = app;
s_needScan = true;
s_newFolder[0] = '\0';
}
static void hide() { s_open = false; }
static void render() {
if (!s_open || !s_app) return;
using namespace material;
App* app = s_app;
const float dp = Layout::dpiScale();
if (s_needScan) { scan(); s_needScan = false; }
if (BeginOverlayDialog(TR("wallets_title"), &s_open, 780.0f, 0.94f)) {
Type().text(TypeStyle::H6, TR("wallets_title"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_intro"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
const std::string active = app->settings() ? app->settings()->getActiveWalletFile() : "wallet.dat";
// Leave room below the table for the add-folder row + footer buttons.
float tableH = ImGui::GetContentRegionAvail().y - 130.0f * dp;
if (tableH < 120.0f * dp) tableH = 120.0f * dp;
const ImGuiTableFlags tflags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg |
ImGuiTableFlags_BordersInnerH;
if (ImGui::BeginTable("##wallets", 6, tflags, ImVec2(0, tableH))) {
ImGui::TableSetupColumn(TR("wallets_col_name"), ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn(TR("wallets_col_size"), ImGuiTableColumnFlags_WidthFixed, 90.0f * dp);
ImGui::TableSetupColumn(TR("wallets_col_addresses"), ImGuiTableColumnFlags_WidthFixed, 80.0f * dp);
ImGui::TableSetupColumn(TR("wallets_col_balance"), ImGuiTableColumnFlags_WidthFixed, 130.0f * dp);
ImGui::TableSetupColumn(TR("wallets_col_opened"), ImGuiTableColumnFlags_WidthFixed, 120.0f * dp);
ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthFixed, 110.0f * dp);
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableHeadersRow();
for (std::size_t i = 0; i < s_rows.size(); ++i) {
const WalletRow& r = s_rows[i];
const data::WalletIndexEntry* meta = app->walletIndex().find(r.fileName);
const bool isCurrent = r.inDatadir && r.fileName == active;
ImGui::TableNextRow();
ImGui::PushID(static_cast<int>(i));
// Name — + "current" badge, or a folder marker for out-of-datadir files.
ImGui::TableNextColumn();
ImGui::TextUnformatted(r.fileName.c_str());
if (isCurrent) {
ImGui::SameLine(0, 8.0f * dp);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Success()), "%s", TR("wallets_current"));
} else if (!r.inDatadir) {
ImGui::SameLine(0, 8.0f * dp);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", ICON_MD_FOLDER);
if (ImGui::IsItemHovered()) Tooltip("%s", r.dir.c_str());
}
ImGui::TableNextColumn();
ImGui::TextUnformatted(util::Platform::formatFileSize(static_cast<uint64_t>(r.sizeBytes)).c_str());
ImGui::TableNextColumn();
if (meta && meta->cachedAddressCount >= 0) ImGui::Text("%lld", meta->cachedAddressCount);
else ImGui::TextDisabled("");
ImGui::TableNextColumn();
if (meta && meta->cachedBalance >= 0.0) ImGui::Text("%.4f", meta->cachedBalance);
else ImGui::TextDisabled("");
ImGui::TableNextColumn();
if (meta && meta->lastOpenedEpoch > 0)
ImGui::TextUnformatted(util::formatTimeAgo(meta->lastOpenedEpoch).c_str());
else ImGui::TextDisabled("%s", TR("wallets_never"));
// Action
ImGui::TableNextColumn();
if (isCurrent) {
ImGui::TextDisabled("%s", TR("wallets_active"));
} else if (r.inDatadir) {
if (StyledButton(TR("wallets_open"), ImVec2(90.0f * dp, 0))) {
app->switchToWallet(r.fileName);
s_open = false; // node restarts; the main UI shows the reconnect overlay
}
} else {
// Out-of-datadir wallets can't be loaded directly (the daemon rejects paths);
// opening one means importing (copy into the datadir) — a P3 action.
ImGui::TextDisabled("%s", TR("wallets_import_hint"));
if (ImGui::IsItemHovered()) Tooltip("%s", TR("wallets_import_tt"));
}
ImGui::PopID();
}
ImGui::EndTable();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Add a folder to also scan for wallet files (no native picker — a validated path input).
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_add_folder"));
ImGui::SetNextItemWidth(-150.0f * dp);
ImGui::InputTextWithHint("##walletFolder", TR("wallets_folder_hint"), s_newFolder, sizeof(s_newFolder));
ImGui::SameLine();
if (StyledButton(TR("wallets_add"), ImVec2(120.0f * dp, 0))) {
std::error_code ec;
std::string dir = s_newFolder;
if (!dir.empty() && std::filesystem::is_directory(dir, ec)) {
if (app->walletIndex().addExtraFolder(dir)) app->walletIndex().save();
s_newFolder[0] = '\0';
s_needScan = true;
} else {
Notifications::instance().warning(TR("wallets_folder_invalid"));
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Separator();
if (StyledButton(TR("refresh"), ImVec2(110.0f * dp, 0))) s_needScan = true;
ImGui::SameLine();
if (StyledButton(TR("close"), ImVec2(110.0f * dp, 0))) s_open = false;
EndOverlayDialog();
}
}
private:
struct WalletRow {
std::string fileName;
std::string dir;
bool inDatadir = false;
long long sizeBytes = 0;
};
// Scan the datadir (wallet-prefixed *.dat only, to skip peers.dat / asmap.dat / fee_estimates.dat)
// and each user-added folder (any *.dat). Exception-safe: iterates with error_codes.
static void scan() {
s_rows.clear();
namespace fs = std::filesystem;
auto addFrom = [](const std::string& dir, bool inDatadir) {
std::error_code ec;
if (dir.empty() || !fs::is_directory(dir, ec)) return;
fs::directory_iterator it(dir, ec), end;
for (; !ec && it != end; it.increment(ec)) {
std::error_code fec;
if (!it->is_regular_file(fec)) continue;
std::string name = it->path().filename().string();
if (name.size() <= 4 || name.substr(name.size() - 4) != ".dat") continue;
if (inDatadir && name.rfind("wallet", 0) != 0) continue;
WalletRow r;
r.fileName = name;
r.dir = dir;
r.inDatadir = inDatadir;
r.sizeBytes = static_cast<long long>(fs::file_size(it->path(), fec));
s_rows.push_back(std::move(r));
}
};
addFrom(util::Platform::getDragonXDataDir(), /*inDatadir=*/true);
if (s_app)
for (const auto& f : s_app->walletIndex().extraFolders())
addFrom(f, /*inDatadir=*/false);
}
static inline bool s_open = false;
static inline App* s_app = nullptr;
static inline bool s_needScan = false;
static inline char s_newFolder[512] = "";
static inline std::vector<WalletRow> s_rows;
};
} // namespace ui
} // namespace dragonx

View File

@@ -250,6 +250,26 @@ void I18n::loadBuiltinEnglish()
strings_["seed_backup_close"] = "Close"; strings_["seed_backup_close"] = "Close";
strings_["seed_backup_reminder"] = "Your wallet has a 24-word recovery seed phrase. Back it up now in Settings → Node & Security."; strings_["seed_backup_reminder"] = "Your wallet has a 24-word recovery seed phrase. Back it up now in Settings → Node & Security.";
strings_["seed_migrate_button"] = "Migrate to seed…"; strings_["seed_migrate_button"] = "Migrate to seed…";
// Multi-wallet: wallet-files list + switcher
strings_["wallets_button"] = "Wallets…";
strings_["tt_wallets_button"] = "List your wallet files and switch between them";
strings_["wallets_title"] = "Wallets";
strings_["wallets_intro"] = "Wallet files in your data directory (and any folders you add). Open one to switch — the node restarts to load it, and each wallet keeps its own data.";
strings_["wallets_col_name"] = "Wallet";
strings_["wallets_col_size"] = "Size";
strings_["wallets_col_addresses"] = "Addresses";
strings_["wallets_col_balance"] = "Balance";
strings_["wallets_col_opened"] = "Last opened";
strings_["wallets_current"] = "current";
strings_["wallets_active"] = "Active";
strings_["wallets_open"] = "Open";
strings_["wallets_never"] = "Never";
strings_["wallets_import_hint"] = "Import to open";
strings_["wallets_import_tt"] = "This wallet is outside the data directory. Opening it will copy it in first (coming soon).";
strings_["wallets_add_folder"] = "Also scan another folder for wallet files:";
strings_["wallets_folder_hint"] = "/path/to/folder with wallet .dat files";
strings_["wallets_add"] = "Add folder";
strings_["wallets_folder_invalid"] = "That folder doesn't exist.";
strings_["tt_seed_migrate"] = "Create a new seed-phrase wallet and move your funds into it"; strings_["tt_seed_migrate"] = "Create a new seed-phrase wallet and move your funds into it";
// Migrate-to-seed modal // Migrate-to-seed modal
strings_["mig_title"] = "Migrate to a seed wallet"; strings_["mig_title"] = "Migrate to a seed wallet";
@@ -1331,6 +1351,7 @@ void I18n::loadBuiltinEnglish()
strings_["mining_payout_tooltip"] = "Address to receive mining rewards"; strings_["mining_payout_tooltip"] = "Address to receive mining rewards";
strings_["mining_generate_z_address_hint"] = "Generate a Z address in the Receive tab to use as your payout address"; strings_["mining_generate_z_address_hint"] = "Generate a Z address in the Receive tab to use as your payout address";
strings_["mining_pool"] = "Pool"; strings_["mining_pool"] = "Pool";
strings_["mining_payout_foreign"] = "⚠ This payout address isn't in your current wallet — mined rewards would go to a different wallet. Update it if you switched wallets.";
strings_["mining_pool_hashrate"] = "Pool Hashrate"; strings_["mining_pool_hashrate"] = "Pool Hashrate";
strings_["mining_pool_url"] = "Pool URL"; strings_["mining_pool_url"] = "Pool URL";
// Pool selection mode + auto-balance (mining tab). // Pool selection mode + auto-balance (mining tab).