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:
@@ -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);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "../../app.h"
|
||||
#include "../../config/settings.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/payment_uri.h"
|
||||
#include "../../util/platform.h"
|
||||
#include "../../util/text_format.h"
|
||||
#include "../../config/version.h"
|
||||
@@ -365,6 +366,7 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
|
||||
if (tx.type != "receive" && tx.type != "mined") continue;
|
||||
recvs.push_back(&tx);
|
||||
}
|
||||
if (recvs.size() > 4) recvs.resize(4); // show only the 4 most recent (newest-first)
|
||||
|
||||
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
|
||||
|
||||
@@ -539,11 +541,10 @@ void RenderReceiveTab(App* app)
|
||||
// Generate QR data
|
||||
std::string qr_data = selected.address;
|
||||
if (s_request_amount > 0) {
|
||||
qr_data = std::string("dragonx:") + selected.address +
|
||||
"?amount=" + std::to_string(s_request_amount);
|
||||
if (s_request_memo[0] && isZ) {
|
||||
qr_data += "&memo=" + std::string(s_request_memo);
|
||||
}
|
||||
// Canonical, URL-encoded "drgx:" URI via the shared builder. The old inline code emitted an
|
||||
// unparseable "dragonx:" scheme and appended the memo raw (spaces/&/= corrupted the QR).
|
||||
qr_data = util::buildPaymentUri(selected.address, s_request_amount, "",
|
||||
(s_request_memo[0] && isZ) ? std::string(s_request_memo) : std::string());
|
||||
}
|
||||
if (qr_data != s_cached_qr_data) {
|
||||
if (s_qr_texture) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user