Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a773635
qt: move masternode list fetch logic to thread and use debounce timer
kwvg Feb 13, 2026
5af2bb9
qt: expose `NotifyGovernanceChanged` signal to UI code
kwvg Feb 13, 2026
c85e5cb
qt: move proposal list fetch to thread and use debounce timer
kwvg Feb 13, 2026
a7328e9
qt: add moon phase icons for governance cycle indicator
kwvg Feb 12, 2026
662703e
qt: add governance cycle status bar icon
kwvg Feb 13, 2026
8257eac
qt: decouple chain sync indicator from governance sync
kwvg Feb 11, 2026
9bb7b0a
qt: reorder columns in "Governance" tab, make width elastic for key cols
kwvg Jan 22, 2026
b2b9aa1
qt: change layout of "Governance" tab controls for readability
kwvg Jan 24, 2026
c0d8454
qt: report compact voting status and expanded details in tooltip
kwvg Feb 13, 2026
f45fe85
qt: use monospace font for hashes in "Governance" tab
kwvg Jan 22, 2026
4295471
qt: report proposal info using QTextEdit instead of an alert with JSON
kwvg Feb 6, 2026
552f591
qt: report more proposal information in description, calculate payments
kwvg Jan 22, 2026
af65c0e
qt: cleanup proposal context menu, add copy JSON and visit URL options
kwvg Feb 11, 2026
5f95737
qt: add voting ballot icon
kwvg Feb 12, 2026
28b67d3
qt: replace "Active" column with icons that reflect voting status
kwvg Feb 13, 2026
1a1d56e
qt: list locally recorded proposals in "Governance" tab
kwvg Feb 13, 2026
2d8cffe
qt: add blank canvas if there's no proposals to display
kwvg Jan 22, 2026
16314e7
qt: adjust layout of details page of proposal wizard to resemble DGT
kwvg Feb 6, 2026
3ca061f
qt: drop JSON/hex confirmation page, use description dialogs instead
kwvg Jan 21, 2026
90e0375
qt: use `SendConfirmationDialog` instead of `QMessageBox` for confirm
kwvg Feb 13, 2026
2883527
qt: remove remaining pages from proposal wizard
kwvg Feb 6, 2026
080c8fb
qt: add "Resume Proposal" for post-confirmation broadcast
kwvg Feb 13, 2026
a0da7de
qt: disable proposal buttons until synced, creation button w/o funds
kwvg Feb 13, 2026
9fb0c35
qt: make governance clock opt-in
kwvg Feb 13, 2026
d1e67ff
fix: disable governance UI when node runs with `--disablegovernance`
kwvg Feb 13, 2026
6cb7a40
qt: add pending status to signal unbroadcast proposals
kwvg Feb 13, 2026
a5ae10c
fix: correct tooltip display unit bug, IBD status flickering
kwvg Feb 13, 2026
3d53afa
fix: hold `cs_main` to for consistent state when fetching GovernanceInfo
kwvg Feb 13, 2026
ce94c3e
fix: improve responsiveness in update after user interaction
kwvg Feb 13, 2026
707ce77
refactor: `qt/proposalwizard.{cpp,h}` -> `qt/proposalcreate.{cpp,h}`
kwvg Jan 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/governance/governance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <governance/validators.h>
#include <masternode/meta.h>
#include <masternode/sync.h>
#include <node/interface_ui.h>
#include <protocol.h>
#include <shutdown.h>
#include <spork.h>
Expand Down Expand Up @@ -318,6 +319,7 @@ void CGovernanceManager::AddGovernanceObjectInternal(CGovernanceObject& insert_o

// SEND NOTIFICATION TO SCRIPT/ZMQ
GetMainSignals().NotifyGovernanceObject(std::make_shared<const Governance::Object>(govobj->Object()), nHash.ToString());
uiInterface.NotifyGovernanceChanged();
}

