feat(wallet): migrate-to-seed, Phase 1 — isolated seed-wallet creation

First, fund-safe step of "migrate a legacy wallet to a seed wallet": mint a
brand-new BIP39 mnemonic wallet in an ISOLATED throwaway datadir so the user
can later sweep funds into it. No funds move and the real wallet.dat / main
daemon are never touched — the isolated node runs concurrently on its own port.

- EmbeddedDaemon: one-shot isolated-datadir override (setNextStartOverride)
  consumed on the next start(); a skip-default-port-check for an isolated
  instance beside the main daemon; and a tcpPortInUse() probe. The datadir's
  basename must be the assetchain name (DRAGONX) and the daemon reads
  <datadir>/DRAGONX.conf itself, so no -conf is passed (both learned from
  live testing against the hd-transparent-keys daemon).
- SeedWalletCreator (src/daemon): picks a free port, writes a throwaway conf,
  starts an isolated dragonxd with -usemnemonic=1 -connect=0, waits for RPC,
  calls z_exportmnemonic + z_getnewaddress (the sweep target), stops it, and
  keeps/scrubs the temp datadir. Off-thread; the seed is wiped after use.
- App: beginCreateSeedWallet (background) -> pumpSeedMigration (main-thread
  handoff) -> renderSeedMigrationDialog (Intro -> Working -> Show seed ->
  Error), reusing the seed_display grid + clipboard auto-clear.
- Settings: "Migrate to seed…" button (full-node gated) + persisted pending
  migration state (dest address + temp datadir) for Phase 2 to adopt.

Validated end-to-end against the hd-transparent-keys daemon: the isolated
node comes up in ~4s and returns a 24-word mnemonic + a shielded z-address.
Requires that daemon (the bundled Jun-28 build lacks z_exportmnemonic). The
migration modal uses English literals for now (as renderBackupDialog does);
the whole flow gets translated once Phase 2 finalizes it. Phase 2 (sweep +
adopt) is next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 12:53:36 -05:00
parent 403e145b30
commit 718cb82c13
12 changed files with 466 additions and 5 deletions

View File

