From 54b60902dccf76932e92daf0bd47d90887d51983 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 22 Oct 2017 21:15:34 -0700 Subject: [PATCH] manage names tab v2 --- src/Makefile.qt.include | 16 +- src/Makefile.test.include | 3 +- src/names/common.h | 19 + src/qt/bitcoin.qrc | 1 + src/qt/bitcoingui.cpp | 24 + src/qt/bitcoingui.h | 4 + src/qt/configurenamedialog.cpp | 128 ++++ src/qt/configurenamedialog.h | 46 ++ src/qt/forms/configurenamedialog.ui | 274 +++++++++ src/qt/forms/managenamespage.ui | 243 ++++++++ src/qt/managenamespage.cpp | 323 ++++++++++ src/qt/managenamespage.h | 60 ++ src/qt/nametablemodel.cpp | 563 ++++++++++++++++++ src/qt/nametablemodel.h | 86 +++ src/qt/res/icons/tx_nameop.png | Bin 0 -> 1136 bytes src/qt/transactionrecord.cpp | 36 +- src/qt/transactionrecord.h | 3 +- src/qt/transactiontablemodel.cpp | 8 +- src/qt/transactionview.cpp | 1 + src/qt/walletframe.cpp | 36 +- src/qt/walletframe.h | 2 + src/qt/walletmodel.cpp | 308 +++++++++- src/qt/walletmodel.h | 20 +- src/qt/walletview.cpp | 9 + src/qt/walletview.h | 5 + src/wallet/test/wallet_name_pending_tests.cpp | 89 +++ src/wallet/wallet.cpp | 45 ++ src/wallet/wallet.h | 7 + src/wallet/walletdb.cpp | 44 ++ src/wallet/walletdb.h | 4 + 30 files changed, 2361 insertions(+), 46 deletions(-) create mode 100644 src/qt/configurenamedialog.cpp create mode 100644 src/qt/configurenamedialog.h create mode 100644 src/qt/forms/configurenamedialog.ui 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 create mode 100644 src/qt/res/icons/tx_nameop.png create mode 100644 src/wallet/test/wallet_name_pending_tests.cpp diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 0f460fbecc..4a4d9e7f5e 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -110,6 +110,8 @@ QT_FORMS_UI = \ qt/forms/debugwindow.ui \ qt/forms/sendcoinsdialog.ui \ qt/forms/sendcoinsentry.ui \ + qt/forms/managenamespage.ui \ + qt/forms/configurenamedialog.ui \ qt/forms/signverifymessagedialog.ui \ qt/forms/transactiondescdialog.ui @@ -126,6 +128,7 @@ QT_MOC_CPP = \ qt/moc_clientmodel.cpp \ qt/moc_coincontroldialog.cpp \ qt/moc_coincontroltreewidget.cpp \ + qt/moc_configurenamedialog.cpp \ qt/moc_csvmodelwriter.cpp \ qt/moc_editaddressdialog.cpp \ qt/moc_guiutil.cpp \ @@ -133,6 +136,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 \ @@ -195,7 +200,9 @@ BITCOIN_QT_H = \ qt/clientmodel.h \ qt/coincontroldialog.h \ qt/coincontroltreewidget.h \ + qt/configurenamedialog.h \ qt/csvmodelwriter.h \ + qt/configurenamedialog.h \ qt/editaddressdialog.h \ qt/guiconstants.h \ qt/guiutil.h \ @@ -203,8 +210,10 @@ BITCOIN_QT_H = \ qt/macdockiconhandler.h \ qt/macnotificationhandler.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 \ @@ -291,8 +300,10 @@ RES_ICONS = \ qt/res/icons/tx_input.png \ qt/res/icons/tx_output.png \ qt/res/icons/tx_mined.png \ + qt/res/icons/tx_nameop.png \ qt/res/icons/warning.png \ - qt/res/icons/verify.png + qt/res/icons/verify.png \ + qt/res/icons/transaction_abandoned.png BITCOIN_QT_BASE_CPP = \ qt/bantablemodel.cpp \ @@ -326,7 +337,10 @@ BITCOIN_QT_WALLET_CPP = \ qt/askpassphrasedialog.cpp \ qt/coincontroldialog.cpp \ qt/coincontroltreewidget.cpp \ + qt/configurenamedialog.cpp \ qt/editaddressdialog.cpp \ + qt/managenamespage.cpp \ + qt/nametablemodel.cpp \ qt/openuridialog.cpp \ qt/overviewpage.cpp \ qt/paymentrequestplus.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 230a6ae10c..ed698fa24f 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -94,7 +94,8 @@ BITCOIN_TESTS += \ wallet/test/wallet_test_fixture.h \ wallet/test/accounting_tests.cpp \ wallet/test/wallet_tests.cpp \ - wallet/test/crypto_tests.cpp + wallet/test/crypto_tests.cpp \ + wallet/test/wallet_name_pending_tests.cpp endif test_test_namecoin_SOURCES = $(BITCOIN_TESTS) $(JSON_TEST_FILES) $(RAW_TEST_FILES) diff --git a/src/names/common.h b/src/names/common.h index a5914531ce..e3a2188199 100644 --- a/src/names/common.h +++ b/src/names/common.h @@ -477,4 +477,23 @@ class CNameCache }; +/* This is where we store the the result of name_new, once initiated + by users of the UI (currently ui-only). We store these as strings + so that we can create a UniValue JSON object and send to RPC + name_firstupdate. This also gets written to the wallet as a + JSON string and loaded back as such. */ +struct NameNewReturn +{ + bool ok; + std::string err_msg; + std::string toaddress; + std::string hex; + std::string rand; + std::string data; +}; + +/* Here is where we store our pending name_firstupdates (see above) + while we're waiting for name_new to confirm. */ +typedef std::map MapNameNewReturn; + #endif // H_BITCOIN_NAMES_COMMON diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc index 451d391237..e950ce5e6a 100644 --- a/src/qt/bitcoin.qrc +++ b/src/qt/bitcoin.qrc @@ -35,6 +35,7 @@ res/icons/tx_input.png res/icons/tx_output.png res/icons/tx_inout.png + res/icons/tx_nameop.png res/icons/lock_closed.png res/icons/lock_open.png res/icons/key.png diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 35eec44a44..841c1c8e55 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -13,6 +13,7 @@ #include "guiconstants.h" #include "guiutil.h" #include "modaloverlay.h" +#include "managenamespage.h" #include "networkstyle.h" #include "notificator.h" #include "openuridialog.h" @@ -47,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -317,6 +319,17 @@ void BitcoinGUI::createActions() historyAction->setShortcut(QKeySequence(Qt::ALT + Qt::Key_4)); tabGroup->addAction(historyAction); + manageNamesAction = new QAction(QIcon(":/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(QIcon(":/icons/bitcoin"), 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. @@ -332,6 +345,8 @@ void BitcoinGUI::createActions() connect(receiveCoinsMenuAction, SIGNAL(triggered()), this, SLOT(gotoReceiveCoinsPage())); connect(historyAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); connect(historyAction, SIGNAL(triggered()), this, SLOT(gotoHistoryPage())); + connect(manageNamesAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); + connect(manageNamesAction, SIGNAL(triggered()), this, SLOT(gotoManageNamesPage())); #endif // ENABLE_WALLET quitAction = new QAction(platformStyle->TextColorIcon(":/icons/quit"), tr("E&xit"), this); @@ -466,6 +481,7 @@ void BitcoinGUI::createToolBars() toolbar->addAction(sendCoinsAction); toolbar->addAction(receiveCoinsAction); toolbar->addAction(historyAction); + toolbar->addAction(manageNamesAction); overviewAction->setChecked(true); } } @@ -565,6 +581,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); @@ -612,6 +629,7 @@ void BitcoinGUI::createTrayIconMenu() trayIconMenu->addSeparator(); trayIconMenu->addAction(sendCoinsMenuAction); trayIconMenu->addAction(receiveCoinsMenuAction); + trayIconMenu->addAction(manageNamesMenuAction); trayIconMenu->addSeparator(); trayIconMenu->addAction(signMessageAction); trayIconMenu->addAction(verifyMessageAction); @@ -707,6 +725,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 aa45ea1f0a..8ccf35cdb6 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -97,6 +97,8 @@ class BitcoinGUI : public QMainWindow QAction *sendCoinsMenuAction; QAction *usedSendingAddressesAction; QAction *usedReceivingAddressesAction; + QAction *manageNamesAction; + QAction *manageNamesMenuAction; QAction *signMessageAction; QAction *verifyMessageAction; QAction *aboutAction; @@ -199,6 +201,8 @@ private 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/configurenamedialog.cpp b/src/qt/configurenamedialog.cpp new file mode 100644 index 0000000000..80c96355e9 --- /dev/null +++ b/src/qt/configurenamedialog.cpp @@ -0,0 +1,128 @@ +#include "configurenamedialog.h" +#include "ui_configurenamedialog.h" + +#include "addressbookpage.h" +#include "guiutil.h" +#include "names/main.h" +#include "platformstyle.h" +#include "wallet/wallet.h" +#include "walletmodel.h" + +#include +#include + +ConfigureNameDialog::ConfigureNameDialog(const PlatformStyle *platformStyle, + const QString &_name, const QString &data, + bool _firstUpdate, QWidget *parent) : + QDialog(parent, Qt::WindowSystemMenuHint | Qt::WindowTitleHint), + ui(new Ui::ConfigureNameDialog), + platformStyle(platformStyle), + name(_name), + firstUpdate(_firstUpdate) +{ + ui->setupUi(this); + +#ifdef Q_OS_MAC + ui->transferToLayout->setSpacing(4); +#endif + + GUIUtil::setupAddressWidget(ui->transferTo, this); + + ui->labelName->setText(name); + ui->dataEdit->setText(data); + + returnData = data; + + if (name.startsWith("d/")) + ui->labelDomain->setText(name.mid(2) + ".bit"); + else + ui->labelDomain->setText(tr("(not a domain name)")); + + if (firstUpdate) + { + ui->labelTransferTo->hide(); + ui->labelTransferToHint->hide(); + ui->transferTo->hide(); + ui->addressBookButton->hide(); + ui->pasteButton->hide(); + ui->labelSubmitHint->setText( + tr("name_firstupdate transaction will be queued and broadcasted when corresponding name_new is %1 blocks old") + .arg(MIN_FIRSTUPDATE_DEPTH)); + } + else + { + ui->labelSubmitHint->setText(tr("name_update transaction will be issued immediately")); + setWindowTitle(tr("Update Name")); + } +} + + +ConfigureNameDialog::~ConfigureNameDialog() +{ + delete ui; +} + +void ConfigureNameDialog::accept() +{ + if (!walletModel) + return; + + QString addr; + if (!firstUpdate) + { + if (!ui->transferTo->text().isEmpty() && !ui->transferTo->hasAcceptableInput()) + { + ui->transferTo->setValid(false); + return; + } + + addr = ui->transferTo->text(); + + if (addr != "" && !walletModel->validateAddress(addr)) + { + ui->transferTo->setValid(false); + return; + } + + } + + WalletModel::UnlockContext ctx(walletModel->requestUnlock()); + if (!ctx.isValid()) + return; + + returnData = ui->dataEdit->text(); + if (!firstUpdate) + returnTransferTo = ui->transferTo->text(); + + QDialog::accept(); +} + +void ConfigureNameDialog::setModel(WalletModel *walletModel) +{ + this->walletModel = walletModel; +} + +void ConfigureNameDialog::on_pasteButton_clicked() +{ + // Paste text from clipboard into recipient field + ui->transferTo->setText(QApplication::clipboard()->text()); +} + +void ConfigureNameDialog::on_addressBookButton_clicked() +{ + if (!walletModel) + return; + + AddressBookPage dlg( + // platformStyle + platformStyle, + // mode + AddressBookPage::ForSelection, + // tab + AddressBookPage::SendingTab, + // *parent + this); + dlg.setModel(walletModel->getAddressTableModel()); + if (dlg.exec()) + ui->transferTo->setText(dlg.getReturnValue()); +} diff --git a/src/qt/configurenamedialog.h b/src/qt/configurenamedialog.h new file mode 100644 index 0000000000..a6321d6875 --- /dev/null +++ b/src/qt/configurenamedialog.h @@ -0,0 +1,46 @@ +#ifndef CONFIGURENAMEDIALOG_H +#define CONFIGURENAMEDIALOG_H + +#include "platformstyle.h" + +#include + +namespace Ui { + class ConfigureNameDialog; +} + +class WalletModel; + +/** Dialog for editing an address and associated information. + */ +class ConfigureNameDialog : public QDialog +{ + Q_OBJECT + +public: + + explicit ConfigureNameDialog(const PlatformStyle *platformStyle, + const QString &_name, const QString &data, + bool _firstUpdate, QWidget *parent = 0); + ~ConfigureNameDialog(); + + void setModel(WalletModel *walletModel); + const QString &getReturnData() const { return returnData; } + const QString &getTransferTo() const { return returnTransferTo; } + +public Q_SLOTS: + void accept() override; + void on_addressBookButton_clicked(); + void on_pasteButton_clicked(); + +private: + Ui::ConfigureNameDialog *ui; + const PlatformStyle *platformStyle; + QString returnData; + QString returnTransferTo; + WalletModel *walletModel; + QString name; + bool firstUpdate; +}; + +#endif // CONFIGURENAMEDIALOG_H diff --git a/src/qt/forms/configurenamedialog.ui b/src/qt/forms/configurenamedialog.ui new file mode 100644 index 0000000000..84c0517c18 --- /dev/null +++ b/src/qt/forms/configurenamedialog.ui @@ -0,0 +1,274 @@ + + + ConfigureNameDialog + + + + 0 + 0 + 545 + 245 + + + + + 0 + 0 + + + + Configure Name + + + + + + QFormLayout::AllNonFixedFieldsGrow + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Name: + + + dataEdit + + + + + + + TextLabel + + + + + + + &Data: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + dataEdit + + + + + + + Enter JSON string that will be associated with the name + + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;"> +<p style=" margin:0px; -qt-block-indent:0; text-indent:0px;">JSON string, e.g. {&quot;ns&quot;: [&quot;1.2.3.4&quot;, &quot;1.2.3.5&quot;]}</p> +<p style=" margin:0px; -qt-block-indent:0; text-indent:0px;">See <a href="https://wiki.namecoin.info/index.php?title=Register_and_Configure_.bit_Domains"><span style=" text-decoration: underline; color:#0000ff;">How to Register and Configure Bit Domains</span></a></p></body></html> + + + + + + + &Transfer to: + + + transferTo + + + + + + + 0 + + + QLayout::SetDefaultConstraint + + + + + The Namecoin address to transfer domain to +(e.g. N1KHAL5C1CRzy58NdJwp1tbLze3XrkFxx9). +Leave empty, if not needed. + + + 34 + + + + + + + Choose address from address book + + + + + + + :/icons/address-book:/icons/address-book + + + Alt+A + + + + + + + Paste address from clipboard + + + + + + + :/icons/editpaste:/icons/editpaste + + + Alt+P + + + + + + + + + Qt::Vertical + + + + 20 + 16 + + + + + + + + Domain name: + + + + + + + TextLabel + + + + + + + Qt::Vertical + + + + 20 + 16 + + + + + + + + (can be left empty) + + + + + + + + + TextLabel + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 10 + + + 10 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + + + + + + + + QValidatedLineEdit + QLineEdit +
../../src/qt/qvalidatedlineedit.h
+
+
+ + + + + + buttonBox + accepted() + ConfigureNameDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + +
diff --git a/src/qt/forms/managenamespage.ui b/src/qt/forms/managenamespage.ui new file mode 100644 index 0000000000..7a8039a8f7 --- /dev/null +++ b/src/qt/forms/managenamespage.ui @@ -0,0 +1,243 @@ + + + ManageNamesPage + + + + 0 + 0 + 776 + 364 + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + + + + 0 + 0 + + + + &New name: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + registerName + + + + + + + Enter a name or domain name (prefixed with d/) to be registered via Namecoin. + + + d/ + + + + + + + + 0 + 0 + + + + <html><head/><body><p>Use <span style=" font-weight:600;">d/</span> prefix for domain names. E.g. <span style=" font-weight:600;">d/mysite</span> will register <span style=" font-weight:600;">mysite.bit</span></p><p>See <a href="https://wiki.namecoin.info/index.php?title=Domain_Name_Specification"><span style=" text-decoration: underline; color:#0000ff;">Domain names</span></a> in Namecoin wiki for reference. Other prefixes can be used for miscellaneous purposes (not domain names).</p></body></html> + + + Qt::RichText + + + true + + + true + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + Confirm the new name action. Sends name_new transaction +to the network and creates a pending name_firstupdate transaction. + + + &Submit + + + + :/icons/send:/icons/send + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Maximum + + + + 20 + 12 + + + + + + + + + 0 + 0 + + + + Your registered names (pending and unconfirmed names have blank expiration): + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + registerName + + + + + + + Qt::CustomContextMenu + + + Double-click name to configure + + + false + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + false + + + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + Configure name and submit update operation + + + &Configure Name... + + + false + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + Renew the name with its current value + + + &Renew Name + + + false + + + + + + + + + + + + + QValidatedLineEdit + QLineEdit +
../../src/qt/qvalidatedlineedit.h
+
+
+ + + + +
diff --git a/src/qt/managenamespage.cpp b/src/qt/managenamespage.cpp new file mode 100644 index 0000000000..7cd9ae0e3e --- /dev/null +++ b/src/qt/managenamespage.cpp @@ -0,0 +1,323 @@ +#include "managenamespage.h" +#include "ui_managenamespage.h" + +#include "base58.h" +#include "configurenamedialog.h" +#include "csvmodelwriter.h" +#include "guiutil.h" +#include "names/common.h" +#include "nametablemodel.h" +#include "platformstyle.h" +#include "ui_interface.h" +#include "util.h" +#include "validation.h" // cs_main +#include "wallet/wallet.h" +#include "walletmodel.h" + +#include + +#include +#include +#include + +ManageNamesPage::ManageNamesPage(const PlatformStyle *platformStyle, QWidget *parent) : + QWidget(parent), + platformStyle(platformStyle), + ui(new Ui::ManageNamesPage), + model(0), + walletModel(0), + proxyModel(0) +{ + ui->setupUi(this); + + // Context menu actions + QAction *copyNameAction = new QAction(tr("Copy &Name"), this); + QAction *copyValueAction = new QAction(tr("Copy &Value"), this); + QAction *configureNameAction = new QAction(tr("&Configure Name..."), this); + QAction *renewNameAction = new QAction(tr("&Renew Name"), this); + + // Build context menu + contextMenu = new QMenu(); + contextMenu->addAction(copyNameAction); + contextMenu->addAction(copyValueAction); + contextMenu->addAction(configureNameAction); + contextMenu->addAction(renewNameAction); + + // Connect signals for context menu actions + connect(copyNameAction, SIGNAL(triggered()), this, SLOT(onCopyNameAction())); + connect(copyValueAction, SIGNAL(triggered()), this, SLOT(onCopyValueAction())); + connect(configureNameAction, SIGNAL(triggered()), this, SLOT(on_configureNameButton_clicked())); + connect(renewNameAction, SIGNAL(triggered()), this, SLOT(on_renewNameButton_clicked())); + + connect(ui->tableView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextualMenu(QPoint))); + connect(ui->tableView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(on_configureNameButton_clicked())); + ui->tableView->setEditTriggers(QAbstractItemView::NoEditTriggers); + + ui->registerName->installEventFilter(this); + 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(); +} + +void ManageNamesPage::on_submitNameButton_clicked() +{ + if (!walletModel) + return; + + QString name = ui->registerName->text(); + + QString reason; + if (!walletModel->nameAvailable(name, &reason)) + { + if(reason.isEmpty()) + QMessageBox::warning(this, tr("Name registration"), tr("Name not available")); + else + QMessageBox::warning(this, tr("Name registration"), tr("Name not available
Reason: %1").arg(reason)); + + ui->registerName->setFocus(); + return; + } + + QString msg; + if (name.startsWith("d/")) + msg = tr("Are you sure you want to register domain name %1, which corresponds to domain %2?

