feat(wallet): new/import wallets + multi-wallet i18n (P3)
Completes the wallet-files dialog and localizes the feature. - Create a new wallet: a name is normalized to wallet-<name>.dat and opened via switchToWallet; the daemon mints it (seed-backed, -usemnemonic=1) on first load. Rejects blank names + name collisions. - Import: out-of-datadir wallets get an Import action that copies the file into the datadir (never overwriting) under a wallet-*.dat name, then opens it — the daemon only loads plain filenames from the datadir. - i18n: the 32 new multi-wallet / per-wallet-contacts / mining-payout strings translated into all 8 languages (de/es/fr/ja/ko/pt/ru/zh), CJK subset font rebuilt. Verified: the Chinese Wallets dialog renders with no missing glyphs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -282,6 +282,12 @@ void App::buildSweepCatalog()
|
||||
e2.lastOpenedEpoch = 1719400000; e2.syncedHere = true;
|
||||
a.walletIndex().upsert(e1);
|
||||
a.walletIndex().upsert(e2);
|
||||
// An out-of-datadir wallet (in an extra folder) so the Import action shows too.
|
||||
const std::string extra = util::Platform::getConfigDir() + "extra-wallets";
|
||||
fs::create_directories(extra, ec);
|
||||
if (!fs::exists(extra + "/my-wallet.dat", ec))
|
||||
std::ofstream(extra + "/my-wallet.dat", std::ios::binary) << std::string(80000, '\0');
|
||||
a.walletIndex().addExtraFolder(extra);
|
||||
if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
|
||||
ui::WalletsDialog::show(&a);
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
@@ -120,9 +121,10 @@ public:
|
||||
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"));
|
||||
// Out-of-datadir wallets can't be loaded directly (the daemon rejects paths).
|
||||
// Import = copy into the datadir under a wallet-*.dat name, then switch to it.
|
||||
if (StyledButton(TR("wallets_import"), ImVec2(90.0f * dp, 0)))
|
||||
importAndOpen(app, r);
|
||||
if (ImGui::IsItemHovered()) Tooltip("%s", TR("wallets_import_tt"));
|
||||
}
|
||||
ImGui::PopID();
|
||||
@@ -132,6 +134,28 @@ public:
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Create a fresh named wallet — the daemon mints it (with a seed phrase) on first load.
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_new_label"));
|
||||
ImGui::SetNextItemWidth(-150.0f * dp);
|
||||
ImGui::InputTextWithHint("##newWalletName", TR("wallets_new_hint"), s_newName, sizeof(s_newName));
|
||||
ImGui::SameLine();
|
||||
if (StyledButton(TR("wallets_create"), ImVec2(120.0f * dp, 0))) {
|
||||
std::string name = normalizeWalletName(s_newName);
|
||||
std::error_code ec;
|
||||
if (name.empty()) {
|
||||
Notifications::instance().warning(TR("wallets_name_invalid"));
|
||||
} else if (std::filesystem::exists(util::Platform::getDragonXDataDir() + "/" + name, ec)) {
|
||||
Notifications::instance().warning(TR("wallets_exists"));
|
||||
} else {
|
||||
s_newName[0] = '\0';
|
||||
Notifications::instance().info(TR("wallets_creating"));
|
||||
app->switchToWallet(name); // daemon creates it on start (-usemnemonic=1 -> seed-backed)
|
||||
s_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -167,6 +191,42 @@ private:
|
||||
long long sizeBytes = 0;
|
||||
};
|
||||
|
||||
// Force a "wallet-...dat" filename (plain, no path) so a new/imported wallet shows in the
|
||||
// datadir scan and can't collide with the daemon's internal .dat files. Returns "" if the
|
||||
// input has no usable characters.
|
||||
static std::string normalizeWalletName(const std::string& raw) {
|
||||
std::string s = raw;
|
||||
while (!s.empty() && static_cast<unsigned char>(s.front()) <= ' ') s.erase(s.begin());
|
||||
while (!s.empty() && static_cast<unsigned char>(s.back()) <= ' ') s.pop_back();
|
||||
auto slash = s.find_last_of("/\\");
|
||||
if (slash != std::string::npos) s = s.substr(slash + 1);
|
||||
if (s.size() > 4 && s.substr(s.size() - 4) == ".dat") s = s.substr(0, s.size() - 4);
|
||||
std::string clean;
|
||||
for (char c : s)
|
||||
if (std::isalnum(static_cast<unsigned char>(c)) || c == '-' || c == '_') clean += c;
|
||||
if (clean.empty()) return "";
|
||||
if (clean.rfind("wallet", 0) != 0) clean = "wallet-" + clean;
|
||||
return clean + ".dat";
|
||||
}
|
||||
|
||||
// Copy an out-of-datadir wallet file into the datadir (the daemon only loads plain filenames
|
||||
// from there) under a wallet-*.dat name, then switch to it. Never overwrites an existing file.
|
||||
static void importAndOpen(App* app, const WalletRow& r) {
|
||||
namespace fs = std::filesystem;
|
||||
std::string dest = normalizeWalletName(r.fileName);
|
||||
if (dest.empty()) { Notifications::instance().warning(TR("wallets_name_invalid")); return; }
|
||||
std::error_code ec;
|
||||
const std::string datadir = util::Platform::getDragonXDataDir();
|
||||
fs::create_directories(datadir, ec);
|
||||
const std::string destPath = datadir + "/" + dest;
|
||||
if (fs::exists(destPath, ec)) { Notifications::instance().warning(TR("wallets_exists")); return; }
|
||||
fs::copy_file(r.dir + "/" + r.fileName, destPath, ec);
|
||||
if (ec) { Notifications::instance().error(TR("wallets_import_failed")); return; }
|
||||
Notifications::instance().success(TR("wallets_imported"));
|
||||
app->switchToWallet(dest);
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
@@ -200,6 +260,7 @@ private:
|
||||
static inline App* s_app = nullptr;
|
||||
static inline bool s_needScan = false;
|
||||
static inline char s_newFolder[512] = "";
|
||||
static inline char s_newName[128] = "";
|
||||
static inline std::vector<WalletRow> s_rows;
|
||||
};
|
||||
|
||||
|
||||
@@ -270,6 +270,15 @@ void I18n::loadBuiltinEnglish()
|
||||
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_["wallets_import"] = "Import";
|
||||
strings_["wallets_new_label"] = "Create a new wallet:";
|
||||
strings_["wallets_new_hint"] = "Name (e.g. savings)";
|
||||
strings_["wallets_create"] = "Create wallet";
|
||||
strings_["wallets_name_invalid"] = "Please enter a valid wallet name.";
|
||||
strings_["wallets_exists"] = "A wallet with that name already exists.";
|
||||
strings_["wallets_imported"] = "Wallet imported — switching…";
|
||||
strings_["wallets_import_failed"] = "Could not import that wallet file.";
|
||||
strings_["wallets_creating"] = "Creating wallet — the node will restart…";
|
||||
strings_["tt_seed_migrate"] = "Create a new seed-phrase wallet and move your funds into it";
|
||||
// Migrate-to-seed modal
|
||||
strings_["mig_title"] = "Migrate to a seed wallet";
|
||||
|
||||
Reference in New Issue
Block a user