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:
48
src/app.cpp
48
src/app.cpp
@@ -2509,7 +2509,28 @@ void App::renderLiteFirstRunPrompt()
|
||||
while (!seedTrim.empty() && std::isspace((unsigned char)seedTrim.front())) seedTrim.erase(seedTrim.begin());
|
||||
while (!seedTrim.empty() && std::isspace((unsigned char)seedTrim.back())) seedTrim.pop_back();
|
||||
|
||||
ImGui::BeginDisabled(seedTrim.empty());
|
||||
// Require a valid BIP39 word count before enabling Restore — otherwise a truncated or
|
||||
// garbage phrase (previously any non-empty text passed) is submitted and fails opaquely.
|
||||
int seedWords = 0;
|
||||
{ bool inWord = false;
|
||||
for (char c : seedTrim) {
|
||||
bool sp = (c == ' ' || c == '\t' || c == '\n' || c == '\r');
|
||||
if (!sp && !inWord) { seedWords++; inWord = true; }
|
||||
else if (sp) inWord = false;
|
||||
} }
|
||||
bool seedLenOk = (seedWords == 12 || seedWords == 15 || seedWords == 18 ||
|
||||
seedWords == 21 || seedWords == 24);
|
||||
if (!seedTrim.empty() && !seedLenOk) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::Warning()));
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||
ImGui::TextUnformatted(("Recovery phrase should be 24 words — you have " +
|
||||
std::to_string(seedWords) + ".").c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
ImGui::BeginDisabled(!seedLenOk);
|
||||
if (ui::material::TactileButton(TR("lite_restore_btn"), ImVec2(btnW, 0))) {
|
||||
wallet::LiteWalletRestoreRequest req;
|
||||
req.seedPhrase = seedTrim;
|
||||
@@ -2782,8 +2803,10 @@ void App::renderBackupDialog()
|
||||
|
||||
ImGui::Text("Backup File Path:");
|
||||
static char backup_path[512] = "dragonx-backup.txt";
|
||||
static bool s_backup_confirm_overwrite = false;
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##backuppath", backup_path, sizeof(backup_path));
|
||||
if (ImGui::InputText("##backuppath", backup_path, sizeof(backup_path)))
|
||||
s_backup_confirm_overwrite = false; // path edited — re-check overwrite on next Save
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
@@ -2800,17 +2823,30 @@ void App::renderBackupDialog()
|
||||
|
||||
if (ui::material::StyledButton("Save Backup", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
|
||||
std::string path(backup_path);
|
||||
// Trim surrounding whitespace (a pasted path can carry a trailing newline).
|
||||
while (!path.empty() && (path.front()==' '||path.front()=='\t'||path.front()=='\n'||path.front()=='\r')) path.erase(path.begin());
|
||||
while (!path.empty() && (path.back()==' '||path.back()=='\t'||path.back()=='\n'||path.back()=='\r')) path.pop_back();
|
||||
if (!path.empty()) {
|
||||
backupWallet(path, [this](bool success, const std::string& msg) {
|
||||
backup_success_ = success;
|
||||
backup_status_ = msg;
|
||||
});
|
||||
std::error_code ec;
|
||||
if (!s_backup_confirm_overwrite && std::filesystem::exists(path, ec)) {
|
||||
// Don't clobber an existing file (possibly a good earlier backup) without confirmation.
|
||||
s_backup_confirm_overwrite = true;
|
||||
backup_success_ = false;
|
||||
backup_status_ = "A file already exists there — click Save Backup again to overwrite it.";
|
||||
} else {
|
||||
s_backup_confirm_overwrite = false;
|
||||
backupWallet(path, [this](bool success, const std::string& msg) {
|
||||
backup_success_ = success;
|
||||
backup_status_ = msg;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) {
|
||||
show_backup_ = false;
|
||||
backup_status_.clear();
|
||||
s_backup_confirm_overwrite = false;
|
||||
}
|
||||
|
||||
ui::material::EndOverlayDialog();
|
||||
|
||||
Reference in New Issue
Block a user