void CGovernanceManager::AddGovernanceObject(CGovernanceObject& govobj, const CNode* pfrom)
Expand Down
2 changes: 2 additions & 0 deletions src/governance/object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <core_io.h>
#include <index/txindex.h>
#include <logging.h>
#include <node/interface_ui.h>
#include <timedata.h>
#include <util/time.h>
#include <validation.h>
Expand Down Expand Up @@ -155,6 +156,7 @@ bool CGovernanceObject::ProcessVote(CMasternodeMetaMan& mn_metaman, CGovernanceM
// SEND NOTIFICATION TO SCRIPT/ZMQ
GetMainSignals().NotifyGovernanceVote(std::make_shared<CDeterministicMNList>(tip_mn_list),
std::make_shared<const CGovernanceVote>(vote), vote.GetHash().ToString());
uiInterface.NotifyGovernanceChanged();
return true;
}

Expand Down
4 changes: 4 additions & 0 deletions src/interfaces/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,10 @@ class Node
std::function<void(SynchronizationState, interfaces::BlockTip tip, double verification_progress)>;
virtual std::unique_ptr<Handler> handleNotifyHeaderTip(NotifyHeaderTipFn fn) = 0;

//! Register handler for governance data messages.
using NotifyGovernanceChangedFn = std::function<void()>;
virtual std::unique_ptr<Handler> handleNotifyGovernanceChanged(NotifyGovernanceChangedFn fn) = 0;

//! Register handler for masternode list update messages.
using NotifyMasternodeListChangedFn =
std::function<void(const CDeterministicMNList& newList,
Expand Down
3 changes: 3 additions & 0 deletions src/node/interface_ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct UISignals {
boost::signals2::signal<CClientUIInterface::NotifyBlockTipSig> NotifyBlockTip;
boost::signals2::signal<CClientUIInterface::NotifyChainLockSig> NotifyChainLock;
boost::signals2::signal<CClientUIInterface::NotifyHeaderTipSig> NotifyHeaderTip;
boost::signals2::signal<CClientUIInterface::NotifyGovernanceChangedSig> NotifyGovernanceChanged;
boost::signals2::signal<CClientUIInterface::NotifyMasternodeListChangedSig> NotifyMasternodeListChanged;
boost::signals2::signal<CClientUIInterface::NotifyAdditionalDataSyncProgressChangedSig> NotifyAdditionalDataSyncProgressChanged;
boost::signals2::signal<CClientUIInterface::BannedListChangedSig> BannedListChanged;
Expand All @@ -46,6 +47,7 @@ ADD_SIGNALS_IMPL_WRAPPER(ShowProgress);
ADD_SIGNALS_IMPL_WRAPPER(NotifyBlockTip);
ADD_SIGNALS_IMPL_WRAPPER(NotifyChainLock);
ADD_SIGNALS_IMPL_WRAPPER(NotifyHeaderTip);
ADD_SIGNALS_IMPL_WRAPPER(NotifyGovernanceChanged);
ADD_SIGNALS_IMPL_WRAPPER(NotifyMasternodeListChanged);
ADD_SIGNALS_IMPL_WRAPPER(NotifyAdditionalDataSyncProgressChanged);
ADD_SIGNALS_IMPL_WRAPPER(BannedListChanged);
Expand All @@ -61,6 +63,7 @@ void CClientUIInterface::ShowProgress(const std::string& title, int nProgress, b
void CClientUIInterface::NotifyBlockTip(SynchronizationState s, const CBlockIndex* i) { return g_ui_signals.NotifyBlockTip(s, i); }
void CClientUIInterface::NotifyChainLock(const std::string& bestChainLockHash, int bestChainLockHeight) { return g_ui_signals.NotifyChainLock(bestChainLockHash, bestChainLockHeight); }
void CClientUIInterface::NotifyHeaderTip(SynchronizationState s, const CBlockIndex* i) { return g_ui_signals.NotifyHeaderTip(s, i); }
void CClientUIInterface::NotifyGovernanceChanged() { return g_ui_signals.NotifyGovernanceChanged(); }
void CClientUIInterface::NotifyMasternodeListChanged(const CDeterministicMNList& list, const CBlockIndex* i) { return g_ui_signals.NotifyMasternodeListChanged(list, i); }
void CClientUIInterface::NotifyAdditionalDataSyncProgressChanged(double nSyncProgress) { return g_ui_signals.NotifyAdditionalDataSyncProgressChanged(nSyncProgress); }
void CClientUIInterface::BannedListChanged() { return g_ui_signals.BannedListChanged(); }
Expand Down
3 changes: 3 additions & 0 deletions src/node/interface_ui.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ class CClientUIInterface
/** Masternode list has changed */
ADD_SIGNALS_DECL_WRAPPER(NotifyMasternodeListChanged, void, const CDeterministicMNList&, const CBlockIndex*);

/** Governance data changed */
ADD_SIGNALS_DECL_WRAPPER(NotifyGovernanceChanged, void);

/** Additional data sync progress changed */
ADD_SIGNALS_DECL_WRAPPER(NotifyAdditionalDataSyncProgressChanged, void, double nSyncProgress);

Expand Down
4 changes: 4 additions & 0 deletions src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,10 @@ class NodeImpl : public Node
/* verification progress is unused when a header was received */ 0);
}));
}
std::unique_ptr<Handler> handleNotifyGovernanceChanged(NotifyGovernanceChangedFn fn) override
{
return MakeHandler(::uiInterface.NotifyGovernanceChanged_connect(fn));
}
std::unique_ptr<Handler> handleNotifyMasternodeListChanged(NotifyMasternodeListChangedFn fn) override
{
return MakeHandler(
Expand Down
4 changes: 4 additions & 0 deletions src/qt/clientmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,10 @@ void ClientModel::subscribeToCoreSignals()
[this](const CDeterministicMNList& newList, const CBlockIndex* pindex) {
setMasternodeList(interfaces::MakeMNList(newList), pindex);
}));
m_event_handlers.emplace_back(m_node.handleNotifyGovernanceChanged(
[this]() {
Q_EMIT governanceChanged();
}));
}

