diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 03f2d12827ba..070f011f42f9 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -24,10 +24,12 @@ QT_FORMS_UI = \ qt/forms/descriptiondialog.ui \ qt/forms/editaddressdialog.ui \ qt/forms/helpmessagedialog.ui \ + qt/forms/informationwidget.ui \ qt/forms/intro.ui \ qt/forms/masternodelist.ui \ qt/forms/mnemonicverificationdialog.ui \ qt/forms/modaloverlay.ui \ + qt/forms/networkwidget.ui \ qt/forms/openuridialog.ui \ qt/forms/optionsdialog.ui \ qt/forms/overviewpage.ui \ @@ -62,6 +64,7 @@ QT_MOC_CPP = \ qt/moc_descriptiondialog.cpp \ qt/moc_editaddressdialog.cpp \ qt/moc_guiutil.cpp \ + qt/moc_informationwidget.cpp \ qt/moc_initexecutor.cpp \ qt/moc_intro.cpp \ qt/moc_macdockiconhandler.cpp \ @@ -70,6 +73,7 @@ QT_MOC_CPP = \ qt/moc_masternodemodel.cpp \ qt/moc_mnemonicverificationdialog.cpp \ qt/moc_modaloverlay.cpp \ + qt/moc_networkwidget.cpp \ qt/moc_notificator.cpp \ qt/moc_openuridialog.cpp \ qt/moc_optionsdialog.cpp \ @@ -145,6 +149,7 @@ BITCOIN_QT_H = \ qt/guiconstants.h \ qt/guiutil_font.h \ qt/guiutil.h \ + qt/informationwidget.h \ qt/initexecutor.h \ qt/intro.h \ qt/macdockiconhandler.h \ @@ -155,6 +160,7 @@ BITCOIN_QT_H = \ qt/mnemonicverificationdialog.h \ qt/modaloverlay.h \ qt/networkstyle.h \ + qt/networkwidget.h \ qt/notificator.h \ qt/openuridialog.h \ qt/optionsdialog.h \ @@ -189,6 +195,7 @@ BITCOIN_QT_H = \ qt/transactionrecord.h \ qt/transactiontablemodel.h \ qt/transactionview.h \ + qt/util.h \ qt/utilitydialog.h \ qt/walletcontroller.h \ qt/walletframe.h \ @@ -254,10 +261,12 @@ BITCOIN_QT_BASE_CPP = \ qt/csvmodelwriter.cpp \ qt/guiutil.cpp \ qt/guiutil_font.cpp \ + qt/informationwidget.cpp \ qt/initexecutor.cpp \ qt/intro.cpp \ qt/modaloverlay.cpp \ qt/networkstyle.cpp \ + qt/networkwidget.cpp \ qt/notificator.cpp \ qt/optionsdialog.cpp \ qt/optionsmodel.cpp \ diff --git a/src/instantsend/net_instantsend.cpp b/src/instantsend/net_instantsend.cpp index b5676bdc9388..64fcfd0fd8ee 100644 --- a/src/instantsend/net_instantsend.cpp +++ b/src/instantsend/net_instantsend.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -307,6 +308,7 @@ void NetInstantSend::ProcessPendingISLocks(std::vector cb) const = 0; - virtual MnEntryCPtr getMN(const uint256& hash) const = 0; - virtual MnEntryCPtr getMNByService(const CService& service) const = 0; - virtual MnEntryCPtr getValidMN(const uint256& hash) const = 0; virtual std::vector getProjectedMNPayees(const CBlockIndex* pindex) const = 0; - virtual void copyContextTo(MnList& mn_list) const = 0; virtual void setContext(node::NodeContext* context) = 0; }; using MnListPtr = std::shared_ptr; -MnListPtr MakeMNList(const CDeterministicMNList& mn_list); - //! Interface for the src/evo part of a dash node (dashd process). class EVO { @@ -517,6 +511,10 @@ class Node std::function; virtual std::unique_ptr handleNotifyHeaderTip(NotifyHeaderTipFn fn) = 0; + //! Register handler for InstantSend data messages. + using NotifyInstantSendChangedFn = std::function; + virtual std::unique_ptr handleNotifyInstantSendChanged(NotifyInstantSendChangedFn fn) = 0; + //! Register handler for governance data messages. using NotifyGovernanceChangedFn = std::function; virtual std::unique_ptr handleNotifyGovernanceChanged(NotifyGovernanceChangedFn fn) = 0; diff --git a/src/node/interface_ui.cpp b/src/node/interface_ui.cpp index 1faf4512207f..6fa66ba5f770 100644 --- a/src/node/interface_ui.cpp +++ b/src/node/interface_ui.cpp @@ -24,6 +24,7 @@ struct UISignals { boost::signals2::signal NotifyChainLock; boost::signals2::signal NotifyHeaderTip; boost::signals2::signal NotifyGovernanceChanged; + boost::signals2::signal NotifyInstantSendChanged; boost::signals2::signal NotifyMasternodeListChanged; boost::signals2::signal NotifyAdditionalDataSyncProgressChanged; boost::signals2::signal BannedListChanged; @@ -48,6 +49,7 @@ ADD_SIGNALS_IMPL_WRAPPER(NotifyBlockTip); ADD_SIGNALS_IMPL_WRAPPER(NotifyChainLock); ADD_SIGNALS_IMPL_WRAPPER(NotifyHeaderTip); ADD_SIGNALS_IMPL_WRAPPER(NotifyGovernanceChanged); +ADD_SIGNALS_IMPL_WRAPPER(NotifyInstantSendChanged); ADD_SIGNALS_IMPL_WRAPPER(NotifyMasternodeListChanged); ADD_SIGNALS_IMPL_WRAPPER(NotifyAdditionalDataSyncProgressChanged); ADD_SIGNALS_IMPL_WRAPPER(BannedListChanged); @@ -64,6 +66,7 @@ void CClientUIInterface::NotifyBlockTip(SynchronizationState s, const CBlockInde 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::NotifyInstantSendChanged() { return g_ui_signals.NotifyInstantSendChanged(); } 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(); } diff --git a/src/node/interface_ui.h b/src/node/interface_ui.h index 7d7856eef9dc..67a6d0f5b1b0 100644 --- a/src/node/interface_ui.h +++ b/src/node/interface_ui.h @@ -115,6 +115,9 @@ class CClientUIInterface /** Masternode list has changed */ ADD_SIGNALS_DECL_WRAPPER(NotifyMasternodeListChanged, void, const CDeterministicMNList&, const CBlockIndex*); + /** InstantSend data changed */ + ADD_SIGNALS_DECL_WRAPPER(NotifyInstantSendChanged, void); + /** Governance data changed */ ADD_SIGNALS_DECL_WRAPPER(NotifyGovernanceChanged, void); diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index d6aa31fddf26..8db8a2c610e5 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -164,21 +164,6 @@ class MnListImpl : public MnList cb(std::make_shared(dmn)); }); } - MnEntryCPtr getMN(const uint256& hash) const override - { - const auto dmn{m_list.GetMN(hash)}; - return dmn ? std::make_shared(dmn) : nullptr; - } - MnEntryCPtr getMNByService(const CService& service) const override - { - const auto dmn{m_list.GetMNByService(service)}; - return dmn ? std::make_shared(dmn) : nullptr; - } - MnEntryCPtr getValidMN(const uint256& hash) const override - { - const auto dmn{m_list.GetValidMN(hash)}; - return dmn ? std::make_shared(dmn) : nullptr; - } std::vector getProjectedMNPayees(const CBlockIndex* pindex) const override { std::vector ret; @@ -188,11 +173,6 @@ class MnListImpl : public MnList return ret; } - void copyContextTo(MnList& mn_list) const override - { - if (!m_context) return; - mn_list.setContext(m_context); - } void setContext(NodeContext* context) override { m_context = context; @@ -1023,6 +1003,10 @@ class NodeImpl : public Node /* verification progress is unused when a header was received */ 0); })); } + std::unique_ptr handleNotifyInstantSendChanged(NotifyInstantSendChangedFn fn) override + { + return MakeHandler(::uiInterface.NotifyInstantSendChanged_connect(fn)); + } std::unique_ptr handleNotifyGovernanceChanged(NotifyGovernanceChangedFn fn) override { return MakeHandler(::uiInterface.NotifyGovernanceChanged_connect(fn)); @@ -1503,5 +1487,4 @@ class ChainImpl : public Chain namespace interfaces { std::unique_ptr MakeNode(node::NodeContext& context) { return std::make_unique(context); } std::unique_ptr MakeChain(node::NodeContext& node) { return std::make_unique(node); } -MnListPtr MakeMNList(const CDeterministicMNList& mn_list) { return std::make_shared(mn_list); } } // namespace interfaces diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 518ba44f2df6..af659cc2c708 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -447,6 +447,8 @@ void BitcoinGUI::createActions() openPeersAction->setStatusTip(tr("Show peers info")); openRepairAction = new QAction(tr("Wallet &Repair"), this); openRepairAction->setStatusTip(tr("Show wallet repair options")); + openDebugLogAction = new QAction(tr("Open &debug log file"), this); + openDebugLogAction->setStatusTip(tr("Open the debug log file from the current data directory")); openConfEditorAction = new QAction(tr("Open &wallet configuration file"), this); openConfEditorAction->setStatusTip(tr("Open configuration file")); // override TextHeuristicRole set by default which confuses this action with application settings @@ -517,7 +519,8 @@ void BitcoinGUI::createActions() connect(openPeersAction, &QAction::triggered, this, &BitcoinGUI::showPeers); connect(openRepairAction, &QAction::triggered, this, &BitcoinGUI::showRepair); - // Open configs and backup folder from menu + // Open logs, configs, and backup folder from menu + connect(openDebugLogAction, &QAction::triggered, GUIUtil::openDebugLogfile); connect(openConfEditorAction, &QAction::triggered, this, &BitcoinGUI::showConfEditor); connect(showBackupsAction, &QAction::triggered, this, &BitcoinGUI::showBackups); @@ -640,6 +643,7 @@ void BitcoinGUI::createMenuBar() file->addAction(m_load_psbt_clipboard_action); file->addSeparator(); } + file->addAction(openDebugLogAction); file->addAction(openConfEditorAction); if(walletFrame) { file->addAction(showBackupsAction); @@ -1142,6 +1146,7 @@ void BitcoinGUI::createIconMenu(QMenu *pmenu) repair_action = pmenu->addAction(openRepairAction->text(), openRepairAction, &QAction::trigger); } pmenu->addSeparator(); + QAction* debuglog_action = pmenu->addAction(openDebugLogAction->text(), openDebugLogAction, &QAction::trigger); QAction* conf_action = pmenu->addAction(openConfEditorAction->text(), openConfEditorAction, &QAction::trigger); QAction* backups_action{nullptr}; if (enableWallet) { @@ -1158,7 +1163,7 @@ void BitcoinGUI::createIconMenu(QMenu *pmenu) // Using QSystemTrayIcon::Context is not reliable. // See https://bugreports.qt.io/browse/QTBUG-91697 pmenu, &QMenu::aboutToShow, - [this, show_hide_action, send_action, cj_send_action, receive_action, sign_action, verify_action, options_action, node_window_action, quit_action, repair_action, backups_action, info_action, graph_action, peer_action, conf_action] { + [this, show_hide_action, send_action, cj_send_action, receive_action, sign_action, verify_action, options_action, node_window_action, quit_action, repair_action, backups_action, info_action, graph_action, peer_action, debuglog_action, conf_action] { if (m_node.shutdownRequested()) return; // nothing to do, node is shutting down. if (show_hide_action) show_hide_action->setText( @@ -1185,6 +1190,7 @@ void BitcoinGUI::createIconMenu(QMenu *pmenu) node_window_action->setEnabled(openRPCConsoleAction->isEnabled()); graph_action->setEnabled(openGraphAction->isEnabled()); peer_action->setEnabled(openPeersAction->isEnabled()); + debuglog_action->setEnabled(openDebugLogAction->isEnabled()); conf_action->setEnabled(openConfEditorAction->isEnabled()); if (quit_action) quit_action->setEnabled(true); } @@ -1809,20 +1815,20 @@ void BitcoinGUI::updateGovernanceCycleIcon() const auto gov_info{m_node.gov().getGovernanceInfo()}; const auto remaining_blocks{std::max(0, gov_info.nextsuperblock - current_height)}; - const auto days{static_cast(static_cast(remaining_blocks) * gov_info.targetSpacing / (24*60*60))}; + const auto remaining_str{GUIUtil::formatBlockDuration(remaining_blocks, gov_info.targetSpacing)}; const bool awaiting_superblock{current_height % gov_info.superblockcycle >= gov_info.superblockcycle - gov_info.superblockmaturitywindow}; QString tooltip1{}; if (awaiting_superblock) { labelGovernanceCycleIcon->setPixmap(m_gov_cycle_pixmaps.at({ToUnderlying(GUIUtil::ThemedColor::BLUE), 0})); - tooltip1 = tr("~%n day(s) (%1 blocks) left for superblock", "", days).arg(remaining_blocks); + tooltip1 = tr("~%1 (%2 blocks) left for superblock").arg(remaining_str).arg(remaining_blocks); } else { const auto cycle_blocks{gov_info.superblockcycle - gov_info.superblockmaturitywindow}; const auto blocks_elapsed{gov_info.superblockcycle - remaining_blocks - gov_info.superblockmaturitywindow}; const auto progress{static_cast(std::max(0, blocks_elapsed)) / static_cast(std::max(1, cycle_blocks))}; const auto frame{std::clamp(static_cast(progress * (GOV_CYCLE_FRAME_COUNT - 1)), 0, GOV_CYCLE_FRAME_COUNT - 2) + 1}; labelGovernanceCycleIcon->setPixmap(m_gov_cycle_pixmaps.at({ToUnderlying(GUIUtil::ThemedColor::GREEN), frame})); - tooltip1 = tr("~%n day(s) (%1 blocks) left for voting", "", days).arg(remaining_blocks); + tooltip1 = tr("~%1 (%2 blocks) left for voting").arg(remaining_str).arg(remaining_blocks); } const auto allocated_budget{m_node.gov().getFundableProposalHashes().allocated}; diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index db820e46b1f3..670ce48f084b 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -173,6 +173,7 @@ class BitcoinGUI : public QMainWindow QAction* openGraphAction = nullptr; QAction* openPeersAction = nullptr; QAction* openRepairAction = nullptr; + QAction* openDebugLogAction = nullptr; QAction* openConfEditorAction = nullptr; QAction* showBackupsAction = nullptr; QAction* openAction = nullptr; diff --git a/src/qt/clientfeeds.cpp b/src/qt/clientfeeds.cpp index e25f907cbd0d..c9dad9391fcb 100644 --- a/src/qt/clientfeeds.cpp +++ b/src/qt/clientfeeds.cpp @@ -14,13 +14,18 @@ #include #include #include +#include #include #include namespace { +constexpr auto CHAINLOCK_UPDATE_INTERVAL{3s}; +constexpr auto CREDITPOOL_UPDATE_INTERVAL{3s}; +constexpr auto INSTANTSEND_UPDATE_INTERVAL{3s}; constexpr auto MASTERNODE_UPDATE_INTERVAL{3s}; constexpr auto PROPOSAL_UPDATE_INTERVAL{10s}; +constexpr auto QUORUM_UPDATE_INTERVAL{3s}; } // anonymous namespace FeedBase::FeedBase(QObject* parent, const FeedBase::Config& config) : @@ -45,6 +50,95 @@ void FeedBase::requestRefresh() } } +ChainLockFeed::ChainLockFeed(QObject* parent, ClientModel& client_model) : + Feed{parent, {/*m_baseline=*/CHAINLOCK_UPDATE_INTERVAL, /*m_throttle=*/CHAINLOCK_UPDATE_INTERVAL*10}}, + m_client_model{client_model} +{ +} + +ChainLockFeed::~ChainLockFeed() = default; + +void ChainLockFeed::fetch() +{ + if (m_client_model.node().shutdownRequested()) { + return; + } + + const auto cl_info = m_client_model.node().llmq().getBestChainLock(); + if (cl_info.m_height == 0) { + // Chainlock at genesis height not possible, assume something went wrong + return; + } + + auto ret = std::make_shared(); + ret->m_block_time = cl_info.m_block_time; + ret->m_height = cl_info.m_height; + ret->m_hash = QString::fromStdString(cl_info.m_hash.ToString()); + + setData(std::move(ret)); +} + +CreditPoolFeed::CreditPoolFeed(QObject* parent, ClientModel& client_model) : + Feed{parent, {/*m_baseline=*/CREDITPOOL_UPDATE_INTERVAL, /*m_throttle=*/CREDITPOOL_UPDATE_INTERVAL*10}}, + m_client_model{client_model} +{ +} + +CreditPoolFeed::~CreditPoolFeed() = default; + +void CreditPoolFeed::fetch() +{ + if (m_client_model.node().shutdownRequested()) { + return; + } + + auto ret = std::make_shared(); + ret->m_counts = m_client_model.node().llmq().getCreditPoolCounts(); + ret->m_pending_unlocks = m_client_model.node().llmq().getPendingAssetUnlocks(); + + setData(std::move(ret)); +} + +InstantSendFeed::InstantSendFeed(QObject* parent, ClientModel& client_model) : + Feed{parent, {/*m_baseline=*/INSTANTSEND_UPDATE_INTERVAL, /*m_throttle=*/INSTANTSEND_UPDATE_INTERVAL*10}}, + m_client_model{client_model} +{ +} + +InstantSendFeed::~InstantSendFeed() = default; + +void InstantSendFeed::fetch() +{ + if (m_client_model.node().shutdownRequested()) { + return; + } + + auto ret = std::make_shared(); + ret->m_counts = m_client_model.node().llmq().getInstantSendCounts(); + + setData(std::move(ret)); +} + +QuorumFeed::QuorumFeed(QObject* parent, ClientModel& client_model) : + Feed{parent, {/*m_baseline=*/QUORUM_UPDATE_INTERVAL, /*m_throttle=*/QUORUM_UPDATE_INTERVAL*10}}, + m_client_model{client_model} +{ +} + +QuorumFeed::~QuorumFeed() = default; + +void QuorumFeed::fetch() +{ + if (m_client_model.node().shutdownRequested()) { + return; + } + + auto ret = std::make_shared(); + ret->m_quorums = m_client_model.node().llmq().getQuorumStats(); + + setData(std::move(ret)); +} + MasternodeFeed::MasternodeFeed(QObject* parent, ClientModel& client_model) : Feed{parent, {/*m_baseline=*/MASTERNODE_UPDATE_INTERVAL, /*m_throttle=*/MASTERNODE_UPDATE_INTERVAL*10}}, m_client_model{client_model} @@ -59,7 +153,7 @@ void MasternodeFeed::fetch() return; } - const auto [dmn, pindex] = m_client_model.getMasternodeList(); + const auto [dmn, pindex] = m_client_model.node().evo().getListAtChainTip(); if (!dmn || !pindex) { return; } @@ -93,16 +187,20 @@ void MasternodeFeed::fetch() if (auto nextPaymentIt = nextPayments.find(dmn->getProTxHash()); nextPaymentIt != nextPayments.end()) { nNextPayment = nextPaymentIt->second; } - ret->m_entries.push_back(std::make_unique(dmn, collateralStr, nNextPayment)); + auto entry = std::make_shared(dmn, collateralStr, nNextPayment); + ret->m_by_protx[dmn->getProTxHash()] = entry.get(); + ret->m_by_service[util::make_array(dmn->getNetInfoPrimary().GetKey())] = entry.get(); + ret->m_entries.push_back(std::move(entry)); }); ret->m_valid = true; setData(std::move(ret)); } -ProposalFeed::ProposalFeed(QObject* parent, ClientModel& client_model) : +ProposalFeed::ProposalFeed(QObject* parent, ClientModel& client_model, MasternodeFeed& feed_masternode) : Feed{parent, {/*m_baseline=*/PROPOSAL_UPDATE_INTERVAL, /*m_throttle=*/PROPOSAL_UPDATE_INTERVAL*6}}, - m_client_model{client_model} + m_client_model{client_model}, + m_feed_masternode{feed_masternode} { } @@ -114,8 +212,8 @@ void ProposalFeed::fetch() return; } - const auto [dmn, pindex] = m_client_model.getMasternodeList(); - if (!dmn || !pindex) { + const auto data_mn = m_feed_masternode.data(); + if (!data_mn || !data_mn->m_valid) { return; } @@ -123,7 +221,7 @@ void ProposalFeed::fetch() // A proposal is considered passing if (YES votes - NO votes) >= (Total Weight of Masternodes / 10), // count total valid (ENABLED) masternodes to determine passing threshold. const auto nMinQuorum = static_cast(Params().GetConsensus().nGovernanceMinQuorum); - ret->m_abs_vote_req = static_cast(std::max(nMinQuorum, dmn->getCounts().m_valid_weighted / 10)); + ret->m_abs_vote_req = static_cast(std::max(nMinQuorum, data_mn->m_counts.m_valid_weighted / 10)); ret->m_gov_info = m_client_model.node().gov().getGovernanceInfo(); std::vector govObjList; m_client_model.getAllGovernanceObjects(govObjList); diff --git a/src/qt/clientfeeds.h b/src/qt/clientfeeds.h index a291bb633d4b..08ab6e7fc853 100644 --- a/src/qt/clientfeeds.h +++ b/src/qt/clientfeeds.h @@ -9,7 +9,9 @@ #include #include #include +#include +#include #include #include @@ -92,13 +94,67 @@ class Feed : public FeedBase std::shared_ptr m_data GUARDED_BY(m_cs); }; -using MasternodeEntryList = std::vector>; +struct ChainLockData { + int32_t m_height{0}; + int64_t m_block_time{0}; + QString m_hash; +}; + +class ChainLockFeed : public Feed { + Q_OBJECT + +public: + explicit ChainLockFeed(QObject* parent, ClientModel& client_model); + ~ChainLockFeed(); + + void fetch() override; + +private: + ClientModel& m_client_model; +}; + +struct CreditPoolData { + interfaces::LLMQ::CreditPoolCounts m_counts{}; + size_t m_pending_unlocks{0}; +}; + +class CreditPoolFeed : public Feed { + Q_OBJECT + +public: + explicit CreditPoolFeed(QObject* parent, ClientModel& client_model); + ~CreditPoolFeed(); + + void fetch() override; + +private: + ClientModel& m_client_model; +}; + +struct InstantSendData { + interfaces::LLMQ::InstantSendCounts m_counts{}; +}; + +class InstantSendFeed : public Feed { + Q_OBJECT + +public: + explicit InstantSendFeed(QObject* parent, ClientModel& client_model); + ~InstantSendFeed(); + + void fetch() override; + +private: + ClientModel& m_client_model; +}; struct MasternodeData { bool m_valid{false}; int m_list_height{0}; interfaces::MnList::Counts m_counts{}; - MasternodeEntryList m_entries; + QHash m_by_service{}; + std::vector> m_entries{}; + Uint256HashMap m_by_protx{}; }; class MasternodeFeed : public Feed { @@ -114,6 +170,23 @@ class MasternodeFeed : public Feed { ClientModel& m_client_model; }; +struct QuorumData { + std::vector m_quorums{}; +}; + +class QuorumFeed : public Feed { + Q_OBJECT + +public: + explicit QuorumFeed(QObject* parent, ClientModel& client_model); + ~QuorumFeed(); + + void fetch() override; + +private: + ClientModel& m_client_model; +}; + using Proposals = std::vector>; struct ProposalData { @@ -127,13 +200,14 @@ class ProposalFeed : public Feed { Q_OBJECT public: - explicit ProposalFeed(QObject* parent, ClientModel& client_model); + explicit ProposalFeed(QObject* parent, ClientModel& client_model, MasternodeFeed& feed_masternode); ~ProposalFeed(); void fetch() override; private: ClientModel& m_client_model; + MasternodeFeed& m_feed_masternode; }; class ClientFeeds : public QObject diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp index fc8a096a3334..c40c9e7fda3b 100644 --- a/src/qt/clientmodel.cpp +++ b/src/qt/clientmodel.cpp @@ -17,7 +17,6 @@ #include #include -#include #include #include #include @@ -52,15 +51,12 @@ ClientModel::ClientModel(interfaces::Node& node, OptionsModel *_optionsModel, QO m_peer_table_sort_proxy->setSourceModel(peerTableModel); banTableModel = new BanTableModel(m_node, this); - mnListCached = interfaces::MakeMNList(CDeterministicMNList{}); - QTimer* timer = new QTimer; timer->setInterval(MODEL_UPDATE_DELAY); connect(timer, &QTimer::timeout, [this] { // no locking required at this point // the following calls will acquire the required lock Q_EMIT mempoolSizeChanged(m_node.getMempoolSize(), m_node.getMempoolDynamicUsage(), m_node.getMempoolMaxUsage()); - Q_EMIT islockCountChanged(m_node.llmq().getInstantSendCounts().m_verified); }); connect(m_thread, &QThread::finished, timer, &QObject::deleteLater); connect(m_thread, &QThread::started, [timer] { timer->start(); }); @@ -75,12 +71,20 @@ ClientModel::ClientModel(interfaces::Node& node, OptionsModel *_optionsModel, QO m_feeds = std::make_unique(this); // Setup feeds + m_feed_chainlock = m_feeds->add(this, *this); + m_feed_creditpool = m_feeds->add(this, *this); + m_feed_instantsend = m_feeds->add(this, *this); m_feed_masternode = m_feeds->add(this, *this); - connect(this, &ClientModel::masternodeListChanged, this, [this] { m_feed_masternode->requestRefresh(); }); - if (m_node.gov().isEnabled()) { - m_feed_proposal = m_feeds->add(this, *this); - connect(this, &ClientModel::governanceChanged, this, [this] { m_feed_proposal->requestRefresh(); }); + m_feed_proposal = m_feeds->add(this, *this, *m_feed_masternode); + } + m_feed_quorum = m_feeds->add(this, *this); + + connect(this, &ClientModel::chainLockChanged, m_feed_chainlock, &ChainLockFeed::requestRefresh); + connect(this, &ClientModel::instantSendChanged, m_feed_instantsend, &InstantSendFeed::requestRefresh); + connect(this, &ClientModel::masternodeListChanged, m_feed_masternode, &MasternodeFeed::requestRefresh); + if (m_feed_proposal) { + connect(this, &ClientModel::governanceChanged, m_feed_proposal, &ProposalFeed::requestRefresh); } // Update sync state to decide delay param, trigger refreshes @@ -88,8 +92,10 @@ ClientModel::ClientModel(interfaces::Node& node, OptionsModel *_optionsModel, QO [this](int, const QDateTime&, const QString&, double, bool header, SynchronizationState sync_state) { if (header) return; m_feeds->setSyncing(sync_state != SynchronizationState::POST_INIT); + if (m_feed_creditpool) m_feed_creditpool->requestRefresh(); if (m_feed_masternode) m_feed_masternode->requestRefresh(); if (m_feed_proposal) m_feed_proposal->requestRefresh(); + if (m_feed_quorum) m_feed_quorum->requestRefresh(); }); // Start all tasks @@ -125,34 +131,6 @@ int ClientModel::getNumConnections(unsigned int flags) const return m_node.getNodeCount(connections); } -void ClientModel::setMasternodeList(interfaces::MnListPtr mnList, const CBlockIndex* tip) -{ - LOCK(cs_mnlist); - if (mnListCached->getBlockHash() == mnList->getBlockHash()) { - return; - } - mnListCached->copyContextTo(*mnList); - mnListCached = std::move(mnList); - mnListTip = tip; - Q_EMIT masternodeListChanged(); -} - -std::pair ClientModel::getMasternodeList() const -{ - LOCK(cs_mnlist); - return {mnListCached, mnListTip}; -} - -void ClientModel::refreshMasternodeList() -{ - auto [mnList, tip] = m_node.evo().getListAtChainTip(); - if (!mnList || !tip) { - return; - } - LOCK(cs_mnlist); - setMasternodeList(std::move(mnList), tip); -} - int ClientModel::getHeaderTipHeight() const { if (cachedBestHeaderHeight == -1) { @@ -344,12 +322,16 @@ void ClientModel::subscribeToCoreSignals() Q_EMIT additionalDataSyncProgressChanged(nSyncProgress); })); m_event_handlers.emplace_back(m_node.handleNotifyChainLock( - [this](const std::string& best_hash, int best_height) { - Q_EMIT chainLockChanged(QString::fromStdString(best_hash), best_height); + [this](const std::string&, int) { + Q_EMIT chainLockChanged(); })); m_event_handlers.emplace_back(m_node.handleNotifyMasternodeListChanged( - [this](const CDeterministicMNList& newList, const CBlockIndex* pindex) { - setMasternodeList(interfaces::MakeMNList(newList), pindex); + [this](const CDeterministicMNList&, const CBlockIndex*) { + Q_EMIT masternodeListChanged(); + })); + m_event_handlers.emplace_back(m_node.handleNotifyInstantSendChanged( + [this]() { + Q_EMIT instantSendChanged(); })); m_event_handlers.emplace_back(m_node.handleNotifyGovernanceChanged( [this]() { diff --git a/src/qt/clientmodel.h b/src/qt/clientmodel.h index 6b08a1a014ea..72ff9a7b3d80 100644 --- a/src/qt/clientmodel.h +++ b/src/qt/clientmodel.h @@ -18,10 +18,13 @@ #include class BanTableModel; -class CBlockIndex; +class ChainLockFeed; class ClientFeeds; +class CreditPoolFeed; +class InstantSendFeed; class MasternodeFeed; class OptionsModel; +class QuorumFeed; class PeerTableModel; class PeerTableSortProxy; class ProposalFeed; @@ -65,8 +68,12 @@ class ClientModel : public QObject PeerTableSortProxy* peerTableSortProxy(); BanTableModel *getBanTableModel(); + ChainLockFeed* feedChainLock() const { return m_feed_chainlock; } + CreditPoolFeed* feedCreditPool() const { return m_feed_creditpool; } + InstantSendFeed* feedInstantSend() const { return m_feed_instantsend; } MasternodeFeed* feedMasternode() const { return m_feed_masternode; } ProposalFeed* feedProposal() const { return m_feed_proposal; } + QuorumFeed* feedQuorum() const { return m_feed_quorum; } //! Return number of connections, default is in- and outbound (total) int getNumConnections(unsigned int flags = CONNECTIONS_ALL) const; @@ -76,10 +83,6 @@ class ClientModel : public QObject int getHeaderTipHeight() const; int64_t getHeaderTipTime() const; - void setMasternodeList(interfaces::MnListPtr mnList, const CBlockIndex* tip); - std::pair getMasternodeList() const; - void refreshMasternodeList(); - void getAllGovernanceObjects(std::vector &obj); //! Returns the block source of the current importing/syncing state @@ -115,16 +118,13 @@ class ClientModel : public QObject //! A thread to interact with m_node asynchronously QThread* const m_thread; - // The cache for mn list is not technically needed because CDeterministicMNManager - // caches it internally for recent blocks but it's not enough to get consistent - // representation of the list in UI during initial sync/reindex, so we cache it here too. - mutable RecursiveMutex cs_mnlist; // protects mnListCached - interfaces::MnListPtr mnListCached GUARDED_BY(cs_mnlist){}; - const CBlockIndex* mnListTip{nullptr}; - //! Data sources from different subsystems coordinated by model + ChainLockFeed* m_feed_chainlock{nullptr}; + CreditPoolFeed* m_feed_creditpool{nullptr}; + InstantSendFeed* m_feed_instantsend{nullptr}; MasternodeFeed* m_feed_masternode{nullptr}; ProposalFeed* m_feed_proposal{nullptr}; + QuorumFeed* m_feed_quorum{nullptr}; std::unique_ptr m_feeds{nullptr}; void TipChanged(SynchronizationState sync_state, interfaces::BlockTip tip, double verification_progress, bool header) EXCLUSIVE_LOCKS_REQUIRED(!m_cached_tip_mutex); @@ -135,11 +135,11 @@ class ClientModel : public QObject void numConnectionsChanged(int count); void governanceChanged(); void masternodeListChanged() const; - void chainLockChanged(const QString& bestChainLockHash, int bestChainLockHeight); + void chainLockChanged(); void numBlocksChanged(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, bool header, SynchronizationState sync_state); void additionalDataSyncProgressChanged(double nSyncProgress); void mempoolSizeChanged(long count, size_t mempoolSizeInBytes, size_t mempoolMaxSizeInBytes); - void islockCountChanged(size_t count); + void instantSendChanged(); void networkActiveChanged(bool networkActive); void alertsChanged(const QString &warnings); diff --git a/src/qt/forms/debugwindow.ui b/src/qt/forms/debugwindow.ui index 16a749a87418..e1749c2d85af 100644 --- a/src/qt/forms/debugwindow.ui +++ b/src/qt/forms/debugwindow.ui @@ -102,526 +102,42 @@ 0 - - - 12 - - - - - General - - - - - - - Client version - - - - - - - IBeamCursor - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - User Agent - - - 10 - - - - - - - IBeamCursor - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Datadir - - - - - - - IBeamCursor - - - To specify a non-default location of the data directory use the '%1' option. - - - N/A - - - Qt::PlainText - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Blocksdir - - - - - - - IBeamCursor - - - To specify a non-default location of the blocks directory use the '%1' option. - - - N/A - - - Qt::PlainText - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Startup time - - - - - - - IBeamCursor - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Network - - - - - - - Name - - - - - - - IBeamCursor - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Number of connections - - - - - - - IBeamCursor - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Local Addresses - - - - - - - IBeamCursor - - - true - - - - 0 - 0 - - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - Network addresses that your Dash node is currently using to communicate with other nodes. - - - - - - - Number of regular Masternodes - - - - - - - IBeamCursor - - - N/A - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Number of EvoNodes - - - - - - - IBeamCursor - - - N/A - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Block chain - - - - - - - Current block height - - - - - - - IBeamCursor - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Last block time - - - - - - - IBeamCursor - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Last block hash - - - - - - - IBeamCursor - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Latest ChainLocked block hash - - - - - - - IBeamCursor - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Latest ChainLocked block height - - - - - - - IBeamCursor - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Memory Pool - - - - - - - Current number of transactions - - - - - - - IBeamCursor - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Memory usage - - - - - - - IBeamCursor - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - 3 - - - - - Qt::Vertical - - - - 10 - 5 - - - - - - - - Debug log file - - - - - - - Open the %1 debug log file from the current data directory. This can take a few seconds for large log files. - - - &Open - - - false - - - - - - - - - InstantSend locks - - - - - - - IBeamCursor - - - N/A - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - + + + + + 0 + + + + &General + + + 0 + 0 + 0 + 0 + + + + + + + + &Network + + + 0 + 0 + 0 + 0 + + + + + - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -1826,6 +1342,18 @@ + + InformationWidget + QWidget +
qt/informationwidget.h
+ 1 +
+ + NetworkWidget + QWidget +
qt/networkwidget.h
+ 1 +
TrafficGraphWidget QWidget diff --git a/src/qt/forms/informationwidget.ui b/src/qt/forms/informationwidget.ui new file mode 100644 index 000000000000..8180076b9b0f --- /dev/null +++ b/src/qt/forms/informationwidget.ui @@ -0,0 +1,388 @@ + + + InformationWidget + + + + 0 + 0 + 700 + 500 + + + + + 12 + + + + + General + + + + + + + Client version + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + User Agent + + + 10 + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Datadir + + + + + + + IBeamCursor + + + To specify a non-default location of the data directory use the '%1' option. + + + N/A + + + Qt::PlainText + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Blocksdir + + + + + + + IBeamCursor + + + To specify a non-default location of the blocks directory use the '%1' option. + + + N/A + + + Qt::PlainText + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Startup time + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Network + + + + + + + Name + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Number of connections + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Local Addresses + + + + + + + IBeamCursor + + + true + + + + 0 + 0 + + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + Network addresses that your Dash node is currently using to communicate with other nodes. + + + + + + + Block chain + + + + + + + Current block height + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Last block time + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Last block hash + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Memory Pool + + + + + + + Current number of transactions + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Memory usage + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/qt/forms/networkwidget.ui b/src/qt/forms/networkwidget.ui new file mode 100644 index 000000000000..24e52d1a4f54 --- /dev/null +++ b/src/qt/forms/networkwidget.ui @@ -0,0 +1,403 @@ + + + NetworkWidget + + + + 0 + 0 + 700 + 500 + + + + + + + + + 12 + + + + + Credit Pool + + + + + + + Last block change + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Total locked + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Pending unlocks + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Withdrawal limit + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + InstantSend + + + + + + + Verified locks + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Unverified locks + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Locks awaiting transaction + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Unprotected transactions + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Masternodes + + + + + + + Masternode count + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + EvoNode count + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + 12 + + + + + Quorums + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + 12 + + + + + ChainLocks + + + + + + + Current block height + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Last block time + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Last block hash + + + + + + + IBeamCursor + + + N/A + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 2b1f819e6c49..2b02ac80e1a4 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -417,6 +417,22 @@ bool isDust(interfaces::Node& node, const QString& address, const CAmount& amoun return IsDust(txOut, node.getDustRelayFee()); } +QString formatAmount(BitcoinUnit unit, CAmount amount, bool is_signed, std::optional truncate) +{ + QString formatted = BitcoinUnits::format(unit, amount, is_signed, BitcoinUnits::SeparatorStyle::ALWAYS); + if (truncate) { + int dotIndex = formatted.indexOf('.'); + if (dotIndex != -1) { + if (*truncate == 0) { + formatted = formatted.left(dotIndex); + } else if (formatted.length() > dotIndex + 1 + *truncate) { + formatted = formatted.left(dotIndex + 1 + *truncate); + } + } + } + return formatted + " " + BitcoinUnits::name(unit); +} + QString HtmlEscape(const QString& str, bool fMultiLine) { QString escaped = str.toHtmlEscaped(); @@ -1215,6 +1231,18 @@ QString formatNiceTimeOffset(qint64 secs) return timeBehindText; } +QString formatBlockDuration(int blocks, int64_t spacing_seconds) +{ + if (blocks <= 0) return QObject::tr("now"); + const double secs = static_cast(blocks) * static_cast(spacing_seconds); + constexpr double MINUTE{60.0}, HOUR{3600.0}, DAY{86400.0}, MONTH{30.44 * 86400.0}, YEAR{365.25 * 86400.0}; + if (secs < HOUR) return QObject::tr("%n minute(s)", "", static_cast(secs / MINUTE + 0.5)); + if (secs < DAY) return QObject::tr("%n hour(s)", "", static_cast(secs / HOUR + 0.5)); + if (secs < MONTH) return QObject::tr("%n day(s)", "", static_cast(secs / DAY + 0.5)); + if (secs < YEAR) return QObject::tr("%n month(s)", "", static_cast(secs / MONTH + 0.5)); + return QObject::tr("%n year(s)", "", static_cast(secs / YEAR + 0.5)); +} + QString formatBytes(uint64_t bytes) { if (bytes < 1'000) diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h index e4701677cc79..0cae58e68685 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -7,11 +7,13 @@ #include #include -#include #include #include #include +#include +#include + #include #include #include @@ -23,6 +25,7 @@ #include #include +#include #include class QValidatedLineEdit; @@ -148,6 +151,9 @@ namespace GUIUtil // Returns true if given address+amount meets "dust" definition bool isDust(interfaces::Node& node, const QString& address, const CAmount& amount); + // Format a CAmount as a string with unit name (and truncate to a given number of decimal places). + QString formatAmount(BitcoinUnit unit, CAmount amount, bool is_signed = false, std::optional truncate = std::nullopt); + // HTML escaping for rich text controls QString HtmlEscape(const QString& str, bool fMultiLine=false); QString HtmlEscape(const std::string& str, bool fMultiLine=false); @@ -353,6 +359,9 @@ namespace GUIUtil QString formatNiceTimeOffset(qint64 secs); + /** Convert a block count to a human-readable duration using the given block spacing. */ + QString formatBlockDuration(int blocks, int64_t spacing_seconds); + QString formatBytes(uint64_t bytes); qreal calculateIdealFontSize(int width, const QString& text, QFont font, qreal minPointSize = 4, qreal startPointSize = 14); diff --git a/src/qt/informationwidget.cpp b/src/qt/informationwidget.cpp new file mode 100644 index 000000000000..d0c9e9bc9ece --- /dev/null +++ b/src/qt/informationwidget.cpp @@ -0,0 +1,129 @@ +// Copyright (c) 2011-2021 The Bitcoin Core developers +// Copyright (c) 2014-2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include + +#include +#include + +#include +#include +#include + +#include + +namespace { +constexpr QChar nonbreaking_hyphen(8209); +} // anonymous namespace + +InformationWidget::InformationWidget(QWidget* parent) : + QWidget(parent), + ui(new Ui::InformationWidget) +{ + ui->setupUi(this); + GUIUtil::setFont({ui->label_9, + ui->label_10, + ui->labelMempoolTitle, + ui->labelNetwork}, + {GUIUtil::FontWeight::Bold, 16}); + + for (auto* element : {ui->label_10, ui->labelNetwork, ui->labelMempoolTitle}) { + element->setContentsMargins(0, 10, 0, 0); + } +} + +InformationWidget::~InformationWidget() +{ + delete ui; +} + +void InformationWidget::setClientModel(ClientModel* model) +{ + clientModel = model; + + if (clientModel) { + // Keep up to date with client + setNumConnections(model->getNumConnections()); + connect(model, &ClientModel::numConnectionsChanged, this, &InformationWidget::setNumConnections); + + updateNetworkState(); + connect(model, &ClientModel::networkActiveChanged, this, &InformationWidget::setNetworkActive); + + connect(model, &ClientModel::mempoolSizeChanged, this, &InformationWidget::setMempoolSize); + + // Provide initial values + ui->blocksDir->setText(model->blocksDir()); + ui->blocksDir->setToolTip(ui->blocksDir->toolTip().arg(QString(nonbreaking_hyphen) + "blocksdir")); + + ui->dataDir->setText(model->dataDir()); + ui->dataDir->setToolTip(ui->dataDir->toolTip().arg(QString(nonbreaking_hyphen) + "datadir")); + + ui->clientUserAgent->setText(model->formatSubVersion()); + ui->clientVersion->setText(model->formatFullVersion()); + ui->networkName->setText(QString::fromStdString(Params().NetworkIDString())); + ui->startupTime->setText(model->formatClientStartupTime()); + } +} + +void InformationWidget::updateNetworkState() +{ + if (!clientModel) return; + QString connections = QString::number(clientModel->getNumConnections()) + " ("; + connections += tr("In:") + " " + QString::number(clientModel->getNumConnections(CONNECTIONS_IN)) + " / "; + connections += tr("Out:") + " " + QString::number(clientModel->getNumConnections(CONNECTIONS_OUT)) + ")"; + + if(!clientModel->node().getNetworkActive()) { + connections += " (" + tr("Network activity disabled") + ")"; + } + + ui->numberOfConnections->setText(connections); + + QString local_addresses; + std::map hosts = clientModel->getNetLocalAddresses(); + for (const auto& [addr, info] : hosts) { + local_addresses += QString::fromStdString(addr.ToStringAddr()); + if (!addr.IsI2P()) local_addresses += ":" + QString::number(info.nPort); + local_addresses += ", "; + } + local_addresses.chop(2); // remove last ", " + if (local_addresses.isEmpty()) local_addresses = tr("None"); + + ui->localAddresses->setText(local_addresses); +} + +void InformationWidget::setNumConnections(int count) +{ + if (!clientModel) + return; + + updateNetworkState(); +} + +void InformationWidget::setNetworkActive(bool networkActive) +{ + updateNetworkState(); +} + +void InformationWidget::setNumBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, bool headers) +{ + if (!headers) { + ui->numberOfBlocks->setText(QString::number(count)); + ui->lastBlockTime->setText(blockDate.toString()); + ui->lastBlockHash->setText(blockHash); + } +} + +void InformationWidget::setMempoolSize(long numberOfTxs, size_t dynUsage, size_t maxUsage) +{ + ui->mempoolNumberTxs->setText(QString::number(numberOfTxs)); + + const auto cur_usage_str = dynUsage < 1000000 ? + QObject::tr("%1 kB").arg(dynUsage / 1000.0, 0, 'f', 2) : + QObject::tr("%1 MB").arg(dynUsage / 1000000.0, 0, 'f', 2); + const auto max_usage_str = QObject::tr("%1 MB").arg(maxUsage / 1000000.0, 0, 'f', 2); + + ui->mempoolSize->setText(cur_usage_str + " / " + max_usage_str); +} diff --git a/src/qt/informationwidget.h b/src/qt/informationwidget.h new file mode 100644 index 000000000000..88eae235a9fd --- /dev/null +++ b/src/qt/informationwidget.h @@ -0,0 +1,45 @@ +// Copyright (c) 2011-2021 The Bitcoin Core developers +// Copyright (c) 2014-2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_INFORMATIONWIDGET_H +#define BITCOIN_QT_INFORMATIONWIDGET_H + +#include + +class ClientModel; +namespace Ui { +class InformationWidget; +} // namespace Ui + +class InformationWidget : public QWidget +{ + Q_OBJECT + +public: + explicit InformationWidget(QWidget* parent = nullptr); + ~InformationWidget() override; + + void setClientModel(ClientModel* model); + +public Q_SLOTS: + /** Set number of connections shown in the UI */ + void setNumConnections(int count); + /** Set network state shown in the UI */ + void setNetworkActive(bool networkActive); + /** Set number of blocks, last block date and last block hash shown in the UI */ + void setNumBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, bool headers); + /** Set size (number of transactions and memory usage) of the mempool in the UI */ + void setMempoolSize(long numberOfTxs, size_t dynUsage, size_t maxUsage); + +private: + /** Update UI with latest network info from model. */ + void updateNetworkState(); + +private: + Ui::InformationWidget* ui; + ClientModel* clientModel{nullptr}; +}; + +#endif // BITCOIN_QT_INFORMATIONWIDGET_H diff --git a/src/qt/masternodelist.cpp b/src/qt/masternodelist.cpp index 0cb065aa7ef0..dbc1df40d87a 100644 --- a/src/qt/masternodelist.cpp +++ b/src/qt/masternodelist.cpp @@ -5,10 +5,7 @@ #include #include -#include -#include -#include -#include +#include