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

@@ -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_) {