@@ -503,6 +503,7 @@ set(APP_SOURCES
src/util/texture_loader.cpp
src/util/noise_texture.cpp
src/daemon/embedded_daemon.cpp
src/daemon/seed_wallet_creator.cpp
src/daemon/daemon_controller.cpp
src/daemon/lifecycle_adapters.cpp
src/daemon/xmrig_manager.cpp
@@ -634,6 +635,7 @@ set(APP_HEADERS
src/util/payment_uri.h
src/util/secure_vault.h
src/daemon/embedded_daemon.h
src/daemon/seed_wallet_creator.h
src/daemon/daemon_controller.h
src/daemon/lifecycle_adapters.h
src/daemon/xmrig_manager.h

View File

@@ -719,6 +719,9 @@ void App::update()
// One-time reminder to back up the wallet's seed phrase (mnemonic wallets only).
maybeRemindSeedBackup();
// Pick up progress/result from a running seed-wallet migration (Phase 1: isolated create).
pumpSeedMigration();
// Auto-lock check (only when connected + encrypted + unlocked)
if (state_.connected && state_.isUnlocked()) {
checkAutoLock();
@@ -1901,6 +1904,10 @@ void App::render()
renderSeedBackupDialog();
}
if (show_seed_migration_) {
renderSeedMigrationDialog();
}
// Security overlay dialogs (encrypt, decrypt, PIN) are rendered AFTER ImGui::End()
// to ensure they appear on top of all other content
@@ -3029,6 +3036,124 @@ void App::renderSeedBackupDialog()
ui::material::EndOverlayDialog();
}
void App::renderSeedMigrationDialog()
{
auto wipeSeed = [this]() {
if (!seed_migration_seed_.empty())
sodium_memzero(&seed_migration_seed_[0], seed_migration_seed_.size());
seed_migration_seed_.clear();
};
auto close = [this, &wipeSeed]() { wipeSeed(); show_seed_migration_ = false; };
if (!ui::material::BeginOverlayDialog("Migrate to a seed wallet", &show_seed_migration_, 580.0f, 0.94f)) {
// Block dismiss while the background create is running (it can't be cancelled cleanly);
// otherwise a dismiss wipes the revealed seed.
if (seed_migration_in_flight_) show_seed_migration_ = true;
else wipeSeed();
return;
}
ui::material::Type().text(ui::material::TypeStyle::H6, "Migrate to a seed wallet");
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
switch (seed_migration_step_) {
case SeedMigrationStep::Intro:
ImGui::TextWrapped(
"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.");
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton("Create seed wallet", ImVec2(180, 0)))
beginCreateSeedWallet();
ImGui::SameLine();
if (ui::material::StyledButton("Cancel", ImVec2(120, 0)))
close();
break;
case SeedMigrationStep::Working:
ImGui::TextWrapped("Creating your new seed wallet in an isolated node — this can take a "
"minute. Your main wallet keeps running.");
ImGui::Spacing();
if (!seed_migration_status_.empty())
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::OnSurfaceMedium()), "%s", seed_migration_status_.c_str());
break;
case SeedMigrationStep::ShowSeed: {
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error());
ImGui::TextWrapped("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.");
ImGui::PopStyleColor();
ImGui::Spacing();
ui::RenderSeedWordGrid(ui::SplitSeedWords(seed_migration_seed_));
ImGui::Spacing();
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::OnSurfaceMedium()), "New wallet receive address:");
ImGui::TextWrapped("%s", seed_migration_dest_.c_str());
ImGui::Spacing();
if (ui::material::StyledButton("Copy seed", ImVec2(120, 0)))
copySecretToClipboard(seed_migration_seed_);
ImGui::SameLine();
if (ui::material::StyledButton("Save to file", ImVec2(130, 0))) {
std::string path = util::Platform::getConfigDir() + "/dragonx-seed-wallet-backup.txt";
std::string content = seed_migration_seed_ + "\nAddress: " + seed_migration_dest_ + "\n";
const bool ok = util::Platform::writeFileAtomically(path, content, /*restrict=*/true);
sodium_memzero(&content[0], content.size());
seed_migration_status_ = (ok ? "Saved to " : "Could not write ") + path;
}
if (!seed_migration_status_.empty()) {
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", seed_migration_status_.c_str());
}
ImGui::Spacing();
ImGui::Checkbox("I've written down my seed phrase", &seed_migration_backed_up_);
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
ImGui::TextWrapped("Step 1 of 2 complete — your funds have NOT moved yet. Sweeping them into "
"this new wallet is the next step.");
ImGui::PopStyleColor();
ImGui::Separator();
if (!seed_migration_backed_up_) ImGui::BeginDisabled();
if (ui::material::StyledButton("Done", ImVec2(120, 0)))
close();
if (!seed_migration_backed_up_) ImGui::EndDisabled();
ImGui::SameLine();
if (ui::material::StyledButton("Discard migration", ImVec2(160, 0))) {
// Throw away the isolated new wallet + clear the pending state.
if (!seed_migration_temp_dir_.empty()) {
std::error_code ec;
std::filesystem::remove_all(seed_migration_temp_dir_, ec);
}
if (settings_) {
settings_->setSeedMigrationPending(false);
settings_->setSeedMigrationDest("");
settings_->setSeedMigrationTempDir("");
settings_->save();
}
seed_migration_temp_dir_.clear();
seed_migration_dest_.clear();
close();
}
break;
}
case SeedMigrationStep::Error:
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error());
ImGui::TextWrapped("%s", seed_migration_status_.c_str());
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::Separator();
if (ui::material::StyledButton("Back", ImVec2(120, 0)))
seed_migration_step_ = SeedMigrationStep::Intro;
ImGui::SameLine();
if (ui::material::StyledButton("Close", ImVec2(120, 0)))
close();
break;
}
ui::material::EndOverlayDialog();
}
void App::renderAntivirusHelpDialog()
{
#ifdef _WIN32

View File

@@ -9,6 +9,7 @@
#include <functional>
#include <thread>
#include <atomic>
#include <mutex>
#include <chrono>
#include <unordered_map>
#include <unordered_set>
@@ -352,6 +353,7 @@ public:
void showExportKeyDialog() { show_export_key_ = true; }
void showBackupDialog() { show_backup_ = true; }
void showSeedBackupDialog() { show_seed_backup_ = true; }
void showSeedMigrationDialog() { show_seed_migration_ = true; seed_migration_step_ = SeedMigrationStep::Intro; }
void showAboutDialog() { show_about_ = true; }
// Legacy tab compat — maps int to NavPage
@@ -610,6 +612,10 @@ private:
// One-time nudge: on a full-node wallet that has a mnemonic, remind the user (once per
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
void maybeRemindSeedBackup();
// Seed-wallet migration (Phase 1: create a new mnemonic wallet in isolation, no funds moved).
void beginCreateSeedWallet(); // starts the isolated create on a background thread
void pumpSeedMigration(); // main thread: pick up background progress/result each frame
void provisionChatIdentityFromSecret(std::string secret);
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr for chat
std::string generateChatLocalId(const char* prefix, int numBytes) const; // unique echo id / cid
@@ -678,6 +684,23 @@ private:
bool seed_backup_no_mnemonic_ = false;
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
// --- Seed-wallet migration (Phase 1: isolated seed-wallet creation) ---
enum class SeedMigrationStep { Intro, Working, ShowSeed, Error };
bool show_seed_migration_ = false;
SeedMigrationStep seed_migration_step_ = SeedMigrationStep::Intro;
bool seed_migration_in_flight_ = false; // main-thread guard while the bg 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)
std::string seed_migration_temp_dir_; // temp datadir holding the new wallet (kept)
bool seed_migration_backed_up_ = false; // "I've written it down" confirmation
std::string seed_migration_status_; // main-thread display (progress / error text)
// Cross-thread handoff from the background create task (guarded by seed_migration_mutex_).
std::mutex seed_migration_mutex_;
std::string seed_migration_progress_; // latest progress line
bool seed_migration_done_ = false; // result ready to consume
bool seed_migration_ok_ = false;
std::string seed_migration_r_seed_, seed_migration_r_dest_, seed_migration_r_tmp_, seed_migration_r_err_;
// Embedded daemon state
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
std::string daemon_status_;
@@ -940,6 +963,7 @@ private:
void renderExportKeyDialog();
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 renderFirstRunWizard();
void renderLockScreen();
void renderEncryptWalletDialog();

View File

@@ -41,6 +41,7 @@
#include "wallet/lite_wallet_state_mapper.h" // LiteWalletAppRefreshModel for lite chat harvest
#include "daemon/daemon_controller.h"
#include "daemon/embedded_daemon.h"
#include "daemon/seed_wallet_creator.h"
#include "daemon/xmrig_manager.h"
#include "util/pool_registry.h"
#include "ui/notifications.h"
@@ -2926,6 +2927,75 @@ void App::maybeRemindSeedBackup()
});
}
void App::beginCreateSeedWallet()
{
if (seed_migration_in_flight_) return;
seed_migration_in_flight_ = true;
seed_migration_step_ = SeedMigrationStep::Working;
{
std::lock_guard<std::mutex> lk(seed_migration_mutex_);
seed_migration_done_ = false;
seed_migration_progress_ = "Starting…";
}
// Runs the isolated create on a background thread; the main thread picks up progress + the
// result in pumpSeedMigration(). No funds move and the real wallet is never touched.
async_tasks_.submit("Create seed wallet", [this](const util::AsyncTaskManager::Token&) {
auto res = daemon::SeedWalletCreator::create(/*keepDatadir=*/true,
[this](const std::string& msg) {
std::lock_guard<std::mutex> lk(seed_migration_mutex_);
seed_migration_progress_ = msg;
});
std::lock_guard<std::mutex> lk(seed_migration_mutex_);
seed_migration_ok_ = res.ok;
seed_migration_r_seed_ = res.seedPhrase;
seed_migration_r_dest_ = res.destAddress;
seed_migration_r_tmp_ = res.tempDatadir;
seed_migration_r_err_ = res.error;
seed_migration_done_ = true;
if (!res.seedPhrase.empty()) sodium_memzero(&res.seedPhrase[0], res.seedPhrase.size());
});
}
void App::pumpSeedMigration()
{
if (!seed_migration_in_flight_) return;
bool done = false, ok = false;
std::string seed, dest, tmp, err;
{
std::lock_guard<std::mutex> lk(seed_migration_mutex_);
seed_migration_status_ = seed_migration_progress_; // live progress for the Working step
done = seed_migration_done_;
if (done) {
ok = seed_migration_ok_;
seed = std::move(seed_migration_r_seed_);
dest = std::move(seed_migration_r_dest_);
tmp = std::move(seed_migration_r_tmp_);
err = std::move(seed_migration_r_err_);
seed_migration_done_ = false;
seed_migration_r_seed_.clear();
}
}
if (!done) return;
seed_migration_in_flight_ = false;
if (ok) {
seed_migration_seed_ = std::move(seed);
seed_migration_dest_ = std::move(dest);
seed_migration_temp_dir_ = std::move(tmp);
seed_migration_backed_up_ = false;
seed_migration_step_ = SeedMigrationStep::ShowSeed;
// Persist the pending migration so a later sweep/adopt step can find the new wallet.
if (settings_) {
settings_->setSeedMigrationPending(true);
settings_->setSeedMigrationDest(seed_migration_dest_);
settings_->setSeedMigrationTempDir(seed_migration_temp_dir_);
settings_->save();
}
} else {
seed_migration_status_ = err.empty() ? std::string("Could not create the seed wallet.") : err;
seed_migration_step_ = SeedMigrationStep::Error;
}
}
void App::backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback)
{
if (!state_.connected || !rpc_) {

View File

@@ -200,6 +200,9 @@ bool Settings::load(const std::string& path)
}
loadScalar(j, "wizard_completed", wizard_completed_);
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
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_);
loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
loadScalar(j, "unlock_duration", unlock_duration_);
loadScalar(j, "pin_enabled", pin_enabled_);
@@ -434,6 +437,9 @@ bool Settings::save(const std::string& path)
}
j["wizard_completed"] = wizard_completed_;
j["seed_backup_reminded"] = seed_backup_reminded_;
j["seed_migration_pending"] = seed_migration_pending_;
j["seed_migration_dest"] = seed_migration_dest_;
j["seed_migration_temp_dir"] = seed_migration_temp_dir_;
j["auto_lock_timeout"] = auto_lock_timeout_;
j["unlock_duration"] = unlock_duration_;
j["pin_enabled"] = pin_enabled_;

