Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ class Wallet
//! Display address on external signer
virtual bool displayAddress(const CTxDestination& dest) = 0;

virtual bool checkAddressForUsage(const std::vector<std::string>& addresses) const = 0;
virtual bool findAddressUsage(const std::vector<std::string>& addresses, std::function<void(const std::string&, const WalletTx&, uint32_t)> callback) const = 0;

//! Lock coin.
virtual bool lockCoin(const COutPoint& output, const bool write_to_db) = 0;

Expand Down
2 changes: 2 additions & 0 deletions src/qt/guiconstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ static const bool DEFAULT_SPLASHSCREEN = true;

/* Invalid field background style */
#define STYLE_INVALID "background:#FF8080"
/* "Warning" field background style */
#define STYLE_INCORRECT "background:#FFFF80"

/* Transaction list -- unconfirmed transaction */
#define COLOR_UNCONFIRMED QColor(128, 128, 128)
Expand Down
12 changes: 11 additions & 1 deletion src/qt/guiutil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,19 @@ void ForceActivation();

namespace GUIUtil {

QString dateStr(const QDate &date)
{
return QLocale::system().toString(date, QLocale::ShortFormat);
}

QString dateStr(qint64 nTime)
{
return dateStr(QDateTime::fromSecsSinceEpoch(nTime).date());
}

QString dateTimeStr(const QDateTime &date)
{
return QLocale::system().toString(date.date(), QLocale::ShortFormat) + QString(" ") + date.toString("hh:mm");
return dateStr(date.date()) + QString(" ") + date.toString("hh:mm");
}

QString dateTimeStr(qint64 nTime)
Expand Down
2 changes: 2 additions & 0 deletions src/qt/guiutil.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ namespace GUIUtil
constexpr auto dialog_flags = Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint;

// Create human-readable string from date
QString dateStr(const QDate &datetime);
QString dateStr(qint64 nTime);
QString dateTimeStr(const QDateTime &datetime);
QString dateTimeStr(qint64 nTime);

Expand Down
48 changes: 43 additions & 5 deletions src/qt/qvalidatedlineedit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,34 @@ QValidatedLineEdit::QValidatedLineEdit(QWidget *parent) :
connect(this, &QValidatedLineEdit::textChanged, this, &QValidatedLineEdit::markValid);
}

QValidatedLineEdit::~QValidatedLineEdit()
{
delete m_warning_validator;
}

void QValidatedLineEdit::setText(const QString& text)
{
QLineEdit::setText(text);
checkValidity();
}

void QValidatedLineEdit::setValid(bool _valid)
void QValidatedLineEdit::setValid(bool _valid, bool with_warning)
{
if(_valid == this->valid)
{
return;
if (with_warning == m_has_warning || !valid) {
return;
}
}

if(_valid)
{
setStyleSheet("");
m_has_warning = with_warning;
if (with_warning) {
setStyleSheet("QValidatedLineEdit { " STYLE_INCORRECT "}");
} else {
setStyleSheet("");
}
}
else
{
Expand Down Expand Up @@ -84,21 +96,22 @@ void QValidatedLineEdit::setEnabled(bool enabled)

void QValidatedLineEdit::checkValidity()
{
const bool has_warning = checkWarning();
if (text().isEmpty())
{
setValid(true);
}
else if (hasAcceptableInput())
{
setValid(true);
setValid(true, has_warning);

// Check contents on focus out
if (checkValidator)
{
QString address = text();
int pos = 0;
if (checkValidator->validate(address, pos) == QValidator::Acceptable)
setValid(true);
setValid(true, has_warning);
else
setValid(false);
}
Expand Down Expand Up @@ -128,3 +141,28 @@ bool QValidatedLineEdit::isValid()

return valid;
}

void QValidatedLineEdit::setWarningValidator(const QValidator *v)
{
delete m_warning_validator;
m_warning_validator = v;
checkValidity();
}

bool QValidatedLineEdit::checkWarning() const
{
if (m_warning_validator && !text().isEmpty()) {
QString address = text();
int pos = 0;
if (m_warning_validator->validate(address, pos) != QValidator::Acceptable) {
return true;
}
}

return false;
}

bool QValidatedLineEdit::hasWarning() const
{
return m_has_warning;
}
8 changes: 7 additions & 1 deletion src/qt/qvalidatedlineedit.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ class QValidatedLineEdit : public QLineEdit

public:
explicit QValidatedLineEdit(QWidget *parent);
~QValidatedLineEdit();
void clear();
void setCheckValidator(const QValidator *v);
bool isValid();
void setWarningValidator(const QValidator *);
bool hasWarning() const;

protected:
void focusInEvent(QFocusEvent *evt) override;
Expand All @@ -27,10 +30,12 @@ class QValidatedLineEdit : public QLineEdit
private:
bool valid;
const QValidator *checkValidator;
bool m_has_warning{false};
const QValidator *m_warning_validator{nullptr};

public Q_SLOTS:
void setText(const QString&);
void setValid(bool valid);
void setValid(bool valid, bool with_warning=false);
void setEnabled(bool enabled);

Q_SIGNALS:
Expand All @@ -39,6 +44,7 @@ public Q_SLOTS:
private Q_SLOTS:
void markValid();
void checkValidity();
bool checkWarning() const;
};

#endif // BITCOIN_QT_QVALIDATEDLINEEDIT_H
126 changes: 114 additions & 12 deletions src/qt/sendcoinsdialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -410,9 +410,80 @@ 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<SendCoinsEntry*>(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<QString, prior_usage_info_t> 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("<br /><br /><span style='font-size:10pt;'>");
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("</span>");

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<QMessageBox::StandardButton>(confirmation_dialog.exec()) == QMessageBox::Cancel) {
fNewRecipientAllowed = true;
return;
}
}

const QString confirmation = tr("Confirm send coins");
auto confirmationDialog = new SendConfirmationDialog(confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, !model->wallet().privateKeysDisabled(), model->getOptionsModel()->getEnablePSBTControls(), this);
confirmationDialog->setAttribute(Qt::WA_DeleteOnClose);
confirmationDialog->m_delete_on_close = true;
// TODO: Replace QDialog::exec() with safer QDialog::show().
const auto retval = static_cast<QMessageBox::StandardButton>(confirmationDialog->exec());

Expand Down Expand Up @@ -1045,30 +1116,61 @@ void SendCoinsDialog::coinControlUpdateLabels()
}

