From e9021c5562f029e55d8cbfad85ab7521acc9c677 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:40:36 -0500 Subject: [PATCH 01/22] Remove the legacy "new user wizard" This removes the "wizard" that appears on start up when no configuration file exists. It prompted for a user's BOINC email address. We replace it with a more sophisticated onboarding process in a later commit. --- src/qt/bitcoingui.cpp | 155 -------------------------------------- src/qt/bitcoingui.h | 5 -- src/qt/bitcoinstrings.cpp | 1 - 3 files changed, 161 deletions(-) diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index f385082d11..0430682b44 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -318,10 +318,6 @@ void BitcoinGUI::createActions() diagnosticsAction->setStatusTip(tr("Diagnostics")); diagnosticsAction->setMenuRole(QAction::TextHeuristicRole); - newUserWizardAction = new QAction(tr("&New User Wizard"), this); - newUserWizardAction->setStatusTip(tr("New User Wizard")); - newUserWizardAction->setMenuRole(QAction::TextHeuristicRole); - optionsAction = new QAction(tr("&Options..."), this); optionsAction->setToolTip(tr("Modify configuration options for Gridcoin")); optionsAction->setMenuRole(QAction::PreferencesRole); @@ -359,7 +355,6 @@ void BitcoinGUI::createActions() connect(signMessageAction, SIGNAL(triggered()), this, SLOT(gotoSignMessageTab())); connect(verifyMessageAction, SIGNAL(triggered()), this, SLOT(gotoVerifyMessageTab())); connect(diagnosticsAction, SIGNAL(triggered()), this, SLOT(diagnosticsClicked())); - connect(newUserWizardAction, SIGNAL(triggered()), this, SLOT(newUserWizardClicked())); connect(snapshotAction, SIGNAL(triggered()), this, SLOT(snapshotClicked())); } @@ -383,7 +378,6 @@ void BitcoinGUI::setIcons() boincAction->setIcon(QPixmap(":/images/boinc")); quitAction->setIcon(QPixmap(":/icons/quit")); aboutAction->setIcon(QPixmap(":/images/gridcoin")); - newUserWizardAction->setIcon(QPixmap(":/images/gridcoin")); diagnosticsAction->setIcon(QPixmap(":/images/gridcoin")); optionsAction->setIcon(QPixmap(":/icons/options")); toggleHideAction->setIcon(QPixmap(":/images/gridcoin")); @@ -429,10 +423,6 @@ void BitcoinGUI::createMenuBar() settings->addAction(unlockWalletAction); settings->addAction(lockWalletAction); settings->addSeparator(); - // This new wizard menu item is disabled until we make the wizard more advanced, because the existing one it makes no sense - // to run it after the conf file is created. - //settings->addAction(newUserWizardAction); - //settings->addSeparator(); settings->addAction(optionsAction); QMenu *community = appMenuBar->addMenu(tr("&Community")); @@ -905,110 +895,6 @@ void BitcoinGUI::askFee(qint64 nFeeRequired, bool *payFee) *payFee = (retval == QMessageBox::Yes); } - -std::string tostdstring(QString q) -{ - std::string ss1 = q.toLocal8Bit().constData(); - return ss1; -} - - -bool CreateNewConfigFile(std::string boinc_email) -{ - fsbridge::ofstream myConfig; - myConfig.open(GetDataDir() / "gridcoinresearch.conf"); - - myConfig << "email=" << boinc_email << "\n" - << "addnode=node.gridcoin.us\n" - << "addnode=www.grcpool.com\n" - << "addnode=seeds.gridcoin.ifoggz-network.xyz\n" - << "addnode=ec2-3-81-39-58.compute-1.amazonaws.com\n" - << "addnode=addnode-us-central.cycy.me\n" - << "addnode=gridcoin.ddns.net\n"; - - myConfig.close(); - - return true; -} - - -bool ForceInAddNode(std::string sMyAddNode) -{ - LOCK(cs_vAddedNodes); - std::vector::iterator it = vAddedNodes.begin(); - for(; it != vAddedNodes.end(); it++) - if (sMyAddNode == *it) - break; - if (it != vAddedNodes.end()) return false; - vAddedNodes.push_back(sMyAddNode); - return true; -} - -void BitcoinGUI::NewUserWizard() -{ - if (IsConfigFileEmpty()) - { - QString boincemail = ""; - //Typhoon- Check to see if boinc exists in default path - 11-19-2014 - - fs::path sourcefile = GetBoincDataDir() / "client_state.xml"; - std::string sout = GetFileContents(sourcefile); - //bool BoincInstalled = true; - std::string sBoincNarr = ""; - if (sout == "-1") - { - LogPrintf("BOINC not installed in default location! "); - //BoincInstalled=false; - sBoincNarr = "BOINC is not installed in the default location " + sourcefile.string() + "! Please set boincdatadir in the gridcoinresearch.conf file to the correct path where the BOINC client_state.xml file resides."; - } - - bool ok; - boincemail = QInputDialog::getText(this, tr("New User Wizard"), - tr("Please enter your BOINC E-mail address, or click to skip for now:"), - QLineEdit::Normal, - "", &ok); - - if (ok && !boincemail.isEmpty()) - { - std::string new_email = tostdstring(boincemail); - boost::to_lower(new_email); - LogPrintf("User entered %s ",new_email); - //Create Config File - CreateNewConfigFile(new_email); - QString strMessage = tr("Created new Configuration File Successfully. "); - QMessageBox::warning(this, tr("New Account Created - Welcome Aboard!"), strMessage); - - // Reload BOINC CPIDs now that we know the user's email address: - NN::Researcher::Reload(); - } - else - { - //Create Config File - CreateNewConfigFile("investor"); - QString strMessage = tr("To get started with BOINC, run the BOINC client, choose projects, then populate the gridcoinresearch.conf file in %appdata%\\GridcoinResearch with your BOINC e-mail address. To run this wizard again, please delete the gridcoinresearch.conf file. "); - QMessageBox::warning(this, tr("New User Wizard - Skipped"), strMessage); - } - // Read in the mapargs, and set the seed nodes 10-13-2015 - ReadConfigFile(mapArgs, mapMultiArgs); - - //Force some addnodes in to get user started - ForceInAddNode("node.gridcoin.us"); - ForceInAddNode("www.grcpool.com"); - ForceInAddNode("seeds.gridcoin.ifoggz-network.xyz"); - ForceInAddNode("ec2-3-81-39-58.compute-1.amazonaws.com"); - ForceInAddNode("addnode-us-central.cycy.me"); - ForceInAddNode("gridcoin.ddns.net"); - - if (sBoincNarr != "") - { - QString qsMessage = tr(sBoincNarr.c_str()); - QMessageBox::warning(this, tr("Attention! - BOINC Path Error!"), qsMessage); - } - } -} - - - void BitcoinGUI::incomingTransaction(const QModelIndex & parent, int start, int end) { if(!walletModel || !clientModel) @@ -1088,12 +974,6 @@ void BitcoinGUI::diagnosticsClicked() diagnosticsDialog->activateWindow(); } -// Note this is for the menu item. The menu item is disabled until we implement a more advanced wizard. -void BitcoinGUI::newUserWizardClicked() -{ - NewUserWizard(); -} - // links to websites and services outside the gridcoin client void BitcoinGUI::bxClicked() { @@ -1422,41 +1302,6 @@ void BitcoinGUI::timerfire() { try { - static bool bNewUserWizardNotified = false; - if (!bNewUserWizardNotified) - { - bNewUserWizardNotified=true; - NewUserWizard(); - } - - // TODO: Check if these SetRPCResponse calls are really needed. - /*if (bGlobalcomInitialized) - { - //R Halford - Allow .NET to talk to Core: 6-21-2015 - #ifdef WIN32 - std::string sData = qtExecuteDotNetStringFunction("GetDotNetMessages",""); - if (!sData.empty()) - { - std::string RPCCommand = ExtractXML(sData,"",""); - std::string Argument1 = ExtractXML(sData,"",""); - std::string Argument2 = ExtractXML(sData,"",""); - - if (RPCCommand=="rain") - { - try - { - std::string response = executeRain(Argument1+Argument2); - qtExecuteGenericFunction("SetRPCResponse"," "+response); - } - catch (const UniValue& objError) - { - qtExecuteGenericFunction("SetRPCResponse", find_value(objError, "message").get_str()); - } - } - } - #endif - }*/ - if (Timer("status_update",5)) { GetGlobalStatus(); diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index c54a389189..aaf34e8442 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -93,7 +93,6 @@ class BitcoinGUI : public QMainWindow QAction *exchangeAction; QAction *votingAction; QAction *diagnosticsAction; - QAction *newUserWizardAction; QAction *verifyMessageAction; QAction *aboutAction; QAction *receiveCoinsAction; @@ -166,7 +165,6 @@ public slots: void askQuestion(std::string caption, std::string body, bool *result); - void NewUserWizard(); void handleURI(QString strURI); void setOptionsStyleSheet(QString qssFileName); @@ -202,9 +200,6 @@ private slots: void chatClicked(); void diagnosticsClicked(); void peersClicked(); - - void newUserWizardClicked(); - void snapshotClicked(); #ifndef Q_OS_MAC diff --git a/src/qt/bitcoinstrings.cpp b/src/qt/bitcoinstrings.cpp index 56211bc14d..85a575a89a 100644 --- a/src/qt/bitcoinstrings.cpp +++ b/src/qt/bitcoinstrings.cpp @@ -228,7 +228,6 @@ QT_TRANSLATE_NOOP("bitcoin-core", "Output extra debugging information. Implies a QT_TRANSLATE_NOOP("bitcoin-core", "Output extra network debugging information"), QT_TRANSLATE_NOOP("bitcoin-core", "POR Blocks Verified"), QT_TRANSLATE_NOOP("bitcoin-core", "Password for JSON-RPC connections"), -QT_TRANSLATE_NOOP("bitcoin-core", "Please wait for new user wizard to start..."), QT_TRANSLATE_NOOP("bitcoin-core", "Prepend debug output with timestamp"), QT_TRANSLATE_NOOP("bitcoin-core", "Print version and exit"), QT_TRANSLATE_NOOP("bitcoin-core", "Project email mismatch"), From 88b510088fefdb2927fa272fdbeaade3ac392c46 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:40:39 -0500 Subject: [PATCH 02/22] Create default configuration file on init If no configuration file exists, create a new one with the defaults. The legacy--and now removed--"new user wizard" did this before. --- src/init.cpp | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 39fe7e55c3..6faa58acfd 100755 --- a/src/init.cpp +++ b/src/init.cpp @@ -194,6 +194,19 @@ bool static Bind(const CService &addr, bool fError = true) { return true; } +static void CreateNewConfigFile() +{ + fsbridge::ofstream myConfig; + myConfig.open(GetConfigFile()); + + myConfig + << "addnode=addnode-us-central.cycy.me\n" + << "addnode=ec2-3-81-39-58.compute-1.amazonaws.com\n" + << "addnode=gridcoin.ddns.net\n" + << "addnode=seeds.gridcoin.ifoggz-network.xyz\n" + << "addnode=www.grcpool.com\n"; +} + // Core-specific options shared between UI and daemon std::string HelpMessage() { @@ -493,11 +506,17 @@ bool AppInit2(ThreadHandlerPtr threads) // ********************************************************* Step 2: parameter interactions - // Gridcoin - Check to see if config is empty? if (IsConfigFileEmpty()) { - uiInterface.ThreadSafeMessageBox( - "Configuration file empty. \n" + _("Please wait for new user wizard to start..."), "", 0); + try + { + CreateNewConfigFile(); + ReadConfigFile(mapArgs, mapMultiArgs); + } + catch (const std::exception& e) + { + LogPrintf("WARNING: failed to create configuration file: %s", e.what()); + } } //6-10-2014: R Halford: Updating Boost version to 1.5.5 to prevent sync issues; print the boost version to verify: From 63089baafdaf6529575a81e93a0a067220765d6c Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:40:43 -0500 Subject: [PATCH 03/22] Move non-developer DNS seeds to default config addnodes This moves DNS seeds not controlled by active developers to the default set of addnodes in a new configuration file. --- src/init.cpp | 3 +++ src/net.cpp | 6 ++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 6faa58acfd..f8baa471eb 100755 --- a/src/init.cpp +++ b/src/init.cpp @@ -202,7 +202,10 @@ static void CreateNewConfigFile() myConfig << "addnode=addnode-us-central.cycy.me\n" << "addnode=ec2-3-81-39-58.compute-1.amazonaws.com\n" + << "addnode=gridcoin.crypto.fans\n" << "addnode=gridcoin.ddns.net\n" + << "addnode=london.grcnode.co.uk\n" + << "addnode=nuad.de\n" << "addnode=seeds.gridcoin.ifoggz-network.xyz\n" << "addnode=www.grcpool.com\n"; } diff --git a/src/net.cpp b/src/net.cpp index cdb93b3a86..485ce43b20 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1507,11 +1507,9 @@ void MapPort() // The first name is used as information source for addrman. // The second name should resolve to a list of seed addresses. static const char *strDNSSeed[][2] = { - {"node.gridcoin.us", "node.gridcoin.us"}, - {"london.grcnode.co.uk", "london.grcnode.co.uk"}, - {"gridcoin.crypto.fans", "gridcoin.crypto.fans"}, + {"addnode-us-central.cycy.me", "addnode-us-central.cycy.me"}, + {"ec2-3-81-39-58.compute-1.amazonaws.com", "ec2-3-81-39-58.compute-1.amazonaws.com"}, {"node.grcpool.com", "node.grcpool.com"}, - {"nuad.de", "nuad.de"}, {"seeds.gridcoin.ifoggz-network.xyz", "seeds.gridcoin.ifoggz-network.xyz"}, {"", ""}, }; From 5f813eb470a9360cb1fb09bbf3c2131efca80244 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:40:46 -0500 Subject: [PATCH 04/22] Move Quorum::MyMagnitude() method to Researcher API This better organizes the responsibility for the APIs that provide the magnitude of the CPID loaded by the wallet. --- src/contract/polls.cpp | 3 ++- src/main.cpp | 6 ++++-- src/neuralnet/quorum.cpp | 10 ---------- src/neuralnet/quorum.h | 8 -------- src/neuralnet/researcher.cpp | 12 ++++++++++++ src/neuralnet/researcher.h | 10 ++++++++++ 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/contract/polls.cpp b/src/contract/polls.cpp index f3efd2b33e..413907495b 100644 --- a/src/contract/polls.cpp +++ b/src/contract/polls.cpp @@ -12,6 +12,7 @@ #include "neuralnet/contract/contract.h" #include "neuralnet/contract/message.h" #include "neuralnet/quorum.h" +#include "neuralnet/researcher.h" #include "neuralnet/superblock.h" #include "neuralnet/tally.h" @@ -174,7 +175,7 @@ std::pair CreateVoteContract(std::string sTitle, std:: const std::string primary_cpid = NN::GetPrimaryCpid(); std::string GRCAddress = DefaultWalletAddress(); - double dmag = NN::Quorum::MyMagnitude().Floating(); + double dmag = NN::Researcher::Get()->Magnitude().Floating(); double poll_duration = PollDuration(sTitle) * 86400; // Prevent Double Voting diff --git a/src/main.cpp b/src/main.cpp index e7fd326dde..adc5b6c0bf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -544,6 +544,8 @@ void GetGlobalStatus() LogPrintf("Error obtaining last poll: %s", e.what()); } + const NN::ResearcherPtr researcher = NN::Researcher::Get(); + LOCK(GlobalStatusStruct.lock); GlobalStatusStruct.blocks = ToString(nBestHeight); @@ -551,8 +553,8 @@ void GetGlobalStatus() GlobalStatusStruct.netWeight = RoundToString(GetEstimatedNetworkWeight() / 80.0,2); //todo: use the real weight from miner status (requires scaling) GlobalStatusStruct.coinWeight = sWeight; - GlobalStatusStruct.magnitude = NN::Quorum::MyMagnitude().ToString(); - GlobalStatusStruct.cpid = NN::GetPrimaryCpid(); + GlobalStatusStruct.magnitude = researcher->Magnitude().ToString(); + GlobalStatusStruct.cpid = researcher->Id().ToString(); GlobalStatusStruct.poll = std::move(current_poll); GlobalStatusStruct.status = msMiningErrors; diff --git a/src/neuralnet/quorum.cpp b/src/neuralnet/quorum.cpp index 48d082001d..ddf1cf2d0a 100644 --- a/src/neuralnet/quorum.cpp +++ b/src/neuralnet/quorum.cpp @@ -3,7 +3,6 @@ #include "neuralnet/claim.h" #include "neuralnet/magnitude.h" #include "neuralnet/quorum.h" -#include "neuralnet/researcher.h" #include "neuralnet/superblock.h" #include "scraper_net.h" #include "util/reverse_iterator.h" @@ -1589,15 +1588,6 @@ bool Quorum::ValidateSuperblock( return result != Result::INVALID; } -Magnitude Quorum::MyMagnitude() -{ - if (const auto cpid_option = NN::Researcher::Get()->Id().TryCpid()) { - return Quorum::CurrentSuperblock()->m_cpids.MagnitudeOf(*cpid_option); - } - - return Magnitude::Zero(); -} - Magnitude Quorum::GetMagnitude(const Cpid cpid) { return Quorum::CurrentSuperblock()->m_cpids.MagnitudeOf(cpid); diff --git a/src/neuralnet/quorum.h b/src/neuralnet/quorum.h index 8c36af7f90..cbf7bb2b9b 100644 --- a/src/neuralnet/quorum.h +++ b/src/neuralnet/quorum.h @@ -108,14 +108,6 @@ class Quorum const bool use_cache = true, const size_t hint_bits = 32); - //! - //! \brief Get the current magnitude of the CPID loaded by the wallet. - //! - //! \return The wallet user's magnitude or zero if the wallet started in - //! investor mode. - //! - static Magnitude MyMagnitude(); - //! //! \brief Get the current magnitude for the specified CPID. //! diff --git a/src/neuralnet/researcher.cpp b/src/neuralnet/researcher.cpp index 2c3407c7cd..ea6bf5c1a8 100644 --- a/src/neuralnet/researcher.cpp +++ b/src/neuralnet/researcher.cpp @@ -5,6 +5,8 @@ #include "global_objects_noui.hpp" #include "init.h" #include "neuralnet/beacon.h" +#include "neuralnet/magnitude.h" +#include "neuralnet/quorum.h" #include "neuralnet/researcher.h" #include "ui_interface.h" #include "util.h" @@ -865,6 +867,16 @@ bool Researcher::IsInvestor() const return m_mining_id.Which() == MiningId::Kind::INVESTOR; } +NN::Magnitude Researcher::Magnitude() const +{ + if (const auto cpid_option = m_mining_id.TryCpid()) { + LOCK(cs_main); + return Quorum::GetMagnitude(*cpid_option); + } + + return NN::Magnitude::Zero(); +} + ResearcherStatus Researcher::Status() const { if (Eligible()) { diff --git a/src/neuralnet/researcher.h b/src/neuralnet/researcher.h index 2273325cfd..9b4c1dd67a 100644 --- a/src/neuralnet/researcher.h +++ b/src/neuralnet/researcher.h @@ -15,6 +15,8 @@ class uint256; namespace NN { +class Magnitude; + //! //! \brief Describes the eligibility status for earning rewards as part of the //! research reward protocol. @@ -385,6 +387,14 @@ class Researcher //! bool IsInvestor() const; + //! + //! \brief Get the current magnitude of the CPID loaded by the wallet. + //! + //! \return The wallet user's magnitude or zero if the wallet started in + //! investor mode. + //! + NN::Magnitude Magnitude() const; + //! //! \brief Get a value that indicates how the wallet participates in the //! Proof-of-Research protocol. From 5b47b466bfebda48fe32b7979d9d4fffe4a2e871 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:40:49 -0500 Subject: [PATCH 05/22] Add Tally API for direct accrual calculation This provides a method that calculates the accrual for a CPID without the need to obtain an IAccrualComputer object beforehand. It improves readability of client code that does not need those extra details and may allow the compiler to perform some additional optimizations since the virtual classes exist within the same translation unit. --- src/main.cpp | 2 +- src/miner.cpp | 3 +-- src/neuralnet/tally.cpp | 8 ++++++++ src/neuralnet/tally.h | 14 ++++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index adc5b6c0bf..34c9b3f955 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2618,7 +2618,7 @@ class ClaimValidator int64_t research_owed = 0; if (const NN::CpidOption cpid = m_claim.m_mining_id.TryCpid()) { - research_owed = NN::Tally::GetComputer(*cpid, m_block.nTime, m_pindex)->Accrual(); + research_owed = NN::Tally::GetAccrual(*cpid, m_block.nTime, m_pindex); } int64_t out_stake_owed; diff --git a/src/miner.cpp b/src/miner.cpp index e9ec5c7481..43380d0edd 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -1050,8 +1050,7 @@ bool CreateGridcoinReward( index.nVersion = blocknew.nVersion; index.nHeight = pindexPrev->nHeight + 1; - const NN::AccrualComputer calc = NN::Tally::GetComputer(*cpid, blocknew.nTime, &index); - claim.m_research_subsidy = calc->Accrual(); + claim.m_research_subsidy = NN::Tally::GetAccrual(*cpid, blocknew.nTime, &index); // If no pending research subsidy value exists, build an investor claim. // This avoids polluting the block index with non-research reward blocks diff --git a/src/neuralnet/tally.cpp b/src/neuralnet/tally.cpp index 5e9ccb4b3e..2ecab7ad00 100644 --- a/src/neuralnet/tally.cpp +++ b/src/neuralnet/tally.cpp @@ -622,6 +622,14 @@ const ResearchAccount& Tally::GetAccount(const Cpid cpid) return g_researcher_tally.GetAccount(cpid); } +int64_t Tally::GetAccrual( + const Cpid cpid, + const int64_t payment_time, + const CBlockIndex* const last_block_ptr) +{ + return GetComputer(cpid, payment_time, last_block_ptr)->Accrual(); +} + AccrualComputer Tally::GetComputer( const Cpid cpid, const int64_t payment_time, diff --git a/src/neuralnet/tally.h b/src/neuralnet/tally.h index b0d837dffd..5ab72c859f 100644 --- a/src/neuralnet/tally.h +++ b/src/neuralnet/tally.h @@ -103,6 +103,20 @@ class Tally //! static const ResearchAccount& GetAccount(const Cpid cpid); + //! + //! \brief Calculate the research reward accrual for the specified CPID. + //! + //! \param cpid CPID to calculate research accrual for. + //! \param payment_time Time of payment to calculate rewards at. + //! \param last_block_ptr Refers to the block for the reward. + //! + //! \return Research reward accrual in units of 1/100000000 GRC. + //! + static int64_t GetAccrual( + const Cpid cpid, + const int64_t payment_time, + const CBlockIndex* const last_block_ptr); + //! //! \brief Get an initialized research reward accrual calculator. //! From e226003e8a1b11df8b25ab90ef0a100f144a3740 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:40:52 -0500 Subject: [PATCH 06/22] Refactor GUI researcher data presentation into a proper view model This adds a Qt view model for the researcher context data displayed in the GUI. It replaces state tracking in the old GlobalStatusStruct. --- src/Makefile.qt.include | 7 +- src/main.cpp | 6 - src/main.h | 3 - src/neuralnet/researcher.cpp | 40 +++- src/neuralnet/researcher.h | 34 ++- src/qt/bitcoin.cpp | 4 + src/qt/bitcoingui.cpp | 114 +++------- src/qt/bitcoingui.h | 7 + src/qt/overviewpage.cpp | 41 +++- src/qt/overviewpage.h | 8 +- src/qt/researcher/researchermodel.cpp | 291 ++++++++++++++++++++++++++ src/qt/researcher/researchermodel.h | 82 ++++++++ src/ui_interface.h | 6 + 13 files changed, 530 insertions(+), 113 deletions(-) create mode 100644 src/qt/researcher/researchermodel.cpp create mode 100644 src/qt/researcher/researchermodel.h diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index f7094c13bd..7095d25d4e 100755 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -85,7 +85,7 @@ QT_FORMS_UI = \ qt/forms/transactiondescdialog.ui \ qt/forms/askpassphrasedialog.ui \ qt/forms/sendcoinsentry.ui - + QT_MOC_CPP = \ qt/moc_aboutdialog.cpp \ qt/moc_addressbookpage.cpp \ @@ -113,6 +113,7 @@ QT_MOC_CPP = \ qt/moc_peertablemodel.cpp \ qt/moc_qvalidatedlineedit.cpp \ qt/moc_qvaluecombobox.cpp \ + qt/researcher/moc_researchermodel.cpp \ qt/moc_rpcconsole.cpp \ qt/moc_sendcoinsdialog.cpp \ qt/moc_sendcoinsentry.cpp \ @@ -169,6 +170,7 @@ GRIDCOINRESEARCH_QT_H = \ qt/qtipcserver.h \ qt/qvalidatedlineedit.h \ qt/qvaluecombobox.h \ + qt/researcher/researchermodel.h \ qt/rpcconsole.h \ qt/sendcoinsdialog.h \ qt/sendcoinsentry.h \ @@ -212,6 +214,7 @@ GRIDCOINRESEARCH_QT_CPP = \ qt/qtipcserver.cpp \ qt/qvalidatedlineedit.cpp \ qt/qvaluecombobox.cpp \ + qt/researcher/researchermodel.cpp \ qt/rpcconsole.cpp \ qt/sendcoinsdialog.cpp \ qt/sendcoinsentry.cpp \ @@ -409,7 +412,7 @@ CLEANFILES += $(CLEAN_QT) gridcoinresearch_clean: FORCE rm -f $(CLEAN_QT) $(qt_gridcoinresearch_OBJECTS) qt/gridcoinresearch$(EXEEXT) -gridcoinresearch: +gridcoinresearch: @echo "$(ZIP_LIBS)" qt/gridcoinresearch$(EXEEXT) diff --git a/src/main.cpp b/src/main.cpp index 34c9b3f955..4dd69dcd96 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -544,8 +544,6 @@ void GetGlobalStatus() LogPrintf("Error obtaining last poll: %s", e.what()); } - const NN::ResearcherPtr researcher = NN::Researcher::Get(); - LOCK(GlobalStatusStruct.lock); GlobalStatusStruct.blocks = ToString(nBestHeight); @@ -553,12 +551,8 @@ void GetGlobalStatus() GlobalStatusStruct.netWeight = RoundToString(GetEstimatedNetworkWeight() / 80.0,2); //todo: use the real weight from miner status (requires scaling) GlobalStatusStruct.coinWeight = sWeight; - GlobalStatusStruct.magnitude = researcher->Magnitude().ToString(); - GlobalStatusStruct.cpid = researcher->Id().ToString(); GlobalStatusStruct.poll = std::move(current_poll); - GlobalStatusStruct.status = msMiningErrors; - unsigned long stk_dropped; { diff --git a/src/main.h b/src/main.h index 96cd0906d0..33dbb8c583 100644 --- a/src/main.h +++ b/src/main.h @@ -219,9 +219,6 @@ struct globalStatusType std::string difficulty; std::string netWeight; std::string coinWeight; - std::string magnitude; - std::string cpid; - std::string status; std::string poll; std::string errors; }; diff --git a/src/neuralnet/researcher.cpp b/src/neuralnet/researcher.cpp index ea6bf5c1a8..5ed445862b 100644 --- a/src/neuralnet/researcher.cpp +++ b/src/neuralnet/researcher.cpp @@ -8,6 +8,7 @@ #include "neuralnet/magnitude.h" #include "neuralnet/quorum.h" #include "neuralnet/researcher.h" +#include "neuralnet/tally.h" #include "ui_interface.h" #include "util.h" @@ -296,6 +297,8 @@ void StoreResearcher(Researcher context) std::atomic_store( &researcher, std::make_shared(std::move(context))); + + uiInterface.ResearcherChanged(); } //! @@ -745,12 +748,17 @@ BeaconError AdvertiseBeaconResult::Error() const Researcher::Researcher() : m_mining_id(MiningId::ForInvestor()) + , m_beacon_error(BeaconError::NONE) { } -Researcher::Researcher(MiningId mining_id, MiningProjectMap projects) +Researcher::Researcher( + MiningId mining_id, + MiningProjectMap projects, + const NN::BeaconError beacon_error) : m_mining_id(std::move(mining_id)) , m_projects(std::move(projects)) + , m_beacon_error(beacon_error) { } @@ -803,7 +811,7 @@ void Researcher::Reload() Reload(MiningProjectMap::Parse(FetchProjectsXml())); } -void Researcher::Reload(MiningProjectMap projects) +void Researcher::Reload(MiningProjectMap projects, NN::BeaconError beacon_error) { const std::set team_whitelist = GetTeamWhitelist(); @@ -830,12 +838,15 @@ void Researcher::Reload(MiningProjectMap projects) LogPrintf("WARNING: no projects eligible for research rewards."); } - StoreResearcher(Researcher(std::move(mining_id), std::move(projects))); + StoreResearcher( + Researcher(std::move(mining_id), std::move(projects), beacon_error)); } void Researcher::Refresh() { - Reload(Get()->m_projects); + const ResearcherPtr researcher = Get(); + + Reload(researcher->m_projects, researcher->m_beacon_error); } const MiningId& Researcher::Id() const @@ -877,6 +888,21 @@ NN::Magnitude Researcher::Magnitude() const return NN::Magnitude::Zero(); } +int64_t Researcher::Accrual() const +{ + const CpidOption cpid = m_mining_id.TryCpid(); + + if (!cpid || !pindexBest) { + return 0; + } + + const int64_t now = OutOfSyncByAge() ? pindexBest->nTime : GetAdjustedTime(); + + LOCK(cs_main); + + return Tally::GetAccrual(*cpid, now, pindexBest); +} + ResearcherStatus Researcher::Status() const { if (Eligible()) { @@ -894,6 +920,11 @@ ResearcherStatus Researcher::Status() const return ResearcherStatus::INVESTOR; } +NN::BeaconError Researcher::BeaconError() const +{ + return m_beacon_error; +} + AdvertiseBeaconResult Researcher::AdvertiseBeacon() { AssertLockHeld(cs_main); @@ -934,6 +965,7 @@ AdvertiseBeaconResult Researcher::AdvertiseBeacon() } m_beacon_error = result.Error(); + uiInterface.BeaconChanged(); if (m_beacon_error == BeaconError::NONE) { last_advertised_height = nBestHeight; diff --git a/src/neuralnet/researcher.h b/src/neuralnet/researcher.h index 9b4c1dd67a..f3774d1b7f 100644 --- a/src/neuralnet/researcher.h +++ b/src/neuralnet/researcher.h @@ -285,8 +285,12 @@ class Researcher //! //! \param mining_id Represents a CPID or an investor. //! \param projects A set of local projects loaded from BOINC. + //! \param beacon_error Last beacon advertisement error, if any. //! - Researcher(MiningId mining_id, MiningProjectMap projects); + Researcher( + MiningId mining_id, + MiningProjectMap projects, + const BeaconError beacon_error = BeaconError::NONE); //! //! \brief Get the configured BOINC account email address. @@ -329,8 +333,11 @@ class Researcher //! //! \param projects Data for one or more projects as loaded from BOINC's //! client_state.xml file. + //! \param beacon_error Set or transfer the last beacon advertisement error. //! - static void Reload(MiningProjectMap projects); + static void Reload( + MiningProjectMap projects, + BeaconError beacon_error = BeaconError::NONE); //! //! \brief Rescan the set of in-memory projects for eligible CPIDs without @@ -395,15 +402,30 @@ class Researcher //! NN::Magnitude Magnitude() const; + //! + //! \brief Get the current research reward accrued for the CPID loaded by + //! the wallet. + //! + //! \return Research reward accrual in units of 1/100000000 GRC. + //! + int64_t Accrual() const; + //! //! \brief Get a value that indicates how the wallet participates in the - //! Proof-of-Research protocol. + //! research reward protocol. //! //! \return The status depends on whether the wallet successfully loaded //! eligible CPIDs from BOINC. //! ResearcherStatus Status() const; + //! + //! \brief Get the error from the last beacon advertisement, if any. + //! + //! \return Describes an error that occurred during beacon advertisement. + //! + NN::BeaconError BeaconError() const; + //! //! \brief Submit a beacon contract to the network for the current CPID. //! @@ -452,9 +474,9 @@ class Researcher bool ImportBeaconKeysFromConfig(CWallet* const pwallet) const; private: - MiningId m_mining_id; //!< CPID or INVESTOR variant. - MiningProjectMap m_projects; //!< Local projects loaded from BOINC. - BeaconError m_beacon_error; //!< Last beacon error that occurred, if any. + MiningId m_mining_id; //!< CPID or INVESTOR variant. + MiningProjectMap m_projects; //!< Local projects loaded from BOINC. + NN::BeaconError m_beacon_error; //!< Last beacon error that occurred, if any. }; // Researcher //! diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index 288d9a381f..9574526f03 100755 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -9,6 +9,7 @@ #include "bitcoingui.h" #include "clientmodel.h" #include "walletmodel.h" +#include "researcher/researchermodel.h" #include "optionsmodel.h" #include "global_objects_noui.hpp" #include "guiutil.h" @@ -477,9 +478,11 @@ int StartGridcoinQt(int argc, char *argv[]) ClientModel clientModel(&optionsModel); WalletModel walletModel(pwalletMain, &optionsModel); + ResearcherModel researcherModel; window.setClientModel(&clientModel); window.setWalletModel(&walletModel); + window.setResearcherModel(&researcherModel); // If -min option passed, start window minimized. if(GetBoolArg("-min")) @@ -506,6 +509,7 @@ int StartGridcoinQt(int argc, char *argv[]) window.hide(); window.setClientModel(0); window.setWalletModel(0); + window.setResearcherModel(0); guiref = 0; } // Shutdown the core and its threads, but don't exit Bitcoin-Qt here diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 0430682b44..d036fef078 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -24,6 +24,7 @@ #include "votingdialog.h" #include "clientmodel.h" #include "walletmodel.h" +#include "researcher/researchermodel.h" #include "editaddressdialog.h" #include "optionsmodel.h" #include "transactiondescdialog.h" @@ -83,9 +84,6 @@ #include "rpcclient.h" #include "rpcprotocol.h" #include "contract/polls.h" -#include "neuralnet/beacon.h" -#include "neuralnet/quorum.h" -#include "neuralnet/researcher.h" #include "neuralnet/superblock.h" #include @@ -517,11 +515,6 @@ void BitcoinGUI::createToolBars() labelStakingIcon->setToolTip(tr("Not staking: Miner is not initialized.")); } - QTimer *timerBeaconIcon = new QTimer(labelBeaconIcon); - connect(timerBeaconIcon, SIGNAL(timeout()), this, SLOT(updateBeaconIcon())); - timerBeaconIcon->start(30 * 1000); - updateBeaconIcon(); - toolbar2->addWidget(frameBlocks); addToolBarBreak(Qt::TopToolBarArea); @@ -625,7 +618,7 @@ void BitcoinGUI::setWalletModel(WalletModel *walletModel) // Put transaction list in tabs transactionView->setModel(walletModel); - overviewPage->setModel(walletModel); + overviewPage->setWalletModel(walletModel); addressBookPage->setModel(walletModel->getAddressTableModel()); receiveCoinsPage->setModel(walletModel->getAddressTableModel()); sendCoinsPage->setModel(walletModel); @@ -643,6 +636,25 @@ void BitcoinGUI::setWalletModel(WalletModel *walletModel) } } +void BitcoinGUI::setResearcherModel(ResearcherModel *researcherModel) +{ + this->researcherModel = researcherModel; + + if (!researcherModel) { + return; + } + + overviewPage->setResearcherModel(researcherModel); + + if (researcherModel->configuredForInvestorMode()) { + labelBeaconIcon->hide(); + return; + } + + updateBeaconIcon(); + connect(researcherModel, SIGNAL(beaconChanged()), this, SLOT(updateBeaconIcon())); +} + void BitcoinGUI::createTrayIcon() { #ifndef Q_OS_MAC @@ -1522,76 +1534,16 @@ void BitcoinGUI::updateScraperIcon(int scraperEventtype, int status) void BitcoinGUI::updateBeaconIcon() { - // Intent is to be an investor. Suppress icon. - if (NN::Researcher::ConfiguredForInvestorMode()) - { - labelBeaconIcon->hide(); - return; - } - - const NN::CpidOption cpid = NN::Researcher::Get()->Id().TryCpid(); - - if (!cpid) - { - labelBeaconIcon->setPixmap(QIcon(":/icons/beacon_red").pixmap(STATUSBAR_ICONSIZE, STATUSBAR_ICONSIZE)); - labelBeaconIcon->setToolTip(tr("Wallet status is INVESTOR, but the wallet is not configured for investor mode. " - "If you intend to be an investor only, please remove the email entry in the config file, " - "otherwise you need to check your email entry and your BOINC installation.")); - return; - } - - const NN::BeaconOption beacon = NN::GetBeaconRegistry().Try(*cpid); - - if (!beacon) - { - labelBeaconIcon->setPixmap(QIcon(":/icons/beacon_red").pixmap(STATUSBAR_ICONSIZE, STATUSBAR_ICONSIZE)); - labelBeaconIcon->setToolTip(tr("There is a CPID, %1, but there is no active beacon. " - "Your beacon may be expired or never advertised. Please " - "advertise a new beacon.") - .arg(cpid->ToString().c_str())); - return; - } - - constexpr double seconds_in_day = 24.0 * 60.0 * 60.0; - constexpr int32_t renewal_warning_days = 15; - - const int64_t now = GetAdjustedTime(); - const int32_t beacon_age_days = std::round(beacon->Age(now) / seconds_in_day); - const int32_t days_to_expiration = (NN::Beacon::MAX_AGE / seconds_in_day) - beacon_age_days; - - QString icon_res; - QString status; - - if (days_to_expiration <= renewal_warning_days) - { - icon_res = ":/icons/beacon_red"; - status = tr("BEACON SHOULD BE RENEWED IMMEDIATELY TO AVOID LAPSE."); - } - else if (beacon->Renewable(now)) - { - icon_res = ":/icons/beacon_yellow"; - status = tr("Beacon should be renewed."); - } - else if (NN::Quorum::GetMagnitude(*cpid) == 0) - { - icon_res = ":/icons/beacon_red"; - status = tr( - "Magnitude is zero, which may prevent staking with research rewards. " - "Please check your magnitude after the next superblock."); - } - else - { - icon_res = ":/icons/beacon_green"; - status = tr("Beacon status is good."); - } - - labelBeaconIcon->setPixmap(QIcon(icon_res).pixmap(STATUSBAR_ICONSIZE, STATUSBAR_ICONSIZE)); - labelBeaconIcon->setToolTip(tr("CPID: %1\n" - "Beacon age: %2 day(s)\n" - "Expires: %3 day(s)\n" - "%4.") - .arg(cpid->ToString().c_str()) - .arg(beacon_age_days) - .arg(days_to_expiration) - .arg(status)); + labelBeaconIcon->setPixmap(researcherModel->getBeaconStatusIcon() + .pixmap(STATUSBAR_ICONSIZE, STATUSBAR_ICONSIZE)); + + labelBeaconIcon->setToolTip(tr( + "CPID: %1\n" + "Beacon age: %2\n" + "Expires: %3\n" + "%4") + .arg(researcherModel->formatCpid()) + .arg(researcherModel->formatBeaconAge()) + .arg(researcherModel->formatTimeToBeaconExpiration()) + .arg(researcherModel->formatBeaconStatus())); } diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index aaf34e8442..746be04620 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -10,6 +10,7 @@ class TransactionTableModel; class ClientModel; class WalletModel; +class ResearcherModel; class TransactionView; class OverviewPage; class AddressBookPage; @@ -52,6 +53,11 @@ class BitcoinGUI : public QMainWindow */ void setWalletModel(WalletModel *walletModel); + /** Set the researcher model. + The researcher model provides the BOINC context for the research reward system. + */ + void setResearcherModel(ResearcherModel *researcherModel); + protected: void changeEvent(QEvent *e); void closeEvent(QCloseEvent *event); @@ -61,6 +67,7 @@ class BitcoinGUI : public QMainWindow private: ClientModel *clientModel; WalletModel *walletModel; + ResearcherModel *researcherModel; QStackedWidget *centralWidget; diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index 81167405af..76dab09ebc 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -9,6 +9,7 @@ #ifndef Q_MOC_RUN #include "main.h" #endif +#include "researcher/researchermodel.h" #include "walletmodel.h" #include "bitcoinunits.h" #include "optionsmodel.h" @@ -106,6 +107,8 @@ class TxViewDelegate : public QAbstractItemDelegate OverviewPage::OverviewPage(QWidget *parent) : QWidget(parent), ui(new Ui::OverviewPage), + researcherModel(nullptr), + walletModel(nullptr), currentBalance(-1), currentStake(0), currentUnconfirmedBalance(-1), @@ -179,7 +182,7 @@ OverviewPage::~OverviewPage() void OverviewPage::setBalance(qint64 balance, qint64 stake, qint64 unconfirmedBalance, qint64 immatureBalance) { - int unit = model->getOptionsModel()->getDisplayUnit(); + int unit = walletModel->getOptionsModel()->getDisplayUnit(); currentBalance = balance; currentStake = stake; currentUnconfirmedBalance = unconfirmedBalance; @@ -206,16 +209,25 @@ void OverviewPage::UpdateBoincUtilization() ui->difficultyLabel->setText(QString::fromUtf8(GlobalStatusStruct.difficulty.c_str())); ui->netWeightLabel->setText(QString::fromUtf8(GlobalStatusStruct.netWeight.c_str())); ui->coinWeightLabel->setText(QString::fromUtf8(GlobalStatusStruct.coinWeight.c_str())); - ui->magnitudeLabel->setText(QString::fromUtf8(GlobalStatusStruct.magnitude.c_str())); - ui->cpidLabel->setText(QString::fromUtf8(GlobalStatusStruct.cpid.c_str())); - ui->statusLabel->setText(QString::fromUtf8(GlobalStatusStruct.status.c_str())); ui->pollLabel->setText(QString::fromUtf8(GlobalStatusStruct.poll.c_str()).replace(QChar('_'),QChar(' '), Qt::CaseSensitive)); ui->errorsLabel->setText(QString::fromUtf8(GlobalStatusStruct.errors.c_str())); } -void OverviewPage::setModel(WalletModel *model) +void OverviewPage::setResearcherModel(ResearcherModel *researcherModel) { - this->model = model; + this->researcherModel = researcherModel; + + if (!researcherModel) { + return; + } + + updateResearcherStatus(); + connect(researcherModel, SIGNAL(researcherChanged()), this, SLOT(updateResearcherStatus())); +} + +void OverviewPage::setWalletModel(WalletModel *model) +{ + this->walletModel = model; if(model && model->getOptionsModel()) { // Set up transaction list @@ -243,18 +255,29 @@ void OverviewPage::setModel(WalletModel *model) void OverviewPage::updateDisplayUnit() { - if(model && model->getOptionsModel()) + if(walletModel && walletModel->getOptionsModel()) { if(currentBalance != -1) - setBalance(currentBalance, model->getStake(), currentUnconfirmedBalance, currentImmatureBalance); + setBalance(currentBalance, walletModel->getStake(), currentUnconfirmedBalance, currentImmatureBalance); // Update txdelegate->unit with the current unit - txdelegate->unit = model->getOptionsModel()->getDisplayUnit(); + txdelegate->unit = walletModel->getOptionsModel()->getDisplayUnit(); ui->listTransactions->update(); } } +void OverviewPage::updateResearcherStatus() +{ + if (!researcherModel) { + return; + } + + ui->magnitudeLabel->setText(researcherModel->formatMagnitude()); + ui->cpidLabel->setText(researcherModel->formatCpid()); + ui->statusLabel->setText(researcherModel->formatStatus()); +} + void OverviewPage::showOutOfSyncWarning(bool fShow) { ui->walletStatusLabel->setVisible(fShow); diff --git a/src/qt/overviewpage.h b/src/qt/overviewpage.h index dcef40981a..db52abed02 100644 --- a/src/qt/overviewpage.h +++ b/src/qt/overviewpage.h @@ -15,6 +15,7 @@ QT_END_NAMESPACE namespace Ui { class OverviewPage; } +class ResearcherModel; class WalletModel; class TxViewDelegate; class TransactionFilterProxy; @@ -28,7 +29,8 @@ class OverviewPage : public QWidget explicit OverviewPage(QWidget *parent = 0); ~OverviewPage(); - void setModel(WalletModel *model); + void setResearcherModel(ResearcherModel *model); + void setWalletModel(WalletModel *model); void showOutOfSyncWarning(bool fShow); void updateglobalstatus(); void UpdateBoincUtilization(); @@ -48,7 +50,8 @@ public slots: void updateTransactions(); Ui::OverviewPage *ui; - WalletModel *model; + ResearcherModel *researcherModel; + WalletModel *walletModel; qint64 currentBalance; qint64 currentStake; qint64 currentUnconfirmedBalance; @@ -59,6 +62,7 @@ public slots: private slots: void updateDisplayUnit(); + void updateResearcherStatus(); void handleTransactionClicked(const QModelIndex &index); void handlePollLabelClicked(); }; diff --git a/src/qt/researcher/researchermodel.cpp b/src/qt/researcher/researchermodel.cpp new file mode 100644 index 0000000000..99bdddf123 --- /dev/null +++ b/src/qt/researcher/researchermodel.cpp @@ -0,0 +1,291 @@ +#include "base58.h" +#include "bitcoinunits.h" +#include "guiutil.h" +#include "neuralnet/beacon.h" +#include "neuralnet/magnitude.h" +#include "neuralnet/researcher.h" +#include "researchermodel.h" +#include "ui_interface.h" + +#include +#include + +using namespace NN; + +extern CCriticalSection cs_main; +extern std::string msMiningErrors; + +namespace { +constexpr double SECONDS_IN_DAY = 24.0 * 60.0 * 60.0; +constexpr int64_t BEACON_RENEWAL_WARNING_AGE = Beacon::MAX_AGE - (15 * SECONDS_IN_DAY); + +//! +//! \brief Model callback bound to the \c ResearcherChanged core signal. +//! +void ResearcherChanged(ResearcherModel* model) +{ + LogPrintf("ResearcherChanged()"); + QMetaObject::invokeMethod( + model, + "resetResearcher", + Qt::QueuedConnection, + Q_ARG(NN::ResearcherPtr, Researcher::Get())); +} + +//! +//! \brief Model callback bound to the \c BeaconChanged core signal. +//! +void BeaconChanged(ResearcherModel* model) +{ + LogPrintf("BeaconChanged()"); + QMetaObject::invokeMethod(model, "updateBeacon", Qt::QueuedConnection); +} + +//! +//! \brief Convert a beacon advertisement error to a beacon status. +//! +//! \param error Beacon advertisement error provided the researcher API. +//! +//! \return Describes the advertisement error as a beacon status. +//! +BeaconStatus MapAdvertiseBeaconError(const BeaconError error) +{ + switch (error) { + case BeaconError::NONE: return BeaconStatus::ACTIVE; + case BeaconError::INSUFFICIENT_FUNDS: return BeaconStatus::ERROR_INSUFFICIENT_FUNDS; + case BeaconError::MISSING_KEY: return BeaconStatus::ERROR_MISSING_KEY; + case BeaconError::NO_CPID: return BeaconStatus::NO_CPID; + case BeaconError::NOT_NEEDED: return BeaconStatus::ACTIVE; + case BeaconError::TOO_SOON: return BeaconStatus::PENDING; + case BeaconError::TX_FAILED: return BeaconStatus::ERROR_TX_FAILED; + case BeaconError::WALLET_LOCKED: return BeaconStatus::ERROR_WALLET_LOCKED; + } + + return BeaconStatus::UNKNOWN; +}; +} // anonymous namespace + +// ----------------------------------------------------------------------------- +// Class: ResearcherModel +// ----------------------------------------------------------------------------- + +ResearcherModel::ResearcherModel() +{ + qRegisterMetaType("NN::ResearcherPtr"); + + resetResearcher(Researcher::Get()); + + if (NN::Researcher::ConfiguredForInvestorMode()) { + m_configured_for_investor_mode = true; + return; + } + + m_configured_for_investor_mode = false; + + subscribeToCoreSignals(); + + QTimer *refresh_timer = new QTimer(this); + connect(refresh_timer, SIGNAL(timeout()), this, SLOT(refresh())); + refresh_timer->start(30 * 1000); +} + +ResearcherModel::~ResearcherModel() +{ + unsubscribeFromCoreSignals(); +} + +bool ResearcherModel::configuredForInvestorMode() const +{ + return m_configured_for_investor_mode; +} + +QString ResearcherModel::formatCpid() const +{ + return QString::fromStdString(m_researcher->Id().ToString()); +} + +QString ResearcherModel::formatMagnitude() const +{ + return QString::fromStdString(m_researcher->Magnitude().ToString()); +} + +QString ResearcherModel::formatAccrual(const int display_unit) const +{ + return BitcoinUnits::formatWithUnit(display_unit, m_researcher->Accrual()); +} + +QString ResearcherModel::formatStatus() const +{ + if (configuredForInvestorMode()) { + return tr("Researcher mode disabled by configuration"); + } + + // TODO: The getmininginfo RPC shares this global. Refactor to remove it: + return QString::fromStdString(msMiningErrors); +} + +BeaconStatus ResearcherModel::getBeaconStatus() const +{ + return m_beacon_status; +} + +QString ResearcherModel::formatBeaconStatus() const +{ + switch (m_beacon_status) { + case BeaconStatus::ACTIVE: + return tr("Beacon is active."); + case BeaconStatus::ERROR_INSUFFICIENT_FUNDS: + return tr("Balance too low to send a beacon contract."); + case BeaconStatus::ERROR_MISSING_KEY: + return tr("Beacon private key missing or invalid."); + case BeaconStatus::ERROR_TX_FAILED: + return tr("Unable to send beacon transaction. See debug.log"); + case BeaconStatus::ERROR_WALLET_LOCKED: + return tr("Unlock wallet fully to send a beacon transaction."); + case BeaconStatus::NO_BEACON: + return tr("No active beacon."); + case BeaconStatus::NO_CPID: + return tr("No CPID detected."); + case BeaconStatus::NO_MAGNITUDE: + return tr("Zero magnitude in the last superblock."); + case BeaconStatus::PENDING: + return tr("Pending beacon is awaiting network confirmation."); + case BeaconStatus::RENEWAL_NEEDED: + return tr("Beacon expires soon. Renew immediately."); + case BeaconStatus::RENEWAL_POSSIBLE: + return tr("Beacon eligible for renewal."); + case BeaconStatus::UNKNOWN: + return tr("Waiting for data."); + } + + return tr("Waiting for data."); +} + +QIcon ResearcherModel::getBeaconStatusIcon() const +{ + constexpr char success[] = ":/icons/beacon_green"; + constexpr char warning[] = ":/icons/beacon_yellow"; + constexpr char danger[] = ":/icons/beacon_red"; + constexpr char inactive[] = ":/icons/beacon_grey"; + + switch (m_beacon_status) { + case BeaconStatus::ACTIVE: return QIcon(success); + case BeaconStatus::ERROR_INSUFFICIENT_FUNDS: return QIcon(danger); + case BeaconStatus::ERROR_MISSING_KEY: return QIcon(danger); + case BeaconStatus::ERROR_TX_FAILED: return QIcon(danger); + case BeaconStatus::ERROR_WALLET_LOCKED: return QIcon(danger); + case BeaconStatus::NO_BEACON: return QIcon(danger); + case BeaconStatus::NO_CPID: return QIcon(inactive); + case BeaconStatus::NO_MAGNITUDE: return QIcon(warning); + case BeaconStatus::PENDING: return QIcon(warning); + case BeaconStatus::RENEWAL_NEEDED: return QIcon(danger); + case BeaconStatus::RENEWAL_POSSIBLE: return QIcon(warning); + case BeaconStatus::UNKNOWN: return QIcon(inactive); + } + + return QIcon(inactive); +} + +QString ResearcherModel::formatBeaconAge() const +{ + if (!m_beacon) { + return QString(); + } + + return GUIUtil::formatDurationStr(m_beacon->Age(GetAdjustedTime())); +} + +QString ResearcherModel::formatTimeToBeaconExpiration() const +{ + if (!m_beacon) { + return QString(); + } + + return GUIUtil::formatDurationStr(Beacon::MAX_AGE - m_beacon->Age(GetAdjustedTime())); +} + +QString ResearcherModel::formatBeaconAddress() const +{ + if (!m_beacon) { + return QString(); + } + + return QString::fromStdString(m_beacon->GetAddress().ToString()); +} + +void ResearcherModel::refresh() +{ + updateBeacon(); + + emit accrualChanged(); +} + +void ResearcherModel::resetResearcher(ResearcherPtr researcher) +{ + m_researcher = std::move(researcher); + + emit researcherChanged(); + + updateBeacon(); +} + +void ResearcherModel::updateBeacon() +{ + const CpidOption cpid = m_researcher->Id().TryCpid(); + + if (!cpid) { + m_beacon.reset(nullptr); + m_beacon_status = BeaconStatus::NO_CPID; + + emit beaconChanged(); + + return; + } + + { + LOCK(cs_main); + const BeaconOption beacon = GetBeaconRegistry().Try(*cpid); + + if (!beacon) { + m_beacon.reset(nullptr); + m_beacon_status = BeaconStatus::NO_BEACON; + + emit beaconChanged(); + + return; + } + + m_beacon.reset(new Beacon(*beacon)); + m_beacon_status = MapAdvertiseBeaconError(m_researcher->BeaconError()); + } + + if (m_beacon_status == BeaconStatus::ACTIVE) { + const int64_t now = GetAdjustedTime(); + + if (m_beacon->Age(now) >= BEACON_RENEWAL_WARNING_AGE) { + m_beacon_status = BeaconStatus::RENEWAL_NEEDED; + } else if (m_beacon->Renewable(now)) { + m_beacon_status = BeaconStatus::RENEWAL_POSSIBLE; + } else if (m_researcher->Magnitude() == 0) { + m_beacon_status = BeaconStatus::NO_MAGNITUDE; + } else { + m_beacon_status = BeaconStatus::ACTIVE; + } + } + + emit beaconChanged(); +} + +void ResearcherModel::subscribeToCoreSignals() +{ + // Connect signals to client + uiInterface.ResearcherChanged.connect(boost::bind(ResearcherChanged, this)); + uiInterface.ResearcherChanged.connect(boost::bind(BeaconChanged, this)); +} + +void ResearcherModel::unsubscribeFromCoreSignals() +{ + // Disconnect signals from client + uiInterface.ResearcherChanged.disconnect(boost::bind(ResearcherChanged, this)); + uiInterface.ResearcherChanged.disconnect(boost::bind(BeaconChanged, this)); +} diff --git a/src/qt/researcher/researchermodel.h b/src/qt/researcher/researchermodel.h new file mode 100644 index 0000000000..8519516a3c --- /dev/null +++ b/src/qt/researcher/researchermodel.h @@ -0,0 +1,82 @@ +#ifndef RESEARCHERMODEL_H +#define RESEARCHERMODEL_H + +#include +#include + +class QIcon; + +namespace NN { +class Beacon; +class Researcher; + +//! +//! \brief A smart pointer around the global BOINC researcher context. +//! +typedef std::shared_ptr ResearcherPtr; +} + +//! +//! \brief Describes the researcher's current beacon status. +//! +enum class BeaconStatus +{ + ACTIVE, + ERROR_INSUFFICIENT_FUNDS, + ERROR_MISSING_KEY, + ERROR_TX_FAILED, + ERROR_WALLET_LOCKED, + NO_BEACON, + NO_CPID, + NO_MAGNITUDE, + PENDING, + RENEWAL_NEEDED, + RENEWAL_POSSIBLE, + UNKNOWN, +}; + +//! +//! \brief Presents researcher context state for UI components. +//! +class ResearcherModel : public QObject +{ + Q_OBJECT + +public: + ResearcherModel(); + ~ResearcherModel(); + + bool configuredForInvestorMode() const; + QString formatCpid() const; + QString formatMagnitude() const; + QString formatAccrual(const int display_unit) const; + QString formatStatus() const; + + BeaconStatus getBeaconStatus() const; + QIcon getBeaconStatusIcon() const; + QString formatBeaconStatus() const; + QString formatBeaconAge() const; + QString formatTimeToBeaconExpiration() const; + QString formatBeaconAddress() const; + +private: + NN::ResearcherPtr m_researcher; + std::unique_ptr m_beacon; + BeaconStatus m_beacon_status; + bool m_configured_for_investor_mode; + + void subscribeToCoreSignals(); + void unsubscribeFromCoreSignals(); + +signals: + void researcherChanged(); + void beaconChanged(); + void accrualChanged(); + +public slots: + void refresh(); + void resetResearcher(NN::ResearcherPtr researcher); + void updateBeacon(); +}; + +#endif // RESEARCHERMODEL_H diff --git a/src/ui_interface.h b/src/ui_interface.h index 281f9f8ac7..eccf966108 100644 --- a/src/ui_interface.h +++ b/src/ui_interface.h @@ -99,6 +99,12 @@ class CClientUIInterface /** Ban list changed. */ boost::signals2::signal BannedListChanged; + /** Researcher context changed */ + boost::signals2::signal ResearcherChanged; + + /** Beacon changed */ + boost::signals2::signal BeaconChanged; + /** * New, updated or cancelled alert. * @note called with lock cs_mapAlerts held. From ef87b5512e2c7898546a7b2cc35d03f8268fe6f2 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:40:54 -0500 Subject: [PATCH 07/22] Tweak design and layout of sections on GUI overview page This splits the chunk of non-balance fields into more logical sections for staking information and researcher context. --- src/qt/forms/overviewpage.ui | 163 ++++++++++++++----- src/qt/overviewpage.cpp | 4 +- src/qt/res/stylesheets/dark_stylesheet.qss | 64 +++----- src/qt/res/stylesheets/light_stylesheet.qss | 78 ++++----- src/qt/res/stylesheets/native_stylesheet.qss | 34 ++-- 5 files changed, 193 insertions(+), 150 deletions(-) diff --git a/src/qt/forms/overviewpage.ui b/src/qt/forms/overviewpage.ui index db277d8a7f..7de6feccd6 100644 --- a/src/qt/forms/overviewpage.ui +++ b/src/qt/forms/overviewpage.ui @@ -203,33 +203,14 @@ - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Qt::Horizontal - - - - + Total: - + Your current total balance @@ -255,17 +236,40 @@ 20 - 40 + 20 - - - Qt::Horizontal + + + 7 - + + + + Staking + + + Qt::PlainText + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + @@ -336,29 +340,94 @@ - - + + - Magnitude: + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + 7 + + + - CPID: + Researcher + + + Qt::PlainText + + + + + + + (action needed) + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + QFormLayout::AllNonFixedFieldsGrow + + + 12 + + + 12 + + Status: - - + + @@ -367,17 +436,14 @@ - - + + - - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + CPID: - + @@ -387,8 +453,15 @@ - - + + + + Magnitude: + + + + + @@ -413,7 +486,7 @@ 20 - 40 + 20 diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index 76dab09ebc..40890dc9f4 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -273,9 +273,9 @@ void OverviewPage::updateResearcherStatus() return; } - ui->magnitudeLabel->setText(researcherModel->formatMagnitude()); - ui->cpidLabel->setText(researcherModel->formatCpid()); ui->statusLabel->setText(researcherModel->formatStatus()); + ui->cpidLabel->setText(researcherModel->formatCpid()); + ui->magnitudeLabel->setText(researcherModel->formatMagnitude()); } void OverviewPage::showOutOfSyncWarning(bool fShow) diff --git a/src/qt/res/stylesheets/dark_stylesheet.qss b/src/qt/res/stylesheets/dark_stylesheet.qss index 16c4d95a91..ba84d21fcd 100644 --- a/src/qt/res/stylesheets/dark_stylesheet.qss +++ b/src/qt/res/stylesheets/dark_stylesheet.qss @@ -136,7 +136,7 @@ QScrollBar::sub-line:vertical { border: none; background-color: rgb(49,54,59); width: 0; - height: 0; + height: 0; } QToolTip { @@ -146,10 +146,10 @@ QToolTip { } QTableView{ - background-color: rgb(49,54,59); + background-color: rgb(49,54,59); color:white; alternate-background-color:rgb(35,38,41); -} +} QTableView::item{ background-color: transparent; @@ -170,23 +170,23 @@ QHeaderView{ background-color: rgb(64,68,82); } -QHeaderView::section { +QHeaderView::section { background-color:rgb(64,68,82); color:white; -} +} QHeaderView::section:hover { background-color:rgb(65,0,127); color:white; } -QComboBox { +QComboBox { background-color:rgb(35,38,41); color: white; selection-color: white; selection-background-color:rgb(65,0,127); padding: 0.065em 0em 0.065em 0.19em; -} +} QComboBox:!editable:hover, QComboBox::drop-down:!editable:hover @@ -215,17 +215,17 @@ QAbstractItemView::item:checked { color: white; } -QLineEdit { +QLineEdit { background-color:rgb(35,38,41); color: white; selection-color: white; selection-background-color:rgb(65,0,127); -} +} -QDateTimeEdit { +QDateTimeEdit { background-color:rgb(35,38,41); color: white; -} +} QDateTimeEdit QAbstractItemView { background-color:rgb(49,54,59); @@ -275,12 +275,12 @@ QPushButton:focus{ } QTreeWidget{ - color: rgb(255,255,255); + color: rgb(255,255,255); background-color: rgb(49,54,59); alternate-background-color: rgb(35,38,41); } -QListView { +QListView { background: rgb(49,54,59); alternate-background-color: rgb(35,38,41); } @@ -305,12 +305,12 @@ QDoubleSpinBox{ } QTabWidget{ - background-color: rgb(49,54,59); + background-color: rgb(49,54,59); } QTabWidget::pane { /* The tab widget frame */ border: 0.065em solid rgb(100,100,100); -} +} QTabBar::tab { background: rgb(64,68,82); @@ -350,7 +350,7 @@ QGroupBox{ QListWidget{ color:white; background-color:rgb(49,54,59); -} +} QRadioButton{ color: white; @@ -440,33 +440,23 @@ QToolBar#toolbar3 QLabel{ /* overview page */ -#overviewWalletLabel{ - font-size:16pt; - font-weight:bold; -} - -#availableLabel{ - font-weight:bold; -} - -#immatureTextLabel{ +#overviewWalletLabel, +#researcherHeaderLabel, +#stakingHeaderLabel, +#recentTransLabel { + font-size:15pt; font-weight:bold; } -#totalBalanceLabel{ +#availableLabel, +#immatureTextLabel, +#totalBalanceLabel { font-weight:bold; } -#recentTransLabel{ - font-size:16pt; - font-weight:bold; -} - -#walletStatusLabel{ - color: red; -} - -#transactionsStatusLabel{ +#walletStatusLabel, +#transactionsStatusLabel, +#researcherWarningLabel { color: red; } diff --git a/src/qt/res/stylesheets/light_stylesheet.qss b/src/qt/res/stylesheets/light_stylesheet.qss index 1d976a3fed..7f0df03f8b 100644 --- a/src/qt/res/stylesheets/light_stylesheet.qss +++ b/src/qt/res/stylesheets/light_stylesheet.qss @@ -136,7 +136,7 @@ QScrollBar::sub-line:vertical { border: none; background-color: rgb(240,240,240); width: 0; - height: 0; + height: 0; } QToolTip { @@ -147,10 +147,10 @@ QToolTip { QTableView{ - background-color: rgb(240,240,240); - color:black; + background-color: rgb(240,240,240); + color:black; alternate-background-color:white; -} +} QTableView::item{ background-color: transparent; @@ -171,23 +171,23 @@ QHeaderView{ background-color: rgb(220,220,220); } -QHeaderView::section { +QHeaderView::section { background-color:rgb(220,220,220); - color:black; -} + color:black; +} QHeaderView::section:hover { background-color:rgb(65,0,127); color:white; } -QComboBox { +QComboBox { background-color:white; - color: black; + color: black; selection-color: white; selection-background-color:rgb(65,0,127); padding: 0.065em 0em 0.065em 0.19em; -} +} QComboBox:!editable:hover, QComboBox::drop-down:!editable:hover @@ -203,7 +203,7 @@ QComboBox:selected { QComboBox QAbstractItemView { background-color:white; - color: black; + color: black; } QAbstractItemView::item:selected { @@ -216,17 +216,17 @@ QAbstractItemView::item:checked { color: white; } -QLineEdit { +QLineEdit { background-color:white; - color: black; + color: black; selection-color: white; selection-background-color:rgb(65,0,127); -} +} -QDateTimeEdit { +QDateTimeEdit { background-color:white; - color: black; -} + color: black; +} QDateTimeEdit QAbstractItemView { background-color:white; @@ -276,12 +276,12 @@ QPushButton:focus{ } QTreeWidget{ - color: rgb(0,0,0); + color: rgb(0,0,0); background-color: rgb(240,240,240); alternate-background-color: white; } -QListView { +QListView { background: rgb(240,240,240); alternate-background-color: white; } @@ -306,12 +306,12 @@ QDoubleSpinBox{ } QTabWidget{ - background-color: rgb(240,240,240); + background-color: rgb(240,240,240); } QTabWidget::pane { /* The tab widget frame */ border: 0.065em solid rgb(100,100,100); -} +} QTabBar::tab { background: rgb(200,200,200); @@ -349,9 +349,9 @@ QGroupBox{ } QListWidget{ - color:black; + color:black; background-color:rgb(240,240,240); -} +} /* Main Window*/ @@ -431,33 +431,23 @@ QToolBar#toolbar3 QLabel{ /* overview page */ -#overviewWalletLabel{ - font-size:16pt; - font-weight:bold; -} - -#availableLabel{ - font-weight:bold; -} - -#immatureTextLabel{ - font-weight:bold; -} - -#totalBalanceLabel{ +#overviewWalletLabel, +#researcherHeaderLabel, +#stakingHeaderLabel, +#recentTransLabel { + font-size:15pt; font-weight:bold; } -#recentTransLabel{ - font-size:16pt; +#availableLabel, +#immatureTextLabel, +#totalBalanceLabel { font-weight:bold; } -#walletStatusLabel{ - color: red; -} - -#transactionsStatusLabel{ +#walletStatusLabel, +#transactionsStatusLabel, +#researcherWarningLabel { color: red; } diff --git a/src/qt/res/stylesheets/native_stylesheet.qss b/src/qt/res/stylesheets/native_stylesheet.qss index b96e7fd38e..4a4915db5e 100644 --- a/src/qt/res/stylesheets/native_stylesheet.qss +++ b/src/qt/res/stylesheets/native_stylesheet.qss @@ -50,7 +50,7 @@ QToolBar#toolbar3 QLabel{ border:none; } -#listTransactions{ +#listTransactions{ background-color:transparent; } @@ -80,33 +80,23 @@ QToolBar#toolbar3 QLabel{ /* overview page */ -#overviewWalletLabel{ - font-size:16pt; +#overviewWalletLabel, +#researcherHeaderLabel, +#stakingHeaderLabel, +#recentTransLabel { + font-size:15pt; font-weight:bold; } -#availableLabel{ +#availableLabel, +#immatureTextLabel, +#totalBalanceLabel { font-weight:bold; } -#immatureTextLabel{ - font-weight:bold; -} - -#totalBalanceLabel{ - font-weight:bold; -} - -#recentTransLabel{ - font-size:16pt; - font-weight:bold; -} - -#walletStatusLabel{ - color: red; -} - -#transactionsStatusLabel{ +#walletStatusLabel, +#transactionsStatusLabel, +#researcherWarningLabel { color: red; } From b585be9516a52c6f3b8c124f2cf56a8bc5ed0239 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:40:57 -0500 Subject: [PATCH 08/22] Add pending research reward accrual to GUI overview page This adds a field to the researcher context section of the overview page that displays the CPID's accrued research rewards. --- src/qt/forms/overviewpage.ui | 17 +++++++++++++++++ src/qt/overviewpage.cpp | 17 +++++++++++++++-- src/qt/overviewpage.h | 1 + 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/qt/forms/overviewpage.ui b/src/qt/forms/overviewpage.ui index 7de6feccd6..a10173056b 100644 --- a/src/qt/forms/overviewpage.ui +++ b/src/qt/forms/overviewpage.ui @@ -470,6 +470,23 @@ + + + + Pending Accrual: + + + + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index 40890dc9f4..d9ee003e04 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -223,6 +223,7 @@ void OverviewPage::setResearcherModel(ResearcherModel *researcherModel) updateResearcherStatus(); connect(researcherModel, SIGNAL(researcherChanged()), this, SLOT(updateResearcherStatus())); + connect(researcherModel, SIGNAL(accrualChanged()), this, SLOT(updatePendingAccrual())); } void OverviewPage::setWalletModel(WalletModel *model) @@ -264,6 +265,7 @@ void OverviewPage::updateDisplayUnit() txdelegate->unit = walletModel->getOptionsModel()->getDisplayUnit(); ui->listTransactions->update(); + updatePendingAccrual(); } } @@ -276,6 +278,19 @@ void OverviewPage::updateResearcherStatus() ui->statusLabel->setText(researcherModel->formatStatus()); ui->cpidLabel->setText(researcherModel->formatCpid()); ui->magnitudeLabel->setText(researcherModel->formatMagnitude()); + + updatePendingAccrual(); +} + +void OverviewPage::updatePendingAccrual() +{ + if (!researcherModel) { + return; + } + + const int unit = walletModel->getOptionsModel()->getDisplayUnit(); + + ui->accrualLabel->setText(researcherModel->formatAccrual(unit)); } void OverviewPage::showOutOfSyncWarning(bool fShow) @@ -287,7 +302,5 @@ void OverviewPage::showOutOfSyncWarning(bool fShow) void OverviewPage::updateglobalstatus() { - OverviewPage::UpdateBoincUtilization(); } - diff --git a/src/qt/overviewpage.h b/src/qt/overviewpage.h index db52abed02..1c9dcd98e6 100644 --- a/src/qt/overviewpage.h +++ b/src/qt/overviewpage.h @@ -63,6 +63,7 @@ public slots: private slots: void updateDisplayUnit(); void updateResearcherStatus(); + void updatePendingAccrual(); void handleTransactionClicked(const QModelIndex &index); void handlePollLabelClicked(); }; From 2c1d1f6952a1d8a2edc2c48974c341fba0bfc509 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:41:00 -0500 Subject: [PATCH 09/22] Fix GUI button focus state styles for light/dark themes The focus pseudo-state styles broke the contextual appearance of the buttons and collapsed the internal button layout in some components. --- src/qt/res/stylesheets/dark_stylesheet.qss | 10 ---------- src/qt/res/stylesheets/light_stylesheet.qss | 10 ---------- 2 files changed, 20 deletions(-) diff --git a/src/qt/res/stylesheets/dark_stylesheet.qss b/src/qt/res/stylesheets/dark_stylesheet.qss index ba84d21fcd..ace8239465 100644 --- a/src/qt/res/stylesheets/dark_stylesheet.qss +++ b/src/qt/res/stylesheets/dark_stylesheet.qss @@ -250,10 +250,6 @@ QWidget{ color: white; } -QWidget:focus{ - border: 0.065em solid rgb(65,0,127); -} - QPushButton{ background-color: rgb(64,68,82); } @@ -268,12 +264,6 @@ QPushButton:hover{ color: white; } -QPushButton:focus{ - background-color: rgb(49,54,59); - color: white; - border: 0.065em solid rgb(65,0,127); -} - QTreeWidget{ color: rgb(255,255,255); background-color: rgb(49,54,59); diff --git a/src/qt/res/stylesheets/light_stylesheet.qss b/src/qt/res/stylesheets/light_stylesheet.qss index 7f0df03f8b..b6fbc6da59 100644 --- a/src/qt/res/stylesheets/light_stylesheet.qss +++ b/src/qt/res/stylesheets/light_stylesheet.qss @@ -251,10 +251,6 @@ QWidget{ color: black; } -QWidget:focus{ - border: 0.065em solid rgb(65,0,127); -} - QPushButton{ background-color: rgb(230,230,230); } @@ -269,12 +265,6 @@ QPushButton:hover{ color: white; } -QPushButton:focus{ - background-color: rgb(240,240,240); - color: black; - border: 0.065em solid rgb(65,0,127); -} - QTreeWidget{ color: rgb(0,0,0); background-color: rgb(240,240,240); From 93d859dbc9af6f6edce45b6aa913a91db297faa0 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:41:03 -0500 Subject: [PATCH 10/22] Add API for updating a researcher's email address This adds the ability to update the BOINC email address configuration at runtime. It provides for GUI or RPC functionality that manages the BOINC researcher context as a convenience for the user. --- src/neuralnet/researcher.cpp | 69 ++++++++++++++++++++++++++++++++++++ src/neuralnet/researcher.h | 14 ++++++++ 2 files changed, 83 insertions(+) diff --git a/src/neuralnet/researcher.cpp b/src/neuralnet/researcher.cpp index 5ed445862b..de4b94b61f 100644 --- a/src/neuralnet/researcher.cpp +++ b/src/neuralnet/researcher.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -33,6 +34,52 @@ namespace { //! ResearcherPtr researcher = std::make_shared(); +//! +//! \brief Rewrite the email directive in the configuration file. +//! +//! \param email The email address to update the directive to. +//! +//! \return \c false if a filesystem error occurs. +//! +bool RewriteConfigurationFileEmail(const std::string& email) +{ + const fs::path config_file_path = GetConfigFile(); + std::string out = strprintf("email=%s\n", email); + + try { + fsbridge::ifstream config_file_in(config_file_path); + std::string line; + + LOCK(cs_main); + + while (std::getline(config_file_in, line)) { + if (!boost::starts_with(line, "email=")) { + out += line; + out += "\n"; + } + } + + config_file_in.close(); + } catch (const std::exception& e) { + error("%s: Failed to read config file: %s", __func__, e.what()); + return false; + } + + try { + fsbridge::ofstream config_file_out(config_file_path); + + LOCK(cs_main); + + config_file_out << out; + config_file_out.close(); + } catch (const std::exception& e) { + error("%s: Failed to write config file: %s", __func__, e.what()); + return false; + } + + return true; +} + //! //! \brief Convert a project name to lowercase change any underscores to spaces. //! @@ -925,6 +972,28 @@ NN::BeaconError Researcher::BeaconError() const return m_beacon_error; } +bool Researcher::UpdateEmail(std::string email) +{ + boost::to_lower(email); + + if (email == Email()) { + return true; + } + + if (!RewriteConfigurationFileEmail(email)) { + return false; + } + + { + LOCK(cs_main); + ForceSetArg("-email", email); + } + + Reload(); + + return true; +} + AdvertiseBeaconResult Researcher::AdvertiseBeacon() { AssertLockHeld(cs_main); diff --git a/src/neuralnet/researcher.h b/src/neuralnet/researcher.h index f3774d1b7f..0bf8637ffb 100644 --- a/src/neuralnet/researcher.h +++ b/src/neuralnet/researcher.h @@ -426,6 +426,20 @@ class Researcher //! NN::BeaconError BeaconError() const; + //! + //! \brief Update the node's BOINC account email address used to detect + //! whitelisted projects from a BOINC installation. + //! + //! This method rewrites the configuration file for the new email address, + //! re-reads local BOINC projects, and reloads the researcher context. + //! + //! \param email The email address to update the directive to. + //! + //! \return \c false if a filesystem error occurs while rewriting the + //! configuration file. + //! + bool UpdateEmail(std::string email); + //! //! \brief Submit a beacon contract to the network for the current CPID. //! From dbb34a8d9a691d28a14b1896eed903c9489410e8 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:41:06 -0500 Subject: [PATCH 11/22] Optimize researcher context refresh hook Instead of refreshing the researcher context after each contract, do it when we're finished processing the blocks. --- src/main.cpp | 1 + src/neuralnet/contract/contract.cpp | 4 +++- src/neuralnet/researcher.cpp | 22 +++++++++++++++++++--- src/neuralnet/researcher.h | 14 ++++++++++++++ src/test/neuralnet/researcher_tests.cpp | 6 ++++++ 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 4dd69dcd96..adbe4a7bc1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2743,6 +2743,7 @@ bool GridcoinConnectBlock( } NN::Tally::RecordRewardBlock(pindex); + NN::Researcher::Refresh(); return true; } diff --git a/src/neuralnet/contract/contract.cpp b/src/neuralnet/contract/contract.cpp index 4bc7518f1b..df1c4102ad 100644 --- a/src/neuralnet/contract/contract.cpp +++ b/src/neuralnet/contract/contract.cpp @@ -350,6 +350,8 @@ void NN::ReplayContracts(const CBlockIndex* pindex) block.GetBlockTime()); } } + + NN::Researcher::Refresh(); } void NN::ApplyContracts(const CBlock& block, bool& found_contract) @@ -388,7 +390,7 @@ void NN::ApplyContracts(const std::vector& contracts) { // Rescan in-memory project CPIDs to resolve a primary CPID // that fits the now active team requirement settings: - NN::Researcher::Refresh(); + NN::Researcher::MarkDirty(); } } diff --git a/src/neuralnet/researcher.cpp b/src/neuralnet/researcher.cpp index de4b94b61f..6da37391fb 100644 --- a/src/neuralnet/researcher.cpp +++ b/src/neuralnet/researcher.cpp @@ -32,7 +32,12 @@ namespace { //! //! \brief Global BOINC researcher mining context. //! -ResearcherPtr researcher = std::make_shared(); +ResearcherPtr g_researcher = std::make_shared(); + +//! +//! \brief Indicates whether the researcher context needs a refresh. +//! +std::atomic g_researcher_dirty(true); //! //! \brief Rewrite the email directive in the configuration file. @@ -342,9 +347,11 @@ void StoreResearcher(Researcher context) } std::atomic_store( - &researcher, + &g_researcher, std::make_shared(std::move(context))); + g_researcher_dirty = false; + uiInterface.ResearcherChanged(); } @@ -837,7 +844,12 @@ bool Researcher::ConfiguredForInvestorMode(bool log) ResearcherPtr Researcher::Get() { - return std::atomic_load(&researcher); + return std::atomic_load(&g_researcher); +} + +void Researcher::MarkDirty() +{ + g_researcher_dirty = true; } void Researcher::Reload() @@ -891,6 +903,10 @@ void Researcher::Reload(MiningProjectMap projects, NN::BeaconError beacon_error) void Researcher::Refresh() { + if (!g_researcher_dirty) { + return; + } + const ResearcherPtr researcher = Get(); Reload(researcher->m_projects, researcher->m_beacon_error); diff --git a/src/neuralnet/researcher.h b/src/neuralnet/researcher.h index 0bf8637ffb..58432d821f 100644 --- a/src/neuralnet/researcher.h +++ b/src/neuralnet/researcher.h @@ -315,6 +315,20 @@ class Researcher //! static ResearcherPtr Get(); + //! + //! \brief Declare that the researcher context needs to be refreshed. + //! + //! When executing batches of operations that may change the validity of + //! the researcher context, it's expensive to call refresh each time one + //! of those operations induces a need update the researcher state. Call + //! this method instead to flag the researcher context for update. Then, + //! refresh the context after a batch finishes. + //! + //! For example, we may do this while processing all of the contracts in + //! transaction or when reading a series of blocks from disk. + //! + static void MarkDirty(); + //! //! \brief Reload the wallet's researcher mining context from BOINC. //! diff --git a/src/test/neuralnet/researcher_tests.cpp b/src/test/neuralnet/researcher_tests.cpp index 9abd8bcbef..793df59206 100644 --- a/src/test/neuralnet/researcher_tests.cpp +++ b/src/test/neuralnet/researcher_tests.cpp @@ -1025,6 +1025,7 @@ BOOST_AUTO_TEST_CASE(it_applies_the_team_requirement_dynamically) WriteCache(Section::PROTOCOL, "REQUIRE_TEAM_WHITELIST_MEMBERSHIP", "false", 1); // Rescan in-memory projects for previously-ineligible teams: + NN::Researcher::MarkDirty(); NN::Researcher::Refresh(); BOOST_CHECK(NN::Researcher::Get()->IsInvestor() == false); @@ -1041,6 +1042,7 @@ BOOST_AUTO_TEST_CASE(it_applies_the_team_requirement_dynamically) WriteCache(Section::PROTOCOL, "REQUIRE_TEAM_WHITELIST_MEMBERSHIP", "true", 1); // Rescan in-memory projects for previously-eligible teams: + NN::Researcher::MarkDirty(); NN::Researcher::Refresh(); BOOST_CHECK(NN::Researcher::Get()->IsInvestor() == true); @@ -1119,6 +1121,7 @@ BOOST_AUTO_TEST_CASE(it_applies_the_team_whitelist_dynamically) WriteCache(Section::PROTOCOL, "TEAM_WHITELIST", "Team 1|Team 2", 1); // Rescan in-memory projects for previously-ineligible teams: + NN::Researcher::MarkDirty(); NN::Researcher::Refresh(); if (const NN::ProjectOption project = NN::Researcher::Get()->Project("p1")) { @@ -1149,6 +1152,7 @@ BOOST_AUTO_TEST_CASE(it_applies_the_team_whitelist_dynamically) WriteCache(Section::PROTOCOL, "TEAM_WHITELIST", "", 1); // Rescan in-memory projects for previously-eligible teams: + NN::Researcher::MarkDirty(); NN::Researcher::Refresh(); if (const NN::ProjectOption project = NN::Researcher::Get()->Project("p1")) { @@ -1216,6 +1220,7 @@ BOOST_AUTO_TEST_CASE(it_ignores_the_team_whitelist_without_the_team_requirement) WriteCache(Section::PROTOCOL, "TEAM_WHITELIST", "Team 1|Team 2", 1); // Rescan in-memory projects for previously-eligible teams: + NN::Researcher::MarkDirty(); NN::Researcher::Refresh(); BOOST_CHECK(NN::Researcher::Get()->Eligible() == true); @@ -1233,6 +1238,7 @@ BOOST_AUTO_TEST_CASE(it_ignores_the_team_whitelist_without_the_team_requirement) WriteCache(Section::PROTOCOL, "REQUIRE_TEAM_WHITELIST_MEMBERSHIP", "true", 1); // Rescan in-memory projects for previously-eligible teams: + NN::Researcher::MarkDirty(); NN::Researcher::Refresh(); BOOST_CHECK(NN::Researcher::Get()->Eligible() == false); From 4ea7f534874681938168c10e76f0063aa3f2507b Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:41:10 -0500 Subject: [PATCH 12/22] Improve local BOINC project to whitelist matching This fixes the ability for the wallet to match local BOINC projects to the corresponding whitelisted projects when the project contracts have names that do not match the canonical names stored by the BOINC client in the client_state.xml file. The wallet will match projects by URL in case an exact name match fails. The URLs in the project contracts will usually contain the correct values even though the names may differ. --- src/neuralnet/researcher.cpp | 95 +++++++++++++++++- src/neuralnet/researcher.h | 25 ++++- src/rpcblockchain.cpp | 31 +++--- src/test/neuralnet/researcher_tests.cpp | 128 ++++++++++++++++++++---- 4 files changed, 241 insertions(+), 38 deletions(-) diff --git a/src/neuralnet/researcher.cpp b/src/neuralnet/researcher.cpp index 6da37391fb..ffbabed83e 100644 --- a/src/neuralnet/researcher.cpp +++ b/src/neuralnet/researcher.cpp @@ -6,9 +6,11 @@ #include "init.h" #include "neuralnet/beacon.h" #include "neuralnet/magnitude.h" +#include "neuralnet/project.h" #include "neuralnet/quorum.h" #include "neuralnet/researcher.h" #include "neuralnet/tally.h" +#include "span.h" #include "ui_interface.h" #include "util.h" @@ -104,6 +106,79 @@ std::string LowerUnderscore(std::string data) return data; } +//! +//! \brief Extract the authority component (hostname:port) from a URL. +//! +//! \param URL to extract the authority component from. +//! +//! \return A view of the URL string for the authority component. +//! +Span ParseUrlHostname(const std::string& url) +{ + const auto url_end = url.end(); + + auto domain_begin = url.begin(); + auto scheme_end = std::find(domain_begin, url_end, ':'); + + if (std::distance(scheme_end, url_end) >= 3) { + if (*++scheme_end == '/' && *++scheme_end == '/') { + domain_begin = ++scheme_end; + } + } + + auto domain_end = std::find(domain_begin, url_end, '/'); + + if (domain_end == url_end) { + domain_end = std::find(domain_begin, url_end, '?'); + } + + return Span(&*domain_begin, &*domain_end); +} + +//! +//! \brief Determine whether two URLs contain the same authority component +//! (hostname and port number). +//! +//! \param url_1 First BOINC project website URL to compare. +//! \param url_2 Second BOINC project website URL to compare. +//! +//! \return \c true if both URLs contain the same authority component. +//! +bool CompareProjectHostname(const std::string& url_1, const std::string& url_2) +{ + return ParseUrlHostname(url_1) == ParseUrlHostname(url_2); +} + +//! +//! \brief Attempt to match the local project loaded from BOINC with a project +//! on the Gridcoin whitelist. +//! +//! \param project Project loaded from BOINC to compare. +//! \param whitelist A snapshot of the current projects on the whitelist. +//! +//! \return A pointer to the whitelist project if it matches. +//! +const Project* ResolveWhitelistProject( + const MiningProject& project, + const WhitelistSnapshot& whitelist) +{ + for (const auto& whitelist_project : whitelist) { + if (project.m_name == whitelist_project.m_name) { + return &whitelist_project; + } + + // Sometimes project whitelist contracts contain a name different from + // the project name specified in client_state.xml. The URLs will often + // match in this case. We just check the authority component: + // + if (CompareProjectHostname(project.m_url, whitelist_project.m_url)) { + return &whitelist_project; + } + } + + return nullptr; +} + //! //! \brief Fetch the contents of BOINC's client_state.xml file from disk. //! @@ -598,10 +673,15 @@ std::string NN::GetPrimaryCpid() // Class: MiningProject // ----------------------------------------------------------------------------- -MiningProject::MiningProject(std::string name, Cpid cpid, std::string team) +MiningProject::MiningProject( + std::string name, + Cpid cpid, + std::string team, + std::string url) : m_name(LowerUnderscore(std::move(name))) , m_cpid(std::move(cpid)) , m_team(std::move(team)) + , m_url(std::move(url)) , m_error(Error::NONE) { boost::to_lower(m_team); @@ -612,7 +692,8 @@ MiningProject MiningProject::Parse(const std::string& xml) MiningProject project( ExtractXML(xml, "", ""), Cpid::Parse(ExtractXML(xml, "", "")), - ExtractXML(xml, "","")); + ExtractXML(xml, "", ""), + ExtractXML(xml, "", "")); if (project.m_cpid.IsZero()) { const std::string external_cpid @@ -660,6 +741,16 @@ bool MiningProject::Eligible() const return m_error == Error::NONE; } +const Project* MiningProject::TryWhitelist(const WhitelistSnapshot& whitelist) const +{ + return ResolveWhitelistProject(*this, whitelist); +} + +bool MiningProject::Whitelisted(const WhitelistSnapshot& whitelist) const +{ + return TryWhitelist(whitelist) != nullptr; +} + std::string MiningProject::ErrorMessage() const { switch (m_error) { diff --git a/src/neuralnet/researcher.h b/src/neuralnet/researcher.h index 58432d821f..d97b9acec4 100644 --- a/src/neuralnet/researcher.h +++ b/src/neuralnet/researcher.h @@ -16,6 +16,8 @@ class uint256; namespace NN { class Magnitude; +class Project; +class WhitelistSnapshot; //! //! \brief Describes the eligibility status for earning rewards as part of the @@ -56,8 +58,9 @@ struct MiningProject //! \param name Project name from the \c element. //! \param cpid External CPID parsed from the \c element. //! \param team Associated team parsed from the \c element. + //! \param url Project website URL parsed from the \c element. //! - MiningProject(std::string name, Cpid cpid, std::string team); + MiningProject(std::string name, Cpid cpid, std::string team, std::string url); //! //! \brief Initialize a MiningProject instance by parsing the project XML @@ -71,6 +74,7 @@ struct MiningProject std::string m_name; //!< Normalized project name. Cpid m_cpid; //!< CPID of the BOINC account for the project. std::string m_team; //!< Name of the team joined for the project. + std::string m_url; //!< URL of the project website. Error m_error; //!< May describe why a project is ineligible. //! @@ -81,6 +85,25 @@ struct MiningProject //! bool Eligible() const; + //! + //! \brief Attempt to resolve a matching project from the current Gridcoin + //! whitelist. + //! + //! \param whitelist A snapshot of the current whitelisted projects. + //! + //! \return A pointer to the whitelist project if it matches. + //! + const Project* TryWhitelist(const WhitelistSnapshot& whitelist) const; + + //! + //! \brief Determine whether the project is whitelisted. + //! + //! \param whitelist A snapshot of the current whitelisted projects. + //! + //! \return \c true if the project matches a project on the whitelist. + //! + bool Whitelisted(const WhitelistSnapshot& whitelist) const; + //! //! \brief Get a friendly, human-readable message that describes why the //! project is ineligible. diff --git a/src/rpcblockchain.cpp b/src/rpcblockchain.cpp index 6355cb388c..b3b9d47e8c 100644 --- a/src/rpcblockchain.cpp +++ b/src/rpcblockchain.cpp @@ -1542,30 +1542,27 @@ UniValue projects(const UniValue& params, bool fHelp) throw runtime_error( "projects\n" "\n" - "Displays information on projects in the network as well as researcher data if available\n"); + "Show the status of locally attached BOINC projects.\n"); UniValue res(UniValue::VARR); - NN::ResearcherPtr researcher = NN::Researcher::Get(); + const NN::ResearcherPtr researcher = NN::Researcher::Get(); + const NN::WhitelistSnapshot whitelist = NN::GetWhitelist().Snapshot(); - for (const auto& item : NN::GetWhitelist().Snapshot().Sorted()) - { - UniValue entry(UniValue::VOBJ); - - entry.pushKV("Project", item.DisplayName()); - entry.pushKV("URL", item.DisplayUrl()); + for (const auto& project_pair : researcher->Projects()) { + const NN::MiningProject& project = project_pair.second; - if (const NN::ProjectOption project = researcher->Project(item.m_name)) { - UniValue researcher(UniValue::VOBJ); + UniValue entry(UniValue::VOBJ); - researcher.pushKV("CPID", project->m_cpid.ToString()); - researcher.pushKV("Team", project->m_team); - researcher.pushKV("Valid for Research", project->Eligible()); + entry.pushKV("name", project.m_name); + entry.pushKV("url", project.m_url); - if (!project->Eligible()) { - researcher.pushKV("Errors", project->ErrorMessage()); - } + entry.pushKV("cpid", project.m_cpid.ToString()); + entry.pushKV("team", project.m_team); + entry.pushKV("eligible", project.Eligible()); + entry.pushKV("whitelisted", project.Whitelisted(whitelist)); - entry.pushKV("Researcher", researcher); + if (!project.Eligible()) { + entry.pushKV("error", project.ErrorMessage()); } res.push_back(entry); diff --git a/src/test/neuralnet/researcher_tests.cpp b/src/test/neuralnet/researcher_tests.cpp index 793df59206..5f5dc98440 100644 --- a/src/test/neuralnet/researcher_tests.cpp +++ b/src/test/neuralnet/researcher_tests.cpp @@ -1,6 +1,7 @@ #include "appcache.h" #include "neuralnet/beacon.h" #include "neuralnet/contract/contract.h" +#include "neuralnet/project.h" #include "neuralnet/researcher.h" #include "util.h" @@ -148,11 +149,12 @@ BOOST_AUTO_TEST_CASE(it_initializes_with_project_data) 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, }); - NN::MiningProject project("project name", expected, "team name"); + NN::MiningProject project("project name", expected, "team name", "url"); BOOST_CHECK(project.m_name == "project name"); BOOST_CHECK(project.m_cpid == expected); BOOST_CHECK(project.m_team == "team name"); + BOOST_CHECK(project.m_url == "url"); BOOST_CHECK(project.m_error == NN::MiningProject::Error::NONE); } @@ -166,6 +168,7 @@ BOOST_AUTO_TEST_CASE(it_parses_a_project_xml_string) NN::MiningProject project = NN::MiningProject::Parse( R"XML( + https://example.com/ Project Name Team Name XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -178,6 +181,7 @@ BOOST_AUTO_TEST_CASE(it_parses_a_project_xml_string) BOOST_CHECK(project.m_name == "project name"); BOOST_CHECK(project.m_cpid == cpid); BOOST_CHECK(project.m_team == "team name"); + BOOST_CHECK(project.m_url == "https://example.com/"); BOOST_CHECK(project.m_error == NN::MiningProject::Error::NONE); // Clean up: @@ -196,6 +200,7 @@ BOOST_AUTO_TEST_CASE(it_falls_back_to_compute_a_missing_external_cpid) NN::MiningProject project = NN::MiningProject::Parse( R"XML( + https://example.com/ Project Name Team Name b8b58cce3c90c7dc9f3601674202b21c @@ -209,6 +214,7 @@ BOOST_AUTO_TEST_CASE(it_falls_back_to_compute_a_missing_external_cpid) BOOST_CHECK(project.m_name == "project name"); BOOST_CHECK(project.m_cpid == cpid); BOOST_CHECK(project.m_team == "team name"); + BOOST_CHECK(project.m_url == "https://example.com/"); BOOST_CHECK(project.m_error == NN::MiningProject::Error::NONE); // Clean up: @@ -217,14 +223,14 @@ BOOST_AUTO_TEST_CASE(it_falls_back_to_compute_a_missing_external_cpid) BOOST_AUTO_TEST_CASE(it_normalizes_project_names) { - NN::MiningProject project("Project_NAME", NN::Cpid(), "team name"); + NN::MiningProject project("Project_NAME", NN::Cpid(), "team name", "url"); BOOST_CHECK(project.m_name == "project name"); } BOOST_AUTO_TEST_CASE(it_converts_team_names_to_lowercase) { - NN::MiningProject project("project name", NN::Cpid(), "TEAM NAME"); + NN::MiningProject project("project name", NN::Cpid(), "TEAM NAME", "url"); BOOST_CHECK(project.m_team == "team name"); } @@ -234,7 +240,7 @@ BOOST_AUTO_TEST_CASE(it_determines_whether_a_project_is_eligible) // Eligibility is determined by the absense of an error set while loading // the project from BOINC's client_state.xml file. - NN::MiningProject project("project name", NN::Cpid(), "team name"); + NN::MiningProject project("project name", NN::Cpid(), "team name", "url"); BOOST_CHECK(project.Eligible() == true); @@ -243,9 +249,63 @@ BOOST_AUTO_TEST_CASE(it_determines_whether_a_project_is_eligible) BOOST_CHECK(project.Eligible() == false); } +BOOST_AUTO_TEST_CASE(it_determines_whether_a_project_is_whitelisted) +{ + NN::MiningProject project("project name", NN::Cpid(), "team name", "url"); + + NN::WhitelistSnapshot s(std::make_shared(NN::ProjectList { + NN::Project("Enigma", "http://enigma.test/@", 1234567), + NN::Project("Einstein@home", "http://einsteinathome.org/@", 1234567), + })); + + BOOST_CHECK(project.Whitelisted(s) == false); + + // By name (exact): + project.m_name = "Enigma"; + BOOST_CHECK(project.Whitelisted(s) == true); + + // Invalidate the name so we can test the URL matching: + project.m_name = "project name"; + + // By URL (exact): + BOOST_CHECK(project.Whitelisted(s) == false); + project.m_url = "http://enigma.test/@"; + BOOST_CHECK(project.Whitelisted(s) == true); + + // By URL (different scheme): + project.m_url = "https://enigma.test/"; + BOOST_CHECK(project.Whitelisted(s) == true); + + // By URL (no scheme): + project.m_url = "enigma.test/"; + BOOST_CHECK(project.Whitelisted(s) == true); + + // By URL (just domain): + project.m_url = "enigma.test"; + BOOST_CHECK(project.Whitelisted(s) == true); + + // By URL (different path): + project.m_url = "enigma.test/boincitty-boinc-boinc"; + BOOST_CHECK(project.Whitelisted(s) == true); + + // By URL (query parameter): + project.m_url = "enigma.test?boincitty-boinc-boinc"; + BOOST_CHECK(project.Whitelisted(s) == true); + + // By URL (mix): + project.m_url = "https://enigma.test/test?boincitty-boinc-boinc"; + BOOST_CHECK(project.Whitelisted(s) == true); + + // By URL (port numbers count): + project.m_url = "https://enigma.test:8000/"; + BOOST_CHECK(project.Whitelisted(s) == false); + project.m_url = "enigma.test:8000"; + BOOST_CHECK(project.Whitelisted(s) == false); +} + BOOST_AUTO_TEST_CASE(it_formats_error_messages_for_display) { - NN::MiningProject project("project name", NN::Cpid(), "team name"); + NN::MiningProject project("project name", NN::Cpid(), "team name", "url"); BOOST_CHECK(project.ErrorMessage().empty() == true); @@ -273,8 +333,8 @@ BOOST_AUTO_TEST_CASE(it_is_iterable) { NN::MiningProjectMap projects; - projects.Set(NN::MiningProject("project name 1", NN::Cpid(), "team name")); - projects.Set(NN::MiningProject("project name 2", NN::Cpid(), "team name")); + projects.Set(NN::MiningProject("project name 1", NN::Cpid(), "team name", "url")); + projects.Set(NN::MiningProject("project name 2", NN::Cpid(), "team name", "url")); auto counter = 0; @@ -294,6 +354,7 @@ BOOST_AUTO_TEST_CASE(it_parses_a_set_of_project_xml_sections) NN::MiningProjectMap projects = NN::MiningProjectMap::Parse({ R"XML( + https://example.com/1 Project Name 1 Gridcoin XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -302,6 +363,7 @@ BOOST_AUTO_TEST_CASE(it_parses_a_set_of_project_xml_sections) )XML", R"XML( + https://example.com/2 Project Name 2 Gridcoin YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY @@ -319,6 +381,7 @@ BOOST_AUTO_TEST_CASE(it_parses_a_set_of_project_xml_sections) BOOST_CHECK(project1->m_name == "project name 1"); BOOST_CHECK(project1->m_cpid == cpid_1); BOOST_CHECK(project1->m_team == "gridcoin"); + BOOST_CHECK(project1->m_url == "https://example.com/1"); BOOST_CHECK(project1->m_error == NN::MiningProject::Error::NONE); BOOST_CHECK(project1->Eligible() == true); } else { @@ -329,6 +392,7 @@ BOOST_AUTO_TEST_CASE(it_parses_a_set_of_project_xml_sections) BOOST_CHECK(project2->m_name == "project name 2"); BOOST_CHECK(project2->m_cpid == cpid_2); BOOST_CHECK(project2->m_team == "gridcoin"); + BOOST_CHECK(project2->m_url == "https://example.com/2"); BOOST_CHECK(project2->m_error == NN::MiningProject::Error::NONE); BOOST_CHECK(project2->Eligible() == true); } else { @@ -346,6 +410,7 @@ BOOST_AUTO_TEST_CASE(it_skips_loading_project_xml_with_empty_project_names) // Empty element: R"XML( + https://example.com/ Gridcoin XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -355,6 +420,7 @@ BOOST_AUTO_TEST_CASE(it_skips_loading_project_xml_with_empty_project_names) // Missing element: R"XML( + https://example.com/ Gridcoin XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX f5d8234352e5a5ae3915debba7258294 @@ -372,8 +438,8 @@ BOOST_AUTO_TEST_CASE(it_counts_the_number_of_projects) BOOST_CHECK(projects.size() == 0); - projects.Set(NN::MiningProject("project name 1", NN::Cpid(), "team name")); - projects.Set(NN::MiningProject("project name 2", NN::Cpid(), "team name")); + projects.Set(NN::MiningProject("project name 1", NN::Cpid(), "team name", "url")); + projects.Set(NN::MiningProject("project name 2", NN::Cpid(), "team name", "url")); BOOST_CHECK(projects.size() == 2); } @@ -384,7 +450,7 @@ BOOST_AUTO_TEST_CASE(it_indicates_whether_it_contains_any_projects) BOOST_CHECK(projects.empty() == true); - projects.Set(NN::MiningProject("project name", NN::Cpid(), "team name")); + projects.Set(NN::MiningProject("project name", NN::Cpid(), "team name", "url")); BOOST_CHECK(projects.empty() == false); } @@ -393,7 +459,7 @@ BOOST_AUTO_TEST_CASE(it_fetches_a_project_by_name) { NN::MiningProjectMap projects; - projects.Set(NN::MiningProject("project name", NN::Cpid(), "team name")); + projects.Set(NN::MiningProject("project name", NN::Cpid(), "team name", "url")); BOOST_CHECK(projects.Try("project name")->m_name == "project name"); BOOST_CHECK(projects.Try("nonexistent") == nullptr); @@ -403,8 +469,8 @@ BOOST_AUTO_TEST_CASE(it_does_not_overwrite_projects_with_the_same_name) { NN::MiningProjectMap projects; - projects.Set(NN::MiningProject("project name", NN::Cpid(), "team name 1")); - projects.Set(NN::MiningProject("project name", NN::Cpid(), "team name 2")); + projects.Set(NN::MiningProject("project name", NN::Cpid(), "team name 1", "url")); + projects.Set(NN::MiningProject("project name", NN::Cpid(), "team name 2", "url")); BOOST_CHECK(projects.Try("project name")->m_team == "team name 1"); } @@ -413,9 +479,9 @@ BOOST_AUTO_TEST_CASE(it_applies_a_provided_team_whitelist) { NN::MiningProjectMap projects; - projects.Set(NN::MiningProject("project 1", NN::Cpid(), "gridcoin")); - projects.Set(NN::MiningProject("project 2", NN::Cpid(), "team 1")); - projects.Set(NN::MiningProject("project 3", NN::Cpid(), "team 2")); + projects.Set(NN::MiningProject("project 1", NN::Cpid(), "gridcoin", "url")); + projects.Set(NN::MiningProject("project 2", NN::Cpid(), "team 1", "url")); + projects.Set(NN::MiningProject("project 3", NN::Cpid(), "team 2", "url")); // Before applying a whitelist, all projects are eligible: @@ -520,7 +586,7 @@ BOOST_AUTO_TEST_CASE(it_initializes_with_researcher_context_data) { NN::MiningProjectMap expected; - expected.Set(NN::MiningProject("project name", NN::Cpid(), "team name")); + expected.Set(NN::MiningProject("project name", NN::Cpid(), "team name", "url")); NN::Researcher researcher(NN::Cpid(), expected); @@ -590,7 +656,7 @@ BOOST_AUTO_TEST_CASE(it_provides_an_overall_status_of_the_reseracher_context) BOOST_CHECK(researcher.Status() == NN::ResearcherStatus::INVESTOR); NN::MiningProjectMap projects; - projects.Set(NN::MiningProject("ineligible", NN::Cpid(), "team name")); + projects.Set(NN::MiningProject("ineligible", NN::Cpid(), "team name", "url")); researcher = NN::Researcher(NN::MiningId::ForInvestor(), projects); @@ -618,6 +684,7 @@ BOOST_AUTO_TEST_CASE(it_parses_project_xml_to_a_global_researcher_singleton) NN::Researcher::Reload(NN::MiningProjectMap::Parse({ R"XML( + https://example.com/1 Project Name 1 Gridcoin XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -626,6 +693,7 @@ BOOST_AUTO_TEST_CASE(it_parses_project_xml_to_a_global_researcher_singleton) )XML", R"XML( + https://example.com/2 Project Name 2 Gridcoin YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY @@ -647,6 +715,7 @@ BOOST_AUTO_TEST_CASE(it_parses_project_xml_to_a_global_researcher_singleton) BOOST_CHECK(project1->m_name == "project name 1"); BOOST_CHECK(project1->m_cpid == cpid_1); BOOST_CHECK(project1->m_team == "gridcoin"); + BOOST_CHECK(project1->m_url == "https://example.com/1"); BOOST_CHECK(project1->m_error == NN::MiningProject::Error::NONE); BOOST_CHECK(project1->Eligible() == true); } else { @@ -657,6 +726,7 @@ BOOST_AUTO_TEST_CASE(it_parses_project_xml_to_a_global_researcher_singleton) BOOST_CHECK(project2->m_name == "project name 2"); BOOST_CHECK(project2->m_cpid == cpid_2); BOOST_CHECK(project2->m_team == "gridcoin"); + BOOST_CHECK(project2->m_url == "https://example.com/2"); BOOST_CHECK(project2->m_error == NN::MiningProject::Error::NONE); BOOST_CHECK(project2->Eligible() == true); } else { @@ -676,6 +746,7 @@ BOOST_AUTO_TEST_CASE(it_looks_up_loaded_boinc_projects_by_name) NN::Researcher::Reload(NN::MiningProjectMap::Parse({ R"XML( + https://example.com/ Name Gridcoin XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -692,6 +763,7 @@ BOOST_AUTO_TEST_CASE(it_looks_up_loaded_boinc_projects_by_name) BOOST_CHECK(project->m_name == "name"); BOOST_CHECK(project->m_cpid == cpid); BOOST_CHECK(project->m_team == "gridcoin"); + BOOST_CHECK(project->m_url == "https://example.com/"); BOOST_CHECK(project->m_error == NN::MiningProject::Error::NONE); BOOST_CHECK(project->Eligible() == true); } else { @@ -720,6 +792,7 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml) // Required team mismatch: R"XML( + https://example.com/ Project Name 1 Not Gridcoin XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -729,6 +802,7 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml) // element missing: R"XML( + https://example.com/ Project Name 2 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX f5d8234352e5a5ae3915debba7258294 @@ -737,6 +811,7 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml) // Malformed external CPID: R"XML( + https://example.com/ Project Name 3 Gridcoin XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -746,6 +821,7 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml) // element missing: R"XML( + https://example.com/ Project Name 4 Gridcoin XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -754,6 +830,7 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml) // Mismatched external CPID to internal CPID/email address: R"XML( + https://example.com/ Project Name 5 Gridcoin invalid @@ -763,6 +840,7 @@ BOOST_AUTO_TEST_CASE(it_tags_invalid_projects_with_errors_when_parsing_xml) // element missing: R"XML( + https://example.com/ Project Name 6 Gridcoin f5d8234352e5a5ae3915debba7258294 @@ -849,6 +927,7 @@ BOOST_AUTO_TEST_CASE(it_skips_loading_project_xml_with_empty_project_names) // Empty element: R"XML( + https://example.com/ Gridcoin XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -858,6 +937,7 @@ BOOST_AUTO_TEST_CASE(it_skips_loading_project_xml_with_empty_project_names) // Missing element: R"XML( + https://example.com/ Gridcoin XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX f5d8234352e5a5ae3915debba7258294 @@ -884,6 +964,7 @@ BOOST_AUTO_TEST_CASE(it_skips_the_team_requirement_when_set_by_protocol) NN::Researcher::Reload(NN::MiningProjectMap::Parse({ R"XML( + https://example.com/ Project Name 1 ! Not Gridcoin ! XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -927,6 +1008,7 @@ BOOST_AUTO_TEST_CASE(it_applies_the_team_whitelist_when_set_by_the_protocol) NN::Researcher::Reload(NN::MiningProjectMap::Parse({ R"XML( + https://example.com/ Project Name 1 ! Not Gridcoin ! XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -935,6 +1017,7 @@ BOOST_AUTO_TEST_CASE(it_applies_the_team_whitelist_when_set_by_the_protocol) )XML", R"XML( + https://example.com/ Project Name 2 Team 1 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -943,6 +1026,7 @@ BOOST_AUTO_TEST_CASE(it_applies_the_team_whitelist_when_set_by_the_protocol) )XML", R"XML( + https://example.com/ Project Name 3 Team 2 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -1003,6 +1087,7 @@ BOOST_AUTO_TEST_CASE(it_applies_the_team_requirement_dynamically) NN::Researcher::Reload(NN::MiningProjectMap::Parse({ R"XML( + https://example.com/ name ! Not Gridcoin ! XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -1069,6 +1154,7 @@ BOOST_AUTO_TEST_CASE(it_applies_the_team_whitelist_dynamically) NN::Researcher::Reload(NN::MiningProjectMap::Parse({ R"XML( + https://example.com/ p1 Gridcoin XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -1077,6 +1163,7 @@ BOOST_AUTO_TEST_CASE(it_applies_the_team_whitelist_dynamically) )XML", R"XML( + https://example.com/ p2 Team 1 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -1085,6 +1172,7 @@ BOOST_AUTO_TEST_CASE(it_applies_the_team_whitelist_dynamically) )XML", R"XML( + https://example.com/ p3 Team 2 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -1198,6 +1286,7 @@ BOOST_AUTO_TEST_CASE(it_ignores_the_team_whitelist_without_the_team_requirement) NN::Researcher::Reload(NN::MiningProjectMap::Parse({ R"XML( + https://example.com/ p1 Gridcoin XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -1296,6 +1385,7 @@ BOOST_AUTO_TEST_CASE(it_parses_project_xml_from_a_client_state_xml_file, BOOST_CHECK(project1->m_name == "valid project 1"); BOOST_CHECK(project1->m_cpid == cpid_1); BOOST_CHECK(project1->m_team == "gridcoin"); + BOOST_CHECK(project1->m_url == "https://project1.example.com/boinc/"); BOOST_CHECK(project1->m_error == NN::MiningProject::Error::NONE); BOOST_CHECK(project1->Eligible() == true); } else { @@ -1306,6 +1396,7 @@ BOOST_AUTO_TEST_CASE(it_parses_project_xml_from_a_client_state_xml_file, BOOST_CHECK(project2->m_name == "valid project 2"); BOOST_CHECK(project2->m_cpid == cpid_2); BOOST_CHECK(project2->m_team == "gridcoin"); + BOOST_CHECK(project2->m_url == "https://project2.example.com/boinc/"); BOOST_CHECK(project2->m_error == NN::MiningProject::Error::NONE); BOOST_CHECK(project2->Eligible() == true); } else { @@ -1317,6 +1408,7 @@ BOOST_AUTO_TEST_CASE(it_parses_project_xml_from_a_client_state_xml_file, BOOST_CHECK(project3->m_name == "invalid project 3"); BOOST_CHECK(project3->m_cpid == cpid_2); BOOST_CHECK(project3->m_team == "gridcoin"); + BOOST_CHECK(project3->m_url == "https://project3.example.com/boinc/"); BOOST_CHECK(project3->m_error == NN::MiningProject::Error::MISMATCHED_CPID); BOOST_CHECK(project3->Eligible() == false); } else { From 86b66c4a2fe4545ac0407b8e4a70cb4a64d473ad Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:41:13 -0500 Subject: [PATCH 13/22] Skip beacon advertisement when already pending This avoids advertising a new beacon when a pending beacon already exists for the CPID so that the wallet doesn't generate additional keys. --- src/qt/researcher/researchermodel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qt/researcher/researchermodel.cpp b/src/qt/researcher/researchermodel.cpp index 99bdddf123..3e72383ea2 100644 --- a/src/qt/researcher/researchermodel.cpp +++ b/src/qt/researcher/researchermodel.cpp @@ -56,7 +56,7 @@ BeaconStatus MapAdvertiseBeaconError(const BeaconError error) case BeaconError::MISSING_KEY: return BeaconStatus::ERROR_MISSING_KEY; case BeaconError::NO_CPID: return BeaconStatus::NO_CPID; case BeaconError::NOT_NEEDED: return BeaconStatus::ACTIVE; - case BeaconError::TOO_SOON: return BeaconStatus::PENDING; + case BeaconError::PENDING: return BeaconStatus::PENDING; case BeaconError::TX_FAILED: return BeaconStatus::ERROR_TX_FAILED; case BeaconError::WALLET_LOCKED: return BeaconStatus::ERROR_WALLET_LOCKED; } From e42efc1234b9049d1d7f46d96e0db2c58f0df4a4 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:41:16 -0500 Subject: [PATCH 14/22] Optimize and refactor explain magnitude After the removal of the the legacy "neural network" messaging system, we no longer need to retain the old string format for explainmagnitude so we can replace it with structured data. This greatly improves speed and provides a usable API for integration with the GUI and other parts of the application. --- src/neuralnet/quorum.cpp | 44 ++++++++++++++++++ src/neuralnet/quorum.h | 35 ++++++++++++++ src/rpcblockchain.cpp | 52 ++++++++++++++------- src/rpcclient.cpp | 5 +- src/scraper/scraper.cpp | 98 ---------------------------------------- src/scraper/scraper.h | 3 +- 6 files changed, 117 insertions(+), 120 deletions(-) diff --git a/src/neuralnet/quorum.cpp b/src/neuralnet/quorum.cpp index ddf1cf2d0a..c7bbb48aed 100644 --- a/src/neuralnet/quorum.cpp +++ b/src/neuralnet/quorum.cpp @@ -1602,6 +1602,50 @@ Magnitude Quorum::GetMagnitude(const MiningId mining_id) return Magnitude::Zero(); } +std::vector Quorum::ExplainMagnitude(const Cpid cpid) +{ + // Force a scraper convergence update if needed: + // TODO: unwrap this from ScraperGetSuperblockContract() + CreateSuperblock(); + + const std::string cpid_str = cpid.ToString(); + const Span cpid_span = MakeSpan(cpid_str); + + // Compare the stats entry CPID and return the project name if it matches: + const auto try_item = [&](const std::string& object_id) { + const Span id_span = MakeSpan(object_id); + const Span cpid_subspan = id_span.last(32); + + if (cpid_subspan != cpid_span) { + return Span(); + } + + return id_span.first(id_span.size() - 33); + }; + + std::vector projects; + + LOCK(cs_ConvergedScraperStatsCache); + + // Although the map is ordered, the keys begin with project names, so we + // cannot binary search for a block of CPID entries yet: + // + for (const auto& entry : ConvergedScraperStatsCache.mScraperConvergedStats) { + if (entry.first.objecttype == statsobjecttype::byCPIDbyProject) { + const Span project_name = try_item(entry.first.objectID); + + if (project_name.size() > 0) { + projects.emplace_back( + std::string(project_name.begin(), project_name.end()), + entry.second.statsvalue.dRAC, + entry.second.statsvalue.dMag); + } + } + } + + return projects; +} + SuperblockPtr Quorum::CurrentSuperblock() { return g_superblock_index.Current(); diff --git a/src/neuralnet/quorum.h b/src/neuralnet/quorum.h index cbf7bb2b9b..92983a2d82 100644 --- a/src/neuralnet/quorum.h +++ b/src/neuralnet/quorum.h @@ -12,6 +12,31 @@ class QuorumHash; class Superblock; class SuperblockPtr; +//! +//! \brief A CPID's project magnitude record produced from scraper statistics. +//! +class ExplainMagnitudeProject +{ +public: + std::string m_name; //!< Project name. + double m_rac; //!< CPID's recent average credit for the project. + double m_magnitude; //!< CPID's magnitude for the project. + + //! + //! \brief Initialize a new project magnitude record. + //! + //! \param name Project name. + //! \param rac CPID's recent average credit for the project. + //! \param magnitude CPID's magnitude for the project. + //! + ExplainMagnitudeProject(std::string name, double rac, double magnitude) + : m_name(std::move(name)) + , m_rac(rac) + , m_magnitude(magnitude) + { + } +}; // ExplainMagnitudeProject + //! //! \brief Produces, stores, and validates superblocks. //! @@ -127,6 +152,16 @@ class Quorum //! static Magnitude GetMagnitude(const MiningId mining_id); + //! + //! \brief Generate a report from scraper statistics that contains the + //! project-level magnitude and recent average credit for a CPID. + //! + //! \param cpid CPID to generate the report for. + //! + //! \return A set of records with statistics for each project. + //! + static std::vector ExplainMagnitude(const Cpid cpid); + //! //! \brief Get a reference to the current active superblock. //! diff --git a/src/rpcblockchain.cpp b/src/rpcblockchain.cpp index b3b9d47e8c..415aa1d352 100644 --- a/src/rpcblockchain.cpp +++ b/src/rpcblockchain.cpp @@ -37,7 +37,6 @@ bool ForceReorganizeToHash(uint256 NewHash); extern UniValue MagnitudeReport(const NN::Cpid cpid); extern UniValue SuperblockReport(int lookback = 14, bool displaycontract = false, std::string cpid = ""); extern bool ScraperSynchronizeDPOR(); -std::string ExplainMagnitude(std::string sCPID); extern ScraperPendingBeaconMap GetPendingBeaconsForReport(); extern ScraperPendingBeaconMap GetVerifiedBeaconsForReport(bool from_global = false); @@ -934,34 +933,53 @@ UniValue beaconstatus(const UniValue& params, bool fHelp) UniValue explainmagnitude(const UniValue& params, bool fHelp) { - if (fHelp || params.size() > 0) + if (fHelp || params.size() > 1) throw runtime_error( - "explainmagnitude\n" + "explainmagnitude ( cpid )\n" + "\n" + "[cpid] -> Optional CPID to explain magnitude for\n" "\n" "Itemize your CPID magnitudes by project.\n"); - UniValue res(UniValue::VOBJ); - - LOCK(cs_main); + const NN::MiningId mining_id = params.size() > 0 + ? NN::MiningId::Parse(params[0].get_str()) + : NN::Researcher::Get()->Id(); - const std::string primary_cpid = NN::GetPrimaryCpid(); - std::string sNeuralResponse = ExplainMagnitude(primary_cpid); + if (!mining_id.Valid()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid CPID."); + } - if (sNeuralResponse.length() < 25) - { - res.pushKV("Neural Response", "false; Try again at a later time"); + const NN::CpidOption cpid = mining_id.TryCpid(); + if (!cpid) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "No data for investor."); } - else - { - res.pushKV("Neural Response", "true (from THIS node)"); - std::vector vMag = split(sNeuralResponse.c_str(),""); + UniValue res(UniValue::VARR); + double total_rac = 0; + double total_magnitude = 0; + + for (const auto& project : NN::Quorum::ExplainMagnitude(*cpid)) { + total_rac += project.m_rac; + total_magnitude += project.m_magnitude; - for (unsigned int i = 0; i < vMag.size(); i++) - res.pushKV(RoundToString(i+1,0),vMag[i].c_str()); + UniValue entry(UniValue::VOBJ); + + entry.pushKV("project", project.m_name); + entry.pushKV("rac", project.m_rac); + entry.pushKV("magnitude", project.m_magnitude); + + res.push_back(entry); } + UniValue total(UniValue::VOBJ); + + total.pushKV("project", "total"); + total.pushKV("rac", total_rac); + total.pushKV("magnitude", total_magnitude); + + res.push_back(total); + return res; } diff --git a/src/rpcclient.cpp b/src/rpcclient.cpp index 03a30ded7d..b6aa45ce01 100644 --- a/src/rpcclient.cpp +++ b/src/rpcclient.cpp @@ -67,12 +67,12 @@ UniValue CallRPC(const string& strMethod, const UniValue& params) // Receive HTTP reply status int nProto = 0; int nStatus = ReadHTTPStatus(stream, nProto); - + // Receive HTTP reply message headers and body map mapHeaders; string strReply; ReadHTTPMessage(stream, mapHeaders, strReply, nProto); - + if (nStatus == HTTP_UNAUTHORIZED) throw runtime_error("incorrect rpcuser or rpcpassword (authorization failed)"); else if (nStatus >= 400 && nStatus != HTTP_BAD_REQUEST && nStatus != HTTP_NOT_FOUND && nStatus != HTTP_INTERNAL_SERVER_ERROR) @@ -160,7 +160,6 @@ static const CRPCConvertParam vRPCConvertParams[] = { "walletpassphrase" , 2 }, // Mining - { "explainmagnitude" , 0 }, { "superblocks" , 0 }, { "superblocks" , 1 }, diff --git a/src/scraper/scraper.cpp b/src/scraper/scraper.cpp index 05c8e129ce..a2cfdd10f2 100755 --- a/src/scraper/scraper.cpp +++ b/src/scraper/scraper.cpp @@ -26,8 +26,6 @@ #include #include -namespace NN { std::string GetPrimaryCpid(); } - // These are initialized empty. GetDataDir() cannot be called here. It is too early. fs::path pathDataDir = {}; fs::path pathScraper = {}; @@ -3397,102 +3395,6 @@ ScraperStatsAndVerifiedBeacons GetScraperStatsFromSingleManifest(CScraperManifes return stats_and_verified_beacons; } - -std::string ExplainMagnitude(std::string sCPID) -{ - // See if converged stats/contract update needed... - bool bConvergenceUpdateNeeded = true; - { - LOCK(cs_ConvergedScraperStatsCache); - _log(logattribute::INFO, "LOCK", "cs_ConvergedScraperStatsCache"); - - - if (GetAdjustedTime() - ConvergedScraperStatsCache.nTime < (nScraperSleep / 1000) || ConvergedScraperStatsCache.bClean) - bConvergenceUpdateNeeded = false; - - // End LOCK(cs_ConvergedScraperStatsCache) - _log(logattribute::INFO, "ENDLOCK", "cs_ConvergedScraperStatsCache"); - } - - if (bConvergenceUpdateNeeded) - // Don't need the output but will use the global cache, which will be updated. - ScraperGetSuperblockContract(false, false); - - // A purposeful copy here to avoid a long-term lock. May want to change to direct reference - // and allow locking during the output. - ScraperStats mScraperConvergedStats; - { - LOCK(cs_ConvergedScraperStatsCache); - _log(logattribute::INFO, "LOCK", "cs_ConvergedScraperStatsCache"); - - mScraperConvergedStats = ConvergedScraperStatsCache.mScraperConvergedStats; - - // End LOCK(cs_ConvergedScraperStatsCache) - _log(logattribute::INFO, "ENDLOCK", "cs_ConvergedScraperStatsCache"); - } - - stringbuilder out; - - out.append("CPID,Project,CPID RAC,Project RAC,Project Mag,CPID Mag"); - - double dCPIDCumulativeRAC = 0.0; - double dCPIDCumulativeMag = 0.0; - - for (auto const& entry : mScraperConvergedStats) - { - // Only select the individual byCPIDbyProject stats for the selected CPID. - - std::size_t found = entry.first.objectID.find(sCPID); - - if (entry.first.objecttype == statsobjecttype::byCPIDbyProject && found!=std::string::npos) - { - dCPIDCumulativeRAC += entry.second.statsvalue.dRAC; - dCPIDCumulativeMag += entry.second.statsvalue.dMag; - - std::string sInput = entry.first.objectID; - - // Remove ,CPID from key objectID to obtain referenced project. - std::string sProject = sInput.erase(sInput.find("," + sCPID), sCPID.length() + 1); - - ScraperObjectStatsKey ProjectKey; - - ProjectKey.objecttype = statsobjecttype::byProject; - ProjectKey.objectID = sProject; - - auto const& iProject = mScraperConvergedStats.find(ProjectKey); - - out.append(sCPID + ","); - out.append(sProject + ","); - out.fixeddoubleappend(entry.second.statsvalue.dRAC, 2); - out.append(","); - out.fixeddoubleappend(iProject->second.statsvalue.dRAC, 2); - out.append(","); - out.fixeddoubleappend(iProject->second.statsvalue.dMag, 2); - out.append(","); - out.fixeddoubleappend(entry.second.statsvalue.dMag, 2); - out.append(""); - } - } - - // "Signature" - // The magic version number of 430 from .NET is there for compatibility with the old NN protocol. - out.append("NN Host Version: 430, "); - out.append("NeuralHash: " + ConvergedScraperStatsCache.NewFormatSuperblock.GetHash().ToString() + ", "); - out.append("SignatureCPID: " + NN::GetPrimaryCpid() + ", "); - out.append("Time: " + DateTimeStrFormat("%x %H:%M:%S", GetAdjustedTime()) + ""); - - //Totals - out.append("Total RAC: "); - out.fixeddoubleappend(dCPIDCumulativeRAC, 2); - out.append(""); - out.append("Total Mag: "); - out.fixeddoubleappend(dCPIDCumulativeMag, 2); - - return out.value(); -} - - - /*********************** * Scraper networking * ************************/ diff --git a/src/scraper/scraper.h b/src/scraper/scraper.h index ae06e80b0e..45b47e5f13 100644 --- a/src/scraper/scraper.h +++ b/src/scraper/scraper.h @@ -115,7 +115,6 @@ CCriticalSection cs_mScrapersExt; uint256 GetFileHash(const fs::path& inputfile); ScraperStatsAndVerifiedBeacons GetScraperStatsByConvergedManifest(const ConvergedManifest& StructConvergedManifest); -std::string ExplainMagnitude(std::string sCPID); bool IsScraperAuthorized(); bool IsScraperAuthorizedToBroadcastManifests(CBitcoinAddress& AddressOut, CKey& KeyOut); bool IsScraperMaximumManifestPublishingRateExceeded(int64_t& nTime, CPubKey& PubKey); @@ -159,7 +158,7 @@ double MagRound(double dMag) unsigned int NumScrapersForSupermajority(unsigned int nScraperCount) { unsigned int nRequired = std::max(SCRAPER_CONVERGENCE_MINIMUM, (unsigned int)std::ceil(SCRAPER_CONVERGENCE_RATIO * nScraperCount)); - + return nRequired; } From 3fca88309109e3aba7b88e7c7ee2e2cfca765c91 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:41:19 -0500 Subject: [PATCH 15/22] Improve local pending beacon tracking The wallet needs to remember when it advertised a beacon so that it can display the status and avoid sending another one. The blockchain is the canonical source for this information, but user-facing pieces must show context for a participant's beacon before it confirms in the chain. The changes here expand the wallet's ability to manage pending beacons. --- src/init.cpp | 3 +- src/neuralnet/beacon.cpp | 33 ++++--- src/neuralnet/beacon.h | 23 ++--- src/neuralnet/researcher.cpp | 185 +++++++++++++++++++++++++++-------- src/neuralnet/researcher.h | 20 ++++ src/rpcblockchain.cpp | 56 ++++++++--- 6 files changed, 235 insertions(+), 85 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index f8baa471eb..895e3451c0 100755 --- a/src/init.cpp +++ b/src/init.cpp @@ -1135,8 +1135,7 @@ bool AppInit2(ThreadHandlerPtr threads) NN::ReplayContracts(pindexBest); - uiInterface.InitMessage(_("Finding first applicable Research Project...")); - NN::Researcher::Reload(); + NN::Researcher::Initialize(); if (!pwalletMain->IsLocked()) NN::Researcher::Get()->ImportBeaconKeysFromConfig(pwalletMain); diff --git a/src/neuralnet/beacon.cpp b/src/neuralnet/beacon.cpp index bc5c100fe3..fd4e3763fe 100644 --- a/src/neuralnet/beacon.cpp +++ b/src/neuralnet/beacon.cpp @@ -262,6 +262,26 @@ BeaconOption BeaconRegistry::TryActive(const Cpid& cpid, const int64_t now) cons return nullptr; } +std::vector BeaconRegistry::FindPending(const Cpid cpid) const +{ + // TODO: consider adding a lookup table for pending beacons keyed by CPID. + // Since the protocol just needs to look up pending beacons by public key, + // we just do a search here. Informational RPCs or local beacon management + // only need to call this occasionally. + + std::vector found; + + for (const auto& pending_beacon_pair : m_pending) { + const PendingBeacon& beacon = pending_beacon_pair.second; + + if (beacon.m_cpid == cpid) { + found.emplace_back(&beacon); + } + } + + return found; +} + bool BeaconRegistry::ContainsActive(const Cpid& cpid, const int64_t now) const { if (const BeaconOption beacon = Try(cpid)) { @@ -276,19 +296,6 @@ bool BeaconRegistry::ContainsActive(const Cpid& cpid) const return ContainsActive(cpid, GetAdjustedTime()); } -std::vector BeaconRegistry::FindPendingKeys(const Cpid& cpid) const -{ - std::vector found; - - for (const auto& beacon_pair : m_pending) { - if (beacon_pair.second.m_cpid == cpid) { - found.emplace_back(beacon_pair.first); - } - } - - return found; -} - void BeaconRegistry::Add(Contract contract) { BeaconPayload payload = contract.CopyPayloadAs(); diff --git a/src/neuralnet/beacon.h b/src/neuralnet/beacon.h index 4b15fc88e8..a6e44c9d19 100644 --- a/src/neuralnet/beacon.h +++ b/src/neuralnet/beacon.h @@ -411,6 +411,15 @@ class BeaconRegistry : public IContractHandler //! BeaconOption TryActive(const Cpid& cpid, const int64_t now) const; + //! + //! \brief Get the set of pending beacons for the specified CPID. + //! + //! \param cpid CPID to find pending beacons for. + //! + //! \return A set of pending beacons advertised for the supplied CPID. + //! + std::vector FindPending(const Cpid cpid) const; + //! //! \brief Determine whether a beacon is active for the specified CPID. //! @@ -433,20 +442,6 @@ class BeaconRegistry : public IContractHandler //! bool ContainsActive(const Cpid& cpid) const; - //! - //! \brief Look up the key IDs of pending beacons for the specified CPID. - //! - //! The wallet matches key IDs returned by this method to determine whether - //! it contains private keys for pending beacons so that it can skip beacon - //! advertisement if it submitted one recently. - //! - //! \param cpid CPID of the beacons to find results for. - //! - //! \return The set of RIPEMD-160 hashes of the keys for the beacons that - //! match the supplied CPID. - //! - std::vector FindPendingKeys(const Cpid& cpid) const; - //! //! \brief Determine whether a beacon contract is valid. //! diff --git a/src/neuralnet/researcher.cpp b/src/neuralnet/researcher.cpp index ffbabed83e..42277319b1 100644 --- a/src/neuralnet/researcher.cpp +++ b/src/neuralnet/researcher.cpp @@ -430,6 +430,90 @@ void StoreResearcher(Researcher context) uiInterface.ResearcherChanged(); } +//! +//! \brief A piece of helper state that keeps track of recently-advertised +//! pending beacons. +//! +class RecentBeacons +{ +public: + //! + //! \brief Load the set of pending beacons that match the node's private + //! keys. + //! + //! THREAD SAFETY: Lock cs_main and pwalletMain->cs_wallet before calling + //! this method. + //! + //! \param beacons Contains the set of pending beacons to import from. + //! + void ImportRegistry(const BeaconRegistry& beacons) + { + AssertLockHeld(cs_main); + AssertLockHeld(pwalletMain->cs_wallet); + + for (const auto& pending_pair : beacons.PendingBeacons()) { + const CKeyID& key_id = pending_pair.first; + const PendingBeacon& beacon = pending_pair.second; + + if (pwalletMain->HaveKey(key_id)) { + auto iter_pair = m_pending.emplace(beacon.m_cpid, beacon); + iter_pair.first->second.m_timestamp = beacon.m_timestamp; + } + } + } + + //! + //! \brief Fetch the pending beacon for the specified CPID if it exists. + //! + //! \param cpid CPID that the beacon was advertised for. + //! + //! \return A pointer to the pending beacon if one exists for the CPID. + //! + const PendingBeacon* Try(const Cpid cpid) + { + AssertLockHeld(cs_main); + + const auto iter = m_pending.find(cpid); + + if (iter == m_pending.end()) { + return nullptr; + } + + if (iter->second.Expired(GetAdjustedTime())) { + m_pending.erase(iter); + return nullptr; + } + + return &iter->second; + } + + //! + //! \brief Stash a beacon that the node just advertised to the network. + //! + //! \param cpid CPID that the beacon was advertised for. + //! \param result Contains the public key if the transaction succeeded. + //! + void Remember(const Cpid cpid, const AdvertiseBeaconResult& result) + { + AssertLockHeld(cs_main); + + if (const CPubKey* key = result.TryPublicKey()) { + auto iter_pair = m_pending.emplace(cpid, PendingBeacon(cpid, *key)); + iter_pair.first->second.m_timestamp = GetAdjustedTime(); + } + } + +private: + std::map m_pending; //!< Known set of pending beacons. +}; // RecentBeacons + +//! +//! \brief A cache of recently-advertised pending beacons. +//! +//! THREAD SAFETY: Lock cs_main before calling methods on this object. +//! +RecentBeacons g_recent_beacons; + //! //! \brief Determine whether the wallet contains a valid private key that //! matches the supplied beacon public key. @@ -457,25 +541,6 @@ bool CheckBeaconPrivateKey(const CWallet* const wallet, const CPubKey& public_ke return true; } -//! -//! \brief Determine whether the wallet contains a key for a pending beacon. -//! -//! \param beacons Fetches pending beacon keys IDs. -//! \param cpid CPID to look up pending beacons for. -//! -//! \return \c true if the a pending beacon exists for the supplied CPID. -//! -bool DetectPendingBeacon(const BeaconRegistry& beacons, const Cpid cpid) -{ - for (const auto& key_id : beacons.FindPendingKeys(cpid)) { - if (pwalletMain->HaveKey(key_id)) { - return true; - } - } - - return false; -} - //! //! \brief Generate a new beacon key pair. //! @@ -907,6 +972,16 @@ Researcher::Researcher( { } +void Researcher::Initialize() +{ + { + LOCK2(cs_main, pwalletMain->cs_wallet); + g_recent_beacons.ImportRegistry(GetBeaconRegistry()); + } + + Reload(); +} + std::string Researcher::Email() { std::string email = GetArgument("email", ""); @@ -1021,6 +1096,7 @@ ProjectOption Researcher::Project(const std::string& name) const bool Researcher::Eligible() const { if (const CpidOption cpid = m_mining_id.TryCpid()) { + LOCK(cs_main); return GetBeaconRegistry().ContainsActive(*cpid); } @@ -1074,6 +1150,44 @@ ResearcherStatus Researcher::Status() const return ResearcherStatus::INVESTOR; } +boost::optional Researcher::TryBeacon() const +{ + const CpidOption cpid = m_mining_id.TryCpid(); + + if (!cpid) { + return boost::none; + } + + LOCK(cs_main); + + const BeaconOption beacon = GetBeaconRegistry().Try(*cpid); + + if (!beacon) { + return boost::none; + } + + return *beacon; +} + +boost::optional Researcher::TryPendingBeacon() const +{ + const CpidOption cpid = m_mining_id.TryCpid(); + + if (!cpid) { + return boost::none; + } + + LOCK(cs_main); + + const PendingBeacon* beacon = g_recent_beacons.Try(*cpid); + + if (!beacon) { + return boost::none; + } + + return *beacon; +} + NN::BeaconError Researcher::BeaconError() const { return m_beacon_error; @@ -1103,26 +1217,13 @@ bool Researcher::UpdateEmail(std::string email) AdvertiseBeaconResult Researcher::AdvertiseBeacon() { - AssertLockHeld(cs_main); - AssertLockHeld(pwalletMain->cs_wallet); - const CpidOption cpid = m_mining_id.TryCpid(); if (!cpid) { return BeaconError::NO_CPID; } - static int64_t last_advertised_height = 0; - - // Disallow users from attempting to advertise a beacon repeatedly before - // the network confirms the previous contract in the chain. This prevents - // unintentional spam caused by users who mistakenly try to advertise new - // beacons manually in quick succession when setting up the wallet. - // - if (last_advertised_height >= (nBestHeight - 5)) { - LogPrintf("ERROR: %s: Beacon awaiting confirmation already", __func__); - return BeaconError::PENDING; - } + LOCK2(cs_main, pwalletMain->cs_wallet); const BeaconRegistry& beacons = GetBeaconRegistry(); const BeaconOption current_beacon = beacons.Try(*cpid); @@ -1130,8 +1231,8 @@ AdvertiseBeaconResult Researcher::AdvertiseBeacon() AdvertiseBeaconResult result(BeaconError::NONE); if (!current_beacon) { - if (DetectPendingBeacon(beacons, *cpid)) { - LogPrintf("%s: Beacon awaiting verification already", __func__); + if (g_recent_beacons.Try(*cpid)) { + LogPrintf("%s: Beacon awaiting confirmation already", __func__); return BeaconError::PENDING; } @@ -1140,20 +1241,22 @@ AdvertiseBeaconResult Researcher::AdvertiseBeacon() result = RenewBeacon(*cpid, *current_beacon); } - m_beacon_error = result.Error(); - uiInterface.BeaconChanged(); + if (result.Error() == BeaconError::NONE) { + g_recent_beacons.Remember(*cpid, result); + } - if (m_beacon_error == BeaconError::NONE) { - last_advertised_height = nBestHeight; + if (result.Error() != BeaconError::NOT_NEEDED) { + m_beacon_error = result.Error(); } + uiInterface.BeaconChanged(); + return result; } AdvertiseBeaconResult Researcher::RevokeBeacon(const Cpid cpid) { - AssertLockHeld(cs_main); - AssertLockHeld(pwalletMain->cs_wallet); + LOCK2(cs_main, pwalletMain->cs_wallet); const BeaconOption beacon = GetBeaconRegistry().Try(cpid); diff --git a/src/neuralnet/researcher.h b/src/neuralnet/researcher.h index d97b9acec4..6dbc618888 100644 --- a/src/neuralnet/researcher.h +++ b/src/neuralnet/researcher.h @@ -15,6 +15,7 @@ class uint256; namespace NN { +class Beacon; class Magnitude; class Project; class WhitelistSnapshot; @@ -315,6 +316,11 @@ class Researcher MiningProjectMap projects, const BeaconError beacon_error = BeaconError::NONE); + //! + //! \brief Set up the local researcher context. + //! + static void Initialize(); + //! //! \brief Get the configured BOINC account email address. //! @@ -456,6 +462,20 @@ class Researcher //! ResearcherStatus Status() const; + //! + //! \brief Get the beacon for the current CPID if it exists. + //! + //! \return Contains the beacon for the CPID or does not. + //! + boost::optional TryBeacon() const; + + //! + //! \brief Get the pending beacon for the current CPID if it exists. + //! + //! \return Contains the pending beacon for the CPID or does not. + //! + boost::optional TryPendingBeacon() const; + //! //! \brief Get the error from the last beacon advertisement, if any. //! diff --git a/src/rpcblockchain.cpp b/src/rpcblockchain.cpp index 415aa1d352..bd8b53624f 100644 --- a/src/rpcblockchain.cpp +++ b/src/rpcblockchain.cpp @@ -907,28 +907,54 @@ UniValue beaconstatus(const UniValue& params, bool fHelp) } const int64_t now = GetAdjustedTime(); + const bool is_mine = NN::Researcher::Get()->Id() == *cpid; + UniValue res(UniValue::VOBJ); + UniValue active(UniValue::VARR); + UniValue pending(UniValue::VARR); LOCK(cs_main); - if (const NN::BeaconOption beacon = NN::GetBeaconRegistry().Try(*cpid)) { - res.pushKV("cpid", cpid->ToString()); - res.pushKV("active", !beacon->Expired(now)); - res.pushKV("expired", beacon->Expired(now)); - res.pushKV("renewable", beacon->Renewable(now)); - res.pushKV("timestamp", TimestampToHRDate(beacon->m_timestamp)); - res.pushKV("address", beacon->GetAddress().ToString()); - res.pushKV("public_key", beacon->m_public_key.ToString()); - res.pushKV("magnitude", NN::Quorum::GetMagnitude(*cpid).Floating()); - res.pushKV("is_mine", NN::Researcher::Get()->Id() == *cpid); + const NN::BeaconRegistry& beacons = NN::GetBeaconRegistry(); - return res; + if (const NN::BeaconOption beacon = beacons.Try(*cpid)) { + UniValue entry(UniValue::VOBJ); + entry.pushKV("cpid", cpid->ToString()); + entry.pushKV("active", !beacon->Expired(now)); + entry.pushKV("pending", false); + entry.pushKV("expired", beacon->Expired(now)); + entry.pushKV("renewable", beacon->Renewable(now)); + entry.pushKV("timestamp", TimestampToHRDate(beacon->m_timestamp)); + entry.pushKV("address", beacon->GetAddress().ToString()); + entry.pushKV("public_key", beacon->m_public_key.ToString()); + entry.pushKV("magnitude", NN::Quorum::GetMagnitude(*cpid).Floating()); + entry.pushKV("verification_code", beacon->GetId().ToString()); + entry.pushKV("is_mine", is_mine); + + active.push_back(entry); + } + + for (const NN::PendingBeacon* beacon : beacons.FindPending(*cpid)) { + UniValue entry(UniValue::VOBJ); + entry.pushKV("cpid", cpid->ToString()); + entry.pushKV("active", false); + entry.pushKV("pending", true); + entry.pushKV("expired", beacon->Expired(now)); + entry.pushKV("renewable", false); + entry.pushKV("timestamp", TimestampToHRDate(beacon->m_timestamp)); + entry.pushKV("address", beacon->GetAddress().ToString()); + entry.pushKV("public_key", beacon->m_public_key.ToString()); + entry.pushKV("magnitude", 0); + entry.pushKV("verification_code", beacon->GetId().ToString()); + entry.pushKV("is_mine", is_mine); + + pending.push_back(entry); } - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf( - "No active beacon found for %s. Use the \"advertisebeacon\" RPC to " - "send a new beacon.", - cpid->ToString())); + res.pushKV("active", active); + res.pushKV("pending", pending); + + return res; } UniValue explainmagnitude(const UniValue& params, bool fHelp) From 9f17e0ddabff1a9e30522b0676db0dd0450c0583 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:41:23 -0500 Subject: [PATCH 16/22] Create GUI researcher configuration wizard This adds a wizard component that guides a user through the inital onboarding process to advertise a beacon. --- gridcoinresearch.pro | 22 + src/Makefile.qt.include | 32 +- src/qt/bitcoin.qrc | 1 + src/qt/forms/overviewpage.ui | 115 ++- src/qt/forms/researcherwizard.ui | 102 ++ src/qt/forms/researcherwizardauthpage.ui | 255 +++++ src/qt/forms/researcherwizardbeaconpage.ui | 268 ++++++ src/qt/forms/researcherwizardemailpage.ui | 90 ++ src/qt/forms/researcherwizardprojectspage.ui | 259 ++++++ src/qt/forms/researcherwizardsummarypage.ui | 874 ++++++++++++++++++ src/qt/overviewpage.cpp | 27 +- src/qt/overviewpage.h | 2 + src/qt/res/icons/warning.svg | 131 +++ src/qt/res/stylesheets/dark_stylesheet.qss | 2 +- src/qt/res/stylesheets/light_stylesheet.qss | 2 +- src/qt/res/stylesheets/native_stylesheet.qss | 2 +- src/qt/researcher/projecttablemodel.cpp | 236 +++++ src/qt/researcher/projecttablemodel.h | 50 + src/qt/researcher/researchermodel.cpp | 399 ++++++-- src/qt/researcher/researchermodel.h | 50 + src/qt/researcher/researcherwizard.cpp | 76 ++ src/qt/researcher/researcherwizard.h | 45 + .../researcher/researcherwizardauthpage.cpp | 56 ++ src/qt/researcher/researcherwizardauthpage.h | 33 + .../researcher/researcherwizardbeaconpage.cpp | 114 +++ .../researcher/researcherwizardbeaconpage.h | 42 + .../researcher/researcherwizardemailpage.cpp | 33 + src/qt/researcher/researcherwizardemailpage.h | 29 + .../researcherwizardprojectspage.cpp | 94 ++ .../researcher/researcherwizardprojectspage.h | 35 + .../researcherwizardsummarypage.cpp | 155 ++++ .../researcher/researcherwizardsummarypage.h | 48 + 32 files changed, 3581 insertions(+), 98 deletions(-) create mode 100644 src/qt/forms/researcherwizard.ui create mode 100644 src/qt/forms/researcherwizardauthpage.ui create mode 100644 src/qt/forms/researcherwizardbeaconpage.ui create mode 100644 src/qt/forms/researcherwizardemailpage.ui create mode 100644 src/qt/forms/researcherwizardprojectspage.ui create mode 100644 src/qt/forms/researcherwizardsummarypage.ui create mode 100755 src/qt/res/icons/warning.svg create mode 100644 src/qt/researcher/projecttablemodel.cpp create mode 100644 src/qt/researcher/projecttablemodel.h create mode 100644 src/qt/researcher/researcherwizard.cpp create mode 100644 src/qt/researcher/researcherwizard.h create mode 100644 src/qt/researcher/researcherwizardauthpage.cpp create mode 100644 src/qt/researcher/researcherwizardauthpage.h create mode 100644 src/qt/researcher/researcherwizardbeaconpage.cpp create mode 100644 src/qt/researcher/researcherwizardbeaconpage.h create mode 100644 src/qt/researcher/researcherwizardemailpage.cpp create mode 100644 src/qt/researcher/researcherwizardemailpage.h create mode 100644 src/qt/researcher/researcherwizardprojectspage.cpp create mode 100644 src/qt/researcher/researcherwizardprojectspage.h create mode 100644 src/qt/researcher/researcherwizardsummarypage.cpp create mode 100644 src/qt/researcher/researcherwizardsummarypage.h diff --git a/gridcoinresearch.pro b/gridcoinresearch.pro index c53e783e4a..9320fa3d59 100755 --- a/gridcoinresearch.pro +++ b/gridcoinresearch.pro @@ -172,6 +172,14 @@ QMAKE_CXXFLAGS_WARN_ON = -fdiagnostics-show-option -Wall -Wextra -Wno-ignored-qu DEPENDPATH += src src/json src/qt HEADERS += src/qt/bitcoingui.h \ + src/qt/researcher/projecttablemodel.h \ + src/qt/researcher/researchermodel.h \ + src/qt/researcher/researcherwizard.h \ + src/qt/researcher/researcherwizardauthpage.h \ + src/qt/researcher/researcherwizardbeaconpage.h \ + src/qt/researcher/researcherwizardemailpage.h \ + src/qt/researcher/researcherwizardprojectspage.h \ + src/qt/researcher/researcherwizardsummarypage.h \ src/qt/transactiontablemodel.h \ src/qt/addresstablemodel.h \ src/qt/optionsdialog.h \ @@ -265,6 +273,14 @@ HEADERS += src/qt/bitcoingui.h \ SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \ + src/qt/researcher/projecttablemodel.cpp \ + src/qt/researcher/researchermodel.cpp \ + src/qt/researcher/researcherwizard.cpp \ + src/qt/researcher/researcherwizardauthpage.cpp \ + src/qt/researcher/researcherwizardbeaconpage.cpp \ + src/qt/researcher/researcherwizardemailpage.cpp \ + src/qt/researcher/researcherwizardprojectspage.cpp \ + src/qt/researcher/researcherwizardsummarypage.cpp \ src/qt/transactiontablemodel.cpp \ src/qt/addresstablemodel.cpp \ src/qt/optionsdialog.cpp \ @@ -354,6 +370,12 @@ RESOURCES += \ FORMS += \ src/qt/forms/coincontroldialog.ui \ + src/qt/forms/researcherwizard.ui \ + src/qt/forms/researcherwizardauthpage.ui \ + src/qt/forms/researcherwizardbeaconpage.ui \ + src/qt/forms/researcherwizardemailpage.ui \ + src/qt/forms/researcherwizardprojectspage.ui \ + src/qt/forms/researcherwizardsummarypage.ui \ src/qt/forms/sendcoinsdialog.ui \ src/qt/forms/addressbookpage.ui \ src/qt/forms/signverifymessagedialog.ui \ diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 7095d25d4e..5af8d30d6f 100755 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -81,6 +81,12 @@ QT_FORMS_UI = \ qt/forms/addressbookpage.ui \ qt/forms/editaddressdialog.ui \ qt/forms/overviewpage.ui \ + qt/forms/researcherwizard.ui \ + qt/forms/researcherwizardauthpage.ui \ + qt/forms/researcherwizardbeaconpage.ui \ + qt/forms/researcherwizardemailpage.ui \ + qt/forms/researcherwizardprojectspage.ui \ + qt/forms/researcherwizardsummarypage.ui \ qt/forms/sendcoinsdialog.ui \ qt/forms/transactiondescdialog.ui \ qt/forms/askpassphrasedialog.ui \ @@ -113,7 +119,6 @@ QT_MOC_CPP = \ qt/moc_peertablemodel.cpp \ qt/moc_qvalidatedlineedit.cpp \ qt/moc_qvaluecombobox.cpp \ - qt/researcher/moc_researchermodel.cpp \ qt/moc_rpcconsole.cpp \ qt/moc_sendcoinsdialog.cpp \ qt/moc_sendcoinsentry.cpp \ @@ -125,7 +130,15 @@ QT_MOC_CPP = \ qt/moc_transactiontablemodel.cpp \ qt/moc_transactionview.cpp \ qt/moc_votingdialog.cpp \ - qt/moc_walletmodel.cpp + qt/moc_walletmodel.cpp \ + qt/researcher/moc_projecttablemodel.cpp \ + qt/researcher/moc_researchermodel.cpp \ + qt/researcher/moc_researcherwizard.cpp \ + qt/researcher/moc_researcherwizardauthpage.cpp \ + qt/researcher/moc_researcherwizardbeaconpage.cpp \ + qt/researcher/moc_researcherwizardemailpage.cpp \ + qt/researcher/moc_researcherwizardprojectspage.cpp \ + qt/researcher/moc_researcherwizardsummarypage.cpp GRIDCOIN_MM = \ qt/macdockiconhandler.mm \ @@ -170,7 +183,14 @@ GRIDCOINRESEARCH_QT_H = \ qt/qtipcserver.h \ qt/qvalidatedlineedit.h \ qt/qvaluecombobox.h \ + qt/researcher/projecttablemodel.h \ qt/researcher/researchermodel.h \ + qt/researcher/researcherwizard.h \ + qt/researcher/researcherwizardauthpage.h \ + qt/researcher/researcherwizardbeaconpage.h \ + qt/researcher/researcherwizardemailpage.h \ + qt/researcher/researcherwizardprojectspage.h \ + qt/researcher/researcherwizardsummarypage.h \ qt/rpcconsole.h \ qt/sendcoinsdialog.h \ qt/sendcoinsentry.h \ @@ -214,7 +234,14 @@ GRIDCOINRESEARCH_QT_CPP = \ qt/qtipcserver.cpp \ qt/qvalidatedlineedit.cpp \ qt/qvaluecombobox.cpp \ + qt/researcher/projecttablemodel.cpp \ qt/researcher/researchermodel.cpp \ + qt/researcher/researcherwizard.cpp \ + qt/researcher/researcherwizardauthpage.cpp \ + qt/researcher/researcherwizardbeaconpage.cpp \ + qt/researcher/researcherwizardemailpage.cpp \ + qt/researcher/researcherwizardprojectspage.cpp \ + qt/researcher/researcherwizardsummarypage.cpp \ qt/rpcconsole.cpp \ qt/sendcoinsdialog.cpp \ qt/sendcoinsentry.cpp \ @@ -280,6 +307,7 @@ RES_ICONS = \ qt/res/icons/tx_output.svg \ qt/res/icons/tx_por_ss.svg \ qt/res/icons/tx_por.svg \ + qt/res/icons/warning.svg \ qt/res/icons/white_and_red_x.svg \ qt/res/icons/www.png \ qt/res/icons/icons_native/overview.svg \ diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc index b7cdd9cf61..f209b990f6 100644 --- a/src/qt/bitcoin.qrc +++ b/src/qt/bitcoin.qrc @@ -73,6 +73,7 @@ res/icons/white_and_red_x.svg res/icons/staking_unable.svg res/icons/superblock.svg + res/icons/warning.svg res/images/splash3.png diff --git a/src/qt/forms/overviewpage.ui b/src/qt/forms/overviewpage.ui index a10173056b..e74a3a38f0 100644 --- a/src/qt/forms/overviewpage.ui +++ b/src/qt/forms/overviewpage.ui @@ -7,7 +7,7 @@ 0 0 948 - 559 + 635 @@ -383,16 +383,6 @@ - - - - (action needed) - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - @@ -473,7 +463,7 @@ - Pending Accrual: + Pending Reward: @@ -487,6 +477,103 @@ + + + + Open the researcher/beacon configuration wizard. + + + &Beacon... + + + false + + + false + + + + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + :/icons/warning + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + Qt::NoTextInteraction + + + + + + + Action needed + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + @@ -688,6 +775,8 @@
clicklabel.h
- + + + diff --git a/src/qt/forms/researcherwizard.ui b/src/qt/forms/researcherwizard.ui new file mode 100644 index 0000000000..eeeb450fb5 --- /dev/null +++ b/src/qt/forms/researcherwizard.ui @@ -0,0 +1,102 @@ + + + ResearcherWizard + + + + 0 + 0 + 660 + 480 + + + + + 0 + 0 + + + + Researcher Configuration + + + true + + + true + + + QWizard::ClassicStyle + + + QWizard::HaveCustomButton1|QWizard::NoBackButtonOnLastPage|QWizard::NoBackButtonOnStartPage|QWizard::NoCancelButtonOnLastPage + + + + + 0 + 0 + + + + + + + 0 + 0 + + + + + + + 0 + 0 + + + + + + + + 0 + 0 + + + + + + + ResearcherWizardSummaryPage + QWizardPage +
researcher/researcherwizardsummarypage.h
+ 1 +
+ + ResearcherWizardEmailPage + QWizardPage +
researcher/researcherwizardemailpage.h
+ 1 +
+ + ResearcherWizardProjectsPage + QWizardPage +
researcher/researcherwizardprojectspage.h
+ 1 +
+ + ResearcherWizardBeaconPage + QWizardPage +
researcher/researcherwizardbeaconpage.h
+ 1 +
+ + ResearcherWizardAuthPage + QWizardPage +
researcher/researcherwizardauthpage.h
+ 1 +
+
+ + +
diff --git a/src/qt/forms/researcherwizardauthpage.ui b/src/qt/forms/researcherwizardauthpage.ui new file mode 100644 index 0000000000..4918375d24 --- /dev/null +++ b/src/qt/forms/researcherwizardauthpage.ui @@ -0,0 +1,255 @@ + + + ResearcherWizardAuthPage + + + + 0 + 0 + 630 + 480 + + + + + 0 + 0 + + + + Beacon Verification + + + Beacon Verification + + + Gridcoin needs to verify your BOINC account CPID. Please follow the instructions below to change your BOINC account username. The network needs 24 to 48 hours to verify a new CPID. + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + 1. Sign in to your account at the website for a whitelisted BOINC project. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + 2. Visit the settings page to change your username. Many projects label it as "other account info". + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + 3. Change your username to the following verification code: + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + 16 + + + 9 + + + 9 + + + 9 + + + + + + 0 + 0 + + + + + Monospace + 10 + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + Copy the verification code to the system clipboard + + + &Copy + + + + :/icons/editcopy:/icons/editcopy + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 4. Wait 24 to 48 hours for the verification process to finish (beacon status will change to "active"). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + 5. After that, you may change the username back to your preference. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + Qt::Horizontal + + + + + + + + + + <html> +<head/> +<body> +<h4 style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"> +<span style=" font-size:medium; font-weight:600;">Remember:</span> +</h4> +<ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 0;"> +<li style=" margin-top:6px; margin-bottom:0px; margin-left:15px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The network only needs to verify the code above at a single whitelisted BOINC project even when you participate in multiple projects. </li> +<li style=" margin-top:6px; margin-bottom:0px; margin-left:15px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The verification code expires after three days pass. </li> +<li style=" margin-top:6px; margin-bottom:0px; margin-left:15px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A beacon expires after six months pass. </li><li style=" margin-top:6px; margin-bottom:0px; margin-left:15px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A beacon becomes eligible for renewal after five months pass. The wallet will remind you to renew the beacon. </li> +<li style=" margin-top:6px; margin-bottom:12px; margin-left:15px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You will not need to change your username again to renew a beacon unless it expires. </li> +</ul> +</body> +</html> + + + Qt::RichText + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/src/qt/forms/researcherwizardbeaconpage.ui b/src/qt/forms/researcherwizardbeaconpage.ui new file mode 100644 index 0000000000..880ea4c71f --- /dev/null +++ b/src/qt/forms/researcherwizardbeaconpage.ui @@ -0,0 +1,268 @@ + + + ResearcherWizardBeaconPage + + + + 0 + 0 + 630 + 480 + + + + + 0 + 0 + + + + Beacon Advertisement + + + Beacon Advertisement + + + A beacon is a special type of Gridcoin transaction that links a BOINC CPID to your wallet. After sending a beacon, the network tracks your BOINC statistics to calculate research rewards. + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + + 48 + 48 + + + + + 48 + 48 + + + + + + + :/icons/beacon_grey + + + true + + + Qt::NoTextInteraction + + + + + + + + 0 + 0 + + + + + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + &Advertise Beacon + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + CPID: + + + + + + + + 0 + 0 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + :/icons/synced + + + true + + + Qt::AlignCenter + + + Qt::NoTextInteraction + + + + + + + + 0 + 0 + + + + Press "Next" to continue. + + + Qt::NoTextInteraction + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/src/qt/forms/researcherwizardemailpage.ui b/src/qt/forms/researcherwizardemailpage.ui new file mode 100644 index 0000000000..5f1ec70570 --- /dev/null +++ b/src/qt/forms/researcherwizardemailpage.ui @@ -0,0 +1,90 @@ + + + ResearcherWizardEmailPage + + + + 0 + 0 + 630 + 480 + + + + + 0 + 0 + + + + BOINC Email Address + + + BOINC Email Address + + + Confirm the email address that you provided to register the accounts for BOINC projects that you participate in. Gridcoin uses the email address to detect BOINC projects on your computer. + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 40 + + + + + + + + + + Email Address: + + + + + + + gridcoin@example.com + + + true + + + + + + + + + + + + + + + 0 + 0 + + + + The wallet will never transmit your email address. + + + + + + + + + + diff --git a/src/qt/forms/researcherwizardprojectspage.ui b/src/qt/forms/researcherwizardprojectspage.ui new file mode 100644 index 0000000000..b3fd507eb9 --- /dev/null +++ b/src/qt/forms/researcherwizardprojectspage.ui @@ -0,0 +1,259 @@ + + + ResearcherWizardProjectsPage + + + + 0 + 0 + 630 + 480 + + + + + 0 + 0 + + + + BOINC CPID Detection + + + BOINC CPID Detection + + + Gridcoin scans the BOINC projects on your computer to find an eligible cross-project identifier (CPID). The network tracks CPIDs to allocate research rewards. + + + + + + QLayout::SetMaximumSize + + + + + + + + + Email Address: + + + + + + + IBeamCursor + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + BOINC Folder: + + + + + + + IBeamCursor + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Selected CPID: + + + + + + + + + IBeamCursor + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + :/icons/white_and_red_x + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + Qt::NoTextInteraction + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Re-scan the BOINC projects on your computer. + + + &Refresh + + + false + + + + + + + + + Qt::ScrollBarAsNeeded + + + Qt::ScrollBarAsNeeded + + + false + + + false + + + QAbstractItemView::NoDragDrop + + + Qt::IgnoreAction + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + Qt::SolidLine + + + true + + + false + + + false + + + true + + + true + + + false + + + true + + + + + + + + + + + + diff --git a/src/qt/forms/researcherwizardsummarypage.ui b/src/qt/forms/researcherwizardsummarypage.ui new file mode 100644 index 0000000000..b8cb4e3f76 --- /dev/null +++ b/src/qt/forms/researcherwizardsummarypage.ui @@ -0,0 +1,874 @@ + + + ResearcherWizardSummaryPage + + + + 0 + 0 + 630 + 480 + + + + + 0 + 0 + + + + Researcher Summary + + + + + + + + + + + + + + + 0 + + + + + 0 + 0 + + + + Summary + + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + CPID + + + Qt::AlignHCenter|Qt::AlignTop + + + + + + + + 0 + 0 + + + + + 11 + + + + IBeamCursor + + + + + + Qt::AlignHCenter|Qt::AlignTop + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + :/icons/synced + + + true + + + Qt::NoTextInteraction + + + + + + + + 0 + 0 + + + + Everything looks good. + + + Qt::AlignCenter + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 30 + + + + + + + + + + + 1 + 0 + + + + + 300 + 0 + + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 48 + 48 + + + + + 24 + 24 + + + + + + + :/images/gridcoin + + + true + + + Qt::NoTextInteraction + + + + + + + Qt::Horizontal + + + + + + + 6 + + + 0 + + + + + Status: + + + + + + + IBeamCursor + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Magnitude: + + + + + + + IBeamCursor + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Pending Reward: + + + + + + + IBeamCursor + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + 1 + 0 + + + + + 300 + 0 + + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 48 + 48 + + + + + 48 + 48 + + + + + + + :/icons/beacon_grey + + + true + + + Qt::NoTextInteraction + + + + + + + Qt::Horizontal + + + + + + + 6 + + + 6 + + + + + Beacon: + + + + + + + IBeamCursor + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Age: + + + + + + + IBeamCursor + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Expires: + + + + + + + IBeamCursor + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Address: + + + + + + + + Monospace + 8 + + + + IBeamCursor + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + &Renew + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + 0 + + + + Projects + + + + + + + + + + Email Address: + + + + + + + IBeamCursor + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + BOINC Folder: + + + + + + + IBeamCursor + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Selected CPID: + + + + + + + + + IBeamCursor + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + :/icons/white_and_red_x + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + Qt::NoTextInteraction + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Re-scan the BOINC projects on your computer. + + + &Refresh + + + false + + + + + + + + + Qt::ScrollBarAsNeeded + + + Qt::ScrollBarAsNeeded + + + false + + + false + + + QAbstractItemView::NoDragDrop + + + Qt::IgnoreAction + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + Qt::SolidLine + + + true + + + false + + + false + + + true + + + true + + + false + + + true + + + + + + + + + + + + + + diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index d9ee003e04..46d7f931e8 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -224,6 +224,8 @@ void OverviewPage::setResearcherModel(ResearcherModel *researcherModel) updateResearcherStatus(); connect(researcherModel, SIGNAL(researcherChanged()), this, SLOT(updateResearcherStatus())); connect(researcherModel, SIGNAL(accrualChanged()), this, SLOT(updatePendingAccrual())); + connect(researcherModel, SIGNAL(beaconChanged()), this, SLOT(updateResearcherAlert())); + connect(ui->beaconButton, SIGNAL(clicked()), this, SLOT(onBeaconButtonClicked())); } void OverviewPage::setWalletModel(WalletModel *model) @@ -280,6 +282,7 @@ void OverviewPage::updateResearcherStatus() ui->magnitudeLabel->setText(researcherModel->formatMagnitude()); updatePendingAccrual(); + updateResearcherAlert(); } void OverviewPage::updatePendingAccrual() @@ -288,11 +291,33 @@ void OverviewPage::updatePendingAccrual() return; } - const int unit = walletModel->getOptionsModel()->getDisplayUnit(); + int unit = BitcoinUnits::BTC; + + if (walletModel) { + unit = walletModel->getOptionsModel()->getDisplayUnit(); + } ui->accrualLabel->setText(researcherModel->formatAccrual(unit)); } +void OverviewPage::updateResearcherAlert() +{ + if (!researcherModel) { + return; + } + + ui->researcherAlertWrapper->setVisible(researcherModel->actionNeeded()); +} + +void OverviewPage::onBeaconButtonClicked() +{ + if (!researcherModel || !walletModel) { + return; + } + + researcherModel->showWizard(walletModel); +} + void OverviewPage::showOutOfSyncWarning(bool fShow) { ui->walletStatusLabel->setVisible(fShow); diff --git a/src/qt/overviewpage.h b/src/qt/overviewpage.h index 1c9dcd98e6..a2b982eaa0 100644 --- a/src/qt/overviewpage.h +++ b/src/qt/overviewpage.h @@ -64,6 +64,8 @@ private slots: void updateDisplayUnit(); void updateResearcherStatus(); void updatePendingAccrual(); + void updateResearcherAlert(); + void onBeaconButtonClicked(); void handleTransactionClicked(const QModelIndex &index); void handlePollLabelClicked(); }; diff --git a/src/qt/res/icons/warning.svg b/src/qt/res/icons/warning.svg new file mode 100755 index 0000000000..2bbd6b6ab4 --- /dev/null +++ b/src/qt/res/icons/warning.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Openclipart + + + warning icon + 2010-03-03T13:31:08 + A very simple warning icon that can be scaled down fairly small and still be recognizable. + https://openclipart.org/detail/29833/warning-icon-by-matthewgarysmith + + + matthewgarysmith + + + + + icon + sign + warning + yellow + + + + + + + + + + + diff --git a/src/qt/res/stylesheets/dark_stylesheet.qss b/src/qt/res/stylesheets/dark_stylesheet.qss index ace8239465..0102943652 100644 --- a/src/qt/res/stylesheets/dark_stylesheet.qss +++ b/src/qt/res/stylesheets/dark_stylesheet.qss @@ -446,7 +446,7 @@ QToolBar#toolbar3 QLabel{ #walletStatusLabel, #transactionsStatusLabel, -#researcherWarningLabel { +#researcherAlertLabel { color: red; } diff --git a/src/qt/res/stylesheets/light_stylesheet.qss b/src/qt/res/stylesheets/light_stylesheet.qss index b6fbc6da59..52661cb072 100644 --- a/src/qt/res/stylesheets/light_stylesheet.qss +++ b/src/qt/res/stylesheets/light_stylesheet.qss @@ -437,7 +437,7 @@ QToolBar#toolbar3 QLabel{ #walletStatusLabel, #transactionsStatusLabel, -#researcherWarningLabel { +#researcherAlertLabel { color: red; } diff --git a/src/qt/res/stylesheets/native_stylesheet.qss b/src/qt/res/stylesheets/native_stylesheet.qss index 4a4915db5e..85966efb7b 100644 --- a/src/qt/res/stylesheets/native_stylesheet.qss +++ b/src/qt/res/stylesheets/native_stylesheet.qss @@ -96,7 +96,7 @@ QToolBar#toolbar3 QLabel{ #walletStatusLabel, #transactionsStatusLabel, -#researcherWarningLabel { +#researcherAlertLabel { color: red; } diff --git a/src/qt/researcher/projecttablemodel.cpp b/src/qt/researcher/projecttablemodel.cpp new file mode 100644 index 0000000000..f99d921acd --- /dev/null +++ b/src/qt/researcher/projecttablemodel.cpp @@ -0,0 +1,236 @@ +#include "neuralnet/researcher.h" + +#include "qt/researcher/projecttablemodel.h" +#include "qt/researcher/researchermodel.h" + +#include + +namespace { +//! +//! \brief Compares rows in a project table for sorting order. +//! +class CompareRowLessThan +{ +public: + CompareRowLessThan(int column, Qt::SortOrder order) + : m_column(column) + , m_order(order) + { + } + + bool operator()(const ProjectRow& left, const ProjectRow& right) const + { + const ProjectRow* pLeft = &left; + const ProjectRow* pRight = &right; + + if (m_order == Qt::DescendingOrder) { + std::swap(pLeft, pRight); + } + + switch (m_column) { + case ProjectTableModel::Name: + return pLeft->m_name < pRight->m_name; + case ProjectTableModel::Eligible: + return pLeft->m_error < pRight->m_error; + case ProjectTableModel::Whitelisted: + return pLeft->m_whitelisted < pRight->m_whitelisted; + case ProjectTableModel::Magnitude: + return pLeft->m_magnitude < pRight->m_magnitude; + case ProjectTableModel::Cpid: + return pLeft->m_cpid < pRight->m_cpid; + } + + return false; + } + +private: + int m_column; + Qt::SortOrder m_order; +}; // CompareRowLessThan +} // Anonymous namespace + +// ----------------------------------------------------------------------------- +// Class: ProjectTableData +// ----------------------------------------------------------------------------- + +class ProjectTableData +{ +public: + ProjectTableData() + : m_sort_column(static_cast(ProjectTableModel::ColumnIndex::Name)) + { + } + + size_t size() const + { + return m_rows.size(); + } + + ProjectRow* index(const int row) + { + if (row > static_cast(m_rows.size())) { + return nullptr; + } + + return &m_rows[row]; + } + + void sort(const int column, const Qt::SortOrder order) + { + m_sort_column = column; + m_sort_order = order; + + DoSort(); + } + + void reload(std::vector rows) + { + m_rows = std::move(rows); + + DoSort(); + } + +private: + int m_sort_column; + Qt::SortOrder m_sort_order; + std::vector m_rows; + + void DoSort() + { + std::stable_sort( + m_rows.begin(), + m_rows.end(), + CompareRowLessThan(m_sort_column, m_sort_order)); + } +}; // ProjectTableStats + +// ----------------------------------------------------------------------------- +// Class: ProjectTableModel +// ----------------------------------------------------------------------------- + +ProjectTableModel::ProjectTableModel(ResearcherModel *model, bool show_magnitude) + : m_model(model) + , m_data(new ProjectTableData()) + , m_show_magnitude(show_magnitude) +{ + m_columns + << tr("Name") + << tr("Eligible") + << tr("Whitelist") + << tr("Magnitude") + << tr("CPID"); +} + +ProjectTableModel::~ProjectTableModel() +{ + // Nothing to do yet... +} + +int ProjectTableModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_data->size(); +} + +int ProjectTableModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_columns.size(); +} + +QVariant ProjectTableModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + const ProjectRow* row = static_cast(index.internalPointer()); + + switch (role) { + case Qt::DisplayRole: + switch (index.column()) { + case Name: + return row->m_name; + case Eligible: + if (!row->m_error.isEmpty()) { + return row->m_error; + } + break; + case Cpid: + return row->m_cpid; + case Magnitude: + return row->m_magnitude; + } + break; + + case Qt::DecorationRole: + switch (index.column()) { + case Eligible: + if (row->m_error.isEmpty()) { + return QIcon(":/icons/synced"); + } + break; + case Whitelisted: + if (row->m_whitelisted) { + return QIcon(":/icons/synced"); + } + break; + } + break; + + case Qt::TextAlignmentRole: + switch (index.column()) { + case Magnitude: + return QVariant(Qt::AlignRight | Qt::AlignVCenter); + } + break; + } + + return QVariant(); +} + +QVariant ProjectTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal) { + if (role == Qt::DisplayRole && section < m_columns.size()) { + return m_columns[section]; + } + } + + return QVariant(); +} + +QModelIndex ProjectTableModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent); + ProjectRow* data = m_data->index(row); + + if (data) { + return createIndex(row, column, data); + } + + return QModelIndex(); +} + +Qt::ItemFlags ProjectTableModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) { + return Qt::NoItemFlags; + } + + return (Qt::ItemIsSelectable | Qt::ItemIsEnabled); +} + +void ProjectTableModel::sort(int column, Qt::SortOrder order) +{ + emit layoutAboutToBeChanged(); + m_data->sort(column, order); + emit layoutChanged(); +} + +void ProjectTableModel::refresh() +{ + emit layoutAboutToBeChanged(); + m_data->reload(m_model->buildProjectTable(m_show_magnitude)); + emit layoutChanged(); +} diff --git a/src/qt/researcher/projecttablemodel.h b/src/qt/researcher/projecttablemodel.h new file mode 100644 index 0000000000..6760b71076 --- /dev/null +++ b/src/qt/researcher/projecttablemodel.h @@ -0,0 +1,50 @@ +#ifndef PROJECTTABLEMODEL_H +#define PROJECTTABLEMODEL_H + +#include +#include +#include + +class ProjectTableData; +class ResearcherModel; + +class ProjectTableModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + enum ColumnIndex { + Name, + Eligible, + Whitelisted, + Magnitude, + Cpid, + }; + + explicit ProjectTableModel( + ResearcherModel *parent = nullptr, + bool show_magnitude = true); + + ~ProjectTableModel(); + + void setModel(ResearcherModel *model); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QModelIndex index(int row, int column, const QModelIndex &parent) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + void sort(int column, Qt::SortOrder order) override; + +public slots: + void refresh(); + +private: + ResearcherModel *m_model; + QStringList m_columns; + std::unique_ptr m_data; + bool m_show_magnitude; +}; + +#endif // PROJECTTABLEMODEL_H diff --git a/src/qt/researcher/researchermodel.cpp b/src/qt/researcher/researchermodel.cpp index 3e72383ea2..e10418a6fa 100644 --- a/src/qt/researcher/researchermodel.cpp +++ b/src/qt/researcher/researchermodel.cpp @@ -1,19 +1,24 @@ #include "base58.h" -#include "bitcoinunits.h" -#include "guiutil.h" +#include "boinc.h" +#include "main.h" #include "neuralnet/beacon.h" #include "neuralnet/magnitude.h" +#include "neuralnet/project.h" +#include "neuralnet/quorum.h" #include "neuralnet/researcher.h" -#include "researchermodel.h" #include "ui_interface.h" +#include "qt/bitcoinunits.h" +#include "qt/guiutil.h" +#include "qt/researcher/researchermodel.h" +#include "qt/researcher/researcherwizard.h" + #include +#include #include using namespace NN; - -extern CCriticalSection cs_main; -extern std::string msMiningErrors; +using LogFlags = BCLog::LogFlags; namespace { constexpr double SECONDS_IN_DAY = 24.0 * 60.0 * 60.0; @@ -24,7 +29,8 @@ constexpr int64_t BEACON_RENEWAL_WARNING_AGE = Beacon::MAX_AGE - (15 * SECONDS_I //! void ResearcherChanged(ResearcherModel* model) { - LogPrintf("ResearcherChanged()"); + LogPrint(LogFlags::QT, "GUI: received ResearcherChanged() core signal"); + QMetaObject::invokeMethod( model, "resetResearcher", @@ -37,7 +43,8 @@ void ResearcherChanged(ResearcherModel* model) //! void BeaconChanged(ResearcherModel* model) { - LogPrintf("BeaconChanged()"); + LogPrint(LogFlags::QT, "GUI: received BeaconChanged() core signal"); + QMetaObject::invokeMethod(model, "updateBeacon", Qt::QueuedConnection); } @@ -55,14 +62,14 @@ BeaconStatus MapAdvertiseBeaconError(const BeaconError error) case BeaconError::INSUFFICIENT_FUNDS: return BeaconStatus::ERROR_INSUFFICIENT_FUNDS; case BeaconError::MISSING_KEY: return BeaconStatus::ERROR_MISSING_KEY; case BeaconError::NO_CPID: return BeaconStatus::NO_CPID; - case BeaconError::NOT_NEEDED: return BeaconStatus::ACTIVE; + case BeaconError::NOT_NEEDED: return BeaconStatus::ERROR_NOT_NEEDED; case BeaconError::PENDING: return BeaconStatus::PENDING; case BeaconError::TX_FAILED: return BeaconStatus::ERROR_TX_FAILED; case BeaconError::WALLET_LOCKED: return BeaconStatus::ERROR_WALLET_LOCKED; } - return BeaconStatus::UNKNOWN; -}; + assert(false); // Suppress warning +} } // anonymous namespace // ----------------------------------------------------------------------------- @@ -70,6 +77,9 @@ BeaconStatus MapAdvertiseBeaconError(const BeaconError error) // ----------------------------------------------------------------------------- ResearcherModel::ResearcherModel() + : m_beacon_status(BeaconStatus::UNKNOWN) + , m_configured_for_investor_mode(false) + , m_wizard_open(false) { qRegisterMetaType("NN::ResearcherPtr"); @@ -80,8 +90,6 @@ ResearcherModel::ResearcherModel() return; } - m_configured_for_investor_mode = false; - subscribeToCoreSignals(); QTimer *refresh_timer = new QTimer(this); @@ -94,50 +102,17 @@ ResearcherModel::~ResearcherModel() unsubscribeFromCoreSignals(); } -bool ResearcherModel::configuredForInvestorMode() const -{ - return m_configured_for_investor_mode; -} - -QString ResearcherModel::formatCpid() const -{ - return QString::fromStdString(m_researcher->Id().ToString()); -} - -QString ResearcherModel::formatMagnitude() const -{ - return QString::fromStdString(m_researcher->Magnitude().ToString()); -} - -QString ResearcherModel::formatAccrual(const int display_unit) const -{ - return BitcoinUnits::formatWithUnit(display_unit, m_researcher->Accrual()); -} - -QString ResearcherModel::formatStatus() const -{ - if (configuredForInvestorMode()) { - return tr("Researcher mode disabled by configuration"); - } - - // TODO: The getmininginfo RPC shares this global. Refactor to remove it: - return QString::fromStdString(msMiningErrors); -} - -BeaconStatus ResearcherModel::getBeaconStatus() const -{ - return m_beacon_status; -} - -QString ResearcherModel::formatBeaconStatus() const +QString ResearcherModel::mapBeaconStatus(const BeaconStatus status) { - switch (m_beacon_status) { + switch (status) { case BeaconStatus::ACTIVE: return tr("Beacon is active."); case BeaconStatus::ERROR_INSUFFICIENT_FUNDS: return tr("Balance too low to send a beacon contract."); case BeaconStatus::ERROR_MISSING_KEY: return tr("Beacon private key missing or invalid."); + case BeaconStatus::ERROR_NOT_NEEDED: + return tr("Current beacon is not renewable yet."); case BeaconStatus::ERROR_TX_FAILED: return tr("Unable to send beacon transaction. See debug.log"); case BeaconStatus::ERROR_WALLET_LOCKED: @@ -158,23 +133,24 @@ QString ResearcherModel::formatBeaconStatus() const return tr("Waiting for data."); } - return tr("Waiting for data."); + assert(false); // Suppress warning } -QIcon ResearcherModel::getBeaconStatusIcon() const +QIcon ResearcherModel::mapBeaconStatusIcon(const BeaconStatus status) { constexpr char success[] = ":/icons/beacon_green"; constexpr char warning[] = ":/icons/beacon_yellow"; constexpr char danger[] = ":/icons/beacon_red"; constexpr char inactive[] = ":/icons/beacon_grey"; - switch (m_beacon_status) { + switch (status) { case BeaconStatus::ACTIVE: return QIcon(success); case BeaconStatus::ERROR_INSUFFICIENT_FUNDS: return QIcon(danger); case BeaconStatus::ERROR_MISSING_KEY: return QIcon(danger); + case BeaconStatus::ERROR_NOT_NEEDED: return QIcon(success); case BeaconStatus::ERROR_TX_FAILED: return QIcon(danger); case BeaconStatus::ERROR_WALLET_LOCKED: return QIcon(danger); - case BeaconStatus::NO_BEACON: return QIcon(danger); + case BeaconStatus::NO_BEACON: return QIcon(inactive); case BeaconStatus::NO_CPID: return QIcon(inactive); case BeaconStatus::NO_MAGNITUDE: return QIcon(warning); case BeaconStatus::PENDING: return QIcon(warning); @@ -183,7 +159,140 @@ QIcon ResearcherModel::getBeaconStatusIcon() const case BeaconStatus::UNKNOWN: return QIcon(inactive); } - return QIcon(inactive); + assert(false); // Suppress warning +} + +void ResearcherModel::showWizard(WalletModel* wallet_model) +{ + if (m_wizard_open) { + return; + } + + m_wizard_open = true; + + ResearcherWizard *wizard = new ResearcherWizard(nullptr, this, wallet_model); + + if (configuredForInvestorMode()) { + QMessageBox::warning( + wizard, + tr("Warning"), + tr("The configuration file contains a setting that explicitly " + "disables researcher mode. Please remove \"investor=1\" or " + "\"email=INVESTOR\" directives from this file to participate " + "as a researcher."), + QMessageBox::Ok, + QMessageBox::Ok); + + wizard->reject(); + return; + } + + if (m_researcher->Id().Which() == MiningId::Kind::CPID) { + if (hasRenewableBeacon()) { + wizard->setStartId(ResearcherWizard::PageBeacon); + } else { + wizard->setStartId(ResearcherWizard::PageSummary); + } + } + + wizard->show(); +} + +bool ResearcherModel::configuredForInvestorMode() const +{ + return m_configured_for_investor_mode; +} + +bool ResearcherModel::actionNeeded() const +{ + if (configuredForInvestorMode()) { + return false; + } + + return !hasEligibleProjects() + || (!hasActiveBeacon() && !hasPendingBeacon()); +} + +bool ResearcherModel::hasEligibleProjects() const +{ + return m_researcher->Id().Which() == MiningId::Kind::CPID; +} + +bool ResearcherModel::hasActiveBeacon() const +{ + return m_beacon && !m_beacon->Expired(GetAdjustedTime()); +} + +bool ResearcherModel::hasPendingBeacon() const +{ + return m_pending_beacon.operator bool(); +} + +bool ResearcherModel::hasRenewableBeacon() const +{ + return m_beacon && m_beacon->Renewable(GetAdjustedTime()); +} + +bool ResearcherModel::hasMagnitude() const +{ + return m_researcher->Magnitude() != 0; +} + +bool ResearcherModel::needsBeaconAuth() const +{ + return !hasRenewableBeacon() + && hasPendingBeacon() + && IsV11Enabled(nBestHeight + 1); +} + +QString ResearcherModel::email() const +{ + return QString::fromStdString(Researcher::Email()); +} + +QString ResearcherModel::formatCpid() const +{ + return QString::fromStdString(m_researcher->Id().ToString()); +} + +QString ResearcherModel::formatMagnitude() const +{ + return QString::fromStdString(m_researcher->Magnitude().ToString()); +} + +QString ResearcherModel::formatAccrual(const int display_unit) const +{ + return BitcoinUnits::formatWithUnit(display_unit, m_researcher->Accrual()); +} + +QString ResearcherModel::formatStatus() const +{ + if (configuredForInvestorMode()) { + return tr("Researcher mode disabled by configuration"); + } + + // TODO: The getmininginfo RPC shares this global. Refactor to remove it: + return QString::fromStdString(msMiningErrors); +} + +QString ResearcherModel::formatBoincPath() const +{ + return QString::fromStdString(GetBoincDataDir().string()); +} + +BeaconStatus ResearcherModel::getBeaconStatus() const +{ + return m_beacon_status; +} + +QString ResearcherModel::formatBeaconStatus() const +{ + return mapBeaconStatus(m_beacon_status); +} + +QIcon ResearcherModel::getBeaconStatusIcon() const +{ + return mapBeaconStatusIcon(m_beacon_status); } QString ResearcherModel::formatBeaconAge() const @@ -213,6 +322,119 @@ QString ResearcherModel::formatBeaconAddress() const return QString::fromStdString(m_beacon->GetAddress().ToString()); } +QString ResearcherModel::formatBeaconKeyId() const +{ + if (!m_pending_beacon) { + return QString(); + } + + return QString::fromStdString(m_pending_beacon->GetId().ToString()); +} + +std::vector ResearcherModel::buildProjectTable(bool with_mag) const +{ + // We do a funny dance here to link-up three loosly-related record types: + // + // - Local BOINC projects detected from client_state.xml + // - Projects on the Gridcoin whitelist + // - Project magnitude statistics produced by the scrapers + // + // ...into an overview of all three that shows how a participant's attached + // projects behave in the network. + // + + const WhitelistSnapshot whitelist = GetWhitelist().Snapshot(); + std::vector explain_mag; + std::map rows; + + if (with_mag) { + if (const CpidOption cpid = m_researcher->Id().TryCpid()) { + explain_mag = NN::Quorum::ExplainMagnitude(*cpid); + } + } + + for (const auto& project_pair : m_researcher->Projects()) { + const MiningProject& project = project_pair.second; + + ProjectRow row; + row.m_magnitude = 0.0; + + if (!project.m_cpid.IsZero()) { + row.m_cpid = QString::fromStdString(project.m_cpid.ToString()); + } + + if (!project.Eligible()) { + row.m_error = QString::fromStdString(project.ErrorMessage()); + } + + // Project whitelist contracts may not contain names that match the + // project names in BOINC's client_state.xml file. We use a routine + // that also compares the project URL to establish the relationship + // between local projects and whitelisted projects: + // + if (const Project* whitelist_project = project.TryWhitelist(whitelist)) { + row.m_whitelisted = true; + row.m_name = QString::fromStdString(whitelist_project->DisplayName()).toLower(); + + for (const auto& explain_mag_project : explain_mag) { + if (explain_mag_project.m_name == whitelist_project->m_name) { + row.m_magnitude = explain_mag_project.m_magnitude; + break; + } + } + + rows.emplace(whitelist_project->m_name, std::move(row)); + } else { + row.m_whitelisted = false; + row.m_name = QString::fromStdString(project.m_name).toLower(); + + if (project.Eligible()) { + row.m_error = tr("Not whitelisted"); + } + + rows.emplace(project.m_name, std::move(row)); + } + } + + // Add any whitelisted projects not detected from the local BOINC client: + // + for (const auto& project : GetWhitelist().Snapshot()) { + if (rows.find(project.m_name) != rows.end()) { + continue; + } + + ProjectRow row; + row.m_whitelisted = true; + row.m_name = QString::fromStdString(project.DisplayName()).toLower(); + row.m_magnitude = 0.0; + row.m_error = tr("Not attached"); + + for (const auto& explain_mag_project : explain_mag) { + if (explain_mag_project.m_name == project.m_name) { + row.m_magnitude = explain_mag_project.m_magnitude; + break; + } + } + + rows.emplace(project.m_name, std::move(row)); + } + + std::vector rows_out; + rows_out.reserve(rows.size()); + + for (auto& row_pair : rows) { + rows_out.emplace_back(std::move(row_pair.second)); + } + + return rows_out; +} + +void ResearcherModel::reload() +{ + Researcher::Reload(); + resetResearcher(Researcher::Get()); +} + void ResearcherModel::refresh() { updateBeacon(); @@ -229,6 +451,11 @@ void ResearcherModel::resetResearcher(ResearcherPtr researcher) updateBeacon(); } +bool ResearcherModel::updateEmail(const QString& email) +{ + return m_researcher->UpdateEmail(email.toUtf8().constData()); +} + void ResearcherModel::updateBeacon() { const CpidOption cpid = m_researcher->Id().TryCpid(); @@ -242,40 +469,54 @@ void ResearcherModel::updateBeacon() return; } - { - LOCK(cs_main); - const BeaconOption beacon = GetBeaconRegistry().Try(*cpid); - - if (!beacon) { - m_beacon.reset(nullptr); - m_beacon_status = BeaconStatus::NO_BEACON; + if (auto beacon_option = m_researcher->TryBeacon()) { + m_beacon.reset(new Beacon(std::move(*beacon_option))); + } else { + m_beacon.reset(nullptr); + } - emit beaconChanged(); + if (auto beacon_option = m_researcher->TryPendingBeacon()) { + m_pending_beacon.reset(new Beacon(std::move(*beacon_option))); + } else { + m_pending_beacon.reset(nullptr); + } - return; - } + m_beacon_status = MapAdvertiseBeaconError(m_researcher->BeaconError()); - m_beacon.reset(new Beacon(*beacon)); - m_beacon_status = MapAdvertiseBeaconError(m_researcher->BeaconError()); + if (m_beacon_status != BeaconStatus::ACTIVE) { + emit beaconChanged(); + return; } - if (m_beacon_status == BeaconStatus::ACTIVE) { - const int64_t now = GetAdjustedTime(); - - if (m_beacon->Age(now) >= BEACON_RENEWAL_WARNING_AGE) { - m_beacon_status = BeaconStatus::RENEWAL_NEEDED; - } else if (m_beacon->Renewable(now)) { - m_beacon_status = BeaconStatus::RENEWAL_POSSIBLE; - } else if (m_researcher->Magnitude() == 0) { - m_beacon_status = BeaconStatus::NO_MAGNITUDE; - } else { - m_beacon_status = BeaconStatus::ACTIVE; - } + const int64_t now = GetAdjustedTime(); + + if (m_pending_beacon) { + m_beacon_status = BeaconStatus::PENDING; + } else if (m_beacon->Age(now) >= BEACON_RENEWAL_WARNING_AGE) { + m_beacon_status = BeaconStatus::RENEWAL_NEEDED; + } else if (m_beacon->Renewable(now)) { + m_beacon_status = BeaconStatus::RENEWAL_POSSIBLE; + } else if (m_researcher->Magnitude() == 0) { + m_beacon_status = BeaconStatus::NO_MAGNITUDE; + } else { + m_beacon_status = BeaconStatus::ACTIVE; } emit beaconChanged(); } +BeaconStatus ResearcherModel::advertiseBeacon() +{ + const AdvertiseBeaconResult result = m_researcher->AdvertiseBeacon(); + + return MapAdvertiseBeaconError(result.Error()); +} + +void ResearcherModel::onWizardClose() +{ + m_wizard_open = false; +} + void ResearcherModel::subscribeToCoreSignals() { // Connect signals to client diff --git a/src/qt/researcher/researchermodel.h b/src/qt/researcher/researchermodel.h index 8519516a3c..d3507b36ee 100644 --- a/src/qt/researcher/researchermodel.h +++ b/src/qt/researcher/researchermodel.h @@ -4,7 +4,12 @@ #include #include +QT_BEGIN_NAMESPACE class QIcon; +QT_END_NAMESPACE + +class ResearcherWizard; +class WalletModel; namespace NN { class Beacon; @@ -24,6 +29,7 @@ enum class BeaconStatus ACTIVE, ERROR_INSUFFICIENT_FUNDS, ERROR_MISSING_KEY, + ERROR_NOT_NEEDED, ERROR_TX_FAILED, ERROR_WALLET_LOCKED, NO_BEACON, @@ -35,6 +41,26 @@ enum class BeaconStatus UNKNOWN, }; +//! +//! \brief Combined information about a BOINC project to display in a table. +//! +//! These objects incorporate BOINC project context from: +//! +//! - The Gridcoin whitelist +//! - Local BOINC projects detected in client_state.xml +//! - Scraper magnitude values for a CPID +//! +class ProjectRow +{ +public: + bool m_eligible; + bool m_whitelisted; + QString m_name; + QString m_cpid; + double m_magnitude; + QString m_error; +}; + //! //! \brief Presents researcher context state for UI components. //! @@ -46,11 +72,26 @@ class ResearcherModel : public QObject ResearcherModel(); ~ResearcherModel(); + static QString mapBeaconStatus(const BeaconStatus status); + static QIcon mapBeaconStatusIcon(const BeaconStatus status); + + void showWizard(WalletModel* wallet_model); + bool configuredForInvestorMode() const; + bool actionNeeded() const; + bool hasEligibleProjects() const; + bool hasActiveBeacon() const; + bool hasPendingBeacon() const; + bool hasRenewableBeacon() const; + bool hasMagnitude() const; + bool needsBeaconAuth() const; + + QString email() const; QString formatCpid() const; QString formatMagnitude() const; QString formatAccrual(const int display_unit) const; QString formatStatus() const; + QString formatBoincPath() const; BeaconStatus getBeaconStatus() const; QIcon getBeaconStatusIcon() const; @@ -58,12 +99,17 @@ class ResearcherModel : public QObject QString formatBeaconAge() const; QString formatTimeToBeaconExpiration() const; QString formatBeaconAddress() const; + QString formatBeaconKeyId() const; + + std::vector buildProjectTable(bool with_mag = true) const; private: NN::ResearcherPtr m_researcher; std::unique_ptr m_beacon; + std::unique_ptr m_pending_beacon; BeaconStatus m_beacon_status; bool m_configured_for_investor_mode; + bool m_wizard_open; void subscribeToCoreSignals(); void unsubscribeFromCoreSignals(); @@ -74,9 +120,13 @@ class ResearcherModel : public QObject void accrualChanged(); public slots: + void reload(); void refresh(); void resetResearcher(NN::ResearcherPtr researcher); + bool updateEmail(const QString& email); void updateBeacon(); + BeaconStatus advertiseBeacon(); + void onWizardClose(); }; #endif // RESEARCHERMODEL_H diff --git a/src/qt/researcher/researcherwizard.cpp b/src/qt/researcher/researcherwizard.cpp new file mode 100644 index 0000000000..645585213c --- /dev/null +++ b/src/qt/researcher/researcherwizard.cpp @@ -0,0 +1,76 @@ +#include "qt/forms/ui_researcherwizard.h" +#include "qt/researcher/researchermodel.h" +#include "qt/researcher/researcherwizard.h" + +#include "logging.h" + +namespace { +//! +//! \brief Alias for the "start over" custom wizard button. +//! +constexpr QWizard::WizardButton START_OVER_BUTTON = QWizard::CustomButton1; +} // Anonymous namespace + +// ----------------------------------------------------------------------------- +// Class: ResearcherWizard +// ----------------------------------------------------------------------------- + +ResearcherWizard::ResearcherWizard( + QWidget *parent, + ResearcherModel* researcher_model, + WalletModel* wallet_model) + : QWizard(parent) + , ui(new Ui::ResearcherWizard) + , m_researcher_model(researcher_model) +{ + ui->setupUi(this); + configureStartOverButton(); + + setAttribute(Qt::WA_DeleteOnClose, true); + + ui->emailPage->setModel(researcher_model); + ui->projectsPage->setModel(researcher_model); + ui->beaconPage->setModel(researcher_model, wallet_model); + ui->authPage->setModel(researcher_model); + ui->summaryPage->setModel(researcher_model); + + // This enables the beacon "renew" button on the summary page to switch the + // current page back to beacon page. Since the summary page cannot navigate + // back to a specific page directly, it emits a signal that we wire up here + // to change the page when requested: + // + connect(ui->summaryPage, SIGNAL(renewBeaconButtonClicked()), + this, SLOT(onRenewBeaconButtonClicked())); +} + +ResearcherWizard::~ResearcherWizard() +{ + m_researcher_model->onWizardClose(); + + delete ui; +} + +void ResearcherWizard::configureStartOverButton() +{ + setButtonText(START_OVER_BUTTON, tr("&Start Over")); + connect(this, SIGNAL(customButtonClicked(int)), + this, SLOT(onCustomButtonClicked(int))); +} + +void ResearcherWizard::onCustomButtonClicked(int which) +{ + if (which == START_OVER_BUTTON) { + setStartId(PageEmail); + restart(); + } +} + +void ResearcherWizard::onRenewBeaconButtonClicked() +{ + // This handles the beacon "renew" button on the summary page. Qt doesn't + // give us a good way to switch directly to the beacon page, so we rewind + // and start from it instead: + // + setStartId(PageBeacon); + restart(); +} diff --git a/src/qt/researcher/researcherwizard.h b/src/qt/researcher/researcherwizard.h new file mode 100644 index 0000000000..3665438563 --- /dev/null +++ b/src/qt/researcher/researcherwizard.h @@ -0,0 +1,45 @@ +#ifndef RESEARCHERWIZARD_H +#define RESEARCHERWIZARD_H + +#include + +class ResearcherModel; +class WalletModel; + +namespace Ui { +class ResearcherWizard; +} + +class ResearcherWizard : public QWizard +{ + Q_OBJECT + +public: + enum Pages + { + PageEmail, + PageProjects, + PageBeacon, + PageAuth, + PageSummary, + }; + + explicit ResearcherWizard( + QWidget *parent = nullptr, + ResearcherModel *researcher_model = nullptr, + WalletModel *wallet_model = nullptr); + + ~ResearcherWizard(); + +private: + Ui::ResearcherWizard *ui; + ResearcherModel *m_researcher_model; + + void configureStartOverButton(); + +private slots: + void onCustomButtonClicked(int which); + void onRenewBeaconButtonClicked(); +}; + +#endif // RESEARCHERWIZARD_H diff --git a/src/qt/researcher/researcherwizardauthpage.cpp b/src/qt/researcher/researcherwizardauthpage.cpp new file mode 100644 index 0000000000..83601f8d69 --- /dev/null +++ b/src/qt/researcher/researcherwizardauthpage.cpp @@ -0,0 +1,56 @@ +#include "qt/forms/ui_researcherwizardauthpage.h" +#include "qt/researcher/researchermodel.h" +#include "qt/researcher/researcherwizardauthpage.h" + +#include + +// ----------------------------------------------------------------------------- +// Class: ResearcherWizardAuthPage +// ----------------------------------------------------------------------------- + +ResearcherWizardAuthPage::ResearcherWizardAuthPage(QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::ResearcherWizardAuthPage) + , m_researcher_model(nullptr) +{ + ui->setupUi(this); +} + +ResearcherWizardAuthPage::~ResearcherWizardAuthPage() +{ + delete ui; +} + +void ResearcherWizardAuthPage::setModel(ResearcherModel* researcher_model) +{ + this->m_researcher_model = researcher_model; + + if (!m_researcher_model) { + return; + } + + connect(m_researcher_model, SIGNAL(researcherChanged()), this, SLOT(refresh())); +} + +void ResearcherWizardAuthPage::initializePage() +{ + if (!m_researcher_model) { + return; + } + + refresh(); +} + +void ResearcherWizardAuthPage::refresh() +{ + if (!m_researcher_model) { + return; + } + + ui->verificationCodeLabel->setText(m_researcher_model->formatBeaconKeyId()); +} + +void ResearcherWizardAuthPage::on_copyToClipboardButton_clicked() +{ + QApplication::clipboard()->setText(ui->verificationCodeLabel->text()); +} diff --git a/src/qt/researcher/researcherwizardauthpage.h b/src/qt/researcher/researcherwizardauthpage.h new file mode 100644 index 0000000000..0e1dfdb763 --- /dev/null +++ b/src/qt/researcher/researcherwizardauthpage.h @@ -0,0 +1,33 @@ +#ifndef RESEARCHERWIZARDAUTHPAGE_H +#define RESEARCHERWIZARDAUTHPAGE_H + +#include + +class ResearcherModel; + +namespace Ui { +class ResearcherWizardAuthPage; +} + +class ResearcherWizardAuthPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit ResearcherWizardAuthPage(QWidget *parent = nullptr); + ~ResearcherWizardAuthPage(); + + void setModel(ResearcherModel *researcher_model); + + void initializePage() override; + +private: + Ui::ResearcherWizardAuthPage *ui; + ResearcherModel *m_researcher_model; + +private slots: + void refresh(); + void on_copyToClipboardButton_clicked(); +}; + +#endif // RESEARCHERWIZARDAUTHPAGE_H diff --git a/src/qt/researcher/researcherwizardbeaconpage.cpp b/src/qt/researcher/researcherwizardbeaconpage.cpp new file mode 100644 index 0000000000..b526aa5dae --- /dev/null +++ b/src/qt/researcher/researcherwizardbeaconpage.cpp @@ -0,0 +1,114 @@ +#include "qt/forms/ui_researcherwizardbeaconpage.h" +#include "qt/researcher/researchermodel.h" +#include "qt/researcher/researcherwizard.h" +#include "qt/researcher/researcherwizardbeaconpage.h" +#include "qt/walletmodel.h" + +// ----------------------------------------------------------------------------- +// Class: ResearcherWizardBeaconPage +// ----------------------------------------------------------------------------- + +ResearcherWizardBeaconPage::ResearcherWizardBeaconPage(QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::ResearcherWizardBeaconPage) + , m_researcher_model(nullptr) + , m_wallet_model(nullptr) +{ + ui->setupUi(this); +} + +ResearcherWizardBeaconPage::~ResearcherWizardBeaconPage() +{ + delete ui; +} + +void ResearcherWizardBeaconPage::setModel( + ResearcherModel* researcher_model, + WalletModel* wallet_model) +{ + this->m_researcher_model = researcher_model; + this->m_wallet_model = wallet_model; + + if (!m_researcher_model || !m_wallet_model) { + return; + } + + connect(m_researcher_model, SIGNAL(researcherChanged()), this, SLOT(refresh())); + connect(ui->sendBeaconButton, SIGNAL(clicked()), this, SLOT(advertiseBeacon())); +} + +void ResearcherWizardBeaconPage::initializePage() +{ + if (!m_researcher_model) { + return; + } + + refresh(); + updateBeaconIcon(m_researcher_model->getBeaconStatusIcon()); + updateBeaconStatus(m_researcher_model->formatBeaconStatus()); +} + +bool ResearcherWizardBeaconPage::isComplete() const +{ + return m_researcher_model->hasActiveBeacon() + || m_researcher_model->hasPendingBeacon(); +} + +int ResearcherWizardBeaconPage::nextId() const +{ + if (m_researcher_model->needsBeaconAuth()) { + return ResearcherWizard::PageAuth; + } + + return ResearcherWizard::PageSummary; +} + +bool ResearcherWizardBeaconPage::isEnabled() const +{ + return !isComplete() || m_researcher_model->hasRenewableBeacon(); +} + +void ResearcherWizardBeaconPage::refresh() +{ + if (!m_researcher_model) { + return; + } + + ui->cpidLabel->setText(m_researcher_model->formatCpid()); + ui->sendBeaconButton->setVisible(isEnabled()); + ui->continuePromptWrapper->setVisible(!isEnabled()); +} + +void ResearcherWizardBeaconPage::advertiseBeacon() +{ + if (!m_researcher_model || !m_wallet_model) { + return; + } + + const WalletModel::UnlockContext unlock_context(m_wallet_model->requestUnlock()); + + if (!unlock_context.isValid()) { + // Unlock wallet was cancelled + return; + } + + const BeaconStatus status = m_researcher_model->advertiseBeacon(); + + refresh(); + updateBeaconStatus(ResearcherModel::mapBeaconStatus(status)); + updateBeaconIcon(ResearcherModel::mapBeaconStatusIcon(status)); + + emit completeChanged(); +} + +void ResearcherWizardBeaconPage::updateBeaconStatus(const QString& status) +{ + ui->beaconStatusLabel->setText(status); +} + +void ResearcherWizardBeaconPage::updateBeaconIcon(const QIcon& icon) +{ + const int icon_size = ui->beaconIconLabel->width(); + + ui->beaconIconLabel->setPixmap(icon.pixmap(icon_size, icon_size)); +} diff --git a/src/qt/researcher/researcherwizardbeaconpage.h b/src/qt/researcher/researcherwizardbeaconpage.h new file mode 100644 index 0000000000..461da6ee71 --- /dev/null +++ b/src/qt/researcher/researcherwizardbeaconpage.h @@ -0,0 +1,42 @@ +#ifndef RESEARCHERWIZARDBEACONPAGE_H +#define RESEARCHERWIZARDBEACONPAGE_H + +#include + +class QIcon; +class ResearcherModel; +class WalletModel; + +namespace Ui { +class ResearcherWizardBeaconPage; +} + +class ResearcherWizardBeaconPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit ResearcherWizardBeaconPage(QWidget *parent = nullptr); + ~ResearcherWizardBeaconPage(); + + void setModel(ResearcherModel *researcher_model, WalletModel *wallet_model); + + void initializePage() override; + bool isComplete() const override; + int nextId() const override; + + bool isEnabled() const; + +private: + Ui::ResearcherWizardBeaconPage *ui; + ResearcherModel *m_researcher_model; + WalletModel *m_wallet_model; + +private slots: + void refresh(); + void advertiseBeacon(); + void updateBeaconStatus(const QString& status); + void updateBeaconIcon(const QIcon& icon); +}; + +#endif // RESEARCHERWIZARDBEACONPAGE_H diff --git a/src/qt/researcher/researcherwizardemailpage.cpp b/src/qt/researcher/researcherwizardemailpage.cpp new file mode 100644 index 0000000000..945df7bec7 --- /dev/null +++ b/src/qt/researcher/researcherwizardemailpage.cpp @@ -0,0 +1,33 @@ +#include "qt/forms/ui_researcherwizardemailpage.h" +#include "qt/researcher/researchermodel.h" +#include "qt/researcher/researcherwizardemailpage.h" + +// ----------------------------------------------------------------------------- +// Class: ResearcherWizardEmailPage +// ----------------------------------------------------------------------------- + +ResearcherWizardEmailPage::ResearcherWizardEmailPage(QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::ResearcherWizardEmailPage) + , m_model(nullptr) +{ + ui->setupUi(this); + + // The asterisk denotes a mandatory field: + registerField("emailAddress*", ui->emailAddressLineEdit); +} + +ResearcherWizardEmailPage::~ResearcherWizardEmailPage() +{ + delete ui; +} + +void ResearcherWizardEmailPage::setModel(ResearcherModel *model) +{ + this->m_model = model; +} + +void ResearcherWizardEmailPage::initializePage() +{ + ui->emailAddressLineEdit->setText(m_model->email()); +} diff --git a/src/qt/researcher/researcherwizardemailpage.h b/src/qt/researcher/researcherwizardemailpage.h new file mode 100644 index 0000000000..cb8ca408c4 --- /dev/null +++ b/src/qt/researcher/researcherwizardemailpage.h @@ -0,0 +1,29 @@ +#ifndef RESEARCHERWIZARDEMAILPAGE_H +#define RESEARCHERWIZARDEMAILPAGE_H + +#include + +class ResearcherModel; + +namespace Ui { +class ResearcherWizardEmailPage; +} + +class ResearcherWizardEmailPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit ResearcherWizardEmailPage(QWidget *parent = nullptr); + ~ResearcherWizardEmailPage(); + + void setModel(ResearcherModel *model); + + void initializePage() override; + +private: + Ui::ResearcherWizardEmailPage *ui; + ResearcherModel *m_model; +}; + +#endif // RESEARCHERWIZARDEMAILPAGE_H diff --git a/src/qt/researcher/researcherwizardprojectspage.cpp b/src/qt/researcher/researcherwizardprojectspage.cpp new file mode 100644 index 0000000000..5abd560923 --- /dev/null +++ b/src/qt/researcher/researcherwizardprojectspage.cpp @@ -0,0 +1,94 @@ +#include "qt/forms/ui_researcherwizardprojectspage.h" +#include "qt/researcher/projecttablemodel.h" +#include "qt/researcher/researchermodel.h" +#include "qt/researcher/researcherwizardprojectspage.h" + +#include + +// ----------------------------------------------------------------------------- +// Class: ResearcherWizardProjectsPage +// ----------------------------------------------------------------------------- + +ResearcherWizardProjectsPage::ResearcherWizardProjectsPage(QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::ResearcherWizardProjectsPage) + , m_researcher_model(nullptr) + , m_table_model(nullptr) +{ + ui->setupUi(this); + ui->projectTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); +} + +ResearcherWizardProjectsPage::~ResearcherWizardProjectsPage() +{ + delete ui; + delete m_table_model; +} + +void ResearcherWizardProjectsPage::setModel(ResearcherModel *model) +{ + this->m_researcher_model = model; + + if (!m_researcher_model) { + return; + } + + m_table_model = new ProjectTableModel(model, false); + + if (!m_table_model) { + return; + } + + ui->projectTableView->setModel(m_table_model); + ui->projectTableView->hideColumn(ProjectTableModel::Magnitude); + + connect(ui->refreshButton, SIGNAL(clicked()), this, SLOT(refresh())); +} + +void ResearcherWizardProjectsPage::initializePage() +{ + if (!m_researcher_model) { + return; + } + + // Process the email address input from the previous page: + if (!m_researcher_model->updateEmail(field("emailAddress").toString())) { + QMessageBox::warning( + this, + windowTitle(), + tr("An error occured while saving the email address to the " + "configuration file. Please see debug.log for details."), + QMessageBox::Ok, + QMessageBox::Ok); + } + + refresh(); +} + +bool ResearcherWizardProjectsPage::isComplete() const +{ + return m_researcher_model->hasEligibleProjects(); +} + +void ResearcherWizardProjectsPage::refresh() +{ + m_researcher_model->reload(); + + ui->emailLabel->setText(m_researcher_model->email()); + ui->boincPathLabel->setText(m_researcher_model->formatBoincPath()); + ui->selectedCpidLabel->setText(m_researcher_model->formatCpid()); + + const int icon_size = ui->selectedCpidIconLabel->width(); + + if (m_researcher_model->hasEligibleProjects()) { + ui->selectedCpidIconLabel->setPixmap( + QIcon(":/icons/synced").pixmap(icon_size, icon_size)); + } else { + ui->selectedCpidIconLabel->setPixmap( + QIcon(":/icons/white_and_red_x").pixmap(icon_size, icon_size)); + } + + m_table_model->refresh(); + + emit completeChanged(); +} diff --git a/src/qt/researcher/researcherwizardprojectspage.h b/src/qt/researcher/researcherwizardprojectspage.h new file mode 100644 index 0000000000..c0f1677bd5 --- /dev/null +++ b/src/qt/researcher/researcherwizardprojectspage.h @@ -0,0 +1,35 @@ +#ifndef RESEARCHERWIZARDPROJECTSPAGE_H +#define RESEARCHERWIZARDPROJECTSPAGE_H + +#include + +class ProjectTableModel; +class ResearcherModel; + +namespace Ui { +class ResearcherWizardProjectsPage; +} + +class ResearcherWizardProjectsPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit ResearcherWizardProjectsPage(QWidget *parent = nullptr); + ~ResearcherWizardProjectsPage(); + + void setModel(ResearcherModel *model); + + void initializePage() override; + bool isComplete() const override; + +private: + Ui::ResearcherWizardProjectsPage *ui; + ResearcherModel *m_researcher_model; + ProjectTableModel *m_table_model; + +private slots: + void refresh(); +}; + +#endif // RESEARCHERWIZARDPROJECTSPAGE_H diff --git a/src/qt/researcher/researcherwizardsummarypage.cpp b/src/qt/researcher/researcherwizardsummarypage.cpp new file mode 100644 index 0000000000..ad9d7f86d0 --- /dev/null +++ b/src/qt/researcher/researcherwizardsummarypage.cpp @@ -0,0 +1,155 @@ +#include "qt/bitcoinunits.h" +#include "qt/forms/ui_researcherwizardsummarypage.h" +#include "qt/researcher/projecttablemodel.h" +#include "qt/researcher/researchermodel.h" +#include "qt/researcher/researcherwizardsummarypage.h" + +// ----------------------------------------------------------------------------- +// Class: ResearcherWizardSummaryPage +// ----------------------------------------------------------------------------- + +ResearcherWizardSummaryPage::ResearcherWizardSummaryPage(QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::ResearcherWizardSummaryPage) + , m_researcher_model(nullptr) + , m_table_model(nullptr) +{ + ui->setupUi(this); + ui->projectTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); +} + +ResearcherWizardSummaryPage::~ResearcherWizardSummaryPage() +{ + delete ui; +} + +void ResearcherWizardSummaryPage::setModel(ResearcherModel *model) +{ + this->m_researcher_model = model; + + if (!m_researcher_model) { + return; + } + + m_table_model = new ProjectTableModel(model); + + if (!m_table_model) { + return; + } + + ui->projectTableView->setModel(m_table_model); + + connect(model, SIGNAL(researcherChanged()), this, SLOT(refreshSummary())); + connect(ui->refreshButton, SIGNAL(clicked()), this, SLOT(reloadProjects())); + connect(ui->tabWidget, SIGNAL(currentChanged(int)), this, SLOT(onTabChanged(int))); +} + +void ResearcherWizardSummaryPage::initializePage() +{ + if (!m_researcher_model) { + return; + } + + refreshSummary(); +} + +void ResearcherWizardSummaryPage::onTabChanged(int index) +{ + switch (index) { + case TabSummary: + refreshSummary(); + break; + case TabProjects: + refreshProjects(); + break; + } +} + +void ResearcherWizardSummaryPage::refreshSummary() +{ + if (ui->tabWidget->currentIndex() != TabSummary) { + return; + } + + const BitcoinUnits::Unit unit = BitcoinUnits::BTC; + const int icon_size = ui->beaconDetailsIconLabel->width(); + + ui->cpidLabel->setText(m_researcher_model->formatCpid()); + ui->statusLabel->setText(m_researcher_model->formatStatus()); + ui->magnitudeLabel->setText(m_researcher_model->formatMagnitude()); + ui->accrualLabel->setText(m_researcher_model->formatAccrual(unit)); + + ui->beaconDetailsIconLabel->setPixmap( + m_researcher_model->getBeaconStatusIcon().pixmap(icon_size, icon_size)); + ui->beaconStatusLabel->setText(m_researcher_model->formatBeaconStatus()); + ui->beaconAgeLabel->setText(m_researcher_model->formatBeaconAge()); + ui->beaconExpiresLabel->setText(m_researcher_model->formatTimeToBeaconExpiration()); + ui->rainAddressLabel->setText(m_researcher_model->formatBeaconAddress()); + ui->renewBeaconButton->setEnabled(m_researcher_model->hasRenewableBeacon()); + + refreshOverallStatus(); +} + +void ResearcherWizardSummaryPage::refreshOverallStatus() +{ + const int icon_size = ui->overallStatusIconLabel->width(); + + QString status; + QIcon icon; + + if (m_researcher_model->hasPendingBeacon()) { + status = tr("Beacon awaiting confirmation."); + icon = QIcon(":/icons/notsynced"); + } else if (m_researcher_model->hasRenewableBeacon()) { + status = tr("Beacon renewal available."); + icon = QIcon(":/icons/warning"); + } else if (!m_researcher_model->hasMagnitude()) { + status = tr("Waiting for magnitude."); + icon = QIcon(":/icons/notsynced"); + } else { + status = tr("Everything looks good."); + icon = QIcon(":/icons/synced"); + } + + ui->overallStatusLabel->setText(status); + ui->overallStatusIconLabel->setPixmap(icon.pixmap(icon_size, icon_size)); +} + +void ResearcherWizardSummaryPage::refreshProjects() +{ + if (ui->tabWidget->currentIndex() != TabProjects) { + return; + } + + ui->emailLabel->setText(m_researcher_model->email()); + ui->boincPathLabel->setText(m_researcher_model->formatBoincPath()); + ui->selectedCpidLabel->setText(m_researcher_model->formatCpid()); + + const int icon_size = ui->selectedCpidIconLabel->width(); + + if (m_researcher_model->hasEligibleProjects()) { + ui->selectedCpidIconLabel->setPixmap( + QIcon(":/icons/synced").pixmap(icon_size, icon_size)); + } else { + ui->selectedCpidIconLabel->setPixmap( + QIcon(":/icons/white_and_red_x").pixmap(icon_size, icon_size)); + } + + m_table_model->refresh(); +} + +void ResearcherWizardSummaryPage::reloadProjects() +{ + m_researcher_model->reload(); + refreshProjects(); +} + +void ResearcherWizardSummaryPage::on_renewBeaconButton_clicked() +{ + // This enables the page's beacon "renew" button to switch the current page + // back to beacon page. Since a wizard page cannot set the wizard's current + // index directly, this emits a signal that the wizard connects to the slot + // that changes the page for us: + // + emit renewBeaconButtonClicked(); +} diff --git a/src/qt/researcher/researcherwizardsummarypage.h b/src/qt/researcher/researcherwizardsummarypage.h new file mode 100644 index 0000000000..23f882cdb7 --- /dev/null +++ b/src/qt/researcher/researcherwizardsummarypage.h @@ -0,0 +1,48 @@ +#ifndef RESEARCHERWIZARDSUMMARYPAGE_H +#define RESEARCHERWIZARDSUMMARYPAGE_H + +#include + +class ProjectTableModel; +class ResearcherModel; + +namespace Ui { +class ResearcherWizardSummaryPage; +} + +class ResearcherWizardSummaryPage : public QWizardPage +{ + Q_OBJECT + + enum Tab + { + TabSummary, + TabProjects, + }; + +public: + explicit ResearcherWizardSummaryPage(QWidget *parent = nullptr); + ~ResearcherWizardSummaryPage(); + + void setModel(ResearcherModel *model); + + void initializePage() override; + +private: + Ui::ResearcherWizardSummaryPage *ui; + ResearcherModel *m_researcher_model; + ProjectTableModel *m_table_model; + +signals: + void renewBeaconButtonClicked(); + +private slots: + void onTabChanged(int index); + void refreshSummary(); + void refreshOverallStatus(); + void refreshProjects(); + void reloadProjects(); + void on_renewBeaconButton_clicked(); +}; + +#endif // RESEARCHERWIZARDSUMMARYPAGE_H From 76c4327c0af4303282699f556d0a0da45de3728d Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:41:27 -0500 Subject: [PATCH 17/22] Add researcher wizard to settings and tray icon menus --- src/qt/bitcoingui.cpp | 18 ++++++++++++++++++ src/qt/bitcoingui.h | 3 +++ 2 files changed, 21 insertions(+) diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index d036fef078..9534ed1bcd 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -319,6 +319,9 @@ void BitcoinGUI::createActions() optionsAction = new QAction(tr("&Options..."), this); optionsAction->setToolTip(tr("Modify configuration options for Gridcoin")); optionsAction->setMenuRole(QAction::PreferencesRole); + researcherAction = new QAction(tr("&Researcher Wizard..."), this); + researcherAction->setToolTip(tr("Open BOINC and beacon settings for Gridcoin")); + researcherAction->setMenuRole(QAction::PreferencesRole); toggleHideAction = new QAction(tr("&Show / Hide"), this); encryptWalletAction = new QAction(tr("&Encrypt Wallet..."), this); encryptWalletAction->setToolTip(tr("Encrypt or decrypt wallet")); @@ -344,6 +347,7 @@ void BitcoinGUI::createActions() connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit())); connect(aboutAction, SIGNAL(triggered()), this, SLOT(aboutClicked())); connect(optionsAction, SIGNAL(triggered()), this, SLOT(optionsClicked())); + connect(researcherAction, SIGNAL(triggered()), this, SLOT(researcherClicked())); connect(toggleHideAction, SIGNAL(triggered()), this, SLOT(toggleHidden())); connect(encryptWalletAction, SIGNAL(triggered(bool)), this, SLOT(encryptWallet(bool))); connect(backupWalletAction, SIGNAL(triggered()), this, SLOT(backupWallet())); @@ -378,6 +382,7 @@ void BitcoinGUI::setIcons() aboutAction->setIcon(QPixmap(":/images/gridcoin")); diagnosticsAction->setIcon(QPixmap(":/images/gridcoin")); optionsAction->setIcon(QPixmap(":/icons/options")); + researcherAction->setIcon(QPixmap(":/images/gridcoin")); toggleHideAction->setIcon(QPixmap(":/images/gridcoin")); backupWalletAction->setIcon(QPixmap(":/icons/filesave")); changePassphraseAction->setIcon(QPixmap(":/icons/key")); @@ -421,6 +426,8 @@ void BitcoinGUI::createMenuBar() settings->addAction(unlockWalletAction); settings->addAction(lockWalletAction); settings->addSeparator(); + settings->addAction(researcherAction); + settings->addSeparator(); settings->addAction(optionsAction); QMenu *community = appMenuBar->addMenu(tr("&Community")); @@ -695,6 +702,8 @@ void BitcoinGUI::createTrayIconMenu() trayIconMenu->addAction(signMessageAction); trayIconMenu->addAction(verifyMessageAction); trayIconMenu->addSeparator(); + trayIconMenu->addAction(researcherAction); + trayIconMenu->addSeparator(); trayIconMenu->addAction(optionsAction); trayIconMenu->addAction(openRPCConsoleAction); #ifndef Q_OS_MAC // This is built-in on Mac @@ -723,6 +732,15 @@ void BitcoinGUI::optionsClicked() dlg.exec(); } +void BitcoinGUI::researcherClicked() +{ + if (!researcherModel || !walletModel) { + return; + } + + researcherModel->showWizard(walletModel); +} + void BitcoinGUI::aboutClicked() { AboutDialog dlg; diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index 746be04620..e4e7bd638c 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -103,6 +103,7 @@ class BitcoinGUI : public QMainWindow QAction *verifyMessageAction; QAction *aboutAction; QAction *receiveCoinsAction; + QAction *researcherAction; QAction *optionsAction; QAction *toggleHideAction; QAction *exportAction; @@ -196,6 +197,8 @@ private slots: /** Show configuration dialog */ void optionsClicked(); + /** Show researcher/beacon configuration dialog */ + void researcherClicked(); /** Show about dialog */ void aboutClicked(); From 5655210d764d2ec4ce4d2afde875a85faf2b335e Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:41:30 -0500 Subject: [PATCH 18/22] Show researcher wizard by clicking beacon status icon This opens the researcher wizard when clicking on the beacon status icon in the status bar. --- src/qt/bitcoingui.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 9534ed1bcd..37878ef96e 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -499,7 +499,8 @@ void BitcoinGUI::createToolBars() connect(labelConnectionsIcon, SIGNAL(clicked()), this, SLOT(peersClicked())); labelBlocksIcon = new QLabel(); labelScraperIcon = new QLabel(); - labelBeaconIcon = new QLabel(); + labelBeaconIcon = new ClickLabel(); + connect(labelBeaconIcon, SIGNAL(clicked()), this, SLOT(researcherClicked())); frameBlocksLayout->addWidget(labelEncryptionIcon); frameBlocksLayout->addWidget(labelStakingIcon); From 0eaa9713ba9972655d5eedfd3ee324aebb7a840f Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 16 Jun 2020 23:41:33 -0500 Subject: [PATCH 19/22] Fix the awful light/dark GUI theme tab styles --- src/qt/res/stylesheets/dark_stylesheet.qss | 48 +++++++++++++------- src/qt/res/stylesheets/light_stylesheet.qss | 38 +++++++++------- src/qt/res/stylesheets/native_stylesheet.qss | 2 - 3 files changed, 52 insertions(+), 36 deletions(-) diff --git a/src/qt/res/stylesheets/dark_stylesheet.qss b/src/qt/res/stylesheets/dark_stylesheet.qss index 0102943652..b6bc4964ae 100644 --- a/src/qt/res/stylesheets/dark_stylesheet.qss +++ b/src/qt/res/stylesheets/dark_stylesheet.qss @@ -10,6 +10,16 @@ BitcoinAmountField{ background-color: rgb(49,54,59); } +/* HLine */ +QFrame[frameShape="4"] { + border-top: 0.065em solid rgb(67, 74, 80); +} + +/* VLine */ +QFrame[frameShape="5"] { + border-left: 0.065em solid rgb(67, 74, 80); +} + QToolButton { color: white; background-color: qlineargradient(x1: 0, y1: 0, x2: 1.0, y2: 1.0,stop: 0 rgb(49,54,59), stop: 1 rgb(49,54,59)); @@ -294,35 +304,41 @@ QDoubleSpinBox{ background-color: rgb(35,38,41); } -QTabWidget{ - background-color: rgb(49,54,59); +QTabWidget::tab-bar { + left: 0.5em; } QTabWidget::pane { /* The tab widget frame */ - border: 0.065em solid rgb(100,100,100); + background-color: rgb(58, 64, 69); + border: 0.065em solid rgb(67, 74, 80); } QTabBar::tab { - background: rgb(64,68,82); - border: 0.065em solid rgb(100,100,100); - min-height:1.25em; + background-color: rgb(49, 54, 59); + border: 0.065em solid rgb(67, 74, 80); + border-bottom: none; + min-height: 1.25em; min-width: 0.5em; - padding: 0.125em; + padding: 0.125em 1em; } -QTabBar::tab:selected { - background: rgb(49,54,59); - border: 0.065em solid rgb(100,100,100); - border-bottom-color: rgb(49,54,59); +QTabBar::tab:!first { + margin-left: -0.065em; } -QTabBar::tab:hover { - background: rgb(65,0,127); - color: white; +QTabBar::tab:selected { + background-color: rgb(58, 64, 69); + border-bottom-color: rgb(58, 64, 69); + border-top-color: rgb(120, 20, 255); + border-top-width: 0.13em; } QTabBar::tab:!selected { - margin-top: 0.18em; /* make non-selected tabs look smaller */ + margin-top: 0.13em; +} + +QTabBar::tab:!selected:hover { + background-color: rgb(58, 64, 69); } QTextEdit{ @@ -526,5 +542,3 @@ QToolBar#toolbar3 QLabel{ #capsLabel{ font-weight:bold; } - - diff --git a/src/qt/res/stylesheets/light_stylesheet.qss b/src/qt/res/stylesheets/light_stylesheet.qss index 52661cb072..19af199f35 100644 --- a/src/qt/res/stylesheets/light_stylesheet.qss +++ b/src/qt/res/stylesheets/light_stylesheet.qss @@ -295,35 +295,41 @@ QDoubleSpinBox{ background-color: white; } -QTabWidget{ - background-color: rgb(240,240,240); +QTabWidget::tab-bar { + left: 0.5em; } QTabWidget::pane { /* The tab widget frame */ - border: 0.065em solid rgb(100,100,100); + background-color: rgb(255, 255, 255); + border: 0.065em solid rgb(200, 200, 200); } QTabBar::tab { - background: rgb(200,200,200); - border: 0.065em solid rgb(100,100,100); - min-height:1.25em; + background-color: rgb(240, 240, 240); + border: 0.065em solid rgb(200, 200, 200); + border-bottom: none; + min-height: 1.25em; min-width: 0.5em; - padding: 0.125em; + padding: 0.125em 1em; } -QTabBar::tab:selected { - background: rgb(240,240,240); - border: 0.065em solid rgb(100,100,100); - border-bottom-color: rgb(240,240,240); +QTabBar::tab:!first { + margin-left: -0.065em; } -QTabBar::tab:hover { - background: rgb(65,0,127); - color: white; +QTabBar::tab:selected { + background-color: rgb(255, 255, 255); + border-bottom-color: rgb(255, 255, 255); + border-top-color: rgb(120, 20, 255); + border-top-width: 0.13em; } QTabBar::tab:!selected { - margin-top: 0.18em; /* make non-selected tabs look smaller */ + margin-top: 0.13em; +} + +QTabBar::tab:!selected:hover { + background: rgb(255, 255, 255); } QTextEdit{ @@ -509,5 +515,3 @@ QToolBar#toolbar3 QLabel{ #capsLabel{ font-weight:bold; } - - diff --git a/src/qt/res/stylesheets/native_stylesheet.qss b/src/qt/res/stylesheets/native_stylesheet.qss index 85966efb7b..4043fc857a 100644 --- a/src/qt/res/stylesheets/native_stylesheet.qss +++ b/src/qt/res/stylesheets/native_stylesheet.qss @@ -168,5 +168,3 @@ QToolBar#toolbar3 QLabel{ #capsLabel{ font-weight:bold; } - - From 402edba20f7dcc2b9ab9bd3fc7eff56dd2b5a252 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Wed, 17 Jun 2020 16:46:43 -0500 Subject: [PATCH 20/22] Fix segfault in accrual age calculation This fixes a segmentation fault caused by dereferencing a null pointer when attempting to calculate accrual age for a CPID with no beacon. --- src/neuralnet/accrual/snapshot.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/neuralnet/accrual/snapshot.h b/src/neuralnet/accrual/snapshot.h index 0d98b5ba5e..29e8da5678 100644 --- a/src/neuralnet/accrual/snapshot.h +++ b/src/neuralnet/accrual/snapshot.h @@ -234,13 +234,15 @@ class SnapshotAccrualComputer : public IAccrualComputer, SnapshotCalculator // a superblock after contract improvements for more accurate age. // if (m_account.IsNew()) { - const int64_t beacon_time = GetBeaconRegistry().Try(m_cpid)->m_timestamp; + if (const BeaconOption beacon = GetBeaconRegistry().Try(m_cpid)) { + const int64_t beacon_time = beacon->m_timestamp; - if (beacon_time <= 0) { - return 0; + if (beacon_time > 0) { + return m_payment_time - beacon_time; + } } - return m_payment_time - beacon_time; + return 0; } return SnapshotCalculator::AccrualAge(m_account); From 0d0ee0414ea67076c81d385175c1f9b635c32ad2 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Thu, 18 Jun 2020 18:30:41 -0500 Subject: [PATCH 21/22] Tweak wizard page descriptions from review suggestions Co-authored-by: RoboticMind <30132912+RoboticMind@users.noreply.github.com> --- src/qt/forms/overviewpage.ui | 2 +- src/qt/forms/researcherwizardbeaconpage.ui | 2 +- src/qt/forms/researcherwizardemailpage.ui | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qt/forms/overviewpage.ui b/src/qt/forms/overviewpage.ui index e74a3a38f0..62bef1618e 100644 --- a/src/qt/forms/overviewpage.ui +++ b/src/qt/forms/overviewpage.ui @@ -551,7 +551,7 @@ - Action needed + Action Needed Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter diff --git a/src/qt/forms/researcherwizardbeaconpage.ui b/src/qt/forms/researcherwizardbeaconpage.ui index 880ea4c71f..09c6d22cce 100644 --- a/src/qt/forms/researcherwizardbeaconpage.ui +++ b/src/qt/forms/researcherwizardbeaconpage.ui @@ -23,7 +23,7 @@ Beacon Advertisement - A beacon is a special type of Gridcoin transaction that links a BOINC CPID to your wallet. After sending a beacon, the network tracks your BOINC statistics to calculate research rewards. + A beacon links your BOINC accounts to your wallet. After sending a beacon, the network tracks your BOINC statistics to calculate research rewards. diff --git a/src/qt/forms/researcherwizardemailpage.ui b/src/qt/forms/researcherwizardemailpage.ui index 5f1ec70570..33a3e202a6 100644 --- a/src/qt/forms/researcherwizardemailpage.ui +++ b/src/qt/forms/researcherwizardemailpage.ui @@ -23,7 +23,7 @@ BOINC Email Address - Confirm the email address that you provided to register the accounts for BOINC projects that you participate in. Gridcoin uses the email address to detect BOINC projects on your computer. + Enter the email address that you use for your BOINC project accounts. Gridcoin uses this email address to find BOINC projects on your computer. From 549868eb7d726841f0ceb3316327cdad8056fef6 Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Thu, 18 Jun 2020 22:46:45 -0500 Subject: [PATCH 22/22] Serialize beacon public key for self-service deletion This removes the condition that omits a beacon public key from a beacon deletion contract so that validation passes for self-service removal. --- src/neuralnet/beacon.cpp | 19 ++++---- src/neuralnet/beacon.h | 29 ++++++++---- src/test/neuralnet/beacon_tests.cpp | 72 +++++++++++------------------ 3 files changed, 57 insertions(+), 63 deletions(-) diff --git a/src/neuralnet/beacon.cpp b/src/neuralnet/beacon.cpp index fd4e3763fe..35f9369013 100644 --- a/src/neuralnet/beacon.cpp +++ b/src/neuralnet/beacon.cpp @@ -167,12 +167,18 @@ BeaconPayload::BeaconPayload() { } -BeaconPayload::BeaconPayload(const Cpid cpid, Beacon beacon) - : m_cpid(cpid) +BeaconPayload::BeaconPayload(const uint32_t version, const Cpid cpid, Beacon beacon) + : m_version(version) + , m_cpid(cpid) , m_beacon(std::move(beacon)) { } +BeaconPayload::BeaconPayload(const Cpid cpid, Beacon beacon) + : BeaconPayload(CURRENT_VERSION, cpid, std::move(beacon)) +{ +} + BeaconPayload BeaconPayload::Parse(const std::string& key, const std::string& value) { const CpidOption cpid = MiningId::Parse(key).TryCpid(); @@ -181,13 +187,8 @@ BeaconPayload BeaconPayload::Parse(const std::string& key, const std::string& va return BeaconPayload(); } - Beacon beacon = Beacon::Parse(value); - - if (!beacon.WellFormed()) { - return BeaconPayload(); - } - - return BeaconPayload(*cpid, std::move(beacon)); + // Legacy beacon payloads always parse to version 1: + return BeaconPayload(1, *cpid, Beacon::Parse(value)); } bool BeaconPayload::Sign(CKey& private_key) diff --git a/src/neuralnet/beacon.h b/src/neuralnet/beacon.h index a6e44c9d19..915e2acd61 100644 --- a/src/neuralnet/beacon.h +++ b/src/neuralnet/beacon.h @@ -217,6 +217,15 @@ class BeaconPayload : public IContractPayload //! BeaconPayload(); + //! + //! \brief Initialize a beacon payload for the specified CPID. + //! + //! \param version Version of the serialized beacon format. + //! \param cpid Identifies the researcher that advertised the beacon. + //! \param beacon Contains the beacon public key. + //! + BeaconPayload(const uint32_t version, const Cpid cpid, Beacon beacon); + //! //! \brief Initialize a beacon payload for the specified CPID. //! @@ -252,15 +261,20 @@ class BeaconPayload : public IContractPayload //! bool WellFormed(const ContractAction action) const override { - return m_version > 0 - && m_version <= CURRENT_VERSION - && (action == ContractAction::REMOVE || m_beacon.WellFormed()) + if (m_version <= 0 || m_version > CURRENT_VERSION) { + return false; + } + + if (m_version == 1) { + return m_beacon.WellFormed() || action == ContractAction::REMOVE; + } + + return m_beacon.WellFormed() // The DER-encoded ASN.1 ECDSA signatures typically contain 70 or // 71 bytes, but may hold up to 73. Sizes as low as 68 bytes seen // on mainnet. We only check the number of bytes here as an early // step: - && (m_version == 1 - || (m_signature.size() >= 64 && m_signature.size() <= 73)); + && (m_signature.size() >= 64 && m_signature.size() <= 73); } //! @@ -316,10 +330,7 @@ class BeaconPayload : public IContractPayload { READWRITE(m_version); READWRITE(m_cpid); - - if (contract_action != ContractAction::REMOVE) { - READWRITE(m_beacon); - } + READWRITE(m_beacon); if (!(s.GetType() & SER_GETHASH)) { READWRITE(m_signature); diff --git a/src/test/neuralnet/beacon_tests.cpp b/src/test/neuralnet/beacon_tests.cpp index 3a44dcc7e3..7b4117d01b 100644 --- a/src/test/neuralnet/beacon_tests.cpp +++ b/src/test/neuralnet/beacon_tests.cpp @@ -255,7 +255,8 @@ BOOST_AUTO_TEST_CASE(it_parses_a_payload_from_a_legacy_contract_key_and_value) const NN::BeaconPayload payload = NN::BeaconPayload::Parse(key, value); - BOOST_CHECK_EQUAL(payload.m_version, NN::BeaconPayload::CURRENT_VERSION); + // Legacy beacon payloads always parse to version 1: + BOOST_CHECK_EQUAL(payload.m_version, 1); BOOST_CHECK(payload.m_cpid == cpid); BOOST_CHECK(payload.m_beacon.m_public_key == TestKey::Public()); BOOST_CHECK_EQUAL(payload.m_beacon.m_timestamp, 0); @@ -274,33 +275,44 @@ BOOST_AUTO_TEST_CASE(it_behaves_like_a_contract_payload) BOOST_CHECK(payload.RequiredBurnAmount() > 0); } -BOOST_AUTO_TEST_CASE(it_checks_whether_the_payload_is_well_formed_for_add) +BOOST_AUTO_TEST_CASE(it_checks_whether_the_payload_is_well_formed) { const NN::Cpid cpid = NN::Cpid::Parse("00010203040506070809101112131415"); NN::BeaconPayload valid(cpid, NN::Beacon(TestKey::Public())); valid.m_signature = TestKey::Signature(); BOOST_CHECK(valid.WellFormed(NN::ContractAction::ADD) == true); + BOOST_CHECK(valid.WellFormed(NN::ContractAction::REMOVE) == true); NN::BeaconPayload zero_cpid{NN::Cpid(), NN::Beacon(TestKey::Public())}; zero_cpid.m_signature = TestKey::Signature(); // A zero CPID is technically valid... BOOST_CHECK(zero_cpid.WellFormed(NN::ContractAction::ADD) == true); + BOOST_CHECK(zero_cpid.WellFormed(NN::ContractAction::REMOVE) == true); NN::BeaconPayload missing_key(cpid, NN::Beacon()); missing_key.m_signature = TestKey::Signature(); BOOST_CHECK(missing_key.WellFormed(NN::ContractAction::ADD) == false); + BOOST_CHECK(missing_key.WellFormed(NN::ContractAction::REMOVE) == false); } -BOOST_AUTO_TEST_CASE(it_checks_whether_the_payload_is_well_formed_for_delete) +BOOST_AUTO_TEST_CASE(it_checks_whether_a_legacy_v1_payload_is_well_formed) { const NN::Cpid cpid = NN::Cpid::Parse("00010203040506070809101112131415"); - NN::BeaconPayload valid(cpid, NN::Beacon()); - valid.m_signature = TestKey::Signature(); + const NN::Beacon beacon(TestKey::Public()); - BOOST_CHECK(valid.WellFormed(NN::ContractAction::REMOVE) == true); + const NN::BeaconPayload add = NN::BeaconPayload(1, cpid, beacon); + + BOOST_CHECK(add.WellFormed(NN::ContractAction::ADD) == true); + // Legacy beacon deletion contracts ignore the value: + BOOST_CHECK(add.WellFormed(NN::ContractAction::REMOVE) == true); + + const NN::BeaconPayload remove = NN::BeaconPayload(1, cpid, NN::Beacon()); + + BOOST_CHECK(remove.WellFormed(NN::ContractAction::ADD) == false); + BOOST_CHECK(remove.WellFormed(NN::ContractAction::REMOVE) == true); } BOOST_AUTO_TEST_CASE(it_signs_the_payload) @@ -335,7 +347,7 @@ BOOST_AUTO_TEST_CASE(it_verifies_the_payload_signature) BOOST_CHECK(payload.VerifySignature()); } -BOOST_AUTO_TEST_CASE(it_serializes_to_a_stream_for_add) +BOOST_AUTO_TEST_CASE(it_serializes_to_a_stream) { const NN::Cpid cpid = NN::Cpid::Parse("00010203040506070809101112131415"); const NN::Beacon beacon(TestKey::Public()); @@ -358,20 +370,22 @@ BOOST_AUTO_TEST_CASE(it_serializes_to_a_stream_for_add) expected.end()); } -BOOST_AUTO_TEST_CASE(it_deserializes_from_a_stream_for_add) +BOOST_AUTO_TEST_CASE(it_deserializes_from_a_stream) { const NN::Cpid cpid = NN::Cpid::Parse("00010203040506070809101112131415"); const NN::Beacon beacon(TestKey::Public()); const std::vector signature = TestKey::Signature(); - CDataStream stream = CDataStream(SER_NETWORK, PROTOCOL_VERSION) + CDataStream stream_add = CDataStream(SER_NETWORK, PROTOCOL_VERSION) << NN::BeaconPayload::CURRENT_VERSION << cpid << beacon << signature; + CDataStream stream_remove = stream_add; + NN::BeaconPayload payload; - payload.Unserialize(stream, NN::ContractAction::ADD); + payload.Unserialize(stream_add, NN::ContractAction::ADD); BOOST_CHECK_EQUAL(payload.m_version, NN::BeaconPayload::CURRENT_VERSION); BOOST_CHECK(payload.m_cpid == cpid); @@ -385,45 +399,13 @@ BOOST_AUTO_TEST_CASE(it_deserializes_from_a_stream_for_add) signature.end()); BOOST_CHECK(payload.WellFormed(NN::ContractAction::ADD) == true); -} -BOOST_AUTO_TEST_CASE(it_serializes_to_a_stream_for_delete) -{ - const NN::Cpid cpid = NN::Cpid::Parse("00010203040506070809101112131415"); - NN::BeaconPayload payload(cpid, NN::Beacon()); - payload.m_signature = TestKey::Signature(); - - const CDataStream expected = CDataStream(SER_NETWORK, PROTOCOL_VERSION) - << NN::BeaconPayload::CURRENT_VERSION - << cpid - << payload.m_signature; - - CDataStream stream(SER_NETWORK, PROTOCOL_VERSION); - payload.Serialize(stream, NN::ContractAction::REMOVE); - - BOOST_CHECK_EQUAL_COLLECTIONS( - stream.begin(), - stream.end(), - expected.begin(), - expected.end()); -} - -BOOST_AUTO_TEST_CASE(it_deserializes_from_a_stream_for_delete) -{ - const NN::Cpid cpid = NN::Cpid::Parse("00010203040506070809101112131415"); - const std::vector signature = TestKey::Signature(); - - CDataStream stream = CDataStream(SER_NETWORK, PROTOCOL_VERSION) - << NN::BeaconPayload::CURRENT_VERSION - << cpid - << signature; - - NN::BeaconPayload payload; - payload.Unserialize(stream, NN::ContractAction::REMOVE); + payload = NN::BeaconPayload(); + payload.Unserialize(stream_remove, NN::ContractAction::REMOVE); BOOST_CHECK_EQUAL(payload.m_version, NN::BeaconPayload::CURRENT_VERSION); BOOST_CHECK(payload.m_cpid == cpid); - BOOST_CHECK(payload.m_beacon.m_public_key.Raw().empty() == true); + BOOST_CHECK(payload.m_beacon.m_public_key == TestKey::Public()); BOOST_CHECK_EQUAL(payload.m_beacon.m_timestamp, 0); BOOST_CHECK_EQUAL_COLLECTIONS(