fix: Tier-1 UX robustness — backup/export integrity, verified installs, input validation

From the app-wide robustness audit (calibrated to the import-key dialog). These
are the safety-critical fixes; several could cost users funds:

- Backup/export false success (FUND LOSS): exportAllKeys pre-seeded a header, so
  a keyless result (the usual case when the wallet is encrypted+locked) still
  looked non-empty and backupWallet wrote a private-key-less file and reported
  "Backup saved". Now exportAllKeys returns an exported count; backupWallet and
  the export-all dialog refuse to write / report success on 0 keys, disclose the
  count, flag partial results as INCOMPLETE, and the backup dialog confirms
  before overwriting an existing file (+ trims the path).
- Bootstrap: fail CLOSED — refuse to install an unverified multi-GB archive when
  no checksum is published (was `return true`), mirroring the xmrig/daemon updaters.
- Restore-from-seed: require a valid BIP39 word count (12/15/18/21/24) with a live
  "should be 24 words — you have N" hint instead of accepting any non-empty text.
- Encryption passphrase: detect leading/trailing whitespace and BLOCK with a
  warning (not a silent trim, which would change the passphrase and lock the user
  out).
- Receive payment QR: emit the canonical `drgx:` scheme with a URL-encoded memo
  via a new shared util::buildPaymentUri (the request-payment dialog now routes
  through it too, so they can't diverge); the old "dragonx:" + raw memo was
  unparseable by the wallet's own scanner.

Also clamps the receive "Recent Received" list to the 4 most recent (companion to
the recent-lists commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 12:54:05 -05:00
parent 24bb32eb61
commit df14533ad3
10 changed files with 188 additions and 100 deletions

View File

@@ -177,9 +177,9 @@ void ExportAllKeysDialog::render(App* app)
if (result.is_string()) {
keys += "# Address: " + addr + "\n";
keys += result.get<std::string>() + "\n\n";
exported++; // count only real successes (locked/failed keys don't count)
}
} catch (...) {}
exported++;
}
}
@@ -193,17 +193,18 @@ void ExportAllKeysDialog::render(App* app)
if (result.is_string()) {
keys += "# Address: " + addr + "\n";
keys += result.get<std::string>() + "\n\n";
exported++; // count only real successes (locked/failed keys don't count)
}
} catch (...) {}
exported++;
}
}
// Save to file (still on worker thread)
// Only write a file if we actually exported keys — a keyless file would be a
// misleading "export" (the usual cause of 0 keys is an encrypted+locked wallet).
std::string configDir = util::Platform::getConfigDir();
std::string filepath = configDir + "/" + filename;
bool writeOk = false;
{
if (exported > 0) {
std::ofstream file(filepath);
if (file.is_open()) {
file << keys;
@@ -211,16 +212,26 @@ void ExportAllKeysDialog::render(App* app)
writeOk = true;
}
}
return [exported, total, filepath, writeOk]() {
s_exported_count = exported;
s_exporting = false;
if (writeOk) {
s_status = "Exported to: " + filepath;
Notifications::instance().success(TR("export_keys_success"), 5.0f);
} else {
if (exported == 0) {
s_status = "No keys exported (0 of " + std::to_string(total) +
") — unlock the wallet (if encrypted) and try again.";
Notifications::instance().error("No keys could be exported — is the wallet unlocked?");
} else if (!writeOk) {
s_status = "Failed to write file";
Notifications::instance().error("Failed to save key file");
} else if (exported < total) {
s_status = "Exported " + std::to_string(exported) + " of " +
std::to_string(total) + " keys to: " + filepath +
" (INCOMPLETE — some addresses had no spending key or the wallet is locked)";
Notifications::instance().warning("Partial export: " + std::to_string(exported) +
" of " + std::to_string(total) + " keys");
} else {
s_status = "Exported to: " + filepath;
Notifications::instance().success(TR("export_keys_success"), 5.0f);
}
};
});