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

@@ -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