diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index c0097299bd..31f6133917 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -1,799 +1,799 @@ // 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. #if defined(HAVE_CONFIG_H) #include "config/bitcoin-config.h" #endif #include "bitcoingui.h" #include "chainparams.h" #include "clientmodel.h" #include "config.h" #include "guiconstants.h" #include "guiutil.h" #include "intro.h" #include "networkstyle.h" #include "optionsmodel.h" #include "platformstyle.h" #include "splashscreen.h" #include "utilitydialog.h" #include "winshutdownmonitor.h" #ifdef ENABLE_WALLET #include "paymentserver.h" #include "walletmodel.h" #endif #include "init.h" #include "rpc/server.h" #include "scheduler.h" #include "ui_interface.h" #include "util.h" #include "warnings.h" #ifdef ENABLE_WALLET #include "wallet/wallet.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(QT_STATICPLUGIN) #include #if QT_VERSION < 0x050000 Q_IMPORT_PLUGIN(qcncodecs) Q_IMPORT_PLUGIN(qjpcodecs) Q_IMPORT_PLUGIN(qtwcodecs) Q_IMPORT_PLUGIN(qkrcodecs) Q_IMPORT_PLUGIN(qtaccessiblewidgets) #else #if QT_VERSION < 0x050400 Q_IMPORT_PLUGIN(AccessibleFactory) #endif #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 #endif #if QT_VERSION < 0x050000 #include #endif // Declare meta types used for QMetaObject::invokeMethod Q_DECLARE_METATYPE(bool *) Q_DECLARE_METATYPE(CAmount) Q_DECLARE_METATYPE(Amount) // Config is non-copyable so we can only register pointers to it Q_DECLARE_METATYPE(Config *) static void InitMessage(const std::string &message) { LogPrintf("init message: %s\n", message); } /** * Translate string to current locale using Qt. */ static std::string Translate(const char *psz) { return QCoreApplication::translate("bitcoin-abc", psz).toStdString(); } 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(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 */ #if QT_VERSION < 0x050000 void DebugMessageHandler(QtMsgType type, const char *msg) { const char *category = (type == QtDebugMsg) ? "qt" : nullptr; LogPrint(category, "GUI: %s\n", msg); } #else void DebugMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { Q_UNUSED(context); const char *category = (type == QtDebugMsg) ? "qt" : nullptr; LogPrint(category, "GUI: %s\n", msg.toStdString()); } #endif /** * Class encapsulating Bitcoin ABC startup and shutdown. * Allows running startup and shutdown in a different thread from the UI thread. */ class BitcoinABC : public QObject { Q_OBJECT public: explicit BitcoinABC(); public Q_SLOTS: void initialize(Config *config); void shutdown(); Q_SIGNALS: void initializeResult(int retval); void shutdownResult(int retval); void runawayException(const QString &message); private: boost::thread_group threadGroup; CScheduler scheduler; /// Pass fatal exception message to UI thread void handleRunawayException(const std::exception *e); }; /** Main Bitcoin application object */ class BitcoinApplication : public QApplication { Q_OBJECT public: explicit BitcoinApplication(int &argc, char **argv); ~BitcoinApplication(); #ifdef ENABLE_WALLET /// Create payment server void createPaymentServer(); #endif /// parameter interaction/setup based on rules void parameterSetup(); /// Create options model void createOptionsModel(bool resetSettings); /// Create main window void createWindow(const Config *, const NetworkStyle *networkStyle); /// Create splash screen void createSplashScreen(const NetworkStyle *networkStyle); /// Request core initialization void requestInitialize(Config &config); /// Request core shutdown void requestShutdown(Config &config); /// Get process return value int getReturnValue() { return returnValue; } /// Get window identifier of QMainWindow (BitcoinGUI) WId getMainWinId() const; public Q_SLOTS: void initializeResult(int retval); void shutdownResult(int retval); /// Handle runaway exceptions. Shows a message box with the problem and /// quits the program. void handleRunawayException(const QString &message); Q_SIGNALS: void requestedInitialize(Config *config); void requestedShutdown(); void stopThread(); void splashFinished(QWidget *window); private: QThread *coreThread; OptionsModel *optionsModel; ClientModel *clientModel; BitcoinGUI *window; QTimer *pollShutdownTimer; #ifdef ENABLE_WALLET PaymentServer *paymentServer; WalletModel *walletModel; #endif int returnValue; const PlatformStyle *platformStyle; std::unique_ptr shutdownWindow; void startThread(); }; #include "bitcoin.moc" BitcoinABC::BitcoinABC() : QObject() {} void BitcoinABC::handleRunawayException(const std::exception *e) { PrintExceptionContinue(e, "Runaway exception"); Q_EMIT runawayException(QString::fromStdString(GetWarnings("gui"))); } void BitcoinABC::initialize(Config *cfg) { Config &config(*cfg); try { qDebug() << __func__ << ": Running AppInit2 in thread"; if (!AppInitBasicSetup()) { Q_EMIT initializeResult(false); return; } if (!AppInitParameterInteraction(config)) { Q_EMIT initializeResult(false); return; } if (!AppInitSanityChecks()) { Q_EMIT initializeResult(false); return; } int rv = AppInitMain(config, threadGroup, scheduler); Q_EMIT initializeResult(rv); } catch (const std::exception &e) { handleRunawayException(&e); } catch (...) { handleRunawayException(nullptr); } } void BitcoinABC::shutdown() { try { qDebug() << __func__ << ": Running Shutdown in thread"; Interrupt(threadGroup); threadGroup.join_all(); Shutdown(); qDebug() << __func__ << ": Shutdown finished"; Q_EMIT shutdownResult(1); } catch (const std::exception &e) { handleRunawayException(&e); } catch (...) { handleRunawayException(nullptr); } } BitcoinApplication::BitcoinApplication(int &argc, char **argv) : QApplication(argc, argv), coreThread(0), optionsModel(0), clientModel(0), window(0), pollShutdownTimer(0), #ifdef ENABLE_WALLET paymentServer(0), walletModel(0), #endif returnValue(0) { setQuitOnLastWindowClosed(false); // 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 = 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"; Q_EMIT stopThread(); coreThread->wait(); qDebug() << __func__ << ": Stopped thread"; } delete window; window = 0; #ifdef ENABLE_WALLET delete paymentServer; paymentServer = 0; #endif delete optionsModel; optionsModel = 0; delete platformStyle; platformStyle = 0; } #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(config, platformStyle, networkStyle, 0); pollShutdownTimer = new QTimer(window); connect(pollShutdownTimer, SIGNAL(timeout()), window, SLOT(detectShutdown())); pollShutdownTimer->start(200); } void BitcoinApplication::createSplashScreen(const NetworkStyle *networkStyle) { SplashScreen *splash = new SplashScreen(0, 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 slotFinish // happens. splash->show(); connect(this, SIGNAL(splashFinished(QWidget *)), splash, SLOT(slotFinish(QWidget *))); connect(this, SIGNAL(requestedShutdown()), splash, SLOT(close())); } void BitcoinApplication::startThread() { if (coreThread) return; coreThread = new QThread(this); BitcoinABC *executor = new BitcoinABC(); executor->moveToThread(coreThread); /* communication to and from thread */ connect(executor, SIGNAL(initializeResult(int)), this, SLOT(initializeResult(int))); connect(executor, SIGNAL(shutdownResult(int)), this, SLOT(shutdownResult(int))); connect(executor, SIGNAL(runawayException(QString)), this, SLOT(handleRunawayException(QString))); // 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, SIGNAL(requestedInitialize(Config *)), executor, SLOT(initialize(Config *))); connect(this, SIGNAL(requestedShutdown()), executor, SLOT(shutdown())); /* make sure executor object is deleted in its own thread */ connect(this, SIGNAL(stopThread()), executor, SLOT(deleteLater())); connect(this, SIGNAL(stopThread()), coreThread, SLOT(quit())); coreThread->start(); } void BitcoinApplication::parameterSetup() { InitLogging(); InitParameterInteraction(); } void BitcoinApplication::requestInitialize(Config &config) { 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); } 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(); window->setClientModel(0); pollShutdownTimer->stop(); #ifdef ENABLE_WALLET window->removeAllWallets(); delete walletModel; walletModel = 0; #endif delete clientModel; clientModel = 0; StartShutdown(); // Request shutdown from core thread Q_EMIT requestedShutdown(); } void BitcoinApplication::initializeResult(int retval) { qDebug() << __func__ << ": Initialization result: " << retval; // Set exit result: 0 if successful, 1 if failure returnValue = retval ? 0 : 1; if (retval) { // Log this only after AppInit2 finishes, as then logging setup is // guaranteed complete. qWarning() << "Platform customization:" << platformStyle->getName(); #ifdef ENABLE_WALLET PaymentServer::LoadRootCAs(); paymentServer->setOptionsModel(optionsModel); #endif clientModel = new ClientModel(optionsModel); window->setClientModel(clientModel); #ifdef ENABLE_WALLET if (pwalletMain) { walletModel = new WalletModel(platformStyle, pwalletMain, optionsModel); window->addWallet(BitcoinGUI::DEFAULT_WALLET, walletModel); window->setCurrentWallet(BitcoinGUI::DEFAULT_WALLET); connect(walletModel, SIGNAL(coinsSent(CWallet *, SendCoinsRecipient, QByteArray)), paymentServer, SLOT(fetchPaymentACK(CWallet *, const SendCoinsRecipient &, QByteArray))); } #endif // If -min option passed, start window minimized. if (GetBoolArg("-min", false)) { window->showMinimized(); } else { window->show(); } Q_EMIT splashFinished(window); #ifdef ENABLE_WALLET // Now that initialization/startup is done, process any command-line // bitcoincash: URIs or payment requests: connect(paymentServer, SIGNAL(receivedPaymentRequest(SendCoinsRecipient)), window, SLOT(handlePaymentRequest(SendCoinsRecipient))); connect(window, SIGNAL(receivedURI(QString)), paymentServer, SLOT(handleURIOrFile(QString))); connect(paymentServer, SIGNAL(message(QString, QString, unsigned int)), window, SLOT(message(QString, QString, unsigned int))); QTimer::singleShot(100, paymentServer, SLOT(uiReady())); #endif } else { // Exit main loop. quit(); } } void BitcoinApplication::shutdownResult(int retval) { qDebug() << __func__ << ": Shutdown result: " << retval; // Exit main loop after shutdown finished. quit(); } void BitcoinApplication::handleRunawayException(const QString &message) { QMessageBox::critical( 0, "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(); } #ifndef BITCOIN_QT_TEST 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 main(int argc, char *argv[]) { SetupEnvironment(); /// 1. Parse command-line options. These take precedence over anything else. // Command-line options take precedence: ParseParameters(argc, argv); // Do not refer to data directory yet, this can be overridden by // Intro::pickDataDirectory /// 2. Basic Qt initialization (not dependent on parameters or configuration) #if QT_VERSION < 0x050000 // Internal string conversion is all UTF-8 QTextCodec::setCodecForTr(QTextCodec::codecForName("UTF-8")); QTextCodec::setCodecForCStrings(QTextCodec::codecForTr()); #endif Q_INIT_RESOURCE(bitcoin); Q_INIT_RESOURCE(bitcoin_locale); BitcoinApplication app(argc, argv); #if QT_VERSION > 0x050100 // Generate high-dpi pixmaps QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif #if QT_VERSION >= 0x050600 QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #endif #ifdef Q_OS_MAC QApplication::setAttribute(Qt::AA_DontShowIconsInMenus); #endif #if QT_VERSION >= 0x050500 // Because of the POODLE attack it is recommended to disable SSLv3 // (https://disablessl3.com/), so set SSL protocols to TLS1.0+. QSslConfiguration sslconf = QSslConfiguration::defaultConfiguration(); sslconf.setProtocol(QSsl::TlsV1_0OrLater); QSslConfiguration::setDefaultConfiguration(sslconf); #endif // Register meta types used for QMetaObject::invokeMethod qRegisterMetaType(); - // Need to pass name here as CAmount is a typedef (see + // Need to pass name here as Amount is a typedef (see // http://qt-project.org/doc/qt-5/qmetatype.html#qRegisterMetaType) // IMPORTANT if it is no longer a typedef use the normal variant above - qRegisterMetaType("CAmount"); + qRegisterMetaType("Amount"); // 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(); /// 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(); GUIUtil::SubstituteFonts(GetLangTerritory()); /// 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); translationInterface.Translate.connect(Translate); // Show help message immediately after parsing command-line options (for // "-lang") and setting locale, but before showing splash screen. if (IsArgSet("-?") || IsArgSet("-h") || IsArgSet("-help") || IsArgSet("-version")) { HelpMessageDialog help(nullptr, 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. if (!Intro::pickDataDirectory()) return EXIT_SUCCESS; /// 6. Determine availability of data directory and parse bitcoin.conf /// - Do not call GetDataDir(true) before this step finishes. if (!boost::filesystem::is_directory(GetDataDir(false))) { QMessageBox::critical( 0, QObject::tr(PACKAGE_NAME), QObject::tr( "Error: Specified data directory \"%1\" does not exist.") .arg(QString::fromStdString(GetArg("-datadir", "")))); return EXIT_FAILURE; } try { ReadConfigFile(GetArg("-conf", BITCOIN_CONF_FILENAME)); } catch (const std::exception &e) { QMessageBox::critical( 0, QObject::tr(PACKAGE_NAME), QObject::tr("Error: Cannot parse configuration file: %1. Only use " "key=value syntax.") .arg(e.what())); 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 -testnet or -regtest parameter (Params() calls are only valid // after this clause) try { SelectParams(ChainNameFromCommandLine()); } catch (std::exception &e) { QMessageBox::critical(0, QObject::tr(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 QScopedPointer networkStyle(NetworkStyle::instantiate( QString::fromStdString(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: app.createPaymentServer(); #endif /// 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 QT_VERSION < 0x050000 // Install qDebug() message handler to route to debug.log qInstallMsgHandler(DebugMessageHandler); #else #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); #endif // Allow parameter interaction before we create the options model app.parameterSetup(); // Load GUI settings from QSettings app.createOptionsModel(IsArgSet("-resetguisettings")); // Subscribe to global signals from core uiInterface.InitMessage.connect(InitMessage); // Get global config Config &config = const_cast(GetConfig()); if (GetBoolArg("-splash", DEFAULT_SPLASHSCREEN) && !GetBoolArg("-min", false)) app.createSplashScreen(networkStyle.data()); try { app.createWindow(&config, networkStyle.data()); app.requestInitialize(config); #if defined(Q_OS_WIN) && QT_VERSION >= 0x050000 WinShutdownMonitor::registerShutdownBlockReason( QObject::tr("%1 didn't yet exit safely...") .arg(QObject::tr(PACKAGE_NAME)), (HWND)app.getMainWinId()); #endif app.exec(); app.requestShutdown(config); app.exec(); } catch (const std::exception &e) { PrintExceptionContinue(&e, "Runaway exception"); app.handleRunawayException(QString::fromStdString(GetWarnings("gui"))); } catch (...) { PrintExceptionContinue(nullptr, "Runaway exception"); app.handleRunawayException(QString::fromStdString(GetWarnings("gui"))); } return app.getReturnValue(); } #endif // BITCOIN_QT_TEST diff --git a/src/qt/bitcoinamountfield.cpp b/src/qt/bitcoinamountfield.cpp index f2b8e3a98e..86e901ed4b 100644 --- a/src/qt/bitcoinamountfield.cpp +++ b/src/qt/bitcoinamountfield.cpp @@ -1,293 +1,293 @@ // Copyright (c) 2011-2015 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 "bitcoinamountfield.h" #include "bitcoinunits.h" #include "guiconstants.h" #include "qvaluecombobox.h" #include #include #include #include #include /** * QSpinBox that uses fixed-point numbers internally and uses our own * formatting/parsing functions. */ class AmountSpinBox : public QAbstractSpinBox { Q_OBJECT public: explicit AmountSpinBox(QWidget *parent) : QAbstractSpinBox(parent), currentUnit(BitcoinUnits::BCH), singleStep(100000 /* satoshis */) { setAlignment(Qt::AlignRight); connect(lineEdit(), SIGNAL(textEdited(QString)), this, SIGNAL(valueChanged())); } QValidator::State validate(QString &text, int &pos) const override { if (text.isEmpty()) { return QValidator::Intermediate; } bool valid = false; parse(text, &valid); // Make sure we return Intermediate so that fixup() is called on // defocus. return valid ? QValidator::Intermediate : QValidator::Invalid; } void fixup(QString &input) const override { bool valid = false; Amount val = parse(input, &valid); if (valid) { input = BitcoinUnits::format(currentUnit, val, false, BitcoinUnits::separatorAlways); lineEdit()->setText(input); } } Amount value(bool *valid_out = 0) const { return parse(text(), valid_out); } void setValue(const Amount value) { lineEdit()->setText(BitcoinUnits::format( currentUnit, value, false, BitcoinUnits::separatorAlways)); Q_EMIT valueChanged(); } void stepBy(int steps) override { bool valid = false; Amount val = value(&valid); val = val + steps * singleStep; val = qMin(qMax(val, Amount(0)), BitcoinUnits::maxMoney()); setValue(val); } void setDisplayUnit(int unit) { bool valid = false; Amount val(value(&valid)); currentUnit = unit; if (valid) { - setValue(val.GetSatoshis()); + setValue(val); } else { clear(); } } void setSingleStep(const Amount step) { singleStep = step; } QSize minimumSizeHint() const override { if (cachedMinimumSizeHint.isEmpty()) { ensurePolished(); const QFontMetrics fm(fontMetrics()); int h = lineEdit()->minimumSizeHint().height(); int w = fm.width(BitcoinUnits::format( BitcoinUnits::BCH, BitcoinUnits::maxMoney(), false, BitcoinUnits::separatorAlways)); // Cursor blinking space. w += 2; QStyleOptionSpinBox opt; initStyleOption(&opt); QSize hint(w, h); QSize extra(35, 6); opt.rect.setSize(hint + extra); extra += hint - style() ->subControlRect(QStyle::CC_SpinBox, &opt, QStyle::SC_SpinBoxEditField, this) .size(); // Get closer to final result by repeating the calculation. opt.rect.setSize(hint + extra); extra += hint - style() ->subControlRect(QStyle::CC_SpinBox, &opt, QStyle::SC_SpinBoxEditField, this) .size(); hint += extra; hint.setHeight(h); opt.rect = rect(); cachedMinimumSizeHint = style() ->sizeFromContents(QStyle::CT_SpinBox, &opt, hint, this) .expandedTo(QApplication::globalStrut()); } return cachedMinimumSizeHint; } private: int currentUnit; Amount singleStep; mutable QSize cachedMinimumSizeHint; /** * Parse a string into a number of base monetary units and * return validity. * @note Must return 0 if !valid. */ Amount parse(const QString &text, bool *valid_out = 0) const { Amount val = 0; bool valid = BitcoinUnits::parse(currentUnit, text, &val); if (valid) { if (val < Amount(0) || val > BitcoinUnits::maxMoney()) { valid = false; } } if (valid_out) { *valid_out = valid; } - return valid ? val.GetSatoshis() : 0; + return valid ? val : Amount(0); } protected: bool event(QEvent *event) override { if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) { QKeyEvent *keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Comma) { // Translate a comma into a period. QKeyEvent periodKeyEvent( event->type(), Qt::Key_Period, keyEvent->modifiers(), ".", keyEvent->isAutoRepeat(), keyEvent->count()); return QAbstractSpinBox::event(&periodKeyEvent); } } return QAbstractSpinBox::event(event); } StepEnabled stepEnabled() const override { if (isReadOnly()) { // Disable steps when AmountSpinBox is read-only. return StepNone; } if (text().isEmpty()) { // Allow step-up with empty field. return StepUpEnabled; } StepEnabled rv = 0; bool valid = false; Amount val = value(&valid); if (valid) { if (val > 0) { rv |= StepDownEnabled; } if (val < BitcoinUnits::maxMoney()) { rv |= StepUpEnabled; } } return rv; } Q_SIGNALS: void valueChanged(); }; #include "bitcoinamountfield.moc" BitcoinAmountField::BitcoinAmountField(QWidget *parent) : QWidget(parent), amount(0) { amount = new AmountSpinBox(this); amount->setLocale(QLocale::c()); amount->installEventFilter(this); amount->setMaximumWidth(170); QHBoxLayout *layout = new QHBoxLayout(this); layout->addWidget(amount); unit = new QValueComboBox(this); unit->setModel(new BitcoinUnits(this)); layout->addWidget(unit); layout->addStretch(1); layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); setFocusPolicy(Qt::TabFocus); setFocusProxy(amount); // If one if the widgets changes, the combined content changes as well connect(amount, SIGNAL(valueChanged()), this, SIGNAL(valueChanged())); connect(unit, SIGNAL(currentIndexChanged(int)), this, SLOT(unitChanged(int))); // Set default based on configuration unitChanged(unit->currentIndex()); } void BitcoinAmountField::clear() { amount->clear(); unit->setCurrentIndex(0); } void BitcoinAmountField::setEnabled(bool fEnabled) { amount->setEnabled(fEnabled); unit->setEnabled(fEnabled); } bool BitcoinAmountField::validate() { bool valid = false; value(&valid); setValid(valid); return valid; } void BitcoinAmountField::setValid(bool valid) { if (valid) { amount->setStyleSheet(""); } else { amount->setStyleSheet(STYLE_INVALID); } } bool BitcoinAmountField::eventFilter(QObject *object, QEvent *event) { if (event->type() == QEvent::FocusIn) { // Clear invalid flag on focus setValid(true); } return QWidget::eventFilter(object, event); } QWidget *BitcoinAmountField::setupTabChain(QWidget *prev) { QWidget::setTabOrder(prev, amount); QWidget::setTabOrder(amount, unit); return unit; } Amount BitcoinAmountField::value(bool *valid_out) const { return amount->value(valid_out); } void BitcoinAmountField::setValue(const Amount value) { amount->setValue(value); } void BitcoinAmountField::setReadOnly(bool fReadOnly) { amount->setReadOnly(fReadOnly); } void BitcoinAmountField::unitChanged(int idx) { // Use description tooltip for current unit for the combobox unit->setToolTip(unit->itemData(idx, Qt::ToolTipRole).toString()); // Determine new unit ID int newUnit = unit->itemData(idx, BitcoinUnits::UnitRole).toInt(); amount->setDisplayUnit(newUnit); } void BitcoinAmountField::setDisplayUnit(int newUnit) { unit->setValue(newUnit); } void BitcoinAmountField::setSingleStep(const Amount step) { amount->setSingleStep(step); } diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index e0a0d7fd6b..c6502395d7 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -1,1297 +1,1297 @@ // 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. #if defined(HAVE_CONFIG_H) #include "config/bitcoin-config.h" #endif #include "bitcoingui.h" #include "bitcoinunits.h" #include "clientmodel.h" #include "guiconstants.h" #include "guiutil.h" #include "modaloverlay.h" #include "networkstyle.h" #include "notificator.h" #include "openuridialog.h" #include "optionsdialog.h" #include "optionsmodel.h" #include "platformstyle.h" #include "rpcconsole.h" #include "utilitydialog.h" #ifdef ENABLE_WALLET #include "walletframe.h" #include "walletmodel.h" #endif // ENABLE_WALLET #ifdef Q_OS_MAC #include "macdockiconhandler.h" #endif #include "chainparams.h" #include "init.h" #include "ui_interface.h" #include "util.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if QT_VERSION < 0x050000 #include #include #else #include #endif const std::string BitcoinGUI::DEFAULT_UIPLATFORM = #if defined(Q_OS_MAC) "macosx" #elif defined(Q_OS_WIN) "windows" #else "other" #endif ; /** * Display name for default wallet name. Uses tilde to avoid name collisions in * the future with additional wallets */ const QString BitcoinGUI::DEFAULT_WALLET = "~Default"; BitcoinGUI::BitcoinGUI(const Config *cfg, const PlatformStyle *_platformStyle, const NetworkStyle *networkStyle, QWidget *parent) : QMainWindow(parent), enableWallet(false), clientModel(0), walletFrame(0), unitDisplayControl(0), labelWalletEncryptionIcon(0), labelWalletHDStatusIcon(0), connectionsControl(0), labelBlocksIcon(0), progressBarLabel(0), progressBar(0), progressDialog(0), appMenuBar(0), overviewAction(0), historyAction(0), quitAction(0), sendCoinsAction(0), sendCoinsMenuAction(0), usedSendingAddressesAction(0), usedReceivingAddressesAction(0), signMessageAction(0), verifyMessageAction(0), aboutAction(0), receiveCoinsAction(0), receiveCoinsMenuAction(0), optionsAction(0), toggleHideAction(0), encryptWalletAction(0), backupWalletAction(0), changePassphraseAction(0), aboutQtAction(0), openRPCConsoleAction(0), openAction(0), showHelpMessageAction(0), trayIcon(0), trayIconMenu(0), notificator(0), rpcConsole(0), helpMessageDialog(0), modalOverlay(0), prevBlocks(0), spinnerFrame(0), platformStyle(_platformStyle), cfg(cfg) { GUIUtil::restoreWindowGeometry("nWindow", QSize(850, 550), this); QString windowTitle = tr(PACKAGE_NAME) + " - "; #ifdef ENABLE_WALLET enableWallet = WalletModel::isWalletEnabled(); #endif // ENABLE_WALLET if (enableWallet) { windowTitle += tr("Wallet"); } else { windowTitle += tr("Node"); } windowTitle += " " + networkStyle->getTitleAddText(); #ifndef Q_OS_MAC QApplication::setWindowIcon(networkStyle->getTrayAndWindowIcon()); setWindowIcon(networkStyle->getTrayAndWindowIcon()); #else MacDockIconHandler::instance()->setIcon(networkStyle->getAppIcon()); #endif setWindowTitle(windowTitle); #if defined(Q_OS_MAC) && QT_VERSION < 0x050000 // This property is not implemented in Qt 5. Setting it has no effect. // A replacement API (QtMacUnifiedToolBar) is available in QtMacExtras. setUnifiedTitleAndToolBarOnMac(true); #endif rpcConsole = new RPCConsole(_platformStyle, 0); helpMessageDialog = new HelpMessageDialog(this, false); #ifdef ENABLE_WALLET if (enableWallet) { /** Create wallet frame and make it the central widget */ walletFrame = new WalletFrame(_platformStyle, cfg, this); setCentralWidget(walletFrame); } else #endif // ENABLE_WALLET { /** * When compiled without wallet or -disablewallet is provided, the * central widget is the rpc console. */ setCentralWidget(rpcConsole); } // Accept D&D of URIs setAcceptDrops(true); // Create actions for the toolbar, menu bar and tray/dock icon // Needs walletFrame to be initialized createActions(); // Create application menu bar createMenuBar(); // Create the toolbars createToolBars(); // Create system tray icon and notification createTrayIcon(networkStyle); // Create status bar statusBar(); // Disable size grip because it looks ugly and nobody needs it statusBar()->setSizeGripEnabled(false); // Status bar notification icons QFrame *frameBlocks = new QFrame(); frameBlocks->setContentsMargins(0, 0, 0, 0); frameBlocks->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); QHBoxLayout *frameBlocksLayout = new QHBoxLayout(frameBlocks); frameBlocksLayout->setContentsMargins(3, 0, 3, 0); frameBlocksLayout->setSpacing(3); unitDisplayControl = new UnitDisplayStatusBarControl(platformStyle); labelWalletEncryptionIcon = new QLabel(); labelWalletHDStatusIcon = new QLabel(); connectionsControl = new GUIUtil::ClickableLabel(); labelBlocksIcon = new GUIUtil::ClickableLabel(); if (enableWallet) { frameBlocksLayout->addStretch(); frameBlocksLayout->addWidget(unitDisplayControl); frameBlocksLayout->addStretch(); frameBlocksLayout->addWidget(labelWalletEncryptionIcon); frameBlocksLayout->addWidget(labelWalletHDStatusIcon); } frameBlocksLayout->addStretch(); frameBlocksLayout->addWidget(connectionsControl); frameBlocksLayout->addStretch(); frameBlocksLayout->addWidget(labelBlocksIcon); frameBlocksLayout->addStretch(); // Progress bar and label for blocks download progressBarLabel = new QLabel(); progressBarLabel->setVisible(false); progressBar = new GUIUtil::ProgressBar(); progressBar->setAlignment(Qt::AlignCenter); progressBar->setVisible(false); // Override style sheet for progress bar for styles that have a segmented // progress bar, as they make the text unreadable (workaround for issue // #1071) // See https://qt-project.org/doc/qt-4.8/gallery.html QString curStyle = QApplication::style()->metaObject()->className(); if (curStyle == "QWindowsStyle" || curStyle == "QWindowsXPStyle") { progressBar->setStyleSheet( "QProgressBar { background-color: #e8e8e8; border: 1px solid grey; " "border-radius: 7px; padding: 1px; text-align: center; } " "QProgressBar::chunk { background: QLinearGradient(x1: 0, y1: 0, " "x2: 1, y2: 0, stop: 0 #FF8000, stop: 1 orange); border-radius: " "7px; margin: 0px; }"); } statusBar()->addWidget(progressBarLabel); statusBar()->addWidget(progressBar); statusBar()->addPermanentWidget(frameBlocks); // Install event filter to be able to catch status tip events // (QEvent::StatusTip) this->installEventFilter(this); // Initially wallet actions should be disabled setWalletActionsEnabled(false); // Subscribe to notifications from core subscribeToCoreSignals(); connect(connectionsControl, SIGNAL(clicked(QPoint)), this, SLOT(toggleNetworkActive())); modalOverlay = new ModalOverlay(this->centralWidget()); #ifdef ENABLE_WALLET if (enableWallet) { connect(walletFrame, SIGNAL(requestedSyncWarningInfo()), this, SLOT(showModalOverlay())); connect(labelBlocksIcon, SIGNAL(clicked(QPoint)), this, SLOT(showModalOverlay())); connect(progressBar, SIGNAL(clicked(QPoint)), this, SLOT(showModalOverlay())); } #endif } BitcoinGUI::~BitcoinGUI() { // Unsubscribe from notifications from core unsubscribeFromCoreSignals(); GUIUtil::saveWindowGeometry("nWindow", this); // Hide tray icon, as deleting will let it linger until quit (on Ubuntu) if (trayIcon) { trayIcon->hide(); } #ifdef Q_OS_MAC delete appMenuBar; MacDockIconHandler::cleanup(); #endif delete rpcConsole; } void BitcoinGUI::createActions() { QActionGroup *tabGroup = new QActionGroup(this); overviewAction = new QAction(platformStyle->SingleColorIcon(":/icons/overview"), tr("&Overview"), this); overviewAction->setStatusTip(tr("Show general overview of wallet")); overviewAction->setToolTip(overviewAction->statusTip()); overviewAction->setCheckable(true); overviewAction->setShortcut(QKeySequence(Qt::ALT + Qt::Key_1)); tabGroup->addAction(overviewAction); sendCoinsAction = new QAction( platformStyle->SingleColorIcon(":/icons/send"), tr("&Send"), this); sendCoinsAction->setStatusTip(tr("Send coins to a Bitcoin address")); sendCoinsAction->setToolTip(sendCoinsAction->statusTip()); sendCoinsAction->setCheckable(true); sendCoinsAction->setShortcut(QKeySequence(Qt::ALT + Qt::Key_2)); tabGroup->addAction(sendCoinsAction); sendCoinsMenuAction = new QAction(platformStyle->TextColorIcon(":/icons/send"), sendCoinsAction->text(), this); sendCoinsMenuAction->setStatusTip(sendCoinsAction->statusTip()); sendCoinsMenuAction->setToolTip(sendCoinsMenuAction->statusTip()); receiveCoinsAction = new QAction( platformStyle->SingleColorIcon(":/icons/receiving_addresses"), tr("&Receive"), this); receiveCoinsAction->setStatusTip( tr("Request payments (generates QR codes and %1: URIs)") .arg(GUIUtil::URI_SCHEME)); receiveCoinsAction->setToolTip(receiveCoinsAction->statusTip()); receiveCoinsAction->setCheckable(true); receiveCoinsAction->setShortcut(QKeySequence(Qt::ALT + Qt::Key_3)); tabGroup->addAction(receiveCoinsAction); receiveCoinsMenuAction = new QAction(platformStyle->TextColorIcon(":/icons/receiving_addresses"), receiveCoinsAction->text(), this); receiveCoinsMenuAction->setStatusTip(receiveCoinsAction->statusTip()); receiveCoinsMenuAction->setToolTip(receiveCoinsMenuAction->statusTip()); historyAction = new QAction(platformStyle->SingleColorIcon(":/icons/history"), tr("&Transactions"), this); historyAction->setStatusTip(tr("Browse transaction history")); historyAction->setToolTip(historyAction->statusTip()); historyAction->setCheckable(true); historyAction->setShortcut(QKeySequence(Qt::ALT + Qt::Key_4)); tabGroup->addAction(historyAction); #ifdef ENABLE_WALLET // These showNormalIfMinimized are needed because Send Coins and Receive // Coins can be triggered from the tray menu, and need to show the GUI to be // useful. connect(overviewAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); connect(overviewAction, SIGNAL(triggered()), this, SLOT(gotoOverviewPage())); connect(sendCoinsAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); connect(sendCoinsAction, SIGNAL(triggered()), this, SLOT(gotoSendCoinsPage())); connect(sendCoinsMenuAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); connect(sendCoinsMenuAction, SIGNAL(triggered()), this, SLOT(gotoSendCoinsPage())); connect(receiveCoinsAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); connect(receiveCoinsAction, SIGNAL(triggered()), this, SLOT(gotoReceiveCoinsPage())); connect(receiveCoinsMenuAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); connect(receiveCoinsMenuAction, SIGNAL(triggered()), this, SLOT(gotoReceiveCoinsPage())); connect(historyAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); connect(historyAction, SIGNAL(triggered()), this, SLOT(gotoHistoryPage())); #endif // ENABLE_WALLET quitAction = new QAction(platformStyle->TextColorIcon(":/icons/quit"), tr("E&xit"), this); quitAction->setStatusTip(tr("Quit application")); quitAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q)); quitAction->setMenuRole(QAction::QuitRole); aboutAction = new QAction(platformStyle->TextColorIcon(":/icons/about"), tr("&About %1").arg(tr(PACKAGE_NAME)), this); aboutAction->setStatusTip( tr("Show information about %1").arg(tr(PACKAGE_NAME))); aboutAction->setMenuRole(QAction::AboutRole); aboutAction->setEnabled(false); aboutQtAction = new QAction(platformStyle->TextColorIcon(":/icons/about_qt"), tr("About &Qt"), this); aboutQtAction->setStatusTip(tr("Show information about Qt")); aboutQtAction->setMenuRole(QAction::AboutQtRole); optionsAction = new QAction(platformStyle->TextColorIcon(":/icons/options"), tr("&Options..."), this); optionsAction->setStatusTip( tr("Modify configuration options for %1").arg(tr(PACKAGE_NAME))); optionsAction->setMenuRole(QAction::PreferencesRole); optionsAction->setEnabled(false); toggleHideAction = new QAction(platformStyle->TextColorIcon(":/icons/about"), tr("&Show / Hide"), this); toggleHideAction->setStatusTip(tr("Show or hide the main Window")); encryptWalletAction = new QAction(platformStyle->TextColorIcon(":/icons/lock_closed"), tr("&Encrypt Wallet..."), this); encryptWalletAction->setStatusTip( tr("Encrypt the private keys that belong to your wallet")); encryptWalletAction->setCheckable(true); backupWalletAction = new QAction(platformStyle->TextColorIcon(":/icons/filesave"), tr("&Backup Wallet..."), this); backupWalletAction->setStatusTip(tr("Backup wallet to another location")); changePassphraseAction = new QAction(platformStyle->TextColorIcon(":/icons/key"), tr("&Change Passphrase..."), this); changePassphraseAction->setStatusTip( tr("Change the passphrase used for wallet encryption")); signMessageAction = new QAction(platformStyle->TextColorIcon(":/icons/edit"), tr("Sign &message..."), this); signMessageAction->setStatusTip( tr("Sign messages with your Bitcoin addresses to prove you own them")); verifyMessageAction = new QAction(platformStyle->TextColorIcon(":/icons/verify"), tr("&Verify message..."), this); verifyMessageAction->setStatusTip( tr("Verify messages to ensure they were signed with specified Bitcoin " "addresses")); openRPCConsoleAction = new QAction(platformStyle->TextColorIcon(":/icons/debugwindow"), tr("&Debug window"), this); openRPCConsoleAction->setStatusTip( tr("Open debugging and diagnostic console")); // initially disable the debug window menu item openRPCConsoleAction->setEnabled(false); usedSendingAddressesAction = new QAction(platformStyle->TextColorIcon(":/icons/address-book"), tr("&Sending addresses..."), this); usedSendingAddressesAction->setStatusTip( tr("Show the list of used sending addresses and labels")); usedReceivingAddressesAction = new QAction(platformStyle->TextColorIcon(":/icons/address-book"), tr("&Receiving addresses..."), this); usedReceivingAddressesAction->setStatusTip( tr("Show the list of used receiving addresses and labels")); openAction = new QAction(platformStyle->TextColorIcon(":/icons/open"), tr("Open &URI..."), this); openAction->setStatusTip( tr("Open a %1: URI or payment request").arg(GUIUtil::URI_SCHEME)); showHelpMessageAction = new QAction(platformStyle->TextColorIcon(":/icons/info"), tr("&Command-line options"), this); showHelpMessageAction->setMenuRole(QAction::NoRole); showHelpMessageAction->setStatusTip( tr("Show the %1 help message to get a list with possible Bitcoin " "command-line options") .arg(tr(PACKAGE_NAME))); connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit())); connect(aboutAction, SIGNAL(triggered()), this, SLOT(aboutClicked())); connect(aboutQtAction, SIGNAL(triggered()), qApp, SLOT(aboutQt())); connect(optionsAction, SIGNAL(triggered()), this, SLOT(optionsClicked())); connect(toggleHideAction, SIGNAL(triggered()), this, SLOT(toggleHidden())); connect(showHelpMessageAction, SIGNAL(triggered()), this, SLOT(showHelpMessageClicked())); connect(openRPCConsoleAction, SIGNAL(triggered()), this, SLOT(showDebugWindow())); // prevents an open debug window from becoming stuck/unusable on client // shutdown connect(quitAction, SIGNAL(triggered()), rpcConsole, SLOT(hide())); #ifdef ENABLE_WALLET if (walletFrame) { connect(encryptWalletAction, SIGNAL(triggered(bool)), walletFrame, SLOT(encryptWallet(bool))); connect(backupWalletAction, SIGNAL(triggered()), walletFrame, SLOT(backupWallet())); connect(changePassphraseAction, SIGNAL(triggered()), walletFrame, SLOT(changePassphrase())); connect(signMessageAction, SIGNAL(triggered()), this, SLOT(gotoSignMessageTab())); connect(verifyMessageAction, SIGNAL(triggered()), this, SLOT(gotoVerifyMessageTab())); connect(usedSendingAddressesAction, SIGNAL(triggered()), walletFrame, SLOT(usedSendingAddresses())); connect(usedReceivingAddressesAction, SIGNAL(triggered()), walletFrame, SLOT(usedReceivingAddresses())); connect(openAction, SIGNAL(triggered()), this, SLOT(openClicked())); } #endif // ENABLE_WALLET new QShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_C), this, SLOT(showDebugWindowActivateConsole())); new QShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_D), this, SLOT(showDebugWindow())); } void BitcoinGUI::createMenuBar() { #ifdef Q_OS_MAC // Create a decoupled menu bar on Mac which stays even if the window is // closed appMenuBar = new QMenuBar(); #else // Get the main window's menu bar on other platforms appMenuBar = menuBar(); #endif // Configure the menus QMenu *file = appMenuBar->addMenu(tr("&File")); if (walletFrame) { file->addAction(openAction); file->addAction(backupWalletAction); file->addAction(signMessageAction); file->addAction(verifyMessageAction); file->addSeparator(); file->addAction(usedSendingAddressesAction); file->addAction(usedReceivingAddressesAction); file->addSeparator(); } file->addAction(quitAction); QMenu *settings = appMenuBar->addMenu(tr("&Settings")); if (walletFrame) { settings->addAction(encryptWalletAction); settings->addAction(changePassphraseAction); settings->addSeparator(); } settings->addAction(optionsAction); QMenu *help = appMenuBar->addMenu(tr("&Help")); if (walletFrame) { help->addAction(openRPCConsoleAction); } help->addAction(showHelpMessageAction); help->addSeparator(); help->addAction(aboutAction); help->addAction(aboutQtAction); } void BitcoinGUI::createToolBars() { if (walletFrame) { QToolBar *toolbar = addToolBar(tr("Tabs toolbar")); toolbar->setMovable(false); toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); toolbar->addAction(overviewAction); toolbar->addAction(sendCoinsAction); toolbar->addAction(receiveCoinsAction); toolbar->addAction(historyAction); overviewAction->setChecked(true); } } void BitcoinGUI::setClientModel(ClientModel *_clientModel) { this->clientModel = _clientModel; if (_clientModel) { // Create system tray menu (or setup the dock menu) that late to prevent // users from calling actions, while the client has not yet fully loaded createTrayIconMenu(); // Keep up to date with client updateNetworkState(); connect(_clientModel, SIGNAL(numConnectionsChanged(int)), this, SLOT(setNumConnections(int))); connect(_clientModel, SIGNAL(networkActiveChanged(bool)), this, SLOT(setNetworkActive(bool))); setNumBlocks(_clientModel->getNumBlocks(), _clientModel->getLastBlockDate(), _clientModel->getVerificationProgress(nullptr), false); connect(_clientModel, SIGNAL(numBlocksChanged(int, QDateTime, double, bool)), this, SLOT(setNumBlocks(int, QDateTime, double, bool))); // Receive and report messages from client model connect(_clientModel, SIGNAL(message(QString, QString, unsigned int)), this, SLOT(message(QString, QString, unsigned int))); // Show progress dialog connect(_clientModel, SIGNAL(showProgress(QString, int)), this, SLOT(showProgress(QString, int))); rpcConsole->setClientModel(_clientModel); #ifdef ENABLE_WALLET if (walletFrame) { walletFrame->setClientModel(_clientModel); } #endif // ENABLE_WALLET unitDisplayControl->setOptionsModel(_clientModel->getOptionsModel()); OptionsModel *optionsModel = _clientModel->getOptionsModel(); if (optionsModel) { // be aware of the tray icon disable state change reported by the // OptionsModel object. connect(optionsModel, SIGNAL(hideTrayIconChanged(bool)), this, SLOT(setTrayIconVisible(bool))); // initialize the disable state of the tray icon with the current // value in the model. setTrayIconVisible(optionsModel->getHideTrayIcon()); } modalOverlay->setKnownBestHeight( clientModel->getHeaderTipHeight(), QDateTime::fromTime_t(clientModel->getHeaderTipTime())); } else { // Disable possibility to show main window via action toggleHideAction->setEnabled(false); if (trayIconMenu) { // Disable context menu on tray icon trayIconMenu->clear(); } // Propagate cleared model to child objects rpcConsole->setClientModel(nullptr); #ifdef ENABLE_WALLET if (walletFrame) { walletFrame->setClientModel(nullptr); } #endif // ENABLE_WALLET unitDisplayControl->setOptionsModel(nullptr); } } #ifdef ENABLE_WALLET bool BitcoinGUI::addWallet(const QString &name, WalletModel *walletModel) { if (!walletFrame) return false; setWalletActionsEnabled(true); return walletFrame->addWallet(name, walletModel); } bool BitcoinGUI::setCurrentWallet(const QString &name) { if (!walletFrame) return false; return walletFrame->setCurrentWallet(name); } void BitcoinGUI::removeAllWallets() { if (!walletFrame) return; setWalletActionsEnabled(false); walletFrame->removeAllWallets(); } #endif // ENABLE_WALLET void BitcoinGUI::setWalletActionsEnabled(bool enabled) { overviewAction->setEnabled(enabled); sendCoinsAction->setEnabled(enabled); sendCoinsMenuAction->setEnabled(enabled); receiveCoinsAction->setEnabled(enabled); receiveCoinsMenuAction->setEnabled(enabled); historyAction->setEnabled(enabled); encryptWalletAction->setEnabled(enabled); backupWalletAction->setEnabled(enabled); changePassphraseAction->setEnabled(enabled); signMessageAction->setEnabled(enabled); verifyMessageAction->setEnabled(enabled); usedSendingAddressesAction->setEnabled(enabled); usedReceivingAddressesAction->setEnabled(enabled); openAction->setEnabled(enabled); } void BitcoinGUI::createTrayIcon(const NetworkStyle *networkStyle) { #ifndef Q_OS_MAC trayIcon = new QSystemTrayIcon(this); QString toolTip = tr("%1 client").arg(tr(PACKAGE_NAME)) + " " + networkStyle->getTitleAddText(); trayIcon->setToolTip(toolTip); trayIcon->setIcon(networkStyle->getTrayAndWindowIcon()); trayIcon->hide(); #endif notificator = new Notificator(QApplication::applicationName(), trayIcon, this); } void BitcoinGUI::createTrayIconMenu() { #ifndef Q_OS_MAC // return if trayIcon is unset (only on non-Mac OSes) if (!trayIcon) return; trayIconMenu = new QMenu(this); trayIcon->setContextMenu(trayIconMenu); connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(trayIconActivated(QSystemTrayIcon::ActivationReason))); #else // Note: On Mac, the dock icon is used to provide the tray's functionality. MacDockIconHandler *dockIconHandler = MacDockIconHandler::instance(); dockIconHandler->setMainWindow((QMainWindow *)this); trayIconMenu = dockIconHandler->dockMenu(); #endif // Configuration of the tray icon (or dock icon) icon menu trayIconMenu->addAction(toggleHideAction); trayIconMenu->addSeparator(); trayIconMenu->addAction(sendCoinsMenuAction); trayIconMenu->addAction(receiveCoinsMenuAction); trayIconMenu->addSeparator(); trayIconMenu->addAction(signMessageAction); trayIconMenu->addAction(verifyMessageAction); trayIconMenu->addSeparator(); trayIconMenu->addAction(optionsAction); trayIconMenu->addAction(openRPCConsoleAction); #ifndef Q_OS_MAC // This is built-in on Mac trayIconMenu->addSeparator(); trayIconMenu->addAction(quitAction); #endif } #ifndef Q_OS_MAC void BitcoinGUI::trayIconActivated(QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Trigger) { // Click on system tray icon triggers show/hide of the main window toggleHidden(); } } #endif void BitcoinGUI::optionsClicked() { if (!clientModel || !clientModel->getOptionsModel()) return; OptionsDialog dlg(this, enableWallet); dlg.setModel(clientModel->getOptionsModel()); dlg.exec(); } void BitcoinGUI::aboutClicked() { if (!clientModel) return; HelpMessageDialog dlg(this, true); dlg.exec(); } void BitcoinGUI::showDebugWindow() { rpcConsole->showNormal(); rpcConsole->show(); rpcConsole->raise(); rpcConsole->activateWindow(); } void BitcoinGUI::showDebugWindowActivateConsole() { rpcConsole->setTabFocus(RPCConsole::TAB_CONSOLE); showDebugWindow(); } void BitcoinGUI::showHelpMessageClicked() { helpMessageDialog->show(); } #ifdef ENABLE_WALLET void BitcoinGUI::openClicked() { OpenURIDialog dlg(cfg, this); if (dlg.exec()) { Q_EMIT receivedURI(dlg.getURI()); } } void BitcoinGUI::gotoOverviewPage() { overviewAction->setChecked(true); if (walletFrame) walletFrame->gotoOverviewPage(); } void BitcoinGUI::gotoHistoryPage() { historyAction->setChecked(true); if (walletFrame) walletFrame->gotoHistoryPage(); } void BitcoinGUI::gotoReceiveCoinsPage() { receiveCoinsAction->setChecked(true); if (walletFrame) walletFrame->gotoReceiveCoinsPage(); } void BitcoinGUI::gotoSendCoinsPage(QString addr) { sendCoinsAction->setChecked(true); if (walletFrame) walletFrame->gotoSendCoinsPage(addr); } void BitcoinGUI::gotoSignMessageTab(QString addr) { if (walletFrame) walletFrame->gotoSignMessageTab(addr); } void BitcoinGUI::gotoVerifyMessageTab(QString addr) { if (walletFrame) walletFrame->gotoVerifyMessageTab(addr); } #endif // ENABLE_WALLET void BitcoinGUI::updateNetworkState() { int count = clientModel->getNumConnections(); QString icon; switch (count) { case 0: icon = ":/icons/connect_0"; break; case 1: case 2: case 3: icon = ":/icons/connect_1"; break; case 4: case 5: case 6: icon = ":/icons/connect_2"; break; case 7: case 8: case 9: icon = ":/icons/connect_3"; break; default: icon = ":/icons/connect_4"; break; } QString tooltip; if (clientModel->getNetworkActive()) { tooltip = tr("%n active connection(s) to Bitcoin network", "", count) + QString(".
") + tr("Click to disable network activity."); } else { tooltip = tr("Network activity disabled.") + QString("
") + tr("Click to enable network activity again."); icon = ":/icons/network_disabled"; } // Don't word-wrap this (fixed-width) tooltip tooltip = QString("") + tooltip + QString(""); connectionsControl->setToolTip(tooltip); connectionsControl->setPixmap(platformStyle->SingleColorIcon(icon).pixmap( STATUSBAR_ICONSIZE, STATUSBAR_ICONSIZE)); } void BitcoinGUI::setNumConnections(int count) { updateNetworkState(); } void BitcoinGUI::setNetworkActive(bool networkActive) { updateNetworkState(); } void BitcoinGUI::updateHeadersSyncProgressLabel() { int64_t headersTipTime = clientModel->getHeaderTipTime(); int headersTipHeight = clientModel->getHeaderTipHeight(); int estHeadersLeft = (GetTime() - headersTipTime) / Params().GetConsensus().nPowTargetSpacing; if (estHeadersLeft > HEADER_HEIGHT_DELTA_SYNC) progressBarLabel->setText( tr("Syncing Headers (%1%)...") .arg(QString::number(100.0 / (headersTipHeight + estHeadersLeft) * headersTipHeight, 'f', 1))); } void BitcoinGUI::setNumBlocks(int count, const QDateTime &blockDate, double nVerificationProgress, bool header) { if (modalOverlay) { if (header) modalOverlay->setKnownBestHeight(count, blockDate); else modalOverlay->tipUpdate(count, blockDate, nVerificationProgress); } if (!clientModel) return; // Prevent orphan statusbar messages (e.g. hover Quit in main menu, wait // until chain-sync starts -> garbled text) statusBar()->clearMessage(); // Acquire current block source enum BlockSource blockSource = clientModel->getBlockSource(); switch (blockSource) { case BLOCK_SOURCE_NETWORK: if (header) { updateHeadersSyncProgressLabel(); return; } progressBarLabel->setText(tr("Synchronizing with network...")); updateHeadersSyncProgressLabel(); break; case BLOCK_SOURCE_DISK: if (header) { progressBarLabel->setText(tr("Indexing blocks on disk...")); } else { progressBarLabel->setText(tr("Processing blocks on disk...")); } break; case BLOCK_SOURCE_REINDEX: progressBarLabel->setText(tr("Reindexing blocks on disk...")); break; case BLOCK_SOURCE_NONE: if (header) { return; } progressBarLabel->setText(tr("Connecting to peers...")); break; } QString tooltip; QDateTime currentDate = QDateTime::currentDateTime(); qint64 secs = blockDate.secsTo(currentDate); tooltip = tr("Processed %n block(s) of transaction history.", "", count); // Set icon state: spinning if catching up, tick otherwise if (secs < 90 * 60) { tooltip = tr("Up to date") + QString(".
") + tooltip; labelBlocksIcon->setPixmap( platformStyle->SingleColorIcon(":/icons/synced") .pixmap(STATUSBAR_ICONSIZE, STATUSBAR_ICONSIZE)); #ifdef ENABLE_WALLET if (walletFrame) { walletFrame->showOutOfSyncWarning(false); modalOverlay->showHide(true, true); } #endif // ENABLE_WALLET progressBarLabel->setVisible(false); progressBar->setVisible(false); } else { QString timeBehindText = GUIUtil::formatNiceTimeOffset(secs); progressBarLabel->setVisible(true); progressBar->setFormat(tr("%1 behind").arg(timeBehindText)); progressBar->setMaximum(1000000000); progressBar->setValue(nVerificationProgress * 1000000000.0 + 0.5); progressBar->setVisible(true); tooltip = tr("Catching up...") + QString("
") + tooltip; if (count != prevBlocks) { labelBlocksIcon->setPixmap( platformStyle ->SingleColorIcon(QString(":/movies/spinner-%1") .arg(spinnerFrame, 3, 10, QChar('0'))) .pixmap(STATUSBAR_ICONSIZE, STATUSBAR_ICONSIZE)); spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES; } prevBlocks = count; #ifdef ENABLE_WALLET if (walletFrame) { walletFrame->showOutOfSyncWarning(true); modalOverlay->showHide(); } #endif // ENABLE_WALLET tooltip += QString("
"); tooltip += tr("Last received block was generated %1 ago.").arg(timeBehindText); tooltip += QString("
"); tooltip += tr("Transactions after this will not yet be visible."); } // Don't word-wrap this (fixed-width) tooltip tooltip = QString("") + tooltip + QString(""); labelBlocksIcon->setToolTip(tooltip); progressBarLabel->setToolTip(tooltip); progressBar->setToolTip(tooltip); } void BitcoinGUI::message(const QString &title, const QString &message, unsigned int style, bool *ret) { // default title QString strTitle = tr("Bitcoin"); // Default to information icon int nMBoxIcon = QMessageBox::Information; int nNotifyIcon = Notificator::Information; QString msgType; // Prefer supplied title over style based title if (!title.isEmpty()) { msgType = title; } else { switch (style) { case CClientUIInterface::MSG_ERROR: msgType = tr("Error"); break; case CClientUIInterface::MSG_WARNING: msgType = tr("Warning"); break; case CClientUIInterface::MSG_INFORMATION: msgType = tr("Information"); break; default: break; } } // Append title to "Bitcoin - " if (!msgType.isEmpty()) strTitle += " - " + msgType; // Check for error/warning icon if (style & CClientUIInterface::ICON_ERROR) { nMBoxIcon = QMessageBox::Critical; nNotifyIcon = Notificator::Critical; } else if (style & CClientUIInterface::ICON_WARNING) { nMBoxIcon = QMessageBox::Warning; nNotifyIcon = Notificator::Warning; } // Display message if (style & CClientUIInterface::MODAL) { // Check for buttons, use OK as default, if none was supplied QMessageBox::StandardButton buttons; if (!(buttons = (QMessageBox::StandardButton)( style & CClientUIInterface::BTN_MASK))) buttons = QMessageBox::Ok; showNormalIfMinimized(); QMessageBox mBox((QMessageBox::Icon)nMBoxIcon, strTitle, message, buttons, this); int r = mBox.exec(); if (ret != nullptr) *ret = r == QMessageBox::Ok; } else notificator->notify((Notificator::Class)nNotifyIcon, strTitle, message); } void BitcoinGUI::changeEvent(QEvent *e) { QMainWindow::changeEvent(e); #ifndef Q_OS_MAC // Ignored on Mac if (e->type() == QEvent::WindowStateChange) { if (clientModel && clientModel->getOptionsModel() && clientModel->getOptionsModel()->getMinimizeToTray()) { QWindowStateChangeEvent *wsevt = static_cast(e); if (!(wsevt->oldState() & Qt::WindowMinimized) && isMinimized()) { QTimer::singleShot(0, this, SLOT(hide())); e->ignore(); } } } #endif } void BitcoinGUI::closeEvent(QCloseEvent *event) { #ifndef Q_OS_MAC // Ignored on Mac if (clientModel && clientModel->getOptionsModel()) { if (!clientModel->getOptionsModel()->getMinimizeOnClose()) { // close rpcConsole in case it was open to make some space for the // shutdown window rpcConsole->close(); QApplication::quit(); } else { QMainWindow::showMinimized(); event->ignore(); } } #else QMainWindow::closeEvent(event); #endif } void BitcoinGUI::showEvent(QShowEvent *event) { // enable the debug window when the main window shows up openRPCConsoleAction->setEnabled(true); aboutAction->setEnabled(true); optionsAction->setEnabled(true); } #ifdef ENABLE_WALLET void BitcoinGUI::incomingTransaction(const QString &date, int unit, - const CAmount &amount, const QString &type, + const Amount amount, const QString &type, const QString &address, const QString &label) { // On new transaction, make an info balloon QString msg = tr("Date: %1\n").arg(date) + tr("Amount: %1\n") .arg(BitcoinUnits::formatWithUnit(unit, amount, true)) + tr("Type: %1\n").arg(type); if (!label.isEmpty()) msg += tr("Label: %1\n").arg(label); else if (!address.isEmpty()) msg += tr("Address: %1\n").arg(address); message((amount) < 0 ? tr("Sent transaction") : tr("Incoming transaction"), msg, CClientUIInterface::MSG_INFORMATION); } #endif // ENABLE_WALLET void BitcoinGUI::dragEnterEvent(QDragEnterEvent *event) { // Accept only URIs if (event->mimeData()->hasUrls()) event->acceptProposedAction(); } void BitcoinGUI::dropEvent(QDropEvent *event) { if (event->mimeData()->hasUrls()) { for (const QUrl &uri : event->mimeData()->urls()) { Q_EMIT receivedURI(uri.toString()); } } event->acceptProposedAction(); } bool BitcoinGUI::eventFilter(QObject *object, QEvent *event) { // Catch status tip events if (event->type() == QEvent::StatusTip) { // Prevent adding text from setStatusTip(), if we currently use the // status bar for displaying other stuff if (progressBarLabel->isVisible() || progressBar->isVisible()) return true; } return QMainWindow::eventFilter(object, event); } #ifdef ENABLE_WALLET bool BitcoinGUI::handlePaymentRequest(const SendCoinsRecipient &recipient) { // URI has to be valid if (walletFrame && walletFrame->handlePaymentRequest(recipient)) { showNormalIfMinimized(); gotoSendCoinsPage(); return true; } return false; } void BitcoinGUI::setHDStatus(int hdEnabled) { labelWalletHDStatusIcon->setPixmap( platformStyle ->SingleColorIcon(hdEnabled ? ":/icons/hd_enabled" : ":/icons/hd_disabled") .pixmap(STATUSBAR_ICONSIZE, STATUSBAR_ICONSIZE)); labelWalletHDStatusIcon->setToolTip( hdEnabled ? tr("HD key generation is enabled") : tr("HD key generation is disabled")); // eventually disable the QLabel to set its opacity to 50% labelWalletHDStatusIcon->setEnabled(hdEnabled); } void BitcoinGUI::setEncryptionStatus(int status) { switch (status) { case WalletModel::Unencrypted: labelWalletEncryptionIcon->hide(); encryptWalletAction->setChecked(false); changePassphraseAction->setEnabled(false); encryptWalletAction->setEnabled(true); break; case WalletModel::Unlocked: labelWalletEncryptionIcon->show(); labelWalletEncryptionIcon->setPixmap( platformStyle->SingleColorIcon(":/icons/lock_open") .pixmap(STATUSBAR_ICONSIZE, STATUSBAR_ICONSIZE)); labelWalletEncryptionIcon->setToolTip( tr("Wallet is encrypted and currently unlocked")); encryptWalletAction->setChecked(true); changePassphraseAction->setEnabled(true); encryptWalletAction->setEnabled( false); // TODO: decrypt currently not supported break; case WalletModel::Locked: labelWalletEncryptionIcon->show(); labelWalletEncryptionIcon->setPixmap( platformStyle->SingleColorIcon(":/icons/lock_closed") .pixmap(STATUSBAR_ICONSIZE, STATUSBAR_ICONSIZE)); labelWalletEncryptionIcon->setToolTip( tr("Wallet is encrypted and currently locked")); encryptWalletAction->setChecked(true); changePassphraseAction->setEnabled(true); encryptWalletAction->setEnabled( false); // TODO: decrypt currently not supported break; } } #endif // ENABLE_WALLET void BitcoinGUI::showNormalIfMinimized(bool fToggleHidden) { if (!clientModel) return; // activateWindow() (sometimes) helps with keyboard focus on Windows if (isHidden()) { show(); activateWindow(); } else if (isMinimized()) { showNormal(); activateWindow(); } else if (GUIUtil::isObscured(this)) { raise(); activateWindow(); } else if (fToggleHidden) hide(); } void BitcoinGUI::toggleHidden() { showNormalIfMinimized(true); } void BitcoinGUI::detectShutdown() { if (ShutdownRequested()) { if (rpcConsole) rpcConsole->hide(); qApp->quit(); } } void BitcoinGUI::showProgress(const QString &title, int nProgress) { if (nProgress == 0) { progressDialog = new QProgressDialog(title, "", 0, 100); progressDialog->setWindowModality(Qt::ApplicationModal); progressDialog->setMinimumDuration(0); progressDialog->setCancelButton(0); progressDialog->setAutoClose(false); progressDialog->setValue(0); } else if (nProgress == 100) { if (progressDialog) { progressDialog->close(); progressDialog->deleteLater(); } } else if (progressDialog) progressDialog->setValue(nProgress); } void BitcoinGUI::setTrayIconVisible(bool fHideTrayIcon) { if (trayIcon) { trayIcon->setVisible(!fHideTrayIcon); } } void BitcoinGUI::showModalOverlay() { if (modalOverlay && (progressBar->isVisible() || modalOverlay->isLayerVisible())) modalOverlay->toggleVisibility(); } static bool ThreadSafeMessageBox(BitcoinGUI *gui, const std::string &message, const std::string &caption, unsigned int style) { bool modal = (style & CClientUIInterface::MODAL); // The SECURE flag has no effect in the Qt GUI. // bool secure = (style & CClientUIInterface::SECURE); style &= ~CClientUIInterface::SECURE; bool ret = false; // In case of modal message, use blocking connection to wait for user to // click a button QMetaObject::invokeMethod(gui, "message", modal ? GUIUtil::blockingGUIThreadConnection() : Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(caption)), Q_ARG(QString, QString::fromStdString(message)), Q_ARG(unsigned int, style), Q_ARG(bool *, &ret)); return ret; } void BitcoinGUI::subscribeToCoreSignals() { // Connect signals to client uiInterface.ThreadSafeMessageBox.connect( boost::bind(ThreadSafeMessageBox, this, _1, _2, _3)); uiInterface.ThreadSafeQuestion.connect( boost::bind(ThreadSafeMessageBox, this, _1, _3, _4)); } void BitcoinGUI::unsubscribeFromCoreSignals() { // Disconnect signals from client uiInterface.ThreadSafeMessageBox.disconnect( boost::bind(ThreadSafeMessageBox, this, _1, _2, _3)); uiInterface.ThreadSafeQuestion.disconnect( boost::bind(ThreadSafeMessageBox, this, _1, _3, _4)); } void BitcoinGUI::toggleNetworkActive() { if (clientModel) { clientModel->setNetworkActive(!clientModel->getNetworkActive()); } } UnitDisplayStatusBarControl::UnitDisplayStatusBarControl( const PlatformStyle *platformStyle) : optionsModel(0), menu(0) { createContextMenu(); setToolTip(tr("Unit to show amounts in. Click to select another unit.")); QList units = BitcoinUnits::availableUnits(); int max_width = 0; const QFontMetrics fm(font()); for (const BitcoinUnits::Unit unit : units) { max_width = qMax(max_width, fm.width(BitcoinUnits::name(unit))); } setMinimumSize(max_width, 0); setAlignment(Qt::AlignRight | Qt::AlignVCenter); setStyleSheet(QString("QLabel { color : %1 }") .arg(platformStyle->SingleColor().name())); } /** So that it responds to button clicks */ void UnitDisplayStatusBarControl::mousePressEvent(QMouseEvent *event) { onDisplayUnitsClicked(event->pos()); } /** Creates context menu, its actions, and wires up all the relevant signals for * mouse events. */ void UnitDisplayStatusBarControl::createContextMenu() { menu = new QMenu(this); for (BitcoinUnits::Unit u : BitcoinUnits::availableUnits()) { QAction *menuAction = new QAction(QString(BitcoinUnits::name(u)), this); menuAction->setData(QVariant(u)); menu->addAction(menuAction); } connect(menu, SIGNAL(triggered(QAction *)), this, SLOT(onMenuSelection(QAction *))); } /** Lets the control know about the Options Model (and its signals) */ void UnitDisplayStatusBarControl::setOptionsModel(OptionsModel *_optionsModel) { if (_optionsModel) { this->optionsModel = _optionsModel; // be aware of a display unit change reported by the OptionsModel // object. connect(_optionsModel, SIGNAL(displayUnitChanged(int)), this, SLOT(updateDisplayUnit(int))); // initialize the display units label with the current value in the // model. updateDisplayUnit(_optionsModel->getDisplayUnit()); } } /** When Display Units are changed on OptionsModel it will refresh the display * text of the control on the status bar */ void UnitDisplayStatusBarControl::updateDisplayUnit(int newUnits) { setText(BitcoinUnits::name(newUnits)); } /** Shows context menu with Display Unit options by the mouse coordinates */ void UnitDisplayStatusBarControl::onDisplayUnitsClicked(const QPoint &point) { QPoint globalPos = mapToGlobal(point); menu->exec(globalPos); } /** Tells underlying optionsModel to update its current display unit. */ void UnitDisplayStatusBarControl::onMenuSelection(QAction *action) { if (action) { optionsModel->setDisplayUnit(action->data()); } } diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index 539439fb2b..3f14e79c04 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -1,293 +1,293 @@ // 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_BITCOINGUI_H #define BITCOIN_QT_BITCOINGUI_H #if defined(HAVE_CONFIG_H) #include "config/bitcoin-config.h" #endif #include "amount.h" #include #include #include #include #include #include class ClientModel; class NetworkStyle; class Notificator; class OptionsModel; class PlatformStyle; class RPCConsole; class SendCoinsRecipient; class UnitDisplayStatusBarControl; class WalletFrame; class WalletModel; class HelpMessageDialog; class ModalOverlay; class Config; class CWallet; QT_BEGIN_NAMESPACE class QAction; class QProgressBar; class QProgressDialog; QT_END_NAMESPACE /** * Bitcoin GUI main class. This class represents the main window of the Bitcoin * UI. It communicates with both the client and wallet models to give the user * an up-to-date view of the current core state. */ class BitcoinGUI : public QMainWindow { Q_OBJECT public: static const QString DEFAULT_WALLET; static const std::string DEFAULT_UIPLATFORM; explicit BitcoinGUI(const Config *, const PlatformStyle *platformStyle, const NetworkStyle *networkStyle, QWidget *parent = 0); ~BitcoinGUI(); /** * Set the client model. * The client model represents the part of the core that communicates with * the P2P network, and is wallet-agnostic. */ void setClientModel(ClientModel *clientModel); #ifdef ENABLE_WALLET /** * Set the wallet model. * The wallet model represents a bitcoin wallet, and offers access to the * list of transactions, address book and sending functionality. */ bool addWallet(const QString &name, WalletModel *walletModel); bool setCurrentWallet(const QString &name); void removeAllWallets(); #endif // ENABLE_WALLET bool enableWallet; protected: void changeEvent(QEvent *e) override; void closeEvent(QCloseEvent *event) override; void showEvent(QShowEvent *event) override; void dragEnterEvent(QDragEnterEvent *event) override; void dropEvent(QDropEvent *event) override; bool eventFilter(QObject *object, QEvent *event) override; private: ClientModel *clientModel; WalletFrame *walletFrame; UnitDisplayStatusBarControl *unitDisplayControl; QLabel *labelWalletEncryptionIcon; QLabel *labelWalletHDStatusIcon; QLabel *connectionsControl; QLabel *labelBlocksIcon; QLabel *progressBarLabel; QProgressBar *progressBar; QProgressDialog *progressDialog; QMenuBar *appMenuBar; QAction *overviewAction; QAction *historyAction; QAction *quitAction; QAction *sendCoinsAction; QAction *sendCoinsMenuAction; QAction *usedSendingAddressesAction; QAction *usedReceivingAddressesAction; QAction *signMessageAction; QAction *verifyMessageAction; QAction *aboutAction; QAction *receiveCoinsAction; QAction *receiveCoinsMenuAction; QAction *optionsAction; QAction *toggleHideAction; QAction *encryptWalletAction; QAction *backupWalletAction; QAction *changePassphraseAction; QAction *aboutQtAction; QAction *openRPCConsoleAction; QAction *openAction; QAction *showHelpMessageAction; QSystemTrayIcon *trayIcon; QMenu *trayIconMenu; Notificator *notificator; RPCConsole *rpcConsole; HelpMessageDialog *helpMessageDialog; ModalOverlay *modalOverlay; /** Keep track of previous number of blocks, to detect progress */ int prevBlocks; int spinnerFrame; const PlatformStyle *platformStyle; const Config *cfg; /** Create the main UI actions. */ void createActions(); /** Create the menu bar and sub-menus. */ void createMenuBar(); /** Create the toolbars */ void createToolBars(); /** Create system tray icon and notification */ void createTrayIcon(const NetworkStyle *networkStyle); /** Create system tray menu (or setup the dock menu) */ void createTrayIconMenu(); /** Enable or disable all wallet-related actions */ void setWalletActionsEnabled(bool enabled); /** Connect core signals to GUI client */ void subscribeToCoreSignals(); /** Disconnect core signals from GUI client */ void unsubscribeFromCoreSignals(); /** Update UI with latest network info from model. */ void updateNetworkState(); void updateHeadersSyncProgressLabel(); Q_SIGNALS: /** Signal raised when a URI was entered or dragged to the GUI */ void receivedURI(const QString &uri); public Q_SLOTS: /** Set number of connections shown in the UI */ void setNumConnections(int count); /** Set network state shown in the UI */ void setNetworkActive(bool networkActive); /** Set number of blocks and last block date shown in the UI */ void setNumBlocks(int count, const QDateTime &blockDate, double nVerificationProgress, bool headers); /** Notify the user of an event from the core network or transaction handling code. @param[in] title the message box / notification title @param[in] message the displayed text @param[in] style modality and style definitions (icon and used buttons - buttons only for message boxes) @see CClientUIInterface::MessageBoxFlags @param[in] ret pointer to a bool that will be modified to whether Ok was clicked (modal only) */ void message(const QString &title, const QString &message, unsigned int style, bool *ret = nullptr); #ifdef ENABLE_WALLET /** Set the encryption status as shown in the UI. @param[in] status current encryption status @see WalletModel::EncryptionStatus */ void setEncryptionStatus(int status); /** Set the hd-enabled status as shown in the UI. @param[in] status current hd enabled status @see WalletModel::EncryptionStatus */ void setHDStatus(int hdEnabled); bool handlePaymentRequest(const SendCoinsRecipient &recipient); /** Show incoming transaction notification for new transactions. */ - void incomingTransaction(const QString &date, int unit, - const CAmount &amount, const QString &type, - const QString &address, const QString &label); + void incomingTransaction(const QString &date, int unit, const Amount amount, + const QString &type, const QString &address, + const QString &label); #endif // ENABLE_WALLET private Q_SLOTS: #ifdef ENABLE_WALLET /** Switch to overview (home) page */ void gotoOverviewPage(); /** Switch to history (transactions) page */ void gotoHistoryPage(); /** Switch to receive coins page */ void gotoReceiveCoinsPage(); /** Switch to send coins page */ void gotoSendCoinsPage(QString addr = ""); /** Show Sign/Verify Message dialog and switch to sign message tab */ void gotoSignMessageTab(QString addr = ""); /** Show Sign/Verify Message dialog and switch to verify message tab */ void gotoVerifyMessageTab(QString addr = ""); /** Show open dialog */ void openClicked(); #endif // ENABLE_WALLET /** Show configuration dialog */ void optionsClicked(); /** Show about dialog */ void aboutClicked(); /** Show debug window */ void showDebugWindow(); /** Show debug window and set focus to the console */ void showDebugWindowActivateConsole(); /** Show help message dialog */ void showHelpMessageClicked(); #ifndef Q_OS_MAC /** Handle tray icon clicked */ void trayIconActivated(QSystemTrayIcon::ActivationReason reason); #endif /** Show window if hidden, unminimize when minimized, rise when obscured or * show if hidden and fToggleHidden is true */ void showNormalIfMinimized(bool fToggleHidden = false); /** Simply calls showNormalIfMinimized(true) for use in SLOT() macro */ void toggleHidden(); /** called by a timer to check if fRequestShutdown has been set **/ void detectShutdown(); /** Show progress dialog e.g. for verifychain */ void showProgress(const QString &title, int nProgress); /** When hideTrayIcon setting is changed in OptionsModel hide or show the * icon accordingly. */ void setTrayIconVisible(bool); /** Toggle networking */ void toggleNetworkActive(); void showModalOverlay(); }; class UnitDisplayStatusBarControl : public QLabel { Q_OBJECT public: explicit UnitDisplayStatusBarControl(const PlatformStyle *platformStyle); /** Lets the control know about the Options Model (and its signals) */ void setOptionsModel(OptionsModel *optionsModel); protected: /** So that it responds to left-button clicks */ void mousePressEvent(QMouseEvent *event) override; private: OptionsModel *optionsModel; QMenu *menu; /** Shows context menu with Display Unit options by the mouse coordinates */ void onDisplayUnitsClicked(const QPoint &point); /** Creates context menu, its actions, and wires up all the relevant signals * for mouse events. */ void createContextMenu(); private Q_SLOTS: /** When Display Units are changed on OptionsModel it will refresh the * display text of the control on the status bar */ void updateDisplayUnit(int newUnits); /** Tells underlying optionsModel to update its current display unit. */ void onMenuSelection(QAction *action); }; #endif // BITCOIN_QT_BITCOINGUI_H diff --git a/src/qt/coincontroldialog.cpp b/src/qt/coincontroldialog.cpp index a5c435f9b8..c5567e58f2 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -1,849 +1,842 @@ // 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 "coincontroldialog.h" #include "ui_coincontroldialog.h" #include "addresstablemodel.h" #include "bitcoinunits.h" #include "guiutil.h" #include "optionsmodel.h" #include "platformstyle.h" #include "txmempool.h" #include "walletmodel.h" #include "dstencode.h" #include "init.h" #include "policy/policy.h" #include "validation.h" // For mempool #include "wallet/coincontrol.h" #include "wallet/wallet.h" #include #include #include #include #include #include #include #include #include #include -QList CoinControlDialog::payAmounts; +QList CoinControlDialog::payAmounts; CCoinControl *CoinControlDialog::coinControl = new CCoinControl(); bool CoinControlDialog::fSubtractFeeFromAmount = false; bool CCoinControlWidgetItem::operator<(const QTreeWidgetItem &other) const { int column = treeWidget()->sortColumn(); if (column == CoinControlDialog::COLUMN_AMOUNT || column == CoinControlDialog::COLUMN_DATE || column == CoinControlDialog::COLUMN_CONFIRMATIONS) return data(column, Qt::UserRole).toLongLong() < other.data(column, Qt::UserRole).toLongLong(); return QTreeWidgetItem::operator<(other); } CoinControlDialog::CoinControlDialog(const PlatformStyle *_platformStyle, QWidget *parent) : QDialog(parent), ui(new Ui::CoinControlDialog), model(0), platformStyle(_platformStyle) { ui->setupUi(this); // context menu actions QAction *copyAddressAction = new QAction(tr("Copy address"), this); QAction *copyLabelAction = new QAction(tr("Copy label"), this); QAction *copyAmountAction = new QAction(tr("Copy amount"), this); // we need to enable/disable this copyTransactionHashAction = new QAction(tr("Copy transaction ID"), this); // we need to enable/disable this lockAction = new QAction(tr("Lock unspent"), this); // we need to enable/disable this unlockAction = new QAction(tr("Unlock unspent"), this); // context menu contextMenu = new QMenu(this); contextMenu->addAction(copyAddressAction); contextMenu->addAction(copyLabelAction); contextMenu->addAction(copyAmountAction); contextMenu->addAction(copyTransactionHashAction); contextMenu->addSeparator(); contextMenu->addAction(lockAction); contextMenu->addAction(unlockAction); // context menu signals connect(ui->treeWidget, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showMenu(QPoint))); connect(copyAddressAction, SIGNAL(triggered()), this, SLOT(copyAddress())); connect(copyLabelAction, SIGNAL(triggered()), this, SLOT(copyLabel())); connect(copyAmountAction, SIGNAL(triggered()), this, SLOT(copyAmount())); connect(copyTransactionHashAction, SIGNAL(triggered()), this, SLOT(copyTransactionHash())); connect(lockAction, SIGNAL(triggered()), this, SLOT(lockCoin())); connect(unlockAction, SIGNAL(triggered()), this, SLOT(unlockCoin())); // clipboard actions QAction *clipboardQuantityAction = new QAction(tr("Copy quantity"), this); QAction *clipboardAmountAction = new QAction(tr("Copy amount"), this); QAction *clipboardFeeAction = new QAction(tr("Copy fee"), this); QAction *clipboardAfterFeeAction = new QAction(tr("Copy after fee"), this); QAction *clipboardBytesAction = new QAction(tr("Copy bytes"), this); QAction *clipboardLowOutputAction = new QAction(tr("Copy dust"), this); QAction *clipboardChangeAction = new QAction(tr("Copy change"), this); connect(clipboardQuantityAction, SIGNAL(triggered()), this, SLOT(clipboardQuantity())); connect(clipboardAmountAction, SIGNAL(triggered()), this, SLOT(clipboardAmount())); connect(clipboardFeeAction, SIGNAL(triggered()), this, SLOT(clipboardFee())); connect(clipboardAfterFeeAction, SIGNAL(triggered()), this, SLOT(clipboardAfterFee())); connect(clipboardBytesAction, SIGNAL(triggered()), this, SLOT(clipboardBytes())); connect(clipboardLowOutputAction, SIGNAL(triggered()), this, SLOT(clipboardLowOutput())); connect(clipboardChangeAction, SIGNAL(triggered()), this, SLOT(clipboardChange())); ui->labelCoinControlQuantity->addAction(clipboardQuantityAction); ui->labelCoinControlAmount->addAction(clipboardAmountAction); ui->labelCoinControlFee->addAction(clipboardFeeAction); ui->labelCoinControlAfterFee->addAction(clipboardAfterFeeAction); ui->labelCoinControlBytes->addAction(clipboardBytesAction); ui->labelCoinControlLowOutput->addAction(clipboardLowOutputAction); ui->labelCoinControlChange->addAction(clipboardChangeAction); // toggle tree/list mode connect(ui->radioTreeMode, SIGNAL(toggled(bool)), this, SLOT(radioTreeMode(bool))); connect(ui->radioListMode, SIGNAL(toggled(bool)), this, SLOT(radioListMode(bool))); // click on checkbox connect(ui->treeWidget, SIGNAL(itemChanged(QTreeWidgetItem *, int)), this, SLOT(viewItemChanged(QTreeWidgetItem *, int))); // click on header #if QT_VERSION < 0x050000 ui->treeWidget->header()->setClickable(true); #else ui->treeWidget->header()->setSectionsClickable(true); #endif connect(ui->treeWidget->header(), SIGNAL(sectionClicked(int)), this, SLOT(headerSectionClicked(int))); // ok button connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonBoxClicked(QAbstractButton *))); // (un)select all connect(ui->pushButtonSelectAll, SIGNAL(clicked()), this, SLOT(buttonSelectAllClicked())); // change coin control first column label due Qt4 bug. // see https://github.com/bitcoin/bitcoin/issues/5716 ui->treeWidget->headerItem()->setText(COLUMN_CHECKBOX, QString()); ui->treeWidget->setColumnWidth(COLUMN_CHECKBOX, 84); ui->treeWidget->setColumnWidth(COLUMN_AMOUNT, 110); ui->treeWidget->setColumnWidth(COLUMN_LABEL, 190); ui->treeWidget->setColumnWidth(COLUMN_ADDRESS, 320); ui->treeWidget->setColumnWidth(COLUMN_DATE, 130); ui->treeWidget->setColumnWidth(COLUMN_CONFIRMATIONS, 110); // store transaction hash in this column, but don't show it ui->treeWidget->setColumnHidden(COLUMN_TXHASH, true); // store vout index in this column, but don't show it ui->treeWidget->setColumnHidden(COLUMN_VOUT_INDEX, true); // default view is sorted by amount desc sortView(COLUMN_AMOUNT, Qt::DescendingOrder); // restore list mode and sortorder as a convenience feature QSettings settings; if (settings.contains("nCoinControlMode") && !settings.value("nCoinControlMode").toBool()) ui->radioTreeMode->click(); if (settings.contains("nCoinControlSortColumn") && settings.contains("nCoinControlSortOrder")) sortView( settings.value("nCoinControlSortColumn").toInt(), ((Qt::SortOrder)settings.value("nCoinControlSortOrder").toInt())); } CoinControlDialog::~CoinControlDialog() { QSettings settings; settings.setValue("nCoinControlMode", ui->radioListMode->isChecked()); settings.setValue("nCoinControlSortColumn", sortColumn); settings.setValue("nCoinControlSortOrder", (int)sortOrder); delete ui; } void CoinControlDialog::setModel(WalletModel *_model) { this->model = _model; if (_model && _model->getOptionsModel() && _model->getAddressTableModel()) { updateView(); updateLabelLocked(); CoinControlDialog::updateLabels(_model, this); } } // ok button void CoinControlDialog::buttonBoxClicked(QAbstractButton *button) { if (ui->buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { // closes the dialog done(QDialog::Accepted); } } // (un)select all void CoinControlDialog::buttonSelectAllClicked() { Qt::CheckState state = Qt::Checked; for (int i = 0; i < ui->treeWidget->topLevelItemCount(); i++) { if (ui->treeWidget->topLevelItem(i)->checkState(COLUMN_CHECKBOX) != Qt::Unchecked) { state = Qt::Unchecked; break; } } ui->treeWidget->setEnabled(false); for (int i = 0; i < ui->treeWidget->topLevelItemCount(); i++) if (ui->treeWidget->topLevelItem(i)->checkState(COLUMN_CHECKBOX) != state) ui->treeWidget->topLevelItem(i)->setCheckState(COLUMN_CHECKBOX, state); ui->treeWidget->setEnabled(true); if (state == Qt::Unchecked) { // just to be sure coinControl->UnSelectAll(); } CoinControlDialog::updateLabels(model, this); } // context menu void CoinControlDialog::showMenu(const QPoint &point) { QTreeWidgetItem *item = ui->treeWidget->itemAt(point); if (item) { contextMenuItem = item; // disable some items (like Copy Transaction ID, lock, unlock) for tree // roots in context menu if (item->text(COLUMN_TXHASH).length() == 64) { // transaction hash is 64 characters (this means its a child node, // so its not a parent node in tree mode) copyTransactionHashAction->setEnabled(true); if (model->isLockedCoin( uint256S(item->text(COLUMN_TXHASH).toStdString()), item->text(COLUMN_VOUT_INDEX).toUInt())) { lockAction->setEnabled(false); unlockAction->setEnabled(true); } else { lockAction->setEnabled(true); unlockAction->setEnabled(false); } } else { // this means click on parent node in tree mode -> disable all copyTransactionHashAction->setEnabled(false); lockAction->setEnabled(false); unlockAction->setEnabled(false); } // show context menu contextMenu->exec(QCursor::pos()); } } // context menu action: copy amount void CoinControlDialog::copyAmount() { GUIUtil::setClipboard( BitcoinUnits::removeSpaces(contextMenuItem->text(COLUMN_AMOUNT))); } // context menu action: copy label void CoinControlDialog::copyLabel() { if (ui->radioTreeMode->isChecked() && contextMenuItem->text(COLUMN_LABEL).length() == 0 && contextMenuItem->parent()) GUIUtil::setClipboard(contextMenuItem->parent()->text(COLUMN_LABEL)); else GUIUtil::setClipboard(contextMenuItem->text(COLUMN_LABEL)); } // context menu action: copy address void CoinControlDialog::copyAddress() { if (ui->radioTreeMode->isChecked() && contextMenuItem->text(COLUMN_ADDRESS).length() == 0 && contextMenuItem->parent()) GUIUtil::setClipboard(contextMenuItem->parent()->text(COLUMN_ADDRESS)); else GUIUtil::setClipboard(contextMenuItem->text(COLUMN_ADDRESS)); } // context menu action: copy transaction id void CoinControlDialog::copyTransactionHash() { GUIUtil::setClipboard(contextMenuItem->text(COLUMN_TXHASH)); } // context menu action: lock coin void CoinControlDialog::lockCoin() { if (contextMenuItem->checkState(COLUMN_CHECKBOX) == Qt::Checked) contextMenuItem->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); COutPoint outpt( uint256S(contextMenuItem->text(COLUMN_TXHASH).toStdString()), contextMenuItem->text(COLUMN_VOUT_INDEX).toUInt()); model->lockCoin(outpt); contextMenuItem->setDisabled(true); contextMenuItem->setIcon( COLUMN_CHECKBOX, platformStyle->SingleColorIcon(":/icons/lock_closed")); updateLabelLocked(); } // context menu action: unlock coin void CoinControlDialog::unlockCoin() { COutPoint outpt( uint256S(contextMenuItem->text(COLUMN_TXHASH).toStdString()), contextMenuItem->text(COLUMN_VOUT_INDEX).toUInt()); model->unlockCoin(outpt); contextMenuItem->setDisabled(false); contextMenuItem->setIcon(COLUMN_CHECKBOX, QIcon()); updateLabelLocked(); } // copy label "Quantity" to clipboard void CoinControlDialog::clipboardQuantity() { GUIUtil::setClipboard(ui->labelCoinControlQuantity->text()); } // copy label "Amount" to clipboard void CoinControlDialog::clipboardAmount() { GUIUtil::setClipboard(ui->labelCoinControlAmount->text().left( ui->labelCoinControlAmount->text().indexOf(" "))); } // copy label "Fee" to clipboard void CoinControlDialog::clipboardFee() { GUIUtil::setClipboard( ui->labelCoinControlFee->text() .left(ui->labelCoinControlFee->text().indexOf(" ")) .replace(ASYMP_UTF8, "")); } // copy label "After fee" to clipboard void CoinControlDialog::clipboardAfterFee() { GUIUtil::setClipboard( ui->labelCoinControlAfterFee->text() .left(ui->labelCoinControlAfterFee->text().indexOf(" ")) .replace(ASYMP_UTF8, "")); } // copy label "Bytes" to clipboard void CoinControlDialog::clipboardBytes() { GUIUtil::setClipboard( ui->labelCoinControlBytes->text().replace(ASYMP_UTF8, "")); } // copy label "Dust" to clipboard void CoinControlDialog::clipboardLowOutput() { GUIUtil::setClipboard(ui->labelCoinControlLowOutput->text()); } // copy label "Change" to clipboard void CoinControlDialog::clipboardChange() { GUIUtil::setClipboard( ui->labelCoinControlChange->text() .left(ui->labelCoinControlChange->text().indexOf(" ")) .replace(ASYMP_UTF8, "")); } // treeview: sort void CoinControlDialog::sortView(int column, Qt::SortOrder order) { sortColumn = column; sortOrder = order; ui->treeWidget->sortItems(column, order); ui->treeWidget->header()->setSortIndicator(sortColumn, sortOrder); } // treeview: clicked on header void CoinControlDialog::headerSectionClicked(int logicalIndex) { // click on most left column -> do nothing if (logicalIndex == COLUMN_CHECKBOX) { ui->treeWidget->header()->setSortIndicator(sortColumn, sortOrder); } else { if (sortColumn == logicalIndex) sortOrder = ((sortOrder == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder); else { sortColumn = logicalIndex; // if label or address then default => asc, else default => desc sortOrder = ((sortColumn == COLUMN_LABEL || sortColumn == COLUMN_ADDRESS) ? Qt::AscendingOrder : Qt::DescendingOrder); } sortView(sortColumn, sortOrder); } } // toggle tree mode void CoinControlDialog::radioTreeMode(bool checked) { if (checked && model) updateView(); } // toggle list mode void CoinControlDialog::radioListMode(bool checked) { if (checked && model) updateView(); } // checkbox clicked by user void CoinControlDialog::viewItemChanged(QTreeWidgetItem *item, int column) { // transaction hash is 64 characters (this means its a child node, so its // not a parent node in tree mode) if (column == COLUMN_CHECKBOX && item->text(COLUMN_TXHASH).length() == 64) { COutPoint outpt(uint256S(item->text(COLUMN_TXHASH).toStdString()), item->text(COLUMN_VOUT_INDEX).toUInt()); if (item->checkState(COLUMN_CHECKBOX) == Qt::Unchecked) { coinControl->UnSelect(outpt); } else if (item->isDisabled()) { // locked (this happens if "check all" through parent node) item->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); } else { coinControl->Select(outpt); } // selection changed -> update labels if (ui->treeWidget->isEnabled()) { // do not update on every click for (un)select all CoinControlDialog::updateLabels(model, this); } } // TODO: Remove this temporary qt5 fix after Qt5.3 and Qt5.4 are no longer used. // Fixed in Qt5.5 and above: https://bugreports.qt.io/browse/QTBUG-43473 #if QT_VERSION >= 0x050000 else if (column == COLUMN_CHECKBOX && item->childCount() > 0) { if (item->checkState(COLUMN_CHECKBOX) == Qt::PartiallyChecked && item->child(0)->checkState(COLUMN_CHECKBOX) == Qt::PartiallyChecked) item->setCheckState(COLUMN_CHECKBOX, Qt::Checked); } #endif } // shows count of locked unspent outputs void CoinControlDialog::updateLabelLocked() { std::vector vOutpts; model->listLockedCoins(vOutpts); if (vOutpts.size() > 0) { ui->labelLocked->setText(tr("(%1 locked)").arg(vOutpts.size())); ui->labelLocked->setVisible(true); } else ui->labelLocked->setVisible(false); } void CoinControlDialog::updateLabels(WalletModel *model, QDialog *dialog) { if (!model) return; // nPayAmount - CAmount nPayAmount = 0; + Amount nPayAmount = 0; bool fDust = false; CMutableTransaction txDummy; - for (const CAmount &amount : CoinControlDialog::payAmounts) { + for (const Amount amount : CoinControlDialog::payAmounts) { nPayAmount += amount; if (amount > 0) { CTxOut txout(Amount(amount), (CScript)std::vector(24, 0)); txDummy.vout.push_back(txout); if (txout.IsDust(dustRelayFee)) fDust = true; } } Amount nAmount(0); Amount nPayFee(0); Amount nAfterFee(0); Amount nChange(0); unsigned int nBytes = 0; unsigned int nBytesInputs = 0; double dPriority = 0; double dPriorityInputs = 0; unsigned int nQuantity = 0; int nQuantityUncompressed = 0; bool fAllowFree = false; std::vector vCoinControl; std::vector vOutputs; coinControl->ListSelected(vCoinControl); model->getOutputs(vCoinControl, vOutputs); for (const COutput &out : vOutputs) { // unselect already spent, very unlikely scenario, this could happen // when selected are spent elsewhere, like rpc or another computer uint256 txhash = out.tx->GetId(); COutPoint outpt(txhash, out.i); if (model->isSpent(outpt)) { coinControl->UnSelect(outpt); continue; } // Quantity nQuantity++; // Amount - nAmount += out.tx->tx->vout[out.i].nValue.GetSatoshis(); + nAmount += out.tx->tx->vout[out.i].nValue; // Priority dPriorityInputs += (double)out.tx->tx->vout[out.i].nValue.GetSatoshis() * (out.nDepth + 1); // Bytes CTxDestination address; if (ExtractDestination(out.tx->tx->vout[out.i].scriptPubKey, address)) { CPubKey pubkey; CKeyID *keyid = boost::get(&address); if (keyid && model->getPubKey(*keyid, pubkey)) { nBytesInputs += (pubkey.IsCompressed() ? 148 : 180); if (!pubkey.IsCompressed()) nQuantityUncompressed++; } else { // in all error cases, simply assume 148 here nBytesInputs += 148; } } else nBytesInputs += 148; } // calculation if (nQuantity > 0) { // Bytes // always assume +1 output for change here nBytes = nBytesInputs + ((CoinControlDialog::payAmounts.size() > 0 ? CoinControlDialog::payAmounts.size() + 1 : 2) * 34) + 10; // in the subtract fee from amount case, we can tell if zero change // already and subtract the bytes, so that fee calculation afterwards is // accurate if (CoinControlDialog::fSubtractFeeFromAmount) if (nAmount - nPayAmount == 0) nBytes -= 34; // Fee nPayFee = CWallet::GetMinimumFee(nBytes, nTxConfirmTarget, mempool); if (nPayFee > 0 && coinControl->nMinimumTotalFee > nPayFee) - nPayFee = coinControl->nMinimumTotalFee.GetSatoshis(); + nPayFee = coinControl->nMinimumTotalFee; // Allow free? (require at least hard-coded threshold and default to // that if no estimate) double mempoolEstimatePriority = mempool.estimateSmartPriority(nTxConfirmTarget); // 29 = 180 - 151 (uncompressed public keys are over the limit. max 151 // bytes of the input are ignored for priority) dPriority = dPriorityInputs / (nBytes - nBytesInputs + (nQuantityUncompressed * 29)); double dPriorityNeeded = std::max(mempoolEstimatePriority, AllowFreeThreshold()); fAllowFree = (dPriority >= dPriorityNeeded); if (fSendFreeTransactions) if (fAllowFree && nBytes <= MAX_FREE_TRANSACTION_CREATE_SIZE) nPayFee = 0; if (nPayAmount > 0) { nChange = nAmount - nPayAmount; if (!CoinControlDialog::fSubtractFeeFromAmount) nChange -= nPayFee; // Never create dust outputs; if we would, just add the dust to the // fee. - if (nChange > 0 && nChange < MIN_CHANGE.GetSatoshis()) { + if (nChange > Amount(0) && nChange < MIN_CHANGE) { CTxOut txout(nChange, (CScript)std::vector(24, 0)); if (txout.IsDust(dustRelayFee)) { // dust-change will be raised until no dust if (CoinControlDialog::fSubtractFeeFromAmount) { - nChange = - txout.GetDustThreshold(dustRelayFee).GetSatoshis(); + nChange = txout.GetDustThreshold(dustRelayFee); } else { nPayFee += nChange; nChange = 0; } } } if (nChange == 0 && !CoinControlDialog::fSubtractFeeFromAmount) { nBytes -= 34; } } // after fee nAfterFee = std::max(nAmount - nPayFee, Amount(0)); } // actually update labels int nDisplayUnit = BitcoinUnits::BCH; if (model && model->getOptionsModel()) { nDisplayUnit = model->getOptionsModel()->getDisplayUnit(); } QLabel *l1 = dialog->findChild("labelCoinControlQuantity"); QLabel *l2 = dialog->findChild("labelCoinControlAmount"); QLabel *l3 = dialog->findChild("labelCoinControlFee"); QLabel *l4 = dialog->findChild("labelCoinControlAfterFee"); QLabel *l5 = dialog->findChild("labelCoinControlBytes"); QLabel *l7 = dialog->findChild("labelCoinControlLowOutput"); QLabel *l8 = dialog->findChild("labelCoinControlChange"); // enable/disable "dust" and "change" dialog->findChild("labelCoinControlLowOutputText") ->setEnabled(nPayAmount > 0); dialog->findChild("labelCoinControlLowOutput") ->setEnabled(nPayAmount > 0); dialog->findChild("labelCoinControlChangeText") ->setEnabled(nPayAmount > 0); dialog->findChild("labelCoinControlChange") ->setEnabled(nPayAmount > 0); // stats // Quantity l1->setText(QString::number(nQuantity)); // Amount - l2->setText( - BitcoinUnits::formatWithUnit(nDisplayUnit, nAmount.GetSatoshis())); + l2->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nAmount)); // Fee - l3->setText( - BitcoinUnits::formatWithUnit(nDisplayUnit, nPayFee.GetSatoshis())); + l3->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nPayFee)); // After Fee - l4->setText( - BitcoinUnits::formatWithUnit(nDisplayUnit, nAfterFee.GetSatoshis())); + l4->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nAfterFee)); // Bytes l5->setText(((nBytes > 0) ? ASYMP_UTF8 : "") + QString::number(nBytes)); // Dust l7->setText(fDust ? tr("yes") : tr("no")); // Change - l8->setText( - BitcoinUnits::formatWithUnit(nDisplayUnit, nChange.GetSatoshis())); + l8->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nChange)); if (nPayFee > Amount(0) && (coinControl->nMinimumTotalFee < nPayFee)) { l3->setText(ASYMP_UTF8 + l3->text()); l4->setText(ASYMP_UTF8 + l4->text()); if (nChange > 0 && !CoinControlDialog::fSubtractFeeFromAmount) { l8->setText(ASYMP_UTF8 + l8->text()); } } // turn label red when dust l7->setStyleSheet((fDust) ? "color:red;" : ""); // tool tips QString toolTipDust = tr("This label turns red if any recipient receives an amount smaller " "than the current dust threshold."); // how many satoshis the estimated fee can vary per byte we guess wrong double dFeeVary; if (payTxFee.GetFeePerK() > Amount(0)) { dFeeVary = (double)std::max(CWallet::GetRequiredFee(1000), payTxFee.GetFeePerK()) .GetSatoshis() / 1000; } else { dFeeVary = (double)std::max( CWallet::GetRequiredFee(1000), mempool.estimateSmartFee(nTxConfirmTarget).GetFeePerK()) .GetSatoshis() / 1000; } QString toolTip4 = tr("Can vary +/- %1 satoshi(s) per input.").arg(dFeeVary); l3->setToolTip(toolTip4); l4->setToolTip(toolTip4); l7->setToolTip(toolTipDust); l8->setToolTip(toolTip4); dialog->findChild("labelCoinControlFeeText") ->setToolTip(l3->toolTip()); dialog->findChild("labelCoinControlAfterFeeText") ->setToolTip(l4->toolTip()); dialog->findChild("labelCoinControlBytesText") ->setToolTip(l5->toolTip()); dialog->findChild("labelCoinControlLowOutputText") ->setToolTip(l7->toolTip()); dialog->findChild("labelCoinControlChangeText") ->setToolTip(l8->toolTip()); // Insufficient funds QLabel *label = dialog->findChild("labelCoinControlInsuffFunds"); if (label) { label->setVisible(nChange < 0); } } void CoinControlDialog::updateView() { if (!model || !model->getOptionsModel() || !model->getAddressTableModel()) { return; } bool treeMode = ui->radioTreeMode->isChecked(); ui->treeWidget->clear(); // performance, otherwise updateLabels would be called for every checked // checkbox ui->treeWidget->setEnabled(false); ui->treeWidget->setAlternatingRowColors(!treeMode); QFlags flgCheckbox = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; QFlags flgTristate = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsTristate; int nDisplayUnit = model->getOptionsModel()->getDisplayUnit(); std::map> mapCoins; model->listCoins(mapCoins); for (const std::pair> &coins : mapCoins) { CCoinControlWidgetItem *itemWalletAddress = new CCoinControlWidgetItem(); itemWalletAddress->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); QString sWalletAddress = coins.first; QString sWalletLabel = model->getAddressTableModel()->labelForAddress(sWalletAddress); if (sWalletLabel.isEmpty()) { sWalletLabel = tr("(no label)"); } if (treeMode) { // wallet address ui->treeWidget->addTopLevelItem(itemWalletAddress); itemWalletAddress->setFlags(flgTristate); itemWalletAddress->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); // label itemWalletAddress->setText(COLUMN_LABEL, sWalletLabel); // address itemWalletAddress->setText(COLUMN_ADDRESS, sWalletAddress); } Amount nSum(0); int nChildren = 0; for (const COutput &out : coins.second) { nSum += out.tx->tx->vout[out.i].nValue; nChildren++; CCoinControlWidgetItem *itemOutput; if (treeMode) { itemOutput = new CCoinControlWidgetItem(itemWalletAddress); } else { itemOutput = new CCoinControlWidgetItem(ui->treeWidget); } itemOutput->setFlags(flgCheckbox); itemOutput->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); // address CTxDestination outputAddress; QString sAddress = ""; if (ExtractDestination(out.tx->tx->vout[out.i].scriptPubKey, outputAddress)) { sAddress = QString::fromStdString(EncodeDestination(outputAddress)); // if listMode or change => show bitcoin address. In tree mode, // address is not shown again for direct wallet address outputs if (!treeMode || (!(sAddress == sWalletAddress))) itemOutput->setText(COLUMN_ADDRESS, sAddress); } // label if (!(sAddress == sWalletAddress)) { // change tooltip from where the change comes from itemOutput->setToolTip(COLUMN_LABEL, tr("change from %1 (%2)") .arg(sWalletLabel) .arg(sWalletAddress)); itemOutput->setText(COLUMN_LABEL, tr("(change)")); } else if (!treeMode) { QString sLabel = model->getAddressTableModel()->labelForAddress(sAddress); if (sLabel.isEmpty()) { sLabel = tr("(no label)"); } itemOutput->setText(COLUMN_LABEL, sLabel); } // amount itemOutput->setText( COLUMN_AMOUNT, - BitcoinUnits::format( - nDisplayUnit, - out.tx->tx->vout[out.i].nValue.GetSatoshis())); + BitcoinUnits::format(nDisplayUnit, + out.tx->tx->vout[out.i].nValue)); // padding so that sorting works correctly itemOutput->setData( COLUMN_AMOUNT, Qt::UserRole, QVariant( (qlonglong)out.tx->tx->vout[out.i].nValue.GetSatoshis())); // date itemOutput->setText(COLUMN_DATE, GUIUtil::dateTimeStr(out.tx->GetTxTime())); itemOutput->setData(COLUMN_DATE, Qt::UserRole, QVariant((qlonglong)out.tx->GetTxTime())); // confirmations itemOutput->setText(COLUMN_CONFIRMATIONS, QString::number(out.nDepth)); itemOutput->setData(COLUMN_CONFIRMATIONS, Qt::UserRole, QVariant((qlonglong)out.nDepth)); // transaction hash uint256 txhash = out.tx->GetId(); itemOutput->setText(COLUMN_TXHASH, QString::fromStdString(txhash.GetHex())); // vout index itemOutput->setText(COLUMN_VOUT_INDEX, QString::number(out.i)); // disable locked coins if (model->isLockedCoin(txhash, out.i)) { COutPoint outpt(txhash, out.i); // just to be sure coinControl->UnSelect(outpt); itemOutput->setDisabled(true); itemOutput->setIcon( COLUMN_CHECKBOX, platformStyle->SingleColorIcon(":/icons/lock_closed")); } // set checkbox if (coinControl->IsSelected(COutPoint(txhash, out.i))) { itemOutput->setCheckState(COLUMN_CHECKBOX, Qt::Checked); } } // amount if (treeMode) { itemWalletAddress->setText(COLUMN_CHECKBOX, "(" + QString::number(nChildren) + ")"); itemWalletAddress->setText( - COLUMN_AMOUNT, - BitcoinUnits::format(nDisplayUnit, nSum.GetSatoshis())); + COLUMN_AMOUNT, BitcoinUnits::format(nDisplayUnit, nSum)); itemWalletAddress->setData(COLUMN_AMOUNT, Qt::UserRole, QVariant((qlonglong)nSum.GetSatoshis())); } } // expand all partially selected if (treeMode) { for (int i = 0; i < ui->treeWidget->topLevelItemCount(); i++) if (ui->treeWidget->topLevelItem(i)->checkState(COLUMN_CHECKBOX) == Qt::PartiallyChecked) ui->treeWidget->topLevelItem(i)->setExpanded(true); } // sort view sortView(sortColumn, sortOrder); ui->treeWidget->setEnabled(true); } diff --git a/src/qt/coincontroldialog.h b/src/qt/coincontroldialog.h index d64e8e52ff..d87b97af48 100644 --- a/src/qt/coincontroldialog.h +++ b/src/qt/coincontroldialog.h @@ -1,112 +1,112 @@ // 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_COINCONTROLDIALOG_H #define BITCOIN_QT_COINCONTROLDIALOG_H #include "amount.h" #include #include #include #include #include #include #include #include class PlatformStyle; class WalletModel; class CCoinControl; class CTxMemPool; namespace Ui { class CoinControlDialog; } #define ASYMP_UTF8 "\xE2\x89\x88" class CCoinControlWidgetItem : public QTreeWidgetItem { public: CCoinControlWidgetItem(QTreeWidget *parent, int type = Type) : QTreeWidgetItem(parent, type) {} CCoinControlWidgetItem(int type = Type) : QTreeWidgetItem(type) {} CCoinControlWidgetItem(QTreeWidgetItem *parent, int type = Type) : QTreeWidgetItem(parent, type) {} bool operator<(const QTreeWidgetItem &other) const override; }; class CoinControlDialog : public QDialog { Q_OBJECT public: explicit CoinControlDialog(const PlatformStyle *platformStyle, QWidget *parent = 0); ~CoinControlDialog(); void setModel(WalletModel *model); // static because also called from sendcoinsdialog static void updateLabels(WalletModel *, QDialog *); - static QList payAmounts; + static QList payAmounts; static CCoinControl *coinControl; static bool fSubtractFeeFromAmount; private: Ui::CoinControlDialog *ui; WalletModel *model; int sortColumn; Qt::SortOrder sortOrder; QMenu *contextMenu; QTreeWidgetItem *contextMenuItem; QAction *copyTransactionHashAction; QAction *lockAction; QAction *unlockAction; const PlatformStyle *platformStyle; void sortView(int, Qt::SortOrder); void updateView(); enum { COLUMN_CHECKBOX = 0, COLUMN_AMOUNT, COLUMN_LABEL, COLUMN_ADDRESS, COLUMN_DATE, COLUMN_CONFIRMATIONS, COLUMN_TXHASH, COLUMN_VOUT_INDEX, }; friend class CCoinControlWidgetItem; private Q_SLOTS: void showMenu(const QPoint &); void copyAmount(); void copyLabel(); void copyAddress(); void copyTransactionHash(); void lockCoin(); void unlockCoin(); void clipboardQuantity(); void clipboardAmount(); void clipboardFee(); void clipboardAfterFee(); void clipboardBytes(); void clipboardLowOutput(); void clipboardChange(); void radioTreeMode(bool); void radioListMode(bool); void viewItemChanged(QTreeWidgetItem *, int); void headerSectionClicked(int); void buttonBoxClicked(QAbstractButton *); void buttonSelectAllClicked(); void updateLabelLocked(); }; #endif // BITCOIN_QT_COINCONTROLDIALOG_H diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 572ba23e06..eff7be52e8 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -1,1018 +1,1017 @@ // 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 "guiutil.h" #include "bitcoinaddressvalidator.h" #include "bitcoinunits.h" #include "dstencode.h" #include "qvalidatedlineedit.h" #include "walletmodel.h" #include "cashaddr.h" #include "config.h" #include "dstencode.h" #include "init.h" #include "policy/policy.h" #include "primitives/transaction.h" #include "protocol.h" #include "script/script.h" #include "script/standard.h" #include "util.h" #include "utilstrencodings.h" #ifdef WIN32 #ifdef _WIN32_WINNT #undef _WIN32_WINNT #endif #define _WIN32_WINNT 0x0501 #ifdef _WIN32_IE #undef _WIN32_IE #endif #define _WIN32_IE 0x0501 #define WIN32_LEAN_AND_MEAN 1 #ifndef NOMINMAX #define NOMINMAX #endif #include "shellapi.h" #include "shlobj.h" #include "shlwapi.h" #endif #include #include #if BOOST_FILESYSTEM_VERSION >= 3 #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for Qt::mightBeRichText #include #if QT_VERSION < 0x050000 #include #else #include #endif #if QT_VERSION >= 0x50200 #include #endif #if BOOST_FILESYSTEM_VERSION >= 3 static boost::filesystem::detail::utf8_codecvt_facet utf8; #endif #if defined(Q_OS_MAC) // These Mac includes must be done in the global namespace #include #include extern double NSAppKitVersionNumber; #if !defined(NSAppKitVersionNumber10_8) #define NSAppKitVersionNumber10_8 1187 #endif #if !defined(NSAppKitVersionNumber10_9) #define NSAppKitVersionNumber10_9 1265 #endif #endif namespace GUIUtil { const QString URI_SCHEME("bitcoincash"); QString dateTimeStr(const QDateTime &date) { return date.date().toString(Qt::SystemLocaleShortDate) + QString(" ") + date.toString("hh:mm"); } QString dateTimeStr(qint64 nTime) { return dateTimeStr(QDateTime::fromTime_t((qint32)nTime)); } QFont fixedPitchFont() { #if QT_VERSION >= 0x50200 return QFontDatabase::systemFont(QFontDatabase::FixedFont); #else QFont font("Monospace"); #if QT_VERSION >= 0x040800 font.setStyleHint(QFont::Monospace); #else font.setStyleHint(QFont::TypeWriter); #endif return font; #endif } static std::string MakeAddrInvalid(std::string addr) { if (addr.size() < 2) { return ""; } // Checksum is at the end of the address. Swapping chars to make it invalid. std::swap(addr[addr.size() - 1], addr[addr.size() - 2]); if (!IsValidDestinationString(addr)) { return addr; } return ""; } std::string DummyAddress(const CChainParams ¶ms, const Config &cfg) { // Just some dummy data to generate an convincing random-looking (but // consistent) address static const std::vector dummydata = { 0xeb, 0x15, 0x23, 0x1d, 0xfc, 0xeb, 0x60, 0x92, 0x58, 0x86, 0xb6, 0x7d, 0x06, 0x52, 0x99, 0x92, 0x59, 0x15, 0xae, 0xb1}; const CTxDestination dstKey = CKeyID(uint160(dummydata)); return MakeAddrInvalid(EncodeDestination(dstKey, params, cfg)); } void setupAddressWidget(QValidatedLineEdit *widget, QWidget *parent) { parent->setFocusProxy(widget); widget->setFont(fixedPitchFont()); const CChainParams ¶ms = Params(); #if QT_VERSION >= 0x040700 // We don't want translators to use own addresses in translations // and this is the only place, where this address is supplied. widget->setPlaceholderText( QObject::tr("Enter a Bitcoin address (e.g. %1)") .arg(QString::fromStdString(DummyAddress(params, GetConfig())))); #endif widget->setValidator( new BitcoinAddressEntryValidator(params.CashAddrPrefix(), parent)); widget->setCheckValidator(new BitcoinAddressCheckValidator(parent)); } void setupAmountWidget(QLineEdit *widget, QWidget *parent) { QDoubleValidator *amountValidator = new QDoubleValidator(parent); amountValidator->setDecimals(8); amountValidator->setBottom(0.0); widget->setValidator(amountValidator); widget->setAlignment(Qt::AlignRight | Qt::AlignVCenter); } static bool IsCashAddrEncoded(const QUrl &uri) { const std::string addr = (uri.scheme() + ":" + uri.path()).toStdString(); auto decoded = cashaddr::Decode(addr); return !decoded.first.empty(); } bool parseBitcoinURI(const QString &scheme, const QUrl &uri, SendCoinsRecipient *out) { // return if URI has wrong scheme. if (!uri.isValid() || uri.scheme() != scheme) { return false; } SendCoinsRecipient rv; if (IsCashAddrEncoded(uri)) { rv.address = uri.scheme() + ":" + uri.path(); } else { // strip out uri scheme for base58 encoded addresses rv.address = uri.path(); } // Trim any following forward slash which may have been added by the OS if (rv.address.endsWith("/")) { rv.address.truncate(rv.address.length() - 1); } rv.amount = 0; #if QT_VERSION < 0x050000 QList> items = uri.queryItems(); #else QUrlQuery uriQuery(uri); QList> items = uriQuery.queryItems(); #endif for (QList>::iterator i = items.begin(); i != items.end(); i++) { bool fShouldReturnFalse = false; if (i->first.startsWith("req-")) { i->first.remove(0, 4); fShouldReturnFalse = true; } if (i->first == "label") { rv.label = i->second; fShouldReturnFalse = false; } if (i->first == "message") { rv.message = i->second; fShouldReturnFalse = false; } else if (i->first == "amount") { if (!i->second.isEmpty()) { - Amount amt; - if (!BitcoinUnits::parse(BitcoinUnits::BCH, i->second, &amt)) { + if (!BitcoinUnits::parse(BitcoinUnits::BCH, i->second, + &rv.amount)) { return false; } - rv.amount = amt.GetSatoshis(); } fShouldReturnFalse = false; } if (fShouldReturnFalse) { return false; } } if (out) { *out = rv; } return true; } bool parseBitcoinURI(const QString &scheme, QString uri, SendCoinsRecipient *out) { // // Cannot handle this later, because bitcoincash:// // will cause Qt to see the part after // as host, // which will lower-case it (and thus invalidate the address). if (uri.startsWith(scheme + "://", Qt::CaseInsensitive)) { uri.replace(0, scheme.length() + 3, scheme + ":"); } QUrl uriInstance(uri); return parseBitcoinURI(scheme, uriInstance, out); } QString formatBitcoinURI(const Config &cfg, const SendCoinsRecipient &info) { QString ret = info.address; if (!cfg.UseCashAddrEncoding()) { // prefix address with uri scheme for base58 encoded addresses. ret = (URI_SCHEME + ":%1").arg(ret); } int paramCount = 0; if (info.amount != 0) { ret += QString("?amount=%1") .arg(BitcoinUnits::format(BitcoinUnits::BCH, info.amount, false, BitcoinUnits::separatorNever)); paramCount++; } if (!info.label.isEmpty()) { QString lbl(QUrl::toPercentEncoding(info.label)); ret += QString("%1label=%2").arg(paramCount == 0 ? "?" : "&").arg(lbl); paramCount++; } if (!info.message.isEmpty()) { QString msg(QUrl::toPercentEncoding(info.message)); ret += QString("%1message=%2").arg(paramCount == 0 ? "?" : "&").arg(msg); paramCount++; } return ret; } bool isDust(const QString &address, const Amount amount) { CTxDestination dest = DecodeDestination(address.toStdString()); CScript script = GetScriptForDestination(dest); CTxOut txOut(amount, script); return txOut.IsDust(dustRelayFee); } QString HtmlEscape(const QString &str, bool fMultiLine) { #if QT_VERSION < 0x050000 QString escaped = Qt::escape(str); #else QString escaped = str.toHtmlEscaped(); #endif if (fMultiLine) { escaped = escaped.replace("\n", "
\n"); } return escaped; } QString HtmlEscape(const std::string &str, bool fMultiLine) { return HtmlEscape(QString::fromStdString(str), fMultiLine); } void copyEntryData(QAbstractItemView *view, int column, int role) { if (!view || !view->selectionModel()) return; QModelIndexList selection = view->selectionModel()->selectedRows(column); if (!selection.isEmpty()) { // Copy first item setClipboard(selection.at(0).data(role).toString()); } } QList getEntryData(QAbstractItemView *view, int column) { if (!view || !view->selectionModel()) return QList(); return view->selectionModel()->selectedRows(column); } QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedSuffixOut) { QString selectedFilter; QString myDir; // Default to user documents location if (dir.isEmpty()) { #if QT_VERSION < 0x050000 myDir = QDesktopServices::storageLocation( QDesktopServices::DocumentsLocation); #else myDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); #endif } else { myDir = dir; } /* Directly convert path to native OS path separators */ QString result = QDir::toNativeSeparators(QFileDialog::getSaveFileName( parent, caption, myDir, filter, &selectedFilter)); /* Extract first suffix from filter pattern "Description (*.foo)" or * "Description (*.foo *.bar ...) */ QRegExp filter_re(".* \\(\\*\\.(.*)[ \\)]"); QString selectedSuffix; if (filter_re.exactMatch(selectedFilter)) { selectedSuffix = filter_re.cap(1); } /* Add suffix if needed */ QFileInfo info(result); if (!result.isEmpty()) { if (info.suffix().isEmpty() && !selectedSuffix.isEmpty()) { /* No suffix specified, add selected suffix */ if (!result.endsWith(".")) result.append("."); result.append(selectedSuffix); } } /* Return selected suffix if asked to */ if (selectedSuffixOut) { *selectedSuffixOut = selectedSuffix; } return result; } QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedSuffixOut) { QString selectedFilter; QString myDir; if (dir.isEmpty()) // Default to user documents location { #if QT_VERSION < 0x050000 myDir = QDesktopServices::storageLocation( QDesktopServices::DocumentsLocation); #else myDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); #endif } else { myDir = dir; } /* Directly convert path to native OS path separators */ QString result = QDir::toNativeSeparators(QFileDialog::getOpenFileName( parent, caption, myDir, filter, &selectedFilter)); if (selectedSuffixOut) { /* Extract first suffix from filter pattern "Description (*.foo)" or * "Description (*.foo *.bar ...) */ QRegExp filter_re(".* \\(\\*\\.(.*)[ \\)]"); QString selectedSuffix; if (filter_re.exactMatch(selectedFilter)) { selectedSuffix = filter_re.cap(1); } *selectedSuffixOut = selectedSuffix; } return result; } Qt::ConnectionType blockingGUIThreadConnection() { if (QThread::currentThread() != qApp->thread()) { return Qt::BlockingQueuedConnection; } else { return Qt::DirectConnection; } } bool checkPoint(const QPoint &p, const QWidget *w) { QWidget *atW = QApplication::widgetAt(w->mapToGlobal(p)); if (!atW) return false; return atW->topLevelWidget() == w; } bool isObscured(QWidget *w) { return !(checkPoint(QPoint(0, 0), w) && checkPoint(QPoint(w->width() - 1, 0), w) && checkPoint(QPoint(0, w->height() - 1), w) && checkPoint(QPoint(w->width() - 1, w->height() - 1), w) && checkPoint(QPoint(w->width() / 2, w->height() / 2), w)); } void openDebugLogfile() { boost::filesystem::path pathDebug = GetDataDir() / "debug.log"; /* Open debug.log with the associated application */ if (boost::filesystem::exists(pathDebug)) QDesktopServices::openUrl( QUrl::fromLocalFile(boostPathToQString(pathDebug))); } void SubstituteFonts(const QString &language) { #if defined(Q_OS_MAC) // Background: // OSX's default font changed in 10.9 and Qt is unable to find it with its // usual fallback methods when building against the 10.7 sdk or lower. // The 10.8 SDK added a function to let it find the correct fallback font. // If this fallback is not properly loaded, some characters may fail to // render correctly. // // The same thing happened with 10.10. .Helvetica Neue DeskInterface is now // default. // // Solution: If building with the 10.7 SDK or lower and the user's platform // is 10.9 or higher at runtime, substitute the correct font. This needs to // happen before the QApplication is created. #if defined(MAC_OS_X_VERSION_MAX_ALLOWED) && \ MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8 if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_8) { if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_9) /* On a 10.9 - 10.9.x system */ QFont::insertSubstitution(".Lucida Grande UI", "Lucida Grande"); else { /* 10.10 or later system */ if (language == "zh_CN" || language == "zh_TW" || language == "zh_HK") // traditional or simplified Chinese QFont::insertSubstitution(".Helvetica Neue DeskInterface", "Heiti SC"); else if (language == "ja") // Japanesee QFont::insertSubstitution(".Helvetica Neue DeskInterface", "Songti SC"); else QFont::insertSubstitution(".Helvetica Neue DeskInterface", "Lucida Grande"); } } #endif #endif } ToolTipToRichTextFilter::ToolTipToRichTextFilter(int _size_threshold, QObject *parent) : QObject(parent), size_threshold(_size_threshold) {} bool ToolTipToRichTextFilter::eventFilter(QObject *obj, QEvent *evt) { if (evt->type() == QEvent::ToolTipChange) { QWidget *widget = static_cast(obj); QString tooltip = widget->toolTip(); if (tooltip.size() > size_threshold && !tooltip.startsWith(" to make sure Qt detects this as rich text // Escape the current message as HTML and replace \n by
tooltip = "" + HtmlEscape(tooltip, true) + ""; widget->setToolTip(tooltip); return true; } } return QObject::eventFilter(obj, evt); } void TableViewLastColumnResizingFixer::connectViewHeadersSignals() { connect(tableView->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), this, SLOT(on_sectionResized(int, int, int))); connect(tableView->horizontalHeader(), SIGNAL(geometriesChanged()), this, SLOT(on_geometriesChanged())); } // We need to disconnect these while handling the resize events, otherwise we // can enter infinite loops. void TableViewLastColumnResizingFixer::disconnectViewHeadersSignals() { disconnect(tableView->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), this, SLOT(on_sectionResized(int, int, int))); disconnect(tableView->horizontalHeader(), SIGNAL(geometriesChanged()), this, SLOT(on_geometriesChanged())); } // Setup the resize mode, handles compatibility for Qt5 and below as the method // signatures changed. // Refactored here for readability. void TableViewLastColumnResizingFixer::setViewHeaderResizeMode( int logicalIndex, QHeaderView::ResizeMode resizeMode) { #if QT_VERSION < 0x050000 tableView->horizontalHeader()->setResizeMode(logicalIndex, resizeMode); #else tableView->horizontalHeader()->setSectionResizeMode(logicalIndex, resizeMode); #endif } void TableViewLastColumnResizingFixer::resizeColumn(int nColumnIndex, int width) { tableView->setColumnWidth(nColumnIndex, width); tableView->horizontalHeader()->resizeSection(nColumnIndex, width); } int TableViewLastColumnResizingFixer::getColumnsWidth() { int nColumnsWidthSum = 0; for (int i = 0; i < columnCount; i++) { nColumnsWidthSum += tableView->horizontalHeader()->sectionSize(i); } return nColumnsWidthSum; } int TableViewLastColumnResizingFixer::getAvailableWidthForColumn(int column) { int nResult = lastColumnMinimumWidth; int nTableWidth = tableView->horizontalHeader()->width(); if (nTableWidth > 0) { int nOtherColsWidth = getColumnsWidth() - tableView->horizontalHeader()->sectionSize(column); nResult = std::max(nResult, nTableWidth - nOtherColsWidth); } return nResult; } // Make sure we don't make the columns wider than the table's viewport width. void TableViewLastColumnResizingFixer::adjustTableColumnsWidth() { disconnectViewHeadersSignals(); resizeColumn(lastColumnIndex, getAvailableWidthForColumn(lastColumnIndex)); connectViewHeadersSignals(); int nTableWidth = tableView->horizontalHeader()->width(); int nColsWidth = getColumnsWidth(); if (nColsWidth > nTableWidth) { resizeColumn(secondToLastColumnIndex, getAvailableWidthForColumn(secondToLastColumnIndex)); } } // Make column use all the space available, useful during window resizing. void TableViewLastColumnResizingFixer::stretchColumnWidth(int column) { disconnectViewHeadersSignals(); resizeColumn(column, getAvailableWidthForColumn(column)); connectViewHeadersSignals(); } // When a section is resized this is a slot-proxy for ajustAmountColumnWidth(). void TableViewLastColumnResizingFixer::on_sectionResized(int logicalIndex, int oldSize, int newSize) { adjustTableColumnsWidth(); int remainingWidth = getAvailableWidthForColumn(logicalIndex); if (newSize > remainingWidth) { resizeColumn(logicalIndex, remainingWidth); } } // When the table's geometry is ready, we manually perform the stretch of the // "Message" column, // as the "Stretch" resize mode does not allow for interactive resizing. void TableViewLastColumnResizingFixer::on_geometriesChanged() { if ((getColumnsWidth() - this->tableView->horizontalHeader()->width()) != 0) { disconnectViewHeadersSignals(); resizeColumn(secondToLastColumnIndex, getAvailableWidthForColumn(secondToLastColumnIndex)); connectViewHeadersSignals(); } } /** * Initializes all internal variables and prepares the * the resize modes of the last 2 columns of the table and */ TableViewLastColumnResizingFixer::TableViewLastColumnResizingFixer( QTableView *table, int lastColMinimumWidth, int allColsMinimumWidth, QObject *parent) : QObject(parent), tableView(table), lastColumnMinimumWidth(lastColMinimumWidth), allColumnsMinimumWidth(allColsMinimumWidth) { columnCount = tableView->horizontalHeader()->count(); lastColumnIndex = columnCount - 1; secondToLastColumnIndex = columnCount - 2; tableView->horizontalHeader()->setMinimumSectionSize( allColumnsMinimumWidth); setViewHeaderResizeMode(secondToLastColumnIndex, QHeaderView::Interactive); setViewHeaderResizeMode(lastColumnIndex, QHeaderView::Interactive); } #ifdef WIN32 static boost::filesystem::path StartupShortcutPath() { std::string chain = ChainNameFromCommandLine(); if (chain == CBaseChainParams::MAIN) return GetSpecialFolderPath(CSIDL_STARTUP) / "Bitcoin.lnk"; // Remove this special case when CBaseChainParams::TESTNET = "testnet4" if (chain == CBaseChainParams::TESTNET) return GetSpecialFolderPath(CSIDL_STARTUP) / "Bitcoin (testnet).lnk"; return GetSpecialFolderPath(CSIDL_STARTUP) / strprintf("Bitcoin (%s).lnk", chain); } bool GetStartOnSystemStartup() { // check for Bitcoin*.lnk return boost::filesystem::exists(StartupShortcutPath()); } bool SetStartOnSystemStartup(bool fAutoStart) { // If the shortcut exists already, remove it for updating boost::filesystem::remove(StartupShortcutPath()); if (fAutoStart) { CoInitialize(nullptr); // Get a pointer to the IShellLink interface. IShellLink *psl = nullptr; HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLink, reinterpret_cast(&psl)); if (SUCCEEDED(hres)) { // Get the current executable path TCHAR pszExePath[MAX_PATH]; GetModuleFileName(nullptr, pszExePath, sizeof(pszExePath)); // Start client minimized QString strArgs = "-min"; // Set -testnet /-regtest options strArgs += QString::fromStdString(strprintf( " -testnet=%d -regtest=%d", GetBoolArg("-testnet", false), GetBoolArg("-regtest", false))); #ifdef UNICODE boost::scoped_array args(new TCHAR[strArgs.length() + 1]); // Convert the QString to TCHAR* strArgs.toWCharArray(args.get()); // Add missing '\0'-termination to string args[strArgs.length()] = '\0'; #endif // Set the path to the shortcut target psl->SetPath(pszExePath); PathRemoveFileSpec(pszExePath); psl->SetWorkingDirectory(pszExePath); psl->SetShowCmd(SW_SHOWMINNOACTIVE); #ifndef UNICODE psl->SetArguments(strArgs.toStdString().c_str()); #else psl->SetArguments(args.get()); #endif // Query IShellLink for the IPersistFile interface for // saving the shortcut in persistent storage. IPersistFile *ppf = nullptr; hres = psl->QueryInterface(IID_IPersistFile, reinterpret_cast(&ppf)); if (SUCCEEDED(hres)) { WCHAR pwsz[MAX_PATH]; // Ensure that the string is ANSI. MultiByteToWideChar(CP_ACP, 0, StartupShortcutPath().string().c_str(), -1, pwsz, MAX_PATH); // Save the link by calling IPersistFile::Save. hres = ppf->Save(pwsz, TRUE); ppf->Release(); psl->Release(); CoUninitialize(); return true; } psl->Release(); } CoUninitialize(); return false; } return true; } #elif defined(Q_OS_LINUX) // Follow the Desktop Application Autostart Spec: // http://standards.freedesktop.org/autostart-spec/autostart-spec-latest.html static boost::filesystem::path GetAutostartDir() { namespace fs = boost::filesystem; char *pszConfigHome = getenv("XDG_CONFIG_HOME"); if (pszConfigHome) return fs::path(pszConfigHome) / "autostart"; char *pszHome = getenv("HOME"); if (pszHome) return fs::path(pszHome) / ".config" / "autostart"; return fs::path(); } static boost::filesystem::path GetAutostartFilePath() { std::string chain = ChainNameFromCommandLine(); if (chain == CBaseChainParams::MAIN) return GetAutostartDir() / "bitcoin.desktop"; return GetAutostartDir() / strprintf("bitcoin-%s.lnk", chain); } bool GetStartOnSystemStartup() { boost::filesystem::ifstream optionFile(GetAutostartFilePath()); if (!optionFile.good()) return false; // Scan through file for "Hidden=true": std::string line; while (!optionFile.eof()) { getline(optionFile, line); if (line.find("Hidden") != std::string::npos && line.find("true") != std::string::npos) return false; } optionFile.close(); return true; } bool SetStartOnSystemStartup(bool fAutoStart) { if (!fAutoStart) boost::filesystem::remove(GetAutostartFilePath()); else { char pszExePath[MAX_PATH + 1]; memset(pszExePath, 0, sizeof(pszExePath)); if (readlink("/proc/self/exe", pszExePath, sizeof(pszExePath) - 1) == -1) return false; boost::filesystem::create_directories(GetAutostartDir()); boost::filesystem::ofstream optionFile( GetAutostartFilePath(), std::ios_base::out | std::ios_base::trunc); if (!optionFile.good()) return false; std::string chain = ChainNameFromCommandLine(); // Write a bitcoin.desktop file to the autostart directory: optionFile << "[Desktop Entry]\n"; optionFile << "Type=Application\n"; if (chain == CBaseChainParams::MAIN) optionFile << "Name=Bitcoin\n"; else optionFile << strprintf("Name=Bitcoin (%s)\n", chain); optionFile << "Exec=" << pszExePath << strprintf(" -min -testnet=%d -regtest=%d\n", GetBoolArg("-testnet", false), GetBoolArg("-regtest", false)); optionFile << "Terminal=false\n"; optionFile << "Hidden=false\n"; optionFile.close(); } return true; } #elif defined(Q_OS_MAC) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" // based on: // https://github.com/Mozketo/LaunchAtLoginController/blob/master/LaunchAtLoginController.m // NB: caller must release returned ref if it's not NULL LSSharedFileListItemRef findStartupItemInList(LSSharedFileListRef list, CFURLRef findUrl); LSSharedFileListItemRef findStartupItemInList(LSSharedFileListRef list, CFURLRef findUrl) { LSSharedFileListItemRef foundItem = nullptr; // loop through the list of startup items and try to find the bitcoin app CFArrayRef listSnapshot = LSSharedFileListCopySnapshot(list, nullptr); for (int i = 0; !foundItem && i < CFArrayGetCount(listSnapshot); ++i) { LSSharedFileListItemRef item = (LSSharedFileListItemRef)CFArrayGetValueAtIndex(listSnapshot, i); UInt32 resolutionFlags = kLSSharedFileListNoUserInteraction | kLSSharedFileListDoNotMountVolumes; CFURLRef currentItemURL = nullptr; #if defined(MAC_OS_X_VERSION_MAX_ALLOWED) && \ MAC_OS_X_VERSION_MAX_ALLOWED >= 10100 if (&LSSharedFileListItemCopyResolvedURL) currentItemURL = LSSharedFileListItemCopyResolvedURL( item, resolutionFlags, nullptr); #if defined(MAC_OS_X_VERSION_MIN_REQUIRED) && \ MAC_OS_X_VERSION_MIN_REQUIRED < 10100 else LSSharedFileListItemResolve(item, resolutionFlags, ¤tItemURL, nullptr); #endif #else LSSharedFileListItemResolve(item, resolutionFlags, ¤tItemURL, nullptr); #endif if (currentItemURL && CFEqual(currentItemURL, findUrl)) { // found CFRetain(foundItem = item); } if (currentItemURL) { CFRelease(currentItemURL); } } CFRelease(listSnapshot); return foundItem; } bool GetStartOnSystemStartup() { CFURLRef bitcoinAppUrl = CFBundleCopyBundleURL(CFBundleGetMainBundle()); LSSharedFileListRef loginItems = LSSharedFileListCreate( nullptr, kLSSharedFileListSessionLoginItems, nullptr); LSSharedFileListItemRef foundItem = findStartupItemInList(loginItems, bitcoinAppUrl); // findStartupItemInList retains the item it returned, need to release if (foundItem) { CFRelease(foundItem); } CFRelease(loginItems); CFRelease(bitcoinAppUrl); return foundItem; } bool SetStartOnSystemStartup(bool fAutoStart) { CFURLRef bitcoinAppUrl = CFBundleCopyBundleURL(CFBundleGetMainBundle()); LSSharedFileListRef loginItems = LSSharedFileListCreate( nullptr, kLSSharedFileListSessionLoginItems, nullptr); LSSharedFileListItemRef foundItem = findStartupItemInList(loginItems, bitcoinAppUrl); if (fAutoStart && !foundItem) { // add bitcoin app to startup item list LSSharedFileListInsertItemURL(loginItems, kLSSharedFileListItemBeforeFirst, nullptr, nullptr, bitcoinAppUrl, nullptr, nullptr); } else if (!fAutoStart && foundItem) { // remove item LSSharedFileListItemRemove(loginItems, foundItem); } // findStartupItemInList retains the item it returned, need to release if (foundItem) { CFRelease(foundItem); } CFRelease(loginItems); CFRelease(bitcoinAppUrl); return true; } #pragma GCC diagnostic pop #else bool GetStartOnSystemStartup() { return false; } bool SetStartOnSystemStartup(bool fAutoStart) { return false; } #endif void saveWindowGeometry(const QString &strSetting, QWidget *parent) { QSettings settings; settings.setValue(strSetting + "Pos", parent->pos()); settings.setValue(strSetting + "Size", parent->size()); } void restoreWindowGeometry(const QString &strSetting, const QSize &defaultSize, QWidget *parent) { QSettings settings; QPoint pos = settings.value(strSetting + "Pos").toPoint(); QSize size = settings.value(strSetting + "Size", defaultSize).toSize(); if (!pos.x() && !pos.y()) { QRect screen = QApplication::desktop()->screenGeometry(); pos.setX((screen.width() - size.width()) / 2); pos.setY((screen.height() - size.height()) / 2); } parent->resize(size); parent->move(pos); } void setClipboard(const QString &str) { QApplication::clipboard()->setText(str, QClipboard::Clipboard); QApplication::clipboard()->setText(str, QClipboard::Selection); } #if BOOST_FILESYSTEM_VERSION >= 3 boost::filesystem::path qstringToBoostPath(const QString &path) { return boost::filesystem::path(path.toStdString(), utf8); } QString boostPathToQString(const boost::filesystem::path &path) { return QString::fromStdString(path.string(utf8)); } #else #warning Conversion between boost path and QString can use invalid character encoding with boost_filesystem v2 and older boost::filesystem::path qstringToBoostPath(const QString &path) { return boost::filesystem::path(path.toStdString()); } QString boostPathToQString(const boost::filesystem::path &path) { return QString::fromStdString(path.string()); } #endif QString formatDurationStr(int secs) { QStringList strList; int days = secs / 86400; int hours = (secs % 86400) / 3600; int mins = (secs % 3600) / 60; int seconds = secs % 60; if (days) strList.append(QString(QObject::tr("%1 d")).arg(days)); if (hours) strList.append(QString(QObject::tr("%1 h")).arg(hours)); if (mins) strList.append(QString(QObject::tr("%1 m")).arg(mins)); if (seconds || (!days && !hours && !mins)) strList.append(QString(QObject::tr("%1 s")).arg(seconds)); return strList.join(" "); } QString formatServicesStr(quint64 mask) { QStringList strList; // Just scan the last 8 bits for now. for (int i = 0; i < 8; i++) { uint64_t check = 1 << i; if (mask & check) { switch (check) { case NODE_NETWORK: strList.append("NETWORK"); break; case NODE_GETUTXO: strList.append("GETUTXO"); break; case NODE_BLOOM: strList.append("BLOOM"); break; case NODE_XTHIN: strList.append("XTHIN"); break; case NODE_BITCOIN_CASH: strList.append("CASH"); break; default: strList.append(QString("%1[%2]").arg("UNKNOWN").arg(check)); } } } if (strList.size()) return strList.join(" & "); else return QObject::tr("None"); } QString formatPingTime(double dPingTime) { return (dPingTime == std::numeric_limits::max() / 1e6 || dPingTime == 0) ? QObject::tr("N/A") : QString(QObject::tr("%1 ms")) .arg(QString::number((int)(dPingTime * 1000), 10)); } QString formatTimeOffset(int64_t nTimeOffset) { return QString(QObject::tr("%1 s")) .arg(QString::number((int)nTimeOffset, 10)); } QString formatNiceTimeOffset(qint64 secs) { // Represent time from last generated block in human readable text QString timeBehindText; const int HOUR_IN_SECONDS = 60 * 60; const int DAY_IN_SECONDS = 24 * 60 * 60; const int WEEK_IN_SECONDS = 7 * 24 * 60 * 60; // Average length of year in Gregorian calendar const int YEAR_IN_SECONDS = 31556952; if (secs < 60) { timeBehindText = QObject::tr("%n second(s)", "", secs); } else if (secs < 2 * HOUR_IN_SECONDS) { timeBehindText = QObject::tr("%n minute(s)", "", secs / 60); } else if (secs < 2 * DAY_IN_SECONDS) { timeBehindText = QObject::tr("%n hour(s)", "", secs / HOUR_IN_SECONDS); } else if (secs < 2 * WEEK_IN_SECONDS) { timeBehindText = QObject::tr("%n day(s)", "", secs / DAY_IN_SECONDS); } else if (secs < YEAR_IN_SECONDS) { timeBehindText = QObject::tr("%n week(s)", "", secs / WEEK_IN_SECONDS); } else { qint64 years = secs / YEAR_IN_SECONDS; qint64 remainder = secs % YEAR_IN_SECONDS; timeBehindText = QObject::tr("%1 and %2") .arg(QObject::tr("%n year(s)", "", years)) .arg(QObject::tr("%n week(s)", "", remainder / WEEK_IN_SECONDS)); } return timeBehindText; } void ClickableLabel::mouseReleaseEvent(QMouseEvent *event) { Q_EMIT clicked(event->pos()); } void ClickableProgressBar::mouseReleaseEvent(QMouseEvent *event) { Q_EMIT clicked(event->pos()); } } // namespace GUIUtil diff --git a/src/qt/paymentrequestplus.cpp b/src/qt/paymentrequestplus.cpp index 75e2e2a972..612a738c48 100644 --- a/src/qt/paymentrequestplus.cpp +++ b/src/qt/paymentrequestplus.cpp @@ -1,232 +1,232 @@ // 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. // // Wraps dumb protocol buffer paymentRequest with some extra methods // #include "paymentrequestplus.h" #include "util.h" #include #include #include #include #include class SSLVerifyError : public std::runtime_error { public: SSLVerifyError(std::string err) : std::runtime_error(err) {} }; bool PaymentRequestPlus::parse(const QByteArray &data) { bool parseOK = paymentRequest.ParseFromArray(data.data(), data.size()); if (!parseOK) { qWarning() << "PaymentRequestPlus::parse: Error parsing payment request"; return false; } if (paymentRequest.payment_details_version() > 1) { qWarning() << "PaymentRequestPlus::parse: Received up-version payment " "details, version=" << paymentRequest.payment_details_version(); return false; } parseOK = details.ParseFromString(paymentRequest.serialized_payment_details()); if (!parseOK) { qWarning() << "PaymentRequestPlus::parse: Error parsing payment details"; paymentRequest.Clear(); return false; } return true; } bool PaymentRequestPlus::SerializeToString(std::string *output) const { return paymentRequest.SerializeToString(output); } bool PaymentRequestPlus::IsInitialized() const { return paymentRequest.IsInitialized(); } bool PaymentRequestPlus::getMerchant(X509_STORE *certStore, QString &merchant) const { merchant.clear(); if (!IsInitialized()) return false; // One day we'll support more PKI types, but just x509 for now: const EVP_MD *digestAlgorithm = nullptr; if (paymentRequest.pki_type() == "x509+sha256") { digestAlgorithm = EVP_sha256(); } else if (paymentRequest.pki_type() == "x509+sha1") { digestAlgorithm = EVP_sha1(); } else if (paymentRequest.pki_type() == "none") { qWarning() << "PaymentRequestPlus::getMerchant: Payment request: " "pki_type == none"; return false; } else { qWarning() << "PaymentRequestPlus::getMerchant: Payment request: " "unknown pki_type " << QString::fromStdString(paymentRequest.pki_type()); return false; } payments::X509Certificates certChain; if (!certChain.ParseFromString(paymentRequest.pki_data())) { qWarning() << "PaymentRequestPlus::getMerchant: Payment request: error " "parsing pki_data"; return false; } std::vector certs; const QDateTime currentTime = QDateTime::currentDateTime(); for (int i = 0; i < certChain.certificate_size(); i++) { QByteArray certData(certChain.certificate(i).data(), certChain.certificate(i).size()); QSslCertificate qCert(certData, QSsl::Der); if (currentTime < qCert.effectiveDate() || currentTime > qCert.expiryDate()) { qWarning() << "PaymentRequestPlus::getMerchant: Payment request: " "certificate expired or not yet active: " << qCert; return false; } #if QT_VERSION >= 0x050000 if (qCert.isBlacklisted()) { qWarning() << "PaymentRequestPlus::getMerchant: Payment request: " "certificate blacklisted: " << qCert; return false; } #endif const uint8_t *data = (const uint8_t *)certChain.certificate(i).data(); X509 *cert = d2i_X509(nullptr, &data, certChain.certificate(i).size()); if (cert) certs.push_back(cert); } if (certs.empty()) { qWarning() << "PaymentRequestPlus::getMerchant: Payment request: empty " "certificate chain"; return false; } // The first cert is the signing cert, the rest are untrusted certs that // chain to a valid root authority. OpenSSL needs them separately. STACK_OF(X509) *chain = sk_X509_new_null(); for (int i = certs.size() - 1; i > 0; i--) { sk_X509_push(chain, certs[i]); } X509 *signing_cert = certs[0]; // Now create a "store context", which is a single use object for checking, // load the signing cert into it and verify. X509_STORE_CTX *store_ctx = X509_STORE_CTX_new(); if (!store_ctx) { qWarning() << "PaymentRequestPlus::getMerchant: Payment request: error " "creating X509_STORE_CTX"; return false; } char *website = nullptr; bool fResult = true; try { if (!X509_STORE_CTX_init(store_ctx, certStore, signing_cert, chain)) { int error = X509_STORE_CTX_get_error(store_ctx); throw SSLVerifyError(X509_verify_cert_error_string(error)); } // Now do the verification! int result = X509_verify_cert(store_ctx); if (result != 1) { int error = X509_STORE_CTX_get_error(store_ctx); // For testing payment requests, we allow self signed root certs! // This option is just shown in the UI options, if -help-debug is // enabled. if (!(error == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && GetBoolArg("-allowselfsignedrootcertificates", DEFAULT_SELFSIGNED_ROOTCERTS))) { throw SSLVerifyError(X509_verify_cert_error_string(error)); } else { qDebug() << "PaymentRequestPlus::getMerchant: Allowing self " "signed root certificate, because " "-allowselfsignedrootcertificates is true."; } } X509_NAME *certname = X509_get_subject_name(signing_cert); // Valid cert; check signature: // Copy payments::PaymentRequest rcopy(paymentRequest); rcopy.set_signature(std::string("")); // Everything but the signature std::string data_to_verify; rcopy.SerializeToString(&data_to_verify); #if HAVE_DECL_EVP_MD_CTX_NEW EVP_MD_CTX *ctx = EVP_MD_CTX_new(); if (!ctx) throw SSLVerifyError("Error allocating OpenSSL context."); #else EVP_MD_CTX _ctx; EVP_MD_CTX *ctx; ctx = &_ctx; #endif EVP_PKEY *pubkey = X509_get_pubkey(signing_cert); EVP_MD_CTX_init(ctx); if (!EVP_VerifyInit_ex(ctx, digestAlgorithm, nullptr) || !EVP_VerifyUpdate(ctx, data_to_verify.data(), data_to_verify.size()) || !EVP_VerifyFinal( ctx, (const uint8_t *)paymentRequest.signature().data(), (unsigned int)paymentRequest.signature().size(), pubkey)) { throw SSLVerifyError("Bad signature, invalid payment request."); } #if HAVE_DECL_EVP_MD_CTX_NEW EVP_MD_CTX_free(ctx); #endif // OpenSSL API for getting human printable strings from certs is // baroque. int textlen = X509_NAME_get_text_by_NID(certname, NID_commonName, nullptr, 0); website = new char[textlen + 1]; if (X509_NAME_get_text_by_NID(certname, NID_commonName, website, textlen + 1) == textlen && textlen > 0) { merchant = website; } else { throw SSLVerifyError("Bad certificate, missing common name."); } // TODO: detect EV certificates and set merchant = business name instead // of unfriendly NID_commonName ? } catch (const SSLVerifyError &err) { fResult = false; qWarning() << "PaymentRequestPlus::getMerchant: SSL error: " << err.what(); } if (website) delete[] website; X509_STORE_CTX_free(store_ctx); for (unsigned int i = 0; i < certs.size(); i++) X509_free(certs[i]); return fResult; } -QList> PaymentRequestPlus::getPayTo() const { - QList> result; +QList> PaymentRequestPlus::getPayTo() const { + QList> result; for (int i = 0; i < details.outputs_size(); i++) { const uint8_t *scriptStr = (const uint8_t *)details.outputs(i).script().data(); CScript s(scriptStr, scriptStr + details.outputs(i).script().size()); result.append(std::make_pair(s, details.outputs(i).amount())); } return result; } diff --git a/src/qt/paymentrequestplus.h b/src/qt/paymentrequestplus.h index 520c73609a..9855550bf1 100644 --- a/src/qt/paymentrequestplus.h +++ b/src/qt/paymentrequestplus.h @@ -1,49 +1,49 @@ // 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_PAYMENTREQUESTPLUS_H #define BITCOIN_QT_PAYMENTREQUESTPLUS_H #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #include "paymentrequest.pb.h" #pragma GCC diagnostic pop #include "base58.h" #include #include #include #include static const bool DEFAULT_SELFSIGNED_ROOTCERTS = false; // // Wraps dumb protocol buffer paymentRequest with extra methods // class PaymentRequestPlus { public: PaymentRequestPlus() {} bool parse(const QByteArray &data); bool SerializeToString(std::string *output) const; bool IsInitialized() const; // Returns true if merchant's identity is authenticated, and returns // human-readable merchant identity in merchant bool getMerchant(X509_STORE *certStore, QString &merchant) const; // Returns list of outputs, amount - QList> getPayTo() const; + QList> getPayTo() const; const payments::PaymentDetails &getDetails() const { return details; } private: payments::PaymentRequest paymentRequest; payments::PaymentDetails details; }; #endif // BITCOIN_QT_PAYMENTREQUESTPLUS_H diff --git a/src/qt/paymentserver.cpp b/src/qt/paymentserver.cpp index 3713bae167..177193821d 100644 --- a/src/qt/paymentserver.cpp +++ b/src/qt/paymentserver.cpp @@ -1,819 +1,819 @@ // 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 "paymentserver.h" #include "bitcoinunits.h" #include "guiutil.h" #include "optionsmodel.h" #include "chainparams.h" #include "dstencode.h" #include "policy/policy.h" #include "ui_interface.h" #include "util.h" #include "wallet/wallet.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if QT_VERSION < 0x050000 #include #else #include #endif const int BITCOIN_IPC_CONNECT_TIMEOUT = 1000; // milliseconds // BIP70 payment protocol messages const char *BIP70_MESSAGE_PAYMENTACK = "PaymentACK"; const char *BIP70_MESSAGE_PAYMENTREQUEST = "PaymentRequest"; // BIP71 payment protocol media types const char *BIP71_MIMETYPE_PAYMENT = "application/bitcoincash-payment"; const char *BIP71_MIMETYPE_PAYMENTACK = "application/bitcoincash-paymentack"; const char *BIP71_MIMETYPE_PAYMENTREQUEST = "application/bitcoincash-paymentrequest"; struct X509StoreDeleter { void operator()(X509_STORE *b) { X509_STORE_free(b); } }; struct X509Deleter { void operator()(X509 *b) { X509_free(b); } }; namespace // Anon namespace { std::unique_ptr certStore; } // // Create a name that is unique for: // testnet / non-testnet // data directory // static QString ipcServerName() { QString name("BitcoinQt"); // Append a simple hash of the datadir // Note that GetDataDir(true) returns a different path for -testnet versus // main net QString ddir(GUIUtil::boostPathToQString(GetDataDir(true))); name.append(QString::number(qHash(ddir))); return name; } // // We store payment URIs and requests received before the main GUI window is up // and ready to ask the user to send payment. // static QList savedPaymentRequests; static void ReportInvalidCertificate(const QSslCertificate &cert) { #if QT_VERSION < 0x050000 qDebug() << QString("%1: Payment server found an invalid certificate: ") .arg(__func__) << cert.serialNumber() << cert.subjectInfo(QSslCertificate::CommonName) << cert.subjectInfo(QSslCertificate::OrganizationalUnitName); #else qDebug() << QString("%1: Payment server found an invalid certificate: ") .arg(__func__) << cert.serialNumber() << cert.subjectInfo(QSslCertificate::CommonName) << cert.subjectInfo(QSslCertificate::DistinguishedNameQualifier) << cert.subjectInfo(QSslCertificate::OrganizationalUnitName); #endif } // // Load OpenSSL's list of root certificate authorities // void PaymentServer::LoadRootCAs(X509_STORE *_store) { // Unit tests mostly use this, to pass in fake root CAs: if (_store) { certStore.reset(_store); return; } // Normal execution, use either -rootcertificates or system certs: certStore.reset(X509_STORE_new()); // Note: use "-system-" default here so that users can pass // -rootcertificates="" and get 'I don't like X.509 certificates, don't // trust anybody' behavior: QString certFile = QString::fromStdString(GetArg("-rootcertificates", "-system-")); // Empty store if (certFile.isEmpty()) { qDebug() << QString("PaymentServer::%1: Payment request authentication " "via X.509 certificates disabled.") .arg(__func__); return; } QList certList; if (certFile != "-system-") { qDebug() << QString("PaymentServer::%1: Using \"%2\" as trusted root " "certificate.") .arg(__func__) .arg(certFile); certList = QSslCertificate::fromPath(certFile); // Use those certificates when fetching payment requests, too: QSslSocket::setDefaultCaCertificates(certList); } else certList = QSslSocket::systemCaCertificates(); int nRootCerts = 0; const QDateTime currentTime = QDateTime::currentDateTime(); for (const QSslCertificate &cert : certList) { // Don't log nullptr certificates if (cert.isNull()) continue; // Not yet active/valid, or expired certificate if (currentTime < cert.effectiveDate() || currentTime > cert.expiryDate()) { ReportInvalidCertificate(cert); continue; } #if QT_VERSION >= 0x050000 // Blacklisted certificate if (cert.isBlacklisted()) { ReportInvalidCertificate(cert); continue; } #endif QByteArray certData = cert.toDer(); const uint8_t *data = (const uint8_t *)certData.data(); std::unique_ptr x509( d2i_X509(0, &data, certData.size())); if (x509 && X509_STORE_add_cert(certStore.get(), x509.get())) { // Note: X509_STORE increases the reference count to the X509 // object, we still have to release our reference to it. ++nRootCerts; } else { ReportInvalidCertificate(cert); continue; } } qWarning() << "PaymentServer::LoadRootCAs: Loaded " << nRootCerts << " root certificates"; // Project for another day: // Fetch certificate revocation lists, and add them to certStore. // Issues to consider: // performance (start a thread to fetch in background?) // privacy (fetch through tor/proxy so IP address isn't revealed) // would it be easier to just use a compiled-in blacklist? // or use Qt's blacklist? // "certificate stapling" with server-side caching is more efficient } // // Sending to the server is done synchronously, at startup. // If the server isn't already running, startup continues, and the items in // savedPaymentRequest will be handled when uiReady() is called. // // Warning: ipcSendCommandLine() is called early in init, so don't use "Q_EMIT // message()", but "QMessageBox::"! // void PaymentServer::ipcParseCommandLine(int argc, char *argv[]) { for (int i = 1; i < argc; i++) { QString arg(argv[i]); if (arg.startsWith("-")) continue; // If the bitcoincash: URI contains a payment request, we are not able // to detect the network as that would require fetching and parsing the // payment request. That means clicking such an URI which contains a // testnet payment request will start a mainnet instance and throw a // "wrong network" error. if (arg.startsWith(GUIUtil::URI_SCHEME + ":", Qt::CaseInsensitive)) // bitcoincash: URI { savedPaymentRequests.append(arg); SendCoinsRecipient r; if (GUIUtil::parseBitcoinURI(GUIUtil::URI_SCHEME, arg, &r) && !r.address.isEmpty()) { if (IsValidDestinationString(r.address.toStdString(), Params(CBaseChainParams::MAIN))) { SelectParams(CBaseChainParams::MAIN); } else if (IsValidDestinationString( r.address.toStdString(), Params(CBaseChainParams::TESTNET))) { SelectParams(CBaseChainParams::TESTNET); } } } else if (QFile::exists(arg)) { // Filename savedPaymentRequests.append(arg); PaymentRequestPlus request; if (readPaymentRequestFromFile(arg, request)) { if (request.getDetails().network() == "main") { SelectParams(CBaseChainParams::MAIN); } else if (request.getDetails().network() == "test") { SelectParams(CBaseChainParams::TESTNET); } } } else { // Printing to debug.log is about the best we can do here, the GUI // hasn't started yet so we can't pop up a message box. qWarning() << "PaymentServer::ipcSendCommandLine: Payment request " "file does not exist: " << arg; } } } // // Sending to the server is done synchronously, at startup. // If the server isn't already running, startup continues, and the items in // savedPaymentRequest will be handled when uiReady() is called. // bool PaymentServer::ipcSendCommandLine() { bool fResult = false; for (const QString &r : savedPaymentRequests) { QLocalSocket *socket = new QLocalSocket(); socket->connectToServer(ipcServerName(), QIODevice::WriteOnly); if (!socket->waitForConnected(BITCOIN_IPC_CONNECT_TIMEOUT)) { delete socket; socket = nullptr; return false; } QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_4_0); out << r; out.device()->seek(0); socket->write(block); socket->flush(); socket->waitForBytesWritten(BITCOIN_IPC_CONNECT_TIMEOUT); socket->disconnectFromServer(); delete socket; socket = nullptr; fResult = true; } return fResult; } PaymentServer::PaymentServer(QObject *parent, bool startLocalServer) : QObject(parent), saveURIs(true), uriServer(0), netManager(0), optionsModel(0) { // Verify that the version of the library that we linked against is // compatible with the version of the headers we compiled against. GOOGLE_PROTOBUF_VERIFY_VERSION; // Install global event filter to catch QFileOpenEvents // on Mac: sent when you click bitcoincash: links // other OSes: helpful when dealing with payment request files if (parent) parent->installEventFilter(this); QString name = ipcServerName(); // Clean up old socket leftover from a crash: QLocalServer::removeServer(name); if (startLocalServer) { uriServer = new QLocalServer(this); if (!uriServer->listen(name)) { // constructor is called early in init, so don't use "Q_EMIT // message()" here QMessageBox::critical(0, tr("Payment request error"), tr("Cannot start %1: click-to-pay handler") .arg(GUIUtil::URI_SCHEME)); } else { connect(uriServer, SIGNAL(newConnection()), this, SLOT(handleURIConnection())); connect(this, SIGNAL(receivedPaymentACK(QString)), this, SLOT(handlePaymentACK(QString))); } } } PaymentServer::~PaymentServer() { google::protobuf::ShutdownProtobufLibrary(); } // // OSX-specific way of handling bitcoincash: URIs and PaymentRequest mime types. // Also used by paymentservertests.cpp and when opening a payment request file // via "Open URI..." menu entry. // bool PaymentServer::eventFilter(QObject *object, QEvent *event) { if (event->type() == QEvent::FileOpen) { QFileOpenEvent *fileEvent = static_cast(event); if (!fileEvent->file().isEmpty()) handleURIOrFile(fileEvent->file()); else if (!fileEvent->url().isEmpty()) handleURIOrFile(fileEvent->url().toString()); return true; } return QObject::eventFilter(object, event); } void PaymentServer::initNetManager() { if (!optionsModel) return; if (netManager != nullptr) delete netManager; // netManager is used to fetch paymentrequests given in bitcoincash: URIs netManager = new QNetworkAccessManager(this); QNetworkProxy proxy; // Query active SOCKS5 proxy if (optionsModel->getProxySettings(proxy)) { netManager->setProxy(proxy); qDebug() << "PaymentServer::initNetManager: Using SOCKS5 proxy" << proxy.hostName() << ":" << proxy.port(); } else qDebug() << "PaymentServer::initNetManager: No active proxy server found."; connect(netManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(netRequestFinished(QNetworkReply *))); connect(netManager, SIGNAL(sslErrors(QNetworkReply *, const QList &)), this, SLOT(reportSslErrors(QNetworkReply *, const QList &))); } void PaymentServer::uiReady() { initNetManager(); saveURIs = false; for (const QString &s : savedPaymentRequests) { handleURIOrFile(s); } savedPaymentRequests.clear(); } void PaymentServer::handleURIOrFile(const QString &s) { if (saveURIs) { savedPaymentRequests.append(s); return; } // bitcoincash: URI if (s.startsWith(GUIUtil::URI_SCHEME + ":", Qt::CaseInsensitive)) { #if QT_VERSION < 0x050000 QUrl uri(s); #else QUrlQuery uri((QUrl(s))); #endif if (uri.hasQueryItem("r")) { // payment request URI QByteArray temp; temp.append(uri.queryItemValue("r")); QString decoded = QUrl::fromPercentEncoding(temp); QUrl fetchUrl(decoded, QUrl::StrictMode); if (fetchUrl.isValid()) { qDebug() << "PaymentServer::handleURIOrFile: fetchRequest(" << fetchUrl << ")"; fetchRequest(fetchUrl); } else { qWarning() << "PaymentServer::handleURIOrFile: Invalid URL: " << fetchUrl; Q_EMIT message(tr("URI handling"), tr("Payment request fetch URL is invalid: %1") .arg(fetchUrl.toString()), CClientUIInterface::ICON_WARNING); } return; } else { // normal URI SendCoinsRecipient recipient; if (GUIUtil::parseBitcoinURI(GUIUtil::URI_SCHEME, s, &recipient)) { if (!IsValidDestinationString( recipient.address.toStdString())) { Q_EMIT message( tr("URI handling"), tr("Invalid payment address %1").arg(recipient.address), CClientUIInterface::MSG_ERROR); } else Q_EMIT receivedPaymentRequest(recipient); } else Q_EMIT message( tr("URI handling"), tr("URI cannot be parsed! This can be caused by an invalid " "Bitcoin address or malformed URI parameters."), CClientUIInterface::ICON_WARNING); return; } } // payment request file if (QFile::exists(s)) { PaymentRequestPlus request; SendCoinsRecipient recipient; if (!readPaymentRequestFromFile(s, request)) { Q_EMIT message(tr("Payment request file handling"), tr("Payment request file cannot be read! This can " "be caused by an invalid payment request file."), CClientUIInterface::ICON_WARNING); } else if (processPaymentRequest(request, recipient)) Q_EMIT receivedPaymentRequest(recipient); return; } } void PaymentServer::handleURIConnection() { QLocalSocket *clientConnection = uriServer->nextPendingConnection(); while (clientConnection->bytesAvailable() < (int)sizeof(quint32)) clientConnection->waitForReadyRead(); connect(clientConnection, SIGNAL(disconnected()), clientConnection, SLOT(deleteLater())); QDataStream in(clientConnection); in.setVersion(QDataStream::Qt_4_0); if (clientConnection->bytesAvailable() < (int)sizeof(quint16)) { return; } QString msg; in >> msg; handleURIOrFile(msg); } // // Warning: readPaymentRequestFromFile() is used in ipcSendCommandLine() // so don't use "Q_EMIT message()", but "QMessageBox::"! // bool PaymentServer::readPaymentRequestFromFile(const QString &filename, PaymentRequestPlus &request) { QFile f(filename); if (!f.open(QIODevice::ReadOnly)) { qWarning() << QString("PaymentServer::%1: Failed to open %2") .arg(__func__) .arg(filename); return false; } // BIP70 DoS protection if (!verifySize(f.size())) { return false; } QByteArray data = f.readAll(); return request.parse(data); } bool PaymentServer::processPaymentRequest(const PaymentRequestPlus &request, SendCoinsRecipient &recipient) { if (!optionsModel) return false; if (request.IsInitialized()) { // Payment request network matches client network? if (!verifyNetwork(request.getDetails())) { Q_EMIT message( tr("Payment request rejected"), tr("Payment request network doesn't match client network."), CClientUIInterface::MSG_ERROR); return false; } // Make sure any payment requests involved are still valid. // This is re-checked just before sending coins in // WalletModel::sendCoins(). if (verifyExpired(request.getDetails())) { Q_EMIT message(tr("Payment request rejected"), tr("Payment request expired."), CClientUIInterface::MSG_ERROR); return false; } } else { Q_EMIT message(tr("Payment request error"), tr("Payment request is not initialized."), CClientUIInterface::MSG_ERROR); return false; } recipient.paymentRequest = request; recipient.message = GUIUtil::HtmlEscape(request.getDetails().memo()); request.getMerchant(certStore.get(), recipient.authenticatedMerchant); - QList> sendingTos = request.getPayTo(); + QList> sendingTos = request.getPayTo(); QStringList addresses; - for (const std::pair &sendingTo : sendingTos) { + for (const std::pair &sendingTo : sendingTos) { // Extract and check destination addresses CTxDestination dest; if (ExtractDestination(sendingTo.first, dest)) { // Append destination address addresses.append(QString::fromStdString(EncodeDestination(dest))); } else if (!recipient.authenticatedMerchant.isEmpty()) { // Unauthenticated payment requests to custom bitcoin addresses are // not supported // (there is no good way to tell the user where they are paying in a // way they'd // have a chance of understanding). Q_EMIT message(tr("Payment request rejected"), tr("Unverified payment requests to custom payment " "scripts are unsupported."), CClientUIInterface::MSG_ERROR); return false; } // Bitcoin amounts are stored as (optional) uint64 in the protobuf - // messages (see paymentrequest.proto), but CAmount is defined as + // messages (see paymentrequest.proto), but Amount is defined as // int64_t. Because of that we need to verify that amounts are in a // valid range and no overflow has happened. if (!verifyAmount(sendingTo.second)) { Q_EMIT message(tr("Payment request rejected"), tr("Invalid payment request."), CClientUIInterface::MSG_ERROR); return false; } // Extract and check amounts CTxOut txOut(Amount(sendingTo.second), sendingTo.first); if (txOut.IsDust(dustRelayFee)) { Q_EMIT message( tr("Payment request error"), tr("Requested payment amount of %1 is too small (considered " "dust).") .arg(BitcoinUnits::formatWithUnit( optionsModel->getDisplayUnit(), sendingTo.second)), CClientUIInterface::MSG_ERROR); return false; } recipient.amount += sendingTo.second; // Also verify that the final amount is still in a valid range after // adding additional amounts. if (!verifyAmount(recipient.amount.GetSatoshis())) { Q_EMIT message(tr("Payment request rejected"), tr("Invalid payment request."), CClientUIInterface::MSG_ERROR); return false; } } // Store addresses and format them to fit nicely into the GUI recipient.address = addresses.join("
"); if (!recipient.authenticatedMerchant.isEmpty()) { qDebug() << "PaymentServer::processPaymentRequest: Secure payment " "request from " << recipient.authenticatedMerchant; } else { qDebug() << "PaymentServer::processPaymentRequest: Insecure payment " "request to " << addresses.join(", "); } return true; } void PaymentServer::fetchRequest(const QUrl &url) { QNetworkRequest netRequest; netRequest.setAttribute(QNetworkRequest::User, BIP70_MESSAGE_PAYMENTREQUEST); netRequest.setUrl(url); netRequest.setRawHeader("User-Agent", CLIENT_NAME.c_str()); netRequest.setRawHeader("Accept", BIP71_MIMETYPE_PAYMENTREQUEST); netManager->get(netRequest); } void PaymentServer::fetchPaymentACK(CWallet *wallet, SendCoinsRecipient recipient, QByteArray transaction) { const payments::PaymentDetails &details = recipient.paymentRequest.getDetails(); if (!details.has_payment_url()) return; QNetworkRequest netRequest; netRequest.setAttribute(QNetworkRequest::User, BIP70_MESSAGE_PAYMENTACK); netRequest.setUrl(QString::fromStdString(details.payment_url())); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, BIP71_MIMETYPE_PAYMENT); netRequest.setRawHeader("User-Agent", CLIENT_NAME.c_str()); netRequest.setRawHeader("Accept", BIP71_MIMETYPE_PAYMENTACK); payments::Payment payment; payment.set_merchant_data(details.merchant_data()); payment.add_transactions(transaction.data(), transaction.size()); // Create a new refund address, or re-use: QString account = tr("Refund from %1").arg(recipient.authenticatedMerchant); std::string strAccount = account.toStdString(); std::set refundAddresses = wallet->GetAccountAddresses(strAccount); if (!refundAddresses.empty()) { CScript s = GetScriptForDestination(*refundAddresses.begin()); payments::Output *refund_to = payment.add_refund_to(); refund_to->set_script(&s[0], s.size()); } else { CPubKey newKey; if (wallet->GetKeyFromPool(newKey)) { CKeyID keyID = newKey.GetID(); wallet->SetAddressBook(keyID, strAccount, "refund"); CScript s = GetScriptForDestination(keyID); payments::Output *refund_to = payment.add_refund_to(); refund_to->set_script(&s[0], s.size()); } else { // This should never happen, because sending coins should have just // unlocked the wallet and refilled the keypool. qWarning() << "PaymentServer::fetchPaymentACK: Error getting " "refund key, refund_to not set"; } } int length = payment.ByteSize(); netRequest.setHeader(QNetworkRequest::ContentLengthHeader, length); QByteArray serData(length, '\0'); if (payment.SerializeToArray(serData.data(), length)) { netManager->post(netRequest, serData); } else { // This should never happen, either. qWarning() << "PaymentServer::fetchPaymentACK: Error serializing " "payment message"; } } void PaymentServer::netRequestFinished(QNetworkReply *reply) { reply->deleteLater(); // BIP70 DoS protection if (!verifySize(reply->size())) { Q_EMIT message( tr("Payment request rejected"), tr("Payment request %1 is too large (%2 bytes, allowed %3 bytes).") .arg(reply->request().url().toString()) .arg(reply->size()) .arg(BIP70_MAX_PAYMENTREQUEST_SIZE), CClientUIInterface::MSG_ERROR); return; } if (reply->error() != QNetworkReply::NoError) { QString msg = tr("Error communicating with %1: %2") .arg(reply->request().url().toString()) .arg(reply->errorString()); qWarning() << "PaymentServer::netRequestFinished: " << msg; Q_EMIT message(tr("Payment request error"), msg, CClientUIInterface::MSG_ERROR); return; } QByteArray data = reply->readAll(); QString requestType = reply->request().attribute(QNetworkRequest::User).toString(); if (requestType == BIP70_MESSAGE_PAYMENTREQUEST) { PaymentRequestPlus request; SendCoinsRecipient recipient; if (!request.parse(data)) { qWarning() << "PaymentServer::netRequestFinished: Error parsing " "payment request"; Q_EMIT message(tr("Payment request error"), tr("Payment request cannot be parsed!"), CClientUIInterface::MSG_ERROR); } else if (processPaymentRequest(request, recipient)) Q_EMIT receivedPaymentRequest(recipient); return; } else if (requestType == BIP70_MESSAGE_PAYMENTACK) { payments::PaymentACK paymentACK; if (!paymentACK.ParseFromArray(data.data(), data.size())) { QString msg = tr("Bad response from server %1") .arg(reply->request().url().toString()); qWarning() << "PaymentServer::netRequestFinished: " << msg; Q_EMIT message(tr("Payment request error"), msg, CClientUIInterface::MSG_ERROR); } else { Q_EMIT receivedPaymentACK(GUIUtil::HtmlEscape(paymentACK.memo())); } } } void PaymentServer::reportSslErrors(QNetworkReply *reply, const QList &errs) { Q_UNUSED(reply); QString errString; for (const QSslError &err : errs) { qWarning() << "PaymentServer::reportSslErrors: " << err; errString += err.errorString() + "\n"; } Q_EMIT message(tr("Network request error"), errString, CClientUIInterface::MSG_ERROR); } void PaymentServer::setOptionsModel(OptionsModel *_optionsModel) { this->optionsModel = _optionsModel; } void PaymentServer::handlePaymentACK(const QString &paymentACKMsg) { // currently we don't further process or store the paymentACK message Q_EMIT message(tr("Payment acknowledged"), paymentACKMsg, CClientUIInterface::ICON_INFORMATION | CClientUIInterface::MODAL); } bool PaymentServer::verifyNetwork( const payments::PaymentDetails &requestDetails) { bool fVerified = requestDetails.network() == Params().NetworkIDString(); if (!fVerified) { qWarning() << QString("PaymentServer::%1: Payment request network " "\"%2\" doesn't match client network \"%3\".") .arg(__func__) .arg(QString::fromStdString(requestDetails.network())) .arg(QString::fromStdString( Params().NetworkIDString())); } return fVerified; } bool PaymentServer::verifyExpired( const payments::PaymentDetails &requestDetails) { bool fVerified = (requestDetails.has_expires() && (int64_t)requestDetails.expires() < GetTime()); if (fVerified) { const QString requestExpires = QString::fromStdString(DateTimeStrFormat( "%Y-%m-%d %H:%M:%S", (int64_t)requestDetails.expires())); qWarning() << QString( "PaymentServer::%1: Payment request expired \"%2\".") .arg(__func__) .arg(requestExpires); } return fVerified; } bool PaymentServer::verifySize(qint64 requestSize) { bool fVerified = (requestSize <= BIP70_MAX_PAYMENTREQUEST_SIZE); if (!fVerified) { qWarning() << QString("PaymentServer::%1: Payment request too large " "(%2 bytes, allowed %3 bytes).") .arg(__func__) .arg(requestSize) .arg(BIP70_MAX_PAYMENTREQUEST_SIZE); } return fVerified; } -bool PaymentServer::verifyAmount(const CAmount &requestAmount) { +bool PaymentServer::verifyAmount(const Amount requestAmount) { bool fVerified = MoneyRange(Amount(requestAmount)); if (!fVerified) { qWarning() << QString("PaymentServer::%1: Payment request amount out " "of allowed range (%2, allowed 0 - %3).") .arg(__func__) - .arg(requestAmount) + .arg(requestAmount.GetSatoshis()) .arg(MAX_MONEY.GetSatoshis()); } return fVerified; } X509_STORE *PaymentServer::getCertStore() { return certStore.get(); } diff --git a/src/qt/paymentserver.h b/src/qt/paymentserver.h index 7dec7a3445..569b371a22 100644 --- a/src/qt/paymentserver.h +++ b/src/qt/paymentserver.h @@ -1,148 +1,148 @@ // 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_PAYMENTSERVER_H #define BITCOIN_QT_PAYMENTSERVER_H // This class handles payment requests from clicking on // bitcoincash: URIs // // This is somewhat tricky, because we have to deal with the situation where the // user clicks on a link during startup/initialization, when the splash-screen // is up but the main window (and the Send Coins tab) is not. // // So, the strategy is: // // Create the server, and register the event handler, when the application is // created. Save any URIs received at or during startup in a list. // // When startup is finished and the main window is shown, a signal is sent to // slot uiReady(), which emits a receivedURI() signal for any payment requests // that happened during startup. // // After startup, receivedURI() happens as usual. // // This class has one more feature: a static method that finds URIs passed in // the command line and, if a server is running in another process, sends them // to the server. // #include "paymentrequestplus.h" #include "walletmodel.h" #include #include class OptionsModel; class CWallet; QT_BEGIN_NAMESPACE class QApplication; class QByteArray; class QLocalServer; class QNetworkAccessManager; class QNetworkReply; class QSslError; class QUrl; QT_END_NAMESPACE // BIP70 max payment request size in bytes (DoS protection) static const qint64 BIP70_MAX_PAYMENTREQUEST_SIZE = 50000; class PaymentServer : public QObject { Q_OBJECT public: // Parse URIs on command line // Returns false on error static void ipcParseCommandLine(int argc, char *argv[]); // Returns true if there were URIs on the command line which were // successfully sent to an already-running process. // Note: if a payment request is given, SelectParams(MAIN/TESTNET) will be // called so we startup in the right mode. static bool ipcSendCommandLine(); // parent should be QApplication object PaymentServer(QObject *parent, bool startLocalServer = true); ~PaymentServer(); // Load root certificate authorities. Pass nullptr (default) to read from // the file specified in the -rootcertificates setting, or, if that's not // set, to use the system default root certificates. If you pass in a store, // you should not X509_STORE_free it: it will be freed either at exit or // when another set of CAs are loaded. static void LoadRootCAs(X509_STORE *store = nullptr); // Return certificate store static X509_STORE *getCertStore(); // OptionsModel is used for getting proxy settings and display unit void setOptionsModel(OptionsModel *optionsModel); // Verify that the payment request network matches the client network static bool verifyNetwork(const payments::PaymentDetails &requestDetails); // Verify if the payment request is expired static bool verifyExpired(const payments::PaymentDetails &requestDetails); // Verify the payment request size is valid as per BIP70 static bool verifySize(qint64 requestSize); // Verify the payment request amount is valid - static bool verifyAmount(const CAmount &requestAmount); + static bool verifyAmount(const Amount requestAmount); Q_SIGNALS: // Fired when a valid payment request is received void receivedPaymentRequest(SendCoinsRecipient); // Fired when a valid PaymentACK is received void receivedPaymentACK(const QString &paymentACKMsg); // Fired when a message should be reported to the user void message(const QString &title, const QString &message, unsigned int style); public Q_SLOTS: // Signal this when the main window's UI is ready to display payment // requests to the user void uiReady(); // Submit Payment message to a merchant, get back PaymentACK: void fetchPaymentACK(CWallet *wallet, SendCoinsRecipient recipient, QByteArray transaction); // Handle an incoming URI, URI with local file scheme or file void handleURIOrFile(const QString &s); private Q_SLOTS: void handleURIConnection(); void netRequestFinished(QNetworkReply *); void reportSslErrors(QNetworkReply *, const QList &); void handlePaymentACK(const QString &paymentACKMsg); protected: // Constructor registers this on the parent QApplication to receive // QEvent::FileOpen and QEvent:Drop events bool eventFilter(QObject *object, QEvent *event) override; private: static bool readPaymentRequestFromFile(const QString &filename, PaymentRequestPlus &request); bool processPaymentRequest(const PaymentRequestPlus &request, SendCoinsRecipient &recipient); void fetchRequest(const QUrl &url); // Setup networking void initNetManager(); // true during startup bool saveURIs; QLocalServer *uriServer; // Used to fetch payment requests QNetworkAccessManager *netManager; OptionsModel *optionsModel; }; #endif // BITCOIN_QT_PAYMENTSERVER_H diff --git a/src/qt/receivecoinsdialog.cpp b/src/qt/receivecoinsdialog.cpp index 433e3109d4..06052c3672 100644 --- a/src/qt/receivecoinsdialog.cpp +++ b/src/qt/receivecoinsdialog.cpp @@ -1,299 +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 "receivecoinsdialog.h" #include "ui_receivecoinsdialog.h" #include "addressbookpage.h" #include "addresstablemodel.h" #include "bitcoinunits.h" #include "guiutil.h" #include "optionsmodel.h" #include "platformstyle.h" #include "receiverequestdialog.h" #include "recentrequeststablemodel.h" #include "walletmodel.h" #include #include #include #include #include #include ReceiveCoinsDialog::ReceiveCoinsDialog(const PlatformStyle *_platformStyle, const Config *cfg, QWidget *parent) : QDialog(parent), ui(new Ui::ReceiveCoinsDialog), columnResizingFixer(0), model(0), platformStyle(_platformStyle), cfg(cfg) { ui->setupUi(this); if (!_platformStyle->getImagesOnButtons()) { ui->clearButton->setIcon(QIcon()); ui->receiveButton->setIcon(QIcon()); ui->showRequestButton->setIcon(QIcon()); ui->removeRequestButton->setIcon(QIcon()); } else { ui->clearButton->setIcon( _platformStyle->SingleColorIcon(":/icons/remove")); ui->receiveButton->setIcon( _platformStyle->SingleColorIcon(":/icons/receiving_addresses")); ui->showRequestButton->setIcon( _platformStyle->SingleColorIcon(":/icons/edit")); ui->removeRequestButton->setIcon( _platformStyle->SingleColorIcon(":/icons/remove")); } // context menu actions QAction *copyURIAction = new QAction(tr("Copy URI"), this); QAction *copyLabelAction = new QAction(tr("Copy label"), this); QAction *copyMessageAction = new QAction(tr("Copy message"), this); QAction *copyAmountAction = new QAction(tr("Copy amount"), this); // context menu contextMenu = new QMenu(this); contextMenu->addAction(copyURIAction); contextMenu->addAction(copyLabelAction); contextMenu->addAction(copyMessageAction); contextMenu->addAction(copyAmountAction); // context menu signals connect(ui->recentRequestsView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showMenu(QPoint))); connect(copyURIAction, SIGNAL(triggered()), this, SLOT(copyURI())); connect(copyLabelAction, SIGNAL(triggered()), this, SLOT(copyLabel())); connect(copyMessageAction, SIGNAL(triggered()), this, SLOT(copyMessage())); connect(copyAmountAction, SIGNAL(triggered()), this, SLOT(copyAmount())); connect(ui->clearButton, SIGNAL(clicked()), this, SLOT(clear())); } void ReceiveCoinsDialog::setModel(WalletModel *_model) { this->model = _model; if (_model && _model->getOptionsModel()) { _model->getRecentRequestsTableModel()->sort( RecentRequestsTableModel::Date, Qt::DescendingOrder); connect(_model->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(updateDisplayUnit())); updateDisplayUnit(); QTableView *tableView = ui->recentRequestsView; tableView->verticalHeader()->hide(); tableView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); tableView->setModel(_model->getRecentRequestsTableModel()); tableView->setAlternatingRowColors(true); tableView->setSelectionBehavior(QAbstractItemView::SelectRows); tableView->setSelectionMode(QAbstractItemView::ContiguousSelection); tableView->setColumnWidth(RecentRequestsTableModel::Date, DATE_COLUMN_WIDTH); tableView->setColumnWidth(RecentRequestsTableModel::Label, LABEL_COLUMN_WIDTH); tableView->setColumnWidth(RecentRequestsTableModel::Amount, AMOUNT_MINIMUM_COLUMN_WIDTH); connect(tableView->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(recentRequestsView_selectionChanged(QItemSelection, QItemSelection))); // Last 2 columns are set by the columnResizingFixer, when the table // geometry is ready. columnResizingFixer = new GUIUtil::TableViewLastColumnResizingFixer( tableView, AMOUNT_MINIMUM_COLUMN_WIDTH, DATE_COLUMN_WIDTH, this); } } ReceiveCoinsDialog::~ReceiveCoinsDialog() { delete ui; } void ReceiveCoinsDialog::clear() { ui->reqAmount->clear(); ui->reqLabel->setText(""); ui->reqMessage->setText(""); ui->reuseAddress->setChecked(false); updateDisplayUnit(); } void ReceiveCoinsDialog::reject() { clear(); } void ReceiveCoinsDialog::accept() { clear(); } void ReceiveCoinsDialog::updateDisplayUnit() { if (model && model->getOptionsModel()) { ui->reqAmount->setDisplayUnit( model->getOptionsModel()->getDisplayUnit()); } } void ReceiveCoinsDialog::on_receiveButton_clicked() { if (!model || !model->getOptionsModel() || !model->getAddressTableModel() || !model->getRecentRequestsTableModel()) return; QString address; QString label = ui->reqLabel->text(); if (ui->reuseAddress->isChecked()) { /* Choose existing receiving address */ AddressBookPage dlg(platformStyle, AddressBookPage::ForSelection, AddressBookPage::ReceivingTab, this); dlg.setModel(model->getAddressTableModel()); if (dlg.exec()) { address = dlg.getReturnValue(); // If no label provided, use the previously used label if (label.isEmpty()) { label = model->getAddressTableModel()->labelForAddress(address); } } else { return; } } else { /* Generate new receiving address */ address = model->getAddressTableModel()->addRow( AddressTableModel::Receive, label, ""); } - SendCoinsRecipient info(address, label, - ui->reqAmount->value().GetSatoshis(), + SendCoinsRecipient info(address, label, ui->reqAmount->value(), ui->reqMessage->text()); ReceiveRequestDialog *dialog = new ReceiveRequestDialog(cfg, this); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setModel(model->getOptionsModel()); dialog->setInfo(info); dialog->show(); clear(); /* Store request for later reference */ model->getRecentRequestsTableModel()->addNewRequest(info); } void ReceiveCoinsDialog::on_recentRequestsView_doubleClicked( const QModelIndex &index) { const RecentRequestsTableModel *submodel = model->getRecentRequestsTableModel(); ReceiveRequestDialog *dialog = new ReceiveRequestDialog(cfg, this); dialog->setModel(model->getOptionsModel()); dialog->setInfo(submodel->entry(index.row()).recipient); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); } void ReceiveCoinsDialog::recentRequestsView_selectionChanged( const QItemSelection &selected, const QItemSelection &deselected) { // Enable Show/Remove buttons only if anything is selected. bool enable = !ui->recentRequestsView->selectionModel()->selectedRows().isEmpty(); ui->showRequestButton->setEnabled(enable); ui->removeRequestButton->setEnabled(enable); } void ReceiveCoinsDialog::on_showRequestButton_clicked() { if (!model || !model->getRecentRequestsTableModel() || !ui->recentRequestsView->selectionModel()) return; QModelIndexList selection = ui->recentRequestsView->selectionModel()->selectedRows(); for (const QModelIndex &index : selection) { on_recentRequestsView_doubleClicked(index); } } void ReceiveCoinsDialog::on_removeRequestButton_clicked() { if (!model || !model->getRecentRequestsTableModel() || !ui->recentRequestsView->selectionModel()) return; QModelIndexList selection = ui->recentRequestsView->selectionModel()->selectedRows(); if (selection.empty()) return; // correct for selection mode ContiguousSelection QModelIndex firstIndex = selection.at(0); model->getRecentRequestsTableModel()->removeRows( firstIndex.row(), selection.length(), firstIndex.parent()); } // We override the virtual resizeEvent of the QWidget to adjust tables column // sizes as the tables width is proportional to the dialogs width. void ReceiveCoinsDialog::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); columnResizingFixer->stretchColumnWidth(RecentRequestsTableModel::Message); } void ReceiveCoinsDialog::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Return) { // press return -> submit form if (ui->reqLabel->hasFocus() || ui->reqAmount->hasFocus() || ui->reqMessage->hasFocus()) { event->ignore(); on_receiveButton_clicked(); return; } } this->QDialog::keyPressEvent(event); } QModelIndex ReceiveCoinsDialog::selectedRow() { if (!model || !model->getRecentRequestsTableModel() || !ui->recentRequestsView->selectionModel()) return QModelIndex(); QModelIndexList selection = ui->recentRequestsView->selectionModel()->selectedRows(); if (selection.empty()) return QModelIndex(); // correct for selection mode ContiguousSelection QModelIndex firstIndex = selection.at(0); return firstIndex; } // copy column of selected row to clipboard void ReceiveCoinsDialog::copyColumnToClipboard(int column) { QModelIndex firstIndex = selectedRow(); if (!firstIndex.isValid()) { return; } GUIUtil::setClipboard( model->getRecentRequestsTableModel() ->data(firstIndex.child(firstIndex.row(), column), Qt::EditRole) .toString()); } // context menu void ReceiveCoinsDialog::showMenu(const QPoint &point) { if (!selectedRow().isValid()) { return; } contextMenu->exec(QCursor::pos()); } // context menu action: copy URI void ReceiveCoinsDialog::copyURI() { QModelIndex sel = selectedRow(); if (!sel.isValid()) { return; } const RecentRequestsTableModel *const submodel = model->getRecentRequestsTableModel(); const QString uri = GUIUtil::formatBitcoinURI(*cfg, submodel->entry(sel.row()).recipient); GUIUtil::setClipboard(uri); } // context menu action: copy label void ReceiveCoinsDialog::copyLabel() { copyColumnToClipboard(RecentRequestsTableModel::Label); } // context menu action: copy message void ReceiveCoinsDialog::copyMessage() { copyColumnToClipboard(RecentRequestsTableModel::Message); } // context menu action: copy amount void ReceiveCoinsDialog::copyAmount() { copyColumnToClipboard(RecentRequestsTableModel::Amount); } diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 27b942b2c4..977dc3cc55 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -1,971 +1,967 @@ // 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 "sendcoinsdialog.h" #include "ui_sendcoinsdialog.h" #include "addresstablemodel.h" #include "bitcoinunits.h" #include "clientmodel.h" #include "coincontroldialog.h" #include "guiutil.h" #include "optionsmodel.h" #include "platformstyle.h" #include "sendcoinsentry.h" #include "walletmodel.h" #include "chainparams.h" #include "dstencode.h" #include "txmempool.h" #include "ui_interface.h" #include "validation.h" // mempool and minRelayTxFee #include "wallet/coincontrol.h" #include "wallet/wallet.h" #include #include #include #include #include #define SEND_CONFIRM_DELAY 3 SendCoinsDialog::SendCoinsDialog(const PlatformStyle *_platformStyle, QWidget *parent) : QDialog(parent), ui(new Ui::SendCoinsDialog), clientModel(0), model(0), fNewRecipientAllowed(true), fFeeMinimized(true), platformStyle(_platformStyle) { ui->setupUi(this); if (!_platformStyle->getImagesOnButtons()) { ui->addButton->setIcon(QIcon()); ui->clearButton->setIcon(QIcon()); ui->sendButton->setIcon(QIcon()); } else { ui->addButton->setIcon(_platformStyle->SingleColorIcon(":/icons/add")); ui->clearButton->setIcon( _platformStyle->SingleColorIcon(":/icons/remove")); ui->sendButton->setIcon( _platformStyle->SingleColorIcon(":/icons/send")); } GUIUtil::setupAddressWidget(ui->lineEditCoinControlChange, this); addEntry(); connect(ui->addButton, SIGNAL(clicked()), this, SLOT(addEntry())); connect(ui->clearButton, SIGNAL(clicked()), this, SLOT(clear())); // Coin Control connect(ui->pushButtonCoinControl, SIGNAL(clicked()), this, SLOT(coinControlButtonClicked())); connect(ui->checkBoxCoinControlChange, SIGNAL(stateChanged(int)), this, SLOT(coinControlChangeChecked(int))); connect(ui->lineEditCoinControlChange, SIGNAL(textEdited(const QString &)), this, SLOT(coinControlChangeEdited(const QString &))); // Coin Control: clipboard actions QAction *clipboardQuantityAction = new QAction(tr("Copy quantity"), this); QAction *clipboardAmountAction = new QAction(tr("Copy amount"), this); QAction *clipboardFeeAction = new QAction(tr("Copy fee"), this); QAction *clipboardAfterFeeAction = new QAction(tr("Copy after fee"), this); QAction *clipboardBytesAction = new QAction(tr("Copy bytes"), this); QAction *clipboardLowOutputAction = new QAction(tr("Copy dust"), this); QAction *clipboardChangeAction = new QAction(tr("Copy change"), this); connect(clipboardQuantityAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardQuantity())); connect(clipboardAmountAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardAmount())); connect(clipboardFeeAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardFee())); connect(clipboardAfterFeeAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardAfterFee())); connect(clipboardBytesAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardBytes())); connect(clipboardLowOutputAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardLowOutput())); connect(clipboardChangeAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardChange())); ui->labelCoinControlQuantity->addAction(clipboardQuantityAction); ui->labelCoinControlAmount->addAction(clipboardAmountAction); ui->labelCoinControlFee->addAction(clipboardFeeAction); ui->labelCoinControlAfterFee->addAction(clipboardAfterFeeAction); ui->labelCoinControlBytes->addAction(clipboardBytesAction); ui->labelCoinControlLowOutput->addAction(clipboardLowOutputAction); ui->labelCoinControlChange->addAction(clipboardChangeAction); // init transaction fee section QSettings settings; if (!settings.contains("fFeeSectionMinimized")) settings.setValue("fFeeSectionMinimized", true); // compatibility if (!settings.contains("nFeeRadio") && settings.contains("nTransactionFee") && settings.value("nTransactionFee").toLongLong() > 0) { // custom settings.setValue("nFeeRadio", 1); } if (!settings.contains("nFeeRadio")) { // recommended settings.setValue("nFeeRadio", 0); } // compatibility if (!settings.contains("nCustomFeeRadio") && settings.contains("nTransactionFee") && settings.value("nTransactionFee").toLongLong() > 0) { // total at least settings.setValue("nCustomFeeRadio", 1); } if (!settings.contains("nCustomFeeRadio")) { // per kilobyte settings.setValue("nCustomFeeRadio", 0); } if (!settings.contains("nSmartFeeSliderPosition")) settings.setValue("nSmartFeeSliderPosition", 0); if (!settings.contains("nTransactionFee")) settings.setValue("nTransactionFee", (qint64)DEFAULT_TRANSACTION_FEE.GetSatoshis()); if (!settings.contains("fPayOnlyMinFee")) settings.setValue("fPayOnlyMinFee", false); ui->groupFee->setId(ui->radioSmartFee, 0); ui->groupFee->setId(ui->radioCustomFee, 1); ui->groupFee ->button( (int)std::max(0, std::min(1, settings.value("nFeeRadio").toInt()))) ->setChecked(true); ui->groupCustomFee->setId(ui->radioCustomPerKilobyte, 0); ui->groupCustomFee->setId(ui->radioCustomAtLeast, 1); ui->groupCustomFee ->button((int)std::max( 0, std::min(1, settings.value("nCustomFeeRadio").toInt()))) ->setChecked(true); ui->customFee->setValue(settings.value("nTransactionFee").toLongLong()); ui->checkBoxMinimumFee->setChecked( settings.value("fPayOnlyMinFee").toBool()); minimizeFeeSection(settings.value("fFeeSectionMinimized").toBool()); } void SendCoinsDialog::setClientModel(ClientModel *_clientModel) { this->clientModel = _clientModel; if (_clientModel) { connect(_clientModel, SIGNAL(numBlocksChanged(int, QDateTime, double, bool)), this, SLOT(updateSmartFeeLabel())); } } void SendCoinsDialog::setModel(WalletModel *_model) { this->model = _model; if (_model && _model->getOptionsModel()) { for (int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast( ui->entries->itemAt(i)->widget()); if (entry) { entry->setModel(_model); } } setBalance(_model->getBalance(), _model->getUnconfirmedBalance(), _model->getImmatureBalance(), _model->getWatchBalance(), _model->getWatchUnconfirmedBalance(), _model->getWatchImmatureBalance()); connect(_model, SIGNAL(balanceChanged(Amount, Amount, Amount, Amount, Amount, Amount)), this, SLOT(setBalance(Amount, Amount, Amount, Amount, Amount, Amount))); connect(_model->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(updateDisplayUnit())); updateDisplayUnit(); // Coin Control connect(_model->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(coinControlUpdateLabels())); connect(_model->getOptionsModel(), SIGNAL(coinControlFeaturesChanged(bool)), this, SLOT(coinControlFeatureChanged(bool))); ui->frameCoinControl->setVisible( _model->getOptionsModel()->getCoinControlFeatures()); coinControlUpdateLabels(); // fee section connect(ui->sliderSmartFee, SIGNAL(valueChanged(int)), this, SLOT(updateSmartFeeLabel())); connect(ui->sliderSmartFee, SIGNAL(valueChanged(int)), this, SLOT(updateGlobalFeeVariables())); connect(ui->sliderSmartFee, SIGNAL(valueChanged(int)), this, SLOT(coinControlUpdateLabels())); connect(ui->groupFee, SIGNAL(buttonClicked(int)), this, SLOT(updateFeeSectionControls())); connect(ui->groupFee, SIGNAL(buttonClicked(int)), this, SLOT(updateGlobalFeeVariables())); connect(ui->groupFee, SIGNAL(buttonClicked(int)), this, SLOT(coinControlUpdateLabels())); connect(ui->groupCustomFee, SIGNAL(buttonClicked(int)), this, SLOT(updateGlobalFeeVariables())); connect(ui->groupCustomFee, SIGNAL(buttonClicked(int)), this, SLOT(coinControlUpdateLabels())); connect(ui->customFee, SIGNAL(valueChanged()), this, SLOT(updateGlobalFeeVariables())); connect(ui->customFee, SIGNAL(valueChanged()), this, SLOT(coinControlUpdateLabels())); connect(ui->checkBoxMinimumFee, SIGNAL(stateChanged(int)), this, SLOT(setMinimumFee())); connect(ui->checkBoxMinimumFee, SIGNAL(stateChanged(int)), this, SLOT(updateFeeSectionControls())); connect(ui->checkBoxMinimumFee, SIGNAL(stateChanged(int)), this, SLOT(updateGlobalFeeVariables())); connect(ui->checkBoxMinimumFee, SIGNAL(stateChanged(int)), this, SLOT(coinControlUpdateLabels())); - ui->customFee->setSingleStep( - CWallet::GetRequiredFee(1000).GetSatoshis()); + ui->customFee->setSingleStep(CWallet::GetRequiredFee(1000)); updateFeeSectionControls(); updateMinFeeLabel(); updateSmartFeeLabel(); updateGlobalFeeVariables(); // set the smartfee-sliders default value (wallets default conf.target // or last stored value) QSettings settings; if (settings.value("nSmartFeeSliderPosition").toInt() == 0) ui->sliderSmartFee->setValue(ui->sliderSmartFee->maximum() - model->getDefaultConfirmTarget() + 2); else ui->sliderSmartFee->setValue( settings.value("nSmartFeeSliderPosition").toInt()); } } SendCoinsDialog::~SendCoinsDialog() { QSettings settings; settings.setValue("fFeeSectionMinimized", fFeeMinimized); settings.setValue("nFeeRadio", ui->groupFee->checkedId()); settings.setValue("nCustomFeeRadio", ui->groupCustomFee->checkedId()); settings.setValue("nSmartFeeSliderPosition", ui->sliderSmartFee->value()); settings.setValue("nTransactionFee", (qint64)ui->customFee->value().GetSatoshis()); settings.setValue("fPayOnlyMinFee", ui->checkBoxMinimumFee->isChecked()); delete ui; } void SendCoinsDialog::on_sendButton_clicked() { if (!model || !model->getOptionsModel()) return; QList recipients; bool valid = true; for (int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); if (entry) { if (entry->validate()) { recipients.append(entry->getValue()); } else { valid = false; } } } if (!valid || recipients.isEmpty()) { return; } fNewRecipientAllowed = false; WalletModel::UnlockContext ctx(model->requestUnlock()); if (!ctx.isValid()) { // Unlock wallet was cancelled fNewRecipientAllowed = true; return; } // prepare transaction for getting txFee earlier WalletModelTransaction currentTransaction(recipients); WalletModel::SendCoinsReturn prepareStatus; // Always use a CCoinControl instance, use the CoinControlDialog instance if // CoinControl has been enabled CCoinControl ctrl; if (model->getOptionsModel()->getCoinControlFeatures()) ctrl = *CoinControlDialog::coinControl; if (ui->radioSmartFee->isChecked()) ctrl.nConfirmTarget = ui->sliderSmartFee->maximum() - ui->sliderSmartFee->value() + 2; else ctrl.nConfirmTarget = 0; prepareStatus = model->prepareTransaction(currentTransaction, &ctrl); // process prepareStatus and on error generate message shown to user processSendCoinsReturn( prepareStatus, BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), currentTransaction.getTransactionFee())); if (prepareStatus.status != WalletModel::OK) { fNewRecipientAllowed = true; return; } Amount txFee = currentTransaction.getTransactionFee(); // Format confirmation message QStringList formatted; for (const SendCoinsRecipient &rcp : currentTransaction.getRecipients()) { // generate bold amount string QString amount = "" + BitcoinUnits::formatHtmlWithUnit( model->getOptionsModel()->getDisplayUnit(), rcp.amount); amount.append(""); // generate monospace address string QString address = "" + rcp.address; address.append(""); QString recipientElement; // normal payment if (!rcp.paymentRequest.IsInitialized()) { if (rcp.label.length() > 0) { // label with address recipientElement = tr("%1 to %2").arg(amount, GUIUtil::HtmlEscape(rcp.label)); recipientElement.append(QString(" (%1)").arg(address)); } else { // just address recipientElement = tr("%1 to %2").arg(amount, address); } } else if (!rcp.authenticatedMerchant.isEmpty()) { // authenticated payment request recipientElement = tr("%1 to %2") .arg(amount, GUIUtil::HtmlEscape(rcp.authenticatedMerchant)); } else { // unauthenticated payment request recipientElement = tr("%1 to %2").arg(amount, address); } formatted.append(recipientElement); } QString questionString = tr("Are you sure you want to send?"); questionString.append("

%1"); if (txFee > 0) { // append fee string if a fee is required questionString.append("
"); questionString.append(BitcoinUnits::formatHtmlWithUnit( model->getOptionsModel()->getDisplayUnit(), txFee)); questionString.append(" "); questionString.append(tr("added as transaction fee")); // append transaction size questionString.append( " (" + QString::number( (double)currentTransaction.getTransactionSize() / 1000) + " kB)"); } // add total amount in all subdivision units questionString.append("
"); Amount totalAmount = currentTransaction.getTotalTransactionAmount() + txFee; QStringList alternativeUnits; for (BitcoinUnits::Unit u : BitcoinUnits::availableUnits()) { if (u != model->getOptionsModel()->getDisplayUnit()) alternativeUnits.append( BitcoinUnits::formatHtmlWithUnit(u, totalAmount)); } questionString.append( tr("Total Amount %1") .arg(BitcoinUnits::formatHtmlWithUnit( model->getOptionsModel()->getDisplayUnit(), totalAmount))); questionString.append( QString("
(=%2)
") .arg(alternativeUnits.join(" " + tr("or") + "
"))); SendConfirmationDialog confirmationDialog( tr("Confirm send coins"), questionString.arg(formatted.join("
")), SEND_CONFIRM_DELAY, this); confirmationDialog.exec(); QMessageBox::StandardButton retval = (QMessageBox::StandardButton)confirmationDialog.result(); if (retval != QMessageBox::Yes) { fNewRecipientAllowed = true; return; } // now send the prepared transaction WalletModel::SendCoinsReturn sendStatus = model->sendCoins(currentTransaction); // process sendStatus and on error generate message shown to user processSendCoinsReturn(sendStatus); if (sendStatus.status == WalletModel::OK) { accept(); CoinControlDialog::coinControl->UnSelectAll(); coinControlUpdateLabels(); } fNewRecipientAllowed = true; } void SendCoinsDialog::clear() { // Remove entries until only one left while (ui->entries->count()) { ui->entries->takeAt(0)->widget()->deleteLater(); } addEntry(); updateTabsAndLabels(); } void SendCoinsDialog::reject() { clear(); } void SendCoinsDialog::accept() { clear(); } SendCoinsEntry *SendCoinsDialog::addEntry() { SendCoinsEntry *entry = new SendCoinsEntry(platformStyle, this); entry->setModel(model); ui->entries->addWidget(entry); connect(entry, SIGNAL(removeEntry(SendCoinsEntry *)), this, SLOT(removeEntry(SendCoinsEntry *))); connect(entry, SIGNAL(payAmountChanged()), this, SLOT(coinControlUpdateLabels())); connect(entry, SIGNAL(subtractFeeFromAmountChanged()), this, SLOT(coinControlUpdateLabels())); // Focus the field, so that entry can start immediately entry->clear(); entry->setFocus(); ui->scrollAreaWidgetContents->resize( ui->scrollAreaWidgetContents->sizeHint()); qApp->processEvents(); QScrollBar *bar = ui->scrollArea->verticalScrollBar(); if (bar) bar->setSliderPosition(bar->maximum()); updateTabsAndLabels(); return entry; } void SendCoinsDialog::updateTabsAndLabels() { setupTabChain(0); coinControlUpdateLabels(); } void SendCoinsDialog::removeEntry(SendCoinsEntry *entry) { entry->hide(); // If the last entry is about to be removed add an empty one if (ui->entries->count() == 1) addEntry(); entry->deleteLater(); updateTabsAndLabels(); } QWidget *SendCoinsDialog::setupTabChain(QWidget *prev) { for (int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); if (entry) { prev = entry->setupTabChain(prev); } } QWidget::setTabOrder(prev, ui->sendButton); QWidget::setTabOrder(ui->sendButton, ui->clearButton); QWidget::setTabOrder(ui->clearButton, ui->addButton); return ui->addButton; } void SendCoinsDialog::setAddress(const QString &address) { SendCoinsEntry *entry = 0; // Replace the first entry if it is still unused if (ui->entries->count() == 1) { SendCoinsEntry *first = qobject_cast(ui->entries->itemAt(0)->widget()); if (first->isClear()) { entry = first; } } if (!entry) { entry = addEntry(); } entry->setAddress(address); } void SendCoinsDialog::pasteEntry(const SendCoinsRecipient &rv) { if (!fNewRecipientAllowed) return; SendCoinsEntry *entry = 0; // Replace the first entry if it is still unused if (ui->entries->count() == 1) { SendCoinsEntry *first = qobject_cast(ui->entries->itemAt(0)->widget()); if (first->isClear()) { entry = first; } } if (!entry) { entry = addEntry(); } entry->setValue(rv); updateTabsAndLabels(); } bool SendCoinsDialog::handlePaymentRequest(const SendCoinsRecipient &rv) { // Just paste the entry, all pre-checks are done in paymentserver.cpp. pasteEntry(rv); return true; } void SendCoinsDialog::setBalance(const Amount balance, const Amount unconfirmedBalance, const Amount immatureBalance, const Amount watchBalance, const Amount watchUnconfirmedBalance, const Amount watchImmatureBalance) { Q_UNUSED(unconfirmedBalance); Q_UNUSED(immatureBalance); Q_UNUSED(watchBalance); Q_UNUSED(watchUnconfirmedBalance); Q_UNUSED(watchImmatureBalance); if (model && model->getOptionsModel()) { ui->labelBalance->setText(BitcoinUnits::formatWithUnit( model->getOptionsModel()->getDisplayUnit(), balance)); } } void SendCoinsDialog::updateDisplayUnit() { setBalance(model->getBalance(), 0, 0, 0, 0, 0); ui->customFee->setDisplayUnit(model->getOptionsModel()->getDisplayUnit()); updateMinFeeLabel(); updateSmartFeeLabel(); } void SendCoinsDialog::processSendCoinsReturn( const WalletModel::SendCoinsReturn &sendCoinsReturn, const QString &msgArg) { QPair msgParams; // Default to a warning message, override if error message is needed msgParams.second = CClientUIInterface::MSG_WARNING; // This comment is specific to SendCoinsDialog usage of // WalletModel::SendCoinsReturn. // WalletModel::TransactionCommitFailed is used only in // WalletModel::sendCoins() all others are used only in // WalletModel::prepareTransaction() switch (sendCoinsReturn.status) { case WalletModel::InvalidAddress: msgParams.first = tr("The recipient address is not valid. Please recheck."); break; case WalletModel::InvalidAmount: msgParams.first = tr("The amount to pay must be larger than 0."); break; case WalletModel::AmountExceedsBalance: msgParams.first = tr("The amount exceeds your balance."); break; case WalletModel::AmountWithFeeExceedsBalance: msgParams.first = tr("The total exceeds your balance when the %1 " "transaction fee is included.") .arg(msgArg); break; case WalletModel::DuplicateAddress: msgParams.first = tr("Duplicate address found: addresses should " "only be used once each."); break; case WalletModel::TransactionCreationFailed: msgParams.first = tr("Transaction creation failed!"); msgParams.second = CClientUIInterface::MSG_ERROR; break; case WalletModel::TransactionCommitFailed: msgParams.first = tr("The transaction was rejected with the following reason: %1") .arg(sendCoinsReturn.reasonCommitFailed); msgParams.second = CClientUIInterface::MSG_ERROR; break; case WalletModel::AbsurdFee: msgParams.first = tr("A fee higher than %1 is considered an absurdly high fee.") .arg(BitcoinUnits::formatWithUnit( - model->getOptionsModel()->getDisplayUnit(), - maxTxFee.GetSatoshis())); + model->getOptionsModel()->getDisplayUnit(), maxTxFee)); break; case WalletModel::PaymentRequestExpired: msgParams.first = tr("Payment request expired."); msgParams.second = CClientUIInterface::MSG_ERROR; break; // included to prevent a compiler warning. case WalletModel::OK: default: return; } Q_EMIT message(tr("Send Coins"), msgParams.first, msgParams.second); } void SendCoinsDialog::minimizeFeeSection(bool fMinimize) { ui->labelFeeMinimized->setVisible(fMinimize); ui->buttonChooseFee->setVisible(fMinimize); ui->buttonMinimizeFee->setVisible(!fMinimize); ui->frameFeeSelection->setVisible(!fMinimize); ui->horizontalLayoutSmartFee->setContentsMargins(0, (fMinimize ? 0 : 6), 0, 0); fFeeMinimized = fMinimize; } void SendCoinsDialog::on_buttonChooseFee_clicked() { minimizeFeeSection(false); } void SendCoinsDialog::on_buttonMinimizeFee_clicked() { updateFeeMinimizedLabel(); minimizeFeeSection(true); } void SendCoinsDialog::setMinimumFee() { ui->radioCustomPerKilobyte->setChecked(true); - ui->customFee->setValue(CWallet::GetRequiredFee(1000).GetSatoshis()); + ui->customFee->setValue(CWallet::GetRequiredFee(1000)); } void SendCoinsDialog::updateFeeSectionControls() { ui->sliderSmartFee->setEnabled(ui->radioSmartFee->isChecked()); ui->labelSmartFee->setEnabled(ui->radioSmartFee->isChecked()); ui->labelSmartFee2->setEnabled(ui->radioSmartFee->isChecked()); ui->labelSmartFee3->setEnabled(ui->radioSmartFee->isChecked()); ui->labelFeeEstimation->setEnabled(ui->radioSmartFee->isChecked()); ui->labelSmartFeeNormal->setEnabled(ui->radioSmartFee->isChecked()); ui->labelSmartFeeFast->setEnabled(ui->radioSmartFee->isChecked()); ui->confirmationTargetLabel->setEnabled(ui->radioSmartFee->isChecked()); ui->checkBoxMinimumFee->setEnabled(ui->radioCustomFee->isChecked()); ui->labelMinFeeWarning->setEnabled(ui->radioCustomFee->isChecked()); ui->radioCustomPerKilobyte->setEnabled( ui->radioCustomFee->isChecked() && !ui->checkBoxMinimumFee->isChecked()); ui->radioCustomAtLeast->setEnabled( ui->radioCustomFee->isChecked() && !ui->checkBoxMinimumFee->isChecked() && CoinControlDialog::coinControl->HasSelected()); ui->customFee->setEnabled(ui->radioCustomFee->isChecked() && !ui->checkBoxMinimumFee->isChecked()); } void SendCoinsDialog::updateGlobalFeeVariables() { if (ui->radioSmartFee->isChecked()) { int nConfirmTarget = ui->sliderSmartFee->maximum() - ui->sliderSmartFee->value() + 2; payTxFee = CFeeRate(Amount(0)); // set nMinimumTotalFee to 0 to not accidentally pay a custom fee CoinControlDialog::coinControl->nMinimumTotalFee = Amount(0); // show the estimated required time for confirmation ui->confirmationTargetLabel->setText( GUIUtil::formatDurationStr( nConfirmTarget * Params().GetConsensus().nPowTargetSpacing) + " / " + tr("%n block(s)", "", nConfirmTarget)); } else { payTxFee = CFeeRate(Amount(ui->customFee->value())); // if user has selected to set a minimum absolute fee, pass the value to // coincontrol // set nMinimumTotalFee to 0 in case of user has selected that the fee // is per KB CoinControlDialog::coinControl->nMinimumTotalFee = Amount( ui->radioCustomAtLeast->isChecked() ? ui->customFee->value() : 0); } } void SendCoinsDialog::updateFeeMinimizedLabel() { if (!model || !model->getOptionsModel()) return; if (ui->radioSmartFee->isChecked()) ui->labelFeeMinimized->setText(ui->labelSmartFee->text()); else { ui->labelFeeMinimized->setText( BitcoinUnits::formatWithUnit( model->getOptionsModel()->getDisplayUnit(), ui->customFee->value()) + ((ui->radioCustomPerKilobyte->isChecked()) ? "/kB" : "")); } } void SendCoinsDialog::updateMinFeeLabel() { if (model && model->getOptionsModel()) ui->checkBoxMinimumFee->setText( tr("Pay only the required fee of %1") .arg(BitcoinUnits::formatWithUnit( model->getOptionsModel()->getDisplayUnit(), - CWallet::GetRequiredFee(1000).GetSatoshis()) + + CWallet::GetRequiredFee(1000)) + "/kB")); } void SendCoinsDialog::updateSmartFeeLabel() { if (!model || !model->getOptionsModel()) return; int nBlocksToConfirm = ui->sliderSmartFee->maximum() - ui->sliderSmartFee->value() + 2; int estimateFoundAtBlocks = nBlocksToConfirm; CFeeRate feeRate = mempool.estimateSmartFee(nBlocksToConfirm, &estimateFoundAtBlocks); // not enough data => minfee if (feeRate <= CFeeRate(Amount(0))) { ui->labelSmartFee->setText( BitcoinUnits::formatWithUnit( model->getOptionsModel()->getDisplayUnit(), std::max(CWallet::fallbackFee.GetFeePerK(), - CWallet::GetRequiredFee(1000)) - .GetSatoshis()) + + CWallet::GetRequiredFee(1000))) + "/kB"); // (Smart fee not initialized yet. This usually takes a few blocks...) ui->labelSmartFee2->show(); ui->labelFeeEstimation->setText(""); } else { ui->labelSmartFee->setText( BitcoinUnits::formatWithUnit( model->getOptionsModel()->getDisplayUnit(), - std::max(feeRate.GetFeePerK(), CWallet::GetRequiredFee(1000)) - .GetSatoshis()) + + std::max(feeRate.GetFeePerK(), CWallet::GetRequiredFee(1000))) + "/kB"); ui->labelSmartFee2->hide(); ui->labelFeeEstimation->setText( tr("Estimated to begin confirmation within %n block(s).", "", estimateFoundAtBlocks)); } updateFeeMinimizedLabel(); } // Coin Control: copy label "Quantity" to clipboard void SendCoinsDialog::coinControlClipboardQuantity() { GUIUtil::setClipboard(ui->labelCoinControlQuantity->text()); } // Coin Control: copy label "Amount" to clipboard void SendCoinsDialog::coinControlClipboardAmount() { GUIUtil::setClipboard(ui->labelCoinControlAmount->text().left( ui->labelCoinControlAmount->text().indexOf(" "))); } // Coin Control: copy label "Fee" to clipboard void SendCoinsDialog::coinControlClipboardFee() { GUIUtil::setClipboard( ui->labelCoinControlFee->text() .left(ui->labelCoinControlFee->text().indexOf(" ")) .replace(ASYMP_UTF8, "")); } // Coin Control: copy label "After fee" to clipboard void SendCoinsDialog::coinControlClipboardAfterFee() { GUIUtil::setClipboard( ui->labelCoinControlAfterFee->text() .left(ui->labelCoinControlAfterFee->text().indexOf(" ")) .replace(ASYMP_UTF8, "")); } // Coin Control: copy label "Bytes" to clipboard void SendCoinsDialog::coinControlClipboardBytes() { GUIUtil::setClipboard( ui->labelCoinControlBytes->text().replace(ASYMP_UTF8, "")); } // Coin Control: copy label "Dust" to clipboard void SendCoinsDialog::coinControlClipboardLowOutput() { GUIUtil::setClipboard(ui->labelCoinControlLowOutput->text()); } // Coin Control: copy label "Change" to clipboard void SendCoinsDialog::coinControlClipboardChange() { GUIUtil::setClipboard( ui->labelCoinControlChange->text() .left(ui->labelCoinControlChange->text().indexOf(" ")) .replace(ASYMP_UTF8, "")); } // Coin Control: settings menu - coin control enabled/disabled by user void SendCoinsDialog::coinControlFeatureChanged(bool checked) { ui->frameCoinControl->setVisible(checked); // coin control features disabled if (!checked && model) CoinControlDialog::coinControl->SetNull(); // make sure we set back the confirmation target updateGlobalFeeVariables(); coinControlUpdateLabels(); } // Coin Control: button inputs -> show actual coin control dialog void SendCoinsDialog::coinControlButtonClicked() { CoinControlDialog dlg(platformStyle); dlg.setModel(model); dlg.exec(); coinControlUpdateLabels(); } // Coin Control: checkbox custom change address void SendCoinsDialog::coinControlChangeChecked(int state) { if (state == Qt::Unchecked) { CoinControlDialog::coinControl->destChange = CNoDestination(); ui->labelCoinControlChangeLabel->clear(); } else { // use this to re-validate an already entered address coinControlChangeEdited(ui->lineEditCoinControlChange->text()); } ui->lineEditCoinControlChange->setEnabled((state == Qt::Checked)); } // Coin Control: custom change address changed void SendCoinsDialog::coinControlChangeEdited(const QString &text) { if (model && model->getAddressTableModel()) { // Default to no change address until verified CoinControlDialog::coinControl->destChange = CNoDestination(); ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:red;}"); const CTxDestination dest = DecodeDestination(text.toStdString()); if (text.isEmpty()) { // Nothing entered ui->labelCoinControlChangeLabel->setText(""); } else if (!IsValidDestination(dest)) { // Invalid address ui->labelCoinControlChangeLabel->setText( tr("Warning: Invalid Bitcoin address")); } else { // Valid address if (!model->IsSpendable(dest)) { ui->labelCoinControlChangeLabel->setText( tr("Warning: Unknown change address")); // confirmation dialog QMessageBox::StandardButton btnRetVal = QMessageBox::question( this, tr("Confirm custom change address"), tr("The address you selected for change is not part of " "this wallet. Any or all funds in your wallet may be " "sent to this address. Are you sure?"), QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel); if (btnRetVal == QMessageBox::Yes) CoinControlDialog::coinControl->destChange = dest; else { ui->lineEditCoinControlChange->setText(""); ui->labelCoinControlChangeLabel->setStyleSheet( "QLabel{color:black;}"); ui->labelCoinControlChangeLabel->setText(""); } } else { // Known change address ui->labelCoinControlChangeLabel->setStyleSheet( "QLabel{color:black;}"); // Query label QString associatedLabel = model->getAddressTableModel()->labelForAddress(text); if (!associatedLabel.isEmpty()) ui->labelCoinControlChangeLabel->setText(associatedLabel); else ui->labelCoinControlChangeLabel->setText(tr("(no label)")); CoinControlDialog::coinControl->destChange = dest; } } } } // Coin Control: update labels void SendCoinsDialog::coinControlUpdateLabels() { if (!model || !model->getOptionsModel()) return; if (model->getOptionsModel()->getCoinControlFeatures()) { // enable minimum absolute fee UI controls ui->radioCustomAtLeast->setVisible(true); // only enable the feature if inputs are selected ui->radioCustomAtLeast->setEnabled( ui->radioCustomFee->isChecked() && !ui->checkBoxMinimumFee->isChecked() && CoinControlDialog::coinControl->HasSelected()); } else { // in case coin control is disabled (=default), hide minimum absolute // fee UI controls ui->radioCustomAtLeast->setVisible(false); return; } // set pay amounts CoinControlDialog::payAmounts.clear(); CoinControlDialog::fSubtractFeeFromAmount = false; for (int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); if (entry && !entry->isHidden()) { SendCoinsRecipient rcp = entry->getValue(); CoinControlDialog::payAmounts.append(rcp.amount.GetSatoshis()); if (rcp.fSubtractFeeFromAmount) CoinControlDialog::fSubtractFeeFromAmount = true; } } if (CoinControlDialog::coinControl->HasSelected()) { // actual coin control calculation CoinControlDialog::updateLabels(model, this); // show coin control stats ui->labelCoinControlAutomaticallySelected->hide(); ui->widgetCoinControl->show(); } else { // hide coin control stats ui->labelCoinControlAutomaticallySelected->show(); ui->widgetCoinControl->hide(); ui->labelCoinControlInsuffFunds->hide(); } } SendConfirmationDialog::SendConfirmationDialog(const QString &title, const QString &text, int _secDelay, QWidget *parent) : QMessageBox(QMessageBox::Question, title, text, QMessageBox::Yes | QMessageBox::Cancel, parent), secDelay(_secDelay) { setDefaultButton(QMessageBox::Cancel); yesButton = button(QMessageBox::Yes); updateYesButton(); connect(&countDownTimer, SIGNAL(timeout()), this, SLOT(countDown())); } int SendConfirmationDialog::exec() { updateYesButton(); countDownTimer.start(1000); return QMessageBox::exec(); } void SendConfirmationDialog::countDown() { secDelay--; updateYesButton(); if (secDelay <= 0) { countDownTimer.stop(); } } void SendConfirmationDialog::updateYesButton() { if (secDelay > 0) { yesButton->setEnabled(false); yesButton->setText(tr("Yes") + " (" + QString::number(secDelay) + ")"); } else { yesButton->setEnabled(true); yesButton->setText(tr("Yes")); } } diff --git a/src/qt/sendcoinsentry.cpp b/src/qt/sendcoinsentry.cpp index 615106c949..cb3549af4b 100644 --- a/src/qt/sendcoinsentry.cpp +++ b/src/qt/sendcoinsentry.cpp @@ -1,260 +1,260 @@ // 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 "sendcoinsentry.h" #include "ui_sendcoinsentry.h" #include "addressbookpage.h" #include "addresstablemodel.h" #include "guiutil.h" #include "optionsmodel.h" #include "platformstyle.h" #include "walletmodel.h" #include #include SendCoinsEntry::SendCoinsEntry(const PlatformStyle *_platformStyle, QWidget *parent) : QStackedWidget(parent), ui(new Ui::SendCoinsEntry), model(0), platformStyle(_platformStyle) { ui->setupUi(this); ui->addressBookButton->setIcon( platformStyle->SingleColorIcon(":/icons/address-book")); ui->pasteButton->setIcon( platformStyle->SingleColorIcon(":/icons/editpaste")); ui->deleteButton->setIcon(platformStyle->SingleColorIcon(":/icons/remove")); ui->deleteButton_is->setIcon( platformStyle->SingleColorIcon(":/icons/remove")); ui->deleteButton_s->setIcon( platformStyle->SingleColorIcon(":/icons/remove")); ui->messageTextLabel->setToolTip( tr("A message that was attached to the %1 URI which will be" " stored with the transaction for your reference. Note: " "This message will not be sent over the Bitcoin network.") .arg(GUIUtil::URI_SCHEME)); setCurrentWidget(ui->SendCoins); if (platformStyle->getUseExtraSpacing()) ui->payToLayout->setSpacing(4); #if QT_VERSION >= 0x040700 ui->addAsLabel->setPlaceholderText( tr("Enter a label for this address to add it to your address book")); #endif // normal bitcoin address field GUIUtil::setupAddressWidget(ui->payTo, this); // just a label for displaying bitcoin address(es) ui->payTo_is->setFont(GUIUtil::fixedPitchFont()); // Connect signals connect(ui->payAmount, SIGNAL(valueChanged()), this, SIGNAL(payAmountChanged())); connect(ui->checkboxSubtractFeeFromAmount, SIGNAL(toggled(bool)), this, SIGNAL(subtractFeeFromAmountChanged())); connect(ui->deleteButton, SIGNAL(clicked()), this, SLOT(deleteClicked())); connect(ui->deleteButton_is, SIGNAL(clicked()), this, SLOT(deleteClicked())); connect(ui->deleteButton_s, SIGNAL(clicked()), this, SLOT(deleteClicked())); } SendCoinsEntry::~SendCoinsEntry() { delete ui; } void SendCoinsEntry::on_pasteButton_clicked() { // Paste text from clipboard into recipient field ui->payTo->setText(QApplication::clipboard()->text()); } void SendCoinsEntry::on_addressBookButton_clicked() { if (!model) return; AddressBookPage dlg(platformStyle, AddressBookPage::ForSelection, AddressBookPage::SendingTab, this); dlg.setModel(model->getAddressTableModel()); if (dlg.exec()) { ui->payTo->setText(dlg.getReturnValue()); ui->payAmount->setFocus(); } } void SendCoinsEntry::on_payTo_textChanged(const QString &address) { updateLabel(address); } void SendCoinsEntry::setModel(WalletModel *_model) { this->model = _model; if (_model && _model->getOptionsModel()) connect(_model->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(updateDisplayUnit())); clear(); } void SendCoinsEntry::clear() { // clear UI elements for normal payment ui->payTo->clear(); ui->addAsLabel->clear(); ui->payAmount->clear(); ui->checkboxSubtractFeeFromAmount->setCheckState(Qt::Unchecked); ui->messageTextLabel->clear(); ui->messageTextLabel->hide(); ui->messageLabel->hide(); // clear UI elements for unauthenticated payment request ui->payTo_is->clear(); ui->memoTextLabel_is->clear(); ui->payAmount_is->clear(); // clear UI elements for authenticated payment request ui->payTo_s->clear(); ui->memoTextLabel_s->clear(); ui->payAmount_s->clear(); // update the display unit, to not use the default ("BCH") updateDisplayUnit(); } void SendCoinsEntry::deleteClicked() { Q_EMIT removeEntry(this); } bool SendCoinsEntry::validate() { if (!model) return false; // Check input validity bool retval = true; // Skip checks for payment request if (recipient.paymentRequest.IsInitialized()) return retval; if (!model->validateAddress(ui->payTo->text())) { ui->payTo->setValid(false); retval = false; } if (!ui->payAmount->validate()) { retval = false; } // Sending a zero amount is invalid if (ui->payAmount->value(0) <= 0) { ui->payAmount->setValid(false); retval = false; } // Reject dust outputs: if (retval && GUIUtil::isDust(ui->payTo->text(), ui->payAmount->value())) { ui->payAmount->setValid(false); retval = false; } return retval; } SendCoinsRecipient SendCoinsEntry::getValue() { // Payment request if (recipient.paymentRequest.IsInitialized()) return recipient; // Normal payment recipient.address = ui->payTo->text(); recipient.label = ui->addAsLabel->text(); - recipient.amount = ui->payAmount->value().GetSatoshis(); + recipient.amount = ui->payAmount->value(); recipient.message = ui->messageTextLabel->text(); recipient.fSubtractFeeFromAmount = (ui->checkboxSubtractFeeFromAmount->checkState() == Qt::Checked); return recipient; } QWidget *SendCoinsEntry::setupTabChain(QWidget *prev) { QWidget::setTabOrder(prev, ui->payTo); QWidget::setTabOrder(ui->payTo, ui->addAsLabel); QWidget *w = ui->payAmount->setupTabChain(ui->addAsLabel); QWidget::setTabOrder(w, ui->checkboxSubtractFeeFromAmount); QWidget::setTabOrder(ui->checkboxSubtractFeeFromAmount, ui->addressBookButton); QWidget::setTabOrder(ui->addressBookButton, ui->pasteButton); QWidget::setTabOrder(ui->pasteButton, ui->deleteButton); return ui->deleteButton; } void SendCoinsEntry::setValue(const SendCoinsRecipient &value) { recipient = value; if (recipient.paymentRequest.IsInitialized()) // payment request { if (recipient.authenticatedMerchant.isEmpty()) // unauthenticated { ui->payTo_is->setText(recipient.address); ui->memoTextLabel_is->setText(recipient.message); ui->payAmount_is->setValue(recipient.amount); ui->payAmount_is->setReadOnly(true); setCurrentWidget(ui->SendCoins_UnauthenticatedPaymentRequest); } else // authenticated { ui->payTo_s->setText(recipient.authenticatedMerchant); ui->memoTextLabel_s->setText(recipient.message); ui->payAmount_s->setValue(recipient.amount); ui->payAmount_s->setReadOnly(true); setCurrentWidget(ui->SendCoins_AuthenticatedPaymentRequest); } } else // normal payment { // message ui->messageTextLabel->setText(recipient.message); ui->messageTextLabel->setVisible(!recipient.message.isEmpty()); ui->messageLabel->setVisible(!recipient.message.isEmpty()); ui->addAsLabel->clear(); ui->payTo->setText( recipient.address); // this may set a label from addressbook if (!recipient.label.isEmpty()) // if a label had been set from the // addressbook, don't overwrite with an // empty label ui->addAsLabel->setText(recipient.label); ui->payAmount->setValue(recipient.amount); } } void SendCoinsEntry::setAddress(const QString &address) { ui->payTo->setText(address); ui->payAmount->setFocus(); } bool SendCoinsEntry::isClear() { return ui->payTo->text().isEmpty() && ui->payTo_is->text().isEmpty() && ui->payTo_s->text().isEmpty(); } void SendCoinsEntry::setFocus() { ui->payTo->setFocus(); } void SendCoinsEntry::updateDisplayUnit() { if (model && model->getOptionsModel()) { // Update payAmount with the current unit ui->payAmount->setDisplayUnit( model->getOptionsModel()->getDisplayUnit()); ui->payAmount_is->setDisplayUnit( model->getOptionsModel()->getDisplayUnit()); ui->payAmount_s->setDisplayUnit( model->getOptionsModel()->getDisplayUnit()); } } bool SendCoinsEntry::updateLabel(const QString &address) { if (!model) return false; // Fill in label from address book, if address has an associated label QString associatedLabel = model->getAddressTableModel()->labelForAddress(address); if (!associatedLabel.isEmpty()) { ui->addAsLabel->setText(associatedLabel); return true; } return false; } diff --git a/src/qt/test/paymentservertests.cpp b/src/qt/test/paymentservertests.cpp index cd569ddcf8..3544913e54 100644 --- a/src/qt/test/paymentservertests.cpp +++ b/src/qt/test/paymentservertests.cpp @@ -1,219 +1,219 @@ // Copyright (c) 2009-2015 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 "paymentservertests.h" #include "optionsmodel.h" #include "paymentrequestdata.h" #include "amount.h" #include "random.h" #include "script/script.h" #include "script/standard.h" #include "util.h" #include "utilstrencodings.h" #include #include #include #include X509 *parse_b64der_cert(const char *cert_data) { std::vector data = DecodeBase64(cert_data); assert(data.size() > 0); const uint8_t *dptr = &data[0]; X509 *cert = d2i_X509(nullptr, &dptr, data.size()); assert(cert); return cert; } // // Test payment request handling // static SendCoinsRecipient handleRequest(PaymentServer *server, std::vector &data) { RecipientCatcher sigCatcher; QObject::connect(server, SIGNAL(receivedPaymentRequest(SendCoinsRecipient)), &sigCatcher, SLOT(getRecipient(SendCoinsRecipient))); // Write data to a temp file: QTemporaryFile f; f.open(); f.write((const char *)&data[0], data.size()); f.close(); // Create a QObject, install event filter from PaymentServer and send a file // open event to the object QObject object; object.installEventFilter(server); QFileOpenEvent event(f.fileName()); // If sending the event fails, this will cause sigCatcher to be empty, which // will lead to a test failure anyway. QCoreApplication::sendEvent(&object, &event); QObject::disconnect(server, SIGNAL(receivedPaymentRequest(SendCoinsRecipient)), &sigCatcher, SLOT(getRecipient(SendCoinsRecipient))); // Return results from sigCatcher return sigCatcher.recipient; } void PaymentServerTests::paymentServerTests() { SelectParams(CBaseChainParams::MAIN); OptionsModel optionsModel; PaymentServer *server = new PaymentServer(nullptr, false); X509_STORE *caStore = X509_STORE_new(); X509_STORE_add_cert(caStore, parse_b64der_cert(caCert1_BASE64)); PaymentServer::LoadRootCAs(caStore); server->setOptionsModel(&optionsModel); server->uiReady(); std::vector data; SendCoinsRecipient r; QString merchant; // Now feed PaymentRequests to server, and observe signals it produces // This payment request validates directly against the caCert1 certificate // authority: data = DecodeBase64(paymentrequest1_cert1_BASE64); r = handleRequest(server, data); r.paymentRequest.getMerchant(caStore, merchant); QCOMPARE(merchant, QString("testmerchant.org")); // Signed, but expired, merchant cert in the request: data = DecodeBase64(paymentrequest2_cert1_BASE64); r = handleRequest(server, data); r.paymentRequest.getMerchant(caStore, merchant); QCOMPARE(merchant, QString("")); // 10-long certificate chain, all intermediates valid: data = DecodeBase64(paymentrequest3_cert1_BASE64); r = handleRequest(server, data); r.paymentRequest.getMerchant(caStore, merchant); QCOMPARE(merchant, QString("testmerchant8.org")); // Long certificate chain, with an expired certificate in the middle: data = DecodeBase64(paymentrequest4_cert1_BASE64); r = handleRequest(server, data); r.paymentRequest.getMerchant(caStore, merchant); QCOMPARE(merchant, QString("")); // Validly signed, but by a CA not in our root CA list: data = DecodeBase64(paymentrequest5_cert1_BASE64); r = handleRequest(server, data); r.paymentRequest.getMerchant(caStore, merchant); QCOMPARE(merchant, QString("")); // Try again with no root CA's, verifiedMerchant should be empty: caStore = X509_STORE_new(); PaymentServer::LoadRootCAs(caStore); data = DecodeBase64(paymentrequest1_cert1_BASE64); r = handleRequest(server, data); r.paymentRequest.getMerchant(caStore, merchant); QCOMPARE(merchant, QString("")); // Load second root certificate caStore = X509_STORE_new(); X509_STORE_add_cert(caStore, parse_b64der_cert(caCert2_BASE64)); PaymentServer::LoadRootCAs(caStore); QByteArray byteArray; // For the tests below we just need the payment request data from // paymentrequestdata.h parsed + stored in r.paymentRequest. // // These tests require us to bypass the following normal client execution // flow shown below to be able to explicitly just trigger a certain // condition! // // handleRequest() // -> PaymentServer::eventFilter() // -> PaymentServer::handleURIOrFile() // -> PaymentServer::readPaymentRequestFromFile() // -> PaymentServer::processPaymentRequest() // Contains a testnet paytoaddress, so payment request network doesn't match // client network: data = DecodeBase64(paymentrequest1_cert2_BASE64); byteArray = QByteArray((const char *)&data[0], data.size()); r.paymentRequest.parse(byteArray); // Ensure the request is initialized, because network "main" is default, // even for uninizialized payment requests and that will fail our test here. QVERIFY(r.paymentRequest.IsInitialized()); QCOMPARE(PaymentServer::verifyNetwork(r.paymentRequest.getDetails()), false); // Expired payment request (expires is set to 1 = 1970-01-01 00:00:01): data = DecodeBase64(paymentrequest2_cert2_BASE64); byteArray = QByteArray((const char *)&data[0], data.size()); r.paymentRequest.parse(byteArray); // Ensure the request is initialized QVERIFY(r.paymentRequest.IsInitialized()); // compares 1 < GetTime() == false (treated as expired payment request) QCOMPARE(PaymentServer::verifyExpired(r.paymentRequest.getDetails()), true); // Unexpired payment request (expires is set to 0x7FFFFFFFFFFFFFFF = max. // int64_t): // 9223372036854775807 (uint64), 9223372036854775807 (int64_t) and -1 // (int32_t) // -1 is 1969-12-31 23:59:59 (for a 32 bit time values) data = DecodeBase64(paymentrequest3_cert2_BASE64); byteArray = QByteArray((const char *)&data[0], data.size()); r.paymentRequest.parse(byteArray); // Ensure the request is initialized QVERIFY(r.paymentRequest.IsInitialized()); // compares 9223372036854775807 < GetTime() == false (treated as unexpired // payment request) QCOMPARE(PaymentServer::verifyExpired(r.paymentRequest.getDetails()), false); // Unexpired payment request (expires is set to 0x8000000000000000 > max. // int64_t, allowed uint64): // 9223372036854775808 (uint64), -9223372036854775808 (int64_t) and 0 // (int32_t) // 0 is 1970-01-01 00:00:00 (for a 32 bit time values) data = DecodeBase64(paymentrequest4_cert2_BASE64); byteArray = QByteArray((const char *)&data[0], data.size()); r.paymentRequest.parse(byteArray); // Ensure the request is initialized QVERIFY(r.paymentRequest.IsInitialized()); // compares -9223372036854775808 < GetTime() == true (treated as expired // payment request) QCOMPARE(PaymentServer::verifyExpired(r.paymentRequest.getDetails()), true); // Test BIP70 DoS protection: uint8_t randData[BIP70_MAX_PAYMENTREQUEST_SIZE + 1]; GetRandBytes(randData, sizeof(randData)); // Write data to a temp file: QTemporaryFile tempFile; tempFile.open(); tempFile.write((const char *)randData, sizeof(randData)); tempFile.close(); // compares 50001 <= BIP70_MAX_PAYMENTREQUEST_SIZE == false QCOMPARE(PaymentServer::verifySize(tempFile.size()), false); // Payment request with amount overflow (amount is set to 21000001 BCH): data = DecodeBase64(paymentrequest5_cert2_BASE64); byteArray = QByteArray((const char *)&data[0], data.size()); r.paymentRequest.parse(byteArray); // Ensure the request is initialized QVERIFY(r.paymentRequest.IsInitialized()); // Extract address and amount from the request - QList> sendingTos = r.paymentRequest.getPayTo(); - for (const std::pair &sendingTo : sendingTos) { + QList> sendingTos = r.paymentRequest.getPayTo(); + for (const std::pair &sendingTo : sendingTos) { CTxDestination dest; if (ExtractDestination(sendingTo.first, dest)) QCOMPARE(PaymentServer::verifyAmount(sendingTo.second), false); } delete server; } void RecipientCatcher::getRecipient(SendCoinsRecipient r) { recipient = r; } diff --git a/src/qt/transactiondesc.cpp b/src/qt/transactiondesc.cpp index eb57277b45..19877bcfa0 100644 --- a/src/qt/transactiondesc.cpp +++ b/src/qt/transactiondesc.cpp @@ -1,389 +1,378 @@ // 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 "transactiondesc.h" #include "bitcoinunits.h" #include "guiutil.h" #include "paymentserver.h" #include "transactionrecord.h" #include "consensus/consensus.h" #include "dstencode.h" #include "script/script.h" #include "timedata.h" #include "util.h" #include "validation.h" #include "wallet/db.h" #include "wallet/finaltx.h" #include "wallet/wallet.h" #include #include QString TransactionDesc::FormatTxStatus(const CWalletTx &wtx) { AssertLockHeld(cs_main); if (!CheckFinalTx(wtx)) { if (wtx.tx->nLockTime < LOCKTIME_THRESHOLD) return tr("Open for %n more block(s)", "", wtx.tx->nLockTime - chainActive.Height()); else return tr("Open until %1") .arg(GUIUtil::dateTimeStr(wtx.tx->nLockTime)); } else { int nDepth = wtx.GetDepthInMainChain(); if (nDepth < 0) return tr("conflicted with a transaction with %1 confirmations") .arg(-nDepth); else if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0) return tr("%1/offline").arg(nDepth); else if (nDepth == 0) return tr("0/unconfirmed, %1") .arg((wtx.InMempool() ? tr("in memory pool") : tr("not in memory pool"))) + (wtx.isAbandoned() ? ", " + tr("abandoned") : ""); else if (nDepth < 6) return tr("%1/unconfirmed").arg(nDepth); else return tr("%1 confirmations").arg(nDepth); } } QString TransactionDesc::toHTML(CWallet *wallet, CWalletTx &wtx, TransactionRecord *rec, int unit) { QString strHTML; LOCK2(cs_main, wallet->cs_wallet); strHTML.reserve(4000); strHTML += ""; int64_t nTime = wtx.GetTxTime(); Amount nCredit = wtx.GetCredit(ISMINE_ALL); Amount nDebit = wtx.GetDebit(ISMINE_ALL); - CAmount nNet = (nCredit - nDebit).GetSatoshis(); + Amount nNet = nCredit - nDebit; strHTML += "" + tr("Status") + ": " + FormatTxStatus(wtx); int nRequests = wtx.GetRequestCount(); if (nRequests != -1) { if (nRequests == 0) strHTML += tr(", has not been successfully broadcast yet"); else if (nRequests > 0) strHTML += tr(", broadcast through %n node(s)", "", nRequests); } strHTML += "
"; strHTML += "" + tr("Date") + ": " + (nTime ? GUIUtil::dateTimeStr(nTime) : "") + "
"; // // From // if (wtx.IsCoinBase()) { strHTML += "" + tr("Source") + ": " + tr("Generated") + "
"; } else if (wtx.mapValue.count("from") && !wtx.mapValue["from"].empty()) { // Online transaction strHTML += "" + tr("From") + ": " + GUIUtil::HtmlEscape(wtx.mapValue["from"]) + "
"; } else { // Offline transaction if (nNet > 0) { // Credit CTxDestination address = DecodeDestination(rec->address); if (IsValidDestination(address)) { if (wallet->mapAddressBook.count(address)) { strHTML += "" + tr("From") + ": " + tr("unknown") + "
"; strHTML += "" + tr("To") + ": "; strHTML += GUIUtil::HtmlEscape(rec->address); QString addressOwned = (::IsMine(*wallet, address) == ISMINE_SPENDABLE) ? tr("own address") : tr("watch-only"); if (!wallet->mapAddressBook[address].name.empty()) strHTML += " (" + addressOwned + ", " + tr("label") + ": " + GUIUtil::HtmlEscape( wallet->mapAddressBook[address].name) + ")"; else strHTML += " (" + addressOwned + ")"; strHTML += "
"; } } } } // // To // if (wtx.mapValue.count("to") && !wtx.mapValue["to"].empty()) { // Online transaction std::string strAddress = wtx.mapValue["to"]; strHTML += "" + tr("To") + ": "; CTxDestination dest = DecodeDestination(strAddress); if (wallet->mapAddressBook.count(dest) && !wallet->mapAddressBook[dest].name.empty()) strHTML += GUIUtil::HtmlEscape(wallet->mapAddressBook[dest].name) + " "; strHTML += GUIUtil::HtmlEscape(strAddress) + "
"; } // // Amount // if (wtx.IsCoinBase() && nCredit == Amount(0)) { // // Coinbase // Amount nUnmatured(0); for (const CTxOut &txout : wtx.tx->vout) { nUnmatured += wallet->GetCredit(txout, ISMINE_ALL); } strHTML += "" + tr("Credit") + ": "; if (wtx.IsInMainChain()) - strHTML += BitcoinUnits::formatHtmlWithUnit( - unit, nUnmatured.GetSatoshis()) + + strHTML += BitcoinUnits::formatHtmlWithUnit(unit, nUnmatured) + " (" + tr("matures in %n more block(s)", "", wtx.GetBlocksToMaturity()) + ")"; else strHTML += "(" + tr("not accepted") + ")"; strHTML += "
"; } else if (nNet > 0) { // // Credit // strHTML += "" + tr("Credit") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, nNet) + "
"; } else { isminetype fAllFromMe = ISMINE_SPENDABLE; for (const CTxIn &txin : wtx.tx->vin) { isminetype mine = wallet->IsMine(txin); if (fAllFromMe > mine) fAllFromMe = mine; } isminetype fAllToMe = ISMINE_SPENDABLE; for (const CTxOut &txout : wtx.tx->vout) { isminetype mine = wallet->IsMine(txout); if (fAllToMe > mine) fAllToMe = mine; } if (fAllFromMe) { if (fAllFromMe & ISMINE_WATCH_ONLY) strHTML += "" + tr("From") + ": " + tr("watch-only") + "
"; // // Debit // for (const CTxOut &txout : wtx.tx->vout) { // Ignore change isminetype toSelf = wallet->IsMine(txout); if ((toSelf == ISMINE_SPENDABLE) && (fAllFromMe == ISMINE_SPENDABLE)) continue; if (!wtx.mapValue.count("to") || wtx.mapValue["to"].empty()) { // Offline transaction CTxDestination address; if (ExtractDestination(txout.scriptPubKey, address)) { strHTML += "" + tr("To") + ": "; if (wallet->mapAddressBook.count(address) && !wallet->mapAddressBook[address].name.empty()) strHTML += GUIUtil::HtmlEscape( wallet->mapAddressBook[address].name) + " "; strHTML += GUIUtil::HtmlEscape(EncodeDestination(address)); if (toSelf == ISMINE_SPENDABLE) strHTML += " (own address)"; else if (toSelf & ISMINE_WATCH_ONLY) strHTML += " (watch-only)"; strHTML += "
"; } } - strHTML += "" + tr("Debit") + ": " + - BitcoinUnits::formatHtmlWithUnit( - unit, -txout.nValue.GetSatoshis()) + - "
"; + strHTML += + "" + tr("Debit") + ": " + + BitcoinUnits::formatHtmlWithUnit(unit, -1 * txout.nValue) + + "
"; if (toSelf) - strHTML += "" + tr("Credit") + ": " + - BitcoinUnits::formatHtmlWithUnit( - unit, txout.nValue.GetSatoshis()) + - "
"; + strHTML += + "" + tr("Credit") + ": " + + BitcoinUnits::formatHtmlWithUnit(unit, txout.nValue) + + "
"; } if (fAllToMe) { // Payment to self Amount nChange = wtx.GetChange(); Amount nValue = nCredit - nChange; strHTML += "" + tr("Total debit") + ": " + - BitcoinUnits::formatHtmlWithUnit( - unit, -nValue.GetSatoshis()) + + BitcoinUnits::formatHtmlWithUnit(unit, -1 * nValue) + "
"; strHTML += "" + tr("Total credit") + ": " + - BitcoinUnits::formatHtmlWithUnit( - unit, nValue.GetSatoshis()) + + BitcoinUnits::formatHtmlWithUnit(unit, nValue) + "
"; } Amount nTxFee = nDebit - wtx.tx->GetValueOut(); if (nTxFee > Amount(0)) strHTML += "" + tr("Transaction fee") + ": " + - BitcoinUnits::formatHtmlWithUnit( - unit, -nTxFee.GetSatoshis()) + + BitcoinUnits::formatHtmlWithUnit(unit, -1 * nTxFee) + "
"; } else { // // Mixed debit transaction // for (const CTxIn &txin : wtx.tx->vin) { if (wallet->IsMine(txin)) strHTML += "" + tr("Debit") + ": " + BitcoinUnits::formatHtmlWithUnit( - unit, - -wallet->GetDebit(txin, ISMINE_ALL).GetSatoshis()) + + unit, -1 * wallet->GetDebit(txin, ISMINE_ALL)) + "
"; } for (const CTxOut &txout : wtx.tx->vout) { if (wallet->IsMine(txout)) strHTML += "" + tr("Credit") + ": " + BitcoinUnits::formatHtmlWithUnit( - unit, wallet->GetCredit(txout, ISMINE_ALL) - .GetSatoshis()) + + unit, wallet->GetCredit(txout, ISMINE_ALL)) + "
"; } } } strHTML += "" + tr("Net amount") + ": " + BitcoinUnits::formatHtmlWithUnit(unit, nNet, true) + "
"; // // Message // if (wtx.mapValue.count("message") && !wtx.mapValue["message"].empty()) strHTML += "
" + tr("Message") + ":
" + GUIUtil::HtmlEscape(wtx.mapValue["message"], true) + "
"; if (wtx.mapValue.count("comment") && !wtx.mapValue["comment"].empty()) strHTML += "
" + tr("Comment") + ":
" + GUIUtil::HtmlEscape(wtx.mapValue["comment"], true) + "
"; strHTML += "" + tr("Transaction ID") + ": " + rec->getTxID() + "
"; strHTML += "" + tr("Transaction total size") + ": " + QString::number(wtx.tx->GetTotalSize()) + " bytes
"; strHTML += "" + tr("Output index") + ": " + QString::number(rec->getOutputIndex()) + "
"; // Message from normal bitcoincash:URI (bitcoincash:123...?message=example) for (const std::pair &r : wtx.vOrderForm) { if (r.first == "Message") strHTML += "
" + tr("Message") + ":
" + GUIUtil::HtmlEscape(r.second, true) + "
"; } // // PaymentRequest info: // for (const std::pair &r : wtx.vOrderForm) { if (r.first == "PaymentRequest") { PaymentRequestPlus req; req.parse( QByteArray::fromRawData(r.second.data(), r.second.size())); QString merchant; if (req.getMerchant(PaymentServer::getCertStore(), merchant)) strHTML += "" + tr("Merchant") + ": " + GUIUtil::HtmlEscape(merchant) + "
"; } } if (wtx.IsCoinBase()) { quint32 numBlocksToMaturity = COINBASE_MATURITY + 1; strHTML += "
" + tr("Generated coins must mature %1 blocks before they can be " "spent. When you generated this block, it was broadcast to the " "network to be added to the block chain. If it fails to get " "into the chain, its state will change to \"not accepted\" and " "it won't be spendable. This may occasionally happen if another " "node generates a block within a few seconds of yours.") .arg(QString::number(numBlocksToMaturity)) + "
"; } // // Debug view // if (fDebug) { strHTML += "

