From 65bb98cd09a2439dacfeba2c4288f77ec05d68c2 Mon Sep 17 00:00:00 2001 From: DanS Date: Sun, 5 Jul 2026 13:18:57 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20Tier-2=20mediums=20=E2=80=94=20input=20t?= =?UTF-8?q?rims,=20confirmations,=20amount=20normalization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More robustness fixes from the audit (the import-key pattern, lower severity): - Trim pasted input before use/validation: network add-server URL/label, lite-wallet key import (+ block empty), validate-address, address-book entry. - Confirmations for destructive/irreversible actions: - Address-book Delete now needs a confirming second click (no undo). - Console `stop` needs a confirming second `stop` (it shuts down the node). - Wizard "Skip" encryption needs a confirming second click (keys stored unencrypted). - Send amount: normalize to 8dp (satoshi precision) on both the DRGX and USD inputs so digits past 8dp aren't silently dropped between the preview/review and what's actually sent. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app_wizard.cpp | 24 ++++++++++++++-------- src/ui/pages/settings_page.cpp | 15 ++++++++++---- src/ui/windows/address_book_dialog.cpp | 22 ++++++++++++++++---- src/ui/windows/console_tab.cpp | 13 ++++++++++++ src/ui/windows/network_tab.cpp | 10 +++++++-- src/ui/windows/send_tab.cpp | 6 +++++- src/ui/windows/validate_address_dialog.cpp | 5 ++++- 7 files changed, 75 insertions(+), 20 deletions(-) diff --git a/src/app_wizard.cpp b/src/app_wizard.cpp index 60ecba4..b2993e5 100644 --- a/src/app_wizard.cpp +++ b/src/app_wizard.cpp @@ -1356,16 +1356,24 @@ void App::renderFirstRunWizard() { ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); if (ui::material::TactileButton("Skip##enc", ImVec2(skipW2, btnH2))) { - wizard_phase_ = WizardPhase::Done; - settings_->setWizardCompleted(true); - settings_->save(); - if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) { - if (!startEmbeddedDaemon()) { - ui::Notifications::instance().warning( - TR("wizard_daemon_start_failed")); + static bool s_skipEncConfirm = false; + if (!s_skipEncConfirm) { + // Skipping stores private keys UNENCRYPTED — require a confirming second click. + s_skipEncConfirm = true; + encrypt_status_ = "Continue WITHOUT encryption? Keys will be stored unencrypted — click Skip again to confirm."; + } else { + s_skipEncConfirm = false; + wizard_phase_ = WizardPhase::Done; + settings_->setWizardCompleted(true); + settings_->save(); + if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) { + if (!startEmbeddedDaemon()) { + ui::Notifications::instance().warning( + TR("wizard_daemon_start_failed")); + } } + tryConnect(); } - tryConnect(); } ImGui::PopStyleVar(); cy += btnH2; diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 6364dd4..4f2fe71 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -1741,10 +1741,17 @@ void RenderSettingsPage(App* app) { ImGuiInputTextFlags_Password); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton(TrId("lite_import", "LiteImportKeyBtn").c_str(), ImVec2(0, 0), S.resolveFont("button"))) { - const auto r = app->liteWallet()->importKey(s_settingsState.lite_import_key); - sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key)); - s_settingsState.lite_backup_status = - r.ok ? TR("lite_key_imported") : r.error; + std::string liteKey(s_settingsState.lite_import_key); + while (!liteKey.empty() && (liteKey.front()==' '||liteKey.front()=='\t'||liteKey.front()=='\n'||liteKey.front()=='\r')) liteKey.erase(liteKey.begin()); + while (!liteKey.empty() && (liteKey.back()==' '||liteKey.back()=='\t'||liteKey.back()=='\n'||liteKey.back()=='\r')) liteKey.pop_back(); + if (liteKey.empty()) { + s_settingsState.lite_backup_status = "Enter a private key to import."; + } else { + const auto r = app->liteWallet()->importKey(liteKey); + sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key)); + s_settingsState.lite_backup_status = + r.ok ? TR("lite_key_imported") : r.error; + } } if (!s_settingsState.lite_backup_status.empty()) { diff --git a/src/ui/windows/address_book_dialog.cpp b/src/ui/windows/address_book_dialog.cpp index 82051c1..5ebeba8 100644 --- a/src/ui/windows/address_book_dialog.cpp +++ b/src/ui/windows/address_book_dialog.cpp @@ -129,7 +129,13 @@ void AddressBookDialog::render(App* app) const char* primaryLabel = isEdit ? TR("save") : TR("add"); if (material::StyledButton(primaryLabel, ImVec2(actionW, 0), S.resolveFont(actionBtn.font))) { - data::AddressBookEntry entry(s_edit_label, s_edit_address, s_edit_notes); + // Trim the label/address (a pasted address often carries a trailing newline); keep notes as-is. + auto trimAB = [](std::string s) { + while (!s.empty() && (s.front()==' '||s.front()=='\t'||s.front()=='\n'||s.front()=='\r')) s.erase(s.begin()); + while (!s.empty() && (s.back()==' '||s.back()=='\t'||s.back()=='\n'||s.back()=='\r')) s.pop_back(); + return s; + }; + data::AddressBookEntry entry(trimAB(s_edit_label), trimAB(s_edit_address), s_edit_notes); if (isEdit) { if (getAddressBook().updateEntry(s_selected_index, entry)) { Notifications::instance().success(TR("address_book_updated")); @@ -183,11 +189,19 @@ void AddressBookDialog::render(App* app) ImGui::SameLine(); + static int s_confirmDeleteIdx = -1; if (material::StyledButton(TR("delete"), ImVec2(0,0), S.resolveFont(actionBtn.font))) { if (has_selection) { - book.removeEntry(s_selected_index); - s_selected_index = -1; - Notifications::instance().success(TR("address_book_deleted")); + if (s_confirmDeleteIdx == s_selected_index) { + book.removeEntry(s_selected_index); + s_selected_index = -1; + s_confirmDeleteIdx = -1; + Notifications::instance().success(TR("address_book_deleted")); + } else { + // Require a second click to confirm (no undo for a removed contact). + s_confirmDeleteIdx = s_selected_index; + Notifications::instance().warning("Click Delete again to remove this entry."); + } } } diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index 24e0689..3f0a0be 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -1298,6 +1298,9 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s std::transform(first.begin(), first.end(), first.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); } + // 'stop' shuts down the node — require a confirming second 'stop'; any other command clears the pending state. + static bool stopConfirmPending = false; + if (first != "stop") stopConfirmPending = false; auto add = [this](const std::string& l, ConsoleChannel c) { addLine(l, c); }; if (first == "clear" || first == "cls") { // View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history). @@ -1307,6 +1310,16 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s exec.printHelp(add); } else if (first == "quit" || first == "exit") { addLine(TR("console_quit_note"), ConsoleChannel::Info); + } else if (first == "stop") { + if (!stopConfirmPending) { + stopConfirmPending = true; + addLine("'stop' will shut down the node and disconnect the wallet. Type 'stop' again to confirm.", + ConsoleChannel::Warning); + } else { + stopConfirmPending = false; + if (!exec.isReady()) addLine(TR("console_not_connected"), ConsoleChannel::Error); + else exec.submit(cmd); + } } else if (!exec.isReady()) { addLine(TR("console_not_connected"), ConsoleChannel::Error); } else { diff --git a/src/ui/windows/network_tab.cpp b/src/ui/windows/network_tab.cpp index 115064d..6b46ded 100644 --- a/src/ui/windows/network_tab.cpp +++ b/src/ui/windows/network_tab.cpp @@ -178,7 +178,13 @@ void RenderLiteNetworkTab(App* app) ImGui::InputTextWithHint("##LiteAddLabel", TR("lite_net_add_label_hint"), s_addLabel, sizeof(s_addLabel)); ImGui::SameLine(); if (TactileButton(TR("lite_net_add"), ImVec2(addBtnW, 0))) { - std::string url = s_addUrl; + auto trimStr = [](std::string s) { + while (!s.empty() && (s.front()==' '||s.front()=='\t'||s.front()=='\n'||s.front()=='\r')) s.erase(s.begin()); + while (!s.empty() && (s.back()==' '||s.back()=='\t'||s.back()=='\n'||s.back()=='\r')) s.pop_back(); + return s; + }; + std::string url = trimStr(s_addUrl); + std::string addLabel = trimStr(s_addLabel); if (!wallet::isLiteServerUrlUsable(url)) { s_addError = TR("lite_net_invalid_url"); } else { @@ -188,7 +194,7 @@ void RenderLiteNetworkTab(App* app) if (!exists) { config::Settings::LiteServerPreference p; p.url = url; - p.label = s_addLabel[0] ? std::string(s_addLabel) : url; + p.label = !addLabel.empty() ? addLabel : url; p.enabled = true; servers.push_back(p); st->setLiteServers(servers); diff --git a/src/ui/windows/send_tab.cpp b/src/ui/windows/send_tab.cpp index 5bd2298..9cdb322 100644 --- a/src/ui/windows/send_tab.cpp +++ b/src/ui/windows/send_tab.cpp @@ -1322,7 +1322,8 @@ void RenderSendTab(App* app) // USD input mode — no step buttons (step=0) ImGui::PushItemWidth(amtInputW); if (ImGui::InputDouble("##AmountUSD", &s_usd_amount, 0, 0, "$%.2f")) { - s_amount = s_usd_amount / market.price_usd; + // Normalize to 8dp (satoshi precision) so the reviewed/sent DRGX matches the preview. + s_amount = std::round((s_usd_amount / market.price_usd) * 1e8) / 1e8; } // Draw DRGX equivalent inside the input field (right-aligned overlay) { @@ -1343,6 +1344,9 @@ void RenderSendTab(App* app) // DRGX input mode — no step buttons (step=0) ImGui::PushItemWidth(amtInputW); if (ImGui::InputDouble("##Amount", &s_amount, 0, 0, "%.8f")) { + // Normalize to 8dp so digits past satoshi precision aren't silently dropped at send. + s_amount = std::round(s_amount * 1e8) / 1e8; + if (s_amount < 0) s_amount = 0; if (market.price_usd > 0) s_usd_amount = s_amount * market.price_usd; } diff --git a/src/ui/windows/validate_address_dialog.cpp b/src/ui/windows/validate_address_dialog.cpp index 09b5cfe..0864f06 100644 --- a/src/ui/windows/validate_address_dialog.cpp +++ b/src/ui/windows/validate_address_dialog.cpp @@ -80,7 +80,10 @@ void ValidateAddressDialog::render(App* app) s_error_message.clear(); std::string address(s_address_input); - + // Trim whitespace/newlines a pasted address often carries (the daemon would reject it). + while (!address.empty() && (address.front()==' '||address.front()=='\t'||address.front()=='\n'||address.front()=='\r')) address.erase(address.begin()); + while (!address.empty() && (address.back()==' '||address.back()=='\t'||address.back()=='\n'||address.back()=='\r')) address.pop_back(); + // Determine if z-address or t-address bool is_zaddr = !address.empty() && address[0] == 'z';