SendConfirmationDialog::SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text, const QString& detailed_text, int _secDelay, bool enable_send, bool always_show_unsigned, QWidget* parent)
: QMessageBox(parent), secDelay(_secDelay), m_enable_send(enable_send)
: QMessageBox(parent), secDelay(_secDelay), m_enable_save(always_show_unsigned || !enable_send), m_enable_send(enable_send)
{
setIcon(QMessageBox::Question);
setWindowTitle(title); // On macOS, the window title is ignored (as required by the macOS Guidelines).
setText(text);
setInformativeText(informative_text);
setDetailedText(detailed_text);
setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
if (always_show_unsigned || !enable_send) addButton(QMessageBox::Save);
setDefaultButton(QMessageBox::Cancel);
yesButton = button(QMessageBox::Yes);
}

int SendConfirmationDialog::exec()
{
setStandardButtons(m_yes_button | m_cancel_button);

yesButton = button(m_yes_button);
QAbstractButton * const cancel_button_obj = button(m_cancel_button);

if (m_yes_button != QMessageBox::Yes || m_cancel_button != QMessageBox::Cancel) {
// We need to ensure the buttons have Yes/No roles, or they'll get ordered weird
// But only do it for customised yes/cancel buttons, so simple code can check results simply too
removeButton(cancel_button_obj);
addButton(cancel_button_obj, QMessageBox::NoRole);
setEscapeButton(cancel_button_obj);

removeButton(yesButton);
addButton(yesButton, QMessageBox::YesRole);
}

if (m_enable_save) addButton(QMessageBox::Save);

setDefaultButton(m_cancel_button);

if (confirmButtonText.isEmpty()) {
confirmButtonText = yesButton->text();
}
m_psbt_button = button(QMessageBox::Save);
updateButtons();
connect(&countDownTimer, &QTimer::timeout, this, &SendConfirmationDialog::countDown);
}

int SendConfirmationDialog::exec()
{
updateButtons();
connect(&countDownTimer, &QTimer::timeout, this, &SendConfirmationDialog::countDown);
countDownTimer.start(1s);
return QMessageBox::exec();

QMessageBox::exec();

int rv;
const auto clicked_button = clickedButton();
if (clicked_button == m_psbt_button) {
rv = QMessageBox::Save;
} else if (clicked_button == yesButton) {
rv = QMessageBox::Yes;
} else {
rv = QMessageBox::Cancel;
}

if (m_delete_on_close) delete this;

return rv;
}

void SendConfirmationDialog::countDown()
Expand Down
8 changes: 7 additions & 1 deletion src/qt/sendcoinsdialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,18 @@ private Q_SLOTS:


#define SEND_CONFIRM_DELAY 3
#define ADDRESS_REUSE_OVERRIDE_DELAY 10

class SendConfirmationDialog : public QMessageBox
{
Q_OBJECT

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};

SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text = "", const QString& detailed_text = "", int secDelay = SEND_CONFIRM_DELAY, bool enable_send = true, bool always_show_unsigned = true, QWidget* parent = nullptr);
int exec() override;

Expand All @@ -128,7 +134,7 @@ private Q_SLOTS:
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")};
};
Expand Down
11 changes: 11 additions & 0 deletions src/qt/sendcoinsentry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ void SendCoinsEntry::setModel(WalletModel *_model)
{
this->model = _model;

if (_model) {
ui->payTo->setWarningValidator(new BitcoinAddressUnusedInWalletValidator(*_model));
} else {
ui->payTo->setWarningValidator(nullptr);
}

if (_model && _model->getOptionsModel())
connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &SendCoinsEntry::updateDisplayUnit);

Expand Down Expand Up @@ -166,6 +172,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();
Expand Down
1 change: 1 addition & 0 deletions src/qt/sendcoinsentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class SendCoinsEntry : public QStackedWidget

void setModel(WalletModel *model);
bool validate(interfaces::Node& node);
bool hasPaytoWarning() const;
SendCoinsRecipient getValue();

/** Return whether the entry is still empty and unedited */
Expand Down
Loading