void ClientModel::unsubscribeFromCoreSignals()
Expand Down
1 change: 1 addition & 0 deletions src/qt/clientmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ class ClientModel : public QObject

Q_SIGNALS:
void numConnectionsChanged(int count);
void governanceChanged();
void masternodeListChanged() const;
void chainLockChanged(const QString& bestChainLockHash, int bestChainLockHeight);
void numBlocksChanged(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, bool header, SynchronizationState sync_state);
Expand Down
171 changes: 112 additions & 59 deletions src/qt/governancelist.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,20 @@

#include <QAbstractItemView>
#include <QMessageBox>
#include <QMetaObject>

//
// 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{new Ui::GovernanceList},
proposalModel{new ProposalModel(this)},
proposalContextMenu{new QMenu(this)},
m_worker(new QObject),
proposalModelProxy{new QSortFilterProxyModel(this)},
m_thread{new QThread(this)},
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this thread doesn't have name; consider assigning it.
Without it threads are just:

  2    Thread 0x726390bfe6c0 (LWP 2145554) "QThread"         __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56
  3    Thread 0x7263913ff6c0 (LWP 2145553) "QThread"         __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56
  4    Thread 0x72639a7fe6c0 (LWP 2145548) "QThread"         __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56
  5    Thread 0x72639afff6c0 (LWP 2145547) "QThread"         __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56
  6    Thread 0x7263ea7ff6c0 (LWP 2145537) "QThread"         __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56
  7    Thread 0x7263f57ff6c0 (LWP 2145536) "QThread"         __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56
  8    Thread 0x7263dbbff6c0 (LWP 2145534) "QThread"         __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56
  9    Thread 0x7263e1fff6c0 (LWP 2145533) "QThread"         __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56

while other do have it:

  12   Thread 0x7264a17fa6c0 (LWP 2145522) "d-qt-clientmodl" __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56

UPDATE: yes, I opened 10 wallets and got 20 more runnign threads, all with the name QThread.

m_timer{new QTimer(this)}
{
ui->setupUi(this);

Expand Down Expand Up @@ -77,31 +79,49 @@ GovernanceList::GovernanceList(QWidget* parent) :
connect(ui->btnCreateProposal, &QPushButton::clicked, this, &GovernanceList::showCreateProposalDialog);
connect(ui->govTableView, &QTableView::doubleClicked, this, &GovernanceList::showAdditionalInfo);

connect(timer, &QTimer::timeout, this, &GovernanceList::updateProposalList);

// Initialize masternode count to 0
ui->mnCountLabel->setText("0");

GUIUtil::updateFonts();

// Background thread for calculating proposal list
m_worker->moveToThread(m_thread);
// Make sure executor object is deleted in its own thread
connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater);
m_thread->start();

