diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index cbb0c7e46..c145ffc07 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -1,798 +1,798 @@ // Copyright (c) 2011-2019 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_WALLET #include #include #include #endif // ENABLE_WALLET #include #include #include #include #include #include #include #include #include #include #if defined(QT_STATICPLUGIN) #include #if defined(QT_QPA_PLATFORM_XCB) Q_IMPORT_PLUGIN(QXcbIntegrationPlugin); #elif defined(QT_QPA_PLATFORM_WINDOWS) Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); #elif defined(QT_QPA_PLATFORM_COCOA) Q_IMPORT_PLUGIN(QCocoaIntegrationPlugin); #endif #endif // Declare meta types used for QMetaObject::invokeMethod Q_DECLARE_METATYPE(bool *) Q_DECLARE_METATYPE(Amount) Q_DECLARE_METATYPE(SynchronizationState) Q_DECLARE_METATYPE(uint256) // Config is non-copyable so we can only register pointers to it Q_DECLARE_METATYPE(Config *) static QString GetLangTerritory() { QSettings settings; // Get desired locale (e.g. "de_DE") // 1) System default language QString lang_territory = QLocale::system().name(); // 2) Language from QSettings QString lang_territory_qsettings = settings.value("language", "").toString(); if (!lang_territory_qsettings.isEmpty()) { lang_territory = lang_territory_qsettings; } // 3) -lang command line argument lang_territory = QString::fromStdString( gArgs.GetArg("-lang", lang_territory.toStdString())); return lang_territory; } /** Set up translations */ static void initTranslations(QTranslator &qtTranslatorBase, QTranslator &qtTranslator, QTranslator &translatorBase, QTranslator &translator) { // Remove old translators QApplication::removeTranslator(&qtTranslatorBase); QApplication::removeTranslator(&qtTranslator); QApplication::removeTranslator(&translatorBase); QApplication::removeTranslator(&translator); // Get desired locale (e.g. "de_DE") // 1) System default language QString lang_territory = GetLangTerritory(); // Convert to "de" only by truncating "_DE" QString lang = lang_territory; lang.truncate(lang_territory.lastIndexOf('_')); // Load language files for configured locale: // - First load the translator for the base language, without territory // - Then load the more specific locale translator // Load e.g. qt_de.qm if (qtTranslatorBase.load( "qt_" + lang, QLibraryInfo::location(QLibraryInfo::TranslationsPath))) { QApplication::installTranslator(&qtTranslatorBase); } // Load e.g. qt_de_DE.qm if (qtTranslator.load( "qt_" + lang_territory, QLibraryInfo::location(QLibraryInfo::TranslationsPath))) { QApplication::installTranslator(&qtTranslator); } // Load e.g. bitcoin_de.qm (shortcut "de" needs to be defined in // bitcoin.qrc) if (translatorBase.load(lang, ":/translations/")) { QApplication::installTranslator(&translatorBase); } // Load e.g. bitcoin_de_DE.qm (shortcut "de_DE" needs to be defined in // bitcoin.qrc) if (translator.load(lang_territory, ":/translations/")) { QApplication::installTranslator(&translator); } } /* qDebug() message handler --> debug.log */ void DebugMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { Q_UNUSED(context); if (type == QtDebugMsg) { LogPrint(BCLog::QT, "GUI: %s\n", msg.toStdString()); } else { LogPrintf("GUI: %s\n", msg.toStdString()); } } BitcoinABC::BitcoinABC(interfaces::Node &node) : QObject(), m_node(node) {} void BitcoinABC::handleRunawayException(const std::exception *e) { PrintExceptionContinue(e, "Runaway exception"); Q_EMIT runawayException(QString::fromStdString(m_node.getWarnings())); } void BitcoinABC::initialize(Config *config, RPCServer *rpcServer, HTTPRPCRequestProcessor *httpRPCRequestProcessor) { try { qDebug() << __func__ << ": Running initialization in thread"; util::ThreadRename("qt-init"); bool rv = m_node.appInitMain(*config, *rpcServer, *httpRPCRequestProcessor); Q_EMIT initializeResult(rv); } catch (const std::exception &e) { handleRunawayException(&e); } catch (...) { handleRunawayException(nullptr); } } void BitcoinABC::shutdown() { try { qDebug() << __func__ << ": Running Shutdown in thread"; m_node.appShutdown(); qDebug() << __func__ << ": Shutdown finished"; Q_EMIT shutdownResult(); } catch (const std::exception &e) { handleRunawayException(&e); } catch (...) { handleRunawayException(nullptr); } } static int qt_argc = 1; static const char *qt_argv = "bitcoin-qt"; BitcoinApplication::BitcoinApplication() : QApplication(qt_argc, const_cast(&qt_argv)), coreThread(nullptr), optionsModel(nullptr), clientModel(nullptr), window(nullptr), pollShutdownTimer(nullptr), returnValue(0), platformStyle(nullptr) { setQuitOnLastWindowClosed(false); } void BitcoinApplication::setupPlatformStyle() { // UI per-platform customization // This must be done inside the BitcoinApplication constructor, or after it, // because PlatformStyle::instantiate requires a QApplication. std::string platformName; platformName = gArgs.GetArg("-uiplatform", BitcoinGUI::DEFAULT_UIPLATFORM); platformStyle = PlatformStyle::instantiate(QString::fromStdString(platformName)); // Fall back to "other" if specified name not found. if (!platformStyle) { platformStyle = PlatformStyle::instantiate("other"); } assert(platformStyle); } BitcoinApplication::~BitcoinApplication() { if (coreThread) { qDebug() << __func__ << ": Stopping thread"; coreThread->quit(); coreThread->wait(); qDebug() << __func__ << ": Stopped thread"; } delete window; window = nullptr; delete optionsModel; optionsModel = nullptr; delete platformStyle; platformStyle = nullptr; } #ifdef ENABLE_WALLET void BitcoinApplication::createPaymentServer() { paymentServer = new PaymentServer(this); } #endif void BitcoinApplication::createOptionsModel(bool resetSettings) { optionsModel = new OptionsModel(nullptr, resetSettings); } void BitcoinApplication::createWindow(const Config *config, const NetworkStyle *networkStyle) { window = new BitcoinGUI(node(), config, platformStyle, networkStyle, nullptr); pollShutdownTimer = new QTimer(window); connect(pollShutdownTimer, &QTimer::timeout, window, &BitcoinGUI::detectShutdown); } void BitcoinApplication::createSplashScreen(const NetworkStyle *networkStyle) { assert(!m_splash); m_splash = new SplashScreen(networkStyle); // We don't hold a direct pointer to the splash screen after creation, but // the splash screen will take care of deleting itself when finish() // happens. m_splash->show(); connect(this, &BitcoinApplication::splashFinished, m_splash, &SplashScreen::finish); connect(this, &BitcoinApplication::requestedShutdown, m_splash, &QWidget::close); } void BitcoinApplication::setNode(interfaces::Node &node) { assert(!m_node); m_node = &node; if (optionsModel) { optionsModel->setNode(*m_node); } if (m_splash) { m_splash->setNode(*m_node); } } bool BitcoinApplication::baseInitialize(Config &config) { return node().baseInitialize(config); } void BitcoinApplication::startThread() { if (coreThread) { return; } coreThread = new QThread(this); BitcoinABC *executor = new BitcoinABC(node()); executor->moveToThread(coreThread); /* communication to and from thread */ connect(executor, &BitcoinABC::initializeResult, this, &BitcoinApplication::initializeResult); connect(executor, &BitcoinABC::shutdownResult, this, &BitcoinApplication::shutdownResult); connect(executor, &BitcoinABC::runawayException, this, &BitcoinApplication::handleRunawayException); // Note on how Qt works: it tries to directly invoke methods if the signal // is emitted on the same thread that the target object 'lives' on. // But if the target object 'lives' on another thread (executor here does) // the SLOT will be invoked asynchronously at a later time in the thread // of the target object. So.. we pass a pointer around. If you pass // a reference around (even if it's non-const) you'll get Qt generating // code to copy-construct the parameter in question (Q_DECLARE_METATYPE // and qRegisterMetaType generate this code). For the Config class, // which is noncopyable, we can't do this. So.. we have to pass // pointers to Config around. Make sure Config &/Config * isn't a // temporary (eg it lives somewhere aside from the stack) or this will // crash because initialize() gets executed in another thread at some // unspecified time (after) requestedInitialize() is emitted! connect(this, &BitcoinApplication::requestedInitialize, executor, &BitcoinABC::initialize); connect(this, &BitcoinApplication::requestedShutdown, executor, &BitcoinABC::shutdown); /* make sure executor object is deleted in its own thread */ connect(coreThread, &QThread::finished, executor, &QObject::deleteLater); coreThread->start(); } void BitcoinApplication::parameterSetup() { // Default printtoconsole to false for the GUI. GUI programs should not // print to the console unnecessarily. gArgs.SoftSetBoolArg("-printtoconsole", false); InitLogging(gArgs); InitParameterInteraction(gArgs); } void BitcoinApplication::SetPrune(bool prune, bool force) { optionsModel->SetPrune(prune, force); } void BitcoinApplication::requestInitialize( Config &config, RPCServer &rpcServer, HTTPRPCRequestProcessor &httpRPCRequestProcessor) { qDebug() << __func__ << ": Requesting initialize"; startThread(); // IMPORTANT: config must NOT be a reference to a temporary because below // signal may be connected to a slot that will be executed as a queued // connection in another thread! Q_EMIT requestedInitialize(&config, &rpcServer, &httpRPCRequestProcessor); } void BitcoinApplication::requestShutdown(Config &config) { // Show a simple window indicating shutdown status. Do this first as some of // the steps may take some time below, for example the RPC console may still // be executing a command. shutdownWindow.reset(ShutdownWindow::showShutdownWindow(window)); qDebug() << __func__ << ": Requesting shutdown"; startThread(); window->hide(); // Must disconnect node signals otherwise current thread can deadlock since // no event loop is running. window->unsubscribeFromCoreSignals(); // Request node shutdown, which can interrupt long operations, like // rescanning a wallet. node().startShutdown(); // Unsetting the client model can cause the current thread to wait for node // to complete an operation, like wait for a RPC execution to complete. window->setClientModel(nullptr); pollShutdownTimer->stop(); delete clientModel; clientModel = nullptr; // Request shutdown from core thread Q_EMIT requestedShutdown(); } void BitcoinApplication::initializeResult(bool success) { qDebug() << __func__ << ": Initialization result: " << success; returnValue = success ? EXIT_SUCCESS : EXIT_FAILURE; if (!success) { // Make sure splash screen doesn't stick around during shutdown. Q_EMIT splashFinished(); // Exit first main loop invocation. quit(); return; } // Log this only after AppInitMain finishes, as then logging setup is // guaranteed complete. qInfo() << "Platform customization:" << platformStyle->getName(); clientModel = new ClientModel(node(), optionsModel); window->setClientModel(clientModel); #ifdef ENABLE_WALLET if (WalletModel::isWalletEnabled()) { m_wallet_controller = - new WalletController(node(), platformStyle, optionsModel, this); + new WalletController(*clientModel, platformStyle, this); window->setWalletController(m_wallet_controller); if (paymentServer) { paymentServer->setOptionsModel(optionsModel); #ifdef ENABLE_BIP70 PaymentServer::LoadRootCAs(); connect(m_wallet_controller, &WalletController::coinsSent, paymentServer, &PaymentServer::fetchPaymentACK); #endif } } #endif // ENABLE_WALLET // If -min option passed, start window minimized(iconified) // or minimized to tray if (!gArgs.GetBoolArg("-min", false)) { window->show(); } else if (clientModel->getOptionsModel()->getMinimizeToTray() && window->hasTrayIcon()) { // do nothing as the window is managed by the tray icon } else { window->showMinimized(); } Q_EMIT splashFinished(); Q_EMIT windowShown(window); #ifdef ENABLE_WALLET // Now that initialization/startup is done, process any command-line // bitcoincash: URIs or payment requests: if (paymentServer) { connect(paymentServer, &PaymentServer::receivedPaymentRequest, window, &BitcoinGUI::handlePaymentRequest); connect(window, &BitcoinGUI::receivedURI, paymentServer, &PaymentServer::handleURIOrFile); connect(paymentServer, &PaymentServer::message, [this](const QString &title, const QString &message, unsigned int style) { window->message(title, message, style); }); QTimer::singleShot(100, paymentServer, &PaymentServer::uiReady); } #endif pollShutdownTimer->start(200); } void BitcoinApplication::shutdownResult() { // Exit second main loop invocation after shutdown finished. quit(); } void BitcoinApplication::handleRunawayException(const QString &message) { QMessageBox::critical( nullptr, "Runaway exception", BitcoinGUI::tr("A fatal error occurred. Bitcoin can no longer continue " "safely and will quit.") + QString("\n\n") + message); ::exit(EXIT_FAILURE); } WId BitcoinApplication::getMainWinId() const { if (!window) { return 0; } return window->winId(); } static void SetupUIArgs(ArgsManager &argsman) { #if defined(ENABLE_WALLET) && defined(ENABLE_BIP70) argsman.AddArg( "-allowselfsignedrootcertificates", strprintf("Allow self signed root certificates (default: %d)", DEFAULT_SELFSIGNED_ROOTCERTS), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::GUI); #endif argsman.AddArg("-choosedatadir", strprintf("Choose data directory on startup (default: %d)", DEFAULT_CHOOSE_DATADIR), ArgsManager::ALLOW_ANY, OptionsCategory::GUI); argsman.AddArg( "-lang=", "Set language, for example \"de_DE\" (default: system locale)", ArgsManager::ALLOW_ANY, OptionsCategory::GUI); argsman.AddArg("-min", "Start minimized", ArgsManager::ALLOW_ANY, OptionsCategory::GUI); argsman.AddArg( "-rootcertificates=", "Set SSL root certificates for payment request (default: -system-)", ArgsManager::ALLOW_ANY, OptionsCategory::GUI); argsman.AddArg("-splash", strprintf("Show splash screen on startup (default: %d)", DEFAULT_SPLASHSCREEN), ArgsManager::ALLOW_ANY, OptionsCategory::GUI); argsman.AddArg("-resetguisettings", "Reset all settings changed in the GUI", ArgsManager::ALLOW_ANY, OptionsCategory::GUI); argsman.AddArg("-uiplatform", strprintf("Select platform to customize UI for (one of " "windows, macosx, other; default: %s)", BitcoinGUI::DEFAULT_UIPLATFORM), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::GUI); } static void MigrateSettings() { assert(!QApplication::applicationName().isEmpty()); static const QString legacyAppName("Bitcoin-Qt"), #ifdef Q_OS_DARWIN // Macs and/or iOS et al use a domain-style name for Settings // files. All other platforms use a simple orgname. This // difference is documented in the QSettings class documentation. legacyOrg("bitcoin.org"); #else legacyOrg("Bitcoin"); #endif QSettings // below picks up settings file location based on orgname,appname legacy(legacyOrg, legacyAppName), // default c'tor below picks up settings file location based on // QApplication::applicationName(), et al -- which was already set // in main() abc; #ifdef Q_OS_DARWIN // Disable bogus OSX keys from MacOS system-wide prefs that may cloud our // judgement ;) (this behavior is also documented in QSettings docs) legacy.setFallbacksEnabled(false); abc.setFallbacksEnabled(false); #endif const QStringList legacyKeys(legacy.allKeys()); // We only migrate settings if we have Core settings but no Bitcoin-ABC // settings if (!legacyKeys.isEmpty() && abc.allKeys().isEmpty()) { for (const QString &key : legacyKeys) { // now, copy settings over abc.setValue(key, legacy.value(key)); } } } int GuiMain(int argc, char *argv[]) { #ifdef WIN32 util::WinCmdLineArgs winArgs; std::tie(argc, argv) = winArgs.get(); #endif SetupEnvironment(); util::ThreadSetInternalName("main"); NodeContext node_context; std::unique_ptr node = interfaces::MakeNode(&node_context); // Subscribe to global signals from core boost::signals2::scoped_connection handler_message_box = ::uiInterface.ThreadSafeMessageBox_connect(noui_ThreadSafeMessageBox); boost::signals2::scoped_connection handler_question = ::uiInterface.ThreadSafeQuestion_connect(noui_ThreadSafeQuestion); boost::signals2::scoped_connection handler_init_message = ::uiInterface.InitMessage_connect(noui_InitMessage); // Do not refer to data directory yet, this can be overridden by // Intro::pickDataDirectory /// 1. Basic Qt initialization (not dependent on parameters or /// configuration) Q_INIT_RESOURCE(bitcoin); Q_INIT_RESOURCE(bitcoin_locale); // Generate high-dpi pixmaps QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); BitcoinApplication app; // Register meta types used for QMetaObject::invokeMethod and // Qt::QueuedConnection qRegisterMetaType(); qRegisterMetaType(); #ifdef ENABLE_WALLET qRegisterMetaType(); #endif // Register typedefs (see // http://qt-project.org/doc/qt-5/qmetatype.html#qRegisterMetaType) // IMPORTANT: if Amount is no longer a typedef use the normal variant above // (see https://doc.qt.io/qt-5/qmetatype.html#qRegisterMetaType-1) qRegisterMetaType("Amount"); qRegisterMetaType("size_t"); qRegisterMetaType>("std::function"); qRegisterMetaType("QMessageBox::Icon"); // Need to register any types Qt doesn't know about if you intend // to use them with the signal/slot mechanism Qt provides. Even pointers. // Note that class Config is noncopyable and so we can't register a // non-pointer version of it with Qt, because Qt expects to be able to // copy-construct non-pointers to objects for invoking slots // behind-the-scenes in the 'Queued' connection case. qRegisterMetaType(); /// 2. Parse command-line options. We do this after qt in order to show an /// error if there are problems parsing these // Command-line options take precedence: SetupServerArgs(node_context); SetupUIArgs(gArgs); std::string error; if (!gArgs.ParseParameters(argc, argv, error)) { InitError(strprintf( Untranslated("Error parsing command line arguments: %s\n"), error)); // Create a message box, because the gui has neither been created nor // has subscribed to core signals QMessageBox::critical( nullptr, PACKAGE_NAME, // message can not be translated because translations have not been // initialized QString::fromStdString("Error parsing command line arguments: %1.") .arg(QString::fromStdString(error))); return EXIT_FAILURE; } // Now that the QApplication is setup and we have parsed our parameters, we // can set the platform style app.setupPlatformStyle(); /// 3. Application identification // must be set before OptionsModel is initialized or translations are // loaded, as it is used to locate QSettings. // Note: If you move these calls somewhere else, be sure to bring // MigrateSettings() below along for the ride. QApplication::setOrganizationName(QAPP_ORG_NAME); QApplication::setOrganizationDomain(QAPP_ORG_DOMAIN); QApplication::setApplicationName(QAPP_APP_NAME_DEFAULT); // Migrate settings from core's/our old GUI settings to Bitcoin ABC // only if core's exist but Bitcoin ABC's doesn't. // NOTE -- this function needs to be called *after* the above 3 lines // that set the app orgname and app name! If you move the above 3 lines // to elsewhere, take this call with you! MigrateSettings(); /// 4. Initialization of translations, so that intro dialog is in user's /// language. Now that QSettings are accessible, initialize translations. QTranslator qtTranslatorBase, qtTranslator, translatorBase, translator; initTranslations(qtTranslatorBase, qtTranslator, translatorBase, translator); // Show help message immediately after parsing command-line options (for // "-lang") and setting locale, but before showing splash screen. if (HelpRequested(gArgs) || gArgs.IsArgSet("-version")) { HelpMessageDialog help(nullptr, gArgs.IsArgSet("-version")); help.showOrPrint(); return EXIT_SUCCESS; } /// 5. Now that settings and translations are available, ask user for data /// directory. User language is set up: pick a data directory. bool did_show_intro = false; // Intro dialog prune check box bool prune = false; // Gracefully exit if the user cancels if (!Intro::showIfNeeded(did_show_intro, prune)) { return EXIT_SUCCESS; } /// 6. Determine availability of data directory and parse /// bitcoin.conf /// - Do not call GetDataDir(true) before this step finishes. if (!CheckDataDirOption()) { InitError(strprintf( Untranslated("Specified data directory \"%s\" does not exist.\n"), gArgs.GetArg("-datadir", ""))); QMessageBox::critical( nullptr, PACKAGE_NAME, QObject::tr( "Error: Specified data directory \"%1\" does not exist.") .arg(QString::fromStdString(gArgs.GetArg("-datadir", "")))); return EXIT_FAILURE; } if (!gArgs.ReadConfigFiles(error)) { InitError(strprintf( Untranslated("Error reading configuration file: %s\n"), error)); QMessageBox::critical( nullptr, PACKAGE_NAME, QObject::tr("Error: Cannot parse configuration file: %1.") .arg(QString::fromStdString(error))); return EXIT_FAILURE; } /// 7. Determine network (and switch to network specific options) // - Do not call Params() before this step. // - Do this after parsing the configuration file, as the network can be // switched there. // - QSettings() will use the new application name after this, resulting in // network-specific settings. // - Needs to be done before createOptionsModel. // Check for -chain, -testnet or -regtest parameter (Params() calls are only // valid after this clause) try { SelectParams(gArgs.GetChainName()); } catch (std::exception &e) { InitError(Untranslated(strprintf("%s\n", e.what()))); QMessageBox::critical(nullptr, PACKAGE_NAME, QObject::tr("Error: %1").arg(e.what())); return EXIT_FAILURE; } #ifdef ENABLE_WALLET // Parse URIs on command line -- this can affect Params() PaymentServer::ipcParseCommandLine(argc, argv); #endif if (!gArgs.InitSettings(error)) { InitError(Untranslated(error)); QMessageBox::critical(nullptr, PACKAGE_NAME, QObject::tr("Error initializing settings: %1") .arg(QString::fromStdString(error))); return EXIT_FAILURE; } QScopedPointer networkStyle( NetworkStyle::instantiate(Params().NetworkIDString())); assert(!networkStyle.isNull()); // Allow for separate UI settings for testnets QApplication::setApplicationName(networkStyle->getAppName()); // Re-initialize translations after changing application name (language in // network-specific settings can be different) initTranslations(qtTranslatorBase, qtTranslator, translatorBase, translator); #ifdef ENABLE_WALLET /// 8. URI IPC sending // - Do this early as we don't want to bother initializing if we are just // calling IPC // - Do this *after* setting up the data directory, as the data directory // hash is used in the name // of the server. // - Do this after creating app and setting up translations, so errors are // translated properly. if (PaymentServer::ipcSendCommandLine()) { exit(EXIT_SUCCESS); } // Start up the payment server early, too, so impatient users that click on // bitcoincash: links repeatedly have their payment requests routed to this // process: if (WalletModel::isWalletEnabled()) { app.createPaymentServer(); } #endif // ENABLE_WALLET /// 9. Main GUI initialization // Install global event filter that makes sure that long tooltips can be // word-wrapped. app.installEventFilter( new GUIUtil::ToolTipToRichTextFilter(TOOLTIP_WRAP_THRESHOLD, &app)); #if defined(Q_OS_WIN) // Install global event filter for processing Windows session related // Windows messages (WM_QUERYENDSESSION and WM_ENDSESSION) qApp->installNativeEventFilter(new WinShutdownMonitor()); #endif // Install qDebug() message handler to route to debug.log qInstallMessageHandler(DebugMessageHandler); // Allow parameter interaction before we create the options model app.parameterSetup(); GUIUtil::LogQtInfo(); // Load GUI settings from QSettings app.createOptionsModel(gArgs.GetBoolArg("-resetguisettings", false)); if (did_show_intro) { // Store intro dialog settings other than datadir (network specific) app.SetPrune(prune, true); } // Get global config Config &config = const_cast(GetConfig()); if (gArgs.GetBoolArg("-splash", DEFAULT_SPLASHSCREEN) && !gArgs.GetBoolArg("-min", false)) { app.createSplashScreen(networkStyle.data()); } app.setNode(*node); RPCServer rpcServer; util::Ref context{node_context}; HTTPRPCRequestProcessor httpRPCRequestProcessor(config, rpcServer, context); try { app.createWindow(&config, networkStyle.data()); // Perform base initialization before spinning up // initialization/shutdown thread. This is acceptable because this // function only contains steps that are quick to execute, so the GUI // thread won't be held up. if (!app.baseInitialize(config)) { // A dialog with detailed error will have been shown by InitError() return EXIT_FAILURE; } app.requestInitialize(config, rpcServer, httpRPCRequestProcessor); #if defined(Q_OS_WIN) WinShutdownMonitor::registerShutdownBlockReason( QObject::tr("%1 didn't yet exit safely...").arg(PACKAGE_NAME), (HWND)app.getMainWinId()); #endif app.exec(); app.requestShutdown(config); app.exec(); return app.getReturnValue(); } catch (const std::exception &e) { PrintExceptionContinue(&e, "Runaway exception"); app.handleRunawayException( QString::fromStdString(app.node().getWarnings())); } catch (...) { PrintExceptionContinue(nullptr, "Runaway exception"); app.handleRunawayException( QString::fromStdString(app.node().getWarnings())); } return EXIT_FAILURE; } diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp index ffecfd96c..096586ad1 100644 --- a/src/qt/clientmodel.cpp +++ b/src/qt/clientmodel.cpp @@ -1,289 +1,298 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static int64_t nLastHeaderTipUpdateNotification = 0; static int64_t nLastBlockTipUpdateNotification = 0; ClientModel::ClientModel(interfaces::Node &node, OptionsModel *_optionsModel, QObject *parent) : QObject(parent), m_node(node), optionsModel(_optionsModel), peerTableModel(nullptr), banTableModel(nullptr), m_thread(new QThread(this)) { cachedBestHeaderHeight = -1; cachedBestHeaderTime = -1; peerTableModel = new PeerTableModel(m_node, this); banTableModel = new BanTableModel(m_node, this); QTimer *timer = new QTimer; timer->setInterval(MODEL_UPDATE_DELAY); connect(timer, &QTimer::timeout, [this] { // no locking required at this point // the following calls will acquire the required lock Q_EMIT mempoolSizeChanged(m_node.getMempoolSize(), m_node.getMempoolDynamicUsage()); Q_EMIT bytesChanged(m_node.getTotalBytesRecv(), m_node.getTotalBytesSent()); }); connect(m_thread, &QThread::finished, timer, &QObject::deleteLater); connect(m_thread, &QThread::started, [timer] { timer->start(); }); // move timer to thread so that polling doesn't disturb main event loop timer->moveToThread(m_thread); m_thread->start(); subscribeToCoreSignals(); } ClientModel::~ClientModel() { unsubscribeFromCoreSignals(); m_thread->quit(); m_thread->wait(); } int ClientModel::getNumConnections(NumConnections flags) const { CConnman::NumConnections connections = CConnman::CONNECTIONS_NONE; if (flags == CONNECTIONS_IN) { connections = CConnman::CONNECTIONS_IN; } else if (flags == CONNECTIONS_OUT) { connections = CConnman::CONNECTIONS_OUT; } else if (flags == CONNECTIONS_ALL) { connections = CConnman::CONNECTIONS_ALL; } return m_node.getNodeCount(connections); } int ClientModel::getHeaderTipHeight() const { if (cachedBestHeaderHeight == -1) { // make sure we initially populate the cache via a cs_main lock // otherwise we need to wait for a tip update int height; int64_t blockTime; if (m_node.getHeaderTip(height, blockTime)) { cachedBestHeaderHeight = height; cachedBestHeaderTime = blockTime; } } return cachedBestHeaderHeight; } int64_t ClientModel::getHeaderTipTime() const { if (cachedBestHeaderTime == -1) { int height; int64_t blockTime; if (m_node.getHeaderTip(height, blockTime)) { cachedBestHeaderHeight = height; cachedBestHeaderTime = blockTime; } } return cachedBestHeaderTime; } +int ClientModel::getNumBlocks() const { + if (m_cached_num_blocks == -1) { + m_cached_num_blocks = m_node.getNumBlocks(); + } + return m_cached_num_blocks; +} + void ClientModel::updateNumConnections(int numConnections) { Q_EMIT numConnectionsChanged(numConnections); } void ClientModel::updateNetworkActive(bool networkActive) { Q_EMIT networkActiveChanged(networkActive); } void ClientModel::updateAlert() { Q_EMIT alertsChanged(getStatusBarWarnings()); } enum BlockSource ClientModel::getBlockSource() const { if (m_node.getReindex()) { return BlockSource::REINDEX; } else if (m_node.getImporting()) { return BlockSource::DISK; } else if (getNumConnections() > 0) { return BlockSource::NETWORK; } return BlockSource::NONE; } QString ClientModel::getStatusBarWarnings() const { return QString::fromStdString(m_node.getWarnings()); } OptionsModel *ClientModel::getOptionsModel() { return optionsModel; } PeerTableModel *ClientModel::getPeerTableModel() { return peerTableModel; } BanTableModel *ClientModel::getBanTableModel() { return banTableModel; } QString ClientModel::formatFullVersion() const { return QString::fromStdString(FormatFullVersion()); } QString ClientModel::formatSubVersion() const { return QString::fromStdString(userAgent(GetConfig())); } bool ClientModel::isReleaseVersion() const { return CLIENT_VERSION_IS_RELEASE; } QString ClientModel::formatClientStartupTime() const { return QDateTime::fromTime_t(GetStartupTime()).toString(); } QString ClientModel::dataDir() const { return GUIUtil::boostPathToQString(GetDataDir()); } QString ClientModel::blocksDir() const { return GUIUtil::boostPathToQString(GetBlocksDir()); } void ClientModel::updateBanlist() { banTableModel->refresh(); } // Handlers for core signals static void ShowProgress(ClientModel *clientmodel, const std::string &title, int nProgress) { // emits signal "showProgress" bool invoked = QMetaObject::invokeMethod( clientmodel, "showProgress", Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(title)), Q_ARG(int, nProgress)); assert(invoked); } static void NotifyNumConnectionsChanged(ClientModel *clientmodel, int newNumConnections) { // Too noisy: qDebug() << "NotifyNumConnectionsChanged: " + // QString::number(newNumConnections); bool invoked = QMetaObject::invokeMethod( clientmodel, "updateNumConnections", Qt::QueuedConnection, Q_ARG(int, newNumConnections)); assert(invoked); } static void NotifyNetworkActiveChanged(ClientModel *clientmodel, bool networkActive) { bool invoked = QMetaObject::invokeMethod(clientmodel, "updateNetworkActive", Qt::QueuedConnection, Q_ARG(bool, networkActive)); assert(invoked); } static void NotifyAlertChanged(ClientModel *clientmodel) { qDebug() << "NotifyAlertChanged"; bool invoked = QMetaObject::invokeMethod(clientmodel, "updateAlert", Qt::QueuedConnection); assert(invoked); } static void BannedListChanged(ClientModel *clientmodel) { qDebug() << QString("%1: Requesting update for peer banlist").arg(__func__); bool invoked = QMetaObject::invokeMethod(clientmodel, "updateBanlist", Qt::QueuedConnection); assert(invoked); } static void BlockTipChanged(ClientModel *clientmodel, SynchronizationState sync_state, int height, int64_t blockTime, double verificationProgress, bool fHeader) { if (fHeader) { // cache best headers time and height to reduce future cs_main locks clientmodel->cachedBestHeaderHeight = height; clientmodel->cachedBestHeaderTime = blockTime; + } else { + clientmodel->m_cached_num_blocks = height; } // Throttle GUI notifications about (a) blocks during initial sync, and (b) // both blocks and headers during reindex. const bool throttle = (sync_state != SynchronizationState::POST_INIT && !fHeader) || sync_state == SynchronizationState::INIT_REINDEX; const int64_t now = throttle ? GetTimeMillis() : 0; int64_t &nLastUpdateNotification = fHeader ? nLastHeaderTipUpdateNotification : nLastBlockTipUpdateNotification; if (throttle && now < nLastUpdateNotification + MODEL_UPDATE_DELAY) { return; } bool invoked = QMetaObject::invokeMethod( clientmodel, "numBlocksChanged", Qt::QueuedConnection, Q_ARG(int, height), Q_ARG(QDateTime, QDateTime::fromTime_t(blockTime)), Q_ARG(double, verificationProgress), Q_ARG(bool, fHeader), Q_ARG(SynchronizationState, sync_state)); assert(invoked); nLastUpdateNotification = now; } void ClientModel::subscribeToCoreSignals() { // Connect signals to client m_handler_show_progress = m_node.handleShowProgress(std::bind( ShowProgress, this, std::placeholders::_1, std::placeholders::_2)); m_handler_notify_num_connections_changed = m_node.handleNotifyNumConnectionsChanged(std::bind( NotifyNumConnectionsChanged, this, std::placeholders::_1)); m_handler_notify_network_active_changed = m_node.handleNotifyNetworkActiveChanged( std::bind(NotifyNetworkActiveChanged, this, std::placeholders::_1)); m_handler_notify_alert_changed = m_node.handleNotifyAlertChanged(std::bind(NotifyAlertChanged, this)); m_handler_banned_list_changed = m_node.handleBannedListChanged(std::bind(BannedListChanged, this)); m_handler_notify_block_tip = m_node.handleNotifyBlockTip(std::bind( BlockTipChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, false)); m_handler_notify_header_tip = m_node.handleNotifyHeaderTip(std::bind( BlockTipChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, true)); } void ClientModel::unsubscribeFromCoreSignals() { // Disconnect signals from client m_handler_show_progress->disconnect(); m_handler_notify_num_connections_changed->disconnect(); m_handler_notify_network_active_changed->disconnect(); m_handler_notify_alert_changed->disconnect(); m_handler_banned_list_changed->disconnect(); m_handler_notify_block_tip->disconnect(); m_handler_notify_header_tip->disconnect(); } bool ClientModel::getProxyInfo(std::string &ip_port) const { proxyType ipv4, ipv6; if (m_node.getProxy((Network)1, ipv4) && m_node.getProxy((Network)2, ipv6)) { ip_port = ipv4.proxy.ToStringIPPort(); return true; } return false; } diff --git a/src/qt/clientmodel.h b/src/qt/clientmodel.h index 56a97b4b0..4c168667e 100644 --- a/src/qt/clientmodel.h +++ b/src/qt/clientmodel.h @@ -1,122 +1,124 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_QT_CLIENTMODEL_H #define BITCOIN_QT_CLIENTMODEL_H #include #include #include #include class BanTableModel; class CBlockIndex; class OptionsModel; class PeerTableModel; class CBlockIndex; enum class SynchronizationState; namespace interfaces { class Handler; class Node; } // namespace interfaces QT_BEGIN_NAMESPACE class QTimer; QT_END_NAMESPACE enum class BlockSource { NONE, REINDEX, DISK, NETWORK }; /** Model for Bitcoin network client. */ class ClientModel : public QObject { Q_OBJECT public: enum NumConnections { CONNECTIONS_NONE = 0, CONNECTIONS_IN = (1U << 0), CONNECTIONS_OUT = (1U << 1), CONNECTIONS_ALL = (CONNECTIONS_IN | CONNECTIONS_OUT), }; explicit ClientModel(interfaces::Node &node, OptionsModel *optionsModel, QObject *parent = nullptr); ~ClientModel(); interfaces::Node &node() const { return m_node; } OptionsModel *getOptionsModel(); PeerTableModel *getPeerTableModel(); BanTableModel *getBanTableModel(); //! Return number of connections, default is in- and outbound (total) int getNumConnections(NumConnections flags = CONNECTIONS_ALL) const; + int getNumBlocks() const; int getHeaderTipHeight() const; int64_t getHeaderTipTime() const; //! Returns enum BlockSource of the current importing/syncing state enum BlockSource getBlockSource() const; //! Return warnings to be displayed in status bar QString getStatusBarWarnings() const; QString formatFullVersion() const; QString formatSubVersion() const; bool isReleaseVersion() const; QString formatClientStartupTime() const; QString dataDir() const; QString blocksDir() const; bool getProxyInfo(std::string &ip_port) const; - // caches for the best header + // caches for the best header, number of blocks mutable std::atomic cachedBestHeaderHeight; mutable std::atomic cachedBestHeaderTime; + mutable std::atomic m_cached_num_blocks{-1}; private: interfaces::Node &m_node; std::unique_ptr m_handler_show_progress; std::unique_ptr m_handler_notify_num_connections_changed; std::unique_ptr m_handler_notify_network_active_changed; std::unique_ptr m_handler_notify_alert_changed; std::unique_ptr m_handler_banned_list_changed; std::unique_ptr m_handler_notify_block_tip; std::unique_ptr m_handler_notify_header_tip; OptionsModel *optionsModel; PeerTableModel *peerTableModel; BanTableModel *banTableModel; //! A thread to interact with m_node asynchronously QThread *const m_thread; void subscribeToCoreSignals(); void unsubscribeFromCoreSignals(); Q_SIGNALS: void numConnectionsChanged(int count); void numBlocksChanged(int count, const QDateTime &blockDate, double nVerificationProgress, bool header, SynchronizationState sync_state); void mempoolSizeChanged(long count, size_t mempoolSizeInBytes); void networkActiveChanged(bool networkActive); void alertsChanged(const QString &warnings); void bytesChanged(quint64 totalBytesIn, quint64 totalBytesOut); //! Fired when a message should be reported to the user void message(const QString &title, const QString &message, unsigned int style); // Show progress dialog e.g. for verifychain void showProgress(const QString &title, int nProgress); public Q_SLOTS: void updateNumConnections(int numConnections); void updateNetworkActive(bool networkActive); void updateAlert(); void updateBanlist(); }; #endif // BITCOIN_QT_CLIENTMODEL_H diff --git a/src/qt/test/addressbooktests.cpp b/src/qt/test/addressbooktests.cpp index 27eac2e73..cce4f701d 100644 --- a/src/qt/test/addressbooktests.cpp +++ b/src/qt/test/addressbooktests.cpp @@ -1,168 +1,170 @@ #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { /** * Fill the edit address dialog box with data, submit it, and ensure that * the resulting message meets expectations. */ void EditAddressAndSubmit(EditAddressDialog *dialog, const QString &label, const QString &address, QString expected_msg) { QString warning_text; dialog->findChild("labelEdit")->setText(label); dialog->findChild("addressEdit")->setText(address); ConfirmMessage(&warning_text, 5); dialog->accept(); QCOMPARE(warning_text, expected_msg); } /** * Test adding various send addresses to the address book. * * There are three cases tested: * * - new_address: a new address which should add as a send address * successfully. * - existing_s_address: an existing sending address which won't add * successfully. * - existing_r_address: an existing receiving address which won't add * successfully. * * In each case, verify the resulting state of the address book and optionally * the warning message presented to the user. */ void TestAddAddressesToSendBook(interfaces::Node &node) { TestChain100Setup test; node.setContext(&test.m_node); std::shared_ptr wallet = std::make_shared(node.context()->chain.get(), WalletLocation(), WalletDatabase::CreateMock()); wallet->SetupLegacyScriptPubKeyMan(); bool firstRun; wallet->LoadWallet(firstRun); auto build_address = [&wallet]() { CKey key; key.MakeNewKey(true); CTxDestination dest(GetDestinationForKey( key.GetPubKey(), wallet->m_default_address_type)); return std::make_pair( dest, QString::fromStdString(EncodeCashAddr(dest, Params()))); }; CTxDestination r_key_dest, s_key_dest; // Add a preexisting "receive" entry in the address book. QString preexisting_r_address; QString r_label("already here (r)"); // Add a preexisting "send" entry in the address book. QString preexisting_s_address; QString s_label("already here (s)"); // Define a new address (which should add to the address book successfully). QString new_address; std::tie(r_key_dest, preexisting_r_address) = build_address(); std::tie(s_key_dest, preexisting_s_address) = build_address(); std::tie(std::ignore, new_address) = build_address(); { LOCK(wallet->cs_wallet); wallet->SetAddressBook(r_key_dest, r_label.toStdString(), "receive"); wallet->SetAddressBook(s_key_dest, s_label.toStdString(), "send"); } auto check_addbook_size = [&wallet](int expected_size) { LOCK(wallet->cs_wallet); QCOMPARE(static_cast(wallet->m_address_book.size()), expected_size); }; // We should start with the two addresses we added earlier and nothing else. check_addbook_size(2); // Initialize relevant QT models. std::unique_ptr platformStyle( PlatformStyle::instantiate("other")); OptionsModel optionsModel; + ClientModel clientModel(node, &optionsModel); AddWallet(wallet); - WalletModel walletModel(interfaces::MakeWallet(wallet), node, - platformStyle.get(), &optionsModel); + WalletModel walletModel(interfaces::MakeWallet(wallet), clientModel, + platformStyle.get()); RemoveWallet(wallet); EditAddressDialog editAddressDialog(EditAddressDialog::NewSendingAddress); editAddressDialog.setModel(walletModel.getAddressTableModel()); EditAddressAndSubmit( &editAddressDialog, QString("uhoh"), preexisting_r_address, QString( "Address \"%1\" already exists as a receiving address with label " "\"%2\" and so cannot be added as a sending address.") .arg(preexisting_r_address) .arg(r_label)); check_addbook_size(2); EditAddressAndSubmit( &editAddressDialog, QString("uhoh, different"), preexisting_s_address, QString( "The entered address \"%1\" is already in the address book with " "label \"%2\".") .arg(preexisting_s_address) .arg(s_label)); check_addbook_size(2); // Submit a new address which should add successfully - we expect the // warning message to be blank. EditAddressAndSubmit(&editAddressDialog, QString("new"), new_address, QString("")); check_addbook_size(3); } } // namespace void AddressBookTests::addressBookTests() { #ifdef Q_OS_MAC if (QApplication::platformName() == "minimal") { // Disable for mac on "minimal" platform to avoid crashes inside the Qt // framework when it tries to look up unimplemented cocoa functions, // and fails to handle returned nulls // (https://bugreports.qt.io/browse/QTBUG-49686). QWARN("Skipping AddressBookTests on mac build with 'minimal' platform " "set due to Qt bugs. To run AppTests, invoke with " "'QT_QPA_PLATFORM=cocoa test_bitcoin-qt' on mac, or else use a " "linux or windows build."); return; } #endif TestAddAddressesToSendBook(m_node); } diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index 3c8591f21..915b1a30b 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -1,275 +1,277 @@ #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { //! Press "Yes" or "Cancel" buttons in modal send confirmation dialog. void ConfirmSend(QString *text = nullptr, bool cancel = false) { QTimer::singleShot(0, Qt::PreciseTimer, [text, cancel]() { for (QWidget *widget : QApplication::topLevelWidgets()) { if (widget->inherits("SendConfirmationDialog")) { SendConfirmationDialog *dialog = qobject_cast(widget); if (text) { *text = dialog->text(); } QAbstractButton *button = dialog->button( cancel ? QMessageBox::Cancel : QMessageBox::Yes); button->setEnabled(true); button->click(); } } }); } //! Send coins to address and return txid. TxId SendCoins(CWallet &wallet, SendCoinsDialog &sendCoinsDialog, const CTxDestination &address, Amount amount) { QVBoxLayout *entries = sendCoinsDialog.findChild("entries"); SendCoinsEntry *entry = qobject_cast(entries->itemAt(0)->widget()); entry->findChild("payTo")->setText( QString::fromStdString(EncodeCashAddr(address, Params()))); entry->findChild("payAmount")->setValue(amount); TxId txid; boost::signals2::scoped_connection c = wallet.NotifyTransactionChanged.connect( [&txid](CWallet *, const TxId &hash, ChangeType status) { if (status == CT_NEW) { txid = hash; } }); ConfirmSend(); bool invoked = QMetaObject::invokeMethod(&sendCoinsDialog, "on_sendButton_clicked"); assert(invoked); return txid; } //! Find index of txid in transaction list. QModelIndex FindTx(const QAbstractItemModel &model, const uint256 &txid) { QString hash = QString::fromStdString(txid.ToString()); int rows = model.rowCount({}); for (int row = 0; row < rows; ++row) { QModelIndex index = model.index(row, 0, {}); if (model.data(index, TransactionTableModel::TxHashRole) == hash) { return index; } } return {}; } //! Simple qt wallet tests. // // Test widgets can be debugged interactively calling show() on them and // manually running the event loop, e.g.: // // sendCoinsDialog.show(); // QEventLoop().exec(); // // This also requires overriding the default minimal Qt platform: // // QT_QPA_PLATFORM=xcb src/qt/test/test_bitcoin-qt # Linux // QT_QPA_PLATFORM=windows src/qt/test/test_bitcoin-qt # Windows // QT_QPA_PLATFORM=cocoa src/qt/test/test_bitcoin-qt # macOS void TestGUI(interfaces::Node &node) { // Set up wallet and chain with 105 blocks (5 mature blocks for spending). TestChain100Setup test; for (int i = 0; i < 5; ++i) { test.CreateAndProcessBlock( {}, GetScriptForRawPubKey(test.coinbaseKey.GetPubKey())); } node.setContext(&test.m_node); std::shared_ptr wallet = std::make_shared(node.context()->chain.get(), WalletLocation(), WalletDatabase::CreateMock()); bool firstRun; wallet->LoadWallet(firstRun); { auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan(); LOCK2(wallet->cs_wallet, spk_man->cs_KeyStore); wallet->SetAddressBook( GetDestinationForKey(test.coinbaseKey.GetPubKey(), wallet->m_default_address_type), "", "receive"); spk_man->AddKeyPubKey(test.coinbaseKey, test.coinbaseKey.GetPubKey()); wallet->SetLastBlockProcessed(105, ::ChainActive().Tip()->GetBlockHash()); } { WalletRescanReserver reserver(*wallet); reserver.reserve(); CWallet::ScanResult result = wallet->ScanForWalletTransactions( Params().GetConsensus().hashGenesisBlock, 0 /* block height */, {} /* max height */, reserver, true /* fUpdate */); QCOMPARE(result.status, CWallet::ScanResult::SUCCESS); QCOMPARE(result.last_scanned_block, ::ChainActive().Tip()->GetBlockHash()); QVERIFY(result.last_failed_block.IsNull()); } wallet->SetBroadcastTransactions(true); // Create widgets for sending coins and listing transactions. std::unique_ptr platformStyle( PlatformStyle::instantiate("other")); OptionsModel optionsModel; + ClientModel clientModel(node, &optionsModel); AddWallet(wallet); - WalletModel walletModel(interfaces::MakeWallet(wallet), node, - platformStyle.get(), &optionsModel); + WalletModel walletModel(interfaces::MakeWallet(wallet), clientModel, + platformStyle.get()); RemoveWallet(wallet); SendCoinsDialog sendCoinsDialog(platformStyle.get(), &walletModel); { // Check balance in send dialog QLabel *balanceLabel = sendCoinsDialog.findChild("labelBalance"); QString balanceText = balanceLabel->text(); int unit = walletModel.getOptionsModel()->getDisplayUnit(); Amount balance = walletModel.wallet().getBalance(); QString balanceComparison = BitcoinUnits::formatWithUnit( unit, balance, false, BitcoinUnits::separatorAlways); QCOMPARE(balanceText, balanceComparison); } // Send two transactions, and verify they are added to transaction list. TransactionTableModel *transactionTableModel = walletModel.getTransactionTableModel(); QCOMPARE(transactionTableModel->rowCount({}), 105); TxId txid1 = SendCoins(*wallet.get(), sendCoinsDialog, CTxDestination(PKHash()), 5 * COIN); TxId txid2 = SendCoins(*wallet.get(), sendCoinsDialog, CTxDestination(PKHash()), 10 * COIN); QCOMPARE(transactionTableModel->rowCount({}), 107); QVERIFY(FindTx(*transactionTableModel, txid1).isValid()); QVERIFY(FindTx(*transactionTableModel, txid2).isValid()); // Check current balance on OverviewPage OverviewPage overviewPage(platformStyle.get()); overviewPage.setWalletModel(&walletModel); QLabel *balanceLabel = overviewPage.findChild("labelBalance"); QString balanceText = balanceLabel->text(); int unit = walletModel.getOptionsModel()->getDisplayUnit(); Amount balance = walletModel.wallet().getBalance(); QString balanceComparison = BitcoinUnits::formatWithUnit( unit, balance, false, BitcoinUnits::separatorAlways); QCOMPARE(balanceText, balanceComparison); // Check Request Payment button ReceiveCoinsDialog receiveCoinsDialog(platformStyle.get()); receiveCoinsDialog.setModel(&walletModel); RecentRequestsTableModel *requestTableModel = walletModel.getRecentRequestsTableModel(); // Label input QLineEdit *labelInput = receiveCoinsDialog.findChild("reqLabel"); labelInput->setText("TEST_LABEL_1"); // Amount input BitcoinAmountField *amountInput = receiveCoinsDialog.findChild("reqAmount"); amountInput->setValue(1 * SATOSHI); // Message input QLineEdit *messageInput = receiveCoinsDialog.findChild("reqMessage"); messageInput->setText("TEST_MESSAGE_1"); int initialRowCount = requestTableModel->rowCount({}); QPushButton *requestPaymentButton = receiveCoinsDialog.findChild("receiveButton"); requestPaymentButton->click(); for (QWidget *widget : QApplication::topLevelWidgets()) { if (widget->inherits("ReceiveRequestDialog")) { ReceiveRequestDialog *receiveRequestDialog = qobject_cast(widget); QTextEdit *rlist = receiveRequestDialog->QObject::findChild("outUri"); QString paymentText = rlist->toPlainText(); QStringList paymentTextList = paymentText.split('\n'); QCOMPARE(paymentTextList.at(0), QString("Payment information")); QVERIFY(paymentTextList.at(1).indexOf(QString("URI: bchreg:")) != -1); QVERIFY(paymentTextList.at(2).indexOf(QString("Address:")) != -1); QCOMPARE(paymentTextList.at(3), QString("Amount: 0.00000001 ") + QString::fromStdString(CURRENCY_UNIT)); QCOMPARE(paymentTextList.at(4), QString("Label: TEST_LABEL_1")); QCOMPARE(paymentTextList.at(5), QString("Message: TEST_MESSAGE_1")); } } // Clear button QPushButton *clearButton = receiveCoinsDialog.findChild("clearButton"); clearButton->click(); QCOMPARE(labelInput->text(), QString("")); QCOMPARE(amountInput->value(), Amount::zero()); QCOMPARE(messageInput->text(), QString("")); // Check addition to history int currentRowCount = requestTableModel->rowCount({}); QCOMPARE(currentRowCount, initialRowCount + 1); // Check Remove button QTableView *table = receiveCoinsDialog.findChild("recentRequestsView"); table->selectRow(currentRowCount - 1); QPushButton *removeRequestButton = receiveCoinsDialog.findChild("removeRequestButton"); removeRequestButton->click(); QCOMPARE(requestTableModel->rowCount({}), currentRowCount - 1); } } // namespace void WalletTests::walletTests() { #ifdef Q_OS_MAC if (QApplication::platformName() == "minimal") { // Disable for mac on "minimal" platform to avoid crashes inside the Qt // framework when it tries to look up unimplemented cocoa functions, // and fails to handle returned nulls // (https://bugreports.qt.io/browse/QTBUG-49686). QWARN( "Skipping WalletTests on mac build with 'minimal' platform set due " "to Qt bugs. To run AppTests, invoke with 'QT_QPA_PLATFORM=cocoa " "test_bitcoin-qt' on mac, or else use a linux or windows build."); return; } #endif TestGUI(m_node); } diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp index f8e783f30..444389f2a 100644 --- a/src/qt/transactiontablemodel.cpp +++ b/src/qt/transactiontablemodel.cpp @@ -1,777 +1,779 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Amount column is right-aligned it contains numbers static int column_alignments[] = { Qt::AlignLeft | Qt::AlignVCenter, /* status */ Qt::AlignLeft | Qt::AlignVCenter, /* watchonly */ Qt::AlignLeft | Qt::AlignVCenter, /* date */ Qt::AlignLeft | Qt::AlignVCenter, /* type */ Qt::AlignLeft | Qt::AlignVCenter, /* address */ Qt::AlignRight | Qt::AlignVCenter /* amount */ }; // Comparison operator for sort/binary search of model tx list struct TxLessThan { bool operator()(const TransactionRecord &a, const TransactionRecord &b) const { return a.txid < b.txid; } bool operator()(const TransactionRecord &a, const TxId &b) const { return a.txid < b; } bool operator()(const TxId &a, const TransactionRecord &b) const { return a < b.txid; } }; // Private implementation class TransactionTablePriv { public: explicit TransactionTablePriv(TransactionTableModel *_parent) : parent(_parent) {} TransactionTableModel *parent; /* Local cache of wallet. * As it is in the same order as the CWallet, by definition this is sorted * by sha256. */ QList cachedWallet; /** * Query entire wallet anew from core. */ void refreshWallet(interfaces::Wallet &wallet) { qDebug() << "TransactionTablePriv::refreshWallet"; cachedWallet.clear(); for (const auto &wtx : wallet.getWalletTxs()) { if (TransactionRecord::showTransaction()) { cachedWallet.append( TransactionRecord::decomposeTransaction(wtx)); } } } /** * Update our model of the wallet incrementally, to synchronize our model of * the wallet with that of the core. * Call with transaction that was added, removed or changed. */ void updateWallet(interfaces::Wallet &wallet, const TxId &txid, int status, bool showTransaction) { qDebug() << "TransactionTablePriv::updateWallet: " + QString::fromStdString(txid.ToString()) + " " + QString::number(status); // Find bounds of this transaction in model QList::iterator lower = std::lower_bound( cachedWallet.begin(), cachedWallet.end(), txid, TxLessThan()); QList::iterator upper = std::upper_bound( cachedWallet.begin(), cachedWallet.end(), txid, TxLessThan()); int lowerIndex = (lower - cachedWallet.begin()); int upperIndex = (upper - cachedWallet.begin()); bool inModel = (lower != upper); if (status == CT_UPDATED) { // Not in model, but want to show, treat as new. if (showTransaction && !inModel) { status = CT_NEW; } // In model, but want to hide, treat as deleted. if (!showTransaction && inModel) { status = CT_DELETED; } } qDebug() << " inModel=" + QString::number(inModel) + " Index=" + QString::number(lowerIndex) + "-" + QString::number(upperIndex) + " showTransaction=" + QString::number(showTransaction) + " derivedStatus=" + QString::number(status); switch (status) { case CT_NEW: if (inModel) { qWarning() << "TransactionTablePriv::updateWallet: " "Warning: Got CT_NEW, but transaction is " "already in model"; break; } if (showTransaction) { // Find transaction in wallet interfaces::WalletTx wtx = wallet.getWalletTx(txid); if (!wtx.tx) { qWarning() << "TransactionTablePriv::updateWallet: " "Warning: Got CT_NEW, but transaction is " "not in wallet"; break; } // Added -- insert at the right position QList toInsert = TransactionRecord::decomposeTransaction(wtx); /* only if something to insert */ if (!toInsert.isEmpty()) { parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex + toInsert.size() - 1); int insert_idx = lowerIndex; for (const TransactionRecord &rec : toInsert) { cachedWallet.insert(insert_idx, rec); insert_idx += 1; } parent->endInsertRows(); } } break; case CT_DELETED: if (!inModel) { qWarning() << "TransactionTablePriv::updateWallet: " "Warning: Got CT_DELETED, but transaction is " "not in model"; break; } // Removed -- remove entire transaction from table parent->beginRemoveRows(QModelIndex(), lowerIndex, upperIndex - 1); cachedWallet.erase(lower, upper); parent->endRemoveRows(); break; case CT_UPDATED: // Miscellaneous updates -- nothing to do, status update will // take care of this, and is only computed for visible // transactions. break; } } int size() { return cachedWallet.size(); } - TransactionRecord *index(interfaces::Wallet &wallet, int idx) { + TransactionRecord *index(interfaces::Wallet &wallet, + const int cur_num_blocks, const int idx) { if (idx >= 0 && idx < cachedWallet.size()) { TransactionRecord *rec = &cachedWallet[idx]; // Get required locks upfront. This avoids the GUI from getting // stuck if the core is holding the locks for a longer time - for // example, during a wallet rescan. // // If a status update is needed (blocks came in since last check), // update the status of this transaction from the wallet. Otherwise, // simply re-use the cached status. interfaces::WalletTxStatus wtx; int numBlocks; int64_t block_time; - if (wallet.tryGetTxStatus(rec->txid, wtx, numBlocks, block_time) && - rec->statusUpdateNeeded(numBlocks)) { + if (rec->statusUpdateNeeded(cur_num_blocks) && + wallet.tryGetTxStatus(rec->txid, wtx, numBlocks, block_time)) { rec->updateStatus(wtx, numBlocks, block_time); } return rec; } return nullptr; } QString describe(interfaces::Node &node, interfaces::Wallet &wallet, TransactionRecord *rec, int unit) { return TransactionDesc::toHTML(node, wallet, rec, unit); } QString getTxHex(interfaces::Wallet &wallet, TransactionRecord *rec) { auto tx = wallet.getTx(rec->txid); if (tx) { std::string strHex = EncodeHexTx(*tx); return QString::fromStdString(strHex); } return QString(); } }; TransactionTableModel::TransactionTableModel( const PlatformStyle *_platformStyle, WalletModel *parent) : QAbstractTableModel(parent), walletModel(parent), priv(new TransactionTablePriv(this)), fProcessingQueuedTransactions(false), platformStyle(_platformStyle) { columns << QString() << QString() << tr("Date") << tr("Type") << tr("Label") << BitcoinUnits::getAmountColumnTitle( walletModel->getOptionsModel()->getDisplayUnit()); priv->refreshWallet(walletModel->wallet()); connect(walletModel->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &TransactionTableModel::updateDisplayUnit); subscribeToCoreSignals(); } TransactionTableModel::~TransactionTableModel() { unsubscribeFromCoreSignals(); delete priv; } /** Updates the column title to "Amount (DisplayUnit)" and emits * headerDataChanged() signal for table headers to react. */ void TransactionTableModel::updateAmountColumnTitle() { columns[Amount] = BitcoinUnits::getAmountColumnTitle( walletModel->getOptionsModel()->getDisplayUnit()); Q_EMIT headerDataChanged(Qt::Horizontal, Amount, Amount); } void TransactionTableModel::updateTransaction(const QString &hash, int status, bool showTransaction) { TxId updated; updated.SetHex(hash.toStdString()); priv->updateWallet(walletModel->wallet(), updated, status, showTransaction); } void TransactionTableModel::updateConfirmations() { // Blocks came in since last poll. // Invalidate status (number of confirmations) and (possibly) description // for all rows. Qt is smart enough to only actually request the data for // the visible rows. Q_EMIT dataChanged(index(0, Status), index(priv->size() - 1, Status)); Q_EMIT dataChanged(index(0, ToAddress), index(priv->size() - 1, ToAddress)); } int TransactionTableModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return priv->size(); } int TransactionTableModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return columns.length(); } QString TransactionTableModel::formatTxStatus(const TransactionRecord *wtx) const { QString status; switch (wtx->status.status) { case TransactionStatus::OpenUntilBlock: status = tr("Open for %n more block(s)", "", wtx->status.open_for); break; case TransactionStatus::OpenUntilDate: status = tr("Open until %1") .arg(GUIUtil::dateTimeStr(wtx->status.open_for)); break; case TransactionStatus::Unconfirmed: status = tr("Unconfirmed"); break; case TransactionStatus::Abandoned: status = tr("Abandoned"); break; case TransactionStatus::Confirming: status = tr("Confirming (%1 of %2 recommended confirmations)") .arg(wtx->status.depth) .arg(TransactionRecord::RecommendedNumConfirmations); break; case TransactionStatus::Confirmed: status = tr("Confirmed (%1 confirmations)").arg(wtx->status.depth); break; case TransactionStatus::Conflicted: status = tr("Conflicted"); break; case TransactionStatus::Immature: status = tr("Immature (%1 confirmations, will be available after %2)") .arg(wtx->status.depth) .arg(wtx->status.depth + wtx->status.matures_in); break; case TransactionStatus::NotAccepted: status = tr("Generated but not accepted"); break; } return status; } QString TransactionTableModel::formatTxDate(const TransactionRecord *wtx) const { if (wtx->time) { return GUIUtil::dateTimeStr(wtx->time); } return QString(); } /** * Look up address in address book, if found return label (address) otherwise * just return (address) */ QString TransactionTableModel::lookupAddress(const std::string &address, bool tooltip) const { QString label = walletModel->getAddressTableModel()->labelForAddress( QString::fromStdString(address)); QString description; if (!label.isEmpty()) { description += label; } if (label.isEmpty() || tooltip) { description += QString(" (") + QString::fromStdString(address) + QString(")"); } return description; } QString TransactionTableModel::formatTxType(const TransactionRecord *wtx) const { switch (wtx->type) { case TransactionRecord::RecvWithAddress: return tr("Received with"); case TransactionRecord::RecvFromOther: return tr("Received from"); case TransactionRecord::SendToAddress: case TransactionRecord::SendToOther: return tr("Sent to"); case TransactionRecord::SendToSelf: return tr("Payment to yourself"); case TransactionRecord::Generated: return tr("Mined"); default: return QString(); } } QVariant TransactionTableModel::txAddressDecoration(const TransactionRecord *wtx) const { switch (wtx->type) { case TransactionRecord::Generated: return QIcon(":/icons/tx_mined"); case TransactionRecord::RecvWithAddress: case TransactionRecord::RecvFromOther: return QIcon(":/icons/tx_input"); case TransactionRecord::SendToAddress: case TransactionRecord::SendToOther: return QIcon(":/icons/tx_output"); default: return QIcon(":/icons/tx_inout"); } } QString TransactionTableModel::formatTxToAddress(const TransactionRecord *wtx, bool tooltip) const { QString watchAddress; if (tooltip) { // Mark transactions involving watch-only addresses by adding " // (watch-only)" watchAddress = wtx->involvesWatchAddress ? QString(" (") + tr("watch-only") + QString(")") : ""; } switch (wtx->type) { case TransactionRecord::RecvFromOther: return QString::fromStdString(wtx->address) + watchAddress; case TransactionRecord::RecvWithAddress: case TransactionRecord::SendToAddress: case TransactionRecord::Generated: return lookupAddress(wtx->address, tooltip) + watchAddress; case TransactionRecord::SendToOther: return QString::fromStdString(wtx->address) + watchAddress; case TransactionRecord::SendToSelf: return lookupAddress(wtx->address, tooltip) + watchAddress; default: return tr("(n/a)") + watchAddress; } } QVariant TransactionTableModel::addressColor(const TransactionRecord *wtx) const { // Show addresses without label in a less visible color switch (wtx->type) { case TransactionRecord::RecvWithAddress: case TransactionRecord::SendToAddress: case TransactionRecord::Generated: { QString label = walletModel->getAddressTableModel()->labelForAddress( QString::fromStdString(wtx->address)); if (label.isEmpty()) { return COLOR_BAREADDRESS; } } break; case TransactionRecord::SendToSelf: return COLOR_BAREADDRESS; default: break; } return QVariant(); } QString TransactionTableModel::formatTxAmount( const TransactionRecord *wtx, bool showUnconfirmed, BitcoinUnits::SeparatorStyle separators) const { QString str = BitcoinUnits::format(walletModel->getOptionsModel()->getDisplayUnit(), wtx->credit + wtx->debit, false, separators); if (showUnconfirmed) { if (!wtx->status.countsForBalance) { str = QString("[") + str + QString("]"); } } return QString(str); } QVariant TransactionTableModel::txStatusDecoration(const TransactionRecord *wtx) const { switch (wtx->status.status) { case TransactionStatus::OpenUntilBlock: case TransactionStatus::OpenUntilDate: return COLOR_TX_STATUS_OPENUNTILDATE; case TransactionStatus::Unconfirmed: return QIcon(":/icons/transaction_0"); case TransactionStatus::Abandoned: return QIcon(":/icons/transaction_abandoned"); case TransactionStatus::Confirming: switch (wtx->status.depth) { case 1: return QIcon(":/icons/transaction_1"); case 2: return QIcon(":/icons/transaction_2"); case 3: return QIcon(":/icons/transaction_3"); case 4: return QIcon(":/icons/transaction_4"); default: return QIcon(":/icons/transaction_5"); }; case TransactionStatus::Confirmed: return QIcon(":/icons/transaction_confirmed"); case TransactionStatus::Conflicted: return QIcon(":/icons/transaction_conflicted"); case TransactionStatus::Immature: { int total = wtx->status.depth + wtx->status.matures_in; int part = (wtx->status.depth * 4 / total) + 1; return QIcon(QString(":/icons/transaction_%1").arg(part)); } case TransactionStatus::NotAccepted: return QIcon(":/icons/transaction_0"); default: return COLOR_BLACK; } } QVariant TransactionTableModel::txWatchonlyDecoration( const TransactionRecord *wtx) const { if (wtx->involvesWatchAddress) { return QIcon(":/icons/eye"); } return QVariant(); } QString TransactionTableModel::formatTooltip(const TransactionRecord *rec) const { QString tooltip = formatTxStatus(rec) + QString("\n") + formatTxType(rec); if (rec->type == TransactionRecord::RecvFromOther || rec->type == TransactionRecord::SendToOther || rec->type == TransactionRecord::SendToAddress || rec->type == TransactionRecord::RecvWithAddress) { tooltip += QString(" ") + formatTxToAddress(rec, true); } return tooltip; } QVariant TransactionTableModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } TransactionRecord *rec = static_cast(index.internalPointer()); switch (role) { case RawDecorationRole: switch (index.column()) { case Status: return txStatusDecoration(rec); case Watchonly: return txWatchonlyDecoration(rec); case ToAddress: return txAddressDecoration(rec); } break; case Qt::DecorationRole: { QIcon icon = qvariant_cast(index.data(RawDecorationRole)); return platformStyle->TextColorIcon(icon); } case Qt::DisplayRole: switch (index.column()) { case Date: return formatTxDate(rec); case Type: return formatTxType(rec); case ToAddress: return formatTxToAddress(rec, false); case Amount: return formatTxAmount(rec, true, BitcoinUnits::separatorAlways); } break; case Qt::EditRole: // Edit role is used for sorting, so return the unformatted values switch (index.column()) { case Status: return QString::fromStdString(rec->status.sortKey); case Date: return rec->time; case Type: return formatTxType(rec); case Watchonly: return (rec->involvesWatchAddress ? 1 : 0); case ToAddress: return formatTxToAddress(rec, true); case Amount: return qint64((rec->credit + rec->debit) / SATOSHI); } break; case Qt::ToolTipRole: return formatTooltip(rec); case Qt::TextAlignmentRole: return column_alignments[index.column()]; case Qt::ForegroundRole: // Use the "danger" color for abandoned transactions if (rec->status.status == TransactionStatus::Abandoned) { return COLOR_TX_STATUS_DANGER; } // Non-confirmed (but not immature) as transactions are grey if (!rec->status.countsForBalance && rec->status.status != TransactionStatus::Immature) { return COLOR_UNCONFIRMED; } if (index.column() == Amount && (rec->credit + rec->debit) < ::Amount::zero()) { return COLOR_NEGATIVE; } if (index.column() == ToAddress) { return addressColor(rec); } break; case TypeRole: return rec->type; case DateRole: return QDateTime::fromTime_t(static_cast(rec->time)); case WatchonlyRole: return rec->involvesWatchAddress; case WatchonlyDecorationRole: return txWatchonlyDecoration(rec); case LongDescriptionRole: return priv->describe( walletModel->node(), walletModel->wallet(), rec, walletModel->getOptionsModel()->getDisplayUnit()); case AddressRole: return QString::fromStdString(rec->address); case LabelRole: return walletModel->getAddressTableModel()->labelForAddress( QString::fromStdString(rec->address)); case AmountRole: return qint64((rec->credit + rec->debit) / SATOSHI); case TxIDRole: return rec->getTxID(); case TxHashRole: return QString::fromStdString(rec->txid.ToString()); case TxHexRole: return priv->getTxHex(walletModel->wallet(), rec); case TxPlainTextRole: { QString details; QDateTime date = QDateTime::fromTime_t(static_cast(rec->time)); QString txLabel = walletModel->getAddressTableModel()->labelForAddress( QString::fromStdString(rec->address)); details.append(date.toString("M/d/yy HH:mm")); details.append(" "); details.append(formatTxStatus(rec)); details.append(". "); if (!formatTxType(rec).isEmpty()) { details.append(formatTxType(rec)); details.append(" "); } if (!rec->address.empty()) { if (txLabel.isEmpty()) { details.append(tr("(no label)") + " "); } else { details.append("("); details.append(txLabel); details.append(") "); } details.append(QString::fromStdString(rec->address)); details.append(" "); } details.append( formatTxAmount(rec, false, BitcoinUnits::separatorNever)); return details; } case ConfirmedRole: return rec->status.countsForBalance; case FormattedAmountRole: // Used for copy/export, so don't include separators return formatTxAmount(rec, false, BitcoinUnits::separatorNever); case StatusRole: return rec->status.status; } return QVariant(); } QVariant TransactionTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (role == Qt::DisplayRole) { return columns[section]; } else if (role == Qt::TextAlignmentRole) { return column_alignments[section]; } else if (role == Qt::ToolTipRole) { switch (section) { case Status: return tr("Transaction status. Hover over this field to " "show number of confirmations."); case Date: return tr( "Date and time that the transaction was received."); case Type: return tr("Type of transaction."); case Watchonly: return tr("Whether or not a watch-only address is involved " "in this transaction."); case ToAddress: return tr( "User-defined intent/purpose of the transaction."); case Amount: return tr("Amount removed from or added to balance."); } } } return QVariant(); } QModelIndex TransactionTableModel::index(int row, int column, const QModelIndex &parent) const { Q_UNUSED(parent); - TransactionRecord *data = priv->index(walletModel->wallet(), row); + TransactionRecord *data = priv->index( + walletModel->wallet(), walletModel->clientModel().getNumBlocks(), row); if (data) { - return createIndex(row, column, - priv->index(walletModel->wallet(), row)); + return createIndex(row, column, data); } return QModelIndex(); } void TransactionTableModel::updateDisplayUnit() { // emit dataChanged to update Amount column with the current unit updateAmountColumnTitle(); Q_EMIT dataChanged(index(0, Amount), index(priv->size() - 1, Amount)); } // queue notifications to show a non freezing progress dialog e.g. for rescan struct TransactionNotification { public: TransactionNotification() {} TransactionNotification(TxId _txid, ChangeType _status, bool _showTransaction) : txid(_txid), status(_status), showTransaction(_showTransaction) {} void invoke(QObject *ttm) { QString strHash = QString::fromStdString(txid.GetHex()); qDebug() << "NotifyTransactionChanged: " + strHash + " status= " + QString::number(status); bool invoked = QMetaObject::invokeMethod( ttm, "updateTransaction", Qt::QueuedConnection, Q_ARG(QString, strHash), Q_ARG(int, status), Q_ARG(bool, showTransaction)); assert(invoked); } private: TxId txid; ChangeType status; bool showTransaction; }; static bool fQueueNotifications = false; static std::vector vQueueNotifications; static void NotifyTransactionChanged(TransactionTableModel *ttm, const TxId &txid, ChangeType status) { // Find transaction in wallet // Determine whether to show transaction or not (determine this here so that // no relocking is needed in GUI thread) bool showTransaction = TransactionRecord::showTransaction(); TransactionNotification notification(txid, status, showTransaction); if (fQueueNotifications) { vQueueNotifications.push_back(notification); return; } notification.invoke(ttm); } static void ShowProgress(TransactionTableModel *ttm, const std::string &title, int nProgress) { if (nProgress == 0) { fQueueNotifications = true; } if (nProgress == 100) { fQueueNotifications = false; if (vQueueNotifications.size() > 10) { // prevent balloon spam, show maximum 10 balloons bool invoked = QMetaObject::invokeMethod( ttm, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, true)); assert(invoked); } for (size_t i = 0; i < vQueueNotifications.size(); ++i) { if (vQueueNotifications.size() - i <= 10) { bool invoked = QMetaObject::invokeMethod( ttm, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, false)); assert(invoked); } vQueueNotifications[i].invoke(ttm); } // clear std::vector().swap(vQueueNotifications); } } void TransactionTableModel::subscribeToCoreSignals() { // Connect signals to wallet m_handler_transaction_changed = walletModel->wallet().handleTransactionChanged( std::bind(NotifyTransactionChanged, this, std::placeholders::_1, std::placeholders::_2)); m_handler_show_progress = walletModel->wallet().handleShowProgress(std::bind( ShowProgress, this, std::placeholders::_1, std::placeholders::_2)); } void TransactionTableModel::unsubscribeFromCoreSignals() { // Disconnect signals from wallet m_handler_transaction_changed->disconnect(); m_handler_show_progress->disconnect(); } diff --git a/src/qt/walletcontroller.cpp b/src/qt/walletcontroller.cpp index 58cc31643..75a6ea51e 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -1,326 +1,328 @@ // Copyright (c) 2019 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include -WalletController::WalletController(interfaces::Node &node, +WalletController::WalletController(ClientModel &client_model, const PlatformStyle *platform_style, - OptionsModel *options_model, QObject *parent) + QObject *parent) : QObject(parent), m_activity_thread(new QThread(this)), - m_activity_worker(new QObject), m_node(node), - m_platform_style(platform_style), m_options_model(options_model) { + m_activity_worker(new QObject), m_client_model(client_model), + m_node(client_model.node()), m_platform_style(platform_style), + m_options_model(client_model.getOptionsModel()) { m_handler_load_wallet = m_node.handleLoadWallet( [this](std::unique_ptr wallet) { getOrCreateWallet(std::move(wallet)); }); for (std::unique_ptr &wallet : m_node.getWallets()) { getOrCreateWallet(std::move(wallet)); } m_activity_worker->moveToThread(m_activity_thread); m_activity_thread->start(); } // Not using the default destructor because not all member types definitions are // available in the header, just forward declared. WalletController::~WalletController() { m_activity_thread->quit(); m_activity_thread->wait(); delete m_activity_worker; } std::vector WalletController::getOpenWallets() const { QMutexLocker locker(&m_mutex); return m_wallets; } std::map WalletController::listWalletDir() const { QMutexLocker locker(&m_mutex); std::map wallets; for (const std::string &name : m_node.listWalletDir()) { wallets[name] = false; } for (WalletModel *wallet_model : m_wallets) { auto it = wallets.find(wallet_model->wallet().getWalletName()); if (it != wallets.end()) { it->second = true; } } return wallets; } void WalletController::closeWallet(WalletModel *wallet_model, QWidget *parent) { QMessageBox box(parent); box.setWindowTitle(tr("Close wallet")); box.setText(tr("Are you sure you wish to close the wallet %1?") .arg(GUIUtil::HtmlEscape(wallet_model->getDisplayName()))); box.setInformativeText( tr("Closing the wallet for too long can result in having to resync the " "entire chain if pruning is enabled.")); box.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); box.setDefaultButton(QMessageBox::Yes); if (box.exec() != QMessageBox::Yes) { return; } // First remove wallet from node. wallet_model->wallet().remove(); // Now release the model. removeAndDeleteWallet(wallet_model); } WalletModel *WalletController::getOrCreateWallet( std::unique_ptr wallet) { QMutexLocker locker(&m_mutex); // Return model instance if exists. if (!m_wallets.empty()) { std::string name = wallet->getWalletName(); for (WalletModel *wallet_model : m_wallets) { if (wallet_model->wallet().getWalletName() == name) { return wallet_model; } } } // Instantiate model and register it. WalletModel *wallet_model = new WalletModel( - std::move(wallet), m_node, m_platform_style, m_options_model, nullptr); + std::move(wallet), m_client_model, m_platform_style, nullptr); // Handler callback runs in a different thread so fix wallet model thread // affinity. wallet_model->moveToThread(thread()); wallet_model->setParent(this); m_wallets.push_back(wallet_model); // WalletModel::startPollBalance needs to be called in a thread managed by // Qt because of startTimer. Considering the current thread can be a RPC // thread, better delegate the calling to Qt with Qt::AutoConnection. const bool called = QMetaObject::invokeMethod(wallet_model, "startPollBalance"); assert(called); connect( wallet_model, &WalletModel::unload, this, [this, wallet_model] { // Defer removeAndDeleteWallet when no modal widget is active. // TODO: remove this workaround by removing usage of QDiallog::exec. if (QApplication::activeModalWidget()) { connect( qApp, &QApplication::focusWindowChanged, wallet_model, [this, wallet_model]() { if (!QApplication::activeModalWidget()) { removeAndDeleteWallet(wallet_model); } }, Qt::QueuedConnection); } else { removeAndDeleteWallet(wallet_model); } }, Qt::QueuedConnection); // Re-emit coinsSent signal from wallet model. connect(wallet_model, &WalletModel::coinsSent, this, &WalletController::coinsSent); // Notify walletAdded signal on the GUI thread. Q_EMIT walletAdded(wallet_model); return wallet_model; } void WalletController::removeAndDeleteWallet(WalletModel *wallet_model) { // Unregister wallet model. { QMutexLocker locker(&m_mutex); m_wallets.erase( std::remove(m_wallets.begin(), m_wallets.end(), wallet_model)); } Q_EMIT walletRemoved(wallet_model); // Currently this can trigger the unload since the model can hold the last // CWallet shared pointer. delete wallet_model; } WalletControllerActivity::WalletControllerActivity( WalletController *wallet_controller, QWidget *parent_widget, const CChainParams &chainparams) : QObject(wallet_controller), m_wallet_controller(wallet_controller), m_parent_widget(parent_widget), m_chainparams(chainparams) {} WalletControllerActivity::~WalletControllerActivity() { delete m_progress_dialog; } void WalletControllerActivity::showProgressDialog(const QString &label_text) { m_progress_dialog = new QProgressDialog(m_parent_widget); m_progress_dialog->setLabelText(label_text); m_progress_dialog->setRange(0, 0); m_progress_dialog->setCancelButton(nullptr); m_progress_dialog->setWindowModality(Qt::ApplicationModal); GUIUtil::PolishProgressDialog(m_progress_dialog); } CreateWalletActivity::CreateWalletActivity(WalletController *wallet_controller, QWidget *parent_widget, const CChainParams &chainparams) : WalletControllerActivity(wallet_controller, parent_widget, chainparams) { m_passphrase.reserve(MAX_PASSPHRASE_SIZE); } CreateWalletActivity::~CreateWalletActivity() { delete m_create_wallet_dialog; delete m_passphrase_dialog; } void CreateWalletActivity::askPassphrase() { m_passphrase_dialog = new AskPassphraseDialog( AskPassphraseDialog::Encrypt, m_parent_widget, &m_passphrase); m_passphrase_dialog->setWindowModality(Qt::ApplicationModal); m_passphrase_dialog->show(); connect(m_passphrase_dialog, &QObject::destroyed, [this] { m_passphrase_dialog = nullptr; }); connect(m_passphrase_dialog, &QDialog::accepted, [this] { createWallet(); }); connect(m_passphrase_dialog, &QDialog::rejected, [this] { Q_EMIT finished(); }); } void CreateWalletActivity::createWallet() { showProgressDialog( tr("Creating Wallet %1...") .arg(m_create_wallet_dialog->walletName().toHtmlEscaped())); std::string name = m_create_wallet_dialog->walletName().toStdString(); uint64_t flags = 0; if (m_create_wallet_dialog->isDisablePrivateKeysChecked()) { flags |= WALLET_FLAG_DISABLE_PRIVATE_KEYS; } if (m_create_wallet_dialog->isMakeBlankWalletChecked()) { flags |= WALLET_FLAG_BLANK_WALLET; } if (m_create_wallet_dialog->isDescriptorWalletChecked()) { flags |= WALLET_FLAG_DESCRIPTORS; } QTimer::singleShot(500, worker(), [this, name, flags] { WalletCreationStatus status; std::unique_ptr wallet = node().createWallet(m_chainparams, m_passphrase, flags, name, m_error_message, m_warning_message, status); if (status == WalletCreationStatus::SUCCESS) { m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(wallet)); } QTimer::singleShot(500, this, &CreateWalletActivity::finish); }); } void CreateWalletActivity::finish() { m_progress_dialog->hide(); if (!m_error_message.empty()) { QMessageBox::critical( m_parent_widget, tr("Create wallet failed"), QString::fromStdString(m_error_message.translated)); } else if (!m_warning_message.empty()) { QMessageBox::warning( m_parent_widget, tr("Create wallet warning"), QString::fromStdString( Join(m_warning_message, Untranslated("\n")).translated)); } if (m_wallet_model) { Q_EMIT created(m_wallet_model); } Q_EMIT finished(); } void CreateWalletActivity::create() { m_create_wallet_dialog = new CreateWalletDialog(m_parent_widget); m_create_wallet_dialog->setWindowModality(Qt::ApplicationModal); m_create_wallet_dialog->show(); connect(m_create_wallet_dialog, &QObject::destroyed, [this] { m_create_wallet_dialog = nullptr; }); connect(m_create_wallet_dialog, &QDialog::rejected, [this] { Q_EMIT finished(); }); connect(m_create_wallet_dialog, &QDialog::accepted, [this] { if (m_create_wallet_dialog->isEncryptWalletChecked()) { askPassphrase(); } else { createWallet(); } }); } OpenWalletActivity::OpenWalletActivity(WalletController *wallet_controller, QWidget *parent_widget, const CChainParams &chainparams) : WalletControllerActivity(wallet_controller, parent_widget, chainparams) {} void OpenWalletActivity::finish() { m_progress_dialog->hide(); if (!m_error_message.empty()) { QMessageBox::critical( m_parent_widget, tr("Open wallet failed"), QString::fromStdString(m_error_message.translated)); } else if (!m_warning_message.empty()) { QMessageBox::warning( m_parent_widget, tr("Open wallet warning"), QString::fromStdString( Join(m_warning_message, Untranslated("\n")).translated)); } if (m_wallet_model) { Q_EMIT opened(m_wallet_model); } Q_EMIT finished(); } void OpenWalletActivity::open(const std::string &path) { QString name = path.empty() ? QString("[" + tr("default wallet") + "]") : QString::fromStdString(path); showProgressDialog( tr("Opening Wallet %1...").arg(name.toHtmlEscaped())); QTimer::singleShot(0, worker(), [this, path] { std::unique_ptr wallet = node().loadWallet( this->m_chainparams, path, m_error_message, m_warning_message); if (wallet) { m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(wallet)); } QTimer::singleShot(0, this, &OpenWalletActivity::finish); }); } diff --git a/src/qt/walletcontroller.h b/src/qt/walletcontroller.h index 868122735..85a01ac1a 100644 --- a/src/qt/walletcontroller.h +++ b/src/qt/walletcontroller.h @@ -1,157 +1,158 @@ // Copyright (c) 2019 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_QT_WALLETCONTROLLER_H #define BITCOIN_QT_WALLETCONTROLLER_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +class ClientModel; class OptionsModel; class PlatformStyle; class WalletModel; namespace interfaces { class Handler; class Node; class Wallet; } // namespace interfaces class AskPassphraseDialog; class CreateWalletActivity; class CreateWalletDialog; class OpenWalletActivity; class WalletControllerActivity; /** * Controller between interfaces::Node, WalletModel instances and the GUI. */ class WalletController : public QObject { Q_OBJECT void removeAndDeleteWallet(WalletModel *wallet_model); public: - WalletController(interfaces::Node &node, - const PlatformStyle *platform_style, - OptionsModel *options_model, QObject *parent); + WalletController(ClientModel &client_model, + const PlatformStyle *platform_style, QObject *parent); ~WalletController(); //! Returns wallet models currently open. std::vector getOpenWallets() const; WalletModel *getOrCreateWallet(std::unique_ptr wallet); //! Returns all wallet names in the wallet dir mapped to whether the wallet //! is loaded. std::map listWalletDir() const; void closeWallet(WalletModel *wallet_model, QWidget *parent = nullptr); Q_SIGNALS: void walletAdded(WalletModel *wallet_model); void walletRemoved(WalletModel *wallet_model); void coinsSent(interfaces::Wallet &wallet, SendCoinsRecipient recipient, QByteArray transaction); private: QThread *const m_activity_thread; QObject *const m_activity_worker; + ClientModel &m_client_model; interfaces::Node &m_node; const PlatformStyle *const m_platform_style; OptionsModel *const m_options_model; mutable QMutex m_mutex; std::vector m_wallets; std::unique_ptr m_handler_load_wallet; friend class WalletControllerActivity; }; class WalletControllerActivity : public QObject { Q_OBJECT public: WalletControllerActivity(WalletController *wallet_controller, QWidget *parent_widget, const CChainParams &chainparams); virtual ~WalletControllerActivity(); Q_SIGNALS: void finished(); protected: interfaces::Node &node() const { return m_wallet_controller->m_node; } QObject *worker() const { return m_wallet_controller->m_activity_worker; } void showProgressDialog(const QString &label_text); WalletController *const m_wallet_controller; QWidget *const m_parent_widget; QProgressDialog *m_progress_dialog{nullptr}; WalletModel *m_wallet_model{nullptr}; bilingual_str m_error_message; std::vector m_warning_message; const CChainParams &m_chainparams; }; class CreateWalletActivity : public WalletControllerActivity { Q_OBJECT public: CreateWalletActivity(WalletController *wallet_controller, QWidget *parent_widget, const CChainParams &chainparams); virtual ~CreateWalletActivity(); void create(); Q_SIGNALS: void created(WalletModel *wallet_model); private: void askPassphrase(); void createWallet(); void finish(); SecureString m_passphrase; CreateWalletDialog *m_create_wallet_dialog{nullptr}; AskPassphraseDialog *m_passphrase_dialog{nullptr}; }; class OpenWalletActivity : public WalletControllerActivity { Q_OBJECT public: OpenWalletActivity(WalletController *wallet_controller, QWidget *parent_widget, const CChainParams &chainparams); void open(const std::string &path); Q_SIGNALS: void opened(WalletModel *wallet_model); private: void finish(); }; #endif // BITCOIN_QT_WALLETCONTROLLER_H diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index f447e8bf9..e9d799cb7 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -1,522 +1,523 @@ // Copyright (c) 2011-2019 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include +#include #include #include #include #include #include // for GetBoolArg #include #include #include // for CRecipient #include #include #include #include WalletModel::WalletModel(std::unique_ptr wallet, - interfaces::Node &node, - const PlatformStyle *platformStyle, - OptionsModel *_optionsModel, QObject *parent) - : QObject(parent), m_wallet(std::move(wallet)), m_node(node), - optionsModel(_optionsModel), addressTableModel(nullptr), + ClientModel &client_model, + const PlatformStyle *platformStyle, QObject *parent) + : QObject(parent), m_wallet(std::move(wallet)), + m_client_model(client_model), m_node(client_model.node()), + optionsModel(client_model.getOptionsModel()), addressTableModel(nullptr), transactionTableModel(nullptr), recentRequestsTableModel(nullptr), cachedEncryptionStatus(Unencrypted), cachedNumBlocks(0) { fHaveWatchOnly = m_wallet->haveWatchOnly(); addressTableModel = new AddressTableModel(this); transactionTableModel = new TransactionTableModel(platformStyle, this); recentRequestsTableModel = new RecentRequestsTableModel(this); subscribeToCoreSignals(); } WalletModel::~WalletModel() { unsubscribeFromCoreSignals(); } void WalletModel::startPollBalance() { // This timer will be fired repeatedly to update the balance QTimer *timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &WalletModel::pollBalanceChanged); timer->start(MODEL_UPDATE_DELAY); } void WalletModel::updateStatus() { EncryptionStatus newEncryptionStatus = getEncryptionStatus(); if (cachedEncryptionStatus != newEncryptionStatus) { Q_EMIT encryptionStatusChanged(); } } void WalletModel::pollBalanceChanged() { // Try to get balances and return early if locks can't be acquired. This // avoids the GUI from getting stuck on periodical polls if the core is // holding the locks for a longer time - for example, during a wallet // rescan. interfaces::WalletBalances new_balances; int numBlocks = -1; if (!m_wallet->tryGetBalances(new_balances, numBlocks)) { return; } if (fForceCheckBalanceChanged || numBlocks != cachedNumBlocks) { fForceCheckBalanceChanged = false; // Balance and number of transactions might have changed cachedNumBlocks = numBlocks; checkBalanceChanged(new_balances); if (transactionTableModel) { transactionTableModel->updateConfirmations(); } } } void WalletModel::checkBalanceChanged( const interfaces::WalletBalances &new_balances) { if (new_balances.balanceChanged(m_cached_balances)) { m_cached_balances = new_balances; Q_EMIT balanceChanged(new_balances); } } void WalletModel::updateTransaction() { // Balance and number of transactions might have changed fForceCheckBalanceChanged = true; } void WalletModel::updateAddressBook(const QString &address, const QString &label, bool isMine, const QString &purpose, int status) { if (addressTableModel) { addressTableModel->updateEntry(address, label, isMine, purpose, status); } } void WalletModel::updateWatchOnlyFlag(bool fHaveWatchonly) { fHaveWatchOnly = fHaveWatchonly; Q_EMIT notifyWatchonlyChanged(fHaveWatchonly); } bool WalletModel::validateAddress(const QString &address) { return IsValidDestinationString(address.toStdString(), getChainParams()); } WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransaction &transaction, const CCoinControl &coinControl) { Amount total = Amount::zero(); bool fSubtractFeeFromAmount = false; QList recipients = transaction.getRecipients(); std::vector vecSend; if (recipients.empty()) { return OK; } // Used to detect duplicates QSet setAddress; int nAddresses = 0; // Pre-check input data for validity for (const SendCoinsRecipient &rcp : recipients) { if (rcp.fSubtractFeeFromAmount) { fSubtractFeeFromAmount = true; } #ifdef ENABLE_BIP70 // PaymentRequest... if (rcp.paymentRequest.IsInitialized()) { Amount subtotal = Amount::zero(); const payments::PaymentDetails &details = rcp.paymentRequest.getDetails(); for (int i = 0; i < details.outputs_size(); i++) { const payments::Output &out = details.outputs(i); if (out.amount() <= 0) { continue; } subtotal += int64_t(out.amount()) * SATOSHI; const uint8_t *scriptStr = (const uint8_t *)out.script().data(); CScript scriptPubKey(scriptStr, scriptStr + out.script().size()); Amount nAmount = int64_t(out.amount()) * SATOSHI; CRecipient recipient = {scriptPubKey, nAmount, rcp.fSubtractFeeFromAmount}; vecSend.push_back(recipient); } if (subtotal <= Amount::zero()) { return InvalidAmount; } total += subtotal; } // User-entered bitcoin address / amount: else #endif { if (!validateAddress(rcp.address)) { return InvalidAddress; } if (rcp.amount <= Amount::zero()) { return InvalidAmount; } setAddress.insert(rcp.address); ++nAddresses; CScript scriptPubKey = GetScriptForDestination( DecodeDestination(rcp.address.toStdString(), getChainParams())); CRecipient recipient = {scriptPubKey, Amount(rcp.amount), rcp.fSubtractFeeFromAmount}; vecSend.push_back(recipient); total += rcp.amount; } } if (setAddress.size() != nAddresses) { return DuplicateAddress; } Amount nBalance = m_wallet->getAvailableBalance(coinControl); if (total > nBalance) { return AmountExceedsBalance; } Amount nFeeRequired = Amount::zero(); int nChangePosRet = -1; bilingual_str error; auto &newTx = transaction.getWtx(); newTx = m_wallet->createTransaction( vecSend, coinControl, !wallet().privateKeysDisabled() /* sign */, nChangePosRet, nFeeRequired, error); transaction.setTransactionFee(nFeeRequired); if (fSubtractFeeFromAmount && newTx) { transaction.reassignAmounts(nChangePosRet); } if (!newTx) { if (!fSubtractFeeFromAmount && (total + nFeeRequired) > nBalance) { return SendCoinsReturn(AmountWithFeeExceedsBalance); } Q_EMIT message(tr("Send Coins"), QString::fromStdString(error.translated), CClientUIInterface::MSG_ERROR); return TransactionCreationFailed; } // Reject absurdly high fee. (This can never happen because the // wallet never creates transactions with fee greater than // m_default_max_tx_fee. This merely a belt-and-suspenders check). if (nFeeRequired > m_wallet->getDefaultMaxTxFee()) { return AbsurdFee; } return SendCoinsReturn(OK); } WalletModel::SendCoinsReturn WalletModel::sendCoins(WalletModelTransaction &transaction) { /* store serialized transaction */ QByteArray transaction_array; std::vector> vOrderForm; for (const SendCoinsRecipient &rcp : transaction.getRecipients()) { #ifdef ENABLE_BIP70 if (rcp.paymentRequest.IsInitialized()) { // Make sure any payment requests involved are still valid. if (PaymentServer::verifyExpired(rcp.paymentRequest.getDetails())) { return PaymentRequestExpired; } // Store PaymentRequests in wtx.vOrderForm in wallet. std::string value; rcp.paymentRequest.SerializeToString(&value); vOrderForm.emplace_back("PaymentRequest", std::move(value)); } else #endif { if (!rcp.message.isEmpty()) { // Message from normal bitcoincash:URI // (bitcoincash:123...?message=example) vOrderForm.emplace_back("Message", rcp.message.toStdString()); } } } auto &newTx = transaction.getWtx(); wallet().commitTransaction(newTx, {} /* mapValue */, std::move(vOrderForm)); CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); ssTx << *newTx; transaction_array.append(&(ssTx[0]), ssTx.size()); // Add addresses / update labels that we've sent to the address book, and // emit coinsSent signal for each recipient for (const SendCoinsRecipient &rcp : transaction.getRecipients()) { // Don't touch the address book when we have a payment request #ifdef ENABLE_BIP70 if (!rcp.paymentRequest.IsInitialized()) #endif { std::string strAddress = rcp.address.toStdString(); CTxDestination dest = DecodeDestination(strAddress, getChainParams()); std::string strLabel = rcp.label.toStdString(); // Check if we have a new address or an updated label std::string name; if (!m_wallet->getAddress(dest, &name, /* is_mine= */ nullptr, /* purpose= */ nullptr)) { m_wallet->setAddressBook(dest, strLabel, "send"); } else if (name != strLabel) { // "" means don't change purpose m_wallet->setAddressBook(dest, strLabel, ""); } } Q_EMIT coinsSent(this->wallet(), rcp, transaction_array); } // update balance immediately, otherwise there could be a short noticeable // delay until pollBalanceChanged hits checkBalanceChanged(m_wallet->getBalances()); return SendCoinsReturn(OK); } OptionsModel *WalletModel::getOptionsModel() { return optionsModel; } AddressTableModel *WalletModel::getAddressTableModel() { return addressTableModel; } TransactionTableModel *WalletModel::getTransactionTableModel() { return transactionTableModel; } RecentRequestsTableModel *WalletModel::getRecentRequestsTableModel() { return recentRequestsTableModel; } WalletModel::EncryptionStatus WalletModel::getEncryptionStatus() const { if (!m_wallet->isCrypted()) { return Unencrypted; } else if (m_wallet->isLocked()) { return Locked; } else { return Unlocked; } } bool WalletModel::setWalletEncrypted(bool encrypted, const SecureString &passphrase) { if (encrypted) { // Encrypt return m_wallet->encryptWallet(passphrase); } else { // Decrypt -- TODO; not supported yet return false; } } bool WalletModel::setWalletLocked(bool locked, const SecureString &passPhrase) { if (locked) { // Lock return m_wallet->lock(); } else { // Unlock return m_wallet->unlock(passPhrase); } } bool WalletModel::changePassphrase(const SecureString &oldPass, const SecureString &newPass) { // Make sure wallet is locked before attempting pass change m_wallet->lock(); return m_wallet->changeWalletPassphrase(oldPass, newPass); } // Handlers for core signals static void NotifyUnload(WalletModel *walletModel) { qDebug() << "NotifyUnload"; bool invoked = QMetaObject::invokeMethod(walletModel, "unload"); assert(invoked); } static void NotifyKeyStoreStatusChanged(WalletModel *walletmodel) { qDebug() << "NotifyKeyStoreStatusChanged"; bool invoked = QMetaObject::invokeMethod(walletmodel, "updateStatus", Qt::QueuedConnection); assert(invoked); } static void NotifyAddressBookChanged(WalletModel *walletmodel, const CTxDestination &address, const std::string &label, bool isMine, const std::string &purpose, ChangeType status) { QString strAddress = QString::fromStdString( EncodeCashAddr(address, walletmodel->getChainParams())); QString strLabel = QString::fromStdString(label); QString strPurpose = QString::fromStdString(purpose); qDebug() << "NotifyAddressBookChanged: " + strAddress + " " + strLabel + " isMine=" + QString::number(isMine) + " purpose=" + strPurpose + " status=" + QString::number(status); bool invoked = QMetaObject::invokeMethod( walletmodel, "updateAddressBook", Qt::QueuedConnection, Q_ARG(QString, strAddress), Q_ARG(QString, strLabel), Q_ARG(bool, isMine), Q_ARG(QString, strPurpose), Q_ARG(int, status)); assert(invoked); } static void NotifyTransactionChanged(WalletModel *walletmodel, const TxId &hash, ChangeType status) { Q_UNUSED(hash); Q_UNUSED(status); bool invoked = QMetaObject::invokeMethod(walletmodel, "updateTransaction", Qt::QueuedConnection); assert(invoked); } static void ShowProgress(WalletModel *walletmodel, const std::string &title, int nProgress) { // emits signal "showProgress" bool invoked = QMetaObject::invokeMethod( walletmodel, "showProgress", Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(title)), Q_ARG(int, nProgress)); assert(invoked); } static void NotifyWatchonlyChanged(WalletModel *walletmodel, bool fHaveWatchonly) { bool invoked = QMetaObject::invokeMethod(walletmodel, "updateWatchOnlyFlag", Qt::QueuedConnection, Q_ARG(bool, fHaveWatchonly)); assert(invoked); } static void NotifyCanGetAddressesChanged(WalletModel *walletmodel) { bool invoked = QMetaObject::invokeMethod(walletmodel, "canGetAddressesChanged"); assert(invoked); } void WalletModel::subscribeToCoreSignals() { // Connect signals to wallet m_handler_unload = m_wallet->handleUnload(std::bind(&NotifyUnload, this)); m_handler_status_changed = m_wallet->handleStatusChanged( std::bind(&NotifyKeyStoreStatusChanged, this)); m_handler_address_book_changed = m_wallet->handleAddressBookChanged( std::bind(NotifyAddressBookChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); m_handler_transaction_changed = m_wallet->handleTransactionChanged( std::bind(NotifyTransactionChanged, this, std::placeholders::_1, std::placeholders::_2)); m_handler_show_progress = m_wallet->handleShowProgress(std::bind( ShowProgress, this, std::placeholders::_1, std::placeholders::_2)); m_handler_watch_only_changed = m_wallet->handleWatchOnlyChanged( std::bind(NotifyWatchonlyChanged, this, std::placeholders::_1)); m_handler_can_get_addrs_changed = m_wallet->handleCanGetAddressesChanged( std::bind(NotifyCanGetAddressesChanged, this)); } void WalletModel::unsubscribeFromCoreSignals() { // Disconnect signals from wallet m_handler_unload->disconnect(); m_handler_status_changed->disconnect(); m_handler_address_book_changed->disconnect(); m_handler_transaction_changed->disconnect(); m_handler_show_progress->disconnect(); m_handler_watch_only_changed->disconnect(); m_handler_can_get_addrs_changed->disconnect(); } // WalletModel::UnlockContext implementation WalletModel::UnlockContext WalletModel::requestUnlock() { bool was_locked = getEncryptionStatus() == Locked; if (was_locked) { // Request UI to unlock wallet Q_EMIT requireUnlock(); } // If wallet is still locked, unlock was failed or cancelled, mark context // as invalid bool valid = getEncryptionStatus() != Locked; return UnlockContext(this, valid, was_locked); } WalletModel::UnlockContext::UnlockContext(WalletModel *_wallet, bool _valid, bool _relock) : wallet(_wallet), valid(_valid), relock(_relock) {} WalletModel::UnlockContext::~UnlockContext() { if (valid && relock) { wallet->setWalletLocked(true); } } void WalletModel::UnlockContext::CopyFrom(UnlockContext &&rhs) { // Transfer context; old object no longer relocks wallet *this = rhs; rhs.relock = false; } void WalletModel::loadReceiveRequests( std::vector &vReceiveRequests) { // receive request vReceiveRequests = m_wallet->getDestValues("rr"); } bool WalletModel::saveReceiveRequest(const std::string &sAddress, const int64_t nId, const std::string &sRequest) { CTxDestination dest = DecodeDestination(sAddress, getChainParams()); std::stringstream ss; ss << nId; // "rr" prefix = "receive request" in destdata std::string key = "rr" + ss.str(); return sRequest.empty() ? m_wallet->eraseDestData(dest, key) : m_wallet->addDestData(dest, key, sRequest); } bool WalletModel::isWalletEnabled() { return !gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET); } QString WalletModel::getWalletName() const { return QString::fromStdString(m_wallet->getWalletName()); } QString WalletModel::getDisplayName() const { const QString name = getWalletName(); return name.isEmpty() ? "[" + tr("default wallet") + "]" : name; } bool WalletModel::isMultiwallet() { return m_node.getWallets().size() > 1; } const CChainParams &WalletModel::getChainParams() const { return Params(); } diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 666a21647..88a05d5b1 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -1,245 +1,248 @@ // Copyright (c) 2011-2019 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_QT_WALLETMODEL_H #define BITCOIN_QT_WALLETMODEL_H #include #include #if defined(HAVE_CONFIG_H) #include #endif #include #include #include #include #include class AddressTableModel; +class ClientModel; class OptionsModel; class PlatformStyle; class RecentRequestsTableModel; class SendCoinsRecipient; class TransactionTableModel; class WalletModelTransaction; class CCoinControl; class CKeyID; class COutPoint; class COutput; class CPubKey; namespace interfaces { class Node; } // namespace interfaces QT_BEGIN_NAMESPACE class QTimer; QT_END_NAMESPACE /** Interface to Bitcoin wallet from Qt view code. */ class WalletModel : public QObject { Q_OBJECT public: explicit WalletModel(std::unique_ptr wallet, - interfaces::Node &node, + ClientModel &client_model, const PlatformStyle *platformStyle, - OptionsModel *optionsModel, QObject *parent = nullptr); + QObject *parent = nullptr); ~WalletModel(); // Returned by sendCoins enum StatusCode { OK, InvalidAmount, InvalidAddress, AmountExceedsBalance, AmountWithFeeExceedsBalance, DuplicateAddress, // Error returned when wallet is still locked TransactionCreationFailed, AbsurdFee, PaymentRequestExpired }; enum EncryptionStatus { // !wallet->IsCrypted() Unencrypted, // wallet->IsCrypted() && wallet->IsLocked() Locked, // wallet->IsCrypted() && !wallet->IsLocked() Unlocked }; OptionsModel *getOptionsModel(); AddressTableModel *getAddressTableModel(); TransactionTableModel *getTransactionTableModel(); RecentRequestsTableModel *getRecentRequestsTableModel(); EncryptionStatus getEncryptionStatus() const; // Check address for validity bool validateAddress(const QString &address); // Return status record for SendCoins, contains error id + information struct SendCoinsReturn { SendCoinsReturn(StatusCode _status = OK, QString _reasonCommitFailed = "") : status(_status), reasonCommitFailed(_reasonCommitFailed) {} StatusCode status; QString reasonCommitFailed; }; // prepare transaction for getting txfee before sending coins SendCoinsReturn prepareTransaction(WalletModelTransaction &transaction, const CCoinControl &coinControl); // Send coins to a list of recipients SendCoinsReturn sendCoins(WalletModelTransaction &transaction); // Wallet encryption bool setWalletEncrypted(bool encrypted, const SecureString &passphrase); // Passphrase only needed when unlocking bool setWalletLocked(bool locked, const SecureString &passPhrase = SecureString()); bool changePassphrase(const SecureString &oldPass, const SecureString &newPass); // RAI object for unlocking wallet, returned by requestUnlock() class UnlockContext { public: UnlockContext(WalletModel *wallet, bool valid, bool relock); ~UnlockContext(); bool isValid() const { return valid; } // Copy constructor is disabled. UnlockContext(const UnlockContext &) = delete; // Move operator and constructor transfer the context UnlockContext(UnlockContext &&obj) { CopyFrom(std::move(obj)); } UnlockContext &operator=(UnlockContext &&rhs) { CopyFrom(std::move(rhs)); return *this; } private: WalletModel *wallet; bool valid; // mutable, as it can be set to false by copying mutable bool relock; UnlockContext &operator=(const UnlockContext &) = default; void CopyFrom(UnlockContext &&rhs); }; UnlockContext requestUnlock(); void loadReceiveRequests(std::vector &vReceiveRequests); bool saveReceiveRequest(const std::string &sAddress, const int64_t nId, const std::string &sRequest); static bool isWalletEnabled(); interfaces::Node &node() const { return m_node; } interfaces::Wallet &wallet() const { return *m_wallet; } + ClientModel &clientModel() const { return m_client_model; } const CChainParams &getChainParams() const; QString getWalletName() const; QString getDisplayName() const; bool isMultiwallet(); AddressTableModel *getAddressTableModel() const { return addressTableModel; } private: std::unique_ptr m_wallet; std::unique_ptr m_handler_unload; std::unique_ptr m_handler_status_changed; std::unique_ptr m_handler_address_book_changed; std::unique_ptr m_handler_transaction_changed; std::unique_ptr m_handler_show_progress; std::unique_ptr m_handler_watch_only_changed; std::unique_ptr m_handler_can_get_addrs_changed; + ClientModel &m_client_model; interfaces::Node &m_node; bool fHaveWatchOnly; bool fForceCheckBalanceChanged{false}; // Wallet has an options model for wallet-specific options (transaction fee, // for example) OptionsModel *optionsModel; AddressTableModel *addressTableModel; TransactionTableModel *transactionTableModel; RecentRequestsTableModel *recentRequestsTableModel; // Cache some values to be able to detect changes interfaces::WalletBalances m_cached_balances; EncryptionStatus cachedEncryptionStatus; int cachedNumBlocks; void subscribeToCoreSignals(); void unsubscribeFromCoreSignals(); void checkBalanceChanged(const interfaces::WalletBalances &new_balances); Q_SIGNALS: // Signal that balance in wallet changed void balanceChanged(const interfaces::WalletBalances &balances); // Encryption status of wallet changed void encryptionStatusChanged(); // Signal emitted when wallet needs to be unlocked // It is valid behaviour for listeners to keep the wallet locked after this // signal; this means that the unlocking failed or was cancelled. void requireUnlock(); // Fired when a message should be reported to the user void message(const QString &title, const QString &message, unsigned int style); // Coins sent: from wallet, to recipient, in (serialized) transaction: void coinsSent(interfaces::Wallet &wallet, SendCoinsRecipient recipient, QByteArray transaction); // Show progress dialog e.g. for rescan void showProgress(const QString &title, int nProgress); // Watch-only address added void notifyWatchonlyChanged(bool fHaveWatchonly); // Signal that wallet is about to be removed void unload(); // Notify that there are now keys in the keypool void canGetAddressesChanged(); public Q_SLOTS: /* Starts a timer to periodically update the balance */ void startPollBalance(); /* Wallet status might have changed */ void updateStatus(); /** New transaction, or transaction changed status. */ void updateTransaction(); /** New, updated or removed address book entry. */ void updateAddressBook(const QString &address, const QString &label, bool isMine, const QString &purpose, int status); /** Watch-only added. */ void updateWatchOnlyFlag(bool fHaveWatchonly); /** * Current, immature or unconfirmed balance might have changed - emit * 'balanceChanged' if so. */ void pollBalanceChanged(); }; #endif // BITCOIN_QT_WALLETMODEL_H