" + tr("Debug information") + "

"; for (const CTxIn &txin : wtx.tx->vin) { if (wallet->IsMine(txin)) - strHTML += - "" + tr("Debit") + ": " + - BitcoinUnits::formatHtmlWithUnit( - unit, - -wallet->GetDebit(txin, ISMINE_ALL).GetSatoshis()) + - "
"; + strHTML += "" + tr("Debit") + ": " + + BitcoinUnits::formatHtmlWithUnit( + unit, -1 * wallet->GetDebit(txin, ISMINE_ALL)) + + "
"; } for (const CTxOut &txout : wtx.tx->vout) { if (wallet->IsMine(txout)) - strHTML += - "" + tr("Credit") + ": " + - BitcoinUnits::formatHtmlWithUnit( - unit, - wallet->GetCredit(txout, ISMINE_ALL).GetSatoshis()) + - "
"; + strHTML += "" + tr("Credit") + ": " + + BitcoinUnits::formatHtmlWithUnit( + unit, wallet->GetCredit(txout, ISMINE_ALL)) + + "
"; } strHTML += "
" + tr("Transaction") + ":
"; strHTML += GUIUtil::HtmlEscape(wtx.tx->ToString(), true); strHTML += "
" + tr("Inputs") + ":"; strHTML += "
    "; for (const CTxIn &txin : wtx.tx->vin) { COutPoint prevout = txin.prevout; Coin prev; if (pcoinsTip->GetCoin(prevout, prev)) { strHTML += "
  • "; const CTxOut &vout = prev.GetTxOut(); CTxDestination address; if (ExtractDestination(vout.scriptPubKey, address)) { if (wallet->mapAddressBook.count(address) && !wallet->mapAddressBook[address].name.empty()) { strHTML += GUIUtil::HtmlEscape( wallet->mapAddressBook[address].name) + " "; } strHTML += QString::fromStdString(EncodeDestination(address)); } strHTML = strHTML + " " + tr("Amount") + "=" + - BitcoinUnits::formatHtmlWithUnit( - unit, vout.nValue.GetSatoshis()); + BitcoinUnits::formatHtmlWithUnit(unit, vout.nValue); strHTML = strHTML + " IsMine=" + (wallet->IsMine(vout) & ISMINE_SPENDABLE ? tr("true") : tr("false")) + "
  • "; strHTML = strHTML + " IsWatchOnly=" + (wallet->IsMine(vout) & ISMINE_WATCH_ONLY ? tr("true") : tr("false")) + ""; } } strHTML += "
