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:
@@ -1356,16 +1356,24 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
|
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ui::material::TactileButton("Skip##enc", ImVec2(skipW2, btnH2))) {
|
if (ui::material::TactileButton("Skip##enc", ImVec2(skipW2, btnH2))) {
|
||||||
wizard_phase_ = WizardPhase::Done;
|
static bool s_skipEncConfirm = false;
|
||||||
settings_->setWizardCompleted(true);
|
if (!s_skipEncConfirm) {
|
||||||
settings_->save();
|
// Skipping stores private keys UNENCRYPTED — require a confirming second click.
|
||||||
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
s_skipEncConfirm = true;
|
||||||
if (!startEmbeddedDaemon()) {
|
encrypt_status_ = "Continue WITHOUT encryption? Keys will be stored unencrypted — click Skip again to confirm.";
|
||||||
ui::Notifications::instance().warning(
|
} else {
|
||||||
TR("wizard_daemon_start_failed"));
|
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();
|
ImGui::PopStyleVar();
|
||||||
cy += btnH2;
|
cy += btnH2;
|
||||||
|
|||||||
@@ -1741,10 +1741,17 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGuiInputTextFlags_Password);
|
ImGuiInputTextFlags_Password);
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
if (TactileButton(TrId("lite_import", "LiteImportKeyBtn").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
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);
|
std::string liteKey(s_settingsState.lite_import_key);
|
||||||
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
|
while (!liteKey.empty() && (liteKey.front()==' '||liteKey.front()=='\t'||liteKey.front()=='\n'||liteKey.front()=='\r')) liteKey.erase(liteKey.begin());
|
||||||
s_settingsState.lite_backup_status =
|
while (!liteKey.empty() && (liteKey.back()==' '||liteKey.back()=='\t'||liteKey.back()=='\n'||liteKey.back()=='\r')) liteKey.pop_back();
|
||||||
r.ok ? TR("lite_key_imported") : r.error;
|
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()) {
|
if (!s_settingsState.lite_backup_status.empty()) {
|
||||||
|
|||||||
@@ -129,7 +129,13 @@ void AddressBookDialog::render(App* app)
|
|||||||
|
|
||||||
const char* primaryLabel = isEdit ? TR("save") : TR("add");
|
const char* primaryLabel = isEdit ? TR("save") : TR("add");
|
||||||
if (material::StyledButton(primaryLabel, ImVec2(actionW, 0), S.resolveFont(actionBtn.font))) {
|
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 (isEdit) {
|
||||||
if (getAddressBook().updateEntry(s_selected_index, entry)) {
|
if (getAddressBook().updateEntry(s_selected_index, entry)) {
|
||||||
Notifications::instance().success(TR("address_book_updated"));
|
Notifications::instance().success(TR("address_book_updated"));
|
||||||
@@ -183,11 +189,19 @@ void AddressBookDialog::render(App* app)
|
|||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
static int s_confirmDeleteIdx = -1;
|
||||||
if (material::StyledButton(TR("delete"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
if (material::StyledButton(TR("delete"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||||
if (has_selection) {
|
if (has_selection) {
|
||||||
book.removeEntry(s_selected_index);
|
if (s_confirmDeleteIdx == s_selected_index) {
|
||||||
s_selected_index = -1;
|
book.removeEntry(s_selected_index);
|
||||||
Notifications::instance().success(TR("address_book_deleted"));
|
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(),
|
std::transform(first.begin(), first.end(), first.begin(),
|
||||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
[](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); };
|
auto add = [this](const std::string& l, ConsoleChannel c) { addLine(l, c); };
|
||||||
if (first == "clear" || first == "cls") {
|
if (first == "clear" || first == "cls") {
|
||||||
// View-only clear — NEVER forwarded (the lite backend's `clear` wipes tx history).
|
// 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);
|
exec.printHelp(add);
|
||||||
} else if (first == "quit" || first == "exit") {
|
} else if (first == "quit" || first == "exit") {
|
||||||
addLine(TR("console_quit_note"), ConsoleChannel::Info);
|
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()) {
|
} else if (!exec.isReady()) {
|
||||||
addLine(TR("console_not_connected"), ConsoleChannel::Error);
|
addLine(TR("console_not_connected"), ConsoleChannel::Error);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -178,7 +178,13 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
ImGui::InputTextWithHint("##LiteAddLabel", TR("lite_net_add_label_hint"), s_addLabel, sizeof(s_addLabel));
|
ImGui::InputTextWithHint("##LiteAddLabel", TR("lite_net_add_label_hint"), s_addLabel, sizeof(s_addLabel));
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (TactileButton(TR("lite_net_add"), ImVec2(addBtnW, 0))) {
|
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)) {
|
if (!wallet::isLiteServerUrlUsable(url)) {
|
||||||
s_addError = TR("lite_net_invalid_url");
|
s_addError = TR("lite_net_invalid_url");
|
||||||
} else {
|
} else {
|
||||||
@@ -188,7 +194,7 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
if (!exists) {
|
if (!exists) {
|
||||||
config::Settings::LiteServerPreference p;
|
config::Settings::LiteServerPreference p;
|
||||||
p.url = url;
|
p.url = url;
|
||||||
p.label = s_addLabel[0] ? std::string(s_addLabel) : url;
|
p.label = !addLabel.empty() ? addLabel : url;
|
||||||
p.enabled = true;
|
p.enabled = true;
|
||||||
servers.push_back(p);
|
servers.push_back(p);
|
||||||
st->setLiteServers(servers);
|
st->setLiteServers(servers);
|
||||||
|
|||||||
@@ -1322,7 +1322,8 @@ void RenderSendTab(App* app)
|
|||||||
// USD input mode — no step buttons (step=0)
|
// USD input mode — no step buttons (step=0)
|
||||||
ImGui::PushItemWidth(amtInputW);
|
ImGui::PushItemWidth(amtInputW);
|
||||||
if (ImGui::InputDouble("##AmountUSD", &s_usd_amount, 0, 0, "$%.2f")) {
|
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)
|
// 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)
|
// DRGX input mode — no step buttons (step=0)
|
||||||
ImGui::PushItemWidth(amtInputW);
|
ImGui::PushItemWidth(amtInputW);
|
||||||
if (ImGui::InputDouble("##Amount", &s_amount, 0, 0, "%.8f")) {
|
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)
|
if (market.price_usd > 0)
|
||||||
s_usd_amount = s_amount * market.price_usd;
|
s_usd_amount = s_amount * market.price_usd;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ void ValidateAddressDialog::render(App* app)
|
|||||||
s_error_message.clear();
|
s_error_message.clear();
|
||||||
|
|
||||||
std::string address(s_address_input);
|
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
|
// Determine if z-address or t-address
|
||||||
bool is_zaddr = !address.empty() && address[0] == 'z';
|
bool is_zaddr = !address.empty() && address[0] == 'z';
|
||||||
|
|||||||
Reference in New Issue
Block a user