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

@@ -5,6 +5,7 @@
#include "request_payment_dialog.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../util/payment_uri.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../widgets/qr_code.h"
@@ -34,59 +35,8 @@ static bool s_uri_dirty = true;
// Helper to build payment URI
static std::string buildPaymentUri()
{
if (s_address[0] == '\0') return "";
std::ostringstream uri;
uri << "drgx:" << s_address;
bool hasParams = false;
auto addParam = [&](const char* key, const std::string& value) {
if (value.empty()) return;
uri << (hasParams ? "&" : "?") << key << "=" << value;
hasParams = true;
};
if (s_amount > 0) {
std::ostringstream amt;
amt << std::fixed << std::setprecision(8) << s_amount;
addParam("amount", amt.str());
}
if (s_label[0] != '\0') {
// URL encode label
std::string encoded;
for (char c : std::string(s_label)) {
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
encoded += c;
} else if (c == ' ') {
encoded += "%20";
} else {
char hex[4];
snprintf(hex, sizeof(hex), "%%%02X", (unsigned char)c);
encoded += hex;
}
}
addParam("label", encoded);
}
if (s_memo[0] != '\0') {
// URL encode memo
std::string encoded;
for (char c : std::string(s_memo)) {
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
encoded += c;
} else if (c == ' ') {
encoded += "%20";
} else {
char hex[4];
snprintf(hex, sizeof(hex), "%%%02X", (unsigned char)c);
encoded += hex;
}
}
addParam("memo", encoded);
}
return uri.str();
// Route through the shared builder so this dialog and the receive tab can't diverge on scheme/encoding.
return dragonx::util::buildPaymentUri(s_address, s_amount, s_label, s_memo);
}
void RequestPaymentDialog::show(const std::string& address)