Migrate-to-seed submits z_mergetoaddress -> an async opid, then only persists the resolved txid once the op completes. An app-close during Sweeping (opid submitted, txid not yet resolved) dropped the opid and resumed at the re-sweep gate, silently losing the tx. Now the opid is persisted and re-tracked on resume. If the daemon forgot it (restart — its op queue is in-memory only), the existing poller flags it stale and the callback falls back to the dismissable Sweep gate; it can never hang (a thrown RPC aborts the poll, so a stale classification only comes from a *successful* poll that omits the opid). - New seed_migration_sweep_opid setting; adopted atomically with clearing any prior txid in the SAME settings.save(), and only once the submit succeeds — so a failed "Sweep remaining" re-sweep keeps the already-mined first sweep's Confirming context, and txid/opid are never both authoritative (resume checks txid first; torn-write safe). - Resume routing extracted to a pure, unit-tested helper (data/seed_migration_resume.h::decideSeedMigrationResume): txid -> Confirming; opid AND connected -> re-track (Sweeping); else -> the dismissable Sweep gate. The connectivity gate keeps a disconnected resume out of the buttonless Sweeping spinner. - Shared makeSweepCompletionCallback(resumed): success -> Confirming; resumed-stale -> Sweep gate (re-fetch balance + "may have already completed" copy); fresh-fail -> Error. Fund safety unchanged: adopt still gated on legacy balance ~0 AND sweep tx mined; legacy wallet.dat only ever moved to a never-deleted timestamped .bak. Reviewed in two adversarial rounds (design + implementation) per the migration-code mandate; both safety facts (no fund loss, no hang) held, and the resume-UX traps they surfaced are fixed. Build-clean; ctest 1/1 (adds testSeedMigrationResume). See docs/wallet-hardening.md. *** Still requires a live mainnet interrupted-sweep run before release (human gate). *** Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
38 lines
2.0 KiB
C++
38 lines
2.0 KiB
C++
#pragma once
|
|
|
|
#include <string>
|
|
|
|
// Pure routing decision for resuming a pending migrate-to-seed flow (finding W3-3). Kept free of
|
|
// App/UI/RPC state so the highest-risk branch — where a reopened migration lands — is unit-testable
|
|
// and reviewable in isolation. App::showSeedMigrationDialog feeds it the persisted state + live
|
|
// connectivity and switches on the result. See src/app_network.cpp.
|
|
namespace dragonx {
|
|
|
|
enum class MigrationResume {
|
|
Intro, // no pending migration → start fresh at the intro
|
|
Confirming, // a sweep txid is persisted → resume at the confirm/adopt gate (re-derived from chain)
|
|
RetrackOpid, // a sweep opid (but no txid yet) is persisted AND we're connected → re-poll it
|
|
SweepGate, // otherwise → the dismissable Sweep step (reload balance, offer re-sweep)
|
|
};
|
|
|
|
// Decide where reopening the migration dialog lands.
|
|
//
|
|
// Invariant: the txid outranks the opid — once a sweep resolves to a txid the opid is cleared in the
|
|
// same settings write, so a persisted txid always means "past the sweep". A persisted opid is only
|
|
// re-tracked when connected, because the buttonless "Sweeping" spinner relies on the opid poller
|
|
// (which needs an RPC connection) to ever exit; disconnected, we fall back to the dismissable Sweep
|
|
// gate (which reloads the balance and, if the earlier sweep already drained it, short-circuits to
|
|
// adopt) — never trapping the user.
|
|
inline MigrationResume decideSeedMigrationResume(bool pending,
|
|
bool haveDest,
|
|
const std::string& sweepTxid,
|
|
const std::string& sweepOpid,
|
|
bool connected) {
|
|
if (!pending || !haveDest) return MigrationResume::Intro;
|
|
if (!sweepTxid.empty()) return MigrationResume::Confirming;
|
|
if (!sweepOpid.empty() && connected) return MigrationResume::RetrackOpid;
|
|
return MigrationResume::SweepGate;
|
|
}
|
|
|
|
} // namespace dragonx
|