diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 82ed8bf14..8b1bdccf1 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -1,1015 +1,1018 @@ // 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 QUrl &uri, SendCoinsRecipient *out) { - // return if URI is not valid or is no bitcoincash: URI - if (!uri.isValid() || uri.scheme() != URI_SCHEME) return false; +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)) { return false; } rv.amount = amt.GetSatoshis(); } fShouldReturnFalse = false; } if (fShouldReturnFalse) { return false; } } if (out) { *out = rv; } return true; } -bool parseBitcoinURI(QString uri, SendCoinsRecipient *out) { - // Convert bitcoincash:// to bitcoincash: +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(URI_SCHEME + "://", Qt::CaseInsensitive)) { - uri.replace(0, URI_SCHEME.length() + 3, URI_SCHEME + ":"); + if (uri.startsWith(scheme + "://", Qt::CaseInsensitive)) { + uri.replace(0, scheme.length() + 3, scheme + ":"); } QUrl uriInstance(uri); - return parseBitcoinURI(uriInstance, out); + 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) { 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/guiutil.h b/src/qt/guiutil.h index 4ff5cdf12..d14946d77 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -1,274 +1,276 @@ // 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_GUIUTIL_H #define BITCOIN_QT_GUIUTIL_H #include "amount.h" #include #include #include #include #include #include #include #include #include class QValidatedLineEdit; class SendCoinsRecipient; class CChainParams; class Config; QT_BEGIN_NAMESPACE class QAbstractItemView; class QDateTime; class QFont; class QLineEdit; class QUrl; class QWidget; QT_END_NAMESPACE /** Utility functions used by the Bitcoin Qt UI. */ namespace GUIUtil { extern const QString URI_SCHEME; // Create human-readable string from date QString dateTimeStr(const QDateTime &datetime); QString dateTimeStr(qint64 nTime); // Return a monospace font QFont fixedPitchFont(); // Generate an invalid, but convincing address. std::string DummyAddress(const CChainParams ¶ms, const Config &cfg); // Set up widgets for address and amounts void setupAddressWidget(QValidatedLineEdit *widget, QWidget *parent); void setupAmountWidget(QLineEdit *widget, QWidget *parent); // Parse "bitcoincash:" URI into recipient object, return true on successful // parsing -bool parseBitcoinURI(const QUrl &uri, SendCoinsRecipient *out); -bool parseBitcoinURI(QString uri, SendCoinsRecipient *out); +bool parseBitcoinURI(const QString &scheme, const QUrl &uri, + SendCoinsRecipient *out); +bool parseBitcoinURI(const QString &scheme, QString uri, + SendCoinsRecipient *out); QString formatBitcoinURI(const Config &cfg, const SendCoinsRecipient &info); // Returns true if given address+amount meets "dust" definition bool isDust(const QString &address, const Amount amount); // HTML escaping for rich text controls QString HtmlEscape(const QString &str, bool fMultiLine = false); QString HtmlEscape(const std::string &str, bool fMultiLine = false); /** Copy a field of the currently selected entry of a view to the clipboard. Does nothing if nothing is selected. @param[in] column Data column to extract from the model @param[in] role Data role to extract from the model @see TransactionView::copyLabel, TransactionView::copyAmount, TransactionView::copyAddress */ void copyEntryData(QAbstractItemView *view, int column, int role = Qt::EditRole); /** Return a field of the currently selected entry as a QString. Does nothing if nothing is selected. @param[in] column Data column to extract from the model @see TransactionView::copyLabel, TransactionView::copyAmount, TransactionView::copyAddress */ QList getEntryData(QAbstractItemView *view, int column); void setClipboard(const QString &str); /** Get save filename, mimics QFileDialog::getSaveFileName, except that it appends a default suffix when no suffix is provided by the user. @param[in] parent Parent window (or 0) @param[in] caption Window caption (or empty, for default) @param[in] dir Starting directory (or empty, to default to documents directory) @param[in] filter Filter specification such as "Comma Separated Files (*.csv)" @param[out] selectedSuffixOut Pointer to return the suffix (file type) that was selected (or 0). Can be useful when choosing the save file format based on suffix. */ QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedSuffixOut); /** Get open filename, convenience wrapper for QFileDialog::getOpenFileName. @param[in] parent Parent window (or 0) @param[in] caption Window caption (or empty, for default) @param[in] dir Starting directory (or empty, to default to documents directory) @param[in] filter Filter specification such as "Comma Separated Files (*.csv)" @param[out] selectedSuffixOut Pointer to return the suffix (file type) that was selected (or 0). Can be useful when choosing the save file format based on suffix. */ QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedSuffixOut); /** Get connection type to call object slot in GUI thread with invokeMethod. The call will be blocking. @returns If called from the GUI thread, return a Qt::DirectConnection. If called from another thread, return a Qt::BlockingQueuedConnection. */ Qt::ConnectionType blockingGUIThreadConnection(); // Determine whether a widget is hidden behind other windows bool isObscured(QWidget *w); // Open debug.log void openDebugLogfile(); // Replace invalid default fonts with known good ones void SubstituteFonts(const QString &language); /** Qt event filter that intercepts ToolTipChange events, and replaces the * tooltip with a rich text representation if needed. This assures that Qt can * word-wrap long tooltip messages. Tooltips longer than the provided size * threshold (in characters) are wrapped. */ class ToolTipToRichTextFilter : public QObject { Q_OBJECT public: explicit ToolTipToRichTextFilter(int size_threshold, QObject *parent = 0); protected: bool eventFilter(QObject *obj, QEvent *evt); private: int size_threshold; }; /** * Makes a QTableView last column feel as if it was being resized from its left * border. * Also makes sure the column widths are never larger than the table's viewport. * In Qt, all columns are resizable from the right, but it's not intuitive * resizing the last column from the right. * Usually our second to last columns behave as if stretched, and when on strech * mode, columns aren't resizable interactively or programmatically. * * This helper object takes care of this issue. * */ class TableViewLastColumnResizingFixer : public QObject { Q_OBJECT public: TableViewLastColumnResizingFixer(QTableView *table, int lastColMinimumWidth, int allColsMinimumWidth, QObject *parent); void stretchColumnWidth(int column); private: QTableView *tableView; int lastColumnMinimumWidth; int allColumnsMinimumWidth; int lastColumnIndex; int columnCount; int secondToLastColumnIndex; void adjustTableColumnsWidth(); int getAvailableWidthForColumn(int column); int getColumnsWidth(); void connectViewHeadersSignals(); void disconnectViewHeadersSignals(); void setViewHeaderResizeMode(int logicalIndex, QHeaderView::ResizeMode resizeMode); void resizeColumn(int nColumnIndex, int width); private Q_SLOTS: void on_sectionResized(int logicalIndex, int oldSize, int newSize); void on_geometriesChanged(); }; bool GetStartOnSystemStartup(); bool SetStartOnSystemStartup(bool fAutoStart); /** Save window size and position */ void saveWindowGeometry(const QString &strSetting, QWidget *parent); /** Restore window size and position */ void restoreWindowGeometry(const QString &strSetting, const QSize &defaultSizeIn, QWidget *parent); /* Convert QString to OS specific boost path through UTF-8 */ boost::filesystem::path qstringToBoostPath(const QString &path); /* Convert OS specific boost path to QString through UTF-8 */ QString boostPathToQString(const boost::filesystem::path &path); /* Convert seconds into a QString with days, hours, mins, secs */ QString formatDurationStr(int secs); /* Format CNodeStats.nServices bitmask into a user-readable string */ QString formatServicesStr(quint64 mask); /* Format a CNodeCombinedStats.dPingTime into a user-readable string or display * N/A, if 0*/ QString formatPingTime(double dPingTime); /* Format a CNodeCombinedStats.nTimeOffset into a user-readable string. */ QString formatTimeOffset(int64_t nTimeOffset); QString formatNiceTimeOffset(qint64 secs); class ClickableLabel : public QLabel { Q_OBJECT Q_SIGNALS: /** Emitted when the label is clicked. The relative mouse coordinates of the * click are passed to the signal. */ void clicked(const QPoint &point); protected: void mouseReleaseEvent(QMouseEvent *event); }; class ClickableProgressBar : public QProgressBar { Q_OBJECT Q_SIGNALS: /** Emitted when the progressbar is clicked. The relative mouse coordinates * of the click are passed to the signal. */ void clicked(const QPoint &point); protected: void mouseReleaseEvent(QMouseEvent *event); }; #if defined(Q_OS_MAC) && QT_VERSION >= 0x050000 // workaround for Qt OSX Bug: // https://bugreports.qt-project.org/browse/QTBUG-15631 // QProgressBar uses around 10% CPU even when app is in background class ProgressBar : public ClickableProgressBar { bool event(QEvent *e) { return (e->type() != QEvent::StyleAnimationUpdate) ? QProgressBar::event(e) : false; } }; #else typedef ClickableProgressBar ProgressBar; #endif } // namespace GUIUtil #endif // BITCOIN_QT_GUIUTIL_H diff --git a/src/qt/openuridialog.cpp b/src/qt/openuridialog.cpp index 43acf2b72..7d9b6779d 100644 --- a/src/qt/openuridialog.cpp +++ b/src/qt/openuridialog.cpp @@ -1,46 +1,46 @@ // 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. #include "openuridialog.h" #include "ui_openuridialog.h" #include "guiutil.h" #include "walletmodel.h" #include OpenURIDialog::OpenURIDialog(const Config *cfg, QWidget *parent) : QDialog(parent), ui(new Ui::OpenURIDialog), cfg(cfg) { ui->setupUi(this); #if QT_VERSION >= 0x040700 ui->uriEdit->setPlaceholderText(GUIUtil::URI_SCHEME + ":"); #endif } OpenURIDialog::~OpenURIDialog() { delete ui; } QString OpenURIDialog::getURI() { return ui->uriEdit->text(); } void OpenURIDialog::accept() { SendCoinsRecipient rcp; - if (GUIUtil::parseBitcoinURI(getURI(), &rcp)) { + if (GUIUtil::parseBitcoinURI(GUIUtil::URI_SCHEME, getURI(), &rcp)) { /* Only accept value URIs */ QDialog::accept(); } else { ui->uriEdit->setValid(false); } } void OpenURIDialog::on_selectFileButton_clicked() { QString filename = GUIUtil::getOpenFileName( this, tr("Select payment request file to open"), "", "", nullptr); if (filename.isEmpty()) return; QUrl fileUri = QUrl::fromLocalFile(filename); ui->uriEdit->setText(GUIUtil::URI_SCHEME + ":?r=" + QUrl::toPercentEncoding(fileUri.toString())); } diff --git a/src/qt/paymentserver.cpp b/src/qt/paymentserver.cpp index e09914eb7..9850f98d1 100644 --- a/src/qt/paymentserver.cpp +++ b/src/qt/paymentserver.cpp @@ -1,818 +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(arg, &r) && !r.address.isEmpty()) { + 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(s, &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(); QStringList addresses; 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 // 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)) { 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 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(MAX_MONEY.GetSatoshis()); } return fVerified; } X509_STORE *PaymentServer::getCertStore() { return certStore.get(); } diff --git a/src/qt/test/uritests.cpp b/src/qt/test/uritests.cpp index 095cdfbdc..8029aa7c0 100644 --- a/src/qt/test/uritests.cpp +++ b/src/qt/test/uritests.cpp @@ -1,199 +1,205 @@ // Copyright (c) 2009-2014 The Bitcoin Core developers +// Copyright (c) 2017 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "uritests.h" +#include "chainparams.h" #include "config.h" #include "guiutil.h" #include "walletmodel.h" #include void URITests::uriTestsBase58() { SendCoinsRecipient rv; + QString scheme = + QString::fromStdString(Params(CBaseChainParams::MAIN).CashAddrPrefix()); QUrl uri; - uri.setUrl(QString( - "bitcoincash:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?req-dontexist=")); - QVERIFY(!GUIUtil::parseBitcoinURI(uri, &rv)); + uri.setUrl(QString("175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?req-dontexist=")); + QVERIFY(!GUIUtil::parseBitcoinURI(scheme, uri, &rv)); uri.setUrl( QString("bitcoincash:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?dontexist=")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); QVERIFY(rv.address == QString("175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W")); QVERIFY(rv.label == QString()); QVERIFY(rv.amount == 0); uri.setUrl(QString("bitcoincash:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?label=" "Wikipedia Example Address")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); QVERIFY(rv.address == QString("175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W")); QVERIFY(rv.label == QString("Wikipedia Example Address")); QVERIFY(rv.amount == 0); uri.setUrl( QString("bitcoincash:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=0.001")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); QVERIFY(rv.address == QString("175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W")); QVERIFY(rv.label == QString()); QVERIFY(rv.amount == 100000); uri.setUrl( QString("bitcoincash:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=1.001")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); QVERIFY(rv.address == QString("175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W")); QVERIFY(rv.label == QString()); QVERIFY(rv.amount == 100100000); uri.setUrl( QString("bitcoincash:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=100&" "label=Wikipedia Example")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); QVERIFY(rv.address == QString("175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W")); QVERIFY(rv.amount == 10000000000LL); QVERIFY(rv.label == QString("Wikipedia Example")); uri.setUrl(QString("bitcoincash:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?message=" "Wikipedia Example Address")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); QVERIFY(rv.address == QString("175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W")); QVERIFY(rv.label == QString()); - QVERIFY(GUIUtil::parseBitcoinURI("bitcoincash://" + QVERIFY(GUIUtil::parseBitcoinURI(scheme, + "bitcoincash://" "175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?" "message=Wikipedia Example Address", &rv)); QVERIFY(rv.address == QString("175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W")); QVERIFY(rv.label == QString()); uri.setUrl( QString("bitcoincash:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?req-message=" "Wikipedia Example Address")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); uri.setUrl( QString("bitcoincash:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=1," "000&label=Wikipedia Example")); - QVERIFY(!GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(!GUIUtil::parseBitcoinURI(scheme, uri, &rv)); uri.setUrl( QString("bitcoincash:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=1," "000.0&label=Wikipedia Example")); - QVERIFY(!GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(!GUIUtil::parseBitcoinURI(scheme, uri, &rv)); } void URITests::uriTestsCashAddr() { SendCoinsRecipient rv; QUrl uri; + QString scheme = + QString::fromStdString(Params(CBaseChainParams::MAIN).CashAddrPrefix()); uri.setUrl(QString("bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a?" "req-dontexist=")); - QVERIFY(!GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(!GUIUtil::parseBitcoinURI(scheme, uri, &rv)); uri.setUrl(QString( "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a?dontexist=")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); QVERIFY(rv.address == QString("bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a")); QVERIFY(rv.label == QString()); QVERIFY(rv.amount == 0); uri.setUrl( QString("bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a?label=" "Wikipedia Example Address")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); QVERIFY(rv.address == QString("bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a")); QVERIFY(rv.label == QString("Wikipedia Example Address")); QVERIFY(rv.amount == 0); uri.setUrl(QString( "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a?amount=0.001")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); QVERIFY(rv.address == QString("bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a")); QVERIFY(rv.label == QString()); QVERIFY(rv.amount == 100000); uri.setUrl(QString( "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a?amount=1.001")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); QVERIFY(rv.address == QString("bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a")); QVERIFY(rv.label == QString()); QVERIFY(rv.amount == 100100000); uri.setUrl(QString( "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a?amount=100&" "label=Wikipedia Example")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); QVERIFY(rv.address == QString("bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a")); QVERIFY(rv.amount == 10000000000LL); QVERIFY(rv.label == QString("Wikipedia Example")); uri.setUrl(QString( "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a?message=" "Wikipedia Example Address")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); QVERIFY(rv.address == QString("bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a")); QVERIFY(rv.label == QString()); - QVERIFY( - GUIUtil::parseBitcoinURI("bitcoincash://" - "qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a?" - "message=Wikipedia Example Address", - &rv)); + QVERIFY(GUIUtil::parseBitcoinURI( + scheme, "bitcoincash://" + "qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a?" + "message=Wikipedia Example Address", + &rv)); QVERIFY(rv.address == QString("bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a")); QVERIFY(rv.label == QString()); uri.setUrl(QString( "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a?req-message=" "Wikipedia Example Address")); - QVERIFY(GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(GUIUtil::parseBitcoinURI(scheme, uri, &rv)); uri.setUrl(QString( "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a?amount=1," "000&label=Wikipedia Example")); - QVERIFY(!GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(!GUIUtil::parseBitcoinURI(scheme, uri, &rv)); uri.setUrl(QString( "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a?amount=1," "000.0&label=Wikipedia Example")); - QVERIFY(!GUIUtil::parseBitcoinURI(uri, &rv)); + QVERIFY(!GUIUtil::parseBitcoinURI(scheme, uri, &rv)); } namespace { class UriTestConfig : public DummyConfig { public: UriTestConfig(bool useCashAddr) : useCashAddr(useCashAddr) {} bool UseCashAddrEncoding() const override { return useCashAddr; } private: bool useCashAddr; }; } // anon ns void URITests::uriTestFormatURI() { { UriTestConfig cfg(true); SendCoinsRecipient r; r.address = "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a"; r.message = "test"; QString uri = GUIUtil::formatBitcoinURI(cfg, r); QVERIFY(uri == "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a?" "message=test"); } { UriTestConfig cfg(false); SendCoinsRecipient r; r.address = "175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W"; r.message = "test"; QString uri = GUIUtil::formatBitcoinURI(cfg, r); QVERIFY(uri == "bitcoincash:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?message=test"); } }