NOTE: If your wallet is locked, you will be prompted to unlock it in 12 blocks.").arg(name).arg(name.mid(2) + ".bit"); + else + msg = tr("Are you sure you want to register non-domain name %1?

NOTE: If your wallet is locked, you will be prompted to unlock it in 12 blocks.").arg(name); + + if (QMessageBox::Yes != QMessageBox::question(this, tr("Confirm name registration"), msg, QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel)) + return; + + WalletModel::UnlockContext ctx(walletModel->requestUnlock()); + if (!ctx.isValid()) + return; + + std::string strName = name.toStdString(); + + NameNewReturn res = walletModel->nameNew(name); + if (!res.ok) + { + QMessageBox::warning(this, tr("Name registration failed"), QString::fromStdString(res.err_msg)); + return; + } + + // reset UI text + ui->registerName->setText("d/"); + ui->submitNameButton->setDefault(true); + + ConfigureNameDialog dlg(platformStyle, name, "", true, this); + dlg.setModel(walletModel); + if (dlg.exec() != QDialog::Accepted) + return; + + QString data = dlg.getReturnData(); + std::string strData = data.toStdString(); + + UniValue jsonData(UniValue::VOBJ); + jsonData.pushKV ("txid", res.hex); + jsonData.pushKV ("rand", res.rand); + jsonData.pushKV ("data", strData); + if (!res.toaddress.empty ()) + jsonData.pushKV ("toaddress", res.toaddress); + + walletModel->writePendingNameFirstUpdate(strName, res.rand, res.hex, strData, res.toaddress); + + int newRowIndex; + model->updateEntry(name, dlg.getReturnData(), NameTableEntry::NAME_NEW, CT_NEW, &newRowIndex); + + ui->tableView->selectRow(newRowIndex); + ui->tableView->setFocus(); + + return; +} + +bool ManageNamesPage::eventFilter(QObject *object, QEvent *event) +{ + if (event->type() == QEvent::FocusIn) + { + if (object == ui->registerName) + { + ui->submitNameButton->setDefault(true); + ui->configureNameButton->setDefault(false); + } + else if (object == ui->tableView) + { + ui->submitNameButton->setDefault(false); + ui->configureNameButton->setDefault(true); + } + } + return QWidget::eventFilter(object, event); +} + +void ManageNamesPage::selectionChanged() +{ + // Set button states based on selected tab and selection + QTableView *table = ui->tableView; + if (!table->selectionModel()) + return; + + const bool state = table->selectionModel()->hasSelection(); + ui->configureNameButton->setEnabled(state); + ui->renewNameButton->setEnabled(state); +} + +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::on_configureNameButton_clicked() +{ + if (!ui->tableView->selectionModel()) + return; + const QModelIndexList &indexes = ui->tableView->selectionModel()->selectedRows(NameTableModel::Name); + if (indexes.isEmpty()) + return; + + const QModelIndex &index = indexes.at(0); + const QString &name = index.data(Qt::EditRole).toString(); + const std::string &strName = name.toStdString(); + const QString &value = index.sibling(index.row(), NameTableModel::Value).data(Qt::EditRole).toString(); + bool fFirstUpdate = walletModel->pendingNameFirstUpdateExists(strName); + + ConfigureNameDialog dlg(platformStyle, name, value, fFirstUpdate, this); + dlg.setModel(walletModel); + if (dlg.exec() != QDialog::Accepted) + return; + + const std::string &strData = dlg.getReturnData().toStdString(); + + if(fFirstUpdate) + { + // update pending first + NameNewReturn res = walletModel->getPendingNameFirstUpdate(strName); + walletModel->writePendingNameFirstUpdate(strName, res.rand, res.hex, strData, res.toaddress); + LogPrintf("configure:changing updating pending name_firstupdate name=%s rand=%s tx=%s value=%s\n", + strName.c_str(), res.rand.c_str(), res.hex.c_str(), strData.c_str()); + } + else + { + const QString &transferToAddress = dlg.getTransferTo(); + QString result = walletModel->nameUpdate(name, value, transferToAddress); + if (!result.isEmpty()) + { + QMessageBox::warning(this, tr("Name update"), tr("Unable to update name.
Reason: %1").arg(result)); + return; + } + } + + model->updateEntry(name, dlg.getReturnData(), NameTableEntry::NAME_NEW, CT_UPDATED); +} + +void ManageNamesPage::on_renewNameButton_clicked () +{ + if (!ui->tableView->selectionModel()) + return; + const QModelIndexList &indexes = ui->tableView->selectionModel()->selectedRows(NameTableModel::Name); + if (indexes.isEmpty()) + return; + + const QModelIndex &index = indexes.at(0); + const QString &name = index.data(Qt::EditRole).toString(); + const QString &value = index.sibling(index.row(), NameTableModel::Value).data(Qt::EditRole).toString(); + + // TODO: Warn if the "expires in" value is still high + const QString msg + = tr ("Are you sure you want to renew the name %1?") + .arg (GUIUtil::HtmlEscape (name)); + const QString title = tr ("Confirm name renewal"); + + QMessageBox::StandardButton res; + res = QMessageBox::question (this, title, msg, + QMessageBox::Yes | QMessageBox::Cancel, + QMessageBox::Cancel); + if (res != QMessageBox::Yes) + return; + + WalletModel::UnlockContext ctx(walletModel->requestUnlock ()); + if (!ctx.isValid ()) + return; + + const QString err_msg = walletModel->nameUpdate(name, value, ""); + + if (err_msg.isEmpty() || err_msg == "ABORTED") + return; + + QMessageBox::critical(this, tr("Name update error"), err_msg); +} + +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); + + 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..8d54b1813e --- /dev/null +++ b/src/qt/managenamespage.h @@ -0,0 +1,60 @@ +#ifndef MANAGENAMESPAGE_H +#define MANAGENAMESPAGE_H + +#include "platformstyle.h" + +#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 = 0); + ~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: + void on_submitNameButton_clicked(); + + bool eventFilter(QObject *object, QEvent *event); + void selectionChanged(); + + /** Spawn contextual menu (right mouse menu) for name table entry */ + void contextualMenu(const QPoint &point); + + void onCopyNameAction(); + void onCopyValueAction(); + void on_configureNameButton_clicked(); + void on_renewNameButton_clicked(); +}; + +#endif // MANAGENAMESPAGE_H diff --git a/src/qt/nametablemodel.cpp b/src/qt/nametablemodel.cpp new file mode 100644 index 0000000000..c78f461ea0 --- /dev/null +++ b/src/qt/nametablemodel.cpp @@ -0,0 +1,563 @@ +#include "nametablemodel.h" + +#include "guiutil.h" +#include "walletmodel.h" +#include "guiconstants.h" +#include "wallet/wallet.h" +#include "ui_interface.h" +#include "platformstyle.h" + +#include "names/common.h" +#include "util.h" +#include "protocol.h" +#include "rpc/server.h" +#include "validation.h" // 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 + }; +} + +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; + + // unconfirmed (name_pending) names + JSONRPCRequest namePendingRequest; + namePendingRequest.strMethod = "name_pending"; + namePendingRequest.params = NullUniValue; + namePendingRequest.fHelp = false; + UniValue pendingNames; + + try { + pendingNames = tableRPC.execute(namePendingRequest); + } catch (const UniValue& e) { + UniValue message = find_value( e, "message"); + LogPrintf ("name_pending lookup error: %s\n", message.get_str().c_str()); + } + + if(pendingNames.isArray()) + { + for (const auto& v : pendingNames.getValues()) + { + std::string name = find_value ( v, "name").get_str(); + std::string data = find_value ( v, "value").get_str(); + vNamesO[name] = NameTableEntry(name, data, NameTableEntry::NAME_UNCONFIRMED); + LogPrintf("found pending name: name=%s\n", name.c_str()); + } + } + + // confirmed names (name_list) + JSONRPCRequest nameListRequest; + nameListRequest.strMethod = "name_list"; + nameListRequest.params = NullUniValue; + nameListRequest.fHelp = false; + UniValue confirmedNames; + + try { + confirmedNames = tableRPC.execute(nameListRequest); + } catch (const UniValue& e) { + 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()) + { + //const UniValue& v = confirmedNames[idx]; + 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); + 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); + + // Add pending names (name_new) + LOCK(wallet->cs_wallet); + for (const auto& item : wallet->pendingNameFirstUpdate) + cachedNameTable.append( + NameTableEntry(item.first, + item.second.data, + NameTableEntry::NAME_NEW)); + + // 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) + { + LOCK(cs_main); + + NameTableEntry nameObj(ValtypeToString(inName), "", NameTableEntry::NAME_NON_EXISTING); + std::string strName = ValtypeToString(inName); + + 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; + } + const int height = heightResult.get_int(); + + UniValue valResult = find_value(res, "value"); + if (!valResult.isStr()) + { + LogPrintf ("No value for name %s\n", strName.c_str()); + return; + } + + std::string data = valResult.get_str(); + + nameObj = NameTableEntry(strName, data, height); + + 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); + } + else + { + LogPrintf("refreshName result : %s - deleted from the table\n", qPrintable(nameObj.name)); + updateEntry(nameObj.name, nameObj.value, nameObj.nHeight, CT_DELETED); + } + } + 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); + } + 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, 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)); + 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; + 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; + } + } +}; + +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"); + priv->refreshNameTable(); + + QTimer *timer = new QTimer(this); + 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); + if(!item->HeightValid()) + continue; // Currently, unconfirmed names do not expire in the table + + const Consensus::Params& params = Params().GetConsensus(); + int nHeight = item->nHeight; + int expirationDepth = params.rules->NameExpirationDepth(nHeight); + + if(nHeight + expirationDepth <= nBestHeight) + { + 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); + + // 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 + std::map::iterator mi = wallet->mapWallet.find(hash256); + if(mi == wallet->mapWallet.end()) + { + LogPrintf ("tx %s has no name in wallet\n", strHash); + return; + } + CTransaction tx = mi->second; + + const auto &vout = tx.vout; + for (const auto it : vout) + { + if(!CNameScript::isNameScript(it.scriptPubKey)) + { + continue; + } + + CNameScript nameScript(it.scriptPubKey); + switch (nameScript.getNameOp()) + { + case OP_NAME_NEW: + break; + + case OP_NAME_FIRSTUPDATE: + case OP_NAME_UPDATE: + priv->refreshName(nameScript.getOpName()); + 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()); + + 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; + } + } + 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); + if(priv->index(row)) + 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 + 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, int *outNewRowIndex /*= NULL*/) +{ + priv->updateEntry(name, value, nHeight, status, 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..1790f6f122 --- /dev/null +++ b/src/qt/nametablemodel.h @@ -0,0 +1,86 @@ +#ifndef NAMETABLEMODEL_H +#define NAMETABLEMODEL_H + +#include "bitcoinunits.h" + +#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 = 0); + virtual ~NameTableModel(); + + enum ColumnIndex { + Name = 0, + Value = 1, + ExpiresIn = 2 + }; + + /** @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::shared_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, int *outNewRowIndex=nullptr); + void updateExpiration(); + void updateTransaction(const QString &hash, int status); + + friend class NameTablePriv; +}; + +struct NameTableEntry +{ + QString name; + QString value; + int nHeight; + + 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): + name(name), value(value), nHeight(nHeight) {} + NameTableEntry(const std::string &name, const std::string &value, int nHeight): + name(QString::fromStdString(name)), value(QString::fromStdString(value)), nHeight(nHeight) {} +}; + +#endif // NAMETABLEMODEL_H diff --git a/src/qt/res/icons/tx_nameop.png b/src/qt/res/icons/tx_nameop.png new file mode 100644 index 0000000000000000000000000000000000000000..4cddf1edb2eae6cfa470125709244f1c1471a0b8 GIT binary patch literal 1136 zcmV-$1dscPP)2hvoi198I3Z?ESI4l6mr*mjP!F09 zR!|Xz7+9c8V^%7Np&z!k@ z-#2+4eEIg%T6^!c&wsDI&f4pIUnX*OTeqwH`~pA$pa4(+C;$`y6m{PHMQ5Euy=`8IHlHd-Bog64 z3-a1@x6CVwwUiv+qyfly%}qhd$bdPf=COLj%wR*4?P&l8n$)f*Qf#HcIjyE7RzG4# z8i2bJkNQ_b*wqj#^vG&cVpKx=)$ud{K$Bby}ifn%P-x%HxoY$aVl-1IVZh zdxpbm&CwWQVUh7!tVX*owm)ZE8h{~2CbZ4i=DncxQ)57F`}K-Slwg!wE8pvL*1UxGMMf~u*r%5Ri;MY#H=C@#K7#fKMlYb*LLW; zyyJKL7>;40HxuK7RV1{ZHKhTljHTAk$U#2R4~=F!5&Gts5|39H7<;TaR|Mn-xIU53 zGQRX}kSCZ2c9|EzdRy-1Fgh_20Y`IR-1z`1+C=jPGaV1UsP+U{Yi$@Uw^T8D8k4Zu z<7gUy%0#yR$6klb6Um^LWd`Gt`Syev)6BxHh9_32%@qOp0E*h=fW7u%tu-K0ZnXmISFpjZ$1N*MWGv^=`;> z0J#B*+C}r>gjVA#5E*GPXfW5sb}O7wmj+;%io|ZVwpsqX>Nvs9@I2VyL&J!CXx;$XFUu?;_u|xth1fv2sNK6B6>FPKwG+R)I*VRr=tp)oo^M&G-0R z00Z0~qzJz_*~y%@Y|(N^lPm;xBt}p8Ee$}qVL{5sR#Kc*6<51e9>>Fp(H;3CAQwQT zzHvY`W{u{Z=4(czr==c^%l8O%y6`h>c7U;3-+=u&N{ZFig_*;xQyP2iV(x1=A3$jw z0Z^Yan%}TD?0Jx0aqkhorvbPpd<(X?^Xr^zoG{Co4wdou{!9ZfT6dR|%4cyIlCAyV z?SL8un}a^+N(5vFcnmEC)#s`^q}Yyaxs6)Cop*ayLsuT3{RfIu>O~G)Z+-59BD70h z*GGTN8r52py9!FSOyLiU0zd(v08juZ02BbOHo$-3umH=vykPGD0000 TransactionRecord::decomposeTransaction(const CWallet * if(fAllFromMe > mine) fAllFromMe = mine; } + // if we find a name script we put it here. display in transaction + // records hand off the boolean, foundNameOp + bool foundNameOp = false; + CNameScript nameScript; + std::string nameAddress; + isminetype fAllToMe = ISMINE_SPENDABLE; for (const CTxOut& txout : wtx.tx->vout) { isminetype mine = wallet->IsMine(txout); if(mine & ISMINE_WATCH_ONLY) involvesWatchAddress = true; if(fAllToMe > mine) fAllToMe = mine; + + // check txout for nameop + const CNameScript cur(txout.scriptPubKey); + CTxDestination address; + if(cur.isNameOp ()) + { + foundNameOp = true; + nameScript = cur; + ExtractDestination(txout.scriptPubKey, address); + nameAddress = EncodeDestination(address); + } } if (fAllFromMe && fAllToMe) @@ -97,8 +114,23 @@ QList TransactionRecord::decomposeTransaction(const CWallet * // Payment to self CAmount nChange = wtx.GetChange(); - parts.append(TransactionRecord(hash, nTime, TransactionRecord::SendToSelf, "", - -(nDebit - nChange), nCredit - nChange)); + if(foundNameOp) + { + std::string opName = GetOpName(nameScript.getNameOp()); + std::string description = nameAddress + " " + opName.substr(3); + + if(nameScript.isAnyUpdate()) + description += " " + ValtypeToString(nameScript.getOpName()); + + parts.append(TransactionRecord(hash, nTime, TransactionRecord::NameOp, description, + -(nDebit - nChange), nCredit - nChange)); + } + else + { + parts.append(TransactionRecord(hash, nTime, TransactionRecord::SendToSelf, "", + -(nDebit - nChange), nCredit - nChange)); + } + parts.last().involvesWatchAddress = involvesWatchAddress; // maybe pass to TransactionRecord as constructor argument } else if (fAllFromMe) diff --git a/src/qt/transactionrecord.h b/src/qt/transactionrecord.h index a26e676142..c3a1abb01d 100644 --- a/src/qt/transactionrecord.h +++ b/src/qt/transactionrecord.h @@ -79,7 +79,8 @@ class TransactionRecord SendToOther, RecvWithAddress, RecvFromOther, - SendToSelf + SendToSelf, + NameOp, }; /** Number of confirmation recommended for accepting a transaction */ diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp index 59cef555b1..eea2cac706 100644 --- a/src/qt/transactiontablemodel.cpp +++ b/src/qt/transactiontablemodel.cpp @@ -382,6 +382,8 @@ QString TransactionTableModel::formatTxType(const TransactionRecord *wtx) const return tr("Payment to yourself"); case TransactionRecord::Generated: return tr("Mined"); + case TransactionRecord::NameOp: + return tr("Name operation"); default: return QString(); } @@ -399,6 +401,8 @@ QVariant TransactionTableModel::txAddressDecoration(const TransactionRecord *wtx case TransactionRecord::SendToAddress: case TransactionRecord::SendToOther: return QIcon(":/icons/tx_output"); + case TransactionRecord::NameOp: + return QIcon(":/icons/tx_nameop"); default: return QIcon(":/icons/tx_inout"); } @@ -421,6 +425,7 @@ QString TransactionTableModel::formatTxToAddress(const TransactionRecord *wtx, b case TransactionRecord::Generated: return lookupAddress(wtx->address, tooltip) + watchAddress; case TransactionRecord::SendToOther: + case TransactionRecord::NameOp: return QString::fromStdString(wtx->address) + watchAddress; case TransactionRecord::SendToSelf: default: @@ -513,7 +518,8 @@ QString TransactionTableModel::formatTooltip(const TransactionRecord *rec) const { QString tooltip = formatTxStatus(rec) + QString("\n") + formatTxType(rec); if(rec->type==TransactionRecord::RecvFromOther || rec->type==TransactionRecord::SendToOther || - rec->type==TransactionRecord::SendToAddress || rec->type==TransactionRecord::RecvWithAddress) + rec->type==TransactionRecord::SendToAddress || rec->type==TransactionRecord::RecvWithAddress || + rec->type==TransactionRecord::NameOp) { tooltip += QString(" ") + formatTxToAddress(rec, true); } diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp index 39dfdb587c..bbeec5f5cb 100644 --- a/src/qt/transactionview.cpp +++ b/src/qt/transactionview.cpp @@ -91,6 +91,7 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa TransactionFilterProxy::TYPE(TransactionRecord::SendToOther)); typeWidget->addItem(tr("To yourself"), TransactionFilterProxy::TYPE(TransactionRecord::SendToSelf)); typeWidget->addItem(tr("Mined"), TransactionFilterProxy::TYPE(TransactionRecord::Generated)); + typeWidget->addItem(tr("Name operation"), TransactionFilterProxy::TYPE(TransactionRecord::NameOp)); typeWidget->addItem(tr("Other"), TransactionFilterProxy::TYPE(TransactionRecord::Other)); hlayout->addWidget(typeWidget); diff --git a/src/qt/walletframe.cpp b/src/qt/walletframe.cpp index 714a594318..a86289f1e2 100644 --- a/src/qt/walletframe.cpp +++ b/src/qt/walletframe.cpp @@ -87,9 +87,8 @@ bool WalletFrame::removeWallet(const QString &name) void WalletFrame::removeAllWallets() { - QMap::const_iterator i; - for (i = mapWalletViews.constBegin(); i != mapWalletViews.constEnd(); ++i) - walletStack->removeWidget(i.value()); + for (const auto& i : mapWalletViews) + walletStack->removeWidget(i); mapWalletViews.clear(); } @@ -105,37 +104,38 @@ bool WalletFrame::handlePaymentRequest(const SendCoinsRecipient &recipient) void WalletFrame::showOutOfSyncWarning(bool fShow) { bOutOfSync = fShow; - QMap::const_iterator i; - for (i = mapWalletViews.constBegin(); i != mapWalletViews.constEnd(); ++i) - i.value()->showOutOfSyncWarning(fShow); + for (const auto& i : mapWalletViews) + i->showOutOfSyncWarning(fShow); } void WalletFrame::gotoOverviewPage() { - QMap::const_iterator i; - for (i = mapWalletViews.constBegin(); i != mapWalletViews.constEnd(); ++i) - i.value()->gotoOverviewPage(); + for (const auto& i : mapWalletViews) + i->gotoOverviewPage(); } void WalletFrame::gotoHistoryPage() { - QMap::const_iterator i; - for (i = mapWalletViews.constBegin(); i != mapWalletViews.constEnd(); ++i) - i.value()->gotoHistoryPage(); + for (const auto& i : mapWalletViews) + i->gotoHistoryPage(); } void WalletFrame::gotoReceiveCoinsPage() { - QMap::const_iterator i; - for (i = mapWalletViews.constBegin(); i != mapWalletViews.constEnd(); ++i) - i.value()->gotoReceiveCoinsPage(); + for (const auto& i : mapWalletViews) + i->gotoReceiveCoinsPage(); } void WalletFrame::gotoSendCoinsPage(QString addr) { - QMap::const_iterator i; - for (i = mapWalletViews.constBegin(); i != mapWalletViews.constEnd(); ++i) - i.value()->gotoSendCoinsPage(addr); + for (const auto& i : mapWalletViews) + i->gotoSendCoinsPage(addr); +} + +void WalletFrame::gotoManageNamesPage() +{ + for(const auto &i : mapWalletViews) + i->gotoManageNamesPage(); } void WalletFrame::gotoSignMessageTab(QString addr) diff --git a/src/qt/walletframe.h b/src/qt/walletframe.h index 42ce69fea1..2bb9123d0a 100644 --- a/src/qt/walletframe.h +++ b/src/qt/walletframe.h @@ -70,6 +70,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 e82add410d..7353bdf800 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -8,6 +8,7 @@ #include "consensus/validation.h" #include "guiconstants.h" #include "guiutil.h" +#include "nametablemodel.h" #include "optionsmodel.h" #include "paymentserver.h" #include "recentrequeststablemodel.h" @@ -17,6 +18,10 @@ #include "base58.h" #include "chain.h" #include "keystore.h" +#include "names/common.h" +#include "names/main.h" +#include "rpc/server.h" +#include "rpc/client.h" #include "validation.h" #include "net.h" // for g_connman #include "policy/fees.h" @@ -36,10 +41,14 @@ #include #include +#include +#include +#include -WalletModel::WalletModel(const PlatformStyle *platformStyle, CWallet *_wallet, OptionsModel *_optionsModel, QObject *parent) : - QObject(parent), wallet(_wallet), optionsModel(_optionsModel), addressTableModel(0), +WalletModel::WalletModel(const PlatformStyle *platformStyle, CWallet *_wallet, OptionsModel *_optionsModel, QWidget *parent) : + QWidget(parent), wallet(_wallet), optionsModel(_optionsModel), addressTableModel(0), transactionTableModel(0), + nameTableModel(0), recentRequestsTableModel(0), cachedBalance(0), cachedUnconfirmedBalance(0), cachedImmatureBalance(0), cachedEncryptionStatus(Unencrypted), @@ -50,6 +59,7 @@ WalletModel::WalletModel(const PlatformStyle *platformStyle, CWallet *_wallet, O addressTableModel = new AddressTableModel(wallet, this); transactionTableModel = new TransactionTableModel(platformStyle, wallet, this); + nameTableModel = new NameTableModel(platformStyle, wallet, this); recentRequestsTableModel = new RecentRequestsTableModel(wallet, this); // This timer will be fired repeatedly to update the balance @@ -119,10 +129,7 @@ void WalletModel::pollBalanceChanged() // periodical polls if the core is holding the locks for a longer time - // for example, during a wallet rescan. TRY_LOCK(cs_main, lockMain); - if(!lockMain) - return; - TRY_LOCK(wallet->cs_wallet, lockWallet); - if(!lockWallet) + if (!lockMain) return; if(fForceCheckBalanceChanged || chainActive.Height() != cachedNumBlocks) @@ -135,6 +142,11 @@ void WalletModel::pollBalanceChanged() checkBalanceChanged(); if(transactionTableModel) transactionTableModel->updateConfirmations(); + + std::vector> successfulNames = sendPendingNameFirstUpdates(); + for (const auto& name : successfulNames) + wallet->ErasePendingNameFirstUpdate(name); + } } @@ -278,7 +290,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact CReserveKey *keyChange = transaction.getPossibleKeyChange(); bool fCreated = wallet->CreateTransaction(vecSend, NULL, *newTx, *keyChange, nFeeRequired, nChangePosRet, strFailReason, coinControl); transaction.setTransactionFee(nFeeRequired); - if (fSubtractFeeFromAmount && fCreated) + if(fSubtractFeeFromAmount && fCreated) transaction.reassignAmounts(nChangePosRet); if(!fCreated) @@ -295,7 +307,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact // reject absurdly high fee. (This can never happen because the // wallet caps the fee at maxTxFee. This merely serves as a // belt-and-suspenders check) - if (nFeeRequired > maxTxFee) + if(nFeeRequired > maxTxFee) return AbsurdFee; } @@ -310,9 +322,9 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(WalletModelTransaction &tran LOCK2(cs_main, wallet->cs_wallet); CWalletTx *newTx = transaction.getTransaction(); - for (const SendCoinsRecipient &rcp : transaction.getRecipients()) + for(const SendCoinsRecipient &rcp : transaction.getRecipients()) { - if (rcp.paymentRequest.IsInitialized()) + if(rcp.paymentRequest.IsInitialized()) { // Make sure any payment requests involved are still valid. if (PaymentServer::verifyExpired(rcp.paymentRequest.getDetails())) { @@ -382,6 +394,12 @@ AddressTableModel *WalletModel::getAddressTableModel() return addressTableModel; } + +NameTableModel *WalletModel::getNameTableModel() +{ + return nameTableModel; +} + TransactionTableModel *WalletModel::getTransactionTableModel() { return transactionTableModel; @@ -394,11 +412,11 @@ RecentRequestsTableModel *WalletModel::getRecentRequestsTableModel() WalletModel::EncryptionStatus WalletModel::getEncryptionStatus() const { - if(!wallet->IsCrypted()) + if (!wallet->IsCrypted()) { return Unencrypted; } - else if(wallet->IsLocked()) + else if (wallet->IsLocked()) { return Locked; } @@ -410,7 +428,7 @@ WalletModel::EncryptionStatus WalletModel::getEncryptionStatus() const bool WalletModel::setWalletEncrypted(bool encrypted, const SecureString &passphrase) { - if(encrypted) + if (encrypted) { // Encrypt return wallet->EncryptWallet(passphrase); @@ -424,7 +442,7 @@ bool WalletModel::setWalletEncrypted(bool encrypted, const SecureString &passphr bool WalletModel::setWalletLocked(bool locked, const SecureString &passPhrase) { - if(locked) + if (locked) { // Lock return wallet->Lock(); @@ -522,7 +540,7 @@ void WalletModel::unsubscribeFromCoreSignals() WalletModel::UnlockContext WalletModel::requestUnlock() { bool was_locked = getEncryptionStatus() == Locked; - if(was_locked) + if (was_locked) { // Request UI to unlock wallet Q_EMIT requireUnlock(); @@ -542,7 +560,7 @@ WalletModel::UnlockContext::UnlockContext(WalletModel *_wallet, bool _valid, boo WalletModel::UnlockContext::~UnlockContext() { - if(valid && relock) + if (valid && relock) { wallet->setWalletLocked(true); } @@ -709,7 +727,7 @@ bool WalletModel::bumpFee(uint256 hash) } WalletModel::UnlockContext ctx(requestUnlock()); - if(!ctx.isValid()) + if (!ctx.isValid()) { return false; } @@ -729,7 +747,7 @@ bool WalletModel::bumpFee(uint256 hash) LOCK2(cs_main, wallet->cs_wallet); res = feeBump->commit(wallet); } - if(!res) { + if (!res) { QMessageBox::critical(0, tr("Fee bump error"), tr("Could not commit transaction") + "
(" + QString::fromStdString(feeBump->getErrors()[0])+")"); return false; @@ -756,3 +774,257 @@ bool WalletModel::getDefaultWalletRbf() const { return fWalletRbf; } + +bool WalletModel::nameAvailable(const QString &name, QString *reason) +{ + UniValue res, isExpired; + + const std::string strName = name.toStdString(); + + UniValue params (UniValue::VOBJ); + params.pushKV ("name", strName); + + JSONRPCRequest jsonRequest; + jsonRequest.strMethod = "name_show"; + jsonRequest.params = params; + jsonRequest.fHelp = false; + + try { + res = tableRPC.execute(jsonRequest); + } catch (const UniValue& e) { + // Make sure we have the correct error response and not something else + UniValue message = find_value( e, "message"); + std::string errorStr = message.get_str(); + + if (errorStr.find("name not found") != std::string::npos) + return true; + + if(reason) + *reason = QString::fromStdString(errorStr); + + LogPrintf ("unexpected nameAvailable response: %s\n", errorStr.c_str()); + return false; + } + + isExpired = find_value( res, "expired"); + if (isExpired.get_bool()) + return true; + + return false; +} + +NameNewReturn WalletModel::nameNew(const QString &name) +{ + std::string strName = name.toStdString (); + std::string data; + + std::vector values; + + UniValue params(UniValue::VOBJ); + params.pushKV("name", strName); + + JSONRPCRequest jsonRequest; + jsonRequest.strMethod = "name_new"; + jsonRequest.params = params; + jsonRequest.fHelp = false; + + NameNewReturn retval; + UniValue res; + try { + res = tableRPC.execute(jsonRequest); + } catch (const UniValue& e) { + UniValue message = find_value( e, "message"); + std::string errorStr = message.get_str(); + LogPrintf ("nameNew error: %s\n", errorStr.c_str()); + retval.err_msg = errorStr; + retval.ok = false; + return retval; + } + + values = res.getValues(); + const auto txid = values[0]; + const auto rand = values[1]; + + retval.ok = true; + retval.hex = txid.get_str(); // txid + retval.rand = rand.get_str(); + + return retval; +} + +const std::string WalletModel::completePendingNameFirstUpdate(const std::string &name, const std::string &rand, const std::string &txid, const std::string &data, const std::string &toaddress) +{ + std::string errorStr; + LogPrintf ("WalletModel::completePendingNameFirstUpdate: %s\n", name.c_str()); + + UniValue params(UniValue::VOBJ); + params.pushKV ("name", name); + params.pushKV ("rand", rand); + params.pushKV ("tx", txid); + params.pushKV ("value", data); + if (!toaddress.empty()) + params.pushKV ("toaddress", toaddress); + + JSONRPCRequest jsonRequest; + jsonRequest.strMethod = "name_firstupdate"; + jsonRequest.params = params; + jsonRequest.fHelp = false; + + LogPrintf("executing name_firstupdate name=%s rand=%s tx=%s value=%s\n", + name.c_str(), rand.c_str(), txid.c_str(), data.c_str()); + + UniValue res; + try { + res = tableRPC.execute(jsonRequest); + } + catch (const UniValue& e) { + UniValue message = find_value( e, "message"); + errorStr = message.get_str(); + LogPrintf ("name_firstupdate error: %s\n", errorStr.c_str()); + } + + return errorStr; +} + +std::vector> WalletModel::sendPendingNameFirstUpdates() +{ + std::vector> successfulNames; + + LOCK(wallet->cs_wallet); + for (const auto& i : wallet->pendingNameFirstUpdate) + { + JSONRPCRequest jsonRequest; + UniValue params1(UniValue::VOBJ); + UniValue res1, val; + + // hold the error returned from name_firstupdate, via + // completePendingNameFirstUpdate, or empty on success + // this will drive the error-handling popup + std::string completedResult; + + std::string name = i.first; + std::string txid = i.second.hex; + std::string rand = i.second.rand; + std::string data = i.second.data; + std::string toaddress = i.second.toaddress; + + params1.pushKV ("txid", txid); + jsonRequest.strMethod = "gettransaction"; + jsonRequest.params = params1; + jsonRequest.fHelp = false; + + // if we're here, the names doesn't exist + // should we remove it from the DB? + try { + res1 = tableRPC.execute(jsonRequest); + } + catch (const UniValue& e) { + UniValue message = find_value( e, "message"); + std::string errorStr = message.get_str(); + LogPrintf ("gettransaction error for name %s: %s\n", + name.c_str(), errorStr.c_str()); + continue; + } + + val = find_value (res1, "confirmations"); + if (!val.isNum ()) + { + LogPrintf ("No confirmations for name %s\n", name.c_str()); + continue; + } + + const int confirms = val.get_int (); + LogPrintf ("Pending Name FirstUpdate Confirms: %d\n", confirms); + + if ((unsigned int)confirms < MIN_FIRSTUPDATE_DEPTH) + continue; + + std::unique_ptr unlock_ctx; + if (getEncryptionStatus() == Locked) + { + if (QMessageBox::Yes != QMessageBox::question(this, tr("Confirm wallet unlock"), + tr("Namecoin Core is about to finalize your name registration for name %1, by sending name_firstupdate. If your wallet is locked, you will be prompted to unlock it. Pressing cancel will delay your name registration by one block, at which point you will be prompted again. Would you like to proceed?").arg(QString::fromStdString(name)), + QMessageBox::Yes|QMessageBox::Cancel, QMessageBox::Cancel)) + { + LogPrintf ("User cancelled wallet unlock pre-name_firstupdate. Waiting 1 block.\n"); + return successfulNames; + } + + Q_EMIT requireUnlock(); + unlock_ctx.reset(new UnlockContext(this, true, true)); + + if (!unlock_ctx->isValid()) + return successfulNames; + } + + completedResult = this->completePendingNameFirstUpdate(name, rand, txid, data, toaddress); + + // Locks wallet + unlock_ctx.reset(nullptr); + + // if we got an error on name_firstupdate. prompt user for what to do + if (!completedResult.empty()) + { + QString errorMsg = tr("Namecoin Core has encountered an error while attempting to complete your name registration for name %1. The name_firstupdate operation caused the following error to occurr:

%2

Would you like to cancel the pending name registration?") + .arg(QString::fromStdString(name)) + .arg(QString::fromStdString(completedResult)); + // if they didnt hit yes, move onto next pending op, otherwise + // the pending transaction will be deleted in the subsequent block + if (QMessageBox::Yes != QMessageBox::question(this, tr("Name registration error"), errorMsg, QMessageBox::Yes|QMessageBox::No, QMessageBox::No)) + continue; + } + + successfulNames.push_back(name); + nameTableModel->updateEntry(QString::fromStdString(name), QString::fromStdString(data), NameTableEntry::NAME_NEW, CT_UPDATED); + } + return successfulNames; +} + +QString WalletModel::nameUpdate(const QString &name, const QString &data, const QString &transferToAddress) +{ + std::string strName = name.toStdString (); + std::string strData = data.toStdString (); + std::string strTransferToAddress = transferToAddress.toStdString (); + LogPrintf ("wallet attempting name_update: name=%s data=%s toaddress=%s\n", + strName.c_str(), strData.c_str(), strTransferToAddress.c_str()); + + UniValue params(UniValue::VOBJ); + params.pushKV ("name", strName); + params.pushKV ("value", strData); + + JSONRPCRequest jsonRequest; + jsonRequest.strMethod = "name_update"; + jsonRequest.params = params; + jsonRequest.fHelp = false; + + if (strTransferToAddress != "") + params.pushKV ("toaddress", strTransferToAddress); + + UniValue res; + try { + res = tableRPC.execute(jsonRequest); + } + catch (const UniValue& e) { + UniValue message = find_value( e, "message"); + std::string errorStr = message.get_str(); + LogPrintf ("name_update error: %s\n", errorStr.c_str()); + return QString::fromStdString(errorStr); + } + return tr (""); +} + +bool WalletModel::writePendingNameFirstUpdate(const std::string &name, std::string &rand, std::string &txid, const std::string &data, std::string &toaddress) +{ + return wallet->WritePendingNameFirstUpdate(name, rand, txid, data, toaddress); +} + +bool WalletModel::pendingNameFirstUpdateExists(const std::string &name) +{ + return wallet->PendingNameFirstUpdateExists(name); +} + +NameNewReturn WalletModel::getPendingNameFirstUpdate(const std::string &name) +{ + return wallet->GetPendingNameFirstUpdate(name); +} + diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 05733f8272..0318205e3d 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -5,6 +5,7 @@ #ifndef BITCOIN_QT_WALLETMODEL_H #define BITCOIN_QT_WALLETMODEL_H +#include "names/common.h" #include "paymentrequestplus.h" #include "walletmodeltransaction.h" @@ -13,12 +14,13 @@ #include #include -#include +#include class AddressTableModel; class OptionsModel; class PlatformStyle; class RecentRequestsTableModel; +class NameTableModel; class TransactionTableModel; class WalletModelTransaction; @@ -95,12 +97,12 @@ class SendCoinsRecipient }; /** Interface to Bitcoin wallet from Qt view code. */ -class WalletModel : public QObject +class WalletModel : public QWidget { Q_OBJECT public: - explicit WalletModel(const PlatformStyle *platformStyle, CWallet *wallet, OptionsModel *optionsModel, QObject *parent = 0); + explicit WalletModel(const PlatformStyle *platformStyle, CWallet *wallet, OptionsModel *optionsModel, QWidget *parent = 0); ~WalletModel(); enum StatusCode // Returned by sendCoins @@ -127,6 +129,7 @@ class WalletModel : public QObject OptionsModel *getOptionsModel(); AddressTableModel *getAddressTableModel(); TransactionTableModel *getTransactionTableModel(); + NameTableModel *getNameTableModel(); RecentRequestsTableModel *getRecentRequestsTableModel(); CAmount getBalance(const CCoinControl *coinControl = nullptr) const; @@ -207,6 +210,15 @@ class WalletModel : public QObject bool transactionCanBeAbandoned(uint256 hash) const; bool abandonTransaction(uint256 hash) const; + bool nameAvailable(const QString &name, QString *reason=nullptr); + NameNewReturn nameNew(const QString &name); + std::vector> sendPendingNameFirstUpdates(); + const std::string completePendingNameFirstUpdate(const std::string &name, const std::string &rand, const std::string &txid, const std::string &data, const std::string &toaddress); + QString nameUpdate(const QString &name, const QString &data, const QString &transferToAddress); + bool writePendingNameFirstUpdate(const std::string &name, std::string &rand, std::string &txid, const std::string &data, std::string &toaddress); + bool pendingNameFirstUpdateExists(const std::string &name); + NameNewReturn getPendingNameFirstUpdate(const std::string &name); + bool transactionCanBeBumped(uint256 hash) const; bool bumpFee(uint256 hash); @@ -217,7 +229,6 @@ class WalletModel : public QObject int getDefaultConfirmTarget() const; bool getDefaultWalletRbf() const; - private: CWallet *wallet; bool fHaveWatchOnly; @@ -229,6 +240,7 @@ class WalletModel : public QObject AddressTableModel *addressTableModel; TransactionTableModel *transactionTableModel; + NameTableModel *nameTableModel; RecentRequestsTableModel *recentRequestsTableModel; // Cache some values to be able to detect changes diff --git a/src/qt/walletview.cpp b/src/qt/walletview.cpp index a56a40037f..87c8cac814 100644 --- a/src/qt/walletview.cpp +++ b/src/qt/walletview.cpp @@ -9,6 +9,7 @@ #include "bitcoingui.h" #include "clientmodel.h" #include "guiutil.h" +#include "managenamespage.h" #include "optionsmodel.h" #include "overviewpage.h" #include "platformstyle.h" @@ -55,6 +56,7 @@ WalletView::WalletView(const PlatformStyle *_platformStyle, QWidget *parent): receiveCoinsPage = new ReceiveCoinsDialog(platformStyle); sendCoinsPage = new SendCoinsDialog(platformStyle); + manageNamesPage = new ManageNamesPage(platformStyle); usedSendingAddressesPage = new AddressBookPage(platformStyle, AddressBookPage::ForEditing, AddressBookPage::SendingTab, this); usedReceivingAddressesPage = new AddressBookPage(platformStyle, AddressBookPage::ForEditing, AddressBookPage::ReceivingTab, this); @@ -63,6 +65,7 @@ WalletView::WalletView(const PlatformStyle *_platformStyle, QWidget *parent): addWidget(transactionsPage); addWidget(receiveCoinsPage); addWidget(sendCoinsPage); + addWidget(manageNamesPage); // Clicking on a transaction on the overview pre-selects the transaction on the transaction history page connect(overviewPage, SIGNAL(transactionClicked(QModelIndex)), transactionView, SLOT(focusTransaction(QModelIndex))); @@ -124,6 +127,7 @@ void WalletView::setWalletModel(WalletModel *_walletModel) sendCoinsPage->setModel(_walletModel); usedReceivingAddressesPage->setModel(_walletModel ? _walletModel->getAddressTableModel() : nullptr); usedSendingAddressesPage->setModel(_walletModel ? _walletModel->getAddressTableModel() : nullptr); + manageNamesPage->setModel(walletModel); if (_walletModel) { @@ -192,6 +196,11 @@ void WalletView::gotoSendCoinsPage(QString addr) sendCoinsPage->setAddress(addr); } +void WalletView::gotoManageNamesPage() +{ + setCurrentWidget(manageNamesPage); +} + void WalletView::gotoSignMessageTab(QString addr) { // calls show() in showTab_SM() diff --git a/src/qt/walletview.h b/src/qt/walletview.h index c1f8422f0c..c017b53017 100644 --- a/src/qt/walletview.h +++ b/src/qt/walletview.h @@ -16,6 +16,7 @@ class PlatformStyle; class ReceiveCoinsDialog; class SendCoinsDialog; class SendCoinsRecipient; +class ManageNamesPage; class TransactionView; class WalletModel; class AddressBookPage; @@ -62,6 +63,7 @@ class WalletView : public QStackedWidget QWidget *transactionsPage; ReceiveCoinsDialog *receiveCoinsPage; SendCoinsDialog *sendCoinsPage; + ManageNamesPage *manageNamesPage; AddressBookPage *usedSendingAddressesPage; AddressBookPage *usedReceivingAddressesPage; @@ -85,6 +87,9 @@ public Q_SLOTS: /** Show Sign/Verify Message dialog and switch to verify message tab */ void gotoVerifyMessageTab(QString addr = ""); + /** NMC names mgmt tab */ + void gotoManageNamesPage(); + /** Show incoming transaction notification for new transactions. The new items are those between start and end inclusive, under the given parent item. diff --git a/src/wallet/test/wallet_name_pending_tests.cpp b/src/wallet/test/wallet_name_pending_tests.cpp new file mode 100644 index 0000000000..70d31bed99 --- /dev/null +++ b/src/wallet/test/wallet_name_pending_tests.cpp @@ -0,0 +1,89 @@ +// Copyright (c) 2016 B.X. Roberts +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "rpc/server.h" +#include "names/common.h" +#include "wallet/wallet.h" +#include "dbwrapper.h" + +#include "test/test_bitcoin.h" +#include "wallet/test/wallet_test_fixture.h" + +#include +#include + +extern CWallet* pwalletMain; + +BOOST_FIXTURE_TEST_SUITE(wallet_name_pending_tests, WalletTestingSetup) + +BOOST_AUTO_TEST_CASE(wallet_name_pending_tests) +{ + const std::string nameGood = "test/name"; + const std::string nameBad = "test/baddata"; + const std::string txid = "9f73e1dfa3cbae23d008307e42e72beb8c010546ea2a7b9ff32619676a9c64a6"; + const std::string rand = "092abbca8a938103abcc"; + const std::string data = "{\"foo\": \"bar\"}"; + + UniValue uniNameUpdateData(UniValue::VOBJ); + uniNameUpdateData.pushKV ("txid", txid); + uniNameUpdateData.pushKV ("rand", rand); + uniNameUpdateData.pushKV ("data", data); + + CWalletDBWrapper& dbw = pwalletMain->GetDBHandle(); + + // this gets written to wallet for pending name_firstupdate + std::string saveData = uniNameUpdateData.write(); + // and some bad data to ensure we dont segfault + std::string badData = "flksjf984j*#)(QUFD039kjdc0e9wjf8{})"; + + { + LOCK(pwalletMain->cs_wallet); + // ensure pending names is blank to start + BOOST_CHECK(pwalletMain->pendingNameFirstUpdate.size() == 0); + } + + // write a valid pending name_update to wallet + { + //CWalletDB walletdb(pwalletMain->strWalletFile); + LOCK(pwalletMain->cs_wallet); + BOOST_CHECK(CWalletDB(dbw).WriteNameFirstUpdate(nameGood, saveData)); + } + + { + // load the wallet and see if we get our pending name loaded + LOCK(pwalletMain->cs_wallet); + BOOST_CHECK_NO_THROW(CWalletDB(dbw).LoadWallet(pwalletMain)); + } + + { + // make sure we've added our pending name + LOCK(pwalletMain->cs_wallet); + BOOST_CHECK(pwalletMain->pendingNameFirstUpdate.size() > 0); + BOOST_CHECK(pwalletMain->pendingNameFirstUpdate.find(nameGood) != pwalletMain->pendingNameFirstUpdate.end()); + } + + { + // put a bad name pending to the wallet + LOCK(pwalletMain->cs_wallet); + BOOST_CHECK(CWalletDB(dbw).WriteNameFirstUpdate(nameBad, badData)); + } + + { + // load the wallet and ensure we don't segfault on the bad data + LOCK(pwalletMain->cs_wallet); + BOOST_CHECK_NO_THROW(CWalletDB(dbw).LoadWallet(pwalletMain)); + // make sure we dont have this bad pending in memory + BOOST_CHECK(pwalletMain->pendingNameFirstUpdate.size() > 0); + BOOST_CHECK(pwalletMain->pendingNameFirstUpdate.find(nameBad) == pwalletMain->pendingNameFirstUpdate.end()); + } + + { + // test removing the names + LOCK(pwalletMain->cs_wallet); + BOOST_CHECK(CWalletDB(dbw).EraseNameFirstUpdate(nameGood)); + BOOST_CHECK(CWalletDB(dbw).EraseNameFirstUpdate(nameBad)); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index c6719e685b..2a05672f18 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -33,6 +33,7 @@ #include "wallet/fees.h" #include +#include #include #include @@ -4098,6 +4099,50 @@ bool CWallet::BackupWallet(const std::string& strDest) return dbw->Backup(strDest); } +bool CWallet::PendingNameFirstUpdateExists(const std::string &name) +{ + LOCK(cs_wallet); + return pendingNameFirstUpdate.end() != pendingNameFirstUpdate.find(name); +} + +bool CWallet::WritePendingNameFirstUpdate(const std::string &name, std::string &rand, std::string &txid, const std::string &data, std::string &toaddress) +{ + LOCK(cs_wallet); + + UniValue jsonData(UniValue::VOBJ); + jsonData.pushKV ("txid", txid); + jsonData.pushKV ("rand", rand); + jsonData.pushKV ("data", data); + if(!toaddress.empty ()) + jsonData.pushKV ("toaddress", toaddress); + std::string jsonStrData = jsonData.write(); + + NameNewReturn newReturn; + newReturn.ok = true; + newReturn.hex = txid; + newReturn.rand = rand; + newReturn.data = data; + newReturn.toaddress = toaddress; + + pendingNameFirstUpdate[name] = newReturn; + return CWalletDB(*dbw).WriteNameFirstUpdate(name, jsonStrData); +} + +bool CWallet::ErasePendingNameFirstUpdate(const std::string &name) +{ + LOCK(cs_wallet); + pendingNameFirstUpdate.erase(name); + return CWalletDB(*dbw).EraseNameFirstUpdate(name); +} + +NameNewReturn CWallet::GetPendingNameFirstUpdate(const std::string &name) +{ + LOCK(cs_wallet); + MapNameNewReturn pn = pendingNameFirstUpdate; + MapNameNewReturn::iterator it = pn.find(name); + return it->second; +} + CKeyPool::CKeyPool() { nTime = GetTime(); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 4770f951ba..67978e1629 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -8,6 +8,7 @@ #include "amount.h" #include "auxpow.h" // contains CMerkleTx +#include "names/common.h" // MapNameNewReturn #include "policy/feerate.h" #include "streams.h" #include "tinyformat.h" @@ -1049,6 +1050,12 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface caller must ensure the current wallet version is correct before calling this function). */ bool SetHDMasterKey(const CPubKey& key); + + MapNameNewReturn pendingNameFirstUpdate; + bool PendingNameFirstUpdateExists(const std::string &name); + bool WritePendingNameFirstUpdate(const std::string &name, std::string &rand, std::string &txid, const std::string &data, std::string &toaddress); + bool ErasePendingNameFirstUpdate(const std::string &name); + NameNewReturn GetPendingNameFirstUpdate(const std::string &name); }; /** A key allocated from the key pool. */ diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index b7f873c1e4..385b55f821 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -9,6 +9,7 @@ #include "consensus/tx_verify.h" #include "consensus/validation.h" #include "fs.h" +#include "names/common.h" #include "protocol.h" #include "serialize.h" #include "sync.h" @@ -17,6 +18,7 @@ #include "wallet/wallet.h" #include +#include #include @@ -150,6 +152,16 @@ bool CWalletDB::WriteMinVersion(int nVersion) return WriteIC(std::string("minversion"), nVersion); } +bool CWalletDB::WriteNameFirstUpdate(const std::string& name, const std::string& data) +{ + return WriteIC(std::make_pair(std::string("pending_firstupdate"), name), data); +} + +bool CWalletDB::EraseNameFirstUpdate(const std::string& name) +{ + return EraseIC(std::make_pair(std::string("pending_firstupdate"), name)); +} + bool CWalletDB::ReadAccount(const std::string& strAccount, CAccount& account) { account.SetNull(); @@ -465,6 +477,38 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, pwallet->LoadKeyPool(nIndex, keypool); } + else if (strType == "pending_firstupdate") + { + std::string strName, strJsonData; + ssKey >> strName; + ssValue >> strJsonData; + + UniValue jsonData; + jsonData.read(strJsonData); + + UniValue uTxid = find_value (jsonData, "txid"); + UniValue uRand = find_value (jsonData, "rand"); + UniValue uPendingData = find_value (jsonData, "data"); + + if(uTxid.type() != UniValue::VSTR || + uRand.type() != UniValue::VSTR || + uPendingData.type() != UniValue::VSTR) { + strErr = strprintf("Bad data while importing pending name firstupdate: %s\n", strName.c_str()); + return false; + } + + std::string txid = uTxid.get_str(); + std::string rand = uRand.get_str(); + std::string pendingData = uPendingData.get_str(); + + NameNewReturn ret; + ret.hex = txid; + ret.rand = rand; + ret.data = pendingData; + + pwallet->pendingNameFirstUpdate[strName] = ret; + LogPrintf("Loaded pending name_firstupdate %s => %s\n", strName, strJsonData); + } else if (strType == "version") { ssValue >> wss.nFileVersion; diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index 3a146179af..df70fa96c5 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -7,6 +7,7 @@ #define BITCOIN_WALLET_WALLETDB_H #include "amount.h" +#include "names/common.h" #include "primitives/transaction.h" #include "wallet/db.h" #include "key.h" @@ -213,6 +214,9 @@ class CWalletDB CAmount GetAccountCreditDebit(const std::string& strAccount); void ListAccountCreditDebit(const std::string& strAccount, std::list& acentries); + bool WriteNameFirstUpdate(const std::string& name, const std::string& data); + bool EraseNameFirstUpdate(const std::string& name); + DBErrors LoadWallet(CWallet* pwallet); DBErrors FindWalletTx(std::vector& vTxHash, std::vector& vWtx); DBErrors ZapWalletTx(std::vector& vWtx);