View File

@@ -250,6 +250,15 @@ public:
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = 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_; }
void setSeedMigrationPending(bool v) { seed_migration_pending_ = v; }
std::string getSeedMigrationDest() const { return seed_migration_dest_; }
void setSeedMigrationDest(const std::string& v) { seed_migration_dest_ = v; }
std::string getSeedMigrationTempDir() const { return seed_migration_temp_dir_; }
void setSeedMigrationTempDir(const std::string& v) { seed_migration_temp_dir_ = v; }
// Security — auto-lock timeout (seconds; 0 = disabled)
int getAutoLockTimeout() const { return auto_lock_timeout_; }
void setAutoLockTimeout(int seconds) { auto_lock_timeout_ = seconds; }
@@ -449,6 +458,9 @@ private:
std::map<std::string, AddressMeta> address_meta_;
bool wizard_completed_ = false;
bool seed_backup_reminded_ = false;
bool seed_migration_pending_ = false;
std::string seed_migration_dest_;
std::string seed_migration_temp_dir_;
int auto_lock_timeout_ = 900; // 15 minutes
int unlock_duration_ = 600; // 10 minutes
bool pin_enabled_ = false;

View File

@@ -448,9 +448,10 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
return true;
}
// Check if something is already listening on the RPC port
// Check if something is already listening on the RPC port. An isolated instance (migrate-to-
// seed) runs on its own non-default port alongside the main daemon, so it skips this bail.
int rpc_port = std::atoi(DRAGONX_DEFAULT_RPC_PORT);
if (isPortInUse(rpc_port)) {
if (!skip_port_check_ && isPortInUse(rpc_port)) {
std::string owner = getPortOwnerInfo(rpc_port);
VERBOSE_LOGF("[INFO] Port %d is already in use by %s — external daemon detected, will connect to it.\\n", rpc_port, owner.c_str());
external_daemon_detected_ = true;
@@ -498,7 +499,20 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
args.push_back("-rescan");
}
// One-shot isolated-datadir override (migrate-to-seed flow): run this start against a
// throwaway datadir, plus any extra args (e.g. -connect=0). Consumed here so later starts
// revert to the normal datadir. The datadir's basename MUST be the assetchain name (DRAGONX)
// or the daemon mis-resolves its conf/port; it reads <datadir>/DRAGONX.conf automatically, so
// no -conf is passed (an explicit -conf confuses the Komodo/Hush path resolution).
if (!override_datadir_.empty()) {
DEBUG_LOGF("[INFO] Isolated start override: -datadir=%s\n", override_datadir_.c_str());
args.push_back("-datadir=" + override_datadir_);
}
for (const auto& a : override_extra_args_) args.push_back(a);
override_datadir_.clear();
override_extra_args_.clear();
if (!startProcess(daemon_path, args)) {
DEBUG_LOGF("[ERROR] Failed to start dragonxd process: %s\\n", last_error_.c_str());
setState(State::Error, "Failed to start dragonxd process");
@@ -1220,5 +1234,10 @@ bool EmbeddedDaemon::isRpcPortInUse()
return isPortInUse(port);
}
bool EmbeddedDaemon::tcpPortInUse(int port)
{
return isPortInUse(port);
}
} // namespace daemon
} // namespace dragonx

