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

@@ -2323,21 +2323,22 @@ void App::exportPrivateKey(const std::string& address, std::function<void(const
});
}
void App::exportAllKeys(std::function<void(const std::string&)> callback)
void App::exportAllKeys(std::function<void(const std::string&, int, int)> callback)
{
if (!state_.connected || !rpc_) {
if (callback) callback("");
if (callback) callback("", 0, 0);
return;
}
// Collect all keys into a string
auto keys_result = std::make_shared<std::string>();
auto pending = std::make_shared<int>(0);
auto total = std::make_shared<int>(0);
auto exported = std::make_shared<int>(0); // keys actually retrieved (vs. failed/locked)
// First get all addresses
auto all_addresses = std::make_shared<std::vector<std::string>>();
// Add t-addresses
for (const auto& addr : state_.t_addresses) {
all_addresses->push_back(addr.address);
@@ -2346,27 +2347,28 @@ void App::exportAllKeys(std::function<void(const std::string&)> callback)
for (const auto& addr : state_.z_addresses) {
all_addresses->push_back(addr.address);
}
*total = all_addresses->size();
*pending = *total;
if (*total == 0) {
if (callback) callback("# No addresses to export\n");
if (callback) callback("# No addresses to export\n", 0, 0);
return;
}
*keys_result = "# DragonX Wallet Private Keys Export\n";
*keys_result += "# WARNING: Keep this file secure! Anyone with these keys can spend your coins!\n\n";
for (const auto& addr : *all_addresses) {
exportPrivateKey(addr, [keys_result, pending, total, callback, addr](const std::string& key) {
exportPrivateKey(addr, [keys_result, pending, total, exported, callback, addr](const std::string& key) {
if (!key.empty()) {
*keys_result += "# " + addr + "\n";
*keys_result += key + "\n\n";
(*exported)++;
}
(*pending)--;
if (*pending == 0 && callback) {
callback(*keys_result);
callback(*keys_result, *exported, *total);
}
});
}
@@ -2413,25 +2415,33 @@ void App::backupWallet(const std::string& destination, std::function<void(bool,
return;
}
// Use z_exportwallet or similar to export all keys
// For now, we'll use exportAllKeys and save to file
exportAllKeys([destination, callback](const std::string& keys) {
if (keys.empty()) {
if (callback) callback(false, "Failed to export keys");
// Export all keys and save to file.
exportAllKeys([destination, callback](const std::string& keys, int exported, int total) {
// NEVER write (or report success for) a keyless backup — the usual cause is an encrypted+locked
// wallet, and a "success" here would leave the user believing they have a recovery file when the
// file contains only the header comments and ZERO keys.
if (exported == 0) {
if (callback) callback(false, total == 0
? "No addresses to back up."
: "Backup failed: 0 of " + std::to_string(total) + " keys could be exported. "
"Unlock the wallet (if encrypted) and try again.");
return;
}
// Write to file
std::ofstream file(destination);
if (!file.is_open()) {
if (callback) callback(false, "Could not open file: " + destination);
return;
}
file << keys;
file.close();
if (callback) callback(true, "Wallet backup saved to: " + destination);
std::string msg = "Wallet backup saved to " + destination + ""
+ std::to_string(exported) + " of " + std::to_string(total) + " keys.";
if (exported < total)
msg += " NOTE: some addresses had no spending key or the wallet is locked — this backup is INCOMPLETE.";
// A partial backup is a real risk for recovery, so surface it as a warning (not a green success).
if (callback) callback(exported == total, msg);
});
}