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:
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|||||||
207
src/ui/windows/wallets_dialog.h
Normal file
207
src/ui/windows/wallets_dialog.h
Normal 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
|
||||||
@@ -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).
|
||||||
|
|||||||
Reference in New Issue
Block a user