View File

@@ -191,6 +191,29 @@ public:
void setZapOnNextStart(bool v) { zap_on_next_start_ = v; }
bool zapOnNextStart() const { return zap_on_next_start_.load(); }
/**
* @brief One-shot isolated-datadir override for the NEXT start(): run the daemon against a
* different datadir (with its own DRAGONX.conf) plus the given extra args. Used by the
* "migrate to a seed wallet" flow to mint a fresh mnemonic wallet in a throwaway datadir
* without touching the real one. Consumed on the next start(); later starts are normal.
* The caller must serialize this with start() (no concurrent starts).
*/
void setNextStartOverride(const std::string& datadir, std::vector<std::string> extraArgs) {
override_datadir_ = datadir;
override_extra_args_ = std::move(extraArgs);
}
void clearNextStartOverride() { override_datadir_.clear(); override_extra_args_.clear(); }
/**
* @brief Skip the "default RPC port already in use → external daemon" bail in start().
* Set true only for an isolated instance running on its OWN (non-default) port
* alongside the main daemon (migrate-to-seed flow).
*/
void setSkipPortCheck(bool v) { skip_port_check_ = v; }
/** @brief Is an arbitrary TCP port currently in use on localhost? (used to pick a free port) */
static bool tcpPortInUse(int port);
/** Get number of consecutive daemon crashes (resets on successful start or manual reset) */
int getCrashCount() const { return crash_count_.load(); }
/** Reset crash counter (call on successful connection or manual restart) */
@@ -231,6 +254,9 @@ private:
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> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start
std::string override_datadir_; // one-shot: -datadir for the next start
std::vector<std::string> override_extra_args_; // one-shot: extra args for the next start
bool skip_port_check_ = false; // isolated instance on a non-default port
};
} // namespace daemon