// Debounce timer to apply proposal list changes
m_timer->setSingleShot(true);
connect(m_timer, &QTimer::timeout, this, &GovernanceList::updateProposalList);
}

GovernanceList::~GovernanceList() = default;
GovernanceList::~GovernanceList()
{
m_timer->stop();
m_thread->quit();
m_thread->wait();
delete ui;
}

void GovernanceList::setClientModel(ClientModel* model)
{
this->clientModel = model;
if (model != nullptr) {
connect(model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &GovernanceList::updateDisplayUnit);

updateProposalList();
if (clientModel) {
connect(clientModel, &ClientModel::governanceChanged, this, &GovernanceList::handleProposalListChanged);
connect(clientModel->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &GovernanceList::updateDisplayUnit);
m_timer->start(0);
} else {
m_timer->stop();
}
}

void GovernanceList::setWalletModel(WalletModel* model)
{
this->walletModel = model;
if (model && clientModel) {
updateVotingCapability();
if (walletModel && clientModel) {
m_timer->start(0);
} else {
m_timer->stop();
}
}

Expand All @@ -113,35 +133,88 @@ void GovernanceList::updateDisplayUnit()
}
}

void GovernanceList::handleProposalListChanged()
{
if (!clientModel || m_timer->isActive()) {
// Too early or already processing, nothing to do
return;
}
int delay{GOVERNANCELIST_UPDATE_SECONDS * 1000};
if (!clientModel->masternodeSync().isBlockchainSynced()) {
// Currently syncing, reduce refreshes
delay *= 6;
}
m_timer->start(delay);
}

