From 65f1d8874e7223d56b027118add553ff21e817b6 Mon Sep 17 00:00:00 2001 From: "James C. Owens" Date: Fri, 6 Oct 2023 19:11:52 -0400 Subject: [PATCH] Implementation of EditSideStakeDialog --- src/Makefile.qt.include | 4 + src/gridcoin/sidestake.cpp | 80 +++++++----- src/gridcoin/sidestake.h | 11 +- src/miner.cpp | 2 +- src/qt/CMakeLists.txt | 1 + src/qt/editsidestakedialog.cpp | 149 ++++++++++++++++++++++ src/qt/editsidestakedialog.h | 50 ++++++++ src/qt/forms/editsidestakedialog.ui | 173 +++++++++++++++++++++++++ src/qt/forms/optionsdialog.ui | 17 +++ src/qt/optionsdialog.cpp | 128 +++++++++++++++++-- src/qt/optionsdialog.h | 6 + src/qt/sidestaketablemodel.cpp | 187 ++++++++++++++++++++++++++-- src/qt/sidestaketablemodel.h | 11 +- src/rpc/mining.cpp | 3 +- 14 files changed, 770 insertions(+), 52 deletions(-) create mode 100644 src/qt/editsidestakedialog.cpp create mode 100644 src/qt/editsidestakedialog.h create mode 100644 src/qt/forms/editsidestakedialog.ui diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index de442f7f93..6e67219bec 100755 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -82,6 +82,7 @@ QT_FORMS_UI = \ qt/forms/consolidateunspentwizardsendpage.ui \ qt/forms/diagnosticsdialog.ui \ qt/forms/editaddressdialog.ui \ + qt/forms/editsidestakedialog.ui \ qt/forms/favoritespage.ui \ qt/forms/intro.ui \ qt/forms/mrcrequestpage.ui \ @@ -143,6 +144,7 @@ QT_MOC_CPP = \ qt/moc_csvmodelwriter.cpp \ qt/moc_diagnosticsdialog.cpp \ qt/moc_editaddressdialog.cpp \ + qt/moc_editsidestakedialog.cpp \ qt/moc_favoritespage.cpp \ qt/moc_guiutil.cpp \ qt/moc_intro.cpp \ @@ -250,6 +252,7 @@ GRIDCOINRESEARCH_QT_H = \ qt/decoration.h \ qt/diagnosticsdialog.h \ qt/editaddressdialog.h \ + qt/editsidestakedialog.h \ qt/favoritespage.h \ qt/guiconstants.h \ qt/guiutil.h \ @@ -342,6 +345,7 @@ GRIDCOINRESEARCH_QT_CPP = \ qt/decoration.cpp \ qt/diagnosticsdialog.cpp \ qt/editaddressdialog.cpp \ + qt/editsidestakedialog.cpp \ qt/favoritespage.cpp \ qt/guiutil.cpp \ qt/intro.cpp \ diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index 90a00f3c25..0220a15b87 100644 --- a/src/gridcoin/sidestake.cpp +++ b/src/gridcoin/sidestake.cpp @@ -208,7 +208,8 @@ const std::vector SideStakeRegistry::SideStakeEntries() const return sidestakes; } -const std::vector SideStakeRegistry::ActiveSideStakeEntries() +const std::vector SideStakeRegistry::ActiveSideStakeEntries(const bool& local_only, + const bool& include_zero_alloc) { std::vector sidestakes; double allocation_sum = 0.0; @@ -221,11 +222,15 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries() LOCK(cs_lock); // Do mandatory sidestakes first. - for (const auto& entry : m_sidestake_entries) - { - if (entry.second->m_status == SideStakeStatus::MANDATORY && allocation_sum + entry.second->m_allocation <= 1.0) { - sidestakes.push_back(entry.second); - allocation_sum += entry.second->m_allocation; + if (!local_only) { + for (const auto& entry : m_sidestake_entries) + { + if (entry.second->m_status == SideStakeStatus::MANDATORY && allocation_sum + entry.second->m_allocation <= 1.0) { + if ((include_zero_alloc && entry.second->m_allocation == 0.0) || entry.second->m_allocation > 0.0) { + sidestakes.push_back(entry.second); + allocation_sum += entry.second->m_allocation; + } + } } } @@ -238,8 +243,10 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries() for (const auto& entry : m_local_sidestake_entries) { if (entry.second->m_status == SideStakeStatus::ACTIVE && allocation_sum + entry.second->m_allocation <= 1.0) { - sidestakes.push_back(entry.second); - allocation_sum += entry.second->m_allocation; + if ((include_zero_alloc && entry.second->m_allocation == 0.0) || entry.second->m_allocation > 0.0) { + sidestakes.push_back(entry.second); + allocation_sum += entry.second->m_allocation; + } } } } @@ -263,7 +270,7 @@ std::vector SideStakeRegistry::Try(const CBitcoinAddressForStorag const auto local_entry = m_local_sidestake_entries.find(key); - if (local_entry != m_sidestake_entries.end()) { + if (local_entry != m_local_sidestake_entries.end()) { result.push_back(local_entry->second); } @@ -368,12 +375,16 @@ void SideStakeRegistry::AddDelete(const ContractContext& ctx) return; } -void SideStakeRegistry::NonContractAdd(SideStake& sidestake) +void SideStakeRegistry::NonContractAdd(const SideStake& sidestake, const bool& save_to_file) { + LOCK(cs_lock); + // Using this form of insert because we want the latest record with the same key to override any previous one. m_local_sidestake_entries[sidestake.m_key] = std::make_shared(sidestake); - + if (save_to_file) { + SaveLocalSideStakesToConfig(); + } } void SideStakeRegistry::Add(const ContractContext& ctx) @@ -381,13 +392,19 @@ void SideStakeRegistry::Add(const ContractContext& ctx) AddDelete(ctx); } -void SideStakeRegistry::NonContractDelete(CBitcoinAddressForStorage& address) +void SideStakeRegistry::NonContractDelete(const CBitcoinAddressForStorage& address, const bool& save_to_file) { + LOCK(cs_lock); + auto sidestake_entry_pair_iter = m_local_sidestake_entries.find(address); if (sidestake_entry_pair_iter != m_local_sidestake_entries.end()) { m_local_sidestake_entries.erase(sidestake_entry_pair_iter); } + + if (save_to_file) { + SaveLocalSideStakesToConfig(); + } } void SideStakeRegistry::Delete(const ContractContext& ctx) @@ -571,17 +588,22 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() bool new_format_valid = false; - if (addresses.size() != allocations.size() || (!descriptions.empty() && descriptions.size() != addresses.size())) - { - LogPrintf("WARN: %s: Malformed new style sidestaking configuration entries. Reverting to original format in read only " - "gridcoinresearch.conf file.", - __func__); - } else { - new_format_valid = true; + if (!addresses.empty()) { + if (addresses.size() != allocations.size() || (!descriptions.empty() && addresses.size() != descriptions.size())) { + LogPrintf("WARN: %s: Malformed new style sidestaking configuration entries. " + "Reverting to original format in read only gridcoinresearch.conf file.", + __func__); + } else { + new_format_valid = true; - for (unsigned int i = 0; i < addresses.size(); ++i) - { - raw_vSideStakeAlloc.push_back(std::make_tuple(addresses[i], allocations[i], descriptions[i])); + for (unsigned int i = 0; i < addresses.size(); ++i) + { + if (descriptions.empty()) { + raw_vSideStakeAlloc.push_back(std::make_tuple(addresses[i], allocations[i], "")); + } else { + raw_vSideStakeAlloc.push_back(std::make_tuple(addresses[i], allocations[i], descriptions[i])); + } + } } } @@ -639,9 +661,9 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() dAllocation /= 100.0; - if (dAllocation <= 0) + if (dAllocation < 0) { - LogPrintf("WARN: %s: Negative or zero allocation provided. Skipping allocation.", __func__); + LogPrintf("WARN: %s: Negative allocation provided. Skipping allocation.", __func__); continue; } @@ -665,7 +687,7 @@ void SideStakeRegistry::LoadLocalSideStakesFromConfig() SideStakeStatus::ACTIVE); // This will add or update (replace) a non-contract entry in the registry for the local sidestake. - NonContractAdd(sidestake); + NonContractAdd(sidestake, false); // This is needed because we need to detect entries in the registry map that are no longer in the config file to mark // them deleted. @@ -708,6 +730,8 @@ bool SideStakeRegistry::SaveLocalSideStakesToConfig() std::vector> settings; + LOCK(cs_lock); + unsigned int i = 0; for (const auto& iter : m_local_sidestake_entries) { if (i) { @@ -721,9 +745,9 @@ bool SideStakeRegistry::SaveLocalSideStakesToConfig() ++i; } - settings.push_back(std::make_pair("addresses", addresses)); - settings.push_back(std::make_pair("allocations", allocations)); - settings.push_back(std::make_pair("descriptions", descriptions)); + settings.push_back(std::make_pair("sidestakeaddresses", addresses)); + settings.push_back(std::make_pair("sidestakeallocations", allocations)); + settings.push_back(std::make_pair("sidestakedescriptions", descriptions)); status = updateRwSettings(settings); diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index 245873c211..dccb6b216a 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -408,9 +408,11 @@ class SideStakeRegistry : public IContractHandler //! Mandatory sidestakes come before local ones, and the method ensures that the sidestakes //! returned do not total an allocation greater than 1.0. //! + //! \param bool true to return local sidestakes only + //! //! \return A vector of smart pointers to sidestake entries. //! - const std::vector ActiveSideStakeEntries(); + const std::vector ActiveSideStakeEntries(const bool& local_only, const bool& include_zero_alloc); //! //! \brief Get the current sidestake entry for the specified key string. @@ -472,8 +474,9 @@ class SideStakeRegistry : public IContractHandler //! the registry db. //! //! \param SideStake object to add + //! \param bool save_to_file if true causes SaveLocalSideStakesToConfig() to be called. //! - void NonContractAdd(SideStake& sidestake); + void NonContractAdd(const SideStake& sidestake, const bool& save_to_file = true); //! //! \brief Add a sidestake entry to the registry from contract data. For the sidestake registry @@ -487,9 +490,11 @@ class SideStakeRegistry : public IContractHandler //! //! \brief Provides for deletion of local (voluntary) sidestakes from the in-memory map that are not persisted //! to the registry db. Deletion is by the map key (CBitcoinAddress). + //! //! \param address + //! \param bool save_to_file if true causes SaveLocalSideStakesToConfig() to be called. //! - void NonContractDelete(CBitcoinAddressForStorage& address); + void NonContractDelete(const CBitcoinAddressForStorage& address, const bool& save_to_file = true); //! //! \brief Mark a sidestake entry deleted in the registry from contract data. For the sidestake registry diff --git a/src/miner.cpp b/src/miner.cpp index ca028355c0..c1225391a0 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -1321,7 +1321,7 @@ void StakeMiner(CWallet *pwallet) // Note that fEnableSideStaking is now processed internal to ActiveSideStakeEntries. The sidestaking flag only // controls local sidestakes. If there exists mandatory sidestakes, they occur regardless of the flag. - vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(); + vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(false, false); // wait for next round if (!MilliSleep(nMinerSleep)) return; diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt index 03b06569f7..b62d55953b 100644 --- a/src/qt/CMakeLists.txt +++ b/src/qt/CMakeLists.txt @@ -29,6 +29,7 @@ add_library(gridcoinqt STATIC decoration.cpp diagnosticsdialog.cpp editaddressdialog.cpp + editsidestakedialog.cpp favoritespage.cpp guiutil.cpp intro.cpp diff --git a/src/qt/editsidestakedialog.cpp b/src/qt/editsidestakedialog.cpp new file mode 100644 index 0000000000..7d0904d351 --- /dev/null +++ b/src/qt/editsidestakedialog.cpp @@ -0,0 +1,149 @@ +// Copyright (c) 2014-2023 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or https://opensource.org/licenses/mit-license.php. + +#include "editsidestakedialog.h" +#include "ui_editsidestakedialog.h" +#include "sidestaketablemodel.h" +#include "guiutil.h" +#include "qt/decoration.h" + +#include + +EditSideStakeDialog::EditSideStakeDialog(Mode mode, QWidget* parent) + : QDialog(parent) + , ui(new Ui::EditSideStakeDialog) + , mode(mode) + , model(nullptr) +{ + ui->setupUi(this); + + resize(GRC::ScaleSize(this, width(), height())); + + GUIUtil::setupAddressWidget(ui->addressLineEdit, this); + + switch (mode) + { + case NewSideStake: + setWindowTitle(tr("New SideStake")); + ui->statusLineEdit->setEnabled(false); + ui->statusLabel->setHidden(true); + ui->statusLineEdit->setHidden(true); + break; + case EditSideStake: + setWindowTitle(tr("Edit SideStake")); + ui->addressLineEdit->setEnabled(false); + ui->statusLabel->setHidden(false); + ui->statusLineEdit->setHidden(false); + ui->statusLineEdit->setEnabled(false); + break; + } + +} + +EditSideStakeDialog::~EditSideStakeDialog() +{ + delete ui; +} + +void EditSideStakeDialog::setModel(SideStakeTableModel* model) +{ + this->model = model; + if (!model) { + return; + } + +} + +void EditSideStakeDialog::loadRow(int row) +{ + m_row = row; + + ui->addressLineEdit->setText(model->index(row, SideStakeTableModel::Address, QModelIndex()).data().toString()); + ui->allocationLineEdit->setText(model->index(row, SideStakeTableModel::Allocation, QModelIndex()).data().toString()); + ui->descriptionLineEdit->setText(model->index(row, SideStakeTableModel::Description, QModelIndex()).data().toString()); + ui->statusLineEdit->setText(model->index(row, SideStakeTableModel::Status, QModelIndex()).data().toString()); +} + +bool EditSideStakeDialog::saveCurrentRow() +{ + if (!model) { + return false; + } + + bool success = true; + + switch (mode) + { + case NewSideStake: + address = model->addRow(ui->addressLineEdit->text(), + ui->allocationLineEdit->text(), + ui->descriptionLineEdit->text()); + + if (address.isEmpty()) { + success = false; + } + + break; + case EditSideStake: + QModelIndex index = model->index(m_row, SideStakeTableModel::Allocation, QModelIndex()); + model->setData(index, ui->allocationLineEdit->text(), Qt::EditRole); + + if (model->getEditStatus() == SideStakeTableModel::OK || model->getEditStatus() == SideStakeTableModel::NO_CHANGES) { + index = model->index(m_row, SideStakeTableModel::Description, QModelIndex()); + model->setData(index, ui->descriptionLineEdit->text(), Qt::EditRole); + + if (model->getEditStatus() == SideStakeTableModel::OK || model->getEditStatus() == SideStakeTableModel::NO_CHANGES) { + break; + } + } + + success = false; + + break; + } + + return success; +} + +void EditSideStakeDialog::accept() +{ + if (!model) { + return; + } + + if (!saveCurrentRow()) + { + switch (model->getEditStatus()) + { + case SideStakeTableModel::OK: + // Failed with unknown reason. Just reject. + break; + case SideStakeTableModel::NO_CHANGES: + // No changes were made during edit operation. Just reject. + break; + case SideStakeTableModel::INVALID_ADDRESS: + QMessageBox::warning(this, windowTitle(), + tr("The entered address \"%1\" is not " + "a valid Gridcoin address.").arg(ui->addressLineEdit->text()), + QMessageBox::Ok, QMessageBox::Ok); + break; + case SideStakeTableModel::DUPLICATE_ADDRESS: + QMessageBox::warning(this, windowTitle(), + tr("The entered address \"%1\" already " + "has a local sidestake entry.").arg(ui->addressLineEdit->text()), + QMessageBox::Ok, QMessageBox::Ok); + break; + case SideStakeTableModel::INVALID_ALLOCATION: + QMessageBox::warning(this, windowTitle(), + tr("The entered allocation is not valid. Check to make sure that the " + "allocation is greater than zero and when added to the other allocations " + "totals less than 100.").arg(ui->allocationLineEdit->text()), + QMessageBox::Ok, QMessageBox::Ok); + } + + return; + } + + QDialog::accept(); +} diff --git a/src/qt/editsidestakedialog.h b/src/qt/editsidestakedialog.h new file mode 100644 index 0000000000..b58a44fad6 --- /dev/null +++ b/src/qt/editsidestakedialog.h @@ -0,0 +1,50 @@ +// Copyright (c) 2014-2023 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or https://opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_EDITSIDESTAKEDIALOG_H +#define BITCOIN_QT_EDITSIDESTAKEDIALOG_H + +#include + +QT_BEGIN_NAMESPACE +class QDataWidgetMapper; +QT_END_NAMESPACE + +namespace Ui { +class EditSideStakeDialog; +} +class SideStakeTableModel; + +/** Dialog for editing an address and associated information. + */ +class EditSideStakeDialog : public QDialog +{ + Q_OBJECT + +public: + enum Mode { + NewSideStake, + EditSideStake + }; + + explicit EditSideStakeDialog(Mode mode, QWidget* parent = nullptr); + ~EditSideStakeDialog(); + + void setModel(SideStakeTableModel* model); + void loadRow(int row); + +public slots: + void accept(); + +private: + bool saveCurrentRow(); + + Ui::EditSideStakeDialog *ui; + Mode mode; + SideStakeTableModel *model; + int m_row; + + QString address; +}; +#endif // BITCOIN_QT_EDITSIDESTAKEDIALOG_H diff --git a/src/qt/forms/editsidestakedialog.ui b/src/qt/forms/editsidestakedialog.ui new file mode 100644 index 0000000000..a2dd71f36b --- /dev/null +++ b/src/qt/forms/editsidestakedialog.ui @@ -0,0 +1,173 @@ + + + EditSideStakeDialog + + + + 0 + 0 + 400 + 300 + + + + Add or Edit SideStake + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Address + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Allocation + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Description + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Status + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + EditSideStakeDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + EditSideStakeDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index 1c34716190..efb8ea568b 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -342,6 +342,9 @@ + + false + Edit @@ -351,6 +354,20 @@ + + + + false + + + Delete + + + + :/icons/remove:/icons/remove + + + diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 25a92ba0d7..f540ec5b71 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -9,7 +9,9 @@ #include "init.h" #include "miner.h" #include "sidestaketablemodel.h" +#include "editsidestakedialog.h" +#include #include #include #include @@ -160,8 +162,22 @@ void OptionsDialog::setModel(OptionsModel *model) ui->sidestakingTableView->horizontalHeader()->setStretchLastSection(true); ui->sidestakingTableView->setShowGrid(true); + ui->sidestakingTableView->sortByColumn(0, Qt::AscendingOrder); + connect(ui->enableSideStaking, &QCheckBox::toggled, this, &OptionsDialog::hideSideStakeEdit); connect(ui->enableSideStaking, &QCheckBox::toggled, this, &OptionsDialog::refreshSideStakeTableModel); + + connect(ui->pushButtonNewSideStake, &QPushButton::clicked, this, &OptionsDialog::newSideStakeButton_clicked); + connect(ui->pushButtonEditSideStake, &QPushButton::clicked, this, &OptionsDialog::editSideStakeButton_clicked); + connect(ui->pushButtonDeleteSideStake, &QPushButton::clicked, this, &OptionsDialog::deleteSideStakeButton_clicked); + + connect(ui->sidestakingTableView->selectionModel(), &QItemSelectionModel::selectionChanged, + this, &OptionsDialog::sidestakeSelectionChanged); + + ui->sidestakingTableView->installEventFilter(this); + + connect(this, &OptionsDialog::sidestakeAllocationInvalid, this, &OptionsDialog::handleSideStakeAllocationInvalid); + } /* update the display unit, to not use the default ("BTC") */ @@ -250,7 +266,8 @@ void OptionsDialog::setSaveButtonState(bool fState) void OptionsDialog::on_okButton_clicked() { - mapper->submit(); + refreshSideStakeTableModel(); + accept(); } @@ -261,8 +278,6 @@ void OptionsDialog::on_cancelButton_clicked() void OptionsDialog::on_applyButton_clicked() { - mapper->submit(); - refreshSideStakeTableModel(); disableApplyButton(); @@ -270,19 +285,65 @@ void OptionsDialog::on_applyButton_clicked() void OptionsDialog::newSideStakeButton_clicked() { + if (!model) { + return; + } + EditSideStakeDialog dialog(EditSideStakeDialog::NewSideStake, this); + + dialog.setModel(model->getSideStakeTableModel()); + + dialog.exec(); } void OptionsDialog::editSideStakeButton_clicked() { + if (!model || !ui->sidestakingTableView->selectionModel()) { + return; + } + + QModelIndexList indexes = ui->sidestakingTableView->selectionModel()->selectedRows(); + + if (indexes.isEmpty()) { + return; + } + + if (indexes.size() > 1) { + QMessageBox::warning(this, tr("Error"), tr("You can only edit one sidestake at a time."), QMessageBox::Ok); + } + + EditSideStakeDialog dialog(EditSideStakeDialog::EditSideStake, this); + + dialog.setModel(model->getSideStakeTableModel()); + dialog.loadRow(indexes.at(0).row()); + dialog.exec(); +} + +void OptionsDialog::deleteSideStakeButton_clicked() +{ + if (!model || !ui->sidestakingTableView->selectionModel()) { + return; + } + + QModelIndexList indexes = ui->sidestakingTableView->selectionModel()->selectedRows(); + + if (indexes.isEmpty()) { + return; + } + + if (indexes.size() > 1) { + QMessageBox::warning(this, tr("Error"), tr("You can only delete one sidestake at a time."), QMessageBox::Ok); + } + model->getSideStakeTableModel()->removeRows(indexes.at(0).row(), 1); } void OptionsDialog::showRestartWarning_Proxy() { if(!fRestartWarningDisplayed_Proxy) { - QMessageBox::warning(this, tr("Warning"), tr("This setting will take effect after restarting Gridcoin."), QMessageBox::Ok); + QMessageBox::warning(this, tr("Warning"), tr("This setting will take effect" + " after restarting Gridcoin."), QMessageBox::Ok); fRestartWarningDisplayed_Proxy = true; } } @@ -416,9 +477,12 @@ void OptionsDialog::handleMinStakeSplitValueValid(QValidatedLineEdit *object, bo void OptionsDialog::refreshSideStakeTableModel() { - mapper->submit(); - - model->getSideStakeTableModel()->refresh(); + if (!mapper->submit() + && model->getSideStakeTableModel()->getEditStatus() == SideStakeTableModel::INVALID_ALLOCATION) { + emit sidestakeAllocationInvalid(); + } else { + model->getSideStakeTableModel()->refresh(); + } } bool OptionsDialog::eventFilter(QObject *object, QEvent *event) @@ -477,5 +541,55 @@ bool OptionsDialog::eventFilter(QObject *object, QEvent *event) } } } + + // This is required to provide immediate feedback on invalid allocation entries on in place editing. + if (object == ui->sidestakingTableView) + { + if (model->getSideStakeTableModel()->getEditStatus() == SideStakeTableModel::INVALID_ALLOCATION) { + LogPrint(BCLog::LogFlags::VERBOSE, "INFO %s: event type = %i", + __func__, + (int) event->type()); + + emit sidestakeAllocationInvalid(); + } + } + return QDialog::eventFilter(object, event); } + +void OptionsDialog::sidestakeSelectionChanged() +{ + QTableView *table = ui->sidestakingTableView; + + if (table->selectionModel()->hasSelection()) { + QModelIndexList indexes = ui->sidestakingTableView->selectionModel()->selectedRows(); + + if (indexes.size() > 1) { + ui->pushButtonEditSideStake->setEnabled(false); + ui->pushButtonDeleteSideStake->setEnabled(false); + } else if (static_cast(indexes.at(0).internalPointer())->m_status + == GRC::SideStakeStatus::MANDATORY) { + ui->pushButtonEditSideStake->setEnabled(false); + ui->pushButtonDeleteSideStake->setEnabled(false); + } else { + ui->pushButtonEditSideStake->setEnabled(true); + ui->pushButtonDeleteSideStake->setEnabled(true); + } + } +} + +void OptionsDialog::handleSideStakeAllocationInvalid() +{ + model->getSideStakeTableModel()->refresh(); + + QMessageBox::warning(this, windowTitle(), + tr("The entered allocation is not valid and is reverted. Check to make sure " + "that the allocation is greater than or equal to zero and when added to the other " + "allocations totals less than 100."), + QMessageBox::Ok, QMessageBox::Ok); +} + +void OptionsDialog::updateSideStakeTableView() +{ + ui->sidestakingTableView->update(); +} diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index bf95e9fce3..c35d983b1b 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -42,6 +42,7 @@ private slots: void newSideStakeButton_clicked(); void editSideStakeButton_clicked(); + void deleteSideStakeButton_clicked(); void showRestartWarning_Proxy(); void showRestartWarning_Lang(); @@ -54,6 +55,7 @@ private slots: void handleProxyIpValid(QValidatedLineEdit *object, bool fState); void handleStakingEfficiencyValid(QValidatedLineEdit *object, bool fState); void handleMinStakeSplitValueValid(QValidatedLineEdit *object, bool fState); + void handleSideStakeAllocationInvalid(); void refreshSideStakeTableModel(); @@ -61,6 +63,7 @@ private slots: void proxyIpValid(QValidatedLineEdit *object, bool fValid); void stakingEfficiencyValid(QValidatedLineEdit *object, bool fValid); void minStakeSplitValueValid(QValidatedLineEdit *object, bool fValid); + void sidestakeAllocationInvalid(); private: Ui::OptionsDialog *ui; @@ -81,6 +84,9 @@ private slots: STATUS_COLUMN_WIDTH = 150 }; +private slots: + void sidestakeSelectionChanged(); + void updateSideStakeTableView(); }; #endif // BITCOIN_QT_OPTIONSDIALOG_H diff --git a/src/qt/sidestaketablemodel.cpp b/src/qt/sidestaketablemodel.cpp index 8514b10df9..c3df02df21 100644 --- a/src/qt/sidestaketablemodel.cpp +++ b/src/qt/sidestaketablemodel.cpp @@ -10,6 +10,16 @@ #include #include +namespace { + +static void RwSettingsUpdated(SideStakeTableModel* sidestake_model) +{ + qDebug() << QString("%1").arg(__func__); + QMetaObject::invokeMethod(sidestake_model, "updateSideStakeTableModel", Qt::QueuedConnection); +} + +} // anonymous namespace + SideStakeLessThan::SideStakeLessThan(int column, Qt::SortOrder order) : m_column(column) , m_order(order) @@ -48,7 +58,7 @@ class SideStakeTablePriv { m_cached_sidestakes.clear(); - std::vector core_sidestakes = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(); + std::vector core_sidestakes = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(false, true); m_cached_sidestakes.reserve(core_sidestakes.size()); @@ -83,6 +93,8 @@ SideStakeTableModel::SideStakeTableModel(OptionsModel* parent) m_columns << tr("Address") << tr("Allocation") << tr("Description") << tr("Status"); m_priv.reset(new SideStakeTablePriv()); + subscribeToCoreSignals(); + // load initial data refresh(); } @@ -116,7 +128,7 @@ QVariant SideStakeTableModel::data(const QModelIndex &index, int role) const GRC::SideStake* rec = static_cast(index.internalPointer()); const auto column = static_cast(index.column()); - if (role == Qt::DisplayRole) { + if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (column) { case Address: return QString::fromStdString(rec->m_key.ToString()); @@ -146,6 +158,126 @@ QVariant SideStakeTableModel::data(const QModelIndex &index, int role) const return QVariant(); } +bool SideStakeTableModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) { + return false; + } + + GRC::SideStakeRegistry& registry = GRC::GetSideStakeRegistry(); + + GRC::SideStake* rec = static_cast(index.internalPointer()); + + if (role != Qt::EditRole) { + return false; + } + + m_edit_status = OK; + + switch (index.column()) + { + case Address: + { + CBitcoinAddress address; + address.SetString(value.toString().toStdString()); + + + if (rec->m_key == address) { + m_edit_status = NO_CHANGES; + return false; + } else if (!address.IsValid()) { + m_edit_status = INVALID_ADDRESS; + return false; + } + + std::vector sidestakes = registry.Try(address, true); + + if (!sidestakes.empty()) { + m_edit_status = DUPLICATE_ADDRESS; + return false; + } + + // There is no valid state change left for address. If you are editing the item, the address field is + // not editable, so will be NO_CHANGES. For a non-matching address, it will be covered by the dialog + // in New mode. + break; + } + case Allocation: + { + double prior_total_allocation = 0.0; + + // Save the original local sidestake (also in the core). + GRC::SideStake orig_sidestake = *rec; + + for (const auto& entry : registry.ActiveSideStakeEntries(false, true)) { + if (entry->m_key == orig_sidestake.m_key) { + continue; + } + + prior_total_allocation += entry->m_allocation * 100.0; + } + + if (rec->m_allocation * 100.0 == value.toDouble()) { + m_edit_status = NO_CHANGES; + return false; + } + + if (value.toDouble() < 0.0 || prior_total_allocation + value.toDouble() > 100.0) { + m_edit_status = INVALID_ALLOCATION; + + LogPrint(BCLog::LogFlags::VERBOSE, "INFO: %s: m_edit_status = %i", + __func__, + (int) m_edit_status); + + return false; + } + + // Delete the original sidestake + registry.NonContractDelete(orig_sidestake.m_key, false); + + // Add back the sidestake with the modified allocation + registry.NonContractAdd(GRC::SideStake(orig_sidestake.m_key, + value.toDouble() / 100.0, + orig_sidestake.m_description, + int64_t {0}, + uint256 {}, + orig_sidestake.m_status.Value()), true); + + break; + } + case Description: + { + if (rec->m_description == value.toString().toStdString()) { + m_edit_status = NO_CHANGES; + return false; + } + + // Save the original local sidestake (also in the core). + GRC::SideStake orig_sidestake = *rec; + + // Delete the original sidestake + registry.NonContractDelete(orig_sidestake.m_key, false); + + // Add back the sidestake with the modified allocation + registry.NonContractAdd(GRC::SideStake(orig_sidestake.m_key, + orig_sidestake.m_allocation, + value.toString().toStdString(), + int64_t {0}, + uint256 {}, + orig_sidestake.m_status.Value()), true); + + break; + } + case Status: + // Status is not editable + return false; + } + + updateSideStakeTableModel(); + + return true; +} + QVariant SideStakeTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if(orientation == Qt::Horizontal) @@ -160,9 +292,19 @@ QVariant SideStakeTableModel::headerData(int section, Qt::Orientation orientatio Qt::ItemFlags SideStakeTableModel::flags(const QModelIndex &index) const { - if (!index.isValid()) return Qt::NoItemFlags; + if (!index.isValid()) { + return Qt::NoItemFlags; + } + + GRC::SideStake* rec = static_cast(index.internalPointer()); Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + + if (rec->m_status == GRC::SideStakeStatus::ACTIVE + && (index.column() == Allocation || index.column() == Description)) { + retval |= Qt::ItemIsEditable; + } + return retval; } @@ -198,13 +340,22 @@ QString SideStakeTableModel::addRow(const QString &address, const QString &alloc // UI model. std::vector core_local_sidestake = registry.Try(sidestake_address, true); + double prior_total_allocation = 0.0; + + // Get total allocation of all active/mandatory sidestake entries + for (const auto& entry : registry.ActiveSideStakeEntries(false, true)) { + prior_total_allocation += entry->m_allocation * 100.0; + } + if (!core_local_sidestake.empty()) { m_edit_status = DUPLICATE_ADDRESS; return QString(); } + // The new allocation must be parseable as a double, must be greater than or equal to 0, and + // must result in a total allocation of less than 100. if (!ParseDouble(allocation.toStdString(), &sidestake_allocation) - && (sidestake_allocation < 0.0 || sidestake_allocation > 1.0)) { + || sidestake_allocation < 0.0 || prior_total_allocation + sidestake_allocation > 100.0) { m_edit_status = INVALID_ALLOCATION; return QString(); } @@ -223,6 +374,25 @@ QString SideStakeTableModel::addRow(const QString &address, const QString &alloc return QString::fromStdString(sidestake_address.ToString()); } +bool SideStakeTableModel::removeRows(int row, int count, const QModelIndex &parent) +{ + Q_UNUSED(parent); + GRC::SideStake* rec = m_priv->index(row); + + if(count != 1 || !rec || rec->m_status == GRC::SideStakeStatus::MANDATORY) + { + // Can only remove one row at a time, and cannot remove rows not in model. + // Also refuse to remove mandatory sidestakes. + return false; + } + + GRC::GetSideStakeRegistry().NonContractDelete(rec->m_key); + + updateSideStakeTableModel(); + + return true; +} + SideStakeTableModel::EditStatus SideStakeTableModel::getEditStatus() const { return m_edit_status; @@ -232,6 +402,9 @@ void SideStakeTableModel::refresh() { Q_EMIT layoutAboutToBeChanged(); m_priv->refreshSideStakes(); + + m_edit_status = OK; + Q_EMIT layoutChanged(); } @@ -249,12 +422,6 @@ void SideStakeTableModel::updateSideStakeTableModel() emit updateSideStakeTableModelSig(); } -static void RwSettingsUpdated(SideStakeTableModel* sidestake_model) -{ - qDebug() << QString("%1").arg(__func__); - QMetaObject::invokeMethod(sidestake_model, "updateSideStakeTableModel", Qt::QueuedConnection); -} - void SideStakeTableModel::subscribeToCoreSignals() { // Connect signals to client diff --git a/src/qt/sidestaketablemodel.h b/src/qt/sidestaketablemodel.h index 24d7263e72..cd265c7461 100644 --- a/src/qt/sidestaketablemodel.h +++ b/src/qt/sidestaketablemodel.h @@ -28,6 +28,10 @@ class SideStakeLessThan Qt::SortOrder m_order; }; +//! +//! \brief The SideStakeTableModel class represents the core sidestake registry as a model which can be consumed +//! and updated by the GUI. +//! class SideStakeTableModel : public QAbstractTableModel { Q_OBJECT @@ -57,9 +61,11 @@ class SideStakeTableModel : public QAbstractTableModel int rowCount(const QModelIndex &parent) const; int columnCount(const QModelIndex &parent) const; QVariant data(const QModelIndex &index, int role) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); QVariant headerData(int section, Qt::Orientation orientation, int role) const; QModelIndex index(int row, int column, const QModelIndex &parent) const; Qt::ItemFlags flags(const QModelIndex &index) const; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); void sort(int column, Qt::SortOrder order); /*@}*/ @@ -80,11 +86,12 @@ public Q_SLOTS: void subscribeToCoreSignals(); void unsubscribeFromCoreSignals(); - void updateSideStakeTableModel(); - signals: void updateSideStakeTableModelSig(); + +public slots: + void updateSideStakeTableModel(); }; #endif // BITCOIN_QT_SIDESTAKETABLEMODEL_H diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 373ba3e98b..dc0cc887ce 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -108,7 +108,8 @@ UniValue getstakinginfo(const UniValue& params, bool fHelp) } obj.pushKV("stake-splitting", stakesplitting); - vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(); + // This is what the miner sees... + vSideStakeAlloc = GRC::GetSideStakeRegistry().ActiveSideStakeEntries(false, false); sidestaking.pushKV("local_side_staking_enabled", fEnableSideStaking);