View File

@@ -0,0 +1,137 @@
#include "daemon/seed_wallet_creator.h"
#include <chrono>
#include <filesystem>
#include <thread>
#include <sodium.h>
#include "daemon/embedded_daemon.h"
#include "rpc/rpc_client.h"
#include "util/platform.h"
namespace fs = std::filesystem;
namespace dragonx {
namespace daemon {
namespace {
// Random alphanumeric token for the isolated node's throwaway RPC credentials (libsodium CSPRNG;
// sodium_init() has already run at app startup for the chat crypto).
std::string randomToken(int n)
{
static const char cs[] =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
std::string s;
s.reserve(n);
for (int i = 0; i < n; ++i)
s.push_back(cs[randombytes_uniform(sizeof(cs) - 1)]);
return s;
}
// A free localhost port for the isolated node — just above the default so it never collides with
// the main daemon (which keeps running on the default port throughout).
int pickFreePort()
{
for (int p = 21770; p < 21900; ++p)
if (!EmbeddedDaemon::tcpPortInUse(p))
return p;
return 0;
}
} // namespace
SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
const std::function<void(const std::string&)>& progress)
{
auto report = [&](const std::string& m) { if (progress) progress(m); };
SeedWalletResult r;
std::error_code ec;
// 1. Isolated throwaway datadir. The Komodo/Hush daemon requires the datadir's basename to be
// the assetchain name (DRAGONX) — mirroring ~/.hush/DRAGONX — or it mis-resolves its conf and
// RPC port. So the wallet lives in <base>/DRAGONX; `base` is the migration root we clean up.
const std::string base = util::Platform::getConfigDir() + "/seed-migrate";
const std::string dataDir = base + "/DRAGONX";
fs::remove_all(base, ec);
fs::create_directories(dataDir, ec);
if (ec) { r.error = "Could not create the temporary wallet directory."; return r; }
// 2. Free port + fresh throwaway RPC credentials for the isolated node.
const int port = pickFreePort();
if (port <= 0) { r.error = "No free local port for the isolated node."; return r; }
const std::string user = randomToken(16);
const std::string pass = randomToken(32);
// 3. Minimal conf for the isolated node (own creds/port; -tls=only comes from the chain args,
// so the RPC client below connects with TLS just like the main daemon does).
const std::string conf = "rpcuser=" + user + "\n"
"rpcpassword=" + pass + "\n"
"rpcport=" + std::to_string(port) + "\n"
"server=1\n";
if (!util::Platform::writeFileAtomically(dataDir + "/DRAGONX.conf", conf,
/*restrictPermissions=*/true)) {
r.error = "Could not write the isolated node config.";
fs::remove_all(base, ec);
return r;
}
// 4. Start the isolated daemon: fresh mnemonic wallet (-usemnemonic=1), no network/sync.
report("Starting an isolated node…");
EmbeddedDaemon temp;
temp.setSkipPortCheck(true); // runs on `port`, beside the main daemon on the default port
temp.setNextStartOverride(dataDir, {"-usemnemonic=1", "-connect=0", "-listen=0",
"-maxconnections=0"});
if (!temp.start("")) {
r.error = "Could not start the isolated node: " + temp.getLastError();
fs::remove_all(base, ec);
return r;
}
// 5. Connect to it, retrying until the RPC is responsive and past warmup.
report("Creating your new seed wallet…");
rpc::RPCClient cli;
bool ready = false;
for (int i = 0; i < 90 && !ready; ++i) {
if (cli.connect("127.0.0.1", std::to_string(port), user, pass, /*useTls=*/true)) {
try { cli.call("getinfo"); ready = true; } // succeeds only once past warmup (-28)
catch (...) { cli.disconnect(); }
}
if (!ready) std::this_thread::sleep_for(std::chrono::seconds(1));
}
if (!ready) {
r.error = "The isolated node did not become ready in time.";
temp.stop(20000);
fs::remove_all(base, ec);
return r;
}
// 6. Export the new seed phrase + a fresh shielded receive address (the future sweep target).
try {
auto m = cli.call("z_exportmnemonic");
if (m.contains("mnemonic") && m["mnemonic"].is_string())
r.seedPhrase = m["mnemonic"].get<std::string>();
r.destAddress = cli.call("z_getnewaddress").get<std::string>();
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();
}
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly).
cli.disconnect();
temp.stop(20000);
// 8. Keep the temp wallet for a later sweep/adopt step, or scrub it. tempDatadir is the
// migration root `base`; the new wallet.dat lives in <base>/DRAGONX.
r.tempDatadir = base;
if (!keepDatadir || !r.ok) {
fs::remove_all(base, ec);
r.tempDatadir.clear();
}
return r;
}
} // namespace daemon
} // namespace dragonx