void GovernanceList::updateProposalList()
{
if (this->clientModel) {
// A proposal is considered passing if (YES votes - NO votes) >= (Total Weight 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 nWeightedMnCount = clientModel->getMasternodeList().first->getValidWeightedMNsCount();
const int nAbsVoteReq = std::max(Params().GetConsensus().nGovernanceMinQuorum, nWeightedMnCount / 10);
proposalModel->setVotingParams(nAbsVoteReq);

std::vector<CGovernanceObject> govObjList;
clientModel->getAllGovernanceObjects(govObjList);
ProposalList newProposals;
for (const auto& govObj : govObjList) {
if (govObj.GetObjectType() != GovernanceObject::PROPOSAL) {
continue; // Skip triggers.
}
newProposals.emplace_back(std::make_unique<Proposal>(this->clientModel, govObj));
}
proposalModel->reconcile(std::move(newProposals));
if (!clientModel || clientModel->node().shutdownRequested()) {
return;
}

// Update voting capability if we now have both client and wallet models
if (walletModel) {
updateVotingCapability();
if (m_in_progress.exchange(true)) {
// Already applying, re-arm for next attempt
handleProposalListChanged();
return;
}

QMetaObject::invokeMethod(m_worker, [this] {
auto result = std::make_shared<CalcProposalList>(calcProposalList());
m_in_progress.store(false);
QTimer::singleShot(0, this, [this, result] {
setProposalList(std::move(*result));
});
});
}

GovernanceList::CalcProposalList GovernanceList::calcProposalList() const
{
CalcProposalList ret;
if (!clientModel || clientModel->node().shutdownRequested()) {
return ret;
}

const auto [dmn, pindex] = clientModel->getMasternodeList();
if (!dmn || !pindex) {
return ret;
}

// A proposal is considered passing if (YES votes - NO votes) >= (Total Weight 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 nWeightedMnCount = dmn->getValidWeightedMNsCount();
ret.m_abs_vote_req = std::max(Params().GetConsensus().nGovernanceMinQuorum, nWeightedMnCount / 10);

std::vector<CGovernanceObject> govObjList;
clientModel->getAllGovernanceObjects(govObjList);
for (const auto& govObj : govObjList) {
if (govObj.GetObjectType() != GovernanceObject::PROPOSAL) {
continue; // Skip triggers.
}
ret.m_proposals.emplace_back(std::make_unique<Proposal>(this->clientModel, govObj));
}

// Schedule next update.
timer->start(GOVERNANCELIST_UPDATE_SECONDS * 1000);
// Discover voting capability if we now have both client and wallet models
if (walletModel) {
dmn->forEachMN(/*only_valid=*/true, [&](const auto& dmn) {
// Check if wallet owns the voting key using the same logic as RPC
const auto script = GetScriptForDestination(PKHash(dmn.getKeyIdVoting()));
if (walletModel->wallet().isSpendable(script)) {
ret.m_votable_masternodes[dmn.getProTxHash()] = dmn.getKeyIdVoting();
}
});
}

return ret;
}
Comment on lines 244 to +322
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -a 'clientmodel.h|walletmodel.h' src/qt

Repository: dashpay/dash

Length of output: 135


🏁 Script executed:

rg -n "class ClientModel|class WalletModel" src/qt --max-count=5 -A 10

Repository: dashpay/dash

Length of output: 17105


🏁 Script executed:

rg -n "getMasternodeList|getAllGovernanceObjects|isSpendable" src/qt -B 2 -A 5

Repository: dashpay/dash

Length of output: 7609


🏁 Script executed:

cat -n src/qt/clientmodel.h | sed -n '60,80p'

Repository: dashpay/dash

Length of output: 1193


🏁 Script executed:

rg -n "interfaces::Node|interfaces::Wallet" src/qt/clientmodel.h -A 3 | head -40

Repository: dashpay/dash

Length of output: 703


🏁 Script executed:

fd -a 'node.h|wallet.h' src/interfaces | head -5

Repository: dashpay/dash

Length of output: 139


🏁 Script executed:

cat -n src/interfaces/node.h | sed -n '1,100p'

Repository: dashpay/dash

Length of output: 3607


🏁 Script executed:

rg -n "thread|Thread|THREAD" src/interfaces/node.h | head -20

Repository: dashpay/dash

Length of output: 38


🏁 Script executed:

cat -n src/interfaces/node.h | sed -n '100,250p'

Repository: dashpay/dash

Length of output: 6373


🏁 Script executed:

rg -n "gov()" src/interfaces/node.h -A 2 -B 2

Repository: dashpay/dash

Length of output: 899


🏁 Script executed:

cat -n src/interfaces/wallet.h | sed -n '1,80p'

Repository: dashpay/dash

Length of output: 2579


🏁 Script executed:

rg -n "isSpendable" src/interfaces/wallet.h -B 3 -A 3

Repository: dashpay/dash

Length of output: 465


🏁 Script executed:

cat -n src/qt/governancelist.h | sed -n '1,150p'

Repository: dashpay/dash

Length of output: 4255


🏁 Script executed:

rg -n "m_worker|QThread" src/qt/governancelist.h -B 2 -A 5

Repository: dashpay/dash

Length of output: 682


🏁 Script executed:

cat -n src/qt/governancelist.cpp | sed -n '1,50p'

Repository: dashpay/dash

Length of output: 1622


🏁 Script executed:

cat -n src/qt/governancelist.cpp | sed -n '50,150p'

Repository: dashpay/dash

Length of output: 5157


🏁 Script executed:

rg -n "m_worker|m_thread" src/qt/governancelist.cpp -B 2 -A 5 | head -100

Repository: dashpay/dash

Length of output: 1408


🏁 Script executed:

cat -n src/qt/governancelist.cpp | sed -n '240,330p'

Repository: dashpay/dash

Length of output: 4822


🏁 Script executed:

rg -n "queryCollateralDepth|getWalletProposals" src/qt/governancelist.cpp -A 10 | head -60

Repository: dashpay/dash

Length of output: 2892


Avoid cross-thread access to ClientModel/WalletModel.

updateProposalList() schedules calcProposalList() on m_worker (worker thread), but calcProposalList() directly calls QObject methods on clientModel and walletModel (both GUI-thread-affine). This includes calls to getMasternodeList(), getAllGovernanceObjects(), node().gov() methods, wallet().isSpendable(), wallet().getGovernanceObjects(), and wallet().tryGetTxStatus(). While some underlying data is protected by mutexes (e.g., cs_mnlist), QObject method calls themselves are not thread-safe off-thread. Capture the raw data you need on the GUI thread (masternode list, governance objects, wallet state) and pass copies into the worker thread, or verify that each accessed method is explicitly documented as thread-safe.

🤖 Prompt for AI Agents
In `@src/qt/governancelist.cpp` around lines 244 - 322, The worker thread is
calling GUI-thread-affine methods; fix by gathering all needed GUI-thread data
inside updateProposalList() before invoking m_worker: call
clientModel->getMasternodeList(), clientModel->getAllGovernanceObjects(),
clientModel->node().gov().getGovernanceInfo()/getFundableProposalHashes(), and
any walletModel queries (getWalletProposals(), wallet().isSpendable(),
queryCollateralDepth()/existsObj()) on the GUI thread, copy their results into
plain structs/containers, then pass those copies into the lambda (or into
calcProposalList via a new overload that accepts the copies) so
calcProposalList()/the worker only operates on thread-safe copies when
constructing Proposal instances and populating ret (refer to updateProposalList,
m_worker, calcProposalList, getMasternodeList, getAllGovernanceObjects,
getWalletProposals, clientModel->node().gov(), walletModel and Proposal).


void GovernanceList::setProposalList(CalcProposalList&& data)
{
proposalModel->setVotingParams(data.m_abs_vote_req);
proposalModel->reconcile(std::move(data.m_proposals));
votableMasternodes = std::move(data.m_votable_masternodes);
updateMasternodeCount();
}

void GovernanceList::updateProposalCount() const
Expand Down Expand Up @@ -213,26 +286,6 @@ void GovernanceList::showAdditionalInfo(const QModelIndex& index)
QMessageBox::information(this, windowTitle, json);
}
Comment on lines 426 to 444
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing null check on getOptionsModel() in showAdditionalInfo.

Line 339 calls clientModel->getOptionsModel()->getDisplayUnit() without guarding getOptionsModel(). The same pattern was flagged and fixed in updateProposalButtons (Lines 397-398) with a null-check ternary. Apply the same guard here for consistency.

Suggested fix
-    DescriptionDialog* dialog = new DescriptionDialog(
-        tr("Details for %1").arg(proposal->title()),
-        proposal->toHtml(clientModel->getOptionsModel()->getDisplayUnit()),
-        /*parent=*/this);
+    const auto unit = clientModel->getOptionsModel() ? clientModel->getOptionsModel()->getDisplayUnit()
+                                                     : BitcoinUnit::DASH;
+    DescriptionDialog* dialog = new DescriptionDialog(
+        tr("Details for %1").arg(proposal->title()),
+        proposal->toHtml(unit),
+        /*parent=*/this);
🤖 Prompt for AI Agents
In `@src/qt/governancelist.cpp` around lines 326 - 344, In
GovernanceList::showAdditionalInfo, guard the call to
clientModel->getOptionsModel()->getDisplayUnit() by checking
clientModel->getOptionsModel() for nullptr (use the same null-check/ternary
pattern used in updateProposalButtons) before passing the display unit to
proposal->toHtml; if getOptionsModel() is null, pass a safe default (e.g.,
DEFAULT_UNIT or 0) so DescriptionDialog construction uses a valid display unit.
Ensure you update the reference in this function only (around the
DescriptionDialog call) and keep all other behavior the same.


void GovernanceList::updateVotingCapability()
{
if (!walletModel || !clientModel) return;

auto [mn_list, pindex] = clientModel->getMasternodeList();
if (!pindex) return;

votableMasternodes.clear();
mn_list->forEachMN(/*only_valid=*/true, [&](const auto& dmn) {
// Check if wallet owns the voting key using the same logic as RPC
const CScript script = GetScriptForDestination(PKHash(dmn.getKeyIdVoting()));
if (walletModel->wallet().isSpendable(script)) {
votableMasternodes[dmn.getProTxHash()] = dmn.getKeyIdVoting();
}
});

// Update masternode count display
updateMasternodeCount();
}

void GovernanceList::updateMasternodeCount() const
{
if (ui && ui->mnCountLabel) {
Expand Down Expand Up @@ -340,5 +393,5 @@ void GovernanceList::voteForProposal(vote_outcome_enum_t outcome)
QMessageBox::information(this, tr("Voting Results"), message);

// Update proposal list to show new vote counts
updateProposalList();
handleProposalListChanged();
}
Loading