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:
125
src/app.cpp
125
src/app.cpp
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user