Merge g562 via wallet_warn_reuse_gui

This commit is contained in:
Luke Dashjr 2024-06-21 19:28:12 +00:00
commit 00cd83d47c
14 changed files with 309 additions and 24 deletions

View File

@ -129,6 +129,9 @@ public:
//! 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;

View File

@ -26,6 +26,8 @@ static const bool DEFAULT_SPLASHSCREEN = true;
/* Invalid field background style */
#define STYLE_INVALID "border: 3px solid #FF8080"
/* "Warning" field background style */
#define STYLE_INCORRECT "border: 3px solid #FFFF80"
/* Transaction list -- unconfirmed transaction */
#define COLOR_UNCONFIRMED QColor(128, 128, 128)

View File

@ -91,9 +91,19 @@ using namespace std::chrono_literals;
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)

View File

@ -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);

View File

@ -13,22 +13,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
{
@ -82,13 +94,14 @@ 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)
@ -96,7 +109,7 @@ void QValidatedLineEdit::checkValidity()
QString address = text();
int pos = 0;
if (checkValidator->validate(address, pos) == QValidator::Acceptable)
setValid(true);
setValid(true, has_warning);
else
setValid(false);
}
@ -126,3 +139,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;
}

View File

@ -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;
@ -27,10 +30,12 @@ protected:
private:
bool valid{true};
const QValidator* checkValidator{nullptr};
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:
@ -39,6 +44,7 @@ Q_SIGNALS:
private Q_SLOTS:
void markValid();
void checkValidity();
bool checkWarning() const;
};
#endif // BITCOIN_QT_QVALIDATEDLINEEDIT_H

View File

@ -483,11 +483,82 @@ 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");
const bool enable_send{!model->wallet().privateKeysDisabled() || model->wallet().hasExternalSigner()};
const bool always_show_unsigned{model->getOptionsModel()->getEnablePSBTControls()};
auto confirmationDialog = new SendConfirmationDialog(confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, enable_send, always_show_unsigned, 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());
@ -1056,30 +1127,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()

View File

@ -123,12 +123,18 @@ Q_SIGNALS:
#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);
/* Returns QMessageBox::Cancel, QMessageBox::Yes when "Send" is
clicked and QMessageBox::Save when "Create Unsigned" is clicked. */
@ -143,7 +149,7 @@ 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")};
};

View File

@ -71,6 +71,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);
@ -144,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();

View File

@ -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 */

View File

@ -58,19 +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<SendConfirmationDialog*>(widget);
if (text) *text = dialog->text();
QAbstractButton* button = dialog->button(confirm_type);
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);
});
}

View File

@ -151,6 +151,22 @@ bool WalletModel::validateAddress(const QString& address) const
return IsValidDestinationString(address.toStdString());
}
bool WalletModel::checkAddressForUsage(const std::vector<std::string>& addresses) const
{
return m_wallet->checkAddressForUsage(addresses);
}
bool WalletModel::findAddressUsage(const QStringList& addresses, std::function<void(const QString&, const interfaces::WalletTx&, uint32_t)> callback) const
{
std::vector<std::string> std_addresses;
for (const auto& address : addresses) {
std_addresses.push_back(address.toStdString());
}
return m_wallet->findAddressUsage(std_addresses, [&callback](const std::string& address, const interfaces::WalletTx& wtx, uint32_t output_index){
callback(QString::fromStdString(address), wtx, output_index);
});
}
WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransaction &transaction, const CCoinControl& coinControl)
{
CAmount total = 0;
@ -520,7 +536,7 @@ bool WalletModel::bumpFee(uint256 hash, uint256& new_hash)
const bool enable_send{!wallet().privateKeysDisabled() || wallet().hasExternalSigner()};
const bool always_show_unsigned{getOptionsModel()->getEnablePSBTControls()};
auto confirmationDialog = new SendConfirmationDialog(tr("Confirm fee bump"), questionString, "", "", SEND_CONFIRM_DELAY, enable_send, always_show_unsigned, nullptr);
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());
@ -627,3 +643,18 @@ CAmount WalletModel::getAvailableBalance(const CCoinControl* control)
// Fetch balance from the wallet, taking into account the selected coins
return wallet().getAvailableBalance(*control);
}
BitcoinAddressUnusedInWalletValidator::BitcoinAddressUnusedInWalletValidator(const WalletModel& wallet_model, QObject *parent) :
QValidator(parent),
m_wallet_model(wallet_model)
{
}
QValidator::State BitcoinAddressUnusedInWalletValidator::validate(QString &input, int &pos) const
{
Q_UNUSED(pos);
if (m_wallet_model.checkAddressForUsage(std::vector<std::string>{input.toStdString()})) {
return QValidator::Invalid;
}
return QValidator::Acceptable;
}

