diff --git a/src/qt/addresstablemodel.cpp b/src/qt/addresstablemodel.cpp index 1805033d1..425e4b66d 100644 --- a/src/qt/addresstablemodel.cpp +++ b/src/qt/addresstablemodel.cpp @@ -1,419 +1,430 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include #include const QString AddressTableModel::Send = "S"; const QString AddressTableModel::Receive = "R"; struct AddressTableEntry { enum Type { Sending, Receiving, /* QSortFilterProxyModel will filter these out */ Hidden }; Type type; QString label; QString address; AddressTableEntry() {} AddressTableEntry(Type _type, const QString &_label, const QString &_address) : type(_type), label(_label), address(_address) {} }; struct AddressTableEntryLessThan { bool operator()(const AddressTableEntry &a, const AddressTableEntry &b) const { return a.address < b.address; } bool operator()(const AddressTableEntry &a, const QString &b) const { return a.address < b; } bool operator()(const QString &a, const AddressTableEntry &b) const { return a < b.address; } }; /* Determine address type from address purpose */ static AddressTableEntry::Type translateTransactionType(const QString &strPurpose, bool isMine) { AddressTableEntry::Type addressType = AddressTableEntry::Hidden; // "refund" addresses aren't shown, and change addresses aren't in // mapAddressBook at all. if (strPurpose == "send") { addressType = AddressTableEntry::Sending; } else if (strPurpose == "receive") { addressType = AddressTableEntry::Receiving; } else if (strPurpose == "unknown" || strPurpose == "") { // if purpose not set, guess addressType = (isMine ? AddressTableEntry::Receiving : AddressTableEntry::Sending); } return addressType; } // Private implementation class AddressTablePriv { public: QList cachedAddressTable; AddressTableModel *parent; AddressTablePriv(AddressTableModel *_parent) : parent(_parent) {} void refreshAddressTable(interfaces::Wallet &wallet) { cachedAddressTable.clear(); for (const auto &address : wallet.getAddresses()) { AddressTableEntry::Type addressType = translateTransactionType( QString::fromStdString(address.purpose), address.is_mine); cachedAddressTable.append(AddressTableEntry( addressType, QString::fromStdString(address.name), QString::fromStdString(EncodeCashAddr( address.dest, parent->walletModel->getChainParams())))); } // qLowerBound() and qUpperBound() require our cachedAddressTable list // to be sorted in asc order. Even though the map is already sorted this // re-sorting step is needed because the originating map is sorted by // binary address, not by base58() address. qSort(cachedAddressTable.begin(), cachedAddressTable.end(), AddressTableEntryLessThan()); } void updateEntry(const QString &address, const QString &label, bool isMine, const QString &purpose, int status) { // Find address / label in model QList::iterator lower = qLowerBound(cachedAddressTable.begin(), cachedAddressTable.end(), address, AddressTableEntryLessThan()); QList::iterator upper = qUpperBound(cachedAddressTable.begin(), cachedAddressTable.end(), address, AddressTableEntryLessThan()); int lowerIndex = (lower - cachedAddressTable.begin()); int upperIndex = (upper - cachedAddressTable.begin()); bool inModel = (lower != upper); AddressTableEntry::Type newEntryType = translateTransactionType(purpose, isMine); switch (status) { case CT_NEW: if (inModel) { qWarning() << "AddressTablePriv::updateEntry: Warning: Got " "CT_NEW, but entry is already in model"; break; } parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex); cachedAddressTable.insert( lowerIndex, AddressTableEntry(newEntryType, label, address)); parent->endInsertRows(); break; case CT_UPDATED: if (!inModel) { qWarning() << "AddressTablePriv::updateEntry: Warning: Got " "CT_UPDATED, but entry is not in model"; break; } lower->type = newEntryType; lower->label = label; parent->emitDataChanged(lowerIndex); break; case CT_DELETED: if (!inModel) { qWarning() << "AddressTablePriv::updateEntry: Warning: Got " "CT_DELETED, but entry is not in model"; break; } parent->beginRemoveRows(QModelIndex(), lowerIndex, upperIndex - 1); cachedAddressTable.erase(lower, upper); parent->endRemoveRows(); break; } } int size() { return cachedAddressTable.size(); } AddressTableEntry *index(int idx) { if (idx >= 0 && idx < cachedAddressTable.size()) { return &cachedAddressTable[idx]; } else { return nullptr; } } }; AddressTableModel::AddressTableModel(WalletModel *parent) : QAbstractTableModel(parent), walletModel(parent), priv(0) { columns << tr("Label") << tr("Address"); priv = new AddressTablePriv(this); priv->refreshAddressTable(parent->wallet()); } AddressTableModel::~AddressTableModel() { delete priv; } int AddressTableModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return priv->size(); } int AddressTableModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return columns.length(); } QVariant AddressTableModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } AddressTableEntry *rec = static_cast(index.internalPointer()); if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (index.column()) { case Label: if (rec->label.isEmpty() && role == Qt::DisplayRole) { return tr("(no label)"); } else { return rec->label; } case Address: return rec->address; } } else if (role == Qt::FontRole) { QFont font; if (index.column() == Address) { font = GUIUtil::fixedPitchFont(); } return font; } else if (role == TypeRole) { switch (rec->type) { case AddressTableEntry::Sending: return Send; case AddressTableEntry::Receiving: return Receive; default: break; } } return QVariant(); } bool AddressTableModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) { return false; } AddressTableEntry *rec = static_cast(index.internalPointer()); std::string strPurpose = (rec->type == AddressTableEntry::Sending ? "send" : "receive"); editStatus = OK; if (role == Qt::EditRole) { CTxDestination curAddress = DecodeDestination( rec->address.toStdString(), walletModel->getChainParams()); if (index.column() == Label) { // Do nothing, if old label == new label if (rec->label == value.toString()) { editStatus = NO_CHANGES; return false; } walletModel->wallet().setAddressBook( curAddress, value.toString().toStdString(), strPurpose); } else if (index.column() == Address) { CTxDestination newAddress = DecodeDestination( value.toString().toStdString(), walletModel->getChainParams()); // Refuse to set invalid address, set error status and return false if (boost::get(&newAddress)) { editStatus = INVALID_ADDRESS; return false; } // Do nothing, if old address == new address else if (newAddress == curAddress) { editStatus = NO_CHANGES; return false; } // Check for duplicate addresses to prevent accidental deletion of // addresses, if you try to paste an existing address over another // address (with a different label) if (walletModel->wallet().getAddress( newAddress, /* name= */ nullptr, /* is_mine= */ nullptr, /* purpose= */ nullptr)) { editStatus = DUPLICATE_ADDRESS; return false; } // Double-check that we're not overwriting a receiving address else if (rec->type == AddressTableEntry::Sending) { // Remove old entry walletModel->wallet().delAddressBook(curAddress); // Add new entry with new address walletModel->wallet().setAddressBook( newAddress, value.toString().toStdString(), strPurpose); } } return true; } return false; } QVariant AddressTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (role == Qt::DisplayRole && section < columns.size()) { return columns[section]; } } return QVariant(); } Qt::ItemFlags AddressTableModel::flags(const QModelIndex &index) const { if (!index.isValid()) { return 0; } AddressTableEntry *rec = static_cast(index.internalPointer()); Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled; // Can edit address and label for sending addresses, and only label for // receiving addresses. if (rec->type == AddressTableEntry::Sending || (rec->type == AddressTableEntry::Receiving && index.column() == Label)) { retval |= Qt::ItemIsEditable; } return retval; } QModelIndex AddressTableModel::index(int row, int column, const QModelIndex &parent) const { Q_UNUSED(parent); AddressTableEntry *data = priv->index(row); if (data) { return createIndex(row, column, priv->index(row)); } else { return QModelIndex(); } } void AddressTableModel::updateEntry(const QString &address, const QString &label, bool isMine, const QString &purpose, int status) { // Update address book model from Bitcoin core priv->updateEntry(address, label, isMine, purpose, status); } QString AddressTableModel::addRow(const QString &type, const QString &label, const QString &address, const OutputType address_type) { std::string strLabel = label.toStdString(); std::string strAddress = address.toStdString(); editStatus = OK; if (type == Send) { if (!walletModel->validateAddress(address)) { editStatus = INVALID_ADDRESS; return QString(); } // Check for duplicate addresses if (walletModel->wallet().getAddress( DecodeDestination(strAddress, walletModel->getChainParams()), /* name= */ nullptr, /* is_mine= */ nullptr, /* purpose= */ nullptr)) { editStatus = DUPLICATE_ADDRESS; return QString(); } } else if (type == Receive) { // Generate a new address to associate with given label CPubKey newKey; if (!walletModel->wallet().getKeyFromPool(false /* internal */, newKey)) { WalletModel::UnlockContext ctx(walletModel->requestUnlock()); if (!ctx.isValid()) { // Unlock wallet failed or was cancelled editStatus = WALLET_UNLOCK_FAILURE; return QString(); } if (!walletModel->wallet().getKeyFromPool(false /* internal */, newKey)) { editStatus = KEY_GENERATION_FAILURE; return QString(); } } walletModel->wallet().learnRelatedScripts(newKey, address_type); strAddress = EncodeCashAddr(GetDestinationForKey(newKey, address_type), walletModel->getChainParams()); } else { return QString(); } // Add entry walletModel->wallet().setAddressBook( DecodeDestination(strAddress, walletModel->getChainParams()), strLabel, (type == Send ? "send" : "receive")); return QString::fromStdString(strAddress); } bool AddressTableModel::removeRows(int row, int count, const QModelIndex &parent) { Q_UNUSED(parent); AddressTableEntry *rec = priv->index(row); if (count != 1 || !rec || rec->type == AddressTableEntry::Receiving) { // Can only remove one row at a time, and cannot remove rows not in // model. // Also refuse to remove receiving addresses. return false; } walletModel->wallet().delAddressBook(DecodeDestination( rec->address.toStdString(), walletModel->getChainParams())); return true; } -/* Look up label for address in address book, if not found return empty string. - */ QString AddressTableModel::labelForAddress(const QString &address) const { - CTxDestination destination = - DecodeDestination(address.toStdString(), walletModel->getChainParams()); std::string name; - if (walletModel->wallet().getAddress(destination, &name, - /* is_mine= */ nullptr, - /* purpose= */ nullptr)) { + if (getAddressData(address, &name, /* purpose= */ nullptr)) { return QString::fromStdString(name); } return QString(); } +QString AddressTableModel::purposeForAddress(const QString &address) const { + std::string purpose; + if (getAddressData(address, /* name= */ nullptr, &purpose)) { + return QString::fromStdString(purpose); + } + return QString(); +} + +bool AddressTableModel::getAddressData(const QString &address, + std::string *name, + std::string *purpose) const { + CTxDestination destination = + DecodeDestination(address.toStdString(), walletModel->getChainParams()); + return walletModel->wallet().getAddress(destination, name, + /* is_mine= */ nullptr, purpose); +} + int AddressTableModel::lookupAddress(const QString &address) const { QModelIndexList lst = match(index(0, Address, QModelIndex()), Qt::EditRole, address, 1, Qt::MatchExactly); if (lst.isEmpty()) { return -1; } else { return lst.at(0).row(); } } OutputType AddressTableModel::GetDefaultAddressType() const { return walletModel->wallet().getDefaultAddressType(); }; void AddressTableModel::emitDataChanged(int idx) { Q_EMIT dataChanged(index(idx, 0, QModelIndex()), index(idx, columns.length() - 1, QModelIndex())); } diff --git a/src/qt/addresstablemodel.h b/src/qt/addresstablemodel.h index 0c2b7ba15..fb8426720 100644 --- a/src/qt/addresstablemodel.h +++ b/src/qt/addresstablemodel.h @@ -1,118 +1,129 @@ // Copyright (c) 2011-2015 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_QT_ADDRESSTABLEMODEL_H #define BITCOIN_QT_ADDRESSTABLEMODEL_H #include #include enum class OutputType; class AddressTablePriv; class WalletModel; namespace interfaces { class Wallet; } /** * Qt model of the address book in the core. This allows views to access and * modify the address book. */ class AddressTableModel : public QAbstractTableModel { Q_OBJECT public: explicit AddressTableModel(WalletModel *parent = 0); ~AddressTableModel(); enum ColumnIndex { /**< User specified label */ Label = 0, /**< Bitcoin address */ Address = 1 }; enum RoleIndex { /**< Type of address (#Send or #Receive) */ TypeRole = Qt::UserRole }; /** Return status of edit/insert operation */ enum EditStatus { /**< Everything ok */ OK, /**< No changes were made during edit operation */ NO_CHANGES, /**< Unparseable address */ INVALID_ADDRESS, /**< Address already in address book */ DUPLICATE_ADDRESS, /**< Wallet could not be unlocked to create new receiving address */ WALLET_UNLOCK_FAILURE, /**< Generating a new public key for a receiving address failed */ KEY_GENERATION_FAILURE }; /**< Specifies send address */ static const QString Send; /**< Specifies receive address */ static const QString Receive; /** @name Methods overridden from QAbstractTableModel @{*/ int rowCount(const QModelIndex &parent) const override; int columnCount(const QModelIndex &parent) const override; QVariant data(const QModelIndex &index, int role) const override; bool setData(const QModelIndex &index, const QVariant &value, int role) override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; QModelIndex index(int row, int column, const QModelIndex &parent) const override; bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; Qt::ItemFlags flags(const QModelIndex &index) const override; /*@}*/ /* Add an address to the model. Returns the added address on success, and an empty string otherwise. */ QString addRow(const QString &type, const QString &label, const QString &address, const OutputType address_type); - /* Look up label for address in address book, if not found return empty + /** + * Look up label for address in address book, if not found return empty * string. */ QString labelForAddress(const QString &address) const; + /** + * Look up purpose for address in address book, if not found return empty + * string. + */ + QString purposeForAddress(const QString &address) const; + /* Look up row index of an address in the model. Return -1 if not found. */ int lookupAddress(const QString &address) const; EditStatus getEditStatus() const { return editStatus; } OutputType GetDefaultAddressType() const; private: WalletModel *walletModel; AddressTablePriv *priv; QStringList columns; EditStatus editStatus; + /** Look up address book data given an address string. */ + bool getAddressData(const QString &address, std::string *name, + std::string *purpose) const; + /** Notify listeners that data changed. */ void emitDataChanged(int index); public Q_SLOTS: /* Update address list from core. */ void updateEntry(const QString &address, const QString &label, bool isMine, const QString &purpose, int status); friend class AddressTablePriv; }; #endif // BITCOIN_QT_ADDRESSTABLEMODEL_H diff --git a/src/qt/editaddressdialog.cpp b/src/qt/editaddressdialog.cpp index e1efbdb6b..474962c12 100644 --- a/src/qt/editaddressdialog.cpp +++ b/src/qt/editaddressdialog.cpp @@ -1,135 +1,152 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include EditAddressDialog::EditAddressDialog(Mode _mode, QWidget *parent) : QDialog(parent), ui(new Ui::EditAddressDialog), mapper(0), mode(_mode), model(0) { ui->setupUi(this); GUIUtil::setupAddressWidget(ui->addressEdit, this); switch (mode) { case NewReceivingAddress: setWindowTitle(tr("New receiving address")); ui->addressEdit->setEnabled(false); break; case NewSendingAddress: setWindowTitle(tr("New sending address")); break; case EditReceivingAddress: setWindowTitle(tr("Edit receiving address")); ui->addressEdit->setEnabled(false); break; case EditSendingAddress: setWindowTitle(tr("Edit sending address")); break; } mapper = new QDataWidgetMapper(this); mapper->setSubmitPolicy(QDataWidgetMapper::ManualSubmit); } EditAddressDialog::~EditAddressDialog() { delete ui; } void EditAddressDialog::setModel(AddressTableModel *_model) { this->model = _model; if (!_model) { return; } mapper->setModel(_model); mapper->addMapping(ui->labelEdit, AddressTableModel::Label); mapper->addMapping(ui->addressEdit, AddressTableModel::Address); } void EditAddressDialog::loadRow(int row) { mapper->setCurrentIndex(row); } bool EditAddressDialog::saveCurrentRow() { if (!model) { return false; } switch (mode) { case NewReceivingAddress: case NewSendingAddress: address = model->addRow( mode == NewSendingAddress ? AddressTableModel::Send : AddressTableModel::Receive, ui->labelEdit->text(), ui->addressEdit->text(), model->GetDefaultAddressType()); break; case EditReceivingAddress: case EditSendingAddress: if (mapper->submit()) { address = ui->addressEdit->text(); } break; } return !address.isEmpty(); } void EditAddressDialog::accept() { if (!model) { return; } if (!saveCurrentRow()) { switch (model->getEditStatus()) { case AddressTableModel::OK: // Failed with unknown reason. Just reject. break; case AddressTableModel::NO_CHANGES: // No changes were made during edit operation. Just reject. break; case AddressTableModel::INVALID_ADDRESS: QMessageBox::warning(this, windowTitle(), tr("The entered address \"%1\" is not a " "valid Bitcoin address.") .arg(ui->addressEdit->text()), QMessageBox::Ok, QMessageBox::Ok); break; case AddressTableModel::DUPLICATE_ADDRESS: QMessageBox::warning(this, windowTitle(), - tr("The entered address \"%1\" is already " - "in the address book.") - .arg(ui->addressEdit->text()), + getDuplicateAddressWarning(), QMessageBox::Ok, QMessageBox::Ok); break; case AddressTableModel::WALLET_UNLOCK_FAILURE: QMessageBox::critical(this, windowTitle(), tr("Could not unlock wallet."), QMessageBox::Ok, QMessageBox::Ok); break; case AddressTableModel::KEY_GENERATION_FAILURE: QMessageBox::critical(this, windowTitle(), tr("New key generation failed."), QMessageBox::Ok, QMessageBox::Ok); break; } return; } QDialog::accept(); } +QString EditAddressDialog::getDuplicateAddressWarning() const { + QString dup_address = ui->addressEdit->text(); + QString existing_label = model->labelForAddress(dup_address); + QString existing_purpose = model->purposeForAddress(dup_address); + + if (existing_purpose == "receive" && + (mode == NewSendingAddress || mode == EditSendingAddress)) { + return tr("Address \"%1\" already exists as a receiving address with " + "label " + "\"%2\" and so cannot be added as a sending address.") + .arg(dup_address) + .arg(existing_label); + } + return tr("The entered address \"%1\" is already in the address book with " + "label \"%2\".") + .arg(dup_address) + .arg(existing_label); +} + QString EditAddressDialog::getAddress() const { return address; } void EditAddressDialog::setAddress(const QString &_address) { this->address = _address; ui->addressEdit->setText(_address); } diff --git a/src/qt/editaddressdialog.h b/src/qt/editaddressdialog.h index ac90c9884..2e03e8a87 100644 --- a/src/qt/editaddressdialog.h +++ b/src/qt/editaddressdialog.h @@ -1,57 +1,63 @@ // Copyright (c) 2011-2015 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_QT_EDITADDRESSDIALOG_H #define BITCOIN_QT_EDITADDRESSDIALOG_H #include class AddressTableModel; namespace Ui { class EditAddressDialog; } QT_BEGIN_NAMESPACE class QDataWidgetMapper; QT_END_NAMESPACE /** * Dialog for editing an address and associated information. */ class EditAddressDialog : public QDialog { Q_OBJECT public: enum Mode { NewReceivingAddress, NewSendingAddress, EditReceivingAddress, EditSendingAddress }; explicit EditAddressDialog(Mode mode, QWidget *parent); ~EditAddressDialog(); void setModel(AddressTableModel *model); void loadRow(int row); QString getAddress() const; void setAddress(const QString &address); public Q_SLOTS: void accept() override; private: bool saveCurrentRow(); + /** + * Return a descriptive string when adding an already-existing address + * fails. + */ + QString getDuplicateAddressWarning() const; + Ui::EditAddressDialog *ui; QDataWidgetMapper *mapper; Mode mode; AddressTableModel *model; QString address; }; #endif // BITCOIN_QT_EDITADDRESSDIALOG_H