mirror of
https://github.com/dashpay/dash.git
synced 2024-12-28 21:42:47 +01:00
71e8caf4b9
* coinjoin: make CCoinJoinServer managed pointer, assign CConnman during init * coinjoin: make CCoinJoinClientQueueManager managed pointer, assign CConnman during init * sporks: move spork validation logic downwards after CConnman initialization * sporks: make CSporkManager a pointer, reduce global invocations * governance: make CGovernanceManager a pointer, reduce global invocations * llmq: migrate LLMQ subsystem raw pointers to managed pointers * masternode: make activeMasternodeManager a managed pointer * masternode: make masternodeSync a managed pointer, assign CConnman during init * refactor: make instantsend helper functions class members * fix: send empty CDeterministicMNList if pointer isn't initialized yet * fix: refactor governance object retrieval logic across node and ui Update src/interfaces/node.cpp Co-authored-by: UdjinM6 <UdjinM6@users.noreply.github.com> Co-authored-by: UdjinM6 <UdjinM6@users.noreply.github.com>
409 lines
13 KiB
C++
409 lines
13 KiB
C++
#include <qt/forms/ui_governancelist.h>
|
|
#include <qt/governancelist.h>
|
|
|
|
#include <chainparams.h>
|
|
#include <clientversion.h>
|
|
#include <coins.h>
|
|
#include <evo/deterministicmns.h>
|
|
#include <netbase.h>
|
|
#include <qt/clientmodel.h>
|
|
#include <qt/guiutil.h>
|
|
|
|
#include <univalue.h>
|
|
|
|
#include <QAbstractItemView>
|
|
#include <QDesktopServices>
|
|
#include <QMessageBox>
|
|
#include <QTableWidgetItem>
|
|
#include <QUrl>
|
|
#include <QtGui/QClipboard>
|
|
|
|
///
|
|
/// Proposal wrapper
|
|
///
|
|
|
|
Proposal::Proposal(const CGovernanceObject& _govObj, QObject* parent) :
|
|
QObject(parent),
|
|
govObj(_govObj)
|
|
{
|
|
UniValue prop_data;
|
|
if (prop_data.read(govObj.GetDataAsPlainString())) {
|
|
if (UniValue titleValue = find_value(prop_data, "name"); titleValue.isStr()) {
|
|
m_title = QString::fromStdString(titleValue.get_str());
|
|
}
|
|
|
|
if (UniValue paymentStartValue = find_value(prop_data, "start_epoch"); paymentStartValue.isNum()) {
|
|
m_startDate = QDateTime::fromSecsSinceEpoch(paymentStartValue.get_int64());
|
|
}
|
|
|
|
if (UniValue paymentEndValue = find_value(prop_data, "end_epoch"); paymentEndValue.isNum()) {
|
|
m_endDate = QDateTime::fromSecsSinceEpoch(paymentEndValue.get_int64());
|
|
}
|
|
|
|
if (UniValue amountValue = find_value(prop_data, "payment_amount"); amountValue.isNum()) {
|
|
m_paymentAmount = amountValue.get_real();
|
|
}
|
|
|
|
if (UniValue urlValue = find_value(prop_data, "url"); urlValue.isStr()) {
|
|
m_url = QString::fromStdString(urlValue.get_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
QString Proposal::title() const { return m_title; }
|
|
|
|
QString Proposal::hash() const { return QString::fromStdString(govObj.GetHash().ToString()); }
|
|
|
|
QDateTime Proposal::startDate() const { return m_startDate; }
|
|
|
|
QDateTime Proposal::endDate() const { return m_endDate; }
|
|
|
|
float Proposal::paymentAmount() const { return m_paymentAmount; }
|
|
|
|
QString Proposal::url() const { return m_url; }
|
|
|
|
bool Proposal::isActive() const
|
|
{
|
|
LOCK(cs_main);
|
|
std::string strError;
|
|
return govObj.IsValidLocally(strError, false);
|
|
}
|
|
|
|
QString Proposal::votingStatus(const int nAbsVoteReq) const
|
|
{
|
|
// Voting status...
|
|
// TODO: determine if voting is in progress vs. funded or not funded for past proposals.
|
|
// see CSuperblock::GetNearestSuperblocksHeights(nBlockHeight, nLastSuperblock, nNextSuperblock);
|
|
const int absYesCount = govObj.GetAbsoluteYesCount(VOTE_SIGNAL_FUNDING);
|
|
QString qStatusString;
|
|
if (absYesCount >= nAbsVoteReq) {
|
|
// Could use govObj.IsSetCachedFunding here, but need nAbsVoteReq to display numbers anyway.
|
|
return tr("Passing +%1").arg(absYesCount - nAbsVoteReq);
|
|
} else {
|
|
return tr("Needs additional %1 votes").arg(nAbsVoteReq - absYesCount);
|
|
}
|
|
}
|
|
|
|
int Proposal::GetAbsoluteYesCount() const
|
|
{
|
|
return govObj.GetAbsoluteYesCount(VOTE_SIGNAL_FUNDING);
|
|
}
|
|
|
|
void Proposal::openUrl() const
|
|
{
|
|
QDesktopServices::openUrl(QUrl(m_url));
|
|
}
|
|
|
|
QString Proposal::toJson() const
|
|
{
|
|
const auto json = govObj.ToJson();
|
|
return QString::fromStdString(json.write(2));
|
|
}
|
|
|
|
///
|
|
/// Proposal Model
|
|
///
|
|
|
|
|
|
int ProposalModel::rowCount(const QModelIndex& index) const
|
|
{
|
|
return m_data.count();
|
|
}
|
|
|
|
int ProposalModel::columnCount(const QModelIndex& index) const
|
|
{
|
|
return Column::_COUNT;
|
|
}
|
|
|
|
QVariant ProposalModel::data(const QModelIndex& index, int role) const
|
|
{
|
|
if (role != Qt::DisplayRole && role != Qt::EditRole) return {};
|
|
const auto proposal = m_data[index.row()];
|
|
switch(role) {
|
|
case Qt::DisplayRole:
|
|
{
|
|
switch (index.column()) {
|
|
case Column::HASH:
|
|
return proposal->hash();
|
|
case Column::TITLE:
|
|
return proposal->title();
|
|
case Column::START_DATE:
|
|
return proposal->startDate().date();
|
|
case Column::END_DATE:
|
|
return proposal->endDate().date();
|
|
case Column::PAYMENT_AMOUNT:
|
|
return proposal->paymentAmount();
|
|
case Column::IS_ACTIVE:
|
|
return proposal->isActive() ? tr("Yes") : tr("No");
|
|
case Column::VOTING_STATUS:
|
|
return proposal->votingStatus(nAbsVoteReq);
|
|
default:
|
|
return {};
|
|
};
|
|
break;
|
|
}
|
|
case Qt::EditRole:
|
|
{
|
|
// Edit role is used for sorting, so return the raw values where possible
|
|
switch (index.column()) {
|
|
case Column::HASH:
|
|
return proposal->hash();
|
|
case Column::TITLE:
|
|
return proposal->title();
|
|
case Column::START_DATE:
|
|
return proposal->startDate();
|
|
case Column::END_DATE:
|
|
return proposal->endDate();
|
|
case Column::PAYMENT_AMOUNT:
|
|
return proposal->paymentAmount();
|
|
case Column::IS_ACTIVE:
|
|
return proposal->isActive();
|
|
case Column::VOTING_STATUS:
|
|
return proposal->GetAbsoluteYesCount();
|
|
default:
|
|
return {};
|
|
};
|
|
break;
|
|
}
|
|
};
|
|
return {};
|
|
}
|
|
|
|
QVariant ProposalModel::headerData(int section, Qt::Orientation orientation, int role) const
|
|
{
|
|
if (orientation != Qt::Horizontal || role != Qt::DisplayRole) return {};
|
|
switch (section) {
|
|
case Column::HASH:
|
|
return tr("Hash");
|
|
case Column::TITLE:
|
|
return tr("Title");
|
|
case Column::START_DATE:
|
|
return tr("Start");
|
|
case Column::END_DATE:
|
|
return tr("End");
|
|
case Column::PAYMENT_AMOUNT:
|
|
return tr("Amount");
|
|
case Column::IS_ACTIVE:
|
|
return tr("Active");
|
|
case Column::VOTING_STATUS:
|
|
return tr("Status");
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
int ProposalModel::columnWidth(int section)
|
|
{
|
|
switch (section) {
|
|
case Column::HASH:
|
|
return 80;
|
|
case Column::TITLE:
|
|
return 220;
|
|
case Column::START_DATE:
|
|
case Column::END_DATE:
|
|
case Column::PAYMENT_AMOUNT:
|
|
return 110;
|
|
case Column::IS_ACTIVE:
|
|
return 80;
|
|
case Column::VOTING_STATUS:
|
|
return 220;
|
|
default:
|
|
return 80;
|
|
}
|
|
}
|
|
|
|
void ProposalModel::append(const Proposal* proposal)
|
|
{
|
|
beginInsertRows({}, m_data.count(), m_data.count());
|
|
m_data.append(proposal);
|
|
endInsertRows();
|
|
}
|
|
|
|
void ProposalModel::remove(int row)
|
|
{
|
|
beginRemoveRows({}, row, row);
|
|
delete m_data.at(row);
|
|
m_data.removeAt(row);
|
|
endRemoveRows();
|
|
}
|
|
|
|
void ProposalModel::reconcile(const std::vector<const Proposal*>& proposals)
|
|
{
|
|
// Vector of m_data.count() false values. Going through new proposals,
|
|
// set keep_index true for each old proposal found in the new proposals.
|
|
// After going through new proposals, remove any existing proposals that
|
|
// weren't found (and are still false).
|
|
std::vector<bool> keep_index(m_data.count(), false);
|
|
for (const auto proposal : proposals) {
|
|
bool found = false;
|
|
for (int i = 0; i < m_data.count(); ++i) {
|
|
if (m_data.at(i)->hash() == proposal->hash()) {
|
|
found = true;
|
|
keep_index.at(i) = true;
|
|
if (m_data.at(i)->GetAbsoluteYesCount() != proposal->GetAbsoluteYesCount()) {
|
|
// replace proposal to update vote count
|
|
delete m_data.at(i);
|
|
m_data.replace(i, proposal);
|
|
Q_EMIT dataChanged(createIndex(i, Column::VOTING_STATUS), createIndex(i, Column::VOTING_STATUS));
|
|
} else {
|
|
// no changes
|
|
delete proposal;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
append(proposal);
|
|
}
|
|
}
|
|
for (unsigned int i = keep_index.size(); i > 0; --i) {
|
|
if (!keep_index.at(i - 1)) {
|
|
remove(i - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void ProposalModel::setVotingParams(int newAbsVoteReq)
|
|
{
|
|
if (this->nAbsVoteReq != newAbsVoteReq) {
|
|
this->nAbsVoteReq = newAbsVoteReq;
|
|
// Changing either of the voting params may change the voting status
|
|
// column. Emit signal to force recalculation.
|
|
Q_EMIT dataChanged(createIndex(0, Column::VOTING_STATUS), createIndex(rowCount(), Column::VOTING_STATUS));
|
|
}
|
|
}
|
|
|
|
const Proposal* ProposalModel::getProposalAt(const QModelIndex& index) const
|
|
{
|
|
return m_data[index.row()];
|
|
}
|
|
|
|
//
|
|
// Governance Tab main widget.
|
|
//
|
|
|
|
GovernanceList::GovernanceList(QWidget* parent) :
|
|
QWidget(parent),
|
|
ui(std::make_unique<Ui::GovernanceList>()),
|
|
proposalModel(new ProposalModel(this)),
|
|
proposalModelProxy(new QSortFilterProxyModel(this)),
|
|
proposalContextMenu(new QMenu(this)),
|
|
timer(new QTimer(this))
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
GUIUtil::setFont({ui->label_count_2, ui->countLabel}, GUIUtil::FontWeight::Bold, 14);
|
|
GUIUtil::setFont({ui->label_filter_2}, GUIUtil::FontWeight::Normal, 15);
|
|
|
|
proposalModelProxy->setSourceModel(proposalModel);
|
|
ui->govTableView->setModel(proposalModelProxy);
|
|
ui->govTableView->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
ui->govTableView->horizontalHeader()->setStretchLastSection(true);
|
|
|
|
for (int i = 0; i < proposalModel->columnCount(); ++i) {
|
|
ui->govTableView->setColumnWidth(i, proposalModel->columnWidth(i));
|
|
}
|
|
|
|
// Set up sorting.
|
|
proposalModelProxy->setSortRole(Qt::EditRole);
|
|
ui->govTableView->setSortingEnabled(true);
|
|
ui->govTableView->sortByColumn(ProposalModel::Column::START_DATE, Qt::DescendingOrder);
|
|
|
|
// Set up filtering.
|
|
proposalModelProxy->setFilterKeyColumn(ProposalModel::Column::TITLE); // filter by title column...
|
|
connect(ui->filterLineEdit, &QLineEdit::textChanged, proposalModelProxy, &QSortFilterProxyModel::setFilterFixedString);
|
|
|
|
// Changes to number of rows should update proposal count display.
|
|
connect(proposalModelProxy, &QSortFilterProxyModel::rowsInserted, this, &GovernanceList::updateProposalCount);
|
|
connect(proposalModelProxy, &QSortFilterProxyModel::rowsRemoved, this, &GovernanceList::updateProposalCount);
|
|
connect(proposalModelProxy, &QSortFilterProxyModel::layoutChanged, this, &GovernanceList::updateProposalCount);
|
|
|
|
// Enable CustomContextMenu on the table to make the view emit customContextMenuRequested signal.
|
|
ui->govTableView->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(ui->govTableView, &QTableView::customContextMenuRequested, this, &GovernanceList::showProposalContextMenu);
|
|
connect(ui->govTableView, &QTableView::doubleClicked, this, &GovernanceList::showAdditionalInfo);
|
|
|
|
connect(timer, &QTimer::timeout, this, &GovernanceList::updateProposalList);
|
|
|
|
GUIUtil::updateFonts();
|
|
}
|
|
|
|
GovernanceList::~GovernanceList() = default;
|
|
|
|
void GovernanceList::setClientModel(ClientModel* model)
|
|
{
|
|
this->clientModel = model;
|
|
updateProposalList();
|
|
}
|
|
|
|
void GovernanceList::updateProposalList()
|
|
{
|
|
if (this->clientModel) {
|
|
// A proposal is considered passing if (YES votes - NO votes) >= (Total Number of Masternodes / 10),
|
|
// count total valid (ENABLED) masternodes to determine passing threshold.
|
|
// Need to query number of masternodes here with access to clientModel.
|
|
const int nMnCount = clientModel->getMasternodeList().GetValidMNsCount();
|
|
const int nAbsVoteReq = std::max(Params().GetConsensus().nGovernanceMinQuorum, nMnCount / 10);
|
|
proposalModel->setVotingParams(nAbsVoteReq);
|
|
|
|
std::vector<CGovernanceObject> govObjList;
|
|
clientModel->getAllGovernanceObjects(govObjList);
|
|
std::vector<const Proposal*> newProposals;
|
|
for (const auto& govObj : govObjList) {
|
|
if (govObj.GetObjectType() != GOVERNANCE_OBJECT_PROPOSAL) {
|
|
continue; // Skip triggers.
|
|
}
|
|
|
|
newProposals.emplace_back(new Proposal(govObj, proposalModel));
|
|
}
|
|
proposalModel->reconcile(newProposals);
|
|
}
|
|
|
|
// Schedule next update.
|
|
timer->start(GOVERNANCELIST_UPDATE_SECONDS * 1000);
|
|
}
|
|
|
|
void GovernanceList::updateProposalCount() const
|
|
{
|
|
ui->countLabel->setText(QString::number(proposalModelProxy->rowCount()));
|
|
}
|
|
|
|
void GovernanceList::showProposalContextMenu(const QPoint& pos)
|
|
{
|
|
const auto index = ui->govTableView->indexAt(pos);
|
|
|
|
if (!index.isValid()) {
|
|
return;
|
|
}
|
|
|
|
const auto proposal = proposalModel->getProposalAt(proposalModelProxy->mapToSource(index));
|
|
if (proposal == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// right click menu with option to open proposal url
|
|
QAction* openProposalUrl = new QAction(proposal->url(), this);
|
|
proposalContextMenu->clear();
|
|
proposalContextMenu->addAction(openProposalUrl);
|
|
connect(openProposalUrl, &QAction::triggered, proposal, &Proposal::openUrl);
|
|
proposalContextMenu->exec(QCursor::pos());
|
|
}
|
|
|
|
void GovernanceList::showAdditionalInfo(const QModelIndex& index)
|
|
{
|
|
if (!index.isValid()) {
|
|
return;
|
|
}
|
|
|
|
const auto proposal = proposalModel->getProposalAt(proposalModelProxy->mapToSource(index));
|
|
if (proposal == nullptr) {
|
|
return;
|
|
}
|
|
|
|
const auto windowTitle = tr("Proposal Info: %1").arg(proposal->title());
|
|
const auto json = proposal->toJson();
|
|
|
|
QMessageBox::information(this, windowTitle, json);
|
|
}
|