View File

@ -6,15 +6,18 @@
#define BITCOIN_QT_WALLETMODEL_H
#include <key.h>
#include <primitives/transaction.h>
#include <qt/walletmodeltransaction.h>
#include <interfaces/wallet.h>
#include <support/allocators/secure.h>
#include <string>
#include <vector>
#include <QObject>
#include <QValidator>
enum class OutputType;
@ -81,6 +84,8 @@ public:
// Check address for validity
bool validateAddress(const QString& address) const;
bool checkAddressForUsage(const std::vector<std::string>& addresses) const;
bool findAddressUsage(const QStringList& addresses, std::function<void(const QString&, const interfaces::WalletTx&, uint32_t)> callback) const;
// Return status record for SendCoins, contains error id + information
struct SendCoinsReturn
@ -238,4 +243,16 @@ public Q_SLOTS:
void pollBalanceChanged();
};
class BitcoinAddressUnusedInWalletValidator : public QValidator
{
Q_OBJECT
const WalletModel& m_wallet_model;
public:
explicit BitcoinAddressUnusedInWalletValidator(const WalletModel&, QObject *parent=nullptr);
State validate(QString &input, int &pos) const override;
};
#endif // BITCOIN_QT_WALLETMODEL_H

View File

@ -8,6 +8,7 @@
#include <consensus/amount.h>
#include <interfaces/chain.h>
#include <interfaces/handler.h>
#include <key_io.h>
#include <policy/fees.h>
#include <primitives/transaction.h>
#include <rpc/server.h>
@ -30,6 +31,7 @@
#include <wallet/wallet.h>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
@ -49,6 +51,16 @@ using interfaces::WalletTxOut;
using interfaces::WalletTxStatus;
using interfaces::WalletValueMap;
std::set<CScript> AddressesToKeys(std::vector<std::string> addresses)
{
std::set<CScript> keys;
for (const auto& address : addresses) {
CScript scriptPubKey = GetScriptForDestination(DecodeDestination(address));
keys.insert(scriptPubKey);
}
return keys;
}
namespace wallet {
// All members of the classes in this namespace are intentionally public, as the
// classes themselves are private.
@ -252,6 +264,23 @@ public:
LOCK(m_wallet->cs_wallet);
return m_wallet->DisplayAddress(dest);
}
bool checkAddressForUsage(const std::vector<std::string>& addresses) const override
{
LOCK(m_wallet->cs_wallet);
return m_wallet->FindScriptPubKeyUsed(AddressesToKeys(addresses));
}
bool findAddressUsage(const std::vector<std::string>& addresses, std::function<void(const std::string&, const WalletTx&, uint32_t)> callback) const override
{
LOCK(m_wallet->cs_wallet);
return m_wallet->FindScriptPubKeyUsed(AddressesToKeys(addresses), [&callback, this](const CWalletTx& wtx, uint32_t output_index){
CTxDestination dest;
bool success = ExtractDestination(wtx.tx->vout[output_index].scriptPubKey, dest);
assert(success); // It shouldn't be possible to end up here with anything unrecognised
std::string address = EncodeDestination(dest);
WalletTx interface_wtx = MakeWalletTx(*m_wallet, wtx);
callback(address, interface_wtx, output_index);
});
}
bool lockCoin(const COutPoint& output, const bool write_to_db) override
{
LOCK(m_wallet->cs_wallet);