From b5a8efb065d5689cd1d27c1974b796f609ad59c0 Mon Sep 17 00:00:00 2001 From: JeremyRand Date: Sat, 27 Jun 2020 00:18:16 +0000 Subject: [PATCH] (WIP) Namecoin: Add Qt GUI for name_list Based on https://github.com/namecoin/namecoin-core/pull/187 by Brandon Roberts. TODO: Untested. --- src/Makefile.qt.include | 7 + src/qt/bitcoingui.cpp | 23 ++ src/qt/bitcoingui.h | 4 + src/qt/forms/managenamespage.ui | 253 +++++++++++++ src/qt/managenamespage.cpp | 148 ++++++++ src/qt/managenamespage.h | 55 +++ src/qt/nametablemodel.cpp | 622 ++++++++++++++++++++++++++++++++ src/qt/nametablemodel.h | 88 +++++ src/qt/walletframe.cpp | 7 + src/qt/walletframe.h | 2 + src/qt/walletmodel.cpp | 19 + src/qt/walletmodel.h | 4 + src/qt/walletview.cpp | 9 + src/qt/walletview.h | 4 + 14 files changed, 1245 insertions(+) create mode 100644 src/qt/forms/managenamespage.ui create mode 100644 src/qt/managenamespage.cpp create mode 100644 src/qt/managenamespage.h create mode 100644 src/qt/nametablemodel.cpp create mode 100644 src/qt/nametablemodel.h diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 780b5cdb78..12077e4130 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -31,6 +31,7 @@ QT_FORMS_UI = \ qt/forms/debugwindow.ui \ qt/forms/sendcoinsdialog.ui \ qt/forms/sendcoinsentry.ui \ + qt/forms/managenamespage.ui \ qt/forms/signverifymessagedialog.ui \ qt/forms/transactiondescdialog.ui @@ -55,6 +56,8 @@ QT_MOC_CPP = \ qt/moc_macdockiconhandler.cpp \ qt/moc_macnotificationhandler.cpp \ qt/moc_modaloverlay.cpp \ + qt/moc_managenamespage.cpp \ + qt/moc_nametablemodel.cpp \ qt/moc_notificator.cpp \ qt/moc_openuridialog.cpp \ qt/moc_optionsdialog.cpp \ @@ -125,8 +128,10 @@ BITCOIN_QT_H = \ qt/macnotificationhandler.h \ qt/macos_appnap.h \ qt/modaloverlay.h \ + qt/managenamespage.h \ qt/networkstyle.h \ qt/notificator.h \ + qt/nametablemodel.h \ qt/openuridialog.h \ qt/optionsdialog.h \ qt/optionsmodel.h \ @@ -245,6 +250,8 @@ BITCOIN_QT_WALLET_CPP = \ qt/coincontroltreewidget.cpp \ qt/createwalletdialog.cpp \ qt/editaddressdialog.cpp \ + qt/managenamespage.cpp \ + qt/nametablemodel.cpp \ qt/openuridialog.cpp \ qt/overviewpage.cpp \ qt/paymentserver.cpp \ diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 339c4eaa18..bca9db4b2c 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -277,6 +278,17 @@ void BitcoinGUI::createActions() historyAction->setShortcut(QKeySequence(Qt::ALT + Qt::Key_4)); tabGroup->addAction(historyAction); + manageNamesAction = new QAction(platformStyle->SingleColorIcon(":/icons/bitcoin"), tr("&Manage Names"), this); + manageNamesAction->setStatusTip(tr("Manage names registered via Namecoin")); + manageNamesAction->setToolTip(manageNamesAction->statusTip()); + manageNamesAction->setCheckable(true); + manageNamesAction->setShortcut(QKeySequence(Qt::ALT + Qt::Key_6)); + tabGroup->addAction(manageNamesAction); + + manageNamesMenuAction = new QAction(manageNamesAction->text(), this); + manageNamesMenuAction->setStatusTip(manageNamesAction->statusTip()); + manageNamesMenuAction->setToolTip(manageNamesMenuAction->statusTip()); + #ifdef ENABLE_WALLET // These showNormalIfMinimized are needed because Send Coins and Receive Coins // can be triggered from the tray menu, and need to show the GUI to be useful. @@ -292,6 +304,8 @@ void BitcoinGUI::createActions() connect(receiveCoinsMenuAction, &QAction::triggered, this, &BitcoinGUI::gotoReceiveCoinsPage); connect(historyAction, &QAction::triggered, [this]{ showNormalIfMinimized(); }); connect(historyAction, &QAction::triggered, this, &BitcoinGUI::gotoHistoryPage); + connect(manageNamesAction, &QAction::triggered, [this]{ showNormalIfMinimized(); }); + connect(manageNamesAction, &QAction::triggered, this, &BitcoinGUI::gotoManageNamesPage); #endif // ENABLE_WALLET quitAction = new QAction(tr("E&xit"), this); @@ -550,6 +564,7 @@ void BitcoinGUI::createToolBars() toolbar->addAction(sendCoinsAction); toolbar->addAction(receiveCoinsAction); toolbar->addAction(historyAction); + toolbar->addAction(manageNamesAction); overviewAction->setChecked(true); #ifdef ENABLE_WALLET @@ -736,6 +751,7 @@ void BitcoinGUI::setWalletActionsEnabled(bool enabled) receiveCoinsAction->setEnabled(enabled); receiveCoinsMenuAction->setEnabled(enabled); historyAction->setEnabled(enabled); + manageNamesAction->setEnabled(enabled); encryptWalletAction->setEnabled(enabled); backupWalletAction->setEnabled(enabled); changePassphraseAction->setEnabled(enabled); @@ -786,6 +802,7 @@ void BitcoinGUI::createTrayIconMenu() if (enableWallet) { trayIconMenu->addAction(sendCoinsMenuAction); trayIconMenu->addAction(receiveCoinsMenuAction); + trayIconMenu->addAction(manageNamesMenuAction); trayIconMenu->addSeparator(); trayIconMenu->addAction(signMessageAction); trayIconMenu->addAction(verifyMessageAction); @@ -881,6 +898,12 @@ void BitcoinGUI::gotoSendCoinsPage(QString addr) if (walletFrame) walletFrame->gotoSendCoinsPage(addr); } +void BitcoinGUI::gotoManageNamesPage() +{ + manageNamesAction->setChecked(true); + if (walletFrame) walletFrame->gotoManageNamesPage(); +} + void BitcoinGUI::gotoSignMessageTab(QString addr) { if (walletFrame) walletFrame->gotoSignMessageTab(addr); diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index 912297a74e..6749be3e90 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -138,6 +138,8 @@ class BitcoinGUI : public QMainWindow QAction* sendCoinsMenuAction = nullptr; QAction* usedSendingAddressesAction = nullptr; QAction* usedReceivingAddressesAction = nullptr; + QAction* manageNamesAction = nullptr; + QAction* manageNamesMenuAction = nullptr; QAction* signMessageAction = nullptr; QAction* verifyMessageAction = nullptr; QAction* m_load_psbt_action = nullptr; @@ -276,6 +278,8 @@ public Q_SLOTS: void gotoReceiveCoinsPage(); /** Switch to send coins page */ void gotoSendCoinsPage(QString addr = ""); + /** Switch to manage names page */ + void gotoManageNamesPage(); /** Show Sign/Verify Message dialog and switch to sign message tab */ void gotoSignMessageTab(QString addr = ""); diff --git a/src/qt/forms/managenamespage.ui b/src/qt/forms/managenamespage.ui new file mode 100644 index 0000000000..4b15880084 --- /dev/null +++ b/src/qt/forms/managenamespage.ui @@ -0,0 +1,253 @@ + + + ManageNamesPage + + + + 0 + 0 + 776 + 364 + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + + + + + 0 + 0 + + + + Your registered names (pending and unconfirmed names have blank expiration): + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + Qt::CustomContextMenu + + + Double-click name to configure + + + false + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + false + + + + + + + + + + + + + + + + + diff --git a/src/qt/managenamespage.cpp b/src/qt/managenamespage.cpp new file mode 100644 index 0000000000..f2c05d21da --- /dev/null +++ b/src/qt/managenamespage.cpp @@ -0,0 +1,148 @@ +// TODO: figure out which of these includes are actually still necessary for name_list +#include +#include + +#include +#include // cs_main +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +// TODO: figure out which of these members are actually still necessary for name_list +ManageNamesPage::ManageNamesPage(const PlatformStyle *platformStyle, QWidget *parent) : + QWidget(parent), + platformStyle(platformStyle), + ui(new Ui::ManageNamesPage), + model(nullptr), + walletModel(nullptr), + proxyModel(nullptr) +{ + ui->setupUi(this); + + // Context menu actions + QAction *copyNameAction = new QAction(tr("Copy &Name"), this); + QAction *copyValueAction = new QAction(tr("Copy &Value"), this); + + // Build context menu + contextMenu = new QMenu(); + contextMenu->addAction(copyNameAction); + contextMenu->addAction(copyValueAction); + + // Connect signals for context menu actions + connect(copyNameAction, SIGNAL(triggered()), this, SLOT(onCopyNameAction())); + connect(copyValueAction, SIGNAL(triggered()), this, SLOT(onCopyValueAction())); + + connect(ui->tableView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextualMenu(QPoint))); + ui->tableView->setEditTriggers(QAbstractItemView::NoEditTriggers); + + ui->tableView->installEventFilter(this); +} + +ManageNamesPage::~ManageNamesPage() +{ + delete ui; +} + +void ManageNamesPage::setModel(WalletModel *walletModel) +{ + this->walletModel = walletModel; + model = walletModel->getNameTableModel(); + + proxyModel = new QSortFilterProxyModel(this); + proxyModel->setSourceModel(model); + proxyModel->setDynamicSortFilter(true); + proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + + ui->tableView->setModel(proxyModel); + ui->tableView->sortByColumn(0, Qt::AscendingOrder); + + ui->tableView->horizontalHeader()->setHighlightSections(false); + + // Set column widths + ui->tableView->horizontalHeader()->resizeSection( + NameTableModel::Name, 320); +#if QT_VERSION >= 0x050000 + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); +#else + // this function introduced in QT5 + ui->tableView->horizontalHeader()->setResizeMode(QHeaderView::Stretch); +#endif + + + connect(ui->tableView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), + this, SLOT(selectionChanged())); + + selectionChanged(); +} + +bool ManageNamesPage::eventFilter(QObject *object, QEvent *event) +{ + if (event->type() == QEvent::FocusIn) + { + if (object == ui->tableView) + { + ui->submitNameButton->setDefault(false); + ui->configureNameButton->setDefault(true); + } + } + return QWidget::eventFilter(object, event); +} + +void ManageNamesPage::contextualMenu(const QPoint &point) +{ + QModelIndex index = ui->tableView->indexAt(point); + if (index.isValid()) + contextMenu->exec(QCursor::pos()); +} + +void ManageNamesPage::onCopyNameAction() +{ + GUIUtil::copyEntryData(ui->tableView, NameTableModel::Name); +} + +void ManageNamesPage::onCopyValueAction() +{ + GUIUtil::copyEntryData(ui->tableView, NameTableModel::Value); +} + +void ManageNamesPage::exportClicked() +{ + // CSV is currently the only supported format + QString suffixOut = ""; + QString filename = GUIUtil::getSaveFileName( + this, + tr("Export Registered Names Data"), + QString(), + tr("Comma separated file (*.csv)"), + &suffixOut); + + if (filename.isNull()) + return; + + CSVModelWriter writer(filename); + + // name, column, role + writer.setModel(proxyModel); + writer.addColumn("Name", NameTableModel::Name, Qt::EditRole); + writer.addColumn("Value", NameTableModel::Value, Qt::EditRole); + writer.addColumn("Expires In", NameTableModel::ExpiresIn, Qt::EditRole); + writer.addColumn("Name Status", NameTableModel::NameStatus, Qt::EditRole); + + if (!writer.write()) + { + QMessageBox::critical(this, tr("Error exporting"), tr("Could not write to file %1.").arg(filename), + QMessageBox::Abort, QMessageBox::Abort); + } +} diff --git a/src/qt/managenamespage.h b/src/qt/managenamespage.h new file mode 100644 index 0000000000..66ffd834f1 --- /dev/null +++ b/src/qt/managenamespage.h @@ -0,0 +1,55 @@ +#ifndef MANAGENAMESPAGE_H +#define MANAGENAMESPAGE_H + +#include + +#include + +class WalletModel; +class NameTableModel; + +namespace Ui { + class ManageNamesPage; +} + +QT_BEGIN_NAMESPACE +class QTableView; +class QItemSelection; +class QSortFilterProxyModel; +class QMenu; +class QModelIndex; +QT_END_NAMESPACE + +/** Page for managing names */ +class ManageNamesPage : public QWidget +{ + Q_OBJECT + +public: + explicit ManageNamesPage(const PlatformStyle *platformStyle, QWidget *parent = nullptr); + ~ManageNamesPage(); + + void setModel(WalletModel *walletModel); + +private: + const PlatformStyle *platformStyle; + Ui::ManageNamesPage *ui; + NameTableModel *model; + WalletModel *walletModel; + QSortFilterProxyModel *proxyModel; + QMenu *contextMenu; + +public Q_SLOTS: + void exportClicked(); + +private Q_SLOTS: + bool eventFilter(QObject *object, QEvent *event); + + /** Spawn contextual menu (right mouse menu) for name table entry */ + void contextualMenu(const QPoint &point); + + void onCopyNameAction(); + void onCopyValueAction(); +}; + +#endif // MANAGENAMESPAGE_H diff --git a/src/qt/nametablemodel.cpp b/src/qt/nametablemodel.cpp new file mode 100644 index 0000000000..7f616672b9 --- /dev/null +++ b/src/qt/nametablemodel.cpp @@ -0,0 +1,622 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // cs_main +#include + +#include +#include +#include + +// ExpiresIn column is right-aligned as it contains numbers +namespace { + int column_alignments[] = { + Qt::AlignLeft|Qt::AlignVCenter, // Name + Qt::AlignLeft|Qt::AlignVCenter, // Value + Qt::AlignRight|Qt::AlignVCenter, // Expires in + Qt::AlignRight|Qt::AlignVCenter, // Name Status + }; +} + +struct NameTableEntryLessThan +{ + bool operator()(const NameTableEntry &a, const NameTableEntry &b) const + { + return a.name < b.name; + } + bool operator()(const NameTableEntry &a, const QString &b) const + { + return a.name < b; + } + bool operator()(const QString &a, const NameTableEntry &b) const + { + return a < b.name; + } +}; + +// Returns true if new height is better +bool NameTableEntry::CompareHeight(int nOldHeight, int nNewHeight) +{ + if(nOldHeight == NAME_NON_EXISTING) + return true; + + // We use optimistic way, assuming that unconfirmed transaction will eventually become confirmed, + // so we update the name in the table immediately. Ideally we need a separate way of displaying + // unconfirmed names (e.g. grayed out) + if(nNewHeight == NAME_UNCONFIRMED) + return true; + + // Here we rely on the fact that dummy height values are always negative + return nNewHeight > nOldHeight; +} + +// Private implementation +class NameTablePriv +{ +public: + CWallet *wallet; + QList cachedNameTable; + NameTableModel *parent; + + NameTablePriv(CWallet *wallet, NameTableModel *parent): + wallet(wallet), parent(parent) {} + + void refreshNameTable() + { + qDebug() << "NameTableModel::refreshNameTable"; + cachedNameTable.clear(); + std::map< std::string, NameTableEntry > vNamesO; + + // confirmed names (name_list) + // TODO: Add unconfirmed names once support for this is added to + // name_list. + // TODO: Set name and value encoding to hex, so that nonstandard + // encodings don't cause errors. + JSONRPCRequest nameListRequest; + nameListRequest.strMethod = "name_list"; + nameListRequest.params = NullUniValue; + nameListRequest.fHelp = false; + UniValue confirmedNames; + + try { + confirmedNames = tableRPC.execute(nameListRequest); + } catch (const UniValue& e) { + // although we shouldn't typically encounter error here, we + // should continue and try to add confirmed names and + // pending names. show error to user in case something + // actually went wrong so they can potentially recover + UniValue message = find_value( e, "message"); + LogPrintf ("name_list lookup error: %s\n", message.get_str().c_str()); + } + + // will be an object if name_list command isn't available/other error + if(confirmedNames.isArray()) + { + for (const auto& v : confirmedNames.getValues()) + { + if(find_value ( v, "expired").get_bool()) + continue; + std::string name = find_value ( v, "name").get_str(); + std::string data = find_value ( v, "value").get_str(); + int height = find_value ( v, "height").get_int(); + vNamesO[name] = NameTableEntry(name, data, height, "confirmed"); + LogPrintf("found confirmed name: name=%s height=%i\n", name.c_str(), height); + } + } + + // Add existing names + for (const auto& item : vNamesO) + cachedNameTable.append(item.second); + + // qLowerBound() and qUpperBound() require our cachedNameTable list to be sorted in asc order + qSort(cachedNameTable.begin(), cachedNameTable.end(), NameTableEntryLessThan()); + } + + bool findInModel(const QString &name, int *lowerIndex=nullptr, int *upperIndex=nullptr) + { + // Find name in model + QList::iterator lower = qLowerBound( + cachedNameTable.begin(), cachedNameTable.end(), name, NameTableEntryLessThan()); + QList::iterator upper = qUpperBound( + cachedNameTable.begin(), cachedNameTable.end(), name, NameTableEntryLessThan()); + if (lowerIndex) + *lowerIndex = (lower - cachedNameTable.begin()); + if (upperIndex) + *upperIndex = (upper - cachedNameTable.begin()); + return lower != upper; + } + + void refreshName(const valtype &inName, QString *qNameStatus = nullptr) + { + LOCK(cs_main); + + std::string strName = ValtypeToString(inName); + int lowerIndex, upperIndex; + bool inModel = findInModel(QString::fromStdString(strName), &lowerIndex, &upperIndex); + QList::iterator lower = (cachedNameTable.begin() + lowerIndex); + + // preserve previous name state if available + std::string strNameStatus; + if (qNameStatus) + strNameStatus = qNameStatus->toStdString(); + else + strNameStatus = ""; + + std::string strData = ""; + int height = NameTableEntry::NAME_NON_EXISTING; + if(inModel) + { + strNameStatus = lower->nameStatus.toStdString(); + height = lower->nHeight; + strData = lower->value.toStdString(); + } + + // NOTE: name_show only reflects the status of the name as it is + // currently existig in the chain. this means that if you issue + // something like a name_update and issue name_show, the name_update + // results will not be reflected until block confirmation + UniValue params (UniValue::VOBJ); + params.pushKV ("name", strName); + + JSONRPCRequest jsonRequest; + jsonRequest.strMethod = "name_show"; + jsonRequest.params = params; + jsonRequest.fHelp = false; + + UniValue res; + try { + res = tableRPC.execute(jsonRequest); + } catch (const UniValue& e) { + UniValue message = find_value(e, "message"); + std::string errorStr = message.get_str(); + LogPrintf ("unexpected name_show response on refreshName=%s: %s\n", + strName.c_str(), errorStr.c_str()); + return; + } + + UniValue heightResult = find_value(res, "height"); + if (!heightResult.isNum()) + { + LogPrintf ("No height for name %s\n", strName.c_str()); + return; + } + + // we have a height, this means we either have a confirmed + // name_firstupdate or we have either an old or new + // name_update (no way to tell via name_show) + height = heightResult.get_int(); + + UniValue valTxid = find_value(res, "txid"); + if (!valTxid.isStr()) + { + LogPrintf ("No txid for name %s\n", strName.c_str()); + return; + } + + std::string strTxid = valTxid.get_str(); + + // get transaction and look for confirms, update name status + // if we have confirms + UniValue txparams (UniValue::VOBJ); + txparams.pushKV ("txid", strTxid); + + JSONRPCRequest txJsonRequest; + txJsonRequest.strMethod = "gettransaction"; + txJsonRequest.params = txparams; + txJsonRequest.fHelp = false; + + UniValue txRes; + try { + txRes = tableRPC.execute(txJsonRequest); + } catch (const UniValue& e) { + UniValue message = find_value(e, "message"); + std::string errorStr = message.get_str(); + LogPrintf ("unexpected gettransaction response on refreshName=%s: %s\n", + strName.c_str(), errorStr.c_str()); + return; + } + + UniValue valConfirms = find_value(txRes, "confirmations"); + if (!valConfirms.isNum()) + { + LogPrintf ("No confirmations for name %s\n", strName.c_str()); + return; + } + + const unsigned int uConfirms = static_cast(valConfirms.get_int()); + if (!qNameStatus && uConfirms >= MIN_FIRSTUPDATE_DEPTH) + strNameStatus = "confirmed"; + + NameTableEntry nameObj(strName, strData, height, strNameStatus); + + if(findInModel(nameObj.name)) + { + // In model - update or delete + if(nameObj.nHeight != NameTableEntry::NAME_NON_EXISTING) + { + LogPrintf ("refreshName result : %s - refreshed in the table\n", qPrintable(nameObj.name)); + updateEntry(nameObj.name, nameObj.value, nameObj.nHeight, CT_UPDATED, nameObj.nameStatus); + } + else + { + LogPrintf("refreshName result : %s - deleted from the table\n", qPrintable(nameObj.name)); + updateEntry(nameObj.name, nameObj.value, nameObj.nHeight, CT_DELETED, nameObj.nameStatus); + } + } + else + { + // Not in model - add or do nothing + if(nameObj.nHeight != NameTableEntry::NAME_NON_EXISTING) + { + LogPrintf("refreshName result : %s - added to the table\n", qPrintable(nameObj.name)); + updateEntry(nameObj.name, nameObj.value, nameObj.nHeight, CT_NEW, nameObj.nameStatus); + } + else + { + LogPrintf("refreshName result : %s - ignored (not in the table)\n", qPrintable(nameObj.name)); + } + } + } + + void updateEntry(const QString &name, const QString &value, int nHeight, + int status, const QString &nameStatus, int *outNewRowIndex=nullptr) + { + int lowerIndex, upperIndex; + bool inModel = findInModel(name, &lowerIndex, &upperIndex); + QList::iterator lower = (cachedNameTable.begin() + lowerIndex); + QList::iterator upper = (cachedNameTable.begin() + upperIndex); + + switch(status) + { + case CT_NEW: + if(inModel) + { + if(outNewRowIndex) + { + *outNewRowIndex = parent->index(lowerIndex, 0).row(); + // HACK: ManageNamesPage uses this to ensure updating and get selected row, + // so we do not write warning into the log in this case + } + else { + LogPrintf ("Warning: NameTablePriv::updateEntry: Got CT_NEW, but entry is already in model\n"); + } + break; + } + parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex); + cachedNameTable.insert(lowerIndex, NameTableEntry(name, value, nHeight, nameStatus)); + parent->endInsertRows(); + if(outNewRowIndex) + *outNewRowIndex = parent->index(lowerIndex, 0).row(); + break; + case CT_UPDATED: + if(!inModel) + { + LogPrintf ("Warning: NameTablePriv::updateEntry: Got CT_UPDATED, but entry is not in model\n"); + break; + } + lower->name = name; + lower->value = value; + lower->nHeight = nHeight; + lower->nameStatus = nameStatus; + parent->emitDataChanged(lowerIndex); + break; + case CT_DELETED: + if(!inModel) + { + LogPrintf ("Warning: NameTablePriv::updateEntry: Got CT_DELETED, but entry is not in model\n"); + break; + } + parent->beginRemoveRows(QModelIndex(), lowerIndex, upperIndex - 1); + cachedNameTable.erase(lower, upper); + parent->endRemoveRows(); + break; + } + } + + int size() + { + return cachedNameTable.size(); + } + + NameTableEntry *index(int idx) + { + if(idx >= 0 && idx < cachedNameTable.size()) + { + return &cachedNameTable[idx]; + } + else + { + return nullptr; + } + } +}; + +// TODO: figure out which of these members are actually still necessary for name_list +NameTableModel::NameTableModel(const PlatformStyle *platformStyle, CWallet* wallet, WalletModel *parent): + QAbstractTableModel(parent), + wallet(wallet), + walletModel(parent), + priv(new NameTablePriv(wallet, this)), + platformStyle(platformStyle) +{ + columns << tr("Name") << tr("Value") << tr("Expires In") << tr("Status"); + priv->refreshNameTable(); + + QTimer *timer = new QTimer(this); + // TODO: move updateExpiration from a constant timer to a legit slot + connect(timer, SIGNAL(timeout()), this, SLOT(updateExpiration())); + timer->start(MODEL_UPDATE_DELAY); + + subscribeToCoreSignals(); +} + +NameTableModel::~NameTableModel() +{ + unsubscribeFromCoreSignals(); +} + +void NameTableModel::updateExpiration() +{ + int nBestHeight = chainActive.Height(); + if(nBestHeight != cachedNumBlocks) + { + LOCK(cs_main); + + cachedNumBlocks = nBestHeight; + std::vector expired; + // Blocks came in since last poll. + // Delete expired names + for (int i = 0, n = priv->size(); i < n; i++) + { + NameTableEntry *item = priv->index(i); + + const Consensus::Params& params = Params().GetConsensus(); + int nHeight = item->nHeight; + int expirationDepth = params.rules->NameExpirationDepth(nHeight); + + priv->refreshName(ValtypeFromString(item->name.toStdString())); + + // remove expired confirmed names + if((nHeight + expirationDepth <= nBestHeight) && (item->nameStatus == "confirmed")) + { + expired.push_back(item); + } + + } + + // process all expirations in bulk (don't mutate table while iterating + for (NameTableEntry *item : expired) + priv->updateEntry(item->name, item->value, item->nHeight, CT_DELETED, "expired"); + + // Invalidate expiration counter for all rows. + // Qt is smart enough to only actually request the data for the + // visible rows. + //emit + dataChanged(index(0, ExpiresIn), index(priv->size()-1, ExpiresIn)); + } +} + +void NameTableModel::updateTransaction(const QString &hash, int status) +{ + uint256 hash256; + std::string strHash = hash.toStdString(); + hash256.SetHex(strHash); + + LOCK(wallet->cs_wallet); + // Find transaction in wallet + // TODO: Use RPC for this instead. + std::map::iterator mi = wallet->mapWallet.find(hash256); + if(mi == wallet->mapWallet.end()) + { + LogPrintf ("tx %s has no name in wallet\n", strHash); + return; + } + CWalletTx wtx = mi->second; + + // const auto vout = tx->vout; + for (const CTxOut& txout : wtx.tx->vout) + { + if(!CNameScript::isNameScript(txout.scriptPubKey)) + { + continue; + } + + CNameScript nameScript(txout.scriptPubKey); + switch (nameScript.getNameOp()) + { + case OP_NAME_NEW: + break; + + case OP_NAME_FIRSTUPDATE: + { + QString qNameStatus = "pending firstupdate"; + priv->refreshName(nameScript.getOpName(), &qNameStatus); + break; + } + case OP_NAME_UPDATE: + { + QString qNameStatus = "pending update"; + priv->refreshName(nameScript.getOpName(), &qNameStatus); + break; + } + + default: + assert (false); + } + } + +} + +int NameTableModel::rowCount(const QModelIndex &parent /*= QModelIndex()*/) const +{ + Q_UNUSED(parent); + return priv->size(); +} + +int NameTableModel::columnCount(const QModelIndex &parent /*= QModelIndex()*/) const +{ + Q_UNUSED(parent); + return columns.length(); +} + +QVariant NameTableModel::data(const QModelIndex &index, int role) const +{ + if(!index.isValid()) + return QVariant(); + + NameTableEntry *rec = static_cast(index.internalPointer()); + + // TODO: implement Qt::ForegroudRole for font color styling for states? + // TODO: implement Qt::ToolTipRole show name status on tooltip + if(role == Qt::DisplayRole || role == Qt::EditRole) + { + switch(index.column()) + { + case Name: + return rec->name; + case Value: + return rec->value; + case ExpiresIn: + { + if(!rec->HeightValid()) { + return QVariant(); + } + int nBestHeight = chainActive.Height(); + const Consensus::Params& params = Params().GetConsensus(); + return rec->nHeight + params.rules->NameExpirationDepth(rec->nHeight) - nBestHeight; + } + case NameStatus: + return rec->nameStatus; + } + } + return QVariant(); +} + +QVariant NameTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation != Qt::Horizontal) + return QVariant(); + + if(role == Qt::DisplayRole) + return columns[section]; + + if(role == Qt::TextAlignmentRole) + return column_alignments[section]; + + if(role == Qt::ToolTipRole) + { + switch(section) + { + case Name: + return tr("Name registered using Namecoin."); + + case Value: + return tr("Data associated with the name."); + + case ExpiresIn: + return tr("Number of blocks, after which the name will expire. Update name to renew it.\nEmpty cell means pending(awaiting automatic name_firstupdate or awaiting network confirmation)."); + } + } + return QVariant(); +} + +Qt::ItemFlags NameTableModel::flags(const QModelIndex &index) const +{ + if(!index.isValid()) + return 0; + + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; +} + +QModelIndex NameTableModel::index(int row, int column, const QModelIndex &parent /* = QModelIndex()*/) const +{ + Q_UNUSED(parent); + NameTableEntry *data = priv->index(row); + if(data) + { + return createIndex(row, column, priv->index(row)); + } + return QModelIndex(); +} + +// queue notifications to show a non freezing progress dialog e.g. for rescan +struct TransactionNotification +{ +public: + TransactionNotification() {} + TransactionNotification(const uint256 hash, const ChangeType status, const bool showTransaction): + hash(hash), status(status), showTransaction(showTransaction) {} + + void invoke(NameTableModel *ntm) + { + QString strHash = QString::fromStdString(hash.GetHex()); + QMetaObject::invokeMethod(ntm, "updateTransaction", Qt::QueuedConnection, + Q_ARG(QString, strHash), + Q_ARG(int, status)); + } +private: + const uint256 hash; + ChangeType status; + bool showTransaction; +}; + +static bool fQueueNotifications = false; +static std::vector< TransactionNotification > vQueueNotifications; + +static void NotifyTransactionChanged(NameTableModel *ntm, CWallet *wallet, const uint256 &hash, ChangeType status) +{ + // Find transaction in wallet + // TODO: Use RPC for this instead. + std::map::iterator mi = wallet->mapWallet.find(hash); + bool inWallet = mi != wallet->mapWallet.end(); + + TransactionNotification notification(hash, status, inWallet); + + if(fQueueNotifications) + { + vQueueNotifications.push_back(notification); + return; + } + notification.invoke(ntm); +} + + +void +NameTableModel::updateEntry(const QString &name, const QString &value, + int nHeight, int status, const QString &nameStatus, + int *outNewRowIndex) +{ + priv->updateEntry(name, value, nHeight, status, nameStatus, outNewRowIndex); +} + +void +NameTableModel::emitDataChanged(int idx) +{ + //emit + dataChanged(index(idx, 0), index(idx, columns.length()-1)); +} + +void +NameTableModel::subscribeToCoreSignals() +{ + // Connect signals to wallet + wallet->NotifyTransactionChanged.connect(boost::bind(NotifyTransactionChanged, this, _1, _2, _3)); + // wallet->ShowProgress.connect(boost::bind(ShowProgress, this, _1, _2)); +} + +void +NameTableModel::unsubscribeFromCoreSignals() +{ + // Disconnect signals from wallet + wallet->NotifyTransactionChanged.disconnect(boost::bind(NotifyTransactionChanged, this, _1, _2, _3)); + // wallet->ShowProgress.disconnect(boost::bind(ShowProgress, this, _1, _2)); +} diff --git a/src/qt/nametablemodel.h b/src/qt/nametablemodel.h new file mode 100644 index 0000000000..1bac416d0b --- /dev/null +++ b/src/qt/nametablemodel.h @@ -0,0 +1,88 @@ +#ifndef NAMETABLEMODEL_H +#define NAMETABLEMODEL_H + +#include + +#include +#include + +#include + +class PlatformStyle; +class NameTablePriv; +class CWallet; +class WalletModel; + +/** + Qt model for "Manage Names" page. + */ +class NameTableModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + explicit NameTableModel(const PlatformStyle *platformStyle, CWallet* wallet, WalletModel *parent=nullptr); + virtual ~NameTableModel(); + + enum ColumnIndex { + Name = 0, + Value = 1, + ExpiresIn = 2, + NameStatus = 3 + }; + + /** @name Methods overridden from QAbstractTableModel + @{*/ + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + /*@}*/ + +private: + CWallet *wallet; + WalletModel *walletModel; + QStringList columns; + std::unique_ptr priv; + const PlatformStyle *platformStyle; + int cachedNumBlocks; + + /** Notify listeners that data changed. */ + void emitDataChanged(int index); + + void subscribeToCoreSignals(); + void unsubscribeFromCoreSignals(); + +public Q_SLOTS: + void updateEntry(const QString &name, const QString &value, int nHeight, int status, const QString &nameStatus, int *outNewRowIndex=nullptr); + void updateExpiration(); + void updateTransaction(const QString &hash, int status); + + friend class NameTablePriv; +}; + +struct NameTableEntry +{ + QString name; + QString value; + int nHeight; + QString nameStatus; + + static const int NAME_NEW = -1; // Dummy nHeight value for not-yet-created names + static const int NAME_NON_EXISTING = -2; // Dummy nHeight value for unitinialized entries + static const int NAME_UNCONFIRMED = -3; // Dummy nHeight value for unconfirmed name transactions + + // NOTE: making this const throws warning indicating it will not be const + bool HeightValid() { return nHeight >= 0; } + static bool CompareHeight(int nOldHeight, int nNewHeight); // Returns true if new height is better + + NameTableEntry() : nHeight(NAME_NON_EXISTING) {} + NameTableEntry(const QString &name, const QString &value, int nHeight, const QString &nameStatus): + name(name), value(value), nHeight(nHeight), nameStatus(nameStatus) {} + NameTableEntry(const std::string &name, const std::string &value, int nHeight, const std::string &nameStatus): + name(QString::fromStdString(name)), value(QString::fromStdString(value)), nHeight(nHeight), nameStatus(QString::fromStdString(nameStatus)) {} +}; + +#endif // NAMETABLEMODEL_H diff --git a/src/qt/walletframe.cpp b/src/qt/walletframe.cpp index f16761d6b2..3c5ee0dbaa 100644 --- a/src/qt/walletframe.cpp +++ b/src/qt/walletframe.cpp @@ -172,6 +172,13 @@ void WalletFrame::gotoSendCoinsPage(QString addr) i.value()->gotoSendCoinsPage(addr); } +void WalletFrame::gotoManageNamesPage() +{ + QMap::const_iterator i; + for (i = mapWalletViews.constBegin(); i != mapWalletViews.constEnd(); ++i) + i.value()->gotoManageNamesPage(); +} + void WalletFrame::gotoSignMessageTab(QString addr) { WalletView *walletView = currentWalletView(); diff --git a/src/qt/walletframe.h b/src/qt/walletframe.h index 2b5f263468..b7a81c8b4c 100644 --- a/src/qt/walletframe.h +++ b/src/qt/walletframe.h @@ -72,6 +72,8 @@ public Q_SLOTS: void gotoReceiveCoinsPage(); /** Switch to send coins page */ void gotoSendCoinsPage(QString addr = ""); + /** Switch to manage names page */ + void gotoManageNamesPage(); /** Show Sign/Verify Message dialog and switch to sign message tab */ void gotoSignMessageTab(QString addr = ""); diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 6a3f903206..b84cd9dc0d 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -28,6 +28,14 @@ #include #include // for CRecipient +// namecoin API-related includes +// TODO: figure out which of these includes are actually still necessary for name_list +#include +#include +#include +#include +#include + #include #include @@ -35,6 +43,10 @@ #include #include +// TODO: figure out which of these includes are actually still necessary for name_list +#include +#include +#include WalletModel::WalletModel(std::unique_ptr wallet, ClientModel& client_model, const PlatformStyle *platformStyle, QObject *parent) : QObject(parent), @@ -44,6 +56,7 @@ WalletModel::WalletModel(std::unique_ptr wallet, ClientModel optionsModel(client_model.getOptionsModel()), addressTableModel(nullptr), transactionTableModel(nullptr), + nameTableModel(nullptr), recentRequestsTableModel(nullptr), cachedEncryptionStatus(Unencrypted), timer(new QTimer(this)) @@ -51,6 +64,7 @@ WalletModel::WalletModel(std::unique_ptr wallet, ClientModel fHaveWatchOnly = m_wallet->haveWatchOnly(); addressTableModel = new AddressTableModel(this); transactionTableModel = new TransactionTableModel(platformStyle, this); + nameTableModel = new NameTableModel(platformStyle, this); recentRequestsTableModel = new RecentRequestsTableModel(this); subscribeToCoreSignals(); @@ -287,6 +301,11 @@ AddressTableModel *WalletModel::getAddressTableModel() return addressTableModel; } +NameTableModel *WalletModel::getNameTableModel() +{ + return nameTableModel; +} + TransactionTableModel *WalletModel::getTransactionTableModel() { return transactionTableModel; diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index fd52db2da3..ad1105d4bd 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -12,6 +12,7 @@ #include #include