diff --git a/LeftPanel.qml b/LeftPanel.qml index 8c6ce0cf..82d5b76d 100644 --- a/LeftPanel.qml +++ b/LeftPanel.qml @@ -32,6 +32,7 @@ import QtGraphicalEffects 1.0 import moneroComponents.Wallet 1.0 import moneroComponents.NetworkType 1.0 import moneroComponents.Clipboard 1.0 +import FontAwesome 1.0 import "components" as MoneroComponents import "components/effects/" as MoneroEffects @@ -44,10 +45,13 @@ Rectangle { property alias unlockedBalanceLabelVisible: unlockedBalanceLabel.visible property alias balanceLabelText: balanceLabel.text property alias balanceText: balanceText.text + property alias balanceTextFiat: balanceTextFiat.text + property alias unlockedBalanceTextFiat: unlockedBalanceTextFiat.text property alias networkStatus : networkStatus property alias progressBar : progressBar property alias daemonProgressBar : daemonProgressBar property alias minutesToUnlockTxt: unlockedBalanceLabel.text + property bool fiatBalance: false property int titleBarHeight: 50 property string copyValue: "" Clipboard { id: clipboard } @@ -202,6 +206,26 @@ Rectangle { } } } + MoneroComponents.Label { + fontSize: 20 + text: "¥" + color: "white" + visible: persistentSettings.fiatPriceEnabled + anchors.right: parent.right + anchors.rightMargin: 45 + anchors.top: parent.top + anchors.topMargin: 28 + themeTransition: false + + MouseArea{ + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + fiatBalance = !fiatBalance + } + } + } } Item { @@ -214,7 +238,7 @@ Rectangle { width: 50 MoneroComponents.TextPlain { - visible: !isMobile + visible: !(fiatBalance && persistentSettings.fiatPriceEnabled) id: balanceText themeTransition: false anchors.left: parent.left @@ -255,9 +279,39 @@ Rectangle { } } + MoneroComponents.TextPlain { + visible: !balanceText.visible + id: balanceTextFiat + themeTransition: false + anchors.left: parent.left + anchors.leftMargin: 20 + anchors.top: parent.top + anchors.topMargin: 76 + font.family: "Arial" + color: "#FFFFFF" + text: "N/A" + font.pixelSize: balanceText.font.pixelSize + MouseArea { + hoverEnabled: true + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onEntered: { + parent.color = MoneroComponents.Style.orange + } + onExited: { + parent.color = MoneroComponents.Style.white + } + onClicked: { + console.log("Copied to clipboard"); + clipboard.setText(parent.text); + appWindow.showStatusMessage(qsTr("Copied to clipboard"),3) + } + } + } + MoneroComponents.TextPlain { id: unlockedBalanceText - visible: true + visible: !(fiatBalance && persistentSettings.fiatPriceEnabled) themeTransition: false anchors.left: parent.left anchors.leftMargin: 20 @@ -297,6 +351,36 @@ Rectangle { } } + MoneroComponents.TextPlain { + id: unlockedBalanceTextFiat + themeTransition: false + visible: !unlockedBalanceText.visible + anchors.left: parent.left + anchors.leftMargin: 20 + anchors.top: parent.top + anchors.topMargin: 126 + font.family: "Arial" + color: "#FFFFFF" + text: "N/A" + font.pixelSize: unlockedBalanceText.font.pixelSize + MouseArea { + hoverEnabled: true + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onEntered: { + parent.color = MoneroComponents.Style.orange + } + onExited: { + parent.color = MoneroComponents.Style.white + } + onClicked: { + console.log("Copied to clipboard"); + clipboard.setText(parent.text); + appWindow.showStatusMessage(qsTr("Copied to clipboard"),3) + } + } + } + MoneroComponents.Label { id: unlockedBalanceLabel visible: true diff --git a/js/Utils.js b/js/Utils.js index f5a7d828..1dc359d9 100644 --- a/js/Utils.js +++ b/js/Utils.js @@ -154,3 +154,20 @@ function qmlEach(item, properties, ignoredObjectNames, arr){ return arr; } + +function capitalize(s){ + if (typeof s !== 'string') return '' + return s.charAt(0).toUpperCase() + s.slice(1) +} + +function formatMoney(n, c, d, t) { + // https://stackoverflow.com/a/149099 + var c = isNaN(c = Math.abs(c)) ? 2 : c, + d = d == undefined ? "." : d, + t = t == undefined ? "," : t, + s = n < 0 ? "-" : "", + i = String(parseInt(n = Math.abs(Number(n) || 0).toFixed(c))), + j = (j = i.length) > 3 ? j % 3 : 0; + + return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : ""); +}; diff --git a/main.cpp b/main.cpp index 8b693215..a51568d2 100644 --- a/main.cpp +++ b/main.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -65,6 +66,7 @@ #include "qt/utils.h" #include "qt/mime.h" #include "src/qt/KeysFiles.h" +#include "qt/prices.h" // IOS exclusions #ifndef Q_OS_IOS @@ -356,6 +358,11 @@ int main(int argc, char *argv[]) builtWithScanner = true; #endif engine.rootContext()->setContextProperty("builtWithScanner", builtWithScanner); + + QNetworkAccessManager *manager = new QNetworkAccessManager(); + Prices prices(manager); + engine.rootContext()->setContextProperty("Prices", &prices); + // Load main window (context properties needs to be defined obove this line) engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); if (engine.rootObjects().isEmpty()) diff --git a/main.qml b/main.qml index 076d2f6f..3da05b4d 100644 --- a/main.qml +++ b/main.qml @@ -91,6 +91,26 @@ ApplicationWindow { property int appEpoch: Math.floor((new Date).getTime() / 1000) property bool themeTransition: false + // fiat price conversion + property int fiatPriceXMRUSD: 0 + property int fiatPriceXMREUR: 0 + property var fiatPriceAPIs: { + return { + "kraken": { + "xmrusd": "https://api.kraken.com/0/public/Ticker?pair=XMRUSD", + "xmreur": "https://api.kraken.com/0/public/Ticker?pair=XMREUR" + }, + "coingecko": { + "xmrusd": "https://api.coingecko.com/api/v3/simple/price?ids=monero&vs_currencies=usd", + "xmreur": "https://api.coingecko.com/api/v3/simple/price?ids=monero&vs_currencies=eur" + }, + "cryptocompare": { + "xmrusd": "https://min-api.cryptocompare.com/data/price?fsym=XMR&tsyms=USD", + "xmreur": "https://min-api.cryptocompare.com/data/price?fsym=XMR&tsyms=EUR", + } + } + } + property string remoteNodeService: { // support user-defined remote node aggregators if(persistentSettings.remoteNodeService){ @@ -394,6 +414,10 @@ ApplicationWindow { middlePanel.balanceText = balance; leftPanel.balanceText = balance; + if (persistentSettings.fiatPriceEnabled) { + appWindow.fiatApiUpdateBalance(balance, balance_unlocked); + } + var accountLabel = currentWallet.getSubaddressLabel(currentWallet.currentSubaddressAccount, 0); leftPanel.balanceLabelText = qsTr("Balance (#%1%2)").arg(currentWallet.currentSubaddressAccount).arg(accountLabel === "" ? "" : (" – " + accountLabel)); } @@ -1102,6 +1126,7 @@ ApplicationWindow { rootItem.state = "wizard" // reset balance leftPanel.balanceText = leftPanel.unlockedBalanceText = walletManager.displayAmount(0); + fiatApiUpdateBalance(0, 0); // disable timers userInActivityTimer.running = false; simpleModeConnectionTimer.running = false; @@ -1126,6 +1151,143 @@ ApplicationWindow { flags: persistentSettings.customDecorations ? Windows.flagsCustomDecorations : Windows.flags onWidthChanged: x -= 0 + Timer { + id: fiatPriceTimer + interval: 1000 * 60; + running: persistentSettings.fiatPriceEnabled; + repeat: true + onTriggered: { + if(persistentSettings.fiatPriceEnabled) + appWindow.fiatApiRefresh(); + } + triggeredOnStart: false + } + + function fiatApiParseTicker(resp, currency){ + // parse & validate incoming JSON + if(resp._url.startsWith("https://api.kraken.com/0/")){ + if(resp.hasOwnProperty("error") && resp.error.length > 0 || !resp.hasOwnProperty("result")){ + appWindow.fiatApiError("Kraken API has error(s)"); + return; + } + + var key = currency === "xmreur" ? "XXMRZEUR" : "XXMRZUSD"; + var ticker = resp.result[key]["o"]; + return ticker; + } else if(resp._url.startsWith("https://api.coingecko.com/api/v3/")){ + var key = currency === "xmreur" ? "eur" : "usd"; + if(!resp.hasOwnProperty("monero") || !resp["monero"].hasOwnProperty(key)){ + appWindow.fiatApiError("Coingecko API has error(s)"); + return; + } + return resp["monero"][key]; + } else if(resp._url.startsWith("https://min-api.cryptocompare.com/data/")){ + var key = currency === "xmreur" ? "EUR" : "USD"; + if(!resp.hasOwnProperty(key)){ + appWindow.fiatApiError("cryptocompare API has error(s)"); + return; + } + return resp[key]; + } + } + + function fiatApiGetCurrency(resp){ + // map response to `appWindow.fiatPriceAPIs` object + if (!resp.hasOwnProperty('_url')){ + appWindow.fiatApiError("invalid JSON"); + return; + } + + var apis = appWindow.fiatPriceAPIs; + for (var api in apis){ + if (!apis.hasOwnProperty(api)) + continue; + + for (var cur in apis[api]){ + if(!apis[api].hasOwnProperty(cur)) + continue; + + var url = apis[api][cur]; + if(url === resp._url){ + return cur; + } + } + } + } + + function fiatApiJsonReceived(resp){ + // handle incoming JSON, set ticker + var currency = appWindow.fiatApiGetCurrency(resp); + if(typeof currency == "undefined"){ + appWindow.fiatApiError("could not get currency"); + return; + } + + var ticker = appWindow.fiatApiParseTicker(resp, currency); + if(ticker <= 0){ + appWindow.fiatApiError("could not get ticker"); + return; + } + + if(persistentSettings.fiatPriceCurrency === "xmrusd") + appWindow.fiatPriceXMRUSD = ticker; + else if(persistentSettings.fiatPriceCurrency === "xmreur") + appWindow.fiatPriceXMREUR = ticker; + + appWindow.updateBalance(); + } + + function fiatApiRefresh(){ + // trigger API call + if(!persistentSettings.fiatPriceEnabled) + return; + + var userProvider = persistentSettings.fiatPriceProvider; + if(!appWindow.fiatPriceAPIs.hasOwnProperty(userProvider)){ + appWindow.fiatApiError("provider \"" + userProvider + "\" not implemented"); + return; + } + + var provider = appWindow.fiatPriceAPIs[userProvider]; + var userCurrency = persistentSettings.fiatPriceCurrency; + if(!provider.hasOwnProperty(userCurrency)){ + appWindow.fiatApiError("currency \"" + userCurrency + "\" not implemented"); + } + + var url = provider[userCurrency]; + Prices.getJSON(url); + } + + function fiatApiUpdateBalance(balance, unlocked_balance){ + // update balance card + var ticker = persistentSettings.fiatPriceCurrency === "xmrusd" ? appWindow.fiatPriceXMRUSD : appWindow.fiatPriceXMREUR; + var symbol = persistentSettings.fiatPriceCurrency === "xmrusd" ? "$" : "€" + if(ticker <= 0){ + console.log(fiatApiError("Could not update balance card; invalid ticker value")); + leftPanel.unlockedBalanceTextFiat = "N/A"; + leftPanel.balanceTextFiat = "N/A"; + return; + } + + var uFiat = Utils.formatMoney(unlocked_balance * ticker); + var bFiat = Utils.formatMoney(balance * ticker); + + leftPanel.unlockedBalanceTextFiat = symbol + uFiat; + leftPanel.balanceTextFiat = symbol + bFiat; + } + + function fiatTimerStart(){ + fiatPriceTimer.start(); + } + + function fiatTimerStop(){ + fiatPriceTimer.stop(); + } + + function fiatApiError(msg){ + console.log("fiatPriceError: " + msg); + } + Component.onCompleted: { x = (Screen.width - width) / 2 y = (Screen.height - maxWindowHeight) / 2 @@ -1137,6 +1299,7 @@ ApplicationWindow { walletManager.checkUpdatesComplete.connect(onWalletCheckUpdatesComplete); walletManager.walletPassphraseNeeded.connect(onWalletPassphraseNeeded); IPC.uriHandler.connect(onUriHandler); + Prices.priceJsonReceived.connect(appWindow.fiatApiJsonReceived); if(typeof daemonManager != "undefined") { daemonManager.daemonStarted.connect(onDaemonStarted); @@ -1144,8 +1307,6 @@ ApplicationWindow { daemonManager.daemonStopped.connect(onDaemonStopped); } - - // Connect app exit to qml window exit handling mainApp.closing.connect(appWindow.close); @@ -1177,6 +1338,11 @@ ApplicationWindow { } checkUpdates(); + + if(persistentSettings.fiatPriceEnabled){ + appWindow.fiatApiRefresh(); + appWindow.fiatTimerStart(); + } } Settings { @@ -1223,6 +1389,10 @@ ApplicationWindow { property bool showPid: false property bool blackTheme: true + property bool fiatPriceEnabled: false + property string fiatPriceProvider: "kraken" + property string fiatPriceCurrency: "xmrusd" + Component.onCompleted: { MoneroComponents.Style.blackTheme = persistentSettings.blackTheme } diff --git a/monero-wallet-gui.pro b/monero-wallet-gui.pro index a1227408..65221605 100644 --- a/monero-wallet-gui.pro +++ b/monero-wallet-gui.pro @@ -66,6 +66,8 @@ HEADERS += \ src/qt/mime.h \ src/qt/KeysFiles.h \ src/qt/utils.h + src/qt/utils.h \ + src/qt/prices.h SOURCES += main.cpp \ filter.cpp \ @@ -98,6 +100,8 @@ SOURCES += main.cpp \ src/qt/mime.cpp \ src/qt/KeysFiles.cpp \ src/qt/utils.cpp + src/qt/utils.cpp \ + src/qt/prices.cpp CONFIG(DISABLE_PASS_STRENGTH_METER) { HEADERS -= src/zxcvbn-c/zxcvbn.h diff --git a/pages/settings/SettingsLayout.qml b/pages/settings/SettingsLayout.qml index d1945281..f4b3d0b6 100644 --- a/pages/settings/SettingsLayout.qml +++ b/pages/settings/SettingsLayout.qml @@ -157,6 +157,106 @@ Rectangle { } } + //! Manage pricing + RowLayout { + MoneroComponents.CheckBox { + id: enableConvertCurrency + text: qsTr("Enable displaying balance in other currencies") + translationManager.emptyString + checked: persistentSettings.fiatPriceEnabled + onCheckedChanged: { + if (!checked) { + console.log("Disabled price conversion"); + persistentSettings.fiatPriceEnabled = false; + appWindow.fiatTimerStop(); + } + } + } + } + + GridLayout { + visible: enableConvertCurrency.checked + columns: 2 + Layout.fillWidth: true + Layout.leftMargin: 36 + columnSpacing: 32 + + ColumnLayout { + spacing: 10 + Layout.fillWidth: true + + MoneroComponents.Label { + Layout.fillWidth: true + fontSize: 14 + text: qsTr("Price source") + translationManager.emptyString + } + + MoneroComponents.StandardDropdown { + id: fiatPriceProviderDropDown + Layout.fillWidth: true + dataModel: fiatPriceProvidersModel + onChanged: { + var obj = dataModel.get(currentIndex); + persistentSettings.fiatPriceProvider = obj.data; + + if(persistentSettings.fiatPriceEnabled) + appWindow.fiatApiRefresh(); + } + } + } + + ColumnLayout { + spacing: 10 + Layout.fillWidth: true + + MoneroComponents.Label { + Layout.fillWidth: true + fontSize: 14 + text: qsTr("Currency") + translationManager.emptyString + } + + MoneroComponents.StandardDropdown { + id: fiatPriceCurrencyDropdown + Layout.fillWidth: true + dataModel: fiatPriceCurrencyModel + onChanged: { + var obj = dataModel.get(currentIndex); + persistentSettings.fiatPriceCurrency = obj.data; + + if(persistentSettings.fiatPriceEnabled) + appWindow.fiatApiRefresh(); + } + } + } + + z: parent.z + 1 + } + + ColumnLayout { + // Feature needs to be double enabled for security purposes (miss-clicks) + visible: enableConvertCurrency.checked && !persistentSettings.fiatPriceEnabled + spacing: 0 + Layout.topMargin: 5 + Layout.leftMargin: 36 + + MoneroComponents.WarningBox { + text: qsTr("Enabling price conversion exposes your IP address to the selected price source.") + translationManager.emptyString; + } + + MoneroComponents.StandardButton { + Layout.topMargin: 10 + Layout.bottomMargin: 10 + small: true + text: qsTr("Confirm and enable") + translationManager.emptyString + + onClicked: { + console.log("Enabled price conversion"); + persistentSettings.fiatPriceEnabled = true; + appWindow.fiatApiRefresh(); + appWindow.fiatTimerStart(); + } + } + } + MoneroComponents.StandardButton { visible: !persistentSettings.customDecorations Layout.topMargin: 10 @@ -177,7 +277,43 @@ Rectangle { } } + ListModel { + id: fiatPriceProvidersModel + } + + ListModel { + id: fiatPriceCurrencyModel + ListElement { + data: "xmrusd" + column1: "USD" + } + ListElement { + data: "xmreur" + column1: "EUR" + } + } + Component.onCompleted: { + // Dynamically fill fiatPrice dropdown based on `appWindow.fiatPriceAPIs` + var apis = appWindow.fiatPriceAPIs; + fiatPriceProvidersModel.clear(); + + var i = 0; + for (var api in apis){ + if (!apis.hasOwnProperty(api)) + continue; + + fiatPriceProvidersModel.append({"column1": Utils.capitalize(api), "data": api}); + + if(api === persistentSettings.fiatPriceProvider) + fiatPriceProviderDropDown.currentIndex = i; + i += 1; + } + + fiatPriceProviderDropDown.update(); + fiatPriceCurrencyDropdown.currentIndex = persistentSettings.fiatPriceCurrency === "xmrusd" ? 0 : 1; + fiatPriceCurrencyDropdown.update(); + console.log('SettingsLayout loaded'); } } diff --git a/src/qt/prices.cpp b/src/qt/prices.cpp new file mode 100644 index 00000000..b85899cf --- /dev/null +++ b/src/qt/prices.cpp @@ -0,0 +1,110 @@ +// Copyright (c) 2014-2019, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include +#include + +#include "utils.h" +#include "prices.h" + + +Prices::Prices(QNetworkAccessManager *networkAccessManager, QObject *parent) + : QObject(parent) { + this->m_networkAccessManager = networkAccessManager; +} + +void Prices::getJSON(const QString url) { + qDebug() << QString("Fetching: %1").arg(url); + QNetworkRequest request; + request.setUrl(QUrl(url)); + request.setRawHeader("User-Agent", randomUserAgent().toUtf8()); + request.setRawHeader("Content-Type", "application/json"); + + m_reply = this->m_networkAccessManager->get(request); + + connect(m_reply, SIGNAL(finished()), this, SLOT(gotJSON())); +} + +void Prices::gotJSON() { + // Check connectivity + if (!m_reply || m_reply->error() != QNetworkReply::NoError){ + this->gotError(); + m_reply->deleteLater(); + return; + } + + // Check json header + QList headerList = m_reply->rawHeaderList(); + QByteArray headerJson = m_reply->rawHeader("Content-Type"); + if(headerJson.length() <= 15){ + this->gotError("Bad Content-Type"); + m_reply->deleteLater(); + return; + } + + QString headerJsonStr = QTextCodec::codecForMib(106)->toUnicode(headerJson); + int _contentType = headerList.indexOf("Content-Type"); + if (_contentType < 0 || !headerJsonStr.startsWith("application/json")){ + this->gotError("Bad Content-Type"); + m_reply->deleteLater(); + return; + } + + // Check valid json document + QByteArray data = m_reply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(data); + QString jsonString = doc.toJson(QJsonDocument::Indented); + if (jsonString.isEmpty()){ + this->gotError("Bad JSON"); + m_reply->deleteLater(); + return; + } + + // Insert source url for later reference + QUrl url = m_reply->url(); + QJsonObject docobj = doc.object(); + docobj["_url"] = url.toString(); + doc.setObject(docobj); + + qDebug() << QString("Fetched: %1").arg(url.toString()); + + // Emit signal + QVariantMap vMap = doc.object().toVariantMap(); + emit priceJsonReceived(vMap); + + m_reply->deleteLater(); +} + +void Prices::gotError() { + this->gotError("Unknown error"); +} + +void Prices::gotError(const QString &message) { + qCritical() << __FUNCTION__ << ": Error: " << message; + emit priceJsonError(message); +} diff --git a/src/qt/prices.h b/src/qt/prices.h new file mode 100644 index 00000000..334fd9dc --- /dev/null +++ b/src/qt/prices.h @@ -0,0 +1,30 @@ +#ifndef PRICES_H +#define PRICES_H + +#include +#include +#include +#include +#include + +class Prices : public QObject +{ +Q_OBJECT +public: + Prices(QNetworkAccessManager *networkAccessManager, QObject *parent = nullptr); + +public slots: + Q_INVOKABLE void getJSON(const QString url); + void gotJSON(); + void gotError(); + void gotError(const QString &message); +signals: + void priceJsonReceived(QVariantMap document); + void priceJsonError(QString message); + +private: + mutable QPointer m_reply; + QNetworkAccessManager *m_networkAccessManager; +}; + +#endif // PRICES_H diff --git a/src/qt/utils.cpp b/src/qt/utils.cpp index 7c6d4b02..2407a50e 100644 --- a/src/qt/utils.cpp +++ b/src/qt/utils.cpp @@ -46,3 +46,84 @@ QString getAccountName(){ accountName = "My monero Account"; return accountName; } + +QString randomUserAgent(){ + QStringList urand; + urand << "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1" + << "Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0" + << "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0" + << "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/31.0" + << "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20130401 Firefox/31.0" + << "Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0" + << "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20120101 Firefox/29.0" + << "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/29.0" + << "Mozilla/5.0 (X11; OpenBSD amd64; rv:28.0) Gecko/20100101 Firefox/28.0" + << "Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0" + << "Mozilla/5.0 (Windows NT 6.1; rv:27.3) Gecko/20130101 Firefox/27.3" + << "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:27.0) Gecko/20121011 Firefox/27.0" + << "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0" + << "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:25.0) Gecko/20100101 Firefox/25.0" + << "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0" + << "Mozilla/5.0 (Windows NT 6.0; WOW64; rv:24.0) Gecko/20100101 Firefox/24.0" + << "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0" + << "Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/23.0" + << "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20130406 Firefox/23.0" + << "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:23.0) Gecko/20131011 Firefox/23.0" + << "Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/22.0" + << "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:22.0) Gecko/20130328 Firefox/22.0" + << "Mozilla/5.0 (Windows NT 6.1; rv:22.0) Gecko/20130405 Firefox/22.0" + << "Mozilla/5.0 (Microsoft Windows NT 6.2.9200.0); rv:22.0) Gecko/20130405 Firefox/22.0" + << "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1" + << "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1" + << "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:21.0.0) Gecko/20121011 Firefox/21.0.0" + << "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20130331 Firefox/21.0" + << "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20100101 Firefox/21.0" + << "Mozilla/5.0 (X11; Linux i686; rv:21.0) Gecko/20100101 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20130514 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 6.2; rv:21.0) Gecko/20130326 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130401 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130331 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130330 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20130401 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20130328 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20100101 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20130401 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20130331 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20100101 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 5.0; rv:21.0) Gecko/20100101 Firefox/21.0" + << "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" + << "Mozilla/5.0 (Windows NT 6.2; Win64; x64;) Gecko/20100101 Firefox/20.0" + << "Mozilla/5.0 (Windows x86; rv:19.0) Gecko/20100101 Firefox/19.0" + << "Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20100101 Firefox/19.0" + << "Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20100101 Firefox/18.0.1" + << "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:18.0) Gecko/20100101 Firefox/18.0" + << "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0.6" + << "Mozilla/5.0 (X11; Ubuntu; Linux armv7l; rv:17.0) Gecko/20100101 Firefox/17.0" + << "Mozilla/6.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1" + << "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1" + << "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1" + << "Mozilla/5.0 (X11; NetBSD amd64; rv:16.0) Gecko/20121102 Firefox/16.0" + << "Mozilla/5.0 (Windows NT 6.1; rv:15.0) Gecko/20120716 Firefox/15.0a2" + << "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.16) Gecko/20120427 Firefox/15.0a1" + << "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0) Gecko/20120427 Firefox/15.0a1" + << "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:15.0) Gecko/20120910144328 Firefox/15.0.2" + << "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:15.0) Gecko/20100101 Firefox/15.0.1" + << "Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:15.0) Gecko/20121011 Firefox/15.0.1" + << "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:14.0) Gecko/20120405 Firefox/14.0a1" + << "Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20120405 Firefox/14.0a1" + << "Mozilla/5.0 (Windows NT 5.1; rv:14.0) Gecko/20120405 Firefox/14.0a1" + << "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1" + << "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:14.0) Gecko/20100101 Firefox/14.0.1" + << "Mozilla/5.0 (Windows; U; Windows NT 6.1; WOW64; en-US; rv:2.0.4) Gecko/20120718 AskTbAVR-IDW/3.12.5.17700 Firefox/14.0.1" + << "Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/14.0.1" + << "Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/ 20120405 Firefox/14.0.1" + << "Mozilla/5.0 (Windows NT 6.0; rv:14.0) Gecko/20100101 Firefox/14.0.1" + << "Mozilla/5.0 (Windows NT 5.1; rv:15.0) Gecko/20100101 Firefox/13.0.1" + << "Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/12.0" + << "Mozilla/5.0 (Windows NT 6.1; de;rv:12.0) Gecko/20120403211507 Firefox/12.0"; + + // @TODO: Qt 5.10 - QRandomGenerator + int irand = rand() % urand.length(); + return urand.at(irand); +} diff --git a/src/qt/utils.h b/src/qt/utils.h index 9d798b8e..c18ffdc1 100644 --- a/src/qt/utils.h +++ b/src/qt/utils.h @@ -35,5 +35,6 @@ bool fileExists(QString path); QString getAccountName(); const static QRegExp reURI = QRegExp("^\\w+:\\/\\/([\\w+\\-?\\-_\\-=\\-&]+)"); +QString randomUserAgent(); #endif // UTILS_H