fix: Tier-2 mediums — input trims, confirmations, amount normalization
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<char>(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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user