diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 4b896c11a37..114ee6a80ed 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -96,6 +96,9 @@ class Wallet //! Get public key. virtual bool getPubKey(const CScript& script, const CKeyID& address, CPubKey& pub_key) = 0; + //! Get Output type from a Destination. + virtual OutputType getOutputType(const CTxDestination& dest) = 0; + //! Sign message virtual SigningResult signMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) = 0; diff --git a/src/qt/addressbookpage.cpp b/src/qt/addressbookpage.cpp index 05e58191fce..3bdae4591cb 100644 --- a/src/qt/addressbookpage.cpp +++ b/src/qt/addressbookpage.cpp @@ -28,15 +28,31 @@ class AddressBookSortFilterProxyModel final : public QSortFilterProxyModel { const QString m_type; + const bool m_nestedFilterEnabled; public: - AddressBookSortFilterProxyModel(const QString& type, QObject* parent) + AddressBookSortFilterProxyModel(const QString& type, QObject* parent, bool enableNestedFilter) : QSortFilterProxyModel(parent) , m_type(type) + , m_nestedFilterEnabled(enableNestedFilter) { setDynamicSortFilter(true); setFilterCaseSensitivity(Qt::CaseInsensitive); setSortCaseSensitivity(Qt::CaseInsensitive); + + if (m_nestedFilterEnabled) { + nextedFilterProxyModel.reset(new AddressBookSortFilterProxyModel(type, this, false)); + nextedFilterProxyModel->setSourceModel(this); + } + } + + AddressBookSortFilterProxyModel* nestedProxyModel() const noexcept{ + if (!m_nestedFilterEnabled) return const_cast(this); + return nextedFilterProxyModel.get(); + } + + bool isNestedFilterEnabled() const { + return m_nestedFilterEnabled; } protected: @@ -50,15 +66,25 @@ class AddressBookSortFilterProxyModel final : public QSortFilterProxyModel } auto address = model->index(row, AddressTableModel::Address, parent); + auto addressType = model->index(row, AddressTableModel::Type, parent); #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) const auto pattern = filterRegularExpression(); #else const auto pattern = filterRegExp(); #endif - return (model->data(address).toString().contains(pattern) || - model->data(label).toString().contains(pattern)); + auto filterBy = model->data(address).toString().contains(pattern) || + model->data(label).toString().contains(pattern); + + if (m_type == AddressTableModel::Receive) { + filterBy = filterBy || model->data(addressType).toString().contains(pattern); + } + + return filterBy; } + +private: + std::unique_ptr nextedFilterProxyModel{nullptr}; }; AddressBookPage::AddressBookPage(const PlatformStyle *platformStyle, Mode _mode, Tabs _tab, QWidget *parent) : @@ -99,11 +125,13 @@ AddressBookPage::AddressBookPage(const PlatformStyle *platformStyle, Mode _mode, ui->labelExplanation->setText(tr("These are your Bitcoin addresses for sending payments. Always check the amount and the receiving address before sending coins.")); ui->deleteAddress->setVisible(true); ui->newAddress->setVisible(true); + ui->searchLineEdit->setPlaceholderText("Enter address or label to search"); break; case ReceivingTab: ui->labelExplanation->setText(tr("These are your Bitcoin addresses for receiving payments. Use the 'Create new receiving address' button in the receive tab to create new addresses.\nSigning is only possible with addresses of the type 'legacy'.")); ui->deleteAddress->setVisible(false); ui->newAddress->setVisible(false); + ui->searchLineEdit->setPlaceholderText("Enter address, address type or label to search"); break; } @@ -135,17 +163,20 @@ void AddressBookPage::setModel(AddressTableModel *_model) return; auto type = tab == ReceivingTab ? AddressTableModel::Receive : AddressTableModel::Send; - proxyModel = new AddressBookSortFilterProxyModel(type, this); + proxyModel.reset(new AddressBookSortFilterProxyModel(type, this, true)); proxyModel->setSourceModel(_model); - connect(ui->searchLineEdit, &QLineEdit::textChanged, proxyModel, &QSortFilterProxyModel::setFilterWildcard); + connect(ui->searchLineEdit, &QLineEdit::textChanged, proxyModel.get(), &QSortFilterProxyModel::setFilterWildcard); - ui->tableView->setModel(proxyModel); + ui->tableView->setModel(proxyModel->nestedProxyModel()); ui->tableView->sortByColumn(0, Qt::AscendingOrder); // Set column widths - ui->tableView->horizontalHeader()->setSectionResizeMode(AddressTableModel::Label, QHeaderView::Stretch); + ui->tableView->horizontalHeader()->setSectionResizeMode(AddressTableModel::Label, QHeaderView::ResizeToContents); ui->tableView->horizontalHeader()->setSectionResizeMode(AddressTableModel::Address, QHeaderView::ResizeToContents); + ui->tableView->horizontalHeader()->setSectionResizeMode(AddressTableModel::Type, QHeaderView::ResizeToContents); + // Show the "Address type" column only on the Receiving tab + ui->tableView->setColumnHidden(AddressTableModel::ColumnIndex::Type, (tab == SendingTab)); connect(ui->tableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &AddressBookPage::selectionChanged); @@ -155,6 +186,8 @@ void AddressBookPage::setModel(AddressTableModel *_model) selectionChanged(); this->updateWindowsTitleWithWalletName(); + + this->setupAddressTypeCombo(); } void AddressBookPage::on_copyAddress_clicked() @@ -183,7 +216,7 @@ void AddressBookPage::onEditAction() EditAddressDialog::EditSendingAddress : EditAddressDialog::EditReceivingAddress, this); dlg->setModel(model); - QModelIndex origIndex = proxyModel->mapToSource(indexes.at(0)); + QModelIndex origIndex = proxyModel->nestedProxyModel()->mapToSource(indexes.at(0)); dlg->loadRow(origIndex.row()); GUIUtil::ShowModalDialogAsynchronously(dlg); } @@ -287,8 +320,9 @@ void AddressBookPage::on_exportButton_clicked() CSVModelWriter writer(filename); // name, column, role - writer.setModel(proxyModel); + writer.setModel(proxyModel->nestedProxyModel()); writer.addColumn("Label", AddressTableModel::Label, Qt::EditRole); + writer.addColumn("Address Type", AddressTableModel::Type, Qt::EditRole); writer.addColumn("Address", AddressTableModel::Address, Qt::EditRole); if(!writer.write()) { @@ -310,7 +344,7 @@ void AddressBookPage::contextualMenu(const QPoint &point) void AddressBookPage::selectNewAddress(const QModelIndex &parent, int begin, int /*end*/) { - QModelIndex idx = proxyModel->mapFromSource(model->index(begin, AddressTableModel::Address, parent)); + QModelIndex idx = proxyModel.get()->mapFromSource(model->index(begin, AddressTableModel::Address, parent)); if(idx.isValid() && (idx.data(Qt::EditRole).toString() == newAddressToSelect)) { // Select row of newly created address, once @@ -332,3 +366,50 @@ void AddressBookPage::updateWindowsTitleWithWalletName() } } } + +std::map AddressBookPage::addressTypeTooltipMap() { + return {{OutputType::LEGACY, QObject::tr("Not recommended due to higher fees and less protection against typos.")}, + {OutputType::P2SH_SEGWIT, QObject::tr("An address compatible with older wallets.")}, + {OutputType::BECH32, QObject::tr("Native segwit address (BIP-173). Some old wallets don't support it.")}, + {OutputType::BECH32M, QObject::tr("Bech32m (BIP-350) is an upgrade to Bech32, wallet support is still limited.")}}; +} + +QString AddressBookPage::showAllTypes() const{ + return QObject::tr("All"); +} + +QString AddressBookPage::showAllTypesToolTip() const{ + return QObject::tr("Select an address type to filter by."); +} + +void AddressBookPage::handleAddressTypeChanged(int index) +{ + QString selectedValue = ui->addressType->currentText(); + // If show all types is selected then clear the selected value + // that will be sent to the filter so it shows everything + if (selectedValue == showAllTypes()) selectedValue.clear(); + // Emit a signal with the selected value + Q_EMIT addressTypeChanged(selectedValue); + // Forcing the resize as if it was selected an item with + // shorter content and right after a longer one, the + // columns are not resizing properly, this fixes it + ui->tableView->resizeColumnsToContents(); +} + +void AddressBookPage::initializeAddressTypeCombo() +{ + const auto index = ui->addressType->count(); + ui->addressType->addItem(showAllTypes(), index); + ui->addressType->setItemData(index, showAllTypesToolTip(), Qt::ToolTipRole); + ui->addressType->setCurrentIndex(index); +} + +void AddressBookPage::setupAddressTypeCombo() +{ + this->initializeAddressTypeCombo(); + ui->labelAddressType->setVisible(tab == ReceivingTab); + ui->addressType->setVisible(tab == ReceivingTab); + GUIUtil::AddItemsToAddressTypeCombo(ui->addressType, true, this->addressTypeTooltipMap()); + connect(ui->addressType, qOverload(&QComboBox::currentIndexChanged), this, &AddressBookPage::handleAddressTypeChanged); + connect(this, &AddressBookPage::addressTypeChanged, proxyModel->nestedProxyModel(), &QSortFilterProxyModel::setFilterFixedString); +} diff --git a/src/qt/addressbookpage.h b/src/qt/addressbookpage.h index b649da4ac8f..5cef233f368 100644 --- a/src/qt/addressbookpage.h +++ b/src/qt/addressbookpage.h @@ -5,6 +5,8 @@ #ifndef BITCOIN_QT_ADDRESSBOOKPAGE_H #define BITCOIN_QT_ADDRESSBOOKPAGE_H +#include + #include class AddressBookSortFilterProxyModel; @@ -53,10 +55,17 @@ public Q_SLOTS: Mode mode; Tabs tab; QString returnValue; - AddressBookSortFilterProxyModel *proxyModel; + std::unique_ptr proxyModel{nullptr}; QMenu *contextMenu; QString newAddressToSelect; void updateWindowsTitleWithWalletName(); + void initializeAddressTypeCombo(); + /** Default selected item of the address type combo to display all address types */ + QString showAllTypes() const; + QString showAllTypesToolTip() const; + /** Tooltip for each address type that will be displayed on the combo*/ + std::map addressTypeTooltipMap(); + void setupAddressTypeCombo(); private Q_SLOTS: /** Delete currently selected address entry */ @@ -78,9 +87,16 @@ private Q_SLOTS: void contextualMenu(const QPoint &point); /** New entry/entries were added to address table */ void selectNewAddress(const QModelIndex &parent, int begin, int /*end*/); + /** Address type combo selection changed */ + void handleAddressTypeChanged(int index); Q_SIGNALS: void sendCoins(QString addr); + /** Emitted when the addressType combobox is changed (handled by handleAddressTypeChange). + * This signal is used as a workaround to connect the combobox and the proxy model filter, + * preventing the compiler error "Signal and slot arguments are not compatible."*/ + void addressTypeChanged(const QString &addressType); + }; #endif // BITCOIN_QT_ADDRESSBOOKPAGE_H diff --git a/src/qt/addresstablemodel.cpp b/src/qt/addresstablemodel.cpp index c52ef7cd67d..ce970ad269d 100644 --- a/src/qt/addresstablemodel.cpp +++ b/src/qt/addresstablemodel.cpp @@ -30,10 +30,13 @@ struct AddressTableEntry Type type; QString label; QString address; + QString addressType; AddressTableEntry() = default; - AddressTableEntry(Type _type, const QString &_label, const QString &_address): - type(_type), label(_label), address(_address) {} + AddressTableEntry(Type _type, const QString &_label, const QString &_address, const OutputType &_addresstype): + type(_type), label(_label), address(_address) { + addressType = GUIUtil::outputTypeDescriptionsMap().at(_addresstype).description; + } }; struct AddressTableEntryLessThan @@ -87,7 +90,8 @@ class AddressTablePriv address.purpose, address.is_mine); cachedAddressTable.append(AddressTableEntry(addressType, QString::fromStdString(address.name), - QString::fromStdString(EncodeDestination(address.dest)))); + QString::fromStdString(EncodeDestination(address.dest)), + wallet.getOutputType(address.dest))); } } // std::lower_bound() and std::upper_bound() require our cachedAddressTable list to be sorted in asc order @@ -96,7 +100,7 @@ class AddressTablePriv std::sort(cachedAddressTable.begin(), cachedAddressTable.end(), AddressTableEntryLessThan()); } - void updateEntry(const QString &address, const QString &label, bool isMine, wallet::AddressPurpose purpose, int status) + void updateEntry(const QString &address, const QString &label, bool isMine, wallet::AddressPurpose purpose, int status, const OutputType &addresstype) { // Find address / label in model QList::iterator lower = std::lower_bound( @@ -117,7 +121,7 @@ class AddressTablePriv break; } parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex); - cachedAddressTable.insert(lowerIndex, AddressTableEntry(newEntryType, label, address)); + cachedAddressTable.insert(lowerIndex, AddressTableEntry(newEntryType, label, address, addresstype)); parent->endInsertRows(); break; case CT_UPDATED: @@ -164,7 +168,7 @@ class AddressTablePriv AddressTableModel::AddressTableModel(WalletModel *parent, bool pk_hash_only) : QAbstractTableModel(parent), walletModel(parent) { - columns << tr("Label") << tr("Address"); + columns << tr("Label") << tr("Address Type") << tr("Address"); priv = new AddressTablePriv(this); priv->refreshAddressTable(parent->wallet(), pk_hash_only); } @@ -208,6 +212,8 @@ QVariant AddressTableModel::data(const QModelIndex &index, int role) const } case Address: return rec->address; + case Type: + return rec->addressType; } // no default case, so the compiler can warn about missing cases assert(false); } else if (role == Qt::FontRole) { @@ -216,6 +222,8 @@ QVariant AddressTableModel::data(const QModelIndex &index, int role) const return QFont(); case Address: return GUIUtil::fixedPitchFont(); + case Type: + return QFont(); } // no default case, so the compiler can warn about missing cases assert(false); } else if (role == TypeRole) { @@ -332,11 +340,10 @@ QModelIndex AddressTableModel::index(int row, int column, const QModelIndex &par } } -void AddressTableModel::updateEntry(const QString &address, - const QString &label, bool isMine, wallet::AddressPurpose purpose, int status) +void AddressTableModel::updateEntry(const QString &address, const QString &label, bool isMine, wallet::AddressPurpose purpose, int status, const OutputType &addressType) { // Update address book model from Bitcoin core - priv->updateEntry(address, label, isMine, purpose, status); + priv->updateEntry(address, label, isMine, purpose, status, addressType); } QString AddressTableModel::addRow(const QString &type, const QString &label, const QString &address, const OutputType address_type) diff --git a/src/qt/addresstablemodel.h b/src/qt/addresstablemodel.h index 44808364ec9..79e8416b92e 100644 --- a/src/qt/addresstablemodel.h +++ b/src/qt/addresstablemodel.h @@ -35,7 +35,8 @@ class AddressTableModel : public QAbstractTableModel enum ColumnIndex { Label = 0, /**< User specified label */ - Address = 1 /**< Bitcoin address */ + Type = 1, /**< Address type */ + Address = 2 /**< Bitcoin address */ }; enum RoleIndex { @@ -104,8 +105,7 @@ class AddressTableModel : public QAbstractTableModel public Q_SLOTS: /* Update address list from core. */ - void updateEntry(const QString &address, const QString &label, bool isMine, wallet::AddressPurpose purpose, int status); - + void updateEntry(const QString &address, const QString &label, bool isMine, wallet::AddressPurpose purpose, int status, const OutputType &address_type); friend class AddressTablePriv; }; diff --git a/src/qt/forms/addressbookpage.ui b/src/qt/forms/addressbookpage.ui index 7ac216286c8..c751d257aa0 100644 --- a/src/qt/forms/addressbookpage.ui +++ b/src/qt/forms/addressbookpage.ui @@ -109,6 +109,16 @@ + + + + Show address type: + + + + + + diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index eb9c65caf28..ee02609e7ee 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -87,6 +88,14 @@ using namespace std::chrono_literals; namespace GUIUtil { +std::map outputTypeDescriptionsMap(){ + return { + {OutputType::LEGACY, {QObject::tr("Base58 (Legacy)"), false}}, + {OutputType::P2SH_SEGWIT, {QObject::tr("Base58 (P2SH-SegWit)"), false}}, + {OutputType::BECH32, {QObject::tr("Bech32 (SegWit)"), false}}, + {OutputType::BECH32M, {QObject::tr("Bech32m (Taproot)"), true}}}; +} + QString dateTimeStr(const QDateTime &date) { return QLocale::system().toString(date.date(), QLocale::ShortFormat) + QString(" ") + date.toString("hh:mm"); @@ -1002,4 +1011,31 @@ void ShowModalDialogAsynchronously(QDialog* dialog) dialog->show(); } +void AddItemsToAddressTypeCombo(QComboBox* addressType, bool taprootEnabled, const std::map& outputTypeTooltipsMap, std::optional defaultType) +{ + auto add_address_type = [&](OutputType type, QString description) { + const auto index{addressType->count()}; + // getting the tooltip (provided by the caller) that will be displayed on the item in the combo + QString tooltip{}; + auto it{outputTypeTooltipsMap.find(type)}; + // if can't find it no tooltip will be displayed + if (it != outputTypeTooltipsMap.end()) tooltip = it->second; + addressType->addItem(description, static_cast(type)); + addressType->setItemData(index, tooltip, Qt::ToolTipRole); + // setting default selected value for the combo if it has been passed + if (defaultType.has_value() && defaultType.value() == type) addressType->setCurrentIndex(index); + }; + + // Adding all output types defined in the map to the combo + for (const auto& pair : outputTypeDescriptionsMap()) { + OutputType type{pair.first}; + OutputTypeInfo typeInfo{pair.second}; + // if it requires taproot check it with arg taprootEnabled + // eg call from receivecoindialog.cpp + if ((!typeInfo.requiresTaprootEnabled) || (typeInfo.requiresTaprootEnabled == taprootEnabled)) { + add_address_type(type, typeInfo.description); + } + } +} + } // namespace GUIUtil diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h index 7f06fdfe37d..f677adb421b 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -40,6 +41,7 @@ QT_BEGIN_NAMESPACE class QAbstractButton; class QAbstractItemView; class QAction; +class QComboBox; class QDateTime; class QDialog; class QFont; @@ -436,6 +438,12 @@ namespace GUIUtil return false; } + struct OutputTypeInfo { + const QString description; + const bool requiresTaprootEnabled; + }; + std::map outputTypeDescriptionsMap(); + void AddItemsToAddressTypeCombo(QComboBox* addressType, bool taprootEnabled, const std::map& outputTypeTooltipsMap, std::optional defaultType = std::nullopt); } // namespace GUIUtil #endif // BITCOIN_QT_GUIUTIL_H diff --git a/src/qt/receivecoinsdialog.cpp b/src/qt/receivecoinsdialog.cpp index be7741e8a8c..75581eb697b 100644 --- a/src/qt/receivecoinsdialog.cpp +++ b/src/qt/receivecoinsdialog.cpp @@ -86,19 +86,15 @@ void ReceiveCoinsDialog::setModel(WalletModel *_model) &QItemSelectionModel::selectionChanged, this, &ReceiveCoinsDialog::recentRequestsView_selectionChanged); + /** Tooltip for each address type that will be displayed on the combo*/ + std::map addressTypeTooltipMap{ + {OutputType::LEGACY, QObject::tr("Not recommended due to higher fees and less protection against typos.")}, + {OutputType::P2SH_SEGWIT, QObject::tr("Generates an address compatible with older wallets.")}, + {OutputType::BECH32, QObject::tr("Generates a native segwit address (BIP-173). Some old wallets don't support it.")}, + {OutputType::BECH32M, QObject::tr("Bech32m (BIP-350) is an upgrade to Bech32, wallet support is still limited.")}}; + // Populate address type dropdown and select default - auto add_address_type = [&](OutputType type, const QString& text, const QString& tooltip) { - const auto index = ui->addressType->count(); - ui->addressType->addItem(text, (int) type); - ui->addressType->setItemData(index, tooltip, Qt::ToolTipRole); - if (model->wallet().getDefaultAddressType() == type) ui->addressType->setCurrentIndex(index); - }; - add_address_type(OutputType::LEGACY, tr("Base58 (Legacy)"), tr("Not recommended due to higher fees and less protection against typos.")); - add_address_type(OutputType::P2SH_SEGWIT, tr("Base58 (P2SH-SegWit)"), tr("Generates an address compatible with older wallets.")); - add_address_type(OutputType::BECH32, tr("Bech32 (SegWit)"), tr("Generates a native segwit address (BIP-173). Some old wallets don't support it.")); - if (model->wallet().taprootEnabled()) { - add_address_type(OutputType::BECH32M, tr("Bech32m (Taproot)"), tr("Bech32m (BIP-350) is an upgrade to Bech32, wallet support is still limited.")); - } + GUIUtil::AddItemsToAddressTypeCombo(ui->addressType, model->wallet().taprootEnabled(), addressTypeTooltipMap, model->wallet().getDefaultAddressType()); // Set the button to be enabled or disabled based on whether the wallet can give out new addresses. ui->receiveButton->setEnabled(model->wallet().canGetAddresses()); diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index ee3327530c4..dc2be4cb9d9 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -140,8 +140,10 @@ void WalletModel::updateTransaction() void WalletModel::updateAddressBook(const QString &address, const QString &label, bool isMine, wallet::AddressPurpose purpose, int status) { - if(addressTableModel) - addressTableModel->updateEntry(address, label, isMine, purpose, status); + if (addressTableModel) { + CTxDestination destination = DecodeDestination(address.toStdString()); + addressTableModel->updateEntry(address, label, isMine, purpose, status, m_wallet->getOutputType(destination)); + } } void WalletModel::updateWatchOnlyFlag(bool fHaveWatchonly) diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 65285187f4c..8e22eeebf9f 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -163,6 +163,13 @@ class WalletImpl : public Wallet } return false; } + OutputType getOutputType(const CTxDestination& dest) override + { + CScript script = GetScriptForDestination(dest); + std::unique_ptr provider = m_wallet->GetSolvingProvider(script); + std::unique_ptr desc = provider ? InferDescriptor(script, *provider) : InferDescriptor(script, FlatSigningProvider()); + return desc->GetOutputType().value_or(OutputType::UNKNOWN); + } SigningResult signMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) override { return m_wallet->SignMessage(message, pkhash, str_sig);