View File

@@ -0,0 +1,32 @@
#pragma once
#include <functional>
#include <string>
namespace dragonx {
namespace daemon {
struct SeedWalletResult {
bool ok = false;
std::string seedPhrase; // SECRET — the caller must wipe it after use
std::string destAddress; // new shielded z-address (the Phase 2 sweep target)
std::string tempDatadir; // datadir holding the new wallet.dat (kept iff keepDatadir + ok)
std::string error;
};
// Mint a fresh BIP39 mnemonic wallet in an ISOLATED throwaway datadir by running a second
// dragonxd on its own port with no network, export its seed + a new z-address, then stop it.
//
// This is the safe first step of "migrate to a seed wallet": it moves no funds and never touches
// the main daemon or the real wallet.dat. Blocking — call it on a background thread.
//
// keepDatadir: keep the temp datadir + its wallet.dat so a later sweep/adopt step can use it, or
// delete it immediately (used when only the seed itself is wanted).
class SeedWalletCreator {
public:
static SeedWalletResult create(bool keepDatadir,
const std::function<void(const std::string&)>& progress = {});
};
} // namespace daemon
} // namespace dragonx

View File

@@ -1304,10 +1304,11 @@ void RenderSettingsPage(App* app) {
naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX;
float wizW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(wizLabel).x + btnPadX : 0.0f;
float bsW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(bsLabel).x + btnPadX : 0.0f;
// Full-node-only "Seed phrase" button trails the Backup button (see below).
// Full-node-only "Seed phrase" + "Migrate to seed" buttons trail the Backup button.
float seedW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(TR("seed_backup_button")).x + btnPadX : 0.0f;
float migrateW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(TR("seed_migrate_button")).x + btnPadX : 0.0f;
float totalW = naturalW + sp * 5;
if (showFullNodeLifecycleActions) totalW += wizW + bsW + seedW + sp * 3;
if (showFullNodeLifecycleActions) totalW += wizW + bsW + seedW + migrateW + sp * 4;
float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
if (scale < 1.0f) ImGui::SetWindowFontScale(scale);
@@ -1334,6 +1335,11 @@ void RenderSettingsPage(App* app) {
if (TactileButton(TR("seed_backup_button"), ImVec2(0, 0), btnFont))
app->showSeedBackupDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_backup"));
// Migrate to a new seed-phrase wallet (create in isolation, then sweep funds).
ImGui::SameLine(0, scaledSp);
if (TactileButton(TR("seed_migrate_button"), ImVec2(0, 0), btnFont))
app->showSeedMigrationDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_migrate"));
}
ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[4], ImVec2(0, 0), btnFont))

View File

@@ -249,6 +249,8 @@ void I18n::loadBuiltinEnglish()
strings_["seed_backup_save_failed"] = "Could not write ";
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_migrate_button"] = "Migrate to seed…";
strings_["tt_seed_migrate"] = "Create a new seed-phrase wallet and move your funds into it";
strings_["contacts_search_placeholder"] = "Search contacts...";
strings_["contacts_search_no_match"] = "No matching contacts";
strings_["address_book_confirm_delete"] = "Confirm delete?";