diff --git a/src/gridcoin/sidestake.cpp b/src/gridcoin/sidestake.cpp index dc27fdce93..e21678a347 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 bool& local_only) +const std::vector SideStakeRegistry::ActiveSideStakeEntries(const bool& local_only, + const bool& include_zero_alloc) { std::vector sidestakes; double allocation_sum = 0.0; @@ -225,8 +226,10 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries(const 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 ((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; + } } } } @@ -240,8 +243,10 @@ const std::vector SideStakeRegistry::ActiveSideStakeEntries(const 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; + } } } } @@ -579,22 +584,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) - { - 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])); + 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])); + } } - } } @@ -652,9 +657,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; } diff --git a/src/gridcoin/sidestake.h b/src/gridcoin/sidestake.h index 54583e6858..dccb6b216a 100644 --- a/src/gridcoin/sidestake.h +++ b/src/gridcoin/sidestake.h @@ -412,7 +412,7 @@ class SideStakeRegistry : public IContractHandler //! //! \return A vector of smart pointers to sidestake entries. //! - const std::vector ActiveSideStakeEntries(const bool& local_only = false); + const std::vector ActiveSideStakeEntries(const bool& local_only, const bool& include_zero_alloc); //! //! \brief Get the current sidestake entry for the specified key string. 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/editsidestakedialog.cpp b/src/qt/editsidestakedialog.cpp index 5fe71ac552..fac1001e4d 100644 --- a/src/qt/editsidestakedialog.cpp +++ b/src/qt/editsidestakedialog.cpp @@ -4,7 +4,6 @@ #include "editsidestakedialog.h" #include "ui_editsidestakedialog.h" -#include "optionsmodel.h" #include "sidestaketablemodel.h" #include "guiutil.h" #include "qt/decoration.h" @@ -29,10 +28,16 @@ EditSideStakeDialog::EditSideStakeDialog(Mode mode, QWidget* parent) { 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; } @@ -45,7 +50,7 @@ EditSideStakeDialog::~EditSideStakeDialog() delete ui; } -void EditSideStakeDialog::setModel(OptionsModel *model) +void EditSideStakeDialog::setModel(SideStakeTableModel* model) { this->model = model; if (!model) { @@ -56,11 +61,28 @@ void EditSideStakeDialog::setModel(OptionsModel *model) mapper->addMapping(ui->addressLineEdit, SideStakeTableModel::Address); mapper->addMapping(ui->allocationLineEdit, SideStakeTableModel::Allocation); mapper->addMapping(ui->descriptionLineEdit, SideStakeTableModel::Description); + mapper->addMapping(ui->statusLineEdit, SideStakeTableModel::Status); } void EditSideStakeDialog::loadRow(int row) { mapper->setCurrentIndex(row); + + GRC::SideStake* sidestake = static_cast(model->index(row, 0, QModelIndex()).internalPointer()); + + LogPrint(BCLog::LogFlags::VERBOSE, "INFO: %s: ui->addressLineEdit = %s, ui->allocationLineEdit = %s, " + "sidestake address = %s, sidestake allocation = %f", + __func__, + ui->addressLineEdit->text().toStdString(), + ui->allocationLineEdit->text().toStdString(), + sidestake->m_key.ToString(), + sidestake->m_allocation); + + // For some reason the mapper is not setting the fields from the selected row. Populate manually. + 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() @@ -72,9 +94,9 @@ bool EditSideStakeDialog::saveCurrentRow() switch (mode) { case NewSideStake: - address = model->getSideStakeTableModel()->addRow(ui->addressLineEdit->text(), - ui->allocationLineEdit->text(), - ui->descriptionLineEdit->text()); + address = model->addRow(ui->addressLineEdit->text(), + ui->allocationLineEdit->text(), + ui->descriptionLineEdit->text()); break; case EditSideStake: if (mapper->submit()) { @@ -93,7 +115,7 @@ void EditSideStakeDialog::accept() if (!saveCurrentRow()) { - switch(model->getSideStakeTableModel()->getEditStatus()) + switch(model->getEditStatus()) { case SideStakeTableModel::OK: // Failed with unknown reason. Just reject. @@ -115,23 +137,14 @@ void EditSideStakeDialog::accept() break; case SideStakeTableModel::INVALID_ALLOCATION: QMessageBox::warning(this, windowTitle(), - tr("The entered allocation is not valid.").arg(ui->allocationLineEdit->text()), + 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(); } - -QString EditSideStakeDialog::getAddress() const -{ - return address; -} - -void EditSideStakeDialog::setAddress(const QString &address) -{ - this->address = address; - ui->addressLineEdit->setText(address); -} diff --git a/src/qt/editsidestakedialog.h b/src/qt/editsidestakedialog.h index b9256342b6..c707eaa446 100644 --- a/src/qt/editsidestakedialog.h +++ b/src/qt/editsidestakedialog.h @@ -14,7 +14,7 @@ QT_END_NAMESPACE namespace Ui { class EditSideStakeDialog; } -class OptionsModel; +class SideStakeTableModel; /** Dialog for editing an address and associated information. */ @@ -31,12 +31,9 @@ class EditSideStakeDialog : public QDialog explicit EditSideStakeDialog(Mode mode, QWidget* parent = nullptr); ~EditSideStakeDialog(); - void setModel(OptionsModel *model); + void setModel(SideStakeTableModel* model); void loadRow(int row); - QString getAddress() const; - void setAddress(const QString &address); - public slots: void accept(); @@ -46,7 +43,7 @@ public slots: Ui::EditSideStakeDialog *ui; QDataWidgetMapper *mapper; Mode mode; - OptionsModel *model; + SideStakeTableModel *model; QString address; }; diff --git a/src/qt/forms/editsidestakedialog.ui b/src/qt/forms/editsidestakedialog.ui index 5e528b2b4c..a2dd71f36b 100644 --- a/src/qt/forms/editsidestakedialog.ui +++ b/src/qt/forms/editsidestakedialog.ui @@ -15,141 +15,114 @@ - - - Qt::Vertical - - - - 20 - 40 - - - - - - - + + + + + Qt::Vertical + + + + 20 + 40 + + + + + Address - - + + + + + - Qt::Horizontal + Qt::Vertical - 10 - 20 + 20 + 40 - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - + Allocation - - + + + + + - Qt::Horizontal + Qt::Vertical - 10 - 20 + 20 + 40 - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - + Description - - + + + + + - Qt::Horizontal - - - QSizePolicy::Expanding + Qt::Vertical - 10 - 20 + 20 + 40 - - + + + + + + + Status + + + + + + + Qt::Vertical + + + + 20 + 40 + + + - - - - Qt::Vertical - - - - 20 - 40 - - - - 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 96757b2e79..29100fc75f 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -11,6 +11,7 @@ #include "sidestaketablemodel.h" #include "editsidestakedialog.h" +#include #include #include #include @@ -147,6 +148,7 @@ void OptionsDialog::setModel(OptionsModel *model) sidestake_model->refresh(); + //ui->sidestakingTableView->setModel(m_sidestake_proxy_model); ui->sidestakingTableView->setModel(sidestake_model); ui->sidestakingTableView->verticalHeader()->hide(); ui->sidestakingTableView->setSelectionBehavior(QAbstractItemView::SelectRows); @@ -161,11 +163,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") */ @@ -254,7 +267,8 @@ void OptionsDialog::setSaveButtonState(bool fState) void OptionsDialog::on_okButton_clicked() { - mapper->submit(); + refreshSideStakeTableModel(); + accept(); } @@ -265,8 +279,6 @@ void OptionsDialog::on_cancelButton_clicked() void OptionsDialog::on_applyButton_clicked() { - mapper->submit(); - refreshSideStakeTableModel(); disableApplyButton(); @@ -280,29 +292,59 @@ void OptionsDialog::newSideStakeButton_clicked() EditSideStakeDialog dialog(EditSideStakeDialog::NewSideStake, this); - dialog.setModel(model); + dialog.setModel(model->getSideStakeTableModel()); dialog.exec(); } void OptionsDialog::editSideStakeButton_clicked() { - if (!model) { + if (!model || !ui->sidestakingTableView->selectionModel()) { return; } - EditSideStakeDialog dialog(EditSideStakeDialog::EditSideStake, this); + QModelIndexList indexes = ui->sidestakingTableView->selectionModel()->selectedRows(); - dialog.setModel(model); + 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; } } @@ -436,9 +478,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) @@ -497,5 +542,51 @@ 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); + +} diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index bf95e9fce3..f734a1e7a4 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -7,6 +7,7 @@ namespace Ui { class OptionsDialog; } class OptionsModel; +class QSortFilterProxyModel; class MonitoredDataMapper; class QValidatedLineEdit; @@ -42,6 +43,7 @@ private slots: void newSideStakeButton_clicked(); void editSideStakeButton_clicked(); + void deleteSideStakeButton_clicked(); void showRestartWarning_Proxy(); void showRestartWarning_Lang(); @@ -54,6 +56,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 +64,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; @@ -72,6 +76,8 @@ private slots: bool fStakingEfficiencyValid; bool fMinStakeSplitValueValid; + //QSortFilterProxyModel *m_sidestake_proxy_model; + enum SideStakeTableColumnWidths { ADDRESS_COLUMN_WIDTH = 200, @@ -81,6 +87,8 @@ private slots: STATUS_COLUMN_WIDTH = 150 }; +private slots: + void sidestakeSelectionChanged(); }; #endif // BITCOIN_QT_OPTIONSDIALOG_H diff --git a/src/qt/sidestaketablemodel.cpp b/src/qt/sidestaketablemodel.cpp index 8514b10df9..93b8fb76de 100644 --- a/src/qt/sidestaketablemodel.cpp +++ b/src/qt/sidestaketablemodel.cpp @@ -48,7 +48,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()); @@ -146,6 +146,120 @@ 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()); + + m_edit_status = OK; + + if (role == Qt::EditRole) { + 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; + } else if (value.toDouble() < 0.0 || prior_total_allocation + value.toDouble() > 100.0) { + m_edit_status = INVALID_ALLOCATION; + 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; + } + + return false; +} + QVariant SideStakeTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if(orientation == Qt::Horizontal) @@ -160,9 +274,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 +322,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 +356,26 @@ 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 +385,9 @@ void SideStakeTableModel::refresh() { Q_EMIT layoutAboutToBeChanged(); m_priv->refreshSideStakes(); + + m_edit_status = OK; + Q_EMIT layoutChanged(); } diff --git a/src/qt/sidestaketablemodel.h b/src/qt/sidestaketablemodel.h index 24d7263e72..ab94e14d38 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); /*@}*/ 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);