fix: FAQ CJK glyphs, outgoing-tx history, lite encryption, migrate & sync safety
Bug-report fixes plus adversarial-audit follow-ups. Bumps full 2.0.1->2.0.2 and lite 1.1.0->1.1.1. i18n / fonts: - Rebuild the NotoSansCJK subset with the glyphs the FAQ back-fill introduced (什/么/门/做); Chinese FAQ titles rendered as "??" on zh (and ja/ko). Transaction history (enumeration gaps): - Surface outgoing shielded z->z sends via z_listsentbyaddress (listtransactions and z_listreceivedbyaddress never report them). - Surface z->t deshields by parsing z_listsentbyaddress transparentSends (which z_viewtransaction does not expose); dedupe t->t against listtransactions. - Key send-row dedup on address so equal-value multi-output sends aren't dropped. Lite wallet (key safety): - The async create/restore/open path the UI uses now applies the passphrase: encrypt a new/restored wallet, unlock an existing one. It previously discarded the passphrase, storing the seed/keys in PLAINTEXT (create/restore) or leaving an encrypted wallet locked (open). Migrate-to-seed (fund safety): - Persist a sweep-submitted marker at broadcast so a resumed migration can't treat a ~0 balance (an unconfirmed in-flight sweep) as an empty wallet and adopt (swap wallet.dat) before the sweep confirms; best-effort locate routes to the confirm gate. Sync / threading: - Gate isSynced() on a peer-derived tip (tip_known) so a peerless node isn't reported synced (Send against a stale balance); add a "no peers" status banner. - Guard daemon_status_ with a mutex (monitor-thread write vs UI-thread read). Tests: extends tests/test_phase4.cpp across all of the above; suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwycPDQSfEKtrTS2uC4JeT
This commit is contained in:
@@ -4701,6 +4701,9 @@ void App::showSeedMigrationDialog()
|
||||
seed_migration_balance_loaded_ = false;
|
||||
seed_migration_nofunds_confirmed_ = false;
|
||||
refreshSeedMigrationBalance();
|
||||
// If a sweep was already broadcast (the marker survives a lost opid), try to locate its tx so
|
||||
// we resume at the confirmation gate instead of risking a premature "no funds -> adopt".
|
||||
if (settings_ && settings_->getSeedMigrationSweepSubmitted()) locateSweepTxid();
|
||||
break;
|
||||
case MigrationResume::Intro:
|
||||
default:
|
||||
@@ -4768,6 +4771,12 @@ void App::beginSweepToSeedWallet()
|
||||
}
|
||||
seed_migration_step_ = SeedMigrationStep::Sweeping;
|
||||
seed_migration_status_ = "Building the sweep transaction…";
|
||||
// Mark the sweep as submitted BEFORE broadcasting (persisted synchronously). If the app or daemon
|
||||
// then dies after the tx reaches the mempool but before its opid/txid is persisted, a resumed
|
||||
// migration still knows a sweep is in flight and won't mistake the resulting ~0 legacy balance for
|
||||
// an empty wallet and adopt (swap wallet.dat) prematurely — funds in an unconfirmed sweep can still
|
||||
// revert on a drop/reorg. Cleared below only if the submit definitively fails (nothing broadcast).
|
||||
if (settings_) { settings_->setSeedMigrationSweepSubmitted(true); settings_->save(); }
|
||||
const std::string dest = seed_migration_dest_;
|
||||
worker_->post([this, dest]() -> rpc::RPCWorker::MainCb {
|
||||
std::string opid, err;
|
||||
@@ -4780,6 +4789,8 @@ void App::beginSweepToSeedWallet()
|
||||
} catch (const std::exception& e) { err = e.what(); }
|
||||
return [this, opid, err]() {
|
||||
if (!err.empty() || opid.empty()) {
|
||||
// Submit failed — nothing was broadcast, so drop the "sweep in flight" marker.
|
||||
if (settings_) { settings_->setSeedMigrationSweepSubmitted(false); settings_->save(); }
|
||||
seed_migration_status_ = err.empty() ? "The sweep failed to start." : err;
|
||||
seed_migration_step_ = SeedMigrationStep::Error;
|
||||
return;
|
||||
@@ -4882,6 +4893,69 @@ void App::pollSweepStatus()
|
||||
});
|
||||
}
|
||||
|
||||
// Best-effort recovery when a sweep was broadcast but its opid was lost (e.g. a daemon restart wiped
|
||||
// the in-memory op queue) so we have no txid to confirm against. The sweep is an outgoing send to
|
||||
// seed_migration_dest_; find it among the wallet's own sends (z_listsentbyaddress) and, if present,
|
||||
// adopt its txid and resume at the confirmation gate (pollSweepStatus / Confirming). Read-only, and
|
||||
// backstopped by that gate (confs >= 1 && legacy balance ~0), so a wrong match cannot lose funds —
|
||||
// it only avoids the premature "no funds -> adopt" short-circuit while a sweep is unconfirmed.
|
||||
void App::locateSweepTxid()
|
||||
{
|
||||
if (capture_mode_) return;
|
||||
if (!rpc_ || !worker_ || !state_.connected) return;
|
||||
if (seed_migration_confirm_in_flight_ || !seed_migration_sweep_txid_.empty()) return;
|
||||
if (seed_migration_dest_.empty()) return;
|
||||
seed_migration_confirm_in_flight_ = true;
|
||||
const std::string dest = seed_migration_dest_;
|
||||
worker_->post([this, dest]() -> rpc::RPCWorker::MainCb {
|
||||
std::string found;
|
||||
rpc::RPCClient::TraceScope trace("Migrate / locate sweep");
|
||||
try {
|
||||
// z_listsentbyaddress ignores its address argument and returns ALL wallet sends
|
||||
// (oldest -> newest); the sweep is the latest send whose outputs include the destination.
|
||||
auto sent = rpc_->call("z_listsentbyaddress", {dest, 0});
|
||||
if (sent.is_array()) {
|
||||
for (const auto& e : sent) {
|
||||
if (!e.is_object() || !e.contains("sends") || !e["sends"].is_object()) continue;
|
||||
const auto& s = e["sends"];
|
||||
bool toDest = false;
|
||||
for (const char* leg : {"saplingSends", "transparentSends"}) {
|
||||
auto it = s.find(leg);
|
||||
if (it == s.end() || !it->is_array()) continue;
|
||||
for (const auto& o : *it) {
|
||||
if (o.is_object() && o.value("address", std::string()) == dest) {
|
||||
toDest = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (toDest) break;
|
||||
}
|
||||
if (toDest) {
|
||||
auto t = e.value("txid", std::string());
|
||||
if (!t.empty()) found = t; // keep scanning; entries are oldest->newest
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (...) {}
|
||||
return [this, found]() {
|
||||
seed_migration_confirm_in_flight_ = false;
|
||||
if (found.empty() || !seed_migration_sweep_txid_.empty()) return;
|
||||
// Adopt the recovered txid and hand off to the confirmation gate (which alone permits adopt).
|
||||
seed_migration_sweep_txid_ = found;
|
||||
if (settings_) {
|
||||
settings_->setSeedMigrationSweepTxid(found);
|
||||
settings_->setSeedMigrationSweepOpid("");
|
||||
settings_->save();
|
||||
}
|
||||
seed_migration_sweep_confs_ = 0;
|
||||
seed_migration_legacy_remaining_ = -1.0;
|
||||
seed_migration_poll_timer_ = 0.0f;
|
||||
seed_migration_status_.clear();
|
||||
seed_migration_step_ = SeedMigrationStep::Confirming;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Phase 2 step 2: adopt the new seed wallet as the primary wallet. Stop the daemon, move the
|
||||
// legacy wallet.dat aside to a timestamped backup (NEVER delete it), copy the new wallet in, and
|
||||
// restart with -rescan so the new keys pick up the swept funds. Runs on a background thread.
|
||||
@@ -5308,6 +5382,7 @@ void App::pumpSeedMigration()
|
||||
settings_->setSeedMigrationTempDir("");
|
||||
settings_->setSeedMigrationSweepTxid("");
|
||||
settings_->setSeedMigrationSweepOpid(""); // W3-3
|
||||
settings_->setSeedMigrationSweepSubmitted(false);
|
||||
settings_->save();
|
||||
}
|
||||
seed_migration_status_ = err; // a non-empty warning here (e.g. restart hiccup) is shown on Done
|
||||
@@ -5355,6 +5430,7 @@ void App::pumpSeedMigration()
|
||||
// txid/opid (the resume block reads these whenever the migration is pending).
|
||||
settings_->setSeedMigrationSweepTxid("");
|
||||
settings_->setSeedMigrationSweepOpid("");
|
||||
settings_->setSeedMigrationSweepSubmitted(false);
|
||||
settings_->save();
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user