From 25ea55c25d7ff3e51752dfc3ea6817e7a1562e4c Mon Sep 17 00:00:00 2001 From: Luke Dashjr Date: Wed, 8 May 2019 21:34:13 +0000 Subject: [PATCH] GUI: Add a warning prompt when sending to an already-used address --- src/qt/sendcoinsdialog.cpp | 71 +++++++++++++++++++++++++++++++++++++ src/qt/sendcoinsdialog.h | 3 +- src/qt/sendcoinsentry.cpp | 5 +++ src/qt/sendcoinsentry.h | 1 + src/qt/test/wallettests.cpp | 44 ++++++++++++++--------- 5 files changed, 107 insertions(+), 17 deletions(-) diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 37dc310e7b..660e95a2b4 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -482,6 +482,77 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked) if (!PrepareSendText(question_string, informative_text, detailed_text)) return; assert(m_current_transaction); + bool have_warning = false; + for (int i = 0; i < ui->entries->count(); ++i) { + SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); + if (entry && entry->hasPaytoWarning()) { + have_warning = true; + break; + } + } + if (have_warning) { + auto recipients = m_current_transaction->getRecipients(); + struct prior_usage_info_t { + CAmount total_amount{0}; + int num_txs{0}; + qint64 tx_time_oldest; + qint64 tx_time_newest; + }; + QMap prior_usage_info; + { + QStringList addresses; + for (const auto& recipient : recipients) { + addresses.append(recipient.address); + } + model->findAddressUsage(addresses, [&prior_usage_info](const QString& address, const interfaces::WalletTx& wtx, uint32_t output_index){ + auto& info = prior_usage_info[address]; + info.total_amount += wtx.tx->vout[output_index].nValue; + ++info.num_txs; + if (info.num_txs == 1 || wtx.time < info.tx_time_oldest) { + info.tx_time_oldest = wtx.time; + } + if (info.num_txs == 1 || wtx.time > info.tx_time_newest) { + info.tx_time_newest = wtx.time; + } + }); + } + + QString reuse_question, reuse_details; + if (recipients.size() > 1) { + reuse_question = tr("You've already paid some of these addresses."); + } else { + reuse_question = tr("You've already paid this address."); + } + + for (const auto& rcp : recipients) { + if (!prior_usage_info.contains(rcp.address)) continue; + if (!reuse_details.isEmpty()) reuse_details.append("\n\n"); + const auto& rcp_prior_usage_info = prior_usage_info.value(rcp.address); + const QString label_and_address = rcp.label.isEmpty() ? rcp.address : (QString("'") + rcp.label + "' (" + rcp.address + ")"); + if (rcp_prior_usage_info.num_txs == 1) { + //: %1 is an amount (eg, "1 BTC"); %2 is a Bitcoin address and its label; %3 is a date (eg, "2019-05-08") + reuse_details.append(tr("Sent %1 to %2 on %3").arg(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp_prior_usage_info.total_amount), label_and_address, GUIUtil::dateStr(rcp_prior_usage_info.tx_time_newest))); + } else { + //: %1 is an amount (eg, "1 BTC"); %2 is a Bitcoin address and its label; %3 is the number of transactions; %4 and %5 are dates (eg, "2019-05-08"), earlier first + reuse_details.append(tr("Sent %1 to %2 across %3 transactions from %4 through %5").arg(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp_prior_usage_info.total_amount), label_and_address, QString::number(rcp_prior_usage_info.num_txs), GUIUtil::dateStr(rcp_prior_usage_info.tx_time_oldest), GUIUtil::dateStr(rcp_prior_usage_info.tx_time_newest))); + } + } + + reuse_question.append("

"); + reuse_question.append(tr("Bitcoin addresses are intended to only be used once, for a single payment. Sending to the same address again will harm the recipient's security, as well as the privacy of all Bitcoin users!")); + reuse_question.append(""); + + SendConfirmationDialog confirmation_dialog(tr("Already paid"), reuse_question, "", reuse_details, ADDRESS_REUSE_OVERRIDE_DELAY, /*enable_send=*/true, /*always_show_unsigned=*/false, this); + confirmation_dialog.setIcon(QMessageBox::Warning); + confirmation_dialog.confirmButtonText = tr("Override"); + confirmation_dialog.m_yes_button = QMessageBox::Ignore; + confirmation_dialog.m_cancel_button = QMessageBox::Ok; + if (static_cast(confirmation_dialog.exec()) == QMessageBox::Cancel) { + fNewRecipientAllowed = true; + return; + } + } + const QString confirmation = tr("Confirm send coins"); const bool enable_send{!model->wallet().privateKeysDisabled() || model->wallet().hasExternalSigner()}; const bool always_show_unsigned{model->getOptionsModel()->getEnablePSBTControls()}; diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h index c1aa9f98ff..872e4eba2b 100644 --- a/src/qt/sendcoinsdialog.h +++ b/src/qt/sendcoinsdialog.h @@ -123,6 +123,7 @@ Q_SIGNALS: #define SEND_CONFIRM_DELAY 3 +#define ADDRESS_REUSE_OVERRIDE_DELAY 10 class SendConfirmationDialog : public QMessageBox { @@ -130,6 +131,7 @@ class SendConfirmationDialog : public QMessageBox public: bool m_delete_on_close{false}; + QString confirmButtonText{tr("Send")}; QMessageBox::StandardButton m_yes_button{QMessageBox::Yes}; QMessageBox::StandardButton m_cancel_button{QMessageBox::Cancel}; @@ -147,7 +149,6 @@ private: QAbstractButton *m_psbt_button; QTimer countDownTimer; int secDelay; - QString confirmButtonText{tr("Send")}; bool m_enable_save; bool m_enable_send; QString m_psbt_button_text{tr("Create Unsigned")}; diff --git a/src/qt/sendcoinsentry.cpp b/src/qt/sendcoinsentry.cpp index 3d83d4bbfb..a310f02765 100644 --- a/src/qt/sendcoinsentry.cpp +++ b/src/qt/sendcoinsentry.cpp @@ -150,6 +150,11 @@ bool SendCoinsEntry::validate(interfaces::Node& node) return retval; } +bool SendCoinsEntry::hasPaytoWarning() const +{ + return ui->payTo->hasWarning(); +} + SendCoinsRecipient SendCoinsEntry::getValue() { recipient.address = ui->payTo->text(); diff --git a/src/qt/sendcoinsentry.h b/src/qt/sendcoinsentry.h index 0edc0d1203..ccb7d956a9 100644 --- a/src/qt/sendcoinsentry.h +++ b/src/qt/sendcoinsentry.h @@ -33,6 +33,7 @@ public: void setModel(WalletModel *model); bool validate(interfaces::Node& node); + bool hasPaytoWarning() const; SendCoinsRecipient getValue(); /** Return whether the entry is still empty and unedited */ diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index 936a6d2742..311b2bcf26 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -58,34 +58,46 @@ using wallet::WalletRescanReserver; namespace { -//! Press "Yes" or "Cancel" buttons in modal send confirmation dialog. -void ConfirmSend(QString* text = nullptr, QMessageBox::StandardButton confirm_type = QMessageBox::Yes) +void ConfirmSendAttempt(QString* text, QMessageBox::StandardButton confirm_type) { - QTimer::singleShot(0, [text, confirm_type]() { for (QWidget* widget : QApplication::topLevelWidgets()) { if (widget->inherits("SendConfirmationDialog")) { SendConfirmationDialog* dialog = qobject_cast(widget); if (text) *text = dialog->text(); QAbstractButton* button = dialog->button(confirm_type); - if (!button) { - const QMessageBox::ButtonRole confirm_role = [confirm_type](){ - switch (confirm_type) { - case QMessageBox::Yes: return QMessageBox::YesRole; - case QMessageBox::Cancel: return QMessageBox::NoRole; - default: assert(0); - } - }(); - for (QAbstractButton* maybe_button : dialog->buttons()) { - if (dialog->buttonRole(maybe_button) == confirm_role) { - button = maybe_button; - break; - } + const QMessageBox::ButtonRole confirm_role = [confirm_type, button](){ + if (button) return QMessageBox::InvalidRole; + switch (confirm_type) { + case QMessageBox::Yes: return QMessageBox::YesRole; + case QMessageBox::Cancel: return QMessageBox::NoRole; + default: return QMessageBox::InvalidRole; + } + }(); + for (QAbstractButton* maybe_button : dialog->buttons()) { + if (dialog->buttonRole(maybe_button) == confirm_role) { + button = maybe_button; + } else if (maybe_button->text().startsWith("Override")) { + button = maybe_button; + break; } } button->setEnabled(true); button->click(); + if (!button->text().startsWith("Override")) return; } } + + // Try again + QTimer::singleShot(0, [text, confirm_type]{ + ConfirmSendAttempt(text, confirm_type); + }); +} + +//! Press "Yes" or "Cancel" buttons in modal send confirmation dialog. +void ConfirmSend(QString* text = nullptr, QMessageBox::StandardButton confirm_type = QMessageBox::Yes) +{ + QTimer::singleShot(0, [text, confirm_type]{ + ConfirmSendAttempt(text, confirm_type); }); }