"; } strHTML += "
"; return strHTML; } diff --git a/src/qt/transactionfilterproxy.cpp b/src/qt/transactionfilterproxy.cpp index 15e372db11..caf3ad1b9c 100644 --- a/src/qt/transactionfilterproxy.cpp +++ b/src/qt/transactionfilterproxy.cpp @@ -1,97 +1,97 @@ // 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 "transactionfilterproxy.h" #include "transactionrecord.h" #include "transactiontablemodel.h" #include #include // Earliest date that can be represented (far in the past) const QDateTime TransactionFilterProxy::MIN_DATE = QDateTime::fromTime_t(0); // Last date that can be represented (far in the future) const QDateTime TransactionFilterProxy::MAX_DATE = QDateTime::fromTime_t(0xFFFFFFFF); TransactionFilterProxy::TransactionFilterProxy(QObject *parent) : QSortFilterProxyModel(parent), dateFrom(MIN_DATE), dateTo(MAX_DATE), addrPrefix(), typeFilter(ALL_TYPES), watchOnlyFilter(WatchOnlyFilter_All), minAmount(0), limitRows(-1), showInactive(true) {} bool TransactionFilterProxy::filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent) const { QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); int type = index.data(TransactionTableModel::TypeRole).toInt(); QDateTime datetime = index.data(TransactionTableModel::DateRole).toDateTime(); bool involvesWatchAddress = index.data(TransactionTableModel::WatchonlyRole).toBool(); QString address = index.data(TransactionTableModel::AddressRole).toString(); QString label = index.data(TransactionTableModel::LabelRole).toString(); qint64 amount = llabs(index.data(TransactionTableModel::AmountRole).toLongLong()); int status = index.data(TransactionTableModel::StatusRole).toInt(); if (!showInactive && status == TransactionStatus::Conflicted) return false; if (!(TYPE(type) & typeFilter)) return false; if (involvesWatchAddress && watchOnlyFilter == WatchOnlyFilter_No) return false; if (!involvesWatchAddress && watchOnlyFilter == WatchOnlyFilter_Yes) return false; if (datetime < dateFrom || datetime > dateTo) return false; if (!address.contains(addrPrefix, Qt::CaseInsensitive) && !label.contains(addrPrefix, Qt::CaseInsensitive)) return false; if (amount < minAmount) return false; return true; } void TransactionFilterProxy::setDateRange(const QDateTime &from, const QDateTime &to) { this->dateFrom = from; this->dateTo = to; invalidateFilter(); } void TransactionFilterProxy::setAddressPrefix(const QString &_addrPrefix) { this->addrPrefix = _addrPrefix; invalidateFilter(); } void TransactionFilterProxy::setTypeFilter(quint32 modes) { this->typeFilter = modes; invalidateFilter(); } -void TransactionFilterProxy::setMinAmount(const CAmount &minimum) { +void TransactionFilterProxy::setMinAmount(const Amount minimum) { this->minAmount = minimum; invalidateFilter(); } void TransactionFilterProxy::setWatchOnlyFilter(WatchOnlyFilter filter) { this->watchOnlyFilter = filter; invalidateFilter(); } void TransactionFilterProxy::setLimit(int limit) { this->limitRows = limit; } void TransactionFilterProxy::setShowInactive(bool _showInactive) { this->showInactive = _showInactive; invalidateFilter(); } int TransactionFilterProxy::rowCount(const QModelIndex &parent) const { if (limitRows != -1) { return std::min(QSortFilterProxyModel::rowCount(parent), limitRows); } else { return QSortFilterProxyModel::rowCount(parent); } } diff --git a/src/qt/transactionfilterproxy.h b/src/qt/transactionfilterproxy.h index 942296406d..2743d37ca3 100644 --- a/src/qt/transactionfilterproxy.h +++ b/src/qt/transactionfilterproxy.h @@ -1,67 +1,67 @@ // Copyright (c) 2011-2014 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_TRANSACTIONFILTERPROXY_H #define BITCOIN_QT_TRANSACTIONFILTERPROXY_H #include "amount.h" #include #include /** Filter the transaction list according to pre-specified rules. */ class TransactionFilterProxy : public QSortFilterProxyModel { Q_OBJECT public: explicit TransactionFilterProxy(QObject *parent = 0); /** Earliest date that can be represented (far in the past) */ static const QDateTime MIN_DATE; /** Last date that can be represented (far in the future) */ static const QDateTime MAX_DATE; /** Type filter bit field (all types) */ static const quint32 ALL_TYPES = 0xFFFFFFFF; static quint32 TYPE(int type) { return 1 << type; } enum WatchOnlyFilter { WatchOnlyFilter_All, WatchOnlyFilter_Yes, WatchOnlyFilter_No }; void setDateRange(const QDateTime &from, const QDateTime &to); void setAddressPrefix(const QString &addrPrefix); /** @note Type filter takes a bit field created with TYPE() or ALL_TYPES */ void setTypeFilter(quint32 modes); - void setMinAmount(const CAmount &minimum); + void setMinAmount(const Amount minimum); void setWatchOnlyFilter(WatchOnlyFilter filter); /** Set maximum number of rows returned, -1 if unlimited. */ void setLimit(int limit); /** Set whether to show conflicted transactions. */ void setShowInactive(bool showInactive); int rowCount(const QModelIndex &parent = QModelIndex()) const override; protected: bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; private: QDateTime dateFrom; QDateTime dateTo; QString addrPrefix; quint32 typeFilter; WatchOnlyFilter watchOnlyFilter; - CAmount minAmount; + Amount minAmount; int limitRows; bool showInactive; }; #endif // BITCOIN_QT_TRANSACTIONFILTERPROXY_H diff --git a/src/qt/transactionrecord.cpp b/src/qt/transactionrecord.cpp index 484d968fad..89655b4070 100644 --- a/src/qt/transactionrecord.cpp +++ b/src/qt/transactionrecord.cpp @@ -1,227 +1,226 @@ // 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 "transactionrecord.h" #include "consensus/consensus.h" #include "dstencode.h" #include "timedata.h" #include "validation.h" #include "wallet/finaltx.h" #include "wallet/wallet.h" #include /** * Return positive answer if transaction should be shown in list. */ bool TransactionRecord::showTransaction(const CWalletTx &wtx) { if (wtx.IsCoinBase()) { // Ensures we show generated coins / mined transactions at depth 1 if (!wtx.IsInMainChain()) { return false; } } return true; } /** * Decompose CWallet transaction to model transaction records. */ QList TransactionRecord::decomposeTransaction(const CWallet *wallet, const CWalletTx &wtx) { QList parts; int64_t nTime = wtx.GetTxTime(); Amount nCredit = wtx.GetCredit(ISMINE_ALL); Amount nDebit = wtx.GetDebit(ISMINE_ALL); - CAmount nNet = (nCredit - nDebit).GetSatoshis(); + Amount nNet = nCredit - nDebit; uint256 hash = wtx.GetId(); std::map mapValue = wtx.mapValue; if (nNet > 0 || wtx.IsCoinBase()) { // // Credit // for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) { const CTxOut &txout = wtx.tx->vout[i]; isminetype mine = wallet->IsMine(txout); if (mine) { TransactionRecord sub(hash, nTime); CTxDestination address; sub.idx = i; // vout index - sub.credit = txout.nValue.GetSatoshis(); + sub.credit = txout.nValue; sub.involvesWatchAddress = mine & ISMINE_WATCH_ONLY; if (ExtractDestination(txout.scriptPubKey, address) && IsMine(*wallet, address)) { // Received by Bitcoin Address sub.type = TransactionRecord::RecvWithAddress; sub.address = EncodeDestination(address); } else { // Received by IP connection (deprecated features), or a // multisignature or other non-simple transaction sub.type = TransactionRecord::RecvFromOther; sub.address = mapValue["from"]; } if (wtx.IsCoinBase()) { // Generated sub.type = TransactionRecord::Generated; } parts.append(sub); } } } else { bool involvesWatchAddress = false; isminetype fAllFromMe = ISMINE_SPENDABLE; for (const CTxIn &txin : wtx.tx->vin) { isminetype mine = wallet->IsMine(txin); if (mine & ISMINE_WATCH_ONLY) involvesWatchAddress = true; if (fAllFromMe > mine) fAllFromMe = mine; } isminetype fAllToMe = ISMINE_SPENDABLE; for (const CTxOut &txout : wtx.tx->vout) { isminetype mine = wallet->IsMine(txout); if (mine & ISMINE_WATCH_ONLY) involvesWatchAddress = true; if (fAllToMe > mine) fAllToMe = mine; } if (fAllFromMe && fAllToMe) { // Payment to self Amount nChange = wtx.GetChange(); - parts.append(TransactionRecord(hash, nTime, - TransactionRecord::SendToSelf, "", - -(nDebit - nChange).GetSatoshis(), - (nCredit - nChange).GetSatoshis())); + parts.append(TransactionRecord( + hash, nTime, TransactionRecord::SendToSelf, "", + -1 * (nDebit - nChange), (nCredit - nChange))); // maybe pass to TransactionRecord as constructor argument parts.last().involvesWatchAddress = involvesWatchAddress; } else if (fAllFromMe) { // // Debit // Amount nTxFee = nDebit - wtx.tx->GetValueOut(); for (unsigned int nOut = 0; nOut < wtx.tx->vout.size(); nOut++) { const CTxOut &txout = wtx.tx->vout[nOut]; TransactionRecord sub(hash, nTime); sub.idx = nOut; sub.involvesWatchAddress = involvesWatchAddress; if (wallet->IsMine(txout)) { // Ignore parts sent to self, as this is usually the change // from a transaction sent back to our own address. continue; } CTxDestination address; if (ExtractDestination(txout.scriptPubKey, address)) { // Sent to Bitcoin Address sub.type = TransactionRecord::SendToAddress; sub.address = EncodeDestination(address); } else { // Sent to IP, or other non-address transaction like OP_EVAL sub.type = TransactionRecord::SendToOther; sub.address = mapValue["to"]; } Amount nValue = txout.nValue; /* Add fee to first output */ if (nTxFee > Amount(0)) { nValue += nTxFee; nTxFee = Amount(0); } - sub.debit = -nValue.GetSatoshis(); + sub.debit = -1 * nValue; parts.append(sub); } } else { // // Mixed debit transaction, can't break down payees // parts.append(TransactionRecord( hash, nTime, TransactionRecord::Other, "", nNet, 0)); parts.last().involvesWatchAddress = involvesWatchAddress; } } return parts; } void TransactionRecord::updateStatus(const CWalletTx &wtx) { AssertLockHeld(cs_main); // Determine transaction status // Find the block the tx is in CBlockIndex *pindex = nullptr; BlockMap::iterator mi = mapBlockIndex.find(wtx.hashBlock); if (mi != mapBlockIndex.end()) pindex = (*mi).second; // Sort order, unrecorded transactions sort to the top status.sortKey = strprintf("%010d-%01d-%010u-%03d", (pindex ? pindex->nHeight : std::numeric_limits::max()), (wtx.IsCoinBase() ? 1 : 0), wtx.nTimeReceived, idx); status.countsForBalance = wtx.IsTrusted() && !(wtx.GetBlocksToMaturity() > 0); status.depth = wtx.GetDepthInMainChain(); status.cur_num_blocks = chainActive.Height(); if (!CheckFinalTx(wtx)) { if (wtx.tx->nLockTime < LOCKTIME_THRESHOLD) { status.status = TransactionStatus::OpenUntilBlock; status.open_for = wtx.tx->nLockTime - chainActive.Height(); } else { status.status = TransactionStatus::OpenUntilDate; status.open_for = wtx.tx->nLockTime; } } // For generated transactions, determine maturity else if (type == TransactionRecord::Generated) { if (wtx.GetBlocksToMaturity() > 0) { status.status = TransactionStatus::Immature; if (wtx.IsInMainChain()) { status.matures_in = wtx.GetBlocksToMaturity(); // Check if the block was requested by anyone if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0) status.status = TransactionStatus::MaturesWarning; } else { status.status = TransactionStatus::NotAccepted; } } else { status.status = TransactionStatus::Confirmed; } } else { if (status.depth < 0) { status.status = TransactionStatus::Conflicted; } else if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0) { status.status = TransactionStatus::Offline; } else if (status.depth == 0) { status.status = TransactionStatus::Unconfirmed; if (wtx.isAbandoned()) status.status = TransactionStatus::Abandoned; } else if (status.depth < RecommendedNumConfirmations) { status.status = TransactionStatus::Confirming; } else { status.status = TransactionStatus::Confirmed; } } } bool TransactionRecord::statusUpdateNeeded() { AssertLockHeld(cs_main); return status.cur_num_blocks != chainActive.Height(); } QString TransactionRecord::getTxID() const { return QString::fromStdString(hash.ToString()); } int TransactionRecord::getOutputIndex() const { return idx; } diff --git a/src/qt/transactionrecord.h b/src/qt/transactionrecord.h index a353e890cf..ef2888aaf8 100644 --- a/src/qt/transactionrecord.h +++ b/src/qt/transactionrecord.h @@ -1,155 +1,155 @@ // 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_TRANSACTIONRECORD_H #define BITCOIN_QT_TRANSACTIONRECORD_H #include "amount.h" #include "uint256.h" #include #include class CWallet; class CWalletTx; /** * UI model for transaction status. The transaction status is the part of a * transaction that will change over time. */ class TransactionStatus { public: TransactionStatus() : countsForBalance(false), sortKey(""), matures_in(0), status(Offline), depth(0), open_for(0), cur_num_blocks(-1) {} enum Status { /**< Have 6 or more confirmations (normal tx) or fully mature (mined tx) **/ Confirmed, /// Normal (sent/received) transactions /**< Transaction not yet final, waiting for date */ OpenUntilDate, /**< Transaction not yet final, waiting for block */ OpenUntilBlock, /**< Not sent to any other nodes **/ Offline, /**< Not yet mined into a block **/ Unconfirmed, /**< Confirmed, but waiting for the recommended number of confirmations **/ Confirming, /**< Conflicts with other transaction or mempool **/ Conflicted, /**< Abandoned from the wallet **/ Abandoned, /// Generated (mined) transactions /**< Mined but waiting for maturity */ Immature, /**< Transaction will likely not mature because no nodes have confirmed */ MaturesWarning, /**< Mined but not accepted */ NotAccepted }; /// Transaction counts towards available balance bool countsForBalance; /// Sorting key based on status std::string sortKey; /** @name Generated (mined) transactions @{*/ int matures_in; /**@}*/ /** @name Reported status @{*/ Status status; qint64 depth; /**< Timestamp if status==OpenUntilDate, otherwise number of additional * blocks that need to be mined before finalization */ qint64 open_for; /**@}*/ /** Current number of blocks (to know whether cached status is still valid) */ int cur_num_blocks; }; /** * UI model for a transaction. A core transaction can be represented by multiple * UI transactions if it has multiple outputs. */ class TransactionRecord { public: enum Type { Other, Generated, SendToAddress, SendToOther, RecvWithAddress, RecvFromOther, SendToSelf }; /** Number of confirmation recommended for accepting a transaction */ static const int RecommendedNumConfirmations = 6; TransactionRecord() : hash(), time(0), type(Other), address(""), debit(0), credit(0), idx(0) {} TransactionRecord(uint256 _hash, qint64 _time) : hash(_hash), time(_time), type(Other), address(""), debit(0), credit(0), idx(0) {} TransactionRecord(uint256 _hash, qint64 _time, Type _type, - const std::string &_address, const CAmount &_debit, - const CAmount &_credit) + const std::string &_address, const Amount _debit, + const Amount _credit) : hash(_hash), time(_time), type(_type), address(_address), debit(_debit), credit(_credit), idx(0) {} /** Decompose CWallet transaction to model transaction records. */ static bool showTransaction(const CWalletTx &wtx); static QList decomposeTransaction(const CWallet *wallet, const CWalletTx &wtx); /** @name Immutable transaction attributes @{*/ uint256 hash; qint64 time; Type type; std::string address; - CAmount debit; - CAmount credit; + Amount debit; + Amount credit; /**@}*/ /** Subtransaction index, for sort key */ int idx; /** Status: can change with block chain update */ TransactionStatus status; /** Whether the transaction was sent/received with a watch-only address */ bool involvesWatchAddress; /** Return the unique identifier for this transaction (part) */ QString getTxID() const; /** Return the output index of the subtransaction */ int getOutputIndex() const; /** Update status from core wallet tx. */ void updateStatus(const CWalletTx &wtx); /** Return whether a status update is needed. */ bool statusUpdateNeeded(); }; #endif // BITCOIN_QT_TRANSACTIONRECORD_H diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp index 2df442ada9..8b6d7d73e1 100644 --- a/src/qt/transactiontablemodel.cpp +++ b/src/qt/transactiontablemodel.cpp @@ -1,787 +1,787 @@ // 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 "transactiontablemodel.h" #include "addresstablemodel.h" #include "guiconstants.h" #include "guiutil.h" #include "optionsmodel.h" #include "platformstyle.h" #include "transactiondesc.h" #include "transactionrecord.h" #include "walletmodel.h" #include "core_io.h" #include "sync.h" #include "uint256.h" #include "util.h" #include "validation.h" #include "wallet/wallet.h" #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.hash < b.hash; } bool operator()(const TransactionRecord &a, const uint256 &b) const { return a.hash < b; } bool operator()(const uint256 &a, const TransactionRecord &b) const { return a < b.hash; } }; // Private implementation class TransactionTablePriv { public: TransactionTablePriv(CWallet *_wallet, TransactionTableModel *_parent) : wallet(_wallet), parent(_parent) {} CWallet *wallet; 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() { qDebug() << "TransactionTablePriv::refreshWallet"; cachedWallet.clear(); { LOCK2(cs_main, wallet->cs_wallet); for (std::map::iterator it = wallet->mapWallet.begin(); it != wallet->mapWallet.end(); ++it) { if (TransactionRecord::showTransaction(it->second)) cachedWallet.append(TransactionRecord::decomposeTransaction( wallet, it->second)); } } } /** * 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(const uint256 &hash, int status, bool showTransaction) { qDebug() << "TransactionTablePriv::updateWallet: " + QString::fromStdString(hash.ToString()) + " " + QString::number(status); // Find bounds of this transaction in model QList::iterator lower = qLowerBound( cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan()); QList::iterator upper = qUpperBound( cachedWallet.begin(), cachedWallet.end(), hash, 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) { LOCK2(cs_main, wallet->cs_wallet); // Find transaction in wallet std::map::iterator mi = wallet->mapWallet.find(hash); if (mi == wallet->mapWallet.end()) { qWarning() << "TransactionTablePriv::updateWallet: " "Warning: Got CT_NEW, but transaction is " "not in wallet"; break; } // Added -- insert at the right position QList toInsert = TransactionRecord::decomposeTransaction(wallet, mi->second); /* 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(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. TRY_LOCK(cs_main, lockMain); if (lockMain) { TRY_LOCK(wallet->cs_wallet, lockWallet); if (lockWallet && rec->statusUpdateNeeded()) { std::map::iterator mi = wallet->mapWallet.find(rec->hash); if (mi != wallet->mapWallet.end()) { rec->updateStatus(mi->second); } } } return rec; } return 0; } QString describe(TransactionRecord *rec, int unit) { { LOCK2(cs_main, wallet->cs_wallet); std::map::iterator mi = wallet->mapWallet.find(rec->hash); if (mi != wallet->mapWallet.end()) { return TransactionDesc::toHTML(wallet, mi->second, rec, unit); } } return QString(); } QString getTxHex(TransactionRecord *rec) { LOCK2(cs_main, wallet->cs_wallet); std::map::iterator mi = wallet->mapWallet.find(rec->hash); if (mi != wallet->mapWallet.end()) { std::string strHex = EncodeHexTx(static_cast(mi->second)); return QString::fromStdString(strHex); } return QString(); } }; TransactionTableModel::TransactionTableModel( const PlatformStyle *_platformStyle, CWallet *_wallet, WalletModel *parent) : QAbstractTableModel(parent), wallet(_wallet), walletModel(parent), priv(new TransactionTablePriv(_wallet, this)), fProcessingQueuedTransactions(false), platformStyle(_platformStyle) { columns << QString() << QString() << tr("Date") << tr("Type") << tr("Label") << BitcoinUnits::getAmountColumnTitle( walletModel->getOptionsModel()->getDisplayUnit()); priv->refreshWallet(); connect(walletModel->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(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) { uint256 updated; updated.SetHex(hash.toStdString()); priv->updateWallet(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::Offline: status = tr("Offline"); 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::MaturesWarning: status = tr("This block was not received by any other nodes and " "will probably not be accepted!"); 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: 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::Offline: return COLOR_TX_STATUS_OFFLINE; 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::MaturesWarning: 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"); else 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); + return qint64((rec->credit + rec->debit).GetSatoshis()); } 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) < 0) { 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( 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); + return qint64((rec->credit + rec->debit).GetSatoshis()); case TxIDRole: return rec->getTxID(); case TxHashRole: return QString::fromStdString(rec->hash.ToString()); case TxHexRole: return priv->getTxHex(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(row); if (data) { return createIndex(row, column, priv->index(row)); } 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(uint256 _hash, ChangeType _status, bool _showTransaction) : hash(_hash), status(_status), showTransaction(_showTransaction) {} void invoke(QObject *ttm) { QString strHash = QString::fromStdString(hash.GetHex()); qDebug() << "NotifyTransactionChanged: " + strHash + " status= " + QString::number(status); QMetaObject::invokeMethod(ttm, "updateTransaction", Qt::QueuedConnection, Q_ARG(QString, strHash), Q_ARG(int, status), Q_ARG(bool, showTransaction)); } private: uint256 hash; ChangeType status; bool showTransaction; }; static bool fQueueNotifications = false; static std::vector vQueueNotifications; static void NotifyTransactionChanged(TransactionTableModel *ttm, CWallet *wallet, const uint256 &hash, ChangeType status) { // Find transaction in wallet std::map::iterator mi = wallet->mapWallet.find(hash); // Determine whether to show transaction or not (determine this here so that // no relocking is needed in GUI thread) bool inWallet = mi != wallet->mapWallet.end(); bool showTransaction = (inWallet && TransactionRecord::showTransaction(mi->second)); TransactionNotification notification(hash, 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 QMetaObject::invokeMethod(ttm, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, true)); for (unsigned int i = 0; i < vQueueNotifications.size(); ++i) { if (vQueueNotifications.size() - i <= 10) QMetaObject::invokeMethod( ttm, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, false)); vQueueNotifications[i].invoke(ttm); } std::vector().swap( vQueueNotifications); // clear } } void TransactionTableModel::subscribeToCoreSignals() { // Connect signals to wallet wallet->NotifyTransactionChanged.connect( boost::bind(NotifyTransactionChanged, this, _1, _2, _3)); wallet->ShowProgress.connect(boost::bind(ShowProgress, this, _1, _2)); } void TransactionTableModel::unsubscribeFromCoreSignals() { // Disconnect signals from wallet wallet->NotifyTransactionChanged.disconnect( boost::bind(NotifyTransactionChanged, this, _1, _2, _3)); wallet->ShowProgress.disconnect(boost::bind(ShowProgress, this, _1, _2)); } diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp index 685bbdd116..3499eb7bc8 100644 --- a/src/qt/transactionview.cpp +++ b/src/qt/transactionview.cpp @@ -1,592 +1,592 @@ // 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 "transactionview.h" #include "addresstablemodel.h" #include "bitcoinunits.h" #include "csvmodelwriter.h" #include "editaddressdialog.h" #include "guiutil.h" #include "optionsmodel.h" #include "platformstyle.h" #include "transactiondescdialog.h" #include "transactionfilterproxy.h" #include "transactionrecord.h" #include "transactiontablemodel.h" #include "walletmodel.h" #include "ui_interface.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *parent) : QWidget(parent), model(0), transactionProxyModel(0), transactionView(0), abandonAction(0), columnResizingFixer(0) { // Build filter row setContentsMargins(0, 0, 0, 0); QHBoxLayout *hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); if (platformStyle->getUseExtraSpacing()) { hlayout->setSpacing(5); hlayout->addSpacing(26); } else { hlayout->setSpacing(0); hlayout->addSpacing(23); } watchOnlyWidget = new QComboBox(this); watchOnlyWidget->setFixedWidth(24); watchOnlyWidget->addItem("", TransactionFilterProxy::WatchOnlyFilter_All); watchOnlyWidget->addItem(platformStyle->SingleColorIcon(":/icons/eye_plus"), "", TransactionFilterProxy::WatchOnlyFilter_Yes); watchOnlyWidget->addItem( platformStyle->SingleColorIcon(":/icons/eye_minus"), "", TransactionFilterProxy::WatchOnlyFilter_No); hlayout->addWidget(watchOnlyWidget); dateWidget = new QComboBox(this); if (platformStyle->getUseExtraSpacing()) { dateWidget->setFixedWidth(121); } else { dateWidget->setFixedWidth(120); } dateWidget->addItem(tr("All"), All); dateWidget->addItem(tr("Today"), Today); dateWidget->addItem(tr("This week"), ThisWeek); dateWidget->addItem(tr("This month"), ThisMonth); dateWidget->addItem(tr("Last month"), LastMonth); dateWidget->addItem(tr("This year"), ThisYear); dateWidget->addItem(tr("Range..."), Range); hlayout->addWidget(dateWidget); typeWidget = new QComboBox(this); if (platformStyle->getUseExtraSpacing()) { typeWidget->setFixedWidth(121); } else { typeWidget->setFixedWidth(120); } typeWidget->addItem(tr("All"), TransactionFilterProxy::ALL_TYPES); typeWidget->addItem( tr("Received with"), TransactionFilterProxy::TYPE(TransactionRecord::RecvWithAddress) | TransactionFilterProxy::TYPE(TransactionRecord::RecvFromOther)); typeWidget->addItem( tr("Sent to"), TransactionFilterProxy::TYPE(TransactionRecord::SendToAddress) | TransactionFilterProxy::TYPE(TransactionRecord::SendToOther)); typeWidget->addItem(tr("To yourself"), TransactionFilterProxy::TYPE( TransactionRecord::SendToSelf)); typeWidget->addItem(tr("Mined"), TransactionFilterProxy::TYPE( TransactionRecord::Generated)); typeWidget->addItem(tr("Other"), TransactionFilterProxy::TYPE(TransactionRecord::Other)); hlayout->addWidget(typeWidget); addressWidget = new QLineEdit(this); #if QT_VERSION >= 0x040700 addressWidget->setPlaceholderText(tr("Enter address or label to search")); #endif hlayout->addWidget(addressWidget); amountWidget = new QLineEdit(this); #if QT_VERSION >= 0x040700 amountWidget->setPlaceholderText(tr("Min amount")); #endif if (platformStyle->getUseExtraSpacing()) { amountWidget->setFixedWidth(97); } else { amountWidget->setFixedWidth(100); } amountWidget->setValidator(new QDoubleValidator(0, 1e20, 8, this)); hlayout->addWidget(amountWidget); QVBoxLayout *vlayout = new QVBoxLayout(this); vlayout->setContentsMargins(0, 0, 0, 0); vlayout->setSpacing(0); QTableView *view = new QTableView(this); vlayout->addLayout(hlayout); vlayout->addWidget(createDateRangeWidget()); vlayout->addWidget(view); vlayout->setSpacing(0); int width = view->verticalScrollBar()->sizeHint().width(); // Cover scroll bar width with spacing if (platformStyle->getUseExtraSpacing()) { hlayout->addSpacing(width + 2); } else { hlayout->addSpacing(width); } // Always show scroll bar view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); view->setTabKeyNavigation(false); view->setContextMenuPolicy(Qt::CustomContextMenu); view->installEventFilter(this); transactionView = view; // Actions abandonAction = new QAction(tr("Abandon transaction"), this); QAction *copyAddressAction = new QAction(tr("Copy address"), this); QAction *copyLabelAction = new QAction(tr("Copy label"), this); QAction *copyAmountAction = new QAction(tr("Copy amount"), this); QAction *copyTxIDAction = new QAction(tr("Copy transaction ID"), this); QAction *copyTxHexAction = new QAction(tr("Copy raw transaction"), this); QAction *copyTxPlainText = new QAction(tr("Copy full transaction details"), this); QAction *editLabelAction = new QAction(tr("Edit label"), this); QAction *showDetailsAction = new QAction(tr("Show transaction details"), this); contextMenu = new QMenu(this); contextMenu->addAction(copyAddressAction); contextMenu->addAction(copyLabelAction); contextMenu->addAction(copyAmountAction); contextMenu->addAction(copyTxIDAction); contextMenu->addAction(copyTxHexAction); contextMenu->addAction(copyTxPlainText); contextMenu->addAction(showDetailsAction); contextMenu->addSeparator(); contextMenu->addAction(abandonAction); contextMenu->addAction(editLabelAction); mapperThirdPartyTxUrls = new QSignalMapper(this); // Connect actions connect(mapperThirdPartyTxUrls, SIGNAL(mapped(QString)), this, SLOT(openThirdPartyTxUrl(QString))); connect(dateWidget, SIGNAL(activated(int)), this, SLOT(chooseDate(int))); connect(typeWidget, SIGNAL(activated(int)), this, SLOT(chooseType(int))); connect(watchOnlyWidget, SIGNAL(activated(int)), this, SLOT(chooseWatchonly(int))); connect(addressWidget, SIGNAL(textChanged(QString)), this, SLOT(changedPrefix(QString))); connect(amountWidget, SIGNAL(textChanged(QString)), this, SLOT(changedAmount(QString))); connect(view, SIGNAL(doubleClicked(QModelIndex)), this, SIGNAL(doubleClicked(QModelIndex))); connect(view, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextualMenu(QPoint))); connect(abandonAction, SIGNAL(triggered()), this, SLOT(abandonTx())); connect(copyAddressAction, SIGNAL(triggered()), this, SLOT(copyAddress())); connect(copyLabelAction, SIGNAL(triggered()), this, SLOT(copyLabel())); connect(copyAmountAction, SIGNAL(triggered()), this, SLOT(copyAmount())); connect(copyTxIDAction, SIGNAL(triggered()), this, SLOT(copyTxID())); connect(copyTxHexAction, SIGNAL(triggered()), this, SLOT(copyTxHex())); connect(copyTxPlainText, SIGNAL(triggered()), this, SLOT(copyTxPlainText())); connect(editLabelAction, SIGNAL(triggered()), this, SLOT(editLabel())); connect(showDetailsAction, SIGNAL(triggered()), this, SLOT(showDetails())); } void TransactionView::setModel(WalletModel *_model) { this->model = _model; if (_model) { transactionProxyModel = new TransactionFilterProxy(this); transactionProxyModel->setSourceModel( _model->getTransactionTableModel()); transactionProxyModel->setDynamicSortFilter(true); transactionProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); transactionProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); transactionProxyModel->setSortRole(Qt::EditRole); transactionView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); transactionView->setModel(transactionProxyModel); transactionView->setAlternatingRowColors(true); transactionView->setSelectionBehavior(QAbstractItemView::SelectRows); transactionView->setSelectionMode(QAbstractItemView::ExtendedSelection); transactionView->setSortingEnabled(true); transactionView->sortByColumn(TransactionTableModel::Date, Qt::DescendingOrder); transactionView->verticalHeader()->hide(); transactionView->setColumnWidth(TransactionTableModel::Status, STATUS_COLUMN_WIDTH); transactionView->setColumnWidth(TransactionTableModel::Watchonly, WATCHONLY_COLUMN_WIDTH); transactionView->setColumnWidth(TransactionTableModel::Date, DATE_COLUMN_WIDTH); transactionView->setColumnWidth(TransactionTableModel::Type, TYPE_COLUMN_WIDTH); transactionView->setColumnWidth(TransactionTableModel::Amount, AMOUNT_MINIMUM_COLUMN_WIDTH); columnResizingFixer = new GUIUtil::TableViewLastColumnResizingFixer( transactionView, AMOUNT_MINIMUM_COLUMN_WIDTH, MINIMUM_COLUMN_WIDTH, this); if (_model->getOptionsModel()) { // Add third party transaction URLs to context menu QStringList listUrls = _model->getOptionsModel()->getThirdPartyTxUrls().split( "|", QString::SkipEmptyParts); for (int i = 0; i < listUrls.size(); ++i) { QString host = QUrl(listUrls[i].trimmed(), QUrl::StrictMode).host(); if (!host.isEmpty()) { QAction *thirdPartyTxUrlAction = new QAction(host, this); // use host as menu item label if (i == 0) contextMenu->addSeparator(); contextMenu->addAction(thirdPartyTxUrlAction); connect(thirdPartyTxUrlAction, SIGNAL(triggered()), mapperThirdPartyTxUrls, SLOT(map())); mapperThirdPartyTxUrls->setMapping(thirdPartyTxUrlAction, listUrls[i].trimmed()); } } } // show/hide column Watch-only updateWatchOnlyColumn(_model->haveWatchOnly()); // Watch-only signal connect(_model, SIGNAL(notifyWatchonlyChanged(bool)), this, SLOT(updateWatchOnlyColumn(bool))); } } void TransactionView::chooseDate(int idx) { if (!transactionProxyModel) return; QDate current = QDate::currentDate(); dateRangeWidget->setVisible(false); switch (dateWidget->itemData(idx).toInt()) { case All: transactionProxyModel->setDateRange( TransactionFilterProxy::MIN_DATE, TransactionFilterProxy::MAX_DATE); break; case Today: transactionProxyModel->setDateRange( QDateTime(current), TransactionFilterProxy::MAX_DATE); break; case ThisWeek: { // Find last Monday QDate startOfWeek = current.addDays(-(current.dayOfWeek() - 1)); transactionProxyModel->setDateRange( QDateTime(startOfWeek), TransactionFilterProxy::MAX_DATE); } break; case ThisMonth: transactionProxyModel->setDateRange( QDateTime(QDate(current.year(), current.month(), 1)), TransactionFilterProxy::MAX_DATE); break; case LastMonth: transactionProxyModel->setDateRange( QDateTime( QDate(current.year(), current.month(), 1).addMonths(-1)), QDateTime(QDate(current.year(), current.month(), 1))); break; case ThisYear: transactionProxyModel->setDateRange( QDateTime(QDate(current.year(), 1, 1)), TransactionFilterProxy::MAX_DATE); break; case Range: dateRangeWidget->setVisible(true); dateRangeChanged(); break; } } void TransactionView::chooseType(int idx) { if (!transactionProxyModel) return; transactionProxyModel->setTypeFilter(typeWidget->itemData(idx).toInt()); } void TransactionView::chooseWatchonly(int idx) { if (!transactionProxyModel) return; transactionProxyModel->setWatchOnlyFilter( (TransactionFilterProxy::WatchOnlyFilter)watchOnlyWidget->itemData(idx) .toInt()); } void TransactionView::changedPrefix(const QString &prefix) { if (!transactionProxyModel) return; transactionProxyModel->setAddressPrefix(prefix); } void TransactionView::changedAmount(const QString &amount) { if (!transactionProxyModel) return; Amount amount_parsed(0); if (BitcoinUnits::parse(model->getOptionsModel()->getDisplayUnit(), amount, &amount_parsed)) { - transactionProxyModel->setMinAmount(amount_parsed.GetSatoshis()); + transactionProxyModel->setMinAmount(amount_parsed); } else { transactionProxyModel->setMinAmount(0); } } void TransactionView::exportClicked() { // CSV is currently the only supported format QString filename = GUIUtil::getSaveFileName( this, tr("Export Transaction History"), QString(), tr("Comma separated file (*.csv)"), nullptr); if (filename.isNull()) return; CSVModelWriter writer(filename); // name, column, role writer.setModel(transactionProxyModel); writer.addColumn(tr("Confirmed"), 0, TransactionTableModel::ConfirmedRole); if (model && model->haveWatchOnly()) writer.addColumn(tr("Watch-only"), TransactionTableModel::Watchonly); writer.addColumn(tr("Date"), 0, TransactionTableModel::DateRole); writer.addColumn(tr("Type"), TransactionTableModel::Type, Qt::EditRole); writer.addColumn(tr("Label"), 0, TransactionTableModel::LabelRole); writer.addColumn(tr("Address"), 0, TransactionTableModel::AddressRole); writer.addColumn(BitcoinUnits::getAmountColumnTitle( model->getOptionsModel()->getDisplayUnit()), 0, TransactionTableModel::FormattedAmountRole); writer.addColumn(tr("ID"), 0, TransactionTableModel::TxIDRole); if (!writer.write()) { Q_EMIT message(tr("Exporting Failed"), tr("There was an error trying to save the transaction " "history to %1.") .arg(filename), CClientUIInterface::MSG_ERROR); } else { Q_EMIT message( tr("Exporting Successful"), tr("The transaction history was successfully saved to %1.") .arg(filename), CClientUIInterface::MSG_INFORMATION); } } void TransactionView::contextualMenu(const QPoint &point) { QModelIndex index = transactionView->indexAt(point); QModelIndexList selection = transactionView->selectionModel()->selectedRows(0); if (selection.empty()) return; // check if transaction can be abandoned, disable context menu action in // case it doesn't uint256 hash; hash.SetHex(selection.at(0) .data(TransactionTableModel::TxHashRole) .toString() .toStdString()); abandonAction->setEnabled(model->transactionCanBeAbandoned(hash)); if (index.isValid()) { contextMenu->exec(QCursor::pos()); } } void TransactionView::abandonTx() { if (!transactionView || !transactionView->selectionModel()) return; QModelIndexList selection = transactionView->selectionModel()->selectedRows(0); // get the hash from the TxHashRole (QVariant / QString) uint256 hash; QString hashQStr = selection.at(0).data(TransactionTableModel::TxHashRole).toString(); hash.SetHex(hashQStr.toStdString()); // Abandon the wallet transaction over the walletModel model->abandonTransaction(hash); // Update the table model->getTransactionTableModel()->updateTransaction(hashQStr, CT_UPDATED, false); } void TransactionView::copyAddress() { GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::AddressRole); } void TransactionView::copyLabel() { GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::LabelRole); } void TransactionView::copyAmount() { GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::FormattedAmountRole); } void TransactionView::copyTxID() { GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::TxIDRole); } void TransactionView::copyTxHex() { GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::TxHexRole); } void TransactionView::copyTxPlainText() { GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::TxPlainTextRole); } void TransactionView::editLabel() { if (!transactionView->selectionModel() || !model) return; QModelIndexList selection = transactionView->selectionModel()->selectedRows(); if (!selection.isEmpty()) { AddressTableModel *addressBook = model->getAddressTableModel(); if (!addressBook) return; QString address = selection.at(0).data(TransactionTableModel::AddressRole).toString(); if (address.isEmpty()) { // If this transaction has no associated address, exit return; } // Is address in address book? Address book can miss address when a // transaction is // sent from outside the UI. int idx = addressBook->lookupAddress(address); if (idx != -1) { // Edit sending / receiving address QModelIndex modelIdx = addressBook->index(idx, 0, QModelIndex()); // Determine type of address, launch appropriate editor dialog type QString type = modelIdx.data(AddressTableModel::TypeRole).toString(); EditAddressDialog dlg(type == AddressTableModel::Receive ? EditAddressDialog::EditReceivingAddress : EditAddressDialog::EditSendingAddress, this); dlg.setModel(addressBook); dlg.loadRow(idx); dlg.exec(); } else { // Add sending address EditAddressDialog dlg(EditAddressDialog::NewSendingAddress, this); dlg.setModel(addressBook); dlg.setAddress(address); dlg.exec(); } } } void TransactionView::showDetails() { if (!transactionView->selectionModel()) return; QModelIndexList selection = transactionView->selectionModel()->selectedRows(); if (!selection.isEmpty()) { TransactionDescDialog *dlg = new TransactionDescDialog(selection.at(0)); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->show(); } } void TransactionView::openThirdPartyTxUrl(QString url) { if (!transactionView || !transactionView->selectionModel()) return; QModelIndexList selection = transactionView->selectionModel()->selectedRows(0); if (!selection.isEmpty()) QDesktopServices::openUrl(QUrl::fromUserInput( url.replace("%s", selection.at(0) .data(TransactionTableModel::TxHashRole) .toString()))); } QWidget *TransactionView::createDateRangeWidget() { dateRangeWidget = new QFrame(); dateRangeWidget->setFrameStyle(QFrame::Panel | QFrame::Raised); dateRangeWidget->setContentsMargins(1, 1, 1, 1); QHBoxLayout *layout = new QHBoxLayout(dateRangeWidget); layout->setContentsMargins(0, 0, 0, 0); layout->addSpacing(23); layout->addWidget(new QLabel(tr("Range:"))); dateFrom = new QDateTimeEdit(this); dateFrom->setDisplayFormat("dd/MM/yy"); dateFrom->setCalendarPopup(true); dateFrom->setMinimumWidth(100); dateFrom->setDate(QDate::currentDate().addDays(-7)); layout->addWidget(dateFrom); layout->addWidget(new QLabel(tr("to"))); dateTo = new QDateTimeEdit(this); dateTo->setDisplayFormat("dd/MM/yy"); dateTo->setCalendarPopup(true); dateTo->setMinimumWidth(100); dateTo->setDate(QDate::currentDate()); layout->addWidget(dateTo); layout->addStretch(); // Hide by default dateRangeWidget->setVisible(false); // Notify on change connect(dateFrom, SIGNAL(dateChanged(QDate)), this, SLOT(dateRangeChanged())); connect(dateTo, SIGNAL(dateChanged(QDate)), this, SLOT(dateRangeChanged())); return dateRangeWidget; } void TransactionView::dateRangeChanged() { if (!transactionProxyModel) return; transactionProxyModel->setDateRange(QDateTime(dateFrom->date()), QDateTime(dateTo->date()).addDays(1)); } void TransactionView::focusTransaction(const QModelIndex &idx) { if (!transactionProxyModel) return; QModelIndex targetIdx = transactionProxyModel->mapFromSource(idx); transactionView->scrollTo(targetIdx); transactionView->setCurrentIndex(targetIdx); transactionView->setFocus(); } // We override the virtual resizeEvent of the QWidget to adjust tables column // sizes as the tables width is proportional to the dialogs width. void TransactionView::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); columnResizingFixer->stretchColumnWidth(TransactionTableModel::ToAddress); } // Need to override default Ctrl+C action for amount as default behaviour is // just to copy DisplayRole text bool TransactionView::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *ke = static_cast(event); if (ke->key() == Qt::Key_C && ke->modifiers().testFlag(Qt::ControlModifier)) { GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::TxPlainTextRole); return true; } } return QWidget::eventFilter(obj, event); } // show/hide column Watch-only void TransactionView::updateWatchOnlyColumn(bool fHaveWatchOnly) { watchOnlyWidget->setVisible(fHaveWatchOnly); transactionView->setColumnHidden(TransactionTableModel::Watchonly, !fHaveWatchOnly); } diff --git a/src/qt/walletview.cpp b/src/qt/walletview.cpp index 754a177e77..d2e6a7ece5 100644 --- a/src/qt/walletview.cpp +++ b/src/qt/walletview.cpp @@ -1,333 +1,333 @@ // 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 "walletview.h" #include "addressbookpage.h" #include "askpassphrasedialog.h" #include "bitcoingui.h" #include "clientmodel.h" #include "guiutil.h" #include "optionsmodel.h" #include "overviewpage.h" #include "platformstyle.h" #include "receivecoinsdialog.h" #include "sendcoinsdialog.h" #include "signverifymessagedialog.h" #include "transactiontablemodel.h" #include "transactionview.h" #include "walletmodel.h" #include "ui_interface.h" #include #include #include #include #include #include #include WalletView::WalletView(const PlatformStyle *_platformStyle, const Config *cfg, QWidget *parent) : QStackedWidget(parent), clientModel(0), walletModel(0), platformStyle(_platformStyle) { // Create tabs overviewPage = new OverviewPage(platformStyle); transactionsPage = new QWidget(this); QVBoxLayout *vbox = new QVBoxLayout(); QHBoxLayout *hbox_buttons = new QHBoxLayout(); transactionView = new TransactionView(platformStyle, this); vbox->addWidget(transactionView); QPushButton *exportButton = new QPushButton(tr("&Export"), this); exportButton->setToolTip( tr("Export the data in the current tab to a file")); if (platformStyle->getImagesOnButtons()) { exportButton->setIcon(platformStyle->SingleColorIcon(":/icons/export")); } hbox_buttons->addStretch(); hbox_buttons->addWidget(exportButton); vbox->addLayout(hbox_buttons); transactionsPage->setLayout(vbox); receiveCoinsPage = new ReceiveCoinsDialog(platformStyle, cfg); sendCoinsPage = new SendCoinsDialog(platformStyle); usedSendingAddressesPage = new AddressBookPage(platformStyle, AddressBookPage::ForEditing, AddressBookPage::SendingTab, this); usedReceivingAddressesPage = new AddressBookPage(platformStyle, AddressBookPage::ForEditing, AddressBookPage::ReceivingTab, this); addWidget(overviewPage); addWidget(transactionsPage); addWidget(receiveCoinsPage); addWidget(sendCoinsPage); // Clicking on a transaction on the overview pre-selects the transaction on // the transaction history page connect(overviewPage, SIGNAL(transactionClicked(QModelIndex)), transactionView, SLOT(focusTransaction(QModelIndex))); connect(overviewPage, SIGNAL(outOfSyncWarningClicked()), this, SLOT(requestedSyncWarningInfo())); // Double-clicking on a transaction on the transaction history page shows // details connect(transactionView, SIGNAL(doubleClicked(QModelIndex)), transactionView, SLOT(showDetails())); // Clicking on "Export" allows to export the transaction list connect(exportButton, SIGNAL(clicked()), transactionView, SLOT(exportClicked())); // Pass through messages from sendCoinsPage connect(sendCoinsPage, SIGNAL(message(QString, QString, unsigned int)), this, SIGNAL(message(QString, QString, unsigned int))); // Pass through messages from transactionView connect(transactionView, SIGNAL(message(QString, QString, unsigned int)), this, SIGNAL(message(QString, QString, unsigned int))); } WalletView::~WalletView() {} void WalletView::setBitcoinGUI(BitcoinGUI *gui) { if (gui) { // Clicking on a transaction on the overview page simply sends you to // transaction history page connect(overviewPage, SIGNAL(transactionClicked(QModelIndex)), gui, SLOT(gotoHistoryPage())); // Receive and report messages connect(this, SIGNAL(message(QString, QString, unsigned int)), gui, SLOT(message(QString, QString, unsigned int))); // Pass through encryption status changed signals connect(this, SIGNAL(encryptionStatusChanged(int)), gui, SLOT(setEncryptionStatus(int))); // Pass through transaction notifications - connect(this, SIGNAL(incomingTransaction(QString, int, CAmount, QString, + connect(this, SIGNAL(incomingTransaction(QString, int, Amount, QString, QString, QString)), - gui, SLOT(incomingTransaction(QString, int, CAmount, QString, + gui, SLOT(incomingTransaction(QString, int, Amount, QString, QString, QString))); // Connect HD enabled state signal connect(this, SIGNAL(hdEnabledStatusChanged(int)), gui, SLOT(setHDStatus(int))); } } void WalletView::setClientModel(ClientModel *_clientModel) { this->clientModel = _clientModel; overviewPage->setClientModel(_clientModel); sendCoinsPage->setClientModel(_clientModel); } void WalletView::setWalletModel(WalletModel *_walletModel) { this->walletModel = _walletModel; // Put transaction list in tabs transactionView->setModel(_walletModel); overviewPage->setWalletModel(_walletModel); receiveCoinsPage->setModel(_walletModel); sendCoinsPage->setModel(_walletModel); usedReceivingAddressesPage->setModel(_walletModel->getAddressTableModel()); usedSendingAddressesPage->setModel(_walletModel->getAddressTableModel()); if (_walletModel) { // Receive and pass through messages from wallet model connect(_walletModel, SIGNAL(message(QString, QString, unsigned int)), this, SIGNAL(message(QString, QString, unsigned int))); // Handle changes in encryption status connect(_walletModel, SIGNAL(encryptionStatusChanged(int)), this, SIGNAL(encryptionStatusChanged(int))); updateEncryptionStatus(); // update HD status Q_EMIT hdEnabledStatusChanged(_walletModel->hdEnabled()); // Balloon pop-up for new transaction connect(_walletModel->getTransactionTableModel(), SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(processNewTransaction(QModelIndex, int, int))); // Ask for passphrase if needed connect(_walletModel, SIGNAL(requireUnlock()), this, SLOT(unlockWallet())); // Show progress dialog connect(_walletModel, SIGNAL(showProgress(QString, int)), this, SLOT(showProgress(QString, int))); } } void WalletView::processNewTransaction(const QModelIndex &parent, int start, int /*end*/) { // Prevent balloon-spam when initial block download is in progress if (!walletModel || !clientModel || clientModel->inInitialBlockDownload()) return; TransactionTableModel *ttm = walletModel->getTransactionTableModel(); if (!ttm || ttm->processingQueuedTransactions()) return; QString date = ttm->index(start, TransactionTableModel::Date, parent) .data() .toString(); qint64 amount = ttm->index(start, TransactionTableModel::Amount, parent) .data(Qt::EditRole) .toULongLong(); QString type = ttm->index(start, TransactionTableModel::Type, parent) .data() .toString(); QModelIndex index = ttm->index(start, 0, parent); QString address = ttm->data(index, TransactionTableModel::AddressRole).toString(); QString label = ttm->data(index, TransactionTableModel::LabelRole).toString(); Q_EMIT incomingTransaction(date, walletModel->getOptionsModel()->getDisplayUnit(), amount, type, address, label); } void WalletView::gotoOverviewPage() { setCurrentWidget(overviewPage); } void WalletView::gotoHistoryPage() { setCurrentWidget(transactionsPage); } void WalletView::gotoReceiveCoinsPage() { setCurrentWidget(receiveCoinsPage); } void WalletView::gotoSendCoinsPage(QString addr) { setCurrentWidget(sendCoinsPage); if (!addr.isEmpty()) sendCoinsPage->setAddress(addr); } void WalletView::gotoSignMessageTab(QString addr) { // calls show() in showTab_SM() SignVerifyMessageDialog *signVerifyMessageDialog = new SignVerifyMessageDialog(platformStyle, this); signVerifyMessageDialog->setAttribute(Qt::WA_DeleteOnClose); signVerifyMessageDialog->setModel(walletModel); signVerifyMessageDialog->showTab_SM(true); if (!addr.isEmpty()) signVerifyMessageDialog->setAddress_SM(addr); } void WalletView::gotoVerifyMessageTab(QString addr) { // calls show() in showTab_VM() SignVerifyMessageDialog *signVerifyMessageDialog = new SignVerifyMessageDialog(platformStyle, this); signVerifyMessageDialog->setAttribute(Qt::WA_DeleteOnClose); signVerifyMessageDialog->setModel(walletModel); signVerifyMessageDialog->showTab_VM(true); if (!addr.isEmpty()) signVerifyMessageDialog->setAddress_VM(addr); } bool WalletView::handlePaymentRequest(const SendCoinsRecipient &recipient) { return sendCoinsPage->handlePaymentRequest(recipient); } void WalletView::showOutOfSyncWarning(bool fShow) { overviewPage->showOutOfSyncWarning(fShow); } void WalletView::updateEncryptionStatus() { Q_EMIT encryptionStatusChanged(walletModel->getEncryptionStatus()); } void WalletView::encryptWallet(bool status) { if (!walletModel) return; AskPassphraseDialog dlg(status ? AskPassphraseDialog::Encrypt : AskPassphraseDialog::Decrypt, this); dlg.setModel(walletModel); dlg.exec(); updateEncryptionStatus(); } void WalletView::backupWallet() { QString filename = GUIUtil::getSaveFileName(this, tr("Backup Wallet"), QString(), tr("Wallet Data (*.dat)"), nullptr); if (filename.isEmpty()) return; if (!walletModel->backupWallet(filename)) { Q_EMIT message( tr("Backup Failed"), tr("There was an error trying to save the wallet data to %1.") .arg(filename), CClientUIInterface::MSG_ERROR); } else { Q_EMIT message( tr("Backup Successful"), tr("The wallet data was successfully saved to %1.").arg(filename), CClientUIInterface::MSG_INFORMATION); } } void WalletView::changePassphrase() { AskPassphraseDialog dlg(AskPassphraseDialog::ChangePass, this); dlg.setModel(walletModel); dlg.exec(); } void WalletView::unlockWallet() { if (!walletModel) return; // Unlock wallet when requested by wallet model if (walletModel->getEncryptionStatus() == WalletModel::Locked) { AskPassphraseDialog dlg(AskPassphraseDialog::Unlock, this); dlg.setModel(walletModel); dlg.exec(); } } void WalletView::usedSendingAddresses() { if (!walletModel) return; usedSendingAddressesPage->show(); usedSendingAddressesPage->raise(); usedSendingAddressesPage->activateWindow(); } void WalletView::usedReceivingAddresses() { if (!walletModel) return; usedReceivingAddressesPage->show(); usedReceivingAddressesPage->raise(); usedReceivingAddressesPage->activateWindow(); } void WalletView::showProgress(const QString &title, int nProgress) { if (nProgress == 0) { progressDialog = new QProgressDialog(title, "", 0, 100); progressDialog->setWindowModality(Qt::ApplicationModal); progressDialog->setMinimumDuration(0); progressDialog->setCancelButton(0); progressDialog->setAutoClose(false); progressDialog->setValue(0); } else if (nProgress == 100) { if (progressDialog) { progressDialog->close(); progressDialog->deleteLater(); } } else if (progressDialog) progressDialog->setValue(nProgress); } void WalletView::requestedSyncWarningInfo() { Q_EMIT outOfSyncWarningClicked(); } diff --git a/src/qt/walletview.h b/src/qt/walletview.h index 00cc9520c7..2721312d38 100644 --- a/src/qt/walletview.h +++ b/src/qt/walletview.h @@ -1,141 +1,141 @@ // 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_WALLETVIEW_H #define BITCOIN_QT_WALLETVIEW_H #include "amount.h" #include class BitcoinGUI; class ClientModel; class OverviewPage; class PlatformStyle; class ReceiveCoinsDialog; class SendCoinsDialog; class SendCoinsRecipient; class TransactionView; class WalletModel; class AddressBookPage; class Config; QT_BEGIN_NAMESPACE class QModelIndex; class QProgressDialog; QT_END_NAMESPACE /* WalletView class. This class represents the view to a single wallet. It was added to support multiple wallet functionality. Each wallet gets its own WalletView instance. It communicates with both the client and the wallet models to give the user an up-to-date view of the current core state. */ class WalletView : public QStackedWidget { Q_OBJECT public: explicit WalletView(const PlatformStyle *platformStyle, const Config *cfg, QWidget *parent); ~WalletView(); void setBitcoinGUI(BitcoinGUI *gui); /** Set the client model. The client model represents the part of the core that communicates with the P2P network, and is wallet-agnostic. */ void setClientModel(ClientModel *clientModel); /** Set the wallet model. The wallet model represents a bitcoin wallet, and offers access to the list of transactions, address book and sending functionality. */ void setWalletModel(WalletModel *walletModel); bool handlePaymentRequest(const SendCoinsRecipient &recipient); void showOutOfSyncWarning(bool fShow); private: ClientModel *clientModel; WalletModel *walletModel; OverviewPage *overviewPage; QWidget *transactionsPage; ReceiveCoinsDialog *receiveCoinsPage; SendCoinsDialog *sendCoinsPage; AddressBookPage *usedSendingAddressesPage; AddressBookPage *usedReceivingAddressesPage; TransactionView *transactionView; QProgressDialog *progressDialog; const PlatformStyle *platformStyle; public Q_SLOTS: /** Switch to overview (home) page */ void gotoOverviewPage(); /** Switch to history (transactions) page */ void gotoHistoryPage(); /** Switch to receive coins page */ void gotoReceiveCoinsPage(); /** Switch to send coins page */ void gotoSendCoinsPage(QString addr = ""); /** Show Sign/Verify Message dialog and switch to sign message tab */ void gotoSignMessageTab(QString addr = ""); /** Show Sign/Verify Message dialog and switch to verify message tab */ void gotoVerifyMessageTab(QString addr = ""); /** Show incoming transaction notification for new transactions. The new items are those between start and end inclusive, under the given parent item. */ void processNewTransaction(const QModelIndex &parent, int start, int /*end*/); /** Encrypt the wallet */ void encryptWallet(bool status); /** Backup the wallet */ void backupWallet(); /** Change encrypted wallet passphrase */ void changePassphrase(); /** Ask for passphrase to unlock wallet temporarily */ void unlockWallet(); /** Show used sending addresses */ void usedSendingAddresses(); /** Show used receiving addresses */ void usedReceivingAddresses(); /** Re-emit encryption status signal */ void updateEncryptionStatus(); /** Show progress dialog e.g. for rescan */ void showProgress(const QString &title, int nProgress); /** User has requested more information about the out of sync state */ void requestedSyncWarningInfo(); Q_SIGNALS: /** Signal that we want to show the main window */ void showNormalIfMinimized(); /** Fired when a message should be reported to the user */ void message(const QString &title, const QString &message, unsigned int style); /** Encryption status of wallet changed */ void encryptionStatusChanged(int status); /** HD-Enabled status of wallet changed (only possible during startup) */ void hdEnabledStatusChanged(int hdEnabled); /** Notify that a new transaction appeared */ - void incomingTransaction(const QString &date, int unit, - const CAmount &amount, const QString &type, - const QString &address, const QString &label); + void incomingTransaction(const QString &date, int unit, const Amount amount, + const QString &type, const QString &address, + const QString &label); /** Notify that the out of sync warning icon has been pressed */ void outOfSyncWarningClicked(); }; #endif // BITCOIN_QT_WALLETVIEW_H